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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ if (rules == null || rules.mayReach(from, to)) { /* send it */ }

`mayReach(from, to)` and `maySpeak(player)` follow exactly the same rules as chat, and are safe from any thread.

`sharesCircle(viewerHas, to)` is for a viewer who isn't in game, such as someone on a web map. It's true only when they share a circle with `to`. World, world group and bypass don't count, and with no circles set up it's always false. The caller passes the viewer's permissions, read while they're offline, and anything it can't check should answer false.

## Limits

- Signs are readable by anyone who walks past, and a book or named item can be carried to another world. HouseRules only checks them when they're written.
Expand Down
1 change: 1 addition & 0 deletions docs/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ Owner decisions for HouseRules, newest first.
| 2026-11-07 | `/r` and `/reply` check reach again at the moment of the reply. | The other player may have moved to a world out of reach since the conversation started. |
| 2026-11-07 | No `/sort` block in the defaults. | Portmanteau, the planned ChestSort replacement, keeps automatic sorting on by itself, and none of its commands turn it off. |
| 2026-11-07 | `/hud share` from BlueMapHUD follows the same reach as chat, with no narrower rule for kids. | One rule for everything players send each other. |
| 2026-11-10 | A viewer who isn't in game, such as someone on the web map, sees only players who share a HouseRules circle with them (owner, D54). | Chat's world rule needs a world, and an away viewer has none. |
| 2026-11-07 | Every list is in config, including circles, circle-only worlds, world groups, the dimension suffixes, message and reply commands, blocked commands and every message. With no circles, chat is vanilla. | Generic and publishable, with no server's names in the code. |
- **BlueMinimap (2026-11-08, owner rename of BlueMapHUD):** 0.2.0 listens for `com.extantape.blueminimap.api.CoordinateShareEvent` when BlueMinimap is installed, and still for the old `com.extantape.bluemaphud.api` event when only BlueMapHUD 0.1.x is, for one release. Released together with BlueMinimap 0.2.0, which turns sharing off when HouseRules is older than 0.2, so no order of upgrades lets a share skip the circles.
11 changes: 11 additions & 0 deletions src/main/java/com/extantape/houserules/HouseRulesPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,15 @@ public boolean maySpeak(Player player) {
State s = state;
return s != null && s.settings().reach().maySpeak(Subject.of(player));
}

@Override
public boolean sharesCircle(java.util.function.Predicate<String> viewerHas, Player to) {
State s = state;
if (s == null || viewerHas == null || to == null) return false;
try {
return s.settings().reach().reachesWhileAway(viewerHas, Subject.of(to));
} catch (RuntimeException e) {
return false;
}
}
}
7 changes: 7 additions & 0 deletions src/main/java/com/extantape/houserules/api/ReachService.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,11 @@ public interface ReachService {

/** Whether the player may chat at all where they are. False in a circle-only world without a circle, or while chat is paused. */
boolean maySpeak(Player player);

/**
* Whether a viewer who isn't in game, such as someone on the web map, may see {@code to}: only if they share a
* circle (owner, 2026-11-10, D54). {@code viewerHas} answers the viewer's permissions, read while they're offline;
* it should answer false for anything it can't check. Safe from any thread.
*/
boolean sharesCircle(java.util.function.Predicate<String> viewerHas, Player to);
}
16 changes: 16 additions & 0 deletions src/main/java/com/extantape/houserules/rules/Reach.java
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,20 @@ public String sharedCircle(Subject from, Subject to) {
}
return null;
}

/**
* For a viewer who isn't in game, such as someone on the web map (owner, 2026-11-10, D54): only a shared circle
* counts. Worlds, world groups and bypass don't, because the viewer has no world. With no circles set up, nobody is
* reached: there's no circle to share.
*
* @param viewer what the viewer is allowed, read by the caller while they're offline
*/
public boolean reachesWhileAway(java.util.function.Predicate<String> viewer, Subject to) {
if (!active()) return false;
for (String circle : circles) {
String node = CIRCLE_PERMISSION + circle;
if (viewer.test(node) && to.has(node)) return true;
}
return false;
}
}
26 changes: 26 additions & 0 deletions src/test/java/com/extantape/houserules/rules/ReachTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,30 @@ void reachIsSymmetric() {
player("n1", "forest"), player("n2", "hall"), player("n3", "meadow"));
for (Subject a : all) for (Subject b : all) assertEquals(reach.mayReach(a, b), reach.mayReach(b, a), a.name() + "/" + b.name());
}

// --- a viewer who isn't in game, such as the web map (D54) ---

@Test
void awayViewersSeeOnlyTheirCircle() {
java.util.function.Predicate<String> kidInRed = circle("red")::equals;
assertTrue(reach.reachesWhileAway(kidInRed, player("mum", "forest", circle("red"))));
assertFalse(reach.reachesWhileAway(kidInRed, player("stranger", "forest")), "the same world doesn't count");
assertFalse(reach.reachesWhileAway(kidInRed, player("blue", "meadow", circle("blue"))));
assertFalse(reach.reachesWhileAway(kidInRed, player("admin", "forest", Reach.BYPASS)), "bypass doesn't count either");
}

@Test
void anAwayViewerWithNoCircleOrNoRulesSeesNobody() {
assertFalse(reach.reachesWhileAway(p -> false, player("mum", "forest", circle("red"))));
assertFalse(reach.reachesWhileAway(p -> true, player("stranger", "forest")));
Reach off = new Reach(List.of(), Set.of(), Map.of(), List.of());
assertFalse(off.reachesWhileAway(p -> true, player("anyone", "forest", circle("red"))), "no circles: nothing to share");
}

@Test
void anAwayViewersOwnPermissionsAreTheOnlyOnesAsked() {
java.util.List<String> asked = new java.util.ArrayList<>();
reach.reachesWhileAway(p -> { asked.add(p); return false; }, player("mum", "forest", circle("red"), circle("blue")));
assertTrue(asked.stream().allMatch(p -> p.startsWith(Reach.CIRCLE_PERMISSION)), asked.toString());
}
}
Loading