From 261e220d1a6fc529676f643ede5aaac7baed8e37 Mon Sep 17 00:00:00 2001 From: SarTron-NorthBlue Date: Sun, 12 Jul 2026 14:33:22 +0400 Subject: [PATCH] Ajoute la portee per-server pour les votes en attente + credits/licence pending-votes.scope (config.properties Velocity) = global (defaut, comportement historique) ou per-server. En per-server, un vote hors ligne incremente le compteur de chaque serveur de direct-vote.servers independamment (nouvelle table nb_votes_attente_serveur) ; /claim sur un serveur ne remet a 0 que ce serveur-la. Necessite server-name (config.yml) et storage.pending-votes-scope assortis cote Spigot. Ajoute LICENSE (MIT) et credit Sar_Tron (plugin.yml, @Plugin Velocity, pom parent, README) en vue de la publication publique. Co-Authored-By: Claude Sonnet 5 --- LICENSE | 21 ++++++ README.md | 29 +++++++- pom.xml | 17 +++++ .../fr/votenetwork/spigot/VoteConfig.java | 17 +++++ .../spigot/storage/MySqlPendingVoteStore.java | 67 +++++++++++++++++-- spigot/src/main/resources/config.yml | 8 +++ spigot/src/main/resources/plugin.yml | 2 +- .../velocity/MySqlPendingVoteStore.java | 60 +++++++++++++---- .../fr/votenetwork/velocity/VoteConfig.java | 21 ++++++ .../velocity/VoteNetworkVelocity.java | 2 +- velocity/src/main/resources/config.properties | 14 ++++ 11 files changed, 238 insertions(+), 20 deletions(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bac21d8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Sar_Tron + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 3c56788..7ea65cf 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # VoteNetwork +Par **Sar_Tron**, sous licence [MIT](LICENSE). + Systeme de vote reseau en deux modules, pour une architecture **proxy Velocity + serveurs Spigot/Paper** (modes A, B, C, ...). - `velocity/` — module proxy. Recoit `votenetwork vote ` (console uniquement) et route le vote en direct ou le met en attente. @@ -110,6 +112,21 @@ Testez la connexion/l'accès au stockage configuré côté Spigot avec : /vote testdb ``` +### Portée des votes en attente : partagée (`global`) ou par serveur (`per-server`) + +En mode `mysql` uniquement, `pending-votes.scope` (dans `config.properties` côté Velocity) contrôle si le compteur de votes en attente est **partagé** entre tous les serveurs ou **indépendant par serveur** : + +- **`global`** (par défaut) : un seul compteur par joueur. Un `/claim` sur n'importe quel serveur le remet à 0 **partout** à la fois. +- **`per-server`** : chaque serveur listé dans `direct-vote.servers` a son propre compteur. Un vote hors ligne incrémente le compteur de **chaque** serveur de la liste. Un `/claim` sur le serveur 1 ne remet à 0 **que** le serveur 1 — le joueur garde ses votes en attente sur le serveur 2 et peut les réclamer séparément là-bas. + + > Exemple : le joueur vote 2 fois hors ligne → 2 votes en attente sur *chaque* serveur. `/claim` sur le serveur 1 → 2 récompenses, serveur 1 repasse à 0, serveur 2 reste à 2. S'il revote une fois → serveur 1 = 1, serveur 2 = 3. + +Pour activer `per-server`, il faut **les deux** : +1. `pending-votes.scope=per-server` + `direct-vote.servers` rempli dans `config.properties` (Velocity). +2. `storage.pending-votes-scope: per-server` + `server-name: "gen1"` (nom exact tiré de `direct-vote.servers`) dans le `config.yml` de **chaque** serveur Spigot concerné. + +Si ces réglages ne correspondent pas entre le proxy et un serveur (scope différent, ou `server-name` absent/mal orthographié), ce serveur ne retrouvera jamais les votes stockés par Velocity. + ## Fonctionnement du vote 1. Un site de vote appelle la console du proxy : `votenetwork vote `. @@ -173,14 +190,24 @@ Utilisez ces placeholders directement dans la config de votre plugin de scoreboa ## Structure de la base de données -Créée automatiquement si absente (mode `mysql`) : +Créées automatiquement si absentes (mode `mysql`), les deux tables coexistent toujours — seule celle correspondant à `pending-votes.scope` est utilisée : ```sql +-- scope = global (par defaut) 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 ); + +-- scope = per-server +CREATE TABLE IF NOT EXISTS nb_votes_attente_serveur ( + player_uuid VARCHAR(36) NOT NULL, + server_name VARCHAR(64) NOT NULL, + player_name VARCHAR(16) NOT NULL, + nombre_votes INT DEFAULT 0, + PRIMARY KEY (player_uuid, server_name) +); ``` --- diff --git a/pom.xml b/pom.xml index 5609b49..68b6a65 100644 --- a/pom.xml +++ b/pom.xml @@ -14,6 +14,23 @@ spigot + VoteNetwork + Systeme de vote reseau Velocity + Spigot/Paper + https://gitea.louconnect.fr/Sar_Tron/VoteNetwork + + + + MIT License + https://opensource.org/licenses/MIT + + + + + + Sar_Tron + + + 17 17 diff --git a/spigot/src/main/java/fr/votenetwork/spigot/VoteConfig.java b/spigot/src/main/java/fr/votenetwork/spigot/VoteConfig.java index 5fca414..0914bd1 100644 --- a/spigot/src/main/java/fr/votenetwork/spigot/VoteConfig.java +++ b/spigot/src/main/java/fr/votenetwork/spigot/VoteConfig.java @@ -30,6 +30,15 @@ public class VoteConfig { return !"yaml".equalsIgnoreCase(getStorageType()); } + /** + * Doit correspondre exactement a pending-votes.scope cote Velocity : + * global : /claim consomme le compteur partage entre tous les serveurs. + * per-server : /claim ne consomme que le compteur de CE serveur (server-name). + */ + public boolean isPerServerScope() { + return "per-server".equalsIgnoreCase(cfg().getString("storage.pending-votes-scope", "global")); + } + public String getMysqlHost() { return cfg().getString("mysql.host", "127.0.0.1"); } @@ -54,6 +63,14 @@ public class VoteConfig { return cfg().getInt("mysql.pool-size", 5); } + /** + * Nom de CE serveur, tel que declare cote Velocity dans direct-vote.servers. + * Uniquement necessaire si le proxy utilise pending-votes.scope=per-server. + */ + public String getServerName() { + return cfg().getString("server-name", ""); + } + public List getVoteRewardCommands() { return cfg().getStringList("rewards.vote-commands"); } diff --git a/spigot/src/main/java/fr/votenetwork/spigot/storage/MySqlPendingVoteStore.java b/spigot/src/main/java/fr/votenetwork/spigot/storage/MySqlPendingVoteStore.java index 12d1c43..78b0a2f 100644 --- a/spigot/src/main/java/fr/votenetwork/spigot/storage/MySqlPendingVoteStore.java +++ b/spigot/src/main/java/fr/votenetwork/spigot/storage/MySqlPendingVoteStore.java @@ -73,16 +73,24 @@ public class MySqlPendingVoteStore implements PendingVoteStore { } private void createTableIfNotExists() { - String sql = "CREATE TABLE IF NOT EXISTS nb_votes_attente (" + + String globalSql = "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" + ")"; + String perServerSql = "CREATE TABLE IF NOT EXISTS nb_votes_attente_serveur (" + + "player_uuid VARCHAR(36) NOT NULL," + + "server_name VARCHAR(64) NOT NULL," + + "player_name VARCHAR(16) NOT NULL," + + "nombre_votes INT DEFAULT 0," + + "PRIMARY KEY (player_uuid, server_name)" + + ")"; try (Connection connection = dataSource.getConnection(); Statement statement = connection.createStatement()) { - statement.execute(sql); + statement.execute(globalSql); + statement.execute(perServerSql); } catch (SQLException e) { - plugin.getLogger().severe("Impossible de creer la table nb_votes_attente: " + e.getMessage()); + plugin.getLogger().severe("Impossible de creer les tables de votes en attente: " + e.getMessage()); } } @@ -92,6 +100,14 @@ public class MySqlPendingVoteStore implements PendingVoteStore { onError.accept(new SQLException("Pool MySQL indisponible (echec de connexion au demarrage)")); return; } + if (config.isPerServerScope()) { + fetchAndClearPerServer(uuid, onResult, onError); + } else { + fetchAndClearGlobal(uuid, onResult, onError); + } + } + + private void fetchAndClearGlobal(UUID uuid, IntConsumer onResult, Consumer onError) { 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 = ?"; @@ -120,16 +136,59 @@ public class MySqlPendingVoteStore implements PendingVoteStore { } } + private void fetchAndClearPerServer(UUID uuid, IntConsumer onResult, Consumer onError) { + if (config.getServerName().isEmpty()) { + onError.accept(new SQLException("storage.pending-votes-scope=per-server mais server-name est vide dans config.yml")); + return; + } + String selectSql = "SELECT nombre_votes FROM nb_votes_attente_serveur WHERE player_uuid = ? AND server_name = ?"; + String resetSql = "UPDATE nb_votes_attente_serveur SET nombre_votes = 0 WHERE player_uuid = ? AND server_name = ?"; + + try (Connection connection = dataSource.getConnection()) { + int pendingVotes = 0; + + try (PreparedStatement select = connection.prepareStatement(selectSql)) { + select.setString(1, uuid.toString()); + select.setString(2, config.getServerName()); + 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.setString(2, config.getServerName()); + reset.executeUpdate(); + } + } + + onResult.accept(pendingVotes); + } catch (SQLException e) { + onError.accept(e); + } + } + @Override public void peek(UUID uuid, IntConsumer onResult, Consumer 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 = ?"; + + boolean perServer = config.isPerServerScope(); + String sql = perServer + ? "SELECT nombre_votes FROM nb_votes_attente_serveur WHERE player_uuid = ? AND server_name = ?" + : "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()); + if (perServer) { + statement.setString(2, config.getServerName()); + } try (ResultSet resultSet = statement.executeQuery()) { onResult.accept(resultSet.next() ? resultSet.getInt("nombre_votes") : 0); } diff --git a/spigot/src/main/resources/config.yml b/spigot/src/main/resources/config.yml index 9421947..c600160 100644 --- a/spigot/src/main/resources/config.yml +++ b/spigot/src/main/resources/config.yml @@ -8,6 +8,14 @@ # partage automatiquement avec le fichier YAML du proxy Velocity si vous etes en reseau. storage: type: mysql + # Doit correspondre EXACTEMENT a pending-votes.scope cote Velocity (config.properties) : + # global (par defaut) : /claim consomme le compteur partage entre tous les serveurs. + # per-server : /claim ne consomme que le compteur de CE serveur (voir server-name ci-dessous). + pending-votes-scope: global + +# Nom de CE serveur, IDENTIQUE a celui utilise dans direct-vote.servers cote Velocity. +# Uniquement necessaire si storage.pending-votes-scope=per-server. +server-name: "" mysql: host: 127.0.0.1 diff --git a/spigot/src/main/resources/plugin.yml b/spigot/src/main/resources/plugin.yml index 644ad54..9031b9a 100644 --- a/spigot/src/main/resources/plugin.yml +++ b/spigot/src/main/resources/plugin.yml @@ -2,7 +2,7 @@ name: votenetwork main: fr.votenetwork.spigot.VoteNetworkSpigot version: ${project.version} api-version: 1.18 -author: VoteNetwork +author: Sar_Tron description: Reception des votes reseau (direct ou en attente) avec VoteParty local. softdepend: [PlaceholderAPI] diff --git a/velocity/src/main/java/fr/votenetwork/velocity/MySqlPendingVoteStore.java b/velocity/src/main/java/fr/votenetwork/velocity/MySqlPendingVoteStore.java index de1fe66..c8a2671 100644 --- a/velocity/src/main/java/fr/votenetwork/velocity/MySqlPendingVoteStore.java +++ b/velocity/src/main/java/fr/votenetwork/velocity/MySqlPendingVoteStore.java @@ -57,16 +57,24 @@ public class MySqlPendingVoteStore implements PendingVoteStore { } private void createTableIfNotExists() { - String sql = "CREATE TABLE IF NOT EXISTS nb_votes_attente (" + + String globalSql = "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" + ")"; + String perServerSql = "CREATE TABLE IF NOT EXISTS nb_votes_attente_serveur (" + + "player_uuid VARCHAR(36) NOT NULL," + + "server_name VARCHAR(64) NOT NULL," + + "player_name VARCHAR(16) NOT NULL," + + "nombre_votes INT DEFAULT 0," + + "PRIMARY KEY (player_uuid, server_name)" + + ")"; try (Connection connection = dataSource.getConnection(); Statement statement = connection.createStatement()) { - statement.execute(sql); + statement.execute(globalSql); + statement.execute(perServerSql); } catch (SQLException e) { - logger.error("Impossible de creer la table nb_votes_attente", e); + logger.error("Impossible de creer les tables de votes en attente", e); } } @@ -77,20 +85,46 @@ public class MySqlPendingVoteStore implements PendingVoteStore { logger.error("Impossible d'enregistrer le vote en attente pour {} : pool MySQL indisponible", playerName); return; } - String sql = "INSERT INTO nb_votes_attente (player_uuid, player_name, nombre_votes) " + - "VALUES (?, ?, 1) " + - "ON DUPLICATE KEY UPDATE nombre_votes = nombre_votes + 1, player_name = VALUES(player_name)"; - try (Connection connection = dataSource.getConnection(); - PreparedStatement statement = connection.prepareStatement(sql)) { - statement.setString(1, uuid.toString()); - statement.setString(2, playerName); - statement.executeUpdate(); - } catch (SQLException e) { - logger.error("Erreur lors de l'insertion du vote en attente pour {}", playerName, e); + if (config.isPerServerScope()) { + addPendingVotePerServer(uuid, playerName); + } else { + addPendingVoteGlobal(uuid, playerName); } }, asyncExecutor); } + private void addPendingVoteGlobal(UUID uuid, String playerName) { + String sql = "INSERT INTO nb_votes_attente (player_uuid, player_name, nombre_votes) " + + "VALUES (?, ?, 1) " + + "ON DUPLICATE KEY UPDATE nombre_votes = nombre_votes + 1, player_name = VALUES(player_name)"; + try (Connection connection = dataSource.getConnection(); + PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, uuid.toString()); + statement.setString(2, playerName); + statement.executeUpdate(); + } catch (SQLException e) { + logger.error("Erreur lors de l'insertion du vote en attente pour {}", playerName, e); + } + } + + private void addPendingVotePerServer(UUID uuid, String playerName) { + String sql = "INSERT INTO nb_votes_attente_serveur (player_uuid, server_name, player_name, nombre_votes) " + + "VALUES (?, ?, ?, 1) " + + "ON DUPLICATE KEY UPDATE nombre_votes = nombre_votes + 1, player_name = VALUES(player_name)"; + try (Connection connection = dataSource.getConnection(); + PreparedStatement statement = connection.prepareStatement(sql)) { + for (String server : config.getDirectVoteServers()) { + statement.setString(1, uuid.toString()); + statement.setString(2, server); + statement.setString(3, playerName); + statement.addBatch(); + } + statement.executeBatch(); + } catch (SQLException e) { + logger.error("Erreur lors de l'insertion du vote en attente (per-server) pour {}", playerName, e); + } + } + @Override public void close() { if (dataSource != null) { diff --git a/velocity/src/main/java/fr/votenetwork/velocity/VoteConfig.java b/velocity/src/main/java/fr/votenetwork/velocity/VoteConfig.java index 3973410..1009b17 100644 --- a/velocity/src/main/java/fr/votenetwork/velocity/VoteConfig.java +++ b/velocity/src/main/java/fr/votenetwork/velocity/VoteConfig.java @@ -26,6 +26,7 @@ public class VoteConfig { private String password; private int poolSize; private Set directVoteServers; + private String pendingVotesScope; public VoteConfig(Path dataDirectory, Logger logger) { this.dataDirectory = dataDirectory; @@ -65,6 +66,13 @@ public class VoteConfig { .filter(s -> !s.isEmpty()) .map(s -> s.toLowerCase(Locale.ROOT)) .collect(Collectors.toSet()); + + this.pendingVotesScope = properties.getProperty("pending-votes.scope", "global"); + if (isPerServerScope() && directVoteServers.isEmpty()) { + logger.warn("pending-votes.scope=per-server mais direct-vote.servers est vide : bascule sur 'global'. " + + "Renseignez direct-vote.servers pour utiliser le mode per-server."); + this.pendingVotesScope = "global"; + } } catch (IOException e) { logger.error("Impossible de charger la configuration Velocity", e); } @@ -119,6 +127,19 @@ public class VoteConfig { return directVoteServers.isEmpty() || directVoteServers.contains(serverName.toLowerCase(Locale.ROOT)); } + /** + * true si chaque serveur doit avoir son propre compteur de votes en attente independant + * (un /claim sur un serveur ne remet a 0 que ce serveur-la). Sinon (par defaut), un seul + * compteur est partage entre tous les serveurs : /claim le remet a 0 partout a la fois. + */ + public boolean isPerServerScope() { + return "per-server".equalsIgnoreCase(pendingVotesScope); + } + + public Set getDirectVoteServers() { + return directVoteServers; + } + public String getJdbcUrl() { return "jdbc:mysql://" + host + ":" + port + "/" + database + "?useSSL=false&autoReconnect=true&characterEncoding=utf8"; diff --git a/velocity/src/main/java/fr/votenetwork/velocity/VoteNetworkVelocity.java b/velocity/src/main/java/fr/votenetwork/velocity/VoteNetworkVelocity.java index 1ac5017..d507155 100644 --- a/velocity/src/main/java/fr/votenetwork/velocity/VoteNetworkVelocity.java +++ b/velocity/src/main/java/fr/votenetwork/velocity/VoteNetworkVelocity.java @@ -16,7 +16,7 @@ import java.nio.file.Path; name = "VoteNetwork", version = "1.0.0", description = "Systeme de vote reseau Velocity <-> Spigot", - authors = {"VoteNetwork"} + authors = {"Sar_Tron"} ) public class VoteNetworkVelocity { diff --git a/velocity/src/main/resources/config.properties b/velocity/src/main/resources/config.properties index 378ac4b..cd68d1d 100644 --- a/velocity/src/main/resources/config.properties +++ b/velocity/src/main/resources/config.properties @@ -20,3 +20,17 @@ mysql.pool-size=5 # Laisser vide = tous les serveurs sont consideres comme ayant le plugin (deconseille des # qu'un lobby/hub sans VoteNetwork-Spigot existe sur le reseau). direct-vote.servers=gen1,gen2 + +# Portee des votes en attente (joueur hors ligne au moment du vote), uniquement en storage.type=mysql : +# global (par defaut) : un seul compteur partage entre tous les serveurs. Un /claim sur +# N'IMPORTE QUEL serveur remet le compteur a 0 pour TOUS les serveurs a la fois. +# per-server : chaque serveur de direct-vote.servers a son propre compteur independant. Un +# vote hors ligne incremente le compteur de CHAQUE serveur de la liste. Un /claim +# sur le serveur 1 ne remet a 0 QUE le compteur du serveur 1 : le joueur garde ses +# votes en attente sur le serveur 2 et peut les reclamer separement la-bas. +# Exemple concret en per-server : le joueur vote 2 fois hors ligne -> 2 votes en attente sur +# CHAQUE serveur. Il fait /claim sur le serveur 1 -> recoit 2 recompenses, serveur 1 repasse a 0, +# serveur 2 reste a 2. S'il revote une fois -> serveur 1 = 1, serveur 2 = 3. +# Necessite direct-vote.servers rempli ET que chaque serveur Spigot declare son server-name +# (config.yml) correspondant exactement a un nom de cette liste. +pending-votes.scope=global