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 <noreply@anthropic.com>
This commit is contained in:
SarTron-NorthBlue
2026-07-12 14:33:22 +04:00
co-authored by Claude Sonnet 5
parent 74b2914529
commit 261e220d1a
11 changed files with 238 additions and 20 deletions
+21
View File
@@ -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.
+28 -1
View File
@@ -1,5 +1,7 @@
# VoteNetwork # 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, ...). 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 <pseudo>` (console uniquement) et route le vote en direct ou le met en attente. - `velocity/` — module proxy. Recoit `votenetwork vote <pseudo>` (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 /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 ## Fonctionnement du vote
1. Un site de vote appelle la console du proxy : `votenetwork vote <pseudo>`. 1. Un site de vote appelle la console du proxy : `votenetwork vote <pseudo>`.
@@ -173,14 +190,24 @@ Utilisez ces placeholders directement dans la config de votre plugin de scoreboa
## Structure de la base de données ## 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 ```sql
-- scope = global (par defaut)
CREATE TABLE IF NOT EXISTS nb_votes_attente ( CREATE TABLE IF NOT EXISTS nb_votes_attente (
player_uuid VARCHAR(36) NOT NULL PRIMARY KEY, player_uuid VARCHAR(36) NOT NULL PRIMARY KEY,
player_name VARCHAR(16) NOT NULL, player_name VARCHAR(16) NOT NULL,
nombre_votes INT DEFAULT 0 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)
);
``` ```
--- ---
+17
View File
@@ -14,6 +14,23 @@
<module>spigot</module> <module>spigot</module>
</modules> </modules>
<name>VoteNetwork</name>
<description>Systeme de vote reseau Velocity + Spigot/Paper</description>
<url>https://gitea.louconnect.fr/Sar_Tron/VoteNetwork</url>
<licenses>
<license>
<name>MIT License</name>
<url>https://opensource.org/licenses/MIT</url>
</license>
</licenses>
<developers>
<developer>
<name>Sar_Tron</name>
</developer>
</developers>
<properties> <properties>
<maven.compiler.source>17</maven.compiler.source> <maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target> <maven.compiler.target>17</maven.compiler.target>
@@ -30,6 +30,15 @@ public class VoteConfig {
return !"yaml".equalsIgnoreCase(getStorageType()); 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() { public String getMysqlHost() {
return cfg().getString("mysql.host", "127.0.0.1"); return cfg().getString("mysql.host", "127.0.0.1");
} }
@@ -54,6 +63,14 @@ public class VoteConfig {
return cfg().getInt("mysql.pool-size", 5); 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<String> getVoteRewardCommands() { public List<String> getVoteRewardCommands() {
return cfg().getStringList("rewards.vote-commands"); return cfg().getStringList("rewards.vote-commands");
} }
@@ -73,16 +73,24 @@ public class MySqlPendingVoteStore implements PendingVoteStore {
} }
private void createTableIfNotExists() { 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_uuid VARCHAR(36) NOT NULL PRIMARY KEY," +
"player_name VARCHAR(16) NOT NULL," + "player_name VARCHAR(16) NOT NULL," +
"nombre_votes INT DEFAULT 0" + "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(); try (Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement()) { Statement statement = connection.createStatement()) {
statement.execute(sql); statement.execute(globalSql);
statement.execute(perServerSql);
} catch (SQLException e) { } 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)")); onError.accept(new SQLException("Pool MySQL indisponible (echec de connexion au demarrage)"));
return; return;
} }
if (config.isPerServerScope()) {
fetchAndClearPerServer(uuid, onResult, onError);
} else {
fetchAndClearGlobal(uuid, onResult, onError);
}
}
private void fetchAndClearGlobal(UUID uuid, IntConsumer onResult, Consumer<Exception> onError) {
String selectSql = "SELECT nombre_votes FROM nb_votes_attente WHERE player_uuid = ?"; 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 = ?"; 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<Exception> 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 @Override
public void peek(UUID uuid, IntConsumer onResult, Consumer<Exception> onError) { public void peek(UUID uuid, IntConsumer onResult, Consumer<Exception> onError) {
if (!isConnected()) { if (!isConnected()) {
onError.accept(new SQLException("Pool MySQL indisponible (echec de connexion au demarrage)")); onError.accept(new SQLException("Pool MySQL indisponible (echec de connexion au demarrage)"));
return; 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(); try (Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement(sql)) { PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, uuid.toString()); statement.setString(1, uuid.toString());
if (perServer) {
statement.setString(2, config.getServerName());
}
try (ResultSet resultSet = statement.executeQuery()) { try (ResultSet resultSet = statement.executeQuery()) {
onResult.accept(resultSet.next() ? resultSet.getInt("nombre_votes") : 0); onResult.accept(resultSet.next() ? resultSet.getInt("nombre_votes") : 0);
} }
+8
View File
@@ -8,6 +8,14 @@
# partage automatiquement avec le fichier YAML du proxy Velocity si vous etes en reseau. # partage automatiquement avec le fichier YAML du proxy Velocity si vous etes en reseau.
storage: storage:
type: mysql 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: mysql:
host: 127.0.0.1 host: 127.0.0.1
+1 -1
View File
@@ -2,7 +2,7 @@ name: votenetwork
main: fr.votenetwork.spigot.VoteNetworkSpigot main: fr.votenetwork.spigot.VoteNetworkSpigot
version: ${project.version} version: ${project.version}
api-version: 1.18 api-version: 1.18
author: VoteNetwork author: Sar_Tron
description: Reception des votes reseau (direct ou en attente) avec VoteParty local. description: Reception des votes reseau (direct ou en attente) avec VoteParty local.
softdepend: [PlaceholderAPI] softdepend: [PlaceholderAPI]
@@ -57,16 +57,24 @@ public class MySqlPendingVoteStore implements PendingVoteStore {
} }
private void createTableIfNotExists() { 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_uuid VARCHAR(36) NOT NULL PRIMARY KEY," +
"player_name VARCHAR(16) NOT NULL," + "player_name VARCHAR(16) NOT NULL," +
"nombre_votes INT DEFAULT 0" + "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(); try (Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement()) { Statement statement = connection.createStatement()) {
statement.execute(sql); statement.execute(globalSql);
statement.execute(perServerSql);
} catch (SQLException e) { } 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,6 +85,15 @@ public class MySqlPendingVoteStore implements PendingVoteStore {
logger.error("Impossible d'enregistrer le vote en attente pour {} : pool MySQL indisponible", playerName); logger.error("Impossible d'enregistrer le vote en attente pour {} : pool MySQL indisponible", playerName);
return; return;
} }
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) " + String sql = "INSERT INTO nb_votes_attente (player_uuid, player_name, nombre_votes) " +
"VALUES (?, ?, 1) " + "VALUES (?, ?, 1) " +
"ON DUPLICATE KEY UPDATE nombre_votes = nombre_votes + 1, player_name = VALUES(player_name)"; "ON DUPLICATE KEY UPDATE nombre_votes = nombre_votes + 1, player_name = VALUES(player_name)";
@@ -88,7 +105,24 @@ public class MySqlPendingVoteStore implements PendingVoteStore {
} catch (SQLException e) { } catch (SQLException e) {
logger.error("Erreur lors de l'insertion du vote en attente pour {}", playerName, e); logger.error("Erreur lors de l'insertion du vote en attente pour {}", playerName, e);
} }
}, asyncExecutor); }
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 @Override
@@ -26,6 +26,7 @@ public class VoteConfig {
private String password; private String password;
private int poolSize; private int poolSize;
private Set<String> directVoteServers; private Set<String> directVoteServers;
private String pendingVotesScope;
public VoteConfig(Path dataDirectory, Logger logger) { public VoteConfig(Path dataDirectory, Logger logger) {
this.dataDirectory = dataDirectory; this.dataDirectory = dataDirectory;
@@ -65,6 +66,13 @@ public class VoteConfig {
.filter(s -> !s.isEmpty()) .filter(s -> !s.isEmpty())
.map(s -> s.toLowerCase(Locale.ROOT)) .map(s -> s.toLowerCase(Locale.ROOT))
.collect(Collectors.toSet()); .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) { } catch (IOException e) {
logger.error("Impossible de charger la configuration Velocity", 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)); 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<String> getDirectVoteServers() {
return directVoteServers;
}
public String getJdbcUrl() { public String getJdbcUrl() {
return "jdbc:mysql://" + host + ":" + port + "/" + database return "jdbc:mysql://" + host + ":" + port + "/" + database
+ "?useSSL=false&autoReconnect=true&characterEncoding=utf8"; + "?useSSL=false&autoReconnect=true&characterEncoding=utf8";
@@ -16,7 +16,7 @@ import java.nio.file.Path;
name = "VoteNetwork", name = "VoteNetwork",
version = "1.0.0", version = "1.0.0",
description = "Systeme de vote reseau Velocity <-> Spigot", description = "Systeme de vote reseau Velocity <-> Spigot",
authors = {"VoteNetwork"} authors = {"Sar_Tron"}
) )
public class VoteNetworkVelocity { public class VoteNetworkVelocity {
@@ -20,3 +20,17 @@ mysql.pool-size=5
# Laisser vide = tous les serveurs sont consideres comme ayant le plugin (deconseille des # Laisser vide = tous les serveurs sont consideres comme ayant le plugin (deconseille des
# qu'un lobby/hub sans VoteNetwork-Spigot existe sur le reseau). # qu'un lobby/hub sans VoteNetwork-Spigot existe sur le reseau).
direct-vote.servers=gen1,gen2 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