diff --git a/README.md b/README.md index 1eed55316..9bcc5dbf8 100644 --- a/README.md +++ b/README.md @@ -87,15 +87,16 @@ If you don't want a certain feature, simply disable it. - [x] Build portals of arbitrary shape and orientation to get around easily (even horizontal!) - [x] Correctly retains velocity of players, so you can fly through it - [x] Apply different styles to portals so they fit your building style -- [ ] Integrates with regions to control portal connection access - [x] Dynmap integration shows icons for global portals #### Regions (vane-regions) -- [ ] Players can buy an arbitrarily shaped patch of land, and may control certain environmental conditions and player permissions for that area -- [ ] Server-owned regions can be used to protect gobal areas (e.g. spawn). -- [ ] Visual selection of any 2D polygon shape with arbitrary heights. -- [ ] Seamless integration into chest-like menus instead of commands. +- [x] Players can buy a patch of land, and may control certain environmental conditions and player permissions for that area +- [x] Regions created by admins can be used to protect gobal areas (e.g. spawn). +- [x] Seamless integration into chest-like menus instead of commands. +- [x] Integrates with portals to allow only players witha the portal permission to operate portals in the region +- [x] Integrates with dynmap to make regions visible on the online map +- [x] Visual region selection indicator #### Proxy plugin (vane-waterfall) diff --git a/vane-admin/src/main/java/org/oddlama/vane/admin/commands/SlimeChunk.java b/vane-admin/src/main/java/org/oddlama/vane/admin/commands/SlimeChunk.java index ac82a11fe..fb5844f93 100644 --- a/vane-admin/src/main/java/org/oddlama/vane/admin/commands/SlimeChunk.java +++ b/vane-admin/src/main/java/org/oddlama/vane/admin/commands/SlimeChunk.java @@ -1,7 +1,6 @@ package org.oddlama.vane.admin.commands; import org.bukkit.entity.Player; -import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; import org.oddlama.vane.admin.Admin; import org.oddlama.vane.annotation.command.Name; diff --git a/vane-bedtime/src/main/java/org/oddlama/vane/bedtime/Bedtime.java b/vane-bedtime/src/main/java/org/oddlama/vane/bedtime/Bedtime.java index 7a8932f31..733fe95d6 100644 --- a/vane-bedtime/src/main/java/org/oddlama/vane/bedtime/Bedtime.java +++ b/vane-bedtime/src/main/java/org/oddlama/vane/bedtime/Bedtime.java @@ -11,9 +11,9 @@ import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; -import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.player.PlayerBedEnterEvent; import org.bukkit.event.player.PlayerBedLeaveEvent; +import org.bukkit.event.player.PlayerQuitEvent; import org.oddlama.vane.annotation.VaneModule; import org.oddlama.vane.annotation.config.ConfigDouble; diff --git a/vane-bedtime/src/main/java/org/oddlama/vane/bedtime/BedtimeDynmapLayerDelegate.java b/vane-bedtime/src/main/java/org/oddlama/vane/bedtime/BedtimeDynmapLayerDelegate.java index 793262c70..969b322c2 100644 --- a/vane-bedtime/src/main/java/org/oddlama/vane/bedtime/BedtimeDynmapLayerDelegate.java +++ b/vane-bedtime/src/main/java/org/oddlama/vane/bedtime/BedtimeDynmapLayerDelegate.java @@ -4,8 +4,8 @@ import java.util.UUID; import java.util.logging.Level; -import org.bukkit.plugin.Plugin; import org.bukkit.OfflinePlayer; +import org.bukkit.plugin.Plugin; import org.dynmap.DynmapAPI; import org.dynmap.markers.Marker; diff --git a/vane-core/src/main/java/org/oddlama/vane/util/ItemUtil.java b/vane-core/src/main/java/org/oddlama/vane/util/ItemUtil.java index 0580616f5..938bd7ec1 100644 --- a/vane-core/src/main/java/org/oddlama/vane/util/ItemUtil.java +++ b/vane-core/src/main/java/org/oddlama/vane/util/ItemUtil.java @@ -21,6 +21,7 @@ import org.bukkit.Bukkit; import org.bukkit.Material; +import org.bukkit.OfflinePlayer; import org.bukkit.craftbukkit.v1_16_R3.enchantments.CraftEnchantment; import org.bukkit.enchantments.Enchantment; import org.bukkit.entity.Player; @@ -214,6 +215,14 @@ public int compare(final ItemStack a, final ItemStack b) { } } + public static ItemStack skull_for_player(final OfflinePlayer player) { + final var item = new ItemStack(Material.PLAYER_HEAD); + final var meta = (SkullMeta)item.getItemMeta(); + meta.setOwningPlayer(player); + item.setItemMeta(meta); + return item; + } + public static ItemStack skull_with_texture(final String name, final String base64_texture) { final var profile = Bukkit.createProfile(SKULL_OWNER); profile.setProperty(new ProfileProperty("textures", base64_texture)); diff --git a/vane-enchantments/src/main/java/org/oddlama/vane/enchantments/Enchantments.java b/vane-enchantments/src/main/java/org/oddlama/vane/enchantments/Enchantments.java index 64f531719..4051cd862 100644 --- a/vane-enchantments/src/main/java/org/oddlama/vane/enchantments/Enchantments.java +++ b/vane-enchantments/src/main/java/org/oddlama/vane/enchantments/Enchantments.java @@ -14,7 +14,6 @@ import org.bukkit.event.inventory.PrepareAnvilEvent; import org.bukkit.event.player.PlayerInteractEntityEvent; import org.bukkit.event.world.LootGenerateEvent; -import org.bukkit.inventory.meta.EnchantmentStorageMeta; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.Merchant; import org.bukkit.inventory.MerchantRecipe; diff --git a/vane-portals/src/main/java/org/oddlama/vane/portals/PortalActivator.java b/vane-portals/src/main/java/org/oddlama/vane/portals/PortalActivator.java index 389912f7f..1927d335f 100644 --- a/vane-portals/src/main/java/org/oddlama/vane/portals/PortalActivator.java +++ b/vane-portals/src/main/java/org/oddlama/vane/portals/PortalActivator.java @@ -40,12 +40,13 @@ public void on_player_interact_console(final PlayerInteractEvent event) { return; } + event.setUseInteractedBlock(Event.Result.DENY); + event.setUseItemInHand(Event.Result.DENY); + final var player = event.getPlayer(); final var portal = get_module().portal_for(portal_block); if (portal.open_console(get_module(), player, block)) { swing_arm(player, event.getHand()); - event.setUseInteractedBlock(Event.Result.DENY); - event.setUseItemInHand(Event.Result.DENY); } } diff --git a/vane-portals/src/main/java/org/oddlama/vane/portals/PortalConstructor.java b/vane-portals/src/main/java/org/oddlama/vane/portals/PortalConstructor.java index e0a008071..1ca469a37 100644 --- a/vane-portals/src/main/java/org/oddlama/vane/portals/PortalConstructor.java +++ b/vane-portals/src/main/java/org/oddlama/vane/portals/PortalConstructor.java @@ -168,7 +168,7 @@ private boolean can_link_console(final Player player, final List blocks, } // Call event - final var event = new PortalLinkConsoleEvent(player, blocks, check_only, existing_portal); + final var event = new PortalLinkConsoleEvent(player, console, blocks, check_only, existing_portal); get_module().getServer().getPluginManager().callEvent(event); if (event.isCancelled()) { lang_link_restricted.send(player); diff --git a/vane-portals/src/main/java/org/oddlama/vane/portals/PortalDynmapLayerDelegate.java b/vane-portals/src/main/java/org/oddlama/vane/portals/PortalDynmapLayerDelegate.java index c273be24c..ba2492477 100644 --- a/vane-portals/src/main/java/org/oddlama/vane/portals/PortalDynmapLayerDelegate.java +++ b/vane-portals/src/main/java/org/oddlama/vane/portals/PortalDynmapLayerDelegate.java @@ -103,8 +103,8 @@ public void update_marker(final Portal portal) { return; } - // Only public portals - if (portal.visibility() != Portal.Visibility.PUBLIC) { + // Don't show private portals + if (portal.visibility() == Portal.Visibility.PRIVATE) { remove_marker(portal.id()); return; } diff --git a/vane-portals/src/main/java/org/oddlama/vane/portals/Portals.java b/vane-portals/src/main/java/org/oddlama/vane/portals/Portals.java index 8e5da0a0f..771b040cf 100644 --- a/vane-portals/src/main/java/org/oddlama/vane/portals/Portals.java +++ b/vane-portals/src/main/java/org/oddlama/vane/portals/Portals.java @@ -30,6 +30,7 @@ import org.bukkit.Sound; import org.bukkit.SoundCategory; import org.bukkit.block.Block; +import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.world.ChunkLoadEvent; @@ -50,6 +51,7 @@ import org.oddlama.vane.annotation.lang.LangMessage; import org.oddlama.vane.annotation.persistent.Persistent; import org.oddlama.vane.core.functional.Consumer2; +import org.oddlama.vane.core.functional.Function2; import org.oddlama.vane.core.lang.TranslatedMessage; import org.oddlama.vane.core.material.ExtendedMaterial; import org.oddlama.vane.core.module.Module; @@ -63,7 +65,7 @@ import org.oddlama.vane.portals.portal.PortalBlockLookup; import org.oddlama.vane.portals.portal.Style; -@VaneModule(name = "portals", bstats = 8642, config_version = 1, lang_version = 1, storage_version = 1) +@VaneModule(name = "portals", bstats = 8642, config_version = 2, lang_version = 2, storage_version = 2) public class Portals extends Module { // Add (de-)serializers static { @@ -181,6 +183,8 @@ public Portals() { constructor = new PortalConstructor(this); new PortalTeleporter(this); dynmap_layer = new PortalDynmapLayer(this); + + persistent_storage_manager.add_migration_to(2, "Portal visibility GROUP_INTERNAL was added. This is a no-op.", (json) -> {}); } @SuppressWarnings("unchecked") @@ -234,6 +238,37 @@ public void on_config_change() { } } + // Lightweight callbacks to the regions module, if it is installed. + // Lifting the callback storage into the portals module saves us + // from having to ship regions api with this module. + private Function2 is_in_same_region_group_callback = null; + public void set_is_in_same_region_group_callback(final Function2 callback) { + is_in_same_region_group_callback = callback; + } + + private Function2 player_can_use_portals_in_region_group_of_callback = null; + public void set_player_can_use_portals_in_region_group_of_callback(final Function2 callback) { + player_can_use_portals_in_region_group_of_callback = callback; + } + + public boolean is_in_same_region_group(final Portal a, final Portal b) { + if (is_in_same_region_group_callback == null) { + return true; + } + return is_in_same_region_group_callback.apply(a, b); + } + + public boolean player_can_use_portals_in_region_group_of(final Player player, final Portal portal) { + if (player_can_use_portals_in_region_group_of_callback == null) { + return true; + } + return player_can_use_portals_in_region_group_of_callback.apply(player, portal); + } + + public boolean is_regions_installed() { + return is_in_same_region_group_callback != null; + } + public Style style(final NamespacedKey key) { final var s = styles.get(key); if (s == null) { @@ -534,13 +569,15 @@ public void disconnect_portals(final Portal src, final Portal dst) { src.on_disconnect(this, dst); dst.on_disconnect(this, src); - // Reset target id's if the target portal was private - if (dst.visibility() == Portal.Visibility.PRIVATE) { + // Reset target id's if the target portal was transient + if (dst.visibility().is_transient_target()) { src.target_id(null); + src.update_blocks(this); mark_persistent_storage_dirty(); } - if (src.visibility() == Portal.Visibility.PRIVATE) { + if (src.visibility().is_transient_target()) { dst.target_id(null); + dst.update_blocks(this); mark_persistent_storage_dirty(); } @@ -654,13 +691,29 @@ public void update_portal_icon(final Portal portal) { public void update_portal_visibility(final Portal portal) { // Replace references to the portal everywhere, if visibility // has changed. - if (portal.visibility() != Portal.Visibility.PUBLIC) { - for (final var other : storage_portals.values()) { - if (Objects.equals(other.target_id(), portal.id())) { - other.target_id(null); + switch (portal.visibility()) { + case PRIVATE: + case GROUP: + // Not visible from outside, these are transient. + for (final var other : storage_portals.values()) { + if (Objects.equals(other.target_id(), portal.id())) { + other.target_id(null); + } } - } - // TODO don't hide for group access + break; + + case GROUP_INTERNAL: + // Remove from portals outside of the group + for (final var other : storage_portals.values()) { + if (Objects.equals(other.target_id(), portal.id()) && + !is_in_same_region_group(other, portal)) { + other.target_id(null); + } + } + break; + + default: // Nothing to do + break; } // Update dynmap marker diff --git a/vane-portals/src/main/java/org/oddlama/vane/portals/event/PortalLinkConsoleEvent.java b/vane-portals/src/main/java/org/oddlama/vane/portals/event/PortalLinkConsoleEvent.java index ddd96eed4..419687600 100644 --- a/vane-portals/src/main/java/org/oddlama/vane/portals/event/PortalLinkConsoleEvent.java +++ b/vane-portals/src/main/java/org/oddlama/vane/portals/event/PortalLinkConsoleEvent.java @@ -14,12 +14,14 @@ public class PortalLinkConsoleEvent extends PortalEvent { private static final HandlerList handlers = new HandlerList(); private Player player; private Portal portal; + private Block console; private List portal_blocks; private boolean check_only; private boolean cancel_if_not_owner = true; - public PortalLinkConsoleEvent(final Player player, final List portal_blocks, boolean check_only, @Nullable final Portal portal) { + public PortalLinkConsoleEvent(final Player player, final Block console, final List portal_blocks, boolean check_only, @Nullable final Portal portal) { this.player = player; + this.console = console; this.portal_blocks = portal_blocks; this.check_only = check_only; this.portal = portal; @@ -33,6 +35,10 @@ public Player getPlayer() { return player; } + public Block getConsole() { + return console; + } + public List getPortalBlocks() { return portal_blocks; } diff --git a/vane-portals/src/main/java/org/oddlama/vane/portals/event/PortalOpenConsoleEvent.java b/vane-portals/src/main/java/org/oddlama/vane/portals/event/PortalOpenConsoleEvent.java index ad10fc71e..5a098f5b7 100644 --- a/vane-portals/src/main/java/org/oddlama/vane/portals/event/PortalOpenConsoleEvent.java +++ b/vane-portals/src/main/java/org/oddlama/vane/portals/event/PortalOpenConsoleEvent.java @@ -1,21 +1,21 @@ package org.oddlama.vane.portals.event; -import java.util.UUID; - import org.bukkit.block.Block; import org.bukkit.entity.Player; import org.bukkit.event.HandlerList; +import org.oddlama.vane.portals.portal.Portal; + public class PortalOpenConsoleEvent extends PortalEvent { private static final HandlerList handlers = new HandlerList(); private Player player; private Block console; - private UUID portal_id; + private Portal portal; - public PortalOpenConsoleEvent(final Player player, final Block console, final UUID portal_id) { + public PortalOpenConsoleEvent(final Player player, final Block console, final Portal portal) { this.player = player; this.console = console; - this.portal_id = portal_id; + this.portal = portal; } public Player getPlayer() { @@ -26,8 +26,8 @@ public Block getConsole() { return console; } - public UUID getPortalId() { - return portal_id; + public Portal getPortal() { + return portal; } public HandlerList getHandlers() { diff --git a/vane-portals/src/main/java/org/oddlama/vane/portals/event/PortalUnlinkConsoleEvent.java b/vane-portals/src/main/java/org/oddlama/vane/portals/event/PortalUnlinkConsoleEvent.java index 2b1dbd7cc..e397a9572 100644 --- a/vane-portals/src/main/java/org/oddlama/vane/portals/event/PortalUnlinkConsoleEvent.java +++ b/vane-portals/src/main/java/org/oddlama/vane/portals/event/PortalUnlinkConsoleEvent.java @@ -1,5 +1,6 @@ package org.oddlama.vane.portals.event; +import org.bukkit.block.Block; import org.bukkit.entity.Player; import org.bukkit.event.HandlerList; @@ -8,12 +9,14 @@ public class PortalUnlinkConsoleEvent extends PortalEvent { private static final HandlerList handlers = new HandlerList(); private Player player; + private Block console; private Portal portal; private boolean check_only; private boolean cancel_if_not_owner = true; - public PortalUnlinkConsoleEvent(final Player player, final Portal portal, boolean check_only) { + public PortalUnlinkConsoleEvent(final Player player, final Block console, final Portal portal, boolean check_only) { this.player = player; + this.console = console; this.portal = portal; this.check_only = check_only; } @@ -26,6 +29,10 @@ public Player getPlayer() { return player; } + public Block getConsole() { + return console; + } + public Portal getPortal() { return portal; } diff --git a/vane-portals/src/main/java/org/oddlama/vane/portals/menu/ConsoleMenu.java b/vane-portals/src/main/java/org/oddlama/vane/portals/menu/ConsoleMenu.java index 46dee6e3b..2eeea4a5b 100644 --- a/vane-portals/src/main/java/org/oddlama/vane/portals/menu/ConsoleMenu.java +++ b/vane-portals/src/main/java/org/oddlama/vane/portals/menu/ConsoleMenu.java @@ -84,7 +84,7 @@ public Menu create(final Portal portal, final Player player, final Block console } // Check if unlink would be allowed - final var unlink_event = new PortalUnlinkConsoleEvent(player, portal, true); + final var unlink_event = new PortalUnlinkConsoleEvent(player, console, portal, true); get_module().getServer().getPluginManager().callEvent(unlink_event); if (!unlink_event.isCancelled()) { console_menu.add(menu_item_unlink_console(portal, console)); @@ -125,9 +125,10 @@ private MenuWidget menu_item_select_target(final Portal portal) { .stream() .filter(p -> { switch (p.visibility()) { - case PUBLIC: return true; - case GROUP: return false; // TODO group visibility - case PRIVATE: return player.getUniqueId().equals(p.owner()); + case PUBLIC: return true; + case GROUP: return get_module().player_can_use_portals_in_region_group_of(player, p); + case GROUP_INTERNAL: return get_module().is_in_same_region_group(portal, p); + case PRIVATE: return player.getUniqueId().equals(p.owner()); } return false; }) @@ -183,7 +184,7 @@ private MenuWidget menu_item_unlink_console(final Portal portal, final Block con MenuFactory.confirm(get_context(), lang_unlink_console_confirm_title.str(), item_unlink_console_confirm_accept.item(), (player2) -> { // Call event - final var event = new PortalUnlinkConsoleEvent(player2, portal, false); + final var event = new PortalUnlinkConsoleEvent(player2, console, portal, false); get_module().getServer().getPluginManager().callEvent(event); if (event.isCancelled()) { get_module().lang_unlink_restricted.send(player2); diff --git a/vane-portals/src/main/java/org/oddlama/vane/portals/menu/SettingsMenu.java b/vane-portals/src/main/java/org/oddlama/vane/portals/menu/SettingsMenu.java index 831415595..72f7ae899 100644 --- a/vane-portals/src/main/java/org/oddlama/vane/portals/menu/SettingsMenu.java +++ b/vane-portals/src/main/java/org/oddlama/vane/portals/menu/SettingsMenu.java @@ -32,6 +32,7 @@ public class SettingsMenu extends ModuleComponent { public TranslatedItemStack item_select_style; public TranslatedItemStack item_visibility_public; public TranslatedItemStack item_visibility_group; + public TranslatedItemStack item_visibility_group_internal; public TranslatedItemStack item_visibility_private; public TranslatedItemStack item_target_lock_on; public TranslatedItemStack item_target_lock_off; @@ -41,15 +42,16 @@ public SettingsMenu(Context context) { super(context.namespace("settings")); final var ctx = get_context(); - item_rename = new TranslatedItemStack<>(ctx, "rename", Material.NAME_TAG, 1, "Used to rename the portal."); - item_select_icon = new TranslatedItemStack<>(ctx, "select_icon", namespaced_key("vane", "decoration_end_portal_orb"), 1, "Used to select the portal's icon."); - item_select_style = new TranslatedItemStack<>(ctx, "select_style", Material.ITEM_FRAME, 1, "Used to change the portal's style."); - item_visibility_public = new TranslatedItemStack<>(ctx, "visibility_public", Material.ENDER_EYE, 1, "Used to change and indicate public visibility."); - item_visibility_group = new TranslatedItemStack<>(ctx, "visibility_group", Material.ENDER_PEARL, 1, "Used to change and indicate group visibility."); - item_visibility_private = new TranslatedItemStack<>(ctx, "visibility_private", Material.FIREWORK_STAR, 1, "Used to change and indicate private visibility."); - item_target_lock_on = new TranslatedItemStack<>(ctx, "target_lock_on", Material.SLIME_BALL, 1, "Used to toggle and indicate enabled target lock."); - item_target_lock_off = new TranslatedItemStack<>(ctx, "target_lock_off", Material.SNOWBALL, 1, "Used to toggle and indicate disabled target lock."); - item_back = new TranslatedItemStack<>(ctx, "back", Material.PRISMARINE_SHARD, 1, "Used to go back to the previous menu."); + item_rename = new TranslatedItemStack<>(ctx, "rename", Material.NAME_TAG, 1, "Used to rename the portal."); + item_select_icon = new TranslatedItemStack<>(ctx, "select_icon", namespaced_key("vane", "decoration_end_portal_orb"), 1, "Used to select the portal's icon."); + item_select_style = new TranslatedItemStack<>(ctx, "select_style", Material.ITEM_FRAME, 1, "Used to change the portal's style."); + item_visibility_public = new TranslatedItemStack<>(ctx, "visibility_public", Material.ENDER_EYE, 1, "Used to change and indicate public visibility."); + item_visibility_group = new TranslatedItemStack<>(ctx, "visibility_group", Material.ENDER_PEARL, 1, "Used to change and indicate group visibility."); + item_visibility_group_internal = new TranslatedItemStack<>(ctx, "visibility_group_internal", Material.FIRE_CHARGE, 1, "Used to change and indicate group internal visibility."); + item_visibility_private = new TranslatedItemStack<>(ctx, "visibility_private", Material.FIREWORK_STAR, 1, "Used to change and indicate private visibility."); + item_target_lock_on = new TranslatedItemStack<>(ctx, "target_lock_on", Material.SLIME_BALL, 1, "Used to toggle and indicate enabled target lock."); + item_target_lock_off = new TranslatedItemStack<>(ctx, "target_lock_off", Material.SNOWBALL, 1, "Used to toggle and indicate disabled target lock."); + item_back = new TranslatedItemStack<>(ctx, "back", Material.PRISMARINE_SHARD, 1, "Used to go back to the previous menu."); } // HINT: We don't capture the previous menu and open a new one on exit, @@ -156,7 +158,13 @@ private MenuWidget menu_item_visibility(final Portal portal) { return ClickResult.ERROR; } - portal.visibility(event.getClick() == ClickType.RIGHT ? portal.visibility().prev() : portal.visibility().next()); + Portal.Visibility new_vis; + // If regions is not installed, we need to skip group visibility. + do { + new_vis = event.getClick() == ClickType.RIGHT ? portal.visibility().prev() : portal.visibility().next(); + } while (new_vis.requires_regions() && !get_module().is_regions_installed()); + + portal.visibility(new_vis); get_module().update_portal_visibility(portal); mark_persistent_storage_dirty(); menu.update(); @@ -165,9 +173,10 @@ private MenuWidget menu_item_visibility(final Portal portal) { @Override public void item(final ItemStack item) { switch (portal.visibility()) { - case PUBLIC: super.item(item_visibility_public.item()); break; - case GROUP: super.item(item_visibility_group.item()); break; - case PRIVATE: super.item(item_visibility_private.item()); break; + case PUBLIC: super.item(item_visibility_public.item()); break; + case GROUP: super.item(item_visibility_group.item()); break; + case GROUP_INTERNAL: super.item(item_visibility_group_internal.item()); break; + case PRIVATE: super.item(item_visibility_private.item()); break; } } }; diff --git a/vane-portals/src/main/java/org/oddlama/vane/portals/portal/Portal.java b/vane-portals/src/main/java/org/oddlama/vane/portals/portal/Portal.java index 1cea32b00..39abe3032 100644 --- a/vane-portals/src/main/java/org/oddlama/vane/portals/portal/Portal.java +++ b/vane-portals/src/main/java/org/oddlama/vane/portals/portal/Portal.java @@ -298,7 +298,7 @@ public void update_blocks(final Portals portals) { public boolean open_console(final Portals portals, final Player player, final Block console) { // Call event - final var event = new PortalOpenConsoleEvent(player, console, id()); + final var event = new PortalOpenConsoleEvent(player, console, this); portals.getServer().getPluginManager().callEvent(event); if (event.isCancelled()) { return false; @@ -323,6 +323,7 @@ public String toString() { public static enum Visibility { PUBLIC, GROUP, + GROUP_INTERNAL, PRIVATE; public Visibility prev() { @@ -339,6 +340,14 @@ public Visibility next() { final var next = (ordinal() + 1) % values().length; return values()[next]; } + + public boolean is_transient_target() { + return this == GROUP || this == PRIVATE; + } + + public boolean requires_regions() { + return this == GROUP || this == GROUP_INTERNAL; + } } public static class TargetSelectionComparator implements Comparator { diff --git a/vane-portals/src/main/resources/lang-de.yml b/vane-portals/src/main/resources/lang-de.yml index 26e21da59..2d2b6c56b 100644 --- a/vane-portals/src/main/resources/lang-de.yml +++ b/vane-portals/src/main/resources/lang-de.yml @@ -12,7 +12,7 @@ # DO NOT CHANGE! The version of this language file. Used to determine # if the file needs to be updated. -version: 1 +version: 2 # The corresponding language code used in resource packs. Used for # resource pack generation. Typically this is a combination of the # language code (ISO 639) and the country code (ISO 3166). @@ -81,7 +81,11 @@ menus: visibility_group: name: "§b§lSichtbarkeit umschalten" - lore: ["", "§bAktuell: §e§lGruppe§r"] + lore: ["", "§bAktuell: §e§lGruppe (Mitglieder)§r"] + + visibility_group_internal: + name: "§b§lSichtbarkeit umschalten" + lore: ["", "§bAktuell: §e§lGruppe (Nur intern sichtbar)§r"] visibility_private: name: "§b§lSichtbarkeit umschalten" diff --git a/vane-portals/src/main/resources/lang-en.yml b/vane-portals/src/main/resources/lang-en.yml index aff6c3003..a847f9341 100644 --- a/vane-portals/src/main/resources/lang-en.yml +++ b/vane-portals/src/main/resources/lang-en.yml @@ -12,7 +12,7 @@ # DO NOT CHANGE! The version of this language file. Used to determine # if the file needs to be updated. -version: 1 +version: 2 # The corresponding language code used in resource packs. Used for # resource pack generation. Typically this is a combination of the # language code (ISO 639) and the country code (ISO 3166). @@ -135,7 +135,12 @@ menus: # This item is used to cycle visibility and indicate group visibility. visibility_group: name: "§b§lToggle Visibility" - lore: ["", "§bCurrent: §e§lGroup§r"] + lore: ["", "§bCurrent: §e§lRegion Group (members)§r"] + + # This item is used to cycle visibility and indicate group visibility. + visibility_group_internal: + name: "§b§lToggle Visibility" + lore: ["", "§bCurrent: §e§lRegion Group (only visible internally)§r"] # This item is used to cycle visibility and indicate private visibility. visibility_private: @@ -332,9 +337,12 @@ menus: - "§7Current: %1$s" - "§8Target selection is currently §6§lLOCKED§r§8!" - # The item used to open the portal's target selector. select_target_title: "§8§lSelect Target Portal" filter_portals_title: "§8§lFilter Portals" + # The item used to represent a target portal. + # %1$s: Portal name + # %2$s: Portal distance + # %3$s: Portal world select_target_portal: name: "%1$s" lore: @@ -352,7 +360,7 @@ menus: unlink_console_confirm_accept: name: "§c§lUNLINK CONSOLE" lore: [] - # The item to cance unlinking. + # The item to cancel unlinking. unlink_console_confirm_cancel: name: "§a§lCancel" lore: [] diff --git a/vane-regions/build.gradle b/vane-regions/build.gradle new file mode 100644 index 000000000..1ff6e3cbe --- /dev/null +++ b/vane-regions/build.gradle @@ -0,0 +1,4 @@ +dependencies { + implementation project(':vane-portals'); + implementation group: 'us.dynmap', name: 'dynmap-api', version: '3.1-SNAPSHOT'; +} diff --git a/vane-regions/src/main/java/org/oddlama/vane/regions/RegionDynmapLayer.java b/vane-regions/src/main/java/org/oddlama/vane/regions/RegionDynmapLayer.java new file mode 100644 index 000000000..94d481475 --- /dev/null +++ b/vane-regions/src/main/java/org/oddlama/vane/regions/RegionDynmapLayer.java @@ -0,0 +1,90 @@ +package org.oddlama.vane.regions; + +import java.util.UUID; + +import org.oddlama.vane.annotation.config.ConfigBoolean; +import org.oddlama.vane.annotation.config.ConfigDouble; +import org.oddlama.vane.annotation.config.ConfigInt; +import org.oddlama.vane.annotation.lang.LangMessage; +import org.oddlama.vane.core.lang.TranslatedMessage; +import org.oddlama.vane.core.module.Context; +import org.oddlama.vane.core.module.ModuleComponent; +import org.oddlama.vane.regions.region.Region; + +public class RegionDynmapLayer extends ModuleComponent { + public static final String LAYER_ID = "vane_regions.regions"; + + @ConfigInt(def = 35, min = 0, desc = "Layer ordering priority.") + public int config_layer_priority; + @ConfigBoolean(def = false, desc = "If the layer should be hidden by default.") + public boolean config_layer_hide; + + @ConfigInt(def = 0x259df9, min = 0, desc = "Area marker fill color (0xRRGGBB).") + public int config_fill_color; + @ConfigDouble(def = 0.05, min = 0.0, max = 1.0, desc = "Area marker fill opacity.") + public double config_fill_opacity; + + @ConfigInt(def = 2, min = 1, desc = "Area marker line weight.") + public int config_line_weight; + @ConfigInt(def = 0x259df9/*0xf9bd25*/, min = 0, desc = "Area marker line color (0xRRGGBB).") + public int config_line_color; + @ConfigDouble(def = 1.0, min = 0.0, max = 1.0, desc = "Area marker line opacity.") + public double config_line_opacity; + + @LangMessage public TranslatedMessage lang_layer_label; + @LangMessage public TranslatedMessage lang_marker_label; + + private RegionDynmapLayerDelegate delegate = null; + + public RegionDynmapLayer(final Context context) { + super(context.group("dynmap", "Enable dynmap integration. Regions will then be shown on a separate dynmap layer.")); + } + + public void delayed_on_enable() { + final var plugin = get_module().getServer().getPluginManager().getPlugin("dynmap"); + if (plugin == null) { + return; + } + + delegate = new RegionDynmapLayerDelegate(this); + delegate.on_enable(plugin); + } + + @Override + public void on_enable() { + schedule_next_tick(this::delayed_on_enable); + } + + @Override + public void on_disable() { + if (delegate != null) { + delegate.on_disable(); + delegate = null; + } + } + + + public void update_marker(final Region region) { + if (delegate != null) { + delegate.update_marker(region); + } + } + + public void remove_marker(final UUID region_id) { + if (delegate != null) { + delegate.remove_marker(region_id); + } + } + + public void remove_marker(final String marker_id) { + if (delegate != null) { + delegate.remove_marker(marker_id); + } + } + + public void update_all_markers() { + if (delegate != null) { + delegate.update_all_markers(); + } + } +} diff --git a/vane-regions/src/main/java/org/oddlama/vane/regions/RegionDynmapLayerDelegate.java b/vane-regions/src/main/java/org/oddlama/vane/regions/RegionDynmapLayerDelegate.java new file mode 100644 index 000000000..27b7c610c --- /dev/null +++ b/vane-regions/src/main/java/org/oddlama/vane/regions/RegionDynmapLayerDelegate.java @@ -0,0 +1,154 @@ +package org.oddlama.vane.regions; + +import java.util.HashSet; +import java.util.UUID; +import java.util.logging.Level; + +import org.bukkit.plugin.Plugin; + +import org.dynmap.DynmapAPI; +import org.dynmap.markers.Marker; +import org.dynmap.markers.MarkerAPI; +import org.dynmap.markers.MarkerSet; + +import org.oddlama.vane.regions.Regions; +import org.oddlama.vane.regions.region.Region; + +public class RegionDynmapLayerDelegate { + private RegionDynmapLayer parent = null; + + private DynmapAPI dynmap_api = null; + private MarkerAPI marker_api = null; + private boolean dynmap_enabled = false; + + private MarkerSet marker_set = null; + + public RegionDynmapLayerDelegate(final RegionDynmapLayer parent) { + this.parent = parent; + } + + public Regions get_module() { + return parent.get_module(); + } + + public void on_enable(final Plugin plugin) { + try { + dynmap_api = (DynmapAPI)plugin; + marker_api = dynmap_api.getMarkerAPI(); + } catch (Exception e) { + get_module().log.log(Level.WARNING, "Error while enabling dynmap integration!", e); + return; + } + + if (marker_api == null) { + return; + } + + get_module().log.info("Enabling dynmap integration"); + dynmap_enabled = true; + create_or_load_layer(); + } + + public void on_disable() { + if (!dynmap_enabled) { + return; + } + + get_module().log.info("Disabling dynmap integration"); + dynmap_enabled = false; + dynmap_api = null; + marker_api = null; + } + + private void create_or_load_layer() { + // Create or retrieve layer + marker_set = marker_api.getMarkerSet(RegionDynmapLayer.LAYER_ID); + if (marker_set == null) { + marker_set = marker_api.createMarkerSet(RegionDynmapLayer.LAYER_ID, parent.lang_layer_label.str(), null, false); + } + + if (marker_set == null) { + get_module().log.severe("Failed to create dynmap region marker set!"); + return; + } + + // Update attributes + marker_set.setMarkerSetLabel(parent.lang_layer_label.str()); + marker_set.setLayerPriority(parent.config_layer_priority); + marker_set.setHideByDefault(parent.config_layer_hide); + + // Initial update + update_all_markers(); + } + + private String id_for(final UUID region_id) { + return region_id.toString(); + } + + private String id_for(final Region region) { + return id_for(region.id()); + } + + public void update_marker(final Region region) { + if (!dynmap_enabled) { + return; + } + + // Area markers can't be updated. + remove_marker(region.id()); + + final var min = region.extent().min(); + final var max = region.extent().max(); + final var world_name = min.getWorld().getName(); + final var marker_id = id_for(region); + final var marker_label = parent.lang_marker_label.str(region.name()); + + final var xs = new double[] { min.getX(), max.getX() + 1 }; + final var zs = new double[] { min.getZ(), max.getZ() + 1 }; + final var area = marker_set.createAreaMarker(marker_id, marker_label, false, world_name, xs, zs, false); + area.setRangeY(max.getY() + 1, min.getY()); + area.setLineStyle(parent.config_line_weight, parent.config_line_opacity, parent.config_line_color); + area.setFillStyle(parent.config_fill_opacity, parent.config_fill_color); + } + + public void remove_marker(final UUID region_id) { + remove_marker(id_for(region_id)); + } + + public void remove_marker(final String marker_id) { + if (!dynmap_enabled || marker_id == null) { + return; + } + + remove_marker(marker_set.findMarker(marker_id)); + } + + public void remove_marker(final Marker marker) { + if (!dynmap_enabled || marker == null) { + return; + } + + marker.deleteMarker(); + } + + public void update_all_markers() { + if (!dynmap_enabled) { + return; + } + + // Update all existing + final var id_set = new HashSet(); + for (final var region : get_module().all_regions()) { + id_set.add(id_for(region)); + update_marker(region); + } + + // Remove orphaned + for (final var marker : marker_set.getMarkers()) { + final var id = marker.getMarkerID(); + if (id != null && !id_set.contains(id)) { + remove_marker(marker); + } + } + } +} diff --git a/vane-regions/src/main/java/org/oddlama/vane/regions/RegionEnvironmentSettingEnforcer.java b/vane-regions/src/main/java/org/oddlama/vane/regions/RegionEnvironmentSettingEnforcer.java new file mode 100644 index 000000000..d875f8a9e --- /dev/null +++ b/vane-regions/src/main/java/org/oddlama/vane/regions/RegionEnvironmentSettingEnforcer.java @@ -0,0 +1,210 @@ +package org.oddlama.vane.regions; + +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.entity.Animals; +import org.bukkit.entity.Monster; +import org.bukkit.entity.Player; +import org.bukkit.entity.Projectile; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.block.Action; +import org.bukkit.event.block.BlockBurnEvent; +import org.bukkit.event.block.BlockExplodeEvent; +import org.bukkit.event.block.BlockSpreadEvent; +import org.bukkit.event.entity.CreatureSpawnEvent; +import org.bukkit.event.entity.EntityChangeBlockEvent; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.entity.EntityExplodeEvent; +import org.bukkit.event.entity.PotionSplashEvent; +import org.bukkit.event.hanging.HangingBreakEvent; +import org.bukkit.event.player.PlayerInteractEvent; + +import org.oddlama.vane.core.Listener; +import org.oddlama.vane.core.module.Context; +import org.oddlama.vane.regions.region.EnvironmentSetting; + +public class RegionEnvironmentSettingEnforcer extends Listener { + public RegionEnvironmentSettingEnforcer(Context context) { + super(context); + } + + public boolean check_setting_at(final Location location, final EnvironmentSetting setting, final boolean check_against) { + final var region = get_module().region_at(location); + if (region == null) { + return false; + } + + final var group = region.region_group(get_module()); + return group.get_setting(setting) == check_against; + } + + public boolean check_setting_at(final Block block, final EnvironmentSetting setting, final boolean check_against) { + final var region = get_module().region_at(block); + if (region == null) { + return false; + } + + final var group = region.region_group(get_module()); + return group.get_setting(setting) == check_against; + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void on_block_explode(final BlockExplodeEvent event) { + // Prevent explosions from removing region blocks + final var it = event.blockList().iterator(); + while (it.hasNext()) { + if (check_setting_at(it.next(), EnvironmentSetting.EXPLOSIONS, false)) { + it.remove(); + } + } + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void on_entity_explode(final EntityExplodeEvent event) { + // Prevent explosions from removing region blocks + final var it = event.blockList().iterator(); + while (it.hasNext()) { + if (check_setting_at(it.next(), EnvironmentSetting.EXPLOSIONS, false)) { + it.remove(); + } + } + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void on_entity_change_block(final EntityChangeBlockEvent event) { + // Prevent entities from changing region blocks + if (check_setting_at(event.getBlock(), EnvironmentSetting.MONSTERS, false)) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void on_block_burn(final BlockBurnEvent event) { + if (check_setting_at(event.getBlock(), EnvironmentSetting.FIRE, false)) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void on_block_spread(final BlockSpreadEvent event) { + EnvironmentSetting setting; + switch (event.getNewState().getType()) { + default: + return; + + case FIRE: setting = EnvironmentSetting.FIRE; break; + case VINE: setting = EnvironmentSetting.VINE_GROWTH; break; + } + + if (check_setting_at(event.getBlock(), setting, false)) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void on_creature_spawn(final CreatureSpawnEvent event) { + // Only cancel natural spawns and alike + switch (event.getSpawnReason()) { + case JOCKEY: + case MOUNT: + case NATURAL: + break; + + default: + return; + } + + final var entity = event.getEntity(); + if (entity instanceof Monster) { + if (check_setting_at(event.getLocation(), EnvironmentSetting.MONSTERS, false)) { + event.setCancelled(true); + } + } else if (entity instanceof Animals) { + if (check_setting_at(event.getLocation(), EnvironmentSetting.ANIMALS, false)) { + event.setCancelled(true); + } + } + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void on_entity_damage_by_entity(final EntityDamageByEntityEvent event) { + final var damaged = event.getEntity(); + final var damager = event.getDamager(); + + switch (damaged.getType()) { + case PLAYER: break; + default: return; + } + + final Player player_damaged = (Player)damaged; + final Player player_damager; + if (damager instanceof Player) { + player_damager = (Player)damager; + } else if (damager instanceof Projectile && ((Projectile)damager).getShooter() instanceof Player) { + player_damager = (Player)((Projectile)damager).getShooter(); + } else { + return; + } + + if (player_damager != null && player_damaged != player_damager && ( + check_setting_at(player_damaged.getLocation(), EnvironmentSetting.PVP, false) || + check_setting_at(player_damager.getLocation(), EnvironmentSetting.PVP, false)) + ) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void on_hanging_break_event(final HangingBreakEvent event) { + switch (event.getCause()) { + default: return; + case ENTITY: return; // Handeled by on_hanging_break_by_entity + case EXPLOSION: { + if (check_setting_at(event.getEntity().getLocation(), EnvironmentSetting.EXPLOSIONS, false)) { + event.setCancelled(true); + } + return; + } + } + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void on_player_interact(final PlayerInteractEvent event) { + if (event.getAction() != Action.PHYSICAL) { + return; + } + + final var block = event.getClickedBlock(); + if (block != null && block.getType() == Material.FARMLAND) { + if (check_setting_at(block, EnvironmentSetting.TRAMPLE, false)) { + event.setCancelled(true); + } + } + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void on_potion_splash(final PotionSplashEvent event) { + // Only if a player threw the potion check for PVP + if (!(event.getEntity().getShooter() instanceof Player)) { + return; + } + + final var thrower = (Player)event.getEntity().getShooter(); + final var source_pvp_restricted = check_setting_at(thrower.getLocation(), EnvironmentSetting.PVP, false); + + // Cancel all damage to players if either thrower or damaged is + // inside no-PVP region + for (final var target : event.getAffectedEntities()) { + if (!(target instanceof Player)) { + continue; + } + + if (source_pvp_restricted || check_setting_at(target.getLocation(), EnvironmentSetting.PVP, false)) { + event.setIntensity(target, 0); + return; + } + } + } +} diff --git a/vane-regions/src/main/java/org/oddlama/vane/regions/RegionRoleSettingEnforcer.java b/vane-regions/src/main/java/org/oddlama/vane/regions/RegionRoleSettingEnforcer.java new file mode 100644 index 000000000..caf625444 --- /dev/null +++ b/vane-regions/src/main/java/org/oddlama/vane/regions/RegionRoleSettingEnforcer.java @@ -0,0 +1,407 @@ +package org.oddlama.vane.regions; + +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Tag; +import org.bukkit.block.Block; +import org.bukkit.block.Container; +import org.bukkit.block.DoubleChest; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.ItemFrame; +import org.bukkit.entity.Minecart; +import org.bukkit.entity.Player; +import org.bukkit.entity.Projectile; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.hanging.HangingBreakByEntityEvent; +import org.bukkit.event.hanging.HangingPlaceEvent; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryDragEvent; +import org.bukkit.event.inventory.InventoryInteractEvent; +import org.bukkit.event.player.PlayerArmorStandManipulateEvent; +import org.bukkit.event.player.PlayerBucketEmptyEvent; +import org.bukkit.event.player.PlayerBucketFillEvent; +import org.bukkit.event.player.PlayerInteractEntityEvent; +import org.bukkit.event.player.PlayerInteractEvent; + +import org.oddlama.vane.core.Listener; +import org.oddlama.vane.core.module.Context; +import org.oddlama.vane.portals.event.PortalActivateEvent; +import org.oddlama.vane.portals.event.PortalChangeSettingsEvent; +import org.oddlama.vane.portals.event.PortalConstructEvent; +import org.oddlama.vane.portals.event.PortalDeactivateEvent; +import org.oddlama.vane.portals.event.PortalDestroyEvent; +import org.oddlama.vane.portals.event.PortalLinkConsoleEvent; +import org.oddlama.vane.portals.event.PortalOpenConsoleEvent; +import org.oddlama.vane.portals.event.PortalSelectTargetEvent; +import org.oddlama.vane.portals.event.PortalUnlinkConsoleEvent; +import org.oddlama.vane.regions.region.RoleSetting; + +public class RegionRoleSettingEnforcer extends Listener { + public RegionRoleSettingEnforcer(Context context) { + super(context); + } + + public boolean check_setting_at(final Location location, final Player player, final RoleSetting setting, final boolean check_against) { + final var region = get_module().region_at(location); + if (region == null) { + return false; + } + + final var group = region.region_group(get_module()); + return group.get_role(player.getUniqueId()).get_setting(setting) == check_against; + } + + public boolean check_setting_at(final Block block, final Player player, final RoleSetting setting, final boolean check_against) { + final var region = get_module().region_at(block); + if (region == null) { + return false; + } + + final var group = region.region_group(get_module()); + return group.get_role(player.getUniqueId()).get_setting(setting) == check_against; + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void on_block_break(final BlockBreakEvent event) { + // Prevent breaking of region blocks + if (check_setting_at(event.getBlock(), event.getPlayer(), RoleSetting.BUILD, false)) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void on_block_place(final BlockPlaceEvent event) { + // Prevent (re-)placing of region blocks + if (check_setting_at(event.getBlock(), event.getPlayer(), RoleSetting.BUILD, false)) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void on_entity_damage_by_entity(final EntityDamageByEntityEvent event) { + final var damaged = event.getEntity(); + final var damager = event.getDamager(); + + switch (damaged.getType()) { + default: + return; + + case ARMOR_STAND: { + if (!(damager instanceof Player)) { + break; + } + + final var player_damager = (Player)damager; + if (check_setting_at(damaged.getLocation().getBlock(), player_damager, RoleSetting.BUILD, false)) { + event.setCancelled(true); + } + return; + } + + case ITEM_FRAME: { + if (!(damager instanceof Player)) { + break; + } + + final var player_damager = (Player)damager; + final var item_frame = (ItemFrame)damaged; + final var item = item_frame.getItem(); + if (item != null && item.getType() != Material.AIR) { + // This is a player taking the item out of an item-frame + if (check_setting_at(damaged.getLocation().getBlock(), player_damager, RoleSetting.CONTAINER, false)) { + event.setCancelled(true); + } + } + return; + } + } + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void on_hanging_break_by_entity(final HangingBreakByEntityEvent event) { + final Entity remover = event.getRemover(); + Player player = null; + + if (remover instanceof Player) { + player = (Player)remover; + } else if (remover instanceof Projectile) { + final var projectile = (Projectile)remover; + final var shooter = projectile.getShooter(); + if (shooter instanceof Player) { + player = (Player)shooter; + } + } + + if (player != null && check_setting_at(event.getEntity().getLocation(), player, RoleSetting.BUILD, false)) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void on_hanging_place(final HangingPlaceEvent event) { + if (check_setting_at(event.getEntity().getLocation(), event.getPlayer(), RoleSetting.BUILD, false)) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void on_player_armor_stand_manipulate(final PlayerArmorStandManipulateEvent event) { + if (check_setting_at(event.getRightClicked().getLocation(), event.getPlayer(), RoleSetting.BUILD, false)) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void on_player_bucket_empty(final PlayerBucketEmptyEvent event) { + if (check_setting_at(event.getBlockClicked(), event.getPlayer(), RoleSetting.BUILD, false)) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void on_player_bucket_fill(final PlayerBucketFillEvent event) { + if (check_setting_at(event.getBlockClicked(), event.getPlayer(), RoleSetting.BUILD, false)) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void on_player_interact_entity(final PlayerInteractEntityEvent event) { + final var entity = event.getRightClicked(); + if (entity == null || entity.getType() != EntityType.ITEM_FRAME) { + return; + } + + // Place or rotate item + if (check_setting_at(entity.getLocation(), event.getPlayer(), RoleSetting.CONTAINER, false)) { + event.setCancelled(true); + } + } + + // The EventPriority is HIGH, so this is executed AFTER the portals try + // to activate, which is a seperate permission. + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void on_player_interact(final PlayerInteractEvent event) { + final var player = event.getPlayer(); + final var block = event.getClickedBlock(); + if (block == null) { + return; + } + + switch (event.getAction()) { + default: return; + case PHYSICAL: { + if (Tag.PRESSURE_PLATES.isTagged(block.getType())) { + if (check_setting_at(block, player, RoleSetting.USE, false)) { + event.setCancelled(true); + } + } else if (block.getType() == Material.TRIPWIRE) { + if (check_setting_at(block, player, RoleSetting.USE, false)) { + event.setCancelled(true); + } + } + return; + } + + case RIGHT_CLICK_BLOCK: { + if (check_setting_at(block, player, RoleSetting.USE, false)) { + event.setCancelled(true); + } + return; + } + } + } + + public void on_player_inventory_interact(final InventoryInteractEvent event) { + final var clicker = event.getWhoClicked(); + if (!(clicker instanceof Player)) { + return; + } + + final var inventory = event.getInventory(); + if (inventory.getLocation() == null || inventory.getHolder() == null) { + // Inventory is virtual / transient + return; + } + + final var holder = inventory.getHolder(); + if (holder instanceof DoubleChest || holder instanceof Container || holder instanceof Minecart) { + if (check_setting_at(inventory.getLocation(), (Player)clicker, RoleSetting.CONTAINER, false)) { + event.setCancelled(true); + } + } + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void on_player_inventory_click(final InventoryClickEvent event) { + on_player_inventory_interact(event); + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void on_player_inventory_drag(final InventoryDragEvent event) { + on_player_inventory_interact(event); + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void on_portal_activate(final PortalActivateEvent event) { + if (check_setting_at(event.getPortal().spawn(), event.getPlayer(), RoleSetting.PORTAL, false)) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void on_portal_deactivate(final PortalDeactivateEvent event) { + if (check_setting_at(event.getPortal().spawn(), event.getPlayer(), RoleSetting.PORTAL, false)) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void on_portal_construct(final PortalConstructEvent event) { + // We have to check all blocks here, because otherwise players + // could "steal" boundary blocks from unowned regions + for (final var block : event.getBoundary().all_blocks()) { + // Portals in regions may only be constructed by region administrators + if (check_setting_at(block, event.getPlayer(), RoleSetting.ADMIN, false)) { + event.setCancelled(true); + return; + } + } + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void on_portal_destroy(final PortalDestroyEvent event) { + if (event.getPortal().owner().equals(event.getPlayer().getUniqueId())) { + // Owner may always use their portals + return; + } + + // We do NOT have to check all blocks here, because + // an existing portal with its spawn inside a region + // that the player controls can be considered proof of authority. + if (check_setting_at(event.getPortal().spawn(), event.getPlayer(), RoleSetting.ADMIN, false)) { + // Portals in regions may only be destroyed by region administrators + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = false) + public void on_portal_link_console(final PortalLinkConsoleEvent event) { + if (event.getPortal() != null && event.getPortal().owner().equals(event.getPlayer().getUniqueId())) { + // Owner may always use their portals + return; + } + + if (event.getPortal() != null && get_module().region_at(event.getPortal().spawn()) != null) { + // Portals in regions may be administrated by region administrators, + // not only be the owner + event.setCancelIfNotOwner(false); + } + + // Portals in regions may only be administrated by region administrators + // Check permission on console + if (check_setting_at(event.getConsole(), event.getPlayer(), RoleSetting.ADMIN, false)) { + event.setCancelled(true); + return; + } + + // Check permission on portal if any + if (event.getPortal() != null && check_setting_at(event.getPortal().spawn(), event.getPlayer(), RoleSetting.ADMIN, false)) { + event.setCancelled(true); + return; + } + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = false) + public void on_portal_unlink_console(final PortalUnlinkConsoleEvent event) { + if (event.getPortal().owner().equals(event.getPlayer().getUniqueId())) { + // Owner may always use their portals + return; + } + + if (get_module().region_at(event.getPortal().spawn()) != null) { + // Portals in regions may be administrated by region administrators, + // not only be the owner + event.setCancelIfNotOwner(false); + } + + // Portals in regions may only be administrated by region administrators + // Check permission on console + if (check_setting_at(event.getConsole(), event.getPlayer(), RoleSetting.ADMIN, false)) { + event.setCancelled(true); + return; + } + + // Check permission on portal + if (check_setting_at(event.getPortal().spawn(), event.getPlayer(), RoleSetting.ADMIN, false)) { + event.setCancelled(true); + return; + } + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void on_portal_open_console(final PortalOpenConsoleEvent event) { + if (event.getPortal().owner().equals(event.getPlayer().getUniqueId())) { + // Owner may always use their portals + return; + } + + // Check permission on console + if (check_setting_at(event.getConsole(), event.getPlayer(), RoleSetting.PORTAL, false)) { + event.setCancelled(true); + return; + } + + // Check permission on portal + if (check_setting_at(event.getPortal().spawn(), event.getPlayer(), RoleSetting.PORTAL, false)) { + event.setCancelled(true); + return; + } + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void on_portal_select_target(final PortalSelectTargetEvent event) { + if (event.getPortal().owner().equals(event.getPlayer().getUniqueId())) { + // Owner may always use their portals + return; + } + + // Check permission on source portal + if (check_setting_at(event.getPortal().spawn(), event.getPlayer(), RoleSetting.PORTAL, false)) { + event.setCancelled(true); + return; + } + + // Check permission on target portal + if (event.getTarget() != null && check_setting_at(event.getTarget().spawn(), event.getPlayer(), RoleSetting.PORTAL, false)) { + event.setCancelled(true); + return; + } + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = false) + public void on_portal_change_settings(final PortalChangeSettingsEvent event) { + if (event.getPortal().owner().equals(event.getPlayer().getUniqueId())) { + // Owner may always use their portals + return; + } + + if (get_module().region_at(event.getPortal().spawn()) == null) { + return; + } + + // Portals in regions may be administrated by region administrators, + // not only be the owner + event.setCancelIfNotOwner(false); + + // Now check if the player has the permission + if (check_setting_at(event.getPortal().spawn(), event.getPlayer(), RoleSetting.ADMIN, false)) { + event.setCancelled(true); + } + } +} diff --git a/vane-regions/src/main/java/org/oddlama/vane/regions/RegionSelectionListener.java b/vane-regions/src/main/java/org/oddlama/vane/regions/RegionSelectionListener.java new file mode 100644 index 000000000..33d5fad37 --- /dev/null +++ b/vane-regions/src/main/java/org/oddlama/vane/regions/RegionSelectionListener.java @@ -0,0 +1,58 @@ +package org.oddlama.vane.regions; + +import org.bukkit.Material; +import org.bukkit.event.Event; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.EquipmentSlot; + +import org.oddlama.vane.core.Listener; +import org.oddlama.vane.core.module.Context; + +public class RegionSelectionListener extends Listener { + public RegionSelectionListener(Context context) { + super(context); + } + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = false) + public void on_player_interact(final PlayerInteractEvent event) { + // Require main hand event + if (event.getHand() != EquipmentSlot.HAND) { + return; + } + + // Require empty hand + if (event.getItem() != null) { + return; + } + + final var player = event.getPlayer(); + final var selection = get_module().get_region_selection(player); + if (selection == null) { + return; + } + + if (player.getEquipment().getItemInMainHand().getType() != Material.AIR || + player.getEquipment().getItemInOffHand().getType() != Material.AIR) { + return; + } + + final var block = event.getClickedBlock(); + switch (event.getAction()) { + default: + return; + + case LEFT_CLICK_BLOCK: + selection.primary = block; + break; + + case RIGHT_CLICK_BLOCK: + selection.secondary = block; + break; + } + + event.setUseInteractedBlock(Event.Result.DENY); + event.setUseItemInHand(Event.Result.DENY); + } +} diff --git a/vane-regions/src/main/java/org/oddlama/vane/regions/Regions.java b/vane-regions/src/main/java/org/oddlama/vane/regions/Regions.java index c2c19233c..7860dd73d 100644 --- a/vane-regions/src/main/java/org/oddlama/vane/regions/Regions.java +++ b/vane-regions/src/main/java/org/oddlama/vane/regions/Regions.java @@ -1,8 +1,535 @@ package org.oddlama.vane.regions; +import static org.oddlama.vane.util.PlayerUtil.take_items; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; + +import net.minecraft.server.v1_16_R3.BlockPosition; + +import org.bukkit.Chunk; +import org.bukkit.Color; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Particle.DustOptions; +import org.bukkit.Particle; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.inventory.ItemStack; + import org.oddlama.vane.annotation.VaneModule; +import org.oddlama.vane.annotation.config.ConfigDouble; +import org.oddlama.vane.annotation.config.ConfigInt; +import org.oddlama.vane.annotation.config.ConfigMaterial; +import org.oddlama.vane.annotation.lang.LangMessage; +import org.oddlama.vane.annotation.persistent.Persistent; +import org.oddlama.vane.core.lang.TranslatedMessage; import org.oddlama.vane.core.module.Module; +import org.oddlama.vane.core.persistent.PersistentSerializer; +import org.oddlama.vane.portals.Portals; +import org.oddlama.vane.regions.menu.RegionGroupMenuTag; +import org.oddlama.vane.regions.menu.RegionMenuGroup; +import org.oddlama.vane.regions.menu.RegionMenuTag; +import org.oddlama.vane.regions.region.EnvironmentSetting; +import org.oddlama.vane.regions.region.Region; +import org.oddlama.vane.regions.region.RegionExtent; +import org.oddlama.vane.regions.region.RegionGroup; +import org.oddlama.vane.regions.region.RegionSelection; +import org.oddlama.vane.regions.region.Role; +import org.oddlama.vane.regions.region.RoleSetting; -@VaneModule(name = "regions", bstats = 8643, config_version = 1, lang_version = 1, storage_version = 1) +@VaneModule(name = "regions", bstats = 8643, config_version = 2, lang_version = 2, storage_version = 1) public class Regions extends Module { + // + // ┌───────────────────────┐ + // ┌────────────┐ is ┌───────────────┐ ┌───────────────────────┐ | belongs to ┌─────────────────┐ + // | Player 1 | ────> | [Role] Admin | ───┬──> | [RegionGroup] Default |─┘ <───┬─────── | [Region] MyHome | + // └────────────┘ └───────────────┘ | └───────────────────────┘ | └─────────────────┘ + // | | + // ┌────────────┐ in ┌───────────────┐ | | ┌─────────────────────┐ + // | Player 2 | ────> | [Role] Friend | ───┤ (are roles of) └─────── | [Region] Drecksloch | + // └────────────┘ └───────────────┘ | └─────────────────────┘ + // | + // ┌────────────┐ in ┌───────────────┐ | + // | Any Player | ────> | [Role] Others | ───┘ + // └────────────┘ └───────────────┘ + + // Add (de-)serializers + static { + PersistentSerializer.serializers.put(EnvironmentSetting.class, x -> ((EnvironmentSetting)x).name()); + PersistentSerializer.deserializers.put(EnvironmentSetting.class, x -> EnvironmentSetting.valueOf((String)x)); + PersistentSerializer.serializers.put(RoleSetting.class, x -> ((RoleSetting)x).name()); + PersistentSerializer.deserializers.put(RoleSetting.class, x -> RoleSetting.valueOf((String)x)); + PersistentSerializer.serializers.put(Role.class, Role::serialize); + PersistentSerializer.deserializers.put(Role.class, Role::deserialize); + PersistentSerializer.serializers.put(Role.RoleType.class, x -> ((Role.RoleType)x).name()); + PersistentSerializer.deserializers.put(Role.RoleType.class, x -> Role.RoleType.valueOf((String)x)); + PersistentSerializer.serializers.put(RegionGroup.class, RegionGroup::serialize); + PersistentSerializer.deserializers.put(RegionGroup.class, RegionGroup::deserialize); + PersistentSerializer.serializers.put(Region.class, Region::serialize); + PersistentSerializer.deserializers.put(Region.class, Region::deserialize); + PersistentSerializer.serializers.put(RegionExtent.class, RegionExtent::serialize); + PersistentSerializer.deserializers.put(RegionExtent.class, RegionExtent::deserialize); + } + + @ConfigInt(def = 4, min = 1, desc = "Minimum region extent in x direction.") + public int config_min_region_extent_x; + @ConfigInt(def = 4, min = 1, desc = "Minimum region extent in y direction.") + public int config_min_region_extent_y; + @ConfigInt(def = 4, min = 1, desc = "Minimum region extent in z direction.") + public int config_min_region_extent_z; + + @ConfigInt(def = 2048, min = 1, desc = "Maximum region extent in x direction.") + public int config_max_region_extent_x; + @ConfigInt(def = 2048, min = 1, desc = "Maximum region extent in y direction.") + public int config_max_region_extent_y; + @ConfigInt(def = 2048, min = 1, desc = "Maximum region extent in z direction.") + public int config_max_region_extent_z; + + @ConfigMaterial(def = Material.DIAMOND, desc = "The currency material for regions.") + public Material config_currency; + @ConfigDouble(def = 2.0, min = 0.0, desc = "The base amount of currency required to buy an area equal to one chunk (256 blocks).") + public double config_cost_xz_base; + @ConfigDouble(def = 1.15, min = 1.0, desc = "The multiplicator determines how much the cost increases for each additional 16 blocks of height. A region of height h will cost multiplicator^(h / 16.0) * base_amount. Rounding is applied at the end.") + public double config_cost_y_multiplicator; + + // Primary storage for all regions (region.id → region) + @Persistent + private Map storage_regions = new HashMap<>(); + // Primary storage for all region_groups (region_group.id → region_group) + @Persistent + private Map storage_region_groups = new HashMap<>(); + // Primary storage for the default region groups for new regions created by a player (player_uuid → region_group.id) + @Persistent + private Map storage_default_region_group = new HashMap<>(); + + // Per-chunk lookup cache (world_id → chunk_key → [possible regions]) + private Map>> regions_in_chunk_in_world = new HashMap<>(); + // A map containing the current extent for each player who is currently selecting a region + // No key → Player not in selection mode + // extent.min or extent.max null → Selection mode active, but no selection has been made yet + private Map region_selections = new HashMap<>(); + + @LangMessage public TranslatedMessage lang_start_region_selection; + + public RegionMenuGroup menus; + public RegionDynmapLayer dynmap_layer; + + public Regions() { + menus = new RegionMenuGroup(this); + + new org.oddlama.vane.regions.commands.Region(this); + + new RegionEnvironmentSettingEnforcer(this); + new RegionRoleSettingEnforcer(this); + new RegionSelectionListener(this); + dynmap_layer = new RegionDynmapLayer(this); + } + + public void delayed_on_enable() { + for (var region : storage_regions.values()) { + index_add_region(region); + } + } + + @Override + public void on_enable() { + final var portals = (Portals)getServer().getPluginManager().getPlugin("vane-portals"); + + // Register callback to portals module so portals + // can find out if two portals are in the same region group + portals.set_is_in_same_region_group_callback((a, b) -> { + final var reg_a = region_at(a.spawn()); + final var reg_b = region_at(b.spawn()); + if (reg_a == null || reg_b == null) { + return reg_a == reg_b; + } + return reg_a.region_group_id().equals(reg_b.region_group_id()); + }); + + portals.set_player_can_use_portals_in_region_group_of_callback((player, portal) -> { + final var region = region_at(portal.spawn()); + if (region == null) { + // No region -> no restriction. + return true; + } + final var group = region.region_group(get_module()); + return group.get_role(player.getUniqueId()).get_setting(RoleSetting.PORTAL); + }); + + schedule_next_tick(this::delayed_on_enable); + // Every second: Visualize selections + schedule_task_timer(this::visualize_selections, 1l, 20l); + } + + public Collection all_regions() { + return storage_regions.values(); + } + + public Collection all_region_groups() { + return storage_region_groups.values(); + } + + public void start_region_selection(final Player player) { + region_selections.put(player.getUniqueId(), new RegionSelection(this)); + lang_start_region_selection.send(player); + } + + public void cancel_region_selection(final Player player) { + region_selections.remove(player.getUniqueId()); + } + + public boolean is_selecting_region(final Player player) { + return region_selections.containsKey(player.getUniqueId()); + } + + public RegionSelection get_region_selection(final Player player) { + return region_selections.get(player.getUniqueId()); + } + + private static final int visualize_max_particels = 20000; + private static final int visualize_particles_per_block = 12; + private static final double visualize_stddev_compensation = 0.25; + private static final DustOptions visualize_dust_invalid = new DustOptions(Color.fromRGB(230, 60, 11), 1.0f); + private static final DustOptions visualize_dust_valid = new DustOptions(Color.fromRGB(120, 220, 60), 1.0f); + + private void visualize_edge(final World world, final BlockPosition c1, final BlockPosition c2, final boolean valid) { + // Unfortunately, particle spawns are normal distributed. + // To still have a good visualization, we need to calculate a stddev that looks + // good. Empirically we chose a 1/2 of the radius. + final double mx = (c1.getX() + c2.getX()) / 2.0 + 0.5; + final double my = (c1.getY() + c2.getY()) / 2.0 + 0.5; + final double mz = (c1.getZ() + c2.getZ()) / 2.0 + 0.5; + double dx = Math.abs(c1.getX() - c2.getX()); + double dy = Math.abs(c1.getY() - c2.getY()); + double dz = Math.abs(c1.getZ() - c2.getZ()); + final double len = dx + dy + dz; + final int count = Math.min(visualize_max_particels, (int)(visualize_particles_per_block * len)); + + // Compensate for using normal distributed particles + dx *= visualize_stddev_compensation; + dy *= visualize_stddev_compensation; + dz *= visualize_stddev_compensation; + + // Spawn base particles + world.spawnParticle(Particle.END_ROD, + mx, my, mz, + count, + dx, dy, dz, + 0.0, // speed + null, // data + true); // force + + // Spawn colored particles indicating validity + world.spawnParticle(Particle.REDSTONE, + mx, my, mz, + count, + dx, dy, dz, + 0.0, // speed + valid ? visualize_dust_valid : visualize_dust_invalid, // data + true); // force + } + + private void visualize_selections() { + for (final var selection_owner : region_selections.keySet()) { + final var selection = region_selections.get(selection_owner); + if (selection == null) { + continue; + } + + // Get player for selection + final var offline_player = getServer().getOfflinePlayer(selection_owner); + if (!offline_player.isOnline()) { + continue; + } + final var player = offline_player.getPlayer(); + + // Both blocks set + if (selection.primary == null || selection.secondary == null) { + continue; + } + + // Worlds match + if (!selection.primary.getWorld().equals(selection.secondary.getWorld())) { + continue; + } + + // Extent is visualizable. Prepare parameters. + final var world = selection.primary.getWorld(); + // Check if selection is valid + final var valid = selection.is_valid(player); + + final var lx = Math.min(selection.primary.getX(), selection.secondary.getX()); + final var ly = Math.min(selection.primary.getY(), selection.secondary.getY()); + final var lz = Math.min(selection.primary.getZ(), selection.secondary.getZ()); + final var hx = Math.max(selection.primary.getX(), selection.secondary.getX()); + final var hy = Math.max(selection.primary.getY(), selection.secondary.getY()); + final var hz = Math.max(selection.primary.getZ(), selection.secondary.getZ()); + + // Corners + final var A = new BlockPosition(lx, ly, lz); + final var B = new BlockPosition(hx, ly, lz); + final var C = new BlockPosition(hx, hy, lz); + final var D = new BlockPosition(lx, hy, lz); + final var E = new BlockPosition(lx, ly, hz); + final var F = new BlockPosition(hx, ly, hz); + final var G = new BlockPosition(hx, hy, hz); + final var H = new BlockPosition(lx, hy, hz); + + // Visualize each edge + visualize_edge(world, A, B, valid); + visualize_edge(world, B, C, valid); + visualize_edge(world, C, D, valid); + visualize_edge(world, D, A, valid); + visualize_edge(world, E, F, valid); + visualize_edge(world, F, G, valid); + visualize_edge(world, G, H, valid); + visualize_edge(world, H, E, valid); + visualize_edge(world, A, E, valid); + visualize_edge(world, B, F, valid); + visualize_edge(world, C, G, valid); + visualize_edge(world, D, H, valid); + } + } + + public void add_region_group(final RegionGroup group) { + storage_region_groups.put(group.id(), group); + mark_persistent_storage_dirty(); + } + + public boolean can_remove_region_group(final RegionGroup group) { + // Returns true if this region group is unused and can be removed. + + // If this region group is the fallback default group, it is permanent! + if (storage_default_region_group.values().contains(group.id())) { + return false; + } + + // If any region uses this group, we can't remove it. + if (storage_regions.values().stream().anyMatch( + r -> r.region_group_id().equals(group.id()))) { + return false; + } + + return true; + } + + public void remove_region_group(final RegionGroup group) { + // Assert that this region group is unused. + if (!can_remove_region_group(group)) { + return; + } + + // Remove region group from storage + if (storage_region_groups.remove(group.id()) == null) { + // Was already removed + return; + } + + mark_persistent_storage_dirty(); + + // Close and taint all related open menus + get_module().core.menu_manager.for_each_open((player, menu) -> { + if (menu.tag() instanceof RegionGroupMenuTag + && Objects.equals(((RegionGroupMenuTag)menu.tag()).region_group_id(), group.id())) { + menu.taint(); + menu.close(player); + } + }); + } + + public RegionGroup get_region_group(final UUID region_group) { + return storage_region_groups.get(region_group); + } + + public boolean create_region_from_selection(final Player player, final String name) { + final var selection = get_region_selection(player); + if (!selection.is_valid(player)) { + return false; + } + + // Take currency items + final var price = selection.price(); + final var map = new HashMap(); + map.put(new ItemStack(config_currency), price); + if (price > 0 && !take_items(player, map)) { + return false; + } + + final var def_region_group = get_or_create_default_region_group(player); + final var region = new Region(name, player.getUniqueId(), selection.extent(), def_region_group.id()); + add_region(region); + cancel_region_selection(player); + return true; + } + + public void add_region(final Region region) { + storage_regions.put(region.id(), region); + mark_persistent_storage_dirty(); + + // Index region for fast lookup + index_add_region(region); + + // Create dynmap marker + dynmap_layer.update_marker(region); + } + + public void remove_region(final Region region) { + // Remove region from storage + if (storage_regions.remove(region.id()) == null) { + // Was already removed + return; + } + + mark_persistent_storage_dirty(); + + // Close and taint all related open menus + get_module().core.menu_manager.for_each_open((player, menu) -> { + if (menu.tag() instanceof RegionMenuTag + && Objects.equals(((RegionMenuTag)menu.tag()).region_id(), region.id())) { + menu.taint(); + menu.close(player); + } + }); + + // Remove region from index + index_remove_region(region); + + // Remove dynmap marker + dynmap_layer.remove_marker(region.id()); + } + + private void index_add_region(final Region region) { + // Adds the region to the lookup map at all intersecting chunks + final var min = region.extent().min(); + final var max = region.extent().max(); + + final var world_id = min.getWorld().getUID(); + var regions_in_chunk = regions_in_chunk_in_world.get(world_id); + if (regions_in_chunk == null) { + regions_in_chunk = new HashMap>(); + regions_in_chunk_in_world.put(world_id, regions_in_chunk); + } + + final var min_chunk = min.getChunk(); + final var max_chunk = max.getChunk(); + + // Iterate all the chunks which intersect the region + for (int cx = min_chunk.getX(); cx <= max_chunk.getX(); ++cx) { + for (int cz = min_chunk.getZ(); cz <= max_chunk.getZ(); ++cz) { + final var chunk_key = Chunk.getChunkKey(cx, cz); + var possible_regions = regions_in_chunk.get(chunk_key); + if (possible_regions == null) { + possible_regions = new ArrayList(); + regions_in_chunk.put(chunk_key, possible_regions); + } + possible_regions.add(region); + } + } + } + + private void index_remove_region(final Region region) { + // Removes the region from the lookup map at all intersecting chunks + final var min = region.extent().min(); + final var max = region.extent().max(); + + final var world_id = min.getWorld().getUID(); + final var regions_in_chunk = regions_in_chunk_in_world.get(world_id); + if (regions_in_chunk == null) { + return; + } + + final var min_chunk = min.getChunk(); + final var max_chunk = max.getChunk(); + + // Iterate all the chunks which intersect the region + for (int cx = min_chunk.getX(); cx <= max_chunk.getX(); ++cx) { + for (int cz = min_chunk.getZ(); cz <= max_chunk.getZ(); ++cz) { + final var chunk_key = Chunk.getChunkKey(cx, cz); + final var possible_regions = regions_in_chunk.get(chunk_key); + if (possible_regions == null) { + continue; + } + possible_regions.remove(region); + } + } + } + + public Region region_at(final Location loc) { + final var world_id = loc.getWorld().getUID(); + final var regions_in_chunk = regions_in_chunk_in_world.get(world_id); + if (regions_in_chunk == null) { + return null; + } + + final var chunk_key = loc.getChunk().getChunkKey(); + final var possible_regions = regions_in_chunk.get(chunk_key); + if (possible_regions == null) { + return null; + } + + for (final var region : possible_regions) { + if (region.extent().is_inside(loc)) { + return region; + } + } + + return null; + } + + public Region region_at(final Block block) { + final var world_id = block.getWorld().getUID(); + final var regions_in_chunk = regions_in_chunk_in_world.get(world_id); + if (regions_in_chunk == null) { + return null; + } + + final var chunk_key = block.getChunk().getChunkKey(); + final var possible_regions = regions_in_chunk.get(chunk_key); + if (possible_regions == null) { + return null; + } + + for (final var region : possible_regions) { + if (region.extent().is_inside(block)) { + return region; + } + } + + return null; + } + + public RegionGroup get_or_create_default_region_group(final Player owner) { + final var owner_id = owner.getUniqueId(); + final var region_group_id = storage_default_region_group.get(owner_id); + if (region_group_id != null) { + return get_region_group(region_group_id); + } + + // Create and save owners's default group + final var region_group = new RegionGroup("[default] " + owner.getName(), owner_id); + add_region_group(region_group); + + // Set group as the default + storage_default_region_group.put(owner_id, region_group.id()); + mark_persistent_storage_dirty(); + + return region_group; + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void on_player_quit(final PlayerQuitEvent event) { + // Remove pending selection + cancel_region_selection(event.getPlayer()); + } } diff --git a/vane-regions/src/main/java/org/oddlama/vane/regions/commands/Region.java b/vane-regions/src/main/java/org/oddlama/vane/regions/commands/Region.java new file mode 100644 index 000000000..ddfb1d75f --- /dev/null +++ b/vane-regions/src/main/java/org/oddlama/vane/regions/commands/Region.java @@ -0,0 +1,26 @@ +package org.oddlama.vane.regions.commands; + +import org.bukkit.entity.Player; + +import org.oddlama.vane.annotation.command.Aliases; +import org.oddlama.vane.annotation.command.Name; +import org.oddlama.vane.core.command.Command; +import org.oddlama.vane.core.module.Context; +import org.oddlama.vane.regions.Regions; + +@Name("region") +@Aliases({"regions", "rg"}) +public class Region extends Command { + public Region(Context context) { + super(context); + + // Add help + params().fixed("help").ignore_case().exec(this::print_help); + // Command parameters + params().exec_player(this::open_menu); + } + + private void open_menu(Player player) { + get_module().menus.main_menu.create(player).open(player); + } +} diff --git a/vane-regions/src/main/java/org/oddlama/vane/regions/menu/EnterRegionGroupNameMenu.java b/vane-regions/src/main/java/org/oddlama/vane/regions/menu/EnterRegionGroupNameMenu.java new file mode 100644 index 000000000..865bf4b1f --- /dev/null +++ b/vane-regions/src/main/java/org/oddlama/vane/regions/menu/EnterRegionGroupNameMenu.java @@ -0,0 +1,40 @@ +package org.oddlama.vane.regions.menu; + +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import org.oddlama.vane.annotation.config.ConfigMaterial; +import org.oddlama.vane.annotation.lang.LangMessage; +import org.oddlama.vane.core.functional.Function2; +import org.oddlama.vane.core.lang.TranslatedMessage; +import org.oddlama.vane.core.menu.Menu.ClickResult; +import org.oddlama.vane.core.menu.Menu; +import org.oddlama.vane.core.menu.MenuFactory; +import org.oddlama.vane.core.module.Context; +import org.oddlama.vane.core.module.ModuleComponent; +import org.oddlama.vane.regions.Regions; + +public class EnterRegionGroupNameMenu extends ModuleComponent { + @LangMessage public TranslatedMessage lang_title; + @ConfigMaterial(def = Material.GLOBE_BANNER_PATTERN, desc = "The item used to name region groups.") + public Material config_material; + + public EnterRegionGroupNameMenu(Context context) { + super(context.namespace("enter_region_group_name")); + } + + public Menu create(final Player player, final Function2 on_click) { + return create(player, "Group", on_click); + } + + public Menu create(final Player player, final String default_name, final Function2 on_click) { + return MenuFactory.anvil_string_input(get_context(), player, lang_title.str(), new ItemStack(config_material), default_name, (p, menu, name) -> { + menu.close(p); + return on_click.apply(p, name); + }); + } + + @Override public void on_enable() {} + @Override public void on_disable() {} +} diff --git a/vane-regions/src/main/java/org/oddlama/vane/regions/menu/EnterRegionNameMenu.java b/vane-regions/src/main/java/org/oddlama/vane/regions/menu/EnterRegionNameMenu.java new file mode 100644 index 000000000..35fefd8af --- /dev/null +++ b/vane-regions/src/main/java/org/oddlama/vane/regions/menu/EnterRegionNameMenu.java @@ -0,0 +1,40 @@ +package org.oddlama.vane.regions.menu; + +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import org.oddlama.vane.annotation.config.ConfigMaterial; +import org.oddlama.vane.annotation.lang.LangMessage; +import org.oddlama.vane.core.functional.Function2; +import org.oddlama.vane.core.lang.TranslatedMessage; +import org.oddlama.vane.core.menu.Menu.ClickResult; +import org.oddlama.vane.core.menu.Menu; +import org.oddlama.vane.core.menu.MenuFactory; +import org.oddlama.vane.core.module.Context; +import org.oddlama.vane.core.module.ModuleComponent; +import org.oddlama.vane.regions.Regions; + +public class EnterRegionNameMenu extends ModuleComponent { + @LangMessage public TranslatedMessage lang_title; + @ConfigMaterial(def = Material.MAP, desc = "The item used to name regions.") + public Material config_material; + + public EnterRegionNameMenu(Context context) { + super(context.namespace("enter_region_name")); + } + + public Menu create(final Player player, final Function2 on_click) { + return create(player, "Region", on_click); + } + + public Menu create(final Player player, final String default_name, final Function2 on_click) { + return MenuFactory.anvil_string_input(get_context(), player, lang_title.str(), new ItemStack(config_material), default_name, (p, menu, name) -> { + menu.close(p); + return on_click.apply(p, name); + }); + } + + @Override public void on_enable() {} + @Override public void on_disable() {} +} diff --git a/vane-regions/src/main/java/org/oddlama/vane/regions/menu/EnterRoleNameMenu.java b/vane-regions/src/main/java/org/oddlama/vane/regions/menu/EnterRoleNameMenu.java new file mode 100644 index 000000000..6ede293a6 --- /dev/null +++ b/vane-regions/src/main/java/org/oddlama/vane/regions/menu/EnterRoleNameMenu.java @@ -0,0 +1,40 @@ +package org.oddlama.vane.regions.menu; + +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import org.oddlama.vane.annotation.config.ConfigMaterial; +import org.oddlama.vane.annotation.lang.LangMessage; +import org.oddlama.vane.core.functional.Function2; +import org.oddlama.vane.core.lang.TranslatedMessage; +import org.oddlama.vane.core.menu.Menu.ClickResult; +import org.oddlama.vane.core.menu.Menu; +import org.oddlama.vane.core.menu.MenuFactory; +import org.oddlama.vane.core.module.Context; +import org.oddlama.vane.core.module.ModuleComponent; +import org.oddlama.vane.regions.Regions; + +public class EnterRoleNameMenu extends ModuleComponent { + @LangMessage public TranslatedMessage lang_title; + @ConfigMaterial(def = Material.BOOK, desc = "The item used to name roles.") + public Material config_material; + + public EnterRoleNameMenu(Context context) { + super(context.namespace("enter_role_name")); + } + + public Menu create(final Player player, final Function2 on_click) { + return create(player, "Role", on_click); + } + + public Menu create(final Player player, final String default_name, final Function2 on_click) { + return MenuFactory.anvil_string_input(get_context(), player, lang_title.str(), new ItemStack(config_material), default_name, (p, menu, name) -> { + menu.close(p); + return on_click.apply(p, name); + }); + } + + @Override public void on_enable() {} + @Override public void on_disable() {} +} diff --git a/vane-regions/src/main/java/org/oddlama/vane/regions/menu/MainMenu.java b/vane-regions/src/main/java/org/oddlama/vane/regions/menu/MainMenu.java new file mode 100644 index 000000000..398390f78 --- /dev/null +++ b/vane-regions/src/main/java/org/oddlama/vane/regions/menu/MainMenu.java @@ -0,0 +1,297 @@ +package org.oddlama.vane.regions.menu; + +import java.util.stream.Collectors; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import org.oddlama.vane.annotation.lang.LangMessage; +import org.oddlama.vane.core.config.TranslatedItemStack; +import org.oddlama.vane.core.lang.TranslatedMessage; +import org.oddlama.vane.core.menu.Filter; +import org.oddlama.vane.core.menu.Menu.ClickResult; +import org.oddlama.vane.core.menu.Menu; +import org.oddlama.vane.core.menu.MenuFactory; +import org.oddlama.vane.core.menu.MenuItem; +import org.oddlama.vane.core.menu.MenuWidget; +import org.oddlama.vane.core.module.Context; +import org.oddlama.vane.core.module.ModuleComponent; +import org.oddlama.vane.regions.Regions; +import org.oddlama.vane.regions.region.Region; +import org.oddlama.vane.regions.region.RegionGroup; +import org.oddlama.vane.regions.region.RegionSelection; +import org.oddlama.vane.regions.region.RoleSetting; + +public class MainMenu extends ModuleComponent { + @LangMessage public TranslatedMessage lang_title; + @LangMessage public TranslatedMessage lang_select_region_title; + @LangMessage public TranslatedMessage lang_filter_regions_title; + @LangMessage public TranslatedMessage lang_select_region_group_title; + @LangMessage public TranslatedMessage lang_filter_region_groups_title; + + public TranslatedItemStack item_create_region_start_selection; + public TranslatedItemStack item_create_region_invalid_selection; + public TranslatedItemStack item_create_region_valid_selection; + public TranslatedItemStack item_cancel_selection; + public TranslatedItemStack item_current_region; + public TranslatedItemStack item_list_regions; + public TranslatedItemStack item_select_region; + public TranslatedItemStack item_create_region_group; + public TranslatedItemStack item_current_region_group; + public TranslatedItemStack item_list_region_groups; + public TranslatedItemStack item_select_region_group; + + public MainMenu(Context context) { + super(context.namespace("main")); + + final var ctx = get_context(); + item_create_region_start_selection = new TranslatedItemStack<>(ctx, "create_region_start_selection", Material.WRITABLE_BOOK, 1, "Used to start creating a new region selection."); + item_create_region_invalid_selection = new TranslatedItemStack<>(ctx, "create_region_invalid_selection", Material.BARRIER, 1, "Used to indicate an invalid selection."); + item_create_region_valid_selection = new TranslatedItemStack<>(ctx, "create_region_valid_selection", Material.WRITABLE_BOOK, 1, "Used to create a new region with the current selection."); + item_cancel_selection = new TranslatedItemStack<>(ctx, "cancel_selection", Material.RED_TERRACOTTA, 1, "Used to cancel region selection."); + item_list_regions = new TranslatedItemStack<>(ctx, "list_regions", Material.COMPASS, 1, "Used to select a region the player owns."); + item_select_region = new TranslatedItemStack<>(ctx, "select_region", Material.FILLED_MAP, 1, "Used to represent a region in the region selection list."); + item_current_region = new TranslatedItemStack<>(ctx, "current_region", Material.FILLED_MAP, 1, "Used to access the region the player currently stands in."); + item_create_region_group = new TranslatedItemStack<>(ctx, "create_region_group", Material.WRITABLE_BOOK, 1, "Used to create a new region group."); + item_list_region_groups = new TranslatedItemStack<>(ctx, "list_region_groups", Material.COMPASS, 1, "Used to select a region group the player may administrate."); + item_current_region_group = new TranslatedItemStack<>(ctx, "current_region_group", Material.GLOBE_BANNER_PATTERN, 1, "Used to access the region group associated with the region the player currently stands in."); + item_select_region_group = new TranslatedItemStack<>(ctx, "select_region_group", Material.GLOBE_BANNER_PATTERN, 1, "Used to represent a region group in the region group selection list."); + } + + public Menu create(final Player player) { + final var columns = 9; + final var title = lang_title.str(); + final var main_menu = new Menu(get_context(), Bukkit.createInventory(null, columns, title)); + + final var selection_mode = get_module().is_selecting_region(player); + final var region = get_module().region_at(player.getLocation()); + if (region != null) { + main_menu.tag(new RegionMenuTag(region.id())); + } + + // Check if target selection would be allowed + if (selection_mode) { + final var selection = get_module().get_region_selection(player); + main_menu.add(menu_item_create_region(player, selection)); + main_menu.add(menu_item_cancel_selection()); + } else { + main_menu.add(menu_item_start_selection()); + main_menu.add(menu_item_list_regions()); + if (region != null) { + main_menu.add(menu_item_current_region(region)); + } + } + + main_menu.add(menu_item_create_region_group()); + main_menu.add(menu_item_list_region_groups()); + if (region != null) { + main_menu.add(menu_item_current_region_group(region.region_group(get_module()))); + } + + return main_menu; + } + + private MenuWidget menu_item_start_selection() { + return new MenuItem(0, item_create_region_start_selection.item(), (player, menu, self) -> { + menu.close(player); + get_module().start_region_selection(player); + return ClickResult.SUCCESS; + }); + } + + private MenuWidget menu_item_cancel_selection() { + return new MenuItem(1, item_cancel_selection.item(), (player, menu, self) -> { + menu.close(player); + get_module().cancel_region_selection(player); + return ClickResult.SUCCESS; + }); + } + + private MenuWidget menu_item_create_region(final Player final_player, final RegionSelection selection) { + return new MenuItem(0, null, (player, menu, self) -> { + if (selection.is_valid(final_player)) { + menu.close(player); + + get_module().menus.enter_region_name_menu.create(player, (player2, name) -> { + if (get_module().create_region_from_selection(final_player, name)) { + return ClickResult.SUCCESS; + } else { + return ClickResult.ERROR; + } + }).on_natural_close(player2 -> { + menu.open(player2); + }).open(player); + + return ClickResult.SUCCESS; + } else { + return ClickResult.ERROR; + } + }) { + @Override + public void item(final ItemStack item) { + if (selection.is_valid(final_player)) { + final var dx = 1 + Math.abs(selection.primary.getX() - selection.secondary.getX()); + final var dy = 1 + Math.abs(selection.primary.getY() - selection.secondary.getY()); + final var dz = 1 + Math.abs(selection.primary.getZ() - selection.secondary.getZ()); + super.item(item_create_region_valid_selection.item( + "§a" + dx, + "§a" + dy, + "§a" + dz, + "§b" + get_module().config_min_region_extent_x, + "§b" + get_module().config_min_region_extent_y, + "§b" + get_module().config_min_region_extent_z, + "§b" + get_module().config_max_region_extent_x, + "§b" + get_module().config_max_region_extent_y, + "§b" + get_module().config_max_region_extent_z, + "§a" + selection.price() + " §b" + String.valueOf(get_module().config_currency).toLowerCase() + )); + } else { + boolean is_primary_set = selection.primary != null; + boolean is_secondary_set = selection.secondary != null; + boolean same_world = is_primary_set && is_secondary_set && selection.primary.getWorld().equals(selection.secondary.getWorld()); + + boolean minimum_satisified, maximum_satisfied, no_intersection, can_afford; + String sdx, sdy, sdz; + String price; + if (is_primary_set && is_secondary_set && same_world) { + final var dx = 1 + Math.abs(selection.primary.getX() - selection.secondary.getX()); + final var dy = 1 + Math.abs(selection.primary.getY() - selection.secondary.getY()); + final var dz = 1 + Math.abs(selection.primary.getZ() - selection.secondary.getZ()); + sdx = Integer.toString(dx); + sdy = Integer.toString(dy); + sdz = Integer.toString(dz); + + minimum_satisified = + dx >= get_module().config_min_region_extent_x && + dy >= get_module().config_min_region_extent_y && + dz >= get_module().config_min_region_extent_z; + maximum_satisfied = + dx <= get_module().config_max_region_extent_x && + dy <= get_module().config_max_region_extent_y && + dz <= get_module().config_max_region_extent_z; + no_intersection = !selection.intersects_existing(); + can_afford = selection.can_afford(final_player); + price = (can_afford ? "§a" : "$§") + selection.price() + " §b" + String.valueOf(get_module().config_currency).toLowerCase(); + } else { + sdx = "§7?"; + sdy = "§7?"; + sdz = "§7?"; + minimum_satisified = false; + maximum_satisfied = false; + no_intersection = true; + can_afford = false; + price = "§7?"; + } + + final var extent_color = minimum_satisified && maximum_satisfied ? "§a" : "§c"; + super.item(item_create_region_invalid_selection.item( + is_primary_set ? "§a✓" : "§c✕", + is_secondary_set ? "§a✓" : "§c✕", + same_world ? "§a✓" : "§c✕", + no_intersection ? "§a✓" : "§c✕", + minimum_satisified ? "§a✓" : "§c✕", + maximum_satisfied ? "§a✓" : "§c✕", + can_afford ? "§a✓" : "§c✕", + extent_color + sdx, + extent_color + sdy, + extent_color + sdz, + "§b" + get_module().config_min_region_extent_x, + "§b" + get_module().config_min_region_extent_y, + "§b" + get_module().config_min_region_extent_z, + "§b" + get_module().config_max_region_extent_x, + "§b" + get_module().config_max_region_extent_y, + "§b" + get_module().config_max_region_extent_z, + price + )); + } + } + }; + } + + private MenuWidget menu_item_list_regions() { + return new MenuItem(1, item_list_regions.item(), (player, menu, self) -> { + menu.close(player); + final var all_regions = get_module().all_regions() + .stream() + .filter(r -> player.getUniqueId().equals(r.owner())) + .sorted((a, b) -> a.name().compareToIgnoreCase(b.name())) + .collect(Collectors.toList()); + + final var filter = new Filter.StringFilter((r, str) -> r.name().toLowerCase().contains(str)); + MenuFactory.generic_selector(get_context(), player, lang_select_region_title.str(), lang_filter_regions_title.str(), all_regions, + r -> item_select_region.item("§a§l" + r.name()), + filter, + (player2, m, region) -> { + m.close(player2); + get_module().menus.region_menu.create(region, player2).open(player2); + return ClickResult.SUCCESS; + }, player2 -> { + menu.open(player2); + }).open(player); + return ClickResult.SUCCESS; + }); + } + + private MenuWidget menu_item_current_region(final Region region) { + return new MenuItem(2, item_current_region.item("§a§l" + region.name()), (player, menu, self) -> { + menu.close(player); + get_module().menus.region_menu.create(region, player).open(player); + return ClickResult.SUCCESS; + }); + } + + private MenuWidget menu_item_create_region_group() { + return new MenuItem(7, item_create_region_group.item(), (player, menu, self) -> { + menu.close(player); + get_module().menus.enter_region_group_name_menu.create(player, (player2, name) -> { + final var group = new RegionGroup(name, player2.getUniqueId()); + get_module().add_region_group(group); + get_module().menus.region_group_menu.create(group, player).open(player); + return ClickResult.SUCCESS; + }).on_natural_close(player2 -> { + menu.open(player2); + }).open(player); + + return ClickResult.SUCCESS; + }); + } + + private MenuWidget menu_item_list_region_groups() { + return new MenuItem(8, item_list_region_groups.item(), (player, menu, self) -> { + menu.close(player); + final var all_region_groups = get_module().all_region_groups() + .stream() + .filter(g -> player.getUniqueId().equals(g.owner()) + || g.get_role(player.getUniqueId()) + .get_setting(RoleSetting.ADMIN)) + .sorted((a, b) -> a.name().compareToIgnoreCase(b.name())) + .collect(Collectors.toList()); + + final var filter = new Filter.StringFilter((r, str) -> r.name().toLowerCase().contains(str)); + MenuFactory.generic_selector(get_context(), player, lang_select_region_group_title.str(), lang_filter_region_groups_title.str(), all_region_groups, + r -> item_select_region_group.item("§a§l" + r.name()), + filter, + (player2, m, group) -> { + m.close(player2); + get_module().menus.region_group_menu.create(group, player).open(player); + return ClickResult.SUCCESS; + }, player2 -> { + menu.open(player2); + }).open(player); + return ClickResult.SUCCESS; + }); + } + + private MenuWidget menu_item_current_region_group(final RegionGroup region_group) { + return new MenuItem(6, item_current_region_group.item("§a§l" + region_group.name()), (player, menu, self) -> { + menu.close(player); + get_module().menus.region_group_menu.create(region_group, player).open(player); + return ClickResult.SUCCESS; + }); + } + + @Override public void on_enable() {} + @Override public void on_disable() {} +} diff --git a/vane-regions/src/main/java/org/oddlama/vane/regions/menu/RegionGroupMenu.java b/vane-regions/src/main/java/org/oddlama/vane/regions/menu/RegionGroupMenu.java new file mode 100644 index 000000000..452f5d949 --- /dev/null +++ b/vane-regions/src/main/java/org/oddlama/vane/regions/menu/RegionGroupMenu.java @@ -0,0 +1,224 @@ +package org.oddlama.vane.regions.menu; + +import static org.oddlama.vane.util.Util.namespaced_key; + +import java.util.stream.Collectors; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import org.oddlama.vane.annotation.lang.LangMessage; +import org.oddlama.vane.core.config.TranslatedItemStack; +import org.oddlama.vane.core.lang.TranslatedMessage; +import org.oddlama.vane.core.menu.Filter; +import org.oddlama.vane.core.menu.Menu.ClickResult; +import org.oddlama.vane.core.menu.Menu; +import org.oddlama.vane.core.menu.MenuFactory; +import org.oddlama.vane.core.menu.MenuItem; +import org.oddlama.vane.core.menu.MenuWidget; +import org.oddlama.vane.core.module.Context; +import org.oddlama.vane.core.module.ModuleComponent; +import org.oddlama.vane.regions.Regions; +import org.oddlama.vane.regions.region.EnvironmentSetting; +import org.oddlama.vane.regions.region.RegionGroup; +import org.oddlama.vane.regions.region.Role; + +public class RegionGroupMenu extends ModuleComponent { + @LangMessage public TranslatedMessage lang_title; + @LangMessage public TranslatedMessage lang_delete_confirm_title; + @LangMessage public TranslatedMessage lang_select_role_title; + @LangMessage public TranslatedMessage lang_filter_roles_title; + + public TranslatedItemStack item_rename; + public TranslatedItemStack item_delete; + public TranslatedItemStack item_delete_confirm_accept; + public TranslatedItemStack item_delete_confirm_cancel; + public TranslatedItemStack item_create_role; + public TranslatedItemStack item_list_roles; + public TranslatedItemStack item_select_role; + + public TranslatedItemStack item_setting_toggle_on; + public TranslatedItemStack item_setting_toggle_off; + public TranslatedItemStack item_setting_info_animals; + public TranslatedItemStack item_setting_info_monsters; + public TranslatedItemStack item_setting_info_explosions; + public TranslatedItemStack item_setting_info_fire; + public TranslatedItemStack item_setting_info_pvp; + public TranslatedItemStack item_setting_info_trample; + public TranslatedItemStack item_setting_info_vine_growth; + + public RegionGroupMenu(Context context) { + super(context.namespace("region_group")); + + final var ctx = get_context(); + item_rename = new TranslatedItemStack<>(ctx, "rename", Material.NAME_TAG, 1, "Used to rename the region group."); + item_delete = new TranslatedItemStack<>(ctx, "delete", namespaced_key("vane", "decoration_tnt_1"), 1, "Used to delete this region group."); + item_delete_confirm_accept = new TranslatedItemStack<>(ctx, "delete_confirm_accept", namespaced_key("vane", "decoration_tnt_1"), 1, "Used to confirm deleting the region group."); + item_delete_confirm_cancel = new TranslatedItemStack<>(ctx, "delete_confirm_cancel", Material.PRISMARINE_SHARD, 1, "Used to cancel deleting the region group."); + item_create_role = new TranslatedItemStack<>(ctx, "create_role", Material.WRITABLE_BOOK, 1, "Used to create a new role."); + item_list_roles = new TranslatedItemStack<>(ctx, "list_roles", Material.GLOBE_BANNER_PATTERN, 1, "Used to list all defined roles."); + item_select_role = new TranslatedItemStack<>(ctx, "select_role", Material.GLOBE_BANNER_PATTERN, 1, "Used to represent a role in the role selection list."); + + item_setting_toggle_on = new TranslatedItemStack<>(ctx, "setting_toggle_on", Material.GREEN_TERRACOTTA, 1, "Used to represent a toggle button with current state on."); + item_setting_toggle_off = new TranslatedItemStack<>(ctx, "setting_toggle_off", Material.RED_TERRACOTTA, 1, "Used to represent a toggle button with current state off."); + item_setting_info_animals = new TranslatedItemStack<>(ctx, "setting_info_animals", namespaced_key("vane", "animals_baby_pig_2"), 1, "Used to represent the info for the animals setting."); + item_setting_info_monsters = new TranslatedItemStack<>(ctx, "setting_info_monsters", Material.ZOMBIE_HEAD, 1, "Used to represent the info for the monsters setting."); + item_setting_info_explosions = new TranslatedItemStack<>(ctx, "setting_info_explosions", namespaced_key("vane", "monsters_creeper_with_tnt_2"), 1, "Used to represent the info for the explosions setting."); + item_setting_info_fire = new TranslatedItemStack<>(ctx, "setting_info_fire", Material.CAMPFIRE, 1, "Used to represent the info for the fire setting."); + item_setting_info_pvp = new TranslatedItemStack<>(ctx, "setting_info_pvp", Material.IRON_SWORD, 1, "Used to represent the info for the pvp setting."); + item_setting_info_trample = new TranslatedItemStack<>(ctx, "setting_info_trample", Material.FARMLAND, 1, "Used to represent the info for the trample setting."); + item_setting_info_vine_growth = new TranslatedItemStack<>(ctx, "setting_info_vine_growth", Material.VINE, 1, "Used to represent the info for the vine growth setting."); + } + + public Menu create(final RegionGroup group, final Player player) { + final var columns = 9; + final var rows = 3; + final var title = lang_title.str("§5§l" + group.name()); + final var region_group_menu = new Menu(get_context(), Bukkit.createInventory(null, rows * columns, title)); + region_group_menu.tag(new RegionGroupMenuTag(group.id())); + + final var is_owner = player.getUniqueId().equals(group.owner()); + if (is_owner) { + region_group_menu.add(menu_item_rename(group)); + // Delete only if this isn't the default group + if (!get_module().get_or_create_default_region_group(player).id().equals(group.id())) { + region_group_menu.add(menu_item_delete(group)); + } + } + + region_group_menu.add(menu_item_create_role(group)); + region_group_menu.add(menu_item_list_roles(group)); + + add_menu_item_setting(region_group_menu, group, 0, item_setting_info_animals, EnvironmentSetting.ANIMALS); + add_menu_item_setting(region_group_menu, group, 1, item_setting_info_monsters, EnvironmentSetting.MONSTERS); + add_menu_item_setting(region_group_menu, group, 3, item_setting_info_explosions, EnvironmentSetting.EXPLOSIONS); + add_menu_item_setting(region_group_menu, group, 4, item_setting_info_fire, EnvironmentSetting.FIRE); + add_menu_item_setting(region_group_menu, group, 5, item_setting_info_pvp, EnvironmentSetting.PVP); + add_menu_item_setting(region_group_menu, group, 7, item_setting_info_trample, EnvironmentSetting.TRAMPLE); + add_menu_item_setting(region_group_menu, group, 8, item_setting_info_vine_growth, EnvironmentSetting.VINE_GROWTH); + + region_group_menu.on_natural_close(player2 -> + get_module().menus.main_menu + .create(player2) + .open(player2)); + + return region_group_menu; + } + + private MenuWidget menu_item_rename(final RegionGroup group) { + return new MenuItem(0, item_rename.item(), (player, menu, self) -> { + menu.close(player); + + get_module().menus.enter_region_group_name_menu.create(player, group.name(), (player2, name) -> { + group.name(name); + mark_persistent_storage_dirty(); + + // Open new menu because of possibly changed title + get_module().menus.region_group_menu.create(group, player2).open(player2); + return ClickResult.SUCCESS; + }).on_natural_close(player2 -> { + // Open new menu because of possibly changed title + get_module().menus.region_group_menu.create(group, player2).open(player2); + }).open(player); + + return ClickResult.SUCCESS; + }); + } + + private MenuWidget menu_item_delete(final RegionGroup group) { + final var orphan_checkbox = group.is_orphan(get_module()) ? "§a✓" : "§c✕"; + return new MenuItem(1, item_delete.item(orphan_checkbox), (player, menu, self) -> { + if (!group.is_orphan(get_module())) { + return ClickResult.ERROR; + } + + menu.close(player); + MenuFactory.confirm(get_context(), lang_delete_confirm_title.str(), + item_delete_confirm_accept.item(), (player2) -> { + if (!player2.getUniqueId().equals(group.owner())) { + return ClickResult.ERROR; + } + + // Assert that this isn't the default group + if (get_module().get_or_create_default_region_group(player2).id().equals(group.id())) { + return ClickResult.ERROR; + } + + get_module().remove_region_group(group); + return ClickResult.SUCCESS; + }, item_delete_confirm_cancel.item(), (player2) -> { + menu.open(player2); + }) + .tag(new RegionGroupMenuTag(group.id())) + .open(player); + return ClickResult.SUCCESS; + }); + } + + private MenuWidget menu_item_create_role(final RegionGroup group) { + return new MenuItem(7, item_create_role.item(), (player, menu, self) -> { + menu.close(player); + get_module().menus.enter_role_name_menu.create(player, (player2, name) -> { + final var role = new Role(name, Role.RoleType.NORMAL); + group.add_role(role); + mark_persistent_storage_dirty(); + get_module().menus.role_menu.create(group, role, player).open(player); + return ClickResult.SUCCESS; + }).on_natural_close(player2 -> { + menu.open(player2); + }).open(player); + + return ClickResult.SUCCESS; + }); + } + + private MenuWidget menu_item_list_roles(final RegionGroup group) { + return new MenuItem(8, item_list_roles.item(), (player, menu, self) -> { + menu.close(player); + final var all_roles = group.roles() + .stream() + .sorted((a, b) -> a.name().compareToIgnoreCase(b.name())) + .collect(Collectors.toList()); + + final var filter = new Filter.StringFilter((r, str) -> r.name().toLowerCase().contains(str)); + MenuFactory.generic_selector(get_context(), player, lang_select_role_title.str(), lang_filter_roles_title.str(), all_roles, + r -> item_select_role.item(r.color() + "§l" + r.name()), + filter, + (player2, m, role) -> { + m.close(player2); + get_module().menus.role_menu.create(group, role, player2).open(player2); + return ClickResult.SUCCESS; + }, player2 -> { + menu.open(player2); + }).open(player); + return ClickResult.SUCCESS; + }); + } + + private void add_menu_item_setting(final Menu region_group_menu, final RegionGroup group, final int col, final TranslatedItemStack item_info, final EnvironmentSetting setting) { + region_group_menu.add(new MenuItem(1 * 9 + col, item_info.item(), (player, menu, self) -> { + return ClickResult.IGNORE; + })); + + region_group_menu.add(new MenuItem(2 * 9 + col, null, (player, menu, self) -> { + group.settings().put(setting, !group.get_setting(setting)); + mark_persistent_storage_dirty(); + menu.update(); + return ClickResult.SUCCESS; + }) { + @Override + public void item(final ItemStack item) { + if (group.get_setting(setting)) { + super.item(item_setting_toggle_on.item()); + } else { + super.item(item_setting_toggle_off.item()); + } + } + }); + } + + @Override public void on_enable() {} + @Override public void on_disable() {} +} diff --git a/vane-regions/src/main/java/org/oddlama/vane/regions/menu/RegionGroupMenuTag.java b/vane-regions/src/main/java/org/oddlama/vane/regions/menu/RegionGroupMenuTag.java new file mode 100644 index 000000000..48d369ee3 --- /dev/null +++ b/vane-regions/src/main/java/org/oddlama/vane/regions/menu/RegionGroupMenuTag.java @@ -0,0 +1,15 @@ +package org.oddlama.vane.regions.menu; + +import java.util.UUID; + +public class RegionGroupMenuTag { + private UUID region_group_id = null; + + public RegionGroupMenuTag(final UUID region_group_id) { + this.region_group_id = region_group_id; + } + + public UUID region_group_id() { + return region_group_id; + } +} diff --git a/vane-regions/src/main/java/org/oddlama/vane/regions/menu/RegionMenu.java b/vane-regions/src/main/java/org/oddlama/vane/regions/menu/RegionMenu.java new file mode 100644 index 000000000..18fb7a3dc --- /dev/null +++ b/vane-regions/src/main/java/org/oddlama/vane/regions/menu/RegionMenu.java @@ -0,0 +1,164 @@ +package org.oddlama.vane.regions.menu; + +import static org.oddlama.vane.util.PlayerUtil.give_items; +import static org.oddlama.vane.util.Util.namespaced_key; + +import java.util.stream.Collectors; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import org.oddlama.vane.annotation.lang.LangMessage; +import org.oddlama.vane.core.config.TranslatedItemStack; +import org.oddlama.vane.core.lang.TranslatedMessage; +import org.oddlama.vane.core.menu.Filter; +import org.oddlama.vane.core.menu.Menu.ClickResult; +import org.oddlama.vane.core.menu.Menu; +import org.oddlama.vane.core.menu.MenuFactory; +import org.oddlama.vane.core.menu.MenuItem; +import org.oddlama.vane.core.menu.MenuWidget; +import org.oddlama.vane.core.module.Context; +import org.oddlama.vane.core.module.ModuleComponent; +import org.oddlama.vane.regions.Regions; +import org.oddlama.vane.regions.region.Region; +import org.oddlama.vane.regions.region.RegionGroup; +import org.oddlama.vane.regions.region.RegionSelection; +import org.oddlama.vane.regions.region.RoleSetting; + +public class RegionMenu extends ModuleComponent { + @LangMessage public TranslatedMessage lang_title; + @LangMessage public TranslatedMessage lang_delete_confirm_title; + @LangMessage public TranslatedMessage lang_select_region_group_title; + @LangMessage public TranslatedMessage lang_filter_region_groups_title; + + public TranslatedItemStack item_rename; + public TranslatedItemStack item_delete; + public TranslatedItemStack item_delete_confirm_accept; + public TranslatedItemStack item_delete_confirm_cancel; + public TranslatedItemStack item_assign_region_group; + public TranslatedItemStack item_select_region_group; + + public RegionMenu(Context context) { + super(context.namespace("region")); + + final var ctx = get_context(); + item_rename = new TranslatedItemStack<>(ctx, "rename", Material.NAME_TAG, 1, "Used to rename the region."); + item_delete = new TranslatedItemStack<>(ctx, "delete", namespaced_key("vane", "decoration_tnt_1"), 1, "Used to delete this region."); + item_delete_confirm_accept = new TranslatedItemStack<>(ctx, "delete_confirm_accept", namespaced_key("vane", "decoration_tnt_1"), 1, "Used to confirm deleting the region."); + item_delete_confirm_cancel = new TranslatedItemStack<>(ctx, "delete_confirm_cancel", Material.PRISMARINE_SHARD, 1, "Used to cancel deleting the region."); + item_assign_region_group = new TranslatedItemStack<>(ctx, "assign_region_group", Material.GLOBE_BANNER_PATTERN, 1, "Used to assign a region group."); + item_select_region_group = new TranslatedItemStack<>(ctx, "select_region_group", Material.GLOBE_BANNER_PATTERN, 1, "Used to represent a region group in the region group assignment list."); + } + + public Menu create(final Region region, final Player player) { + final var columns = 9; + final var title = lang_title.str("§5§l" + region.name()); + final var region_menu = new Menu(get_context(), Bukkit.createInventory(null, columns, title)); + region_menu.tag(new RegionMenuTag(region.id())); + + final var is_owner = player.getUniqueId().equals(region.owner()); + if (is_owner) { + region_menu.add(menu_item_rename(region)); + region_menu.add(menu_item_delete(region)); + region_menu.add(menu_item_assign_region_group(region)); + } + + region_menu.on_natural_close(player2 -> + get_module().menus.main_menu + .create(player2) + .open(player2)); + + return region_menu; + } + + private MenuWidget menu_item_rename(final Region region) { + return new MenuItem(0, item_rename.item(), (player, menu, self) -> { + menu.close(player); + if (!player.getUniqueId().equals(region.owner())) { + return ClickResult.ERROR; + } + + get_module().menus.enter_region_name_menu.create(player, region.name(), (player2, name) -> { + region.name(name); + mark_persistent_storage_dirty(); + + // Update dynmap marker + get_module().dynmap_layer.update_marker(region); + + // Open new menu because of possibly changed title + get_module().menus.region_menu.create(region, player2).open(player2); + return ClickResult.SUCCESS; + }).on_natural_close(player2 -> { + // Open new menu because of possibly changed title + get_module().menus.region_menu.create(region, player2).open(player2); + }).open(player); + + return ClickResult.SUCCESS; + }); + } + + private MenuWidget menu_item_delete(final Region region) { + return new MenuItem(1, item_delete.item(), (player, menu, self) -> { + menu.close(player); + MenuFactory.confirm(get_context(), lang_delete_confirm_title.str(), + item_delete_confirm_accept.item(), (player2) -> { + if (!player2.getUniqueId().equals(region.owner())) { + return ClickResult.ERROR; + } + + get_module().remove_region(region); + + // Give back money + final var temp_sel = new RegionSelection(get_module()); + temp_sel.primary = region.extent().min(); + temp_sel.secondary = region.extent().max(); + + final var price = temp_sel.price(); + give_items(player2, new ItemStack(get_module().config_currency), price); + return ClickResult.SUCCESS; + }, item_delete_confirm_cancel.item(), (player2) -> { + menu.open(player2); + }) + .tag(new RegionMenuTag(region.id())) + .open(player); + return ClickResult.SUCCESS; + }); + } + + private MenuWidget menu_item_assign_region_group(final Region region) { + return new MenuItem(2, item_assign_region_group.item(), (player, menu, self) -> { + menu.close(player); + final var all_region_groups = get_module().all_region_groups() + .stream() + .filter(g -> player.getUniqueId().equals(g.owner()) + || g.get_role(player.getUniqueId()) + .get_setting(RoleSetting.ADMIN)) + .sorted((a, b) -> a.name().compareToIgnoreCase(b.name())) + .collect(Collectors.toList()); + + final var filter = new Filter.StringFilter((r, str) -> r.name().toLowerCase().contains(str)); + MenuFactory.generic_selector(get_context(), player, lang_select_region_group_title.str(), lang_filter_region_groups_title.str(), all_region_groups, + r -> item_select_region_group.item("§a§l" + r.name()), + filter, + (player2, m, group) -> { + if (!player2.getUniqueId().equals(region.owner())) { + return ClickResult.ERROR; + } + + m.close(player2); + region.region_group_id(group.id()); + mark_persistent_storage_dirty(); + menu.open(player2); + return ClickResult.SUCCESS; + }, player2 -> { + menu.open(player2); + }).open(player); + return ClickResult.SUCCESS; + }); + } + + @Override public void on_enable() {} + @Override public void on_disable() {} +} diff --git a/vane-regions/src/main/java/org/oddlama/vane/regions/menu/RegionMenuGroup.java b/vane-regions/src/main/java/org/oddlama/vane/regions/menu/RegionMenuGroup.java new file mode 100644 index 000000000..2b4bdb9f6 --- /dev/null +++ b/vane-regions/src/main/java/org/oddlama/vane/regions/menu/RegionMenuGroup.java @@ -0,0 +1,30 @@ +package org.oddlama.vane.regions.menu; + +import org.oddlama.vane.core.module.Context; +import org.oddlama.vane.core.module.ModuleComponent; +import org.oddlama.vane.regions.Regions; + +public class RegionMenuGroup extends ModuleComponent { + public EnterRegionNameMenu enter_region_name_menu; + public EnterRegionGroupNameMenu enter_region_group_name_menu; + public EnterRoleNameMenu enter_role_name_menu; + public MainMenu main_menu; + public RegionGroupMenu region_group_menu; + public RegionMenu region_menu; + public RoleMenu role_menu; + + public RegionMenuGroup(Context context) { + super(context.namespace("menus")); + + enter_region_name_menu = new EnterRegionNameMenu(get_context()); + enter_region_group_name_menu = new EnterRegionGroupNameMenu(get_context()); + enter_role_name_menu = new EnterRoleNameMenu(get_context()); + main_menu = new MainMenu(get_context()); + region_group_menu = new RegionGroupMenu(get_context()); + region_menu = new RegionMenu(get_context()); + role_menu = new RoleMenu(get_context()); + } + + @Override public void on_enable() {} + @Override public void on_disable() {} +} diff --git a/vane-regions/src/main/java/org/oddlama/vane/regions/menu/RegionMenuTag.java b/vane-regions/src/main/java/org/oddlama/vane/regions/menu/RegionMenuTag.java new file mode 100644 index 000000000..acd80c637 --- /dev/null +++ b/vane-regions/src/main/java/org/oddlama/vane/regions/menu/RegionMenuTag.java @@ -0,0 +1,15 @@ +package org.oddlama.vane.regions.menu; + +import java.util.UUID; + +public class RegionMenuTag { + private UUID region_id = null; + + public RegionMenuTag(final UUID region_id) { + this.region_id = region_id; + } + + public UUID region_id() { + return region_id; + } +} diff --git a/vane-regions/src/main/java/org/oddlama/vane/regions/menu/RoleMenu.java b/vane-regions/src/main/java/org/oddlama/vane/regions/menu/RoleMenu.java new file mode 100644 index 000000000..548f715a9 --- /dev/null +++ b/vane-regions/src/main/java/org/oddlama/vane/regions/menu/RoleMenu.java @@ -0,0 +1,230 @@ +package org.oddlama.vane.regions.menu; + +import static org.oddlama.vane.util.Util.namespaced_key; + +import java.util.Arrays; +import java.util.stream.Collectors; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import org.oddlama.vane.annotation.lang.LangMessage; +import org.oddlama.vane.core.config.TranslatedItemStack; +import org.oddlama.vane.core.lang.TranslatedMessage; +import org.oddlama.vane.core.menu.Filter; +import org.oddlama.vane.core.menu.Menu.ClickResult; +import org.oddlama.vane.core.menu.Menu; +import org.oddlama.vane.core.menu.MenuFactory; +import org.oddlama.vane.core.menu.MenuItem; +import org.oddlama.vane.core.menu.MenuWidget; +import org.oddlama.vane.core.module.Context; +import org.oddlama.vane.core.module.ModuleComponent; +import org.oddlama.vane.regions.Regions; +import org.oddlama.vane.regions.region.RegionGroup; +import org.oddlama.vane.regions.region.Role; +import org.oddlama.vane.regions.region.RoleSetting; +import org.oddlama.vane.util.ItemUtil; + +public class RoleMenu extends ModuleComponent { + @LangMessage public TranslatedMessage lang_title; + @LangMessage public TranslatedMessage lang_delete_confirm_title; + @LangMessage public TranslatedMessage lang_select_assign_player_title; + @LangMessage public TranslatedMessage lang_select_remove_player_title; + @LangMessage public TranslatedMessage lang_filter_players_title; + + public TranslatedItemStack item_rename; + public TranslatedItemStack item_delete; + public TranslatedItemStack item_delete_confirm_accept; + public TranslatedItemStack item_delete_confirm_cancel; + public TranslatedItemStack item_assign_player; + public TranslatedItemStack item_remove_player; + public TranslatedItemStack item_select_player; + + public TranslatedItemStack item_setting_toggle_on; + public TranslatedItemStack item_setting_toggle_off; + public TranslatedItemStack item_setting_info_admin; + public TranslatedItemStack item_setting_info_build; + public TranslatedItemStack item_setting_info_use; + public TranslatedItemStack item_setting_info_container; + public TranslatedItemStack item_setting_info_portal; + + public RoleMenu(Context context) { + super(context.namespace("role")); + + final var ctx = get_context(); + item_rename = new TranslatedItemStack<>(ctx, "rename", Material.NAME_TAG, 1, "Used to rename the role."); + item_delete = new TranslatedItemStack<>(ctx, "delete", namespaced_key("vane", "decoration_tnt_1"), 1, "Used to delete this role."); + item_delete_confirm_accept = new TranslatedItemStack<>(ctx, "delete_confirm_accept", namespaced_key("vane", "decoration_tnt_1"), 1, "Used to confirm deleting the role."); + item_delete_confirm_cancel = new TranslatedItemStack<>(ctx, "delete_confirm_cancel", Material.PRISMARINE_SHARD, 1, "Used to cancel deleting the role."); + item_assign_player = new TranslatedItemStack<>(ctx, "assign_player", Material.PLAYER_HEAD, 1, "Used to assign players to this role."); + item_remove_player = new TranslatedItemStack<>(ctx, "remove_player", Material.PLAYER_HEAD, 1, "Used to remove players from this role."); + item_select_player = new TranslatedItemStack<>(ctx, "select_player", Material.PLAYER_HEAD, 1, "Used to represent a player in the role assignment/removal list."); + + item_setting_toggle_on = new TranslatedItemStack<>(ctx, "setting_toggle_on", Material.GREEN_TERRACOTTA, 1, "Used to represent a toggle button with current state on."); + item_setting_toggle_off = new TranslatedItemStack<>(ctx, "setting_toggle_off", Material.RED_TERRACOTTA, 1, "Used to represent a toggle button with current state off."); + item_setting_info_admin = new TranslatedItemStack<>(ctx, "setting_info_admin", Material.GOLDEN_APPLE, 1, "Used to represent the info for the admin setting."); + item_setting_info_build = new TranslatedItemStack<>(ctx, "setting_info_build", Material.IRON_PICKAXE, 1, "Used to represent the info for the build setting."); + item_setting_info_use = new TranslatedItemStack<>(ctx, "setting_info_use", Material.DARK_OAK_DOOR, 1, "Used to represent the info for the use setting."); + item_setting_info_container = new TranslatedItemStack<>(ctx, "setting_info_container", Material.CHEST, 1, "Used to represent the info for the container setting."); + item_setting_info_portal = new TranslatedItemStack<>(ctx, "setting_info_portal", Material.ENDER_PEARL, 1, "Used to represent the info for the portal setting."); + } + + public Menu create(final RegionGroup group, final Role role, final Player player) { + final var columns = 9; + final var rows = 3; + final var title = lang_title.str(role.color() + "§l" + role.name()); + final var role_menu = new Menu(get_context(), Bukkit.createInventory(null, rows * columns, title)); + + final var is_admin = player.getUniqueId().equals(group.owner()) + || group.get_role(player.getUniqueId()) + .get_setting(RoleSetting.ADMIN); + + if (is_admin && role.role_type() == Role.RoleType.NORMAL) { + role_menu.add(menu_item_rename(group, role)); + role_menu.add(menu_item_delete(group, role)); + } + + if (role.role_type() != Role.RoleType.OTHERS) { + role_menu.add(menu_item_assign_player(group, role)); + role_menu.add(menu_item_remove_player(group, role)); + } + + add_menu_item_setting(role_menu, role, 0, item_setting_info_admin, RoleSetting.ADMIN); + add_menu_item_setting(role_menu, role, 2, item_setting_info_build, RoleSetting.BUILD); + add_menu_item_setting(role_menu, role, 4, item_setting_info_use, RoleSetting.USE); + add_menu_item_setting(role_menu, role, 5, item_setting_info_container, RoleSetting.CONTAINER); + add_menu_item_setting(role_menu, role, 8, item_setting_info_portal, RoleSetting.PORTAL); + + role_menu.on_natural_close(player2 -> + get_module().menus.region_group_menu + .create(group, player2) + .open(player2)); + + return role_menu; + } + + private MenuWidget menu_item_rename(final RegionGroup group, final Role role) { + return new MenuItem(0, item_rename.item(), (player, menu, self) -> { + menu.close(player); + + get_module().menus.enter_role_name_menu.create(player, role.name(), (player2, name) -> { + role.name(name); + mark_persistent_storage_dirty(); + + // Open new menu because of possibly changed title + get_module().menus.role_menu.create(group, role, player2).open(player2); + return ClickResult.SUCCESS; + }).on_natural_close(player2 -> { + // Open new menu because of possibly changed title + get_module().menus.role_menu.create(group, role, player2).open(player2); + }).open(player); + + return ClickResult.SUCCESS; + }); + } + + private MenuWidget menu_item_delete(final RegionGroup group, final Role role) { + return new MenuItem(1, item_delete.item(), (player, menu, self) -> { + menu.close(player); + MenuFactory.confirm(get_context(), lang_delete_confirm_title.str(), + item_delete_confirm_accept.item(), (player2) -> { + group.remove_role(role.id()); + mark_persistent_storage_dirty(); + return ClickResult.SUCCESS; + }, item_delete_confirm_cancel.item(), (player2) -> { + menu.open(player2); + }) + .open(player); + return ClickResult.SUCCESS; + }); + } + + private MenuWidget menu_item_assign_player(final RegionGroup group, final Role role) { + return new MenuItem(7, item_assign_player.item(), (player, menu, self) -> { + menu.close(player); + final var all_players = Arrays.stream(get_module().getServer().getOfflinePlayers()) + .filter(p -> !role.id().equals(group.player_to_role().get(p.getUniqueId()))) + .sorted((a, b) -> { + int c = Boolean.compare(b.isOnline(), a.isOnline()); + if (c != 0) { return c; } + return a.getName().compareToIgnoreCase(b.getName()); + }) + .collect(Collectors.toList()); + + final var filter = new Filter.StringFilter((p, str) -> p.getName().toLowerCase().contains(str)); + MenuFactory.generic_selector(get_context(), player, lang_select_assign_player_title.str(), lang_filter_players_title.str(), all_players, + p -> item_select_player.alternative(ItemUtil.skull_for_player(p), "§a§l" + p.getName()), + filter, + (player2, m, p) -> { + all_players.remove(p); + m.update(); + group.player_to_role().put(p.getUniqueId(), role.id()); + return ClickResult.SUCCESS; + }, player2 -> { + menu.open(player2); + }).open(player); + return ClickResult.SUCCESS; + }); + } + + private MenuWidget menu_item_remove_player(final RegionGroup group, final Role role) { + return new MenuItem(8, item_remove_player.item(), (player, menu, self) -> { + menu.close(player); + final var all_players = Arrays.stream(get_module().getServer().getOfflinePlayers()) + .filter(p -> role.id().equals(group.player_to_role().get(p.getUniqueId()))) + .sorted((a, b) -> { + int c = Boolean.compare(b.isOnline(), a.isOnline()); + if (c != 0) { return c; } + return a.getName().compareToIgnoreCase(b.getName()); + }) + .collect(Collectors.toList()); + + final var filter = new Filter.StringFilter((p, str) -> p.getName().toLowerCase().contains(str)); + MenuFactory.generic_selector(get_context(), player, lang_select_remove_player_title.str(), lang_filter_players_title.str(), all_players, + p -> item_select_player.alternative(ItemUtil.skull_for_player(p), "§a§l" + p.getName()), + filter, + (player2, m, p) -> { + all_players.remove(p); + m.update(); + group.player_to_role().remove(p.getUniqueId()); + return ClickResult.SUCCESS; + }, player2 -> { + menu.open(player2); + }).open(player); + return ClickResult.SUCCESS; + }); + } + + private void add_menu_item_setting(final Menu role_menu, final Role role, final int col, final TranslatedItemStack item_info, final RoleSetting setting) { + role_menu.add(new MenuItem(1 * 9 + col, item_info.item(), (player, menu, self) -> { + return ClickResult.IGNORE; + })); + + role_menu.add(new MenuItem(2 * 9 + col, null, (player, menu, self) -> { + if (setting == RoleSetting.ADMIN) { + // Admin setting is immutable + return ClickResult.ERROR; + } + + role.settings().put(setting, !role.get_setting(setting)); + mark_persistent_storage_dirty(); + menu.update(); + return ClickResult.SUCCESS; + }) { + @Override + public void item(final ItemStack item) { + if (role.get_setting(setting)) { + super.item(item_setting_toggle_on.item()); + } else { + super.item(item_setting_toggle_off.item()); + } + } + }); + } + + @Override public void on_enable() {} + @Override public void on_disable() {} +} diff --git a/vane-regions/src/main/java/org/oddlama/vane/regions/region/EnvironmentSetting.java b/vane-regions/src/main/java/org/oddlama/vane/regions/region/EnvironmentSetting.java new file mode 100644 index 000000000..471e3f57b --- /dev/null +++ b/vane-regions/src/main/java/org/oddlama/vane/regions/region/EnvironmentSetting.java @@ -0,0 +1,26 @@ +package org.oddlama.vane.regions.region; + +public enum EnvironmentSetting { + // Spawning + ANIMALS(true), + MONSTERS(false), + + // Hazards + EXPLOSIONS(false), + FIRE(false), + PVP(true), + + // Environment + TRAMPLE(false), + VINE_GROWTH(false), + ; + + private boolean def; + private EnvironmentSetting(final boolean def) { + this.def = def; + } + + public boolean default_value() { + return def; + } +} diff --git a/vane-regions/src/main/java/org/oddlama/vane/regions/region/Region.java b/vane-regions/src/main/java/org/oddlama/vane/regions/region/Region.java new file mode 100644 index 000000000..05de2996f --- /dev/null +++ b/vane-regions/src/main/java/org/oddlama/vane/regions/region/Region.java @@ -0,0 +1,68 @@ +package org.oddlama.vane.regions.region; + +import static org.oddlama.vane.core.persistent.PersistentSerializer.from_json; +import static org.oddlama.vane.core.persistent.PersistentSerializer.to_json; + +import java.io.IOException; +import java.util.UUID; + +import org.jetbrains.annotations.NotNull; + +import org.oddlama.vane.external.json.JSONObject; +import org.oddlama.vane.regions.Regions; + +public class Region { + public static Object serialize(@NotNull final Object o) throws IOException { + final var region = (Region)o; + final var json = new JSONObject(); + json.put("id", to_json(UUID.class, region.id)); + json.put("name", to_json(String.class, region.name)); + json.put("owner", to_json(UUID.class, region.owner)); + json.put("region_group", to_json(UUID.class, region.region_group)); + json.put("extent", to_json(RegionExtent.class, region.extent)); + return json; + } + + @SuppressWarnings("unchecked") + public static Region deserialize(@NotNull final Object o) throws IOException { + final var json = (JSONObject)o; + final var region = new Region(); + region.id = from_json(UUID.class, json.get("id")); + region.name = from_json(String.class, json.get("name")); + region.owner = from_json(UUID.class, json.get("owner")); + region.region_group = from_json(UUID.class, json.get("region_group")); + region.extent = from_json(RegionExtent.class, json.get("extent")); + return region; + } + + private Region() {} + public Region(final String name, final UUID owner, final RegionExtent extent, final UUID region_group) { + this.id = UUID.randomUUID(); + this.name = name; + this.owner = owner; + this.extent = extent; + this.region_group = region_group; + } + + private UUID id; + private String name; + private UUID owner; + private RegionExtent extent; + private UUID region_group; + + public UUID id() { return id; } + public String name() { return name; } + public void name(final String name) { this.name = name; } + public UUID owner() { return owner; } + public RegionExtent extent() { return extent; } + + private RegionGroup cached_region_group = null; + public UUID region_group_id() { return region_group; } + public void region_group_id(final UUID region_group) { this.region_group = region_group; } + public RegionGroup region_group(final Regions regions) { + if (cached_region_group == null) { + cached_region_group = regions.get_region_group(region_group); + } + return cached_region_group; + } +} diff --git a/vane-regions/src/main/java/org/oddlama/vane/regions/region/RegionExtent.java b/vane-regions/src/main/java/org/oddlama/vane/regions/region/RegionExtent.java new file mode 100644 index 000000000..69cc6c51c --- /dev/null +++ b/vane-regions/src/main/java/org/oddlama/vane/regions/region/RegionExtent.java @@ -0,0 +1,154 @@ +package org.oddlama.vane.regions.region; + +import static org.oddlama.vane.core.persistent.PersistentSerializer.from_json; +import static org.oddlama.vane.core.persistent.PersistentSerializer.to_json; + +import java.io.IOException; + +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.block.Block; + +import org.jetbrains.annotations.NotNull; + +import org.oddlama.vane.external.json.JSONObject; +import org.oddlama.vane.util.LazyBlock; + +public class RegionExtent { + public static Object serialize(@NotNull final Object o) throws IOException { + final var region_extent = (RegionExtent)o; + final var json = new JSONObject(); + json.put("min", to_json(LazyBlock.class, region_extent.min)); + json.put("max", to_json(LazyBlock.class, region_extent.max)); + return json; + } + + public static RegionExtent deserialize(@NotNull final Object o) throws IOException { + final var json = (JSONObject)o; + final var min = from_json(LazyBlock.class, json.get("min")); + final var max = from_json(LazyBlock.class, json.get("max")); + return new RegionExtent(min, max); + } + + // Both inclusive, so we don't run into errors with + // blocks outside of the world (ymax_height). + // Also, coordinates are sorted, so min is always the smaller coordinate on each axis. + // For each x,y,z: min.[x,y,z] <= max.[x,y,z] + private LazyBlock min; // inclusive + private LazyBlock max; // inclusive + + public RegionExtent(final LazyBlock min, final LazyBlock max) { + this.min = min; + this.max = max; + } + + public RegionExtent(final Block from, final Block to) { + if (!from.getWorld().equals(to.getWorld())) { + throw new RuntimeException("Invalid region extent across dimensions!"); + } + + // Sort coordinates along axes. + this.min = new LazyBlock(from.getWorld().getBlockAt( + Math.min(from.getX(), to.getX()), + Math.min(from.getY(), to.getY()), + Math.min(from.getZ(), to.getZ()))); + this.max = new LazyBlock(from.getWorld().getBlockAt( + Math.max(from.getX(), to.getX()), + Math.max(from.getY(), to.getY()), + Math.max(from.getZ(), to.getZ()))); + } + + public Block min() { return min.block(); } + public Block max() { return max.block(); } + + public boolean is_inside(final Location loc) { + if (!loc.getWorld().equals(min().getWorld())) { + return false; + } + + final var l = min(); + final var h = max(); + return loc.getX() >= l.getX() && loc.getX() < (h.getX() + 1) + && loc.getY() >= l.getY() && loc.getY() < (h.getY() + 1) + && loc.getZ() >= l.getZ() && loc.getZ() < (h.getZ() + 1); + } + + public boolean is_inside(final Block block) { + if (!block.getWorld().equals(min().getWorld())) { + return false; + } + + final var l = min(); + final var h = max(); + return block.getX() >= l.getX() && block.getX() <= h.getX() + && block.getY() >= l.getY() && block.getY() <= h.getY() + && block.getZ() >= l.getZ() && block.getZ() <= h.getZ(); + } + + public boolean intersects_extent(final RegionExtent other) { + if (!min().getWorld().equals(other.min().getWorld())) { + return false; + } + + final var l1 = min(); + final var h1 = max(); + final var l2 = other.min(); + final var h2 = other.max(); + + // Compute global min and max for each axis + final var llx = Math.min(l1.getX(), l2.getX()); + final var lly = Math.min(l1.getY(), l2.getY()); + final var llz = Math.min(l1.getZ(), l2.getZ()); + final var hhx = Math.max(h1.getX(), h2.getX()); + final var hhy = Math.max(h1.getY(), h2.getY()); + final var hhz = Math.max(h1.getZ(), h2.getZ()); + + // Compute global extent length + final var extent_global_x = (hhx - llx) + 1; + final var extent_global_y = (hhy - lly) + 1; + final var extent_global_z = (hhz - llz) + 1; + + // Compute sum of local extent lengths + final var extent_sum_x = (h2.getX() - l2.getX()) + (h1.getX() - l1.getX()) + 2; + final var extent_sum_y = (h2.getY() - l2.getY()) + (h1.getY() - l1.getY()) + 2; + final var extent_sum_z = (h2.getZ() - l2.getZ()) + (h1.getZ() - l1.getZ()) + 2; + + // It intersects exactly when: + // for all a in axis: global_extent(a) < individual_extent_sum(a) + return extent_global_x < extent_sum_x + && extent_global_y < extent_sum_y + && extent_global_z < extent_sum_z; + } + + public boolean intersects_chunk(final Chunk chunk) { + if (!chunk.getWorld().equals(min().getWorld())) { + return false; + } + + final var l1 = min(); + final var h1 = max(); + final var l2x = chunk.getX() * 16; + final var l2z = chunk.getZ() * 16; + final var h2x = (chunk.getX() + 1) * 16 - 1; + final var h2z = (chunk.getZ() + 1) * 16 - 1; + + // Compute global min and max for each axis + final var llx = Math.min(l1.getX(), l2x); + final var llz = Math.min(l1.getZ(), l2z); + final var hhx = Math.max(h1.getX(), h2x); + final var hhz = Math.max(h1.getZ(), h2z); + + // Compute global extent length + final var extent_global_x = (hhx - llx) + 1; + final var extent_global_z = (hhz - llz) + 1; + + // Compute sum of local extent lengths + final var extent_sum_x = (h2x - l2x) + (h1.getX() - l1.getX()) + 2; + final var extent_sum_z = (h2z - l2z) + (h1.getZ() - l1.getZ()) + 2; + + // It intersects exactly when: + // for all a in axis: global_extent(a) < individual_extent_sum(a) + return extent_global_x < extent_sum_x + && extent_global_z < extent_sum_z; + } +} diff --git a/vane-regions/src/main/java/org/oddlama/vane/regions/region/RegionGroup.java b/vane-regions/src/main/java/org/oddlama/vane/regions/region/RegionGroup.java new file mode 100644 index 000000000..37f318ded --- /dev/null +++ b/vane-regions/src/main/java/org/oddlama/vane/regions/region/RegionGroup.java @@ -0,0 +1,133 @@ +package org.oddlama.vane.regions.region; + +import static org.oddlama.vane.core.persistent.PersistentSerializer.from_json; +import static org.oddlama.vane.core.persistent.PersistentSerializer.to_json; + +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import org.jetbrains.annotations.NotNull; + +import org.oddlama.vane.external.json.JSONObject; +import org.oddlama.vane.regions.Regions; + +public class RegionGroup { + public static Object serialize(@NotNull final Object o) throws IOException { + final var region_group = (RegionGroup)o; + final var json = new JSONObject(); + json.put("id", to_json(UUID.class, region_group.id)); + json.put("name", to_json(String.class, region_group.name)); + json.put("owner", to_json(UUID.class, region_group.owner)); + try { + json.put("roles", to_json(RegionGroup.class.getDeclaredField("roles"), region_group.roles)); + } catch (NoSuchFieldException e) { throw new RuntimeException("Invalid field. This is a bug.", e); } + try { + json.put("player_to_role", to_json(RegionGroup.class.getDeclaredField("player_to_role"), region_group.player_to_role)); + } catch (NoSuchFieldException e) { throw new RuntimeException("Invalid field. This is a bug.", e); } + json.put("role_others", to_json(UUID.class, region_group.role_others)); + try { + json.put("settings", to_json(RegionGroup.class.getDeclaredField("settings"), region_group.settings)); + } catch (NoSuchFieldException e) { throw new RuntimeException("Invalid field. This is a bug.", e); } + + return json; + } + + @SuppressWarnings("unchecked") + public static RegionGroup deserialize(@NotNull final Object o) throws IOException { + final var json = (JSONObject)o; + final var region_group = new RegionGroup(); + region_group.id = from_json(UUID.class, json.get("id")); + region_group.name = from_json(String.class, json.get("name")); + region_group.owner = from_json(UUID.class, json.get("owner")); + try { + region_group.roles = (Map)from_json(RegionGroup.class.getDeclaredField("roles"), json.get("roles")); + } catch (NoSuchFieldException e) { throw new RuntimeException("Invalid field. This is a bug.", e); } + try { + region_group.player_to_role = (Map)from_json(RegionGroup.class.getDeclaredField("player_to_role"), json.get("player_to_role")); + } catch (NoSuchFieldException e) { throw new RuntimeException("Invalid field. This is a bug.", e); } + region_group.role_others = from_json(UUID.class, json.get("role_others")); + try { + region_group.settings = (Map)from_json(RegionGroup.class.getDeclaredField("settings"), json.get("settings")); + } catch (NoSuchFieldException e) { throw new RuntimeException("Invalid field. This is a bug.", e); } + return region_group; + } + + private UUID id; + private String name; + private UUID owner; + + private Map roles = new HashMap<>(); + private Map player_to_role = new HashMap<>(); + private UUID role_others; + + private Map settings = new HashMap<>(); + + private RegionGroup() { } + public RegionGroup(final String name, final UUID owner) { + this.id = UUID.randomUUID(); + this.name = name; + this.owner = owner; + + // Add admins role + final var admins = new Role("[Admins]", Role.RoleType.ADMINS); + this.add_role(admins); + + // Add others role + final var others = new Role("[Others]", Role.RoleType.OTHERS); + this.add_role(others); + this.role_others = others.id(); + + // Add friends role + final var friends = new Role("Friends", Role.RoleType.NORMAL); + friends.settings().put(RoleSetting.BUILD, true); + friends.settings().put(RoleSetting.USE, true); + friends.settings().put(RoleSetting.CONTAINER, true); + friends.settings().put(RoleSetting.PORTAL, true); + this.add_role(friends); + + // Add owner to admins + this.player_to_role.put(owner, admins.id()); + + // Set setting defaults + for (var es : EnvironmentSetting.values()) { + this.settings.put(es, es.default_value()); + } + } + + public UUID id() { return id; } + public String name() { return name; } + public void name(final String name) { this.name = name; } + public UUID owner() { return owner; } + public Map settings() { return settings; } + public boolean get_setting(final EnvironmentSetting setting) { + return settings.getOrDefault(setting, setting.default_value()); + } + + public void add_role(final Role role) { + this.roles.put(role.id(), role); + } + + public Map player_to_role() { return player_to_role; } + public Role get_role(final UUID player) { + return roles.get(player_to_role.getOrDefault(player, role_others)); + } + + public void remove_role(final UUID role_id) { + player_to_role.values().removeIf(r -> role_id.equals(r)); + roles.remove(role_id); + } + + public Collection roles() { + return roles.values(); + } + + public boolean is_orphan(final Regions regions) { + return !regions.all_regions() + .stream() + .anyMatch(r -> id.equals(r.region_group_id())); + } +} diff --git a/vane-regions/src/main/java/org/oddlama/vane/regions/region/RegionSelection.java b/vane-regions/src/main/java/org/oddlama/vane/regions/region/RegionSelection.java new file mode 100644 index 000000000..aba76ca5d --- /dev/null +++ b/vane-regions/src/main/java/org/oddlama/vane/regions/region/RegionSelection.java @@ -0,0 +1,95 @@ +package org.oddlama.vane.regions.region; + +import static org.oddlama.vane.util.PlayerUtil.has_items; + +import java.util.HashMap; + +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import org.oddlama.vane.regions.Regions; + +public class RegionSelection { + private Regions regions; + public Block primary = null; + public Block secondary = null; + + public RegionSelection(final Regions regions) { + this.regions = regions; + } + + public boolean intersects_existing() { + final var extent = extent(); + for (final var r : regions.all_regions()) { + if (!r.extent().min().getWorld().equals(primary.getWorld())) { + continue; + } + + if (extent.intersects_extent(r.extent())) { + return true; + } + } + + return false; + } + + public int price() { + final var dx = 1 + Math.abs(primary.getX() - secondary.getX()); + final var dy = 1 + Math.abs(primary.getY() - secondary.getY()); + final var dz = 1 + Math.abs(primary.getZ() - secondary.getZ()); + return (int)Math.ceil(Math.pow(regions.config_cost_y_multiplicator, dy / 16.0) * regions.config_cost_xz_base / 256.0 * dx * dz); + } + + public boolean can_afford(final Player player) { + final var price = price(); + if (price <= 0) { + return true; + } + final var map = new HashMap(); + map.put(new ItemStack(regions.config_currency), price); + return has_items(player, map); + } + + public boolean is_valid(final Player player) { + // Both blocks set + if (primary == null || secondary == null) { + return false; + } + + // Worlds match + if (!primary.getWorld().equals(secondary.getWorld())) { + return false; + } + + final var dx = 1 + Math.abs(primary.getX() - secondary.getX()); + final var dy = 1 + Math.abs(primary.getY() - secondary.getY()); + final var dz = 1 + Math.abs(primary.getZ() - secondary.getZ()); + + // min <= extent <= max + if (dx < regions.config_min_region_extent_x || + dy < regions.config_min_region_extent_y || + dz < regions.config_min_region_extent_z || + dx > regions.config_max_region_extent_x || + dy > regions.config_max_region_extent_y || + dz > regions.config_max_region_extent_z) { + return false; + } + + // Assert that it doesn't intersect an existing region + if (intersects_existing()) { + return false; + } + + // Check that the player can afford it + if (!can_afford(player)) { + return false; + } + + return true; + } + + public RegionExtent extent() { + return new RegionExtent(primary, secondary); + } +} diff --git a/vane-regions/src/main/java/org/oddlama/vane/regions/region/Role.java b/vane-regions/src/main/java/org/oddlama/vane/regions/region/Role.java new file mode 100644 index 000000000..63da10a8b --- /dev/null +++ b/vane-regions/src/main/java/org/oddlama/vane/regions/region/Role.java @@ -0,0 +1,80 @@ +package org.oddlama.vane.regions.region; + +import static org.oddlama.vane.core.persistent.PersistentSerializer.from_json; +import static org.oddlama.vane.core.persistent.PersistentSerializer.to_json; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import org.jetbrains.annotations.NotNull; + +import org.oddlama.vane.external.json.JSONObject; + +public class Role { + public enum RoleType { + ADMINS, + OTHERS, + NORMAL; + } + + public static Object serialize(@NotNull final Object o) throws IOException { + final var role = (Role)o; + final var json = new JSONObject(); + json.put("id", to_json(UUID.class, role.id)); + json.put("name", to_json(String.class, role.name)); + json.put("role_type", to_json(RoleType.class, role.role_type)); + try { + json.put("settings", to_json(Role.class.getDeclaredField("settings"), role.settings)); + } catch (NoSuchFieldException e) { throw new RuntimeException("Invalid field. This is a bug.", e); } + + return json; + } + + @SuppressWarnings("unchecked") + public static Role deserialize(@NotNull final Object o) throws IOException { + final var json = (JSONObject)o; + final var role = new Role(); + role.id = from_json(UUID.class, json.get("id")); + role.name = from_json(String.class, json.get("name")); + role.role_type = from_json(RoleType.class, json.get("role_type")); + try { + role.settings = (Map)from_json(Role.class.getDeclaredField("settings"), json.get("settings")); + } catch (NoSuchFieldException e) { throw new RuntimeException("Invalid field. This is a bug.", e); } + return role; + } + + private UUID id; + private String name; + private RoleType role_type; + private Map settings = new HashMap<>(); + + private Role() { } + public Role(final String name, final RoleType role_type) { + this.id = UUID.randomUUID(); + this.name = name; + this.role_type = role_type; + for (final var rs : RoleSetting.values()) { + this.settings.put(rs, rs.default_value(role_type == RoleType.ADMINS)); + } + } + + public UUID id() { return id; } + public String name() { return name; } + public void name(final String name) { this.name = name; } + public RoleType role_type() { return role_type; } + public Map settings() { return settings; } + public boolean get_setting(final RoleSetting setting) { + return settings.getOrDefault(setting, setting.default_value(false)); + } + + public String color() { + switch (role_type) { + case ADMINS: return "§c"; + case OTHERS: return "§a"; + default: + case NORMAL: return "§b"; + } + } +} diff --git a/vane-regions/src/main/java/org/oddlama/vane/regions/region/RoleSetting.java b/vane-regions/src/main/java/org/oddlama/vane/regions/region/RoleSetting.java new file mode 100644 index 000000000..6c1e383ac --- /dev/null +++ b/vane-regions/src/main/java/org/oddlama/vane/regions/region/RoleSetting.java @@ -0,0 +1,24 @@ +package org.oddlama.vane.regions.region; + +public enum RoleSetting { + ADMIN(false, true), + BUILD(false, true), + USE(true, true), + CONTAINER(false, true), + PORTAL(false, true), + ; + + private boolean def; + private boolean def_admin; + private RoleSetting(final boolean def, final boolean def_admin) { + this.def = def; + this.def_admin = def_admin; + } + + public boolean default_value(final boolean admin) { + if (admin) { + return def_admin; + } + return def; + } +} diff --git a/vane-regions/src/main/resources/lang-de.yml b/vane-regions/src/main/resources/lang-de.yml index 623252a6d..d5394e23c 100644 --- a/vane-regions/src/main/resources/lang-de.yml +++ b/vane-regions/src/main/resources/lang-de.yml @@ -12,8 +12,307 @@ # DO NOT CHANGE! The version of this language file. Used to determine # if the file needs to be updated. -version: 1 +version: 2 # The corresponding language code used in resource packs. Used for # resource pack generation. Typically this is a combination of the # language code (ISO 639) and the country code (ISO 3166). resource_pack_lang_code: 'de_de' + +start_region_selection: "§aChoose an area by selecting two blocks via §bleft-§a and §bright-click§a with an empty hand." + +command_region: + usage: "%1$s" + description: "Öffnet das Regions-Management-Menü." + help: "Ausführen um das Regins-Management-Menü zu öffnen." + +dynmap: + layer_label: "Regionen" + marker_label: "%1$s" + +menus: + enter_region_group_name: + title: "§8§lRegions-Gruppe benennen" + + enter_region_name: + title: "§8§lRegion benennen" + + enter_role_name: + title: "§8§lRolle benennen" + + main: + title: "§8§lRegions-Management" + + create_region_start_selection: + name: "§a§lRegion erstellen" + lore: + - "" + - "§7Beginnt eine neue Regionsauswahl. Mit leerer Hand," + - "§6links-clicke§7 um den ersten block auszuwählen and" + - "§6rechts-clicke§7 um den zweiten block auszuwählen." + - "§7Das Volumen dazwischen wird die Region." + + create_region_invalid_selection: + name: "§c§lUnzulässige Auswahl" + lore: + - "" + - "§6Deine Auswahl (%8$s§6 x %9$s§6 x %10$s§6) ist unzulässig!" + - "§6Folgende Anforderungen müssen erfüllt sein:" + - "§7- %1$s§7 Erster block gesetzt" + - "§7- %2$s§7 Zweiter block gesetzt" + - "§7- %3$s§7 Blöcke sind in der selben Welt" + - "§7- %4$s§7 Schneidet keine vorhandene Region" + - "§7- %5$s§7 Größer gleich (%11$s§7 x %12$s§7 x %13$s§7)" + - "§7- %6$s§7 Kleiner gleich (%14$s§7 x %15$s§7 x %16$s§7)" + - "§7- %7$s§7 Außreichend Währung im Inventar (%17$s§7)" + + create_region_valid_selection: + name: "§a§lRegion erstellen" + lore: + - "" + - "§7Deine Auswahl (%1$s§7 x %2$s§7 x %3$s§7) is zulässig." + - "§7Clicke hier um eine neue Region zu erstellen." + - "" + - "§7- §a✓§7 Erster block gesetzt" + - "§7- §a✓§7 Zweiter block gesetzt" + - "§7- §a✓§7 Blöcke sind in der selben Welt" + - "§7- §a✓§7 Schneidet keine vorhandene Region" + - "§7- §a✓§7 Größer gleich (%11$s§7 x %12$s§7 x %13$s§7)" + - "§7- §a✓§7 Kleiner gleich (%14$s§7 x %15$s§7 x %16$s§7)" + - "§7- §a✓§7 Außreichend Währung im Inventar (%17$s§7)" + + cancel_selection: + name: "§c§lAuswahl abbrechen" + lore: [] + + list_regions: + name: "§b§lRegion auswählen" + lore: [] + + select_region_title: "§8§lRegion auswählen" + filter_regions_title: "§8§lRegionen filtern" + select_region: + name: "%1$s" + lore: [] + + current_region: + name: "§b§lAktuelle Region" + lore: + - "" + - "§7Region: %1$s" + - "" + - "§7Wählt die Region aus, in der du dich gerade befindest." + + create_region_group: + name: "§a§lRegions-Gruppe erstellen" + lore: + - "" + - "§7Erstelle eine neue new §bRegions-Gruppe§7. Regions-Gruppen" + - "§7werden verwendet um Rechte an Spieler zu vergeben," + - "§7indem diesen Rollen zugewiesen werden. Alle Regionen in" + - "§7einer Regions-Gruppe teilen diese Einstellungen." + + list_region_groups: + name: "§b§lRegions-Gruppe auswählen" + lore: [] + + current_region_group: + name: "§b§lAktuelle Regions-Gruppe" + lore: + - "" + - "§7Regions-Gruppe: %1$s" + - "" + - "§7Wählt die Regions-Gruppe aus," + - "§7in der du dich gerade befindest." + + select_region_group_title: "§8§lRegions-Gruppe auswählen" + filter_region_groups_title: "§8§lRegions-Gruppen filtern" + select_region_group: + name: "%1$s" + lore: [] + + region: + title: "§8§lRegion: %1$s" + + rename: + name: "§b§lRegion Umbenennen" + lore: [] + + delete: + name: "§c§lRegion Entfernen" + lore: [] + delete_confirm_title: "§c§lRegion entfernen?" + delete_confirm_accept: + name: "§c§lREGION ENTFERNEN" + lore: [] + delete_confirm_cancel: + name: "§a§lAbbrechen" + lore: [] + + assign_region_group: + name: "§b§lGruppe zuweisen" + lore: [] + + select_region_group_title: "§8§lRegions-Gruppe auswählen" + filter_region_groups_title: "§8§lRegions-Gruppen filtern" + select_region_group: + name: "%1$s" + lore: [] + + region_group: + title: "§8§lGruppe: %1$s" + + setting_toggle_on: + name: "§a§lAN" + lore: [] + setting_toggle_off: + name: "§c§lAUS" + lore: [] + setting_info_animals: + name: "§b§lTiere" + lore: + - "" + - "§7Kontrolliert ob Tiere spawnen." + setting_info_monsters: + name: "§b§lMonster" + lore: + - "" + - "§7Kontrolliert ob Monster / feindliche Mobs spawnen." + setting_info_explosions: + name: "§b§lExplosionen" + lore: + - "" + - "§7Kontrolliert alle Arten von Explosionen." + setting_info_fire: + name: "§b§lFeuer" + lore: + - "" + - "§7Kontrolliert ob Feuer Blöcke verbrennt und sich verbreitet." + setting_info_pvp: + name: "§b§lPVP" + lore: + - "" + - "§7Kontrolliert ob PVP-Kämpfe möglich sind." + setting_info_trample: + name: "§b§lZertrampeln" + lore: + - "" + - "§7Kontrolliert ob Acker zertrampelt werden können." + setting_info_vine_growth: + name: "§b§lRanken" + lore: + - "" + - "§7Kontrolliert ob Ranken in dieser Region wachsen." + + rename: + name: "§b§lGruppe Umbenennen" + lore: [] + + delete: + name: "§c§lGruppe Entfernen" + lore: + - "" + - "§6Vorraussetzungen:" + - "§7- %1$s §7Keine Region verwendet diese Gruppe" + delete_confirm_title: "§c§lRegions-Gruppe entfernen?" + delete_confirm_accept: + name: "§c§lREGIONS-GRUPPE ENTFERNEN" + lore: [] + delete_confirm_cancel: + name: "§a§lAbbrechen" + lore: [] + + create_role: + name: "§a§lRolle Erstellen" + lore: + - "" + - "§7Erstellt eine neue §bRolle§7. Rollen werden" + - "§7verwendet, um mehreren Spielern Rechte zuzuweisen." + + list_roles: + name: "§b§lRollen" + lore: [] + + select_role_title: "§8§lRolle auswählen" + filter_roles_title: "§8§lRollen filtern" + select_role: + name: "%1$s" + lore: [] + + role: + title: "§8§lGruppe: %1$s" + + setting_toggle_on: + name: "§a§lAN" + lore: [] + setting_toggle_off: + name: "§c§lAUS" + lore: [] + setting_info_admin: + name: "§b§lAdmin" + lore: + - "" + - "§7Die Admin-berechtigung kann nicht geändert werden," + - "§7und ist nur für die Admin-gruppe aktiviert." + setting_info_build: + name: "§b§lBauen" + lore: + - "" + - "§7Kontrolliert, ob Blöcke gesetzt und abgebaut werden können." + setting_info_use: + name: "§b§lDinge verwenden" + lore: + - "" + - "§7Kontrolliert, ob Dinge wie Türen, Schalter," + - "§7Knöpfe, Redstone-Komponenten, ... verwendet werden können." + - "§7Kontrolliert außerdem §6Lese-Zugriff§7 auf Inventare." + setting_info_container: + name: "§b§lInventare" + lore: + - "" + - "§7Erlaubt modifizierende Interaktion mit Inventaren." + - "§7Ohne diese Einstellung kann nichts aus Inventaren" + - "§7genommen oder hineingelegt werden." + setting_info_portal: + name: "§b§lPortale Verwenden" + lore: + - "" + - "§7Kontrolliert, ob mit Portalen interagiert werden kann." + + rename: + name: "§b§lRolle Umbenennen" + lore: [] + + delete: + name: "§c§lRolle Entfernen" + lore: [] + delete_confirm_title: "§c§lRolle entfernen?" + delete_confirm_accept: + name: "§c§lROLLE ENTFERNEN" + lore: [] + delete_confirm_cancel: + name: "§a§lAbbrechen" + lore: [] + + assign_player: + name: "§b§lSpieler Zuweisen" + lore: + - "" + - "§7Weise Spieler dieser Rolle zu, indem du sie anklickst." + - "§7Wenn ein Spieler vorher einer andere Rolle zugewiesen war," + - "§7wird seine Zuweisung zu dieser Rolle geändert. Diese Liste zeigt" + - "§7nur Spieler, welche nicht bereits dieser Rolle zugewiesen sind. Du kannst" + - "§7Spieler wieder entfernen indem du das §bSpieler Entfernen§7 Menü verwendest." + remove_player: + name: "§b§lSpieler Entfernen" + lore: + - "" + - "§7Entferne Spieler von dieser Rolle, indem du sie anklickst." + - "§7Dies bedeutet, dass diesen danach keine Rolle zugewiesen ist" + - "§7und sie von der Auffang-Rolle §6[others]§7 verwaltet werden." + + select_assign_player_title: "§8§lSpieler zuweisen" + select_remove_player_title: "§8§lSpieler entfernen" + filter_players_title: "§8§lSpieler filtern" + select_player: + name: "%1$s" + lore: [] diff --git a/vane-regions/src/main/resources/lang-en.yml b/vane-regions/src/main/resources/lang-en.yml index 1ce538784..483f5ffcd 100644 --- a/vane-regions/src/main/resources/lang-en.yml +++ b/vane-regions/src/main/resources/lang-en.yml @@ -12,8 +12,400 @@ # DO NOT CHANGE! The version of this language file. Used to determine # if the file needs to be updated. -version: 1 +version: 2 # The corresponding language code used in resource packs. Used for # resource pack generation. Typically this is a combination of the # language code (ISO 639) and the country code (ISO 3166). resource_pack_lang_code: 'en_us' + +# This message is sent when the player needs to select an area for +# a new region. +start_region_selection: "§aChoose an area by selecting two blocks via §bleft-§a and §bright-click§a with an empty hand." + +command_region: + usage: "%1$s" + description: "Open the region management menu." + help: "Execute to open the region management menu." + +dynmap: + # The label for the dynmap layer + layer_label: "Regions" + # The label for the dynmap markers + # %1$s: Region name + marker_label: "%1$s" + +menus: + # Settings for the region group naming menu. + enter_region_group_name: + # The title for the naming menu. + title: "§8§lEnter Region Group Name" + + # Settings for the region naming menu. + enter_region_name: + # The title for the naming menu. + title: "§8§lEnter Region Name" + + # Settings for the role naming menu. + enter_role_name: + # The title for the naming menu. + title: "§8§lEnter Role Name" + + # Settings for the main menu. + main: + # The title for the main menu. + title: "§8§lManage Regions" + + # The item used to start a new region selection. + create_region_start_selection: + name: "§a§lCreate Region" + lore: + - "" + - "§7Starts a new region selection. With an empty hand," + - "§6left-click§7 to select the first block and" + - "§6right-click§7 to select the second block. The" + - "§7volume between the blocks will become the region." + + # This item is shown when the selection is invalid + # %1$s: Checkmark: primary block set + # %2$s: Checkmark: secondary block set + # %3$s: Checkmark: same world + # %4$s: Checkmark: doesn't interact existing region + # %5$s: Checkmark: minimum area condition met + # %6$s: Checkmark: not bigger than max + # %7$s: Checkmark: can afford + # %8$s: Selection extent X + # %9$s: Selection extent Y + # %10$s: Selection extent Z + # %11$s: min extent X + # %12$s: min extent Y + # %13$s: min extent Z + # %14$s: max extent X + # %15$s: max extent Y + # %16$s: max extent Z + # %17$s: price and currency + create_region_invalid_selection: + name: "§c§lInvalid Selection" + lore: + - "" + - "§6Your selection (%8$s§6 x %9$s§6 x %10$s§6) is invalid!" + - "§6It must meet the following requirements:" + - "§7- %1$s§7 Primary block set" + - "§7- %2$s§7 Secondary block set" + - "§7- %3$s§7 Blocks are in same world" + - "§7- %4$s§7 Doesn't intersect existing region" + - "§7- %5$s§7 Covers minimum area (%11$s§7 x %12$s§7 x %13$s§7)" + - "§7- %6$s§7 Not bigger than (%14$s§7 x %15$s§7 x %16$s§7)" + - "§7- %7$s§7 Can afford (%17$s§7)" + + # This item is shown when the selection is valid + # %1$s: Selection extent X + # %2$s: Selection extent Y + # %3$s: Selection extent Z + # %4$s: min extent X + # %5$s: min extent Y + # %6$s: min extent Z + # %7$s: max extent X + # %8$s: max extent Y + # %9$s: max extent Z + # %10$s: price and currency + create_region_valid_selection: + name: "§a§lCreate Region" + lore: + - "" + - "§7Your selection (%1$s§7 x %2$s§7 x %3$s§7) is valid." + - "§7Click here to create a new region." + - "" + - "§7- §a✓§7 Primary block set" + - "§7- §a✓§7 Secondary block set" + - "§7- §a✓§7 Blocks are in same world" + - "§7- §a✓§7 Doesn't intersect existing region" + - "§7- §a✓§7 Covers minimum area (%4$s§7 x %5$s§7 x %6$s§7)" + - "§7- §a✓§7 Not bigger than (%7$s§7 x %8$s§7 x %9$s§7)" + - "§7- §a✓§7 Can afford (%10$s§7)" + + # This item is used to cancel a pending selection. + cancel_selection: + name: "§c§lCancel Selection" + lore: [] + + # This item is used to select a region where the player is administrator. + list_regions: + name: "§b§lSelect Region" + lore: [] + + # The title for the region selection menu + select_region_title: "§8§lSelect Region" + # The title for the region selection menu filter + filter_regions_title: "§8§lFilter Regions" + # This item is used to represent a region in the selection menu. + # %1$s: Region name + select_region: + name: "%1$s" + lore: [] + + # This item is a shortcut to select the region the player is standing in. + # %1$s: Region name + current_region: + name: "§b§lCurrent Region" + lore: + - "" + - "§7Region: %1$s" + - "" + - "§7Select the region you are standing in." + + # This item is used to create a new region group. + create_region_group: + name: "§a§lCreate Region Group" + lore: + - "" + - "§7Create a new §bregion group§7. Region groups" + - "§7are used to set permissions for players" + - "§7by assigning them roles. All regions in" + - "§7a region group will share these permissions" + + # This item is used to select a region group. + list_region_groups: + name: "§b§lSelect Region Group" + lore: [] + + # This item is a shortcut to select the region group of the region the player is standing in. + # %1$s: Region group name + current_region_group: + name: "§b§lCurrent Region Group" + lore: + - "" + - "§7Region Group: %1$s" + - "" + - "§7Select the region group of the" + - "§7region you are standing in." + + select_region_group_title: "§8§lSelect Region Group" + filter_region_groups_title: "§8§lFilter Region Groups" + # This item is used to represent a region group in the selection menu. + # %1$s: Region group name + select_region_group: + name: "%1$s" + lore: [] + + # Settings for the region menu. + region: + # The title for the region menu. + # %1$s: Region name + title: "§8§lRegion: %1$s" + + # The item used to rename a region. + rename: + name: "§b§lRename Region" + lore: [] + + # The item used to delete a region. + delete: + name: "§c§lDelete Region" + lore: [] + # The title for the delete confirmation dialog. + delete_confirm_title: "§c§lDelete region?" + # The item to accept deleting. + delete_confirm_accept: + name: "§c§lDELETE REGION" + lore: [] + # The item to cancel deleting. + delete_confirm_cancel: + name: "§a§lCancel" + lore: [] + + # The item used to open the list of region groups to assign the region to one + assign_region_group: + name: "§b§lAssign Group" + lore: [] + + select_region_group_title: "§8§lSelect Region Group" + filter_region_groups_title: "§8§lFilter Region Groups" + # The item used to represent a region group. + # %1$s: Region group name + select_region_group: + name: "%1$s" + lore: [] + + # Settings for the region group menu. + region_group: + # The title for the region group menu. + # %1$s: Region group name + title: "§8§lGroup: %1$s" + + setting_toggle_on: + name: "§a§lENABLED" + lore: [] + setting_toggle_off: + name: "§c§lDISABLED" + lore: [] + setting_info_animals: + name: "§b§lAnimals" + lore: + - "" + - "§7Controls animal mob spawns." + setting_info_monsters: + name: "§b§lMonsters" + lore: + - "" + - "§7Controls monsters (hostile mob) spawns." + setting_info_explosions: + name: "§b§lExplosions" + lore: + - "" + - "§7Controls all types of explosions." + setting_info_fire: + name: "§b§lFire" + lore: + - "" + - "§7Controls whether fire burns blocks and spreads." + setting_info_pvp: + name: "§b§lPVP" + lore: + - "" + - "§7Controls whether PVP combat is allowed." + setting_info_trample: + name: "§b§lTrampling" + lore: + - "" + - "§7Controls whether farmland can be trampled." + setting_info_vine_growth: + name: "§b§lVine Growth" + lore: + - "" + - "§7Controls whether vines grow in the region." + + # The item used to rename a region group. + rename: + name: "§b§lRename Group" + lore: [] + + # The item used to delete a region group. + # %1$s: Checkmark: No region uses this group + delete: + name: "§c§lDelete Group" + lore: + - "" + - "§6Deletion requirements:" + - "§7- %1$s §7No region uses this group" + # The title for the delete confirmation dialog. + delete_confirm_title: "§c§lDelete region group?" + # The item to accept deleting. + delete_confirm_accept: + name: "§c§lDELETE REGION GROUP" + lore: [] + # The item to cancel deleting. + delete_confirm_cancel: + name: "§a§lCancel" + lore: [] + + # This item is used to create a new role. + create_role: + name: "§a§lCreate Role" + lore: + - "" + - "§7Create a new §brole§7. Roles are used" + - "§7to set permissions for groups of players." + + # This item is used to open the role menu for a role. + list_roles: + name: "§b§lRoles" + lore: [] + + select_role_title: "§8§lSelect Role" + filter_roles_title: "§8§lFilter Roles" + # This item is used to represent a role in the selection menu. + # %1$s: Role name + select_role: + name: "%1$s" + lore: [] + + # Settings for the role menu. + role: + # The title for the role menu. + # %1$s: Role name + title: "§8§lGroup: %1$s" + + setting_toggle_on: + name: "§a§lENABLED" + lore: [] + setting_toggle_off: + name: "§c§lDISABLED" + lore: [] + setting_info_admin: + name: "§b§lAdmin" + lore: + - "" + - "§7The admin flag cannot be toggled" + - "§7and is only set for the admin group." + setting_info_build: + name: "§b§lBuild" + lore: + - "" + - "§7Allows building and mining blocks." + setting_info_use: + name: "§b§lUse Things" + lore: + - "" + - "§7Allows using various things like doors," + - "§7levers, buttons or redstone components." + - "§7Also controls §6view-access§7 to inventories." + setting_info_container: + name: "§b§lContainers" + lore: + - "" + - "§7Allows modifying interactions with inventories." + - "§7Without this flag, nothing can be taken" + - "§7from or put into inventories." + setting_info_portal: + name: "§b§lUse Portals" + lore: + - "" + - "§7Allows interacting with portals." + + # The item used to rename a role. + rename: + name: "§b§lRename Role" + lore: [] + + # The item used to delete a role. + delete: + name: "§c§lDelete Role" + lore: [] + # The title for the delete confirmation dialog. + delete_confirm_title: "§c§lDelete role?" + # The item to accept deleting. + delete_confirm_accept: + name: "§c§lDELETE ROLE" + lore: [] + # The item to cancel deleting. + delete_confirm_cancel: + name: "§a§lCancel" + lore: [] + + # This item is used to open the player assignment menu for a role. + assign_player: + name: "§b§lAssign Players" + lore: + - "" + - "§7Assign players to this role by clicking on them." + - "§7If the player had a different role before, they will" + - "§7be reassigned to this role. The list will only show" + - "§7players that are not already assigned to this role." + - "§7You can remove players by using the §bRemove Player§7 menu." + # This item is used to open the player removing menu for a role. + remove_player: + name: "§b§lRemove Players" + lore: + - "" + - "§7Remove players from this role by clicking on them." + - "§7This means they will have no assigned role afterwards" + - "§7and will be handled by the catch-all role §6[others]" + + select_assign_player_title: "§8§lAssign players" + select_remove_player_title: "§8§lRemove players" + filter_players_title: "§8§lFilter players" + # This item is used to represent a player in the selection menu. + # %1$s: Player name + select_player: + name: "%1$s" + lore: [] diff --git a/vane-regions/src/main/resources/plugin.yml b/vane-regions/src/main/resources/plugin.yml index ec4374ef4..19828a653 100644 --- a/vane-regions/src/main/resources/plugin.yml +++ b/vane-regions/src/main/resources/plugin.yml @@ -5,7 +5,8 @@ description: Regions module for vane authors: [oddlama] website: 'https://github.com/oddlama/vane' -depend: [vane-core] +depend: [vane-core, vane-portals] +softdepend: [dynmap] main: org.oddlama.vane.regions.Regions database: false