Skip to content

feat: PlayerNotifications delivery adapter - #26

Merged
md5sha256 merged 14 commits into
mainfrom
feat/pn-adapter
Aug 23, 2026
Merged

feat: PlayerNotifications delivery adapter#26
md5sha256 merged 14 commits into
mainfrom
feat/pn-adapter

Conversation

@md5sha256

Copy link
Copy Markdown
Collaborator

Summary

Adds player-notifications-adapter, a notification delivery module that forwards RealtyNotificationEvent to PlayerNotifications as registered dataTypes, so players can toggle Realty notification categories in /notifications preferences.

What's in it

  • New adapter module under realty-paper-adapters/player-notifications-adapter, compiled against player-notifications-api 1.0.1, published as its own jar like the chat and EssentialsX adapters.
  • Operator-defined categories. The category set lives in plugins/Realty/modules/player-notifications-adapter/categories.yml — each entry carries its label, description, title, priority and the Realty message keys it claims. NotificationCategoryMapper.dataTypes() is the single source; no category list is hardcoded, so categories can be added or re-split without a rebuild.
  • Reference copies for every operator config. Each operator-editable config now also writes an untouched defaults/default-<name>.yml on every start, so an upgrading operator can see what a new version added. Applies to Realty core (copyResourceTemplate) and the adapter (CategoriesConfig.writeReferenceCopy).
  • Self-healing pre-1.4.2 categories.yml. That format never actually worked (Bukkit split the dotted keys at load time), so a stale file is replaced with the current default and kept as categories.yml.pre-1.4.2.bak rather than converted. CategoriesConfig.load sets the path separator to NUL before loading.
  • EssentialsX mail is now gated behind notifications-enabled in the essentials adapter's config.yml; the teleport-safety SafeBlockPredicate is a correctness fix and still applies regardless.
  • No bundled modules. BundledModuleExtractor is gone — the plugin jar ships no modules and startModules() warns when no delivery module is installed. runServer still stages all three as a dev convenience.

Notable detail

The module tears down using the mapper it registered with, never a freshly-parsed one — otherwise a category deleted between reloads would be orphaned in PlayerNotifications' registry.

Testing

New test coverage for the config parse/migration path, the category mapper, the event listener, the payload, the reference copy, and the registration lifecycle.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SUskvuegwBzQMufaYxp5eM

md5sha256 and others added 10 commits August 23, 2026 00:27
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018VWZAdCQBEFBP9TtVpDwtx
Adds a pn-adapter module that routes Realty notifications into the
PlayerNotifications plugin, giving players per-category preferences,
sink fan-out, offline delivery and an inbox.

RealtyNotificationEvent gains a messageKey field carrying the
messages.yml path the fire site rendered from. The key was already in
scope at all 33 fire sites as the first argument to messageFor(); the
event was simply discarding it. It is an identity for routing, never
rendered, and consumers must tolerate unknown keys.

The adapter maps those keys to five PN dataTypes (realty.auction,
.offer, .lease, .agent, .general) via a configurable categories.yml,
falling back to realty.general so an unmapped key is never dropped.
All five share one payload class, which makes PN's registry cascade a
partial unregister into the shared serializer and renderer; shutdown
therefore unregisters all five, and the hazard is asserted in
RegistrationLifecycleTest.

chat-adapter is no longer bundled in the plugin jar or extracted on
first enable -- operators install delivery modules themselves.
Existing servers keep the jar they already have, so behaviour is
unchanged on upgrade; new installs deliver nothing until a module is
installed, which the startup warning now states plainly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018VWZAdCQBEFBP9TtVpDwtx
The API publishes to maven.minecraftcitiesnetwork.com, which Realty did
not declare; add it and depend on the released 1.0.0 rather than the
locally-installed snapshot.

Rename pn-adapter to player-notifications-adapter, including its package
(adapter.pn -> adapter.playernotifications), so it reads like the module
it delivers to and matches how chat-adapter and essentials-adapter name
their packages after themselves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018VWZAdCQBEFBP9TtVpDwtx
processResources captured the project version into the expand() closure
but never declared it as a task input, so Gradle hashed only
paper-plugin.yml, found it unchanged across a bump, and reused the
previously-expanded output. Every release therefore shipped a jar whose
manifest announced the previous version -- a 1.4.1 jar reporting v1.4.0,
which is exactly what made a stale deployment impossible to spot in the
server log.

Declaring the version via inputs.property invalidates the task on a bump.
Applied to realty-paper-plan-extension too, which had the same pattern.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018VWZAdCQBEFBP9TtVpDwtx
1.0.1 carries the category change listener the plugin now subscribes to,
so a Realty registration arriving after PN's snapshot is built reaches
the preference dialogs instead of falling into uncategorized. The
adapter's own code is unchanged -- the fix is entirely host-side.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018VWZAdCQBEFBP9TtVpDwtx
…ries.yml

The adapter's categories.yml already carried the message-key routing, but the set
of PlayerNotifications data types was compiled in: DATA_TYPES listed five strings
and RealtyDataTypes held their labels and descriptions. An operator who routed a
key to a category of their own got a notification enqueued under a data type that
was never registered -- no renderer, no category claim. The labels players read in
/notifications preferences also came from the code, not from the `titles:` block,
so editing the file appeared to do nothing.

categories.yml now mirrors PlayerNotifications' own shape: each category owns its
label, description, title, priority and the keys it claims. NotificationCategoryMapper
derives the data types from it and is the only source; RealtyDataTypes loops over
mapper.dataTypes(). Adding or re-splitting a category needs no rebuild.

Two hazards the change introduces, both covered by tests: a key claimed by two
categories and a fallback-category that is not declared are rejected at load rather
than resolving unpredictably or enqueueing into an unregistered type; and teardown
uses the mapper the registrations were made with, never a freshly-parsed one, so a
category deleted between reloads is not orphaned in PN's registry.

Fixes a latent bug found while testing the parse: Bukkit splits configuration keys
on '.' as it loads, and every key in this file contains a dot. The previous flat
format therefore never parsed -- `notification.outbid: realty.auction` became a
nested section, getKeys(false) returned only "notification", and the routing map
came out empty, so every notification fell through to realty.general regardless of
what the file said. CategoriesConfig.load sets the path separator before loading.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018VWZAdCQBEFBP9TtVpDwtx
Realty core already wrote defaults/default-<name>.yml for four of its six configs;
database.yml and region-tags.yml had none, and player-notifications-adapter had none
at all. Since a live config is seeded once and then never rewritten -- correctly, it
holds the operator's edits -- an upgrade's new keys were invisible unless the operator
read the source.

Every operator-editable config now ships an untouched copy of the bundled default
beside it, rewritten on every start rather than only when absent: a copy left over
from an older version answers "what does a current file look like?" wrongly, which is
worse than not having one.

The adapter's file handling moves from the module into CategoriesConfig so it can be
tested without dragging plugin-infrastructure onto the test classpath. Tests cover both
halves of the contract -- the operator's file survives a restart untouched, the
reference copy does not -- and assert the reference copy itself parses, since one that
would fail to load documents a lie.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018VWZAdCQBEFBP9TtVpDwtx
An operator upgrading had the old flat categories.yml on disk, so the parse threw and
took the whole module down on start. CategoriesConfig now detects that format
structurally -- any direct child of `categories` that is not a section -- backs the
file up as categories.yml.pre-1.4.2.bak, and writes the current default in its place.

Replaced rather than converted, and this costs the operator nothing: the old format
never took effect. Bukkit splits configuration keys on '.' as it loads and every key in
that file contained a dot, so the routing map always parsed empty and every notification
fell through to the fallback category regardless of what was written. There is no
working configuration in it to carry over -- only the operator's intent, which the
backup keeps readable.

Detection is structural rather than a catch of the parse failure, so the decision to
rewrite someone's file is never made from an exception a different mistake could also
produce. A missing categories section stays an error.

Also adds essentials-adapter/config.yml with notifications-enabled (default true), for
servers where another delivery module already covers offline players and the same
notification would otherwise arrive twice. It gates only mail delivery: the teleport
safety predicate the module installs is a correctness fix rather than a delivery
channel, so it applies either way. Defaulting to true keeps existing installs behaving
as they did. Both configs follow the reference-copy rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018VWZAdCQBEFBP9TtVpDwtx
1.4.2 shipped from main as the government-account bugfix release, without
the notification adapter. This branch adds a delivery module and a new
config format, so it is a minor bump, not a patch.

Retarget the categories.yml format marker with it. The adapter has never
been released, so the format it replaces predates 1.5.0, not 1.4.2 --
including the .pre-1.5.0.bak backup suffix and the operator-facing warning
that names the version. Detection itself is format-based, not version-based,
so an existing legacy file still migrates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUskvuegwBzQMufaYxp5eM
md5sha256 and others added 4 commits August 23, 2026 14:47
Two separate reasons player-notifications-api:1.1.0-SNAPSHOT would not
resolve.

Only mccities-releases was declared, and the snapshot lives in the
snapshots repository beside it.

jitpack answers 401 rather than 404 for coordinates it does not host,
and Gradle treats a 401 as fatal instead of moving on to the next
repository. It sat above MCCities in the order, so it killed resolution
before MCCities was ever consulted -- for any new dependency, not just
this one. Both MCCities repositories now come first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016k8N9JrH8fHmQVe3RxJpzD
…cations

PlayerNotifications 1.1.0 lets a module register its categories and its
data type display names in code, dumps them to a generated
categories-defaults.yml and type-names-defaults.yml for the operator to
reconcile, and rebuilds its merged snapshot when a plugin registers after
startup. That makes this module's own categories.yml redundant: two files
claiming the same job, one of which PlayerNotifications cannot see.

The category set is now the RealtyCategory enum -- five categories, each
registered as both a category and the single dataType it claims, each
holding the message keys that route to it. Labels and descriptions are
defaults; an operator overrides them, or regroups the data types
entirely, in PlayerNotifications' categories.yml. Each data type also
carries a display name, without which PN title-cases the registry key and
the preference screens read "Realty.auction".

Dropped with the file: per-category titles and priorities, which PN is
the right place to decide, and the pre-1.5.0 legacy-format backup, which
converted a file nothing reads any more. The module keeps a config.yml
holding only expiry-days.

Because the set is compile-time constant it can no longer drift across a
reload, so the module no longer has to tear down with the mapper it
registered with. The display name needs its own unregister: it is keyed
by data type while the serializer and renderer are keyed by payload
class, so the unregisterPayloadMapping cascade does not reach it.

An upgrader's categories.yml is left on disk and logged at INFO as no
longer read. It holds the grouping they meant, which is what they need in
front of them while re-entering it in PN.

RealtyCategoryTest asserts the enum covers every notification.* key in
messages.yml in both directions, so a new key fails the build until it is
given a deliberate category rather than silently falling back to
realty.general.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016k8N9JrH8fHmQVe3RxJpzD
…ge key

PlayerNotifications lists an inbox row by its rendered title alone and
reveals the body only when the row is opened, so a category-level title
gave a player a screen of identical rows -- fourteen lease notifications
all reading "Realty leases" -- with no way to tell a rent payment from an
eviction without opening each.

Each message key a category claims now carries its own short summary, and
the renderer titles a notification with that, suffixed by the region when
the payload names one. An unclaimed key still falls back to its category
label, which remains the only honest thing to say about a key the enum has
never been taught about.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xrJUKJ8gHdwjpApr9mjkJ
…n rows

PlayerNotifications lists an inbox row by its title alone, so the title is
the only text a player reads before deciding whether to open a notification
-- and it was compiled in, leaving an operator no way to reword a row for
their server's vocabulary.

Titles now come from a titles.yml the module seeds on first start, listing
every message key Realty can fire at its current title. A key present there
wins; a key absent falls back to the RealtyCategory table, so a key added by
a newer Realty keeps working at its new default until the operator chooses
otherwise, and deleting a line restores the default. Values are MiniMessage,
so a row can be coloured; a blank value falls back rather than rendering a
row a player cannot read.

Titles are their own file rather than a block in config.yml because they are
the one thing here edited in bulk -- expiry-days would be buried under sixty
rows. Like every operator config it ships a defaults/default-titles.yml,
rewritten on every start.

Message keys are dotted, which Bukkit splits into nested sections at load
time, so the path separator is neutralised before the document is loaded.
A test asserts the bundled file and the enum claim the same keys at the same
titles, in both directions -- a new notification key now fails the build
until it is given a title in both places rather than silently diverging.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xrJUKJ8gHdwjpApr9mjkJ
@md5sha256
md5sha256 merged commit c934b6b into main Aug 23, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant