Compare commits

...

15 Commits

Author SHA1 Message Date
5a100ef988 feat: inheritance-aware plot limits / permission ranges 2025-05-31 15:28:26 +02:00
711fba0b2a Update junit5 monorepo (#4662)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-30 12:06:20 +00:00
671a27fa6f Update dependency com.diffplug.spotless to v7.0.4 (#4661)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-27 22:39:24 +00:00
03de685dc4 Update dependency net.essentialsx:EssentialsX to v2.21.1 (#4660)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-25 21:59:36 +00:00
f616885206 Update dependency net.luckperms:api to v5.5 (#4659)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-25 10:29:47 +00:00
7da4eb1ab5 fix: correctly parse help messages with prefix (#4652) 2025-05-24 14:39:31 +02:00
629646ab06 fix: cancel structure locate in classic plot worlds (#4653) 2025-05-24 14:39:14 +02:00
74a1a1f954 Update dependency gradle to v8.14.1 (#4658)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-22 17:04:52 +00:00
aa44078018 Back to snapshot for development
Signed-off-by: Alexander Brandes <mc.cache@web.de>
2025-05-18 19:30:24 +02:00
bfbf406418 Release 7.5.3
Signed-off-by: Alexander Brandes <mc.cache@web.de>
2025-05-18 19:27:33 +02:00
2accedf264 fix: Don't use list to proof block is copper. Instead check with name (#4641)
* fix: Don't use list to proof block is copper. Instead check with name

* chore: Code-Cleanup
2025-05-18 14:04:39 +02:00
6ef0d58480 fix: cancel teleport to denied plot (#4650)
* chore: add 1.21.5 to runServer task, download Paper FAWE jar

* fix: don't allow teleport to denied plots
2025-05-18 14:03:51 +02:00
fbf4a638b4 chore/fix: use #getBlock in PlayerBucketEmptyEvent (#4651)
chore/fix: use #getBlock method for block retrieval in PlayerBucketEmptyEvent
2025-05-18 14:03:28 +02:00
9abfa21078 Add 'world' placeholder to status messages (#4632)
Signed-off-by: Alexander Brandes <mc.cache@web.de>
2025-05-17 23:20:03 +02:00
b84599b4b3 Add server brand to debugpaste (#4640)
Signed-off-by: Alexander Brandes <mc.cache@web.de>
2025-05-17 23:18:52 +02:00
26 changed files with 502 additions and 169 deletions

View File

@ -252,6 +252,11 @@ public final class BukkitPlatform extends JavaPlugin implements Listener, PlotPl
return Bukkit.getVersion();
}
@Override
public @NonNull String serverBrand() {
return Bukkit.getName();
}
@Override
@SuppressWarnings("deprecation") // Paper deprecation
public void onEnable() {

View File

@ -22,12 +22,20 @@ import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.Singleton;
import com.plotsquared.bukkit.permissions.BukkitPermissionHandler;
import com.plotsquared.bukkit.permissions.BukkitRangedPermissionResolver;
import com.plotsquared.bukkit.permissions.LuckPermsRangedPermissionResolver;
import com.plotsquared.bukkit.permissions.VaultPermissionHandler;
import com.plotsquared.core.configuration.Settings;
import com.plotsquared.core.permissions.PermissionHandler;
import com.plotsquared.core.permissions.RangedPermissionResolver;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bukkit.Bukkit;
public class PermissionModule extends AbstractModule {
private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + PermissionModule.class.getSimpleName());
@Provides
@Singleton
PermissionHandler providePermissionHandler() {
@ -40,4 +48,18 @@ public class PermissionModule extends AbstractModule {
return new BukkitPermissionHandler();
}
@Provides
@Singleton
RangedPermissionResolver provideRangedPermissionResolver() {
if (Settings.Permissions.USE_LUCKPERMS_RANGE_RESOLVER) {
if (Bukkit.getPluginManager().isPluginEnabled("LuckPerms")) {
LOGGER.info("Using experimental LuckPerms ranged permission resolver");
return new LuckPermsRangedPermissionResolver();
}
LOGGER.warn("Enabled LuckPerms ranged permission resolver, but LuckPerms is not installed. " +
"Falling back to default Bukkit ranged permission resolver");
}
return new BukkitRangedPermissionResolver();
}
}

View File

@ -28,7 +28,6 @@ import com.plotsquared.core.plot.flag.implementations.CopperOxideFlag;
import com.plotsquared.core.plot.flag.implementations.MiscInteractFlag;
import com.plotsquared.core.plot.flag.implementations.SculkSensorInteractFlag;
import com.plotsquared.core.util.PlotFlagUtil;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Item;
@ -42,31 +41,11 @@ import org.bukkit.event.block.BlockReceiveGameEvent;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
@SuppressWarnings("unused")
public class BlockEventListener117 implements Listener {
private static final Set<Material> COPPER_OXIDIZING = Set.of(
Material.COPPER_BLOCK,
Material.EXPOSED_COPPER,
Material.WEATHERED_COPPER,
Material.OXIDIZED_COPPER,
Material.CUT_COPPER,
Material.EXPOSED_CUT_COPPER,
Material.WEATHERED_CUT_COPPER,
Material.OXIDIZED_CUT_COPPER,
Material.CUT_COPPER_STAIRS,
Material.EXPOSED_CUT_COPPER_STAIRS,
Material.WEATHERED_CUT_COPPER_STAIRS,
Material.OXIDIZED_CUT_COPPER_STAIRS,
Material.CUT_COPPER_SLAB,
Material.EXPOSED_CUT_COPPER_SLAB,
Material.WEATHERED_CUT_COPPER_SLAB,
Material.OXIDIZED_CUT_COPPER_SLAB
);
@Inject
public BlockEventListener117() {
}
@ -184,7 +163,7 @@ public class BlockEventListener117 implements Listener {
if (plot == null) {
return;
}
if (COPPER_OXIDIZING.contains(event.getNewState().getType())) {
if (event.getNewState().getType().name().contains("COPPER")) {
if (!plot.getFlag(CopperOxideFlag.class)) {
plot.debug("Copper could not oxide because copper-oxide = false");
event.setCancelled(true);

View File

@ -29,6 +29,7 @@ import com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent;
import com.destroystokyo.paper.event.server.AsyncTabCompleteEvent;
import com.google.inject.Inject;
import com.plotsquared.bukkit.util.BukkitUtil;
import com.plotsquared.core.PlotSquared;
import com.plotsquared.core.command.Command;
import com.plotsquared.core.command.MainCommand;
import com.plotsquared.core.configuration.Settings;
@ -38,6 +39,7 @@ import com.plotsquared.core.permissions.Permission;
import com.plotsquared.core.player.PlotPlayer;
import com.plotsquared.core.plot.Plot;
import com.plotsquared.core.plot.PlotArea;
import com.plotsquared.core.plot.PlotAreaType;
import com.plotsquared.core.plot.flag.FlagContainer;
import com.plotsquared.core.plot.flag.implementations.BeaconEffectsFlag;
import com.plotsquared.core.plot.flag.implementations.DoneFlag;
@ -48,6 +50,7 @@ import com.plotsquared.core.plot.flag.types.BooleanFlag;
import com.plotsquared.core.plot.world.PlotAreaManager;
import com.plotsquared.core.util.PlotFlagUtil;
import io.papermc.paper.event.entity.EntityMoveEvent;
import io.papermc.paper.event.world.StructuresLocateEvent;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.tag.Tag;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
@ -458,6 +461,21 @@ public class PaperListener implements Listener {
}
}
/**
* Don't let the server die when populating cartographers (villager offering maps) in classic plot worlds
* (as those don't generate POIs)
*/
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
public void onStructuresLocate(StructuresLocateEvent event) {
if (!PlotSquared.get().getPlotAreaManager().hasPlotArea(event.getWorld().getName())) {
return;
}
final PlotArea area = PlotSquared.get().getPlotAreaManager().getPlotAreaByString(event.getWorld().getName());
if (area != null && area.getType() == PlotAreaType.NORMAL) {
event.setCancelled(true);
}
}
private boolean getBooleanFlagValue(
@NonNull FlagContainer container,
@NonNull Class<? extends BooleanFlag<?>> flagClass,

View File

@ -88,10 +88,8 @@ import org.bukkit.Bukkit;
import org.bukkit.FluidCollisionMode;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.BlockState;
import org.bukkit.block.Sign;
import org.bukkit.block.data.Waterlogged;
import org.bukkit.command.PluginCommand;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.Boat;
@ -600,7 +598,11 @@ public class PlayerEventListener implements Listener {
// i.e. untrusted-visit can override deny-teleport
// this is acceptable, because otherwise it wouldn't make sense to have both flags set
if (result || (plot.getFlag(UntrustedVisitFlag.class) && plot.getHomeSynchronous().equals(BukkitUtil.adaptComplete(to)))) {
plotListener.plotEntry(pp, plot);
// returns false if the player is not allowed to enter the plot (if they are denied, for example)
// don't let the move event cancel the entry after teleport, but rather catch and cancel early (#4647)
if (!plotListener.plotEntry(pp, plot)) {
event.setCancelled(true);
}
} else {
pp.sendMessage(
TranslatableCaption.of("deny.no_enter"),
@ -1370,22 +1372,7 @@ public class PlayerEventListener implements Listener {
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onBucketEmpty(PlayerBucketEmptyEvent event) {
BlockFace bf = event.getBlockFace();
// Note: a month after Bukkit 1.14.4 released, they added the API method
// PlayerBucketEmptyEvent#getBlock(), which returns the block the
// bucket contents is going to be placed at. Currently we determine this
// block ourselves to retain compatibility with 1.13.
final Block block;
// if the block can be waterlogged, the event might waterlog the block
// sometimes
if (event.getBlockClicked().getBlockData() instanceof Waterlogged waterlogged
&& !waterlogged.isWaterlogged() && event.getBucket() != Material.LAVA_BUCKET) {
block = event.getBlockClicked();
} else {
block = event.getBlockClicked().getLocation()
.add(bf.getModX(), bf.getModY(), bf.getModZ())
.getBlock();
}
final Block block = event.getBlock();
Location location = BukkitUtil.adapt(block.getLocation());
PlotArea area = location.getPlotArea();
if (area == null) {

View File

@ -0,0 +1,107 @@
/*
* PlotSquared, a land and world management plugin for Minecraft.
* Copyright (C) IntellectualSites <https://intellectualsites.com>
* Copyright (C) IntellectualSites team and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.plotsquared.bukkit.permissions;
import com.plotsquared.bukkit.player.BukkitPlayer;
import com.plotsquared.core.permissions.RangedPermissionResolver;
import com.plotsquared.core.player.PlotPlayer;
import com.plotsquared.core.util.MathMan;
import org.bukkit.permissions.PermissionAttachmentInfo;
import org.checkerframework.checker.index.qual.NonNegative;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.Set;
import java.util.stream.IntStream;
public class BukkitRangedPermissionResolver implements RangedPermissionResolver {
private static boolean CHECK_EFFECTIVE = true;
@Override
public @NonNegative int getPermissionRange(
final @NonNull PlotPlayer<?> generic,
final @NonNull String stub,
final @Nullable String worldContext,
@NonNegative final int range
) {
if (!(generic instanceof BukkitPlayer player)) {
throw new IllegalArgumentException("PlotPlayer is not a BukkitPlayer");
}
if (hasWildcardRange(player, stub, worldContext)) {
return INFINITE_RANGE_VALUE;
}
int max = 0;
if (CHECK_EFFECTIVE) {
boolean hasAny = false;
String stubPlus = stub + ".";
final Set<PermissionAttachmentInfo> effective = player.getPlatformPlayer().getEffectivePermissions();
if (!effective.isEmpty()) {
for (PermissionAttachmentInfo attach : effective) {
// Ignore all "false" permissions
if (!attach.getValue()) {
continue;
}
String permStr = attach.getPermission();
if (permStr.startsWith(stubPlus)) {
hasAny = true;
String end = permStr.substring(stubPlus.length());
if (MathMan.isInteger(end)) {
int val = Integer.parseInt(end);
if (val > range) {
return val;
}
if (val > max) {
max = val;
}
}
}
}
if (hasAny) {
return max;
}
// Workaround
for (PermissionAttachmentInfo attach : effective) {
String permStr = attach.getPermission();
if (permStr.startsWith("plots.") && !permStr.equals("plots.use")) {
return max;
}
}
CHECK_EFFECTIVE = false;
}
}
for (int i = range; i > 0; i--) {
if (player.hasPermission(worldContext, stub + "." + i)) {
return i;
}
}
return max;
}
@Override
public @NonNull IntStream streamFullPermissionRange(
final @NonNull PlotPlayer<?> player,
final @NonNull String stub,
final @Nullable String worldContext,
@NonNegative final int range
) {
return IntStream.of(getPermissionRange(player, stub, worldContext, range));
}
}

View File

@ -0,0 +1,95 @@
/*
* PlotSquared, a land and world management plugin for Minecraft.
* Copyright (C) IntellectualSites <https://intellectualsites.com>
* Copyright (C) IntellectualSites team and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.plotsquared.bukkit.permissions;
import com.plotsquared.core.permissions.RangedPermissionResolver;
import com.plotsquared.core.player.PlotPlayer;
import com.plotsquared.core.util.MathMan;
import net.luckperms.api.LuckPerms;
import net.luckperms.api.context.ImmutableContextSet;
import net.luckperms.api.model.user.User;
import net.luckperms.api.node.NodeType;
import net.luckperms.api.node.types.PermissionNode;
import net.luckperms.api.query.QueryOptions;
import org.bukkit.Bukkit;
import org.checkerframework.checker.index.qual.NonNegative;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.Objects;
import java.util.stream.IntStream;
public class LuckPermsRangedPermissionResolver implements RangedPermissionResolver {
private final LuckPerms luckPerms;
public LuckPermsRangedPermissionResolver() {
this.luckPerms = Objects.requireNonNull(
Bukkit.getServicesManager().getRegistration(LuckPerms.class),
"LuckPerms is not available"
).getProvider();
}
@Override
public @NonNegative int getPermissionRange(
final @NonNull PlotPlayer<?> player,
final @NonNull String stub,
final @Nullable String worldContext,
final @NonNegative int range
) {
// no need to use LuckPerms for basic checks
if (this.hasWildcardRange(player, stub, worldContext)) {
return INFINITE_RANGE_VALUE;
}
return this.streamFullPermissionRange(player, stub, worldContext, range)
.sorted()
.reduce((first, second) -> second)
.orElse(0);
}
@NonNull
public IntStream streamFullPermissionRange(
final @NonNull PlotPlayer<?> player,
final @NonNull String stub,
final @Nullable String worldContext,
@NonNegative final int range
) {
final User user = this.luckPerms.getUserManager().getUser(player.getUUID());
if (user == null) {
throw new IllegalStateException("Luckperms User is null - is the Player online? (UUID: %s)".formatted(player.getUUID()));
}
final QueryOptions queryOptions = worldContext == null ?
QueryOptions.nonContextual() :
QueryOptions.contextual(ImmutableContextSet.of("world", worldContext));
return user.resolveInheritedNodes(queryOptions).stream()
// only support normal permission nodes (regex permission nodes would be a pita to support)
.filter(NodeType.PERMISSION::matches)
.map(node -> ((PermissionNode) node).getPermission())
// check that the node actually has additional data after the stub
.filter(permission -> permission.startsWith(stub + "."))
// extract the raw data after the stub
.map(permission -> permission.substring(stub.length() + 1))
// check if data is integer and parse
.filter(MathMan::isInteger)
.mapToInt(Integer::parseInt)
// only use values that are positive
.filter(value -> value > -1);
}
}

View File

@ -24,14 +24,12 @@ import com.plotsquared.core.PlotSquared;
import com.plotsquared.core.configuration.Settings;
import com.plotsquared.core.events.TeleportCause;
import com.plotsquared.core.location.Location;
import com.plotsquared.core.permissions.Permission;
import com.plotsquared.core.permissions.PermissionHandler;
import com.plotsquared.core.player.ConsolePlayer;
import com.plotsquared.core.player.PlotPlayer;
import com.plotsquared.core.plot.PlotWeather;
import com.plotsquared.core.plot.world.PlotAreaManager;
import com.plotsquared.core.util.EventDispatcher;
import com.plotsquared.core.util.MathMan;
import com.plotsquared.core.util.WorldUtil;
import com.sk89q.worldedit.bukkit.BukkitAdapter;
import com.sk89q.worldedit.extension.platform.Actor;
@ -47,13 +45,11 @@ import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.bukkit.event.EventException;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.permissions.PermissionAttachmentInfo;
import org.bukkit.plugin.RegisteredListener;
import org.bukkit.potion.PotionEffectType;
import org.checkerframework.checker.index.qual.NonNegative;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.Set;
import java.util.UUID;
import static com.sk89q.worldedit.world.gamemode.GameModes.ADVENTURE;
@ -150,77 +146,13 @@ public class BukkitPlayer extends PlotPlayer<Player> {
}
}
@SuppressWarnings("StringSplitter")
@Override
@NonNegative
public int hasPermissionRange(
final @NonNull String stub,
@NonNegative final int range
) {
if (hasPermission(Permission.PERMISSION_ADMIN.toString())) {
return Integer.MAX_VALUE;
}
final String[] nodes = stub.split("\\.");
final StringBuilder n = new StringBuilder();
// Wildcard check from less specific permission to more specific permission
for (int i = 0; i < (nodes.length - 1); i++) {
n.append(nodes[i]).append(".");
if (!stub.equals(n + Permission.PERMISSION_STAR.toString())) {
if (hasPermission(n + Permission.PERMISSION_STAR.toString())) {
return Integer.MAX_VALUE;
}
}
}
// Wildcard check for the full permission
if (hasPermission(stub + ".*")) {
return Integer.MAX_VALUE;
}
// Permission value cache for iterative check
int max = 0;
if (CHECK_EFFECTIVE) {
boolean hasAny = false;
String stubPlus = stub + ".";
final Set<PermissionAttachmentInfo> effective = player.getEffectivePermissions();
if (!effective.isEmpty()) {
for (PermissionAttachmentInfo attach : effective) {
// Ignore all "false" permissions
if (!attach.getValue()) {
continue;
}
String permStr = attach.getPermission();
if (permStr.startsWith(stubPlus)) {
hasAny = true;
String end = permStr.substring(stubPlus.length());
if (MathMan.isInteger(end)) {
int val = Integer.parseInt(end);
if (val > range) {
return val;
}
if (val > max) {
max = val;
}
}
}
}
if (hasAny) {
return max;
}
// Workaround
for (PermissionAttachmentInfo attach : effective) {
String permStr = attach.getPermission();
if (permStr.startsWith("plots.") && !permStr.equals("plots.use")) {
return max;
}
}
CHECK_EFFECTIVE = false;
}
}
for (int i = range; i > 0; i--) {
if (hasPermission(stub + "." + i)) {
return i;
}
}
return max;
return PlotSquared.platform().rangedPermissionResolver().getPermissionRange(this, stub, null, range);
}
@Override

View File

@ -31,6 +31,7 @@ import com.plotsquared.core.generator.IndependentPlotGenerator;
import com.plotsquared.core.inject.annotations.DefaultGenerator;
import com.plotsquared.core.location.World;
import com.plotsquared.core.permissions.PermissionHandler;
import com.plotsquared.core.permissions.RangedPermissionResolver;
import com.plotsquared.core.player.PlotPlayer;
import com.plotsquared.core.plot.expiration.ExpireManager;
import com.plotsquared.core.plot.world.PlotAreaManager;
@ -120,6 +121,14 @@ public interface PlotPlatform<P> extends LocaleHolder {
*/
@NonNull String serverImplementation();
/**
* Gets the server brand name
*
* @return server brand
* @since 7.5.3
*/
@NonNull String serverBrand();
/**
* Gets the native server code package prefix.
*
@ -341,6 +350,10 @@ public interface PlotPlatform<P> extends LocaleHolder {
return injector().getInstance(PermissionHandler.class);
}
default @NonNull RangedPermissionResolver rangedPermissionResolver() {
return injector().getInstance(RangedPermissionResolver.class);
}
/**
* Get the {@link ServicePipeline} implementation
*

View File

@ -144,6 +144,7 @@ public class Buy extends Command {
plot.getPlotModificationManager().setSign(player.getName());
player.sendMessage(
TranslatableCaption.of("working.claimed"),
TagResolver.resolver("world", Tag.inserting(Component.text(plot.getArea().getWorldName()))),
TagResolver.resolver("plot", Tag.inserting(Component.text(plot.getId().toString())))
);
this.eventDispatcher.callPostPlayerBuyPlot(player, previousOwner, plot, price);

View File

@ -131,6 +131,7 @@ public class Clear extends Command {
player.sendMessage(
TranslatableCaption.of("working.clearing_done"),
TagResolver.builder()
.tag("world", Tag.inserting(Component.text(plot.getArea().getWorldName())))
.tag("amount", Tag.inserting(Component.text(System.currentTimeMillis() - start)))
.tag("plot", Tag.inserting(Component.text(plot.getId().toString())))
.build()

View File

@ -24,6 +24,7 @@ import com.plotsquared.core.configuration.caption.TranslatableCaption;
import com.plotsquared.core.player.PlotPlayer;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.jetbrains.annotations.NotNull;
@ -98,6 +99,14 @@ public enum CommandCategory implements Caption {
return MiniMessage.miniMessage().deserialize(getComponent(localeHolder));
}
@Override
public @NonNull Component toComponent(
@NonNull final LocaleHolder localeHolder,
final @NonNull TagResolver @NonNull ... tagResolvers
) {
return MiniMessage.miniMessage().deserialize(getComponent(localeHolder));
}
/**
* Checks if a player has access to this command category
*

View File

@ -86,7 +86,8 @@ public class DebugPaste extends SubCommand {
b.append("# WorldEdit implementation:\n");
b.append(PlotSquared.platform().worldEditImplementations()).append("\n\n");
b.append("# Server Information\n");
b.append("Server Version: ").append(PlotSquared.platform().serverImplementation())
b.append("Server Version: ").append(PlotSquared.platform().serverBrand()).append(": ")
.append(PlotSquared.platform().serverImplementation()).append("\n")
.append("\n");
b.append("online_mode: ").append(!Settings.UUID.OFFLINE).append(';')
.append(!Settings.UUID.OFFLINE).append('\n');

View File

@ -124,6 +124,7 @@ public class Delete extends SubCommand {
"amount",
Tag.inserting(Component.text(String.valueOf(System.currentTimeMillis() - start)))
),
TagResolver.resolver("world", Tag.inserting(Component.text(plotArea.getWorldName()))),
TagResolver.resolver("plot", Tag.inserting(Component.text(plot.getId().toString())))
);
eventDispatcher.callPostDelete(plot);

View File

@ -113,38 +113,34 @@ public class Help extends Command {
}
if (cat == null && page == 0) {
TextComponent.Builder builder = Component.text();
builder.append(MINI_MESSAGE.deserialize(TranslatableCaption.of("help.help_header").getComponent(player)));
builder.append(TranslatableCaption.of("help.help_header").toComponent(player));
for (CommandCategory c : CommandCategory.values()) {
if (!c.canAccess(player)) {
continue;
}
builder.append(Component.newline()).append(MINI_MESSAGE
.deserialize(
TranslatableCaption.of("help.help_info_item").getComponent(player),
TagResolver.builder()
.tag("command", Tag.inserting(Component.text("/plot help")))
.tag("category", Tag.inserting(Component.text(c.name().toLowerCase())))
.tag("category_desc", Tag.inserting(c.toComponent(player)))
.build()
));
builder.append(Component.newline());
builder.append(TranslatableCaption.of("help.help_info_item").toComponent(
player, TagResolver.builder()
.tag("command", Tag.inserting(Component.text("/plot help")))
.tag("category", Tag.inserting(Component.text(c.name().toLowerCase())))
.tag("category_desc", Tag.inserting(c.toComponent(player)))
.build()
));
}
builder.append(Component.newline()).append(MINI_MESSAGE
.deserialize(
TranslatableCaption.of("help.help_info_item").getComponent(player),
TagResolver.builder()
.tag("command", Tag.inserting(Component.text("/plot help")))
.tag("category", Tag.inserting(Component.text("all")))
.tag(
"category_desc",
Tag.inserting(TranslatableCaption
.of("help.help_display_all_commands")
.toComponent(player))
)
.build()
));
builder.append(Component.newline()).append(MINI_MESSAGE.deserialize(TranslatableCaption
.of("help.help_footer")
.getComponent(player)));
builder.append(Component.newline());
builder.append(TranslatableCaption.of("help.help_info_item").toComponent(
player, TagResolver.builder()
.tag("command", Tag.inserting(Component.text("/plot help")))
.tag("category", Tag.inserting(Component.text("all")))
.tag(
"category_desc", Tag.inserting(TranslatableCaption
.of("help.help_display_all_commands")
.toComponent(player))
)
.build()
));
builder.append(Component.newline());
builder.append(TranslatableCaption.of("help.help_footer").toComponent(player));
player.sendMessage(StaticCaption.of(MINI_MESSAGE.serialize(builder.asComponent())));
return true;
}

View File

@ -525,8 +525,8 @@ public class Settings extends Config {
public static final class Limit {
@Comment("Should the limit be global (over multiple worlds)")
public static boolean GLOBAL =
false;
public static boolean GLOBAL = false;
@Comment({"The max range of integer permissions to check for, e.g. 'plots.plot.127' or 'plots.set.flag.mob-cap.127'",
"The value covers the permission range to check, you need to assign the permission to players/groups still",
"Modifying the value does NOT change the amount of plots players can claim"})
@ -737,6 +737,7 @@ public class Settings extends Config {
@Comment("If \"instabreak\" should consider the used tool.")
public static boolean INSTABREAK_CONSIDER_TOOL = false;
}
@Comment({"Enable or disable parts of the plugin",
@ -830,4 +831,17 @@ public class Settings extends Config {
}
@Comment({"Permission-Resolver specific settings"})
public static final class Permissions {
@Comment({
"use the new LuckPerms resolver for ranged permissions (e.g., plots.plot.<number>).",
"in contrary to the default resolver, this resolver can handle inheritance of permissions.",
"e.g. if the player has multiple matching permissions, for example, due to multiple group memberships " +
"(plots.plot.5 & plots.plot.15), this resolver would use 15 as the limit as opposed to the previous 5."
})
public static boolean USE_LUCKPERMS_RANGE_RESOLVER = false;
}
}

View File

@ -20,6 +20,7 @@ package com.plotsquared.core.configuration.caption;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import org.checkerframework.checker.nullness.qual.NonNull;
/**
@ -44,6 +45,16 @@ public interface Caption {
*/
@NonNull Component toComponent(@NonNull LocaleHolder localeHolder);
/**
* Get the Adventure {@link ComponentLike} for this caption while applying custom {@link TagResolver}
* (apart from the default {@code core.prefix})
* @param localeHolder Local holder
* @param tagResolvers custom tag resolvers to replace placeholders / parameters
* @return {@link ComponentLike}
* @since TODO
*/
@NonNull Component toComponent(@NonNull LocaleHolder localeHolder, @NonNull TagResolver @NonNull... tagResolvers);
@NonNull String toString();
}

View File

@ -21,6 +21,7 @@ package com.plotsquared.core.configuration.caption;
import com.google.common.base.Preconditions;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import org.checkerframework.checker.nullness.qual.NonNull;
public final class StaticCaption implements Caption {
@ -51,6 +52,14 @@ public final class StaticCaption implements Caption {
return MiniMessage.miniMessage().deserialize(this.value);
}
@Override
public @NonNull Component toComponent(
@NonNull final LocaleHolder localeHolder,
final @NonNull TagResolver @NonNull ... tagResolvers
) {
return MiniMessage.miniMessage().deserialize(this.value, tagResolvers);
}
@Override
public @NonNull String toString() {
return "StaticCaption(" + value + ")";

View File

@ -27,6 +27,7 @@ import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.Locale;
import java.util.regex.Pattern;
@ -96,13 +97,23 @@ public final class TranslatableCaption implements NamespacedCaption {
@Override
public @NonNull Component toComponent(@NonNull final LocaleHolder localeHolder) {
return this.toComponent(localeHolder, new TagResolver[0]);
}
@Override
public @NonNull Component toComponent(
@NonNull final LocaleHolder localeHolder,
final @NonNull TagResolver @NonNull ... tagResolvers
) {
if (getKey().equals("core.prefix")) {
return MiniMessage.miniMessage().deserialize(getComponent(localeHolder));
}
return MiniMessage.miniMessage().deserialize(getComponent(localeHolder), TagResolver.resolver(
TagResolver[] finalResolvers = Arrays.copyOf(tagResolvers, tagResolvers.length + 1);
finalResolvers[finalResolvers.length - 1] = TagResolver.resolver(
"prefix",
Tag.inserting(TranslatableCaption.of("core.prefix").toComponent(localeHolder))
));
);
return MiniMessage.miniMessage().deserialize(getComponent(localeHolder), finalResolvers);
}
@Override

View File

@ -0,0 +1,126 @@
/*
* PlotSquared, a land and world management plugin for Minecraft.
* Copyright (C) IntellectualSites <https://intellectualsites.com>
* Copyright (C) IntellectualSites team and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.plotsquared.core.permissions;
import com.plotsquared.core.player.PlotPlayer;
import org.checkerframework.checker.index.qual.NonNegative;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.stream.IntStream;
/**
* Represents a resolver for ranged permissions. Return values depend on the actual implementation (see Bukkit module).
* <br>
* Even though this interface is not linked to platform implementations by design, implementation-specific details are added to
* the Javadocs.
*
* @since TODO
*/
public interface RangedPermissionResolver {
int INFINITE_RANGE_VALUE = Integer.MAX_VALUE;
/**
* Gets the applicable range value of a player for a specific permission stub
* ({@code plots.plot} would check for {@code plots.plot.<numeric>}).
* <br>
* The standard bukkit implementation would return the lowest numeric value, while the LuckPerms specific resolver would
* try returning the highest possible value.
*
* @param player the permission holder
* @param stub the permission stub to check against
* @param worldContext the optional world context of the action requiring the range
* @param range the maximum permission range to check against (for the default bukkit resolver)
* @return the applicable range value of the player for the given permission stub
* @since TODO
*/
@NonNegative
int getPermissionRange(
final @NonNull PlotPlayer<?> player,
final @NonNull String stub,
final @Nullable String worldContext,
final @NonNegative int range
);
/**
* Gets a stream of all applicable permission range values for the given stub. The stream is unordered by default. If a
* specific order is needed, use the stateful {@link IntStream#sorted()} operation.
* <br>
* The standard bukkit implementation will only return a stream containing a single value equal to
* {@link #getPermissionRange(PlotPlayer, String, String, int)}. For LuckPerms, all applicable node values will be in the
* stream.
*
* @param player the permission holder
* @param stub the permission stub to check against
* @param worldContext the optional world context of the action requiring the range
* @param range the maximum permission range to check against (for the default bukkit resolver)
* @return a stream of all applicable permission range values for the given stub
* @since TODO
*/
@NonNull
IntStream streamFullPermissionRange(
final @NonNull PlotPlayer<?> player,
final @NonNull String stub,
final @Nullable String worldContext,
final @NonNegative int range
);
/**
* Checks if the given player has a wildcard range for the given permission stub.
* <br>
* For example, if checking for the stub {@code plots.plot}, this method would check for:
* <ul>
* <li>{@code *}</li>
* <li>{@code plots.admin}</li>
* <li>{@code plots.plot.*}</li>
* <li>{@code plots.*}</li>
* </ul>
*
* @param player the permission holder
* @param stub the permission stub to check against
* @param worldContext the optional world context of the action requiring the range
* @return {@code true} if the player has a wildcard range for the given permission stub, else {@code false}
* @since TODO
*/
default boolean hasWildcardRange(
final @NonNull PlotPlayer<?> player,
final @NonNull String stub,
final @Nullable String worldContext
) {
if (player.hasPermission(Permission.PERMISSION_STAR) ||
player.hasPermission(Permission.PERMISSION_ADMIN) ||
player.hasPermission(worldContext, stub + ".*")) {
return true;
}
String node = stub;
while (true) {
int lastIndex = node.lastIndexOf('.');
if (lastIndex == -1) {
break;
}
node = node.substring(0, lastIndex);
if (player.hasPermission(worldContext, node + ".*")) {
return true;
}
}
return false;
}
}

View File

@ -1717,6 +1717,7 @@ public class Plot {
}
player.sendMessage(
TranslatableCaption.of("working.claimed"),
TagResolver.resolver("world", Tag.inserting(Component.text(this.getWorldName()))),
TagResolver.resolver("plot", Tag.inserting(Component.text(this.getId().toString())))
);
if (teleport) {

View File

@ -25,30 +25,24 @@ import com.plotsquared.core.player.PlotPlayer;
import com.plotsquared.core.util.StringMan;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.Tag;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import org.jetbrains.annotations.NotNull;
public class HelpObject implements ComponentLike {
static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage();
private final Component rendered;
public HelpObject(final Command command, final String label, final PlotPlayer<?> audience) {
rendered = MINI_MESSAGE.deserialize(
TranslatableCaption.of("help.help_item").getComponent(audience),
TagResolver.builder()
.tag("usage", Tag.inserting(Component.text(command.getUsage().replace("{label}", label))))
.tag("alias", Tag.inserting(Component.text(
command.getAliases().isEmpty() ? "" : StringMan.join(command.getAliases(), " | ")
)))
.tag("desc", Tag.inserting(command.getDescription().toComponent(audience)))
.tag("arguments", Tag.inserting(Component.text(buildArgumentList(command.getRequiredArguments()))))
.tag("label", Tag.inserting(Component.text(label)))
.build()
);
this.rendered = TranslatableCaption.of("help.help_item").toComponent(audience, TagResolver.builder()
.tag("usage", Tag.inserting(Component.text(command.getUsage().replace("{label}", label))))
.tag("alias", Tag.inserting(Component.text(
command.getAliases().isEmpty() ? "" : StringMan.join(command.getAliases(), " | ")
)))
.tag("desc", Tag.inserting(command.getDescription().toComponent(audience)))
.tag("arguments", Tag.inserting(Component.text(buildArgumentList(command.getRequiredArguments()))))
.tag("label", Tag.inserting(Component.text(label)))
.build());
}
private String buildArgumentList(final Argument<?>[] arguments) {

View File

@ -390,11 +390,11 @@
"info.area_list_tooltip": "<gold>Claimed=</gold><gray><claimed></gray>\n<gold>Usage=</gold><gray><usage></gray>\n<gold>Clusters=</gold><gray><clusters></gray>\n<gold>Region=</gold><gray><region></gray>\n<gold>Generator=</gold><gray><generator></gray>",
"info.area_list_item": "<click:run_command:'<command_tp>'><hover:show_text:'<command_tp>'><dark_gray>[</dark_gray><gold><number></gold><dark_gray>]</dark_gray></hover></click> <click:run_command:'<command_info>'><hover:show_text:'<hover_info>'><gold><area_name></gold></hover></click><gray> - </gray><gray><area_type>:<area_terrain></gray>",
"working.generating_component": "<prefix><gold>Started generating component from your settings.</gold>",
"working.clearing_done": "<prefix><dark_aqua>Plot </dark_aqua><gold><plot></gold><dark_aqua> has been cleared! Took </dark_aqua><gold><amount>ms</gold><dark_aqua>.</dark_aqua>",
"working.deleting_done": "<prefix><dark_aqua>Plot </dark_aqua><gold><plot></gold><dark_aqua> has been deleted! Took </dark_aqua><gold><amount>ms</gold><dark_aqua>.</dark_aqua>",
"working.clearing_done": "<prefix><dark_aqua>Plot </dark_aqua><gold><world>;<plot></gold><dark_aqua> has been cleared! Took </dark_aqua><gold><amount>ms</gold><dark_aqua>.</dark_aqua>",
"working.deleting_done": "<prefix><dark_aqua>Plot </dark_aqua><gold><world>;<plot></gold><dark_aqua> has been deleted! Took </dark_aqua><gold><amount>ms</gold><dark_aqua>.</dark_aqua>",
"working.plot_not_claimed": "<prefix><gray>Plot not claimed.</gray>",
"working.plot_is_claimed": "<prefix><gray>This plot is already claimed.</gray>",
"working.claimed": "<prefix><dark_aqua>You successfully claimed the plot </dark_aqua><gold><plot></gold><dark_aqua>.</dark_aqua>",
"working.claimed": "<prefix><dark_aqua>You successfully claimed the plot </dark_aqua><gold><world>;<plot></gold><dark_aqua>.</dark_aqua>",
"working.progress": "<prefix><gray>Current progress: </gray><gold><progress></gold><gray>%</gray>",
"working.component_complete": "<prefix><gold>Component generation has finished for plot <plot>.</gold>",
"list.comment_list_header_paged": "<gray>(Page </gray><gold><cur></gold><gray>/</gray><gold><max></gold><gray>) </gray><gold>List of <amount> comment(s):</gold>",

View File

@ -22,7 +22,7 @@ plugins {
}
group = "com.intellectualsites.plotsquared"
version = "7.5.3-SNAPSHOT"
version = "7.5.4-SNAPSHOT"
if (!File("$rootDir/.git").exists()) {
logger.lifecycle("""
@ -69,8 +69,8 @@ subprojects {
dependencies {
// Tests
testImplementation("org.junit.jupiter:junit-jupiter:5.12.2")
testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.12.2")
testImplementation("org.junit.jupiter:junit-jupiter:5.13.0")
testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.13.0")
}
plugins.withId("java") {
@ -206,14 +206,14 @@ tasks.getByName<Jar>("jar") {
enabled = false
}
val supportedVersions = listOf("1.19.4", "1.20.6", "1.21.1", "1.21.3", "1.21.4")
val supportedVersions = listOf("1.19.4", "1.20.6", "1.21.1", "1.21.3", "1.21.4", "1.21.5")
tasks {
register("cacheLatestFaweArtifact") {
val lastSuccessfulBuildUrl = uri("https://ci.athion.net/job/FastAsyncWorldEdit/lastSuccessfulBuild/api/json").toURL()
val artifact = ((JsonSlurper().parse(lastSuccessfulBuildUrl) as Map<*, *>)["artifacts"] as List<*>)
.map { it as Map<*, *> }
.map { it["fileName"] as String }
.first { it -> it.contains("Bukkit") }
.first { it -> it.contains("Paper") }
project.ext["faweArtifact"] = artifact
}

View File

@ -15,8 +15,8 @@ log4j = "2.19.0"
worldedit = "7.2.20"
fawe = "2.13.0"
placeholderapi = "2.11.6"
luckperms = "5.4"
essentialsx = "2.21.0"
luckperms = "5.5"
essentialsx = "2.21.1"
mvdwapi = "3.1.1"
# Third party
@ -35,7 +35,7 @@ serverlib = "2.3.7"
# Gradle plugins
shadow = "8.3.6"
grgit = "4.1.1"
spotless = "7.0.3"
spotless = "7.0.4"
publish = "0.32.0"
runPaper = "2.3.1"

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME