Skip to content

Commit 94b2bde

Browse files
committed
Improve PDC UX and safety: add /lock off, harden owner rules, cache name mapping, and fix permission tab-complete parsing
1 parent 9ca4766 commit 94b2bde

File tree

8 files changed

+197
-13
lines changed

8 files changed

+197
-13
lines changed

src/main/java/me/crafter/mc/lockettepro/Config.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,14 @@ public static boolean isContainerBypassSignString(String message) {
349349
return containerbypassstrings.contains(message);
350350
}
351351

352+
public static Set<String> getEveryoneSignStrings() {
353+
return Collections.unmodifiableSet(everyonestrings);
354+
}
355+
356+
public static Set<String> getContainerBypassSignStrings() {
357+
return Collections.unmodifiableSet(containerbypassstrings);
358+
}
359+
352360
public static String getLockedContainerPdcKeyString() {
353361
return lockedcontainerpdckey;
354362
}

src/main/java/me/crafter/mc/lockettepro/ContainerPdcLockManager.java

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.bukkit.ChatColor;
77
import org.bukkit.Material;
88
import org.bukkit.NamespacedKey;
9+
import org.bukkit.OfflinePlayer;
910
import org.bukkit.block.Block;
1011
import org.bukkit.block.BlockState;
1112
import org.bukkit.block.Container;
@@ -51,7 +52,9 @@ public final class ContainerPdcLockManager {
5152

5253
private static final String ENTITY_HOPPER = "#hopper";
5354
private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray();
55+
private static final String PLAYER_NAME_CACHE_MISS = "";
5456
private static volatile Cache<String, LockData> runtimeLockDataCache = createRuntimeLockDataCache();
57+
private static volatile Cache<UUID, String> runtimePlayerNameCache = createRuntimePlayerNameCache();
5558
private static volatile int runtimeCacheConfigSignature = Integer.MIN_VALUE;
5659

5760
private ContainerPdcLockManager() {
@@ -207,6 +210,13 @@ private static Cache<String, LockData> createRuntimeLockDataCache() {
207210
return builder.build();
208211
}
209212

213+
private static Cache<UUID, String> createRuntimePlayerNameCache() {
214+
return CacheBuilder.newBuilder()
215+
.maximumSize(8192)
216+
.expireAfterAccess(30, TimeUnit.MINUTES)
217+
.build();
218+
}
219+
210220
private static int getRuntimeCacheConfigSignature() {
211221
int signature = Config.isRuntimeKvCacheEnabled() ? 1 : 0;
212222
signature = 31 * signature + Config.getRuntimeKvCacheTtlMillis();
@@ -231,13 +241,15 @@ private static void ensureRuntimeCacheConfiguration() {
231241
public static void refreshRuntimeCacheConfig() {
232242
synchronized (ContainerPdcLockManager.class) {
233243
runtimeLockDataCache = createRuntimeLockDataCache();
244+
runtimePlayerNameCache = createRuntimePlayerNameCache();
234245
runtimeCacheConfigSignature = getRuntimeCacheConfigSignature();
235246
}
236247
}
237248

238249
public static void clearRuntimeCache() {
239250
ensureRuntimeCacheConfiguration();
240251
runtimeLockDataCache.invalidateAll();
252+
runtimePlayerNameCache.invalidateAll();
241253
}
242254

243255
public static void invalidateRuntimeCache(Block block) {
@@ -594,6 +606,7 @@ public static PermissionMutation parsePermissionMutation(String raw) {
594606

595607
String subject = normalizeSubject(subjectRaw);
596608
if (subject == null || subject.isBlank()) return null;
609+
if (access == PermissionAccess.OWNER && !isOwnerSubject(subject)) return null;
597610
return new PermissionMutation(access, subject);
598611
}
599612

@@ -638,14 +651,57 @@ public static String describeSubject(String subject) {
638651
UUID uuid = UUID.fromString(subject);
639652
Player online = Bukkit.getPlayer(uuid);
640653
if (online != null) {
654+
runtimePlayerNameCache.put(uuid, online.getName());
641655
return online.getName() + "#" + uuid;
642656
}
657+
String cachedName = runtimePlayerNameCache.getIfPresent(uuid);
658+
if (cachedName != null) {
659+
if (cachedName.isEmpty()) {
660+
return subject;
661+
}
662+
return cachedName + "#" + uuid;
663+
}
664+
OfflinePlayer offline = Bukkit.getOfflinePlayer(uuid);
665+
String offlineName = offline.getName();
666+
if (offlineName == null || offlineName.isBlank()) {
667+
runtimePlayerNameCache.put(uuid, PLAYER_NAME_CACHE_MISS);
668+
return subject;
669+
}
670+
runtimePlayerNameCache.put(uuid, offlineName);
671+
return offlineName + "#" + uuid;
643672
} catch (IllegalArgumentException ignored) {
644673
}
645674
}
646675
return subject;
647676
}
648677

678+
public static boolean isSelfSubject(Player player, String subject) {
679+
if (player == null || subject == null) return false;
680+
if (!isOwnerSubject(subject)) return false;
681+
if (isUuid(subject)) {
682+
return player.getUniqueId().toString().equalsIgnoreCase(subject);
683+
}
684+
return player.getName().equalsIgnoreCase(subject);
685+
}
686+
687+
private static boolean isOwnerSubject(String subject) {
688+
if (subject == null || subject.isBlank()) return false;
689+
if (subject.startsWith("[") || subject.startsWith("#")) {
690+
return false;
691+
}
692+
if (isUuid(subject)) return true;
693+
for (int i = 0; i < subject.length(); i++) {
694+
char c = subject.charAt(i);
695+
if (!((c >= 'a' && c <= 'z')
696+
|| (c >= 'A' && c <= 'Z')
697+
|| (c >= '0' && c <= '9')
698+
|| c == '_')) {
699+
return false;
700+
}
701+
}
702+
return true;
703+
}
704+
649705
public static void applyPermissionMutation(Block block, PermissionMutation mutation) {
650706
LockData data = getLockData(block);
651707
LinkedHashMap<String, PermissionAccess> permissions = new LinkedHashMap<>(data.permissions);

src/main/java/me/crafter/mc/lockettepro/LockettePro.java

Lines changed: 115 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
import org.jetbrains.annotations.NotNull;
1414

1515
import java.util.ArrayList;
16+
import java.util.Arrays;
1617
import java.util.EnumSet;
18+
import java.util.LinkedHashSet;
1719
import java.util.LinkedHashMap;
1820
import java.util.List;
1921
import java.util.Locale;
@@ -23,6 +25,7 @@
2325
public class LockettePro extends JavaPlugin {
2426

2527
private static final String PERM_PDC_ON = "lockettepro.pdc.on";
28+
private static final String PERM_PDC_OFF = "lockettepro.pdc.off";
2629
private static final String PERM_PDC_INFO = "lockettepro.pdc.info";
2730
private static final String PERM_PDC_RENAME = "lockettepro.pdc.rename";
2831
private static final String PERM_PDC_PERMISSION = "lockettepro.pdc.permission";
@@ -122,13 +125,8 @@ public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Comman
122125
}
123126
return list;
124127
}
125-
if (args != null && args.length == 2 && "permission".equalsIgnoreCase(args[0]) && sender.hasPermission(PERM_PDC_PERMISSION)) {
126-
List<String> list = new ArrayList<>();
127-
list.add("xx:");
128-
list.add("rw:");
129-
list.add("ro:");
130-
list.add("--:");
131-
return list;
128+
if (args != null && args.length >= 2 && "permission".equalsIgnoreCase(args[0]) && sender.hasPermission(PERM_PDC_PERMISSION)) {
129+
return tabCompletePermission(args, sender);
132130
}
133131
if (args != null && args.length == 2 && "group".equalsIgnoreCase(args[0]) && sender.hasPermission(PERM_PDC_GROUP)) {
134132
return List.of("create", "delete", "add", "remove", "info", "list");
@@ -145,6 +143,71 @@ public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Comman
145143
return null;
146144
}
147145

146+
private List<String> tabCompletePermission(String[] args, CommandSender sender) {
147+
List<String> modes = List.of("xx:", "rw:", "ro:", "--:");
148+
if (args.length == 2) {
149+
String token = args[1] == null ? "" : args[1];
150+
int split = token.indexOf(':');
151+
if (split < 0) {
152+
return filterByPrefix(modes, token);
153+
}
154+
155+
String modePrefix = token.substring(0, split + 1);
156+
String subjectPrefix = token.substring(split + 1);
157+
if (!modes.contains(modePrefix.toLowerCase(Locale.ROOT))) {
158+
return filterByPrefix(modes, token);
159+
}
160+
return filterByPrefix(
161+
buildPermissionSubjectCandidates(sender).stream()
162+
.map(subject -> modePrefix + subject)
163+
.toList(),
164+
token
165+
);
166+
}
167+
168+
if (args.length == 3 && args[1] != null && args[1].contains(":")) {
169+
return filterByPrefix(buildPermissionSubjectCandidates(sender), args[2]);
170+
}
171+
172+
return List.of();
173+
}
174+
175+
private List<String> buildPermissionSubjectCandidates(CommandSender sender) {
176+
LinkedHashSet<String> candidates = new LinkedHashSet<>();
177+
178+
if (sender instanceof Player player) {
179+
PermissionGroupStore.GroupSnapshot own = PermissionGroupStore.getOwnedGroup(player.getUniqueId());
180+
if (own != null) {
181+
candidates.add("[g:" + own.name() + "]");
182+
}
183+
}
184+
185+
List<String> onlineNames = Bukkit.getOnlinePlayers()
186+
.stream()
187+
.map(Player::getName)
188+
.sorted(String::compareToIgnoreCase)
189+
.toList();
190+
candidates.addAll(onlineNames);
191+
192+
candidates.add("#hopper");
193+
194+
candidates.addAll(Config.getEveryoneSignStrings());
195+
candidates.addAll(Config.getContainerBypassSignStrings());
196+
197+
return new ArrayList<>(candidates);
198+
}
199+
200+
private List<String> filterByPrefix(List<String> candidates, String rawPrefix) {
201+
String prefix = rawPrefix == null ? "" : rawPrefix.toLowerCase(Locale.ROOT);
202+
List<String> list = new ArrayList<>();
203+
for (String candidate : candidates) {
204+
if (candidate.toLowerCase(Locale.ROOT).startsWith(prefix)) {
205+
list.add(candidate);
206+
}
207+
}
208+
return list;
209+
}
210+
148211
public boolean onCommand(@NotNull CommandSender sender, Command cmd, @NotNull String commandLabel, final String[] args) {
149212
if (cmd.getName().equals("lockettepro")) {
150213
if (args.length == 0) {
@@ -334,6 +397,7 @@ public boolean onCommand(@NotNull CommandSender sender, Command cmd, @NotNull St
334397
private void registerPlayerSubCommands() {
335398
playerSubCommands.clear();
336399
playerSubCommands.put("on", new PlayerSubCommand(PERM_PDC_ON, (player, args) -> handlePdcLockOn(player)));
400+
playerSubCommands.put("off", new PlayerSubCommand(PERM_PDC_OFF, (player, args) -> handlePdcLockOff(player)));
337401
playerSubCommands.put("info", new PlayerSubCommand(PERM_PDC_INFO, (player, args) -> handlePdcInfo(player)));
338402
playerSubCommands.put("rename", new PlayerSubCommand(PERM_PDC_RENAME, this::handlePdcRename));
339403
playerSubCommands.put("permission", new PlayerSubCommand(PERM_PDC_PERMISSION, this::handlePdcPermission));
@@ -387,6 +451,35 @@ private void handlePdcLockOn(Player player) {
387451
}
388452
}
389453

454+
private void handlePdcLockOff(Player player) {
455+
Block block = ContainerPdcLockManager.getTargetedContainer(player);
456+
if (block == null) {
457+
Utils.sendMessages(player, Config.getLang("pdc-target-container-needed"));
458+
return;
459+
}
460+
461+
ContainerPdcLockManager.LockData data = ContainerPdcLockManager.getLockData(block);
462+
if (!data.hasPdcData() || !data.isLocked()) {
463+
Utils.sendMessages(player, Config.getLang("pdc-not-locked"));
464+
return;
465+
}
466+
467+
boolean owner = ContainerPdcLockManager.isOwner(block, player);
468+
boolean adminOverride = player.hasPermission("lockettepro.admin.break");
469+
if (!owner && !adminOverride) {
470+
Utils.sendMessages(player, Config.getLang("pdc-no-owner-permission"));
471+
Utils.playAccessDenyEffect(player, block);
472+
return;
473+
}
474+
475+
if (ContainerPdcLockManager.writeLockData(block, false, java.util.Collections.emptyMap())) {
476+
Utils.sendMessages(player, Config.getLang("pdc-lock-disabled"));
477+
Utils.refreshLockedContainerPdcTagLater(block);
478+
} else {
479+
Utils.sendMessages(player, Config.getLang("pdc-lock-disable-failed"));
480+
}
481+
}
482+
390483
private void handlePdcInfo(Player player) {
391484
Block block = ContainerPdcLockManager.getTargetedContainer(player);
392485
if (block == null) {
@@ -478,17 +571,29 @@ private void handlePdcPermission(Player player, String[] args) {
478571
return;
479572
}
480573

481-
if (args.length != 2) {
574+
if (args.length < 2) {
482575
Utils.sendMessages(player, Config.getLang("pdc-permission-usage"));
483576
return;
484577
}
485578

486-
ContainerPdcLockManager.PermissionMutation mutation = ContainerPdcLockManager.parsePermissionMutation(args[1]);
579+
String mutationRaw = String.join(" ", Arrays.copyOfRange(args, 1, args.length)).trim();
580+
if (mutationRaw.isEmpty()) {
581+
Utils.sendMessages(player, Config.getLang("pdc-permission-usage"));
582+
return;
583+
}
584+
585+
ContainerPdcLockManager.PermissionMutation mutation = ContainerPdcLockManager.parsePermissionMutation(mutationRaw);
487586
if (mutation == null) {
488587
Utils.sendMessages(player, Config.getLang("pdc-permission-invalid"));
489588
return;
490589
}
491590

591+
if (ContainerPdcLockManager.isSelfSubject(player, mutation.subject())
592+
&& mutation.access() != ContainerPdcLockManager.PermissionAccess.OWNER) {
593+
Utils.sendMessages(player, Config.getLang("pdc-self-owner-change-blocked"));
594+
return;
595+
}
596+
492597
LinkedHashMap<String, ContainerPdcLockManager.PermissionAccess> preview = new LinkedHashMap<>(data.permissions());
493598
if (mutation.access() == ContainerPdcLockManager.PermissionAccess.NONE) {
494599
preview.remove(mutation.subject());
@@ -606,8 +711,7 @@ private void sendGroupInfo(Player player, PermissionGroupStore.GroupSnapshot gro
606711
Utils.sendMessages(player, Config.getLang("group-info-header"));
607712
player.sendMessage(ChatColor.GOLD + " - Name: " + ChatColor.RESET + group.name());
608713
UUID ownerId = group.owner();
609-
Player online = Bukkit.getPlayer(ownerId);
610-
String ownerText = online == null ? ownerId.toString() : online.getName() + "#" + ownerId;
714+
String ownerText = ContainerPdcLockManager.describeSubject(ownerId.toString());
611715
player.sendMessage(ChatColor.GOLD + " - Owner: " + ChatColor.RESET + ownerText);
612716
if (group.nodes().isEmpty()) {
613717
player.sendMessage(ChatColor.GOLD + " - Nodes: " + ChatColor.RESET + ChatColor.GRAY + "(none)" + ChatColor.RESET);

src/main/resources/lang.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
command-usage: "&6[LockettePro] &bLockettePro plugin help\n&c1. Sign lock edit: right click a sign, then /lock <line> <text>.\n&c2. PDC lock: /lock on, /lock info, /lock rename <name>, /lock permission <node>, /lock clone.\n&c3. Permission group: /lock group <create|delete|add|remove|info|list> ...\n&c4. Reload configuration: /lock reload.\nLockettePro by connection_lost"
1+
command-usage: "&6[LockettePro] &bLockettePro plugin help\n&c1. Sign lock edit: right click a sign, then /lock <line> <text>.\n&c2. PDC lock: /lock on, /lock off, /lock info, /lock rename <name>, /lock permission <node>, /lock clone.\n&c3. Permission group: /lock group <create|delete|add|remove|info|list> ...\n&c4. Reload configuration: /lock reload.\nLockettePro by connection_lost"
22
you-can-quick-lock-it: '&6[LockettePro] &aRight click the block with a sign to lock.'
33
you-can-manual-lock-it: '&6[LockettePro] &aPlace a sign and write [Private] to lock it.'
44
config-reloaded: '&6[LockettePro] &aConfig reloaded.'
@@ -31,8 +31,10 @@ cannot-interfere-with-others: '&6[LockettePro] &cYou cannot place a block that m
3131
sign-error: '&4[ERROR]'
3232
pdc-target-container-needed: '&6[LockettePro] &cLook at a container first.'
3333
pdc-lock-enabled: '&6[LockettePro] &aContainer PDC lock enabled.'
34+
pdc-lock-disabled: '&6[LockettePro] &aContainer PDC lock disabled.'
3435
pdc-lock-already-enabled: '&6[LockettePro] &eContainer is already PDC-locked.'
3536
pdc-lock-failed: '&6[LockettePro] &cFailed to update container lock.'
37+
pdc-lock-disable-failed: '&6[LockettePro] &cFailed to disable container lock.'
3638
pdc-no-owner-permission: '&6[LockettePro] &cYou are not an owner of this container.'
3739
pdc-not-locked: '&6[LockettePro] &cThis container is not PDC-locked.'
3840
pdc-info-header: '&6[LockettePro] &aContainer PDC lock info:'
@@ -42,6 +44,7 @@ pdc-rename-failed: '&6[LockettePro] &cFailed to rename this container.'
4244
pdc-permission-usage: '&6[LockettePro] &cUsage: /lock permission <xx|rw|ro|-->:<subject>'
4345
pdc-permission-invalid: '&6[LockettePro] &cInvalid permission node.'
4446
pdc-permission-updated: '&6[LockettePro] &aContainer permission updated.'
47+
pdc-self-owner-change-blocked: '&6[LockettePro] &cYou cannot remove or downgrade your own owner permission. Use /lock off instead.'
4548
pdc-clone-given: '&6[LockettePro] &aPermission clone item added to your inventory.'
4649
pdc-clone-applied: '&6[LockettePro] &aPermissions cloned to this container.'
4750
pdc-clone-failed: '&6[LockettePro] &cFailed to clone permissions.'

src/main/resources/lang_es.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@ cannot-interfere-with-others: '&6[Private] &c?No puedes poner un cartel interfir
3232
sign-error: '&4[ERROR]'
3333
pdc-target-container-needed: '&6[LockettePro] &cLook at a container first.'
3434
pdc-lock-enabled: '&6[LockettePro] &aContainer PDC lock enabled.'
35+
pdc-lock-disabled: '&6[LockettePro] &aContainer PDC lock disabled.'
3536
pdc-lock-already-enabled: '&6[LockettePro] &eContainer is already PDC-locked.'
3637
pdc-lock-failed: '&6[LockettePro] &cFailed to update container lock.'
38+
pdc-lock-disable-failed: '&6[LockettePro] &cFailed to disable container lock.'
3739
pdc-no-owner-permission: '&6[LockettePro] &cYou are not an owner of this container.'
3840
pdc-not-locked: '&6[LockettePro] &cThis container is not PDC-locked.'
3941
pdc-info-header: '&6[LockettePro] &aContainer PDC lock info:'
@@ -43,6 +45,7 @@ pdc-rename-failed: '&6[LockettePro] &cFailed to rename this container.'
4345
pdc-permission-usage: '&6[LockettePro] &cUsage: /lock permission <xx|rw|ro|-->:<subject>'
4446
pdc-permission-invalid: '&6[LockettePro] &cInvalid permission node.'
4547
pdc-permission-updated: '&6[LockettePro] &aContainer permission updated.'
48+
pdc-self-owner-change-blocked: '&6[LockettePro] &cYou cannot remove or downgrade your own owner permission. Use /lock off instead.'
4649
pdc-clone-given: '&6[LockettePro] &aPermission clone item added to your inventory.'
4750
pdc-clone-applied: '&6[LockettePro] &aPermissions cloned to this container.'
4851
pdc-clone-failed: '&6[LockettePro] &cFailed to clone permissions.'

src/main/resources/lang_it.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@ cannot-interfere-with-others: '&6[LockettePro] &cNon puoi piazzare un blocco che
3232
sign-error: '&4[ERRORE]'
3333
pdc-target-container-needed: '&6[LockettePro] &cLook at a container first.'
3434
pdc-lock-enabled: '&6[LockettePro] &aContainer PDC lock enabled.'
35+
pdc-lock-disabled: '&6[LockettePro] &aContainer PDC lock disabled.'
3536
pdc-lock-already-enabled: '&6[LockettePro] &eContainer is already PDC-locked.'
3637
pdc-lock-failed: '&6[LockettePro] &cFailed to update container lock.'
38+
pdc-lock-disable-failed: '&6[LockettePro] &cFailed to disable container lock.'
3739
pdc-no-owner-permission: '&6[LockettePro] &cYou are not an owner of this container.'
3840
pdc-not-locked: '&6[LockettePro] &cThis container is not PDC-locked.'
3941
pdc-info-header: '&6[LockettePro] &aContainer PDC lock info:'
@@ -43,6 +45,7 @@ pdc-rename-failed: '&6[LockettePro] &cFailed to rename this container.'
4345
pdc-permission-usage: '&6[LockettePro] &cUsage: /lock permission <xx|rw|ro|-->:<subject>'
4446
pdc-permission-invalid: '&6[LockettePro] &cInvalid permission node.'
4547
pdc-permission-updated: '&6[LockettePro] &aContainer permission updated.'
48+
pdc-self-owner-change-blocked: '&6[LockettePro] &cYou cannot remove or downgrade your own owner permission. Use /lock off instead.'
4649
pdc-clone-given: '&6[LockettePro] &aPermission clone item added to your inventory.'
4750
pdc-clone-applied: '&6[LockettePro] &aPermissions cloned to this container.'
4851
pdc-clone-failed: '&6[LockettePro] &cFailed to clone permissions.'

0 commit comments

Comments
 (0)