diff --git a/CHANGELOG.md b/CHANGELOG.md index acf1624..4badf34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Added independent control of initial connections on newly placed fences, panes/bars, walls, stairs, fence gates, and chests. - Kept placement-time connections separate from neighbor `block-updates`, with per-material overrides for both sides of a placement. - Added a GitHub Actions workflow that tests both Spigot and Paper API profiles and uploads the shaded JAR. +- Added `openphysicscontrol.set.current-world` for delegating rule changes only in a player's current world, across commands and the GUI. ## 2.7.0 - 2026-07-30 diff --git a/README.md b/README.md index 7303d85..a8d08ad 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,15 @@ OpenPhysicsControl 是從零撰寫、採 MIT 授權的 Bukkit 世界物理控制 /opc reload ``` +### 權限 + +- `openphysicscontrol.menu`:開啟玩家當下世界的物理規則選單。 +- `openphysicscontrol.set`:透過指令或選單修改任何已載入世界。 +- `openphysicscontrol.set.current-world`:只允許玩家透過指令或選單修改當下所在世界;即使在指令中指定其他世界,或開啟選單後移動到其他世界,也不會放行。 +- `openphysicscontrol.reload`:重新載入設定與世界規則。 + +OP 仍可修改所有世界。若玩家同時擁有 `openphysicscontrol.set` 與 `openphysicscontrol.set.current-world`,範圍較大的 `openphysicscontrol.set` 優先。 + 指令狀態以規則行為為準:大多數規則的 `on` 代表物理正常運作、`off` 代表停止;`block-hit-projectile-removal` 則是相容舊版的可選清理,`on` 時會移除命中方塊的箭矢與三叉戟,預設 `off` 以保留原版箭矢留存。 `oxygen-depletion` 的 `off` 只會阻止水中氧氣減少,離水後仍會恢復氧氣;`drowning-damage` 是獨立規則。`fire-damage` 不會攔截仙人掌、甜莓叢或尖滴水石等一般接觸傷害,這些不屬於火焰與高溫控制範圍。 diff --git a/src/main/java/dev/openphysicscontrol/OpenPhysicsControlPlugin.java b/src/main/java/dev/openphysicscontrol/OpenPhysicsControlPlugin.java index f666efc..9574e40 100644 --- a/src/main/java/dev/openphysicscontrol/OpenPhysicsControlPlugin.java +++ b/src/main/java/dev/openphysicscontrol/OpenPhysicsControlPlugin.java @@ -19,6 +19,9 @@ import java.util.Map; public final class OpenPhysicsControlPlugin extends JavaPlugin implements Listener { + static final String SET_PERMISSION = "openphysicscontrol.set"; + static final String SET_CURRENT_WORLD_PERMISSION = "openphysicscontrol.set.current-world"; + private LocaleService locales; private Messages messages; private RuleStore rules; @@ -69,7 +72,7 @@ private boolean openMenu(CommandSender sender) { } private boolean setRule(CommandSender sender, String[] args) { - if (!sender.isOp() && !sender.hasPermission("openphysicscontrol.set")) { + if (!hasAnyWorldEditPermission(sender)) { this.messages.send(sender, "no-permission"); return true; } @@ -104,6 +107,7 @@ private boolean setRule(CommandSender sender, String[] args) { this.messages.send(sender, "world-not-found", Map.of("world", args.length >= 4 ? args[3] : "?")); return true; } + if (!ensureCanEditWorld(sender, world)) return true; boolean enabled = this.rules.set(world, rule, requested); this.messages.send(sender, "rule-changed", Map.of( "rule", this.messages.plain(sender, rule.messageKey()), @@ -114,7 +118,7 @@ private boolean setRule(CommandSender sender, String[] args) { } private boolean setMaterial(CommandSender sender, String[] args) { - if (!sender.isOp() && !sender.hasPermission("openphysicscontrol.set")) { + if (!hasAnyWorldEditPermission(sender)) { this.messages.send(sender, "no-permission"); return true; } @@ -143,6 +147,7 @@ private boolean setMaterial(CommandSender sender, String[] args) { this.messages.send(sender, "world-not-found", Map.of("world", args.length >= 5 ? args[4] : "?")); return true; } + if (!ensureCanEditWorld(sender, world)) return true; String state = args[3].toLowerCase(Locale.ROOT); if (state.equals("clear") || state.equals("reset")) { this.rules.clearMaterial(world, rule, material); @@ -217,7 +222,7 @@ public List onTabComplete(CommandSender sender, Command command, String } else if (args.length == 3 && args[0].equalsIgnoreCase("set")) { options.addAll(List.of("on", "off", "toggle")); } else if (args.length == 4 && args[0].equalsIgnoreCase("set")) { - for (World world : this.getServer().getWorlds()) options.add(world.getName()); + addEditableWorlds(sender, options); } else if (args.length == 2 && (args[0].equalsIgnoreCase("material") || args[0].equalsIgnoreCase("materials"))) { for (Rule rule : Rule.values()) options.add(rule.key()); } else if (args.length == 3 && (args[0].equalsIgnoreCase("material") || args[0].equalsIgnoreCase("materials"))) { @@ -227,7 +232,7 @@ public List onTabComplete(CommandSender sender, Command command, String } else if (args.length == 4 && (args[0].equalsIgnoreCase("material") || args[0].equalsIgnoreCase("materials"))) { options.addAll(List.of("on", "off", "toggle", "clear")); } else if (args.length == 5 && (args[0].equalsIgnoreCase("material") || args[0].equalsIgnoreCase("materials"))) { - for (World world : this.getServer().getWorlds()) options.add(world.getName()); + addEditableWorlds(sender, options); } else if (args.length == 2 && (args[0].equalsIgnoreCase("language") || args[0].equalsIgnoreCase("lang"))) { options.add("auto"); options.addAll(this.locales.available()); @@ -237,6 +242,39 @@ public List onTabComplete(CommandSender sender, Command command, String return options; } + boolean ensureCanEditWorld(CommandSender sender, World world) { + if (canEditWorld(sender, world)) return true; + String message = sender instanceof Player && sender.hasPermission(SET_CURRENT_WORLD_PERMISSION) + ? "current-world-only" + : "no-permission"; + this.messages.send(sender, message); + return false; + } + + private boolean canEditWorld(CommandSender sender, World world) { + boolean playerWithCurrentWorldPermission = sender instanceof Player player + && player.hasPermission(SET_CURRENT_WORLD_PERMISSION); + boolean currentWorld = sender instanceof Player player && player.getWorld() == world; + return grantsWorldEdit(sender.isOp(), sender.hasPermission(SET_PERMISSION), + playerWithCurrentWorldPermission, currentWorld); + } + + private boolean hasAnyWorldEditPermission(CommandSender sender) { + return sender.isOp() || sender.hasPermission(SET_PERMISSION) + || sender instanceof Player && sender.hasPermission(SET_CURRENT_WORLD_PERMISSION); + } + + private void addEditableWorlds(CommandSender sender, List options) { + for (World world : this.getServer().getWorlds()) { + if (canEditWorld(sender, world)) options.add(world.getName()); + } + } + + static boolean grantsWorldEdit(boolean operator, boolean allWorldsPermission, + boolean currentWorldPermission, boolean currentWorld) { + return operator || allWorldsPermission || (currentWorldPermission && currentWorld); + } + @EventHandler(ignoreCancelled = true) public void inventoryClick(InventoryClickEvent event) { if (!(event.getInventory().getHolder() instanceof RulesMenu menu)) return; diff --git a/src/main/java/dev/openphysicscontrol/RulesMenu.java b/src/main/java/dev/openphysicscontrol/RulesMenu.java index 8ac15d3..cbb819f 100644 --- a/src/main/java/dev/openphysicscontrol/RulesMenu.java +++ b/src/main/java/dev/openphysicscontrol/RulesMenu.java @@ -79,10 +79,7 @@ public void click(int rawSlot) { Rule rule = this.ruleSlots.get(rawSlot); if (rule == null) return; - if (!this.viewer.isOp() && !this.viewer.hasPermission("openphysicscontrol.set")) { - this.plugin.messages().send(this.viewer, "no-permission"); - return; - } + if (!this.plugin.ensureCanEditWorld(this.viewer, this.world)) return; boolean enabled = this.plugin.rules().set(this.world, rule, null); refreshRule(rawSlot, rule); this.plugin.messages().send(this.viewer, "rule-changed", Map.of( diff --git a/src/main/resources/lang/en.yml b/src/main/resources/lang/en.yml index c0c5294..e8ba18b 100644 --- a/src/main/resources/lang/en.yml +++ b/src/main/resources/lang/en.yml @@ -1,5 +1,6 @@ prefix: '[OpenPhysicsControl]' no-permission: ' You do not have permission.' +current-world-only: ' You may only change physics in your current world.' players-only: ' This command is only available to players.' world-not-found: ' World was not found.' rule-not-found: ' Unknown rule .' diff --git a/src/main/resources/lang/zh_tw.yml b/src/main/resources/lang/zh_tw.yml index 6c68fdf..c5963a1 100644 --- a/src/main/resources/lang/zh_tw.yml +++ b/src/main/resources/lang/zh_tw.yml @@ -1,5 +1,6 @@ prefix: '[OpenPhysicsControl]' no-permission: ' 你沒有權限。' +current-world-only: ' 你只能修改目前所在世界的物理規則。' players-only: ' 此指令只能由玩家使用。' world-not-found: ' 找不到世界 。' rule-not-found: ' 未知規則 。' diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 01cb971..411efd8 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -12,8 +12,14 @@ commands: usage: '/ [set|material|language|reload]' permissions: openphysicscontrol.menu: + description: 'Open the physics rules menu for the current world' default: op openphysicscontrol.set: + description: 'Change physics rules in any loaded world' default: op + openphysicscontrol.set.current-world: + description: "Change physics rules only in the player's current world" + default: false openphysicscontrol.reload: + description: 'Reload OpenPhysicsControl configuration and world rules' default: op diff --git a/src/test/java/dev/openphysicscontrol/ResourceTest.java b/src/test/java/dev/openphysicscontrol/ResourceTest.java index b4ee359..22a8523 100644 --- a/src/test/java/dev/openphysicscontrol/ResourceTest.java +++ b/src/test/java/dev/openphysicscontrol/ResourceTest.java @@ -29,6 +29,11 @@ void descriptorKeepsRequestedCompatibility() { Map commands = (Map) plugin.get("commands"); Map command = (Map) commands.get("openphysics"); assertEquals(List.of("ophysics", "opc", "pc"), command.get("aliases")); + Map permissions = (Map) plugin.get("permissions"); + assertTrue(permissions.containsKey(OpenPhysicsControlPlugin.SET_PERMISSION)); + Map currentWorld = (Map) permissions.get( + OpenPhysicsControlPlugin.SET_CURRENT_WORLD_PERMISSION); + assertEquals(Boolean.FALSE, currentWorld.get("default")); } @Test diff --git a/src/test/java/dev/openphysicscontrol/WorldEditPermissionTest.java b/src/test/java/dev/openphysicscontrol/WorldEditPermissionTest.java new file mode 100644 index 0000000..99656c9 --- /dev/null +++ b/src/test/java/dev/openphysicscontrol/WorldEditPermissionTest.java @@ -0,0 +1,21 @@ +package dev.openphysicscontrol; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class WorldEditPermissionTest { + @Test + void operatorsAndAllWorldPermissionAlwaysGrantWorldEdits() { + assertTrue(OpenPhysicsControlPlugin.grantsWorldEdit(true, false, false, false)); + assertTrue(OpenPhysicsControlPlugin.grantsWorldEdit(false, true, false, false)); + } + + @Test + void currentWorldPermissionOnlyGrantsThePlayersCurrentWorld() { + assertTrue(OpenPhysicsControlPlugin.grantsWorldEdit(false, false, true, true)); + assertFalse(OpenPhysicsControlPlugin.grantsWorldEdit(false, false, true, false)); + assertFalse(OpenPhysicsControlPlugin.grantsWorldEdit(false, false, false, true)); + } +}