Made the vote clickable

This commit is contained in:
2026-06-09 20:45:18 -04:00
parent 9cb21f03cf
commit 535af85ecb
11 changed files with 336 additions and 145 deletions
@@ -1,5 +1,8 @@
package com.bitnix.dirtsleep;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.GameMode;
@@ -9,14 +12,14 @@ import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitTask;
import java.util.*;
import java.util.stream.Collectors;
public final class DirtSleepPlugin extends JavaPlugin implements TabExecutor {
private final Map<String, Set<UUID>> dayVotes = new HashMap<>();
private final Map<String, Set<UUID>> nightVotes = new HashMap<>();
private final Map<String, ActiveVote> activeVotes = new HashMap<>();
@Override
public void onEnable() {
@@ -34,8 +37,12 @@ public final class DirtSleepPlugin extends JavaPlugin implements TabExecutor {
@Override
public void onDisable() {
dayVotes.clear();
nightVotes.clear();
for (ActiveVote vote : activeVotes.values()) {
if (vote.timeoutTask != null) {
vote.timeoutTask.cancel();
}
}
activeVotes.clear();
}
@Override
@@ -46,7 +53,12 @@ public final class DirtSleepPlugin extends JavaPlugin implements TabExecutor {
return true;
}
handleVoteDay(player);
if (args.length == 1 && (args[0].equalsIgnoreCase("yes") || args[0].equalsIgnoreCase("no"))) {
handleResponseVote(player, VoteType.DAY, args[0].equalsIgnoreCase("yes"));
return true;
}
startVote(player, VoteType.DAY);
return true;
}
@@ -56,7 +68,12 @@ public final class DirtSleepPlugin extends JavaPlugin implements TabExecutor {
return true;
}
handleVoteNight(player);
if (args.length == 1 && (args[0].equalsIgnoreCase("yes") || args[0].equalsIgnoreCase("no"))) {
handleResponseVote(player, VoteType.NIGHT, args[0].equalsIgnoreCase("yes"));
return true;
}
startVote(player, VoteType.NIGHT);
return true;
}
@@ -68,8 +85,14 @@ public final class DirtSleepPlugin extends JavaPlugin implements TabExecutor {
}
reloadConfig();
dayVotes.clear();
nightVotes.clear();
for (ActiveVote vote : activeVotes.values()) {
if (vote.timeoutTask != null) {
vote.timeoutTask.cancel();
}
}
activeVotes.clear();
sender.sendMessage(message("messages.reloaded"));
return true;
}
@@ -81,164 +104,184 @@ public final class DirtSleepPlugin extends JavaPlugin implements TabExecutor {
return false;
}
private void handleVoteDay(Player player) {
if (!getConfig().getBoolean("vote-day.enabled", true)) {
player.sendMessage(message("messages.disabled"));
private void startVote(Player starter, VoteType type) {
if (!getConfig().getBoolean("vote-" + type.configName + ".enabled", true)) {
starter.sendMessage(message(type == VoteType.DAY ? "messages.disabled" : "messages.night-disabled"));
return;
}
World world = player.getWorld();
World world = starter.getWorld();
if (!isWorldEnabled(world)) {
player.sendMessage(message("messages.world-disabled"));
starter.sendMessage(message("messages.world-disabled"));
return;
}
if (!canVoteDayInWorld(world)) {
player.sendMessage(message("messages.not-night"));
clearDayVotes(world);
if (!canStartVote(world, type)) {
starter.sendMessage(message(type == VoteType.DAY ? "messages.not-night" : "messages.not-day"));
return;
}
String worldKey = world.getName().toLowerCase(Locale.ROOT);
Set<UUID> votes = dayVotes.computeIfAbsent(worldKey, k -> new HashSet<>());
if (votes.contains(player.getUniqueId())) {
player.sendMessage(message("messages.already-voted"));
if (activeVotes.containsKey(worldKey)) {
starter.sendMessage(message("messages.vote-already-running"));
return;
}
votes.add(player.getUniqueId());
int requiredYesVotes = getRequiredVotes(world, type);
long durationSeconds = Math.max(5, getConfig().getLong("vote-session.duration-seconds", 30));
int currentVotes = countValidVotes(world, votes);
int requiredVotes = getRequiredVotes(world);
ActiveVote activeVote = new ActiveVote(type, requiredYesVotes);
activeVote.yesVotes.add(starter.getUniqueId());
String voteMessage = message("messages.vote-added")
.replace("%player%", player.getName())
.replace("%votes%", String.valueOf(currentVotes))
.replace("%required%", String.valueOf(requiredVotes))
.replace("%remaining%", String.valueOf(Math.max(0, requiredVotes - currentVotes)));
BukkitTask task = Bukkit.getScheduler().runTaskLater(this, () -> timeoutVote(world), durationSeconds * 20L);
activeVote.timeoutTask = task;
broadcast(world, voteMessage);
activeVotes.put(worldKey, activeVote);
if (currentVotes >= requiredVotes) {
makeDay(world);
clearDayVotes(world);
clearNightVotes(world);
broadcast(world, message("messages.vote-passed"));
String startedMessage = message(type == VoteType.DAY ? "messages.vote-started-day" : "messages.vote-started-night")
.replace("%player%", starter.getName())
.replace("%yes%", String.valueOf(activeVote.yesVotes.size()))
.replace("%no%", String.valueOf(activeVote.noVotes.size()))
.replace("%required%", String.valueOf(requiredYesVotes));
String clickPrompt = message("messages.click-prompt");
for (Player player : world.getPlayers()) {
player.sendMessage(startedMessage);
player.sendMessage(clickPrompt);
sendClickableVoteMessage(player, type);
}
checkVoteResult(world);
}
private void handleVoteNight(Player player) {
if (!getConfig().getBoolean("vote-night.enabled", true)) {
player.sendMessage(message("messages.night-disabled"));
return;
}
private void handleResponseVote(Player player, VoteType commandType, boolean yes) {
World world = player.getWorld();
if (!isWorldEnabled(world)) {
player.sendMessage(message("messages.world-disabled"));
return;
}
if (!canVoteNightInWorld(world)) {
player.sendMessage(message("messages.not-day"));
clearNightVotes(world);
return;
}
String worldKey = world.getName().toLowerCase(Locale.ROOT);
Set<UUID> votes = nightVotes.computeIfAbsent(worldKey, k -> new HashSet<>());
ActiveVote activeVote = activeVotes.get(worldKey);
if (votes.contains(player.getUniqueId())) {
player.sendMessage(message("messages.night-already-voted"));
if (activeVote == null) {
player.sendMessage(message("messages.no-active-vote"));
return;
}
votes.add(player.getUniqueId());
if (activeVote.type != commandType) {
player.sendMessage(message("messages.wrong-vote-command"));
return;
}
int currentVotes = countValidVotes(world, votes);
int requiredVotes = getRequiredVotes(world);
activeVote.yesVotes.remove(player.getUniqueId());
activeVote.noVotes.remove(player.getUniqueId());
String voteMessage = message("messages.night-vote-added")
.replace("%player%", player.getName())
.replace("%votes%", String.valueOf(currentVotes))
.replace("%required%", String.valueOf(requiredVotes))
.replace("%remaining%", String.valueOf(Math.max(0, requiredVotes - currentVotes)));
if (yes) {
activeVote.yesVotes.add(player.getUniqueId());
player.sendMessage(message("messages.vote-yes-confirm"));
} else {
activeVote.noVotes.add(player.getUniqueId());
player.sendMessage(message("messages.vote-no-confirm"));
}
broadcast(world, voteMessage);
broadcastVoteStatus(world, activeVote);
checkVoteResult(world);
}
if (currentVotes >= requiredVotes) {
makeNight(world);
clearNightVotes(world);
clearDayVotes(world);
broadcast(world, message("messages.night-vote-passed"));
private void broadcastVoteStatus(World world, ActiveVote activeVote) {
cleanupInvalidVotes(world, activeVote);
String path = activeVote.type == VoteType.DAY ? "messages.vote-progress-day" : "messages.vote-progress-night";
String msg = message(path)
.replace("%yes%", String.valueOf(activeVote.yesVotes.size()))
.replace("%no%", String.valueOf(activeVote.noVotes.size()))
.replace("%required%", String.valueOf(activeVote.requiredYesVotes));
broadcast(world, msg);
}
private void checkVoteResult(World world) {
String worldKey = world.getName().toLowerCase(Locale.ROOT);
ActiveVote activeVote = activeVotes.get(worldKey);
if (activeVote == null) {
return;
}
cleanupInvalidVotes(world, activeVote);
int yesVotes = activeVote.yesVotes.size();
int noVotes = activeVote.noVotes.size();
int requiredYes = activeVote.requiredYesVotes;
int eligible = getEligiblePlayers(world, activeVote.type);
if (yesVotes >= requiredYes) {
if (activeVote.type == VoteType.DAY) {
makeDay(world);
broadcast(world, message("messages.vote-passed"));
} else {
makeNight(world);
broadcast(world, message("messages.night-vote-passed"));
}
endVote(world);
return;
}
int remainingPossibleYes = Math.max(0, eligible - noVotes);
if (remainingPossibleYes < requiredYes) {
broadcast(world, message("messages.vote-failed"));
endVote(world);
}
}
private void makeDay(World world) {
world.setTime(1000);
world.setStorm(false);
world.setThundering(false);
private void timeoutVote(World world) {
String worldKey = world.getName().toLowerCase(Locale.ROOT);
ActiveVote activeVote = activeVotes.get(worldKey);
if (activeVote == null) {
return;
}
cleanupInvalidVotes(world, activeVote);
if (activeVote.type == VoteType.DAY) {
broadcast(world, message("messages.vote-expired"));
} else {
broadcast(world, message("messages.night-vote-expired"));
}
endVote(world);
}
private void makeNight(World world) {
world.setTime(13000);
private void endVote(World world) {
String worldKey = world.getName().toLowerCase(Locale.ROOT);
ActiveVote activeVote = activeVotes.remove(worldKey);
if (activeVote != null && activeVote.timeoutTask != null) {
activeVote.timeoutTask.cancel();
}
}
private void clearDayVotes(World world) {
dayVotes.remove(world.getName().toLowerCase(Locale.ROOT));
private void cleanupInvalidVotes(World world, ActiveVote activeVote) {
activeVote.yesVotes.removeIf(uuid -> !isEligibleVoter(world, uuid, activeVote.type));
activeVote.noVotes.removeIf(uuid -> !isEligibleVoter(world, uuid, activeVote.type));
}
private void clearNightVotes(World world) {
nightVotes.remove(world.getName().toLowerCase(Locale.ROOT));
}
private int countValidVotes(World world, Set<UUID> votes) {
votes.removeIf(uuid -> {
Player p = Bukkit.getPlayer(uuid);
return p == null || !p.isOnline() || !p.getWorld().equals(world);
});
return votes.size();
}
private boolean isWorldEnabled(World world) {
List<String> worlds = getConfig().getStringList("vote-day.worlds");
return worlds.isEmpty() || worlds.contains(world.getName());
}
private boolean canVoteDayInWorld(World world) {
if (world.getEnvironment() != World.Environment.NORMAL) {
private boolean isEligibleVoter(World world, UUID uuid, VoteType type) {
Player player = Bukkit.getPlayer(uuid);
if (player == null || !player.isOnline()) {
return false;
}
if (!player.getWorld().equals(world)) {
return false;
}
if (player.getGameMode() == GameMode.SPECTATOR) {
return false;
}
if (!getConfig().getBoolean("vote-day.only-during-night", true)) {
return true;
}
long time = world.getTime();
int nightStartsAt = getConfig().getInt("vote-day.night-starts-at", 12542);
return time >= nightStartsAt && time < 24000;
boolean excludeBypass = getConfig().getBoolean("vote-" + type.configName + ".exclude-bypass", true);
return !excludeBypass || !player.hasPermission("dirtsleep.bypass");
}
private boolean canVoteNightInWorld(World world) {
if (world.getEnvironment() != World.Environment.NORMAL) {
return false;
}
if (!getConfig().getBoolean("vote-night.only-during-day", true)) {
return true;
}
long time = world.getTime();
int dayEndsAt = getConfig().getInt("vote-night.day-ends-at", 12542);
return time >= 0 && time < dayEndsAt;
}
private int getRequiredVotes(World world) {
private int getEligiblePlayers(World world, VoteType type) {
int eligible = 0;
boolean excludeBypass = getConfig().getBoolean("vote-day.exclude-bypass", true);
boolean excludeBypass = getConfig().getBoolean("vote-" + type.configName + ".exclude-bypass", true);
for (Player player : world.getPlayers()) {
if (player.getGameMode() == GameMode.SPECTATOR) {
@@ -252,11 +295,40 @@ public final class DirtSleepPlugin extends JavaPlugin implements TabExecutor {
eligible++;
}
if (eligible <= 0) {
return 1;
return Math.max(1, eligible);
}
private boolean isWorldEnabled(World world) {
List<String> worlds = getConfig().getStringList("vote-day.worlds");
return worlds.isEmpty() || worlds.contains(world.getName());
}
private boolean canStartVote(World world, VoteType type) {
if (world.getEnvironment() != World.Environment.NORMAL) {
return false;
}
String raw = getConfig().getString("vote-day.required-votes", "50%");
long time = world.getTime();
if (type == VoteType.DAY) {
if (!getConfig().getBoolean("vote-day.only-during-night", true)) {
return true;
}
int nightStartsAt = getConfig().getInt("vote-day.night-starts-at", 12542);
return time >= nightStartsAt && time < 24000;
}
if (!getConfig().getBoolean("vote-night.only-during-day", true)) {
return true;
}
int dayEndsAt = getConfig().getInt("vote-night.day-ends-at", 12542);
return time >= 0 && time < dayEndsAt;
}
private int getRequiredVotes(World world, VoteType type) {
int eligible = getEligiblePlayers(world, type);
String raw = getConfig().getString("vote-" + type.configName + ".required-votes", "50%");
if (raw == null) {
return 1;
}
@@ -281,6 +353,29 @@ public final class DirtSleepPlugin extends JavaPlugin implements TabExecutor {
}
}
private void makeDay(World world) {
world.setTime(1000);
world.setStorm(false);
world.setThundering(false);
}
private void makeNight(World world) {
world.setTime(13000);
}
private void sendClickableVoteMessage(Player player, VoteType type) {
String yesCommand = type == VoteType.DAY ? "/voteday yes" : "/votenight yes";
String noCommand = type == VoteType.DAY ? "/voteday no" : "/votenight no";
Component yes = Component.text("[YES]", NamedTextColor.GREEN)
.clickEvent(ClickEvent.runCommand(yesCommand));
Component spacer = Component.text(" ", NamedTextColor.GRAY);
Component no = Component.text("[NO]", NamedTextColor.RED)
.clickEvent(ClickEvent.runCommand(noCommand));
player.sendMessage(yes.append(spacer).append(no));
}
private void broadcast(World world, String message) {
for (Player player : world.getPlayers()) {
player.sendMessage(message);
@@ -307,6 +402,38 @@ public final class DirtSleepPlugin extends JavaPlugin implements TabExecutor {
}
}
if (command.getName().equalsIgnoreCase("voteday") || command.getName().equalsIgnoreCase("votenight")) {
if (args.length == 1) {
return Arrays.asList("yes", "no").stream()
.filter(s -> s.toLowerCase(Locale.ROOT).startsWith(args[0].toLowerCase(Locale.ROOT)))
.collect(Collectors.toList());
}
}
return Collections.emptyList();
}
private enum VoteType {
DAY("day"),
NIGHT("night");
private final String configName;
VoteType(String configName) {
this.configName = configName;
}
}
private static final class ActiveVote {
private final VoteType type;
private final int requiredYesVotes;
private final Set<UUID> yesVotes = new HashSet<>();
private final Set<UUID> noVotes = new HashSet<>();
private BukkitTask timeoutTask;
private ActiveVote(VoteType type, int requiredYesVotes) {
this.type = type;
this.requiredYesVotes = requiredYesVotes;
}
}
}
+39 -8
View File
@@ -11,7 +11,7 @@ vote-day:
# Tick time where night starts
night-starts-at: 12542
# Required votes before changing to day
# Required YES votes before changing to day
# Examples: "1", "2", "50%", "75%"
required-votes: "50%"
@@ -28,19 +28,50 @@ vote-night:
# Tick time where day effectively ends for vote checks
day-ends-at: 12542
# Required YES votes before changing to night
# Examples: "1", "2", "50%", "75%"
required-votes: "50%"
# Ignore players with this permission from vote requirement:
# dirtsleep.bypass
exclude-bypass: true
vote-session:
# How long a vote stays open
duration-seconds: 30
messages:
prefix: "&8[&6DirtBagMC&8] &7"
vote-added: "%prefix%&e%player% &7voted for day. &f(%votes%/%required% votes, %remaining% remaining)"
vote-passed: "%prefix%&aVote passed! Skipping to day."
already-voted: "%prefix%&cYou already voted."
not-night: "%prefix%&cYou can only use this at night."
disabled: "%prefix%&cVote day is disabled."
vote-started-day: "%prefix%&e%player% &7started a vote to make it &fday&7. &aYES: &f%yes%&7/&f%required% &8| &cNO: &f%no%"
vote-started-night: "%prefix%&e%player% &7started a vote to make it &fnight&7. &aYES: &f%yes%&7/&f%required% &8| &cNO: &f%no%"
night-vote-added: "%prefix%&e%player% &7voted for night. &f(%votes%/%required% votes, %remaining% remaining)"
click-prompt: "%prefix%&7Click below to vote."
vote-progress-day: "%prefix%&7Day vote status: &aYES: &f%yes%&7/&f%required% &8| &cNO: &f%no%"
vote-progress-night: "%prefix%&7Night vote status: &aYES: &f%yes%&7/&f%required% &8| &cNO: &f%no%"
vote-passed: "%prefix%&aVote passed! Skipping to day."
night-vote-passed: "%prefix%&aVote passed! Skipping to night."
vote-failed: "%prefix%&cVote failed."
vote-expired: "%prefix%&cVote to make it day expired."
night-vote-expired: "%prefix%&cVote to make it night expired."
vote-yes-confirm: "%prefix%&aYou voted YES."
vote-no-confirm: "%prefix%&cYou voted NO."
vote-already-running: "%prefix%&cA vote is already running in this world."
no-active-vote: "%prefix%&cThere is no active vote in this world."
wrong-vote-command: "%prefix%&cUse the correct vote buttons for the current vote."
already-voted: "%prefix%&cYou already voted."
night-already-voted: "%prefix%&cYou already voted for night."
not-day: "%prefix%&cYou can only use this during the day."
not-night: "%prefix%&cYou can only start this vote at night."
not-day: "%prefix%&cYou can only start this vote during the day."
disabled: "%prefix%&cVote day is disabled."
night-disabled: "%prefix%&cVote night is disabled."
world-disabled: "%prefix%&cVote commands are disabled in this world."
+3 -3
View File
@@ -3,13 +3,13 @@ version: 1.0
main: com.bitnix.dirtsleep.DirtSleepPlugin
api-version: '1.21'
author: bitnix
description: Simple vote day plugin
description: Simple vote day/night plugin
commands:
voteday:
description: Vote to make it day
description: Start a vote to make it day
usage: /voteday
votenight:
description: Vote to make it night
description: Start a vote to make it night
usage: /votenight
dirtsleep:
description: DirtSleep admin command
Binary file not shown.
+39 -8
View File
@@ -11,7 +11,7 @@ vote-day:
# Tick time where night starts
night-starts-at: 12542
# Required votes before changing to day
# Required YES votes before changing to day
# Examples: "1", "2", "50%", "75%"
required-votes: "50%"
@@ -28,19 +28,50 @@ vote-night:
# Tick time where day effectively ends for vote checks
day-ends-at: 12542
# Required YES votes before changing to night
# Examples: "1", "2", "50%", "75%"
required-votes: "50%"
# Ignore players with this permission from vote requirement:
# dirtsleep.bypass
exclude-bypass: true
vote-session:
# How long a vote stays open
duration-seconds: 30
messages:
prefix: "&8[&6DirtBagMC&8] &7"
vote-added: "%prefix%&e%player% &7voted for day. &f(%votes%/%required% votes, %remaining% remaining)"
vote-passed: "%prefix%&aVote passed! Skipping to day."
already-voted: "%prefix%&cYou already voted."
not-night: "%prefix%&cYou can only use this at night."
disabled: "%prefix%&cVote day is disabled."
vote-started-day: "%prefix%&e%player% &7started a vote to make it &fday&7. &aYES: &f%yes%&7/&f%required% &8| &cNO: &f%no%"
vote-started-night: "%prefix%&e%player% &7started a vote to make it &fnight&7. &aYES: &f%yes%&7/&f%required% &8| &cNO: &f%no%"
night-vote-added: "%prefix%&e%player% &7voted for night. &f(%votes%/%required% votes, %remaining% remaining)"
click-prompt: "%prefix%&7Click below to vote."
vote-progress-day: "%prefix%&7Day vote status: &aYES: &f%yes%&7/&f%required% &8| &cNO: &f%no%"
vote-progress-night: "%prefix%&7Night vote status: &aYES: &f%yes%&7/&f%required% &8| &cNO: &f%no%"
vote-passed: "%prefix%&aVote passed! Skipping to day."
night-vote-passed: "%prefix%&aVote passed! Skipping to night."
vote-failed: "%prefix%&cVote failed."
vote-expired: "%prefix%&cVote to make it day expired."
night-vote-expired: "%prefix%&cVote to make it night expired."
vote-yes-confirm: "%prefix%&aYou voted YES."
vote-no-confirm: "%prefix%&cYou voted NO."
vote-already-running: "%prefix%&cA vote is already running in this world."
no-active-vote: "%prefix%&cThere is no active vote in this world."
wrong-vote-command: "%prefix%&cUse the correct vote buttons for the current vote."
already-voted: "%prefix%&cYou already voted."
night-already-voted: "%prefix%&cYou already voted for night."
not-day: "%prefix%&cYou can only use this during the day."
not-night: "%prefix%&cYou can only start this vote at night."
not-day: "%prefix%&cYou can only start this vote during the day."
disabled: "%prefix%&cVote day is disabled."
night-disabled: "%prefix%&cVote night is disabled."
world-disabled: "%prefix%&cVote commands are disabled in this world."
+3 -3
View File
@@ -3,13 +3,13 @@ version: 1.0
main: com.bitnix.dirtsleep.DirtSleepPlugin
api-version: '1.21'
author: bitnix
description: Simple vote day plugin
description: Simple vote day/night plugin
commands:
voteday:
description: Vote to make it day
description: Start a vote to make it day
usage: /voteday
votenight:
description: Vote to make it night
description: Start a vote to make it night
usage: /votenight
dirtsleep:
description: DirtSleep admin command
+1 -1
View File
@@ -1,5 +1,5 @@
#Generated by Maven
#Tue Jun 09 20:09:21 EDT 2026
#Tue Jun 09 20:37:04 EDT 2026
artifactId=DirtSleep
groupId=com.bitnix
version=1.0-SNAPSHOT
@@ -1 +1,3 @@
com/bitnix/dirtsleep/DirtSleepPlugin$VoteType.class
com/bitnix/dirtsleep/DirtSleepPlugin.class
com/bitnix/dirtsleep/DirtSleepPlugin$ActiveVote.class