Compare commits

...

4 commits

Author SHA1 Message Date
Nick Guy
7936a525b8 Add Hardcore mode with player elimination mechanics.
- Introduced Hardcore mode toggle via `/skyblock hardcore` command.
- Implemented player elimination and spectator mode handling upon death.
- Added mechanics to clear Hardcore status during player reset.
- Enhanced respawn logic to respect island or Hardcore mode settings.
- Registered `HardcoreHandler` for relevant events.
2025-08-11 19:02:50 +01:00
Nick Guy
3edf908b89 Add dialog-based confirmation for island teleportation.
- Introduced player dialog system for confirmation prompts.
- Enhanced teleportation logic to include cancel and confirm options.
- Refactored inventory checks and confirmation flow for better usability.
- Added utility for command-style text components.
2025-08-11 18:23:47 +01:00
Nick Guy
6332a26ce0 Add inventory check and confirmation for island teleportation.
- Introduced inventory validation to prevent accidental item loss.
- Added `/skyblock confirm` command for explicit teleport confirmation.
2025-08-11 17:53:07 +01:00
Nick Guy
e67f70af56 Bump version number to target MC 1.21.8 2025-08-11 17:43:42 +01:00
8 changed files with 318 additions and 24 deletions

View file

@ -30,7 +30,7 @@ repositories {
}
dependencies {
compileOnly("io.papermc.paper:paper-api:1.21.5-R0.1-SNAPSHOT")
compileOnly("io.papermc.paper:paper-api:${minecraft_version}${paper_version_suffix}")
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
@ -44,7 +44,7 @@ tasks {
// Configure the Minecraft version for our task.
// This is the only required configuration besides applying the plugin.
// Your plugin's jar (or shadowJar if present) will be used automatically.
minecraftVersion("1.21.5")
minecraftVersion("${minecraft_version}")
}
}

View file

@ -0,0 +1,2 @@
minecraft_version=1.21.8
paper_version_suffix=-R0.1-SNAPSHOT

View file

@ -4,6 +4,7 @@ import com.ncguy.usefulskyblock.pdc.AnonymousDataContainerRef;
import com.ncguy.usefulskyblock.pdc.IDataContainerRef;
import com.ncguy.usefulskyblock.pdc.VectorPersistentDataType;
import io.papermc.paper.datacomponent.DataComponentType;
import net.kyori.adventure.key.Key;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.block.Block;
@ -21,6 +22,9 @@ public class Reference {
return new NamespacedKey("usefulskyblock", key);
}
public static Key DIALOG_AGREE = key("dialog/agree");
public static Key DIALOG_DISAGREE = key("dialog/disagree");
public static IDataContainerRef<BlockVector, World> SKYBLOCK_TEAM_ROOT = new AnonymousDataContainerRef<>(key("world.skyblock.team"), VectorPersistentDataType.Instance);
public static IDataContainerRef<Integer, World> SKYBLOCK_TEAM_COUNT = SKYBLOCK_TEAM_ROOT.withSuffix(".count").withType(PersistentDataType.INTEGER);
public static IDataContainerRef<Boolean, World> WORLD_INIT = new AnonymousDataContainerRef<>(key("world.init"), PersistentDataType.BOOLEAN);
@ -29,4 +33,7 @@ public class Reference {
public static IDataContainerRef<Boolean, Item> ITEM_LAVA_IMMUNE = new AnonymousDataContainerRef<>(key("item.lava.immune"), PersistentDataType.BOOLEAN);
public static IDataContainerRef<Boolean, Player> HARDCORE_ENABLED = new AnonymousDataContainerRef<>(key("player.hardcore.enabled"), PersistentDataType.BOOLEAN);
public static IDataContainerRef<Boolean, Player> HARDCORE_ELIMINATED = new AnonymousDataContainerRef<>(key("player.hardcore.eliminated"), PersistentDataType.BOOLEAN);
}

View file

@ -6,7 +6,22 @@ import com.ncguy.usefulskyblock.recipe.BiomeRod;
import com.ncguy.usefulskyblock.recipe.IRecipeProvider;
import com.ncguy.usefulskyblock.recipe.SmeltingCraftingHandler;
import com.ncguy.usefulskyblock.world.PortalHandler;
import io.papermc.paper.connection.PlayerCommonConnection;
import io.papermc.paper.dialog.Dialog;
import io.papermc.paper.event.player.PlayerCustomClickEvent;
import io.papermc.paper.registry.RegistryBuilderFactory;
import io.papermc.paper.registry.data.dialog.ActionButton;
import io.papermc.paper.registry.data.dialog.DialogBase;
import io.papermc.paper.registry.data.dialog.DialogRegistryEntry;
import io.papermc.paper.registry.data.dialog.action.DialogAction;
import io.papermc.paper.registry.data.dialog.type.DialogType;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.*;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.inventory.Recipe;
import org.bukkit.plugin.PluginManager;
@ -15,11 +30,22 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
public final class UsefulSkyblock extends JavaPlugin implements Listener {
private static final Logger log = LoggerFactory.getLogger(UsefulSkyblock.class);
public UsefulSkyblock() {
dialogMaps = new java.util.concurrent.ConcurrentHashMap<>();
}
public static UsefulSkyblock instance() {
return getPlugin(UsefulSkyblock.class);
}
@Override
public void onEnable() {
saveDefaultConfig();
@ -42,6 +68,7 @@ public final class UsefulSkyblock extends JavaPlugin implements Listener {
pluginManager.registerEvents(new InitialisationHandler(), this);
pluginManager.registerEvents(new TeamProgressHandler(), this);
pluginManager.registerEvents(new FishingHandler(), this);
pluginManager.registerEvents(new HardcoreHandler(), this);
Server server = Bukkit.getServer();
for (int i = recipeProviders.length-1; i >= 0; i--) {
@ -69,4 +96,43 @@ public final class UsefulSkyblock extends JavaPlugin implements Listener {
// Plugin shutdown logic
}
private final Map<PlayerCommonConnection, CompletableFuture<Boolean>> dialogMaps;
@EventHandler
void onHandleDialog(PlayerCustomClickEvent event) {
Key key = event.getIdentifier();
CompletableFuture<Boolean> f = dialogMaps.get(event.getCommonConnection());
Boolean res = null;
if(key.equals(Reference.DIALOG_AGREE))
res = true;
else if(key.equals(Reference.DIALOG_DISAGREE))
res = false;
if(res != null && f != null) {
f.complete(res);
dialogMaps.remove(event.getCommonConnection());
}
}
public CompletableFuture<Boolean> confirm(Player player, Component base, Consumer<DialogBase.Builder> builder) {
var confirmBtn = ActionButton.builder(Component.text("Confirm", NamedTextColor.GREEN)).tooltip(Component.text("Click to confirm")).action(DialogAction.customClick(Reference.DIALOG_AGREE, null)).build();
var cancelBtn = ActionButton.builder(Component.text("Cancel", NamedTextColor.RED)).tooltip(Component.text("Click to cancel")).action(DialogAction.customClick(Reference.DIALOG_DISAGREE, null)).build();
Dialog dialog = Dialog.create(innerBuilder -> {
DialogBase.Builder baseBuilder = DialogBase.builder(base);
baseBuilder.canCloseWithEscape(false);
builder.accept(baseBuilder);
innerBuilder.empty()
.base(baseBuilder.build())
.type(DialogType.confirmation(confirmBtn, cancelBtn));
});
player.showDialog(dialog);
CompletableFuture<Boolean> f = new CompletableFuture<>();
dialogMaps.put(player.getConnection(), f);
return f;
}
}

View file

@ -3,21 +3,23 @@ package com.ncguy.usefulskyblock.command;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import com.mojang.brigadier.tree.LiteralCommandNode;
import com.ncguy.usefulskyblock.Advancements;
import com.ncguy.usefulskyblock.IslandNetworkGenerator;
import com.ncguy.usefulskyblock.Reference;
import com.ncguy.usefulskyblock.StructureRef;
import com.ncguy.usefulskyblock.*;
import com.ncguy.usefulskyblock.data.BiomedStructure;
import com.ncguy.usefulskyblock.utils.MathsUtils;
import com.ncguy.usefulskyblock.pdc.IDataContainerRef;
import com.ncguy.usefulskyblock.utils.Components;
import com.ncguy.usefulskyblock.utils.TextUtils;
import io.papermc.paper.command.brigadier.CommandSourceStack;
import io.papermc.paper.command.brigadier.Commands;
import io.papermc.paper.command.brigadier.argument.ArgumentTypes;
import io.papermc.paper.command.brigadier.argument.CustomArgumentType;
import io.papermc.paper.command.brigadier.argument.resolvers.selector.PlayerSelectorArgumentResolver;
import io.papermc.paper.registry.data.dialog.body.DialogBody;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextColor;
@ -30,6 +32,8 @@ import org.bukkit.block.structure.StructureRotation;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.inventory.PlayerInventory;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scoreboard.Scoreboard;
import org.bukkit.scoreboard.ScoreboardManager;
import org.bukkit.scoreboard.Team;
@ -37,10 +41,10 @@ import org.bukkit.util.BlockVector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.Supplier;
import static com.ncguy.usefulskyblock.Reference.key;
@ -133,7 +137,8 @@ public class SkyblockGenCommand extends AbstractSkyblockCommand {
teamSpawn.set(tpLoc.toVector().toBlockVector());
worldTeamCount.set(count + 1);
executor.teleport(tpLoc);
player.teleport(tpLoc);
player.setGameMode(GameMode.SURVIVAL);
islandHomeLoc.set(tpLoc.toVector().toBlockVector());
Advancement a = Advancements.skyblock.SKYBLOCK_BEGIN;
@ -217,22 +222,52 @@ public class SkyblockGenCommand extends AbstractSkyblockCommand {
return gen.generateIslandNetwork(origin, def);
}
private int executeRoot(CommandContext<CommandSourceStack> ctx) {
private int executeRoot(CommandContext<CommandSourceStack> ctx, boolean confirmationGiven) {
Entity executor = ctx.getSource().getExecutor();
if (!(executor instanceof Player)) return 0;
Player player = (Player) executor;
var islandHomeLoc = Reference.ISLAND_HOME_LOC.assign(player);
World overworld = getServer().getWorld(new NamespacedKey("minecraft", "overworld"));
if (islandHomeLoc.has()) {
Runnable doTeleport = () -> {
executor.sendMessage("Teleported to island home..");
BlockVector blockVector = islandHomeLoc.get();
Location loc = new Location(overworld, blockVector.getX(), blockVector.getY(), blockVector.getZ());
if (!player.teleport(loc, PlayerTeleportEvent.TeleportCause.COMMAND)) {
player.sendMessage("Failed to teleport you to your island home, please notify an administrator");
return -1;
return;
}
player.getInventory().clear();
if (player.getGameMode() != GameMode.SURVIVAL)
player.setGameMode(GameMode.SURVIVAL);
};
Runnable tryTeleport = () -> {
if (!isInventoryEmpty(confirmationGiven, executor, player)) {
JavaPlugin.getPlugin(UsefulSkyblock.class)
.confirm(player, Component.text("Teleporting to an island will clear your inventory"), b -> {
b.body(List.of(
DialogBody.plainMessage(Component.text("Know that this action is irreversible")),
DialogBody.plainMessage(Component.newline()),
DialogBody.plainMessage(Component.text("To skip this notice in future, use the command variant ")
.append(Components.Command("/skyblock confirm"))
.append(Component.text(".")))
));
}).thenAccept(res -> {
if (res) {
doTeleport.run();
} else {
player.sendMessage("Teleport cancelled");
}
});
return;
}
doTeleport.run();
};
if (islandHomeLoc.has()) {
tryTeleport.run();
return 0;
}
@ -251,13 +286,7 @@ public class SkyblockGenCommand extends AbstractSkyblockCommand {
var teamSpawn = Reference.SKYBLOCK_TEAM_ROOT.withSuffix("." + sanitizedTeamName).assign(overworld);
if (teamSpawn.has()) {
islandHomeLoc.set(teamSpawn.get());
executor.sendMessage("Teleported to island home..");
BlockVector blockVector = islandHomeLoc.get();
Location loc = new Location(overworld, blockVector.getX(), blockVector.getY(), blockVector.getZ());
if (!player.teleport(loc, PlayerTeleportEvent.TeleportCause.COMMAND)) {
player.sendMessage("Failed to teleport you to your island home, please notify an administrator");
return -1;
}
tryTeleport.run();
return 0;
}
@ -268,6 +297,23 @@ public class SkyblockGenCommand extends AbstractSkyblockCommand {
return 0;
}
private boolean isInventoryEmpty(boolean confirmationGiven, Entity executor, Player player) {
PlayerInventory inventory = player.getInventory();
boolean hasArmour = Arrays.stream(inventory.getArmorContents()).anyMatch(Objects::nonNull);
boolean hasItemInOffhand = inventory.getItemInOffHand().getType() != Material.AIR;
boolean hasItemInInventory = Arrays.stream(inventory.getContents()).anyMatch(Objects::nonNull);
if (hasArmour || hasItemInOffhand || hasItemInInventory) {
if (!confirmationGiven) {
executor.sendMessage(Component.text("You have items in your inventory, teleporting you to your island will remove them."));
executor.sendMessage(Component.text("If you wish to keep your items, please confirm via the command ")
.append(Component.text("/skyblock confirm", Style.style(TextColor.color(0x00, 0xff, 0xff)))));
return false;
}
}
return true;
}
public static LiteralCommandNode<CommandSourceStack> create() {
var root = Commands.literal("skyblock");
@ -295,11 +341,80 @@ public class SkyblockGenCommand extends AbstractSkyblockCommand {
.executes(cmd::executeTeamRemove)));
root.then(team);
root.executes(cmd::executeRoot);
root.executes(cmd::executeRootNoConfirm);
root.then(Commands.literal("confirm").executes(cmd::executeRootConfirm));
LiteralArgumentBuilder<CommandSourceStack> hardcore = Commands.literal("hardcore");
hardcore.executes(cmd::executeToggleHardcore);
hardcore.then(Commands.argument("player", ArgumentTypes.player()).requires(cmd::auth).executes(cmd::executeToggleHardcoreForPlayer));
root.then(hardcore);
return root.build();
}
private void toggleHardcoreForPlayer(Player player, boolean skipConfirmationForEnable) {
if (Reference.HARDCORE_ENABLED.assign(player).getOrDefault(false))
disableHardcore(player);
else
enableHardcore(player, skipConfirmationForEnable);
}
private void disableHardcore(Player player) {
player.sendMessage(Component.text("Hardcore mode disabled"));
Reference.HARDCORE_ENABLED.assign(player).set(false);
Reference.HARDCORE_ELIMINATED.assign(player).set(false);
}
private void enableHardcore(Player player, boolean skipConfirmation) {
Runnable _enableHardcore = () -> {
player.sendMessage(Component.text("Hardcore mode enabled"));
Reference.HARDCORE_ENABLED.assign(player).set(true);
Reference.HARDCORE_ELIMINATED.assign(player).set(false);
};
if(skipConfirmation) {
_enableHardcore.run();
return;
}
UsefulSkyblock.instance().confirm(player, Component.text("Enabling hardcore mode"), b -> {})
.thenAccept(res -> {
if (res) {
_enableHardcore.run();
} else {
player.sendMessage(Component.text("Hardcore mode not enabled"));
}
});
}
private int executeToggleHardcoreForPlayer(CommandContext<CommandSourceStack> ctx) {
PlayerSelectorArgumentResolver selector = ctx.getArgument("player", PlayerSelectorArgumentResolver.class);
Player player = null;
try {
player = selector.resolve(ctx.getSource()).getFirst();
toggleHardcoreForPlayer(player, true);
} catch (CommandSyntaxException e) {
throw new RuntimeException(e);
}
return 0;
}
private int executeToggleHardcore(CommandContext<CommandSourceStack> ctx) {
Entity executor = ctx.getSource().getExecutor();
if (!(executor instanceof Player player)) return -1;
toggleHardcoreForPlayer(player, false);
return 0;
}
private int executeRootConfirm(CommandContext<CommandSourceStack> ctx) {
return executeRoot(ctx, true);
}
private int executeRootNoConfirm(CommandContext<CommandSourceStack> ctx) {
return executeRoot(ctx, false);
}
private int executeGetProgress(CommandContext<CommandSourceStack> ctx) {
Entity executor = ctx.getSource().getExecutor();
if (!(executor instanceof Player player)) return -1;
@ -542,6 +657,13 @@ public class SkyblockGenCommand extends AbstractSkyblockCommand {
prg.getAwardedCriteria().forEach(prg::revokeCriteria);
}
// Clear hardcore status
IDataContainerRef<Boolean, Player> assign = Reference.HARDCORE_ENABLED.assign(player);
if(assign.getOrDefault(false))
player.sendMessage("Hardcore status cleared.");
assign.remove();
Reference.HARDCORE_ELIMINATED.assign(player).remove();
return 0;
}

View file

@ -0,0 +1,54 @@
package com.ncguy.usefulskyblock.handlers;
import com.ncguy.usefulskyblock.Reference;
import com.ncguy.usefulskyblock.UsefulSkyblock;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.player.PlayerGameModeChangeEvent;
import org.bukkit.event.player.PlayerRespawnEvent;
public class HardcoreHandler implements Listener {
private boolean isPlayerHardcore(Player player) {
return Reference.HARDCORE_ENABLED.assign(player).getOrDefault(false);
}
private boolean isPlayerEliminated(Player player) {
return Reference.HARDCORE_ELIMINATED.assign(player).getOrDefault(false);
}
private void eliminate(Player player) {
Reference.HARDCORE_ELIMINATED.assign(player).set(true);
}
@EventHandler
public void onPlayerDeath(PlayerDeathEvent event) {
Player player = event.getPlayer();
if(!isPlayerHardcore(player)) return;
eliminate(player);
}
@EventHandler
public void onRespawn(PlayerRespawnEvent e) {
Player p = e.getPlayer();
if (isPlayerHardcore(p) && isPlayerEliminated(p) && p.getGameMode() != GameMode.SPECTATOR) {
// Delay gamemode change by 1 tick to avoid race conflicts
Bukkit.getScheduler().runTask(UsefulSkyblock.instance(), () -> p.setGameMode(GameMode.SPECTATOR));
}
}
@EventHandler
public void onGamemodeChange(PlayerGameModeChangeEvent e) {
Player p = e.getPlayer();
if (isPlayerHardcore(p) && isPlayerEliminated(p) && e.getNewGameMode() != GameMode.SPECTATOR) {
e.setCancelled(true);
p.sendMessage("You are eliminated in Hardcore and cannot change gamemode.");
}
}
}

View file

@ -14,6 +14,7 @@ import org.bukkit.block.structure.StructureRotation;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.event.server.ServerLoadEvent;
@ -23,6 +24,7 @@ import org.bukkit.persistence.PersistentDataType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Objects;
import java.util.Random;
import static com.ncguy.usefulskyblock.Reference.key;
@ -45,19 +47,47 @@ public class InitialisationHandler implements Listener {
player.setRespawnLocation(world.getSpawnLocation());
}
@EventHandler
public void onPlayerDeath(PlayerDeathEvent event) {
Player player = event.getPlayer();
Location resLoc = player.getRespawnLocation();
if(resLoc != null)
return;
var islandHome = Reference.ISLAND_HOME_LOC.assign(player);
if(!islandHome.has())
return;
World overworld = Bukkit.getWorld(NamespacedKey.minecraft("overworld"));
player.setRespawnLocation(islandHome.get().toLocation(Objects.requireNonNull(overworld)));
}
@EventHandler
public void onPlayerPostRespawn(PlayerPostRespawnEvent event) {
Player player = event.getPlayer();
player.sendMessage(Component.text("Hello, " + player.getName() + "!"));
World overworld = Objects.requireNonNull(Bukkit.getWorld(NamespacedKey.minecraft("overworld")));
var islandHome = Reference.ISLAND_HOME_LOC.assign(player);
if(islandHome.has()) {
// TODO Handle respawning and such, especially when bed is missing
Location respawnLocation = player.getRespawnLocation();
if(respawnLocation == null) {
player.teleport(islandHome.get().toLocation(overworld), PlayerTeleportEvent.TeleportCause.PLUGIN);
return;
}
System.out.println("Respawn location: " + respawnLocation);
System.out.println("Island home: " + islandHome.get());
System.out.println("World respawn location: " + player.getWorld().getSpawnLocation());
return;
}
// If the player doesn't have a home island, respawn them in the server lobby
World world = Bukkit.getWorld(key("void"));
World world = Objects.requireNonNull(Bukkit.getWorld(key("void")));
player.teleport(world.getSpawnLocation(), PlayerTeleportEvent.TeleportCause.PLUGIN);
player.setRespawnLocation(world.getSpawnLocation());
}

View file

@ -0,0 +1,13 @@
package com.ncguy.usefulskyblock.utils;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextColor;
public class Components {
public static Component Command(String command) {
return Component.text(command, Style.style(TextColor.color(0x00, 0xff, 0xff)));
}
}