Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,49 @@

public class VotingPluginMain extends AdvancedCorePlugin {

private final Object votingNPCLifecycle = new Object();
private final java.util.concurrent.atomic.AtomicLong votingNPCConfiguration = new java.util.concurrent.atomic.AtomicLong();
private volatile com.bencodez.votingplugin.npc.VotingNPCManager votingNPCManager;

public com.bencodez.votingplugin.npc.VotingNPCManager getVotingNPCManager() { return votingNPCManager; }

/** Optional NPC storage is lazy and asynchronous; configuration only schedules owner work. */
private void refreshVotingNPCs() {
boolean requested = getConfigFile().isVotingNPCsEnabled();
long generation = votingNPCConfiguration.incrementAndGet();
if (!requested && votingNPCManager == null) return;
getBukkitScheduler().runTask(this, () -> {
com.bencodez.votingplugin.npc.VotingNPCManager manager;
synchronized (votingNPCLifecycle) {
if (!isEnabled() || generation != votingNPCConfiguration.get()) return;
if (votingNPCManager == null && requested) {
try {
var created = new com.bencodez.votingplugin.npc.VotingNPCManager(this);
Bukkit.getPluginManager().registerEvents(created, this);
votingNPCManager = created;
} catch (RuntimeException | LinkageError unsupported) {
getLogger().warning("Voting NPCs are unavailable on this server; normal voting is unaffected.");
return;
}
}
manager = votingNPCManager;
}
if (manager != null) manager.configure(requested);
});
}

private void closeVotingNPCs() {
com.bencodez.votingplugin.npc.VotingNPCManager manager;
synchronized (votingNPCLifecycle) {
votingNPCConfiguration.incrementAndGet();
manager = votingNPCManager;
votingNPCManager = null;
}
if (manager != null) {
try { manager.close(); } catch (RuntimeException | LinkageError failure) { debug(failure); }
}
}

@Getter
public static VotingPluginMain plugin;

Expand Down Expand Up @@ -748,6 +791,7 @@ public void onPostLoad() {
checkVotifier();
registerEvents();
refreshPlaceholderPlayerPresence();
refreshVotingNPCs();

loadVoteBroadcast();

Expand Down Expand Up @@ -1961,6 +2005,7 @@ private void shutdownVoteReminders() {

@Override
public void onUnLoad() {
closeVotingNPCs();
placeholderPlayerPresence.clear();
stopBackendHostedControlLifecycle();
stopBackendControlConnectorLifecycle();
Expand Down Expand Up @@ -2172,6 +2217,7 @@ private void reloadPlugin(boolean userStorage, boolean reconcileHostedControl,

loadDirectlyDefined();

refreshVotingNPCs();
if (reconcileHostedControl) restartBackendControlConnector();

setUpdate(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,7 @@ public boolean isVotingPluginCommand(Player player, String cmd) {
*/
private void loadAdminVoteCommand() {
plugin.setAdminVoteCommand(new ArrayList<>());
com.bencodez.votingplugin.npc.VotingNPCCommands.register(plugin);

plugin.getAdminVoteCommand().add(new CommandHandler(plugin, new String[] { "CurrentPluginTime" },
"VotingPlugin.Commands.AdminVote.CurrentPluginTime|" + adminPerm, "Current plugin time") {
Expand Down Expand Up @@ -3299,6 +3300,14 @@ public void onInput(Player player, String value) {
* Load tab complete.
*/
public void loadTabComplete() {
TabCompleteHandler.getInstance().addTabCompleteOption(new TabCompleteHandle("(npcid)", new ArrayList<>()) {
@Override public void reload() { updateReplacements(); }
@Override public void updateReplacements() {
var manager = plugin.getVotingNPCManager();
setReplace(manager == null ? new ArrayList<>() : new ArrayList<>(manager.definitions().stream()
.map(com.bencodez.votingplugin.npc.VotingNPCDefinition::id).toList()));
}
});
ArrayList<String> sites = new ArrayList<>();
for (VoteSite site : plugin.getVoteSiteManager().getVoteSitesEnabled()) {
sites.add(site.getKey());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
import lombok.Getter;

public class Config extends YMLFile {
/** Opt-in persistent vanilla voting NPCs; missing configuration preserves legacy behavior. */
public boolean isVotingNPCsEnabled() { return getData().getBoolean("VotingNPCs.Enabled", false); }


/** Explicit permission for executable deployment over private-network plaintext HTTP. */
public boolean getControlAllowInsecureHttpPluginDeployment() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package com.bencodez.votingplugin.npc;

import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.CompletionStage;

import org.bukkit.Location;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;

import com.bencodez.advancedcore.api.command.CommandHandler;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.util.BukkitCompletionScheduler;

/** Normal /av command handlers; no separate command map or player GUI registry. */
public final class VotingNPCCommands {
private static final String PERMISSION = "VotingPlugin.Commands.AdminVote.NPC";
private VotingNPCCommands() { }

public static void register(VotingPluginMain plugin) {
add(plugin, new String[] { "NPC", "Create", "(string)" }, false);
add(plugin, new String[] { "NPC", "Create", "(string)", "(list)" }, false);
add(plugin, new String[] { "NPC", "List" }, true);
add(plugin, new String[] { "NPC", "Inspect", "(npcid)" }, true);
add(plugin, new String[] { "NPC", "Remove", "(npcid)" }, true);
}

private static void add(VotingPluginMain plugin, String[] pattern, boolean console) {
plugin.getAdminVoteCommand().add(new CommandHandler(plugin, pattern, PERMISSION + "|VotingPlugin.Admin",
"Manage saved vanilla voting NPCs", console) {
@Override public void execute(CommandSender sender, String[] args) {
// CommandHandler invokes execute asynchronously. Capture location,
// permission and messages only after reaching the sender's owner.
String[] captured = args.clone();
onSender(plugin, sender, () -> executeOwned(plugin, sender, captured));
}
});
}

private static boolean allowed(CommandSender sender) {
return (!(sender instanceof Player player) || player.isOnline())
&& (sender.hasPermission(PERMISSION) || sender.hasPermission("VotingPlugin.Admin"));
}

private static void onSender(VotingPluginMain plugin, CommandSender sender, Runnable action) {
Player player = sender instanceof Player candidate ? candidate : null;
BukkitCompletionScheduler.run(plugin, player, action, player == null ? action : () -> { }, () -> { });
}

private static void executeOwned(VotingPluginMain plugin, CommandSender sender, String[] args) {
if (!allowed(sender)) return;
VotingNPCManager manager = plugin.getVotingNPCManager();
if (manager == null || !manager.available()) {
sender.sendMessage("Voting NPCs are disabled, unsupported, or waiting for valid storage. Check VotingNPCs.Enabled in Config.yml and reload.");
return;
}
switch (args[1].toLowerCase(java.util.Locale.ROOT)) {
case "create":
if (!(sender instanceof Player player)) { sender.sendMessage("Create a voting NPC from in game."); return; }
try {
Location at = player.getLocation();
String name = args.length > 3 ? String.join(" ", Arrays.copyOfRange(args, 3, args.length)) : "&aVote";
VotingNPCDefinition definition = new VotingNPCDefinition(args[2], UUID.randomUUID(), at.getWorld().getUID(),
at.getX(), at.getY(), at.getZ(), at.getYaw(), at.getPitch(), name);
confirm(plugin, sender, manager, manager.create(definition),
ignored -> "Saved voting NPC '" + definition.id() + "'. Its villager appears when the area is loaded and available.");
} catch (IllegalArgumentException invalid) {
sender.sendMessage("Use a lowercase ID of 1-40 letters, digits, underscores or hyphens, and a name of 1-128 characters without controls.");
}
break;
case "remove":
confirm(plugin, sender, manager, manager.remove(args[2]), removed -> removed
? "Removed saved voting NPC '" + args[2] + "'; exact entity cleanup has been requested."
: "No voting NPC has that ID.");
break;
case "inspect":
manager.definitions().stream().filter(value -> value.id().equals(args[2])).findFirst().ifPresentOrElse(value ->
sender.sendMessage("Voting NPC " + value.id() + ": world " + value.world() + ", "
+ value.x() + ", " + value.y() + ", " + value.z() + "; " + manager.runtimeState(value.id())),
() -> sender.sendMessage("No voting NPC has that ID."));
break;
case "list":
sender.sendMessage("Saved voting NPCs: " + manager.definitions().size() + "/64; tracked runtime slots: " + manager.runtimeCount());
for (var value : manager.definitions()) sender.sendMessage(value.id() + ": " + manager.runtimeState(value.id()));
break;
default:
sender.sendMessage("Use /av npc create <id> [name], list, inspect <id>, or remove <id>.");
}
}

private static String failureMessage(VotingNPCManager manager, Throwable failure) {
while ((failure instanceof java.util.concurrent.CompletionException
|| failure instanceof java.util.concurrent.ExecutionException) && failure.getCause() != null) {
failure = failure.getCause();
}
if (manager.available()) {
if (failure instanceof VotingNPCRegistry.ValidationFailure validation) {
return switch (validation.reason) {
case DUPLICATE_ID -> "That voting NPC ID already exists. Use a different ID or remove it first.";
case DUPLICATE_IDENTITY -> "That voting NPC identity already exists. No definition was changed.";
case LIMIT -> "The 64 voting NPC limit has been reached. Remove an NPC before creating another.";
};
}
if (failure instanceof java.util.concurrent.RejectedExecutionException) {
return "Voting NPC storage is busy. Wait for pending changes to finish before retrying.";
}
}
return "Voting NPC change was not confirmed. Check storage and reload before retrying; no success is assumed.";
}

private static <T> void confirm(VotingPluginMain plugin, CommandSender sender, VotingNPCManager manager, CompletionStage<T> completion,
java.util.function.Function<T, String> message) {
completion.whenComplete((value, failure) -> onSender(plugin, sender, () -> {
if (!allowed(sender)) return;
sender.sendMessage(failure == null ? message.apply(value)
: failureMessage(manager, failure));
}));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.bencodez.votingplugin.npc;

import java.util.Objects;
import java.util.UUID;
import java.util.regex.Pattern;

/** Durable definition; it contains no live world, player, entity or reward state. */
public record VotingNPCDefinition(String id, UUID identity, UUID world, double x, double y, double z,
float yaw, float pitch, String name) {
private static final Pattern ID = Pattern.compile("[a-z0-9][a-z0-9_-]{0,39}");

public VotingNPCDefinition {
if (id == null || !ID.matcher(id).matches()) throw new IllegalArgumentException("Invalid voting NPC ID");
Objects.requireNonNull(identity, "identity");
Objects.requireNonNull(world, "world");
if (!Double.isFinite(x) || !Double.isFinite(y) || !Double.isFinite(z)
|| Math.abs(x) > 30_000_000 || Math.abs(z) > 30_000_000
|| !Float.isFinite(yaw) || !Float.isFinite(pitch)) {
throw new IllegalArgumentException("Invalid voting NPC position");
}
if (name == null || name.isEmpty() || name.length() > 128
|| name.codePoints().anyMatch(Character::isISOControl)) {
throw new IllegalArgumentException("Invalid voting NPC name");
}
}

public int chunkX() { return Math.floorDiv((int) Math.floor(x), 16); }
public int chunkZ() { return Math.floorDiv((int) Math.floor(z), 16); }
}
Loading
Loading