A modular lobby plugin. Every behavior is a self-contained, independently toggleable feature — persistent spawn, no hunger, no death, no player collision, and a fixed gamemode — managed by a small feature-registry framework so new features can be added without touching existing code.
Every behavior implements LobbyFeature:
public interface LobbyFeature {
String name();
void enable();
void disable();
}LobbyPlugin.onEnable() registers each feature together with a BooleanSupplier
that says whether it should be active — normally a LobbyConfig getter:
featureManager.register(new NoHungerFeature(this), lobbyConfig::isNoHungerEnabled);FeatureManager is the only class that knows how to turn that on/off, and it
does so generically — it has no idea what "no-hunger" even means. Calling
featureManager.reload() (wired to /lobbyreload) re-checks every supplier
and enables/disables features to match, without a restart. This is what makes
runtime management work: flip a value in config.yml, run /lobbyreload,
done.
Adding a new feature later means: write one class implementing
LobbyFeature, add one featureManager.register(...) line in LobbyPlugin.
Nothing else changes.
features:
spawn: true
no-hunger: true
no-death: true
no-collision: true
fixed-gamemode:
enabled: true
gamemode: CREATIVERun /lobbyreload (permission lobby.reload, default op) after editing this
file to apply changes live.
- no-hunger — cancels
FoodLevelChangeEventfor players, so hunger never drops. - no-death — cancels
EntityDamageEventfor players outright. This is independent of gamemode, so it still protects players even iffixed-gamemodeis turned off separately. - fixed-gamemode — sets the configured
GameModeon join, and immediately re-applies it to everyone online when the feature is (re-)enabled (e.g. right after a/lobbyreload). - no-collision — uses the standard
LivingEntity#setCollidable(false)flag (plain Bukkit API) on join; restores collisions on disable.
SOLID
- SRP —
LobbyPluginonly wires things together; each feature class owns exactly one behavior;LobbyConfigis the only class that reads config.yml;FeatureManageronly tracks enabled/active state. - OCP — new features are added by registering them, never by editing
FeatureManageror other features. - LSP — any
LobbyFeatureorSpawnRepositoryimplementation is a drop-in replacement. - ISP —
LobbyFeatureexposes exactly three methods; nothing unused. - DIP —
FeatureManagerdepends on theLobbyFeatureinterface and aBooleanSupplier, never on concrete feature classes orLobbyConfigitself.
Patterns used
- Strategy / plugin-module pattern —
LobbyFeature+FeatureManageris a small framework for interchangeable, independently toggleable behaviors. - Repository pattern —
SpawnRepository/YamlSpawnRepository(unchanged). - Facade —
SpawnService(unchanged);LobbyConfigis a facade over raw YAML config. - Command pattern — Bukkit's
CommandExecutor, one class per command. - Composition root / manual DI —
LobbyPluginbuilds the whole object graph once at startup.
Modern JDK usage — Java 25 toolchain, records, pattern-matching
instanceof, Optional, Supplier/BooleanSupplier for lazy, always-current
config reads.
Spawn data lives in plugins/LobbyPlugin/spawn.yml, loaded once and cached in
memory. Since this lobby only ever has one world, SpawnPoint stores just
x, y, z, yaw, pitch — coordinates are always resolved against the server's
primary world (Bukkit.getWorlds().get(0)).
Targets Paper (Java 25 toolchain, paper-api) but runs unmodified on
Spigot: commands are registered the classic plugin.yml + CommandExecutor
way, and setCollidable/event cancellation are plain Bukkit API. The only
Paper-only surface is Adventure's Component for chat messages — if you need
plain Spigot support, swap those sendMessage(Component...) calls for legacy
ChatColor strings (see the command classes).
Requires JDK 25 and Gradle.
./gradlew buildThe shaded jar is produced at build/libs/lobby-*.jar.
| Permission | Default | Effect |
|---|---|---|
lobby.setspawn |
op | Allows /setspawn |
lobby.spawn |
true | Allows /spawn |
lobby.reload |
op | Allows /lobbyreload |