This commit is contained in:
@@ -151,6 +151,31 @@ Commands run as the console, so no Vault/economy plugin is required by BlockPart
|
|||||||
if you reference an economy command (like `eco give`) you need that plugin installed
|
if you reference an economy command (like `eco give`) you need that plugin installed
|
||||||
separately. `%player%`, `%arena%` and `%round%` are replaced before dispatch.
|
separately. `%player%`, `%arena%` and `%round%` are replaced before dispatch.
|
||||||
|
|
||||||
|
## Statistics storage
|
||||||
|
|
||||||
|
By default player statistics are stored in a local SQLite file (`plugins/BlockParty/blockparty.db`) —
|
||||||
|
zero setup required. To use a shared MySQL database instead (useful across a network of servers),
|
||||||
|
set `storage.type: mysql` in `config.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
storage:
|
||||||
|
type: mysql
|
||||||
|
mysql:
|
||||||
|
host: localhost
|
||||||
|
port: 3306
|
||||||
|
database: blockparty
|
||||||
|
username: blockparty
|
||||||
|
password: password
|
||||||
|
use-ssl: false
|
||||||
|
table-prefix: "bp_"
|
||||||
|
```
|
||||||
|
|
||||||
|
The MySQL JDBC driver (`mysql-connector-j`) is bundled in the plugin jar, so no extra
|
||||||
|
download is required. The stats table is created automatically on startup if it doesn't
|
||||||
|
exist. All queries run asynchronously off the main thread regardless of storage type.
|
||||||
|
Changing `storage.type` requires a server restart (it's read once at startup, not on
|
||||||
|
`/blockparty reload`).
|
||||||
|
|
||||||
## PlaceholderAPI support
|
## PlaceholderAPI support
|
||||||
|
|
||||||
If PlaceholderAPI is installed, BlockParty registers a `%blockparty_...%` expansion
|
If PlaceholderAPI is installed, BlockParty registers a `%blockparty_...%` expansion
|
||||||
@@ -169,7 +194,7 @@ automatically (soft-dependency — startup never fails if PAPI is absent):
|
|||||||
./gradlew clean build
|
./gradlew clean build
|
||||||
```
|
```
|
||||||
|
|
||||||
The shaded plugin jar (with `sqlite-jdbc` bundled and relocated) is produced at
|
The shaded plugin jar (with `sqlite-jdbc` and `mysql-connector-j` bundled) is produced at
|
||||||
`build/libs/BlockParty-<version>.jar`.
|
`build/libs/BlockParty-<version>.jar`.
|
||||||
|
|
||||||
Run unit tests only:
|
Run unit tests only:
|
||||||
@@ -208,6 +233,6 @@ elimination/winner determination, arena config validation, and player-count/join
|
|||||||
- `listener/` — connection/quit handling and gameplay restrictions.
|
- `listener/` — connection/quit handling and gameplay restrictions.
|
||||||
- `scoreboard/` — interval-based sidebar scoreboard.
|
- `scoreboard/` — interval-based sidebar scoreboard.
|
||||||
- `reward/` — console-command reward dispatch.
|
- `reward/` — console-command reward dispatch.
|
||||||
- `persistence/` — async SQLite-backed stats (`StatsDatabase`, `StatsManager`).
|
- `persistence/` — async stats storage over SQLite or MySQL (`StatsDatabase`, `StatsManager`).
|
||||||
- `placeholder/` — optional PlaceholderAPI expansion, loaded only when PAPI is present.
|
- `placeholder/` — optional PlaceholderAPI expansion, loaded only when PAPI is present.
|
||||||
- `util/` — sound/location helpers.
|
- `util/` — sound/location helpers.
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ dependencies {
|
|||||||
compileOnly 'io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT'
|
compileOnly 'io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT'
|
||||||
compileOnly 'me.clip:placeholderapi:2.11.6'
|
compileOnly 'me.clip:placeholderapi:2.11.6'
|
||||||
implementation 'org.xerial:sqlite-jdbc:3.47.1.0'
|
implementation 'org.xerial:sqlite-jdbc:3.47.1.0'
|
||||||
|
implementation 'com.mysql:mysql-connector-j:9.1.0'
|
||||||
|
|
||||||
testImplementation platform('org.junit:junit-bom:5.11.3')
|
testImplementation platform('org.junit:junit-bom:5.11.3')
|
||||||
testImplementation 'org.junit.jupiter:junit-jupiter'
|
testImplementation 'org.junit.jupiter:junit-jupiter'
|
||||||
@@ -46,6 +47,9 @@ shadowJar {
|
|||||||
archiveClassifier.set('')
|
archiveClassifier.set('')
|
||||||
archiveBaseName.set('BlockParty')
|
archiveBaseName.set('BlockParty')
|
||||||
relocate 'org.sqlite', 'us.tss3.blockparty.libs.sqlite'
|
relocate 'org.sqlite', 'us.tss3.blockparty.libs.sqlite'
|
||||||
|
// com.mysql is intentionally left unrelocated: mysql-connector-j is loaded by fully
|
||||||
|
// qualified class name (com.mysql.cj.jdbc.Driver) at runtime via Class.forName.
|
||||||
|
mergeServiceFiles()
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ public class BlockPartyPlugin extends JavaPlugin implements Listener {
|
|||||||
this.sessionManager = new SessionManager();
|
this.sessionManager = new SessionManager();
|
||||||
this.soundUtil = new SoundUtil(this);
|
this.soundUtil = new SoundUtil(this);
|
||||||
this.rewardManager = new RewardManager(this);
|
this.rewardManager = new RewardManager(this);
|
||||||
this.statsManager = new StatsManager(this);
|
this.statsManager = new StatsManager(this, configManager.getStorageSettings());
|
||||||
statsManager.init();
|
statsManager.init();
|
||||||
|
|
||||||
this.arenaManager = new ArenaManager(this);
|
this.arenaManager = new ArenaManager(this);
|
||||||
|
|||||||
@@ -80,4 +80,27 @@ public class ConfigManager {
|
|||||||
public boolean isPlaceholderApiEnabled() {
|
public boolean isPlaceholderApiEnabled() {
|
||||||
return config.getBoolean("integrations.placeholderapi", true);
|
return config.getBoolean("integrations.placeholderapi", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public StorageSettings getStorageSettings() {
|
||||||
|
String type = config.getString("storage.type", "sqlite");
|
||||||
|
return new StorageSettings(
|
||||||
|
type,
|
||||||
|
config.getString("storage.mysql.host", "localhost"),
|
||||||
|
config.getInt("storage.mysql.port", 3306),
|
||||||
|
config.getString("storage.mysql.database", "blockparty"),
|
||||||
|
config.getString("storage.mysql.username", "blockparty"),
|
||||||
|
config.getString("storage.mysql.password", ""),
|
||||||
|
config.getBoolean("storage.mysql.use-ssl", false),
|
||||||
|
config.getString("storage.mysql.table-prefix", "bp_")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Immutable snapshot of the storage.* config section. */
|
||||||
|
public record StorageSettings(String type, String host, int port, String database,
|
||||||
|
String username, String password, boolean useSsl, String tablePrefix) {
|
||||||
|
|
||||||
|
public boolean isMysql() {
|
||||||
|
return "mysql".equalsIgnoreCase(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package us.tss3.blockparty.persistence;
|
package us.tss3.blockparty.persistence;
|
||||||
|
|
||||||
import org.bukkit.plugin.Plugin;
|
import org.bukkit.plugin.Plugin;
|
||||||
|
import us.tss3.blockparty.config.ConfigManager.StorageSettings;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
@@ -12,35 +13,70 @@ import java.sql.Statement;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
/** SQLite-backed statistics storage. All public methods are safe to call from any thread;
|
/** SQLite- or MySQL-backed statistics storage, selected via storage.type in config.yml.
|
||||||
* callers are expected to invoke them off the main thread (see StatsManager). */
|
* All public methods are safe to call from any thread; callers are expected to invoke
|
||||||
|
* them off the main thread (see StatsManager). */
|
||||||
public class StatsDatabase {
|
public class StatsDatabase {
|
||||||
|
|
||||||
private final Plugin plugin;
|
private final Plugin plugin;
|
||||||
|
private final StorageSettings settings;
|
||||||
|
private final String table;
|
||||||
private Connection connection;
|
private Connection connection;
|
||||||
|
|
||||||
public StatsDatabase(Plugin plugin) {
|
public StatsDatabase(Plugin plugin, StorageSettings settings) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
|
this.settings = settings;
|
||||||
|
this.table = settings.isMysql() ? settings.tablePrefix() + "player_stats" : "player_stats";
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void connect() {
|
public synchronized void connect() {
|
||||||
try {
|
try {
|
||||||
|
if (settings.isMysql()) {
|
||||||
|
connectMysql();
|
||||||
|
} else {
|
||||||
|
connectSqlite();
|
||||||
|
}
|
||||||
|
try (Statement st = connection.createStatement()) {
|
||||||
|
st.execute(createTableStatement());
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
plugin.getLogger().log(Level.SEVERE, "Failed to initialize " +
|
||||||
|
(settings.isMysql() ? "MySQL" : "SQLite") + " stats database", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void connectSqlite() throws Exception {
|
||||||
File dbFile = new File(plugin.getDataFolder(), "blockparty.db");
|
File dbFile = new File(plugin.getDataFolder(), "blockparty.db");
|
||||||
Class.forName("org.sqlite.JDBC");
|
Class.forName("org.sqlite.JDBC");
|
||||||
connection = DriverManager.getConnection("jdbc:sqlite:" + dbFile.getAbsolutePath());
|
connection = DriverManager.getConnection("jdbc:sqlite:" + dbFile.getAbsolutePath());
|
||||||
try (Statement st = connection.createStatement()) {
|
}
|
||||||
st.execute("CREATE TABLE IF NOT EXISTS player_stats (" +
|
|
||||||
|
private void connectMysql() throws Exception {
|
||||||
|
Class.forName("com.mysql.cj.jdbc.Driver");
|
||||||
|
String url = "jdbc:mysql://" + settings.host() + ":" + settings.port() + "/" + settings.database()
|
||||||
|
+ "?useSSL=" + settings.useSsl() + "&autoReconnect=true&characterEncoding=utf8";
|
||||||
|
connection = DriverManager.getConnection(url, settings.username(), settings.password());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createTableStatement() {
|
||||||
|
if (settings.isMysql()) {
|
||||||
|
return "CREATE TABLE IF NOT EXISTS " + table + " (" +
|
||||||
|
"uuid VARCHAR(36) PRIMARY KEY," +
|
||||||
|
"games_played INT NOT NULL DEFAULT 0," +
|
||||||
|
"wins INT NOT NULL DEFAULT 0," +
|
||||||
|
"eliminations INT NOT NULL DEFAULT 0," +
|
||||||
|
"best_round INT NOT NULL DEFAULT 0," +
|
||||||
|
"total_rounds_survived INT NOT NULL DEFAULT 0" +
|
||||||
|
")";
|
||||||
|
}
|
||||||
|
return "CREATE TABLE IF NOT EXISTS " + table + " (" +
|
||||||
"uuid TEXT PRIMARY KEY," +
|
"uuid TEXT PRIMARY KEY," +
|
||||||
"games_played INTEGER NOT NULL DEFAULT 0," +
|
"games_played INTEGER NOT NULL DEFAULT 0," +
|
||||||
"wins INTEGER NOT NULL DEFAULT 0," +
|
"wins INTEGER NOT NULL DEFAULT 0," +
|
||||||
"eliminations INTEGER NOT NULL DEFAULT 0," +
|
"eliminations INTEGER NOT NULL DEFAULT 0," +
|
||||||
"best_round INTEGER NOT NULL DEFAULT 0," +
|
"best_round INTEGER NOT NULL DEFAULT 0," +
|
||||||
"total_rounds_survived INTEGER NOT NULL DEFAULT 0" +
|
"total_rounds_survived INTEGER NOT NULL DEFAULT 0" +
|
||||||
")");
|
")";
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
plugin.getLogger().log(Level.SEVERE, "Failed to initialize SQLite stats database", ex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void close() {
|
public synchronized void close() {
|
||||||
@@ -53,8 +89,10 @@ public class StatsDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private synchronized void ensureRow(String uuid) throws SQLException {
|
private synchronized void ensureRow(String uuid) throws SQLException {
|
||||||
try (PreparedStatement ps = connection.prepareStatement(
|
String sql = settings.isMysql()
|
||||||
"INSERT OR IGNORE INTO player_stats(uuid) VALUES (?)")) {
|
? "INSERT IGNORE INTO " + table + "(uuid) VALUES (?)"
|
||||||
|
: "INSERT OR IGNORE INTO " + table + "(uuid) VALUES (?)";
|
||||||
|
try (PreparedStatement ps = connection.prepareStatement(sql)) {
|
||||||
ps.setString(1, uuid);
|
ps.setString(1, uuid);
|
||||||
ps.executeUpdate();
|
ps.executeUpdate();
|
||||||
}
|
}
|
||||||
@@ -67,7 +105,7 @@ public class StatsDatabase {
|
|||||||
try {
|
try {
|
||||||
ensureRow(uuid.toString());
|
ensureRow(uuid.toString());
|
||||||
try (PreparedStatement ps = connection.prepareStatement(
|
try (PreparedStatement ps = connection.prepareStatement(
|
||||||
"SELECT games_played, wins, eliminations, best_round, total_rounds_survived FROM player_stats WHERE uuid=?")) {
|
"SELECT games_played, wins, eliminations, best_round, total_rounds_survived FROM " + table + " WHERE uuid=?")) {
|
||||||
ps.setString(1, uuid.toString());
|
ps.setString(1, uuid.toString());
|
||||||
try (ResultSet rs = ps.executeQuery()) {
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
if (rs.next()) {
|
if (rs.next()) {
|
||||||
@@ -100,7 +138,7 @@ public class StatsDatabase {
|
|||||||
try {
|
try {
|
||||||
ensureRow(uuid.toString());
|
ensureRow(uuid.toString());
|
||||||
try (PreparedStatement ps = connection.prepareStatement(
|
try (PreparedStatement ps = connection.prepareStatement(
|
||||||
"UPDATE player_stats SET best_round = MAX(best_round, ?), total_rounds_survived = total_rounds_survived + 1 WHERE uuid=?")) {
|
"UPDATE " + table + " SET best_round = MAX(best_round, ?), total_rounds_survived = total_rounds_survived + 1 WHERE uuid=?")) {
|
||||||
ps.setInt(1, round);
|
ps.setInt(1, round);
|
||||||
ps.setString(2, uuid.toString());
|
ps.setString(2, uuid.toString());
|
||||||
ps.executeUpdate();
|
ps.executeUpdate();
|
||||||
@@ -117,7 +155,7 @@ public class StatsDatabase {
|
|||||||
try {
|
try {
|
||||||
ensureRow(uuid.toString());
|
ensureRow(uuid.toString());
|
||||||
try (PreparedStatement ps = connection.prepareStatement(
|
try (PreparedStatement ps = connection.prepareStatement(
|
||||||
"UPDATE player_stats SET " + setClause + " WHERE uuid=?")) {
|
"UPDATE " + table + " SET " + setClause + " WHERE uuid=?")) {
|
||||||
ps.setString(1, uuid.toString());
|
ps.setString(1, uuid.toString());
|
||||||
ps.executeUpdate();
|
ps.executeUpdate();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package us.tss3.blockparty.persistence;
|
package us.tss3.blockparty.persistence;
|
||||||
|
|
||||||
import org.bukkit.plugin.Plugin;
|
import org.bukkit.plugin.Plugin;
|
||||||
|
import us.tss3.blockparty.config.ConfigManager.StorageSettings;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
@@ -11,9 +12,9 @@ public class StatsManager {
|
|||||||
private final Plugin plugin;
|
private final Plugin plugin;
|
||||||
private final StatsDatabase database;
|
private final StatsDatabase database;
|
||||||
|
|
||||||
public StatsManager(Plugin plugin) {
|
public StatsManager(Plugin plugin, StorageSettings settings) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
this.database = new StatsDatabase(plugin);
|
this.database = new StatsDatabase(plugin, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void init() {
|
public void init() {
|
||||||
|
|||||||
@@ -45,6 +45,18 @@ default-floor-materials:
|
|||||||
- LIGHT_GRAY_CONCRETE
|
- LIGHT_GRAY_CONCRETE
|
||||||
- BROWN_CONCRETE
|
- BROWN_CONCRETE
|
||||||
|
|
||||||
|
storage:
|
||||||
|
# type: sqlite (default, file-based, zero setup) or mysql
|
||||||
|
type: sqlite
|
||||||
|
mysql:
|
||||||
|
host: localhost
|
||||||
|
port: 3306
|
||||||
|
database: blockparty
|
||||||
|
username: blockparty
|
||||||
|
password: password
|
||||||
|
use-ssl: false
|
||||||
|
table-prefix: "bp_"
|
||||||
|
|
||||||
rewards:
|
rewards:
|
||||||
winner:
|
winner:
|
||||||
commands:
|
commands:
|
||||||
|
|||||||
Reference in New Issue
Block a user