feat: PlayerNotifications delivery adapter - #26
Merged
Conversation
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
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
force-pushed
the
feat/pn-adapter
branch
from
August 22, 2026 14:28
3a535fe to
60fd887
Compare
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
player-notifications-adapter, a notification delivery module that forwardsRealtyNotificationEventto PlayerNotifications as registereddataTypes, so players can toggle Realty notification categories in/notifications preferences.What's in it
realty-paper-adapters/player-notifications-adapter, compiled againstplayer-notifications-api 1.0.1, published as its own jar like the chat and EssentialsX adapters.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.defaults/default-<name>.ymlon every start, so an upgrading operator can see what a new version added. Applies to Realty core (copyResourceTemplate) and the adapter (CategoriesConfig.writeReferenceCopy).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 ascategories.yml.pre-1.4.2.bakrather than converted.CategoriesConfig.loadsets the path separator to NUL before loading.notifications-enabledin the essentials adapter'sconfig.yml; the teleport-safetySafeBlockPredicateis a correctness fix and still applies regardless.BundledModuleExtractoris gone — the plugin jar ships no modules andstartModules()warns when no delivery module is installed.runServerstill 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