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
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,5 @@ jobs:
- run: ./gradlew build
- uses: actions/upload-artifact@v4
with:
name: HouseRules
path: build/libs/HouseRules-*.jar
name: Mayor
path: build/libs/Mayor-*.jar
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@ jobs:
GH_TOKEN: ${{ github.token }}
run: |
notes=$(awk -v v="${GITHUB_REF_NAME#v}" '$0 ~ "^## " v {f=1; next} /^## /{f=0} f' CHANGELOG.md)
gh release create "$GITHUB_REF_NAME" build/libs/HouseRules-*.jar --title "HouseRules ${GITHUB_REF_NAME#v}" --notes "$notes" --prerelease
gh release create "$GITHUB_REF_NAME" build/libs/Mayor-*.jar --title "Mayor ${GITHUB_REF_NAME#v}" --notes "$notes" --prerelease
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 0.5.0

- **A new name: Mayor** (D198). The mayor sets how folks in town talk to each other. How it works is unchanged. As the owner asked (D196), no old names are kept: the package, plugin, jar, commands, permissions and data folder all use Mayor.
- `/hud share` filtering follows BlueMapMini, the map plugin's new name (D194). Mayor hooks only BlueMapMini's share event; the hooks for its earlier names are gone.
- Install it together with BlueMapMini 0.3.0, BlueMapAuth 0.2.0 and OneVoid, which find it by its new name. BlueMapMini keeps sharing off until Mayor 0.5.0 is installed, so shares can never skip chat circles.
- To install over the previous version, with the server stopped: rename its plugin folder to `plugins/Mayor`, move its LuckPerms permissions to the matching `mayor.*` nodes (circles, bypass and filter bypass), then swap the jar.
- Tested: unit tests pass, and the live share check uses a stand-in named BlueMapMini. The real BlueMapMini 0.3.0 and Mayor 0.5.0 haven't been run together yet.

## 0.4.0

- `ReachService.sharesCircle(viewerHas, to)`: for a viewer who isn't in game, such as someone on Mapkeeper's web map, only a shared circle counts (owner decision D54, 2026-11-10). World, world group and bypass don't, and with no circles set up nobody is reached.
Expand Down
50 changes: 25 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# HouseRules
# Mayor

Chat rules for family and community Paper servers. Players hear people in the same world, plus anyone who shares a "circle" with them, so groups such as families or age groups keep their own conversations across every world.

**Status:** pre-release. It passes its acceptance tests on throwaway Paper 26.2 and 26.3 servers, using protocol bots with LuckPerms: chat and private messages for every pair of players, replies, blocked commands, the word filter and its bypass, books, `/circles`, reloads, a broken config, and 20 bots chatting at once. BlueMinimap's share was tested with a stand-in plugin here, and with the real BlueMinimap 0.2.0 and HouseRules 0.2.0 jars together on Paper 26.1.2, 26.2 and 26.3. Signs and anvil renames weren't tested. It hasn't been seen in a real game client yet, so the wording and colours haven't been checked by a person. See [the live tests](test/live/README.md) and the [test audit](docs/test-audit.md).
**Status:** pre-release. It passes its acceptance tests on throwaway Paper 26.2 and 26.3 servers, using protocol bots with LuckPerms: chat and private messages for every pair of players, replies, blocked commands, the word filter and its bypass, books, `/circles`, reloads, a broken config, and 20 bots chatting at once. BlueMapMini's share was tested with a stand-in plugin here; the real BlueMapMini 0.3.0 and Mayor 0.5.0 haven't been run together yet. Signs and anvil renames weren't tested. It hasn't been seen in a real game client yet, so the wording and colours haven't been checked by a person. See [the live tests](test/live/README.md) and the [test audit](docs/test-audit.md).

- Requires Paper 26.1 or newer and Java 25. Paper 1.21.11 and older aren't supported.
- With no circles set up it changes nothing about who hears whom, so it's safe to install first and configure after.

## What it does

**Circles.** A circle is a permission, `houserules.circle.<name>`, and the circle names are listed in `config.yml`. A player can be in several. Two players can hear each other if:
**Circles.** A circle is a permission, `mayor.circle.<name>`, and the circle names are listed in `config.yml`. A player can be in several. Two players can hear each other if:

1. they're in the same world group, unless it's a circle-only world, or
2. they share a circle.
Expand All @@ -24,53 +24,53 @@ Otherwise they can't. A world group is a world with its Nether and End (`surviva
- Private messages (`/msg`, `/tell`, `/w` and the others in `private-message-commands`) to someone out of reach aren't sent, and you're told who you can talk to instead.
- `/r` and `/reply` are checked again when you reply, because the other player may have moved.
- Commands that would reach the whole server, `/me` and `/teammsg` by default, are off while circles are in use.
- BlueMinimap's `/hud share`, if BlueMinimap is installed, goes only to players who could hear you in chat. BlueMinimap was called BlueMapHUD up to 0.1.0; HouseRules 0.2.x still filters shares from BlueMapHUD 0.1.x, and that goes in the next release. Update the two together: BlueMinimap turns sharing off if HouseRules is older than 0.2.
- BlueMapMini's `/hud share`, if BlueMapMini is installed, goes only to players who could hear you in chat. BlueMapMini keeps sharing off until Mayor is 0.5.0 or newer.

**Blocked commands.** Any command can be turned off, each with its own message. `sort` also blocks `anyplugin:sort`, in any letter case.

**Word filter.** `filter.yml` lists words and phrases to keep out of chat, private messages, signs, books and item names. It matches whole words, so "class" and "Scunthorpe" are fine, and it sees through `sh1t`, `$` for s and stretched letters. Spaced-out spellings get through. Blocked text is never logged or stored: the player is told kindly and can try again. Players with `houserules.filter.bypass`, such as an adults-only group, aren't filtered anywhere.
**Word filter.** `filter.yml` lists words and phrases to keep out of chat, private messages, signs, books and item names. It matches whole words, so "class" and "Scunthorpe" are fine, and it sees through `sh1t`, `$` for s and stretched letters. Spaced-out spellings get through. Blocked text is never logged or stored: the player is told kindly and can try again. Players with `mayor.filter.bypass`, such as an adults-only group, aren't filtered anywhere.

**Nobody heard you.** If you chat and nobody online can hear you, you get a light-hearted note, such as "A squid nods politely. It didn't understand a word." There are 75 in `quips.yml`, and you see them all before any comes round again. At most one every 3 minutes.

Every message players read is in `config.yml`, in [MiniMessage](https://docs.advntr.dev/minimessage/format.html). The defaults are short and kind, say what to do instead, and never rely on colour alone.

## Install

1. Put `HouseRules-<version>.jar` in `plugins/` and start the server.
2. List your circles in `plugins/HouseRules/config.yml`, and any circle-only worlds.
3. Give each player their circle permission with your permission plugin, for example with LuckPerms: `/lp group family permission set houserules.circle.family`.
4. Run `/houserules reload`.
1. Put `Mayor-<version>.jar` in `plugins/` and start the server.
2. List your circles in `plugins/Mayor/config.yml`, and any circle-only worlds.
3. Give each player their circle permission with your permission plugin, for example with LuckPerms: `/lp group family permission set mayor.circle.family`.
4. Run `/mayor reload`.

Permission changes take effect straight away, with no relog.

If the config can't be read when the server starts, there are no rules to follow, so chat and private messages are paused, and admins are told when they join. Fix the file and run `/houserules reload`. A reload with mistakes keeps the rules that were working, and says what's wrong.
If the config can't be read when the server starts, there are no rules to follow, so chat and private messages are paused, and admins are told when they join. Fix the file and run `/mayor reload`. A reload with mistakes keeps the rules that were working, and says what's wrong.

## Commands

| Command | What it does |
| --- | --- |
| `/circles` | Shows your circles and who online can hear you right now. |
| `/houserules reload` | Reads `config.yml`, `filter.yml` and `quips.yml` again. |
| `/houserules status` | Shows the circles, whether the filter is on, and any config warnings. |
| `/houserules check <player> <player>` | Says whether the first can reach the second, and why. |
| `/houserules describe` | Lists these admin commands. |
| `/mayor reload` | Reads `config.yml`, `filter.yml` and `quips.yml` again. |
| `/mayor status` | Shows the circles, whether the filter is on, and any config warnings. |
| `/mayor check <player> <player>` | Says whether the first can reach the second, and why. |
| `/mayor describe` | Lists these admin commands. |

Every `/houserules` command also takes `--json <id>` at the end, for scripts and the Leaf Juice app. It then answers with one `ea-result <id> {...}` line in the shared [Signalbox](https://github.com/ExtantApe/Signalbox) format: `status` gives the circles, the filter and any problems; `reload` whether the new rules were applied; `check` both players with their UUIDs, `reaches` and a reason code such as `shared-circle`; `describe` the commands. Players are named or given by UUID; selectors like `@p` aren't accepted.
Every `/mayor` command also takes `--json <id>` at the end, for scripts and the Leaf Juice app. It then answers with one `ea-result <id> {...}` line in the shared [Signalbox](https://github.com/ExtantApe/Signalbox) format: `status` gives the circles, the filter and any problems; `reload` whether the new rules were applied; `check` both players with their UUIDs, `reaches` and a reason code such as `shared-circle`; `describe` the commands. Players are named or given by UUID; selectors like `@p` aren't accepted.

## Permissions

| Permission | Default | Allows |
| --- | --- | --- |
| `houserules.circle.<name>` | nobody | Being in that circle. |
| `houserules.circles` | everyone | `/circles`. |
| `houserules.circles.names` | everyone | Names in `/circles`. Without it, only a count. |
| `houserules.bypass` | nobody, ops included | Reaching and being reached by everyone, for moderators. It doesn't show anyone else's chat. |
| `houserules.filter.bypass` | nobody, ops included | Skipping the word filter everywhere it applies, for adult groups. It doesn't change who hears whom. |
| `houserules.admin` | ops | `/houserules`, and config warnings on joining. |
| `mayor.circle.<name>` | nobody | Being in that circle. |
| `mayor.circles` | everyone | `/circles`. |
| `mayor.circles.names` | everyone | Names in `/circles`. Without it, only a count. |
| `mayor.bypass` | nobody, ops included | Reaching and being reached by everyone, for moderators. It doesn't show anyone else's chat. |
| `mayor.filter.bypass` | nobody, ops included | Skipping the word filter everywhere it applies, for adult groups. It doesn't change who hears whom. |
| `mayor.admin` | ops | `/mayor`, and config warnings on joining. |

## For other plugins

HouseRules registers a `ReachService` with Bukkit's services manager:
Mayor registers a `ReachService` with Bukkit's services manager:

```java
ReachService rules = Bukkit.getServicesManager().load(ReachService.class);
Expand All @@ -83,17 +83,17 @@ if (rules == null || rules.mayReach(from, to)) { /* send it */ }

## 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.
- Signs are readable by anyone who walks past, and a book or named item can be carried to another world. Mayor only checks them when they're written.
- Chat plugins that send messages their own way, rather than through Paper's chat event, aren't covered. Add their message commands to `private-message-commands`.
- Only the commands listed in `private-message-commands` and `reply-commands` are checked. `/r` is checked against the last conversation HouseRules saw.
- Only the commands listed in `private-message-commands` and `reply-commands` are checked. `/r` is checked against the last conversation Mayor saw.

## Building

```bash
./gradlew build
```

The jar is `build/libs/HouseRules-<version>.jar`. Live tests with bots are in `test/live`.
The jar is `build/libs/Mayor-<version>.jar`. Live tests with bots are in `test/live`.

## License

Expand Down
10 changes: 5 additions & 5 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ plugins {
}

group = "com.extantape"
version = "0.4.0"
version = "0.5.0"

java {
toolchain.languageVersion.set(JavaLanguageVersion.of(25))
Expand All @@ -23,8 +23,8 @@ extra["paperApi"] = paperApi

dependencies {
compileOnly(paperApi)
// BlueMapHUD's share event, for the optional hook. Supplied at runtime by BlueMapHUD itself.
compileOnly(project(":bluemaphud-stub"))
// BlueMapMini's share event, for the optional hook. Supplied at runtime by BlueMapMini itself.
compileOnly(project(":bluemapmini-stub"))
// The shared command format (--json, describe), shaded in below.
implementation("com.github.ExtantApe:Signalbox:v0.3.1")

Expand Down Expand Up @@ -56,10 +56,10 @@ tasks.test {
// Signalbox goes inside the jar, relocated, so it can never clash with another plugin's copy. The shaded jar is the
// only jar built.
tasks.shadowJar {
archiveBaseName.set("HouseRules")
archiveBaseName.set("Mayor")
archiveClassifier.set("")
from("LICENSE")
relocate("com.extantape.signalbox", "com.extantape.houserules.libs.signalbox")
relocate("com.extantape.signalbox", "com.extantape.mayor.libs.signalbox")
}
tasks.jar { enabled = false }
tasks.assemble { dependsOn(tasks.shadowJar) }
5 changes: 5 additions & 0 deletions docs/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,8 @@ Owner decisions for HouseRules, newest first.
| 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.

## 2026-11-14

- **Map plugin's new name (owner, D194, D196):** the share hook follows BlueMapMini only. No hooks or stand-ins for its earlier names are kept.
- **Name (owner, D198):** the plugin is renamed Mayor. The owner chose "Mayor (Recommended)". Released as 0.5.0 with no old names kept, installed together with BlueMapMini 0.3.0.
10 changes: 5 additions & 5 deletions docs/test-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Kept by this repo's owning session (Minecraft: Plugins Lead), per `~/Code/CLAUDE

## 2026-11-10: the --json console

What's new: every `/houserules` command takes `--json <id>` (Signalbox format 1), and there's `/houserules describe`. Since the first audit, only CI changes had landed, so this covers the console alone.
What's new: every `/mayor` command takes `--json <id>` (Signalbox format 1), and there's `/mayor describe`. Since the first audit, only CI changes had landed, so this covers the console alone.

| Promise | Covered by |
| --- | --- |
Expand All @@ -14,7 +14,7 @@ What's new: every `/houserules` command takes `--json <id>` (Signalbox format 1)
| While chat is paused, `check` refuses rather than guess | unit `whilePausedCheckRefusesInsteadOfGuessing` |
| An offline player is said plainly | unit `anOfflinePlayerIsSaidPlainly`; live |
| `status` and `reload` report circles, filter, problems and whether a reload applied | unit; live `status` |
| A player without `houserules.admin` gets nothing, `--json` or not | live "a player without houserules.admin gets no answer line" |
| A player without `mayor.admin` gets nothing, `--json` or not | live "a player without mayor.admin gets no answer line" |
| Without `--json`, the commands read as before | the existing live checks (`check explains a shared circle`, the reload checks), unchanged |

Live suite: 67/67 on Paper 26.3 (2 skipped, as before). The test bots' two-letter names aren't valid Minecraft names, so the `--json` checks name them by UUID, which the format allows.
Expand All @@ -33,19 +33,19 @@ Each promise in the README, and the test that holds it to it. "Live" is `test/li
| Private messages out of reach aren't sent; told who instead; selectors refused | live private-message matrix (every command), "/msg with a selector is refused" |
| `/r` re-checked on reply | unit `replies`; live "Replies" |
| `/me` and `/teammsg` off while circles are in use; bypass keeps them | unit `broadcastCommandsOffWhileCirclesAreOn`; live "Blocked and broadcast commands" |
| BlueMinimap shares follow chat; old BlueMapHUD event for one release | live share section with `HR_SHARE_PLUGIN=new` and `old`; BlueMinimap's `test/live/houserules.js` with the real jars |
| BlueMapMini shares follow chat | live share section, with a stand-in BlueMapMini firing the share event; BlueMapMini's own guard keeps sharing off with a Mayor older than 0.5.0 |
| Blocked commands in every form, own message | unit `blockedCommandsInEveryForm`; live |
| Filter: whole words, disguises, allow list; Scunthorpe passes | unit `wholeWordsOnly`, `seesThroughDisguises`, `allowListWins`, `phrases`, `defaultList*`; live |
| **Blocked text is never logged or stored** | **new** live "filtered words never reach the server log" (chat and `/msg`) |
| Filter bypass, granted and removed without a relog; others still filtered | unit `filterBypass*`; live |
| `houserules.bypass` and `.filter.bypass` off for everyone, ops included | unit `bypassesAreOffForEveryoneOpsIncluded` |
| `mayor.bypass` and `.filter.bypass` off for everyone, ops included | unit `bypassesAreOffForEveryoneOpsIncluded` |
| Nobody-heard notes: all before any repeat, cooldown | unit `everyNoteBeforeAnyRepeatAndNeverTwiceInARow`, `cooldown`, `emptyIsQuiet`; live |
| Permission changes take effect without a relog | live "Circles granted and removed with LuckPerms, no relog" |
| Broken config at startup pauses chat and messages; console says why | unit `pausedBlocksMessagesButNotOtherCommands`; live |
| **Admins are told about a broken config when they join** | **new** live "an admin joining is told…", and "a player who isn't an admin isn't" |
| A broken reload keeps the working rules | unit `brokenYamlIsAnErrorWithNoSettings`; live |
| `/circles`, names only with permission | live `/circles` section |
| `/houserules check` explains | live |
| `/mayor check` explains | live |
| 20 players chatting at once stay quick | live "Load" (TPS, 95th percentile under 500 ms) |

**Gaps added this audit:** the two in bold. Both passed on 26.2 and 26.3 (61/61, 2 real-client skips).
Expand Down
11 changes: 5 additions & 6 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
rootProject.name = "HouseRules"
rootProject.name = "Mayor"

// Compile-only copies of BlueMinimap's share event, under its new and old (BlueMapHUD) names, so HouseRules builds without BlueMapHUD. Never shipped.
include("bluemaphud-stub")
project(":bluemaphud-stub").projectDir = file("test/stub")
// A compile-only copy of BlueMapMini's share event, so Mayor builds without BlueMapMini. Never shipped.
include("bluemapmini-stub")
project(":bluemapmini-stub").projectDir = file("test/stub")

// Test-only plugins for the live tests in test/live. Never shipped.
include("live-test-helper", "live-test-fakehud", "live-test-fakeminimap")
include("live-test-helper", "live-test-fakeminimap")
project(":live-test-helper").projectDir = file("test/live/helper")
project(":live-test-fakehud").projectDir = file("test/live/fakehud")
project(":live-test-fakeminimap").projectDir = file("test/live/fakeminimap")
Loading
Loading