VotingPlugin is the vote-processing data plane for Bukkit/Paper and BungeeCord/Velocity networks. The optional Control integration is a management adapter, never a runtime dependency: vote receipt, routing, storage, rewards, joins, commands, reload, and shutdown must keep working when Control is disabled, unreachable, incompatible, or restarting.
Requirements: JDK 21+ and Maven. The Maven project lives in the VotingPlugin/ subdirectory.
mvn -B -f VotingPlugin/pom.xml test
mvn -B -f VotingPlugin/pom.xml packageFor a focused Control change:
mvn -B -f VotingPlugin/pom.xml -Dtest=BackendControlConnectorProtocolTest,ControlInspectionServiceTest testCI runs mvn -B -f VotingPlugin/pom.xml package; see .github/workflows/maven.yml. Do not use the dev Maven profile in
automation because it copies a JAR into a developer-specific server directory.
Keep the downloadable VotingPlugin JAR as small as practical. Inspect the shaded artifact when dependencies change, avoid duplicate embedded packages, and update the package-phase size and runtime checks when a necessary dependency increases the artifact budget.
Treat compatibility as a release invariant for every new feature, refactor, fix, storage change, protocol change, and dependency change. Unless the task explicitly says otherwise, a VotingPlugin upgrade must be a drop-in JAR replacement: an administrator replaces the existing VotingPlugin JAR, starts/reloads as normally supported, and does not need any other deployment change.
That default contract means:
- Do not require manual edits, regenerated configs, deleted keys, renamed files, data resets, one-off conversion commands/scripts, or manual database changes. New keys must have safe defaults when absent and preserve established behavior.
- Existing YAML, vote-site definitions, rewards, user data, vote totals/streaks, cached/offline votes, logged data, and supported database state must continue to load. Required migrations must be automatic, idempotent, restart-safe, and preserve existing state.
- Do not require a simultaneous update of AdvancedCore, SimpleAPI, Votifier/VotifierPlus, PlaceholderAPI, VotingPlugin-Control, a proxy JAR, backend JARs, or every server in a network merely to keep previously working behavior working. New cross-component behavior must be additive/capability-negotiated with safe fallback for older peers.
- Preserve existing commands, permissions, placeholders, events, public/de-facto APIs, configuration semantics, proxy methods, message/reward behavior, and supported platform behavior unless the request explicitly authorizes a break.
- Mixed-version proxy/backend deployments must fail safe and retain legacy behavior for features not mutually supported; do not make upgrade order a hidden requirement.
- New optional integrations and features must default to non-disruptive behavior for existing installations and must not become mandatory runtime dependencies.
- Packaging changes must preserve the normal downloadable artifact and startup path. Do not make administrators install extra libraries or companion JARs for an upgrade unless explicitly requested.
- When a requested implementation cannot preserve drop-in compatibility, stop and clearly surface the required compatibility break before implementing it unless the request explicitly permits that break.
For compatibility-sensitive changes, add regression coverage that exercises the prior installation/state or protocol shape as well as the new behavior. Review the final diff from the perspective of an administrator upgrading only the VotingPlugin JAR on an existing installation.
VotingPluginMainis the Bukkit entry point and lifecycle owner.com.bencodez.votingplugin.coreand its subpackages are platform-independent. Classes there must not import Bukkit, Paper, Fabric, Forge, or NeoForge APIs. Keep loader-specific adapters outside this package and enforce the boundary withCorePlatformIsolationTest.proxy/VotingPluginProxyand the Bungee/Velocity platform packages own proxy lifecycle and vote routing.listeners/receives Bukkit-side vote/player events;proxy/cache/owns proxy pending-vote queues.votesites/resolves configured service names. Be alert to the distinction between read-only resolution and paths that may auto-create a site.user/owns player totals, points, streaks, last-vote values, and backend offline rewards.rewards/andspecialrewards/parse and execute rewards. A Control simulation must never invoke these executors.votelog/owns optional SQL-backed logged events and its in-game admin GUI.control/BackendConfigurationServiceis the bounded Bukkit YAML/quick-setup adapter.control/BackendControlConnectoris the Bukkit outbound Control connector and task dispatcher.control/ControlInspectionServiceis the typed read-only inspection allow-list.control/ControlRewardProposalis the shared strict parser for reward simulation and reward-builder persistence.control/BackendControlResultStorejournals configuration results that must survive acknowledgement failure/restart.proxy/control/contains proxy discovery/configuration, communication tests, automatic enrollment, and hosted-Control lifecycle.VotingPlugin/src/main/resources/contains the default Bukkit and proxy configuration.docs/control-connector.mdexplains deployment;docs/control-agent-contract.mdis the exact agent/client contract.
- Treat AdvancedCore/VotingPlugin user-data, cache, and storage APIs as potentially blocking unless an API is explicitly documented as snapshot-only. Do not perform cache population, SQL-backed reads or writes, flush/dump/clear/remove operations, or shared-runtime admission on the Bukkit/Paper primary server thread. Capture platform-owned state there, hand user-data work to the existing persistence/storage worker, and schedule only the required Bukkit/Folia interaction back onto the platform owner.
- Preserve the shared-user lock order: shared-runtime/per-user admission before the
UserDataCachemonitor. Never holdsynchronized (UserDataCache)while calling APIs that can acquire shared-runtime admission, includingdump(),clearCache(),removeCache(), cache population, or storage access. Keep cache-monitor sections short and cache-local.
- Control connectors initiate outbound HTTP(S); do not add an inbound admin port to VotingPlugin.
- All connector network and database work stays off Bukkit's primary thread. Keep the dedicated inspection daemon separate from the presence/configuration executor; it has a five-second shutdown bound. Schedule only the minimum reload/runtime interaction onto the server thread, then return the bounded result to the correct connector worker.
- Connector failure is isolated. Never block vote handling, joins, commands, reload, or shutdown on Control I/O; keep timeouts, body limits, daemon workers, and bounded shutdown waits.
- Capabilities are explicit and versioned. Do not dispatch a task merely because its JSON shape looks familiar. An unaccepted capability must remain inactive. Control and the node both enforce fixed quick-setup preset/option schemas; keep phase-specific validation here even when Control already rejected the same input.
- Configuration writes are limited to the managed VotingPlugin YAML allow-list and typed quick setups. Preserve path
containment, no-follow reads, size limits, YAML parsing, secret masking/restoration, revision checks, atomic staging,
.control-backup, reload, and rollback-on-reload-failure. Control snapshots persist this redacted read output, so new credential fields and sensitive comments must be covered by masking tests before release. - A configuration result is durable and idempotent: journal it before acknowledgement, echo the current
attemptId, and do not apply the same operation twice when a lease or acknowledgement is retried. - Inspections are read-only, typed, bounded, and safe to retry. Never add raw SQL, table names, filesystem paths, commands, arbitrary placeholders, generic configuration lookup, fuzzy/all-player search, or mutable live objects.
- Never return credentials, passwords, tokens, database/Redis/MQTT connection details, webhook URLs, raw configuration, raw logs, or unrestricted player records. Keep diagnostics deliberately redacted. Unexpected configuration/read/reload exceptions return fixed action-specific external text and keep their detailed cause only in the backend log. Unexpected inspection exceptions return a generic external message; local logging may identify the exception class but must omit its message.
- An inspection's
playerquery is exact name or UUID lookup and must check existence before loading. Do not turn it into enumeration or autocomplete. - A reward inspection only validates/normalizes a typed proposal. It must report
wouldExecute:falseandsideEffects:false; persistence still goes through configuration preview/apply.
Keep these paths separate:
- discovery/presence advertises current node identity and topology;
- configuration capabilities (
config.*.v1plus explicitly negotiated successors) poll/operations, may read/preview/apply typed configuration, and journal results; - inspection capability
data.inspect.v1polls/inspections, executes onlyControlInspectionService, and does not journal because a lost acknowledgement can safely repeat a read. Repeated failures back this lane off exponentially from one second to five minutes without changing voting or configuration availability.
Every claimed task is bound to a node session and attemptId. Echo both. An HTTP 204 means no work. Authentication,
protocol, or capability failure changes only connector state/backoff.
auto-create-vote-sites is intentionally narrower than common-settings: it reads/writes only
Config.yml -> AutoCreateVoteSites. Do not fold it back into a multi-setting update. Turning automatic creation off must
not erase detected service-site observations, and explicit administrator-created sites must remain a separate action.
vote-logging is also narrow: it owns only VoteLogging.Enabled, VoteLogging.PurgeDays (-1 or 1–3650), and
VoteLogging.UseMainMySQL. It must reject database host/name/user/password or any unknown option. Dedicated connection
credentials remain a redacted full-editor task.
reward-builder is PREVIEW/APPLY-only. It requires exactly one <=64 KiB proposal option using the inspection proposal
schema, and replaces exactly VoteSites.<site>.Rewards, EverySiteReward, or VoteParty.Rewards. Keep it deterministic:
do not merge stale actions, change another scope, execute a reward, expose the proposal in a result, or journal its value.
The allow-listed kinds are overview, vote-site-health, player, vote-log-summary, vote-log-search, vote-trace,
vote-site-resolution, reward-simulation, and diagnostics. The exact filters and result semantics are in
docs/control-agent-contract.md.
Maintain these global bounds unless a versioned contract deliberately replaces them:
- result JSON: 512 KiB;
- general result rows: 100 (including detected plugin names in diagnostics);
- top lists: 20;
- lookback: 365 days;
- exact player lookup only;
- no mutation in resolution, simulation, or diagnostics.
Unknown query/filter/proposal fields must fail validation. vote-site-resolution must use the non-creating resolver path;
do not call a convenience method that can auto-generate configuration.
vote-site-health may expose at most 100 case-insensitively deduplicated persisted GottenServiceSites values that lack a
configured ServiceSite. Snapshot the stored list before iterating and keep it observational; this signal must work with
VoteLogging disabled and must never create a vote site.
VoteLogging is optional and SQL-backed. It may use the main MySQL connection or a dedicated one. The current quick setup
changes Config.yml but does not recreate or close the runtime VoteLog manager, so a server restart is required after
either VoteLogging.Enabled transition. Inspections must gate on the configured enabled state: disabled means unavailable
even if an old adapter remains, while newly enabled can report enabled but unavailable until restart. A dependent query
must return UNAVAILABLE for disabled, missing-adapter, or unreadable state rather than treating an empty result as
authoritative.
Legacy VoteLog read methods catch SQL failures and return empty/zero values, so the inspection layer must probe readability
first. Preserve the 10-second JDBC statement timeout: summary/search/trace return UNAVAILABLE when logging is disabled,
the adapter is missing, or the probe fails, while vote-site health exposes voteLogReadable:false, skips aggregates, and uses explicit unavailable or
unreadable statuses instead of NO_RECENT_VOTES. The probe is point-in-time; legacy methods can still return empty if the
database fails after it succeeds, so removing that race requires an explicit table error-result API.
VoteLog records selected events: vote receipt, vote milestone, vote-streak reward, top-voter reward, and vote-shop
purchase. IMMEDIATE and CACHED describe processing status. A shared voteId correlates written rows, but the table is
not a complete network delivery trace: it does not record every validation rejection, transport hop, duplicate decision,
reward command, command outcome, or expiry. Documentation and UI must call these logged events.
Queries must use the bounded methods on VoteLogMysqlTable. Preserve prepared parameters, exact filters, row limits, and
stable ordering. The recent service-health window is not proof that an omitted configured service has no votes; query the
at-most-100 displayed configured services through prepared exact filters. Health matching and SQL aggregation use the
full, case-normalized ServiceSite (up to the 2048-character validator bound); truncate only serialized display fields,
and classify unmatched logged services against every configured site rather than only the displayed page. Do not accept
raw SQL from Control or expose the database/table configuration.
Keep changes focused and avoid unrelated formatting. Before any commit, push, PR update, review reply, or other remote change:
- run relevant focused tests;
- run
mvn -B -f VotingPlugin/pom.xml clean package; - verify the package invocation produced a fresh downloadable JAR and that expected tests were discovered;
- run
git diff --cached --checkandgit diff --check; - inspect
git diff --cachedandgit diff, then inspect the complete base-to-HEAD diff for compatibility, concurrency, persistence, lifecycle, security, packaging, and platform regressions.
Steps 1-3 may be skipped only for documentation/instruction-only changes that do not modify executable source, tests, build or dependency configuration, workflows, packaged resources, generated output, or runtime/deployment behavior. Record that exemption in the PR. Steps 4-5 and the review requirements below still apply.
For substantive work, obtain a fresh source-read-only $code-review of the exact intended change before the first push or PR update. The implementation agent verifies and fixes accepted findings, reruns all required checks, and obtains a new review of the updated snapshot. Any substantive repository change after a clean review—including source, tests, build or dependency configuration, workflow files, resources, contracts, documentation, or instructions—invalidates the previous clean verdict. Rerun applicable validation and obtain a fresh review of the exact intended snapshot; do not reuse an earlier verdict. Hosted PR review is confirmation, not the first full review, and merge still requires explicit authorization.
Do not commit server runtime data, credentials, generated JARs, dependency caches, IDE output, or unrelated formatting.
The server-side peer is BenCodez/VotingPlugin-Control. When changing a DTO, endpoint, capability, preset, error code, or
limit:
- inspect both repositories and their root
AGENTS.mdfiles; - keep the change additive/capability-negotiated so either old side stays safe;
- update connector/service tests here and coordinator/HTTP tests in Control;
- update
docs/control-agent-contract.md,docs/control-connector.md, and the Control management docs; - link the paired PRs and state a safe merge/deployment order.
Prefer one cohesive PR per repository for a paired feature, keeping its implementation, tests, and docs together. Split further only when a part is independently deployable or has materially different review/rollback risk.
config.proxy-method.v1 covers plugin messaging, Redis, MQTT, sockets, and MySQL; config.proxy-method.v2 adds HTTP. Dispatch and validate the
exact capability for the requested method. config.quick-setup.v2 adds VoteParty.Enabled; keep legacy Vote Party
payloads on v1, preserve the installed Enabled value when they omit it, and reject the enabled field unless v2 was
accepted. The VotingPlugin connector may deploy first and
advertise these successors without using them until Control accepts them. A newer Control deployed first must leave its
v2-only actions unavailable on older nodes. Merge the VotingPlugin capability implementation before relying on the new
Control behavior in production.
- Trace whether the code runs on the connector worker, proxy thread, Bukkit primary thread, or a SQL executor.
- Preserve queued votes across saturation, shutdown, and restart; overflow handling must be bounded, durable when promised, and observable rather than silently dropping work.
- Proxy-to-backend guaranteed delivery is capability negotiated and at least once. Journal a reward-bearing envelope before reporting transport acceptance, retain it until the matching backend completion acknowledgement is durable, persist completed IDs before acknowledgement for restart-safe deduplication, retire receipts only through the durable proxy-confirmed release handshake, retain a bounded durable tombstone for in-flight retries, and keep legacy send behavior for backends that do not advertise the capability.
- Treat scheduler units explicitly. Verify whether each delay is in ticks, milliseconds, or seconds, especially across Bukkit, Folia, BungeeCord, and Velocity adapters.
- Register listeners and lifecycle wakeups before producers can publish work; startup/reload ordering must not strand already-persisted or newly-arriving operations.
- Protocol-mode changes must not silently broaden legacy v1/RSA acceptance when token-only operation is configured or intended; cover downgrade behavior with tests.
- Add strict type/field/range/count validation before calling plugin services.
- Snapshot synchronized live collections before iterating; do not return mutable collections across threads.
- Distinguish “not configured/unavailable”, “not found”, and a genuine empty result.
- Test unknown fields, invalid bounds, disabled VoteLogging, oversized results, exact-player misses, non-creating resolution, reward no-side-effects, lease retry/idempotency, and redaction as applicable.
- Preserve connector shutdown bounds and avoid blocking waits on Bukkit lifecycle paths.
- At the start of every session, read
.mex/AGENTS.mdand.mex/ROUTER.mdbefore project work; followROUTER.mdto load only the relevant context. - Read
mex logging --jsonat session start and before optional logging. Its checkout-local advisory mode issignificant(quiet default: material decisions, risks, blockers, or durable discoveries),checkpoints(batch useful notes at task/session boundaries), ormanual(no unsolicited notes). Skip routine tool calls, edits, repeated status, and empty summaries. Honor explicit user log requests in every mode; never suppress mandatory workflow Activity or recovery audit records. Report a policy read failure instead of guessing or changing the preference. - When earlier work may inform the task, retrieve bounded relevant notes with
mex timeline --query "subject phrase" --file src/example.ts --limit 10 --json, using the known subject or exact recorded file path, or both. Treat matches as historical evidence, not accepted current knowledge; verify conclusions before reuse or explicit promotion with their source retained. - Use
$mex-inboxfor explicit contributions to project knowledge and$mex-relayfor durable team handoffs. Invoke them automatically when intent clearly matches; ordinary GROW upkeep remains available without Inbox. - When MEX context materially helps your work, mention MEX and the relevant finding naturally in your explanation. Tie the mention to what it helped you understand, decide, or verify. Avoid fixed phrases, standalone acknowledgements, repeated mentions, or narrating routine context loading. This replaces older MEX instructions requiring a fixed acknowledgement or context-loading narration.
- Do not claim an author, date, or historical event unless the retrieved data actually provides it.
- After a MEX write, say exactly what changed and its sharing boundary: a local draft is checkout-only and nothing is shared; a canonical artifact is written to the working tree and requires commit/push to share.
- Skill activation is not approval for canonical actions.