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
|
||||
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
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
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`.
|
||||
|
||||
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.
|
||||
- `scoreboard/` — interval-based sidebar scoreboard.
|
||||
- `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.
|
||||
- `util/` — sound/location helpers.
|
||||
|
||||
@@ -22,6 +22,7 @@ dependencies {
|
||||
compileOnly 'io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT'
|
||||
compileOnly 'me.clip:placeholderapi:2.11.6'
|
||||
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 'org.junit.jupiter:junit-jupiter'
|
||||
@@ -46,6 +47,9 @@ shadowJar {
|
||||
archiveClassifier.set('')
|
||||
archiveBaseName.set('BlockParty')
|
||||
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 {
|
||||
|
||||
@@ -42,7 +42,7 @@ public class BlockPartyPlugin extends JavaPlugin implements Listener {
|
||||
this.sessionManager = new SessionManager();
|
||||
this.soundUtil = new SoundUtil(this);
|
||||
this.rewardManager = new RewardManager(this);
|
||||
this.statsManager = new StatsManager(this);
|
||||
this.statsManager = new StatsManager(this, configManager.getStorageSettings());
|
||||
statsManager.init();
|
||||
|
||||
this.arenaManager = new ArenaManager(this);
|
||||
|
||||
@@ -80,4 +80,27 @@ public class ConfigManager {
|
||||
public boolean isPlaceholderApiEnabled() {
|
||||
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;
|
||||
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import us.tss3.blockparty.config.ConfigManager.StorageSettings;
|
||||
|
||||
import java.io.File;
|
||||
import java.sql.Connection;
|
||||
@@ -12,37 +13,72 @@ import java.sql.Statement;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/** SQLite-backed statistics storage. All public methods are safe to call from any thread;
|
||||
* callers are expected to invoke them off the main thread (see StatsManager). */
|
||||
/** SQLite- or MySQL-backed statistics storage, selected via storage.type in config.yml.
|
||||
* 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 {
|
||||
|
||||
private final Plugin plugin;
|
||||
private final StorageSettings settings;
|
||||
private final String table;
|
||||
private Connection connection;
|
||||
|
||||
public StatsDatabase(Plugin plugin) {
|
||||
public StatsDatabase(Plugin plugin, StorageSettings settings) {
|
||||
this.plugin = plugin;
|
||||
this.settings = settings;
|
||||
this.table = settings.isMysql() ? settings.tablePrefix() + "player_stats" : "player_stats";
|
||||
}
|
||||
|
||||
public synchronized void connect() {
|
||||
try {
|
||||
File dbFile = new File(plugin.getDataFolder(), "blockparty.db");
|
||||
Class.forName("org.sqlite.JDBC");
|
||||
connection = DriverManager.getConnection("jdbc:sqlite:" + dbFile.getAbsolutePath());
|
||||
if (settings.isMysql()) {
|
||||
connectMysql();
|
||||
} else {
|
||||
connectSqlite();
|
||||
}
|
||||
try (Statement st = connection.createStatement()) {
|
||||
st.execute("CREATE TABLE IF NOT EXISTS player_stats (" +
|
||||
"uuid TEXT PRIMARY KEY," +
|
||||
"games_played INTEGER NOT NULL DEFAULT 0," +
|
||||
"wins INTEGER NOT NULL DEFAULT 0," +
|
||||
"eliminations INTEGER NOT NULL DEFAULT 0," +
|
||||
"best_round INTEGER NOT NULL DEFAULT 0," +
|
||||
"total_rounds_survived INTEGER NOT NULL DEFAULT 0" +
|
||||
")");
|
||||
st.execute(createTableStatement());
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to initialize SQLite stats database", 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");
|
||||
Class.forName("org.sqlite.JDBC");
|
||||
connection = DriverManager.getConnection("jdbc:sqlite:" + dbFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
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," +
|
||||
"games_played INTEGER NOT NULL DEFAULT 0," +
|
||||
"wins INTEGER NOT NULL DEFAULT 0," +
|
||||
"eliminations INTEGER NOT NULL DEFAULT 0," +
|
||||
"best_round INTEGER NOT NULL DEFAULT 0," +
|
||||
"total_rounds_survived INTEGER NOT NULL DEFAULT 0" +
|
||||
")";
|
||||
}
|
||||
|
||||
public synchronized void close() {
|
||||
try {
|
||||
if (connection != null && !connection.isClosed()) {
|
||||
@@ -53,8 +89,10 @@ public class StatsDatabase {
|
||||
}
|
||||
|
||||
private synchronized void ensureRow(String uuid) throws SQLException {
|
||||
try (PreparedStatement ps = connection.prepareStatement(
|
||||
"INSERT OR IGNORE INTO player_stats(uuid) VALUES (?)")) {
|
||||
String sql = settings.isMysql()
|
||||
? "INSERT IGNORE INTO " + table + "(uuid) VALUES (?)"
|
||||
: "INSERT OR IGNORE INTO " + table + "(uuid) VALUES (?)";
|
||||
try (PreparedStatement ps = connection.prepareStatement(sql)) {
|
||||
ps.setString(1, uuid);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
@@ -67,7 +105,7 @@ public class StatsDatabase {
|
||||
try {
|
||||
ensureRow(uuid.toString());
|
||||
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());
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
@@ -100,7 +138,7 @@ public class StatsDatabase {
|
||||
try {
|
||||
ensureRow(uuid.toString());
|
||||
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.setString(2, uuid.toString());
|
||||
ps.executeUpdate();
|
||||
@@ -117,7 +155,7 @@ public class StatsDatabase {
|
||||
try {
|
||||
ensureRow(uuid.toString());
|
||||
try (PreparedStatement ps = connection.prepareStatement(
|
||||
"UPDATE player_stats SET " + setClause + " WHERE uuid=?")) {
|
||||
"UPDATE " + table + " SET " + setClause + " WHERE uuid=?")) {
|
||||
ps.setString(1, uuid.toString());
|
||||
ps.executeUpdate();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package us.tss3.blockparty.persistence;
|
||||
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import us.tss3.blockparty.config.ConfigManager.StorageSettings;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -11,9 +12,9 @@ public class StatsManager {
|
||||
private final Plugin plugin;
|
||||
private final StatsDatabase database;
|
||||
|
||||
public StatsManager(Plugin plugin) {
|
||||
public StatsManager(Plugin plugin, StorageSettings settings) {
|
||||
this.plugin = plugin;
|
||||
this.database = new StatsDatabase(plugin);
|
||||
this.database = new StatsDatabase(plugin, settings);
|
||||
}
|
||||
|
||||
public void init() {
|
||||
|
||||
@@ -45,6 +45,18 @@ default-floor-materials:
|
||||
- LIGHT_GRAY_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:
|
||||
winner:
|
||||
commands:
|
||||
|
||||
Reference in New Issue
Block a user