Initial commit
Build main / build (21) (push) Failing after 13m38s

This commit is contained in:
Laura
2026-08-09 15:17:12 +02:00
commit 6bf2a93935
285 changed files with 32332 additions and 0 deletions
@@ -0,0 +1,460 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem;
import de.eintosti.buildsystem.api.BuildSystem;
import de.eintosti.buildsystem.api.BuildSystemApi;
import de.eintosti.buildsystem.api.player.BuildPlayer;
import de.eintosti.buildsystem.api.player.settings.Settings;
import de.eintosti.buildsystem.api.world.navigator.settings.NavigatorType;
import de.eintosti.buildsystem.command.BackCommand;
import de.eintosti.buildsystem.command.BlocksCommand;
import de.eintosti.buildsystem.command.BuildCommand;
import de.eintosti.buildsystem.command.BuildSystemCommand;
import de.eintosti.buildsystem.command.ConfigCommand;
import de.eintosti.buildsystem.command.ExplosionsCommand;
import de.eintosti.buildsystem.command.GamemodeCommand;
import de.eintosti.buildsystem.command.NoAICommand;
import de.eintosti.buildsystem.command.PhysicsCommand;
import de.eintosti.buildsystem.command.SettingsCommand;
import de.eintosti.buildsystem.command.SetupCommand;
import de.eintosti.buildsystem.command.SkullCommand;
import de.eintosti.buildsystem.command.SpawnCommand;
import de.eintosti.buildsystem.command.SpeedCommand;
import de.eintosti.buildsystem.command.TimeCommand;
import de.eintosti.buildsystem.command.TopCommand;
import de.eintosti.buildsystem.command.WorldsCommand;
import de.eintosti.buildsystem.command.tabcomplete.BuildTabCompleter;
import de.eintosti.buildsystem.command.tabcomplete.ConfigTabCompleter;
import de.eintosti.buildsystem.command.tabcomplete.EmptyTabCompleter;
import de.eintosti.buildsystem.command.tabcomplete.GamemodeTabCompleter;
import de.eintosti.buildsystem.command.tabcomplete.PhysicsTabCompleter;
import de.eintosti.buildsystem.command.tabcomplete.SpawnTabCompleter;
import de.eintosti.buildsystem.command.tabcomplete.SpeedTabCompleter;
import de.eintosti.buildsystem.command.tabcomplete.TimeTabCompleter;
import de.eintosti.buildsystem.command.tabcomplete.WorldsTabCompleter;
import de.eintosti.buildsystem.config.Config;
import de.eintosti.buildsystem.config.Config.Folder;
import de.eintosti.buildsystem.config.Config.Settings.Archive;
import de.eintosti.buildsystem.config.Config.Settings.Builder;
import de.eintosti.buildsystem.config.Config.World;
import de.eintosti.buildsystem.config.Config.World.Unload;
import de.eintosti.buildsystem.config.migration.ConfigMigrationManager;
import de.eintosti.buildsystem.expansion.luckperms.LuckPermsExpansion;
import de.eintosti.buildsystem.expansion.placeholderapi.PlaceholderApiExpansion;
import de.eintosti.buildsystem.listener.AsyncPlayerChatListener;
import de.eintosti.buildsystem.listener.AsyncPlayerPreLoginListener;
import de.eintosti.buildsystem.listener.BlockPhysicsListener;
import de.eintosti.buildsystem.listener.BuildModePreventationListener;
import de.eintosti.buildsystem.listener.BuildWorldResetUnloadListener;
import de.eintosti.buildsystem.listener.EditSessionListener;
import de.eintosti.buildsystem.listener.EntityDamageListener;
import de.eintosti.buildsystem.listener.EntitySpawnListener;
import de.eintosti.buildsystem.listener.FoodLevelChangeListener;
import de.eintosti.buildsystem.listener.InventoryCreativeListener;
import de.eintosti.buildsystem.listener.InventoryListener;
import de.eintosti.buildsystem.listener.NavigatorListener;
import de.eintosti.buildsystem.listener.PlayerChangedWorldListener;
import de.eintosti.buildsystem.listener.PlayerCommandPreprocessListener;
import de.eintosti.buildsystem.listener.PlayerInventoryClearListener;
import de.eintosti.buildsystem.listener.PlayerJoinListener;
import de.eintosti.buildsystem.listener.PlayerMoveListener;
import de.eintosti.buildsystem.listener.PlayerQuitListener;
import de.eintosti.buildsystem.listener.PlayerRespawnListener;
import de.eintosti.buildsystem.listener.PlayerTeleportListener;
import de.eintosti.buildsystem.listener.SettingsInteractListener;
import de.eintosti.buildsystem.listener.SignChangeListener;
import de.eintosti.buildsystem.listener.WeatherChangeListener;
import de.eintosti.buildsystem.listener.WorldManipulateByAxiomListener;
import de.eintosti.buildsystem.listener.WorldManipulateListener;
import de.eintosti.buildsystem.player.LogoutLocationImpl;
import de.eintosti.buildsystem.player.PlayerServiceImpl;
import de.eintosti.buildsystem.player.customblock.CustomBlockManager;
import de.eintosti.buildsystem.player.settings.NoClipManager;
import de.eintosti.buildsystem.player.settings.SettingsManager;
import de.eintosti.buildsystem.util.UpdateChecker;
import de.eintosti.buildsystem.util.inventory.InventoryManager;
import de.eintosti.buildsystem.world.SpawnManager;
import de.eintosti.buildsystem.world.WorldServiceImpl;
import de.eintosti.buildsystem.world.backup.BackupService;
import de.eintosti.buildsystem.world.display.CustomizableIcons;
import de.eintosti.buildsystem.world.navigator.ArmorStandManager;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import org.bstats.bukkit.Metrics;
import org.bstats.charts.AdvancedPie;
import org.bstats.charts.SimplePie;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.entity.Player;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.ServicePriority;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitTask;
public class BuildSystemPlugin extends JavaPlugin {
public static final int SPIGOT_ID = 60441;
public static final int METRICS_ID = 7427;
public static final String ADMIN_PERMISSION = "buildsystem.admin";
private static BuildSystemPlugin instance;
private ArmorStandManager armorStandManager;
private CustomBlockManager customBlockManager;
private InventoryManager inventoryManager;
private PlayerServiceImpl playerService;
private NoClipManager noClipManager;
private SettingsManager settingsManager;
private SpawnManager spawnManager;
private WorldServiceImpl worldService;
private BackupService backupService;
private CustomizableIcons customizableIcons;
private LuckPermsExpansion luckPermsExpansion;
private PlaceholderApiExpansion placeholderApiExpansion;
private BuildSystemApi api;
private BukkitTask configSaveTask;
@Override
public void onLoad() {
instance = this;
new ConfigMigrationManager(this).migrate();
this.getConfig().options().copyDefaults(true);
this.saveConfig();
Config.load();
Messages.createMessageFile();
createTemplateFolder();
}
@Override
public void onEnable() {
initClasses();
registerCommands();
registerTabCompleters();
registerListeners();
registerExpansions();
performUpdateCheck();
this.api = new BuildSystemApi(this);
this.api.register();
getServer().getServicesManager().register(BuildSystem.class, api, this, ServicePriority.Normal);
Bukkit.getOnlinePlayers().forEach(pl -> {
BuildPlayer buildPlayer = playerService.getPlayerStorage().createBuildPlayer(pl);
Settings settings = buildPlayer.getSettings();
noClipManager.startNoClip(pl, settings);
settingsManager.displayScoreboard(pl);
});
registerStats();
this.configSaveTask = Bukkit.getScheduler().runTaskTimer(this, this::saveBuildConfig, 6000L, 6000L);
Bukkit.getConsoleSender().sendMessage(
"%sBuildSystem » Plugin %senabled%s!".formatted(ChatColor.RESET, ChatColor.GREEN, ChatColor.RESET)
);
}
@Override
public void onDisable() {
Bukkit.getOnlinePlayers().forEach(pl -> {
BuildPlayer buildPlayer = playerService.getPlayerStorage().getBuildPlayer(pl);
buildPlayer.getCachedValues().resetCachedValues(pl);
buildPlayer.setLogoutLocation(new LogoutLocationImpl(pl.getWorld().getName(), pl.getLocation()));
settingsManager.hideScoreboard(pl);
noClipManager.stopNoClip(pl.getUniqueId());
playerService.closeNewNavigator(pl);
});
this.backupService.getStorage().close();
reloadConfigData(false);
saveConfig();
saveBuildConfig();
if (this.configSaveTask != null) {
this.configSaveTask.cancel();
}
unregisterExpansions();
this.api.unregister();
Bukkit.getConsoleSender().sendMessage(
"%sBuildSystem » Plugin %sdisabled%s!".formatted(ChatColor.RESET, ChatColor.RED, ChatColor.RESET)
);
instance = null;
}
public static BuildSystemPlugin get() {
if (instance == null) {
throw new IllegalStateException("BuildSystemPlugin instance is not initialized. Make sure the plugin is enabled.");
}
return instance;
}
private void initClasses() {
this.customizableIcons = new CustomizableIcons(this);
this.inventoryManager = new InventoryManager();
this.armorStandManager = new ArmorStandManager();
this.customBlockManager = new CustomBlockManager(this);
(this.playerService = new PlayerServiceImpl(this)).init();
this.noClipManager = new NoClipManager(this);
(this.worldService = new WorldServiceImpl(this)).init();
this.backupService = new BackupService(this);
this.settingsManager = new SettingsManager(this);
this.spawnManager = new SpawnManager(this);
}
private void registerCommands() {
new BackCommand(this);
new BlocksCommand(this);
new BuildCommand(this);
new BuildSystemCommand(this);
new ConfigCommand(this);
new ExplosionsCommand(this);
new GamemodeCommand(this);
new NoAICommand(this);
new PhysicsCommand(this);
new SettingsCommand(this);
new SetupCommand(this);
new SkullCommand(this);
new SpawnCommand(this);
new SpeedCommand(this);
new TimeCommand(this);
new TopCommand(this);
new WorldsCommand(this);
}
private void registerTabCompleters() {
new BuildTabCompleter(this);
new ConfigTabCompleter(this);
new EmptyTabCompleter(this);
new GamemodeTabCompleter(this);
new PhysicsTabCompleter(this);
new SpawnTabCompleter(this);
new SpeedTabCompleter(this);
new TimeTabCompleter(this);
new WorldsTabCompleter(this);
}
private void registerListeners() {
new AsyncPlayerChatListener(this);
new AsyncPlayerPreLoginListener(this);
new BlockPhysicsListener(this);
new CustomBlockManager(this);
new BuildModePreventationListener(this);
new BuildWorldResetUnloadListener(this);
new EntitySpawnListener(this);
new FoodLevelChangeListener(this);
new InventoryCreativeListener(this);
new InventoryListener(this);
new NavigatorListener(this);
new PlayerChangedWorldListener(this);
new EntityDamageListener(this);
new PlayerCommandPreprocessListener(this);
new PlayerInventoryClearListener(this);
new PlayerJoinListener(this);
new PlayerMoveListener(this);
new PlayerQuitListener(this);
new PlayerRespawnListener(this);
new PlayerTeleportListener(this);
new SettingsInteractListener(this);
new SignChangeListener(this);
new WeatherChangeListener(this);
new WorldManipulateListener(this);
}
private void registerStats() {
Metrics metrics = new Metrics(this, METRICS_ID);
metrics.addCustomChart(new SimplePie("archive_vanish", () -> String.valueOf(Archive.vanish)));
metrics.addCustomChart(new SimplePie("block_world_edit", () -> String.valueOf(Builder.blockWorldEditNonBuilder)));
metrics.addCustomChart(new SimplePie("join_quit_messages", () -> String.valueOf(Config.Messages.joinQuitMessages)));
metrics.addCustomChart(new SimplePie("lock_weather", () -> String.valueOf(World.lockWeather)));
metrics.addCustomChart(new SimplePie("scoreboard", () -> String.valueOf(Config.Settings.scoreboard)));
metrics.addCustomChart(new SimplePie("update_checker", () -> String.valueOf(Config.Settings.updateChecker)));
metrics.addCustomChart(new SimplePie("unload_worlds", () -> String.valueOf(Unload.enabled)));
metrics.addCustomChart(new AdvancedPie("navigator_type", new Callable<>() {
@Override
public Map<String, Integer> call() {
Map<String, Integer> valueMap = new HashMap<>();
valueMap.put("Old", getPlayersWithNavigator(NavigatorType.OLD));
valueMap.put("New", getPlayersWithNavigator(NavigatorType.NEW));
return valueMap;
}
private int getPlayersWithNavigator(NavigatorType navigatorType) {
return (int) playerService.getPlayerStorage().getBuildPlayers().stream()
.filter(buildPlayer -> buildPlayer.getSettings().getNavigatorType() == navigatorType)
.count();
}
}));
metrics.addCustomChart(new SimplePie("folder_override_permissions", () -> String.valueOf(Folder.overridePermissions)));
metrics.addCustomChart(new SimplePie("folder_override_projects", () -> String.valueOf(Folder.overrideProjects)));
}
private void registerExpansions() {
PluginManager pluginManager = Bukkit.getPluginManager();
if (pluginManager.getPlugin("PlaceholderAPI") != null) {
this.placeholderApiExpansion = new PlaceholderApiExpansion(this);
this.placeholderApiExpansion.register();
}
if (pluginManager.getPlugin("LuckPerms") != null) {
this.luckPermsExpansion = new LuckPermsExpansion(this);
this.luckPermsExpansion.registerAll();
}
if (pluginManager.getPlugin("AxiomPaper") != null) {
new WorldManipulateByAxiomListener(this);
}
boolean isWorldEdit = pluginManager.getPlugin("WorldEdit") != null
|| pluginManager.getPlugin("FastAsyncWorldEdit") != null;
if (isWorldEdit && Builder.blockWorldEditNonBuilder) {
new EditSessionListener(this);
}
}
private void unregisterExpansions() {
if (this.placeholderApiExpansion != null) {
this.placeholderApiExpansion.unregister();
}
if (this.luckPermsExpansion != null) {
this.luckPermsExpansion.unregisterAll();
}
}
private void performUpdateCheck() {
if (!Config.Settings.updateChecker) {
return;
}
UpdateChecker.init(this, SPIGOT_ID).requestUpdateCheck().whenComplete((result, e) -> {
if (result.requiresUpdate()) {
Bukkit.getConsoleSender().sendMessage(
ChatColor.YELLOW + "[BuildSystem] Great! a new update is available: "
+ ChatColor.GREEN + "v" + result.getNewestVersion()
);
Bukkit.getConsoleSender().sendMessage(
ChatColor.YELLOW + " ➥ Your current version: " +
ChatColor.RED + this.getDescription().getVersion()
);
return;
}
UpdateChecker.UpdateReason reason = result.getReason();
switch (reason) {
case COULD_NOT_CONNECT, INVALID_JSON, UNAUTHORIZED_QUERY, UNKNOWN_ERROR, UNSUPPORTED_VERSION_SCHEME -> Bukkit.getConsoleSender().sendMessage(
ChatColor.RED + "[BuildSystem] Could not check for a new version of BuildSystem. Reason: " + reason
);
}
}
);
}
private void createTemplateFolder() {
File templateFolder = new File(getDataFolder() + File.separator + "templates");
if (templateFolder.mkdirs()) {
getLogger().info("Created \"templates\" folder");
}
}
private void saveBuildConfig() {
worldService.save();
playerService.save();
spawnManager.save();
}
/**
* Reloads the config and config data.
*
* @param init Whether the plugin should reinitialize classes
*/
public void reloadConfigData(boolean init) {
for (Player pl : Bukkit.getOnlinePlayers()) {
getSettingsManager().hideScoreboard(pl);
}
reloadConfig();
Config.load();
if (init) {
worldService.getWorldStorage().getBuildWorlds().forEach(buildWorld -> buildWorld.getUnloader().manageUnload());
if (Config.Settings.scoreboard) {
getSettingsManager().displayScoreboard();
} else {
getSettingsManager().hideScoreboards();
}
}
}
public ArmorStandManager getArmorStandManager() {
return armorStandManager;
}
public CustomBlockManager getCustomBlockManager() {
return customBlockManager;
}
public InventoryManager getInventoryManager() {
return inventoryManager;
}
public PlayerServiceImpl getPlayerService() {
return playerService;
}
public NoClipManager getNoClipManager() {
return noClipManager;
}
public SettingsManager getSettingsManager() {
return settingsManager;
}
public SpawnManager getSpawnManager() {
return spawnManager;
}
public WorldServiceImpl getWorldService() {
return worldService;
}
public BackupService getBackupService() {
return backupService;
}
public CustomizableIcons getCustomizableIcons() {
return customizableIcons;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,51 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.api;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.api.player.PlayerService;
import de.eintosti.buildsystem.api.world.WorldService;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class BuildSystemApi implements BuildSystem {
private final BuildSystemPlugin plugin;
public BuildSystemApi(BuildSystemPlugin plugin) {
this.plugin = plugin;
}
@Override
public WorldService getWorldService() {
return plugin.getWorldService();
}
@Override
public PlayerService getPlayerService() {
return plugin.getPlayerService();
}
public void register() {
BuildSystemProvider.register(this);
}
public void unregister() {
BuildSystemProvider.unregister();
}
}
@@ -0,0 +1,84 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command;
import com.cryptomorin.xseries.XSound;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.player.BuildPlayer;
import de.eintosti.buildsystem.api.storage.PlayerStorage;
import io.papermc.lib.PaperLib;
import org.bukkit.Location;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class BackCommand implements CommandExecutor {
private final BuildSystemPlugin plugin;
private final PlayerStorage playerStorage;
public BackCommand(BuildSystemPlugin plugin) {
this.plugin = plugin;
this.playerStorage = plugin.getPlayerService().getPlayerStorage();
plugin.getCommand("back").setExecutor(this);
}
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
if (!(sender instanceof Player player)) {
plugin.getLogger().warning(Messages.getString("sender_not_player", sender));
return true;
}
if (!player.hasPermission("buildsystem.back")) {
Messages.sendPermissionError(player);
return true;
}
if (args.length == 0) {
teleportBack(player);
} else {
Messages.sendMessage(player, "back_usage");
}
return true;
}
private void teleportBack(Player player) {
BuildPlayer buildPlayer = playerStorage.getBuildPlayer(player);
Location previousLocation = buildPlayer.getPreviousLocation();
if (previousLocation == null) {
Messages.sendMessage(player, "back_failed");
return;
}
PaperLib.teleportAsync(player, previousLocation)
.whenComplete((completed, throwable) -> {
if (!completed) {
return;
}
XSound.ENTITY_ZOMBIE_INFECT.play(player);
Messages.sendMessage(player, "back_teleported");
buildPlayer.setPreviousLocation(null);
});
}
}
@@ -0,0 +1,56 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command;
import com.cryptomorin.xseries.XSound;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.player.customblock.CustomBlockInventory;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class BlocksCommand implements CommandExecutor {
private final BuildSystemPlugin plugin;
public BlocksCommand(BuildSystemPlugin plugin) {
this.plugin = plugin;
plugin.getCommand("blocks").setExecutor(this);
}
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
if (!(sender instanceof Player player)) {
plugin.getLogger().warning(Messages.getString("sender_not_player", sender));
return true;
}
if (!player.hasPermission("buildsystem.blocks")) {
Messages.sendPermissionError(player);
return true;
}
XSound.BLOCK_CHEST_OPEN.play(player);
new CustomBlockInventory(plugin).openInventory(player);
return true;
}
}
@@ -0,0 +1,140 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command;
import com.cryptomorin.xseries.XSound;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.event.world.PlayerBuildModeToggleEvent;
import de.eintosti.buildsystem.api.player.BuildPlayer;
import de.eintosti.buildsystem.api.player.CachedValues;
import de.eintosti.buildsystem.api.player.PlayerService;
import java.util.Map;
import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class BuildCommand implements CommandExecutor {
private final BuildSystemPlugin plugin;
private final PlayerService playerService;
public BuildCommand(BuildSystemPlugin plugin) {
this.plugin = plugin;
this.playerService = plugin.getPlayerService();
plugin.getCommand("build").setExecutor(this);
}
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
if (!(sender instanceof Player player)) {
plugin.getLogger().warning(Messages.getString("sender_not_player", sender));
return true;
}
if (!player.hasPermission("buildsystem.build")) {
Messages.sendPermissionError(player);
return true;
}
switch (args.length) {
case 0 -> {
toggleBuildMode(player, player);
}
case 1 -> {
if (!player.hasPermission("buildsystem.build.other")) {
Messages.sendPermissionError(player);
return true;
}
Player target = Bukkit.getPlayer(args[0]);
if (target == null) {
Messages.sendMessage(player, "build_player_not_found");
return true;
}
toggleBuildMode(target, player);
}
default -> {
Messages.sendMessage(player, "build_usage");
}
}
return true;
}
/**
* Toggles the build mode for a target player.
*
* @param target The player whose build mode is being changed
* @param sender The player who initiated the action, may be the target player themselves
*/
private void toggleBuildMode(Player target, Player sender) {
UUID targetUuid = target.getUniqueId();
boolean isEnteringBuildMode = !playerService.getBuildModePlayers().contains(targetUuid);
PlayerBuildModeToggleEvent toggleEvent = new PlayerBuildModeToggleEvent(target, isEnteringBuildMode, sender);
Bukkit.getServer().getPluginManager().callEvent(toggleEvent);
if (toggleEvent.isCancelled()) {
return;
}
BuildPlayer buildPlayer = playerService.getPlayerStorage().getBuildPlayer(target);
CachedValues cachedValues = buildPlayer.getCachedValues();
if (isEnteringBuildMode) {
playerService.getBuildModePlayers().add(targetUuid);
cachedValues.saveGameMode(target.getGameMode());
cachedValues.saveInventory(target.getInventory().getContents());
target.setGameMode(GameMode.CREATIVE);
XSound.ENTITY_EXPERIENCE_ORB_PICKUP.play(target);
if (sender.equals(target)) {
Messages.sendMessage(target, "build_activated_self");
} else {
XSound.ENTITY_EXPERIENCE_ORB_PICKUP.play(sender);
Messages.sendMessage(sender, "build_activated_other_sender", Map.entry("%target%", target.getName()));
Messages.sendMessage(target, "build_activated_other_target", Map.entry("%sender%", sender.getName()));
}
} else {
if (!playerService.getBuildModePlayers().remove(targetUuid)) {
return;
}
cachedValues.resetGameModeIfPresent(target);
cachedValues.resetInventoryIfPresent(target);
XSound.ENTITY_EXPERIENCE_ORB_PICKUP.play(target);
if (sender.equals(target)) {
Messages.sendMessage(target, "build_deactivated_self");
} else {
XSound.ENTITY_EXPERIENCE_ORB_PICKUP.play(sender);
Messages.sendMessage(sender, "build_deactivated_other_sender", Map.entry("%target%", target.getName()));
Messages.sendMessage(target, "build_deactivated_other_target", Map.entry("%sender%", sender.getName()));
}
}
}
}
@@ -0,0 +1,94 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command;
import com.google.common.collect.Lists;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import java.util.List;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class BuildSystemCommand extends PagedCommand implements CommandExecutor {
private final BuildSystemPlugin plugin;
public BuildSystemCommand(BuildSystemPlugin plugin) {
super("buildsystem_title_with_page", "buildsystem_permission");
this.plugin = plugin;
plugin.getCommand("buildsystem").setExecutor(this);
}
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
if (!(sender instanceof Player player)) {
plugin.getLogger().warning(Messages.getString("sender_not_player", sender));
return true;
}
if (!player.hasPermission("buildsystem.help.buildsystem")) {
Messages.sendPermissionError(player);
return true;
}
if (args.length == 0) {
sendMessage(player, 1);
} else if (args.length == 1) {
try {
int page = Integer.parseInt(args[0]);
sendMessage(player, page);
} catch (NumberFormatException e) {
Messages.sendMessage(player, "buildsystem_invalid_page");
}
} else {
Messages.sendMessage(player, "buildsystem_usage");
}
return true;
}
@Override
protected List<TextComponent> getCommands(Player player) {
List<TextComponent> commands = Lists.newArrayList(
createComponent(player, "/back", "buildsystem_back", "/back", "buildsystem.back"),
createComponent(player, "/blocks", "buildsystem_blocks", "/blocks", "buildsystem.blocks"),
createComponent(player, "/build [player]", "buildsystem_build", "/build", "buildsystem.build"),
createComponent(player, "/config reload", "buildsystem_config", "/config reload", "buildsystem.config"),
createComponent(player, "/day [world]", "buildsystem_day", "/day", "buildsystem.day"),
createComponent(player, "/explosions [world]", "buildsystem_explosions", "/explosions", "buildsystem.explosions"),
createComponent(player, "/gm <gamemode> [player]", "buildsystem_gamemode", "/gm ", "buildsystem.gamemode"),
createComponent(player, "/night [world]", "buildsystem_night", "/night", "buildsystem.night"),
createComponent(player, "/noai [world]", "buildsystem_noai", "/noai", "buildsystem.noai"),
createComponent(player, "/physics [world]", "buildsystem_physics", "/physics", "buildsystem.physics"),
createComponent(player, "/settings", "buildsystem_settings", "/settings", "buildsystem.settings"),
createComponent(player, "/setup", "buildsystem_setup", "/setup", "buildsystem.setup"),
createComponent(player, "/skull [player/id]", "buildsystem_skull", "/skull", "buildsystem.skull"),
createComponent(player, "/spawn", "buildsystem_spawn", "/spawn", "-"),
createComponent(player, "/speed <1-5>", "buildsystem_speed", "/speed ", "buildsystem.speed"),
createComponent(player, "/top", "buildsystem_top", "/top", "buildsystem.top"),
createComponent(player, "/worlds help", "buildsystem_worlds", "/worlds help", "-")
);
commands.removeIf(textComponent -> textComponent.getText().isEmpty());
return commands;
}
}
@@ -0,0 +1,64 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import java.util.Locale;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class ConfigCommand implements CommandExecutor {
private final BuildSystemPlugin plugin;
public ConfigCommand(BuildSystemPlugin plugin) {
this.plugin = plugin;
plugin.getCommand("config").setExecutor(this);
}
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
if (!sender.hasPermission("buildsystem.config")) {
Messages.sendPermissionError(sender);
return true;
}
if (args.length != 1) {
Messages.sendMessage(sender, "config_usage");
return true;
}
switch (args[0].toLowerCase(Locale.ROOT)) {
case "rl":
case "reload":
Messages.reloadMessages();
plugin.reloadConfigData(true);
Messages.sendMessage(sender, "config_reloaded");
break;
default:
Messages.sendMessage(sender, "config_usage");
break;
}
return true;
}
}
@@ -0,0 +1,92 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.api.world.data.WorldData;
import de.eintosti.buildsystem.storage.WorldStorageImpl;
import de.eintosti.buildsystem.world.util.WorldPermissionsImpl;
import java.util.Map;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public class ExplosionsCommand implements CommandExecutor {
private final BuildSystemPlugin plugin;
private final WorldStorageImpl worldStorage;
public ExplosionsCommand(BuildSystemPlugin plugin) {
this.plugin = plugin;
this.worldStorage = plugin.getWorldService().getWorldStorage();
plugin.getCommand("explosions").setExecutor(this);
}
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
if (!(sender instanceof Player player)) {
plugin.getLogger().warning(Messages.getString("sender_not_player", sender));
return true;
}
String worldName = args.length == 0 ? player.getWorld().getName() : args[0];
BuildWorld buildWorld = worldStorage.getBuildWorld(worldName);
if (!WorldPermissionsImpl.of(buildWorld).canPerformCommand(player, "buildsystem.explosions")) {
Messages.sendPermissionError(player);
return true;
}
switch (args.length) {
case 0 -> toggleExplosions(player, player.getWorld());
case 1 -> toggleExplosions(player, Bukkit.getWorld(args[0]));
default -> Messages.sendMessage(player, "explosions_usage");
}
return true;
}
private void toggleExplosions(Player player, @Nullable World world) {
if (world == null) {
Messages.sendMessage(player, "explosions_unknown_world");
return;
}
BuildWorld buildWorld = worldStorage.getBuildWorld(world);
if (buildWorld == null) {
Messages.sendMessage(player, "explosions_world_not_imported");
return;
}
WorldData worldData = buildWorld.getData();
if (!worldData.explosions().get()) {
worldData.explosions().set(true);
Messages.sendMessage(player, "explosions_activated", Map.entry("%world%", buildWorld.getName()));
} else {
worldData.explosions().set(false);
Messages.sendMessage(player, "explosions_deactivated", Map.entry("%world%", buildWorld.getName()));
}
}
}
@@ -0,0 +1,124 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import java.util.Locale;
import java.util.Map;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class GamemodeCommand implements CommandExecutor {
private final BuildSystemPlugin plugin;
public GamemodeCommand(BuildSystemPlugin plugin) {
this.plugin = plugin;
plugin.getCommand("gamemode").setExecutor(this);
}
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
if (!(sender instanceof Player player)) {
plugin.getLogger().warning(Messages.getString("sender_not_player", sender));
return true;
}
if (args.length == 0) {
sendUsageMessage(player);
return true;
}
switch (args[0].toLowerCase(Locale.ROOT)) {
case "survival":
case "s":
case "0":
setGamemode(player, args, GameMode.SURVIVAL, Messages.getString("gamemode_survival", player));
break;
case "creative":
case "c":
case "1":
setGamemode(player, args, GameMode.CREATIVE, Messages.getString("gamemode_creative", player));
break;
case "adventure":
case "a":
case "2":
setGamemode(player, args, GameMode.ADVENTURE, Messages.getString("gamemode_adventure", player));
break;
case "spectator":
case "sp":
case "3":
setGamemode(player, args, GameMode.SPECTATOR, Messages.getString("gamemode_spectator", player));
break;
default:
sendUsageMessage(player);
break;
}
return true;
}
private void setGamemode(Player player, String[] args, GameMode gameMode, String gameModeName) {
switch (args.length) {
case 1 -> this.setPlayerGamemode(player, gameMode, gameModeName);
case 2 -> this.setTargetGamemode(player, args, gameMode, gameModeName);
default -> this.sendUsageMessage(player);
}
}
private void sendUsageMessage(Player player) {
Messages.sendMessage(player, "gamemode_usage");
}
private void setPlayerGamemode(Player player, GameMode gameMode, String gameModeName) {
if (!player.hasPermission("buildsystem.gamemode.%s".formatted(gameMode.name().toLowerCase(Locale.ROOT)))) {
Messages.sendPermissionError(player);
return;
}
player.setGameMode(gameMode);
Messages.sendMessage(player, "gamemode_set_self", Map.entry("%gamemode%", gameModeName));
}
private void setTargetGamemode(Player player, String[] args, GameMode gameMode, String gameModeName) {
if (!player.hasPermission("buildsystem.gamemode.%s.other".formatted(gameMode.name().toLowerCase(Locale.ROOT)))) {
Messages.sendPermissionError(player);
return;
}
Player target = Bukkit.getPlayerExact(args[1]);
if (target == null) {
Messages.sendMessage(player, "gamemode_player_not_found");
return;
}
target.setGameMode(gameMode);
Messages.sendMessage(target, "gamemode_set_self", Map.entry("%gamemode%", gameModeName));
Messages.sendMessage(player, "gamemode_set_other",
Map.entry("%target%", target.getName()),
Map.entry("%gamemode%", gameModeName)
);
}
}
@@ -0,0 +1,95 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.api.world.data.WorldData;
import de.eintosti.buildsystem.storage.WorldStorageImpl;
import de.eintosti.buildsystem.world.util.WorldPermissionsImpl;
import java.util.Map;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public class NoAICommand implements CommandExecutor {
private final BuildSystemPlugin plugin;
private final WorldStorageImpl worldStorage;
public NoAICommand(BuildSystemPlugin plugin) {
this.plugin = plugin;
this.worldStorage = plugin.getWorldService().getWorldStorage();
plugin.getCommand("noai").setExecutor(this);
}
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
if (!(sender instanceof Player player)) {
plugin.getLogger().warning(Messages.getString("sender_not_player", sender));
return true;
}
String worldName = args.length == 0 ? player.getWorld().getName() : args[0];
BuildWorld buildWorld = worldStorage.getBuildWorld(worldName);
if (!WorldPermissionsImpl.of(buildWorld).canPerformCommand(player, "buildsystem.noai")) {
Messages.sendPermissionError(player);
return true;
}
switch (args.length) {
case 0 -> toggleAI(player, player.getWorld());
case 1 -> toggleAI(player, Bukkit.getWorld(args[0]));
default -> Messages.sendMessage(player, "noai_usage");
}
return true;
}
private void toggleAI(Player player, @Nullable World world) {
if (world == null) {
Messages.sendMessage(player, "noai_unknown_world");
return;
}
BuildWorld buildWorld = worldStorage.getBuildWorld(world);
if (buildWorld == null) {
Messages.sendMessage(player, "noai_world_not_imported");
return;
}
WorldData worldData = buildWorld.getData();
if (worldData.mobAi().get()) {
worldData.mobAi().set(false);
Messages.sendMessage(player, "noai_activated", Map.entry("%world%", buildWorld.getName()));
} else {
worldData.mobAi().set(true);
Messages.sendMessage(player, "noai_deactivated", Map.entry("%world%", buildWorld.getName()));
}
boolean hasAi = worldData.mobAi().get();
world.getLivingEntities().forEach(entity -> entity.setAI(hasAi));
}
}
@@ -0,0 +1,97 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command;
import de.eintosti.buildsystem.Messages;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.IntStream;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.chat.hover.content.Text;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.Contract;
import org.jspecify.annotations.NullMarked;
@NullMarked
public abstract class PagedCommand {
private static final int MAX_COMMANDS_PER_PAGE = 7;
private final String title, permissionTemplate;
public PagedCommand(String title, String permissionTemplate) {
this.title = title;
this.permissionTemplate = permissionTemplate;
}
protected void sendMessage(Player player, int pageNum) {
List<TextComponent> commands = getCommands(player);
int numPages = (int) Math.ceil((double) commands.size() / MAX_COMMANDS_PER_PAGE);
List<TextComponent> page = createPage(commands, numPages, pageNum);
page.add(0, new TextComponent("§7§m----------------------------------------------------"));
page.add(1, new TextComponent(Messages.getString(this.title, player)
.replace("%page%", String.valueOf(pageNum))
.replace("%max%", String.valueOf(numPages))
.concat("\n"))
);
page.add(new TextComponent("§7§m----------------------------------------------------"));
page.forEach(line -> player.spigot().sendMessage(line));
}
private List<TextComponent> createPage(List<TextComponent> commands, int numPages, int page) {
List<List<TextComponent>> pages = new ArrayList<>(numPages);
IntStream.range(0, numPages).forEach(i -> pages.add(new ArrayList<>()));
int currentPage = 0;
int commandsInPage = 0;
for (TextComponent command : commands) {
pages.get(currentPage).add(command);
commandsInPage++;
if (commandsInPage >= MAX_COMMANDS_PER_PAGE) {
currentPage++;
commandsInPage = 0;
}
}
return pages.get(page - 1);
}
protected abstract List<TextComponent> getCommands(Player player);
@Contract("_, _, _, _, _-> new")
protected TextComponent createComponent(Player player, String command, String commandDescriptionKey, String suggest, String permission) {
if (command.isEmpty()) {
return new TextComponent();
}
TextComponent commandComponent = new TextComponent("§b" + command);
TextComponent textComponent = new TextComponent(" §8» " + Messages.getString(commandDescriptionKey, player));
commandComponent.setClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, suggest));
commandComponent.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
new Text(Messages.getString(this.permissionTemplate, player, Map.entry("%permission%", permission)))
));
commandComponent.addExtra(textComponent);
return commandComponent;
}
}
@@ -0,0 +1,103 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.api.world.data.WorldData;
import de.eintosti.buildsystem.storage.WorldStorageImpl;
import de.eintosti.buildsystem.world.util.WorldPermissionsImpl;
import java.util.Map;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public class PhysicsCommand implements CommandExecutor {
private final BuildSystemPlugin plugin;
private final WorldStorageImpl worldStorage;
public PhysicsCommand(BuildSystemPlugin plugin) {
this.plugin = plugin;
this.worldStorage = plugin.getWorldService().getWorldStorage();
plugin.getCommand("physics").setExecutor(this);
}
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
if (!(sender instanceof Player player)) {
plugin.getLogger().warning(Messages.getString("sender_not_player", sender));
return true;
}
String worldName = args.length == 0 ? player.getWorld().getName() : args[0];
BuildWorld buildWorld = worldStorage.getBuildWorld(worldName);
if (!WorldPermissionsImpl.of(buildWorld).canPerformCommand(player, "buildsystem.physics")) {
Messages.sendPermissionError(player);
return true;
}
switch (args.length) {
case 0 -> {
togglePhysics(player, player.getWorld());
}
case 1 -> {
// TODO: Check each world for permission individually?
if (args[0].equalsIgnoreCase("all") && !worldStorage.worldExists("all")) {
worldStorage.getBuildWorlds().forEach(world -> world.getData().physics().set(true));
Messages.sendMessage(player, "physics_activated_all");
} else {
togglePhysics(player, Bukkit.getWorld(args[0]));
}
}
default -> {
Messages.sendMessage(player, "physics_usage");
}
}
return true;
}
private void togglePhysics(Player player, @Nullable World world) {
if (world == null) {
Messages.sendMessage(player, "physics_unknown_world");
return;
}
BuildWorld buildWorld = worldStorage.getBuildWorld(world.getName());
if (buildWorld == null) {
Messages.sendMessage(player, "physics_world_not_imported");
return;
}
WorldData worldData = buildWorld.getData();
if (!worldData.physics().get()) {
worldData.physics().set(true);
Messages.sendMessage(player, "physics_activated", Map.entry("%world%", buildWorld.getName()));
} else {
worldData.physics().set(false);
Messages.sendMessage(player, "physics_deactivated", Map.entry("%world%", buildWorld.getName()));
}
}
}
@@ -0,0 +1,54 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.player.settings.SettingsInventory;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class SettingsCommand implements CommandExecutor {
private final BuildSystemPlugin plugin;
public SettingsCommand(BuildSystemPlugin plugin) {
this.plugin = plugin;
plugin.getCommand("settings").setExecutor(this);
}
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
if (!(sender instanceof Player player)) {
plugin.getLogger().warning(Messages.getString("sender_not_player", sender));
return true;
}
if (!player.hasPermission("buildsystem.settings")) {
Messages.sendPermissionError(player);
return true;
}
new SettingsInventory(plugin).openInventory(player);
return true;
}
}
@@ -0,0 +1,54 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.world.modification.SetupInventory;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class SetupCommand implements CommandExecutor {
private final BuildSystemPlugin plugin;
public SetupCommand(BuildSystemPlugin plugin) {
this.plugin = plugin;
plugin.getCommand("setup").setExecutor(this);
}
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
if (!(sender instanceof Player player)) {
plugin.getLogger().warning(Messages.getString("sender_not_player", sender));
return true;
}
if (!player.hasPermission("buildsystem.setup")) {
Messages.sendPermissionError(player);
return true;
}
new SetupInventory(plugin).openInventory(player);
return true;
}
}
@@ -0,0 +1,78 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command;
import com.cryptomorin.xseries.profiles.objects.Profileable;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.util.inventory.InventoryUtils;
import java.util.Map;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class SkullCommand implements CommandExecutor {
private final BuildSystemPlugin plugin;
public SkullCommand(BuildSystemPlugin plugin) {
this.plugin = plugin;
plugin.getCommand("skull").setExecutor(this);
}
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
if (!(sender instanceof Player player)) {
plugin.getLogger().warning(Messages.getString("sender_not_player", sender));
return true;
}
if (!player.hasPermission("buildsystem.skull")) {
Messages.sendPermissionError(player);
return true;
}
switch (args.length) {
case 0 -> {
addSkull(player, "§b" + player.getName(), Profileable.of(player));
Messages.sendMessage(player, "skull_player_received", Map.entry("%player%", player.getName()));
}
case 1 -> {
String identifier = args[0];
if (identifier.length() > 16) {
addSkull(player, Messages.getString("custom_skull_item", player), Profileable.detect(identifier));
Messages.sendMessage(player, "skull_custom_received");
} else {
addSkull(player, "§b" + identifier, Profileable.detect(identifier));
Messages.sendMessage(player, "skull_player_received", Map.entry("%player%", identifier));
}
}
default -> {
Messages.sendMessage(player, "skull_usage");
}
}
return true;
}
private void addSkull(Player player, String displayName, Profileable profileable) {
player.getInventory().addItem(InventoryUtils.createSkull(displayName, profileable));
}
}
@@ -0,0 +1,117 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.config.Config;
import de.eintosti.buildsystem.storage.WorldStorageImpl;
import de.eintosti.buildsystem.world.SpawnManager;
import java.util.Locale;
import java.util.Map;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class SpawnCommand implements CommandExecutor {
private final BuildSystemPlugin plugin;
private final SpawnManager spawnManager;
private final WorldStorageImpl worldStorage;
public SpawnCommand(BuildSystemPlugin plugin) {
this.plugin = plugin;
this.spawnManager = plugin.getSpawnManager();
this.worldStorage = plugin.getWorldService().getWorldStorage();
plugin.getCommand("spawn").setExecutor(this);
}
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
if (!(sender instanceof Player player)) {
plugin.getLogger().warning(Messages.getString("sender_not_player", sender));
return true;
}
switch (args.length) {
case 0 -> {
if (!spawnManager.teleport(player)) {
Messages.sendMessage(player, "spawn_unavailable");
} else if (Config.Messages.spawnTeleportMessage) {
Messages.sendMessage(player, "spawn_teleported");
}
}
case 1 -> {
if (!player.hasPermission("buildsystem.spawn")) {
Messages.sendMessage(player, "spawn_usage");
return true;
}
switch (args[0].toLowerCase(Locale.ROOT)) {
case "set" -> {
Location playerLocation = player.getLocation();
World bukkitWorld = playerLocation.getWorld();
if (bukkitWorld == null) {
Messages.sendMessage(player, "spawn_world_not_imported");
return true;
}
BuildWorld buildWorld = worldStorage.getBuildWorld(bukkitWorld);
if (buildWorld == null) {
Messages.sendMessage(player, "spawn_world_not_imported");
return true;
}
spawnManager.set(playerLocation, buildWorld.getName());
Messages.sendMessage(player, "spawn_set",
Map.entry("%x%", round(playerLocation.getX())),
Map.entry("%y%", round(playerLocation.getY())),
Map.entry("%z%", round(playerLocation.getZ())),
Map.entry("%world%", playerLocation.getWorld().getName())
);
}
case "remove" -> {
spawnManager.remove();
Messages.sendMessage(player, "spawn_remove");
}
default -> {
Messages.sendMessage(player, "spawn_admin");
}
}
}
default -> {
String key = player.hasPermission("buildsystem.spawn") ? "spawn_admin" : "spawn_usage";
Messages.sendMessage(player, key);
}
}
return true;
}
private String round(double value) {
int scale = (int) Math.pow(10, 2);
return String.valueOf((double) Math.round(value * scale) / scale);
}
}
@@ -0,0 +1,93 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.player.settings.SpeedInventory;
import java.util.Map;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class SpeedCommand implements CommandExecutor {
private static final float INVALID_SPEED = -1.0f;
private final BuildSystemPlugin plugin;
public SpeedCommand(BuildSystemPlugin plugin) {
this.plugin = plugin;
plugin.getCommand("speed").setExecutor(this);
}
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
if (!(sender instanceof Player player)) {
plugin.getLogger().warning(Messages.getString("sender_not_player", sender));
return true;
}
if (!player.hasPermission("buildsystem.speed")) {
Messages.sendPermissionError(player);
return true;
}
switch (args.length) {
case 0:
new SpeedInventory(plugin).openInventory(player);
break;
case 1:
String speedString = args[0];
float speed = switch (speedString) {
case "1" -> 0.2f;
case "2" -> 0.4f;
case "3" -> 0.6f;
case "4" -> 0.8f;
case "5" -> 1.0f;
default -> INVALID_SPEED;
};
if (speed == INVALID_SPEED) {
Messages.sendMessage(player, "speed_usage");
return true;
}
setSpeed(player, speed, speedString);
break;
default:
Messages.sendMessage(player, "speed_usage");
break;
}
return true;
}
private void setSpeed(Player player, float speed, String speedString) {
if (player.isFlying()) {
player.setFlySpeed(speed - 0.1f);
Messages.sendMessage(player, "speed_set_flying", Map.entry("%speed%", speedString));
} else {
player.setWalkSpeed(speed);
Messages.sendMessage(player, "speed_set_walking", Map.entry("%speed%", speedString));
}
}
}
@@ -0,0 +1,99 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.api.world.util.WorldPermissions;
import de.eintosti.buildsystem.config.Config.World.Default.Time;
import de.eintosti.buildsystem.world.util.WorldPermissionsImpl;
import java.util.Locale;
import java.util.Map;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class TimeCommand implements CommandExecutor {
private final BuildSystemPlugin plugin;
public TimeCommand(BuildSystemPlugin plugin) {
this.plugin = plugin;
plugin.getCommand("day").setExecutor(this);
plugin.getCommand("night").setExecutor(this);
}
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
if (!(sender instanceof Player player)) {
plugin.getLogger().warning(Messages.getString("sender_not_player", sender));
return true;
}
String worldName = args.length == 0 ? player.getWorld().getName() : args[0];
World world = Bukkit.getWorld(worldName);
if (world == null) {
Messages.sendMessage(player, "day_unknown_world");
return true;
}
BuildWorld buildWorld = plugin.getWorldService().getWorldStorage().getBuildWorld(world);
WorldPermissions permissions = WorldPermissionsImpl.of(buildWorld);
switch (label.toLowerCase(Locale.ROOT)) {
case "day" -> {
if (!permissions.canPerformCommand(player, "buildsystem.day")) {
Messages.sendPermissionError(player);
return true;
}
switch (args.length) {
case 0, 1 -> {
world.setTime(Time.noon);
Messages.sendMessage(player, "day_set", Map.entry("%world%", world.getName()));
}
default -> Messages.sendMessage(player, "day_usage");
}
}
case "night" -> {
if (!permissions.canPerformCommand(player, "buildsystem.night")) {
Messages.sendPermissionError(player);
return true;
}
switch (args.length) {
case 0, 1 -> {
world.setTime(Time.night);
Messages.sendMessage(player, "night_set", Map.entry("%world%", world.getName()));
}
default -> {
Messages.sendMessage(player, "night_usage");
}
}
}
}
return true;
}
}
@@ -0,0 +1,84 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command;
import com.cryptomorin.xseries.XSound;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.world.util.WorldTeleporterImpl;
import io.papermc.lib.PaperLib;
import org.bukkit.Location;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class TopCommand implements CommandExecutor {
private final BuildSystemPlugin plugin;
public TopCommand(BuildSystemPlugin plugin) {
this.plugin = plugin;
plugin.getCommand("top").setExecutor(this);
}
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
if (!(sender instanceof Player player)) {
plugin.getLogger().warning(Messages.getString("sender_not_player", sender));
return true;
}
if (!player.hasPermission("buildsystem.top")) {
Messages.sendPermissionError(player);
return true;
}
if (args.length != 0) {
Messages.sendMessage(player, "top_usage");
return true;
}
sendToTop(player);
return true;
}
private void sendToTop(Player player) {
Location playerLocation = player.getLocation();
Location blockLocation = player.getWorld()
.getHighestBlockAt(playerLocation.getBlockX(), playerLocation.getBlockZ())
.getLocation();
boolean failed = !WorldTeleporterImpl.isSafeLocation(blockLocation) || blockLocation.getBlock().getY() < playerLocation.getBlock().getY();
if (failed) {
Messages.sendMessage(player, "top_failed");
return;
}
PaperLib.teleportAsync(player, blockLocation.add(0.5, 1, 0.5))
.whenComplete((completed, throwable) -> {
if (!completed) {
return;
}
XSound.ENTITY_ZOMBIE_INFECT.play(player);
Messages.sendMessage(player, "top_teleported");
});
}
}
@@ -0,0 +1,125 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command;
import com.cryptomorin.xseries.XSound;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.command.subcommand.SubCommand;
import de.eintosti.buildsystem.command.subcommand.worlds.AddBuilderSubCommand;
import de.eintosti.buildsystem.command.subcommand.worlds.ArchiveSubCommand;
import de.eintosti.buildsystem.command.subcommand.worlds.BackupsSubCommand;
import de.eintosti.buildsystem.command.subcommand.worlds.BuildersSubCommand;
import de.eintosti.buildsystem.command.subcommand.worlds.DeleteSubCommand;
import de.eintosti.buildsystem.command.subcommand.worlds.EditSubCommand;
import de.eintosti.buildsystem.command.subcommand.worlds.FolderSubCommand;
import de.eintosti.buildsystem.command.subcommand.worlds.HelpSubCommand;
import de.eintosti.buildsystem.command.subcommand.worlds.ImportAllSubCommand;
import de.eintosti.buildsystem.command.subcommand.worlds.ImportSubCommand;
import de.eintosti.buildsystem.command.subcommand.worlds.InfoSubCommand;
import de.eintosti.buildsystem.command.subcommand.worlds.ItemSubCommand;
import de.eintosti.buildsystem.command.subcommand.worlds.PrivateSubCommand;
import de.eintosti.buildsystem.command.subcommand.worlds.PublicSubCommand;
import de.eintosti.buildsystem.command.subcommand.worlds.RemoveBuilderSubCommand;
import de.eintosti.buildsystem.command.subcommand.worlds.RemoveSpawnSubCommand;
import de.eintosti.buildsystem.command.subcommand.worlds.RenameSubCommand;
import de.eintosti.buildsystem.command.subcommand.worlds.SetCreatorSubCommand;
import de.eintosti.buildsystem.command.subcommand.worlds.SetItemSubCommand;
import de.eintosti.buildsystem.command.subcommand.worlds.SetPermissionSubCommand;
import de.eintosti.buildsystem.command.subcommand.worlds.SetProjectSubCommand;
import de.eintosti.buildsystem.command.subcommand.worlds.SetSpawnSubCommand;
import de.eintosti.buildsystem.command.subcommand.worlds.SetStatusSubCommand;
import de.eintosti.buildsystem.command.subcommand.worlds.TeleportSubCommand;
import de.eintosti.buildsystem.command.subcommand.worlds.UnimportSubCommand;
import de.eintosti.buildsystem.command.tabcomplete.WorldsTabCompleter;
import de.eintosti.buildsystem.world.navigator.inventory.NavigatorInventory;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class WorldsCommand implements CommandExecutor {
private final BuildSystemPlugin plugin;
public WorldsCommand(BuildSystemPlugin plugin) {
this.plugin = plugin;
plugin.getCommand("worlds").setExecutor(this);
}
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
if (!(sender instanceof Player player)) {
plugin.getLogger().warning(Messages.getString("sender_not_player", sender));
return true;
}
if (args.length == 0) {
if (!player.hasPermission("buildsystem.navigator")) {
Messages.sendPermissionError(player);
return true;
}
new NavigatorInventory(plugin).openInventory(player);
XSound.BLOCK_CHEST_OPEN.play(player);
return true;
}
WorldsTabCompleter.WorldsArgument argument = WorldsTabCompleter.WorldsArgument.matchArgument(args[0]);
if (argument == null) {
Messages.sendMessage(player, "worlds_unknown_command");
return true;
}
// Most commands use the structure /worlds <argument> <world> <...> which is why we assume that args[1] is the world name
// Make sure to change if this is not the case for any specific command
String worldName = args.length >= 2 ? args[1] : player.getWorld().getName();
SubCommand subCommand = switch (argument) {
case ARCHIVE -> new ArchiveSubCommand(plugin);
case ADD_BUILDER -> new AddBuilderSubCommand(plugin, player.getWorld().getName());
case BACKUP -> new BackupsSubCommand(plugin);
case BUILDERS -> new BuildersSubCommand(plugin, worldName);
case DELETE -> new DeleteSubCommand(plugin, worldName);
case EDIT -> new EditSubCommand(plugin, worldName);
case FOLDER -> new FolderSubCommand(plugin);
case HELP -> new HelpSubCommand();
case IMPORT_ALL -> new ImportAllSubCommand(plugin);
case IMPORT -> new ImportSubCommand(plugin, worldName);
case INFO -> new InfoSubCommand(plugin, worldName);
case ITEM -> new ItemSubCommand();
case PRIVATE -> new PrivateSubCommand(plugin);
case PUBLIC -> new PublicSubCommand(plugin);
case REMOVE_BUILDER -> new RemoveBuilderSubCommand(plugin, player.getWorld().getName());
case REMOVE_SPAWN -> new RemoveSpawnSubCommand(plugin);
case RENAME -> new RenameSubCommand(plugin, worldName);
case SET_CREATOR -> new SetCreatorSubCommand(plugin, worldName);
case SET_ITEM -> new SetItemSubCommand(plugin, worldName);
case SET_PERMISSION -> new SetPermissionSubCommand(plugin, worldName);
case SET_PROJECT -> new SetProjectSubCommand(plugin, worldName);
case SET_SPAWN -> new SetSpawnSubCommand(plugin);
case SET_STATUS -> new SetStatusSubCommand(plugin, worldName);
case TP -> new TeleportSubCommand(plugin);
case UNIMPORT -> new UnimportSubCommand(plugin, worldName);
};
subCommand.execute(player, args);
return true;
}
}
@@ -0,0 +1,40 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.subcommand;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public interface Argument {
/**
* The name of the command argument.
*
* @return The command argument name
*/
String getName();
/**
* The permission required to run the command.
*
* @return The permission required to run the command
*/
@Nullable
String getPermission();
}
@@ -0,0 +1,34 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.subcommand;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public interface SubCommand {
void execute(Player player, String[] args);
Argument getArgument();
default boolean hasPermission(Player player) {
String permission = getArgument().getPermission();
return permission == null || player.hasPermission(permission);
}
}
@@ -0,0 +1,130 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.subcommand.worlds;
import com.cryptomorin.xseries.XSound;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.api.world.builder.Builder;
import de.eintosti.buildsystem.api.world.builder.Builders;
import de.eintosti.buildsystem.api.world.util.WorldPermissions;
import de.eintosti.buildsystem.command.subcommand.Argument;
import de.eintosti.buildsystem.command.subcommand.SubCommand;
import de.eintosti.buildsystem.command.tabcomplete.WorldsTabCompleter.WorldsArgument;
import de.eintosti.buildsystem.util.PlayerChatInput;
import de.eintosti.buildsystem.util.UUIDFetcher;
import de.eintosti.buildsystem.world.builder.BuilderInventory;
import de.eintosti.buildsystem.world.util.WorldPermissionsImpl;
import java.util.Map;
import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public class AddBuilderSubCommand implements SubCommand {
private final BuildSystemPlugin plugin;
@Nullable
private final BuildWorld buildWorld;
private final WorldPermissions permissions;
public AddBuilderSubCommand(BuildSystemPlugin plugin, String worldName) {
this.plugin = plugin;
this.buildWorld = plugin.getWorldService().getWorldStorage().getBuildWorld(worldName);
this.permissions = WorldPermissionsImpl.of(buildWorld);
}
@Override
public void execute(Player player, String[] args) {
if (!permissions.canPerformCommand(player, getArgument().getPermission())) {
Messages.sendPermissionError(player);
return;
}
if (buildWorld == null) {
Messages.sendMessage(player, "worlds_addbuilder_unknown_world");
return;
}
switch (args.length) {
case 1 -> getAddBuilderInput(player, buildWorld, true);
case 2 -> addBuilder(player, buildWorld, args[1], true);
default -> Messages.sendMessage(player, "worlds_addbuilder_usage");
}
}
private void addBuilder(Player player, BuildWorld buildWorld, String builderName, boolean closeInventory) {
Player builderPlayer = Bukkit.getPlayerExact(builderName);
Builder builder;
UUID builderId;
if (builderPlayer == null) {
builderId = UUIDFetcher.getUUID(builderName);
if (builderId == null) {
Messages.sendMessage(player, "worlds_addbuilder_player_not_found");
player.closeInventory();
return;
}
builder = Builder.of(builderId, builderName);
} else {
builder = Builder.of(builderPlayer);
builderId = builderPlayer.getUniqueId();
}
Builders builders = buildWorld.getBuilders();
if (builderId.equals(player.getUniqueId()) && builders.isCreator(player)) {
Messages.sendMessage(player, "worlds_addbuilder_already_creator");
player.closeInventory();
return;
}
if (builders.isBuilder(builderId)) {
Messages.sendMessage(player, "worlds_addbuilder_already_added");
player.closeInventory();
return;
}
builders.addBuilder(builder);
XSound.ENTITY_PLAYER_LEVELUP.play(player);
Messages.sendMessage(player, "worlds_addbuilder_added",
Map.entry("%builder%", builderName)
);
if (closeInventory) {
player.closeInventory();
} else {
new BuilderInventory(plugin).openInventory(buildWorld, player);
}
}
public void getAddBuilderInput(Player player, BuildWorld buildWorld, boolean closeInventory) {
new PlayerChatInput(plugin, player, "enter_player_name", input -> {
String builderName = input.trim();
addBuilder(player, buildWorld, builderName, closeInventory);
});
}
@Override
public Argument getArgument() {
return WorldsArgument.ADD_BUILDER;
}
}
@@ -0,0 +1,52 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.subcommand.worlds;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.command.subcommand.Argument;
import de.eintosti.buildsystem.command.subcommand.SubCommand;
import de.eintosti.buildsystem.command.tabcomplete.WorldsTabCompleter.WorldsArgument;
import de.eintosti.buildsystem.world.navigator.inventory.ArchivedWorldsInventory;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class ArchiveSubCommand implements SubCommand {
private final BuildSystemPlugin plugin;
public ArchiveSubCommand(BuildSystemPlugin plugin) {
this.plugin = plugin;
}
@Override
public void execute(Player player, String[] args) {
if (!hasPermission(player)) {
Messages.sendPermissionError(player);
return;
}
new ArchivedWorldsInventory(plugin, player).openInventory();
}
@Override
public Argument getArgument() {
return WorldsArgument.ARCHIVE;
}
}
@@ -0,0 +1,87 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.subcommand.worlds;
import com.cryptomorin.xseries.XSound;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.command.subcommand.Argument;
import de.eintosti.buildsystem.command.subcommand.SubCommand;
import de.eintosti.buildsystem.command.tabcomplete.WorldsTabCompleter.WorldsArgument;
import de.eintosti.buildsystem.world.backup.BackupsInventory;
import de.eintosti.buildsystem.world.util.WorldPermissionsImpl;
import java.util.Map;
import java.util.Map.Entry;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class BackupsSubCommand implements SubCommand {
private final BuildSystemPlugin plugin;
public BackupsSubCommand(BuildSystemPlugin plugin) {
this.plugin = plugin;
}
@Override
public void execute(Player player, String[] args) {
BuildWorld buildWorld = plugin.getWorldService().getWorldStorage().getBuildWorld(player.getWorld().getName());
if (!WorldPermissionsImpl.of(buildWorld).canPerformCommand(player, getArgument().getPermission())) {
Messages.sendPermissionError(player);
return;
}
if (buildWorld == null) {
Messages.sendMessage(player, "worlds_backup_world_not_imported");
return;
}
switch (args.length) {
case 1 -> {
XSound.BLOCK_CHEST_OPEN.play(player);
new BackupsInventory(plugin).openBackupsInventory(player, buildWorld);
}
case 2 -> {
if (args[1].equalsIgnoreCase("create")) {
if (!player.hasPermission(getArgument().getPermission() + ".create")) {
Messages.sendPermissionError(player);
return;
}
Entry<String, Object> worldNamePlaceholder = Map.entry("%world%", buildWorld.getName());
plugin.getBackupService().backup(buildWorld,
() -> Messages.sendMessage(player, "worlds_backup_created", worldNamePlaceholder),
() -> Messages.sendMessage(player, "worlds_backup_failed", worldNamePlaceholder)
);
} else {
Messages.sendMessage(player, "worlds_backup_usage");
}
}
default -> {
Messages.sendMessage(player, "worlds_backup_usage");
}
}
}
@Override
public Argument getArgument() {
return WorldsArgument.BACKUP;
}
}
@@ -0,0 +1,69 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.subcommand.worlds;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.command.subcommand.Argument;
import de.eintosti.buildsystem.command.subcommand.SubCommand;
import de.eintosti.buildsystem.command.tabcomplete.WorldsTabCompleter.WorldsArgument;
import de.eintosti.buildsystem.world.builder.BuilderInventory;
import de.eintosti.buildsystem.world.util.WorldPermissionsImpl;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public class BuildersSubCommand implements SubCommand {
private final BuildSystemPlugin plugin;
@Nullable
private final BuildWorld buildWorld;
public BuildersSubCommand(BuildSystemPlugin plugin, String worldName) {
this.plugin = plugin;
this.buildWorld = plugin.getWorldService().getWorldStorage().getBuildWorld(worldName);
}
@Override
public void execute(Player player, String[] args) {
if (!WorldPermissionsImpl.of(buildWorld).canPerformCommand(player, getArgument().getPermission())) {
Messages.sendPermissionError(player);
return;
}
if (args.length > 2) {
Messages.sendMessage(player, "worlds_builders_usage");
return;
}
if (buildWorld == null) {
Messages.sendMessage(player, "worlds_builders_unknown_world");
return;
}
new BuilderInventory(plugin).openInventory(buildWorld, player);
}
@Override
public Argument getArgument() {
return WorldsArgument.BUILDERS;
}
}
@@ -0,0 +1,75 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.subcommand.worlds;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.command.subcommand.Argument;
import de.eintosti.buildsystem.command.subcommand.SubCommand;
import de.eintosti.buildsystem.command.tabcomplete.WorldsTabCompleter.WorldsArgument;
import de.eintosti.buildsystem.config.Config.World;
import de.eintosti.buildsystem.world.modification.DeleteInventory;
import de.eintosti.buildsystem.world.util.WorldPermissionsImpl;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public class DeleteSubCommand implements SubCommand {
private final BuildSystemPlugin plugin;
@Nullable
private final BuildWorld buildWorld;
public DeleteSubCommand(BuildSystemPlugin plugin, String worldName) {
this.plugin = plugin;
this.buildWorld = plugin.getWorldService().getWorldStorage().getBuildWorld(worldName);
}
@Override
public void execute(Player player, String[] args) {
if (!WorldPermissionsImpl.of(buildWorld).canPerformCommand(player, getArgument().getPermission())) {
Messages.sendPermissionError(player);
return;
}
if (args.length > 2) {
Messages.sendMessage(player, "worlds_delete_usage");
return;
}
if (buildWorld == null) {
Messages.sendMessage(player, "worlds_delete_unknown_world");
return;
}
if (World.deletionBlacklist.contains(buildWorld.getName().toLowerCase())) {
Messages.sendMessage(player, "worlds_delete_forbidden");
return;
}
new DeleteInventory(plugin).openInventory(player, buildWorld);
}
@Override
public Argument getArgument() {
return WorldsArgument.DELETE;
}
}
@@ -0,0 +1,76 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.subcommand.worlds;
import com.cryptomorin.xseries.XSound;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.command.subcommand.Argument;
import de.eintosti.buildsystem.command.subcommand.SubCommand;
import de.eintosti.buildsystem.command.tabcomplete.WorldsTabCompleter.WorldsArgument;
import de.eintosti.buildsystem.world.modification.EditInventory;
import de.eintosti.buildsystem.world.util.WorldPermissionsImpl;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public class EditSubCommand implements SubCommand {
private final BuildSystemPlugin plugin;
@Nullable
private final BuildWorld buildWorld;
public EditSubCommand(BuildSystemPlugin plugin, String worldName) {
this.plugin = plugin;
this.buildWorld = plugin.getWorldService().getWorldStorage().getBuildWorld(worldName);
}
@Override
public void execute(Player player, String[] args) {
if (!WorldPermissionsImpl.of(buildWorld).canPerformCommand(player, getArgument().getPermission())) {
Messages.sendPermissionError(player);
return;
}
if (args.length > 2) {
Messages.sendMessage(player, "worlds_edit_usage");
return;
}
if (buildWorld == null) {
Messages.sendMessage(player, "worlds_edit_unknown_world");
return;
}
if (buildWorld.isLoaded()) {
XSound.BLOCK_CHEST_OPEN.play(player);
new EditInventory(plugin).openInventory(player, buildWorld);
} else {
XSound.ENTITY_ZOMBIE_BREAK_WOODEN_DOOR.play(player);
player.sendTitle(" ", Messages.getString("world_not_loaded", player), 5, 70, 20);
}
}
@Override
public Argument getArgument() {
return WorldsArgument.EDIT;
}
}
@@ -0,0 +1,241 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.subcommand.worlds;
import com.cryptomorin.xseries.XMaterial;
import com.cryptomorin.xseries.XSound;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.storage.FolderStorage;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.api.world.display.Folder;
import de.eintosti.buildsystem.api.world.display.NavigatorCategory;
import de.eintosti.buildsystem.command.subcommand.Argument;
import de.eintosti.buildsystem.command.subcommand.SubCommand;
import de.eintosti.buildsystem.command.tabcomplete.WorldsTabCompleter.WorldsArgument;
import de.eintosti.buildsystem.util.PlayerChatInput;
import de.eintosti.buildsystem.world.WorldServiceImpl;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class FolderSubCommand implements SubCommand {
private final BuildSystemPlugin plugin;
private final WorldServiceImpl worldService;
private final FolderStorage folderStorage;
public FolderSubCommand(BuildSystemPlugin plugin) {
this.plugin = plugin;
this.worldService = plugin.getWorldService();
this.folderStorage = worldService.getFolderStorage();
}
@Override
public void execute(Player player, String[] args) {
if (args.length < 2) {
Messages.sendMessage(player, "worlds_folder_usage");
return;
}
String folderName = args[1];
String operation = args.length > 2 ? args[2].toLowerCase(Locale.ROOT) : "";
Folder folder = this.folderStorage.getFolder(folderName);
if (folder == null) {
Messages.sendMessage(player, "worlds_folder_unknown_folder");
return;
}
switch (operation) {
case "add":
case "remove": {
if (args.length != 4) {
Messages.sendMessage(player, "worlds_folder_usage");
return;
}
handleWorldFolderOperation(player, folder, operation, args[3]);
break;
}
case "setpermission": {
handlePermissionInput(player, folder);
break;
}
case "setproject": {
handleProjectInput(player, folder);
break;
}
case "setitem": {
handleIconChange(player, folder);
break;
}
case "delete": {
handleDeletion(player, folder);
break;
}
default: {
Messages.sendMessage(player, "worlds_folder_usage");
break;
}
}
}
private void handleWorldFolderOperation(Player player, Folder folder, String operation, String worldName) {
BuildWorld buildWorld = worldService.getWorldStorage().getBuildWorld(worldName);
if (buildWorld == null) {
Messages.sendMessage(player, "worlds_folder_unknown_world");
return;
}
Entry<String, Object> folderPlaceholder = Map.entry("%folder%", folder.getName());
Entry<String, Object> worldPlaceholder = Map.entry("%world%", buildWorld.getName());
switch (operation) {
case "add":
if (!player.hasPermission("buildsystem.folder.add")) {
Messages.sendPermissionError(player);
return;
}
if (folder.containsWorld(buildWorld)) {
Messages.sendMessage(player, "worlds_folder_world_already_in_folder", folderPlaceholder, worldPlaceholder);
return;
}
if (buildWorld.isAssignedToFolder()) {
Messages.sendMessage(player, "worlds_folder_world_already_in_another_folder", worldPlaceholder);
return;
}
if (folder.getCategory() != NavigatorCategory.of(buildWorld)) {
Messages.sendMessage(player, "worlds_folder_world_category_mismatch",
Map.entry("%folder_category%", folder.getCategory().name()),
Map.entry("%world_category%", NavigatorCategory.of(buildWorld).name())
);
return;
}
folder.addWorld(buildWorld);
Messages.sendMessage(player, "worlds_folder_world_added_to_folder", folderPlaceholder, worldPlaceholder);
break;
case "remove":
if (!player.hasPermission("buildsystem.folder.remove")) {
Messages.sendPermissionError(player);
return;
}
if (!folder.containsWorld(buildWorld)) {
Messages.sendMessage(player, "worlds_folder_world_not_in_folder", folderPlaceholder, worldPlaceholder);
return;
}
folder.removeWorld(buildWorld);
Messages.sendMessage(player, "worlds_folder_world_removed_from_folder", folderPlaceholder, worldPlaceholder);
break;
}
}
private void handlePermissionInput(Player player, Folder folder) {
if (!player.hasPermission("buildsystem.folder.setpermission")) {
Messages.sendPermissionError(player);
return;
}
new PlayerChatInput(this.plugin, player, "enter_world_permission", input -> {
folder.setPermission(input.trim());
XSound.ENTITY_PLAYER_LEVELUP.play(player);
Messages.sendMessage(player, "worlds_folder_permission_set",
Map.entry("%folder%", folder.getName())
);
});
}
private void handleProjectInput(Player player, Folder folder) {
if (!player.hasPermission("buildsystem.folder.setproject")) {
Messages.sendPermissionError(player);
return;
}
new PlayerChatInput(this.plugin, player, "enter_world_project", input -> {
folder.setProject(input.trim());
XSound.ENTITY_PLAYER_LEVELUP.play(player);
Messages.sendMessage(player, "worlds_folder_project_set",
Map.entry("%folder%", folder.getName())
);
});
}
private void handleIconChange(Player player, Folder folder) {
if (!player.hasPermission("buildsystem.folder.setitem")) {
Messages.sendPermissionError(player);
return;
}
ItemStack itemStack = player.getInventory().getItemInMainHand();
if (itemStack.getType() == Material.AIR) {
Messages.sendMessage(player, "worlds_setitem_hand_empty");
return;
}
folder.setIcon(XMaterial.matchXMaterial(itemStack));
Messages.sendMessage(player, "worlds_folder_item_set",
Map.entry("%folder%", folder.getName())
);
}
private void handleDeletion(Player player, Folder folder) {
if (!player.hasPermission("buildsystem.folder.delete")) {
Messages.sendPermissionError(player);
return;
}
boolean hasWorlds = folder.getWorldCount() > 0;
boolean hasSubFolders = this.folderStorage.getFolders().stream().anyMatch(f -> Objects.equals(f.getParent(), folder));
if (hasWorlds || hasSubFolders) {
Messages.sendMessage(player, "worlds_folder_not_empty",
Map.entry("%folder%", folder.getName())
);
return;
}
this.folderStorage.removeFolder(folder);
Messages.sendMessage(player, "worlds_folder_deleted",
Map.entry("%folder%", folder.getName())
);
XSound.ENTITY_PLAYER_LEVELUP.play(player);
}
@Override
public Argument getArgument() {
return WorldsArgument.FOLDER;
}
}
@@ -0,0 +1,91 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.subcommand.worlds;
import com.google.common.collect.Lists;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.command.PagedCommand;
import de.eintosti.buildsystem.command.subcommand.Argument;
import de.eintosti.buildsystem.command.subcommand.SubCommand;
import de.eintosti.buildsystem.command.tabcomplete.WorldsTabCompleter.WorldsArgument;
import java.util.List;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class HelpSubCommand extends PagedCommand implements SubCommand {
public HelpSubCommand() {
super("worlds_help_title_with_page", "worlds_help_permission");
}
@Override
public void execute(Player player, String[] args) {
if (!hasPermission(player)) {
Messages.sendPermissionError(player);
return;
}
if (args.length == 1) {
sendMessage(player, 1);
} else if (args.length == 2) {
try {
int page = Integer.parseInt(args[1]);
sendMessage(player, page);
} catch (NumberFormatException e) {
Messages.sendMessage(player, "worlds_help_invalid_page");
}
} else {
Messages.sendMessage(player, "worlds_help_usage");
}
}
@Override
protected List<TextComponent> getCommands(Player player) {
List<TextComponent> commands = Lists.newArrayList(
createComponent(player, "/worlds help <page>", "worlds_help_help", "/worlds help", "-"),
createComponent(player, "/worlds info", "worlds_help_info", "/worlds info", "buildsystem.info"),
createComponent(player, "/worlds item", "worlds_help_item", "/worlds item", "buildsystem.navigator.item"),
createComponent(player, "/worlds tp <world>", "worlds_help_tp", "/worlds tp ", "buildsystem.worldtp"),
createComponent(player, "/worlds edit <world>", "worlds_help_edit", "/worlds edit ", "buildsystem.edit"),
createComponent(player, "/worlds addBuilder <world>", "worlds_help_addbuilder", "/worlds addBuilder ", "buildsystem.addbuilder"),
createComponent(player, "/worlds removeBuilder <world>", "worlds_help_removebuilder", "/worlds removeBuilder ", "buildsystem.removebuilder"),
createComponent(player, "/worlds builders <world>", "worlds_help_builders", "/worlds builders ", "buildsystem.builders"),
createComponent(player, "/worlds rename <world>", "worlds_help_rename", "/worlds rename ", "buildsystem.rename"),
createComponent(player, "/worlds setItem <world>", "worlds_help_setitem", "/worlds setItem ", "buildsystem.setitem"),
createComponent(player, "/worlds setCreator <world>", "worlds_help_setcreator", "/worlds setCreator ", "buildsystem.setcreator"),
createComponent(player, "/worlds setProject <world>", "worlds_help_setproject", "/worlds setProject ", "buildsystem.setproject"),
createComponent(player, "/worlds setPermission <world>", "worlds_help_setpermission", "/worlds setPermission ", "buildsystem.setpermission"),
createComponent(player, "/worlds setStatus <world>", "worlds_help_setstatus", "/worlds setStatus ", "buildsystem.setstatus"),
createComponent(player, "/worlds setSpawn", "worlds_help_setspawn", "/worlds setSpawn", "buildsystem.setspawn"),
createComponent(player, "/worlds removeSpawn", "worlds_help_removespawn", "/worlds removeSpawn", "buildsystem.removespawn"),
createComponent(player, "/worlds delete <world>", "worlds_help_delete", "/worlds delete ", "buildsystem.delete"),
createComponent(player, "/worlds import <world>", "worlds_help_import", "/worlds import ", "buildsystem.import"),
createComponent(player, "/worlds importAll", "worlds_help_importall", "/worlds importAll", "buildsystem.import.all"),
createComponent(player, "/worlds unimport", "worlds_help_unimport", "/worlds unimport", "buildsystem.unimport")
);
commands.removeIf(textComponent -> textComponent.getText().isEmpty());
return commands;
}
@Override
public Argument getArgument() {
return WorldsArgument.HELP;
}
}
@@ -0,0 +1,120 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.subcommand.worlds;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.world.builder.Builder;
import de.eintosti.buildsystem.api.world.creation.generator.Generator;
import de.eintosti.buildsystem.command.subcommand.Argument;
import de.eintosti.buildsystem.command.subcommand.SubCommand;
import de.eintosti.buildsystem.command.tabcomplete.WorldsTabCompleter.WorldsArgument;
import de.eintosti.buildsystem.util.ArgumentParser;
import de.eintosti.buildsystem.util.UUIDFetcher;
import de.eintosti.buildsystem.world.WorldServiceImpl;
import java.io.File;
import java.util.Locale;
import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class ImportAllSubCommand implements SubCommand {
private final BuildSystemPlugin plugin;
public ImportAllSubCommand(BuildSystemPlugin plugin) {
this.plugin = plugin;
}
@Override
public void execute(Player player, String[] args) {
if (!hasPermission(player)) {
Messages.sendPermissionError(player);
return;
}
if (args.length != 1) {
Messages.sendMessage(player, "worlds_importall_usage");
return;
}
WorldServiceImpl worldService = plugin.getWorldService();
if (worldService.isImportingAllWorlds()) {
Messages.sendMessage(player, "worlds_importall_already_started");
return;
}
File worldContainer = Bukkit.getWorldContainer();
String[] directories = worldContainer.list((dir, name) -> {
File worldFolder = new File(dir, name);
if (!worldFolder.isDirectory()) {
return false;
}
if (!new File(worldFolder, "level.dat").exists()) {
return false;
}
return !worldService.getWorldStorage().worldExists(name);
});
if (directories == null || directories.length == 0) {
Messages.sendMessage(player, "worlds_importall_no_worlds");
return;
}
ArgumentParser parser = new ArgumentParser(args);
Generator generator = Generator.VOID;
Builder creator = null;
if (parser.isArgument("g")) {
String generatorArg = parser.getValue("g");
if (generatorArg == null) {
Messages.sendMessage(player, "worlds_importall_usage");
return;
}
try {
generator = Generator.valueOf(generatorArg.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException ignored) {
}
}
if (parser.isArgument("c")) {
String creatorArg = parser.getValue("c");
if (creatorArg == null) {
Messages.sendMessage(player, "worlds_importall_usage");
return;
}
UUID creatorId = UUIDFetcher.getUUID(creatorArg);
if (creatorId == null) {
Messages.sendMessage(player, "worlds_importall_player_not_found");
return;
}
creator = Builder.of(creatorId, creatorArg);
}
worldService.importWorlds(player, directories, generator, creator);
}
@Override
public Argument getArgument() {
return WorldsArgument.IMPORT_ALL;
}
}
@@ -0,0 +1,147 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.subcommand.worlds;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.world.builder.Builder;
import de.eintosti.buildsystem.api.world.creation.generator.Generator;
import de.eintosti.buildsystem.api.world.data.BuildWorldType;
import de.eintosti.buildsystem.command.subcommand.Argument;
import de.eintosti.buildsystem.command.subcommand.SubCommand;
import de.eintosti.buildsystem.command.tabcomplete.WorldsTabCompleter.WorldsArgument;
import de.eintosti.buildsystem.util.ArgumentParser;
import de.eintosti.buildsystem.util.StringCleaner;
import de.eintosti.buildsystem.util.UUIDFetcher;
import de.eintosti.buildsystem.world.WorldServiceImpl;
import java.io.File;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class ImportSubCommand implements SubCommand {
private final BuildSystemPlugin plugin;
private final String worldName;
public ImportSubCommand(BuildSystemPlugin plugin, String worldName) {
this.plugin = plugin;
this.worldName = worldName;
}
@Override
public void execute(Player player, String[] args) {
if (!hasPermission(player)) {
Messages.sendPermissionError(player);
return;
}
if (args.length < 2) {
Messages.sendMessage(player, "worlds_import_usage");
return;
}
WorldServiceImpl worldService = plugin.getWorldService();
if (worldService.getWorldStorage().worldExists(worldName)) {
Messages.sendMessage(player, "worlds_import_world_is_imported");
return;
}
File worldFolder = new File(Bukkit.getWorldContainer(), args[1]);
File levelFile = new File(worldFolder, "level.dat");
if (!worldFolder.isDirectory() || !levelFile.exists()) {
Messages.sendMessage(player, "worlds_import_unknown_world");
return;
}
String invalidChar = StringCleaner.firstInvalidChar(worldName);
if (invalidChar != null) {
Messages.sendMessage(player, "worlds_import_invalid_character",
Map.entry("%world%", worldName),
Map.entry("%char%", invalidChar)
);
return;
}
Builder creator = null;
Generator generator = Generator.VOID;
String generatorName = generator.name();
BuildWorldType worldType = BuildWorldType.IMPORTED;
if (args.length != 2) {
ArgumentParser parser = new ArgumentParser(args);
if (parser.isArgument("g")) {
String generatorArg = parser.getValue("g");
if (generatorArg == null) {
Messages.sendMessage(player, "worlds_import_usage");
return;
}
try {
generator = Generator.valueOf(generatorArg.toUpperCase(Locale.ROOT));
generatorName = generator.name();
} catch (IllegalArgumentException ignored) {
generator = Generator.CUSTOM;
generatorName = generatorArg;
}
}
if (parser.isArgument("c")) {
String creatorArg = parser.getValue("c");
if (creatorArg == null) {
Messages.sendMessage(player, "worlds_import_usage");
return;
}
UUID creatorId = UUIDFetcher.getUUID(creatorArg);
if (creatorId == null) {
Messages.sendMessage(player, "worlds_import_player_not_found");
return;
}
creator = Builder.of(creatorId, creatorArg);
}
if (parser.isArgument("t")) {
String worldTypeArg = parser.getValue("t");
if (worldTypeArg == null) {
Messages.sendMessage(player, "worlds_import_usage");
return;
}
try {
worldType = BuildWorldType.valueOf(worldTypeArg.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException ignored) {
}
}
}
Messages.sendMessage(player, "worlds_import_started",
Map.entry("%world%", worldName)
);
if (worldService.importWorld(player, worldName, creator, worldType, generator, generatorName, true)) {
Messages.sendMessage(player, "worlds_import_finished");
}
}
@Override
public Argument getArgument() {
return WorldsArgument.IMPORT;
}
}
@@ -0,0 +1,118 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.subcommand.worlds;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.api.world.builder.Builder;
import de.eintosti.buildsystem.api.world.builder.Builders;
import de.eintosti.buildsystem.api.world.data.WorldData;
import de.eintosti.buildsystem.command.subcommand.Argument;
import de.eintosti.buildsystem.command.subcommand.SubCommand;
import de.eintosti.buildsystem.command.tabcomplete.WorldsTabCompleter.WorldsArgument;
import de.eintosti.buildsystem.world.util.WorldPermissionsImpl;
import java.util.Map;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public class InfoSubCommand implements SubCommand {
@Nullable
private final BuildWorld buildWorld;
public InfoSubCommand(BuildSystemPlugin plugin, String worldName) {
this.buildWorld = plugin.getWorldService().getWorldStorage().getBuildWorld(worldName);
}
@Override
public void execute(Player player, String[] args) {
if (!WorldPermissionsImpl.of(buildWorld).canPerformCommand(player, getArgument().getPermission())) {
Messages.sendPermissionError(player);
return;
}
if (args.length > 2) {
Messages.sendMessage(player, "worlds_info_usage");
return;
}
if (buildWorld == null) {
Messages.sendMessage(player, "worlds_info_unknown_world");
return;
}
//TODO: Print information about the custom generator?
Builders builders = buildWorld.getBuilders();
WorldData worldData = buildWorld.getData();
Messages.sendMessage(player, "world_info",
Map.entry("%world%", buildWorld.getName()),
Map.entry("%uuid%", buildWorld.getUniqueId().toString()),
Map.entry("%creator%", getCreator(builders)),
Map.entry("%item%", worldData.material().get().name()),
Map.entry("%type%", Messages.getString(Messages.getMessageKey(buildWorld.getType()), player)),
Map.entry("%private%", worldData.privateWorld().get()),
Map.entry("%builders_enabled%", worldData.buildersEnabled().get()),
Map.entry("%builders%", builders.asPlaceholder(player)),
Map.entry("%block_breaking%", worldData.blockBreaking().get()),
Map.entry("%block_placement%", worldData.blockPlacement().get()),
Map.entry("%status%", Messages.getString(Messages.getMessageKey(worldData.status().get()), player)),
Map.entry("%project%", worldData.project().get()),
Map.entry("%permission%", worldData.permission().get()),
Map.entry("%time%", buildWorld.getWorldTime()),
Map.entry("%creation%", Messages.formatDate(buildWorld.getCreation())),
Map.entry("%physics%", worldData.physics().get()),
Map.entry("%explosions%", worldData.explosions().get()),
Map.entry("%mobai%", worldData.mobAi().get()),
Map.entry("%custom_spawn%", getCustomSpawn(buildWorld)),
Map.entry("%lastedited%", Messages.formatDate(worldData.lastEdited().get())),
Map.entry("%lastloaded%", Messages.formatDate(worldData.lastLoaded().get())),
Map.entry("%lastunloaded%", Messages.formatDate(worldData.lastUnloaded().get()))
);
}
private String getCreator(Builders builders) {
Builder creator = builders.getCreator();
if (creator == null) {
return "-";
}
return creator.getName();
}
private String getCustomSpawn(BuildWorld buildWorld) {
WorldData worldData = buildWorld.getData();
Location spawn = worldData.getCustomSpawnLocation();
if (spawn == null) {
return "-";
}
return "XYZ: " + round(spawn.getX()) + " / " + round(spawn.getY()) + " / " + round(spawn.getZ());
}
private double round(double value) {
int scale = (int) Math.pow(10, 2);
return (double) Math.round(value * scale) / scale;
}
@Override
public Argument getArgument() {
return WorldsArgument.INFO;
}
}
@@ -0,0 +1,49 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.subcommand.worlds;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.command.subcommand.Argument;
import de.eintosti.buildsystem.command.subcommand.SubCommand;
import de.eintosti.buildsystem.command.tabcomplete.WorldsTabCompleter.WorldsArgument;
import de.eintosti.buildsystem.util.inventory.InventoryUtils;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class ItemSubCommand implements SubCommand {
public ItemSubCommand() {
}
@Override
public void execute(Player player, String[] args) {
if (!hasPermission(player)) {
Messages.sendPermissionError(player);
return;
}
player.getInventory().addItem(InventoryUtils.createNavigatorItem(player));
Messages.sendMessage(player, "worlds_item_receive");
}
@Override
public Argument getArgument() {
return WorldsArgument.ITEM;
}
}
@@ -0,0 +1,52 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.subcommand.worlds;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.command.subcommand.Argument;
import de.eintosti.buildsystem.command.subcommand.SubCommand;
import de.eintosti.buildsystem.command.tabcomplete.WorldsTabCompleter.WorldsArgument;
import de.eintosti.buildsystem.world.navigator.inventory.PrivateWorldsInventory;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class PrivateSubCommand implements SubCommand {
private final BuildSystemPlugin plugin;
public PrivateSubCommand(BuildSystemPlugin plugin) {
this.plugin = plugin;
}
@Override
public void execute(Player player, String[] args) {
if (!hasPermission(player)) {
Messages.sendPermissionError(player);
return;
}
new PrivateWorldsInventory(plugin, player).openInventory();
}
@Override
public Argument getArgument() {
return WorldsArgument.PRIVATE;
}
}
@@ -0,0 +1,52 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.subcommand.worlds;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.command.subcommand.Argument;
import de.eintosti.buildsystem.command.subcommand.SubCommand;
import de.eintosti.buildsystem.command.tabcomplete.WorldsTabCompleter.WorldsArgument;
import de.eintosti.buildsystem.world.navigator.inventory.PublicWorldsInventory;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class PublicSubCommand implements SubCommand {
private final BuildSystemPlugin plugin;
public PublicSubCommand(BuildSystemPlugin plugin) {
this.plugin = plugin;
}
@Override
public void execute(Player player, String[] args) {
if (!hasPermission(player)) {
Messages.sendPermissionError(player);
return;
}
new PublicWorldsInventory(plugin, player).openInventory();
}
@Override
public Argument getArgument() {
return WorldsArgument.PUBLIC;
}
}
@@ -0,0 +1,119 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.subcommand.worlds;
import com.cryptomorin.xseries.XSound;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.api.world.builder.Builders;
import de.eintosti.buildsystem.api.world.util.WorldPermissions;
import de.eintosti.buildsystem.command.subcommand.Argument;
import de.eintosti.buildsystem.command.subcommand.SubCommand;
import de.eintosti.buildsystem.command.tabcomplete.WorldsTabCompleter.WorldsArgument;
import de.eintosti.buildsystem.util.PlayerChatInput;
import de.eintosti.buildsystem.util.UUIDFetcher;
import de.eintosti.buildsystem.world.util.WorldPermissionsImpl;
import java.util.Map;
import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public class RemoveBuilderSubCommand implements SubCommand {
private final BuildSystemPlugin plugin;
@Nullable
private final BuildWorld buildWorld;
private final WorldPermissions permissions;
public RemoveBuilderSubCommand(BuildSystemPlugin plugin, String worldName) {
this.plugin = plugin;
this.buildWorld = plugin.getWorldService().getWorldStorage().getBuildWorld(worldName);
this.permissions = WorldPermissionsImpl.of(buildWorld);
}
@Override
public void execute(Player player, String[] args) {
if (!permissions.canPerformCommand(player, getArgument().getPermission())) {
Messages.sendPermissionError(player);
return;
}
if (buildWorld == null) {
Messages.sendMessage(player, "worlds_removebuilder_unknown_world");
return;
}
switch (args.length) {
case 1 -> getRemoveBuilderInput(player, buildWorld);
case 2 -> removeBuilder(player, buildWorld, args[1]);
default -> Messages.sendMessage(player, "worlds_removebuilder_usage");
}
}
private void removeBuilder(Player player, BuildWorld buildWorld, String builderName) {
Player builderPlayer = Bukkit.getPlayerExact(builderName);
UUID builderId;
if (builderPlayer == null) {
builderId = UUIDFetcher.getUUID(builderName);
if (builderId == null) {
Messages.sendMessage(player, "worlds_removebuilder_player_not_found");
player.closeInventory();
return;
}
} else {
builderId = builderPlayer.getUniqueId();
}
Builders builders = buildWorld.getBuilders();
if (builderId.equals(player.getUniqueId()) && builders.isCreator(player)) {
Messages.sendMessage(player, "worlds_removebuilder_not_yourself");
player.closeInventory();
return;
}
if (!builders.isBuilder(builderId)) {
Messages.sendMessage(player, "worlds_removebuilder_not_builder");
player.closeInventory();
return;
}
builders.removeBuilder(builderId);
XSound.ENTITY_PLAYER_LEVELUP.play(player);
Messages.sendMessage(player, "worlds_removebuilder_removed", Map.entry("%builder%", builderName));
player.closeInventory();
}
private void getRemoveBuilderInput(Player player, BuildWorld buildWorld) {
new PlayerChatInput(plugin, player, "enter_player_name", input -> {
String builderName = input.trim();
removeBuilder(player, buildWorld, builderName);
});
}
@Override
public Argument getArgument() {
return WorldsArgument.REMOVE_BUILDER;
}
}
@@ -0,0 +1,63 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.subcommand.worlds;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.command.subcommand.Argument;
import de.eintosti.buildsystem.command.subcommand.SubCommand;
import de.eintosti.buildsystem.command.tabcomplete.WorldsTabCompleter.WorldsArgument;
import de.eintosti.buildsystem.world.util.WorldPermissionsImpl;
import java.util.Map;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class RemoveSpawnSubCommand implements SubCommand {
private final BuildSystemPlugin plugin;
public RemoveSpawnSubCommand(BuildSystemPlugin plugin) {
this.plugin = plugin;
}
@Override
public void execute(Player player, String[] args) {
BuildWorld buildWorld = plugin.getWorldService().getWorldStorage().getBuildWorld(player.getWorld());
if (!WorldPermissionsImpl.of(buildWorld).canPerformCommand(player, getArgument().getPermission())) {
Messages.sendPermissionError(player);
return;
}
if (buildWorld == null) {
Messages.sendMessage(player, "worlds_removespawn_world_not_imported");
return;
}
buildWorld.getData().customSpawn().set("");
Messages.sendMessage(player, "worlds_removespawn_world_spawn_removed",
Map.entry("%world%", buildWorld.getName())
);
}
@Override
public Argument getArgument() {
return WorldsArgument.REMOVE_SPAWN;
}
}
@@ -0,0 +1,74 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.subcommand.worlds;
import com.cryptomorin.xseries.XSound;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.command.subcommand.Argument;
import de.eintosti.buildsystem.command.subcommand.SubCommand;
import de.eintosti.buildsystem.command.tabcomplete.WorldsTabCompleter.WorldsArgument;
import de.eintosti.buildsystem.util.PlayerChatInput;
import de.eintosti.buildsystem.world.util.WorldPermissionsImpl;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public class RenameSubCommand implements SubCommand {
private final BuildSystemPlugin plugin;
@Nullable
private final BuildWorld buildWorld;
public RenameSubCommand(BuildSystemPlugin plugin, String worldName) {
this.plugin = plugin;
this.buildWorld = plugin.getWorldService().getWorldStorage().getBuildWorld(worldName);
}
@Override
public void execute(Player player, String[] args) {
if (!WorldPermissionsImpl.of(buildWorld).canPerformCommand(player, getArgument().getPermission())) {
Messages.sendPermissionError(player);
return;
}
if (args.length > 2) {
Messages.sendMessage(player, "worlds_rename_usage");
return;
}
if (buildWorld == null) {
Messages.sendMessage(player, "worlds_rename_unknown_world");
return;
}
new PlayerChatInput(plugin, player, "enter_world_name", input -> {
player.closeInventory();
plugin.getWorldService().renameWorld(player, buildWorld, input.trim());
XSound.ENTITY_PLAYER_LEVELUP.play(player);
});
}
@Override
public Argument getArgument() {
return WorldsArgument.RENAME;
}
}
@@ -0,0 +1,87 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.subcommand.worlds;
import com.cryptomorin.xseries.XSound;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.api.world.builder.Builder;
import de.eintosti.buildsystem.command.subcommand.Argument;
import de.eintosti.buildsystem.command.subcommand.SubCommand;
import de.eintosti.buildsystem.command.tabcomplete.WorldsTabCompleter.WorldsArgument;
import de.eintosti.buildsystem.util.PlayerChatInput;
import de.eintosti.buildsystem.util.UUIDFetcher;
import de.eintosti.buildsystem.world.util.WorldPermissionsImpl;
import java.util.Map;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public class SetCreatorSubCommand implements SubCommand {
private final BuildSystemPlugin plugin;
@Nullable
private final BuildWorld buildWorld;
public SetCreatorSubCommand(BuildSystemPlugin plugin, String worldName) {
this.plugin = plugin;
this.buildWorld = plugin.getWorldService().getWorldStorage().getBuildWorld(worldName);
}
@Override
public void execute(Player player, String[] args) {
if (!WorldPermissionsImpl.of(buildWorld).canPerformCommand(player, getArgument().getPermission())) {
Messages.sendPermissionError(player);
return;
}
if (args.length > 2) {
Messages.sendMessage(player, "worlds_setcreator_usage");
return;
}
if (buildWorld == null) {
Messages.sendMessage(player, "worlds_setcreator_unknown_world");
return;
}
new PlayerChatInput(plugin, player, "enter_world_creator", input -> {
String creatorName = input.trim();
Builder creator = null;
if (!creatorName.equalsIgnoreCase("-")) {
creator = Builder.of(UUIDFetcher.getUUID(creatorName), creatorName);
}
buildWorld.getBuilders().setCreator(creator);
plugin.getPlayerService().forceUpdateSidebar(buildWorld);
XSound.ENTITY_PLAYER_LEVELUP.play(player);
Messages.sendMessage(player, "worlds_setcreator_set",
Map.entry("%world%", buildWorld.getName())
);
player.closeInventory();
});
}
@Override
public Argument getArgument() {
return WorldsArgument.SET_CREATOR;
}
}
@@ -0,0 +1,78 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.subcommand.worlds;
import com.cryptomorin.xseries.XMaterial;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.command.subcommand.Argument;
import de.eintosti.buildsystem.command.subcommand.SubCommand;
import de.eintosti.buildsystem.command.tabcomplete.WorldsTabCompleter.WorldsArgument;
import de.eintosti.buildsystem.world.util.WorldPermissionsImpl;
import java.util.Map;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public class SetItemSubCommand implements SubCommand {
@Nullable
private final BuildWorld buildWorld;
public SetItemSubCommand(BuildSystemPlugin plugin, String worldName) {
this.buildWorld = plugin.getWorldService().getWorldStorage().getBuildWorld(worldName);
}
@Override
public void execute(Player player, String[] args) {
if (!WorldPermissionsImpl.of(buildWorld).canPerformCommand(player, getArgument().getPermission())) {
Messages.sendPermissionError(player);
return;
}
if (args.length > 2) {
Messages.sendMessage(player, "worlds_setitem_usage");
return;
}
if (buildWorld == null) {
Messages.sendMessage(player, "worlds_setitem_unknown_world");
return;
}
ItemStack itemStack = player.getInventory().getItemInMainHand();
if (itemStack.getType() == Material.AIR) {
Messages.sendMessage(player, "worlds_setitem_hand_empty");
return;
}
buildWorld.getData().material().set(XMaterial.matchXMaterial(itemStack));
Messages.sendMessage(player, "worlds_setitem_set",
Map.entry("%world%", buildWorld.getName())
);
}
@Override
public Argument getArgument() {
return WorldsArgument.SET_ITEM;
}
}
@@ -0,0 +1,90 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.subcommand.worlds;
import com.cryptomorin.xseries.XSound;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.command.subcommand.Argument;
import de.eintosti.buildsystem.command.subcommand.SubCommand;
import de.eintosti.buildsystem.command.tabcomplete.WorldsTabCompleter.WorldsArgument;
import de.eintosti.buildsystem.util.PlayerChatInput;
import de.eintosti.buildsystem.world.modification.EditInventory;
import de.eintosti.buildsystem.world.util.WorldPermissionsImpl;
import java.util.Map;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public class SetPermissionSubCommand implements SubCommand {
private final BuildSystemPlugin plugin;
@Nullable
private final BuildWorld buildWorld;
public SetPermissionSubCommand(BuildSystemPlugin plugin, String worldName) {
this.plugin = plugin;
this.buildWorld = plugin.getWorldService().getWorldStorage().getBuildWorld(worldName);
}
@Override
public void execute(Player player, String[] args) {
if (!WorldPermissionsImpl.of(buildWorld).canPerformCommand(player, getArgument().getPermission())) {
Messages.sendPermissionError(player);
return;
}
if (args.length > 2) {
Messages.sendMessage(player, "worlds_setpermission_usage");
return;
}
if (buildWorld == null) {
Messages.sendMessage(player, "worlds_setpermission_unknown_world");
return;
}
getPermissionInput(player, buildWorld, true);
}
public void getPermissionInput(Player player, BuildWorld buildWorld, boolean closeInventory) {
new PlayerChatInput(plugin, player, "enter_world_permission", input -> {
buildWorld.getData().permission().set(input.trim());
plugin.getPlayerService().forceUpdateSidebar(buildWorld);
XSound.ENTITY_PLAYER_LEVELUP.play(player);
Messages.sendMessage(player, "worlds_setpermission_set",
Map.entry("%world%", buildWorld.getName())
);
if (closeInventory) {
player.closeInventory();
} else {
new EditInventory(plugin).openInventory(player, buildWorld);
}
});
}
@Override
public Argument getArgument() {
return WorldsArgument.SET_PERMISSION;
}
}
@@ -0,0 +1,90 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.subcommand.worlds;
import com.cryptomorin.xseries.XSound;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.command.subcommand.Argument;
import de.eintosti.buildsystem.command.subcommand.SubCommand;
import de.eintosti.buildsystem.command.tabcomplete.WorldsTabCompleter.WorldsArgument;
import de.eintosti.buildsystem.util.PlayerChatInput;
import de.eintosti.buildsystem.world.modification.EditInventory;
import de.eintosti.buildsystem.world.util.WorldPermissionsImpl;
import java.util.Map;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public class SetProjectSubCommand implements SubCommand {
private final BuildSystemPlugin plugin;
@Nullable
private final BuildWorld buildWorld;
public SetProjectSubCommand(BuildSystemPlugin plugin, String worldName) {
this.plugin = plugin;
this.buildWorld = plugin.getWorldService().getWorldStorage().getBuildWorld(worldName);
}
@Override
public void execute(Player player, String[] args) {
if (!WorldPermissionsImpl.of(buildWorld).canPerformCommand(player, getArgument().getPermission())) {
Messages.sendPermissionError(player);
return;
}
if (args.length > 2) {
Messages.sendMessage(player, "worlds_setproject_usage");
return;
}
if (buildWorld == null) {
Messages.sendMessage(player, "worlds_setproject_unknown_world");
return;
}
getProjectInput(player, buildWorld, true);
}
public void getProjectInput(Player player, BuildWorld buildWorld, boolean closeInventory) {
new PlayerChatInput(plugin, player, "enter_world_project", input -> {
buildWorld.getData().project().set(input.trim());
plugin.getPlayerService().forceUpdateSidebar(buildWorld);
XSound.ENTITY_PLAYER_LEVELUP.play(player);
Messages.sendMessage(player, "worlds_setproject_set",
Map.entry("%world%", buildWorld.getName())
);
if (closeInventory) {
player.closeInventory();
} else {
new EditInventory(plugin).openInventory(player, buildWorld);
}
});
}
@Override
public Argument getArgument() {
return WorldsArgument.SET_PROJECT;
}
}
@@ -0,0 +1,67 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.subcommand.worlds;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.command.subcommand.Argument;
import de.eintosti.buildsystem.command.subcommand.SubCommand;
import de.eintosti.buildsystem.command.tabcomplete.WorldsTabCompleter.WorldsArgument;
import de.eintosti.buildsystem.world.util.WorldPermissionsImpl;
import java.util.Map;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class SetSpawnSubCommand implements SubCommand {
private final BuildSystemPlugin plugin;
public SetSpawnSubCommand(BuildSystemPlugin plugin) {
this.plugin = plugin;
}
@Override
public void execute(Player player, String[] args) {
BuildWorld buildWorld = plugin.getWorldService().getWorldStorage().getBuildWorld(player.getWorld());
if (!WorldPermissionsImpl.of(buildWorld).canPerformCommand(player, getArgument().getPermission())) {
Messages.sendPermissionError(player);
return;
}
if (buildWorld == null) {
Messages.sendMessage(player, "worlds_setspawn_world_not_imported");
return;
}
Location playerLocation = player.getLocation();
buildWorld.getData().customSpawn().set("%s;%s;%s;%s;%s".formatted(
playerLocation.getX(), playerLocation.getY(), playerLocation.getZ(), playerLocation.getYaw(), playerLocation.getPitch()
));
Messages.sendMessage(player, "worlds_setspawn_world_spawn_set",
Map.entry("%world%", buildWorld.getName())
);
}
@Override
public Argument getArgument() {
return WorldsArgument.SET_SPAWN;
}
}
@@ -0,0 +1,69 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.subcommand.worlds;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.command.subcommand.Argument;
import de.eintosti.buildsystem.command.subcommand.SubCommand;
import de.eintosti.buildsystem.command.tabcomplete.WorldsTabCompleter.WorldsArgument;
import de.eintosti.buildsystem.world.data.StatusInventory;
import de.eintosti.buildsystem.world.util.WorldPermissionsImpl;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public class SetStatusSubCommand implements SubCommand {
private final BuildSystemPlugin plugin;
@Nullable
private final BuildWorld buildWorld;
public SetStatusSubCommand(BuildSystemPlugin plugin, String worldName) {
this.plugin = plugin;
this.buildWorld = plugin.getWorldService().getWorldStorage().getBuildWorld(worldName);
}
@Override
public void execute(Player player, String[] args) {
if (!WorldPermissionsImpl.of(buildWorld).canPerformCommand(player, getArgument().getPermission())) {
Messages.sendPermissionError(player);
return;
}
if (args.length > 2) {
Messages.sendMessage(player, "worlds_setstatus_usage");
return;
}
if (buildWorld == null) {
Messages.sendMessage(player, "worlds_setstatus_unknown_world");
return;
}
new StatusInventory(plugin).openInventory(player, buildWorld);
}
@Override
public Argument getArgument() {
return WorldsArgument.SET_STATUS;
}
}
@@ -0,0 +1,76 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.subcommand.worlds;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.command.subcommand.Argument;
import de.eintosti.buildsystem.command.subcommand.SubCommand;
import de.eintosti.buildsystem.command.tabcomplete.WorldsTabCompleter.WorldsArgument;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class TeleportSubCommand implements SubCommand {
private final BuildSystemPlugin plugin;
public TeleportSubCommand(BuildSystemPlugin plugin) {
this.plugin = plugin;
}
@Override
public void execute(Player player, String[] args) {
if (!hasPermission(player)) {
Messages.sendPermissionError(player);
return;
}
if (args.length != 2) {
Messages.sendMessage(player, "worlds_tp_usage");
return;
}
BuildWorld buildWorld = plugin.getWorldService().getWorldStorage().getBuildWorld(args[1]);
if (buildWorld == null) {
Messages.sendMessage(player, "worlds_tp_unknown_world");
return;
}
World bukkitWorld = Bukkit.getServer().getWorld(args[1]);
if (buildWorld.isLoaded() && bukkitWorld == null) {
Messages.sendMessage(player, "worlds_tp_unknown_world");
return;
}
if (!buildWorld.getPermissions().canEnter(player)) {
Messages.sendMessage(player, "worlds_tp_entry_forbidden");
return;
}
buildWorld.getTeleporter().teleport(player);
}
@Override
public Argument getArgument() {
return WorldsArgument.TP;
}
}
@@ -0,0 +1,71 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.subcommand.worlds;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.command.subcommand.Argument;
import de.eintosti.buildsystem.command.subcommand.SubCommand;
import de.eintosti.buildsystem.command.tabcomplete.WorldsTabCompleter.WorldsArgument;
import de.eintosti.buildsystem.world.util.WorldPermissionsImpl;
import java.util.Map;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public class UnimportSubCommand implements SubCommand {
private final BuildSystemPlugin plugin;
@Nullable
private final BuildWorld buildWorld;
public UnimportSubCommand(BuildSystemPlugin plugin, String worldName) {
this.plugin = plugin;
this.buildWorld = plugin.getWorldService().getWorldStorage().getBuildWorld(worldName);
}
@Override
public void execute(Player player, String[] args) {
if (!WorldPermissionsImpl.of(buildWorld).canPerformCommand(player, getArgument().getPermission())) {
Messages.sendPermissionError(player);
return;
}
if (args.length > 2) {
Messages.sendMessage(player, "worlds_unimport_usage");
return;
}
if (buildWorld == null) {
Messages.sendMessage(player, "worlds_unimport_unknown_world");
return;
}
plugin.getWorldService()
.unimportWorld(buildWorld, true)
.thenRun(() -> Messages.sendMessage(player, "worlds_unimport_finished", Map.entry("%world%", buildWorld.getName())));
}
@Override
public Argument getArgument() {
return WorldsArgument.UNIMPORT;
}
}
@@ -0,0 +1,31 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.tabcomplete;
import java.util.List;
import org.jspecify.annotations.NullMarked;
@NullMarked
abstract class ArgumentSorter {
public void addArgument(String input, String argument, List<String> arrayList) {
if (input.isEmpty() || argument.toLowerCase().startsWith(input.toLowerCase())) {
arrayList.add(argument);
}
}
}
@@ -0,0 +1,54 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.tabcomplete;
import de.eintosti.buildsystem.BuildSystemPlugin;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class BuildTabCompleter extends ArgumentSorter implements TabCompleter {
public BuildTabCompleter(BuildSystemPlugin plugin) {
plugin.getCommand("build").setTabCompleter(this);
}
@Override
public List<String> onTabComplete(CommandSender sender, Command cmd, String label, String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
if (!(sender instanceof Player player)) {
return arrayList;
}
if (!player.hasPermission("buildsystem.build")) {
return arrayList;
}
if (args.length == 1 && !player.hasPermission("buildsystem.build.other")) {
Bukkit.getOnlinePlayers().forEach(pl -> addArgument(args[0], pl.getName(), arrayList));
}
return arrayList;
}
}
@@ -0,0 +1,45 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.tabcomplete;
import de.eintosti.buildsystem.BuildSystemPlugin;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class ConfigTabCompleter implements TabCompleter {
public ConfigTabCompleter(BuildSystemPlugin plugin) {
plugin.getCommand("config").setTabCompleter(this);
}
@Override
public List<String> onTabComplete(CommandSender sender, Command cmd, String label, String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
if (sender.hasPermission("buildsystem.config")) {
arrayList.add("reload");
}
return arrayList;
}
}
@@ -0,0 +1,46 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.tabcomplete;
import de.eintosti.buildsystem.BuildSystemPlugin;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.jetbrains.annotations.Contract;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class EmptyTabCompleter implements TabCompleter {
public EmptyTabCompleter(BuildSystemPlugin plugin) {
plugin.getCommand("back").setTabCompleter(this);
plugin.getCommand("blocks").setTabCompleter(this);
plugin.getCommand("buildsystem").setTabCompleter(this);
plugin.getCommand("settings").setTabCompleter(this);
plugin.getCommand("setup").setTabCompleter(this);
plugin.getCommand("top").setTabCompleter(this);
}
@Override
@Contract("_, _, _, _ -> new")
public List<String> onTabComplete(CommandSender sender, Command cmd, String label, String[] args) {
return new ArrayList<>();
}
}
@@ -0,0 +1,68 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.tabcomplete;
import de.eintosti.buildsystem.BuildSystemPlugin;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class GamemodeTabCompleter extends ArgumentSorter implements TabCompleter {
public GamemodeTabCompleter(BuildSystemPlugin plugin) {
plugin.getCommand("gamemode").setTabCompleter(this);
}
@Override
public List<String> onTabComplete(CommandSender sender, Command cmd, String label, String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
if (!(sender instanceof Player player)) {
return arrayList;
}
if (args.length == 1) {
Arrays.stream(GameMode.values())
.map(gameMode -> gameMode.name().toLowerCase(Locale.ROOT))
.filter(gameModeName -> player.hasPermission("buildsystem.gamemode.%s".formatted(gameModeName)))
.forEach(gameModeName -> addArgument(args[0], gameModeName, arrayList));
} else if (args.length == 2) {
String gameModeName = switch (args[0].toLowerCase(Locale.ROOT)) {
case "survival", "s", "0" -> GameMode.SURVIVAL.name().toLowerCase(Locale.ROOT);
case "creative", "c", "1" -> GameMode.CREATIVE.name().toLowerCase(Locale.ROOT);
case "adventure", "a", "2" -> GameMode.ADVENTURE.name().toLowerCase(Locale.ROOT);
case "spectator", "sp", "3" -> GameMode.SPECTATOR.name().toLowerCase(Locale.ROOT);
default -> null;
};
if (gameModeName != null && player.hasPermission("buildsystem.gamemode.%s.other".formatted(gameModeName))) {
Bukkit.getOnlinePlayers().forEach(pl -> addArgument(args[1], pl.getName(), arrayList));
}
}
return arrayList;
}
}
@@ -0,0 +1,55 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.tabcomplete;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.api.storage.WorldStorage;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class PhysicsTabCompleter extends ArgumentSorter implements TabCompleter {
private final WorldStorage worldStorage;
public PhysicsTabCompleter(BuildSystemPlugin plugin) {
this.worldStorage = plugin.getWorldService().getWorldStorage();
plugin.getCommand("physics").setTabCompleter(this);
}
@Override
public List<String> onTabComplete(CommandSender sender, Command cmd, String label, String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
if (!(sender instanceof Player player)) {
return arrayList;
}
if (args.length == 1) {
worldStorage.getBuildWorlds().stream()
.filter(world -> world.getPermissions().canPerformCommand(player, "buildsystem.physics"))
.forEach(world -> addArgument(args[0], world.getName(), arrayList));
}
return arrayList;
}
}
@@ -0,0 +1,56 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.tabcomplete;
import de.eintosti.buildsystem.BuildSystemPlugin;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class SpawnTabCompleter extends ArgumentSorter implements TabCompleter {
public SpawnTabCompleter(BuildSystemPlugin plugin) {
plugin.getCommand("spawn").setTabCompleter(this);
}
@Override
public List<String> onTabComplete(CommandSender sender, Command cmd, String label, String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
if (!(sender instanceof Player player)) {
return arrayList;
}
if (player.hasPermission("buildsystem.spawn")) {
for (Argument argument : Argument.values()) {
String argumentName = argument.name();
addArgument(args[0], argumentName, arrayList);
}
}
return arrayList;
}
private enum Argument {
set, remove
}
}
@@ -0,0 +1,43 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.tabcomplete;
import de.eintosti.buildsystem.BuildSystemPlugin;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class SpeedTabCompleter implements TabCompleter {
public SpeedTabCompleter(BuildSystemPlugin plugin) {
plugin.getCommand("speed").setTabCompleter(this);
}
@Override
public List<String> onTabComplete(CommandSender sender, Command cmd, String label, String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
for (int i = 1; i <= 5; i++) {
arrayList.add(String.valueOf(i));
}
return arrayList;
}
}
@@ -0,0 +1,61 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.tabcomplete;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.api.storage.WorldStorage;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class TimeTabCompleter extends ArgumentSorter implements TabCompleter {
private final WorldStorage worldStorage;
public TimeTabCompleter(BuildSystemPlugin plugin) {
this.worldStorage = plugin.getWorldService().getWorldStorage();
plugin.getCommand("day").setTabCompleter(this);
plugin.getCommand("night").setTabCompleter(this);
}
@Override
public List<String> onTabComplete(CommandSender sender, Command cmd, String label, String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
if (!(sender instanceof Player player)) {
return arrayList;
}
String labelLowerCase = label.toLowerCase(Locale.ROOT);
switch (labelLowerCase) {
case "day":
case "night":
worldStorage.getBuildWorlds().stream()
.filter(world -> world.getPermissions().canPerformCommand(player, "buildsystem." + labelLowerCase))
.forEach(world -> addArgument(args[0], world.getName(), arrayList));
break;
}
return arrayList;
}
}
@@ -0,0 +1,301 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.command.tabcomplete;
import com.google.common.collect.Lists;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.api.storage.FolderStorage;
import de.eintosti.buildsystem.api.storage.WorldStorage;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.api.world.WorldService;
import de.eintosti.buildsystem.api.world.builder.Builders;
import de.eintosti.buildsystem.api.world.creation.generator.Generator;
import de.eintosti.buildsystem.api.world.data.BuildWorldType;
import de.eintosti.buildsystem.api.world.display.Displayable;
import de.eintosti.buildsystem.api.world.display.Folder;
import de.eintosti.buildsystem.api.world.display.NavigatorCategory;
import de.eintosti.buildsystem.command.subcommand.Argument;
import de.eintosti.buildsystem.util.StringCleaner;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.annotation.Nullable;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class WorldsTabCompleter extends ArgumentSorter implements TabCompleter {
private final WorldStorage worldStorage;
private final FolderStorage folderStorage;
public WorldsTabCompleter(BuildSystemPlugin plugin) {
WorldService worldService = plugin.getWorldService();
this.worldStorage = worldService.getWorldStorage();
this.folderStorage = worldService.getFolderStorage();
plugin.getCommand("worlds").setTabCompleter(this);
}
@Override
public List<String> onTabComplete(CommandSender sender, Command cmd, String label, String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
if (!(sender instanceof Player player)) {
return arrayList;
}
switch (args.length) {
case 1: {
for (WorldsArgument argument : WorldsArgument.values()) {
String command = argument.getName();
String permission = argument.getPermission();
if (player.hasPermission(permission)) {
addArgument(args[0], command, arrayList);
}
}
return arrayList;
}
case 2: {
switch (args[0].toLowerCase(Locale.ROOT)) {
case "builders":
case "edit":
case "info":
case "rename":
case "setcreator":
case "setitem":
case "setpermission":
case "setproject":
case "setstatus":
case "tp":
case "unimport": {
worldStorage.getBuildWorlds().stream()
.filter(world -> {
String permission = world.getData().permission().get();
return player.hasPermission(permission) || permission.equalsIgnoreCase("-");
})
.filter(world -> world.getPermissions().canPerformCommand(player, "buildsystem." + args[0].toLowerCase(Locale.ROOT)))
.forEach(world -> addArgument(args[1], world.getName(), arrayList));
break;
}
case "backup": {
if (player.hasPermission(WorldsArgument.BACKUP.getPermission() + ".create")) {
addArgument(args[1], "create", arrayList);
}
break;
}
case "delete": {
worldStorage.getBuildWorlds().stream()
.filter(world -> world.getPermissions().canPerformCommand(player, "buildsystem." + args[0].toLowerCase(Locale.ROOT)))
.forEach(world -> addArgument(args[1], world.getName(), arrayList));
break;
}
case "addbuilder": {
BuildWorld buildWorld = worldStorage.getBuildWorld(player.getWorld().getName());
if (buildWorld == null) {
break;
}
Builders builders = buildWorld.getBuilders();
Bukkit.getOnlinePlayers().stream()
.filter(pl -> !builders.isBuilder(pl) && !builders.isCreator(pl))
.forEach(pl -> addArgument(args[1], pl.getName(), arrayList));
break;
}
case "removebuilder": {
BuildWorld buildWorld = worldStorage.getBuildWorld(player.getWorld().getName());
if (buildWorld == null) {
break;
}
Builders builders = buildWorld.getBuilders();
if (builders.isCreator(player)) {
builders.getBuilderNames().forEach(builderName -> addArgument(args[1], builderName, arrayList));
}
break;
}
case "import": {
String[] directories = Bukkit.getWorldContainer().list((dir, name) -> {
if (StringCleaner.hasInvalidNameCharacters(name)) {
return false;
}
File worldFolder = new File(dir, name);
if (!worldFolder.isDirectory()) {
return false;
}
if (!new File(worldFolder, "level.dat").exists()) {
return false;
}
return !worldStorage.worldExists(name);
});
if (directories == null || directories.length == 0) {
return arrayList;
}
for (String projectName : directories) {
addArgument(args[1], projectName, arrayList);
}
break;
}
case "folder": {
folderStorage.getFolders().stream()
.map(Displayable::getName)
.forEach(folderName -> addArgument(args[1], folderName, arrayList));
break;
}
}
return arrayList;
}
default:
switch (args[0].toLowerCase(Locale.ROOT)) {
case "import": {
Map<String, List<String>> arguments = Map.of(
"-g", Arrays.stream(Generator.values()).filter(generator -> generator != Generator.CUSTOM).map(Enum::name).toList(),
"-c", List.of(),
"-t", Arrays.stream(BuildWorldType.values()).map(Enum::name).toList()
);
if (args.length % 2 == 1) {
arguments.keySet().stream()
.filter(key -> !Lists.newArrayList(args).contains(key))
.forEach(argument -> addArgument(args[args.length - 1], argument, arrayList));
} else {
List<String> values = arguments.get(args[args.length - 2]);
if (values != null) {
for (String argument : values) {
addArgument(args[args.length - 1], argument, arrayList);
}
}
}
break;
}
case "folder": {
switch (args.length) {
case 3:
Map<String, String> subCommands = Map.of(
"add", "buildsystem.folder.add",
"remove", "buildsystem.folder.remove",
"delete", "buildsystem.folder.delete",
"setPermission", "buildsystem.folder.setpermission",
"setProject", "buildsystem.folder.setproject",
"setItem", "buildsystem.folder.setitem"
);
subCommands.entrySet().stream()
.filter(entry -> player.hasPermission(entry.getKey()))
.forEach(entry -> addArgument(args[2], entry.getKey(), arrayList));
break;
case 4:
if (!args[2].equalsIgnoreCase("add") && !args[2].equalsIgnoreCase("remove")) {
return arrayList;
}
Folder folder = folderStorage.getFolder(args[1]);
if (folder == null) {
return arrayList;
}
worldStorage.getBuildWorlds().stream()
.filter(buildWorld -> NavigatorCategory.of(buildWorld) == folder.getCategory())
.filter(buildWorld -> {
if (args[2].equalsIgnoreCase("add")) {
return !buildWorld.isAssignedToFolder();
} else if (args[2].equalsIgnoreCase("remove")) {
return folder.containsWorld(buildWorld);
}
return false;
})
.forEach(world -> addArgument(args[3], world.getName(), arrayList));
break;
}
}
}
return arrayList;
}
}
@NullMarked
public enum WorldsArgument implements Argument {
ARCHIVE("archive", "buildsystem.navigator"),
ADD_BUILDER("addBuilder", "buildsystem.addbuilder"),
BACKUP("backup", "buildsystem.backup"),
BUILDERS("builders", "buildsystem.builders"),
DELETE("delete", "buildsystem.delete"),
EDIT("edit", "buildsystem.edit"),
FOLDER("folder", "buildsystem.folder"),
HELP("help", "buildsystem.help.worlds"),
IMPORT("import", "buildsystem.import"),
IMPORT_ALL("importAll", "buildsystem.import.all"),
INFO("info", "buildsystem.info"),
ITEM("item", "buildsystem.navigator.item"),
PRIVATE("private", "buildsystem.navigator"),
PUBLIC("public", "buildsystem.navigator"),
REMOVE_BUILDER("removeBuilder", "buildsystem.removebuilder"),
RENAME("rename", "buildsystem.rename"),
SET_CREATOR("setCreator", "buildsystem.setcreator"),
SET_ITEM("setItem", "buildsystem.setitem"),
SET_PERMISSION("setPermission", "buildsystem.setpermission"),
SET_PROJECT("setProject", "buildsystem.setproject"),
SET_STATUS("setStatus", "buildsystem.setstatus"),
SET_SPAWN("setSpawn", "buildsystem.setspawn"),
REMOVE_SPAWN("removeSpawn", "buildsystem.removespawn"),
TP("tp", "buildsystem.worldtp"),
UNIMPORT("unimport", "buildsystem.unimport");
private final String command;
private final String permission;
WorldsArgument(String command, String permission) {
this.command = command;
this.permission = permission;
}
@Nullable
public static WorldsArgument matchArgument(String input) {
return Arrays.stream(values())
.filter(argument -> argument.getName().equalsIgnoreCase(input))
.findFirst()
.orElse(null);
}
@Override
public String getName() {
return command;
}
@Override
public String getPermission() {
return permission;
}
}
}
@@ -0,0 +1,646 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.config;
import com.cryptomorin.xseries.XMaterial;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.api.world.backup.BackupStorage;
import de.eintosti.buildsystem.config.Config.Settings.Archive;
import de.eintosti.buildsystem.config.Config.Settings.BuildMode;
import de.eintosti.buildsystem.config.Config.Settings.Builder;
import de.eintosti.buildsystem.config.Config.Settings.DisabledPhysics;
import de.eintosti.buildsystem.config.Config.Settings.Navigator;
import de.eintosti.buildsystem.config.Config.Settings.SaveFromDeath;
import de.eintosti.buildsystem.config.Config.World.Backup;
import de.eintosti.buildsystem.config.Config.World.Backup.AutoBackup;
import de.eintosti.buildsystem.config.Config.World.Default;
import de.eintosti.buildsystem.config.Config.World.Default.Permission;
import de.eintosti.buildsystem.config.Config.World.Default.Settings.BuildersEnabled;
import de.eintosti.buildsystem.config.Config.World.Default.Time;
import de.eintosti.buildsystem.config.Config.World.Limits;
import de.eintosti.buildsystem.config.Config.World.Unload;
import de.eintosti.buildsystem.config.migration.ConfigMigrationManager;
import de.eintosti.buildsystem.world.backup.storage.LocalBackupStorage;
import de.eintosti.buildsystem.world.backup.storage.S3BackupStorage;
import de.eintosti.buildsystem.world.backup.storage.SftpBackupStorage;
import de.eintosti.buildsystem.world.modification.GameRuleEntry;
import java.io.File;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import org.bukkit.Difficulty;
import org.bukkit.GameMode;
import org.bukkit.GameRule;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.jetbrains.annotations.Contract;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
/**
* Manages the plugin's configuration, loading and providing access to various settings.
*/
@NullMarked
public class Config {
private static final BuildSystemPlugin PLUGIN = BuildSystemPlugin.get();
private static final FileConfiguration CONFIG = PLUGIN.getConfig();
/**
* Gets the plugin's configuration.
*
* @return The plugin's configuration
*/
public static FileConfiguration getConfig() {
return CONFIG;
}
/**
* Gets the current version of the plugin's configuration.
*
* @return The version number of the configuration
* @see ConfigMigrationManager#LATEST_VERSION
*/
public static int getVersion() {
return CONFIG.getInt("version", 1);
}
/**
* Sets the version of the plugin's configuration.
*
* @param version The version number to set
*/
public static void setVersion(int version) {
CONFIG.set("version", version);
CONFIG.setComments("version", List.of("Internal, do not change manually!"));
PLUGIN.saveConfig();
}
/**
* Stores messages related configurations.
*/
public static class Messages {
/**
* Whether a message should be sent to the player when teleporting to a world's spawn.
*/
public static boolean spawnTeleportMessage = false;
/**
* Whether join and quit messages should be enabled.
*/
public static boolean joinQuitMessages = true;
/**
* The date format used for various date displays.
*/
public static String dateFormat = "dd/MM/yyyy";
}
/**
* Stores general settings for the plugin.
*/
public static class Settings {
/**
* Whether the update checker should be enabled.
*/
public static boolean updateChecker = true;
/**
* Whether the scoreboard should be enabled.
*/
public static boolean scoreboard = true;
/**
* Stores archive-related settings.
*/
public static class Archive {
/**
* Whether players should be vanished when entering an archived world.
*/
public static boolean vanish = true;
/**
* Whether the gamemode should be changed when entering an archived world.
*/
public static boolean changeGamemode = true;
/**
* The gamemode players are set to when entering an archived world.
*/
public static GameMode worldGameMode = GameMode.ADVENTURE;
}
/**
* Stores settings related to worlds with disabled physics.
*/
public static class DisabledPhysics {
/**
* Whether connections should be prevented in worlds with disabled physics.
*/
public static boolean preventConnections = true;
/**
* Whether fluid flow should be prevented in worlds with disabled physics.
*/
public static boolean preventFluidFlow = true;
/**
* Whether falling blocks should be prevented in worlds with disabled physics.
*/
public static boolean preventFallingBlocks = true;
}
/**
* Stores settings related to saving players from death.
*/
public static class SaveFromDeath {
/**
* Whether saving players from death should be enabled.
*/
public static boolean enabled = true;
/**
* Whether players should be teleported to the map spawn after being saved from death.
*/
public static boolean teleportToMapSpawn = true;
}
/**
* Stores build mode related settings.
*/
public static class BuildMode {
/**
* Whether items should be dropped when a player dies in build mode.
*/
public static boolean dropItems = true;
/**
* Whether players can move items in their inventory in build mode.
*/
public static boolean moveItems = true;
}
/**
* Stores builder-related settings.
*/
public static class Builder {
/**
* Whether WorldEdit usage should be blocked for non-builders.
*/
public static boolean blockWorldEditNonBuilder = true;
/**
* The {@link XMaterial} of the WorldEdit wand.
*/
public static XMaterial worldEditWand = XMaterial.WOODEN_AXE;
}
/**
* Stores navigator-related settings.
*/
public static class Navigator {
/**
* The {@link XMaterial} of the navigator item.
*/
public static XMaterial item = XMaterial.CLOCK;
/**
* Whether the navigator item should be given to players on join.
*/
public static boolean giveItemOnJoin = true;
}
}
/**
* Stores world-related configurations.
*/
public static class World {
/**
* Whether the weather should be locked in worlds.
*/
public static boolean lockWeather = true;
/**
* A regex string of invalid characters for world names.
*/
public static String invalidCharacters = "^\b$";
/**
* The delay in seconds for the /build import all command.
*/
public static int importAllDelay = 30;
/**
* A blacklist of worlds that cannot be deleted. This is used to prevent deletion of important worlds.
*/
public static Set<String> deletionBlacklist = Set.of("world", "world_nether", "worth_the_end");
;
/**
* Stores default world settings.
*/
public static class Default {
/**
* The default world border size.
*/
public static int worldBoarderSize = 6000000;
/**
* The default difficulty for new worlds.
*/
public static Difficulty difficulty = Difficulty.PEACEFUL;
/**
* Default game rules for new worlds.
*/
public static List<GameRuleEntry<?>> gameRules = List.of(
new GameRuleEntry<>(GameRule.DO_DAYLIGHT_CYCLE, false),
new GameRuleEntry<>(GameRule.DO_MOB_SPAWNING, false),
new GameRuleEntry<>(GameRule.DO_FIRE_TICK, false)
);
/**
* Stores permission-related settings for default worlds.
*/
public static class Permission {
/**
* The permission required to access public worlds by default.
*/
public static String publicPermission = "-";
/**
* The permission required to access private worlds by default. The `%world%` placeholder will be replaced with the world name.
*/
public static String privatePermission = "worlds.%world%";
}
/**
* Stores time-related settings for default worlds.
*/
public static class Time {
/**
* The time of sunrise in ticks.
*/
public static int sunrise = 0;
/**
* The time of noon in ticks.
*/
public static int noon = 6000;
/**
* The time of night in ticks.
*/
public static int night = 18000;
}
/**
* Stores various settings for default worlds.
*/
public static class Settings {
/**
* Whether physics should be enabled by default.
*/
public static boolean physics = true;
/**
* Whether explosions should be enabled by default.
*/
public static boolean explosions = true;
/**
* Whether mob AI should be enabled by default.
*/
public static boolean mobAi = true;
/**
* Whether block breaking should be enabled by default.
*/
public static boolean blockBreaking = true;
/**
* Whether block placement should be enabled by default.
*/
public static boolean blockPlacement = true;
/**
* Whether block interactions should be enabled by default.
*/
public static boolean blockInteractions = true;
/**
* Stores settings for builders being enabled by default.
*/
public static class BuildersEnabled {
/**
* Whether builders are enabled by default for public worlds.
*/
public static boolean publicBuilders = false;
/**
* Whether builders are enabled by default for private worlds.
*/
public static boolean privateBuilders = true;
}
}
}
/**
* Stores world limit settings.
*/
public static class Limits {
/**
* The maximum number of public worlds a player can have. -1 for unlimited.
*/
public static int publicWorlds = -1;
/**
* The maximum number of private worlds a player can have. -1 for unlimited.
*/
public static int privateWorlds = -1;
}
/**
* Stores world unload settings.
*/
public static class Unload {
/**
* Whether world unloading is enabled.
*/
public static boolean enabled = true;
/**
* The time until a world is unloaded after all players have left (HH:mm:ss).
*/
public static String timeUntilUnload = "01:00:00";
/**
* A set of worlds that should not be unloaded.
*/
public static Set<String> blacklistedWorlds = Set.of("world", "world_nether", "worth_the_end");
}
/**
* Stores backup-related settings.
*/
public static class Backup {
/**
* The maximum number of backups to keep per world.
*/
public static int maxBackupsPerWorld = 5;
/**
* The storage for backups.
*/
public static BackupStorage storage = createBackupStorage();
public static class AutoBackup {
/**
* Whether automatic backups are enabled.
*/
public static boolean enabled = true;
/**
* Whether only active worlds should be backed up.
*/
public static boolean onlyActiveWorlds;
/**
* The interval in seconds between automatic backups.
*/
public static int interval = 900;
}
}
}
/**
* Stores folder-related settings.
*/
public static class Folder {
/**
* Whether permissions should be overridden for folders.
*/
public static boolean overridePermissions = true;
/**
* Whether projects should be overridden for folders.
*/
public static boolean overrideProjects = false;
}
/**
* Loads the configuration values from the plugin's config.yml into the static fields.
*/
public static void load() {
final Logger logger = PLUGIN.getLogger();
// Messages
Messages.spawnTeleportMessage = CONFIG.getBoolean("messages.spawn-teleport-message", false);
Messages.joinQuitMessages = CONFIG.getBoolean("messages.join-quit-messages", true);
Messages.dateFormat = CONFIG.getString("messages.date-format", "dd/MM/yyyy");
// Settings
Settings.updateChecker = CONFIG.getBoolean("settings.update-checker", true);
Settings.scoreboard = CONFIG.getBoolean("settings.scoreboard", true);
// Settings - Archive
Archive.vanish = CONFIG.getBoolean("settings.archive.vanish", true);
Archive.changeGamemode = CONFIG.getBoolean("settings.archive.change-gamemode", true);
Archive.worldGameMode = parseGameMode(CONFIG.getString("settings.archive.world-gamemode"));
// Settings - Disabled physics
DisabledPhysics.preventConnections = CONFIG.getBoolean("settings.disabled-physics.prevent-connections", true);
DisabledPhysics.preventFluidFlow = CONFIG.getBoolean("settings.disabled-physics.prevent-fluid-flow", true);
DisabledPhysics.preventFallingBlocks = CONFIG.getBoolean("settings.disabled-physics.prevent-falling-blocks", true);
// Settings - Save from death
SaveFromDeath.enabled = CONFIG.getBoolean("settings.save-from-death.enabled", true);
SaveFromDeath.teleportToMapSpawn = CONFIG.getBoolean("settings.save-from-death.teleport-to-map-spawn", true);
// Settings - Build mode
BuildMode.dropItems = CONFIG.getBoolean("settings.build-mode.drop-items", true);
BuildMode.moveItems = CONFIG.getBoolean("settings.build-mode.move-items", true);
// Settings - Builder
Builder.blockWorldEditNonBuilder = CONFIG.getBoolean("settings.builder.block-worldedit-non-builder", true);
Builder.worldEditWand = parseWorldEditWand();
// Settings - Navigator
Navigator.item = XMaterial.valueOf(CONFIG.getString("settings.navigator.item", "CLOCK"));
Navigator.giveItemOnJoin = CONFIG.getBoolean("settings.navigator.give-item-on-join", true);
// World
World.lockWeather = CONFIG.getBoolean("world.lock-weather", true);
World.invalidCharacters = CONFIG.getString("world.invalid-characters", "^\b$");
World.importAllDelay = CONFIG.getInt("world.import-all.delay", 30);
World.deletionBlacklist = CONFIG.getStringList("world.deletion-blacklist").stream().map(String::toLowerCase).collect(Collectors.toSet());
// World - Default
Default.worldBoarderSize = CONFIG.getInt("world.default.worldborder.size", 6000000);
Default.difficulty = Difficulty.valueOf(CONFIG.getString("world.default.difficulty", "PEACEFUL").toUpperCase(Locale.ROOT));
Default.gameRules = CONFIG.getConfigurationSection("world.default.gamerules")
.getValues(true)
.entrySet()
.stream()
.map(entry -> {
String key = entry.getKey();
Object value = entry.getValue();
GameRule<?> rule = GameRule.getByName(key);
if (rule == null || value == null) {
logger.warning("Could not parse game rule '%s' with value '%s'".formatted(key, value));
return null;
}
return switch (value) {
case Boolean booleanValue -> {
if (rule.getType() != Boolean.class) {
PLUGIN.getLogger().warning("Game rule '%s' is not a boolean type, but a boolean value was provided".formatted(key));
yield null;
}
yield (GameRuleEntry<?>) new GameRuleEntry<>((GameRule<Boolean>) rule, booleanValue);
}
case Integer integerValue -> {
if (rule.getType() != Integer.class) {
logger.warning("Game rule '%s' is not an integer type, but an integer value was provided".formatted(key));
yield null;
}
yield (GameRuleEntry<?>) new GameRuleEntry<>((GameRule<Integer>) rule, integerValue);
}
default -> {
logger.warning("Invalid game rule value type. Must be of type Boolean or Integer. Found %s".formatted(value.getClass().getName()));
yield null;
}
};
})
.filter(Objects::nonNull)
.toList();
// World - Default - Permission
Permission.publicPermission = CONFIG.getString("world.default.permission.public", "-");
Permission.privatePermission = CONFIG.getString("world.default.permission.private", "-");
// World - Default - Time
Time.sunrise = CONFIG.getInt("world.default.time.sunrise", 0);
Time.noon = CONFIG.getInt("world.default.time.noon", 6000);
Time.night = CONFIG.getInt("world.default.time.night", 18000);
// World - Default - Settings
Default.Settings.physics = CONFIG.getBoolean("world.default.settings.physics", true);
Default.Settings.explosions = CONFIG.getBoolean("world.default.settings.explosions", true);
Default.Settings.mobAi = CONFIG.getBoolean("world.default.settings.mob-ai", true);
Default.Settings.blockBreaking = CONFIG.getBoolean("world.default.settings.block-breaking", true);
Default.Settings.blockPlacement = CONFIG.getBoolean("world.default.settings.block-placement", true);
Default.Settings.blockInteractions = CONFIG.getBoolean("world.default.settings.block-interactions", true);
// World - Default - Settings - Builders Enabled
BuildersEnabled.publicBuilders = CONFIG.getBoolean("world.default.settings.builders-enabled.public", false);
BuildersEnabled.privateBuilders = CONFIG.getBoolean("world.default.settings.builders-enabled.private", true);
// World - Limits
Limits.publicWorlds = CONFIG.getInt("world.default.settings.public-worlds", -1);
Limits.privateWorlds = CONFIG.getInt("world.default.settings.private-worlds", -1);
// World - Unload
Unload.enabled = CONFIG.getBoolean("world.unload.enabled", false);
Unload.timeUntilUnload = CONFIG.getString("world.unload.time-until-unload", "01:00:00");
Unload.blacklistedWorlds = new HashSet<>(CONFIG.getStringList("world.unload.blacklisted-worlds"));
// World - Backup
Backup.maxBackupsPerWorld = Math.min(CONFIG.getInt("world.backup.max-backups-per-world", 5), 18);
Backup.storage = createBackupStorage();
// World - Backup - AutoBackup
AutoBackup.enabled = CONFIG.getBoolean("world.backup.auto-backup.enabled", true);
AutoBackup.interval = CONFIG.getInt("world.backup.auto-backup.interval", 900);
AutoBackup.onlyActiveWorlds = CONFIG.getBoolean("world.backup.auto-backup.only-active-worlds", true);
// Folder
Folder.overridePermissions = CONFIG.getBoolean("folder.override-permissions", true);
Folder.overrideProjects = CONFIG.getBoolean("folder.override-projects", false);
}
/**
* Parsing the {@link GameMode} from a string. Defaulting to {@link GameMode#ADVENTURE} if the string is not a valid {@link GameMode}.
*
* @param gameModeName The name of the {@link GameMode} to parse.
* @return The parsed {@link GameMode}.
*/
private static GameMode parseGameMode(@Nullable String gameModeName) {
if (gameModeName == null) {
return GameMode.ADVENTURE;
}
return Arrays.stream(GameMode.values())
.filter(gameMode -> gameMode.name().equalsIgnoreCase(gameModeName))
.findAny()
.orElse(GameMode.ADVENTURE);
}
/**
* Parses the WorldEdit wand from the plugin's configuration.
*
* @return The parsed {@link XMaterial} for the WorldEdit wand.
*/
private static XMaterial parseWorldEditWand() {
File pluginDir = PLUGIN.getDataFolder().getParentFile();
File configFile = null;
File weConfig = new File(pluginDir + File.separator + "WorldEdit", "config.yml");
if (weConfig.exists()) {
configFile = weConfig;
}
File faweConfig = new File(pluginDir + File.separator + "FastAsyncWorldEdit", "worldedit-config.yml");
if (faweConfig.exists()) {
configFile = faweConfig;
}
XMaterial defaultWand = XMaterial.WOODEN_AXE;
if (configFile == null) {
return defaultWand;
}
YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile);
String wand = config.getString("wand-item");
if (wand == null) {
return defaultWand;
}
String namespace = "minecraft:";
if (wand.toLowerCase(Locale.ROOT).startsWith(namespace)) {
wand = wand.substring(namespace.length());
}
return XMaterial.matchXMaterial(wand).orElse(defaultWand);
}
@Contract("-> new")
private static BackupStorage createBackupStorage() {
String type = CONFIG.getString("world.backup.storage.type", "local").toLowerCase();
switch (type) {
case "s3" -> {
ConfigurationSection s3 = CONFIG.getConfigurationSection("world.backup.storage.s3");
return new S3BackupStorage(
PLUGIN,
s3.getString("url"),
s3.getString("access-key"),
s3.getString("secret-key"),
s3.getString("region"),
s3.getString("bucket"),
s3.getString("path")
);
}
case "sftp" -> {
ConfigurationSection sftp = CONFIG.getConfigurationSection("world.backup.storage.sftp");
return new SftpBackupStorage(
PLUGIN,
sftp.getString("host"),
sftp.getInt("port"),
sftp.getString("username"),
sftp.getString("password"),
sftp.getString("path")
);
}
default -> {
if (!type.equals("local")) {
PLUGIN.getLogger().warning("Unknown backup storage type '" + type + "', defaulting to local storage.");
}
return new LocalBackupStorage(PLUGIN);
}
}
}
}
@@ -0,0 +1,82 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.config.migration;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.config.Config;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;
import org.jspecify.annotations.NullMarked;
/**
* Manages the migration of configurations. It holds a registry of available migration steps and applies them sequentially.
*/
@NullMarked
public class ConfigMigrationManager {
public static final int LATEST_VERSION = 2;
private final BuildSystemPlugin plugin;
private final Map<Integer, Migration> migrations;
/**
* Constructs a new {@link ConfigMigrationManager} instance.
*
* @param plugin The BuildSystemPlugin instance
*/
public ConfigMigrationManager(BuildSystemPlugin plugin) {
this.plugin = plugin;
this.migrations = new HashMap<>();
registerMigration(1, new MigrationV1ToV2());
}
/**
* Registers a {@link Migration} step.
*
* @param fromVersion The version from which this migration applies. For example, {@link MigrationV1ToV2} is registered with {@code fromVersion = 1}
* @param migration The migration instance
*/
public void registerMigration(int fromVersion, Migration migration) {
this.migrations.put(fromVersion, migration);
}
/**
* Migrates the given configuration to the latest version.
*
* @throws IllegalStateException If a required migration step is missing.
*/
public void migrate() {
Logger logger = plugin.getLogger();
while (Config.getVersion() < LATEST_VERSION) {
int currentVersion = Config.getVersion();
Migration migration = migrations.get(currentVersion);
if (migration == null) {
throw new IllegalStateException("Missing migration from version " + currentVersion + " to " + (currentVersion + 1));
}
logger.info("Migrating from version " + currentVersion + " to " + (currentVersion + 1) + "...");
migration.migrate(Config.getConfig());
Config.setVersion(currentVersion + 1);
}
logger.info("Config is at the latest version: " + Config.getVersion());
}
}
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.config.migration;
import org.bukkit.configuration.file.FileConfiguration;
import org.jspecify.annotations.NullMarked;
/**
* Interface for a single configuration migration step. Each implementation should know how to migrate a configuration from a specific source version to the next version.
*/
@NullMarked
public interface Migration {
/**
* Applies the migration logic to the given configuration.
* <p>
* The implementation is responsible for modifying the config's internal data and potentially its version, though the manager will handle version incrementing.
*
* @param config The configuration to migrate
*/
void migrate(FileConfiguration config);
}
@@ -0,0 +1,53 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.config.migration;
import java.util.List;
import org.bukkit.configuration.file.FileConfiguration;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class MigrationV1ToV2 implements Migration {
@Override
public void migrate(FileConfiguration config) {
if (config.contains("settings.archive-vanish")) {
config.set("settings.archive.vanish", config.getBoolean("settings.archive-vanish"));
config.set("settings.archive-vanish", null);
}
if (config.contains("settings.archive-should-change-gamemode")) {
config.set("settings.archive.change-gamemode", config.getBoolean("settings.archive-should-change-gamemode"));
config.set("settings.archive-should-change-gamemode", null);
}
if (config.contains("settings.archive-world-game-mode")) {
config.set("settings.archive.world-gamemode", config.getString("settings.archive-world-game-mode"));
config.setComments("settings.archive.world-gamemode", List.of("Options: SURVIVAL, CREATIVE, ADVENTURE, SPECTATOR"));
config.set("settings.archive-world-game-mode", null);
}
if (config.contains("settings.teleport-after-creation")) {
config.set("settings.teleport-after-creation", null);
}
if (config.contains("world.void-block")) {
config.set("world.void-block", null);
}
}
}
@@ -0,0 +1,73 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.event;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.event.world.BuildWorldManipulationEvent;
import de.eintosti.buildsystem.storage.WorldStorageImpl;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.jspecify.annotations.NullMarked;
/**
* Manages the dispatching of custom events related to build world manipulations.
*/
@NullMarked
public class EventDispatcher {
private final WorldStorageImpl worldStorage;
/**
* Creates a new {@link EventDispatcher} instance.
*
* @param worldStorage The world storage used to retrieve {@link BuildWorld} information
*/
public EventDispatcher(WorldStorageImpl worldStorage) {
this.worldStorage = worldStorage;
}
/**
* <p>Dispatches a build world manipulation event if the player is in a build world
* and the parent event has not been canceled.</p>
*
* <p>This method checks if:</p>
* <ol>
* <li>The parent event is not yet canceled</li>
* <li>The player is in a valid build world</li>
* </ol>
* <p>
* If both conditions are met, it triggers a {@link BuildWorldManipulationEvent}
* to allow further processing of the player's action.
*
* @param player The player who performed the manipulation
* @param parentEvent The original event that triggered this potential manipulation
*/
public void dispatchManipulationEventIfPlayerInBuildWorld(Player player, Cancellable parentEvent) {
if (parentEvent.isCancelled()) {
return;
}
BuildWorld buildWorld = worldStorage.getBuildWorld(player.getWorld());
if (buildWorld == null) {
return;
}
Bukkit.getPluginManager().callEvent(new BuildWorldManipulationEvent(parentEvent, player, buildWorld));
}
}
@@ -0,0 +1,58 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.event.player;
import java.util.List;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import org.bukkit.event.player.PlayerEvent;
import org.jspecify.annotations.NullMarked;
/**
* Called when a player's inventory is cleared.
*/
@NullMarked
public class PlayerInventoryClearEvent extends PlayerEvent {
private static final HandlerList HANDLERS = new HandlerList();
private final List<Integer> navigatorSlots;
public PlayerInventoryClearEvent(Player player, List<Integer> navigatorSlots) {
super(player);
this.navigatorSlots = navigatorSlots;
}
public static HandlerList getHandlerList() {
return HANDLERS;
}
/**
* Gets all slots containing a navigator item in a player's inventory.
*
* @return A list of slot numbers containing navigator items
*/
public List<Integer> getNavigatorSlots() {
return navigatorSlots;
}
@Override
public HandlerList getHandlers() {
return HANDLERS;
}
}
@@ -0,0 +1,78 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.event.world;
import de.eintosti.buildsystem.api.event.world.BuildWorldEvent;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.listener.WorldManipulateListener;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.jspecify.annotations.NullMarked;
/**
* This event reduces duplicated code.
* <p>
* It will be called when:
* <ul>
* <li>Breaking Blocks</li>
* <li>Placing Blocks</li>
* <li>Other modification-related stuff</li>
* </ul>
* Cancelling this event will affect the parent-Event, which has caused the ManipulationEvent to fire.
* <p>Expect the manipulation event to be canceled at {@link org.bukkit.event.EventPriority#LOW} if the player is not allowed to interact with the world.</p>
*
* @see WorldManipulateListener
*/
@NullMarked
public class BuildWorldManipulationEvent extends BuildWorldEvent implements Cancellable {
private final Cancellable parentEvent;
private final Player player;
public BuildWorldManipulationEvent(Cancellable parentEvent, Player player, BuildWorld buildWorld) {
super(buildWorld);
this.parentEvent = parentEvent;
this.player = player;
}
@Override
public boolean isCancelled() {
return parentEvent.isCancelled();
}
@Override
public void setCancelled(boolean cancelled) {
parentEvent.setCancelled(cancelled);
}
/**
* @return the event which has caused the manipulation event to fire.
*/
public Cancellable getParentEvent() {
return parentEvent;
}
/**
* Returns the player manipulating the {@link BuildWorld}.
*
* @return The player manipulating the world
*/
public Player getPlayer() {
return player;
}
}
@@ -0,0 +1,67 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.expansion.luckperms;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.expansion.luckperms.calculators.BuildModeCalculator;
import de.eintosti.buildsystem.expansion.luckperms.calculators.RoleCalculator;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
import net.luckperms.api.LuckPerms;
import net.luckperms.api.context.ContextCalculator;
import net.luckperms.api.context.ContextManager;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class LuckPermsExpansion {
private final BuildSystemPlugin plugin;
private final ContextManager contextManager;
private final List<ContextCalculator<Player>> registeredCalculators;
public LuckPermsExpansion(BuildSystemPlugin plugin) {
LuckPerms luckPerms = plugin.getServer().getServicesManager().load(LuckPerms.class);
if (luckPerms == null) {
throw new IllegalStateException("LuckPerms API not loaded.");
}
this.plugin = plugin;
this.contextManager = luckPerms.getContextManager();
this.registeredCalculators = new ArrayList<>();
}
public void registerAll() {
register("build-mode", () -> new BuildModeCalculator(plugin));
register("role", () -> new RoleCalculator(plugin));
plugin.getLogger().info("LuckPerms expansion initialized");
}
private void register(String option, Supplier<ContextCalculator<Player>> calculatorSupplier) {
plugin.getLogger().info("Registering '" + option + "' calculator");
ContextCalculator<Player> calculator = calculatorSupplier.get();
this.contextManager.registerCalculator(calculator);
this.registeredCalculators.add(calculator);
}
public void unregisterAll() {
this.registeredCalculators.forEach(this.contextManager::unregisterCalculator);
this.registeredCalculators.clear();
}
}
@@ -0,0 +1,53 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.expansion.luckperms.calculators;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.player.PlayerServiceImpl;
import net.luckperms.api.context.ContextCalculator;
import net.luckperms.api.context.ContextConsumer;
import net.luckperms.api.context.ContextSet;
import net.luckperms.api.context.ImmutableContextSet;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class BuildModeCalculator implements ContextCalculator<Player> {
private static final String KEY = "buildsystem:build-mode";
private final PlayerServiceImpl playerManager;
public BuildModeCalculator(BuildSystemPlugin plugin) {
this.playerManager = plugin.getPlayerService();
}
@Override
public void calculate(Player player, ContextConsumer contextConsumer) {
boolean isInBuildMode = playerManager.isInBuildMode(player);
contextConsumer.accept(KEY, String.valueOf(isInBuildMode));
}
@Override
public ContextSet estimatePotentialContexts() {
return ImmutableContextSet.builder()
.add(KEY, "true")
.add(KEY, "false")
.build();
}
}
@@ -0,0 +1,98 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.expansion.luckperms.calculators;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.api.world.builder.Builders;
import de.eintosti.buildsystem.storage.WorldStorageImpl;
import java.util.Locale;
import net.luckperms.api.context.ContextCalculator;
import net.luckperms.api.context.ContextConsumer;
import net.luckperms.api.context.ContextSet;
import net.luckperms.api.context.ImmutableContextSet;
import org.bukkit.entity.Player;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public class RoleCalculator implements ContextCalculator<Player> {
private static final String KEY = "buildsystem:role";
private final WorldStorageImpl worldStorage;
public RoleCalculator(BuildSystemPlugin plugin) {
this.worldStorage = plugin.getWorldService().getWorldStorage();
}
@Override
public void calculate(@NonNull Player player, @NonNull ContextConsumer contextConsumer) {
BuildWorld buildWorld = worldStorage.getBuildWorld(player.getWorld());
contextConsumer.accept(KEY, Role.matchRole(player, buildWorld).toString());
}
@Override
public ContextSet estimatePotentialContexts() {
ImmutableContextSet.Builder builder = ImmutableContextSet.builder();
for (Role role : Role.values()) {
builder.add(KEY, role.toString());
}
return builder.build();
}
@NullMarked
private enum Role {
/**
* The creator of a {@link BuildWorld}.
*/
CREATOR,
/**
* A player which has been added to the list of trusted players and is therefore allowed to build in a {@link BuildWorld}.
*/
BUILDER,
/**
* A player which is neither the {@link #CREATOR} nor a {@link #BUILDER} in a {@link BuildWorld}.
*/
GUEST;
public static Role matchRole(Player player, @Nullable BuildWorld buildWorld) {
if (buildWorld == null) {
return GUEST;
}
Builders builders = buildWorld.getBuilders();
if (builders.isCreator(player)) {
return CREATOR;
} else if (builders.isBuilder(player.getUniqueId())) {
return BUILDER;
} else {
return GUEST;
}
}
@Override
public String toString() {
return this.name().toLowerCase(Locale.ROOT);
}
}
}
@@ -0,0 +1,209 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.expansion.placeholderapi;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.player.settings.Settings;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.api.world.builder.Builders;
import de.eintosti.buildsystem.api.world.data.WorldData;
import de.eintosti.buildsystem.player.settings.SettingsManager;
import de.eintosti.buildsystem.storage.WorldStorageImpl;
import java.util.Locale;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public class PlaceholderApiExpansion extends PlaceholderExpansion {
private static final String SETTINGS_KEY = "settings";
private final BuildSystemPlugin plugin;
private final SettingsManager settingsManager;
private final WorldStorageImpl worldStorage;
public PlaceholderApiExpansion(BuildSystemPlugin plugin) {
this.plugin = plugin;
this.settingsManager = plugin.getSettingsManager();
this.worldStorage = plugin.getWorldService().getWorldStorage();
plugin.getLogger().info("PlaceholderAPI expansion initialized");
}
/**
* Because this is an internal class, you must override this method to let PlaceholderAPI know to not unregister your expansion class when PlaceholderAPI is reloaded
*
* @return true to persist through reloads
*/
@Override
public boolean persist() {
return true;
}
/**
* Because this is an internal class, this check is not needed, and we can simply return {@code true}
*
* @return Always true since it's an internal class.
*/
@Override
public boolean canRegister() {
return true;
}
/**
* The name of the person who created this expansion should go here. For convenience do we return the author from the plugin.yml
*
* @return The name of the author as a String.
*/
@Override
public String getAuthor() {
return plugin.getDescription().getAuthors().toString();
}
/**
* The placeholder identifier should go here. This is what tells PlaceholderAPI to call our onRequest method to get a value if a placeholder starts with our identifier. This
* must be unique and cannot contain % or _
*
* @return The identifier in {@code %<identifier>_<value>%} as String.
*/
@Override
public String getIdentifier() {
return "buildsystem";
}
/**
* This is the version of the expansion. You don't have to use numbers since it is set as a String.
* <p>
* For convenience do we return the version from the plugin.yml
*
* @return The version as a string.
*/
@Override
public String getVersion() {
return plugin.getDescription().getVersion();
}
/**
* This is the method called when a placeholder with our identifier is found and needs a value. We specify the value identifier in this method.
*
* @param player The player for which the placeholder is requested.
* @param identifier The identifier of the placeholder
* @return The value of the placeholder as a string, or {@code null} if the identifier is not recognized.
*/
@Override
@Nullable
public String onPlaceholderRequest(@Nullable Player player, String identifier) {
if (player == null) {
return "";
}
if (identifier.matches(".*_.*") && identifier.split("_")[0].equalsIgnoreCase(SETTINGS_KEY)) {
return parseSettingsPlaceholder(player, identifier);
} else {
return parseBuildWorldPlaceholder(player, identifier);
}
}
/**
* This is the method called when a placeholder with the identifier {@code %buildsystem_settings_<setting>%} is found.
*
* @param player The player for which the placeholder is requested
* @param identifier The identifier
* @return The requested setting as a string, or {@code null} if the setting is not recognized
*/
@Nullable
private String parseSettingsPlaceholder(Player player, String identifier) {
Settings settings = settingsManager.getSettings(player);
String settingIdentifier = identifier.split("_")[1];
return switch (settingIdentifier.toLowerCase(Locale.ROOT)) {
case "navigatortype" -> settings.getNavigatorType().toString();
case "glasscolor" -> settings.getDesignColor().toString();
case "worldsort" -> settings.getWorldDisplay().getWorldSort().toString();
case "clearinventory" -> String.valueOf(settings.isClearInventory());
case "disableinteract" -> String.valueOf(settings.isDisableInteract());
case "hideplayers" -> String.valueOf(settings.isHidePlayers());
case "instantplacesigns" -> String.valueOf(settings.isInstantPlaceSigns());
case "keepnavigator" -> String.valueOf(settings.isKeepNavigator());
case "nightvision" -> String.valueOf(settings.isNightVision());
case "noclip" -> String.valueOf(settings.isNoClip());
case "placeplants" -> String.valueOf(settings.isPlacePlants());
case "scoreboard" -> String.valueOf(settings.isScoreboard());
case "slabbreaking" -> String.valueOf(settings.isSlabBreaking());
case "spawnteleport" -> String.valueOf(settings.isSpawnTeleport());
case "opentrapdoors" -> String.valueOf(settings.isOpenTrapDoors());
default -> null;
};
}
/**
* This is the method called when a placeholder with the identifier needed for {@link PlaceholderApiExpansion#parseSettingsPlaceholder(Player, String)} is not found.
* <p>
* The default layout for a world placeholder is {@code %buildsystem_<value>%}. If a world is not specified by using the format {@code %buildsystem_<value>_<world>%} then the
* world the player is currently in will be used.
*
* @param player The player for which the placeholder is requested
* @param identifier The identifier
* @return The requested value as a string, or {@code null} if the identifier is not recognized or the world does not exist
*/
@Nullable
private String parseBuildWorldPlaceholder(Player player, String identifier) {
String worldName = player.getWorld().getName();
if (identifier.matches(".*_.*")) {
String[] splitString = identifier.split("_");
worldName = splitString[1];
identifier = splitString[0];
}
BuildWorld buildWorld = worldStorage.getBuildWorld(worldName);
if (buildWorld == null) {
return "-";
}
Builders builders = buildWorld.getBuilders();
WorldData worldData = buildWorld.getData();
return switch (identifier.toLowerCase(Locale.ROOT)) {
case "blockbreaking" -> String.valueOf(worldData.blockBreaking().get());
case "blockplacement" -> String.valueOf(worldData.blockPlacement().get());
case "builders" -> builders.asPlaceholder(player);
case "buildersenabled" -> String.valueOf(worldData.buildersEnabled().get());
case "creation" -> Messages.formatDate(buildWorld.getCreation());
case "creator" -> builders.hasCreator() ? builders.getCreator().getName() : "-";
case "creatorid" -> builders.hasCreator() ? String.valueOf(builders.getCreator().getUniqueId()) : "-";
case "explosions" -> String.valueOf(worldData.explosions().get());
case "lastedited" -> Messages.formatDate(worldData.lastEdited().get());
case "lastloaded" -> Messages.formatDate(worldData.lastLoaded().get());
case "lastunloaded" -> Messages.formatDate(worldData.lastUnloaded().get());
case "loaded" -> String.valueOf(buildWorld.isLoaded());
case "material" -> worldData.material().get().name();
case "mobai" -> String.valueOf(worldData.mobAi().get());
case "permission" -> worldData.permission().get();
case "private" -> String.valueOf(worldData.privateWorld().get());
case "project" -> worldData.project().get();
case "physics" -> String.valueOf(worldData.physics().get());
case "spawn" -> worldData.customSpawn().get();
case "status" -> Messages.getString(Messages.getMessageKey(worldData.status().get()), player);
case "time" -> buildWorld.getWorldTime();
case "type" -> Messages.getString(Messages.getMessageKey(buildWorld.getType()), player);
case "world" -> buildWorld.getName();
default -> null;
};
}
}
@@ -0,0 +1,46 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.listener;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.util.color.ColorAPI;
import org.bukkit.ChatColor;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class AsyncPlayerChatListener implements Listener {
public AsyncPlayerChatListener(BuildSystemPlugin plugin) {
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@EventHandler
public void onPlayerChat(AsyncPlayerChatEvent event) {
Player player = event.getPlayer();
if (!player.hasPermission("buildsystem.color.chat")) {
return;
}
String coloredMessage = ChatColor.translateAlternateColorCodes('&', ColorAPI.process(event.getMessage()));
event.setMessage(coloredMessage);
}
}
@@ -0,0 +1,76 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.listener;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.api.player.BuildPlayer;
import de.eintosti.buildsystem.api.player.LogoutLocation;
import de.eintosti.buildsystem.api.player.settings.Settings;
import de.eintosti.buildsystem.api.storage.PlayerStorage;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.storage.WorldStorageImpl;
import de.eintosti.buildsystem.world.SpawnManager;
import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class AsyncPlayerPreLoginListener implements Listener {
private final BuildSystemPlugin plugin;
private final PlayerStorage playerStorage;
private final SpawnManager spawnManager;
private final WorldStorageImpl worldStorage;
public AsyncPlayerPreLoginListener(BuildSystemPlugin plugin) {
this.plugin = plugin;
this.playerStorage = plugin.getPlayerService().getPlayerStorage();
this.spawnManager = plugin.getSpawnManager();
this.worldStorage = plugin.getWorldService().getWorldStorage();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@EventHandler
public void onAsyncPlayerPreLogin(AsyncPlayerPreLoginEvent event) {
UUID uuid = event.getUniqueId();
BuildPlayer buildPlayer = playerStorage.getBuildPlayer(uuid);
if (buildPlayer == null) {
return;
}
Settings settings = buildPlayer.getSettings();
if (settings.isSpawnTeleport() && spawnManager.spawnExists()) {
return;
}
LogoutLocation logoutLocation = buildPlayer.getLogoutLocation();
if (logoutLocation == null) {
return;
}
BuildWorld buildWorld = worldStorage.getBuildWorld(logoutLocation.worldName());
if (buildWorld == null) {
buildPlayer.setLogoutLocation(null);
} else {
Bukkit.getScheduler().runTask(plugin, () -> buildWorld.getLoader().load());
}
}
}
@@ -0,0 +1,233 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.listener;
import com.cryptomorin.xseries.XMaterial;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.config.Config.Settings.DisabledPhysics;
import de.eintosti.buildsystem.storage.WorldStorageImpl;
import de.eintosti.buildsystem.util.DirectionUtil;
import java.util.List;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.type.Fence;
import org.bukkit.block.data.type.Gate;
import org.bukkit.block.data.type.GlassPane;
import org.bukkit.block.data.type.Stairs;
import org.bukkit.block.data.type.Wall;
import org.bukkit.entity.EntityType;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockExplodeEvent;
import org.bukkit.event.block.BlockFadeEvent;
import org.bukkit.event.block.BlockFormEvent;
import org.bukkit.event.block.BlockFromToEvent;
import org.bukkit.event.block.BlockGrowEvent;
import org.bukkit.event.block.BlockPhysicsEvent;
import org.bukkit.event.block.BlockRedstoneEvent;
import org.bukkit.event.block.BlockSpreadEvent;
import org.bukkit.event.block.LeavesDecayEvent;
import org.bukkit.event.entity.EntityChangeBlockEvent;
import org.bukkit.event.entity.EntityExplodeEvent;
import org.bukkit.metadata.MetadataValue;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class BlockPhysicsListener implements Listener {
private final WorldStorageImpl worldStorage;
public BlockPhysicsListener(BuildSystemPlugin plugin) {
this.worldStorage = plugin.getWorldService().getWorldStorage();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@EventHandler
public void onBlockPhysics(BlockPhysicsEvent event) {
Block block = event.getBlock();
BuildWorld buildWorld = worldStorage.getBuildWorld(block.getWorld());
if (buildWorld == null || buildWorld.getData().physics().get()) {
return;
}
if (!DisabledPhysics.preventConnections) {
boolean canConnect = switch (block.getBlockData()) {
case Fence fence -> true;
case Gate gate -> true;
case GlassPane glassPane -> true;
case Stairs stairs -> true;
case Wall wall -> true;
default -> false;
};
if (canConnect) {
event.setCancelled(false);
return;
}
}
switch (XMaterial.matchXMaterial(block.getType())) {
case REDSTONE_BLOCK -> {
for (BlockFace blockFace : DirectionUtil.BLOCK_SIDES) {
if (isCustomRedstoneLamp(block.getRelative(blockFace))) {
event.setCancelled(false);
return;
}
}
}
case REDSTONE_LAMP -> {
for (BlockFace blockFace : DirectionUtil.BLOCK_SIDES) {
if (block.getRelative(blockFace).getType() == XMaterial.REDSTONE_BLOCK.get()) {
event.setCancelled(false);
return;
}
}
}
}
event.setCancelled(true);
}
@EventHandler
public void onLeavesDecay(LeavesDecayEvent event) {
BuildWorld buildWorld = worldStorage.getBuildWorld(event.getBlock().getWorld());
if (buildWorld == null || buildWorld.getData().physics().get()) {
return;
}
event.setCancelled(true);
}
@EventHandler
public void onBlockFade(BlockFadeEvent event) {
BuildWorld buildWorld = worldStorage.getBuildWorld(event.getBlock().getWorld());
if (buildWorld == null || buildWorld.getData().physics().get()) {
return;
}
event.setCancelled(true);
}
@EventHandler
public void onBlockForm(BlockFormEvent event) {
BuildWorld buildWorld = worldStorage.getBuildWorld(event.getBlock().getWorld());
if (buildWorld == null || buildWorld.getData().physics().get()) {
return;
}
event.setCancelled(true);
}
@EventHandler
public void onBlockFromTo(BlockFromToEvent event) {
BuildWorld buildWorld = worldStorage.getBuildWorld(event.getBlock().getWorld());
if (buildWorld == null || buildWorld.getData().physics().get()) {
return;
}
if (event.getBlock().isLiquid() && !DisabledPhysics.preventFluidFlow) {
event.setCancelled(false);
return;
}
event.setCancelled(true);
}
@EventHandler
public void onBlockGrow(BlockGrowEvent event) {
BuildWorld buildWorld = worldStorage.getBuildWorld(event.getBlock().getWorld());
if (buildWorld == null || buildWorld.getData().physics().get()) {
return;
}
event.setCancelled(true);
}
@EventHandler
public void onBlockSpread(BlockSpreadEvent event) {
BuildWorld buildWorld = worldStorage.getBuildWorld(event.getBlock().getWorld());
if (buildWorld == null || buildWorld.getData().physics().get()) {
return;
}
event.setCancelled(true);
}
@EventHandler
public void onEntityChangeBlock(EntityChangeBlockEvent event) {
BuildWorld buildWorld = worldStorage.getBuildWorld(event.getBlock().getWorld());
if (buildWorld == null || buildWorld.getData().physics().get()) {
return;
}
if (event.getEntityType() == EntityType.FALLING_BLOCK && DisabledPhysics.preventFallingBlocks) {
event.setCancelled(true);
event.getBlock().getState().update(false, false);
}
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onBlockRedstone(BlockRedstoneEvent event) {
Block block = event.getBlock();
if (isCustomRedstoneLamp(block)) {
event.setNewCurrent(15);
}
XMaterial xMaterial = XMaterial.matchXMaterial(block.getType());
if (xMaterial != XMaterial.REDSTONE_BLOCK) {
return;
}
for (BlockFace blockFace : DirectionUtil.BLOCK_SIDES) {
if (isCustomRedstoneLamp(block.getRelative(blockFace))) {
event.setNewCurrent(15);
}
}
}
@EventHandler
public void onBlockExplode(BlockExplodeEvent event) {
BuildWorld buildWorld = worldStorage.getBuildWorld(event.getBlock().getWorld());
if (buildWorld == null || buildWorld.getData().physics().get()) {
return;
}
event.setCancelled(true);
}
@EventHandler
public void onEntityExplode(EntityExplodeEvent event) {
World world = event.getLocation().getWorld();
if (world == null) {
return;
}
BuildWorld buildWorld = worldStorage.getBuildWorld(world);
if (buildWorld == null || buildWorld.getData().physics().get()) {
return;
}
event.setCancelled(true);
}
private boolean isCustomRedstoneLamp(Block block) {
List<MetadataValue> metadataValues = block.getMetadata("CustomRedstoneLamp");
for (MetadataValue value : metadataValues) {
if (value.asBoolean()) {
return true;
}
}
return block.getType().name().equals("REDSTONE_LAMP_ON");
}
}
@@ -0,0 +1,81 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.listener;
import com.cryptomorin.xseries.inventory.XInventoryView;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.config.Config.Settings.BuildMode;
import de.eintosti.buildsystem.player.PlayerServiceImpl;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.event.player.PlayerDropItemEvent;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class BuildModePreventationListener implements Listener {
private final PlayerServiceImpl playerManager;
public BuildModePreventationListener(BuildSystemPlugin plugin) {
this.playerManager = plugin.getPlayerService();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
/**
* Stops a player from dropping items when in build-mode.
*
* @param event The drop item event
*/
@EventHandler
public void onPlayerDropItem(PlayerDropItemEvent event) {
if (BuildMode.dropItems) {
return;
}
if (playerManager.isInBuildMode(event.getPlayer())) {
event.getItemDrop().remove();
}
}
/**
* Stops a player from moving items into another inventory when in build-mode.
*
* @param event The inventory click event
*/
@EventHandler
public void onInventoryClick(InventoryClickEvent event) {
if (BuildMode.moveItems) {
return;
}
if (!(event.getWhoClicked() instanceof Player player)) {
return;
}
if (!playerManager.isInBuildMode(player)) {
return;
}
if (XInventoryView.of(event.getView()).getTopInventory().getType() != InventoryType.CRAFTING) {
event.setCancelled(true);
}
}
}
@@ -0,0 +1,61 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.listener;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.storage.WorldStorageImpl;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class BuildWorldResetUnloadListener implements Listener {
private final WorldStorageImpl worldStorage;
public BuildWorldResetUnloadListener(BuildSystemPlugin plugin) {
this.worldStorage = plugin.getWorldService().getWorldStorage();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@EventHandler
public void onPlayerChangedWorld(PlayerChangedWorldEvent event) {
resetUnloadTask(event.getFrom());
}
@EventHandler
public void onPlayerQuit(PlayerQuitEvent event) {
resetUnloadTask(event.getPlayer().getWorld());
}
/**
* Resets the unload task for the given world.
*
* @param world The world for which the unload task should be reset
*/
private void resetUnloadTask(World world) {
BuildWorld buildWorld = worldStorage.getBuildWorld(world.getName());
if (buildWorld != null) {
buildWorld.getUnloader().resetUnloadTask();
}
}
}
@@ -0,0 +1,119 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.listener;
import com.sk89q.worldedit.EditSession;
import com.sk89q.worldedit.WorldEdit;
import com.sk89q.worldedit.event.extent.EditSessionEvent;
import com.sk89q.worldedit.extension.platform.Actor;
import com.sk89q.worldedit.extent.NullExtent;
import com.sk89q.worldedit.util.eventbus.Subscribe;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.api.world.builder.Builders;
import de.eintosti.buildsystem.api.world.data.BuildWorldStatus;
import de.eintosti.buildsystem.storage.WorldStorageImpl;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class EditSessionListener implements Listener {
private final WorldStorageImpl worldStorage;
public EditSessionListener(BuildSystemPlugin plugin) {
this.worldStorage = plugin.getWorldService().getWorldStorage();
WorldEdit.getInstance().getEventBus().register(this);
}
@Subscribe
public void onEditSession(EditSessionEvent event) {
Actor actor = event.getActor();
if (actor == null || !actor.isPlayer()) {
return;
}
Player player = Bukkit.getPlayer(actor.getName());
if (player == null) {
return;
}
BuildWorld buildWorld = worldStorage.getBuildWorld(player.getWorld());
if (buildWorld == null) {
return;
}
if (event.getStage() != EditSession.Stage.BEFORE_CHANGE) {
return;
}
boolean hasAdminPermission = buildWorld.getPermissions().hasAdminPermission(player);
if (hasAdminPermission || (!disableArchivedWorlds(buildWorld, player, event) && !disableNonBuilders(buildWorld, player, event))) {
buildWorld.getData().lastEdited().set(System.currentTimeMillis());
}
}
/**
* Disable the editing of archived worlds for players without the necessary permission.
*
* @param buildWorld The build world
* @param player The player
* @param event The EditSessionEvent
* @return {@code true} if the edit was canceled, otherwise {@code false}
*/
private boolean disableArchivedWorlds(BuildWorld buildWorld, Player player, EditSessionEvent event) {
if (buildWorld.getPermissions().canBypassBuildRestriction(player) || player.hasPermission("buildsystem.bypass.archive")) {
return false;
}
if (buildWorld.getData().status().get() == BuildWorldStatus.ARCHIVE) {
event.setExtent(new NullExtent());
return true;
}
return false;
}
/**
* Disable the editing of worlds in which a player is not a builder.
*
* @param buildWorld The build world
* @param player The player
* @param event The EditSessionEvent
* @return {@code true} if the edit was canceled, otherwise {@code false}
*/
private boolean disableNonBuilders(BuildWorld buildWorld, Player player, EditSessionEvent event) {
if (buildWorld.getPermissions().canBypassBuildRestriction(player) || player.hasPermission("buildsystem.bypass.builders")) {
return false;
}
Builders builders = buildWorld.getBuilders();
if (builders.isCreator(player)) {
return false;
}
if (buildWorld.getData().buildersEnabled().get() && !builders.isBuilder(player)) {
event.setExtent(new NullExtent());
return true;
}
return false;
}
}
@@ -0,0 +1,77 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.listener;
import com.cryptomorin.xseries.XSound;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.config.Config.Settings.SaveFromDeath;
import io.papermc.lib.PaperLib;
import org.bukkit.Location;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageEvent;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class EntityDamageListener implements Listener {
private final BuildSystemPlugin plugin;
public EntityDamageListener(BuildSystemPlugin plugin) {
this.plugin = plugin;
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@EventHandler
public void onEntityDamage(EntityDamageEvent event) {
if (event.getEntityType() != EntityType.PLAYER) {
return;
}
// Teleport player up if void damage is taken
if (event.getCause() != EntityDamageEvent.DamageCause.VOID || !SaveFromDeath.enabled) {
return;
}
Player player = (Player) event.getEntity();
Location teleportLoc = player.getLocation().clone().add(0, 200, 0);
if (SaveFromDeath.teleportToMapSpawn) {
BuildWorld buildWorld = plugin.getWorldService().getWorldStorage().getBuildWorld(player.getWorld());
if (buildWorld != null) {
Location spawn = buildWorld.getData().getCustomSpawnLocation();
if (spawn != null) {
teleportLoc = spawn;
}
}
}
event.setCancelled(true);
player.setFallDistance(0);
PaperLib.teleportAsync(player, teleportLoc)
.whenComplete((completed, throwable) -> {
if (!completed) {
return;
}
XSound.ENTITY_ZOMBIE_INFECT.play(player);
});
}
}
@@ -0,0 +1,56 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.listener;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.api.storage.WorldStorage;
import de.eintosti.buildsystem.api.world.BuildWorld;
import org.bukkit.World;
import org.bukkit.entity.LivingEntity;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntitySpawnEvent;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class EntitySpawnListener implements Listener {
private final WorldStorage worldStorage;
public EntitySpawnListener(BuildSystemPlugin plugin) {
this.worldStorage = plugin.getWorldService().getWorldStorage();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@EventHandler
public void onEntitySpawn(EntitySpawnEvent event) {
World bukkitWorld = event.getLocation().getWorld();
if (bukkitWorld == null) {
return;
}
BuildWorld buildWorld = worldStorage.getBuildWorld(bukkitWorld.getName());
if (buildWorld == null || buildWorld.getData().mobAi().get()) {
return;
}
if (event.getEntity() instanceof LivingEntity livingEntity) {
livingEntity.setAI(false);
}
}
}
@@ -0,0 +1,51 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.listener;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.api.world.data.BuildWorldStatus;
import de.eintosti.buildsystem.storage.WorldStorageImpl;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.FoodLevelChangeEvent;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class FoodLevelChangeListener implements Listener {
private final WorldStorageImpl worldStorage;
public FoodLevelChangeListener(BuildSystemPlugin plugin) {
this.worldStorage = plugin.getWorldService().getWorldStorage();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@EventHandler
public void onFoodLevelChange(FoodLevelChangeEvent event) {
if (!(event.getEntity() instanceof Player player)) {
return;
}
BuildWorld buildWorld = worldStorage.getBuildWorld(player.getWorld());
if (buildWorld != null && buildWorld.getData().status().get() == BuildWorldStatus.ARCHIVE) {
event.setCancelled(true);
}
}
}
@@ -0,0 +1,61 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.listener;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.event.player.PlayerInventoryClearEvent;
import de.eintosti.buildsystem.util.inventory.InventoryUtils;
import java.util.List;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.event.inventory.InventoryAction;
import org.bukkit.event.inventory.InventoryCreativeEvent;
import org.bukkit.event.inventory.InventoryType;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class InventoryCreativeListener implements Listener {
private final BuildSystemPlugin plugin;
public InventoryCreativeListener(BuildSystemPlugin plugin) {
this.plugin = plugin;
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@EventHandler
public void onClearInventory(InventoryCreativeEvent event) {
if (event.getClick() != ClickType.CREATIVE
|| event.getSlotType() != InventoryType.SlotType.QUICKBAR
|| event.getAction() != InventoryAction.PLACE_ALL
) {
return;
}
Player player = (Player) event.getWhoClicked();
List<Integer> navigatorSlots = InventoryUtils.getNavigatorSlots(player);
Bukkit.getScheduler().runTaskLater(plugin, () -> {
PlayerInventoryClearEvent playerInventoryClearEvent = new PlayerInventoryClearEvent(player, navigatorSlots);
Bukkit.getServer().getPluginManager().callEvent(playerInventoryClearEvent);
}, 2L);
}
}
@@ -0,0 +1,53 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.listener;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.util.inventory.InventoryManager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryOpenEvent;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class InventoryListener implements Listener {
private final InventoryManager inventoryManager;
public InventoryListener(BuildSystemPlugin plugin) {
this.inventoryManager = plugin.getInventoryManager();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@EventHandler
public void onInventoryOpen(InventoryOpenEvent event) {
this.inventoryManager.handleOpen(event);
}
@EventHandler
public void onInventoryClick(InventoryClickEvent event) {
this.inventoryManager.handleClick(event);
}
@EventHandler
public void onInventoryClose(InventoryCloseEvent event) {
this.inventoryManager.handleClose(event);
}
}
@@ -0,0 +1,273 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.listener;
import com.cryptomorin.xseries.XMaterial;
import com.cryptomorin.xseries.XPotion;
import com.cryptomorin.xseries.XSound;
import com.cryptomorin.xseries.inventory.XInventoryView;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.player.CachedValues;
import de.eintosti.buildsystem.api.player.settings.Settings;
import de.eintosti.buildsystem.api.storage.WorldStorage;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.api.world.data.BuildWorldStatus;
import de.eintosti.buildsystem.api.world.display.NavigatorCategory;
import de.eintosti.buildsystem.api.world.navigator.settings.NavigatorType;
import de.eintosti.buildsystem.config.Config.Settings.Navigator;
import de.eintosti.buildsystem.player.PlayerServiceImpl;
import de.eintosti.buildsystem.player.settings.SettingsManager;
import de.eintosti.buildsystem.util.inventory.InventoryUtils;
import de.eintosti.buildsystem.world.navigator.ArmorStandManager;
import de.eintosti.buildsystem.world.navigator.inventory.ArchivedWorldsInventory;
import de.eintosti.buildsystem.world.navigator.inventory.DisplayablesInventory;
import de.eintosti.buildsystem.world.navigator.inventory.NavigatorInventory;
import de.eintosti.buildsystem.world.navigator.inventory.PrivateWorldsInventory;
import de.eintosti.buildsystem.world.navigator.inventory.PublicWorldsInventory;
import java.util.Objects;
import java.util.UUID;
import org.bukkit.Material;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.event.player.PlayerArmorStandManipulateEvent;
import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.event.player.PlayerInteractAtEntityEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.potion.PotionEffect;
import org.bukkit.util.Vector;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public class NavigatorListener implements Listener {
private static final double MAX_HEIGHT = 2.074631929397583;
private static final double MIN_HEIGHT = 1.4409877061843872;
private final BuildSystemPlugin plugin;
private final ArmorStandManager armorStandManager;
private final PlayerServiceImpl playerService;
private final SettingsManager settingsManager;
private final WorldStorage worldStorage;
public NavigatorListener(BuildSystemPlugin plugin) {
this.plugin = plugin;
this.armorStandManager = plugin.getArmorStandManager();
this.playerService = plugin.getPlayerService();
this.settingsManager = plugin.getSettingsManager();
this.worldStorage = plugin.getWorldService().getWorldStorage();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
/**
* Called whenever the player interacts with the navigator item.
*
* @param event The event which calls this method
*/
@EventHandler
public void manageNavigatorItemInteraction(PlayerInteractEvent event) {
ItemStack itemStack = event.getItem();
if (itemStack == null || itemStack.getType() == Material.AIR) {
return;
}
Player player = event.getPlayer();
if (XInventoryView.of(player.getOpenInventory()).getTopInventory().getType() != InventoryType.CRAFTING) {
return;
}
if (InventoryUtils.isNavigator(itemStack)) {
event.setCancelled(true);
if (!player.hasPermission("buildsystem.navigator.item")) {
Messages.sendPermissionError(player);
return;
}
openNavigator(player);
} else if (isCloseNavigatorItem(player, itemStack)) {
event.setCancelled(true);
playerService.closeNewNavigator(player);
}
}
private void openNavigator(Player player) {
Settings settings = settingsManager.getSettings(player);
switch (settings.getNavigatorType()) {
case OLD -> {
new NavigatorInventory(plugin).openInventory(player);
XSound.BLOCK_CHEST_OPEN.play(player);
}
case NEW -> {
if (playerService.getOpenNavigator().contains(player)) {
Messages.sendMessage(player, "worlds_navigator_open");
return;
}
summonNewNavigator(player);
String findItemName = Messages.getString("navigator_item", player);
ItemStack replaceItem = InventoryUtils.createItem(XMaterial.BARRIER, Messages.getString("barrier_item", player));
InventoryUtils.replaceItem(player, findItemName, Navigator.item, replaceItem);
}
}
}
private void summonNewNavigator(Player player) {
CachedValues cachedValues = playerService.getPlayerStorage().getBuildPlayer(player).getCachedValues();
cachedValues.saveWalkSpeed(player.getWalkSpeed());
cachedValues.saveFlySpeed(player.getFlySpeed());
// Cancel movement and disable flying
player.setSprinting(false);
player.setWalkSpeed(0.0f);
player.setFlySpeed(0.0f);
player.setVelocity(new Vector(0, 0, 0));
player.teleport(player.getLocation());
player.addPotionEffect(new PotionEffect(XPotion.BLINDNESS.get(), PotionEffect.INFINITE_DURATION, 0, false, false));
player.addPotionEffect(new PotionEffect(XPotion.JUMP_BOOST.get(), PotionEffect.INFINITE_DURATION, 250, false, false));
armorStandManager.spawnArmorStands(player);
playerService.getOpenNavigator().add(player);
}
/**
* Manages a player's interaction with the {@link NavigatorType#NEW} navigator.
*
* @param event The event which calls this method
*/
@EventHandler
public void manageNewNavigatorInteraction(PlayerInteractAtEntityEvent event) {
Player player = event.getPlayer();
Entity entity = event.getRightClicked();
// Disable interaction entities in archived worlds
disableArchivedWorlds(player, event);
if (!playerService.getOpenNavigator().contains(player) || !(entity instanceof ArmorStand armorStand)) {
return;
}
if (isCloseNavigatorItem(player, player.getInventory().getItemInMainHand())) {
event.setCancelled(true);
playerService.closeNewNavigator(player);
return;
}
Vector clickedPosition = event.getClickedPosition();
if (clickedPosition.getY() > MIN_HEIGHT && clickedPosition.getY() < MAX_HEIGHT) {
NavigatorCategory category = ArmorStandManager.matchNavigatorCategory(armorStand);
if (category == null) {
return;
}
UUID ownerUUID = ArmorStandManager.getOwner(armorStand);
if (!Objects.equals(ownerUUID, player.getUniqueId())) {
return;
}
DisplayablesInventory inventory = switch (category) {
case PUBLIC -> new PublicWorldsInventory(plugin, player);
case ARCHIVE -> new ArchivedWorldsInventory(plugin, player);
case PRIVATE -> new PrivateWorldsInventory(plugin, player);
};
XSound.BLOCK_CHEST_OPEN.play(player);
inventory.openInventory();
}
}
/**
* Cancels an event if the player is in an archived world.
*
* @param player The player object
* @param cancellable The event to cancel
*/
private void disableArchivedWorlds(Player player, Cancellable cancellable) {
BuildWorld buildWorld = worldStorage.getBuildWorld(player.getWorld());
if (buildWorld == null || buildWorld.getData().status().get() != BuildWorldStatus.ARCHIVE) {
return;
}
if (!playerService.isInBuildMode(player)) {
cancellable.setCancelled(true);
}
}
/**
* Disables players from manipulating with armor stands which make up the {@link NavigatorType#NEW} navigator.
*
* @param event The event which calls this method
*/
@EventHandler
public void preventNewNavigatorManipulation(PlayerArmorStandManipulateEvent event) {
ArmorStand armorStand = event.getRightClicked();
if (ArmorStandManager.matchNavigatorCategory(armorStand) == null || ArmorStandManager.getOwner(armorStand) == null) {
return;
}
event.setCancelled(true);
}
/**
* Prevents players from dropping the item which is used to close the {@link NavigatorType#NEW} navigator.
*
* @param event The event which calls this method
*/
@EventHandler
public void preventBarrierDrop(PlayerDropItemEvent event) {
Player player = event.getPlayer();
if (!playerService.getOpenNavigator().contains(player)) {
return;
}
if (isCloseNavigatorItem(player, event.getItemDrop().getItemStack())) {
event.setCancelled(true);
}
}
/**
* Checks if the given item is the item that is used to close the {@link NavigatorType#NEW} navigator.
*
* @param player The player used to get the item name
* @param itemStack The item stack to check
* @return {@code true} if the item is the navigator close item, {@code false} otherwise
*/
private boolean isCloseNavigatorItem(Player player, @Nullable ItemStack itemStack) {
if (itemStack == null || itemStack.getType() == Material.AIR) {
return false;
}
ItemMeta itemMeta = itemStack.getItemMeta();
if (itemMeta == null || !itemMeta.hasDisplayName()) {
return false;
}
if (XMaterial.matchXMaterial(itemStack) != XMaterial.BARRIER) {
return false;
}
return itemMeta.getDisplayName().equals(Messages.getString("barrier_item", player));
}
}
@@ -0,0 +1,200 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.listener;
import com.cryptomorin.xseries.XPotion;
import com.cryptomorin.xseries.XSound;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.player.CachedValues;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.api.world.data.BuildWorldStatus;
import de.eintosti.buildsystem.api.world.data.BuildWorldType;
import de.eintosti.buildsystem.config.Config.Settings.Archive;
import de.eintosti.buildsystem.config.Config.World.Unload;
import de.eintosti.buildsystem.player.PlayerServiceImpl;
import de.eintosti.buildsystem.player.settings.SettingsManager;
import de.eintosti.buildsystem.storage.WorldStorageImpl;
import de.eintosti.buildsystem.world.navigator.ArmorStandManager;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.bukkit.potion.PotionEffect;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public class PlayerChangedWorldListener implements Listener {
private final ArmorStandManager armorStandManager;
private final PlayerServiceImpl playerManager;
private final SettingsManager settingsManager;
private final WorldStorageImpl worldStorage;
private final Map<UUID, GameMode> playerGamemode;
private final Map<UUID, ItemStack[]> playerInventory;
private final Map<UUID, ItemStack[]> playerArmor;
public PlayerChangedWorldListener(BuildSystemPlugin plugin) {
this.armorStandManager = plugin.getArmorStandManager();
this.playerManager = plugin.getPlayerService();
this.settingsManager = plugin.getSettingsManager();
this.worldStorage = plugin.getWorldService().getWorldStorage();
this.playerGamemode = new HashMap<>();
this.playerInventory = new HashMap<>();
this.playerArmor = new HashMap<>();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@EventHandler
public void onPlayerChangedWorld(PlayerChangedWorldEvent event) {
Player player = event.getPlayer();
String worldName = player.getWorld().getName();
event.getPlayer().setAllowFlight(true);
BuildWorld oldWorld = worldStorage.getBuildWorld(event.getFrom());
if (oldWorld != null && Unload.enabled) {
oldWorld.getUnloader().resetUnloadTask();
}
BuildWorld newWorld = worldStorage.getBuildWorld(worldName);
if (newWorld != null && !newWorld.getData().physics().get() && player.hasPermission("buildsystem.physics.message")) {
Messages.sendMessage(player, "physics_deactivated_in_world", Map.entry("%world%", newWorld.getName()));
}
removeOldNavigator(player);
removeBuildMode(player);
setGoldBlock(newWorld);
checkWorldStatus(player);
if (settingsManager.getSettings(player).isScoreboard()) {
playerManager.forceUpdateSidebar(player);
}
}
private void removeOldNavigator(Player player) {
armorStandManager.removeArmorStands(player);
player.removePotionEffect(XPotion.BLINDNESS.get());
}
private void removeBuildMode(Player player) {
if (!playerManager.getBuildModePlayers().remove(player.getUniqueId())) {
return;
}
CachedValues cachedValues = playerManager.getPlayerStorage().getBuildPlayer(player).getCachedValues();
cachedValues.resetGameModeIfPresent(player);
cachedValues.resetInventoryIfPresent(player);
XSound.ENTITY_EXPERIENCE_ORB_PICKUP.play(player);
Messages.sendMessage(player, "build_deactivated_self");
}
private void setGoldBlock(@Nullable BuildWorld buildWorld) {
if (buildWorld == null || buildWorld.getType() != BuildWorldType.VOID
|| buildWorld.getData().status().get() != BuildWorldStatus.NOT_STARTED) {
return;
}
World bukkitWorld = Bukkit.getWorld(buildWorld.getName());
if (bukkitWorld == null) {
return;
}
bukkitWorld.getBlockAt(0, 64, 0).setType(Material.GOLD_BLOCK);
}
@SuppressWarnings("deprecation")
private void checkWorldStatus(Player player) {
BuildWorld buildWorld = worldStorage.getBuildWorld(player.getWorld());
if (buildWorld == null) {
return;
}
UUID playerUUID = player.getUniqueId();
PlayerInventory playerInventory = player.getInventory();
if (this.playerGamemode.containsKey(playerUUID)) {
player.setGameMode(this.playerGamemode.get(playerUUID));
this.playerGamemode.remove(playerUUID);
}
if (this.playerInventory.containsKey(playerUUID)) {
playerInventory.clear();
playerInventory.setContents(this.playerInventory.get(playerUUID));
this.playerInventory.remove(playerUUID);
}
if (this.playerArmor.containsKey(playerUUID)) {
removeArmorContent(player);
playerInventory.setArmorContents(this.playerArmor.get(playerUUID));
this.playerArmor.remove(playerUUID);
}
if (buildWorld.getData().status().get() == BuildWorldStatus.ARCHIVE) {
this.playerGamemode.put(playerUUID, player.getGameMode());
this.playerInventory.put(playerUUID, playerInventory.getContents());
this.playerArmor.put(playerUUID, playerInventory.getArmorContents());
removeArmorContent(player);
playerInventory.clear();
setSpectatorMode(player);
if (Archive.vanish) {
player.addPotionEffect(new PotionEffect(XPotion.INVISIBILITY.get(), PotionEffect.INFINITE_DURATION, 0, false, false), false);
Bukkit.getOnlinePlayers().forEach(pl -> pl.hidePlayer(player));
}
} else {
player.removePotionEffect(XPotion.INVISIBILITY.get());
Bukkit.getOnlinePlayers().forEach(pl -> pl.showPlayer(player));
}
playerManager.giveNavigator(player);
}
private void setSpectatorMode(Player player) {
// Checking if the game mode should be set to adventure mode on archive worlds
if (Archive.changeGamemode) {
player.setGameMode(Archive.worldGameMode);
}
player.setSaturation(20);
player.setHealth(20);
player.setAllowFlight(true);
player.setFlying(true);
}
private void removeArmorContent(Player player) {
PlayerInventory playerInventory = player.getInventory();
playerInventory.setHelmet(null);
playerInventory.setChestplate(null);
playerInventory.setLeggings(null);
playerInventory.setBoots(null);
}
}
@@ -0,0 +1,303 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.listener;
import com.google.common.collect.Sets;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.api.world.builder.Builders;
import de.eintosti.buildsystem.api.world.data.BuildWorldStatus;
import de.eintosti.buildsystem.config.Config.Settings.Builder;
import de.eintosti.buildsystem.event.player.PlayerInventoryClearEvent;
import de.eintosti.buildsystem.player.settings.SettingsManager;
import de.eintosti.buildsystem.storage.WorldStorageImpl;
import de.eintosti.buildsystem.util.inventory.InventoryUtils;
import java.util.List;
import java.util.Set;
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.event.player.PlayerCommandPreprocessEvent;
import org.bukkit.inventory.ItemStack;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class PlayerCommandPreprocessListener implements Listener {
private static final Set<String> DISABLED_COMMANDS = Sets.newHashSet(
"/worldedit",
"/we",
// History Control
"//undo",
"//redo",
"/clearhistory",
// Region Selection
"//wand",
"/toggleeditwand",
"//sel",
"//desel",
"//pos1",
"//pos2",
"//0",
"//1",
"//2",
"//hpos1",
"//hpos2",
"//chunk",
"//expand",
"//contract",
"//outset",
"//inset",
"//count",
"//distr",
// Region Operation
"//set",
"//replace",
"//repl",
"//overlay",
"//walls",
"//outline",
"//center",
"//smooth",
"//deform",
"//regen",
"//hollow",
"//move",
"//stack",
"//naturalize",
"//line",
"//curve",
"//forest",
"//flora",
"//air",
// Clipboards and Schematics
"//copy",
"//cut",
"//paste",
"//rotate",
"//flip",
"//schematic",
"//schem",
"/clearclipboard",
// Generation
"//generate",
"//generatebiome",
"//hcyl",
"//cyl",
"//sphere",
"//hsphere",
"//pyramid",
"/forestgen",
"/pumpkins",
// Utilities
"/toggleplace",
"//fill",
"//fillr",
"//drain",
"//fixwater",
"//fixlava",
"/removeabove",
"/removebelow",
"/replacenear",
"/removenear",
"/snow",
"/thaw",
"/ex",
"/butcher",
"/remove",
"/green",
"//calc",
// Chunk Tools
"/chunkinfo",
"/listchunks",
"/delchunks",
// Superpickaxe Tools
"//",
"/sp single",
"/sp area",
"/sp recur",
// General Tools
"/tool",
"/none",
"/farwand",
"/lrbuild",
"/tree",
"/deltree",
"/repl",
"/cycler",
"/flood",
// Brushes
"//brush",
"//br",
"/brush",
"/br",
"/size",
"/mat",
"/range",
"/mask",
"//gmask",
// Quick-Travel
"/unstuck",
"/ascend",
"/asc",
"/descend",
"/desc",
"/thru",
"/jumpto",
"/up",
// Snapshots
"//restore",
"/snapshot",
// Java Scriptings
"//cs",
"/.s",
// Biomes
"/biomelist",
"/biomels",
"/biomeinfo",
"//setbiome",
// Voxel Sniper
"/vs",
"/voxel",
"/voxel_chunk",
"/voxel_height",
"/voxel_ink",
"/voxel_ink_replace",
"/voxel_list",
"/voxel_replace",
"/voxel_sniper",
"/b",
"/brush",
"/brush_toolkit",
"/d",
"/default",
"/goto",
"/p",
"/paint",
"/perf",
"/performer",
"/v",
"/vc",
"/vchunk",
"/vh",
"/vi",
"/vir",
"/vl",
"/vr--"
);
private final BuildSystemPlugin plugin;
private final SettingsManager settingsManager;
private final WorldStorageImpl worldStorage;
public PlayerCommandPreprocessListener(BuildSystemPlugin plugin) {
this.plugin = plugin;
this.settingsManager = plugin.getSettingsManager();
this.worldStorage = plugin.getWorldService().getWorldStorage();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event) {
if (event.isCancelled()) {
return;
}
String command = event.getMessage().split(" ")[0];
Player player = event.getPlayer();
if (command.equalsIgnoreCase("/clear")) {
ItemStack navigatorItem = InventoryUtils.createNavigatorItem(player);
if (!player.getInventory().contains(navigatorItem)) {
return;
}
if (settingsManager.getSettings(player).isKeepNavigator()) {
List<Integer> navigatorSlots = InventoryUtils.getNavigatorSlots(player);
Bukkit.getScheduler().runTaskLater(plugin, () -> {
PlayerInventoryClearEvent playerInventoryClearEvent = new PlayerInventoryClearEvent(player, navigatorSlots);
Bukkit.getServer().getPluginManager().callEvent(playerInventoryClearEvent);
}, 2L);
}
return;
}
if (Builder.blockWorldEditNonBuilder) {
if (!DISABLED_COMMANDS.contains(command)) {
return;
}
BuildWorld buildWorld = worldStorage.getBuildWorld(player.getWorld());
if (buildWorld == null) {
return;
}
if (disableArchivedWorlds(buildWorld, player, event)) {
return;
}
checkBuilders(buildWorld, player, event);
}
}
private boolean disableArchivedWorlds(BuildWorld buildWorld, Player player, PlayerCommandPreprocessEvent event) {
if (buildWorld.getPermissions().canBypassBuildRestriction(player) || player.hasPermission("buildsystem.bypass.archive")) {
return false;
}
if (buildWorld.getData().status().get() == BuildWorldStatus.ARCHIVE) {
event.setCancelled(true);
Messages.sendMessage(player, "command_archive_world");
return true;
}
return false;
}
private void checkBuilders(BuildWorld buildWorld, Player player, PlayerCommandPreprocessEvent event) {
if (buildWorld.getPermissions().canBypassBuildRestriction(player) || player.hasPermission("buildsystem.bypass.builders")) {
return;
}
Builders builders = buildWorld.getBuilders();
if (builders.isCreator(player)) {
return;
}
if (buildWorld.getData().buildersEnabled().get() && !builders.isBuilder(player)) {
event.setCancelled(true);
Messages.sendMessage(player, "command_not_builder");
}
}
}
@@ -0,0 +1,54 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.listener;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.api.player.settings.Settings;
import de.eintosti.buildsystem.event.player.PlayerInventoryClearEvent;
import de.eintosti.buildsystem.player.settings.SettingsManager;
import de.eintosti.buildsystem.util.inventory.InventoryUtils;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class PlayerInventoryClearListener implements Listener {
private final SettingsManager settingsManager;
public PlayerInventoryClearListener(BuildSystemPlugin plugin) {
this.settingsManager = plugin.getSettingsManager();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@EventHandler
public void onPlayerInventoryClear(PlayerInventoryClearEvent event) {
Player player = event.getPlayer();
Settings settings = settingsManager.getSettings(player);
if (!settings.isKeepNavigator() || !player.hasPermission("buildsystem.navigator.item")) {
return;
}
PlayerInventory playerInventory = player.getInventory();
ItemStack navigatorItem = InventoryUtils.createNavigatorItem(player);
event.getNavigatorSlots().forEach(slot -> playerInventory.setItem(slot, navigatorItem));
}
}
@@ -0,0 +1,201 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.listener;
import com.cryptomorin.xseries.XPotion;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.player.BuildPlayer;
import de.eintosti.buildsystem.api.player.LogoutLocation;
import de.eintosti.buildsystem.api.player.settings.Settings;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.api.world.data.BuildWorldStatus;
import de.eintosti.buildsystem.api.world.data.WorldData;
import de.eintosti.buildsystem.config.Config;
import de.eintosti.buildsystem.config.Config.Settings.Archive;
import de.eintosti.buildsystem.player.LogoutLocationImpl;
import de.eintosti.buildsystem.player.PlayerServiceImpl;
import de.eintosti.buildsystem.player.settings.SettingsImpl;
import de.eintosti.buildsystem.player.settings.SettingsManager;
import de.eintosti.buildsystem.storage.WorldStorageImpl;
import de.eintosti.buildsystem.util.UUIDFetcher;
import de.eintosti.buildsystem.util.UpdateChecker;
import de.eintosti.buildsystem.world.SpawnManager;
import io.papermc.lib.PaperLib;
import java.util.Map;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.potion.PotionEffect;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class PlayerJoinListener implements Listener {
private final BuildSystemPlugin plugin;
private final PlayerServiceImpl playerManager;
private final SettingsManager settingsManager;
private final SpawnManager spawnManager;
private final WorldStorageImpl worldStorage;
public PlayerJoinListener(BuildSystemPlugin plugin) {
this.plugin = plugin;
this.playerManager = plugin.getPlayerService();
this.settingsManager = plugin.getSettingsManager();
this.spawnManager = plugin.getSpawnManager();
this.worldStorage = plugin.getWorldService().getWorldStorage();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@EventHandler(priority = EventPriority.LOWEST)
public void sendPlayerJoinMessage(PlayerJoinEvent event) {
Player player = event.getPlayer();
String message = Config.Messages.joinQuitMessages
? Messages.getString("player_join", player, Map.entry("%player%", player.getName()))
: null;
event.setJoinMessage(message);
}
@EventHandler
@SuppressWarnings("deprecation")
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
UUIDFetcher.cacheUser(player.getUniqueId(), player.getName());
BuildPlayer buildPlayer = playerManager.getPlayerStorage().createBuildPlayer(player);
manageHidePlayer(player, buildPlayer);
manageSettings(player, buildPlayer.getSettings());
teleportToCorrectLocation(player, buildPlayer);
playerManager.giveNavigator(player);
String worldName = player.getWorld().getName();
BuildWorld buildWorld = worldStorage.getBuildWorld(worldName);
if (buildWorld != null) {
WorldData worldData = buildWorld.getData();
if (!worldData.physics().get() && player.hasPermission("buildsystem.physics.message")) {
Messages.sendMessage(player, "physics_deactivated_in_world", Map.entry("%world%", worldName));
}
if (Archive.vanish && worldData.status().get() == BuildWorldStatus.ARCHIVE) {
player.addPotionEffect(new PotionEffect(XPotion.INVISIBILITY.get(), PotionEffect.INFINITE_DURATION, 0, false, false), false);
Bukkit.getOnlinePlayers().forEach(pl -> pl.hidePlayer(player));
}
}
if (player.hasPermission("buildsystem.updates")) {
performUpdateCheck(player);
}
}
/**
* Teleports the player to the correct location.
* <ul>
* <li>If the spawn exists and {@link SettingsImpl#isSpawnTeleport()} is enabled for the player, the player will be teleported to the spawn</li>
* <li>If the player has a {@link LogoutLocationImpl}, teleport to that location</li>
* <li>Otherwise, do nothing</li>
* </ul>
*
* @param player The player to teleport
* @param buildPlayer The build-player for the given player
*/
private void teleportToCorrectLocation(Player player, BuildPlayer buildPlayer) {
if (buildPlayer.getSettings().isSpawnTeleport() && spawnManager.spawnExists()) {
spawnManager.teleport(player);
return;
}
LogoutLocation logoutLocation = buildPlayer.getLogoutLocation();
if (logoutLocation == null) {
return;
}
BuildWorld buildWorld = worldStorage.getBuildWorld(logoutLocation.worldName());
if (buildWorld == null) {
return;
}
int delay = buildWorld.isLoaded() ? 0 : 20;
Bukkit.getScheduler().runTaskLater(plugin, () -> {
Location location = logoutLocation.location();
if (location != null) {
PaperLib.teleportAsync(player, location);
}
}, delay);
}
@SuppressWarnings("deprecation")
private void manageHidePlayer(Player player, BuildPlayer buildPlayer) {
// Hide all players to player
if (buildPlayer.getSettings().isHidePlayers()) {
Bukkit.getOnlinePlayers().forEach(player::hidePlayer);
}
// Hide player from all players who have hidePlayers enabled
for (Player pl : Bukkit.getOnlinePlayers()) {
if (!settingsManager.getSettings(pl).isHidePlayers()) {
continue;
}
pl.hidePlayer(player);
}
}
/**
* Activates features for the player according to their {@link SettingsImpl}.
*
* @param player The player to activate the features for
* @param settings The player's settings
*/
private void manageSettings(Player player, Settings settings) {
if (settings.isNoClip()) {
plugin.getNoClipManager().startNoClip(player);
}
if (settings.isScoreboard()) {
settingsManager.displayScoreboard(player);
plugin.getPlayerService().forceUpdateSidebar(player);
}
if (settings.isClearInventory()) {
player.getInventory().clear();
}
}
private void performUpdateCheck(Player player) {
if (!Config.Settings.updateChecker) {
return;
}
UpdateChecker.init(plugin, BuildSystemPlugin.SPIGOT_ID)
.requestUpdateCheck()
.whenComplete((result, e) -> {
if (result.requiresUpdate()) {
StringBuilder stringBuilder = new StringBuilder();
Messages.getStringList("update_available", player).forEach(line ->
stringBuilder.append(line
.replace("%new_version%", result.getNewestVersion())
.replace("%current_version%", plugin.getDescription().getVersion()))
.append("\n"));
player.sendMessage(stringBuilder.toString());
}
});
}
}
@@ -0,0 +1,69 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.listener;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.api.player.settings.Settings;
import de.eintosti.buildsystem.api.world.navigator.settings.NavigatorType;
import de.eintosti.buildsystem.player.PlayerServiceImpl;
import de.eintosti.buildsystem.player.settings.SettingsManager;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class PlayerMoveListener implements Listener {
private final BuildSystemPlugin plugin;
private final PlayerServiceImpl playerManager;
private final SettingsManager settingsManager;
public PlayerMoveListener(BuildSystemPlugin plugin) {
this.plugin = plugin;
this.playerManager = plugin.getPlayerService();
this.settingsManager = plugin.getSettingsManager();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@EventHandler
public void onPlayerMove(PlayerMoveEvent event) {
Player player = event.getPlayer();
if (!playerManager.getOpenNavigator().contains(player)) {
return;
}
Settings settings = settingsManager.getSettings(player);
if (!settings.getNavigatorType().equals(NavigatorType.NEW)) {
return;
}
Location to = event.getTo();
if (to == null) {
return;
}
Location from = event.getFrom();
if (from.getBlockX() != to.getBlockX() || from.getBlockZ() != to.getBlockZ()) {
Bukkit.getServer().getScheduler().runTaskLater(plugin, () -> playerManager.closeNewNavigator(player), 5L);
}
}
}
@@ -0,0 +1,108 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.listener;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.player.BuildPlayer;
import de.eintosti.buildsystem.api.player.CachedValues;
import de.eintosti.buildsystem.api.player.settings.Settings;
import de.eintosti.buildsystem.config.Config;
import de.eintosti.buildsystem.player.LogoutLocationImpl;
import de.eintosti.buildsystem.player.PlayerServiceImpl;
import de.eintosti.buildsystem.player.settings.SettingsManager;
import java.util.Map;
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.event.player.PlayerQuitEvent;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class PlayerQuitListener implements Listener {
private final BuildSystemPlugin plugin;
private final PlayerServiceImpl playerManager;
private final SettingsManager settingsManager;
public PlayerQuitListener(BuildSystemPlugin plugin) {
this.plugin = plugin;
this.playerManager = plugin.getPlayerService();
this.settingsManager = plugin.getSettingsManager();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@EventHandler(priority = EventPriority.LOWEST)
public void sendPlayerQuitMessage(PlayerQuitEvent event) {
Player player = event.getPlayer();
String message = Config.Messages.joinQuitMessages
? Messages.getString("player_quit", player, Map.entry("%player%", player.getName()))
: null;
event.setQuitMessage(message);
}
@EventHandler
public void onPlayerQuit(PlayerQuitEvent event) {
Player player = event.getPlayer();
playerManager.closeNewNavigator(player);
Settings settings = settingsManager.getSettings(player);
if (settings.isNoClip()) {
plugin.getNoClipManager().stopNoClip(player.getUniqueId());
}
if (settings.isScoreboard()) {
settingsManager.hideScoreboard(player);
}
if (settings.isClearInventory()) {
player.getInventory().clear();
}
BuildPlayer buildPlayer = playerManager.getPlayerStorage().getBuildPlayer(player);
buildPlayer.setLogoutLocation(new LogoutLocationImpl(
player.getWorld().getName(),
player.getLocation()
));
CachedValues cachedValues = buildPlayer.getCachedValues();
cachedValues.resetGameModeIfPresent(player);
cachedValues.resetInventoryIfPresent(player);
playerManager.getBuildModePlayers().remove(player.getUniqueId());
manageHidePlayer(player);
}
@SuppressWarnings("deprecation")
private void manageHidePlayer(Player player) {
// Show all hidden players to player
if (settingsManager.getSettings(player).isHidePlayers()) {
Bukkit.getOnlinePlayers().forEach(player::showPlayer);
}
// Show player to all players who had him/her hidden
for (Player pl : Bukkit.getOnlinePlayers()) {
if (!settingsManager.getSettings(pl).isHidePlayers()) {
continue;
}
pl.showPlayer(player);
}
}
}
@@ -0,0 +1,51 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.listener;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.api.player.settings.Settings;
import de.eintosti.buildsystem.player.settings.SettingsManager;
import de.eintosti.buildsystem.world.SpawnManager;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerRespawnEvent;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class PlayerRespawnListener implements Listener {
private final SettingsManager settingsManager;
private final SpawnManager spawnManager;
public PlayerRespawnListener(BuildSystemPlugin plugin) {
this.settingsManager = plugin.getSettingsManager();
this.spawnManager = plugin.getSpawnManager();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@EventHandler
public void onPlayerRespawn(PlayerRespawnEvent event) {
Player player = event.getPlayer();
Settings settings = settingsManager.getSettings(player);
if (settings.isSpawnTeleport() && spawnManager.spawnExists()) {
event.setRespawnLocation(spawnManager.getSpawn());
}
}
}
@@ -0,0 +1,79 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.listener;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.storage.PlayerStorage;
import de.eintosti.buildsystem.api.storage.WorldStorage;
import de.eintosti.buildsystem.api.world.BuildWorld;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class PlayerTeleportListener implements Listener {
private final PlayerStorage playerStorage;
private final WorldStorage worldStorage;
public PlayerTeleportListener(BuildSystemPlugin plugin) {
this.playerStorage = plugin.getPlayerService().getPlayerStorage();
this.worldStorage = plugin.getWorldService().getWorldStorage();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@EventHandler
public void onPlayerTeleport(PlayerTeleportEvent event) {
Player player = event.getPlayer();
if (event.getCause() != PlayerTeleportEvent.TeleportCause.UNKNOWN) {
playerStorage.getBuildPlayer(player).setPreviousLocation(event.getFrom());
}
Location to = event.getTo();
if (to == null) {
return;
}
World toWorld = to.getWorld();
if (toWorld == null) {
return;
}
BuildWorld buildWorld = worldStorage.getBuildWorld(toWorld);
if (buildWorld == null) {
return;
}
// Users can always teleport to the main server world
if (Bukkit.getWorlds().getFirst().equals(toWorld)) {
return;
}
if (!buildWorld.getPermissions().canEnter(player)) {
Messages.sendMessage(player, "worlds_tp_entry_forbidden");
event.setCancelled(true);
}
}
}
@@ -0,0 +1,425 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.listener;
import com.cryptomorin.xseries.XBlock;
import com.cryptomorin.xseries.XMaterial;
import com.cryptomorin.xseries.XTag;
import com.google.common.collect.Sets;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.api.player.settings.Settings;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.api.world.builder.Builder;
import de.eintosti.buildsystem.api.world.builder.Builders;
import de.eintosti.buildsystem.api.world.data.BuildWorldStatus;
import de.eintosti.buildsystem.api.world.data.WorldData;
import de.eintosti.buildsystem.config.Config;
import de.eintosti.buildsystem.player.customblock.CustomBlockManager;
import de.eintosti.buildsystem.player.settings.SettingsManager;
import de.eintosti.buildsystem.storage.WorldStorageImpl;
import de.eintosti.buildsystem.util.DirectionUtil;
import de.eintosti.buildsystem.util.inventory.MaterialUtils;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.DyeColor;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.MultipleFacing;
import org.bukkit.block.data.Openable;
import org.bukkit.block.data.type.Slab;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.inventory.InventoryOpenEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.RayTraceResult;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class SettingsInteractListener implements Listener {
private static final EnumSet<XMaterial> OTHER_PLANTS = Sets.newEnumSet(Sets.newHashSet(
XMaterial.TORCHFLOWER, XMaterial.PITCHER_PLANT, XMaterial.LILY_PAD, XMaterial.PINK_PETALS,
XMaterial.BROWN_MUSHROOM, XMaterial.RED_MUSHROOM, XMaterial.CRIMSON_FUNGUS, XMaterial.WARPED_FUNGUS,
XMaterial.SHORT_GRASS, XMaterial.FERN, XMaterial.DEAD_BUSH, XMaterial.LARGE_FERN, XMaterial.TALL_GRASS,
XMaterial.NETHER_SPROUTS, XMaterial.WARPED_ROOTS, XMaterial.CRIMSON_ROOTS, XMaterial.SUGAR_CANE, XMaterial.BAMBOO,
XMaterial.BIG_DRIPLEAF, XMaterial.SMALL_DRIPLEAF, XMaterial.SEAGRASS, XMaterial.SWEET_BERRIES
), XMaterial.class);
private final CustomBlockManager customBlockManager;
private final SettingsManager settingsManager;
private final WorldStorageImpl worldStorage;
private final Set<UUID> cachePlayers;
public SettingsInteractListener(BuildSystemPlugin plugin) {
this.customBlockManager = plugin.getCustomBlockManager();
this.settingsManager = plugin.getSettingsManager();
this.worldStorage = plugin.getWorldService().getWorldStorage();
this.cachePlayers = new HashSet<>();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@EventHandler
public void manageIronDoorSetting(PlayerInteractEvent event) {
if (!isValid(event)) {
return;
}
Block block = event.getClickedBlock();
if (block == null) {
return;
}
if (event.getHand() != EquipmentSlot.HAND) {
return;
}
Player player = event.getPlayer();
if (player.isSneaking() || event.getAction() != Action.RIGHT_CLICK_BLOCK) {
return;
}
Settings settings = settingsManager.getSettings(player);
if (!settings.isOpenTrapDoors()) {
return;
}
XMaterial material = XMaterial.matchXMaterial(block.getType());
if (material != XMaterial.IRON_DOOR && material != XMaterial.IRON_TRAPDOOR) {
return;
}
event.setCancelled(true);
Openable openable = (Openable) block.getBlockData();
openable.setOpen(!openable.isOpen());
block.setBlockData(openable);
}
@EventHandler
public void manageSlabSetting(PlayerInteractEvent event) {
if (!isValid(event)) {
return;
}
Action action = event.getAction();
if (action != Action.LEFT_CLICK_BLOCK) {
return;
}
Player player = event.getPlayer();
Settings settings = settingsManager.getSettings(player);
if (!settings.isSlabBreaking()) {
return;
}
Block block = event.getClickedBlock();
if (block == null || !(block.getBlockData() instanceof Slab slab) || slab.getType() != Slab.Type.DOUBLE) {
return;
}
event.setCancelled(true);
if (isTopHalf(player)) {
slab.setType(Slab.Type.BOTTOM);
} else {
slab.setType(Slab.Type.TOP);
}
block.setBlockData(slab);
}
/**
* Determines if the player's ray trace hit position is on the top half of a block.
*
* @param player The {@link Player} to check.
* @return {@code true} if the player's hit position is on the top half of a block, {@code false} otherwise.
*/
public boolean isTopHalf(Player player) {
RayTraceResult result = player.rayTraceBlocks(6);
if (result == null) {
return false;
}
return Math.abs(result.getHitPosition().getY() % 1) < 0.5;
}
@EventHandler
public void managePlacePlantsSetting(PlayerInteractEvent event) {
if (!isValid(event)) {
return;
}
Block block = event.getClickedBlock();
if (block == null || event.getAction() != Action.RIGHT_CLICK_BLOCK) {
return;
}
Settings settings = settingsManager.getSettings(event.getPlayer());
if (!settings.isPlacePlants()) {
return;
}
ItemStack itemStack = event.getItem();
if (itemStack == null) {
return;
}
XMaterial xMaterial = XMaterial.matchXMaterial(itemStack.getType());
if (!XTag.FLOWERS.isTagged(xMaterial)
&& !XTag.REPLACEABLE_PLANTS.isTagged(xMaterial)
&& !XTag.ALIVE_CORAL_PLANTS.isTagged(xMaterial)
&& !XTag.DEAD_CORAL_PLANTS.isTagged(xMaterial)
&& !XTag.SAPLINGS.isTagged(xMaterial)
&& !OTHER_PLANTS.contains(xMaterial)
) {
return;
}
event.setCancelled(true);
Block adjacent = block.getRelative(event.getBlockFace());
switch (xMaterial) {
case SWEET_BERRIES:
adjacent.setType(XMaterial.SWEET_BERRY_BUSH.get());
break;
case VINE:
BlockFace toPlace = event.getBlockFace().getOppositeFace();
if (toPlace == BlockFace.DOWN) { // Cannot place vines facing down
break;
}
adjacent.setType(xMaterial.get());
MultipleFacing multipleFacing = (MultipleFacing) adjacent.getBlockData();
Arrays.stream(DirectionUtil.BLOCK_SIDES).forEach(blockFace -> multipleFacing.setFace(blockFace, blockFace == toPlace));
adjacent.setBlockData(multipleFacing);
break;
default:
adjacent.setType(xMaterial.get());
break;
}
}
@EventHandler
public void manageInstantPlaceSignsSetting(PlayerInteractEvent event) {
if (event.getAction() != Action.RIGHT_CLICK_BLOCK || !isValid(event)) {
return;
}
Player player = event.getPlayer();
Settings settings = settingsManager.getSettings(player);
if (!settings.isInstantPlaceSigns()) {
return;
}
ItemStack itemStack = event.getItem();
if (itemStack == null) {
return;
}
Material material = itemStack.getType();
XMaterial xMaterial = XMaterial.matchXMaterial(itemStack);
if (!XTag.SIGNS.isTagged(xMaterial) && !XTag.HANGING_SIGNS.isTagged(xMaterial)) {
return;
}
Block clickedBlock = event.getClickedBlock();
BlockFace blockFace = event.getBlockFace();
if (clickedBlock == null) {
return;
}
Block adjacent = clickedBlock.getRelative(blockFace);
if (adjacent.getType() != XMaterial.AIR.get()) {
return;
}
event.setUseItemInHand(Event.Result.DENY);
event.setCancelled(true);
boolean isHangingSign = XTag.HANGING_SIGNS.isTagged(xMaterial);
switch (blockFace) {
case UP -> {
if (isHangingSign) {
return;
}
adjacent.setType(material);
customBlockManager.rotateBlock(adjacent, DirectionUtil.getPlayerDirection(player).getOppositeFace());
}
case DOWN -> {
if (!isHangingSign) {
return;
}
adjacent.setType(material);
customBlockManager.rotateBlock(adjacent, getHangingSignDirection(event));
}
case NORTH, EAST, SOUTH, WEST -> {
String woodType = xMaterial.name()
.replace("_HANGING", "") // Replace hanging if present
.replace("_SIGN", ""); // Get wood type
String block = isHangingSign ? "_WALL_HANGING_SIGN" : "_WALL_SIGN";
BlockFace facing = isHangingSign ? getHangingSignDirection(event) : blockFace;
XMaterial.matchXMaterial(woodType + block).ifPresent(value -> adjacent.setType(value.get()));
customBlockManager.rotateBlock(adjacent, facing);
}
}
}
private BlockFace getHangingSignDirection(PlayerInteractEvent event) {
BlockFace clickedFace = event.getBlockFace();
BlockFace playerFacing = DirectionUtil.getCardinalDirection(event.getPlayer()).getOppositeFace();
if (clickedFace != playerFacing && clickedFace != playerFacing.getOppositeFace()) {
return playerFacing;
}
return (clickedFace == BlockFace.NORTH || clickedFace == BlockFace.SOUTH) ? BlockFace.EAST : BlockFace.SOUTH;
}
@EventHandler
public void manageDisabledInteractSetting(PlayerInteractEvent event) {
Player player = event.getPlayer();
if (event.getAction() != Action.RIGHT_CLICK_BLOCK || !isValid(event)) {
return;
}
Settings settings = settingsManager.getSettings(player);
if (!settings.isDisableInteract()) {
return;
}
Block block = event.getClickedBlock();
if (block == null || !MaterialUtils.isInteractable(XMaterial.matchXMaterial(block.getType()))) {
return;
}
ItemStack itemStack = event.getItem();
if (itemStack == null) {
return;
}
Material material = itemStack.getType();
XMaterial xMaterial = XMaterial.matchXMaterial(material);
if (xMaterial == Config.Settings.Builder.worldEditWand) {
return;
}
cachePlayers.add(player.getUniqueId());
event.setCancelled(true);
event.setUseItemInHand(Event.Result.DENY);
event.setUseInteractedBlock(Event.Result.DENY);
if (XTag.SIGNS.isTagged(xMaterial) && event.getBlockFace() != BlockFace.UP) {
String[] splitMaterial = material.toString().split("_");
material = Material.valueOf(splitMaterial[0] + "_WALL_SIGN");
}
if (!material.isBlock()) {
return;
}
Block adjacent = block.getRelative(event.getBlockFace());
adjacent.setType(material);
XBlock.setColor(adjacent, DyeColor.getByWoolData((byte) itemStack.getDurability()));
customBlockManager.rotateBlock(adjacent, DirectionUtil.getBlockDirection(player, false));
EquipmentSlot hand = event.getHand();
if (hand == null) {
hand = EquipmentSlot.HAND;
}
BlockPlaceEvent blockPlaceEvent = new BlockPlaceEvent(adjacent, adjacent.getState(), block, itemStack, player, true, hand);
Bukkit.getServer().getPluginManager().callEvent(blockPlaceEvent);
}
/**
* Not every player can always interact with the {@link BuildWorld} they are in.
* <p>
* Reasons an interaction could be canceled:
* <ul>
* <li>The world has its {@link BuildWorldStatus} set to archive</li>
* <li>The world has a setting enabled which disallows certain events</li>
* <li>The world only allows {@link Builder}s to build and the player is not such a builder</li>
* </ul>
* <p>
* However, a player can override these reasons if:
* <ul>
* <li>The player has the permission <b>buildsystem.admin</b></li>
* <li>The player has the permission <b>buildsystem.bypass.archive</b></li>
* <li>The player has used <b>/build</b> to enter build-mode</li>
* </ul>
*
* @param event the event which was called by the world manipulation
* @return if the interaction with the world is valid
*/
private boolean isValid(PlayerInteractEvent event) {
if (event.isCancelled()) {
return false;
}
Player player = event.getPlayer();
BuildWorld buildWorld = worldStorage.getBuildWorld(player.getWorld().getName());
if (buildWorld == null) {
return true;
}
if (buildWorld.getPermissions().canBypassBuildRestriction(player)) {
return true;
}
WorldData worldData = buildWorld.getData();
if (worldData.status().get() == BuildWorldStatus.ARCHIVE && !player.hasPermission("buildsystem.bypass.archive")) {
return false;
}
if (!worldData.blockPlacement().get()) {
return false;
}
Builders builders = buildWorld.getBuilders();
if (buildWorld.getData().buildersEnabled().get()
&& !builders.isBuilder(player)
&& !player.hasPermission("buildsystem.bypass.builders")) {
return builders.isCreator(player);
}
return true;
}
/**
* Stop {@link Player} from opening {@link Inventory} because the event should be canceled as it was fired due to an interaction caused in
* {@link SettingsInteractListener#manageDisabledInteractSetting}
*/
@EventHandler
public void onInventoryOpen(InventoryOpenEvent event) {
if (cachePlayers.remove(event.getPlayer().getUniqueId())) {
event.setCancelled(true);
}
}
}
@@ -0,0 +1,53 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.listener;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.util.color.ColorAPI;
import org.bukkit.ChatColor;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.SignChangeEvent;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class SignChangeListener implements Listener {
public SignChangeListener(BuildSystemPlugin plugin) {
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@EventHandler
public void onSignChange(SignChangeEvent event) {
Player player = event.getPlayer();
if (!player.hasPermission("buildsystem.color.sign")) {
return;
}
for (int i = 0; i < event.getLines().length; i++) {
String line = event.getLine(i);
if (line == null) {
continue;
}
String coloredLine = ChatColor.translateAlternateColorCodes('&', ColorAPI.process(line));
event.setLine(i, coloredLine);
}
}
}
@@ -0,0 +1,57 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.listener;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.config.Config.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.weather.ThunderChangeEvent;
import org.bukkit.event.weather.WeatherChangeEvent;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class WeatherChangeListener implements Listener {
public WeatherChangeListener(BuildSystemPlugin plugin) {
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onWeatherChange(WeatherChangeEvent event) {
if (!World.lockWeather) {
return;
}
if (event.toWeatherState()) {
event.setCancelled(true);
}
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onThunderChange(ThunderChangeEvent event) {
if (!World.lockWeather) {
return;
}
if (event.toThunderState()) {
event.setCancelled(true);
}
}
}
@@ -0,0 +1,55 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.listener;
import com.moulberry.axiom.event.AxiomModifyWorldEvent;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.event.EventDispatcher;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.jspecify.annotations.NullMarked;
/**
* Only register if axiom is available
*/
@NullMarked
public class WorldManipulateByAxiomListener implements Listener {
private final EventDispatcher dispatcher;
/**
* Creates a new {@link WorldManipulateByAxiomListener} instance.
*
* @param plugin plugin to register.
*/
public WorldManipulateByAxiomListener(BuildSystemPlugin plugin) {
this.dispatcher = new EventDispatcher(plugin.getWorldService().getWorldStorage());
plugin.getServer().getPluginManager().registerEvents(this, plugin);
plugin.getLogger().info("Axiom build-world manipulation prevention has been enabled.");
}
@EventHandler()
public void onWorldModification(AxiomModifyWorldEvent event) {
// I don't know if it is possible. Just to be safe.
if (!event.getPlayer().getWorld().equals(event.getWorld())) {
event.setCancelled(true);
throw new IllegalStateException("Player modifies a world in which he is not present! The event got cancelled for safety reasons.");
}
dispatcher.dispatchManipulationEventIfPlayerInBuildWorld(event.getPlayer(), event);
}
}
@@ -0,0 +1,151 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.listener;
import com.cryptomorin.xseries.XMaterial;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.api.data.Type;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.api.world.data.BuildWorldStatus;
import de.eintosti.buildsystem.api.world.data.WorldData;
import de.eintosti.buildsystem.config.Config.Settings.Builder;
import de.eintosti.buildsystem.event.EventDispatcher;
import de.eintosti.buildsystem.event.world.BuildWorldManipulationEvent;
import de.eintosti.buildsystem.storage.WorldStorageImpl;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
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.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.player.PlayerArmorStandManipulateEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.ItemStack;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class WorldManipulateListener implements Listener {
private final BuildSystemPlugin plugin;
private final WorldStorageImpl worldStorage;
private final EventDispatcher dispatcher;
public WorldManipulateListener(BuildSystemPlugin plugin) {
this.plugin = plugin;
this.worldStorage = plugin.getWorldService().getWorldStorage();
this.dispatcher = new EventDispatcher(worldStorage);
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onBlockBreak(BlockBreakEvent event) {
dispatcher.dispatchManipulationEventIfPlayerInBuildWorld(event.getPlayer(), event);
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onBlockPlace(BlockPlaceEvent event) {
dispatcher.dispatchManipulationEventIfPlayerInBuildWorld(event.getPlayer(), event);
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onEntityDamageByEntity(EntityDamageByEntityEvent event) {
if (!(event.getDamager() instanceof Player player) || !(event.getEntity() instanceof ArmorStand)) {
return;
}
dispatcher.dispatchManipulationEventIfPlayerInBuildWorld(player, event);
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onPlayerArmorStandManipulate(PlayerArmorStandManipulateEvent event) {
dispatcher.dispatchManipulationEventIfPlayerInBuildWorld(event.getPlayer(), event);
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onPlayerInteract(PlayerInteractEvent event) {
ItemStack itemStack = event.getItem();
if (itemStack != null && itemStack.getType() == Builder.worldEditWand.get()) {
return;
}
Player player = event.getPlayer();
BuildWorld buildWorld = worldStorage.getBuildWorld(player.getWorld());
if (buildWorld == null) {
return;
}
dispatcher.dispatchManipulationEventIfPlayerInBuildWorld(player, event);
if (!buildWorld.getData().physics().get() && event.getClickedBlock() != null) {
if (event.getAction() == Action.PHYSICAL && event.getClickedBlock().getType() == XMaterial.FARMLAND.get()) {
event.setCancelled(true);
}
}
}
@EventHandler(priority = EventPriority.LOW)
public void onWorldManipulation(BuildWorldManipulationEvent event) {
if (event.isCancelled()) {
return;
}
Player player = event.getPlayer();
BuildWorld buildWorld = event.getBuildWorld();
WorldData worldData = buildWorld.getData();
Cancellable parentEvent = event.getParentEvent();
boolean canModify = buildWorld.getPermissions().canModify(player, getRelatedWorldSetting(parentEvent, worldData));
if (!canModify) {
parentEvent.setCancelled(true);
denyPlayerInteraction(event);
return;
}
worldData.lastEdited().set(System.currentTimeMillis());
updateStatus(worldData, player);
}
private Type<Boolean> getRelatedWorldSetting(Cancellable event, WorldData data) {
return switch (event) {
case BlockBreakEvent ignored -> data.blockBreaking();
case BlockPlaceEvent ignored -> data.blockPlacement();
case PlayerInteractEvent ignored -> data.blockInteractions();
default -> throw new IllegalStateException("Unexpected event type: " + event.getClass().getName());
};
}
private void denyPlayerInteraction(Event event) {
if (event instanceof PlayerInteractEvent interactEvent) {
interactEvent.setUseItemInHand(Event.Result.DENY);
interactEvent.setUseInteractedBlock(Event.Result.DENY);
}
}
private void updateStatus(WorldData worldData, Player player) {
if (worldData.status().get() == BuildWorldStatus.NOT_STARTED) {
worldData.status().set(BuildWorldStatus.IN_PROGRESS);
plugin.getPlayerService().forceUpdateSidebar(player);
}
}
}
@@ -0,0 +1,97 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.player;
import de.eintosti.buildsystem.api.player.BuildPlayer;
import de.eintosti.buildsystem.api.player.CachedValues;
import de.eintosti.buildsystem.api.player.LogoutLocation;
import de.eintosti.buildsystem.api.player.settings.Settings;
import de.eintosti.buildsystem.api.world.display.NavigatorCategory;
import java.util.UUID;
import org.bukkit.Location;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public class BuildPlayerImpl implements BuildPlayer {
private final UUID uuid;
private final Settings settings;
private final CachedValues cachedValues;
@Nullable
private LogoutLocation logoutLocation;
@Nullable
private Location previousLocation;
@Nullable
private NavigatorCategory lastLookedAt;
public BuildPlayerImpl(UUID uuid, Settings settings) {
this.uuid = uuid;
this.settings = settings;
this.cachedValues = new CachedValuesImpl();
}
@Override
public UUID getUniqueId() {
return uuid;
}
@Override
public Settings getSettings() {
return settings;
}
@Override
public CachedValues getCachedValues() {
return cachedValues;
}
@Override
@Nullable
public LogoutLocation getLogoutLocation() {
return logoutLocation;
}
@Override
public void setLogoutLocation(@Nullable LogoutLocation logoutLocation) {
this.logoutLocation = logoutLocation;
}
@Override
@Nullable
public Location getPreviousLocation() {
return previousLocation;
}
@Override
public void setPreviousLocation(@Nullable Location location) {
this.previousLocation = location;
}
@Override
@Nullable
public NavigatorCategory getLastLookedAt() {
return lastLookedAt;
}
@Override
public void setLastLookedAt(@Nullable NavigatorCategory type) {
this.lastLookedAt = type;
}
}
@@ -0,0 +1,104 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.player;
import de.eintosti.buildsystem.api.player.CachedValues;
import java.util.List;
import org.bukkit.GameMode;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public class CachedValuesImpl implements CachedValues {
@Nullable
private GameMode gameMode;
@Nullable
private List<ItemStack> inventory;
@Nullable
private Float walkSpeed;
@Nullable
private Float flySpeed;
@Override
public void saveGameMode(GameMode gameMode) {
this.gameMode = gameMode;
}
@Override
public void resetGameModeIfPresent(Player player) {
if (this.gameMode == null) {
return;
}
player.setGameMode(gameMode);
this.gameMode = null;
}
@Override
public void saveInventory(ItemStack[] inventory) {
this.inventory = List.of(inventory);
}
@Override
public void resetInventoryIfPresent(Player player) {
if (this.inventory == null) {
return;
}
player.getInventory().clear();
player.getInventory().setContents(this.inventory.toArray(new ItemStack[0]));
this.inventory = null;
}
@Override
public void saveWalkSpeed(float walkSpeed) {
this.walkSpeed = walkSpeed;
}
@Override
public void resetWalkSpeedIfPresent(Player player) {
if (this.walkSpeed == null) {
return;
}
player.setWalkSpeed(walkSpeed);
this.walkSpeed = null;
}
@Override
public void saveFlySpeed(float flySpeed) {
this.flySpeed = flySpeed;
}
@Override
public void resetFlySpeedIfPresent(Player player) {
if (this.flySpeed == null) {
return;
}
player.setFlySpeed(flySpeed);
this.flySpeed = null;
}
@Override
public void resetCachedValues(Player player) {
resetGameModeIfPresent(player);
resetInventoryIfPresent(player);
resetWalkSpeedIfPresent(player);
resetFlySpeedIfPresent(player);
}
}
@@ -0,0 +1,71 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.player;
import de.eintosti.buildsystem.api.player.LogoutLocation;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public class LogoutLocationImpl implements LogoutLocation {
private final String worldName;
private final double x, y, z;
private final float yaw, pitch;
public LogoutLocationImpl(String worldName, Location location) {
this.worldName = worldName;
this.x = location.getX();
this.y = location.getY();
this.z = location.getZ();
this.yaw = location.getYaw();
this.pitch = location.getPitch();
}
public LogoutLocationImpl(String worldName, double x, double y, double z, float yaw, float pitch) {
this.worldName = worldName;
this.x = x;
this.y = y;
this.z = z;
this.yaw = yaw;
this.pitch = pitch;
}
@Override
public String worldName() {
return worldName;
}
@Nullable
@Override
public Location location() {
World world = Bukkit.getWorld(worldName);
if (world == null) {
return null;
}
return new Location(world, x, y, z, yaw, pitch);
}
@Override
public String toString() {
return worldName + ":" + x + ":" + y + ":" + z + ":" + yaw + ":" + pitch;
}
}
@@ -0,0 +1,391 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.player;
import com.cryptomorin.xseries.XMaterial;
import com.cryptomorin.xseries.XPotion;
import com.cryptomorin.xseries.XSound;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.api.player.BuildPlayer;
import de.eintosti.buildsystem.api.player.CachedValues;
import de.eintosti.buildsystem.api.player.PlayerService;
import de.eintosti.buildsystem.api.storage.PlayerStorage;
import de.eintosti.buildsystem.api.world.BuildWorld;
import de.eintosti.buildsystem.api.world.data.Visibility;
import de.eintosti.buildsystem.api.world.display.NavigatorCategory;
import de.eintosti.buildsystem.config.Config.Settings;
import de.eintosti.buildsystem.config.Config.Settings.Navigator;
import de.eintosti.buildsystem.config.Config.World.Limits;
import de.eintosti.buildsystem.player.settings.SettingsManager;
import de.eintosti.buildsystem.storage.PlayerStorageImpl;
import de.eintosti.buildsystem.storage.WorldStorageImpl;
import de.eintosti.buildsystem.storage.factory.PlayerStorageFactory;
import de.eintosti.buildsystem.util.inventory.InventoryUtils;
import de.eintosti.buildsystem.world.navigator.ArmorStandManager;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletionException;
import java.util.logging.Level;
import net.md_5.bungee.api.ChatMessageType;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.bukkit.permissions.PermissionAttachmentInfo;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.Contract;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public class PlayerServiceImpl implements PlayerService {
private final BuildSystemPlugin plugin;
private final PlayerStorageImpl playerStorage;
private final Set<Player> openNavigator;
private final Set<UUID> buildModePlayers;
public PlayerServiceImpl(BuildSystemPlugin plugin) {
this.plugin = plugin;
this.playerStorage = new PlayerStorageFactory(plugin).createStorage();
this.openNavigator = new HashSet<>();
this.buildModePlayers = new HashSet<>();
initEntityChecker();
}
public void init() {
this.playerStorage.loadPlayers();
}
@Override
public PlayerStorage getPlayerStorage() {
return playerStorage;
}
public Set<Player> getOpenNavigator() {
return openNavigator;
}
@Override
public Set<UUID> getBuildModePlayers() {
return buildModePlayers;
}
@Override
public boolean isInBuildMode(Player player) {
return buildModePlayers.contains(player.getUniqueId());
}
@Override
public boolean canCreateWorld(Player player, Visibility visibility) {
boolean showPrivateWorlds = visibility == Visibility.PRIVATE;
WorldStorageImpl worldStorage = plugin.getWorldService().getWorldStorage();
int maxWorldAmountConfig = showPrivateWorlds
? Limits.privateWorlds
: Limits.publicWorlds;
if (maxWorldAmountConfig >= 0 && worldStorage.getBuildWorlds().size() >= maxWorldAmountConfig) {
return false;
}
int maxWorldAmountPlayer = getMaxWorlds(player, showPrivateWorlds ? Visibility.PRIVATE : Visibility.PUBLIC);
return maxWorldAmountPlayer < 0 || worldStorage.getBuildWorldsCreatedByPlayer(player, visibility).size() < maxWorldAmountPlayer;
}
@Override
public int getMaxWorlds(Player player, Visibility visibility) {
int max = -1;
if (player.hasPermission(BuildSystemPlugin.ADMIN_PERMISSION)) {
return max;
}
for (PermissionAttachmentInfo permission : player.getEffectivePermissions()) {
String permissionString = permission.getPermission();
String[] splitPermission = permissionString.split("\\.");
if (splitPermission.length != 4) {
continue;
}
if (!splitPermission[1].equalsIgnoreCase("create")) {
continue;
}
if (!splitPermission[2].equalsIgnoreCase(visibility.name())) {
continue;
}
String amountString = splitPermission[3];
if (amountString.equals("*")) {
return -1;
}
try {
int amount = Integer.parseInt(amountString);
if (amount > max) {
max = amount;
}
} catch (NumberFormatException e) {
plugin.getLogger().log(Level.WARNING, "Invalid max. world amount (must be int)", e);
}
}
return max;
}
/**
* Forces an update of the sidebar for all players in the given {@link BuildWorld}.
*
* @param buildWorld The world for which the sidebar should be updated
*/
public void forceUpdateSidebar(BuildWorld buildWorld) {
if (!Settings.scoreboard) {
return;
}
World bukkitWorld = Bukkit.getWorld(buildWorld.getName());
if (bukkitWorld == null) {
return;
}
bukkitWorld.getPlayers().forEach(this::forceUpdateSidebar);
}
/**
* Forces an update of the sidebar for a specific player.
*
* @param player The player for whom the sidebar should be updated
*/
public void forceUpdateSidebar(Player player) {
SettingsManager settingsManager = plugin.getSettingsManager();
if (!Settings.scoreboard || !settingsManager.getSettings(player).isScoreboard()) {
return;
}
settingsManager.updateScoreboard(player);
}
/**
* Gives the navigator item to the player if they have the permission and do not already have it in their inventory.
*
* @param player The player to whom the navigator item should be given
*/
public void giveNavigator(Player player) {
if (!Navigator.giveItemOnJoin || !player.hasPermission("buildsystem.navigator.item") || InventoryUtils.hasNavigator(player)) {
return;
}
ItemStack navigatorItem = InventoryUtils.createNavigatorItem(player);
PlayerInventory playerInventory = player.getInventory();
ItemStack slot8 = playerInventory.getItem(8);
if (slot8 == null || slot8.getType() == XMaterial.AIR.get()) {
playerInventory.setItem(8, navigatorItem);
} else {
playerInventory.addItem(navigatorItem);
}
}
/**
* Initializes the entity checker that periodically checks if players with open navigators are looking at armor stands representing navigator categories.
*/
private void initEntityChecker() {
Bukkit.getScheduler().runTaskTimer(plugin, this::checkForAmorStandNavigator, 0L, 2L);
}
/**
* Checks if players with open navigators are looking at armor stands that represent a {@link NavigatorCategory} and updates their UI accordingly.
* <p>
* This method iterates through all players who have the navigator open and determines if they are looking at an armor stand within the valid look height range. When a player
* looks at a different navigator category or stops looking at one entirely, appropriate UI updates are sent.
*/
private void checkForAmorStandNavigator() {
for (Player player : openNavigator) {
if (!(getTargetEntity(player) instanceof ArmorStand armorStand)) {
continue;
}
BuildPlayer buildPlayer = playerStorage.getBuildPlayer(player);
if (isLookingAtArmorStandHead(player, armorStand)) {
NavigatorCategory category = ArmorStandManager.matchNavigatorCategory(armorStand);
sendTypeInfo(player, category);
} else {
sendTypeInfo(player, null);
}
}
}
/**
* Checks if the player is looking at the head of an armor stand within a 3-block radius.
*
* @param player The player to check
* @param armorStand The armor stand to check against
* @return {@code true} if the player is looking at the armor stand's head, {@code false} otherwise
*/
private boolean isLookingAtArmorStandHead(Player player, ArmorStand armorStand) {
Location eyeLocation = player.getEyeLocation();
Vector direction = eyeLocation.getDirection().normalize();
Location standEyeLocation = armorStand.getEyeLocation().clone();
double boxHalfSize = 0.3;
double minX = standEyeLocation.getX() - boxHalfSize;
double maxX = standEyeLocation.getX() + boxHalfSize;
double minY = standEyeLocation.getY() - boxHalfSize;
double maxY = standEyeLocation.getY() + boxHalfSize;
double minZ = standEyeLocation.getZ() - boxHalfSize;
double maxZ = standEyeLocation.getZ() + boxHalfSize;
for (double distance = 0; distance <= 3; distance += 0.05) {
Vector point = eyeLocation.toVector().add(direction.clone().multiply(distance));
if (point.getX() >= minX && point.getX() <= maxX
&& point.getY() >= minY && point.getY() <= maxY
&& point.getZ() >= minZ && point.getZ() <= maxZ) {
return true;
}
}
return false;
}
/**
* Finds the closest target entity that the player is looking at within a 3-block radius.
*
* @param entity The entity from which to find the target
* @return The closest target entity that the given entity is looking at, or {@code null} if no suitable target is found
*/
@Nullable
private Entity getTargetEntity(Entity entity) {
return getTarget(entity, entity.getNearbyEntities(3, 3, 3));
}
/**
* Finds the closest target entity from a given entity collection within a specified threshold.
*
* @param entity The entity from which to find the target
* @param entities The collection of entities to search through
* @param <T> The type of entity to search for, which must extend {@link Entity}
* @return The closest target entity that the given entity is looking at, or {@code null} if no suitable target is found
*/
@Nullable
@Contract("null, _ -> null")
private <T extends Entity> T getTarget(@Nullable Entity entity, Iterable<T> entities) {
if (entity == null) {
return null;
}
T target = null;
final Location entityLocation = entity.getLocation();
final double threshold = 0.5;
for (T other : entities) {
final Location otherLocation = other.getLocation();
final Vector vector = otherLocation.toVector().subtract(entityLocation.toVector());
if (entityLocation.getDirection().normalize().crossProduct(vector).lengthSquared() < threshold
&& vector.normalize().dot(entityLocation.getDirection().normalize()) >= 0) {
if (target == null
|| target.getLocation().distanceSquared(entityLocation) > otherLocation.distanceSquared(entityLocation)) {
target = other;
}
}
}
return target;
}
/**
* Sends type information to the player based on the {@link NavigatorCategory} they are looking at in the navigator.
*
* @param player The player to whom the type information should be sent
* @param category The {@link NavigatorCategory} that the player is looking at, or {@code null} if they are not looking at any category
*/
private void sendTypeInfo(Player player, @Nullable NavigatorCategory category) {
BuildPlayer buildPlayer = playerStorage.getBuildPlayer(player);
if (category == null) {
buildPlayer.setLastLookedAt(null);
displayActionBarMessage(player, "§0");
return;
}
NavigatorCategory lastLookedAt = buildPlayer.getLastLookedAt();
if (lastLookedAt == null || lastLookedAt != category) {
buildPlayer.setLastLookedAt(category);
XSound.ENTITY_CHICKEN_EGG.play(player);
}
String message = switch (category) {
case PUBLIC -> "new_navigator_world_navigator";
case ARCHIVE -> "new_navigator_world_archive";
case PRIVATE -> "new_navigator_private_worlds";
};
displayActionBarMessage(player, Messages.getString(message, player));
}
private void displayActionBarMessage(Player player, String message) {
player.spigot().sendMessage(ChatMessageType.ACTION_BAR, new TextComponent(message));
}
/**
* Closes the new navigator for the given player.
* <p>
* This method removes the armor stands, resets the player's cached values, and updates the player's inventory. It also plays a sound effect and clears the action bar message.
*
* @param player The player whose navigator should be closed
*/
public void closeNewNavigator(Player player) {
if (!openNavigator.contains(player)) {
return;
}
BuildPlayer buildPlayer = playerStorage.getBuildPlayer(player);
buildPlayer.setLastLookedAt(null);
plugin.getArmorStandManager().removeArmorStands(player);
XSound.ENTITY_ITEM_BREAK.play(player);
displayActionBarMessage(player, "");
InventoryUtils.replaceItem(player, Messages.getString("barrier_item", player), XMaterial.BARRIER, InventoryUtils.createNavigatorItem(player));
CachedValues cachedValues = buildPlayer.getCachedValues();
cachedValues.resetWalkSpeedIfPresent(player);
cachedValues.resetFlySpeedIfPresent(player);
player.removePotionEffect(XPotion.JUMP_BOOST.get());
player.removePotionEffect(XPotion.BLINDNESS.get());
openNavigator.remove(player);
}
public void save() {
this.playerStorage
.save(this.playerStorage.getBuildPlayers())
.exceptionally(e -> {
plugin.getLogger().log(Level.SEVERE, "Failed to save player data", e);
throw new CompletionException("Failed to save player data", e);
});
}
}
@@ -0,0 +1,126 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.player.customblock;
import de.eintosti.buildsystem.BuildSystemPlugin;
import java.util.Locale;
import org.bukkit.NamespacedKey;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.jetbrains.annotations.Contract;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public enum CustomBlock {
BARRIER("blocks_barrier", "3ed1aba73f639f4bc42bd48196c715197be2712c3b962c97ebf9e9ed8efa025"),
BROWN_MUSHROOM("blocks_brown_mushroom", "fa49eca0369d1e158e539d78149acb1572949b88ba921d9ee694fea4c726b3"),
BURNING_FURNACE("blocks_burning_furnace", "d17b8b43f8c4b5cfeb919c9f8fe93f26ceb6d2b133c2ab1eb339bd6621fd309c"),
COMMAND_BLOCK("blocks_command_block", "8514d225b262d847c7e557b474327dcef758c2c5882e41ee6d8c5e9cd3bc914"),
DOUBLE_STONE_SLAB("blocks_double_stone_slab", "151e70169ea00f04a9439221cf33770844159dd775fc8830e311fd9b5ccd2969"),
DRAGON_EGG("blocks_dragon_egg", "3c151fb54b21fe5769ffb4825b5bc92da73657f214380e5d0301e45b6c13f7d"),
END_PORTAL("blocks_end_portal", "7840b87d52271d2a755dedc82877e0ed3df67dcc42ea479ec146176b02779a5"),
FULL_ACACIA_BARCH("blocks_full_acacia_barch", "96a3bba2b7a2b4fa46945b1471777abe4599695545229e782259aed41d6"),
FULL_BIRCH_BARCH("blocks_full_birch_barch", "a221f813dacee0fef8c59f76894dbb26415478d9ddfc44c2e708a6d3b7549b"),
FULL_DARK_OAK_BARCH("blocks_full_dark_oak_barch", "cde9d4e4c343afdb3ed68038450fc6a67cd208b2efc99fb622c718d24aac"),
FULL_JUNGLE_BARCH("blocks_full_jungle_barch", "1cefc19380683015e47c666e5926b15ee57ab33192f6a7e429244cdffcc262"),
FULL_MUSHROOM_STEM("blocks_full_mushroom_stem", "f55fa642d5ebcba2c5246fe6499b1c4f6803c10f14f5299c8e59819d5dc"),
FULL_OAK_BARCH("blocks_full_oak_barch", "22e4bb979efefd2ddb3f8b1545e59cd360492e12671ec371efc1f88af21ab83"),
FULL_SPRUCE_BARCH("blocks_full_spruce_barch", "966cbdef8efb914d43a213be66b5396f75e5c1b9124f76f67d7cd32525748"),
INVISIBLE_ITEM_FRAME("blocks_invisible_item_frame", "8122a503d7a6f57802b03af7624194a4c4f5077a99ae21dd276ce7db88bc38ae"),
MOB_SPAWNER("blocks_mob_spawner", "db6bd9727abb55d5415265789d4f2984781a343c68dcaf57f554a5e9aa1cd"),
MUSHROOM_BLOCK("blocks_mushroom_block", "3fa39ccf4788d9179a8795e6b72382d49297b39217146eda68ae78384355b13"),
MUSHROOM_STEM("blocks_mushroom_stem", "84d541275c7f924bcb9eb2dbbf4b866b7649c330a6a013b53d584fd4ddf186ca"),
NETHER_PORTAL("blocks_nether_portal", "b0bfc2577f6e26c6c6f7365c2c4076bccee653124989382ce93bca4fc9e39b"),
PISTON_HEAD("blocks_piston_head", "aa868ce917c09af8e4c350a5807041f6509bf2b89aca45e591fbbd7d4b117d"),
POWERED_REDSTONE_LAMP("blocks_powered_redstone_lamp", "7eb4b34519fe15847dbea7229179feeb6ea57712d165dcc8ff6b785bb58911b0"),
RED_MUSHROOM("blocks_red_mushroom", "732dbd6612e9d3f42947b5ca8785bfb334258f3ceb83ad69a5cdeebea4cd65"),
SMOOTH_RED_SANDSTONE("blocks_smooth_red_sandstone", "a2da7aa1ae6cc9d6c36c18a460d2398162edc2207fdfc9e28a7bf84d7441b8a2"),
SMOOTH_SANDSTONE("blocks_smooth_sandstone", "38fffbb0b8fdec6f62b17c451ab214fb86e4e355b116be961a9ae93eb49a43"),
SMOOTH_STONE("blocks_smooth_stone", "8dd0cd158c2bb6618650e3954b2d29237f5b4c0ddc7d258e17380ab6979f071"),
DEBUG_STICK("blocks_debug_stick", "badc048a7ce78f7dad72a07da27d85c0916881e5522eeed1e3daf217a38c1a");
public static final NamespacedKey CUSTOM_BLOCK_KEY = new NamespacedKey(BuildSystemPlugin.get(), "custom-block");
private final String messageKey;
private final String skullUrl;
CustomBlock(String messageKey, String skullUrl) {
this.messageKey = messageKey;
this.skullUrl = skullUrl;
}
@Nullable
@Contract("null -> null")
public static CustomBlock of(@Nullable ItemStack itemStack) {
if (itemStack == null) {
return null;
}
ItemMeta itemMeta = itemStack.getItemMeta();
if (itemMeta == null) {
return null;
}
String identifier = itemMeta.getPersistentDataContainer().get(CUSTOM_BLOCK_KEY, PersistentDataType.STRING);
if (identifier == null) {
return null;
}
String customBlock = identifier.toUpperCase(Locale.ROOT);
try {
return CustomBlock.valueOf(customBlock);
} catch (IllegalArgumentException e) {
return null;
}
}
/**
* Gets the message key for this custom block.
*
* @return The message key associated with this custom block
*/
public String getMessageKey() {
return messageKey;
}
/**
* Gets the URL for the skull texture of this custom block.
*
* @return The URL of the skull texture for this custom block
*/
public String getSkullUrl() {
return skullUrl;
}
/**
* Stores the custom block identifier in the provided item's {@link PersistentDataContainer}.
*
* @param itemStack The item to store the custom block identifier in
* @return The modified item stack with the custom block identifier stored
*/
public ItemStack storeCustomBlock(ItemStack itemStack) {
ItemMeta itemMeta = itemStack.getItemMeta();
itemMeta.getPersistentDataContainer().set(CUSTOM_BLOCK_KEY, PersistentDataType.STRING, this.name());
itemStack.setItemMeta(itemMeta);
return itemStack;
}
}
@@ -0,0 +1,166 @@
/*
* Copyright (c) 2018-2025, Thomas Meaney
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.eintosti.buildsystem.player.customblock;
import com.cryptomorin.xseries.XEnchantment;
import com.cryptomorin.xseries.XMaterial;
import com.cryptomorin.xseries.profiles.objects.Profileable;
import de.eintosti.buildsystem.BuildSystemPlugin;
import de.eintosti.buildsystem.Messages;
import de.eintosti.buildsystem.util.inventory.BuildSystemHolder;
import de.eintosti.buildsystem.util.inventory.InventoryHandler;
import de.eintosti.buildsystem.util.inventory.InventoryManager;
import de.eintosti.buildsystem.util.inventory.InventoryUtils;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class CustomBlockInventory implements InventoryHandler {
private final InventoryManager inventoryManager;
public CustomBlockInventory(BuildSystemPlugin plugin) {
this.inventoryManager = plugin.getInventoryManager();
}
public void openInventory(Player player) {
Inventory inventory = getInventory(player);
this.inventoryManager.registerInventoryHandler(inventory, this);
player.openInventory(inventory);
}
private Inventory getInventory(Player player) {
Inventory inventory = new BlocksInventoryHolder(player).getInventory();
fillGuiWithGlass(player, inventory);
setCustomBlock(inventory, player, 1, CustomBlock.FULL_OAK_BARCH);
setCustomBlock(inventory, player, 2, CustomBlock.FULL_SPRUCE_BARCH);
setCustomBlock(inventory, player, 3, CustomBlock.FULL_BIRCH_BARCH);
setCustomBlock(inventory, player, 4, CustomBlock.FULL_JUNGLE_BARCH);
setCustomBlock(inventory, player, 5, CustomBlock.FULL_ACACIA_BARCH);
setCustomBlock(inventory, player, 6, CustomBlock.FULL_DARK_OAK_BARCH);
setCustomBlock(inventory, player, 10, CustomBlock.RED_MUSHROOM);
setCustomBlock(inventory, player, 11, CustomBlock.BROWN_MUSHROOM);
setCustomBlock(inventory, player, 12, CustomBlock.FULL_MUSHROOM_STEM);
setCustomBlock(inventory, player, 13, CustomBlock.MUSHROOM_STEM);
setCustomBlock(inventory, player, 14, CustomBlock.MUSHROOM_BLOCK);
setCustomBlock(inventory, player, 19, CustomBlock.SMOOTH_STONE);
setCustomBlock(inventory, player, 20, CustomBlock.DOUBLE_STONE_SLAB);
setCustomBlock(inventory, player, 21, CustomBlock.SMOOTH_SANDSTONE);
setCustomBlock(inventory, player, 22, CustomBlock.SMOOTH_RED_SANDSTONE);
setCustomBlock(inventory, player, 28, CustomBlock.POWERED_REDSTONE_LAMP);
setCustomBlock(inventory, player, 29, CustomBlock.BURNING_FURNACE);
setCustomBlock(inventory, player, 30, CustomBlock.PISTON_HEAD);
setCustomBlock(inventory, player, 31, CustomBlock.COMMAND_BLOCK);
setCustomBlock(inventory, player, 32, CustomBlock.BARRIER);
setCustomBlock(inventory, player, 33, CustomBlock.INVISIBLE_ITEM_FRAME);
setCustomBlock(inventory, player, 37, CustomBlock.MOB_SPAWNER);
setCustomBlock(inventory, player, 38, CustomBlock.NETHER_PORTAL);
setCustomBlock(inventory, player, 39, CustomBlock.END_PORTAL);
setCustomBlock(inventory, player, 40, CustomBlock.DRAGON_EGG);
setCustomBlock(inventory, player, 41, CustomBlock.DEBUG_STICK);
return inventory;
}
private void setCustomBlock(Inventory inventory, Player player, int position, CustomBlock customBlock) {
inventory.setItem(position, InventoryUtils.createSkull(Messages.getString(customBlock.getMessageKey(), player), Profileable.detect(customBlock.getSkullUrl())));
}
private void fillGuiWithGlass(Player player, Inventory inventory) {
int[] glassSlots = {0, 8, 9, 17, 18, 26, 27, 35, 36, 44};
for (int i : glassSlots) {
InventoryUtils.addGlassPane(player, inventory, i);
}
}
@Override
public void onClick(InventoryClickEvent event) {
if (!(event.getInventory().getHolder() instanceof BlocksInventoryHolder)) {
return;
}
event.setCancelled(true);
Player player = (Player) event.getWhoClicked();
switch (event.getSlot()) {
case 1 -> giveCustomBlock(player, CustomBlock.FULL_OAK_BARCH);
case 2 -> giveCustomBlock(player, CustomBlock.FULL_SPRUCE_BARCH);
case 3 -> giveCustomBlock(player, CustomBlock.FULL_BIRCH_BARCH);
case 4 -> giveCustomBlock(player, CustomBlock.FULL_JUNGLE_BARCH);
case 5 -> giveCustomBlock(player, CustomBlock.FULL_ACACIA_BARCH);
case 6 -> giveCustomBlock(player, CustomBlock.FULL_DARK_OAK_BARCH);
case 10 -> giveCustomBlock(player, CustomBlock.RED_MUSHROOM);
case 11 -> giveCustomBlock(player, CustomBlock.BROWN_MUSHROOM);
case 12 -> giveCustomBlock(player, CustomBlock.FULL_MUSHROOM_STEM);
case 13 -> giveCustomBlock(player, CustomBlock.MUSHROOM_STEM);
case 14 -> giveCustomBlock(player, CustomBlock.MUSHROOM_BLOCK);
case 19 -> giveCustomBlock(player, CustomBlock.SMOOTH_STONE);
case 20 -> giveCustomBlock(player, CustomBlock.DOUBLE_STONE_SLAB);
case 21 -> giveCustomBlock(player, CustomBlock.SMOOTH_SANDSTONE);
case 22 -> giveCustomBlock(player, CustomBlock.SMOOTH_RED_SANDSTONE);
case 28 -> giveCustomBlock(player, CustomBlock.POWERED_REDSTONE_LAMP);
case 29 -> giveCustomBlock(player, CustomBlock.BURNING_FURNACE);
case 30 -> giveCustomBlock(player, CustomBlock.PISTON_HEAD);
case 31 -> giveCustomBlock(player, CustomBlock.COMMAND_BLOCK);
case 32 -> giveCustomBlock(player, CustomBlock.BARRIER, XMaterial.BARRIER);
case 33 -> giveCustomBlock(player, CustomBlock.INVISIBLE_ITEM_FRAME, XMaterial.ITEM_FRAME);
case 37 -> giveCustomBlock(player, CustomBlock.MOB_SPAWNER);
case 38 -> giveCustomBlock(player, CustomBlock.NETHER_PORTAL);
case 39 -> giveCustomBlock(player, CustomBlock.END_PORTAL);
case 40 -> giveCustomBlock(player, CustomBlock.DRAGON_EGG);
case 41 -> giveCustomBlock(player, CustomBlock.DEBUG_STICK, XMaterial.DEBUG_STICK);
}
}
private void giveCustomBlock(Player player, CustomBlock customBlock) {
giveCustomBlock(player, customBlock, XMaterial.PLAYER_HEAD);
}
private void giveCustomBlock(Player player, CustomBlock customBlock, XMaterial material) {
ItemStack itemStack;
if (material == XMaterial.PLAYER_HEAD) {
itemStack = InventoryUtils.createSkull(Messages.getString(customBlock.getMessageKey(), player), Profileable.detect(customBlock.getSkullUrl()));
} else {
itemStack = InventoryUtils.createItem(material, Messages.getString(customBlock.getMessageKey(), player));
ItemMeta itemMeta = itemStack.getItemMeta();
itemMeta.addEnchant(XEnchantment.UNBREAKING.get(), 1, true);
itemStack.setItemMeta(itemMeta);
}
player.getInventory().addItem(customBlock.storeCustomBlock(itemStack));
}
private static class BlocksInventoryHolder extends BuildSystemHolder {
public BlocksInventoryHolder(Player player) {
super(45, Messages.getString("blocks_title", player));
}
}
}

Some files were not shown because too many files have changed in this diff Show More