Files
2026-08-08 11:36:29 -04:00

24 KiB

BlockParty

BlockParty is a colored-floor elimination minigame for PaperMC servers. Players stand on a floor of colored blocks; each round a target color is announced and players must move onto it before time runs out. All other blocks vanish, anyone not standing on the target color (or standing on nothing) falls and is eliminated, and the floor is restored for the next, slightly faster round. Last player standing wins.

Requirements

  • PaperMC 1.21.x (API version 1.21)
  • Java 21
  • No other plugins required. Optional soft-dependencies: PlaceholderAPI, Vault, WorldEdit.

Installation

  1. Download BlockParty-<version>.jar (see Building from source) or grab it from the CI build artifacts.
  2. Drop it into your server's plugins/ folder.
  3. Start/restart the server. config.yml, messages.yml and the arenas/ folder are created automatically under plugins/BlockParty/.

Commands

All admin/config commands live under /blockparty (alias /bp) and work from console where it makes sense (create, delete, enable, disable, info, list, reload). Commands that require a physical location (setlobby, setspawn, setspectator, setbillboard, pos1, pos2) require a player sender. delbillboard, billboards, info work fine from console.

Admin

Command Description
/blockparty help Show command help
/blockparty create <arena> Create a new, disabled arena
/blockparty delete <arena> Delete an arena and its config file
/blockparty enable <arena> Validate and enable an arena
/blockparty disable <arena> Disable an arena, kicking anyone inside
/blockparty setlobby <arena> Set the waiting-room location
/blockparty setspawn <arena> Set the in-game spawn location
/blockparty setspectator <arena> Set the spectator/eliminated location
/blockparty setbillboard <arena> [name] (Optional) place a named display block here that mirrors the current target color; name defaults to default, so you can have any number by giving each a unique name
/blockparty delbillboard <arena> [name] Remove a named billboard (defaults to default)
/blockparty billboards <arena> List an arena's billboards and whether each is enabled
/blockparty pos1 <arena> Set floor region corner 1
/blockparty pos2 <arena> Set floor region corner 2
/blockparty setfloor <arena> <mat1,mat2,...> Set the allowed floor materials
/blockparty generate <arena> Randomly generate and persist the floor layout
/blockparty info <arena> Show arena configuration/status and validation errors
/blockparty list List all arenas
/blockparty reload Reload config.yml, messages.yml and arena definitions

Player

Command Description
/blockparty join <arena> Join an arena's waiting lobby
/blockparty leave Leave your current arena
/blockparty arenas List arenas (alias of list)
/blockparty stats [player] View your or another player's stats
/blockparty musicvote (Only if the jukebox is enabled) get a one-time code to vote for the next track on the jukebox web page

Permissions

Permission Default Description
blockparty.play true Baseline permission to use player commands
blockparty.join true Allows joining arenas
blockparty.spectate true Allows spectating
blockparty.admin op Parent permission, grants all blockparty.admin.*
blockparty.admin.create op Create arenas
blockparty.admin.delete op Delete arenas
blockparty.admin.setup op Configure lobby/spawn/floor/etc, enable/disable
blockparty.admin.reload op Reload the plugin

Creating an arena — full walkthrough

  1. /bp create lobby1 — creates plugins/BlockParty/arenas/lobby1.yml.
  2. Stand where players should wait and run /bp setlobby lobby1.
  3. Stand where players should spawn on the floor and run /bp setspawn lobby1.
  4. Stand where eliminated players/spectators should go and run /bp setspectator lobby1.
  5. Stand at one corner of the floor area and run /bp pos1 lobby1.
  6. Stand at the opposite corner and run /bp pos2 lobby1.
  7. Either:
    • Run /bp setfloor lobby1 RED_CONCRETE,BLUE_CONCRETE,GREEN_CONCRETE,YELLOW_CONCRETE to choose the color palette (defaults to the full concrete rainbow if you skip this and just generate), then /bp generate lobby1 to lay out and persist a random floor, or
    • Build the floor by hand out of allowed materials, then run /bp generate lobby1 is still required once so BlockParty captures/serializes the exact layout it should restore each round (BlockParty does not scan the world every round — it restores from the saved layout for performance and determinism).
  8. Optionally, stand somewhere visible near/above the floor and run /bp setbillboard lobby1 (or /bp setbillboard lobby1 <name> to add more than one, e.g. one per side of the arena) to place a big display block that mirrors the round's target color — purely cosmetic and entirely optional (see "Billboards" below).
  9. /bp info lobby1 to confirm there are no validation errors.
  10. /bp enable lobby1.
  11. Players run /bp join lobby1. Once min-players is reached the countdown starts automatically; joining an arena mid-countdown is allowed by default (allow-join-while-waiting: true).

Floor selection details

  • pos1 / pos2 define the two opposite corners of the rectangular floor region (any Y). If WorldEdit is installed and you have an active WorldEdit selection (made with the wand — left-click for position 1, right-click for position 2, or //pos1 / //pos2), running /bp pos1 <arena> / /bp pos2 <arena> adopts that selection's corners directly instead of your standing location — no need to physically stand at each corner. This is entirely optional: without a WorldEdit selection (or without WorldEdit installed at all), the commands fall back to your current location exactly as before. Disable it with integrations.worldedit: false in config.yml if you'd rather always use your location even when WorldEdit is present.
  • setfloor only changes which materials are allowed to be used/regenerated — it does not touch blocks in the world.
  • generate is the step that actually writes a random layout (biased evenly across the configured palette) across the whole region and serializes it into the arena's yml file (generated-layout) so it can be restored exactly, every round, without re-scanning or re-randomizing the world.
  • By default the positions from that saved layout are reused every round, but which color sits at which position is reshuffled (floor.shuffle-each-round: true in config.yml) at the start of the match and before every round's restore — so the board isn't a static, memorizable pattern for the whole game. The shuffle only permutes materials in memory; the saved generated-layout itself is never modified, so a restart or another /bp generate still starts from the original. Set shuffle-each-round: false to always restore the exact saved layout unchanged instead.
  • While shuffling, the target color's share of the floor shrinks each round instead of always averaging ~1/(palette size) — round 1 starts at floor.starting-target-fraction (default 0.5) and drops by floor.target-fraction-reduction-per-round (default 0.04) per round, never going below floor.minimum-target-fraction (default 0.06), so the target color gets progressively rarer and harder to spot as a match goes on.

Billboards

An arena billboard is a BlockDisplay entity that visually shows the currently selected target color/block each round, in addition to the title/actionbar/hotbar-item cues. An arena may have any number of them (e.g. one visible from each side of the floor), each independently named, positioned and toggled. Entirely optional:

  • Per-billboard: /bp setbillboard <arena> [name] places/updates one at your current location; name defaults to default if omitted, so the simple single-billboard flow needs no name at all. Give each a distinct name to have several at once (e.g. /bp setbillboard lobby1 north, /bp setbillboard lobby1 south). /bp delbillboard <arena> [name] removes one. /bp billboards <arena> lists an arena's billboards and whether each is enabled.
  • Per-billboard toggle: stored per name in the arena's yml (billboards.<name>.enabled, default true) — set to false to keep the location saved but temporarily stop displaying it, without running delbillboard.
  • Global toggle: ui.billboard in config.yml (default true) disables every billboard on every arena at once, e.g. for performance-constrained servers.

Every billboard shows the same current target color in sync — they're just multiple vantage points on the same information. Entities are non-persistent (not saved to the world, so they never survive a server restart on their own — BlockParty re-creates them on demand) and are all despawned automatically when the arena is disabled, deleted, or the plugin shuts down, so nothing leaks.

Player usage

  • /bp join <arena> to enter the waiting lobby. Your inventory, armor, offhand, health, food, XP, game mode, flight state, location and potion effects are all captured and will be restored exactly when you leave, get eliminated-and-the-match-ends, or the match ends.
  • /bp leave at any time to exit and be restored.
  • During a round, a title/subtitle/action bar/sound announce the target color, and the target block is placed in your hotbar (slot 9) as a visual reminder. A boss bar (if enabled) shows the remaining time as a shrinking progress bar.
  • Get eliminated by not standing on the target color when the floor clears, or by falling off entirely. Eliminated players are teleported to the configured spectator location and marked as spectators for gameplay purposes (no damage, can't interfere), but stay in adventure mode rather than true spectator mode — no noclip/flight, just watching from where they landed.
  • Last player standing wins; rewards, stats and celebration effects (fireworks, titles, sounds) fire automatically.
  • Solo play: if a match starts with only one player (e.g. min-players: 1 for testing), "one player remaining" is the expected steady state, not a win — the match keeps running rounds, each shorter than the last, until that player is actually eliminated or leaves. With two or more players, the match ends the moment only one remains.

Configuration overview

  • config.yml — global settings: ui.* toggles for scoreboard/bossbar/titles/actionbar, sounds.* (Bukkit Sound enum names, played directly to each participating player rather than at one world location, so distance never mutes them; invalid names are logged and skipped, never crash the plugin), performance.floor-blocks-per-tick (reserved for future/optional batched floor operations — round-end removal and each round's floor reset are always instantaneous, all blocks at once, by design), defaults.* (used only when /bp create seeds a new arena), default-floor-materials (the full 16-color concrete palette by default), floor.shuffle-each-round and floor.*-target-fraction* (difficulty ramp, see above), powerups.* (see "Powerups" below), music.* (see "Music" below), rewards.winner.commands, and integrations.placeholderapi.
  • messages.yml — every user-facing string, in MiniMessage format (Adventure, bundled with Paper). Placeholders like %player%, %arena%, %round%, %time%, %color% are substituted per-message.
  • arenas/<name>.yml — one file per arena; a broken file only disables that one arena (the rest still load) and the error is logged.

Powerups

Temporary abilities that spawn on the floor during a match, on by default:

powerups:
  enabled: true
  spawn-chance-per-round: 0.35
  max-active: 1
  min-lifetime-rounds: 2
  max-lifetime-rounds: 3
  types:
    super-speed:
      marker-block: TORCH
      duration-seconds: 8
      amplifier: 1
    air-blast:
      marker-block: END_ROD
      radius: 4.0
      strength: 1.4

Each round, a powerup may appear as a marker block (torch/end rod by default) floating just above a random floor cell — it never removes a player's footing, since it sits in their foot space rather than replacing the floor block itself. Claim one by walking into its marker or left-clicking it:

  • Super Speed — a temporary Speed potion effect (duration-seconds / amplifier).
  • Air Blast — pushes every other player within radius blocks away from the claimer, scaled by distance (strength).

Unclaimed powerups disappear after a random 2-3 rounds (min-lifetime-rounds / max-lifetime-rounds). Every claim is recorded per-player as a powerups_used stat, shown in /blockparty stats.

Music

Optional background music for the duration of a match, off by default:

music:
  enabled: true
  track: MUSIC_DISC_PIGSTEP
  volume: 1.0

track is any Bukkit Sound enum name that's actually a track (one of the MUSIC_DISC_* sounds, or a MUSIC_* ambient track) — invalid names are logged and skipped, never crash the plugin. It plays on the client's dedicated Music volume slider (via SoundCategory.MUSIC), separately from sound effects, starting when a match begins. It's paused the instant the floor wipes each round and resumed once the floor has fully reset for the next round. Note this is a stop/replay-from-the-start rather than a true mid-track pause/resume — vanilla Minecraft's sound API has no seek/resume-from-position, so each "resume" restarts the track from the beginning. It's stopped entirely at match end, arena disable, or plugin shutdown.

Jukebox (web-based track voting)

Optional: let players vote for an arena's music track from a web page, live, instead of (or as a live override to) the fixed music.track. Off by default — enabling it opens a real HTTP port on your server. Config:

jukebox:
  enabled: true
  bind-address: 127.0.0.1
  port: 8642
  allowed-origin: "*"
  vote-code-minutes: 10
  tracks:
    - "MUSIC_DISC_PIGSTEP|Pigstep"
    - "MUSIC_DISC_CAT|Cat"
    # ... id|label pairs; "id" must be a valid Bukkit Sound enum name

The voting page: web/jukebox.html in this repo is a ready-to-use, dependency-free static page implementing all of this — self-host it anywhere (same reverse proxy as the API, GitHub Pages, S3, or just open the file locally for testing). It asks the player for your server's HTTPS address once (saved in their browser), then for their vote code, then shows arenas as tabs and tracks as clickable "discs" with a live vote tally that polls every few seconds. A live preview of the same page: https://claude.ai/code/artifact/bfed1aa7-8040-47bc-a55b-970c07f83c4f (hosted for previewing the UI only — point it at your own server's address to actually vote; it won't work against a plain-HTTP address for the mixed-content reason above).

How it works:

  1. A player runs /blockparty musicvote in-game and gets a short numeric code (valid for vote-code-minutes). This is the only way to get a code — it proves you're a real player on the server, without needing any separate account/password system on the web page.
  2. On the voting page, they enter the code plus their arena and pick a track. Each vote is a simple HTTP POST /api/vote?arena=<arena>&code=<code>&track=<id> — the plugin resolves the code back to their player UUID, so re-voting just changes their pick rather than adding a duplicate.
  3. The plugin re-checks the current vote leader every time music resumes after a floor-wipe pause, so the crowd can change the track between rounds within a match, not just before it starts. Votes for an arena are cleared when it returns to WAITING.
  4. GET /api/tracks lists the configured, valid tracks; GET /api/arenas lists each arena's state, current track, and live vote tally, for the page to render.

Security model and requirements — read before enabling:

  • This is a plain, unauthenticated-by-default HTTP API on your server. bind-address: 127.0.0.1 (the default) means it's only reachable from the same machine — you must put a reverse proxy (Caddy, nginx, Cloudflare Tunnel, etc.) with a real TLS certificate in front of it to expose it to the internet safely. Do not set bind-address to 0.0.0.0 and forward the raw port directly without TLS in front of it.
  • A web voting page is normally served over HTTPS (e.g. a published page); browsers block an HTTPS page from calling a plain http:// address ("mixed content"), so the API being behind HTTPS isn't optional if you want a hosted page to reach it — it's required.
  • The only "auth" is the short-lived numeric code from /blockparty musicvote; anyone who obtains a valid code (e.g. a player shares it) can vote as that player until it expires. There's no rate-limiting on the HTTP endpoints — treat this as a fun, low-stakes feature, not a hardened public API.
  • allowed-origin sets the Access-Control-Allow-Origin response header; tighten it from * to your actual voting page's origin if you want to restrict which sites can call it.
  • Changes to any jukebox.* setting require a full server restart to take effect — the HTTP server isn't restarted by /blockparty reload.

Reload behavior

/blockparty reload re-reads config.yml, messages.yml, and every arena file. Arenas currently in the RUNNING or ENDING state are skipped during arena reload (their config keeps running unmodified) to avoid corrupting an active match; a warning is logged per skipped arena. Arenas that are WAITING, STARTING or DISABLED are reloaded fully from disk. This is a deliberate design choice: rather than queuing a reload for when the arena goes idle, in-progress matches simply finish on their old configuration and pick up the new configuration the next time they return to WAITING.

Rewards

rewards:
  winner:
    money: 100
    commands:
      - "give %player% diamond 3"
      - "say %player% won BlockParty in %arena% after %round% rounds!"

integrations:
  vault: true

Two independent reward mechanisms fire on a win, both optional:

  • money is deposited straight into the winner's balance through the Vault Economy API — no console command string parsing involved. This requires Vault and an economy plugin that registers a Vault Economy provider (e.g. EssentialsX, CMI). If Vault or an economy provider isn't present, BlockParty logs one warning and simply skips the money portion — it never crashes and console-command rewards still run normally. Set money: 0 to disable this entirely, or integrations.vault: false to force-disable it even with Vault installed.
  • commands run as the console with %player%, %arena% and %round% substituted, for anything beyond currency (items, permissions, external plugin hooks, announcements, etc.).

Vault is declared as a softdepend in plugin.yml, so BlockParty starts up fine whether or not it's installed; the Economy service lookup happens fresh each time a reward is dispatched (not cached at startup), so plugin load order relative to your economy plugin doesn't matter.

Statistics storage

By default player statistics are stored in a local SQLite file (plugins/BlockParty/blockparty.db) — zero setup required. To use a shared MySQL database instead (useful across a network of servers), set storage.type: mysql in config.yml:

storage:
  type: mysql
  mysql:
    host: localhost
    port: 3306
    database: blockparty
    username: blockparty
    password: password
    table-prefix: "bp_"
    connection-parameters: "sslMode=PREFERRED&verifyServerCertificate=false"

connection-parameters is appended verbatim to the JDBC URL, so TLS behavior is controlled with real MySQL Connector/J flags rather than a single use-ssl toggle. Common values:

  • sslMode=DISABLED — no TLS.
  • sslMode=PREFERRED&verifyServerCertificate=false (default) — TLS if the server offers it, without validating the certificate; good for a private/internal database host.
  • sslMode=REQUIRED&verifyServerCertificate=false — TLS mandatory, certificate not validated.
  • sslMode=VERIFY_IDENTITY — TLS mandatory and the server certificate is validated against a trust store (production-grade; requires a properly signed/trusted cert).

MySQL and MariaDB are both supported through the same mysql driver/config. If a stats query ever fails (bad credentials, dropped connection, etc.), BlockParty logs the full error once for diagnosis and then throttles repeats of that same failure to once every 5 minutes — it never spams a stack trace on every round, and gameplay is unaffected either way since all stats I/O is async and best-effort.

The MySQL JDBC driver (mysql-connector-j) is bundled in the plugin jar, so no extra download is required. The stats table is created automatically on startup if it doesn't exist. All queries run asynchronously off the main thread regardless of storage type. Changing storage.type requires a server restart (it's read once at startup, not on /blockparty reload).

PlaceholderAPI support

If PlaceholderAPI is installed, BlockParty registers a %blockparty_...% expansion automatically (soft-dependency — startup never fails if PAPI is absent):

  • %blockparty_wins%
  • %blockparty_games_played%
  • %blockparty_best_round%
  • %blockparty_arena%
  • %blockparty_players_<arena>%
  • %blockparty_state_<arena>%

Building from source

./gradlew clean build

The shaded plugin jar (with sqlite-jdbc and mysql-connector-j bundled) is produced at build/libs/BlockParty-<version>.jar.

Run unit tests only:

./gradlew test

Unit tests cover the pure-logic package (us.tss3.blockparty.logic) with no Bukkit dependency: arena state transitions, countdown start/cancel logic, round-time progression/clamping math, target-color selection (deterministic via an injectable RNG), elimination/winner determination, arena config validation, and player-count/join logic.

Troubleshooting

  • "Arena failed validation" on enable — run /bp info <arena> to see exactly which fields are missing (lobby/spawn/spectator/region/materials/etc).
  • World unloaded warning — if an arena's world is unloaded while the plugin is running, BlockParty detects it, safely disables that arena and restores any players inside it.
  • Invalid sound/material in config — logged as a warning at startup/use and skipped; it will not crash the plugin.
  • Nothing happens on /bp join — check the arena is enabled (/bp info <arena>), not full, and that you are not already in another arena (/bp leave first).
  • Floor doesn't look right — remember setfloor only changes the allowed palette; you must run /bp generate <arena> to actually (re)generate and persist a layout.

Architecture

  • logic/ — pure, Bukkit-free game logic (unit tested).
  • arena/Arena (per-arena runtime state machine + round loop) and ArenaManager.
  • config/ArenaConfig, ArenaConfigLoader, ConfigManager, MessagesManager.
  • floor/FloorManager, tick-batched floor generation/removal/restoration.
  • session/PlayerSession, SessionManager (single-arena-membership enforcement).
  • model/PlayerState snapshot/restore.
  • command/BlockPartyCommand (single entry point, dispatches to handlers).
  • listener/ — connection/quit handling and gameplay restrictions.
  • scoreboard/ — interval-based sidebar scoreboard.
  • reward/ — console-command reward dispatch.
  • persistence/ — async stats storage over SQLite or MySQL (StatsDatabase, StatsManager).
  • placeholder/ — optional PlaceholderAPI expansion, loaded only when PAPI is present.
  • util/ — sound/location helpers.