VoteNetwork: plugin de vote reseau Velocity <-> Spigot

Reception directe en jeu (plugin messaging) ou stockage des votes en
attente (MySQL ou YAML configurable), /claim, VoteParty local par
serveur, mode maintenance, API publique pour scoreboard/GUI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
SarTron-NorthBlue
2026-07-12 13:12:32 +04:00
co-authored by Claude Sonnet 5
commit 8c69a0dc9a
28 changed files with 1893 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>fr.northblue</groupId>
<artifactId>northblue-vote-parent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>northblue-vote-spigot</artifactId>
<packaging>jar</packaging>
<!--
Compiled against Spigot 1.18.2 API on purpose: the Bukkit API is additive
across versions, so a plugin built against an older API version keeps
loading fine on newer server jars (1.19 -> 1.21.x and beyond) as long as
it only touches stable, non-removed API surface (which this plugin does).
-->
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.18.2-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>${hikaricp.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql.version}</version>
</dependency>
</dependencies>
<repositories>
<repository>
<id>spigotmc-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
</repository>
<repository>
<id>sonatype</id>
<url>https://oss.sonatype.org/content/groups/public/</url>
</repository>
</repositories>
<build>
<finalName>VoteNetwork-Spigot</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<relocations>
<relocation>
<pattern>com.zaxxer.hikari</pattern>
<shadedPattern>fr.northblue.vote.libs.hikari</shadedPattern>
</relocation>
<relocation>
<pattern>com.mysql</pattern>
<shadedPattern>fr.northblue.vote.libs.mysql</shadedPattern>
</relocation>
</relocations>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,16 @@
package fr.northblue.vote.spigot;
import java.util.concurrent.atomic.AtomicBoolean;
public class MaintenanceState {
private final AtomicBoolean stopped = new AtomicBoolean(false);
public boolean isStopped() {
return stopped.get();
}
public void setStopped(boolean value) {
stopped.set(value);
}
}
@@ -0,0 +1,86 @@
package fr.northblue.vote.spigot;
import fr.northblue.vote.spigot.api.NorthBlueVoteAPI;
import fr.northblue.vote.spigot.api.NorthBlueVoteAPIHolder;
import fr.northblue.vote.spigot.api.NorthBlueVoteAPIImpl;
import fr.northblue.vote.spigot.command.ClaimCommand;
import fr.northblue.vote.spigot.command.VoteAdminCommand;
import fr.northblue.vote.spigot.listener.VoteMessageListener;
import fr.northblue.vote.spigot.storage.MySqlPendingVoteStore;
import fr.northblue.vote.spigot.storage.PendingVoteStore;
import fr.northblue.vote.spigot.storage.YamlPendingVoteStore;
import fr.northblue.vote.spigot.voteparty.VoteParty;
import org.bukkit.plugin.ServicePriority;
import org.bukkit.plugin.java.JavaPlugin;
public class NorthBlueVoteSpigot extends JavaPlugin {
public static final String CHANNEL = "votenetwork:vote";
private VoteConfig voteConfig;
private PendingVoteStore pendingVoteStore;
private VoteParty voteParty;
private MaintenanceState maintenanceState;
@Override
public void onEnable() {
saveDefaultConfig();
this.voteConfig = new VoteConfig(this);
this.maintenanceState = new MaintenanceState();
if (voteConfig.isMysqlStorage()) {
MySqlPendingVoteStore mysqlStore = new MySqlPendingVoteStore(this, voteConfig);
mysqlStore.connect();
this.pendingVoteStore = mysqlStore;
getLogger().info("Stockage des votes en attente : MySQL.");
} else {
this.pendingVoteStore = new YamlPendingVoteStore(this);
getLogger().info("Stockage des votes en attente : fichier YAML local (pending-votes.yml).");
}
this.voteParty = new VoteParty(this, voteConfig);
this.voteParty.load();
getServer().getMessenger().registerIncomingPluginChannel(this, CHANNEL, new VoteMessageListener(voteConfig, voteParty));
getServer().getMessenger().registerOutgoingPluginChannel(this, CHANNEL);
getCommand("claim").setExecutor(new ClaimCommand(this, voteConfig, pendingVoteStore, voteParty, maintenanceState));
getCommand("vote").setExecutor(new VoteAdminCommand(this, voteConfig, pendingVoteStore, maintenanceState));
NorthBlueVoteAPI api = new NorthBlueVoteAPIImpl(this, voteConfig, pendingVoteStore, voteParty, maintenanceState);
NorthBlueVoteAPIHolder.set(api);
getServer().getServicesManager().register(NorthBlueVoteAPI.class, api, this, ServicePriority.Normal);
getLogger().info("VoteNetwork (Spigot) demarre.");
}
@Override
public void onDisable() {
getServer().getServicesManager().unregisterAll(this);
NorthBlueVoteAPIHolder.clear();
if (voteParty != null) {
voteParty.save();
}
if (pendingVoteStore != null) {
pendingVoteStore.close();
}
}
public VoteConfig getVoteConfig() {
return voteConfig;
}
public PendingVoteStore getPendingVoteStore() {
return pendingVoteStore;
}
public VoteParty getVoteParty() {
return voteParty;
}
public MaintenanceState getMaintenanceState() {
return maintenanceState;
}
}
@@ -0,0 +1,125 @@
package fr.northblue.vote.spigot;
import org.bukkit.ChatColor;
import org.bukkit.configuration.file.FileConfiguration;
import java.util.List;
import java.util.stream.Collectors;
public class VoteConfig {
private final NorthBlueVoteSpigot plugin;
public VoteConfig(NorthBlueVoteSpigot plugin) {
this.plugin = plugin;
}
private FileConfiguration cfg() {
return plugin.getConfig();
}
public void reload() {
plugin.reloadConfig();
}
public String getStorageType() {
return cfg().getString("storage.type", "mysql");
}
public boolean isMysqlStorage() {
return !"yaml".equalsIgnoreCase(getStorageType());
}
public String getMysqlHost() {
return cfg().getString("mysql.host", "127.0.0.1");
}
public int getMysqlPort() {
return cfg().getInt("mysql.port", 3306);
}
public String getMysqlDatabase() {
return cfg().getString("mysql.database", "northblue");
}
public String getMysqlUser() {
return cfg().getString("mysql.user", "root");
}
public String getMysqlPassword() {
return cfg().getString("mysql.password", "");
}
public int getMysqlPoolSize() {
return cfg().getInt("mysql.pool-size", 5);
}
public List<String> getVoteRewardCommands() {
return cfg().getStringList("rewards.vote-commands");
}
public int getVotePartyVotesRequired() {
return cfg().getInt("voteparty.votes-requis", 100);
}
public List<String> getVotePartyCommands() {
return cfg().getStringList("voteparty.commands");
}
public boolean isVotePartyBroadcastEnabled() {
return cfg().getBoolean("voteparty.broadcast.enabled", true);
}
public String getVotePartyProgressMessage() {
return colorize(cfg().getString("voteparty.broadcast.progress-message",
"&b[Vote] &f%current%&7/&f%required% &fvotes avant le prochain VoteParty !"));
}
public String getVotePartyTriggeredMessage() {
return colorize(cfg().getString("voteparty.broadcast.triggered-message",
"&a[Vote] &fVoteParty declenche ! Profitez des recompenses !"));
}
public String getMessageClaimSuccess() {
return colorize(cfg().getString("messages.claim-success",
"&a[Vote] &fVous avez recupere &e%amount% &fvote(s) en attente !"));
}
public String getMessageClaimEmpty() {
return colorize(cfg().getString("messages.claim-empty",
"&c[Vote] &fVous n'avez aucun vote en attente."));
}
public String getMessageMaintenance() {
return colorize(cfg().getString("messages.maintenance",
"&c[Vote] &fLe systeme de vote est actuellement en maintenance."));
}
public String getMessageMaintenanceEnabled() {
return colorize(cfg().getString("messages.maintenance-enabled",
"&c[Vote] &fMode maintenance active."));
}
public String getMessageMaintenanceDisabled() {
return colorize(cfg().getString("messages.maintenance-disabled",
"&a[Vote] &fMode maintenance desactive."));
}
public String getMessageDirectVote() {
return colorize(cfg().getString("messages.direct-vote",
"&a[Vote] &fMerci pour ton vote !"));
}
public String getMessageNoPermission() {
return colorize(cfg().getString("messages.no-permission",
"&cVous n'avez pas la permission d'utiliser cette commande."));
}
private String colorize(String input) {
return input == null ? "" : ChatColor.translateAlternateColorCodes('&', input);
}
public List<String> colorizeList(List<String> input) {
return input.stream().map(this::colorize).collect(Collectors.toList());
}
}
@@ -0,0 +1,34 @@
package fr.northblue.vote.spigot.api;
import java.util.UUID;
import java.util.function.Consumer;
/**
* API publique NorthBlueVote, pour affichage scoreboard / GUI par d'autres plugins.
*
* Recuperation :
* NorthBlueVoteAPI api = NorthBlueVoteAPI.get();
* ou via le ServicesManager Bukkit :
* RegisteredServiceProvider<NorthBlueVoteAPI> rsp = Bukkit.getServicesManager().getRegistration(NorthBlueVoteAPI.class);
*/
public interface NorthBlueVoteAPI {
/** Compteur VoteParty actuel sur CE serveur (thread principal uniquement). */
int getVotePartyCurrentVotes();
/** Objectif VoteParty configure sur CE serveur (thread principal uniquement). */
int getVotePartyRequiredVotes();
/** true si /claim est actuellement bloque (mode maintenance). */
boolean isMaintenanceEnabled();
/**
* Lit (sans les consommer) les votes en attente en base pour ce joueur.
* Requete asynchrone : le callback est rappele sur le thread principal du serveur.
*/
void getPendingVotes(UUID playerUuid, Consumer<Integer> callback);
static NorthBlueVoteAPI get() {
return NorthBlueVoteAPIHolder.INSTANCE;
}
}
@@ -0,0 +1,17 @@
package fr.northblue.vote.spigot.api;
public final class NorthBlueVoteAPIHolder {
static volatile NorthBlueVoteAPI INSTANCE;
private NorthBlueVoteAPIHolder() {
}
public static void set(NorthBlueVoteAPI api) {
INSTANCE = api;
}
public static void clear() {
INSTANCE = null;
}
}
@@ -0,0 +1,58 @@
package fr.northblue.vote.spigot.api;
import fr.northblue.vote.spigot.MaintenanceState;
import fr.northblue.vote.spigot.NorthBlueVoteSpigot;
import fr.northblue.vote.spigot.VoteConfig;
import fr.northblue.vote.spigot.storage.PendingVoteStore;
import fr.northblue.vote.spigot.voteparty.VoteParty;
import org.bukkit.Bukkit;
import java.util.UUID;
import java.util.function.Consumer;
public class NorthBlueVoteAPIImpl implements NorthBlueVoteAPI {
private final NorthBlueVoteSpigot plugin;
private final VoteConfig config;
private final PendingVoteStore pendingVoteStore;
private final VoteParty voteParty;
private final MaintenanceState maintenanceState;
public NorthBlueVoteAPIImpl(NorthBlueVoteSpigot plugin, VoteConfig config, PendingVoteStore pendingVoteStore,
VoteParty voteParty, MaintenanceState maintenanceState) {
this.plugin = plugin;
this.config = config;
this.pendingVoteStore = pendingVoteStore;
this.voteParty = voteParty;
this.maintenanceState = maintenanceState;
}
@Override
public int getVotePartyCurrentVotes() {
return voteParty.getCurrentVotes();
}
@Override
public int getVotePartyRequiredVotes() {
return config.getVotePartyVotesRequired();
}
@Override
public boolean isMaintenanceEnabled() {
return maintenanceState.isStopped();
}
@Override
public void getPendingVotes(UUID playerUuid, Consumer<Integer> callback) {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () ->
pendingVoteStore.peek(
playerUuid,
pending -> Bukkit.getScheduler().runTask(plugin, () -> callback.accept(pending)),
exception -> {
plugin.getLogger().severe("Erreur API getPendingVotes: " + exception.getMessage());
Bukkit.getScheduler().runTask(plugin, () -> callback.accept(0));
}
)
);
}
}
@@ -0,0 +1,76 @@
package fr.northblue.vote.spigot.command;
import fr.northblue.vote.spigot.MaintenanceState;
import fr.northblue.vote.spigot.NorthBlueVoteSpigot;
import fr.northblue.vote.spigot.VoteConfig;
import fr.northblue.vote.spigot.storage.PendingVoteStore;
import fr.northblue.vote.spigot.voteparty.VoteParty;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
public class ClaimCommand implements CommandExecutor {
private final NorthBlueVoteSpigot plugin;
private final VoteConfig config;
private final PendingVoteStore pendingVoteStore;
private final VoteParty voteParty;
private final MaintenanceState maintenanceState;
public ClaimCommand(NorthBlueVoteSpigot plugin, VoteConfig config, PendingVoteStore pendingVoteStore,
VoteParty voteParty, MaintenanceState maintenanceState) {
this.plugin = plugin;
this.config = config;
this.pendingVoteStore = pendingVoteStore;
this.voteParty = voteParty;
this.maintenanceState = maintenanceState;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!(sender instanceof Player)) {
sender.sendMessage("Cette commande est reservee aux joueurs.");
return true;
}
Player player = (Player) sender;
if (maintenanceState.isStopped()) {
player.sendMessage(config.getMessageMaintenance());
return true;
}
Bukkit.getScheduler().runTaskAsynchronously(plugin, () ->
pendingVoteStore.fetchAndClear(
player.getUniqueId(),
pendingVotes -> Bukkit.getScheduler().runTask(plugin, () -> applyClaim(player, pendingVotes)),
exception -> Bukkit.getScheduler().runTask(plugin, () -> {
plugin.getLogger().severe("Erreur /claim pour " + player.getName() + ": " + exception.getMessage());
player.sendMessage(config.getMessageMaintenance());
})
)
);
return true;
}
private void applyClaim(Player player, int pendingVotes) {
if (pendingVotes <= 0) {
player.sendMessage(config.getMessageClaimEmpty());
return;
}
for (int i = 0; i < pendingVotes; i++) {
for (String cmd : config.getVoteRewardCommands()) {
String parsed = cmd.replace("%player%", player.getName());
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), parsed);
}
}
voteParty.addVotes(pendingVotes);
player.sendMessage(config.getMessageClaimSuccess().replace("%amount%", String.valueOf(pendingVotes)));
}
}
@@ -0,0 +1,70 @@
package fr.northblue.vote.spigot.command;
import fr.northblue.vote.spigot.MaintenanceState;
import fr.northblue.vote.spigot.NorthBlueVoteSpigot;
import fr.northblue.vote.spigot.VoteConfig;
import fr.northblue.vote.spigot.storage.PendingVoteStore;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
public class VoteAdminCommand implements CommandExecutor {
private final NorthBlueVoteSpigot plugin;
private final VoteConfig config;
private final PendingVoteStore pendingVoteStore;
private final MaintenanceState maintenanceState;
public VoteAdminCommand(NorthBlueVoteSpigot plugin, VoteConfig config, PendingVoteStore pendingVoteStore, MaintenanceState maintenanceState) {
this.plugin = plugin;
this.config = config;
this.pendingVoteStore = pendingVoteStore;
this.maintenanceState = maintenanceState;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!sender.hasPermission("votenetwork.admin")) {
sender.sendMessage(config.getMessageNoPermission());
return true;
}
if (args.length != 1) {
sender.sendMessage("Usage: /vote <stop|start|testdb>");
return true;
}
switch (args[0].toLowerCase()) {
case "stop" -> {
maintenanceState.setStopped(true);
sender.sendMessage(config.getMessageMaintenanceEnabled());
}
case "start" -> {
maintenanceState.setStopped(false);
sender.sendMessage(config.getMessageMaintenanceDisabled());
}
case "testdb" -> testStorage(sender);
default -> sender.sendMessage("Usage: /vote <stop|start|testdb>");
}
return true;
}
private void testStorage(CommandSender sender) {
sender.sendMessage(ChatColor.GRAY + "[Vote] Test du stockage en cours...");
Bukkit.getScheduler().runTaskAsynchronously(plugin, () ->
pendingVoteStore.testConnection((success, message) ->
Bukkit.getScheduler().runTask(plugin, () -> {
if (success) {
sender.sendMessage(ChatColor.GREEN + "[Vote] " + message);
} else {
sender.sendMessage(ChatColor.RED + "[Vote] Echec : " + message);
}
})
)
);
}
}
@@ -0,0 +1,50 @@
package fr.northblue.vote.spigot.listener;
import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteStreams;
import fr.northblue.vote.spigot.NorthBlueVoteSpigot;
import fr.northblue.vote.spigot.VoteConfig;
import fr.northblue.vote.spigot.voteparty.VoteParty;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.messaging.PluginMessageListener;
import java.util.UUID;
public class VoteMessageListener implements PluginMessageListener {
private final VoteConfig config;
private final VoteParty voteParty;
public VoteMessageListener(VoteConfig config, VoteParty voteParty) {
this.config = config;
this.voteParty = voteParty;
}
@Override
public void onPluginMessageReceived(String channel, Player receivingPlayer, byte[] message) {
if (!channel.equals(NorthBlueVoteSpigot.CHANNEL)) {
return;
}
ByteArrayDataInput in = ByteStreams.newDataInput(message);
UUID uuid = UUID.fromString(in.readUTF());
in.readUTF(); // pseudo, non utilise ici (on retrouve le joueur via son UUID)
Player target = Bukkit.getPlayer(uuid);
if (target == null || !target.isOnline()) {
// Le joueur a quitte ce serveur juste avant l'arrivee du message : le vote reste
// gere par le fallback base de donnees cote Velocity lors du prochain vote.
return;
}
for (String command : config.getVoteRewardCommands()) {
String parsed = command.replace("%player%", target.getName());
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), parsed);
}
target.sendMessage(config.getMessageDirectVote());
voteParty.addVotes(1);
}
}
@@ -0,0 +1,147 @@
package fr.northblue.vote.spigot.storage;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import fr.northblue.vote.spigot.NorthBlueVoteSpigot;
import fr.northblue.vote.spigot.VoteConfig;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.UUID;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.IntConsumer;
public class MySqlPendingVoteStore implements PendingVoteStore {
private static final String DRIVER_CLASS = "fr.northblue.vote.libs.mysql.cj.jdbc.Driver";
private final NorthBlueVoteSpigot plugin;
private final VoteConfig config;
private HikariDataSource dataSource;
public MySqlPendingVoteStore(NorthBlueVoteSpigot plugin, VoteConfig config) {
this.plugin = plugin;
this.config = config;
}
public void connect() {
try {
HikariConfig hikariConfig = new HikariConfig();
hikariConfig.setJdbcUrl("jdbc:mysql://" + config.getMysqlHost() + ":" + config.getMysqlPort()
+ "/" + config.getMysqlDatabase() + "?useSSL=false&autoReconnect=true&characterEncoding=utf8");
hikariConfig.setUsername(config.getMysqlUser());
hikariConfig.setPassword(config.getMysqlPassword());
hikariConfig.setMaximumPoolSize(config.getMysqlPoolSize());
hikariConfig.setPoolName("VoteNetwork-Spigot-Pool");
hikariConfig.setDriverClassName(DRIVER_CLASS);
hikariConfig.setInitializationFailTimeout(-1);
hikariConfig.addDataSourceProperty("cachePrepStmts", "true");
hikariConfig.addDataSourceProperty("prepStmtCacheSize", "250");
hikariConfig.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
this.dataSource = new HikariDataSource(hikariConfig);
createTableIfNotExists();
} catch (Exception e) {
plugin.getLogger().severe("Connexion MySQL impossible au demarrage: " + e.getMessage());
plugin.getLogger().severe("Corrigez config.yml puis utilisez /vote testdb, ou redemarrez le plugin.");
}
}
public boolean isConnected() {
return dataSource != null && !dataSource.isClosed();
}
@Override
public void testConnection(BiConsumer<Boolean, String> callback) {
String url = "jdbc:mysql://" + config.getMysqlHost() + ":" + config.getMysqlPort()
+ "/" + config.getMysqlDatabase() + "?useSSL=false&connectTimeout=3000&socketTimeout=3000";
long start = System.currentTimeMillis();
try {
Class.forName(DRIVER_CLASS);
try (Connection connection = DriverManager.getConnection(url, config.getMysqlUser(), config.getMysqlPassword())) {
long latency = System.currentTimeMillis() - start;
callback.accept(true, "Connexion reussie en " + latency + " ms (" + config.getMysqlHost() + ":" + config.getMysqlPort() + "/" + config.getMysqlDatabase() + ")");
}
} catch (Exception e) {
callback.accept(false, e.getMessage());
}
}
private void createTableIfNotExists() {
String sql = "CREATE TABLE IF NOT EXISTS nb_votes_attente (" +
"player_uuid VARCHAR(36) NOT NULL PRIMARY KEY," +
"player_name VARCHAR(16) NOT NULL," +
"nombre_votes INT DEFAULT 0" +
")";
try (Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement()) {
statement.execute(sql);
} catch (SQLException e) {
plugin.getLogger().severe("Impossible de creer la table nb_votes_attente: " + e.getMessage());
}
}
@Override
public void fetchAndClear(UUID uuid, IntConsumer onResult, Consumer<Exception> onError) {
if (!isConnected()) {
onError.accept(new SQLException("Pool MySQL indisponible (echec de connexion au demarrage)"));
return;
}
String selectSql = "SELECT nombre_votes FROM nb_votes_attente WHERE player_uuid = ?";
String resetSql = "UPDATE nb_votes_attente SET nombre_votes = 0 WHERE player_uuid = ?";
try (Connection connection = dataSource.getConnection()) {
int pendingVotes = 0;
try (PreparedStatement select = connection.prepareStatement(selectSql)) {
select.setString(1, uuid.toString());
try (ResultSet resultSet = select.executeQuery()) {
if (resultSet.next()) {
pendingVotes = resultSet.getInt("nombre_votes");
}
}
}
if (pendingVotes > 0) {
try (PreparedStatement reset = connection.prepareStatement(resetSql)) {
reset.setString(1, uuid.toString());
reset.executeUpdate();
}
}
onResult.accept(pendingVotes);
} catch (SQLException e) {
onError.accept(e);
}
}
@Override
public void peek(UUID uuid, IntConsumer onResult, Consumer<Exception> onError) {
if (!isConnected()) {
onError.accept(new SQLException("Pool MySQL indisponible (echec de connexion au demarrage)"));
return;
}
String sql = "SELECT nombre_votes FROM nb_votes_attente WHERE player_uuid = ?";
try (Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, uuid.toString());
try (ResultSet resultSet = statement.executeQuery()) {
onResult.accept(resultSet.next() ? resultSet.getInt("nombre_votes") : 0);
}
} catch (SQLException e) {
onError.accept(e);
}
}
@Override
public void close() {
if (dataSource != null) {
dataSource.close();
}
}
}
@@ -0,0 +1,27 @@
package fr.northblue.vote.spigot.storage;
import java.util.UUID;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.IntConsumer;
/**
* Stockage des votes en attente (joueurs hors ligne au moment du vote).
* Deux implementations : MySQL (partage reseau) ou fichier YAML local.
*
* Toutes les methodes doivent etre appelees depuis un thread asynchrone
* (Bukkit.getScheduler().runTaskAsynchronously) : les deux implementations font de l'I/O bloquant.
*/
public interface PendingVoteStore {
/** Lit le compteur de votes en attente puis le remet a 0 (utilise par /claim). */
void fetchAndClear(UUID uuid, IntConsumer onResult, Consumer<Exception> onError);
/** Lit le compteur de votes en attente sans le modifier (utilise par l'API). */
void peek(UUID uuid, IntConsumer onResult, Consumer<Exception> onError);
/** Teste que le stockage est joignable/fonctionnel, pour /vote testdb. */
void testConnection(BiConsumer<Boolean, String> callback);
void close();
}
@@ -0,0 +1,80 @@
package fr.northblue.vote.spigot.storage;
import fr.northblue.vote.spigot.NorthBlueVoteSpigot;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.IntConsumer;
/**
* Stockage local des votes en attente dans plugins/votenetwork/pending-votes.yml.
*
* ATTENTION : ce fichier est local a CE serveur. Si le proxy Velocity utilise aussi
* le stockage "yaml", son fichier pending-votes.yml (dossier du plugin Velocity) est
* distinct de celui-ci et n'est PAS synchronise automatiquement entre machines.
* Le mode "yaml" n'est donc fiable que pour un reseau mono-machine (proxy + serveurs
* sur le meme disque, via un lien/point de montage partage) ou pour des tests locaux.
* Pour un vrai reseau multi-serveurs, utilisez "mysql".
*/
public class YamlPendingVoteStore implements PendingVoteStore {
private final File file;
private final ReentrantLock lock = new ReentrantLock();
public YamlPendingVoteStore(NorthBlueVoteSpigot plugin) {
this.file = new File(plugin.getDataFolder(), "pending-votes.yml");
}
@Override
public void fetchAndClear(UUID uuid, IntConsumer onResult, Consumer<Exception> onError) {
lock.lock();
try {
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file);
String path = "players." + uuid;
int votes = yaml.getInt(path + ".votes", 0);
if (votes > 0) {
yaml.set(path + ".votes", 0);
yaml.save(file);
}
onResult.accept(votes);
} catch (IOException e) {
onError.accept(e);
} finally {
lock.unlock();
}
}
@Override
public void peek(UUID uuid, IntConsumer onResult, Consumer<Exception> onError) {
lock.lock();
try {
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file);
onResult.accept(yaml.getInt("players." + uuid + ".votes", 0));
} finally {
lock.unlock();
}
}
@Override
public void testConnection(BiConsumer<Boolean, String> callback) {
lock.lock();
try {
File parent = file.getParentFile();
boolean writable = parent.exists() || parent.mkdirs();
callback.accept(writable, writable
? "Stockage local YAML actif : " + file.getPath()
: "Impossible d'ecrire dans " + file.getPath());
} finally {
lock.unlock();
}
}
@Override
public void close() {
}
}
@@ -0,0 +1,77 @@
package fr.northblue.vote.spigot.voteparty;
import fr.northblue.vote.spigot.NorthBlueVoteSpigot;
import fr.northblue.vote.spigot.VoteConfig;
import org.bukkit.Bukkit;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;
public class VoteParty {
private final NorthBlueVoteSpigot plugin;
private final VoteConfig config;
private final File storageFile;
private final AtomicInteger currentVotes = new AtomicInteger(0);
public VoteParty(NorthBlueVoteSpigot plugin, VoteConfig config) {
this.plugin = plugin;
this.config = config;
this.storageFile = new File(plugin.getDataFolder(), "voteparty.yml");
}
public void load() {
if (!storageFile.exists()) {
currentVotes.set(0);
return;
}
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(storageFile);
currentVotes.set(yaml.getInt("current-votes", 0));
}
public void save() {
YamlConfiguration yaml = new YamlConfiguration();
yaml.set("current-votes", currentVotes.get());
try {
yaml.save(storageFile);
} catch (IOException e) {
plugin.getLogger().severe("Impossible de sauvegarder le VoteParty local: " + e.getMessage());
}
}
/**
* A appeler uniquement depuis le thread principal du serveur.
*/
public void addVotes(int amount) {
int required = config.getVotePartyVotesRequired();
int updated = currentVotes.addAndGet(amount);
if (config.isVotePartyBroadcastEnabled()) {
Bukkit.broadcastMessage(config.getVotePartyProgressMessage()
.replace("%current%", String.valueOf(Math.min(updated, required)))
.replace("%required%", String.valueOf(required)));
}
while (updated >= required && required > 0) {
trigger();
updated = currentVotes.addAndGet(-required);
}
save();
}
private void trigger() {
if (config.isVotePartyBroadcastEnabled()) {
Bukkit.broadcastMessage(config.getVotePartyTriggeredMessage());
}
for (String command : config.getVotePartyCommands()) {
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), command);
}
}
public int getCurrentVotes() {
return currentVotes.get();
}
}
+44
View File
@@ -0,0 +1,44 @@
# =========================================
# VoteNetwork - Configuration (Spigot)
# =========================================
# Stockage des votes en attente (joueurs hors ligne au moment du vote) :
# - mysql : recommande pour un vrai reseau (proxy + plusieurs serveurs). Partage entre toutes les machines.
# - yaml : fichier local plugins/votenetwork/pending-votes.yml. Utile en solo/test, mais N'EST PAS
# partage automatiquement avec le fichier YAML du proxy Velocity si vous etes en reseau.
storage:
type: mysql
mysql:
host: 127.0.0.1
port: 3306
database: votenetwork
user: root
password: changeme
pool-size: 5
# Commandes executees par la console pour recompenser UN vote (vote direct en jeu ou /claim, une fois par vote).
# %player% est remplace par le pseudo du joueur.
rewards:
vote-commands:
- "goldencrates give %player% vote_key 1"
voteparty:
# Nombre de votes cumules (sur CE serveur) declenchant le VoteParty.
votes-requis: 100
# Commandes executees par la console quand le VoteParty se declenche.
commands:
- "goldencrates broadcast_give vote_key 1"
broadcast:
enabled: true
progress-message: "&b[Vote] &f%current%&7/&f%required% &fvotes avant le prochain VoteParty !"
triggered-message: "&a[Vote] &fVoteParty declenche ! Profitez des recompenses !"
messages:
direct-vote: "&a[Vote] &fMerci pour ton vote !"
claim-success: "&a[Vote] &fVous avez recupere &e%amount% &fvote(s) en attente !"
claim-empty: "&c[Vote] &fVous n'avez aucun vote en attente."
maintenance: "&c[Vote] &fLe systeme de vote est actuellement en maintenance."
maintenance-enabled: "&c[Vote] &fMode maintenance active."
maintenance-disabled: "&a[Vote] &fMode maintenance desactive."
no-permission: "&cVous n'avez pas la permission d'utiliser cette commande."
+20
View File
@@ -0,0 +1,20 @@
name: votenetwork
main: fr.northblue.vote.spigot.NorthBlueVoteSpigot
version: ${project.version}
api-version: 1.18
author: VoteNetwork
description: Reception des votes reseau (direct ou en attente) avec VoteParty local.
commands:
claim:
description: Recupere vos votes en attente.
usage: /claim
vote:
description: Administration du systeme de vote (maintenance, test du stockage).
usage: /vote <stop|start|testdb>
permission: votenetwork.admin
permissions:
votenetwork.admin:
description: Autorise l'utilisation de /vote stop|start|testdb.
default: op