From 41a042c470d6e2231cb366234e6946ed2a2602e1 Mon Sep 17 00:00:00 2001 From: EarthCow <56940983+EarthCow@users.noreply.github.com> Date: Sat, 14 Mar 2026 17:44:11 -0400 Subject: [PATCH 01/14] Adds #exportAll to storage interfaces --- .../networkjoinmessages/common/storage/PlayerDataStore.java | 3 +++ .../networkjoinmessages/common/storage/PlayerJoinTracker.java | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/PlayerDataStore.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/PlayerDataStore.java index 45d2174..486dfef 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/PlayerDataStore.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/PlayerDataStore.java @@ -3,6 +3,7 @@ import org.jetbrains.annotations.Nullable; import xyz.earthcow.networkjoinmessages.common.util.PlayerDataSnapshot; +import java.util.Map; import java.util.UUID; public interface PlayerDataStore extends AutoCloseable { @@ -29,6 +30,8 @@ public interface PlayerDataStore extends AutoCloseable { @Nullable UUID resolveUuid(String playerName); + Map exportAll(); + /** No-op default so callers don't need to handle checked exceptions for backends that don't need closing. */ @Override default void close() throws Exception {} diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/PlayerJoinTracker.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/PlayerJoinTracker.java index fadafa1..7510158 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/PlayerJoinTracker.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/PlayerJoinTracker.java @@ -9,6 +9,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Map; import java.util.UUID; /** @@ -53,6 +54,8 @@ default boolean addUsersFromUserCache(String userCacheStr) { } } + Map exportAll(); + /** No-op default so callers don't need to handle checked exceptions for backends that don't need closing. */ @Override default void close() throws Exception {} From 9aa6c00dd618d16ce674662a13b5659b657217fa Mon Sep 17 00:00:00 2001 From: EarthCow <56940983+EarthCow@users.noreply.github.com> Date: Sat, 14 Mar 2026 17:45:36 -0400 Subject: [PATCH 02/14] Implements H2PlayerJoinTracker#exportAll --- .../common/storage/H2PlayerJoinTracker.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/H2PlayerJoinTracker.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/H2PlayerJoinTracker.java index 0e063ae..c8e07ff 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/H2PlayerJoinTracker.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/H2PlayerJoinTracker.java @@ -3,6 +3,8 @@ import xyz.earthcow.networkjoinmessages.common.abstraction.CoreLogger; import java.sql.*; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.UUID; /** @@ -22,6 +24,9 @@ public class H2PlayerJoinTracker extends H2Handler implements PlayerJoinTracker private static final String UPSERT_SQL = "MERGE INTO players_joined (player_uuid, player_name) KEY(player_uuid) VALUES (?, ?)"; + private static final String EXPORT_SQL = + "SELECT player_uuid, player_name FROM players_joined"; + public H2PlayerJoinTracker(CoreLogger logger, String dbPath) throws SQLException { super(logger, dbPath); } @@ -60,4 +65,23 @@ public synchronized void markAsJoined(UUID playerUuid, String playerName) { logger.severe("SQL failure: Could not mark player '" + playerName + "' (" + playerUuid + ") as joined"); } } + + /** + * Exports all first-join records as a UUID -> player name snapshot. + * Used during storage type migration. + */ + @Override + public synchronized Map exportAll() { + Map result = new LinkedHashMap<>(); + if (isConnectionInvalid()) return result; + try (Statement stmt = connection().createStatement(); + ResultSet rs = stmt.executeQuery(EXPORT_SQL)) { + while (rs.next()) { + result.put(UUID.fromString(rs.getString("player_uuid")), rs.getString("player_name")); + } + } catch (SQLException e) { + logger.severe("[H2PlayerJoinTracker] SQL failure during exportAll(): " + e.getMessage()); + } + return result; + } } From dfe6efcbc6fe2134ef05ed286201bab73682d90c Mon Sep 17 00:00:00 2001 From: EarthCow <56940983+EarthCow@users.noreply.github.com> Date: Sat, 14 Mar 2026 17:49:28 -0400 Subject: [PATCH 03/14] Implements H2PlayerDataStore#exportAll --- .../common/storage/H2PlayerDataStore.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/H2PlayerDataStore.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/H2PlayerDataStore.java index a3999a8..f09c877 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/H2PlayerDataStore.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/H2PlayerDataStore.java @@ -5,6 +5,8 @@ import xyz.earthcow.networkjoinmessages.common.util.PlayerDataSnapshot; import java.sql.*; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.UUID; /** @@ -36,6 +38,9 @@ public class H2PlayerDataStore extends H2Handler implements PlayerDataStore { private static final String RESOLVE_SQL = "SELECT player_uuid FROM players WHERE LOWER(player_name) = LOWER(?)"; + private static final String EXPORT_SQL = + "SELECT * FROM players"; + // MERGE upserts the identity columns on first insert, then UPDATE sets all // preference columns so an existing row is fully overwritten on save. private static final String UPSERT_SQL = @@ -107,4 +112,26 @@ public synchronized UUID resolveUuid(String playerName) { } return null; } + + @Override + public synchronized Map exportAll() { + Map result = new LinkedHashMap<>(); + if (isConnectionInvalid()) return result; + try (Statement stmt = connection().createStatement(); + ResultSet rs = stmt.executeQuery(EXPORT_SQL)) { + while (rs.next()) { + UUID uuid = UUID.fromString(rs.getString("player_uuid")); + result.put(uuid, new PlayerDataSnapshot( + rs.getString("player_name"), + rs.getObject("silent_state", Boolean.class), + rs.getObject("ignore_join", Boolean.class), + rs.getObject("ignore_swap", Boolean.class), + rs.getObject("ignore_leave", Boolean.class) + )); + } + } catch (SQLException e) { + logger.severe("[H2PlayerDataStore] SQL failure during exportAll(): " + e.getMessage()); + } + return result; + } } From bd59d855689feade3c0c4fbf7eb6851495190bd0 Mon Sep 17 00:00:00 2001 From: EarthCow <56940983+EarthCow@users.noreply.github.com> Date: Sat, 14 Mar 2026 17:57:15 -0400 Subject: [PATCH 04/14] Implements SQLPlayerJoinTracker#exportAll --- .../common/storage/SQLPlayerJoinTracker.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLPlayerJoinTracker.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLPlayerJoinTracker.java index fec9be3..d925b2e 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLPlayerJoinTracker.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLPlayerJoinTracker.java @@ -5,6 +5,8 @@ import java.nio.file.Path; import java.sql.*; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.UUID; /** @@ -28,6 +30,7 @@ public class SQLPlayerJoinTracker extends SQLHandler implements PlayerJoinTracke private final String SELECT_SQL; private final String UPSERT_MYSQL; private final String UPSERT_POSTGRES; + private final String EXPORT_SQL; public SQLPlayerJoinTracker(CoreLogger logger, SQLConfig sqlConfig, Path dataFolder) throws SQLException, SQLDriverLoader.DriverLoadException { @@ -55,6 +58,8 @@ public SQLPlayerJoinTracker(CoreLogger logger, SQLConfig sqlConfig, Path dataFol this.UPSERT_POSTGRES = "INSERT INTO " + tableName + " (player_uuid, player_name) VALUES (?, ?) " + "ON CONFLICT (player_uuid) DO UPDATE SET player_name = EXCLUDED.player_name"; + this.EXPORT_SQL = + "SELECT player_uuid, player_name FROM " + tableName; } @@ -89,4 +94,22 @@ public synchronized void markAsJoined(UUID playerUuid, String playerName) { logger.severe("[SQLPlayerJoinTracker] SQL failure marking player '" + playerName + "' (" + playerUuid + ") as joined: " + e.getMessage()); } } + + @Override + public synchronized Map exportAll() { + Map result = new LinkedHashMap<>(); + if (isConnectionInvalid()) return result; + try (Statement stmt = connection().createStatement(); + ResultSet rs = stmt.executeQuery(EXPORT_SQL)) { + while (rs.next()) { + result.put( + UUID.fromString(rs.getString("player_uuid")), + rs.getString("player_name") + ); + } + } catch (SQLException e) { + logger.severe("[SQLPlayerJoinTracker] SQL failure during exportAll(): " + e.getMessage()); + } + return result; + } } From eb7a79d00333146ee1bc4aa075cfba57f9b85ded Mon Sep 17 00:00:00 2001 From: EarthCow <56940983+EarthCow@users.noreply.github.com> Date: Sat, 14 Mar 2026 17:58:08 -0400 Subject: [PATCH 05/14] Implements SQLPlayerDataStore#exportAll --- .../common/storage/SQLPlayerDataStore.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLPlayerDataStore.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLPlayerDataStore.java index 74ce6cc..c0e0be3 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLPlayerDataStore.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLPlayerDataStore.java @@ -7,6 +7,8 @@ import java.nio.file.Path; import java.sql.*; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.UUID; /** @@ -32,6 +34,7 @@ public class SQLPlayerDataStore extends SQLHandler implements PlayerDataStore { private final String RESOLVE_SQL; private final String UPSERT_MYSQL; private final String UPSERT_POSTGRES; + private final String EXPORT_SQL; public SQLPlayerDataStore(CoreLogger logger, SQLConfig sqlConfig, Path dataFolder) throws SQLException, SQLDriverLoader.DriverLoadException { @@ -83,6 +86,9 @@ public SQLPlayerDataStore(CoreLogger logger, SQLConfig sqlConfig, Path dataFolde " ignore_join = EXCLUDED.ignore_join," + " ignore_swap = EXCLUDED.ignore_swap," + " ignore_leave = EXCLUDED.ignore_leave"; + this.EXPORT_SQL = + "SELECT player_uuid, player_name, silent_state, ignore_join, ignore_swap, ignore_leave" + + " FROM " + tableName; } @@ -147,4 +153,26 @@ public synchronized UUID resolveUuid(String playerName) { } return null; } + + @Override + public synchronized Map exportAll() { + Map result = new LinkedHashMap<>(); + if (isConnectionInvalid()) return result; + try (Statement stmt = connection().createStatement(); + ResultSet rs = stmt.executeQuery(EXPORT_SQL)) { + while (rs.next()) { + UUID uuid = UUID.fromString(rs.getString("player_uuid")); + result.put(uuid, new PlayerDataSnapshot( + rs.getString("player_name"), + rs.getObject("silent_state", Boolean.class), + rs.getObject("ignore_join", Boolean.class), + rs.getObject("ignore_swap", Boolean.class), + rs.getObject("ignore_leave", Boolean.class) + )); + } + } catch (SQLException e) { + logger.severe("[SQLPlayerDataStore] SQL failure during exportAll(): " + e.getMessage()); + } + return result; + } } \ No newline at end of file From fa4a261b2693ec96846f9d20d31dd10b9b91e447 Mon Sep 17 00:00:00 2001 From: EarthCow <56940983+EarthCow@users.noreply.github.com> Date: Sat, 14 Mar 2026 18:04:25 -0400 Subject: [PATCH 06/14] Implements TextPlayerJoinTracker#exportAll --- .../common/storage/TextPlayerJoinTracker.java | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/TextPlayerJoinTracker.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/TextPlayerJoinTracker.java index b0e564c..987083f 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/TextPlayerJoinTracker.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/TextPlayerJoinTracker.java @@ -7,9 +7,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; -import java.util.LinkedHashSet; -import java.util.Set; -import java.util.UUID; +import java.util.*; /** * Tracks which players have ever joined the network using a plain text file. @@ -32,8 +30,11 @@ public class TextPlayerJoinTracker implements PlayerJoinTracker { private final CoreLogger logger; private final Path filePath; - /** In-memory set of every UUID that has ever joined. */ - private final Set joinedUuids = new LinkedHashSet<>(); + /** + * In-memory map of every UUID that has ever joined, keyed by UUID and + * valued by the player name last seen on that line (or {@code ""} + */ + private final Map joinedPlayers = new LinkedHashMap<>(); public TextPlayerJoinTracker(CoreLogger logger, Path filePath) throws IOException { this.logger = logger; @@ -43,12 +44,12 @@ public TextPlayerJoinTracker(CoreLogger logger, Path filePath) throws IOExceptio @Override public synchronized boolean hasJoined(UUID playerUuid) { - return joinedUuids.contains(playerUuid); + return joinedPlayers.containsKey(playerUuid); } @Override public synchronized void markAsJoined(UUID playerUuid, String playerName) { - if (joinedUuids.add(playerUuid)) { + if (joinedPlayers.putIfAbsent(playerUuid, playerName) == null) { appendLine(playerUuid, playerName); } } @@ -77,20 +78,31 @@ private void load() throws IOException { String line = rawLine.trim(); if (line.isEmpty() || line.startsWith("#")) continue; - // If line contains ":" then there must be a player name - // otherwise, the line is simply the UUID + String uuidPart; + String namePart; + if (line.contains(":")) { - line = line.split(":")[0].trim(); + // Splits on first colon + int colon = line.indexOf(':'); + uuidPart = line.substring(0, colon).trim(); + namePart = line.substring(colon + 1).trim(); + } else { + uuidPart = line; + namePart = ""; } try { - joinedUuids.add(UUID.fromString(line)); + UUID uuid = UUID.fromString(uuidPart); + // putIfAbsent preserves the first occurrence when the same UUID + // appears more than once in the file (shouldn't happen, but safe) + joinedPlayers.putIfAbsent(uuid, namePart); } catch (IllegalArgumentException ignored) { - logger.info("[TextPlayerJoinTracker] Skipping unrecognised line in joined.txt: " + rawLine.trim()); + logger.info("[TextPlayerJoinTracker] Skipping unrecognised line in " + + filePath.getFileName() + ": " + rawLine.trim()); } } - logger.debug("[TextPlayerJoinTracker] Loaded " + joinedUuids.size() + " joined-player UUIDs from " + filePath.getFileName()); + logger.debug("[TextPlayerJoinTracker] Loaded " + joinedPlayers.size() + " joined-player UUIDs from " + filePath.getFileName()); } /** @@ -105,4 +117,9 @@ private void appendLine(UUID uuid, String playerName) { logger.severe("[TextPlayerJoinTracker] Failed to persist UUID " + uuid + " (" + playerName + "): " + e.getMessage()); } } + + @Override + public synchronized Map exportAll() { + return Collections.unmodifiableMap(new LinkedHashMap<>(joinedPlayers)); + } } From 5125310801dd72f5ee983781efbeea40d9feb659 Mon Sep 17 00:00:00 2001 From: EarthCow <56940983+EarthCow@users.noreply.github.com> Date: Sat, 14 Mar 2026 18:10:09 -0400 Subject: [PATCH 07/14] Creates StorageMigrator util --- .../common/storage/StorageMigrator.java | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 src/main/java/xyz/earthcow/networkjoinmessages/common/storage/StorageMigrator.java diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/StorageMigrator.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/StorageMigrator.java new file mode 100644 index 0000000..c997188 --- /dev/null +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/StorageMigrator.java @@ -0,0 +1,93 @@ +package xyz.earthcow.networkjoinmessages.common.storage; + +import xyz.earthcow.networkjoinmessages.common.abstraction.CoreLogger; +import xyz.earthcow.networkjoinmessages.common.util.PlayerDataSnapshot; + +import java.util.Map; +import java.util.UUID; + +/** + * Stateless utility that migrates records between storage back-ends. + * + *

Both migration methods follow the same pattern: + *

    + *
  1. Bulk-export all records from {@code source} via its {@code exportAll()} method.
  2. + *
  3. Upsert each record into {@code target} one at a time.
  4. + *
  5. Return the number of records successfully written.
  6. + *
+ * + *

Target implementations use upsert semantics, so calling these methods against a + * partially-populated target is safe — duplicate records are overwritten, not duplicated. + * + *

Neither method closes the source or target; that responsibility belongs to the caller. + */ +public final class StorageMigrator { + + private StorageMigrator() {} + + /** + * Copies every first-join record from {@code source} into {@code target}. + * + * @param source the backend to read from + * @param target the backend to write into + * @param logger used for progress and error messages + * @return the number of records written to {@code target} + */ + public static int migrateJoinTracker( + PlayerJoinTracker source, + PlayerJoinTracker target, + CoreLogger logger) { + + logger.info("[StorageMigrator] Exporting first-join records from " + source.getClass().getSimpleName() + "..."); + Map entries = source.exportAll(); + + if (entries.isEmpty()) { + logger.info("[StorageMigrator] Source contains no first-join records — nothing to migrate."); + return 0; + } + + logger.info("[StorageMigrator] Migrating " + entries.size() + " first-join record(s) into " + + target.getClass().getSimpleName() + "..."); + int count = 0; + for (Map.Entry entry : entries.entrySet()) { + target.markAsJoined(entry.getKey(), entry.getValue()); + count++; + } + + logger.info("[StorageMigrator] First-join migration complete — " + count + " record(s) written."); + return count; + } + + /** + * Copies every player-data record from {@code source} into {@code target}. + * + * @param source the backend to read from + * @param target the backend to write into + * @param logger used for progress and error messages + * @return the number of records written to {@code target} + */ + public static int migratePlayerDataStore( + PlayerDataStore source, + PlayerDataStore target, + CoreLogger logger) { + + logger.info("[StorageMigrator] Exporting player-data records from " + source.getClass().getSimpleName() + "..."); + Map entries = source.exportAll(); + + if (entries.isEmpty()) { + logger.info("[StorageMigrator] Source contains no player-data records — nothing to migrate."); + return 0; + } + + logger.info("[StorageMigrator] Migrating " + entries.size() + " player-data record(s) into " + + target.getClass().getSimpleName() + "..."); + int count = 0; + for (Map.Entry entry : entries.entrySet()) { + target.saveData(entry.getKey(), entry.getValue()); + count++; + } + + logger.info("[StorageMigrator] Player-data migration complete — " + count + " record(s) written."); + return count; + } +} \ No newline at end of file From 892236e2c2dc2a024a9901e1b9373f96b1c881b2 Mon Sep 17 00:00:00 2001 From: EarthCow <56940983+EarthCow@users.noreply.github.com> Date: Sat, 14 Mar 2026 18:51:41 -0400 Subject: [PATCH 08/14] StorageType enum --- .../common/config/PluginConfig.java | 43 ++++++++----------- .../common/storage/StorageType.java | 9 ++++ 2 files changed, 28 insertions(+), 24 deletions(-) create mode 100644 src/main/java/xyz/earthcow/networkjoinmessages/common/storage/StorageType.java diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/config/PluginConfig.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/config/PluginConfig.java index 3ef0aca..5567271 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/config/PluginConfig.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/config/PluginConfig.java @@ -7,6 +7,7 @@ import xyz.earthcow.networkjoinmessages.common.ConfigManager; import xyz.earthcow.networkjoinmessages.common.abstraction.CorePlugin; import xyz.earthcow.networkjoinmessages.common.storage.SQLConfig; +import xyz.earthcow.networkjoinmessages.common.storage.StorageType; import java.util.*; import java.util.concurrent.ThreadLocalRandom; @@ -57,8 +58,8 @@ public final class PluginConfig { @Getter private String consoleSilentLeave; // Storage backends - @Getter private String firstJoinStorageType; - @Getter private String playerDataStorageType; + @Getter private StorageType firstJoinStorageType; + @Getter private StorageType playerDataStorageType; // SQL backend config (only used when StorageType is SQL) @Getter private String sqlHost; @@ -177,8 +178,8 @@ public void reload() { leaveCacheDuration = config.getInt("Settings.LeaveNetworkMessageCacheDuration"); leaveJoinBufferDuration = config.getInt("Settings.LeaveJoinBufferDuration"); silentJoinDefaultState = config.getBoolean("Settings.SilentJoinDefaultState"); - firstJoinStorageType = config.getString("Settings.FirstJoinStorageType").toUpperCase(); - playerDataStorageType = config.getString("Settings.PlayerDataStorageType").toUpperCase(); + firstJoinStorageType = config.getEnum("Settings.FirstJoinStorageType", StorageType.class); + playerDataStorageType = config.getEnum("Settings.PlayerDataStorageType", StorageType.class); sqlHost = config.getString("Settings.SQL.Host"); sqlPort = config.getInt("Settings.SQL.Port"); @@ -237,27 +238,21 @@ public void reload() { /** Validates fields with a constrained set of valid values and resets invalid ones. */ private void validateConstrainedFields() { - switch (firstJoinStorageType) { - case "H2", "TEXT", "SQL" -> { /* valid */ } - default -> { - plugin.getCoreLogger().info( - "Setting error: Settings.FirstJoinStorageType only allows H2, TEXT, or SQL. " + + if (firstJoinStorageType == null) { + plugin.getCoreLogger().info( + "Setting error: Settings.FirstJoinStorageType only allows H2, TEXT, or SQL. " + "Got '" + firstJoinStorageType + "'. Defaulting to H2." - ); - firstJoinStorageType = "H2"; - } + ); + firstJoinStorageType = StorageType.H2; } - switch (playerDataStorageType) { - case "H2", "SQL" -> { /* valid */ } - default -> { - plugin.getCoreLogger().info( - "Setting error: Settings.PlayerDataStorageType only allows H2 or SQL. " + - "Got '" + playerDataStorageType + "'. Defaulting to H2." - ); - playerDataStorageType = "H2"; - } + if (playerDataStorageType == null || playerDataStorageType == StorageType.TEXT) { + plugin.getCoreLogger().info( + "Setting error: Settings.PlayerDataStorageType only allows H2 or SQL. " + + "Got '" + playerDataStorageType + "'. Defaulting to H2." + ); + playerDataStorageType = StorageType.H2; } - if ("SQL".equals(firstJoinStorageType)) { + if (firstJoinStorageType == StorageType.SQL || playerDataStorageType == StorageType.SQL) { switch (sqlDriver) { case "mysql", "mariadb", "postgresql" -> { /* valid */ } default -> { @@ -339,8 +334,8 @@ public Collection getCustomCharts() { YamlDocument defaults = Objects.requireNonNull(configManager.getPluginConfig().getDefaults()); charts.add(new SimplePie("leave_cache_duration", () -> String.valueOf(leaveCacheDuration))); - charts.add(new SimplePie("first_join_storage_type", () -> firstJoinStorageType)); - charts.add(new SimplePie("player_data_storage_type", () -> firstJoinStorageType)); + charts.add(new SimplePie("first_join_storage_type", () -> String.valueOf(firstJoinStorageType))); + charts.add(new SimplePie("player_data_storage_type", () -> String.valueOf(firstJoinStorageType))); charts.add(new SimplePie("swap_enabled", () -> String.valueOf(swapServerMessageEnabled))); charts.add(new SimplePie("first_join_enabled", () -> String.valueOf(firstJoinNetworkMessageEnabled))); charts.add(new SimplePie("join_enabled", () -> String.valueOf(joinNetworkMessageEnabled))); diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/StorageType.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/StorageType.java new file mode 100644 index 0000000..a3857cd --- /dev/null +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/StorageType.java @@ -0,0 +1,9 @@ +package xyz.earthcow.networkjoinmessages.common.storage; + +public enum StorageType { + + H2, + SQL, + TEXT; + +} \ No newline at end of file From f907970397f85ee37fa6347fad39a0ad069024ea Mon Sep 17 00:00:00 2001 From: EarthCow <56940983+EarthCow@users.noreply.github.com> Date: Sat, 14 Mar 2026 19:03:20 -0400 Subject: [PATCH 09/14] Adds StorageInitializer and ActiveStorageBackends --- .../common/storage/ActiveStorageBackends.java | 33 ++ .../common/storage/StorageInitializer.java | 349 ++++++++++++++++++ 2 files changed, 382 insertions(+) create mode 100644 src/main/java/xyz/earthcow/networkjoinmessages/common/storage/ActiveStorageBackends.java create mode 100644 src/main/java/xyz/earthcow/networkjoinmessages/common/storage/StorageInitializer.java diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/ActiveStorageBackends.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/ActiveStorageBackends.java new file mode 100644 index 0000000..62582ed --- /dev/null +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/ActiveStorageBackends.java @@ -0,0 +1,33 @@ +package xyz.earthcow.networkjoinmessages.common.storage; + +/** + * Holds the live {@link PlayerJoinTracker} and {@link PlayerDataStore} instances + * that the plugin should use after a successful call to {@link StorageInitializer#initialize}. + * + *

Both fields are guaranteed non-null when returned by the initializer. The plugin + * is responsible for calling {@link #close()} during shutdown so that any underlying + * connections or file handles are released cleanly. + */ +public record ActiveStorageBackends( + PlayerJoinTracker joinTracker, + PlayerDataStore playerDataStore +) implements AutoCloseable { + + /** + * Closes both backends. Exceptions from each are caught independently so that + * a failure in one does not prevent the other from being closed. + */ + @Override + public void close() { + try { + joinTracker.close(); + } catch (Exception e) { + // Logged by the caller; swallowed here so playerDataStore still closes. + } + try { + playerDataStore.close(); + } catch (Exception e) { + // Logged by the caller. + } + } +} \ No newline at end of file diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/StorageInitializer.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/StorageInitializer.java new file mode 100644 index 0000000..5c7044d --- /dev/null +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/StorageInitializer.java @@ -0,0 +1,349 @@ +package xyz.earthcow.networkjoinmessages.common.storage; + +import xyz.earthcow.networkjoinmessages.common.abstraction.CoreLogger; +import xyz.earthcow.networkjoinmessages.common.util.SQLDriverLoader; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.sql.SQLException; + +/** + * Entry point for all storage initialisation at plugin startup. + * + *

Migration detection

+ * Rather than maintaining a separate state file, migration is triggered by the + * presence of leftover data files from a previous backend. + * + *

SQL source limitation

+ * If the configured type is H2 or TEXT but the previous type was SQL, there are + * no local files to detect — the data lives on a remote server. Automatic + * migration in that direction is not supported. Administrators who need to move + * data from SQL to a local backend should use the + * {@code /njimport} command after startup. + * + *

Multiple remnant files

+ * If both {@code joined.mv.db} and {@code joined.txt} exist simultaneously + * (which should not happen under normal operation), the H2 file takes priority + * since it was the default backend and is more likely to be authoritative. + * + *

File archiving

+ * After a successful migration the source files are moved into a + * {@code migrate/} subdirectory. This provides a recoverable backup and + * prevents the same files from triggering a re-migration on the next restart. + * + *

Error handling

+ * Failure to open the source backend skips migration with a warn; + * the new backend starts empty. Failure to open the target backend is + * fatal and propagates as {@link StorageInitializationException}. + */ +public final class StorageInitializer { + + // H2 appends these suffixes to the base path passed to its constructor + private static final String H2_MAIN_SUFFIX = ".mv.db"; + private static final String H2_TRACE_SUFFIX = ".trace.db"; + private static final String TEXT_JOINED_FILE = "joined.txt"; + + // Base names passed to H2 constructors (relative to dataFolder) + private static final String H2_FIRSTJOIN_BASE = "joined"; + private static final String H2_PLAYERDATA_BASE = "player_data"; + + private StorageInitializer() {} + + /** + * Initializes both storage backends, automatically migrating data if + * leftover files from a previous backend are detected. + * + * @param firstJoinType the first-join tracker backend from config + * @param playerDataType the player-data store backend from config (TEXT is rejected) + * @param sqlConfig SQL connection parameters + * @param dataFolder the plugin's data directory + * @param logger plugin logger + * @return the fully initialised, ready-to-use backends + * @throws StorageInitializationException if a target backend cannot be created, + * or if {@code playerDataType} is {@link StorageType#TEXT} + */ + public static ActiveStorageBackends initialize( + StorageType firstJoinType, + StorageType playerDataType, + SQLConfig sqlConfig, + Path dataFolder, + CoreLogger logger) throws StorageInitializationException { + + if (playerDataType == StorageType.TEXT) { + throw new StorageInitializationException( + "PlayerDataStorageType cannot be TEXT — player-data storage does not support " + + "plain-text files. Valid options are H2 and SQL.", null); + } + + PlayerJoinTracker joinTracker = initJoinTracker(firstJoinType, sqlConfig, dataFolder, logger); + PlayerDataStore playerDataStore = initPlayerDataStore(playerDataType, sqlConfig, dataFolder, logger); + + return new ActiveStorageBackends(joinTracker, playerDataStore); + } + + // --- Per-interface initialisation --- + + private static PlayerJoinTracker initJoinTracker( + StorageType configuredType, + SQLConfig sqlConfig, + Path dataFolder, + CoreLogger logger) throws StorageInitializationException { + + PlayerJoinTracker target = buildJoinTracker(configuredType, sqlConfig, dataFolder, logger); + + // Detect what, if any, legacy backend files are sitting alongside us. + boolean h2Remnant = Files.exists(dataFolder.resolve(H2_FIRSTJOIN_BASE + H2_MAIN_SUFFIX)); + boolean textRemnant = Files.exists(dataFolder.resolve(TEXT_JOINED_FILE)); + + if (configuredType == StorageType.H2 && !h2Remnant && !textRemnant) { + // Normal H2 startup — no migration needed regardless. + return target; + } + + // H2 remnant takes priority if both somehow coexist. + if (h2Remnant && configuredType != StorageType.H2) { + logger.info("[StorageInitializer] Detected joined.mv.db alongside " + + configuredType + " — migrating H2 → " + configuredType + "."); + runJoinTrackerMigration(StorageType.H2, sqlConfig, dataFolder, logger, target); + } else if (textRemnant && configuredType != StorageType.TEXT) { + logger.info("[StorageInitializer] Detected joined.txt alongside " + + configuredType + " — migrating TEXT → " + configuredType + "."); + runJoinTrackerMigration(StorageType.TEXT, sqlConfig, dataFolder, logger, target); + } + + return target; + } + + private static PlayerDataStore initPlayerDataStore( + StorageType configuredType, + SQLConfig sqlConfig, + Path dataFolder, + CoreLogger logger) throws StorageInitializationException { + + PlayerDataStore target = buildPlayerDataStore(configuredType, sqlConfig, dataFolder, logger); + + boolean h2Remnant = Files.exists(dataFolder.resolve(H2_PLAYERDATA_BASE + H2_MAIN_SUFFIX)); + + if (h2Remnant && configuredType != StorageType.H2) { + logger.info("[StorageInitializer] Detected player_data.mv.db alongside " + + configuredType + " — migrating H2 → " + configuredType + "."); + runPlayerDataMigration(sqlConfig, dataFolder, logger, target); + } + + return target; + } + + // --- Migration runners --- + + private static void runJoinTrackerMigration( + StorageType sourceType, + SQLConfig sqlConfig, + Path dataFolder, + CoreLogger logger, + PlayerJoinTracker target) { + + PlayerJoinTracker source = null; + try { + source = buildJoinTracker(sourceType, sqlConfig, dataFolder, logger); + } catch (StorageInitializationException e) { + logger.warn("[StorageInitializer] Could not open " + sourceType + + " first-join source for migration: " + e.getMessage()); + logger.warn("[StorageInitializer] First-join migration skipped."); + return; + } + + try { + int count = StorageMigrator.migrateJoinTracker(source, target, logger); + logger.info("[StorageInitializer] First-join migration complete — " + + count + " record(s) transferred."); + archiveJoinTrackerFiles(sourceType, dataFolder, logger); + } finally { + closeSilently(source, logger, "source first-join tracker"); + } + } + + private static void runPlayerDataMigration( + SQLConfig sqlConfig, + Path dataFolder, + CoreLogger logger, + PlayerDataStore target) { + + // The only detectable source for player data is H2 (TEXT is invalid, + // SQL leaves no local files). + PlayerDataStore source = null; + try { + source = buildPlayerDataStore(StorageType.H2, sqlConfig, dataFolder, logger); + } catch (StorageInitializationException e) { + logger.warn("[StorageInitializer] Could not open H2 player-data source for migration: " + + e.getMessage()); + logger.warn("[StorageInitializer] Player-data migration skipped."); + return; + } + + try { + int count = StorageMigrator.migratePlayerDataStore(source, target, logger); + logger.info("[StorageInitializer] Player-data migration complete — " + + count + " record(s) transferred."); + archivePlayerDataFiles(dataFolder, logger); + } finally { + closeSilently(source, logger, "source player-data store"); + } + } + + // ------------------------------------------------------------------------- + // File archiving + // ------------------------------------------------------------------------- + + private static void archiveJoinTrackerFiles( + StorageType sourceType, Path dataFolder, CoreLogger logger) { + switch (sourceType) { + case H2 -> { + archiveFile(dataFolder.resolve(H2_FIRSTJOIN_BASE + H2_MAIN_SUFFIX), dataFolder, logger); + archiveFile(dataFolder.resolve(H2_FIRSTJOIN_BASE + H2_TRACE_SUFFIX), dataFolder, logger); + } + case TEXT -> + archiveFile(dataFolder.resolve(TEXT_JOINED_FILE), dataFolder, logger); + default -> + logger.debug("[StorageInitializer] No local files to archive for source type " + sourceType + "."); + } + } + + private static void archivePlayerDataFiles(Path dataFolder, CoreLogger logger) { + archiveFile(dataFolder.resolve(H2_PLAYERDATA_BASE + H2_MAIN_SUFFIX), dataFolder, logger); + archiveFile(dataFolder.resolve(H2_PLAYERDATA_BASE + H2_TRACE_SUFFIX), dataFolder, logger); + } + + /** + * Moves {@code file} into {@code /migrate/}, creating that + * directory if needed. Silently skips files that do not exist (H2 only + * creates the {@code .trace.db} file when debug logging is active). + * Appends a numeric suffix if an archive of the same name already exists. + */ + private static void archiveFile(Path file, Path dataFolder, CoreLogger logger) { + if (!Files.exists(file)) return; + try { + Path archiveDir = dataFolder.resolve("migrate"); + Files.createDirectories(archiveDir); + Path destination = resolveNonConflicting(archiveDir.resolve(file.getFileName())); + Files.move(file, destination, StandardCopyOption.ATOMIC_MOVE); + logger.info("[StorageInitializer] Archived " + file.getFileName() + + " → migrate/" + destination.getFileName()); + } catch (IOException e) { + logger.warn("[StorageInitializer] Could not archive " + file.getFileName() + + ": " + e.getMessage() + " — file remains in place."); + } + } + + /** + * Returns {@code path} unchanged if it does not exist, otherwise appends + * {@code .1}, {@code .2}, … until a free name is found. + */ + private static Path resolveNonConflicting(Path path) { + if (!Files.exists(path)) return path; + String name = path.getFileName().toString(); + Path parent = path.getParent(); + int n = 1; + Path candidate; + do { + candidate = parent.resolve(name + "." + n++); + } while (Files.exists(candidate)); + return candidate; + } + + // ------------------------------------------------------------------------- + // Backend factories + // ------------------------------------------------------------------------- + + private static PlayerJoinTracker buildJoinTracker( + StorageType type, + SQLConfig sqlConfig, + Path dataFolder, + CoreLogger logger) throws StorageInitializationException { + return switch (type) { + case H2 -> { + try { + yield new H2PlayerJoinTracker( + logger, + dataFolder.resolve(H2_FIRSTJOIN_BASE).toAbsolutePath().toString() + ); + } catch (SQLException e) { + throw new StorageInitializationException("Failed to open H2PlayerJoinTracker", e); + } + } + case SQL -> { + try { + yield new SQLPlayerJoinTracker(logger, sqlConfig, dataFolder); + } catch (SQLException | SQLDriverLoader.DriverLoadException e) { + throw new StorageInitializationException("Failed to open SQLPlayerJoinTracker", e); + } + } + case TEXT -> { + try { + yield new TextPlayerJoinTracker( + logger, + dataFolder.resolve(TEXT_JOINED_FILE) + ); + } catch (IOException e) { + throw new StorageInitializationException("Failed to open TextPlayerJoinTracker", e); + } + } + }; + } + + private static PlayerDataStore buildPlayerDataStore( + StorageType type, + SQLConfig sqlConfig, + Path dataFolder, + CoreLogger logger) throws StorageInitializationException { + return switch (type) { + case H2 -> { + try { + yield new H2PlayerDataStore( + logger, + dataFolder.resolve(H2_PLAYERDATA_BASE).toAbsolutePath().toString() + ); + } catch (SQLException e) { + throw new StorageInitializationException("Failed to open H2PlayerDataStore", e); + } + } + case SQL -> { + try { + yield new SQLPlayerDataStore(logger, sqlConfig, dataFolder); + } catch (SQLException | SQLDriverLoader.DriverLoadException e) { + throw new StorageInitializationException("Failed to open SQLPlayerDataStore", e); + } + } + case TEXT -> throw new StorageInitializationException( + "TEXT is not a valid PlayerDataStorageType.", null); + }; + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + private static void closeSilently(AutoCloseable c, CoreLogger logger, String label) { + if (c == null) return; + try { + c.close(); + } catch (Exception e) { + logger.warn("[StorageInitializer] Error closing " + label + ": " + e.getMessage()); + } + } + + // ------------------------------------------------------------------------- + // Checked exception + // ------------------------------------------------------------------------- + + /** + * Thrown when a storage backend cannot be created during plugin startup. + * Wraps the underlying cause so callers need only catch one type. + */ + public static final class StorageInitializationException extends Exception { + public StorageInitializationException(String message, Throwable cause) { + super(message, cause); + } + } +} \ No newline at end of file From 480e0bfb196132758ce4d484a810ef5ecaec2c9b Mon Sep 17 00:00:00 2001 From: EarthCow <56940983+EarthCow@users.noreply.github.com> Date: Sat, 14 Mar 2026 19:03:43 -0400 Subject: [PATCH 10/14] Implements StorageInitializer in Core --- .../networkjoinmessages/common/Core.java | 90 ++++++------------- 1 file changed, 27 insertions(+), 63 deletions(-) diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/Core.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/Core.java index dc102b6..4c49d16 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/Core.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/Core.java @@ -50,30 +50,27 @@ public Core(CorePlugin plugin, PremiumVanish premiumVanish) { plugin.getCoreLogger().info("Successfully hooked into SayanVanish!"); } - PlayerDataStore playerDataStore = null; - String playerDataStorageType = config.getPlayerDataStorageType(); - try { - if ("SQL".equalsIgnoreCase(playerDataStorageType)) { - playerDataStore = new SQLPlayerDataStore( - plugin.getCoreLogger(), - config.buildSqlConfig(), - plugin.getDataFolder().toPath() - ); - plugin.getCoreLogger().info("Using SQL storage for player data."); - } else { - playerDataStore = new H2PlayerDataStore( - plugin.getCoreLogger(), - plugin.getDataFolder().toPath().resolve("player_data").toAbsolutePath().toString() - ); - plugin.getCoreLogger().info("Using H2 storage for player data."); - } - } catch (SQLDriverLoader.DriverLoadException ex) { - plugin.getCoreLogger().severe("Failed to download/load the SQL driver — player data is unavailable. " + - "Check your internet connection or place the driver JAR manually in the plugins/NetworkJoinMessages/drivers/ folder."); - plugin.getCoreLogger().debug("Exception: " + ex); - } catch (Exception ex) { - plugin.getCoreLogger().severe("Failed to load player data handler! Persistent player data will be unavailable."); - plugin.getCoreLogger().debug("Exception: " + ex); + PlayerJoinTracker firstJoinTracker = null; + PlayerDataStore playerDataStore = null; + + StorageType firstJoinType = config.getFirstJoinStorageType(); + StorageType playerDataType = config.getPlayerDataStorageType(); + + try (ActiveStorageBackends backends = StorageInitializer.initialize( + firstJoinType, + playerDataType, + config.buildSqlConfig(), + plugin.getDataFolder().toPath(), + plugin.getCoreLogger())) + { + firstJoinTracker = backends.joinTracker(); + playerDataStore = backends.playerDataStore(); + plugin.getCoreLogger().info("Storage initialised — first-join: " + firstJoinType + + ", player-data: " + playerDataType + "."); + } catch (StorageInitializer.StorageInitializationException e) { + plugin.getCoreLogger().severe("Storage initialisation failed: " + e.getMessage() + + " — first-join tracking and persistent player data will be unavailable."); + plugin.getCoreLogger().debug("Exception: " + e); } // Core data / state @@ -88,46 +85,13 @@ public Core(CorePlugin plugin, PremiumVanish premiumVanish) { MessageHandler messageHandler = new MessageHandler(plugin, config, stateStore, placeholderResolver, receiverResolver); // Player event helpers - SilenceChecker silenceChecker = new SilenceChecker(plugin, config, stateStore, sayanVanishHook, premiumVanish); - LeaveMessageCache leaveMessageCache = new LeaveMessageCache(plugin, config, messageFormatter, placeholderResolver); - LeaveJoinBufferManager leaveJoinBuffer = new LeaveJoinBufferManager(plugin, config); + SilenceChecker silenceChecker = new SilenceChecker(plugin, config, stateStore, sayanVanishHook, premiumVanish); + LeaveMessageCache leaveMessageCache = new LeaveMessageCache(plugin, config, messageFormatter, placeholderResolver); + LeaveJoinBufferManager leaveJoinBuffer = new LeaveJoinBufferManager(plugin, config); // Discord integration - DiscordWebhookBuilder webhookBuilder = new DiscordWebhookBuilder(plugin, configManager.getDiscordConfig()); - DiscordIntegration discordIntegration = new DiscordIntegration(plugin, placeholderResolver, messageFormatter, webhookBuilder, configManager.getDiscordConfig()); - - // First-join tracker — backend selected from config (nullable; callers guard against null) - PlayerJoinTracker firstJoinTracker = null; - String firstJoinStorageType = config.getFirstJoinStorageType(); - try { - if ("TEXT".equalsIgnoreCase(firstJoinStorageType)) { - firstJoinTracker = new TextPlayerJoinTracker( - plugin.getCoreLogger(), - plugin.getDataFolder().toPath().resolve("joined.txt") - ); - plugin.getCoreLogger().info("Using TEXT storage for first-join tracking (joined.txt)."); - } else if ("SQL".equalsIgnoreCase(firstJoinStorageType)) { - firstJoinTracker = new SQLPlayerJoinTracker( - plugin.getCoreLogger(), - config.buildSqlConfig(), - plugin.getDataFolder().toPath() - ); - plugin.getCoreLogger().info("Using SQL storage for first-join tracking."); - } else { - firstJoinTracker = new H2PlayerJoinTracker( - plugin.getCoreLogger(), - plugin.getDataFolder().toPath().resolve("joined").toAbsolutePath().toString() - ); - plugin.getCoreLogger().info("Using H2 storage for first-join tracking."); - } - } catch (SQLDriverLoader.DriverLoadException ex) { - plugin.getCoreLogger().severe("Failed to download/load the SQL driver — first-join tracking is disabled. " + - "Check your internet connection or place the driver JAR manually in the plugins/NetworkJoinMessages/drivers/ folder."); - plugin.getCoreLogger().debug("Exception: " + ex); - } catch (Exception ex) { - plugin.getCoreLogger().severe("Failed to load first-join tracker! First-join messages will be unavailable."); - plugin.getCoreLogger().debug("Exception: " + ex); - } + DiscordWebhookBuilder webhookBuilder = new DiscordWebhookBuilder(plugin, configManager.getDiscordConfig()); + DiscordIntegration discordIntegration = new DiscordIntegration(plugin, placeholderResolver, messageFormatter, webhookBuilder, configManager.getDiscordConfig()); // Spoof SpoofManager spoofManager = new SpoofManager(plugin, config, messageHandler, messageFormatter, placeholderResolver); @@ -150,4 +114,4 @@ public Core(CorePlugin plugin, PremiumVanish premiumVanish) { this.corePremiumVanishListener = premiumVanish == null ? null : new CorePremiumVanishListener(plugin.getCoreLogger(), config, spoofManager); } -} +} \ No newline at end of file From c4760578c716d2d7e5fef99b86a8f249ef73d3f9 Mon Sep 17 00:00:00 2001 From: EarthCow <56940983+EarthCow@users.noreply.github.com> Date: Sat, 14 Mar 2026 19:10:02 -0400 Subject: [PATCH 11/14] initialise -> initialize --- src/main/java/xyz/earthcow/networkjoinmessages/common/Core.java | 2 +- .../networkjoinmessages/common/commands/CoreImportCommand.java | 2 +- .../networkjoinmessages/common/storage/StorageInitializer.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/Core.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/Core.java index 4c49d16..fcb26e8 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/Core.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/Core.java @@ -65,7 +65,7 @@ public Core(CorePlugin plugin, PremiumVanish premiumVanish) { { firstJoinTracker = backends.joinTracker(); playerDataStore = backends.playerDataStore(); - plugin.getCoreLogger().info("Storage initialised — first-join: " + firstJoinType + plugin.getCoreLogger().info("Storage initialized — first-join: " + firstJoinType + ", player-data: " + playerDataType + "."); } catch (StorageInitializer.StorageInitializationException e) { plugin.getCoreLogger().severe("Storage initialisation failed: " + e.getMessage() diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/commands/CoreImportCommand.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/commands/CoreImportCommand.java index 106c7fe..431eb66 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/commands/CoreImportCommand.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/commands/CoreImportCommand.java @@ -20,7 +20,7 @@ public CoreImportCommand(PlayerJoinTracker playerJoinTracker) { @Override public void execute(CoreCommandSender coreCommandSender, String[] args) { if (playerJoinTracker == null) { - coreCommandSender.sendMessage(Component.text("Import is unavailable: the first-join database failed to initialise on startup.", NamedTextColor.RED)); + coreCommandSender.sendMessage(Component.text("Import is unavailable: the first-join database failed to initialize on startup.", NamedTextColor.RED)); return; } if (args.length < 1) { diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/StorageInitializer.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/StorageInitializer.java index 5c7044d..0751882 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/StorageInitializer.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/StorageInitializer.java @@ -60,7 +60,7 @@ private StorageInitializer() {} * @param sqlConfig SQL connection parameters * @param dataFolder the plugin's data directory * @param logger plugin logger - * @return the fully initialised, ready-to-use backends + * @return the fully initialized, ready-to-use backends * @throws StorageInitializationException if a target backend cannot be created, * or if {@code playerDataType} is {@link StorageType#TEXT} */ From 45ba066c726eba2384f40a80c4835a889a7f2b33 Mon Sep 17 00:00:00 2001 From: EarthCow <56940983+EarthCow@users.noreply.github.com> Date: Sat, 14 Mar 2026 19:24:58 -0400 Subject: [PATCH 12/14] Fix comments --- .../common/storage/StorageInitializer.java | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/StorageInitializer.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/StorageInitializer.java index 0751882..cd67080 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/StorageInitializer.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/StorageInitializer.java @@ -192,9 +192,7 @@ private static void runPlayerDataMigration( } } - // ------------------------------------------------------------------------- - // File archiving - // ------------------------------------------------------------------------- + // --- File archiving --- private static void archiveJoinTrackerFiles( StorageType sourceType, Path dataFolder, CoreLogger logger) { @@ -252,9 +250,7 @@ private static Path resolveNonConflicting(Path path) { return candidate; } - // ------------------------------------------------------------------------- - // Backend factories - // ------------------------------------------------------------------------- + // --- Backend factories --- private static PlayerJoinTracker buildJoinTracker( StorageType type, @@ -320,10 +316,6 @@ yield new H2PlayerDataStore( }; } - // ------------------------------------------------------------------------- - // Helpers - // ------------------------------------------------------------------------- - private static void closeSilently(AutoCloseable c, CoreLogger logger, String label) { if (c == null) return; try { @@ -333,10 +325,6 @@ private static void closeSilently(AutoCloseable c, CoreLogger logger, String lab } } - // ------------------------------------------------------------------------- - // Checked exception - // ------------------------------------------------------------------------- - /** * Thrown when a storage backend cannot be created during plugin startup. * Wraps the underlying cause so callers need only catch one type. From e9e55ed8a19c3095daf5e5bff8cfa7b434e47113 Mon Sep 17 00:00:00 2001 From: EarthCow <56940983+EarthCow@users.noreply.github.com> Date: Sat, 14 Mar 2026 19:44:10 -0400 Subject: [PATCH 13/14] Fixes major SQL connection issue Queries were being initialized after the connection was setup and attempting to run the create table query. --- .../earthcow/networkjoinmessages/common/Core.java | 14 +++++++------- .../common/storage/SQLHandler.java | 5 ++--- .../common/storage/SQLPlayerDataStore.java | 1 + .../common/storage/SQLPlayerJoinTracker.java | 1 + 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/Core.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/Core.java index fcb26e8..46ccfa0 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/Core.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/Core.java @@ -56,13 +56,13 @@ public Core(CorePlugin plugin, PremiumVanish premiumVanish) { StorageType firstJoinType = config.getFirstJoinStorageType(); StorageType playerDataType = config.getPlayerDataStorageType(); - try (ActiveStorageBackends backends = StorageInitializer.initialize( - firstJoinType, - playerDataType, - config.buildSqlConfig(), - plugin.getDataFolder().toPath(), - plugin.getCoreLogger())) - { + try { + ActiveStorageBackends backends = StorageInitializer.initialize( + firstJoinType, + playerDataType, + config.buildSqlConfig(), + plugin.getDataFolder().toPath(), + plugin.getCoreLogger()); firstJoinTracker = backends.joinTracker(); playerDataStore = backends.playerDataStore(); plugin.getCoreLogger().info("Storage initialized — first-join: " + firstJoinType diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLHandler.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLHandler.java index e10586d..9c0030c 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLHandler.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLHandler.java @@ -17,13 +17,12 @@ abstract class SQLHandler implements AutoCloseable { private Connection connection; protected SQLHandler(CoreLogger logger, SQLConfig sqlConfig, Path dataFolder) - throws SQLException, SQLDriverLoader.DriverLoadException { + throws SQLDriverLoader.DriverLoadException { this.logger = logger; this.sqlConfig = sqlConfig; this.isPostgres = "postgresql".equals(sqlConfig.driver()); this.logPrefix = "[" + getClass().getSimpleName() + "] "; new SQLDriverLoader(logger, dataFolder).ensureLoaded(sqlConfig.driver()); - setUpConnection(); } protected abstract String createTableSql(); @@ -52,7 +51,7 @@ protected synchronized boolean isConnectionInvalid() { /** * Opens a new connection and ensures the table exists. */ - private void setUpConnection() throws SQLException { + protected void setUpConnection() throws SQLException { String url = buildJdbcUrl(); this.connection = DriverManager.getConnection(url, sqlConfig.username(), sqlConfig.password()); try (Statement stmt = connection.createStatement()) { diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLPlayerDataStore.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLPlayerDataStore.java index c0e0be3..794167d 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLPlayerDataStore.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLPlayerDataStore.java @@ -90,6 +90,7 @@ public SQLPlayerDataStore(CoreLogger logger, SQLConfig sqlConfig, Path dataFolde "SELECT player_uuid, player_name, silent_state, ignore_join, ignore_swap, ignore_leave" + " FROM " + tableName; + setUpConnection(); } @Override diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLPlayerJoinTracker.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLPlayerJoinTracker.java index d925b2e..cc99f17 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLPlayerJoinTracker.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/storage/SQLPlayerJoinTracker.java @@ -61,6 +61,7 @@ public SQLPlayerJoinTracker(CoreLogger logger, SQLConfig sqlConfig, Path dataFol this.EXPORT_SQL = "SELECT player_uuid, player_name FROM " + tableName; + setUpConnection(); } @Override From c585d75224523f910716293f92effec671a1d49c Mon Sep 17 00:00:00 2001 From: EarthCow <56940983+EarthCow@users.noreply.github.com> Date: Sat, 14 Mar 2026 19:45:24 -0400 Subject: [PATCH 14/14] Fixes comment in config --- src/main/resources/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index c8ba421..25f3bb8 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -133,7 +133,7 @@ Settings: # SQL - External SQL server (MySQL, MariaDB, or PostgreSQL). Requires filling in the SQL section below. # H2 - Embedded SQL database (default). Best for most servers; no setup required. # TEXT - Plain text file (joined.txt). Easiest to inspect and edit by hand. (Player data will still use H2) - # Changing this option requires a restart. Data is NOT migrated automatically. + # Changing this option requires a restart. Data is migrated automatically except when migrating from SQL. FirstJoinStorageType: H2 # Storage backend for player data