Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` 不會攔截仙人掌、甜莓叢或尖滴水石等一般接觸傷害,這些不屬於火焰與高溫控制範圍。
Expand Down
46 changes: 42 additions & 4 deletions src/main/java/dev/openphysicscontrol/OpenPhysicsControlPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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()),
Expand All @@ -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;
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -217,7 +222,7 @@ public List<String> 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"))) {
Expand All @@ -227,7 +232,7 @@ public List<String> 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());
Expand All @@ -237,6 +242,39 @@ public List<String> 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<String> 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;
Expand Down
5 changes: 1 addition & 4 deletions src/main/java/dev/openphysicscontrol/RulesMenu.java
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/lang/en.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
prefix: '<gold>[OpenPhysicsControl]</gold>'
no-permission: '<prefix> <red>You do not have permission.</red>'
current-world-only: '<prefix> <red>You may only change physics in your current world.</red>'
players-only: '<prefix> <red>This command is only available to players.</red>'
world-not-found: '<prefix> <red>World <world> was not found.</red>'
rule-not-found: '<prefix> <red>Unknown rule <rule>.</red>'
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/lang/zh_tw.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
prefix: '<gold>[OpenPhysicsControl]</gold>'
no-permission: '<prefix> <red>你沒有權限。</red>'
current-world-only: '<prefix> <red>你只能修改目前所在世界的物理規則。</red>'
players-only: '<prefix> <red>此指令只能由玩家使用。</red>'
world-not-found: '<prefix> <red>找不到世界 <world>。</red>'
rule-not-found: '<prefix> <red>未知規則 <rule>。</red>'
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,14 @@ commands:
usage: '/<command> [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
5 changes: 5 additions & 0 deletions src/test/java/dev/openphysicscontrol/ResourceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ void descriptorKeepsRequestedCompatibility() {
Map<String, Object> commands = (Map<String, Object>) plugin.get("commands");
Map<String, Object> command = (Map<String, Object>) commands.get("openphysics");
assertEquals(List.of("ophysics", "opc", "pc"), command.get("aliases"));
Map<String, Object> permissions = (Map<String, Object>) plugin.get("permissions");
assertTrue(permissions.containsKey(OpenPhysicsControlPlugin.SET_PERMISSION));
Map<String, Object> currentWorld = (Map<String, Object>) permissions.get(
OpenPhysicsControlPlugin.SET_CURRENT_WORLD_PERMISSION);
assertEquals(Boolean.FALSE, currentWorld.get("default"));
}

@Test
Expand Down
21 changes: 21 additions & 0 deletions src/test/java/dev/openphysicscontrol/WorldEditPermissionTest.java
Original file line number Diff line number Diff line change
@@ -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));
}
}