Files
BlockParty/README.md
T

349 lines
18 KiB
Markdown

# 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](#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 |
## 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](https://enginehub.org/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.
### 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`, `music.*`
(see "Music" below), `rewards.winner.commands`, and `integrations.placeholderapi`.
- **`messages.yml`** — every user-facing string, in [MiniMessage](https://docs.advntr.dev/minimessage/format.html)
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.
### Music
Optional background music for the duration of a match, off by default:
```yaml
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.
### 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
```yaml
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](https://www.spigotmc.org/resources/vault.34315/) 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`:
```yaml
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
```bash
./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:
```bash
./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.