Become independent from BattleArena

This commit is contained in:
NotAlexNoyle committed 2026-07-13 15:38:38 -04:00
1 parent c9f1685786
commit e664104257
55 files changed
+4017 -2755

No files matched your search

+11 -13
View File
@@ -1,16 +1,14 @@
# Changelog
## 2.0.1-SNAPSHOT:
## 2.0.1-SNAPSHOT
- First beta-ready snapshot of the Spleef-OG fork. Targets Purpur / Paper 1.19.4.
- **Mode scope**: Spleef-OG now supports only Classic Spleef and Bow Spleef. Unsupported projectile-cannon and TNT Run code paths and templates were removed.
- **Classic snowballs**: Breaking configured snow floor layers now grants throwable snowballs, and thrown snowballs break configured Spleef layer blocks.
- **WorldGuard region flow**: Arenas are intended to run in WorldGuard regions in the main spawn world. The default `world-whitelist` now targets `world`, and `/spleef worldguardregion <map> <region>` stores the map region ID.
- **Region whitelist**: `config.yml` now supports `region-whitelist` for limiting Spleef arena creation, joining, resets, floor breaking, projectile floor breaking, and Bow Spleef TNT handling to specific WorldGuard regions.
- **Editor layer lookup**: `SpleefMap.positionToLayers` is now maintained on `addLayer`, `removeLayer`, and `clearLayers`. Layers created via `/spleef layer add` are immediately usable; no server restart required.
- **NPE on layer removal**: `SpleefMap.removeLayer` and `clearLayers` null-guard `this.layers`, matching the lazy init in `addLayer`. `/spleef layer clear` on a fresh map no longer throws.
- **Bow Spleef projectiles**: Arrows break layer blocks in bow-spleef mode.
- **`SpleefConfig.getShovel` NPE**: Now null-safe when the configured shovel name is unknown, honoring the `@Nullable` contract. Prevents crash on typos in arena YAML (e.g. `give-shovel{shovel=clasic}`).
- **PlayerMoveEvent hot path**: `SpleefArena.onMove` now returns early when `from` and `to` share block coordinates, cutting per-tick overhead for death-region checks by a large margin.
- **Scheduler cleanup on disable**: `ArenaSpleef.onDisable` cancels all scheduler tasks owned by the plugin, preventing delayed tasks from firing against stale map references after reload.
- Gradle build updated to use the TrueOG bootstrap maven-local repository when available, falling back to `~/.m2`.
- Replaced the external arena framework with a native WorldGuard-region arena engine.
- Added persistent arena definitions, player recovery snapshots, and Spleef-owned statistics.
- Added multiple concurrent Classic and Bow Spleef arenas in whitelisted worlds.
- Added a 20-second automatic countdown once at least two players are waiting.
- Added Duels-style region setup commands for waiting, spectator, and player spawns.
- Added click-based layer and death-region editors.
- Added inventory, location, gamemode, health, experience, effect, flight, and scoreboard restoration.
- Added waiting, countdown, match, victory, spectator, floor reset, and restart recovery lifecycles.
- Renamed the plugin and API implementation packages to `net.trueog.spleefog`.
- Kept GameModeInventories-OG, HorseTp-OG, PetTeleport-OG, PlayerBounties-OG, and BattleTracker integrations.
+86 -89
View File
@@ -1,121 +1,118 @@
# Spleef-OG
A BattleArena-backed Spleef plugin for Purpur / Paper 1.19.4.
A standalone Classic and Bow Spleef plugin for Purpur / Paper 1.19.4.
Spleef-OG is maintained for TrueOG-style server-spawn minigames. It runs arenas in WorldGuard regions in the main world, not in MyWorlds world copies.
Spleef-OG runs multiple concurrent arenas in WorldGuard regions inside normal or MyWorlds-managed worlds. It does not create or copy worlds.
## Supported Modes
- **Classic Spleef**: players receive a shovel and break configured snow floor layers. Breaking `snow_block` floors grants 4 snowballs; breaking `snow` layers grants snowballs equal to the layer count. Thrown snowballs break configured Spleef layer blocks.
- **Bow Spleef**: players receive a bow and arrow. Arrows break configured layer blocks, and TNT projectile handling is gated by a configured WorldGuard region.
## Gameplay
For example configurations, see the [templates](./templates) folder.
- Classic Spleef gives each player a shovel. Broken snow floors award snowballs, and thrown snowballs break configured floor blocks.
- Bow Spleef gives each player an infinity flame bow and one arrow. Arrow impacts break configured floor blocks.
- An arena starts a 20-second countdown as soon as at least two players are waiting. The countdown pauses if fewer than two remain.
- The last surviving player wins. Matches draw when the configured time limit expires.
- Waiting players, active players, and spectators have their inventory, location, gamemode, health, experience, effects, flight state, and scoreboard restored when they leave.
- Recovery snapshots survive plugin or server restarts.
## Region Model
- Spleef arenas are intended to live in WorldGuard regions in the main `world` near server spawn.
- `config.yml` defaults `world-whitelist` to `world`. Add your real spawn-world name there if it differs.
- `region-whitelist` can restrict Spleef to specific WorldGuard region IDs. Leave it empty to allow any region in an allowed world.
- Arena creation requires standing in a WorldGuard region with `allow-spleef` set to `allow`.
- Maps can set `worldguard-region` to their region ID. Bow Spleef maps should set it to the region that covers the TNT/layer area.
- When `region-whitelist` is set, joins, layer resets, block breaks, snowball/arrow block hits, and Bow Spleef TNT handling are limited to whitelisted regions.
- Spleef-OG does not depend on MyWorlds and does not create per-match world copies.
Spleef statistics are stored in `plugins/Spleef-OG/stats.yml`. Use `/spleef stats [player]` to see wins, losses, ties, and games played.
Example setup flow:
```text
/rg define spleef_classic
/rg flag spleef_classic allow-spleef allow
/spleef create classic
/spleef layer add classic
/spleef deathregion classic
```
## Requirements
For Bow Spleef, also run:
```text
/spleef worldguardregion bowspleef spleef_bow
```
- Java 17+
- Purpur / Paper 1.19.4
- WorldGuard 7
- MyWorlds is optional. Declaring it as a soft dependency ensures managed worlds load before arena locations are read.
## Configuration
`config.yml` defaults to one allowed world:
Example `config.yml` whitelist:
```yaml
world-whitelist:
- world
region-whitelist:
- spleef_classic
- spleef_bow
```
## Compatibility
- **Server**: Purpur / Paper 1.19.4
- **Java**: 17+
- **Depends**: BattleArena 4.0.0-SNAPSHOT, WorldGuard
- **Soft-depends**: GameModeInventories-OG, HorseTp-OG, PetTeleport-OG, PlayerBounties-OG, WorldEdit
It also controls the minimum player count, 20-second waiting period, match time limit, victory delay, tools, and scoreboard.
## Arena Setup
Create a WorldGuard region and allow Spleef in it:
```text
/rg define spleef_classic
/rg flag spleef_classic allow-spleef allow
```
Stand inside the region, then configure the arena:
```text
/spleef create classic classic
/spleef setwait classic
/spleef setspectator classic
/spleef addspawn classic
/spleef addspawn classic
/spleef layer add classic
/spleef deathregion classic
/spleef info classic
```
`layer add` and `deathregion` prompt for two block clicks. Layer setup then prompts for Bukkit block data such as `minecraft:snow_block`. Arena definitions are stored in `plugins/Spleef-OG/arenas.yml`.
Use `bow` instead of `classic` in the create or mode command for Bow Spleef.
## Player Commands
| Command | Description |
|---|---|
| `/spleef arenas` | Show currently available arenas and their join commands. |
| `/spleef join` | Show currently available arenas. |
| `/spleef join <arena>` | Join a named arena. |
| `/spleef leave` | Leave the current arena or spectator session. |
| `/spleef spectate <arena>` | Spectate an arena. |
| `/spleef stats [player]` | Show Spleef statistics. |
## Administration
| Command | Description |
|---|---|
| `/spleef create <name> [classic\|bow]` | Bind a new arena to the smallest WorldGuard region containing you. |
| `/spleef delete <arena>` | Delete an inactive arena. |
| `/spleef setwait <arena>` | Set its waiting spawn. |
| `/spleef setspectator <arena>` | Set its spectator spawn. |
| `/spleef addspawn <arena>` | Add a player spawn. |
| `/spleef clearspawns <arena>` | Remove all player spawns. |
| `/spleef layer <add\|remove\|list\|clear> <arena> [index]` | Manage floor layers. |
| `/spleef deathregion <arena>` | Select the elimination volume. |
| `/spleef mode <arena> <classic\|bow>` | Change game mode. |
| `/spleef enable <arena>` | Enable an arena. |
| `/spleef disable <arena>` | Disable an inactive arena. |
| `/spleef info <arena>` | Show setup and runtime state. |
| `/spleef reload` | Reload `config.yml`. |
Player permissions are `spleef.play`, `spleef.spectate`, and `spleef.stats`. Administration uses `spleef.admin`.
## Integrations
- **PetTeleport-OG**: pet teleports are suppressed while a player is in a Spleef match. No configuration required; PetTeleport-OG reads `SpleefAPI.isInSpleef`.
- **HorseTp-OG**: horse teleports are suppressed while a player is in a Spleef match. No configuration required; HorseTp-OG reads `SpleefAPI.isInSpleef`.
- **PlayerBounties-OG**: bounty claims and new bounty placements are cancelled whenever the claimant, victim, or target is in a Spleef match.
- **GameModeInventories-OG**: gamemode-specific inventories are preserved while players enter and leave Spleef.
## Commands
| Command | Description |
|-----------------------------------------------|-----------------------------------------------------|
| `/spleef deathregion <map>` | Sets the death region for a Spleef arena. |
| `/spleef layer add <map>` | Adds a floor layer to a Spleef arena. |
| `/spleef layer remove <map> <index>` | Removes a layer from a Spleef arena. |
| `/spleef layer clear <map>` | Clears all layers from a Spleef arena. |
| `/spleef layer index <map> <from> <to>` | Changes the index of a layer. |
| `/spleef layer list <map>` | Lists all layers in a Spleef arena. |
| `/spleef worldguardregion <map> <region>` | Sets the map WorldGuard region ID. |
## Permissions
| Permission | Command |
|----------------------------------------------|---------------------------------|
| `battlearena.command.spleef.deathregion` | `/spleef deathregion` |
| `battlearena.command.spleef.layer.add` | `/spleef layer add` |
| `battlearena.command.spleef.layer.remove` | `/spleef layer remove` |
| `battlearena.command.spleef.layer.clear` | `/spleef layer clear` |
| `battlearena.command.spleef.layer.index` | `/spleef layer index` |
| `battlearena.command.spleef.layer.list` | `/spleef layer list` |
| `battlearena.command.spleef.worldguardregion` | `/spleef worldguardregion` |
- BattleTracker combat tags prevent joining. Duels and Spleef activity are excluded from BattleTracker's SMP statistics.
- GameModeInventories-OG is suspended while Spleef owns a player's temporary inventory.
- HorseTp-OG and PetTeleport-OG suppress teleports while `SpleefAPI.isInSpleef` is true.
- PlayerBounties-OG claims and placements involving Spleef players are cancelled.
## API
Spleef-OG exposes a small Bukkit-native API for other plugins. No BattleArena types on the surface, only `org.bukkit.entity.Player`.
### Check if a player is in a Spleef match
Java:
```java
import org.battleplugins.arena.spleef.api.SpleefAPI;
import net.trueog.spleefog.api.SpleefAPI;
if (SpleefAPI.isInSpleef(player)) {
// ...
// Player is waiting, playing, or spectating.
}
```
Kotlin:
```kotlin
import org.battleplugins.arena.spleef.api.SpleefAPI
if (SpleefAPI.isInSpleef(player)) {
// ...
}
```
### Events
Both extend `org.bukkit.event.player.PlayerEvent`.
| Event | Fires when | `SpleefAPI.isInSpleef(player)` during event |
|--------------------|-------------------------------------------|---------------------------------------------|
| `SpleefJoinEvent` | Player joins any Spleef-mode competition | `true` |
| `SpleefLeaveEvent` | Player leaves any Spleef-mode competition | `false` |
Covers Classic Spleef and Bow Spleef. State flips before the event fires, so listeners observe the post-transition value.
`SpleefJoinEvent` and `SpleefLeaveEvent` are available in the same package.
## Building
```text
./gradlew build
```
Output: `build/libs/Spleef-OG-<version>.jar`
## Credits
Upstream: [BattlePlugins/ArenaSpleef](https://github.com/BattlePlugins/ArenaSpleef). Documentation: [BattleDocs](https://docs.battleplugins.org/books/additional-gamemodes/chapter/spleef).
## License
Released into the public domain. See `LICENSE`.
The jar is written to `build/libs`.
+1 -4
View File
@@ -17,7 +17,7 @@ java {
}
/* ----------------------------- Metadata ------------------------------ */
group = "org.battleplugins.arena"
group = "net.trueog"
version = "2.0.1-SNAPSHOT"
@@ -37,8 +37,6 @@ repositories {
gradlePluginPortal()
maven("https://maven.enginehub.org/repo/")
maven("https://repo.papermc.io/repository/maven-public/")
maven("https://repo.battleplugins.org/releases/")
maven("https://repo.battleplugins.org/snapshots/")
maven { url = uri("file://${System.getProperty("user.home")}/.m2/repository") }
System.getProperty("SELF_MAVEN_LOCAL_REPO")?.let {
val dir = file(it)
@@ -55,7 +53,6 @@ repositories {
/* ---------------------------- Dependencies --------------------------- */
dependencies {
compileOnly("io.papermc.paper:paper-api:1.19.4-R0.1-SNAPSHOT")
compileOnly("org.battleplugins:arena:4.0.0-SNAPSHOT")
compileOnly("com.sk89q.worldguard:worldguard-bukkit:7.0.8") // Import WorldGuard API.
}
-2
View File
@@ -4,8 +4,6 @@ dependencyResolutionManagement {
repositories {
mavenCentral()
maven("https://repo.papermc.io/repository/maven-public/")
maven("https://repo.battleplugins.org/releases/")
maven("https://repo.battleplugins.org/snapshots/")
}
}
@@ -0,0 +1,19 @@
package net.trueog.spleefog;
import org.bukkit.command.CommandSender;
public final class Messages {
private static final String PREFIX = "&8[&bSpleef&8] &r";
private Messages() {
}
public static void send(CommandSender sender, String message) {
sender.sendMessage(SpleefConfig.color(PREFIX + message));
}
}
@@ -0,0 +1,142 @@
package net.trueog.spleefog;
import java.util.List;
import java.util.Locale;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
public final class SpleefConfig {
private final SpleefPlugin plugin;
public SpleefConfig(SpleefPlugin plugin) {
this.plugin = plugin;
}
public List<String> worldWhitelist() {
return this.plugin.getConfig().getStringList("world-whitelist");
}
public boolean isWorldAllowed(String worldName) {
if (worldName == null) {
return false;
}
List<String> worlds = this.worldWhitelist();
if (worlds.isEmpty()) {
return true;
}
return worlds.stream().filter(name -> name != null).anyMatch(name -> name.equalsIgnoreCase(worldName));
}
public int minimumPlayers() {
return Math.max(2, this.plugin.getConfig().getInt("match.minimum-players", 2));
}
public int waitingSeconds() {
return Math.max(1, this.plugin.getConfig().getInt("match.waiting-seconds", 20));
}
public int timeLimitSeconds() {
return Math.max(30, this.plugin.getConfig().getInt("match.time-limit-seconds", 300));
}
public int victorySeconds() {
return Math.max(1, this.plugin.getConfig().getInt("match.victory-seconds", 5));
}
public boolean scoreboardEnabled() {
return this.plugin.getConfig().getBoolean("scoreboard.enabled", true);
}
public String scoreboardTitle() {
return color(this.plugin.getConfig().getString("scoreboard.title", "&b&lSPLEEF"));
}
public ItemStack classicTool() {
return this.readItem("items.classic", Material.DIAMOND_SHOVEL);
}
public ItemStack bowTool() {
return this.readItem("items.bow", Material.BOW);
}
private ItemStack readItem(String path, Material fallback) {
ConfigurationSection section = this.plugin.getConfig().getConfigurationSection(path);
if (section == null) {
return new ItemStack(fallback);
}
Material material = Material.matchMaterial(section.getString("material", fallback.name()));
ItemStack item = new ItemStack(material == null ? fallback : material);
ItemMeta meta = item.getItemMeta();
String name = section.getString("name");
if (name != null) {
meta.setDisplayName(color(name));
}
ConfigurationSection enchantments = section.getConfigurationSection("enchantments");
if (enchantments != null) {
for (String key : enchantments.getKeys(false)) {
Enchantment enchantment = Enchantment.getByName(key.toUpperCase(Locale.ROOT));
if (enchantment != null) {
meta.addEnchant(enchantment, enchantments.getInt(key), true);
}
}
}
item.setItemMeta(meta);
return item;
}
@SuppressWarnings("deprecation")
public static String color(String message) {
return ChatColor.translateAlternateColorCodes('&', message == null ? "" : message);
}
}
@@ -0,0 +1,50 @@
package net.trueog.spleefog;
import com.sk89q.worldguard.WorldGuard;
import com.sk89q.worldguard.protection.flags.Flag;
import com.sk89q.worldguard.protection.flags.StateFlag;
import com.sk89q.worldguard.protection.flags.registry.FlagConflictException;
import com.sk89q.worldguard.protection.flags.registry.FlagRegistry;
public final class SpleefFlags {
public static final String ALLOW_SPLEEF = "allow-spleef";
private static StateFlag allowSpleefFlag;
private SpleefFlags() {
}
public static void register(SpleefPlugin plugin) {
FlagRegistry registry = WorldGuard.getInstance().getFlagRegistry();
try {
StateFlag flag = new StateFlag(ALLOW_SPLEEF, false);
registry.register(flag);
allowSpleefFlag = flag;
} catch (FlagConflictException conflict) {
Flag<?> existing = registry.get(ALLOW_SPLEEF);
if (existing instanceof StateFlag stateFlag) {
allowSpleefFlag = stateFlag;
} else {
plugin.getLogger().severe("WorldGuard flag '" + ALLOW_SPLEEF + "' has an incompatible type.");
}
}
}
public static StateFlag allowSpleef() {
return allowSpleefFlag;
}
}
@@ -0,0 +1,78 @@
package net.trueog.spleefog;
import net.trueog.spleefog.api.SpleefAPI;
import net.trueog.spleefog.arena.ArenaManager;
import net.trueog.spleefog.command.SpleefCommand;
import net.trueog.spleefog.data.ArenaRepository;
import net.trueog.spleefog.data.RecoveryStore;
import net.trueog.spleefog.data.StatsRepository;
import net.trueog.spleefog.editor.ArenaEditor;
import net.trueog.spleefog.hook.GameModeInventoriesHook;
import net.trueog.spleefog.hook.PlayerBountiesHook;
import org.bukkit.command.PluginCommand;
import org.bukkit.plugin.java.JavaPlugin;
public final class SpleefPlugin extends JavaPlugin {
private ArenaManager arenaManager;
@Override
public void onLoad() {
SpleefFlags.register(this);
}
@Override
public void onEnable() {
this.saveDefaultConfig();
SpleefConfig config = new SpleefConfig(this);
WorldGuardSupport worldGuard = new WorldGuardSupport();
ArenaRepository arenaRepository = new ArenaRepository(this);
StatsRepository stats = new StatsRepository(this);
RecoveryStore recovery = new RecoveryStore(this);
GameModeInventoriesHook gameModeInventories = new GameModeInventoriesHook(this);
this.arenaManager = new ArenaManager(this, config, worldGuard, arenaRepository, stats, recovery,
gameModeInventories);
ArenaEditor editor = new ArenaEditor(this, this.arenaManager);
this.getServer().getPluginManager().registerEvents(editor, this);
SpleefCommand commandHandler = new SpleefCommand(this, this.arenaManager, editor);
PluginCommand command = this.getCommand("spleef");
if (command == null) {
throw new IllegalStateException("The spleef command is missing from plugin.yml");
}
command.setExecutor(commandHandler);
command.setTabCompleter(commandHandler);
this.arenaManager.start();
PlayerBountiesHook.register(this);
this.getLogger().info("Loaded " + this.arenaManager.sessions().size() + " standalone Spleef arena(s).");
}
@Override
public void onDisable() {
if (this.arenaManager != null) {
this.arenaManager.shutdown();
}
SpleefAPI.clear();
}
public ArenaManager getArenaManager() {
return this.arenaManager;
}
}
@@ -0,0 +1,85 @@
package net.trueog.spleefog;
import com.sk89q.worldedit.bukkit.BukkitAdapter;
import com.sk89q.worldedit.math.BlockVector3;
import com.sk89q.worldguard.WorldGuard;
import com.sk89q.worldguard.protection.ApplicableRegionSet;
import com.sk89q.worldguard.protection.flags.StateFlag;
import com.sk89q.worldguard.protection.managers.RegionManager;
import com.sk89q.worldguard.protection.regions.ProtectedRegion;
import java.util.Comparator;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import net.trueog.spleefog.model.BlockBounds;
public final class WorldGuardSupport {
public RegionBinding findArenaRegion(Player player) {
Location location = player.getLocation();
RegionManager manager = this.manager(location.getWorld());
if (manager == null || SpleefFlags.allowSpleef() == null) {
return null;
}
BlockVector3 point = BlockVector3.at(location.getBlockX(), location.getBlockY(), location.getBlockZ());
ApplicableRegionSet applicable = manager.getApplicableRegions(point);
return applicable.getRegions().stream()
.filter(region -> !region.getId().equalsIgnoreCase(ProtectedRegion.GLOBAL_REGION))
.filter(region -> region.getFlag(SpleefFlags.allowSpleef()) == StateFlag.State.ALLOW).map(this::binding)
.min(Comparator.comparingLong(binding -> binding.bounds().volume())).orElse(null);
}
public boolean regionExists(World world, String regionId) {
RegionManager manager = this.manager(world);
return manager != null && manager.getRegion(regionId) != null;
}
public boolean isInside(Location location, String regionId) {
if (location == null || location.getWorld() == null) {
return false;
}
RegionManager manager = this.manager(location.getWorld());
ProtectedRegion region = manager == null ? null : manager.getRegion(regionId);
return region != null && region.contains(location.getBlockX(), location.getBlockY(), location.getBlockZ());
}
public BlockBounds regionBounds(World world, String regionId) {
RegionManager manager = this.manager(world);
ProtectedRegion region = manager == null ? null : manager.getRegion(regionId);
return region == null ? null : this.binding(region).bounds();
}
private RegionManager manager(World world) {
return world == null ? null
: WorldGuard.getInstance().getPlatform().getRegionContainer().get(BukkitAdapter.adapt(world));
}
private RegionBinding binding(ProtectedRegion region) {
BlockVector3 min = region.getMinimumPoint();
BlockVector3 max = region.getMaximumPoint();
return new RegionBinding(region.getId(),
new BlockBounds(min.getX(), min.getY(), min.getZ(), max.getX(), max.getY(), max.getZ()));
}
public record RegionBinding(String id, BlockBounds bounds) {
}
}
@@ -0,0 +1,41 @@
package net.trueog.spleefog.api;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import org.bukkit.entity.Player;
public final class SpleefAPI {
private static final Set<UUID> ACTIVE_PLAYERS = Collections.synchronizedSet(new HashSet<>());
private SpleefAPI() {
}
public static boolean isInSpleef(Player player) {
return player != null && ACTIVE_PLAYERS.contains(player.getUniqueId());
}
public static boolean markJoined(Player player) {
return player != null && ACTIVE_PLAYERS.add(player.getUniqueId());
}
public static boolean markLeft(Player player) {
return player != null && ACTIVE_PLAYERS.remove(player.getUniqueId());
}
public static void clear() {
ACTIVE_PLAYERS.clear();
}
}
@@ -0,0 +1,47 @@
package net.trueog.spleefog.api;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import org.bukkit.event.player.PlayerEvent;
import org.jetbrains.annotations.NotNull;
public final class SpleefJoinEvent extends PlayerEvent {
private static final HandlerList HANDLERS = new HandlerList();
private final String arenaName;
private final boolean spectator;
public SpleefJoinEvent(Player player, String arenaName, boolean spectator) {
super(player);
this.arenaName = arenaName;
this.spectator = spectator;
}
public String getArenaName() {
return this.arenaName;
}
public boolean isSpectator() {
return this.spectator;
}
@Override
public @NotNull HandlerList getHandlers() {
return HANDLERS;
}
public static HandlerList getHandlerList() {
return HANDLERS;
}
}
@@ -1,23 +1,30 @@
package org.battleplugins.arena.spleef.api;
package net.trueog.spleefog.api;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import org.bukkit.event.player.PlayerEvent;
import org.jetbrains.annotations.NotNull;
public class SpleefLeaveEvent extends PlayerEvent {
public final class SpleefLeaveEvent extends PlayerEvent {
private static final HandlerList HANDLERS = new HandlerList();
private final String arenaName;
public SpleefLeaveEvent(Player player) {
public SpleefLeaveEvent(Player player, String arenaName) {
super(player);
this.arenaName = arenaName;
}
public String getArenaName() {
return this.arenaName;
}
@NotNull
@Override
public HandlerList getHandlers() {
public @NotNull HandlerList getHandlers() {
return HANDLERS;
@@ -0,0 +1,857 @@
package net.trueog.spleefog.arena;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import net.trueog.spleefog.Messages;
import net.trueog.spleefog.SpleefConfig;
import net.trueog.spleefog.SpleefPlugin;
import net.trueog.spleefog.WorldGuardSupport;
import net.trueog.spleefog.api.SpleefAPI;
import net.trueog.spleefog.api.SpleefJoinEvent;
import net.trueog.spleefog.api.SpleefLeaveEvent;
import net.trueog.spleefog.data.ArenaRepository;
import net.trueog.spleefog.data.RecoveryStore;
import net.trueog.spleefog.data.StatsRepository;
import net.trueog.spleefog.hook.GameModeInventoriesHook;
import net.trueog.spleefog.hook.BattleTrackerHook;
import net.trueog.spleefog.model.ArenaState;
import net.trueog.spleefog.model.GameType;
import net.trueog.spleefog.model.SpleefArena;
import net.trueog.spleefog.model.SpleefLayer;
import net.trueog.spleefog.player.PlayerSnapshot;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.attribute.Attribute;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.type.Snow;
import org.bukkit.entity.AbstractArrow;
import org.bukkit.entity.Player;
import org.bukkit.entity.Projectile;
import org.bukkit.entity.Snowball;
import org.bukkit.entity.TNTPrimed;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.block.TNTPrimeEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.EntityExplodeEvent;
import org.bukkit.event.entity.EntityPickupItemEvent;
import org.bukkit.event.entity.FoodLevelChangeEvent;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.entity.ProjectileHitEvent;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerRespawnEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.potion.PotionEffect;
import org.bukkit.projectiles.ProjectileSource;
import org.bukkit.scoreboard.Criteria;
import org.bukkit.scoreboard.DisplaySlot;
import org.bukkit.scoreboard.Objective;
import org.bukkit.scoreboard.Scoreboard;
import org.bukkit.scheduler.BukkitTask;
public final class ArenaManager implements Listener {
private final SpleefPlugin plugin;
private final SpleefConfig config;
private final WorldGuardSupport worldGuard;
private final ArenaRepository arenaRepository;
private final StatsRepository stats;
private final RecoveryStore recovery;
private final GameModeInventoriesHook gameModeInventories;
private final BattleTrackerHook battleTracker;
private final Map<String, ArenaSession> sessions = new HashMap<>();
private final Map<UUID, ArenaSession> playerSessions = new HashMap<>();
private final Map<UUID, ArenaSession> pendingRespawns = new HashMap<>();
private final Set<UUID> forcedDeaths = new HashSet<>();
private BukkitTask tickTask;
public ArenaManager(SpleefPlugin plugin, SpleefConfig config, WorldGuardSupport worldGuard,
ArenaRepository arenaRepository, StatsRepository stats, RecoveryStore recovery,
GameModeInventoriesHook gameModeInventories)
{
this.plugin = plugin;
this.config = config;
this.worldGuard = worldGuard;
this.arenaRepository = arenaRepository;
this.stats = stats;
this.recovery = recovery;
this.gameModeInventories = gameModeInventories;
this.battleTracker = new BattleTrackerHook(plugin);
for (SpleefArena arena : arenaRepository.load()) {
this.sessions.put(normalize(arena.name()), new ArenaSession(this, arena));
}
}
public void start() {
Bukkit.getPluginManager().registerEvents(this, this.plugin);
for (Player player : Bukkit.getOnlinePlayers()) {
this.restoreRecovery(player);
}
for (ArenaSession session : this.sessions.values()) {
if (this.isArenaRuntimeValid(session.arena())) {
this.resetLayers(session.arena());
}
}
this.tickTask = Bukkit.getScheduler().runTaskTimer(this.plugin,
() -> new ArrayList<>(this.sessions.values()).forEach(ArenaSession::tick), 20L, 20L);
}
public void shutdown() {
if (this.tickTask != null) {
this.tickTask.cancel();
}
for (ArenaSession session : new ArrayList<>(this.sessions.values())) {
session.shutdown();
}
this.playerSessions.clear();
this.pendingRespawns.clear();
this.forcedDeaths.clear();
this.stats.save();
this.gameModeInventories.releaseAll();
}
public SpleefConfig config() {
return this.config;
}
public StatsRepository stats() {
return this.stats;
}
public ArenaSession session(Player player) {
return player == null ? null : this.playerSessions.get(player.getUniqueId());
}
public ArenaSession get(String name) {
return name == null ? null : this.sessions.get(normalize(name));
}
public boolean isInCombat(Player player) {
return this.battleTracker.isInCombat(player);
}
public List<ArenaSession> sessions() {
return this.sessions.values().stream().sorted(Comparator.comparing(value -> value.arena().name())).toList();
}
public List<ArenaSession> availableSessions() {
return this.sessions().stream().filter(ArenaSession::canJoin).toList();
}
public CreateResult create(Player player, String name, GameType gameType) {
if (!name.matches("[A-Za-z0-9_-]+")) {
return new CreateResult(false, "Arena names may contain letters, numbers, underscores, and hyphens.", null);
}
if (this.get(name) != null) {
return new CreateResult(false, "An arena with that name already exists.", null);
}
if (!this.config.isWorldAllowed(player.getWorld().getName())) {
return new CreateResult(false, "World &b" + player.getWorld().getName() + " &7is not whitelisted.", null);
}
WorldGuardSupport.RegionBinding region = this.worldGuard.findArenaRegion(player);
if (region == null) {
return new CreateResult(false,
"Stand inside a WorldGuard region with &ballow-spleef=allow &7before creating an arena.", null);
}
SpleefArena arena = new SpleefArena(name, player.getWorld().getName(), region.id(), region.bounds(), gameType);
this.sessions.put(normalize(name), new ArenaSession(this, arena));
this.saveArenas();
return new CreateResult(true, "Created &b" + name + " &7in region &b" + region.id() + "&7.", arena);
}
public boolean delete(SpleefArena arena) {
ArenaSession session = this.get(arena.name());
if (session == null || session.isActive()) {
return false;
}
this.sessions.remove(normalize(arena.name()));
this.saveArenas();
return true;
}
public void saveArenas() {
this.arenaRepository.save(this.sessions.values().stream().map(ArenaSession::arena).toList());
}
public boolean isArenaRuntimeValid(SpleefArena arena) {
World world = Bukkit.getWorld(arena.worldName());
return world != null && this.config.isWorldAllowed(arena.worldName())
&& this.worldGuard.regionExists(world, arena.regionId());
}
public boolean isSetupLocationValid(SpleefArena arena, Location location) {
return location != null && location.getWorld() != null
&& location.getWorld().getName().equalsIgnoreCase(arena.worldName())
&& this.worldGuard.isInside(location, arena.regionId());
}
boolean enter(Player player, ArenaSession session, boolean spectator, Location destination) {
if (destination == null || destination.getWorld() == null || this.recovery.contains(player.getUniqueId())) {
return false;
}
this.recovery.capture(player);
this.gameModeInventories.suspend(player);
this.playerSessions.put(player.getUniqueId(), session);
SpleefAPI.markJoined(player);
this.preparePlayer(player, spectator ? GameMode.SPECTATOR : GameMode.ADVENTURE, destination);
Bukkit.getPluginManager().callEvent(new SpleefJoinEvent(player, session.arena().name(), spectator));
return true;
}
void exit(Player player, ArenaSession expectedSession) {
ArenaSession current = this.playerSessions.get(player.getUniqueId());
if (current != expectedSession) {
return;
}
this.playerSessions.remove(player.getUniqueId());
this.pendingRespawns.remove(player.getUniqueId());
this.forcedDeaths.remove(player.getUniqueId());
SpleefAPI.markLeft(player);
Bukkit.getPluginManager().callEvent(new SpleefLeaveEvent(player, expectedSession.arena().name()));
PlayerSnapshot snapshot = this.recovery.get(player.getUniqueId());
if (snapshot != null && !player.isDead()) {
try {
snapshot.restore(player);
this.recovery.remove(player.getUniqueId());
} catch (RuntimeException ex) {
this.plugin.getLogger().log(java.util.logging.Level.SEVERE,
"Could not restore " + player.getName() + " after Spleef; recovery data was retained.", ex);
}
} else {
player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard());
}
Bukkit.getScheduler().runTask(this.plugin, () -> this.gameModeInventories.release(player));
}
void startPlayer(Player player, SpleefArena arena, Location spawn) {
this.preparePlayer(player, GameMode.SURVIVAL, spawn);
if (arena.gameType() == GameType.CLASSIC) {
player.getInventory().setItem(0, this.config.classicTool());
} else {
player.getInventory().setItem(0, this.config.bowTool());
player.getInventory().setItem(9, new ItemStack(Material.ARROW));
}
player.updateInventory();
}
void showEnding(ArenaSession session) {
for (UUID playerId : session.allPresent()) {
Player player = Bukkit.getPlayer(playerId);
if (player != null && !player.isDead()) {
this.preparePlayer(player, GameMode.ADVENTURE, session.arena().waitingSpawn());
}
}
}
void resetLayers(SpleefArena arena) {
World world = Bukkit.getWorld(arena.worldName());
if (world == null) {
return;
}
for (SpleefLayer layer : arena.layers()) {
for (int x = layer.bounds().minX(); x <= layer.bounds().maxX(); x++) {
for (int y = layer.bounds().minY(); y <= layer.bounds().maxY(); y++) {
for (int z = layer.bounds().minZ(); z <= layer.bounds().maxZ(); z++) {
Location location = new Location(world, x, y, z);
if (this.worldGuard.isInside(location, arena.regionId())) {
world.getBlockAt(x, y, z).setBlockData(layer.blockData(), false);
}
}
}
}
}
}
void updateScoreboards(ArenaSession session) {
if (!this.config.scoreboardEnabled()) {
return;
}
for (UUID playerId : session.allPresent()) {
Player player = Bukkit.getPlayer(playerId);
if (player != null) {
player.setScoreboard(this.createScoreboard(session));
}
}
}
private Scoreboard createScoreboard(ArenaSession session) {
Scoreboard board = Bukkit.getScoreboardManager().getNewScoreboard();
Objective objective = board.registerNewObjective("spleef", Criteria.DUMMY, this.config.scoreboardTitle());
objective.setDisplaySlot(DisplaySlot.SIDEBAR);
objective.getScore(ChatColor.GRAY + "Arena: " + ChatColor.WHITE + session.arena().name()).setScore(5);
objective.getScore(ChatColor.GRAY + "Mode: " + ChatColor.WHITE + display(session.arena().gameType()))
.setScore(4);
objective.getScore(ChatColor.GRAY + "State: " + ChatColor.WHITE + display(session.state())).setScore(3);
objective
.getScore(ChatColor.GRAY + "Players: " + ChatColor.AQUA
+ (session.state() == ArenaState.IN_GAME ? session.aliveCount() : session.playerCount()))
.setScore(2);
String timer = session.state() == ArenaState.WAITING ? "Waiting" : formatTime(session.secondsRemaining());
objective.getScore(ChatColor.GRAY + "Time: " + ChatColor.AQUA + timer).setScore(1);
return board;
}
private void preparePlayer(Player player, GameMode gameMode, Location destination) {
player.closeInventory();
player.getInventory().clear();
player.getInventory().setArmorContents(new ItemStack[4]);
player.getInventory().setItemInOffHand(null);
for (PotionEffect effect : player.getActivePotionEffects()) {
player.removePotionEffect(effect.getType());
}
player.setFireTicks(0);
player.setFallDistance(0.0F);
player.setHealth(player.getAttribute(Attribute.GENERIC_MAX_HEALTH).getValue());
player.setFoodLevel(20);
player.setSaturation(5.0F);
player.setAllowFlight(gameMode == GameMode.SPECTATOR);
player.setGameMode(gameMode);
player.teleport(destination);
}
private void restoreRecovery(Player player) {
if (this.playerSessions.containsKey(player.getUniqueId())) {
return;
}
PlayerSnapshot snapshot = this.recovery.get(player.getUniqueId());
if (snapshot != null) {
this.gameModeInventories.suspend(player);
try {
snapshot.restore(player);
this.recovery.remove(player.getUniqueId());
Messages.send(player, "&7Your pre-Spleef state was recovered after a restart.");
} catch (RuntimeException ex) {
this.plugin.getLogger().log(java.util.logging.Level.SEVERE,
"Could not recover " + player.getName() + " after a restart; recovery data was retained.", ex);
} finally {
Bukkit.getScheduler().runTask(this.plugin, () -> this.gameModeInventories.release(player));
}
}
}
private ArenaSession activeSessionAt(Location location) {
return this.sessions.values().stream().filter(ArenaSession::isActive)
.filter(session -> this.worldGuard.isInside(location, session.arena().regionId())).findFirst()
.orElse(null);
}
private SpleefLayer layerAt(SpleefArena arena, Location location) {
if (!this.worldGuard.isInside(location, arena.regionId())) {
return null;
}
return arena.layers().stream().filter(layer -> layer.bounds().contains(location)).findFirst().orElse(null);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
public void onBlockBreak(BlockBreakEvent event) {
Player player = event.getPlayer();
ArenaSession session = this.session(player);
ArenaSession regionSession = this.activeSessionAt(event.getBlock().getLocation());
if (session == null && regionSession == null) {
return;
}
event.setCancelled(true);
if (session == null || session.state() != ArenaState.IN_GAME || !session.isAlive(player.getUniqueId())
|| session.arena().gameType() != GameType.CLASSIC)
{
return;
}
SpleefLayer layer = this.layerAt(session.arena(), event.getBlock().getLocation());
if (layer == null) {
return;
}
event.setCancelled(false);
event.setDropItems(false);
int snowballs = snowballCount(event.getBlock());
if (snowballs > 0) {
player.getInventory().addItem(new ItemStack(Material.SNOWBALL, snowballs));
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
public void onBlockPlace(BlockPlaceEvent event) {
if (this.session(event.getPlayer()) != null) {
event.setCancelled(true);
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
public void onInteract(PlayerInteractEvent event) {
ArenaSession session = this.session(event.getPlayer());
if (session != null && session.state() != ArenaState.IN_GAME) {
event.setCancelled(true);
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
public void onDrop(PlayerDropItemEvent event) {
if (this.session(event.getPlayer()) != null) {
event.setCancelled(true);
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
public void onPickup(EntityPickupItemEvent event) {
if (event.getEntity() instanceof Player player && this.session(player) != null) {
event.setCancelled(true);
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
public void onInventoryClick(InventoryClickEvent event) {
if (event.getWhoClicked() instanceof Player player && this.session(player) != null) {
event.setCancelled(true);
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
public void onDamage(EntityDamageEvent event) {
if (!(event.getEntity() instanceof Player player) || this.session(player) == null) {
return;
}
if (!this.forcedDeaths.contains(player.getUniqueId())) {
event.setCancelled(true);
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
public void onFood(FoodLevelChangeEvent event) {
if (event.getEntity() instanceof Player player && this.session(player) != null) {
event.setCancelled(true);
}
}
@EventHandler(priority = EventPriority.LOW)
public void onMove(PlayerMoveEvent event) {
if (event.getTo() == null || sameBlock(event.getFrom(), event.getTo())) {
return;
}
ArenaSession session = this.session(event.getPlayer());
if (session == null || !session.isAlive(event.getPlayer().getUniqueId())) {
return;
}
if (session.arena().deathRegion().contains(event.getTo())
|| !this.worldGuard.isInside(event.getTo(), session.arena().regionId()))
{
this.forcedDeaths.add(event.getPlayer().getUniqueId());
event.getPlayer().setHealth(0.0D);
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
public void onDeath(PlayerDeathEvent event) {
Player player = event.getEntity();
ArenaSession session = this.session(player);
if (session == null || !session.isAlive(player.getUniqueId())) {
return;
}
event.setKeepInventory(true);
event.setKeepLevel(true);
event.getDrops().clear();
event.deathMessage(null);
this.forcedDeaths.remove(player.getUniqueId());
this.pendingRespawns.put(player.getUniqueId(), session);
session.eliminate(player);
Bukkit.getScheduler().runTask(this.plugin, () -> {
if (player.isOnline() && player.isDead()) {
player.spigot().respawn();
}
});
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onRespawn(PlayerRespawnEvent event) {
ArenaSession session = this.pendingRespawns.remove(event.getPlayer().getUniqueId());
if (session == null) {
if (this.recovery.contains(event.getPlayer().getUniqueId())) {
Bukkit.getScheduler().runTask(this.plugin, () -> this.restoreRecovery(event.getPlayer()));
}
return;
}
Location destination = session.state() == ArenaState.IN_GAME ? session.arena().spectatorSpawn()
: session.arena().waitingSpawn();
event.setRespawnLocation(destination);
Bukkit.getScheduler().runTask(this.plugin, () -> {
if (this.session(event.getPlayer()) == session) {
this.preparePlayer(event.getPlayer(),
session.state() == ArenaState.IN_GAME ? GameMode.SPECTATOR : GameMode.ADVENTURE, destination);
} else {
this.restoreRecovery(event.getPlayer());
}
});
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onProjectileHit(ProjectileHitEvent event) {
Block block = event.getHitBlock();
Projectile projectile = event.getEntity();
ProjectileSource source = projectile.getShooter();
if (block == null || !(source instanceof Player player)) {
return;
}
ArenaSession session = this.session(player);
if (session == null || !session.isAlive(player.getUniqueId()) || session.state() != ArenaState.IN_GAME) {
return;
}
boolean validProjectile = session.arena().gameType() == GameType.CLASSIC && projectile instanceof Snowball
|| session.arena().gameType() == GameType.BOW && projectile instanceof AbstractArrow;
if (!validProjectile || this.layerAt(session.arena(), block.getLocation()) == null) {
return;
}
block.setType(Material.AIR, false);
projectile.remove();
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
public void onTntPrime(TNTPrimeEvent event) {
ArenaSession session = this.activeSessionAt(event.getBlock().getLocation());
if (session == null || session.state() != ArenaState.IN_GAME || session.arena().gameType() != GameType.BOW
|| this.layerAt(session.arena(), event.getBlock().getLocation()) == null)
{
return;
}
event.setCancelled(true);
event.getBlock().setType(Material.AIR, false);
TNTPrimed visual = event.getBlock().getWorld().spawn(event.getBlock().getLocation().toCenterLocation(),
TNTPrimed.class, tnt -> tnt.setFuseTicks(Integer.MAX_VALUE));
Bukkit.getScheduler().runTaskLater(this.plugin, visual::remove, 20L);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
public void onExplosion(EntityExplodeEvent event) {
event.blockList().removeIf(block -> {
ArenaSession session = this.activeSessionAt(block.getLocation());
return session != null && this.layerAt(session.arena(), block.getLocation()) != null;
});
}
@EventHandler(priority = EventPriority.MONITOR)
public void onQuit(PlayerQuitEvent event) {
ArenaSession session = this.session(event.getPlayer());
if (session != null) {
session.leave(event.getPlayer());
}
}
@EventHandler(priority = EventPriority.MONITOR)
public void onJoin(PlayerJoinEvent event) {
Bukkit.getScheduler().runTask(this.plugin, () -> this.restoreRecovery(event.getPlayer()));
}
private static int snowballCount(Block block) {
if (block.getType() == Material.SNOW_BLOCK) {
return 4;
}
BlockData data = block.getBlockData();
if (data instanceof Snow snow) {
return snow.getLayers();
}
return 0;
}
private static boolean sameBlock(Location first, Location second) {
return first.getBlockX() == second.getBlockX() && first.getBlockY() == second.getBlockY()
&& first.getBlockZ() == second.getBlockZ();
}
private static String formatTime(int seconds) {
return String.format(Locale.ROOT, "%d:%02d", Math.max(0, seconds) / 60, Math.max(0, seconds) % 60);
}
private static String display(Enum<?> value) {
String name = value.name().toLowerCase(Locale.ROOT).replace('_', ' ');
return Character.toUpperCase(name.charAt(0)) + name.substring(1);
}
private static String normalize(String name) {
return name.toLowerCase(Locale.ROOT);
}
public record CreateResult(boolean success, String message, SpleefArena arena) {
}
}
@@ -0,0 +1,480 @@
package net.trueog.spleefog.arena;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import net.trueog.spleefog.Messages;
import net.trueog.spleefog.model.ArenaState;
import net.trueog.spleefog.model.SpleefArena;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
public final class ArenaSession {
private final ArenaManager manager;
private final SpleefArena arena;
private final Set<UUID> players = new LinkedHashSet<>();
private final Set<UUID> spectators = new LinkedHashSet<>();
private final Set<UUID> alive = new LinkedHashSet<>();
private final Set<UUID> matchPlayers = new LinkedHashSet<>();
private final Set<UUID> eliminated = new LinkedHashSet<>();
private final Map<UUID, String> playerNames = new LinkedHashMap<>();
private ArenaState state = ArenaState.WAITING;
private int secondsRemaining;
ArenaSession(ArenaManager manager, SpleefArena arena) {
this.manager = manager;
this.arena = arena;
}
public SpleefArena arena() {
return this.arena;
}
public ArenaState state() {
return this.state;
}
public int secondsRemaining() {
return this.secondsRemaining;
}
public int playerCount() {
return this.players.size();
}
public int spectatorCount() {
return this.spectators.size();
}
public int aliveCount() {
return this.alive.size();
}
public boolean hasPlayer(UUID playerId) {
return this.players.contains(playerId);
}
public boolean hasSpectator(UUID playerId) {
return this.spectators.contains(playerId);
}
public boolean isAlive(UUID playerId) {
return this.state == ArenaState.IN_GAME && this.alive.contains(playerId);
}
public boolean isActive() {
return !this.players.isEmpty() || !this.spectators.isEmpty();
}
public boolean canJoin() {
return this.arena.enabled() && this.arena.isComplete()
&& (this.state == ArenaState.WAITING || this.state == ArenaState.COUNTDOWN)
&& this.players.size() < this.arena.capacity() && this.manager.isArenaRuntimeValid(this.arena);
}
public boolean join(Player player) {
if (!this.canJoin() || this.manager.session(player) != null) {
return false;
}
if (this.manager.isInCombat(player)) {
Messages.send(player, "&cYou cannot join Spleef while combat tagged.");
return false;
}
if (!this.manager.enter(player, this, false, this.arena.waitingSpawn())) {
return false;
}
this.players.add(player.getUniqueId());
this.playerNames.put(player.getUniqueId(), player.getName());
this.broadcast(
"&f" + player.getName() + " &7joined. &b(" + this.players.size() + "/" + this.arena.capacity() + ")");
return true;
}
public boolean spectate(Player player) {
if (!this.arena.enabled() || !this.arena.isComplete() || this.state == ArenaState.ENDING
|| this.manager.session(player) != null || !this.manager.isArenaRuntimeValid(this.arena))
{
return false;
}
if (!this.manager.enter(player, this, true, this.arena.spectatorSpawn())) {
return false;
}
this.spectators.add(player.getUniqueId());
return true;
}
public void leave(Player player) {
UUID playerId = player.getUniqueId();
boolean participant = this.players.remove(playerId);
boolean spectator = this.spectators.remove(playerId);
if (!participant && !spectator) {
return;
}
if (participant && this.state == ArenaState.IN_GAME && this.matchPlayers.contains(playerId)) {
this.alive.remove(playerId);
this.eliminated.add(playerId);
}
this.manager.exit(player, this);
if (participant && (this.state == ArenaState.WAITING || this.state == ArenaState.COUNTDOWN)) {
this.broadcast(
"&f" + player.getName() + " &7left. &b(" + this.players.size() + "/" + this.arena.capacity() + ")");
}
if (this.state == ArenaState.IN_GAME) {
this.checkForWinner();
}
}
public void eliminate(Player player) {
UUID playerId = player.getUniqueId();
if (!this.alive.remove(playerId)) {
return;
}
this.eliminated.add(playerId);
this.broadcast("&f" + player.getName() + " &7was eliminated. &b" + this.alive.size() + " &7remain.");
this.checkForWinner();
}
public void tick() {
switch (this.state) {
case WAITING -> this.tickWaiting();
case COUNTDOWN -> this.tickCountdown();
case IN_GAME -> this.tickGame();
case ENDING -> this.tickEnding();
}
this.manager.updateScoreboards(this);
}
public void shutdown() {
for (UUID playerId : this.allPresent()) {
Player player = Bukkit.getPlayer(playerId);
if (player != null) {
this.manager.exit(player, this);
}
}
this.clearRuntime();
this.manager.resetLayers(this.arena);
}
private void tickWaiting() {
if (this.players.size() < this.manager.config().minimumPlayers()) {
return;
}
this.state = ArenaState.COUNTDOWN;
this.secondsRemaining = this.manager.config().waitingSeconds();
this.broadcast("&7Match starts in &b" + this.secondsRemaining + " seconds&7.");
}
private void tickCountdown() {
if (this.players.size() < this.manager.config().minimumPlayers()) {
this.state = ArenaState.WAITING;
this.secondsRemaining = 0;
this.broadcast("&7Countdown paused until at least &b" + this.manager.config().minimumPlayers()
+ " players &7are waiting.");
return;
}
if (this.secondsRemaining <= 1) {
this.startMatch();
return;
}
this.secondsRemaining--;
if (this.secondsRemaining <= 5 || this.secondsRemaining == 10 || this.secondsRemaining == 15) {
this.broadcast("&7Match starts in &b" + this.secondsRemaining + " seconds&7.");
}
}
private void startMatch() {
if (!this.canStart()) {
this.state = ArenaState.WAITING;
return;
}
this.manager.resetLayers(this.arena);
this.state = ArenaState.IN_GAME;
this.secondsRemaining = this.manager.config().timeLimitSeconds();
this.alive.clear();
this.alive.addAll(this.players);
this.matchPlayers.clear();
this.matchPlayers.addAll(this.players);
this.eliminated.clear();
List<org.bukkit.Location> spawns = this.arena.spawns();
int spawnIndex = 0;
for (UUID playerId : this.players) {
Player player = Bukkit.getPlayer(playerId);
if (player != null) {
this.manager.startPlayer(player, this.arena, spawns.get(spawnIndex++));
}
}
this.broadcast("&aGo!");
}
private void tickGame() {
if (this.secondsRemaining <= 1) {
this.finish(null, true);
return;
}
this.secondsRemaining--;
if (this.secondsRemaining <= 10 || this.secondsRemaining == 30 || this.secondsRemaining == 60) {
this.broadcast("&b" + this.secondsRemaining + " seconds &7remaining.");
}
}
private void tickEnding() {
if (this.secondsRemaining <= 1) {
this.resetAfterMatch();
return;
}
this.secondsRemaining--;
}
private boolean canStart() {
return this.players.size() >= this.manager.config().minimumPlayers()
&& this.players.size() <= this.arena.capacity() && this.manager.isArenaRuntimeValid(this.arena);
}
private void checkForWinner() {
if (this.state != ArenaState.IN_GAME || this.alive.size() > 1) {
return;
}
UUID winner = this.alive.stream().findFirst().orElse(null);
this.finish(winner, false);
}
private void finish(UUID winner, boolean timedOut) {
if (this.state != ArenaState.IN_GAME) {
return;
}
this.state = ArenaState.ENDING;
this.secondsRemaining = this.manager.config().victorySeconds();
for (UUID playerId : this.matchPlayers) {
String name = this.playerNames.getOrDefault(playerId, "Unknown");
if (playerId.equals(winner)) {
this.manager.stats().recordWin(playerId, name);
} else if (timedOut && this.alive.contains(playerId)) {
this.manager.stats().recordTie(playerId, name);
} else {
this.manager.stats().recordLoss(playerId, name);
}
}
this.manager.stats().save();
if (winner != null) {
String winnerName = this.playerNames.getOrDefault(winner, "Unknown");
this.broadcast("&a" + winnerName + " won the match!");
} else if (timedOut && !this.alive.isEmpty()) {
this.broadcast("&eTime expired. The match is a draw.");
} else {
this.broadcast("&eThe match ended without a winner.");
}
this.manager.showEnding(this);
}
private void resetAfterMatch() {
for (UUID playerId : this.allPresent()) {
Player player = Bukkit.getPlayer(playerId);
if (player != null) {
this.manager.exit(player, this);
}
}
this.clearRuntime();
this.manager.resetLayers(this.arena);
}
private void clearRuntime() {
this.players.clear();
this.spectators.clear();
this.alive.clear();
this.matchPlayers.clear();
this.eliminated.clear();
this.playerNames.clear();
this.state = ArenaState.WAITING;
this.secondsRemaining = 0;
}
public Set<UUID> allPresent() {
Set<UUID> result = new LinkedHashSet<>(this.players);
result.addAll(this.spectators);
return result;
}
public Set<UUID> players() {
return Set.copyOf(this.players);
}
public Set<UUID> spectators() {
return Set.copyOf(this.spectators);
}
private void broadcast(String message) {
for (UUID playerId : this.allPresent()) {
Player player = Bukkit.getPlayer(playerId);
if (player != null) {
Messages.send(player, message);
}
}
}
}
@@ -0,0 +1,644 @@
package net.trueog.spleefog.command;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import net.trueog.spleefog.Messages;
import net.trueog.spleefog.SpleefPlugin;
import net.trueog.spleefog.arena.ArenaManager;
import net.trueog.spleefog.arena.ArenaSession;
import net.trueog.spleefog.data.StatsRepository.PlayerStats;
import net.trueog.spleefog.editor.ArenaEditor;
import net.trueog.spleefog.model.GameType;
import net.trueog.spleefog.model.SpleefArena;
import net.trueog.spleefog.model.SpleefLayer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
public final class SpleefCommand implements CommandExecutor, TabCompleter {
private final SpleefPlugin plugin;
private final ArenaManager arenaManager;
private final ArenaEditor editor;
public SpleefCommand(SpleefPlugin plugin, ArenaManager arenaManager, ArenaEditor editor) {
this.plugin = plugin;
this.arenaManager = arenaManager;
this.editor = editor;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args)
{
if (args.length == 0) {
this.help(sender);
return true;
}
String subcommand = args[0].toLowerCase(Locale.ROOT);
switch (subcommand) {
case "arenas" -> this.showAvailable(sender);
case "join" -> this.join(sender, args);
case "leave" -> this.leave(sender);
case "spectate", "spec" -> this.spectate(sender, args);
case "stats" -> this.stats(sender, args);
case "create" -> this.create(sender, args);
case "delete" -> this.delete(sender, args);
case "setwait" -> this.setLocation(sender, args, LocationType.WAITING);
case "setspectator" -> this.setLocation(sender, args, LocationType.SPECTATOR);
case "addspawn" -> this.setLocation(sender, args, LocationType.PLAYER);
case "clearspawns" -> this.clearSpawns(sender, args);
case "mode" -> this.mode(sender, args);
case "enable", "disable" -> this.enabled(sender, args, subcommand.equals("enable"));
case "layer" -> this.layer(sender, args);
case "deathregion" -> this.deathRegion(sender, args);
case "info" -> this.info(sender, args);
case "cancel" -> this.cancel(sender);
case "reload" -> this.reload(sender);
default -> this.help(sender);
}
return true;
}
private void join(CommandSender sender, String[] args) {
Player player = this.player(sender);
if (player == null || !this.require(sender, "spleef.play")) {
return;
}
if (args.length < 2) {
this.showAvailable(sender);
return;
}
ArenaSession session = this.arenaManager.get(args[1]);
if (session == null) {
Messages.send(sender, "&cArena not found. Use &b/spleef arenas &cto see available arenas.");
} else if (!session.join(player)) {
Messages.send(sender, "&cThat arena is unavailable, full, or already in progress.");
}
}
private void leave(CommandSender sender) {
Player player = this.player(sender);
if (player == null) {
return;
}
ArenaSession session = this.arenaManager.session(player);
if (session == null) {
Messages.send(sender, "&cYou are not in Spleef.");
return;
}
session.leave(player);
Messages.send(sender, "&7You left Spleef.");
}
private void spectate(CommandSender sender, String[] args) {
Player player = this.player(sender);
if (player == null || !this.require(sender, "spleef.spectate")) {
return;
}
if (args.length < 2) {
Messages.send(sender, "&7Usage: &b/spleef spectate <arena>");
return;
}
ArenaSession session = this.arenaManager.get(args[1]);
if (session == null || !session.spectate(player)) {
Messages.send(sender, "&cThat arena cannot be spectated right now.");
}
}
private void showAvailable(CommandSender sender) {
List<ArenaSession> available = this.arenaManager.availableSessions();
if (available.isEmpty()) {
Messages.send(sender, "&7No arenas are currently available.");
return;
}
Messages.send(sender, "&7Available arenas:");
for (ArenaSession session : available) {
Messages.send(sender,
"&b" + session.arena().name() + " &8- &7" + session.arena().gameType().name() + " &8- &7"
+ session.playerCount() + "/" + session.arena().capacity() + " &8- &f/spleef join "
+ session.arena().name());
}
}
private void stats(CommandSender sender, String[] args) {
if (!this.require(sender, "spleef.stats")) {
return;
}
PlayerStats stats;
if (args.length < 2) {
Player player = this.player(sender);
if (player == null) {
return;
}
stats = this.arenaManager.stats().get(player.getUniqueId(), player.getName());
} else {
stats = this.arenaManager.stats().findByName(args[1]);
if (stats == null) {
Messages.send(sender, "&cNo Spleef statistics were found for that player.");
return;
}
}
Messages.send(sender, "&b" + stats.name() + "&7: &a" + stats.wins() + " wins&7, &c" + stats.losses()
+ " losses&7, &e" + stats.ties() + " ties&7, &f" + stats.games() + " games");
}
private void create(CommandSender sender, String[] args) {
Player player = this.adminPlayer(sender);
if (player == null || args.length < 2) {
Messages.send(sender, "&7Usage: &b/spleef create <name> [classic|bow]");
return;
}
GameType gameType;
try {
gameType = GameType.parse(args.length >= 3 ? args[2] : "classic");
} catch (IllegalArgumentException ex) {
Messages.send(sender, "&cMode must be &bclassic &cor &bbow&c.");
return;
}
ArenaManager.CreateResult result = this.arenaManager.create(player, args[1], gameType);
Messages.send(sender, (result.success() ? "&a" : "&c") + result.message());
}
private void delete(CommandSender sender, String[] args) {
SpleefArena arena = this.adminArena(sender, args, 1);
if (arena == null) {
return;
}
if (this.arenaManager.delete(arena)) {
Messages.send(sender, "&aDeleted arena &b" + arena.name() + "&a.");
} else {
Messages.send(sender, "&cAn active arena cannot be deleted.");
}
}
private void setLocation(CommandSender sender, String[] args, LocationType type) {
Player player = this.adminPlayer(sender);
SpleefArena arena = this.adminArena(sender, args, 1);
if (player == null || arena == null) {
return;
}
if (!this.arenaManager.isSetupLocationValid(arena, player.getLocation())) {
Messages.send(sender, "&cStand inside arena region &b" + arena.regionId() + "&c.");
return;
}
switch (type) {
case WAITING -> arena.waitingSpawn(player.getLocation());
case SPECTATOR -> arena.spectatorSpawn(player.getLocation());
case PLAYER -> arena.addSpawn(player.getLocation());
}
this.arenaManager.saveArenas();
Messages.send(sender, "&aSaved the " + type.description + " for &b" + arena.name() + "&a.");
}
private void clearSpawns(CommandSender sender, String[] args) {
SpleefArena arena = this.adminArena(sender, args, 1);
if (arena == null) {
return;
}
arena.clearSpawns();
this.arenaManager.saveArenas();
Messages.send(sender, "&aCleared player spawns for &b" + arena.name() + "&a.");
}
private void mode(CommandSender sender, String[] args) {
SpleefArena arena = this.adminArena(sender, args, 1);
if (arena == null || args.length < 3) {
Messages.send(sender, "&7Usage: &b/spleef mode <arena> <classic|bow>");
return;
}
try {
arena.gameType(GameType.parse(args[2]));
this.arenaManager.saveArenas();
Messages.send(sender, "&aMode set to &b" + arena.gameType().name() + "&a.");
} catch (IllegalArgumentException ex) {
Messages.send(sender, "&cMode must be &bclassic &cor &bbow&c.");
}
}
private void enabled(CommandSender sender, String[] args, boolean enabled) {
SpleefArena arena = this.adminArena(sender, args, 1);
if (arena == null) {
return;
}
ArenaSession session = this.arenaManager.get(arena.name());
if (!enabled && session.isActive()) {
Messages.send(sender, "&cWait for the active match to finish before disabling this arena.");
return;
}
arena.enabled(enabled);
this.arenaManager.saveArenas();
Messages.send(sender, "&aArena &b" + arena.name() + " &ais now " + (enabled ? "enabled" : "disabled") + ".");
}
private void layer(CommandSender sender, String[] args) {
if (!this.require(sender, "spleef.admin") || args.length < 3) {
Messages.send(sender, "&7Usage: &b/spleef layer <add|remove|list|clear> <arena> [index]");
return;
}
SpleefArena arena = this.arenaManager.get(args[2]) == null ? null : this.arenaManager.get(args[2]).arena();
if (arena == null) {
Messages.send(sender, "&cArena not found.");
return;
}
switch (args[1].toLowerCase(Locale.ROOT)) {
case "add" -> {
Player player = this.player(sender);
if (player != null) {
this.editor.beginLayer(player, arena);
}
}
case "remove" -> {
if (args.length < 4) {
Messages.send(sender, "&7Usage: &b/spleef layer remove <arena> <index>");
return;
}
try {
int index = Integer.parseInt(args[3]) - 1;
if (index < 0 || index >= arena.layers().size()) {
throw new NumberFormatException();
}
arena.removeLayer(index);
this.arenaManager.saveArenas();
Messages.send(sender, "&aLayer removed.");
} catch (NumberFormatException ex) {
Messages.send(sender, "&cInvalid layer index.");
}
}
case "list" -> {
if (arena.layers().isEmpty()) {
Messages.send(sender, "&7This arena has no layers.");
}
int index = 1;
for (SpleefLayer layer : arena.layers()) {
Messages.send(sender, "&b#" + index++ + " &7" + layer.blockData().getAsString() + " &8- &7"
+ layer.bounds().minX() + "," + layer.bounds().minY() + "," + layer.bounds().minZ() + " to "
+ layer.bounds().maxX() + "," + layer.bounds().maxY() + "," + layer.bounds().maxZ());
}
}
case "clear" -> {
arena.clearLayers();
this.arenaManager.saveArenas();
Messages.send(sender, "&aAll layers cleared.");
}
default -> Messages.send(sender, "&7Usage: &b/spleef layer <add|remove|list|clear> <arena> [index]");
}
}
private void deathRegion(CommandSender sender, String[] args) {
Player player = this.adminPlayer(sender);
SpleefArena arena = this.adminArena(sender, args, 1);
if (player != null && arena != null) {
this.editor.beginDeathRegion(player, arena);
}
}
private void info(CommandSender sender, String[] args) {
SpleefArena arena = this.adminArena(sender, args, 1);
if (arena == null) {
return;
}
ArenaSession session = this.arenaManager.get(arena.name());
Messages.send(sender, "&b" + arena.name() + " &8- &7region &f" + arena.regionId() + "&7, world &f"
+ arena.worldName() + "&7, mode &f" + arena.gameType() + "&7, state &f" + session.state());
Messages.send(sender,
"&7Spawns: &f" + arena.spawns().size() + "&7, layers: &f" + arena.layers().size() + "&7, complete: "
+ (arena.isComplete() ? "&aYes" : "&cNo") + "&7, enabled: "
+ (arena.enabled() ? "&aYes" : "&cNo"));
}
private void cancel(CommandSender sender) {
Player player = this.player(sender);
if (player != null) {
Messages.send(sender,
this.editor.cancel(player) ? "&7Arena edit cancelled." : "&7You are not editing an arena.");
}
}
private void reload(CommandSender sender) {
if (!this.require(sender, "spleef.admin")) {
return;
}
this.plugin.reloadConfig();
Messages.send(sender, "&aConfiguration reloaded.");
}
private SpleefArena adminArena(CommandSender sender, String[] args, int index) {
if (!this.require(sender, "spleef.admin")) {
return null;
}
if (args.length <= index) {
Messages.send(sender, "&cAn arena name is required.");
return null;
}
ArenaSession session = this.arenaManager.get(args[index]);
if (session == null) {
Messages.send(sender, "&cArena not found.");
return null;
}
return session.arena();
}
private Player adminPlayer(CommandSender sender) {
return this.require(sender, "spleef.admin") ? this.player(sender) : null;
}
private Player player(CommandSender sender) {
if (sender instanceof Player player) {
return player;
}
Messages.send(sender, "&cThat command can only be used by a player.");
return null;
}
private boolean require(CommandSender sender, String permission) {
if (sender.hasPermission(permission)) {
return true;
}
Messages.send(sender, "&cYou do not have permission to do that.");
return false;
}
private void help(CommandSender sender) {
Messages.send(sender, "&b/spleef arenas &8- &7show available arenas");
Messages.send(sender, "&b/spleef join <arena> &8- &7join an arena");
Messages.send(sender, "&b/spleef leave &8- &7leave Spleef");
Messages.send(sender, "&b/spleef spectate <arena> &8- &7spectate an arena");
Messages.send(sender, "&b/spleef stats [player] &8- &7show Spleef statistics");
if (sender.hasPermission("spleef.admin")) {
Messages.send(sender,
"&b/spleef create <name> [classic|bow] &8- &7create an arena in your WorldGuard region");
Messages.send(sender, "&b/spleef info <arena> &8- &7show setup status");
}
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] args)
{
if (args.length == 1) {
List<String> commands = new ArrayList<>(List.of("arenas", "join", "leave", "spectate", "stats"));
if (sender.hasPermission("spleef.admin")) {
commands.addAll(List.of("create", "delete", "setwait", "setspectator", "addspawn", "clearspawns",
"mode", "enable", "disable", "layer", "deathregion", "info", "cancel", "reload"));
}
return matches(args[0], commands);
}
if (args.length == 2
&& List.of("join", "spectate", "delete", "setwait", "setspectator", "addspawn", "clearspawns", "mode",
"enable", "disable", "deathregion", "info").contains(args[0].toLowerCase(Locale.ROOT)))
{
return matches(args[1], this.arenaManager.sessions().stream().map(value -> value.arena().name()).toList());
}
if (args.length == 2 && args[0].equalsIgnoreCase("layer")) {
return matches(args[1], List.of("add", "remove", "list", "clear"));
}
if (args.length == 3 && args[0].equalsIgnoreCase("layer")) {
return matches(args[2], this.arenaManager.sessions().stream().map(value -> value.arena().name()).toList());
}
if (args.length == 3 && (args[0].equalsIgnoreCase("create") || args[0].equalsIgnoreCase("mode"))) {
return matches(args[2], List.of("classic", "bow"));
}
return List.of();
}
private static List<String> matches(String input, List<String> values) {
String prefix = input.toLowerCase(Locale.ROOT);
return values.stream().filter(value -> value.toLowerCase(Locale.ROOT).startsWith(prefix)).toList();
}
private enum LocationType {
WAITING("waiting spawn"), SPECTATOR("spectator spawn"), PLAYER("player spawn");
private final String description;
LocationType(String description) {
this.description = description;
}
}
}
@@ -0,0 +1,187 @@
package net.trueog.spleefog.data;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import net.trueog.spleefog.SpleefPlugin;
import net.trueog.spleefog.model.BlockBounds;
import net.trueog.spleefog.model.GameType;
import net.trueog.spleefog.model.SpleefArena;
import net.trueog.spleefog.model.SpleefLayer;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
public final class ArenaRepository {
private final SpleefPlugin plugin;
private final File file;
public ArenaRepository(SpleefPlugin plugin) {
this.plugin = plugin;
this.file = new File(plugin.getDataFolder(), "arenas.yml");
}
public List<SpleefArena> load() {
List<SpleefArena> arenas = new ArrayList<>();
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(this.file);
ConfigurationSection root = yaml.getConfigurationSection("arenas");
if (root == null) {
return arenas;
}
for (String key : root.getKeys(false)) {
ConfigurationSection section = root.getConfigurationSection(key);
try {
SpleefArena arena = this.readArena(key, section);
if (arena != null) {
arenas.add(arena);
}
} catch (RuntimeException ex) {
this.plugin.getLogger().warning("Could not load Spleef arena '" + key + "': " + ex.getMessage());
}
}
return arenas;
}
public void save(Collection<SpleefArena> arenas) {
YamlConfiguration yaml = new YamlConfiguration();
for (SpleefArena arena : arenas) {
String path = "arenas." + arena.name();
yaml.set(path + ".world", arena.worldName());
yaml.set(path + ".region", arena.regionId());
yaml.set(path + ".region-bounds", writeBounds(arena.regionBounds()));
yaml.set(path + ".game", arena.gameType().name());
yaml.set(path + ".enabled", arena.enabled());
yaml.set(path + ".waiting-spawn", arena.waitingSpawn());
yaml.set(path + ".spectator-spawn", arena.spectatorSpawn());
yaml.set(path + ".spawns", arena.spawns());
yaml.set(path + ".death-region", writeBounds(arena.deathRegion()));
int index = 0;
for (SpleefLayer layer : arena.layers()) {
String layerPath = path + ".layers." + index++;
yaml.set(layerPath + ".bounds", writeBounds(layer.bounds()));
yaml.set(layerPath + ".block-data", layer.blockData().getAsString());
}
}
try {
yaml.save(this.file);
} catch (IOException ex) {
this.plugin.getLogger().severe("Could not save arenas.yml: " + ex.getMessage());
}
}
private SpleefArena readArena(String name, ConfigurationSection section) {
if (section == null) {
return null;
}
String worldName = section.getString("world");
String region = section.getString("region");
BlockBounds regionBounds = readBounds(section.getConfigurationSection("region-bounds"));
if (worldName == null || region == null || regionBounds == null || Bukkit.getWorld(worldName) == null) {
throw new IllegalArgumentException("world, region, or region bounds are unavailable");
}
SpleefArena arena = new SpleefArena(name, worldName, region, regionBounds,
GameType.parse(section.getString("game", "CLASSIC")));
arena.enabled(section.getBoolean("enabled", true));
arena.waitingSpawn(section.getLocation("waiting-spawn"));
arena.spectatorSpawn(section.getLocation("spectator-spawn"));
for (Object value : section.getList("spawns", List.of())) {
if (value instanceof Location location) {
arena.addSpawn(location);
}
}
arena.deathRegion(readBounds(section.getConfigurationSection("death-region")));
ConfigurationSection layers = section.getConfigurationSection("layers");
if (layers != null) {
for (String key : layers.getKeys(false)) {
ConfigurationSection layer = layers.getConfigurationSection(key);
BlockBounds bounds = readBounds(layer == null ? null : layer.getConfigurationSection("bounds"));
String blockData = layer == null ? null : layer.getString("block-data");
if (bounds != null && blockData != null) {
arena.addLayer(new SpleefLayer(bounds, Bukkit.createBlockData(blockData)));
}
}
}
return arena;
}
private static Map<String, Object> writeBounds(BlockBounds bounds) {
if (bounds == null) {
return null;
}
return Map.of("min-x", bounds.minX(), "min-y", bounds.minY(), "min-z", bounds.minZ(), "max-x", bounds.maxX(),
"max-y", bounds.maxY(), "max-z", bounds.maxZ());
}
private static BlockBounds readBounds(ConfigurationSection section) {
if (section == null) {
return null;
}
return new BlockBounds(section.getInt("min-x"), section.getInt("min-y"), section.getInt("min-z"),
section.getInt("max-x"), section.getInt("max-y"), section.getInt("max-z"));
}
}
@@ -0,0 +1,104 @@
package net.trueog.spleefog.data;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import net.trueog.spleefog.SpleefPlugin;
import net.trueog.spleefog.player.PlayerSnapshot;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
public final class RecoveryStore {
private final SpleefPlugin plugin;
private final File file;
private final Map<UUID, PlayerSnapshot> snapshots = new HashMap<>();
public RecoveryStore(SpleefPlugin plugin) {
this.plugin = plugin;
this.file = new File(plugin.getDataFolder(), "recovery.yml");
this.load();
}
public PlayerSnapshot capture(org.bukkit.entity.Player player) {
PlayerSnapshot snapshot = PlayerSnapshot.capture(player);
this.snapshots.put(player.getUniqueId(), snapshot);
this.save();
return snapshot;
}
public PlayerSnapshot get(UUID playerId) {
return this.snapshots.get(playerId);
}
public PlayerSnapshot remove(UUID playerId) {
PlayerSnapshot value = this.snapshots.remove(playerId);
this.save();
return value;
}
public boolean contains(UUID playerId) {
return this.snapshots.containsKey(playerId);
}
private void load() {
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(this.file);
ConfigurationSection players = yaml.getConfigurationSection("players");
if (players == null) {
return;
}
for (String key : players.getKeys(false)) {
try {
UUID playerId = UUID.fromString(key);
this.snapshots.put(playerId, PlayerSnapshot.read(yaml, "players." + key));
} catch (RuntimeException ex) {
this.plugin.getLogger().warning("Ignoring invalid recovery entry '" + key + "'.");
}
}
}
private void save() {
YamlConfiguration yaml = new YamlConfiguration();
for (Map.Entry<UUID, PlayerSnapshot> entry : this.snapshots.entrySet()) {
entry.getValue().write(yaml, "players." + entry.getKey());
}
try {
yaml.save(this.file);
} catch (IOException ex) {
this.plugin.getLogger().severe("Could not save recovery.yml: " + ex.getMessage());
}
}
}
@@ -0,0 +1,175 @@
package net.trueog.spleefog.data;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import net.trueog.spleefog.SpleefPlugin;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
public final class StatsRepository {
private final SpleefPlugin plugin;
private final File file;
private final Map<UUID, PlayerStats> stats = new HashMap<>();
public StatsRepository(SpleefPlugin plugin) {
this.plugin = plugin;
this.file = new File(plugin.getDataFolder(), "stats.yml");
this.load();
}
public PlayerStats get(UUID playerId, String name) {
PlayerStats value = this.stats.computeIfAbsent(playerId, ignored -> new PlayerStats(name));
if (name != null && !name.isBlank()) {
value.name = name;
}
return value;
}
public PlayerStats findByName(String name) {
return this.stats.values().stream().filter(value -> value.name.equalsIgnoreCase(name)).findFirst().orElse(null);
}
public void recordWin(UUID playerId, String name) {
PlayerStats value = this.get(playerId, name);
value.wins++;
value.games++;
}
public void recordLoss(UUID playerId, String name) {
PlayerStats value = this.get(playerId, name);
value.losses++;
value.games++;
}
public void recordTie(UUID playerId, String name) {
PlayerStats value = this.get(playerId, name);
value.ties++;
value.games++;
}
public void save() {
YamlConfiguration yaml = new YamlConfiguration();
for (Map.Entry<UUID, PlayerStats> entry : this.stats.entrySet()) {
String path = "players." + entry.getKey();
PlayerStats value = entry.getValue();
yaml.set(path + ".name", value.name);
yaml.set(path + ".wins", value.wins);
yaml.set(path + ".losses", value.losses);
yaml.set(path + ".ties", value.ties);
yaml.set(path + ".games", value.games);
}
try {
yaml.save(this.file);
} catch (IOException ex) {
this.plugin.getLogger().severe("Could not save stats.yml: " + ex.getMessage());
}
}
private void load() {
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(this.file);
ConfigurationSection players = yaml.getConfigurationSection("players");
if (players == null) {
return;
}
for (String key : players.getKeys(false)) {
try {
UUID playerId = UUID.fromString(key);
String path = "players." + key;
PlayerStats value = new PlayerStats(yaml.getString(path + ".name", "Unknown"));
value.wins = yaml.getInt(path + ".wins");
value.losses = yaml.getInt(path + ".losses");
value.ties = yaml.getInt(path + ".ties");
value.games = yaml.getInt(path + ".games");
this.stats.put(playerId, value);
} catch (IllegalArgumentException ignored) {
this.plugin.getLogger().warning("Ignoring invalid player UUID in stats.yml: " + key);
}
}
}
public static final class PlayerStats {
private String name;
private int wins;
private int losses;
private int ties;
private int games;
private PlayerStats(String name) {
this.name = name == null ? "Unknown" : name;
}
public String name() {
return this.name;
}
public int wins() {
return this.wins;
}
public int losses() {
return this.losses;
}
public int ties() {
return this.ties;
}
public int games() {
return this.games;
}
}
}
@@ -0,0 +1,216 @@
package net.trueog.spleefog.editor;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import net.trueog.spleefog.Messages;
import net.trueog.spleefog.SpleefPlugin;
import net.trueog.spleefog.arena.ArenaManager;
import net.trueog.spleefog.arena.ArenaSession;
import net.trueog.spleefog.model.ArenaState;
import net.trueog.spleefog.model.BlockBounds;
import net.trueog.spleefog.model.SpleefArena;
import net.trueog.spleefog.model.SpleefLayer;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerQuitEvent;
public final class ArenaEditor implements Listener {
private final SpleefPlugin plugin;
private final ArenaManager arenaManager;
private final Map<UUID, EditSession> edits = new HashMap<>();
public ArenaEditor(SpleefPlugin plugin, ArenaManager arenaManager) {
this.plugin = plugin;
this.arenaManager = arenaManager;
}
public boolean beginLayer(Player player, SpleefArena arena) {
if (!this.canEdit(player, arena)) {
return false;
}
this.edits.put(player.getUniqueId(), new EditSession(arena, EditType.LAYER));
Messages.send(player, "&7Click the first corner block of the layer, or type &bcancel&7.");
return true;
}
public boolean beginDeathRegion(Player player, SpleefArena arena) {
if (!this.canEdit(player, arena)) {
return false;
}
this.edits.put(player.getUniqueId(), new EditSession(arena, EditType.DEATH_REGION));
Messages.send(player, "&7Click the first corner block of the death region, or type &bcancel&7.");
return true;
}
public boolean cancel(Player player) {
return this.edits.remove(player.getUniqueId()) != null;
}
private boolean canEdit(Player player, SpleefArena arena) {
ArenaSession session = this.arenaManager.get(arena.name());
if (session == null || session.state() != ArenaState.WAITING || session.isActive()) {
Messages.send(player, "&cThat arena cannot be edited while it is active.");
return false;
}
return true;
}
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = false)
public void onInteract(PlayerInteractEvent event) {
EditSession edit = this.edits.get(event.getPlayer().getUniqueId());
if (edit == null || edit.awaitingMaterial || event.getClickedBlock() == null
|| event.getAction() != Action.LEFT_CLICK_BLOCK && event.getAction() != Action.RIGHT_CLICK_BLOCK)
{
return;
}
event.setCancelled(true);
Location location = event.getClickedBlock().getLocation();
if (!this.arenaManager.isSetupLocationValid(edit.arena, location)) {
Messages.send(event.getPlayer(),
"&cBoth corners must be inside WorldGuard region &b" + edit.arena.regionId() + "&c.");
return;
}
if (edit.first == null) {
edit.first = location;
Messages.send(event.getPlayer(), "&7First corner set. Click the second corner block.");
return;
}
edit.second = location;
if (edit.type == EditType.DEATH_REGION) {
edit.arena.deathRegion(new BlockBounds(edit.first, edit.second));
this.arenaManager.saveArenas();
this.edits.remove(event.getPlayer().getUniqueId());
Messages.send(event.getPlayer(), "&aDeath region saved.");
} else {
edit.awaitingMaterial = true;
Messages.send(event.getPlayer(),
"&7Enter the Bukkit block data for this layer, for example &bminecraft:snow_block&7.");
}
}
@SuppressWarnings("deprecation")
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = false)
public void onChat(AsyncPlayerChatEvent event) {
EditSession edit = this.edits.get(event.getPlayer().getUniqueId());
if (edit == null) {
return;
}
event.setCancelled(true);
String input = event.getMessage().trim();
Bukkit.getScheduler().runTask(this.plugin, () -> this.handleChat(event.getPlayer(), edit, input));
}
private void handleChat(Player player, EditSession edit, String input) {
if (input.equalsIgnoreCase("cancel")) {
this.edits.remove(player.getUniqueId());
Messages.send(player, "&7Arena edit cancelled.");
return;
}
if (!edit.awaitingMaterial) {
Messages.send(player, "&7Click the requested block, or type &bcancel&7.");
return;
}
BlockData blockData;
try {
blockData = Bukkit.createBlockData(input);
} catch (IllegalArgumentException ex) {
Messages.send(player, "&cInvalid block data. Try again or type &bcancel&c.");
return;
}
edit.arena.addLayer(new SpleefLayer(new BlockBounds(edit.first, edit.second), blockData));
this.arenaManager.saveArenas();
this.edits.remove(player.getUniqueId());
Messages.send(player, "&aLayer added to &b" + edit.arena.name() + "&a.");
}
@EventHandler
public void onQuit(PlayerQuitEvent event) {
this.edits.remove(event.getPlayer().getUniqueId());
}
private enum EditType {
LAYER, DEATH_REGION
}
private static final class EditSession {
private final SpleefArena arena;
private final EditType type;
private Location first;
private Location second;
private boolean awaitingMaterial;
private EditSession(SpleefArena arena, EditType type) {
this.arena = arena;
this.type = type;
}
}
}
@@ -0,0 +1,77 @@
package net.trueog.spleefog.hook;
import java.lang.reflect.Method;
import net.trueog.spleefog.SpleefPlugin;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
/** Keeps combat-tagged SMP players from escaping into a Spleef match. */
public final class BattleTrackerHook {
private final SpleefPlugin owner;
private Plugin cachedPlugin;
private Method getCombatLog;
private Method isInCombat;
private boolean warned;
public BattleTrackerHook(SpleefPlugin owner) {
this.owner = owner;
}
public boolean isInCombat(Player player) {
Plugin plugin = Bukkit.getPluginManager().getPlugin("BattleTracker");
if (plugin == null || !plugin.isEnabled()) {
return false;
}
try {
this.resolve(plugin);
Object combatLog = this.getCombatLog.invoke(plugin);
return combatLog != null && Boolean.TRUE.equals(this.isInCombat.invoke(combatLog, player));
} catch (ReflectiveOperationException ex) {
if (!this.warned) {
this.warned = true;
this.owner.getLogger()
.warning("Could not query BattleTracker combat state; Spleef join protection is disabled.");
}
return false;
}
}
private void resolve(Plugin plugin) throws ReflectiveOperationException {
if (plugin == this.cachedPlugin && this.getCombatLog != null && this.isInCombat != null) {
return;
}
this.getCombatLog = plugin.getClass().getMethod("getCombatLog");
Object combatLog = this.getCombatLog.invoke(plugin);
if (combatLog == null) {
throw new NoSuchMethodException("BattleTracker combat log is unavailable");
}
this.isInCombat = combatLog.getClass().getMethod("isInCombat", Player.class);
this.cachedPlugin = plugin;
this.warned = false;
}
}
@@ -0,0 +1,76 @@
package net.trueog.spleefog.hook;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import net.trueog.spleefog.SpleefPlugin;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.permissions.PermissionAttachment;
import org.bukkit.plugin.Plugin;
public final class GameModeInventoriesHook {
private static final String PLUGIN_NAME = "GameModeInventories-OG";
private static final String USE_PERMISSION = "gamemodeinventories.use";
private final SpleefPlugin owner;
private final boolean enabled;
private final Map<UUID, PermissionAttachment> attachments = new HashMap<>();
public GameModeInventoriesHook(SpleefPlugin owner) {
this.owner = owner;
Plugin plugin = Bukkit.getPluginManager().getPlugin(PLUGIN_NAME);
this.enabled = plugin != null && plugin.isEnabled();
}
public void suspend(Player player) {
if (this.enabled) {
this.attachments.computeIfAbsent(player.getUniqueId(),
ignored -> player.addAttachment(this.owner, USE_PERMISSION, false));
}
}
public void release(Player player) {
PermissionAttachment attachment = this.attachments.remove(player.getUniqueId());
if (attachment == null) {
return;
}
try {
attachment.remove();
} catch (IllegalArgumentException ignored) {
}
}
public void releaseAll() {
for (PermissionAttachment attachment : this.attachments.values()) {
try {
attachment.remove();
} catch (IllegalArgumentException ignored) {
}
}
this.attachments.clear();
}
}
@@ -0,0 +1,92 @@
package net.trueog.spleefog.hook;
import java.lang.reflect.Method;
import net.trueog.spleefog.SpleefPlugin;
import net.trueog.spleefog.api.SpleefAPI;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.EventException;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.plugin.EventExecutor;
import org.bukkit.plugin.Plugin;
public final class PlayerBountiesHook implements Listener {
private static final String PLUGIN_NAME = "PlayerBounties-OG";
private static final String CLAIM_EVENT = "com.tcoded.playerbountiesplus.event.BountyClaimEvent";
private static final String SET_EVENT = "com.tcoded.playerbountiesplus.event.BountySetEvent";
private PlayerBountiesHook() {
}
public static boolean register(SpleefPlugin owner) {
Plugin plugin = Bukkit.getPluginManager().getPlugin(PLUGIN_NAME);
if (plugin == null || !plugin.isEnabled()) {
return false;
}
try {
Class<? extends Event> claimClass = Class.forName(CLAIM_EVENT).asSubclass(Event.class);
Class<? extends Event> setClass = Class.forName(SET_EVENT).asSubclass(Event.class);
Method claimant = claimClass.getMethod("getClaimant");
Method victim = claimClass.getMethod("getVictim");
Method target = setClass.getMethod("getTarget");
if (!Cancellable.class.isAssignableFrom(claimClass) || !Cancellable.class.isAssignableFrom(setClass)) {
return false;
}
PlayerBountiesHook listener = new PlayerBountiesHook();
Bukkit.getPluginManager().registerEvent(claimClass, listener, EventPriority.NORMAL,
executor(claimant, victim), owner, true);
Bukkit.getPluginManager().registerEvent(setClass, listener, EventPriority.NORMAL, executor(target), owner,
true);
return true;
} catch (ReflectiveOperationException | ClassCastException ex) {
owner.getLogger().warning("PlayerBounties-OG API is incompatible; bounty protection is disabled.");
return false;
}
}
private static EventExecutor executor(Method... accessors) {
return (listener, event) -> {
try {
for (Method accessor : accessors) {
Object value = accessor.invoke(event);
if (value instanceof Player player && SpleefAPI.isInSpleef(player)) {
((Cancellable) event).setCancelled(true);
return;
}
}
} catch (ReflectiveOperationException | ClassCastException ex) {
throw new EventException(ex);
}
};
}
}
@@ -0,0 +1,5 @@
package net.trueog.spleefog.model;
public enum ArenaState {
WAITING, COUNTDOWN, IN_GAME, ENDING
}
@@ -0,0 +1,29 @@
package net.trueog.spleefog.model;
import org.bukkit.Location;
public record BlockBounds(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
public BlockBounds(Location first, Location second) {
this(Math.min(first.getBlockX(), second.getBlockX()), Math.min(first.getBlockY(), second.getBlockY()),
Math.min(first.getBlockZ(), second.getBlockZ()), Math.max(first.getBlockX(), second.getBlockX()),
Math.max(first.getBlockY(), second.getBlockY()), Math.max(first.getBlockZ(), second.getBlockZ()));
}
public boolean contains(Location location) {
return location != null && location.getBlockX() >= this.minX && location.getBlockX() <= this.maxX
&& location.getBlockY() >= this.minY && location.getBlockY() <= this.maxY
&& location.getBlockZ() >= this.minZ && location.getBlockZ() <= this.maxZ;
}
public long volume() {
return (long) (this.maxX - this.minX + 1) * (this.maxY - this.minY + 1) * (this.maxZ - this.minZ + 1);
}
}
@@ -0,0 +1,28 @@
package net.trueog.spleefog.model;
import java.util.Locale;
public enum GameType {
CLASSIC, BOW;
public static GameType parse(String value) {
if (value == null) {
return CLASSIC;
}
String normalized = value.toUpperCase(Locale.ROOT).replace('-', '_');
if (normalized.equals("BOW_SPLEEF") || normalized.equals("BOWSPLEEF")) {
return BOW;
}
return valueOf(normalized);
}
}
@@ -0,0 +1,185 @@
package net.trueog.spleefog.model;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.Location;
import org.bukkit.World;
public final class SpleefArena {
private final String name;
private final String worldName;
private final String regionId;
private final BlockBounds regionBounds;
private final List<Location> spawns = new ArrayList<>();
private final List<SpleefLayer> layers = new ArrayList<>();
private GameType gameType;
private boolean enabled = true;
private Location waitingSpawn;
private Location spectatorSpawn;
private BlockBounds deathRegion;
public SpleefArena(String name, String worldName, String regionId, BlockBounds regionBounds, GameType gameType) {
this.name = name;
this.worldName = worldName;
this.regionId = regionId;
this.regionBounds = regionBounds;
this.gameType = gameType;
}
public String name() {
return this.name;
}
public String worldName() {
return this.worldName;
}
public String regionId() {
return this.regionId;
}
public BlockBounds regionBounds() {
return this.regionBounds;
}
public GameType gameType() {
return this.gameType;
}
public void gameType(GameType gameType) {
this.gameType = gameType;
}
public boolean enabled() {
return this.enabled;
}
public void enabled(boolean enabled) {
this.enabled = enabled;
}
public Location waitingSpawn() {
return clone(this.waitingSpawn);
}
public void waitingSpawn(Location location) {
this.waitingSpawn = clone(location);
}
public Location spectatorSpawn() {
return clone(this.spectatorSpawn);
}
public void spectatorSpawn(Location location) {
this.spectatorSpawn = clone(location);
}
public List<Location> spawns() {
return this.spawns.stream().map(SpleefArena::clone).toList();
}
public List<SpleefLayer> layers() {
return List.copyOf(this.layers);
}
public BlockBounds deathRegion() {
return this.deathRegion;
}
public void deathRegion(BlockBounds bounds) {
this.deathRegion = bounds;
}
public void addSpawn(Location location) {
this.spawns.add(clone(location));
}
public void clearSpawns() {
this.spawns.clear();
}
public void addLayer(SpleefLayer layer) {
this.layers.add(layer);
}
public void removeLayer(int index) {
this.layers.remove(index);
}
public void clearLayers() {
this.layers.clear();
}
public boolean contains(Location location) {
World world = location == null ? null : location.getWorld();
return world != null && world.getName().equalsIgnoreCase(this.worldName)
&& this.regionBounds.contains(location);
}
public boolean isComplete() {
return this.waitingSpawn != null && this.spectatorSpawn != null && this.spawns.size() >= 2
&& !this.layers.isEmpty() && this.deathRegion != null;
}
public int capacity() {
return this.spawns.size();
}
private static Location clone(Location location) {
return location == null ? null : location.clone();
}
}
@@ -0,0 +1,6 @@
package net.trueog.spleefog.model;
import org.bukkit.block.data.BlockData;
public record SpleefLayer(BlockBounds bounds, BlockData blockData) {
}
@@ -0,0 +1,243 @@
package net.trueog.spleefog.player;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.attribute.Attribute;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.scoreboard.Scoreboard;
public final class PlayerSnapshot {
private final Location location;
private final ItemStack[] storage;
private final ItemStack[] armor;
private final ItemStack[] extra;
private final List<PotionEffect> potionEffects;
private final GameMode gameMode;
private final float experience;
private final int level;
private final int totalExperience;
private final double health;
private final int foodLevel;
private final float saturation;
private final float exhaustion;
private final boolean allowFlight;
private final boolean flying;
private final int fireTicks;
private final float fallDistance;
private final transient Scoreboard scoreboard;
private PlayerSnapshot(Location location, ItemStack[] storage, ItemStack[] armor, ItemStack[] extra,
List<PotionEffect> potionEffects, GameMode gameMode, float experience, int level, int totalExperience,
double health, int foodLevel, float saturation, float exhaustion, boolean allowFlight, boolean flying,
int fireTicks, float fallDistance, Scoreboard scoreboard)
{
this.location = location;
this.storage = storage;
this.armor = armor;
this.extra = extra;
this.potionEffects = potionEffects;
this.gameMode = gameMode;
this.experience = experience;
this.level = level;
this.totalExperience = totalExperience;
this.health = health;
this.foodLevel = foodLevel;
this.saturation = saturation;
this.exhaustion = exhaustion;
this.allowFlight = allowFlight;
this.flying = flying;
this.fireTicks = fireTicks;
this.fallDistance = fallDistance;
this.scoreboard = scoreboard;
}
public static PlayerSnapshot capture(Player player) {
return new PlayerSnapshot(player.getLocation().clone(), clone(player.getInventory().getStorageContents()),
clone(player.getInventory().getArmorContents()), clone(player.getInventory().getExtraContents()),
new ArrayList<>(player.getActivePotionEffects()), player.getGameMode(), player.getExp(),
player.getLevel(), player.getTotalExperience(), player.getHealth(), player.getFoodLevel(),
player.getSaturation(), player.getExhaustion(), player.getAllowFlight(), player.isFlying(),
player.getFireTicks(), player.getFallDistance(), player.getScoreboard());
}
public void restore(Player player) {
player.getInventory().setStorageContents(clone(this.storage));
player.getInventory().setArmorContents(clone(this.armor));
player.getInventory().setExtraContents(clone(this.extra));
player.updateInventory();
for (PotionEffect effect : player.getActivePotionEffects()) {
player.removePotionEffect(effect.getType());
}
player.addPotionEffects(this.potionEffects);
player.setGameMode(this.gameMode);
player.setExp(this.experience);
player.setLevel(this.level);
player.setTotalExperience(this.totalExperience);
double maximumHealth = player.getAttribute(Attribute.GENERIC_MAX_HEALTH).getValue();
player.setHealth(Math.max(0.5D, Math.min(this.health, maximumHealth)));
player.setFoodLevel(this.foodLevel);
player.setSaturation(this.saturation);
player.setExhaustion(this.exhaustion);
player.setAllowFlight(this.allowFlight);
player.setFlying(this.allowFlight && this.flying);
player.setFireTicks(this.fireTicks);
player.setFallDistance(this.fallDistance);
player.setScoreboard(
this.scoreboard == null ? Bukkit.getScoreboardManager().getMainScoreboard() : this.scoreboard);
if (this.location != null && this.location.getWorld() != null) {
player.teleport(this.location);
}
}
public void write(ConfigurationSection yaml, String path) {
yaml.set(path + ".location", this.location);
yaml.set(path + ".storage", Arrays.asList(this.storage));
yaml.set(path + ".armor", Arrays.asList(this.armor));
yaml.set(path + ".extra", Arrays.asList(this.extra));
yaml.set(path + ".game-mode", this.gameMode.name());
yaml.set(path + ".experience", this.experience);
yaml.set(path + ".level", this.level);
yaml.set(path + ".total-experience", this.totalExperience);
yaml.set(path + ".health", this.health);
yaml.set(path + ".food-level", this.foodLevel);
yaml.set(path + ".saturation", this.saturation);
yaml.set(path + ".exhaustion", this.exhaustion);
yaml.set(path + ".allow-flight", this.allowFlight);
yaml.set(path + ".flying", this.flying);
yaml.set(path + ".fire-ticks", this.fireTicks);
yaml.set(path + ".fall-distance", this.fallDistance);
List<Map<String, Object>> effects = new ArrayList<>();
for (PotionEffect effect : this.potionEffects) {
Map<String, Object> value = new LinkedHashMap<>();
value.put("type", effect.getType().getName());
value.put("duration", effect.getDuration());
value.put("amplifier", effect.getAmplifier());
value.put("ambient", effect.isAmbient());
value.put("particles", effect.hasParticles());
value.put("icon", effect.hasIcon());
effects.add(value);
}
yaml.set(path + ".potion-effects", effects);
}
public static PlayerSnapshot read(ConfigurationSection yaml, String path) {
Location location = yaml.getLocation(path + ".location");
ItemStack[] storage = readItems(yaml.getList(path + ".storage", List.of()), 36);
ItemStack[] armor = readItems(yaml.getList(path + ".armor", List.of()), 4);
ItemStack[] extra = readItems(yaml.getList(path + ".extra", List.of()), 1);
List<PotionEffect> effects = readEffects(yaml.getMapList(path + ".potion-effects"));
GameMode gameMode;
try {
gameMode = GameMode.valueOf(yaml.getString(path + ".game-mode", "SURVIVAL"));
} catch (IllegalArgumentException ignored) {
gameMode = GameMode.SURVIVAL;
}
return new PlayerSnapshot(location, storage, armor, extra, effects, gameMode,
(float) yaml.getDouble(path + ".experience"), yaml.getInt(path + ".level"),
yaml.getInt(path + ".total-experience"), yaml.getDouble(path + ".health", 20.0D),
yaml.getInt(path + ".food-level", 20), (float) yaml.getDouble(path + ".saturation", 5.0D),
(float) yaml.getDouble(path + ".exhaustion"), yaml.getBoolean(path + ".allow-flight"),
yaml.getBoolean(path + ".flying"), yaml.getInt(path + ".fire-ticks"),
(float) yaml.getDouble(path + ".fall-distance"), null);
}
private static ItemStack[] clone(ItemStack[] contents) {
ItemStack[] result = new ItemStack[contents.length];
for (int i = 0; i < contents.length; i++) {
result[i] = contents[i] == null ? null : contents[i].clone();
}
return result;
}
private static ItemStack[] readItems(List<?> values, int defaultSize) {
ItemStack[] result = new ItemStack[Math.max(defaultSize, values.size())];
for (int i = 0; i < values.size(); i++) {
if (values.get(i) instanceof ItemStack item) {
result[i] = item;
}
}
return result;
}
private static List<PotionEffect> readEffects(List<Map<?, ?>> values) {
List<PotionEffect> result = new ArrayList<>();
for (Map<?, ?> value : values) {
PotionEffectType type = PotionEffectType.getByName(String.valueOf(value.get("type")));
if (type == null) {
continue;
}
result.add(new PotionEffect(type, number(value.get("duration")), number(value.get("amplifier")),
bool(value.get("ambient")), bool(value.get("particles")), bool(value.get("icon"))));
}
return result;
}
private static int number(Object value) {
return value instanceof Number number ? number.intValue() : 0;
}
private static boolean bool(Object value) {
return value instanceof Boolean bool && bool;
}
}
@@ -1,141 +0,0 @@
package org.battleplugins.arena.spleef;
import org.battleplugins.arena.BattleArena;
import org.battleplugins.arena.competition.phase.CompetitionPhaseType;
import org.battleplugins.arena.competition.phase.phases.CountdownPhase;
import org.battleplugins.arena.config.ArenaConfigParser;
import org.battleplugins.arena.config.ParseException;
import org.battleplugins.arena.event.action.EventActionType;
import org.battleplugins.arena.spleef.action.GiveShovelAction;
import org.battleplugins.arena.spleef.action.PasteLayersAction;
import org.battleplugins.arena.spleef.api.SpleefApiListener;
import org.battleplugins.arena.spleef.arena.SpleefArena;
import org.battleplugins.arena.spleef.hook.GameModeInventoriesListener;
import org.battleplugins.arena.spleef.hook.PlayerBountiesListener;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.bukkit.configuration.Configuration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
public class ArenaSpleef extends JavaPlugin {
public static EventActionType<GiveShovelAction> GIVE_SHOVEL = EventActionType.create("give-shovel",
GiveShovelAction.class, GiveShovelAction::new);
public static EventActionType<PasteLayersAction> PASTE_LAYERS = EventActionType.create("paste-layers",
PasteLayersAction.class, PasteLayersAction::new);
public static final CompetitionPhaseType<?, ? extends CountdownPhase<?>> WAITING_FOR_PLAYERS = CompetitionPhaseType
.create("waitingforplayers", CountdownPhase.class);
private static ArenaSpleef instance;
private SpleefConfig config;
private GameModeInventoriesListener gameModeInventoriesListener;
private final NamespacedKey spleefItemKey = new NamespacedKey(this, "spleef_item");
@Override
public void onLoad() {
if (Bukkit.getPluginManager().getPlugin("WorldGuard") != null) {
SpleefFlags.register(this);
} else {
this.getSLF4JLogger().warn(
"WorldGuard not present at load time; the 'allow-spleef' region flag will not be registered.");
}
}
@Override
public void onEnable() {
instance = this;
this.saveDefaultConfig();
File configFile = new File(this.getDataFolder(), "config.yml");
Configuration config = YamlConfiguration.loadConfiguration(configFile);
try {
this.config = ArenaConfigParser.newInstance(configFile.toPath(), SpleefConfig.class, config);
} catch (ParseException e) {
ParseException.handle(e);
this.getSLF4JLogger().error("Failed to load Spleef configuration! Disabling plugin.");
this.getServer().getPluginManager().disablePlugin(this);
return;
}
Path dataFolder = this.getDataFolder().toPath();
Path arenasPath = dataFolder.resolve("arenas");
if (Files.notExists(arenasPath)) {
this.saveResource("arenas/spleef.yml", false);
}
BattleArena.getInstance().registerArena(this, "Spleef", SpleefArena.class, SpleefArena::new);
this.getServer().getPluginManager().registerEvents(new SpleefApiListener(), this);
this.gameModeInventoriesListener = GameModeInventoriesListener.register(this);
if (this.gameModeInventoriesListener != null) {
this.getSLF4JLogger()
.info("Hooked into GameModeInventories-OG; Spleef will preserve gamemode-specific inventories.");
}
if (PlayerBountiesListener.register(this)) {
this.getSLF4JLogger().info(
"Hooked into PlayerBounties-OG; bounty claims and placements for players in Spleef will be cancelled.");
}
}
@Override
public void onDisable() {
if (this.gameModeInventoriesListener != null) {
this.gameModeInventoriesListener.releaseAll();
}
Bukkit.getScheduler().cancelTasks(this);
}
public SpleefConfig getMainConfig() {
return this.config;
}
public NamespacedKey getSpleefItemKey() {
return this.spleefItemKey;
}
public static ArenaSpleef getInstance() {
return instance;
}
}
@@ -1,125 +0,0 @@
package org.battleplugins.arena.spleef;
import org.battleplugins.arena.config.ArenaOption;
import org.bukkit.Location;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Locale;
import java.util.Map;
public class SpleefConfig {
@ArenaOption(name = "shovels", description = "The shovels for this spleef game.", required = true)
private Map<String, ItemStack> shovels;
@ArenaOption(name = "world-whitelist", description = "Worlds where Spleef arenas may be created and run. Empty list = all worlds allowed.")
private List<String> worldWhitelist;
@ArenaOption(name = "region-whitelist", description = "WorldGuard regions where Spleef arenas may be created and matches may modify blocks. Empty list = all regions allowed.")
private List<String> regionWhitelist;
@Nullable
public ItemStack getShovel(String name) {
if (this.shovels == null) {
return null;
}
ItemStack shovel = this.shovels.get(name);
return shovel == null ? null : shovel.clone();
}
public boolean isWorldAllowed(String worldName) {
if (this.worldWhitelist == null || this.worldWhitelist.isEmpty()) {
return true;
}
if (worldName == null) {
return false;
}
String target = worldName.toLowerCase(Locale.ROOT);
for (String allowed : this.worldWhitelist) {
if (allowed != null && allowed.toLowerCase(Locale.ROOT).equals(target)) {
return true;
}
}
return false;
}
public List<String> getWorldWhitelist() {
return this.worldWhitelist == null ? List.of() : List.copyOf(this.worldWhitelist);
}
public boolean hasRegionWhitelist() {
return this.regionWhitelist != null && !this.regionWhitelist.isEmpty();
}
public boolean isRegionAllowed(String regionName) {
if (!this.hasRegionWhitelist()) {
return true;
}
if (regionName == null) {
return false;
}
String target = regionName.toLowerCase(Locale.ROOT);
for (String allowed : this.regionWhitelist) {
if (allowed != null && allowed.toLowerCase(Locale.ROOT).equals(target)) {
return true;
}
}
return false;
}
public boolean isRegionAllowedAt(Location location) {
if (!this.hasRegionWhitelist()) {
return true;
}
return WorldGuardSupport.isInsideAnyRegion(location, this.regionWhitelist);
}
public List<String> getRegionWhitelist() {
return this.regionWhitelist == null ? List.of() : List.copyOf(this.regionWhitelist);
}
}
@@ -1,148 +0,0 @@
package org.battleplugins.arena.spleef;
import org.battleplugins.arena.ArenaPlayer;
import org.battleplugins.arena.BattleArena;
import org.battleplugins.arena.competition.Competition;
import org.battleplugins.arena.competition.LiveCompetition;
import org.battleplugins.arena.event.ArenaEventHandler;
import org.battleplugins.arena.spleef.arena.SpleefArena;
import org.battleplugins.arena.spleef.arena.SpleefCompetition;
import org.battleplugins.arena.spleef.arena.SpleefGame;
import org.battleplugins.arena.spleef.arena.SpleefMap;
import org.bukkit.entity.AbstractArrow;
import org.bukkit.entity.Player;
import org.bukkit.entity.Projectile;
import org.bukkit.entity.Snowball;
import org.bukkit.event.block.TNTPrimeEvent;
import org.bukkit.event.entity.ProjectileHitEvent;
import org.bukkit.projectiles.ProjectileSource;
import java.util.List;
import java.util.function.Function;
/**
* Contains custom event resolvers for Spleef events.
* <p>
* The events below would normally not be captured by the
* {@link ArenaEventHandler} annotation since there is no default logic for them
* to link back to a {@link Competition}. However, these custom resolvers allow
* for these events to be listened on, along with segmenting the logic to the
* appropriate arena and spleef game mode.
*/
public final class SpleefEventResolvers {
public static final Function<ProjectileHitEvent, LiveCompetition<?>> PROJECTILE_HIT = event -> {
if (event.getHitBlock() == null) {
return null;
}
Projectile projectile = event.getEntity();
ProjectileSource shooter = projectile.getShooter();
if (!(shooter instanceof Player player)) {
return null;
}
ArenaPlayer arenaPlayer = ArenaPlayer.getArenaPlayer(player);
if (arenaPlayer == null || !(arenaPlayer.getArena() instanceof SpleefArena arena)) {
return null;
}
SpleefConfig config = ArenaSpleef.getInstance().getMainConfig();
if (config != null && !config.isRegionAllowedAt(event.getHitBlock().getLocation())) {
return null;
}
if (projectile instanceof Snowball) {
if (arena.getGame() == SpleefGame.CLASSIC) {
return arenaPlayer.getCompetition();
}
return null;
}
if (projectile instanceof AbstractArrow) {
if (arena.getGame() == SpleefGame.BOW_SPLEEF) {
return arenaPlayer.getCompetition();
}
return null;
}
return null;
};
public static final Function<SpleefArena, Function<TNTPrimeEvent, LiveCompetition<?>>> TNT_PRIME = arena -> event -> {
if (arena.getGame() != SpleefGame.BOW_SPLEEF) {
return null;
}
if (!WorldGuardSupport.isEnabled()) {
return null;
}
List<Competition<?>> competitions = BattleArena.getInstance().getCompetitions(arena);
for (Competition<?> competition : competitions) {
if (!(competition instanceof SpleefCompetition spleefCompetition)) {
continue;
}
if (!spleefCompetition.getMap().getWorld().equals(event.getBlock().getWorld())) {
continue;
}
SpleefMap map = (SpleefMap) spleefCompetition.getMap();
String worldGuardRegion = map.getWorldGuardRegion();
if (worldGuardRegion == null || worldGuardRegion.isBlank()) {
continue;
}
SpleefConfig config = ArenaSpleef.getInstance().getMainConfig();
if (config != null && !config.isRegionAllowedAt(event.getBlock().getLocation())) {
continue;
}
if (WorldGuardSupport.isInsideRegion(event.getBlock().getLocation(), worldGuardRegion)) {
return spleefCompetition;
}
}
return null;
};
}
@@ -1,234 +0,0 @@
package org.battleplugins.arena.spleef;
import org.battleplugins.arena.BattleArena;
import org.battleplugins.arena.command.ArenaCommand;
import org.battleplugins.arena.command.ArenaCommandExecutor;
import org.battleplugins.arena.command.Argument;
import org.battleplugins.arena.competition.map.CompetitionMap;
import org.battleplugins.arena.competition.map.options.Bounds;
import org.battleplugins.arena.config.ParseException;
import org.battleplugins.arena.messages.Messages;
import org.battleplugins.arena.spleef.arena.SpleefArena;
import org.battleplugins.arena.spleef.arena.SpleefMap;
import org.battleplugins.arena.spleef.editor.SpleefEditorWizards;
import org.bukkit.entity.Player;
import java.io.IOException;
public class SpleefExecutor extends ArenaCommandExecutor {
public SpleefExecutor(SpleefArena arena) {
super(arena);
}
@Override
public void create(Player player) {
SpleefConfig config = ArenaSpleef.getInstance().getMainConfig();
String worldName = player.getWorld().getName();
if (config != null && !config.isWorldAllowed(worldName)) {
SpleefMessages.CREATE_WORLD_NOT_WHITELISTED.send(player, worldName);
return;
}
if (!WorldGuardSupport.isEnabled()) {
SpleefMessages.CREATE_WG_MISSING.send(player);
return;
}
if (config != null && !config.isRegionAllowedAt(player.getLocation())) {
SpleefMessages.CREATE_REGION_NOT_WHITELISTED.send(player);
return;
}
if (!WorldGuardSupport.isSpleefAllowedAt(player)) {
SpleefMessages.CREATE_WG_FLAG_REQUIRED.send(player);
return;
}
super.create(player);
}
@ArenaCommand(commands = "layer", subCommands = "add", description = "Adds a layer to a spleef arena.", permissionNode = "layer.add")
public void layer(Player player, CompetitionMap map) {
if (!(map instanceof SpleefMap spleefMap)) {
return; // Should not happen but just incase
}
SpleefEditorWizards.ADD_LAYER.openWizard(player, this.arena, ctx -> ctx.setMap(spleefMap));
}
@ArenaCommand(commands = "layer", subCommands = "remove", description = "Removes a layer from a spleef arena.", permissionNode = "layer.remove")
public void removeLayer(Player player, CompetitionMap map, @Argument(name = "index") int index) {
if (!(map instanceof SpleefMap spleefMap)) {
return; // Should not happen but just incase
}
index--;
if (index < 0 || index >= spleefMap.getLayers().size()) {
SpleefMessages.INVALID_LAYER.send(player, Integer.toString(spleefMap.getLayers().size()));
return;
}
spleefMap.removeLayer(spleefMap.getLayers().get(index));
SpleefMessages.LAYER_REMOVED.send(player);
}
@ArenaCommand(commands = "layer", subCommands = "clear", description = "Clears all layers from a spleef arena.", permissionNode = "layer.clear")
public void clearLayer(Player player, CompetitionMap map) {
if (!(map instanceof SpleefMap spleefMap)) {
return; // Should not happen but just incase
}
spleefMap.clearLayers();
SpleefMessages.LAYER_REMOVED_ALL.send(player);
}
@ArenaCommand(commands = "layer", subCommands = "index", description = "Changes the index of a layer.", permissionNode = "layer.index")
public void indexLayer(Player player, CompetitionMap map, @Argument(name = "from") int from,
@Argument(name = "to") int to)
{
if (!(map instanceof SpleefMap spleefMap)) {
return; // Should not happen but just incase
}
from--;
to--;
if (spleefMap.getLayers().isEmpty()) {
SpleefMessages.NO_LAYERS.send(player);
return;
}
if (from < 0 || from >= spleefMap.getLayers().size() || to < 0 || to >= spleefMap.getLayers().size()) {
SpleefMessages.INVALID_LAYER.send(player, Integer.toString(spleefMap.getLayers().size()));
return;
}
SpleefMap.Layer layer = spleefMap.getLayers().get(from);
spleefMap.removeLayer(layer);
spleefMap.addLayer(to, layer);
SpleefMessages.LAYER_INDEX_CHANGED.send(player);
}
@ArenaCommand(commands = "layer", subCommands = "list", description = "Lists all layers in a spleef arena.", permissionNode = "layer.list")
public void listLayers(Player player, CompetitionMap map) {
if (!(map instanceof SpleefMap spleefMap)) {
return; // Should not happen but just incase
}
if (spleefMap.getLayers().isEmpty()) {
SpleefMessages.NO_LAYERS.send(player);
return;
}
this.sendHeader(player);
for (int i = 0; i < spleefMap.getLayers().size(); i++) {
SpleefMap.Layer layer = spleefMap.getLayers().get(i);
Bounds bounds = layer.getBounds();
String minBounds = bounds.getMinX() + ", " + bounds.getMinY() + ", " + bounds.getMinZ();
String maxBounds = bounds.getMaxX() + ", " + bounds.getMaxY() + ", " + bounds.getMaxZ();
SpleefMessages.LAYER_INFO.send(player, Integer.toString(i + 1), minBounds, maxBounds,
layer.getBlockData().getAsString());
}
}
@ArenaCommand(commands = "deathregion", description = "Sets the death region for a spleef arena.", permissionNode = "deathregion")
public void setDeathRegion(Player player, CompetitionMap map) {
if (!(map instanceof SpleefMap spleefMap)) {
return; // Should not happen but just incase
}
SpleefEditorWizards.DEATH_REGION.openWizard(player, this.arena, ctx -> ctx.setMap(spleefMap));
}
@ArenaCommand(commands = "worldguardregion", description = "Sets the WorldGuard region for a spleef arena.", permissionNode = "worldguardregion")
public void setWorldGuardRegion(Player player, CompetitionMap map, @Argument(name = "region") String region) {
if (!(map instanceof SpleefMap spleefMap)) {
return; // Should not happen but just incase
}
if (!WorldGuardSupport.isEnabled()) {
SpleefMessages.CREATE_WG_MISSING.send(player);
return;
}
if (spleefMap.getWorld() == null || !WorldGuardSupport.regionExists(spleefMap.getWorld(), region)) {
SpleefMessages.WORLDGUARD_REGION_NOT_FOUND.send(player, region);
return;
}
spleefMap.setWorldGuardRegion(region);
try {
spleefMap.save();
} catch (ParseException | IOException e) {
BattleArena.getInstance().error("Failed to save map file for arena {}", this.arena.getName(), e);
Messages.MAP_FAILED_TO_SAVE.send(player, spleefMap.getName());
return;
}
SpleefMessages.WORLDGUARD_REGION_SET.send(player, region);
}
}
@@ -1,61 +0,0 @@
package org.battleplugins.arena.spleef;
import com.sk89q.worldguard.WorldGuard;
import com.sk89q.worldguard.protection.flags.Flag;
import com.sk89q.worldguard.protection.flags.StateFlag;
import com.sk89q.worldguard.protection.flags.registry.FlagConflictException;
import com.sk89q.worldguard.protection.flags.registry.FlagRegistry;
import org.bukkit.plugin.Plugin;
public final class SpleefFlags {
public static final String ALLOW_SPLEEF_FLAG_NAME = "allow-spleef";
private static StateFlag allowSpleefFlag;
private SpleefFlags() {
}
public static void register(Plugin plugin) {
FlagRegistry registry = WorldGuard.getInstance().getFlagRegistry();
try {
StateFlag flag = new StateFlag(ALLOW_SPLEEF_FLAG_NAME, false);
registry.register(flag);
allowSpleefFlag = flag;
} catch (FlagConflictException conflict) {
Flag<?> existing = registry.get(ALLOW_SPLEEF_FLAG_NAME);
if (existing instanceof StateFlag stateFlag) {
allowSpleefFlag = stateFlag;
plugin.getSLF4JLogger().info("Reusing existing WorldGuard flag '{}'.", ALLOW_SPLEEF_FLAG_NAME);
} else {
plugin.getSLF4JLogger().error(
"WorldGuard flag '{}' is already registered with an incompatible type; spleef will not enforce it.",
ALLOW_SPLEEF_FLAG_NAME);
}
} catch (IllegalStateException late) {
plugin.getSLF4JLogger().error(
"Failed to register WorldGuard flag '{}' — registry already locked. Ensure Spleef-OG loads before WorldGuard enables.",
ALLOW_SPLEEF_FLAG_NAME, late);
}
}
public static StateFlag getAllowSpleefFlag() {
return allowSpleefFlag;
}
}
@@ -1,54 +0,0 @@
package org.battleplugins.arena.spleef;
import org.battleplugins.arena.messages.Message;
import org.battleplugins.arena.messages.Messages;
public final class SpleefMessages {
public static final Message LAYER_SET_MIN_POSITION = Messages.info("spleef-editor-layer-set-min-position",
"Click a block to set the minimum (first) position of the layer.");
public static final Message LAYER_SET_MAX_POSITION = Messages.info("spleef-editor-layer-set-max-position",
"Click a block to set the maximum (second) position of the layer.");
public static final Message LAYER_SET_BLOCK_DATA = Messages.info("spleef-editor-set-block-data",
"Enter the name of the block the layer should be made of. Type \"cancel\" to cancel.");
public static final Message LAYER_SET_BLOCK_DATA_INVALID = Messages.error("spleef-set-block-data-invalid",
"Invalid block name. Please try again.");
public static final Message DEATH_REGION_SET_MIN_POSITION = Messages.info(
"spleef-editor-death-region-set-min-position",
"Click a block to set the minimum (first) position of the death region.");
public static final Message DEATH_REGION_SET_MAX_POSITION = Messages.info(
"spleef-editor-death-region-set-max-position",
"Click a block to set the maximum (second) position of the death region.");
public static final Message LAYER_ADDED = Messages.success("spleef-layer-added", "Layer added successfully!");
public static final Message LAYER_REMOVED = Messages.success("spleef-layer-removed", "Layer removed successfully!");
public static final Message LAYER_REMOVED_ALL = Messages.success("spleef-layer-removed-all",
"All layers removed successfully!");
public static final Message LAYER_INDEX_CHANGED = Messages.success("spleef-layer-index-changed",
"Layer index changed successfully!");
public static final Message LAYER_INFO = Messages.info("spleef-layer-info",
"Layer <secondary>#{}</secondary>: <secondary>Min:</secondary> <primary>{}</primary>, <secondary>Max:</secondary> <primary>{}</primary>, <secondary>Block:</secondary> <primary>{}</primary>");
public static final Message INVALID_LAYER = Messages.error("spleef-invalid-layer",
"Invalid layer! There are only <secondary>{}</secondary> layers.");
public static final Message NO_LAYERS = Messages.error("spleef-no-layers", "There are no layers in this map!");
public static final Message DEATH_REGION_SET = Messages.success("spleef-death-region-set",
"Death region set successfully!");
public static final Message CREATE_WORLD_NOT_WHITELISTED = Messages.error("spleef-create-world-not-whitelisted",
"Spleef arenas cannot be created in this world. World <secondary>{}</secondary> is not in the Spleef world whitelist.");
public static final Message CREATE_REGION_NOT_WHITELISTED = Messages.error("spleef-create-region-not-whitelisted",
"Spleef arenas cannot be created here. Stand inside a WorldGuard region listed in the Spleef region whitelist.");
public static final Message CREATE_WG_FLAG_REQUIRED = Messages.error("spleef-create-wg-flag-required",
"You must stand inside a WorldGuard region that has the <secondary>allow-spleef</secondary> flag set to <secondary>allow</secondary> to create a Spleef arena here.");
public static final Message CREATE_WG_MISSING = Messages.error("spleef-create-wg-missing",
"WorldGuard is not loaded; Spleef arena creation is disabled.");
public static final Message WORLD_BLOCKED_AT_RUNTIME = Messages.error("spleef-world-blocked",
"Spleef matches are not permitted in world <secondary>{}</secondary>.");
public static final Message MATCH_REGION_NOT_WHITELISTED = Messages.error("spleef-match-region-not-whitelisted",
"This Spleef map is not inside a whitelisted WorldGuard region.");
public static final Message WORLDGUARD_REGION_NOT_FOUND = Messages.error("spleef-worldguard-region-not-found",
"WorldGuard region <secondary>{}</secondary> was not found in this map's world.");
public static final Message WORLDGUARD_REGION_SET = Messages.success("spleef-worldguard-region-set",
"WorldGuard region set to <secondary>{}</secondary>.");
}
@@ -1,114 +0,0 @@
package org.battleplugins.arena.spleef;
import com.sk89q.worldedit.bukkit.BukkitAdapter;
import com.sk89q.worldedit.math.BlockVector3;
import com.sk89q.worldguard.LocalPlayer;
import com.sk89q.worldguard.WorldGuard;
import com.sk89q.worldguard.bukkit.WorldGuardPlugin;
import com.sk89q.worldguard.protection.ApplicableRegionSet;
import com.sk89q.worldguard.protection.flags.StateFlag;
import com.sk89q.worldguard.protection.managers.RegionManager;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import java.util.List;
public final class WorldGuardSupport {
private WorldGuardSupport() {
}
public static boolean isEnabled() {
return Bukkit.getPluginManager().isPluginEnabled("WorldGuard");
}
public static boolean isInsideRegion(Location location, String regionId) {
if (location.getWorld() == null) {
return false;
}
RegionManager regionManager = WorldGuard.getInstance().getPlatform().getRegionContainer()
.get(BukkitAdapter.adapt(location.getWorld()));
if (regionManager == null) {
return false;
}
BlockVector3 position = BlockVector3.at(location.getBlockX(), location.getBlockY(), location.getBlockZ());
ApplicableRegionSet regions = regionManager.getApplicableRegions(position);
return regions.getRegions().stream().anyMatch(region -> region.getId().equalsIgnoreCase(regionId));
}
public static boolean isInsideAnyRegion(Location location, List<String> regionIds) {
if (location == null || location.getWorld() == null || regionIds == null || regionIds.isEmpty()) {
return false;
}
RegionManager regionManager = WorldGuard.getInstance().getPlatform().getRegionContainer()
.get(BukkitAdapter.adapt(location.getWorld()));
if (regionManager == null) {
return false;
}
BlockVector3 position = BlockVector3.at(location.getBlockX(), location.getBlockY(), location.getBlockZ());
ApplicableRegionSet regions = regionManager.getApplicableRegions(position);
return regions.getRegions().stream().anyMatch(region -> regionIds.stream()
.anyMatch(allowed -> allowed != null && region.getId().equalsIgnoreCase(allowed)));
}
public static boolean regionExists(World world, String regionId) {
RegionManager regionManager = WorldGuard.getInstance().getPlatform().getRegionContainer()
.get(BukkitAdapter.adapt(world));
if (regionManager == null) {
return false;
}
return regionManager.getRegion(regionId) != null;
}
public static boolean isSpleefAllowedAt(Player player) {
StateFlag flag = SpleefFlags.getAllowSpleefFlag();
if (flag == null) {
return false;
}
LocalPlayer localPlayer = WorldGuardPlugin.inst().wrapPlayer(player);
Location location = player.getLocation();
RegionManager regionManager = WorldGuard.getInstance().getPlatform().getRegionContainer()
.get(BukkitAdapter.adapt(location.getWorld()));
if (regionManager == null) {
return false;
}
BlockVector3 position = BlockVector3.at(location.getBlockX(), location.getBlockY(), location.getBlockZ());
ApplicableRegionSet regions = regionManager.getApplicableRegions(position);
return regions.testState(localPlayer, flag);
}
}
@@ -1,40 +0,0 @@
package org.battleplugins.arena.spleef.action;
import org.battleplugins.arena.ArenaPlayer;
import org.battleplugins.arena.event.action.EventAction;
import org.battleplugins.arena.resolver.Resolvable;
import org.battleplugins.arena.spleef.ArenaSpleef;
import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataType;
import java.util.Map;
public class GiveShovelAction extends EventAction {
private static final String SHOVEL_KEY = "shovel";
public GiveShovelAction(Map<String, String> params) {
super(params, SHOVEL_KEY);
}
@Override
public void call(ArenaPlayer arenaPlayer, Resolvable resolvable) {
ItemStack shovel = ArenaSpleef.getInstance().getMainConfig().getShovel(this.get(SHOVEL_KEY));
if (shovel == null) {
ArenaSpleef.getInstance().getSLF4JLogger()
.warn("Invalid shovel " + this.get(SHOVEL_KEY) + ". Not giving shovel to player.");
return;
}
shovel.editMeta(meta -> meta.getPersistentDataContainer().set(ArenaSpleef.getInstance().getSpleefItemKey(),
PersistentDataType.BOOLEAN, true));
arenaPlayer.getPlayer().getInventory().addItem(shovel);
}
}
@@ -1,38 +0,0 @@
package org.battleplugins.arena.spleef.action;
import org.battleplugins.arena.Arena;
import org.battleplugins.arena.ArenaPlayer;
import org.battleplugins.arena.competition.Competition;
import org.battleplugins.arena.event.action.EventAction;
import org.battleplugins.arena.resolver.Resolvable;
import org.battleplugins.arena.spleef.arena.SpleefCompetition;
import java.util.Map;
public class PasteLayersAction extends EventAction {
public PasteLayersAction(Map<String, String> params) {
super(params);
}
@Override
public void call(ArenaPlayer arenaPlayer, Resolvable resolvable) {
}
@Override
public void postProcess(Arena arena, Competition<?> competition, Resolvable resolvable) {
if (!(competition instanceof SpleefCompetition spleefCompetition)) {
return;
}
spleefCompetition.pasteLayers();
}
}
@@ -1,50 +0,0 @@
package org.battleplugins.arena.spleef.api;
import org.battleplugins.arena.ArenaPlayer;
import org.battleplugins.arena.spleef.arena.SpleefCompetition;
import org.bukkit.entity.Player;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
public final class SpleefAPI {
private static final Set<UUID> PLAYERS_IN_SPLEEF = Collections.synchronizedSet(new HashSet<>());
private SpleefAPI() {
}
public static boolean isInSpleef(Player player) {
if (player == null) {
return false;
}
return PLAYERS_IN_SPLEEF.contains(player.getUniqueId());
}
static boolean markJoined(Player player) {
return PLAYERS_IN_SPLEEF.add(player.getUniqueId());
}
static boolean markLeft(Player player) {
return PLAYERS_IN_SPLEEF.remove(player.getUniqueId());
}
static boolean isSpleefPlayer(ArenaPlayer arenaPlayer) {
return arenaPlayer != null && arenaPlayer.getCompetition() instanceof SpleefCompetition;
}
}
@@ -1,49 +0,0 @@
package org.battleplugins.arena.spleef.api;
import org.battleplugins.arena.event.player.ArenaJoinEvent;
import org.battleplugins.arena.event.player.ArenaLeaveEvent;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
public final class SpleefApiListener implements Listener {
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onArenaJoin(ArenaJoinEvent event) {
if (!SpleefAPI.isSpleefPlayer(event.getArenaPlayer())) {
return;
}
Player player = event.getPlayer();
if (SpleefAPI.markJoined(player)) {
Bukkit.getPluginManager().callEvent(new SpleefJoinEvent(player));
}
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onArenaLeave(ArenaLeaveEvent event) {
if (!SpleefAPI.isSpleefPlayer(event.getArenaPlayer())) {
return;
}
Player player = event.getPlayer();
if (SpleefAPI.markLeft(player)) {
Bukkit.getPluginManager().callEvent(new SpleefLeaveEvent(player));
}
}
}
@@ -1,32 +0,0 @@
package org.battleplugins.arena.spleef.api;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import org.bukkit.event.player.PlayerEvent;
import org.jetbrains.annotations.NotNull;
public class SpleefJoinEvent extends PlayerEvent {
private static final HandlerList HANDLERS = new HandlerList();
public SpleefJoinEvent(Player player) {
super(player);
}
@NotNull
@Override
public HandlerList getHandlers() {
return HANDLERS;
}
public static HandlerList getHandlerList() {
return HANDLERS;
}
}
@@ -1,286 +0,0 @@
package org.battleplugins.arena.spleef.arena;
import io.papermc.paper.math.BlockPosition;
import org.battleplugins.arena.Arena;
import org.battleplugins.arena.ArenaPlayer;
import org.battleplugins.arena.command.ArenaCommandExecutor;
import org.battleplugins.arena.competition.map.MapFactory;
import org.battleplugins.arena.competition.phase.CompetitionPhaseType;
import org.battleplugins.arena.config.ArenaOption;
import org.battleplugins.arena.event.ArenaEventHandler;
import org.battleplugins.arena.event.arena.ArenaPhaseStartEvent;
import org.battleplugins.arena.event.player.ArenaLeaveEvent;
import org.battleplugins.arena.options.ArenaOptionType;
import org.battleplugins.arena.options.types.BooleanArenaOption;
import org.battleplugins.arena.spleef.ArenaSpleef;
import org.battleplugins.arena.spleef.SpleefEventResolvers;
import org.battleplugins.arena.spleef.SpleefExecutor;
import org.battleplugins.arena.spleef.SpleefMessages;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.type.Snow;
import org.bukkit.entity.Player;
import org.bukkit.entity.TNTPrimed;
import org.bukkit.event.EventPriority;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.TNTPrimeEvent;
import org.bukkit.event.entity.ProjectileHitEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.inventory.ItemStack;
import java.util.ArrayList;
import java.util.List;
public class SpleefArena extends Arena {
@ArenaOption(name = "game", description = "The spleef game.")
private SpleefGame game = SpleefGame.CLASSIC;
public SpleefArena() {
super();
this.getEventManager().registerArenaResolver(ProjectileHitEvent.class, SpleefEventResolvers.PROJECTILE_HIT);
this.getEventManager().registerArenaResolver(TNTPrimeEvent.class, SpleefEventResolvers.TNT_PRIME.apply(this));
}
@Override
public ArenaCommandExecutor createCommandExecutor() {
return new SpleefExecutor(this);
}
@Override
public MapFactory getMapFactory() {
return SpleefMap.FACTORY;
}
@ArenaEventHandler
public void onPhaseStart(ArenaPhaseStartEvent event) {
if (!CompetitionPhaseType.INGAME.equals(event.getPhase().getType())) {
return;
}
if (event.getCompetition() instanceof SpleefCompetition spleefCompetition) {
org.bukkit.World mapWorld = spleefCompetition.getMap().getWorld();
String worldName = mapWorld == null ? null : mapWorld.getName();
if (!ArenaSpleef.getInstance().getMainConfig().isWorldAllowed(worldName)) {
ArenaSpleef.getInstance().getSLF4JLogger().warn(
"Refusing to start Spleef competition '{}' — world '{}' is not in the Spleef world whitelist.",
spleefCompetition.getMap().getName(), worldName);
this.leaveWorldBlockedCompetition(spleefCompetition, worldName);
return;
}
if (!spleefCompetition.isMatchRegionAllowed()) {
ArenaSpleef.getInstance().getSLF4JLogger().warn(
"Refusing to start Spleef competition '{}' — map is not inside a whitelisted WorldGuard region.",
spleefCompetition.getMap().getName());
this.leaveRegionBlockedCompetition(spleefCompetition);
return;
}
}
}
@ArenaEventHandler(priority = EventPriority.LOW)
public void onBlockBreak(BlockBreakEvent event, ArenaPlayer player) {
// Ensure that the game is the classic spleef game
if (this.game != SpleefGame.CLASSIC) {
return;
}
if (player.getCompetition().option(ArenaOptionType.BLOCK_BREAK).map(BooleanArenaOption::isEnabled)
.orElse(true))
{
return; // Game already allows block breaking
}
SpleefMap spleefMap = (SpleefMap) player.getCompetition().getMap();
BlockPosition pos = event.getBlock().getLocation().toBlock();
SpleefMap.Layer layer = spleefMap.getLayer(pos);
if (layer != null) {
if (!(player.getCompetition() instanceof SpleefCompetition competition)
|| !competition.isLocationAllowed(event.getBlock().getLocation()))
{
return;
}
event.setCancelled(false);
this.giveSnowballsFromFloor(event);
}
}
@ArenaEventHandler(priority = EventPriority.LOW)
public void onMove(PlayerMoveEvent event, SpleefCompetition competition) {
if (event.getFrom().getBlockX() == event.getTo().getBlockX()
&& event.getFrom().getBlockY() == event.getTo().getBlockY()
&& event.getFrom().getBlockZ() == event.getTo().getBlockZ())
{
return; // Same block, skip hot-path work
}
SpleefMap spleefMap = (SpleefMap) competition.getMap();
if (spleefMap.getDeathRegion() != null && spleefMap.getDeathRegion().isInside(event.getTo())) {
event.getPlayer().damage(10000.0D); // Kill the player
}
}
// For the events below:
// We'd only ever capture this event if the conditions in the event resolvers
// are met, so we can safely run the below code without having to check for a
// lot of conditions that would otherwise be necessary.
@ArenaEventHandler(priority = EventPriority.LOW)
public void onProjectileHit(ProjectileHitEvent event, SpleefCompetition competition) {
Block block = event.getHitBlock();
SpleefMap spleefMap = (SpleefMap) competition.getMap();
BlockPosition pos = block.getLocation().toBlock();
SpleefMap.Layer layer = spleefMap.getLayer(pos);
if (layer == null) {
return;
}
if (!competition.isLocationAllowed(block.getLocation())) {
return;
}
block.setType(Material.AIR);
}
@ArenaEventHandler(priority = EventPriority.LOW)
public void onTntPrime(TNTPrimeEvent event) {
event.setCancelled(true);
if (event.getCause() == TNTPrimeEvent.PrimeCause.PROJECTILE) {
if (event.getPrimingEntity() != null && !(event.getPrimingEntity() instanceof Player)) {
event.getPrimingEntity().remove();
}
event.getBlock().setType(Material.AIR);
TNTPrimed tntEntity = event.getBlock().getWorld().spawn(event.getBlock().getLocation().toCenterLocation(),
TNTPrimed.class, tnt ->
{
tnt.setFuseTicks(Integer.MAX_VALUE);
});
Bukkit.getScheduler().runTaskLater(ArenaSpleef.getInstance(), tntEntity::remove, 20);
}
}
private void giveSnowballsFromFloor(BlockBreakEvent event) {
int amount = this.getSnowballDropCount(event.getBlock());
if (amount <= 0) {
return;
}
event.setDropItems(false);
event.getPlayer().getInventory().addItem(new ItemStack(Material.SNOWBALL, amount));
}
private int getSnowballDropCount(Block block) {
if (block.getType() == Material.SNOW_BLOCK) {
return 4;
}
if (block.getType() != Material.SNOW) {
return 0;
}
BlockData blockData = block.getBlockData();
if (blockData instanceof Snow snow) {
return snow.getLayers();
}
return 1;
}
private void leaveWorldBlockedCompetition(SpleefCompetition competition, String worldName) {
List<ArenaPlayer> players = new ArrayList<>(competition.getPlayers());
players.addAll(competition.getSpectators());
for (ArenaPlayer player : players) {
SpleefMessages.WORLD_BLOCKED_AT_RUNTIME.send(player.getPlayer(), worldName);
competition.leave(player, ArenaLeaveEvent.Cause.PLUGIN);
}
}
private void leaveRegionBlockedCompetition(SpleefCompetition competition) {
List<ArenaPlayer> players = new ArrayList<>(competition.getPlayers());
players.addAll(competition.getSpectators());
for (ArenaPlayer player : players) {
SpleefMessages.MATCH_REGION_NOT_WHITELISTED.send(player.getPlayer());
competition.leave(player, ArenaLeaveEvent.Cause.PLUGIN);
}
}
public SpleefGame getGame() {
return this.game;
}
}
@@ -1,140 +0,0 @@
package org.battleplugins.arena.spleef.arena;
import org.battleplugins.arena.competition.CompetitionType;
import org.battleplugins.arena.competition.JoinResult;
import org.battleplugins.arena.competition.LiveCompetition;
import org.battleplugins.arena.competition.PlayerRole;
import org.battleplugins.arena.competition.map.options.Bounds;
import org.battleplugins.arena.spleef.ArenaSpleef;
import org.battleplugins.arena.spleef.SpleefConfig;
import org.battleplugins.arena.spleef.SpleefMessages;
import org.battleplugins.arena.spleef.WorldGuardSupport;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class SpleefCompetition extends LiveCompetition<SpleefCompetition> {
private static final JoinResult REGION_NOT_JOINABLE = new JoinResult(false,
SpleefMessages.MATCH_REGION_NOT_WHITELISTED);
private final SpleefMap map;
public SpleefCompetition(SpleefArena arena, CompetitionType type, SpleefMap map) {
super(arena, type, map);
this.map = map;
}
@Override
public CompletableFuture<JoinResult> canJoin(Collection<Player> players, PlayerRole role) {
if (!this.isMatchRegionAllowed()) {
return CompletableFuture.completedFuture(REGION_NOT_JOINABLE);
}
return super.canJoin(players, role);
}
public boolean isMatchRegionAllowed() {
SpleefConfig config = ArenaSpleef.getInstance().getMainConfig();
if (config == null || !config.hasRegionWhitelist()) {
return true;
}
World world = this.map.getWorld();
if (world == null) {
return false;
}
String worldGuardRegion = this.map.getWorldGuardRegion();
if (config.isRegionAllowed(worldGuardRegion) && WorldGuardSupport.regionExists(world, worldGuardRegion)) {
return true;
}
List<SpleefMap.Layer> layers = this.map.getLayers();
if (!layers.isEmpty()) {
return layers.stream().allMatch(layer -> this.isBoundsInAllowedRegion(layer.getBounds()));
}
return this.map.bounds().map(this::isBoundsInAllowedRegion).orElse(false);
}
public boolean isLocationAllowed(Location location) {
SpleefConfig config = ArenaSpleef.getInstance().getMainConfig();
return config == null || config.isRegionAllowedAt(location);
}
public void pasteLayers() {
List<SpleefMap.Layer> layers = this.map.getLayers();
for (SpleefMap.Layer layer : layers) {
for (int x = layer.getBounds().getMinX(); x <= layer.getBounds().getMaxX(); x++) {
for (int y = layer.getBounds().getMinY(); y <= layer.getBounds().getMaxY(); y++) {
for (int z = layer.getBounds().getMinZ(); z <= layer.getBounds().getMaxZ(); z++) {
Block block = this.map.getWorld().getBlockAt(x, y, z);
if (!this.isLocationAllowed(block.getLocation())) {
continue;
}
block.setBlockData(layer.getBlockData());
}
}
}
}
}
private boolean isBoundsInAllowedRegion(Bounds bounds) {
World world = this.map.getWorld();
if (world == null) {
return false;
}
return this.isLocationAllowed(new Location(world, bounds.getMinX(), bounds.getMinY(), bounds.getMinZ()))
&& this.isLocationAllowed(new Location(world, bounds.getMinX(), bounds.getMinY(), bounds.getMaxZ()))
&& this.isLocationAllowed(new Location(world, bounds.getMinX(), bounds.getMaxY(), bounds.getMinZ()))
&& this.isLocationAllowed(new Location(world, bounds.getMinX(), bounds.getMaxY(), bounds.getMaxZ()))
&& this.isLocationAllowed(new Location(world, bounds.getMaxX(), bounds.getMinY(), bounds.getMinZ()))
&& this.isLocationAllowed(new Location(world, bounds.getMaxX(), bounds.getMinY(), bounds.getMaxZ()))
&& this.isLocationAllowed(new Location(world, bounds.getMaxX(), bounds.getMaxY(), bounds.getMinZ()))
&& this.isLocationAllowed(new Location(world, bounds.getMaxX(), bounds.getMaxY(), bounds.getMaxZ()));
}
}
@@ -1,5 +0,0 @@
package org.battleplugins.arena.spleef.arena;
public enum SpleefGame {
CLASSIC, BOW_SPLEEF
}
@@ -1,236 +0,0 @@
package org.battleplugins.arena.spleef.arena;
import io.papermc.paper.math.Position;
import org.battleplugins.arena.Arena;
import org.battleplugins.arena.competition.LiveCompetition;
import org.battleplugins.arena.competition.map.LiveCompetitionMap;
import org.battleplugins.arena.competition.map.MapFactory;
import org.battleplugins.arena.competition.map.MapType;
import org.battleplugins.arena.competition.map.options.Bounds;
import org.battleplugins.arena.competition.map.options.Spawns;
import org.battleplugins.arena.config.ArenaOption;
import org.bukkit.block.data.BlockData;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
public class SpleefMap extends LiveCompetitionMap {
static final MapFactory FACTORY = MapFactory.create(SpleefMap.class, SpleefMap::new);
@ArenaOption(name = "layers", description = "The layers for this spleef map.")
private List<Layer> layers;
@ArenaOption(name = "death-region", description = "The region where players will die if they fall into.")
private Bounds deathRegion;
@ArenaOption(name = "worldguard-region", description = "The WorldGuard region ID used for match region checks and Bow Spleef TNT.")
private String worldGuardRegion;
private final Map<Position, Layer> positionToLayers = new HashMap<>();
public SpleefMap() {
}
public SpleefMap(String name, Arena arena, MapType type, String world, @Nullable Bounds bounds,
@Nullable Spawns spawns)
{
super(name, arena, type, world, bounds, spawns);
}
@Override
public void postProcess() {
super.postProcess();
if (this.layers != null) {
for (Layer layer : this.layers) {
indexLayer(layer);
}
}
}
private void indexLayer(Layer layer) {
Bounds bounds = layer.getBounds();
for (int x = bounds.getMinX(); x <= bounds.getMaxX(); x++) {
for (int y = bounds.getMinY(); y <= bounds.getMaxY(); y++) {
for (int z = bounds.getMinZ(); z <= bounds.getMaxZ(); z++) {
this.positionToLayers.put(Position.block(x, y, z), layer);
}
}
}
}
private void unindexLayer(Layer layer) {
Iterator<Map.Entry<Position, Layer>> it = this.positionToLayers.entrySet().iterator();
while (it.hasNext()) {
if (it.next().getValue() == layer) {
it.remove();
}
}
}
@Override
public LiveCompetition<SpleefCompetition> createCompetition(Arena arena) {
if (!(arena instanceof SpleefArena spleefArena)) {
throw new IllegalArgumentException("Arena must be a spleef arena!");
}
return new SpleefCompetition(spleefArena, arena.getType(), this);
}
public List<Layer> getLayers() {
return this.layers == null ? List.of() : List.copyOf(this.layers);
}
public void addLayer(Layer layer) {
if (this.layers == null) {
this.layers = new ArrayList<>();
}
this.layers.add(layer);
indexLayer(layer);
}
public void addLayer(int index, Layer layer) {
if (this.layers == null) {
this.layers = new ArrayList<>();
}
this.layers.add(index, layer);
indexLayer(layer);
}
public void removeLayer(Layer layer) {
if (this.layers == null) {
return;
}
this.layers.remove(layer);
unindexLayer(layer);
}
public void clearLayers() {
if (this.layers == null) {
return;
}
this.layers.clear();
this.positionToLayers.clear();
}
@Nullable
public Layer getLayer(Position position) {
return this.positionToLayers.get(position);
}
@Nullable
public Bounds getDeathRegion() {
return this.deathRegion;
}
public void setDeathRegion(Bounds deathRegion) {
this.deathRegion = deathRegion;
}
@Nullable
public String getWorldGuardRegion() {
return this.worldGuardRegion;
}
public void setWorldGuardRegion(String worldGuardRegion) {
this.worldGuardRegion = worldGuardRegion;
}
public static class Layer {
@ArenaOption(name = "bounds", description = "The bounds of this layer.", required = true)
private Bounds bounds;
@ArenaOption(name = "block-data", description = "The block data of this layer.", required = true)
private BlockData blockData;
public Layer() {
}
public Layer(Bounds bounds, BlockData blockData) {
this.bounds = bounds;
this.blockData = blockData;
}
public Bounds getBounds() {
return this.bounds;
}
public BlockData getBlockData() {
return this.blockData;
}
}
}
@@ -1,65 +0,0 @@
package org.battleplugins.arena.spleef.editor;
import io.papermc.paper.math.Position;
import org.battleplugins.arena.Arena;
import org.battleplugins.arena.editor.ArenaEditorWizard;
import org.battleplugins.arena.editor.EditorContext;
import org.battleplugins.arena.spleef.arena.SpleefMap;
import org.bukkit.entity.Player;
public class DeathRegionContext extends EditorContext<DeathRegionContext> {
private SpleefMap map;
private Position min;
private Position max;
public DeathRegionContext(ArenaEditorWizard<DeathRegionContext> wizard, Arena arena, Player player) {
super(wizard, arena, player);
}
public SpleefMap getMap() {
return this.map;
}
public void setMap(SpleefMap map) {
this.map = map;
}
public Position getMin() {
return this.min;
}
public void setMin(Position min) {
this.min = min;
}
public Position getMax() {
return this.max;
}
public void setMax(Position max) {
this.max = max;
}
@Override
public boolean isComplete() {
return this.map != null && this.min != null && this.max != null;
}
}
@@ -1,79 +0,0 @@
package org.battleplugins.arena.spleef.editor;
import io.papermc.paper.math.Position;
import org.battleplugins.arena.Arena;
import org.battleplugins.arena.editor.ArenaEditorWizard;
import org.battleplugins.arena.editor.EditorContext;
import org.battleplugins.arena.spleef.arena.SpleefMap;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Player;
public class LayerContext extends EditorContext<LayerContext> {
private SpleefMap map;
private Position min;
private Position max;
private BlockData blockData;
public LayerContext(ArenaEditorWizard<LayerContext> wizard, Arena arena, Player player) {
super(wizard, arena, player);
}
public SpleefMap getMap() {
return this.map;
}
public void setMap(SpleefMap map) {
this.map = map;
}
public Position getMin() {
return this.min;
}
public void setMin(Position min) {
this.min = min;
}
public Position getMax() {
return this.max;
}
public void setMax(Position max) {
this.max = max;
}
public BlockData getBlockData() {
return this.blockData;
}
public void setBlockData(BlockData blockData) {
this.blockData = blockData;
}
@Override
public boolean isComplete() {
return this.map != null && this.min != null && this.max != null && this.blockData != null;
}
}
@@ -1,24 +0,0 @@
package org.battleplugins.arena.spleef.editor;
import org.battleplugins.arena.editor.type.EditorKey;
public enum LayerOption implements EditorKey {
BLOCK_DATA("blockData");
private final String key;
LayerOption(String key) {
this.key = key;
}
@Override
public String getKey() {
return this.key;
}
}
@@ -1,97 +0,0 @@
package org.battleplugins.arena.spleef.editor;
import org.battleplugins.arena.BattleArena;
import org.battleplugins.arena.competition.map.options.Bounds;
import org.battleplugins.arena.config.ParseException;
import org.battleplugins.arena.editor.ArenaEditorWizard;
import org.battleplugins.arena.editor.ArenaEditorWizards;
import org.battleplugins.arena.editor.stage.PositionInputStage;
import org.battleplugins.arena.editor.stage.TextInputStage;
import org.battleplugins.arena.editor.type.MapOption;
import org.battleplugins.arena.messages.Messages;
import org.battleplugins.arena.spleef.SpleefMessages;
import org.battleplugins.arena.spleef.arena.SpleefMap;
import org.bukkit.Bukkit;
import java.io.IOException;
public final class SpleefEditorWizards {
public static final ArenaEditorWizard<LayerContext> ADD_LAYER = ArenaEditorWizards.createWizard(LayerContext::new)
.addStage(MapOption.MIN_POS,
new PositionInputStage<>(SpleefMessages.LAYER_SET_MIN_POSITION, ctx -> ctx::setMin))
.addStage(MapOption.MAX_POS,
new PositionInputStage<>(SpleefMessages.LAYER_SET_MAX_POSITION, ctx -> ctx::setMax))
.addStage(LayerOption.BLOCK_DATA, new TextInputStage<>(SpleefMessages.LAYER_SET_BLOCK_DATA,
SpleefMessages.LAYER_SET_BLOCK_DATA_INVALID, (ctx, str) ->
{
try {
Bukkit.createBlockData(str);
} catch (IllegalArgumentException e) {
return false;
}
return true;
}, ctx -> str -> ctx.setBlockData(Bukkit.createBlockData(str)))).onCreationComplete(ctx -> {
Bounds bounds = new Bounds(ctx.getMin(), ctx.getMax());
SpleefMap map = ctx.getMap();
SpleefMap.Layer layer = new SpleefMap.Layer(bounds, ctx.getBlockData());
map.addLayer(layer);
try {
map.save();
} catch (ParseException | IOException e) {
BattleArena.getInstance().error("Failed to save map file for arena {}",
ctx.getArena().getName(), e);
Messages.MAP_FAILED_TO_SAVE.send(ctx.getPlayer(), map.getName());
return;
}
SpleefMessages.LAYER_ADDED.send(ctx.getPlayer());
});
public static final ArenaEditorWizard<DeathRegionContext> DEATH_REGION = ArenaEditorWizards
.createWizard(DeathRegionContext::new)
.addStage(MapOption.MIN_POS,
new PositionInputStage<>(SpleefMessages.DEATH_REGION_SET_MIN_POSITION, ctx -> ctx::setMin))
.addStage(MapOption.MAX_POS,
new PositionInputStage<>(SpleefMessages.DEATH_REGION_SET_MAX_POSITION, ctx -> ctx::setMax))
.onCreationComplete(ctx ->
{
Bounds bounds = new Bounds(ctx.getMin(), ctx.getMax());
SpleefMap map = ctx.getMap();
map.setDeathRegion(bounds);
try {
map.save();
} catch (ParseException | IOException e) {
BattleArena.getInstance().error("Failed to save map file for arena {}", ctx.getArena().getName(),
e);
Messages.MAP_FAILED_TO_SAVE.send(ctx.getPlayer(), map.getName());
return;
}
SpleefMessages.DEATH_REGION_SET.send(ctx.getPlayer());
});
}
@@ -1,139 +0,0 @@
package org.battleplugins.arena.spleef.hook;
import org.battleplugins.arena.event.player.ArenaJoinEvent;
import org.battleplugins.arena.event.player.ArenaLeaveEvent;
import org.battleplugins.arena.event.player.ArenaSpectateEvent;
import org.battleplugins.arena.spleef.api.SpleefAPI;
import org.battleplugins.arena.spleef.arena.SpleefCompetition;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.permissions.PermissionAttachment;
import org.bukkit.plugin.Plugin;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public final class GameModeInventoriesListener implements Listener {
public static final String PLUGIN_NAME = "GameModeInventories-OG";
private static final String USE_PERMISSION = "gamemodeinventories.use";
private final Plugin owner;
private final Map<UUID, PermissionAttachment> attachments = new HashMap<>();
private GameModeInventoriesListener(Plugin owner) {
this.owner = owner;
}
public static GameModeInventoriesListener register(Plugin owner) {
final Plugin gameModeInventories = Bukkit.getPluginManager().getPlugin(PLUGIN_NAME);
if (gameModeInventories == null || !gameModeInventories.isEnabled()) {
return null;
}
final GameModeInventoriesListener listener = new GameModeInventoriesListener(owner);
Bukkit.getPluginManager().registerEvents(listener, owner);
return listener;
}
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
public void onArenaJoin(ArenaJoinEvent event) {
if (event.getArenaPlayer().getCompetition() instanceof SpleefCompetition) {
this.suspend(event.getPlayer());
}
}
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
public void onArenaSpectate(ArenaSpectateEvent event) {
if (event.getArenaPlayer().getCompetition() instanceof SpleefCompetition) {
this.suspend(event.getPlayer());
}
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onArenaLeave(ArenaLeaveEvent event) {
if (!(event.getArenaPlayer().getCompetition() instanceof SpleefCompetition)) {
return;
}
final Player player = event.getPlayer();
Bukkit.getScheduler().runTask(this.owner, () -> {
if (!SpleefAPI.isInSpleef(player)) {
this.release(player);
}
});
}
public void releaseAll() {
for (PermissionAttachment attachment : this.attachments.values()) {
try {
attachment.remove();
} catch (IllegalArgumentException ignored) {
}
}
this.attachments.clear();
}
private void suspend(Player player) {
// Keep GMI from saving arena-cleared contents over real inventories.
this.attachments.computeIfAbsent(player.getUniqueId(),
key -> player.addAttachment(this.owner, USE_PERMISSION, false));
}
private void release(Player player) {
final PermissionAttachment attachment = this.attachments.remove(player.getUniqueId());
if (attachment == null) {
return;
}
try {
attachment.remove();
} catch (IllegalArgumentException ignored) {
}
}
}
@@ -1,121 +0,0 @@
package org.battleplugins.arena.spleef.hook;
import java.lang.reflect.Method;
import org.battleplugins.arena.spleef.api.SpleefAPI;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.EventException;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.plugin.EventExecutor;
import org.bukkit.plugin.Plugin;
public final class PlayerBountiesListener implements Listener {
public static final String PLUGIN_NAME = "PlayerBounties-OG";
private static final String CLAIM_EVENT_CLASS = "com.tcoded.playerbountiesplus.event.BountyClaimEvent";
private static final String SET_EVENT_CLASS = "com.tcoded.playerbountiesplus.event.BountySetEvent";
private PlayerBountiesListener() {
}
public static boolean register(Plugin owner) {
Plugin bounties = Bukkit.getPluginManager().getPlugin(PLUGIN_NAME);
if (bounties == null) {
return false;
}
Class<? extends Event> claimEventClass;
Class<? extends Event> setEventClass;
Method getClaimant;
Method getVictim;
Method getTarget;
try {
claimEventClass = Class.forName(CLAIM_EVENT_CLASS).asSubclass(Event.class);
setEventClass = Class.forName(SET_EVENT_CLASS).asSubclass(Event.class);
getClaimant = claimEventClass.getMethod("getClaimant");
getVictim = claimEventClass.getMethod("getVictim");
getTarget = setEventClass.getMethod("getTarget");
} catch (ClassNotFoundException | NoSuchMethodException | ClassCastException ex) {
owner.getSLF4JLogger().warn(
"PlayerBounties-OG is present but the expected API is missing; skipping bounty protection.", ex);
return false;
}
if (!Cancellable.class.isAssignableFrom(claimEventClass)
|| !Cancellable.class.isAssignableFrom(setEventClass))
{
owner.getSLF4JLogger().warn("PlayerBounties-OG events are not Cancellable; skipping bounty protection.");
return false;
}
PlayerBountiesListener listener = new PlayerBountiesListener();
Bukkit.getPluginManager().registerEvent(claimEventClass, listener, EventPriority.NORMAL, new EventExecutor() {
@Override
public void execute(Listener listener, Event event) throws EventException {
cancelIfInSpleef(event, getClaimant, getVictim);
}
}, owner, true);
Bukkit.getPluginManager().registerEvent(setEventClass, listener, EventPriority.NORMAL, new EventExecutor() {
@Override
public void execute(Listener listener, Event event) throws EventException {
cancelIfInSpleef(event, getTarget);
}
}, owner, true);
return true;
}
private static void cancelIfInSpleef(Event event, Method... playerAccessors) throws EventException {
for (Method accessor : playerAccessors) {
Player player;
try {
player = (Player) accessor.invoke(event);
} catch (ReflectiveOperationException | ClassCastException ex) {
throw new EventException(ex);
}
if (SpleefAPI.isInSpleef(player)) {
((Cancellable) event).setCancelled(true);
return;
}
}
}
}
-118
View File
@@ -1,118 +0,0 @@
name: Spleef
type: Match
# Optional. If config.yml region-whitelist is set, use a whitelisted region ID here.
# worldguard-region: spleef_classic
team-options:
named-teams: false
team-size: 1
team-amount: 2+
team-selection: none
modules:
- scoreboards
lives:
enabled: false
victory-conditions:
teams-alive:
amount: 1
time-limit:
time-limit: 5m
game: classic
events:
on-join:
- store{types=all}
- change-gamemode{gamemode=adventure}
- flight{enabled=false}
- teleport{location=waitroom}
on-spectate:
- store{types=all}
- change-gamemode{gamemode=spectator}
- flight{enabled=true}
- teleport{location=spectator}
on-leave:
- clear-effects
- restore{types=all}
- remove-scoreboard
on-death:
- clear-inventory
- respawn
- delay{ticks=1}
- change-gamemode{gamemode=adventure}
- teleport{location=waitroom}
options:
- block-break{enabled=false}
- block-place{enabled=false}
- block-drops{enabled=false}
- block-interact{enabled=false}
- item-drops{enabled=false}
- keep-inventory{enabled=true}
- keep-experience{enabled=true}
- damage-entities{option=never}
- damage-players{option=never}
initial-phase: waiting
phases:
waiting:
allow-join: true
next-phase: waitingforplayers
events:
on-start:
- apply-scoreboard{scoreboard=waiting}
- paste-layers
on-join:
- apply-scoreboard{scoreboard=waiting}
waitingforplayers:
allow-join: true
allow-spectate: true
revert-phase: true
next-phase: countdown
countdown-time: 45s
events:
on-start:
- broadcast{message=A spleef game is about to start! Do <aqua>/spleef join</aqua> to join;type=chat;audience=server}
- apply-scoreboard{scoreboard=countdown}
on-join:
- apply-scoreboard{scoreboard=countdown}
on-complete:
- teleport{location=team_spawn}
- play-sound{sound=block.note_block.pling;pitch=2;volume=1}
countdown:
allow-join: false
allow-spectate: true
revert-phase: true
next-phase: ingame
countdown-time: 5s
events:
on-start:
- apply-scoreboard{scoreboard=countdown}
- give-effects{effects=[slowness{duration=100;amplifier=255}]}
on-complete:
- play-sound{sound=block.note_block.pling;pitch=2;volume=1}
- clear-effects
ingame:
allow-join: false
allow-spectate: true
next-phase: victory
options:
- block-interact{enabled=true}
events:
on-start:
- change-gamemode{gamemode=survival}
- give-shovel{shovel=classic}
- apply-scoreboard{scoreboard=ingame-list-alive}
victory:
allow-join: false
allow-spectate: false
next-phase: waiting
duration: 5s
events:
on-complete:
- leave
- remove-scoreboard
on-victory:
- send-message{message=<green>Congrats, you won!</green>}
- play-sound{sound=entity.player.levelup;pitch=1;volume=1}
on-lose:
- send-message{message=<red>Sorry, you lost!</red>}
- play-sound{sound=block.anvil.place;pitch=0;volume=1}
on-draw:
- send-message{message=<yellow>It's a draw!</yellow>}
- play-sound{sound=block.beacon.deactivate;pitch=0;volume=1}
+22 -18
View File
@@ -1,22 +1,26 @@
# -----------------
# Main configuration for ArenaSpleef
#
# Documentation: https://docs.battleplugins.org/books/additional-gamemodes/chapter/spleef
# Support: https://discord.gg/tMVPVJf
# GitHub: https://github.com/BattlePlugins/ArenaSpleef
# -----------------
config-version: 2.0 # The config version, do not change!
# Worlds where Spleef arenas may be created and where matches may run.
# Leave empty to allow every world. Names are matched case-insensitively.
# Worlds where Spleef arenas may be created and matches may run.
# Spleef never copies worlds; MyWorlds is only used to ensure managed worlds load first.
world-whitelist:
- world
# WorldGuard regions where Spleef arenas may be created and matches may modify blocks.
# Leave empty to allow any region in an allowed world.
region-whitelist: []
match:
minimum-players: 2
waiting-seconds: 20
time-limit-seconds: 300
victory-seconds: 5
# The tools to use for Spleef.
shovels:
classic: diamond_shovel{display-name=<yellow>Spleef Shovel}
bow: bow{display-name=<red><bold>Fire Bow;enchantments=[unbreaking:10,infinity:1,flame:1]}
items:
classic:
material: DIAMOND_SHOVEL
name: '&eSpleef Shovel'
bow:
material: BOW
name: '&c&lFire Bow'
enchantments:
ARROW_INFINITE: 1
ARROW_FIRE: 1
DURABILITY: 10
scoreboard:
enabled: true
title: '&b&lSPLEEF'
+19 -6
View File
@@ -1,9 +1,22 @@
name: Spleef-OG
main: org.battleplugins.arena.spleef.ArenaSpleef
main: net.trueog.spleefog.SpleefPlugin
version: ${version}
description: Classic and Bow Spleef modes for BattleArena WorldGuard regions.
author: BattlePlugins
website: https://battleplugins.org
description: Classic and Bow Spleef in WorldGuard regions.
author: TrueOG
website: https://github.com/true-og/Spleef-OG
api-version: ${apiVersion}
depend: [BattleArena, WorldGuard]
softdepend: [GameModeInventories-OG, HorseTp-OG, PetTeleport-OG, PlayerBounties-OG, WorldEdit]
depend: [WorldGuard]
softdepend: [MyWorlds, GameModeInventories-OG, PlayerBounties-OG]
commands:
spleef:
description: Join, spectate, and manage Spleef arenas.
usage: /spleef
permissions:
spleef.play:
default: true
spleef.spectate:
default: true
spleef.stats:
default: true
spleef.admin:
default: op
-104
View File
@@ -1,104 +0,0 @@
name: BowSpleef
mode: Spleef
type: Match
team-options:
named-teams: false
team-size: 1
team-amount: 2+
team-selection: none
modules:
- scoreboards
lives:
enabled: false
victory-conditions:
teams-alive:
amount: 1
time-limit:
time-limit: 5m
game: bow_spleef
# Required for Bow Spleef TNT handling. If config.yml region-whitelist is set, use a whitelisted region ID here.
# worldguard-region: bowspleef
events:
on-join:
- store{types=all}
- change-gamemode{gamemode=adventure}
- flight{enabled=false}
- teleport{location=waitroom}
on-spectate:
- store{types=all}
- change-gamemode{gamemode=spectator}
- flight{enabled=true}
- teleport{location=spectator}
on-leave:
- clear-effects
- restore{types=all}
- remove-scoreboard
on-death:
- clear-inventory
- respawn
- delay{ticks=1}
- change-gamemode{gamemode=adventure}
- teleport{location=waitroom}
options:
- block-break{enabled=false}
- block-place{enabled=false}
- block-drops{enabled=false}
- block-interact{enabled=false}
- item-drops{enabled=false}
- keep-inventory{enabled=true}
- keep-experience{enabled=true}
- damage-entities{option=never}
- damage-players{option=never}
initial-phase: waiting
phases:
waiting:
allow-join: true
next-phase: countdown
events:
on-start:
- apply-scoreboard{scoreboard=waiting}
on-join:
- apply-scoreboard{scoreboard=waiting}
countdown:
allow-join: false
allow-spectate: true
revert-phase: true
next-phase: ingame
countdown-time: 5s
events:
on-start:
- paste-layers
- apply-scoreboard{scoreboard=countdown}
on-complete:
- teleport{location=team_spawn}
- play-sound{sound=block.note_block.pling;pitch=2;volume=1}
ingame:
allow-join: false
allow-spectate: true
next-phase: victory
options:
- block-interact{enabled=true}
events:
on-start:
- give-shovel{shovel=bow}
- give-item{item=arrow}
- change-gamemode{gamemode=survival}
- apply-scoreboard{scoreboard=ingame-list-alive}
victory:
allow-join: false
allow-spectate: false
next-phase: waiting
duration: 5s
events:
on-complete:
- leave
- remove-scoreboard
on-victory:
- send-message{message=<green>Congrats, you won!</green>}
- play-sound{sound=entity.player.levelup;pitch=1;volume=1}
on-lose:
- send-message{message=<red>Sorry, you lost!</red>}
- play-sound{sound=block.anvil.place;pitch=0;volume=1}
on-draw:
- send-message{message=<yellow>It's a draw!</yellow>}
- play-sound{sound=block.beacon.deactivate;pitch=0;volume=1}
-118
View File
@@ -1,118 +0,0 @@
name: Spleef
type: Match
# Optional. If config.yml region-whitelist is set, use a whitelisted region ID here.
# worldguard-region: spleef_classic
team-options:
named-teams: false
team-size: 1
team-amount: 2+
team-selection: none
modules:
- scoreboards
lives:
enabled: false
victory-conditions:
teams-alive:
amount: 1
time-limit:
time-limit: 5m
game: classic
events:
on-join:
- store{types=all}
- change-gamemode{gamemode=adventure}
- flight{enabled=false}
- teleport{location=waitroom}
on-spectate:
- store{types=all}
- change-gamemode{gamemode=spectator}
- flight{enabled=true}
- teleport{location=spectator}
on-leave:
- clear-effects
- restore{types=all}
- remove-scoreboard
on-death:
- clear-inventory
- respawn
- delay{ticks=1}
- change-gamemode{gamemode=adventure}
- teleport{location=waitroom}
options:
- block-break{enabled=false}
- block-place{enabled=false}
- block-drops{enabled=false}
- block-interact{enabled=false}
- item-drops{enabled=false}
- keep-inventory{enabled=true}
- keep-experience{enabled=true}
- damage-entities{option=never}
- damage-players{option=never}
initial-phase: waiting
phases:
waiting:
allow-join: true
next-phase: waitingforplayers
events:
on-start:
- apply-scoreboard{scoreboard=waiting}
- paste-layers
on-join:
- apply-scoreboard{scoreboard=waiting}
waitingforplayers:
allow-join: true
allow-spectate: true
revert-phase: true
next-phase: countdown
countdown-time: 45s
events:
on-start:
- broadcast{message=A spleef game is about to start! Do <aqua>/spleef join</aqua> to join;type=chat;audience=server}
- apply-scoreboard{scoreboard=countdown}
on-join:
- apply-scoreboard{scoreboard=countdown}
on-complete:
- teleport{location=team_spawn}
- play-sound{sound=block.note_block.pling;pitch=2;volume=1}
countdown:
allow-join: false
allow-spectate: true
revert-phase: true
next-phase: ingame
countdown-time: 5s
events:
on-start:
- apply-scoreboard{scoreboard=countdown}
- give-effects{effects=[slowness{duration=100;amplifier=255}]}
on-complete:
- play-sound{sound=block.note_block.pling;pitch=2;volume=1}
- clear-effects
ingame:
allow-join: false
allow-spectate: true
next-phase: victory
options:
- block-interact{enabled=true}
events:
on-start:
- change-gamemode{gamemode=survival}
- give-shovel{shovel=classic}
- apply-scoreboard{scoreboard=ingame-list-alive}
victory:
allow-join: false
allow-spectate: false
next-phase: waiting
duration: 5s
events:
on-complete:
- leave
- remove-scoreboard
on-victory:
- send-message{message=<green>Congrats, you won!</green>}
- play-sound{sound=entity.player.levelup;pitch=1;volume=1}
on-lose:
- send-message{message=<red>Sorry, you lost!</red>}
- play-sound{sound=block.anvil.place;pitch=0;volume=1}
on-draw:
- send-message{message=<yellow>It's a draw!</yellow>}
- play-sound{sound=block.beacon.deactivate;pitch=0;volume=1}