pumpkin-plugin-examples/rust/minigames/spleef/src/lib.rs
2026-09-05 12:45:36 +02:00

413 lines
14 KiB
Rust

//! # Spleef Minigame for Pumpkin
//!
//! A complete, runnable Spleef minigame showcasing:
//! - State management (`Waiting`, `Active`, `Ended`).
//! - Commands: `/spleef join`, `/spleef leave`, `/spleef start`, `/spleef status`, `/spleef reset`.
//! - [`BlockBreakEvent`]:
//! - Allows breaking snow blocks inside the active arena.
//! - Protects blocks outside the arena boundary or before the game starts.
//! - [`PlayerMoveEvent`]:
//! - Detects players falling below the snow platform into the elimination zone.
//! - Automatic win condition check (last player standing).
//! - Arena regeneration (`/spleef reset`).
use std::{
collections::HashSet,
sync::{Arc, Mutex},
};
use pumpkin_plugin_api::{
command::{Command, CommandError, CommandNode, CommandSender, ConsumedArgs},
commands::CommandHandler,
common::{BlockPos, Position},
events::{
EventHandler, EventPriority,
block::BlockBreakEvent,
player::PlayerMoveEvent,
},
events_wit::{BlockBreakEventData, PlayerMoveEventData},
world::BlockFlags,
Context, Plugin, PluginMetadata, register_plugin, Server,
text::TextComponent,
};
/// Spleef arena boundaries and platform heights.
#[derive(Clone, Debug)]
pub struct SpleefConfig {
pub min_x: i32,
pub max_x: i32,
pub min_z: i32,
pub max_z: i32,
pub floor_y: i32,
pub elimination_y: f64,
pub spawn_pos: Position,
pub lobby_pos: Position,
}
impl Default for SpleefConfig {
fn default() -> Self {
Self {
min_x: -12,
max_x: 12,
min_z: -12,
max_z: 12,
floor_y: 100,
elimination_y: 92.0,
spawn_pos: (0.5, 101.0, 0.5),
lobby_pos: (0.5, 115.0, 0.5),
}
}
}
impl SpleefConfig {
pub fn is_inside_floor(&self, x: i32, y: i32, z: i32) -> bool {
y == self.floor_y && x >= self.min_x && x <= self.max_x && z >= self.min_z && z <= self.max_z
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GameState {
Waiting,
Active,
Ended,
}
pub struct SpleefSession {
pub state: GameState,
pub config: SpleefConfig,
pub queued_players: HashSet<String>,
pub alive_players: HashSet<String>,
}
impl SpleefSession {
pub fn new(config: SpleefConfig) -> Self {
Self {
state: GameState::Waiting,
config,
queued_players: HashSet::new(),
alive_players: HashSet::new(),
}
}
}
static SESSION: Mutex<Option<Arc<Mutex<SpleefSession>>>> = Mutex::new(None);
fn get_session() -> Arc<Mutex<SpleefSession>> {
SESSION
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
.expect("SpleefSession must be initialized in on_load")
}
// -----------------------------------------------------------------------------
// Commands: /spleef <subcommand>
// -----------------------------------------------------------------------------
struct SpleefRootHandler;
impl CommandHandler for SpleefRootHandler {
fn handle(
&self,
sender: CommandSender,
_server: Server,
_args: ConsumedArgs,
) -> Result<i32, CommandError> {
let help_text = "Spleef Commands:\n\
/spleef join - Join the game queue\n\
/spleef leave - Leave the queue or game\n\
/spleef start - Force start the game\n\
/spleef status - View game status\n\
/spleef reset - Regenerate snow arena floor";
sender.send_message(TextComponent::text(help_text));
Ok(0)
}
}
struct SpleefJoinHandler;
impl CommandHandler for SpleefJoinHandler {
fn handle(
&self,
sender: CommandSender,
server: Server,
_args: ConsumedArgs,
) -> Result<i32, CommandError> {
let player_name = sender.get_name();
let session_arc = get_session();
let mut session = session_arc.lock().unwrap_or_else(|e| e.into_inner());
match session.state {
GameState::Active => {
sender.send_message(TextComponent::text("Game is already in progress! Please wait."));
}
GameState::Waiting | GameState::Ended => {
if session.queued_players.insert(player_name.clone()) {
let count = session.queued_players.len();
server.broadcast(&format!("{player_name} joined Spleef! ({count} players queued)"));
sender.send_message(TextComponent::text("You joined the queue! Use /spleef start when ready."));
} else {
sender.send_message(TextComponent::text("You are already in the queue!"));
}
}
}
Ok(0)
}
}
struct SpleefLeaveHandler;
impl CommandHandler for SpleefLeaveHandler {
fn handle(
&self,
sender: CommandSender,
server: Server,
_args: ConsumedArgs,
) -> Result<i32, CommandError> {
let player_name = sender.get_name();
let session_arc = get_session();
let mut session = session_arc.lock().unwrap_or_else(|e| e.into_inner());
if session.queued_players.remove(&player_name) || session.alive_players.remove(&player_name) {
server.broadcast(&format!("{player_name} left Spleef."));
sender.send_message(TextComponent::text("You left Spleef."));
} else {
sender.send_message(TextComponent::text("You are not currently in a Spleef game."));
}
Ok(0)
}
}
struct SpleefStartHandler;
impl CommandHandler for SpleefStartHandler {
fn handle(
&self,
sender: CommandSender,
server: Server,
_args: ConsumedArgs,
) -> Result<i32, CommandError> {
let session_arc = get_session();
let mut session = session_arc.lock().unwrap_or_else(|e| e.into_inner());
if session.state == GameState::Active {
sender.send_message(TextComponent::text("Game is already running!"));
return Ok(0);
}
if session.queued_players.is_empty() {
session.queued_players.insert(sender.get_name());
}
session.alive_players = session.queued_players.clone();
session.state = GameState::Active;
let count = session.alive_players.len();
server.broadcast(&format!("Spleef has STARTED with {count} players! Spleef your opponents!"));
let spawn = session.config.spawn_pos;
for player_name in &session.alive_players {
if let Some(player) = server.get_player_by_name(player_name) {
let world = player.get_world();
player.teleport(spawn, Some(0.0), Some(0.0), world);
player.show_title(TextComponent::text("SPLEEF!"));
player.show_subtitle(TextComponent::text("Break blocks beneath your rivals!"));
}
}
Ok(0)
}
}
struct SpleefStatusHandler;
impl CommandHandler for SpleefStatusHandler {
fn handle(
&self,
sender: CommandSender,
_server: Server,
_args: ConsumedArgs,
) -> Result<i32, CommandError> {
let session_arc = get_session();
let session = session_arc.lock().unwrap_or_else(|e| e.into_inner());
let status_msg = match session.state {
GameState::Waiting => format!("Spleef: WAITING ({} queued)", session.queued_players.len()),
GameState::Active => format!(
"Spleef: ACTIVE ({} alive: {:?})",
session.alive_players.len(),
session.alive_players
),
GameState::Ended => "Spleef: ENDED (Use /spleef reset or /spleef start)".to_string(),
};
sender.send_message(TextComponent::text(&status_msg));
Ok(0)
}
}
struct SpleefResetHandler;
impl CommandHandler for SpleefResetHandler {
fn handle(
&self,
sender: CommandSender,
server: Server,
_args: ConsumedArgs,
) -> Result<i32, CommandError> {
let session_arc = get_session();
let mut session = session_arc.lock().unwrap_or_else(|e| e.into_inner());
session.state = GameState::Waiting;
session.alive_players.clear();
let worlds = server.get_all_worlds();
if let Some(world) = worlds.first() {
let flags = BlockFlags::NOTIFY_LISTENERS | BlockFlags::NOTIFY_NEIGHBORS;
for x in session.config.min_x..=session.config.max_x {
for z in session.config.min_z..=session.config.max_z {
let pos = BlockPos { x, y: session.config.floor_y, z };
world.set_block_by_name(pos, "minecraft:snow_block", flags);
}
}
}
server.broadcast("Spleef arena has been regenerated and reset!");
sender.send_message(TextComponent::text("Spleef reset complete!"));
Ok(0)
}
}
// -----------------------------------------------------------------------------
// Event Listeners: BlockBreakEvent & PlayerMoveEvent
// -----------------------------------------------------------------------------
struct SpleefBreakListener;
impl EventHandler<BlockBreakEvent> for SpleefBreakListener {
fn handle(&self, _server: Server, mut event: BlockBreakEventData) -> BlockBreakEventData {
let session_arc = get_session();
let session = session_arc.lock().unwrap_or_else(|e| e.into_inner());
let pos = &event.block_pos;
// If the block broken is on the Spleef floor:
if session.config.is_inside_floor(pos.x, pos.y, pos.z) {
// Only allow breaking during Active state by alive players
if session.state == GameState::Active {
if let Some(player) = &event.player {
if session.alive_players.contains(&player.get_name()) {
event.should_drop = false; // Don't drop cluttering snowballs
event.cancelled = false;
return event;
}
}
}
// Disallow breaking floor before start or by spectators
event.cancelled = true;
}
event
}
}
struct SpleefMoveListener;
impl EventHandler<PlayerMoveEvent> for SpleefMoveListener {
fn handle(&self, server: Server, event: PlayerMoveEventData) -> PlayerMoveEventData {
let player_name = event.player.get_name();
let session_arc = get_session();
let mut session = session_arc.lock().unwrap_or_else(|e| e.into_inner());
if session.state != GameState::Active || !session.alive_players.contains(&player_name) {
return event;
}
let to = &event.to_position;
// Elimination Check: player fell below elimination threshold
if to.1 < session.config.elimination_y {
session.alive_players.remove(&player_name);
server.broadcast(&format!("{player_name} was SPLEEFED and eliminated!"));
let lobby = session.config.lobby_pos;
let world = event.player.get_world();
event.player.teleport(lobby, Some(0.0), Some(0.0), world);
event.player.show_title(TextComponent::text("SPLEEFED!"));
if session.alive_players.len() == 1 {
let winner_name = session.alive_players.iter().next().cloned().unwrap();
server.broadcast(&format!("VICTORY! {winner_name} is the last player standing and WINS Spleef!"));
if let Some(winner) = server.get_player_by_name(&winner_name) {
winner.show_title(TextComponent::text("VICTORY!"));
winner.show_subtitle(TextComponent::text("You won Spleef!"));
}
session.state = GameState::Ended;
} else if session.alive_players.is_empty() {
server.broadcast("Game Over! No players remaining.");
session.state = GameState::Ended;
}
}
event
}
}
// -----------------------------------------------------------------------------
// Plugin Lifecycle
// -----------------------------------------------------------------------------
pub struct SpleefPlugin;
impl Plugin for SpleefPlugin {
fn new() -> Self {
SpleefPlugin
}
fn metadata(&self) -> PluginMetadata {
PluginMetadata {
name: "spleef".into(),
version: env!("CARGO_PKG_VERSION").into(),
authors: vec!["Pumpkin Developer".into()],
description: "A complete Spleef minigame for Pumpkin Minecraft server.".into(),
dependencies: vec![],
permissions: vec![],
}
}
fn on_load(&self, context: Context) -> Result<(), String> {
let config = SpleefConfig::default();
let session = Arc::new(Mutex::new(SpleefSession::new(config)));
*SESSION.lock().unwrap_or_else(|e| e.into_inner()) = Some(session);
let names = ["spleef".to_string(), "sp".to_string()];
let command = Command::new(&names, "Spleef minigame management command")
.then(CommandNode::literal("join").execute(SpleefJoinHandler))
.then(CommandNode::literal("leave").execute(SpleefLeaveHandler))
.then(CommandNode::literal("start").execute(SpleefStartHandler))
.then(CommandNode::literal("status").execute(SpleefStatusHandler))
.then(CommandNode::literal("reset").execute(SpleefResetHandler))
.execute(SpleefRootHandler);
context.register_command(command, "pumpkin.command.spleef");
// Register BlockBreakEvent (blocking = true to allow/cancel break)
context.register_event_handler(
SpleefBreakListener,
EventPriority::High,
true,
)?;
// Register PlayerMoveEvent for void detection
context.register_event_handler(
SpleefMoveListener,
EventPriority::Normal,
false,
)?;
tracing::info!("Spleef minigame plugin successfully loaded!");
Ok(())
}
fn on_unload(&self, _context: Context) -> Result<(), String> {
tracing::info!("Spleef plugin unloaded.");
Ok(())
}
}
register_plugin!(SpleefPlugin);