Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 26 additions & 62 deletions src/main/java/xyz/earthcow/networkjoinmessages/common/Core.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,30 +50,27 @@ public Core(CorePlugin plugin, PremiumVanish premiumVanish) {
plugin.getCoreLogger().info("Successfully hooked into SayanVanish!");
}

PlayerDataStore playerDataStore = null;
String playerDataStorageType = config.getPlayerDataStorageType();
PlayerJoinTracker firstJoinTracker = null;
PlayerDataStore playerDataStore = null;

StorageType firstJoinType = config.getFirstJoinStorageType();
StorageType playerDataType = 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);
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
+ ", 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
Expand All @@ -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);
Expand All @@ -150,4 +114,4 @@ public Core(CorePlugin plugin, PremiumVanish premiumVanish) {
this.corePremiumVanishListener = premiumVanish == null ? null
: new CorePremiumVanishListener(plugin.getCoreLogger(), config, spoofManager);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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 -> {
Expand Down Expand Up @@ -339,8 +334,8 @@ public Collection<CustomChart> 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)));
Expand Down
Original file line number Diff line number Diff line change
@@ -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}.
*
* <p>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.
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -107,4 +112,26 @@ public synchronized UUID resolveUuid(String playerName) {
}
return null;
}

@Override
public synchronized Map<UUID, PlayerDataSnapshot> exportAll() {
Map<UUID, PlayerDataSnapshot> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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);
}
Expand Down Expand Up @@ -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<UUID, String> exportAll() {
Map<UUID, String> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -29,6 +30,8 @@ public interface PlayerDataStore extends AutoCloseable {
@Nullable
UUID resolveUuid(String playerName);

Map<UUID, PlayerDataSnapshot> 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 {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -53,6 +54,8 @@ default boolean addUsersFromUserCache(String userCacheStr) {
}
}

Map<UUID, String> 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 {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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()) {
Expand Down
Loading