From d49a5437aac9e5da98e0aad45261e2ab594e06c6 Mon Sep 17 00:00:00 2001 From: June Hobart Date: Tue, 14 Oct 2025 14:21:39 +0200 Subject: [PATCH 1/7] Added /fly command and adjusted sleep requirement. ### Changes - Added `/fly` command Players can now fly within their own region or any region where they have build permissions. - Adjusted bedtime percentage Reduced required sleep percentage from **50%** to **20%**. --- TODO | 42 --- build.gradle.kts | 8 + .../org/oddlama/vane/bedtime/Bedtime.java | 2 +- vane-core/build.gradle.kts | 3 +- vane-portals/build.gradle.kts | 7 +- vane-regions/build.gradle.kts | 5 +- .../vane/regions/RegionFlyManager.java | 281 ++++++++++++++++++ .../org/oddlama/vane/regions/Regions.java | 138 +++++++-- .../oddlama/vane/regions/commands/Fly.java | 81 +++++ .../oddlama/vane/regions/commands/Region.java | 27 ++ vane-regions/src/main/resources/lang-en.yml | 20 +- vane-trifles/build.gradle.kts | 8 +- 12 files changed, 540 insertions(+), 82 deletions(-) delete mode 100644 TODO create mode 100644 vane-regions/src/main/java/org/oddlama/vane/regions/RegionFlyManager.java create mode 100644 vane-regions/src/main/java/org/oddlama/vane/regions/commands/Fly.java diff --git a/TODO b/TODO deleted file mode 100644 index 03b002777..000000000 --- a/TODO +++ /dev/null @@ -1,42 +0,0 @@ -#### Important - -- Wrong world name in config should issue warning --> hazard protection doesn't do this on wither worlds - -#### IDEAS - -- [ ] portals need to be discovered before they can be used. (maybe overridable on a per-portal basis? for admins?) -- [ ] tab shows ordered by world -- [ ] angel shift scroll selects level or some percentage. -- [ ] autostop at certain times. 12am 12pm -- [ ] trampelpfad (a trail of path-block that is created automatically when players take a route often, could get fancy.) -- [ ] add a mechanic to update in-game maps (e.g. with a custom item, or redstone) -- [ ] player heads from /heads -- [ ] lodestone portal scrolls teleport on top of the stone -- [ ] world rebuild check for armor stands and item frames. don't let them be destroyed -- [ ] minecart speed ++++++ only if player in minecart, or maybe with an extra type of minecart. is it time for the netherite minecart? -- [ ] on first join message: e.g. for link to map and discord link - -- permission audit for all players with moderator level -- sleep day -> get night -- portal scrolls show total uses since creation -- ultra infinity for all arrow types (put on arrow?) - -portal domes? projector like setup for full-dome or maybe even a sphere. good that this sentence makes no sense. -limit range to prevent clutter and require multiple hops (either multiple hops in the menu o. - -- [ ] descr level bottle -- [ ] bottle fill max - -- [ ] better pack icon -- [ ] github readme banners -- [ ] crafting recipes in readme -- [ ] armor stand poser manipulation by perpendicular intersection between Line of sight and arm -- [ ] item frame invisibilinator (visible if empty) -- [ ] player list footer header configuration -- [ ] transmutation tool copy pasta with direction awareness -- [ ] sign editing (a tool ?) -- [ ] add items to shulker box by right click with item in inventory on box? -- [ ] Maybe "Advancements to guide you through most additions" -- can we inherit spectator skins? maybe hook into data loading? -- golden sickle -> golden mistletoe, mistel (viscum album) -- used as enchantment binding agent/ catalyst?. diff --git a/build.gradle.kts b/build.gradle.kts index 9ac93ef3b..479091f29 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,6 +10,8 @@ dependencies { java { toolchain.languageVersion.set(JavaLanguageVersion.of(21)) + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } // We don't need to generate an empty `vane.jar` @@ -36,9 +38,15 @@ subprojects { maven("https://repo.bluecolored.de/releases") } + java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + tasks.withType { options.compilerArgs.addAll(arrayOf("-Xlint:all", "-Xlint:-processing", "-Xdiags:verbose")) options.encoding = "UTF-8" + options.release.set(21) } dependencies { 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 02dfa21d9..04ba9ece0 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 @@ -29,7 +29,7 @@ public class Bedtime extends Module { // Configuration @ConfigDouble( - def = 0.5, + def = 0.2, min = 0.0, max = 1.0, desc = "The percentage of sleeping players required to advance time." diff --git a/vane-core/build.gradle.kts b/vane-core/build.gradle.kts index 26fab6e10..2f58c076c 100644 --- a/vane-core/build.gradle.kts +++ b/vane-core/build.gradle.kts @@ -45,13 +45,12 @@ tasks { include(dependency("org.bstats:bstats-bukkit")) include(dependency("org.reflections:reflections")) include(dependency("org.json:json")) - include(dependency(":vane-annotations")) + include(project(":vane-annotations")) include(dependency("org.apache.commons:commons-lang3")) include(dependency("org.apache.commons:commons-text")) } relocate("org.bstats", "org.oddlama.vane.external.bstats") relocate("org.reflections", "org.oddlama.vane.external.reflections") - relocate("org.json", "org.oddlama.vane.external.json") relocate("org.apache.commons.lang3", "org.oddlama.vane.external.apache.commons.lang3") relocate("org.apache.commons.text", "org.oddlama.vane.external.apache.commons.text") } diff --git a/vane-portals/build.gradle.kts b/vane-portals/build.gradle.kts index a686f4048..53690a3a5 100644 --- a/vane-portals/build.gradle.kts +++ b/vane-portals/build.gradle.kts @@ -7,8 +7,7 @@ dependencies { } tasks { - shadowJar { - configurations = listOf() - relocate("org.json", "org.oddlama.vane.external.json") - } + shadowJar { + // No special configuration needed + } } diff --git a/vane-regions/build.gradle.kts b/vane-regions/build.gradle.kts index c190acac2..bc342278c 100644 --- a/vane-regions/build.gradle.kts +++ b/vane-regions/build.gradle.kts @@ -10,7 +10,8 @@ dependencies { tasks { shadowJar { - configurations = listOf() - relocate("org.json", "org.oddlama.vane.external.json") + dependencies { + include(dependency(":vane-portals")) + } } } diff --git a/vane-regions/src/main/java/org/oddlama/vane/regions/RegionFlyManager.java b/vane-regions/src/main/java/org/oddlama/vane/regions/RegionFlyManager.java new file mode 100644 index 000000000..cc50311e2 --- /dev/null +++ b/vane-regions/src/main/java/org/oddlama/vane/regions/RegionFlyManager.java @@ -0,0 +1,281 @@ +package org.oddlama.vane.regions; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import org.bukkit.GameMode; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.player.PlayerMoveEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.player.PlayerGameModeChangeEvent; +import org.bukkit.event.player.PlayerToggleFlightEvent; +import org.oddlama.vane.core.Listener; +import org.oddlama.vane.core.module.Context; +import org.oddlama.vane.regions.region.Region; +import org.oddlama.vane.regions.region.RoleSetting; + +public class RegionFlyManager extends Listener { + + // Track which players are flying via the region fly feature + // Maps player UUID -> region they were in when they enabled flying + private Map flying_players = new HashMap<>(); + + // Track players who have auto-fly enabled (allowFlight but not actively flying) + private Map auto_fly_enabled = new HashMap<>(); + + // Track players who are protected from fall damage after leaving a region + private Map fall_damage_protected = new HashMap<>(); + private static final long FALL_DAMAGE_PROTECTION_MS = 15000; // 15 seconds + + // Track players who manually disabled flight via /fly (opt-out of auto-fly) + private java.util.HashSet manual_fly_opt_out = new java.util.HashSet<>(); + + public RegionFlyManager(Context context) { + super(context); + } + + public void add_flying_player(final UUID player_id, final Region region) { + flying_players.put(player_id, region.id()); + // Also add to auto-fly when manually enabling + auto_fly_enabled.put(player_id, region.id()); + // Clear manual opt-out when explicitly enabling + manual_fly_opt_out.remove(player_id); + } + + public void remove_flying_player(final UUID player_id) { + flying_players.remove(player_id); + auto_fly_enabled.remove(player_id); + } + + public void set_manual_opt_out(final UUID player_id, final boolean opt_out) { + if (opt_out) { + manual_fly_opt_out.add(player_id); + } else { + manual_fly_opt_out.remove(player_id); + } + } + + public boolean is_manual_opt_out(final UUID player_id) { + return manual_fly_opt_out.contains(player_id); + } + + public boolean is_region_flying(final UUID player_id) { + return flying_players.containsKey(player_id) || auto_fly_enabled.containsKey(player_id); + } + + public boolean is_actively_flying(final UUID player_id) { + return flying_players.containsKey(player_id); + } + + private boolean can_fly_in_region(final Player player, final Region region) { + if (region == null) { + return false; + } + + final var group = region.region_group(get_module()); + final var role = group.get_role(player.getUniqueId()); + + // Check if player has BUILD permission (which friends and admins have) + // or ADMIN permission + return role.get_setting(RoleSetting.ADMIN) || role.get_setting(RoleSetting.BUILD); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void on_player_move(final PlayerMoveEvent event) { + final var player = event.getPlayer(); + final var player_id = player.getUniqueId(); + + // Skip if player is in creative or spectator mode (they can fly anyway) + if (player.getGameMode() == GameMode.CREATIVE || player.getGameMode() == GameMode.SPECTATOR) { + return; + } + + // Check if they moved to a different block + if (event.getFrom().getBlockX() == event.getTo().getBlockX() && + event.getFrom().getBlockY() == event.getTo().getBlockY() && + event.getFrom().getBlockZ() == event.getTo().getBlockZ()) { + return; + } + + // Check current region + final var current_region = get_module().region_at(event.getTo()); + final var old_region_id = flying_players.get(player_id); + final var auto_fly_region_id = auto_fly_enabled.get(player_id); + + // Check if they can fly in the current region + final boolean can_fly = can_fly_in_region(player, current_region); + + if (is_region_flying(player_id)) { + // Player has fly enabled (either actively or auto) + if (!can_fly) { + // They left the region or don't have permission anymore + disable_flying(player); + } else if (current_region != null && + old_region_id != null && + !current_region.id().equals(old_region_id)) { + // They entered a different region where they have permissions (and are actively flying) + flying_players.put(player_id, current_region.id()); + auto_fly_enabled.put(player_id, current_region.id()); + // Update visualization to show new region boundaries + get_module().start_visualizing_region(player_id, current_region); + } else if (current_region != null && + auto_fly_region_id != null && + !current_region.id().equals(auto_fly_region_id)) { + // Update auto-fly region tracking + auto_fly_enabled.put(player_id, current_region.id()); + // Update visualization if they're actively flying + if (player.isFlying()) { + get_module().start_visualizing_region(player_id, current_region); + } + } + } else if (can_fly) { + // Player entered a region where they can fly - enable auto-fly + // only if they didn't manually opt-out via /fly + if (!is_manual_opt_out(player_id)) { + player.setAllowFlight(true); + auto_fly_enabled.put(player_id, current_region.id()); + } + } + } + + @EventHandler(priority = EventPriority.NORMAL) + public void on_player_toggle_flight(final PlayerToggleFlightEvent event) { + final var player = event.getPlayer(); + final var player_id = player.getUniqueId(); + + // Skip if player is in creative or spectator mode + if (player.getGameMode() == GameMode.CREATIVE || player.getGameMode() == GameMode.SPECTATOR) { + return; + } + + // Check if player has auto-fly enabled and is starting to fly + if (event.isFlying() && auto_fly_enabled.containsKey(player_id)) { + final var region_id = auto_fly_enabled.get(player_id); + final var region = get_module().all_regions().stream() + .filter(r -> r.id().equals(region_id)) + .findFirst() + .orElse(null); + + if (region != null) { + // Mark as actively flying + flying_players.put(player_id, region_id); + // Start visualizing the region + get_module().start_visualizing_region(player_id, region); + } + } + } + + private void disable_flying(final Player player) { + final var player_id = player.getUniqueId(); + + // Don't disable flight for creative/spectator players + if (player.getGameMode() == GameMode.CREATIVE || player.getGameMode() == GameMode.SPECTATOR) { + remove_flying_player(player_id); + return; + } + + // Check if player is high in the air + final boolean high_in_air = is_player_high_in_air(player); + + // Disable flying + player.setAllowFlight(false); + player.setFlying(false); + + // Stop visualizing the region + get_module().stop_visualizing_region(player_id); + + // If they're high in the air, protect them from fall damage + if (high_in_air) { + // Stop all downward velocity immediately to prevent momentum damage + final var velocity = player.getVelocity(); + if (velocity.getY() < 0) { + player.setVelocity(velocity.setY(0)); + } + + // Reset fall distance to prevent accumulated fall damage + player.setFallDistance(0); + + // Enable fall damage protection for 15 seconds + fall_damage_protected.put(player_id, System.currentTimeMillis()); + } + + // No messages - silent auto-disable + remove_flying_player(player_id); + } + + private boolean is_player_high_in_air(final Player player) { + final var location = player.getLocation(); + final var world = location.getWorld(); + + // Check blocks below the player + for (int y = location.getBlockY(); y >= Math.max(world.getMinHeight(), location.getBlockY() - 10); y--) { + final var block = world.getBlockAt(location.getBlockX(), y, location.getBlockZ()); + if (block.getType().isSolid()) { + // Found solid ground within 10 blocks, not high in air + return false; + } + } + + // No solid ground within 10 blocks below + return true; + } + + @EventHandler(priority = EventPriority.LOWEST) + public void on_fall_damage(final EntityDamageEvent event) { + // Only handle fall damage for players + if (!(event.getEntity() instanceof Player)) { + return; + } + + // Only handle fall damage + if (event.getCause() != EntityDamageEvent.DamageCause.FALL) { + return; + } + + final var player = (Player) event.getEntity(); + final var player_id = player.getUniqueId(); + + // Check if player is protected from fall damage + final var protection_time = fall_damage_protected.get(player_id); + if (protection_time != null) { + final long elapsed = System.currentTimeMillis() - protection_time; + + if (elapsed < FALL_DAMAGE_PROTECTION_MS) { + // Cancel fall damage + event.setCancelled(true); + // Remove protection after first use + fall_damage_protected.remove(player_id); + } else { + // Protection expired + fall_damage_protected.remove(player_id); + } + } + } + + @EventHandler(priority = EventPriority.MONITOR) + public void on_player_quit(final PlayerQuitEvent event) { + final var player_id = event.getPlayer().getUniqueId(); + // Clean up when player quits + remove_flying_player(player_id); + fall_damage_protected.remove(player_id); + // Also stop visualizing + get_module().stop_visualizing_region(player_id); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void on_gamemode_change(final PlayerGameModeChangeEvent event) { + final var player = event.getPlayer(); + final var new_mode = event.getNewGameMode(); + + // If switching to creative or spectator, remove from tracking + // (they can fly anyway) + if (new_mode == GameMode.CREATIVE || new_mode == GameMode.SPECTATOR) { + remove_flying_player(player.getUniqueId()); + } + } +} + + 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 286494b82..f7d68c2a7 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 @@ -64,7 +64,7 @@ import org.oddlama.vane.regions.region.RoleSetting; import org.oddlama.vane.util.StorageUtil; -@VaneModule(name = "regions", bstats = 8643, config_version = 4, lang_version = 3, storage_version = 1) +@VaneModule(name = "regions", bstats = 8643, config_version = 4, lang_version = 4, storage_version = 1) public class Regions extends Module { // // ┌───────────────────────┐ @@ -179,16 +179,38 @@ public class Regions extends Module { // 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<>(); + + // A map containing regions being visualized by players + private Map region_visualizations = new HashMap<>(); @LangMessage public TranslatedMessage lang_start_region_selection; + @LangMessage + public TranslatedMessage lang_fly_enabled; + + @LangMessage + public TranslatedMessage lang_fly_disabled; + + @LangMessage + public TranslatedMessage lang_fly_not_in_region; + + @LangMessage + public TranslatedMessage lang_fly_no_permission; + + @LangMessage + public TranslatedMessage lang_fly_disabled_safe_landing; + + @LangMessage + public TranslatedMessage lang_fly_disabled_region_exit; + // This permission allows players (usually admins) to always administrate // any region (rename, delete), regardless of whether other restrictions // would block access. public final Permission admin_permission; public RegionMenuGroup menus; + public RegionFlyManager fly_manager; public RegionDynmapLayer dynmap_layer; public RegionBlueMapLayer blue_map_layer; @@ -205,11 +227,13 @@ public Regions() { environment_overrides = new RegionGlobalEnvironmentOverrides(this); new org.oddlama.vane.regions.commands.Region(this); + new org.oddlama.vane.regions.commands.Fly(this); new RegionEnvironmentSettingEnforcer(this); new RegionRoleSettingEnforcer(this); new RegionSelectionListener(this); + fly_manager = new RegionFlyManager(this); dynmap_layer = new RegionDynmapLayer(this); blue_map_layer = new RegionBlueMapLayer(this); @@ -287,6 +311,18 @@ public boolean is_selecting_region(final Player player) { public RegionSelection get_region_selection(final Player player) { return region_selections.get(player.getUniqueId()); } + + public void start_visualizing_region(final UUID player_id, final Region region) { + region_visualizations.put(player_id, region); + } + + public void stop_visualizing_region(final UUID player_id) { + region_visualizations.remove(player_id); + } + + public boolean is_visualizing_region(final UUID player_id) { + return region_visualizations.containsKey(player_id); + } private static final int visualize_max_particles = 20000; private static final int visualize_particles_per_block = 12; @@ -294,7 +330,7 @@ public RegionSelection get_region_selection(final Player player) { 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 BlockPos c1, final BlockPos c2, final boolean valid) { + private void visualize_edge(final World world, final BlockPos c1, final BlockPos c2, final boolean valid, final Player viewer) { // Unfortunately, particle spawns are normally distributed. // To still have a good visualization, we need to calculate a stddev that looks // good. @@ -313,8 +349,8 @@ private void visualize_edge(final World world, final BlockPos c1, final BlockPos dy *= visualize_stddev_compensation; dz *= visualize_stddev_compensation; - // Spawn base particles - world.spawnParticle( + // Spawn base particles (local to viewer) + viewer.spawnParticle( Particle.END_ROD, mx, my, @@ -323,13 +359,11 @@ private void visualize_edge(final World world, final BlockPos c1, final BlockPos dx, dy, dz, - 0.0, // speed - null, // data - true - ); // force + 0.0 // speed + ); - // Spawn colored particles indicating validity - world.spawnParticle( + // Spawn colored particles indicating validity (local to viewer) + viewer.spawnParticle( Particle.DUST, mx, my, @@ -339,12 +373,12 @@ private void visualize_edge(final World world, final BlockPos c1, final BlockPos dy, dz, 0.0, // speed - valid ? visualize_dust_valid : visualize_dust_invalid, // data - true - ); // force + valid ? visualize_dust_valid : visualize_dust_invalid // data + ); } private void visualize_selections() { + // Visualize region selections for (final var selection_owner : region_selections.keySet()) { final var selection = region_selections.get(selection_owner); if (selection == null) { @@ -390,19 +424,68 @@ private void visualize_selections() { final var G = new BlockPos(hx, hy, hz); final var H = new BlockPos(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); + // Visualize each edge (local to player) + visualize_edge(world, A, B, valid, player); + visualize_edge(world, B, C, valid, player); + visualize_edge(world, C, D, valid, player); + visualize_edge(world, D, A, valid, player); + visualize_edge(world, E, F, valid, player); + visualize_edge(world, F, G, valid, player); + visualize_edge(world, G, H, valid, player); + visualize_edge(world, H, E, valid, player); + visualize_edge(world, A, E, valid, player); + visualize_edge(world, B, F, valid, player); + visualize_edge(world, C, G, valid, player); + visualize_edge(world, D, H, valid, player); + } + + // Visualize existing regions + for (final var visualize_owner : region_visualizations.keySet()) { + final var region = region_visualizations.get(visualize_owner); + if (region == null) { + continue; + } + + // Get player for visualization + final var offline_player = getServer().getOfflinePlayer(visualize_owner); + if (!offline_player.isOnline()) { + continue; + } + final var player = offline_player.getPlayer(); + + // Get the region extent + final var extent = region.extent(); + final var world = getServer().getWorld(extent.world()); + if (world == null) { + continue; + } + + final var min = extent.min(); + final var max = extent.max(); + + // Corners + final var A = new BlockPos(min.getX(), min.getY(), min.getZ()); + final var B = new BlockPos(max.getX(), min.getY(), min.getZ()); + final var C = new BlockPos(max.getX(), max.getY(), min.getZ()); + final var D = new BlockPos(min.getX(), max.getY(), min.getZ()); + final var E = new BlockPos(min.getX(), min.getY(), max.getZ()); + final var F = new BlockPos(max.getX(), min.getY(), max.getZ()); + final var G = new BlockPos(max.getX(), max.getY(), max.getZ()); + final var H = new BlockPos(min.getX(), max.getY(), max.getZ()); + + // Visualize each edge - always valid (green) and local to player + visualize_edge(world, A, B, true, player); + visualize_edge(world, B, C, true, player); + visualize_edge(world, C, D, true, player); + visualize_edge(world, D, A, true, player); + visualize_edge(world, E, F, true, player); + visualize_edge(world, F, G, true, player); + visualize_edge(world, G, H, true, player); + visualize_edge(world, H, E, true, player); + visualize_edge(world, A, E, true, player); + visualize_edge(world, B, F, true, player); + visualize_edge(world, C, G, true, player); + visualize_edge(world, D, H, true, player); } } @@ -675,8 +758,11 @@ public RegionGroup get_or_create_default_region_group(final Player owner) { @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void on_player_quit(final PlayerQuitEvent event) { + final var player_id = event.getPlayer().getUniqueId(); // Remove pending selection cancel_region_selection(event.getPlayer()); + // Remove region visualization + stop_visualizing_region(player_id); } @EventHandler diff --git a/vane-regions/src/main/java/org/oddlama/vane/regions/commands/Fly.java b/vane-regions/src/main/java/org/oddlama/vane/regions/commands/Fly.java new file mode 100644 index 000000000..3e3290258 --- /dev/null +++ b/vane-regions/src/main/java/org/oddlama/vane/regions/commands/Fly.java @@ -0,0 +1,81 @@ +package org.oddlama.vane.regions.commands; + +import static com.mojang.brigadier.Command.SINGLE_SUCCESS; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import io.papermc.paper.command.brigadier.CommandSourceStack; +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; +import org.oddlama.vane.regions.region.RoleSetting; + +@Name("fly") +@Aliases({ "regionfly" }) +public class Fly extends Command { + + public Fly(Context context) { + super(context); + } + + @Override + public LiteralArgumentBuilder get_command_base() { + return super.get_command_base() + .then(help()) + .requires(ctx -> ctx.getSender() instanceof Player) + .executes(ctx -> { + toggle_fly((Player) ctx.getSource().getSender()); + return SINGLE_SUCCESS; + }); + } + + private void toggle_fly(Player player) { + // Check if player is in a region + final var region = get_module().region_at(player.getLocation()); + + if (region == null) { + player.sendMessage("§cYou must be inside a region to use this command."); + return; + } + + // Get the region group and check permissions + final var group = region.region_group(get_module()); + final var role = group.get_role(player.getUniqueId()); + + // Check if player has BUILD permission (which friends and admins have) + // or ADMIN permission + final boolean hasPermission = role.get_setting(RoleSetting.ADMIN) || + role.get_setting(RoleSetting.BUILD); + + if (!hasPermission) { + player.sendMessage("§cYou don't have permission to fly here."); + return; + } + + // Toggle flying + if (player.getAllowFlight()) { + // Disable flying + player.setAllowFlight(false); + player.setFlying(false); + get_module().fly_manager.remove_flying_player(player.getUniqueId()); + // Set manual opt-out so auto-fly won't re-enable until toggled again + get_module().fly_manager.set_manual_opt_out(player.getUniqueId(), true); + // Stop visualizing the region + get_module().stop_visualizing_region(player.getUniqueId()); + player.sendMessage("§aFlight disabled."); + } else { + // Enable flying + player.setAllowFlight(true); + player.setFlying(true); + get_module().fly_manager.add_flying_player(player.getUniqueId(), region); + // Clear manual opt-out now that the player opted in again + get_module().fly_manager.set_manual_opt_out(player.getUniqueId(), false); + // Start visualizing the region + get_module().start_visualizing_region(player.getUniqueId(), region); + player.sendMessage("§aFlight enabled."); + } + } +} + 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 index 5b8e17cf1..faf318be7 100644 --- 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 @@ -1,6 +1,7 @@ package org.oddlama.vane.regions.commands; import static com.mojang.brigadier.Command.SINGLE_SUCCESS; +import static com.mojang.brigadier.builder.LiteralArgumentBuilder.literal; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import io.papermc.paper.command.brigadier.CommandSourceStack; @@ -23,6 +24,14 @@ public Region(Context context) { public LiteralArgumentBuilder get_command_base() { return super.get_command_base() .then(help()) + .then( + LiteralArgumentBuilder.literal("visualize") + .requires(ctx -> ctx.getSender() instanceof Player) + .executes(ctx -> { + toggle_region_visualization((Player) ctx.getSource().getSender()); + return SINGLE_SUCCESS; + }) + ) .requires(ctx -> ctx.getSender() instanceof Player) .executes(ctx -> { open_menu((Player) ctx.getSource().getSender()); @@ -33,4 +42,22 @@ public LiteralArgumentBuilder get_command_base() { private void open_menu(Player player) { get_module().menus.main_menu.create(player).open(player); } + + private void toggle_region_visualization(Player player) { + final var player_id = player.getUniqueId(); + + if (get_module().is_visualizing_region(player_id)) { + get_module().stop_visualizing_region(player_id); + player.sendMessage("§cRegion visualization disabled."); + } else { + final var region = get_module().region_at(player.getLocation()); + if (region == null) { + player.sendMessage("§cYou must be inside a region to visualize it."); + return; + } + + get_module().start_visualizing_region(player_id, region); + player.sendMessage("§aVisualizing region boundaries. Use §b/region visualize§a again to disable."); + } + } } diff --git a/vane-regions/src/main/resources/lang-en.yml b/vane-regions/src/main/resources/lang-en.yml index d24052f95..c1869a420 100644 --- a/vane-regions/src/main/resources/lang-en.yml +++ b/vane-regions/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: 3 +version: 4 # 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). @@ -34,11 +34,29 @@ select_primary_block: "§aSelected (%1$s§a, %2$s§a, %3$s§a) as primary block. # %3$s: Block Z select_secondary_block: "§aSelected (%1$s§a, %2$s§a, %3$s§a) as secondary block." +# This message is sent when flying is enabled in a region. +fly_enabled: "§aFlight enabled! You can fly in regions where you have permission." +# This message is sent when flying is manually disabled. +fly_disabled: "§cFlight disabled." +# This message is sent when the player tries to use /fly but is not in a region. +fly_not_in_region: "§cYou must be inside a region to use this command." +# This message is sent when the player tries to use /fly but doesn't have permission. +fly_no_permission: "§cYou don't have permission to fly here. Ask the region owner for friend or admin role." +# This message is sent when flying is disabled because the player left the region and they are protected from fall damage. +fly_disabled_safe_landing: "§eYou left the area! Flight disabled but you're protected from fall damage." +# This message is sent when flying is disabled because the player left the region but they were close to the ground. +fly_disabled_region_exit: "§eYou left the area! Flight disabled." + command_region: usage: "%1$s" description: "Open the region management menu." help: "Execute to open the region management menu." +command_fly: + usage: "%1$s" + description: "Toggle flying in regions where you have permission." + help: "Execute to toggle flying. You must be in a region where you have friend or admin permissions." + dynmap: # The label for the dynmap layer layer_label: "Regions" diff --git a/vane-trifles/build.gradle.kts b/vane-trifles/build.gradle.kts index 0e45aa01c..b9e28dd8d 100644 --- a/vane-trifles/build.gradle.kts +++ b/vane-trifles/build.gradle.kts @@ -13,8 +13,8 @@ dependencies { } tasks { - shadowJar { - configurations = listOf() - relocate("org.json", "org.oddlama.vane.external.json") - } + shadowJar { + // Do not include dependencies (vane-core is provided as a separate plugin) + configurations = listOf() + } } From 0213f1dc29ad8d1c9b6b04f684d324bbd5f1c66b Mon Sep 17 00:00:00 2001 From: June Hobart Date: Tue, 14 Oct 2025 14:49:55 +0200 Subject: [PATCH 2/7] Updated the webpage to reflect my previous changes Updated the webpage to reflect my previous changes being /fly in regions and 50% sleep requirement to 20% --- docs/index.html | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/index.html b/docs/index.html index f2e3d92dd..00c9b52a0 100644 --- a/docs/index.html +++ b/docs/index.html @@ -259,7 +259,7 @@

@@ -2159,6 +2159,15 @@

Portals & Regions

  • Admins of the region can change the portal settings
  • The visibility of the portal can be restricted to either players that have the portal permission or other portals that are in a region with the same region group.
  • +
+

Flight in Regions

+

Players with admin or friend roles in a region can fly within their claims using the /fly command.

+
    +
  • Upon entering a region where you have permissions, flight is automatically enabled—just double-jump to start flying
  • +
  • Use /fly to manually toggle flight on/off with visual feedback
  • +
  • Particle visualization shows region boundaries while flying, automatically updating when moving between regions
  • +
  • Fall damage protection is provided when exiting a region while airborne
  • +
  • Flight automatically disables when leaving the region or entering an area without permissions
From afac110ed3f77c488cc6c78f6aaef315a2bb13c9 Mon Sep 17 00:00:00 2001 From: June Hobart Date: Tue, 14 Oct 2025 16:21:44 +0200 Subject: [PATCH 3/7] Added clearer documentation for graylist --- docs/index.html | 10 ++++++++-- documentation/content/vane-admin/graylist.md | 12 ++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/index.html b/docs/index.html index 00c9b52a0..8446810c5 100644 --- a/docs/index.html +++ b/docs/index.html @@ -2449,7 +2449,7 @@