From efd31b7eb921fad5173f29e6a2a5d0c53b610d6b Mon Sep 17 00:00:00 2001 From: XiaoMoMi <972454774@qq.com> Date: Thu, 14 Sep 2023 02:54:32 +0800 Subject: [PATCH] added export legacy command --- .../api/data/DataStorageInterface.java | 7 +- .../api/data/LegacyDataStorageInterface.java | 10 ++ .../customfishing/api/data/StatisticData.java | 2 +- .../customfishing/api/data/StorageType.java | 1 - .../api/manager/StorageManager.java | 5 +- .../command/CommandManagerImpl.java | 1 + .../command/sub/DataCommand.java | 112 ++++++++++++++++++ .../command/sub/FishingBagCommand.java | 3 +- .../customfishing/setting/CFConfig.java | 3 + .../storage/StorageManagerImpl.java | 9 +- .../storage/method/AbstractStorage.java | 5 + .../method/database/nosql/MongoDBImpl.java | 46 +++++-- .../method/database/nosql/RedisManager.java | 9 +- .../database/sql/AbstractHikariDatabase.java | 64 +++++++++- .../database/sql/AbstractSQLDatabase.java | 39 ++++-- .../method/database/sql/SQLiteImpl.java | 16 ++- .../storage/method/file/JsonImpl.java | 21 +++- .../storage/method/file/YAMLImpl.java | 68 ++++++++++- .../util/CompletableFutures.java | 31 +++++ plugin/src/main/resources/config.yml | 4 + 20 files changed, 403 insertions(+), 53 deletions(-) create mode 100644 api/src/main/java/net/momirealms/customfishing/api/data/LegacyDataStorageInterface.java create mode 100644 plugin/src/main/java/net/momirealms/customfishing/command/sub/DataCommand.java create mode 100644 plugin/src/main/java/net/momirealms/customfishing/util/CompletableFutures.java diff --git a/api/src/main/java/net/momirealms/customfishing/api/data/DataStorageInterface.java b/api/src/main/java/net/momirealms/customfishing/api/data/DataStorageInterface.java index 65be3246..793ec026 100644 --- a/api/src/main/java/net/momirealms/customfishing/api/data/DataStorageInterface.java +++ b/api/src/main/java/net/momirealms/customfishing/api/data/DataStorageInterface.java @@ -21,6 +21,7 @@ import net.momirealms.customfishing.api.data.user.OfflineUser; import java.util.Collection; import java.util.Optional; +import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -31,9 +32,13 @@ public interface DataStorageInterface { StorageType getStorageType(); - CompletableFuture> getPlayerData(UUID uuid, boolean force); + CompletableFuture> getPlayerData(UUID uuid, boolean lock); CompletableFuture savePlayerData(UUID uuid, PlayerData playerData, boolean unlock); void savePlayersData(Collection users, boolean unlock); + + void lockPlayerData(UUID uuid, boolean lock); + + Set getUniqueUsers(boolean legacy); } diff --git a/api/src/main/java/net/momirealms/customfishing/api/data/LegacyDataStorageInterface.java b/api/src/main/java/net/momirealms/customfishing/api/data/LegacyDataStorageInterface.java new file mode 100644 index 00000000..99e4b982 --- /dev/null +++ b/api/src/main/java/net/momirealms/customfishing/api/data/LegacyDataStorageInterface.java @@ -0,0 +1,10 @@ +package net.momirealms.customfishing.api.data; + +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public interface LegacyDataStorageInterface extends DataStorageInterface { + + CompletableFuture> getLegacyPlayerData(UUID uuid); +} diff --git a/api/src/main/java/net/momirealms/customfishing/api/data/StatisticData.java b/api/src/main/java/net/momirealms/customfishing/api/data/StatisticData.java index 86b9ab43..94f18122 100644 --- a/api/src/main/java/net/momirealms/customfishing/api/data/StatisticData.java +++ b/api/src/main/java/net/momirealms/customfishing/api/data/StatisticData.java @@ -25,7 +25,7 @@ import java.util.Map; public class StatisticData { - @SerializedName("stats") + @SerializedName("map") public Map statisticMap; public StatisticData(@NotNull Map data) { diff --git a/api/src/main/java/net/momirealms/customfishing/api/data/StorageType.java b/api/src/main/java/net/momirealms/customfishing/api/data/StorageType.java index cf54a9c5..da6a3fe4 100644 --- a/api/src/main/java/net/momirealms/customfishing/api/data/StorageType.java +++ b/api/src/main/java/net/momirealms/customfishing/api/data/StorageType.java @@ -27,5 +27,4 @@ public enum StorageType { MariaDB, MongoDB, Redis - } diff --git a/api/src/main/java/net/momirealms/customfishing/api/manager/StorageManager.java b/api/src/main/java/net/momirealms/customfishing/api/manager/StorageManager.java index 985ccdde..6332277c 100644 --- a/api/src/main/java/net/momirealms/customfishing/api/manager/StorageManager.java +++ b/api/src/main/java/net/momirealms/customfishing/api/manager/StorageManager.java @@ -44,14 +44,13 @@ public interface StorageManager { /** * Get an offline user's data - * force reading would ignore the database lock * Otherwise it would return Optional.empty() if data is locked * It an offline user never played the server, its name would equal "" (empty string) * @param uuid uuid - * @param force force + * @param lock lock * @return offline user data */ - CompletableFuture> getOfflineUser(UUID uuid, boolean force); + CompletableFuture> getOfflineUser(UUID uuid, boolean lock); CompletableFuture saveUserData(OfflineUser offlineUser, boolean unlock); diff --git a/plugin/src/main/java/net/momirealms/customfishing/command/CommandManagerImpl.java b/plugin/src/main/java/net/momirealms/customfishing/command/CommandManagerImpl.java index 273f7e8d..0806d90d 100644 --- a/plugin/src/main/java/net/momirealms/customfishing/command/CommandManagerImpl.java +++ b/plugin/src/main/java/net/momirealms/customfishing/command/CommandManagerImpl.java @@ -52,6 +52,7 @@ public class CommandManagerImpl implements CommandManager { getReloadCommand(), getMarketCommand(), getAboutCommand(), + DataCommand.INSTANCE.getDataCommand(), CompetitionCommand.INSTANCE.getCompetitionCommand(), ItemCommand.INSTANCE.getItemCommand(), DebugCommand.INSTANCE.getDebugCommand(), diff --git a/plugin/src/main/java/net/momirealms/customfishing/command/sub/DataCommand.java b/plugin/src/main/java/net/momirealms/customfishing/command/sub/DataCommand.java new file mode 100644 index 00000000..e174d912 --- /dev/null +++ b/plugin/src/main/java/net/momirealms/customfishing/command/sub/DataCommand.java @@ -0,0 +1,112 @@ +package net.momirealms.customfishing.command.sub; + +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import dev.jorel.commandapi.CommandAPICommand; +import dev.jorel.commandapi.arguments.ArgumentSuggestions; +import dev.jorel.commandapi.arguments.StringArgument; +import net.momirealms.customfishing.adventure.AdventureManagerImpl; +import net.momirealms.customfishing.api.CustomFishingPlugin; +import net.momirealms.customfishing.api.data.LegacyDataStorageInterface; +import net.momirealms.customfishing.api.util.LogUtils; +import net.momirealms.customfishing.storage.method.database.sql.MariaDBImpl; +import net.momirealms.customfishing.storage.method.database.sql.MySQLImpl; +import net.momirealms.customfishing.storage.method.file.YAMLImpl; +import net.momirealms.customfishing.util.CompletableFutures; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.zip.GZIPOutputStream; + +public class DataCommand { + + public static DataCommand INSTANCE = new DataCommand(); + + public CommandAPICommand getDataCommand() { + return new CommandAPICommand("data") + .withSubcommands( + getExportLegacyCommand() + ); + } + + public CommandAPICommand getExportLegacyCommand() { + return new CommandAPICommand("export-legacy") + .withArguments(new StringArgument("method") + .replaceSuggestions(ArgumentSuggestions.strings("MySQL", "MariaDB", "YAML"))) + .executes((sender, args) -> { + String arg = (String) args.get("method"); + if (arg == null) return; + CustomFishingPlugin plugin = CustomFishingPlugin.get(); + plugin.getScheduler().runTaskAsync(() -> { + LegacyDataStorageInterface dataStorageInterface; + switch (arg) { + case "MySQL" -> dataStorageInterface = new MySQLImpl(plugin); + case "MariaDB" -> dataStorageInterface = new MariaDBImpl(plugin); + case "YAML" -> dataStorageInterface = new YAMLImpl(plugin); + default -> { + AdventureManagerImpl.getInstance().sendMessageWithPrefix(sender, "No such legacy storage method."); + return; + } + } + + dataStorageInterface.initialize(); + Set uuids = dataStorageInterface.getUniqueUsers(true); + Set> futures = new HashSet<>(); + AtomicInteger userCount = new AtomicInteger(0); + Map out = Collections.synchronizedMap(new TreeMap<>()); + + for (UUID uuid : uuids) { + futures.add(dataStorageInterface.getLegacyPlayerData(uuid).thenAccept(it -> { + if (it.isPresent()) { + out.put(uuid, plugin.getStorageManager().toJson(it.get())); + userCount.incrementAndGet(); + } + })); + } + + CompletableFuture overallFuture = CompletableFutures.allOf(futures); + + while (true) { + try { + overallFuture.get(3, TimeUnit.SECONDS); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + break; + } catch (TimeoutException e) { + LogUtils.info("Progress: " + userCount.get() + "/" + uuids.size()); + continue; + } + break; + } + + JsonObject outJson = new JsonObject(); + for (Map.Entry entry : out.entrySet()) { + outJson.addProperty(entry.getKey().toString(), entry.getValue()); + } + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm"); + String formattedDate = formatter.format(new Date()); + File outFile = new File(plugin.getDataFolder(), "exported-" + formattedDate + ".json.gz"); + try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new GZIPOutputStream(Files.newOutputStream(outFile.toPath())), StandardCharsets.UTF_8))) { + new GsonBuilder().disableHtmlEscaping().create().toJson(outJson, writer); + } catch (IOException e) { + e.printStackTrace(); + } + + plugin.getScheduler().runTaskAsyncLater(dataStorageInterface::disable, 1, TimeUnit.SECONDS); + + AdventureManagerImpl.getInstance().sendMessageWithPrefix(sender, "Finished."); + }); + }); + } +} diff --git a/plugin/src/main/java/net/momirealms/customfishing/command/sub/FishingBagCommand.java b/plugin/src/main/java/net/momirealms/customfishing/command/sub/FishingBagCommand.java index 581534e0..0eaafe01 100644 --- a/plugin/src/main/java/net/momirealms/customfishing/command/sub/FishingBagCommand.java +++ b/plugin/src/main/java/net/momirealms/customfishing/command/sub/FishingBagCommand.java @@ -23,6 +23,7 @@ import dev.jorel.commandapi.arguments.UUIDArgument; import net.momirealms.customfishing.adventure.AdventureManagerImpl; import net.momirealms.customfishing.api.CustomFishingPlugin; import net.momirealms.customfishing.api.data.user.OfflineUser; +import net.momirealms.customfishing.setting.CFConfig; import net.momirealms.customfishing.setting.CFLocale; import net.momirealms.customfishing.storage.user.OfflineUserImpl; import org.bukkit.Bukkit; @@ -79,7 +80,7 @@ public class FishingBagCommand { return; } } - CustomFishingPlugin.get().getStorageManager().getOfflineUser(uuid, false).thenAccept(optional -> { + CustomFishingPlugin.get().getStorageManager().getOfflineUser(uuid, CFConfig.lockData).thenAccept(optional -> { if (optional.isEmpty()) { AdventureManagerImpl.getInstance().sendMessageWithPrefix(player, CFLocale.MSG_Never_Played); return; diff --git a/plugin/src/main/java/net/momirealms/customfishing/setting/CFConfig.java b/plugin/src/main/java/net/momirealms/customfishing/setting/CFConfig.java index 96879474..76c4362d 100644 --- a/plugin/src/main/java/net/momirealms/customfishing/setting/CFConfig.java +++ b/plugin/src/main/java/net/momirealms/customfishing/setting/CFConfig.java @@ -83,6 +83,8 @@ public class CFConfig { // Data save interval public static int dataSaveInterval; + // Lock data on join + public static boolean lockData; // Legacy color code support public static boolean legacyColorSupport; @@ -140,6 +142,7 @@ public class CFConfig { placeholderLimit = config.getInt("mechanics.competition.placeholder-limit", 3); dataSaveInterval = config.getInt("other-settings.data-saving-interval", 600); + lockData = config.getBoolean("other-settings.lock-data", true); legacyColorSupport = config.getBoolean("other-settings.legacy-color-code-support", false); OffsetUtils.loadConfig(config.getConfigurationSection("other-settings.offset-characters")); diff --git a/plugin/src/main/java/net/momirealms/customfishing/storage/StorageManagerImpl.java b/plugin/src/main/java/net/momirealms/customfishing/storage/StorageManagerImpl.java index 55860c73..9b689b2f 100644 --- a/plugin/src/main/java/net/momirealms/customfishing/storage/StorageManagerImpl.java +++ b/plugin/src/main/java/net/momirealms/customfishing/storage/StorageManagerImpl.java @@ -113,7 +113,7 @@ public class StorageManagerImpl implements StorageManager, Listener { this.timerSaveTask = this.plugin.getScheduler().runTaskAsyncTimer( () -> { long time1 = System.currentTimeMillis(); - this.dataSource.savePlayersData(this.onlineUserMap.values(), false); + this.dataSource.savePlayersData(this.onlineUserMap.values(), !CFConfig.lockData); LogUtils.info("Data Saved for online players. Took " + (System.currentTimeMillis() - time1) + "ms."); }, CFConfig.dataSaveInterval, @@ -143,8 +143,8 @@ public class StorageManagerImpl implements StorageManager, Listener { } @Override - public CompletableFuture> getOfflineUser(UUID uuid, boolean force) { - var optionalDataFuture = dataSource.getPlayerData(uuid, force); + public CompletableFuture> getOfflineUser(UUID uuid, boolean lock) { + var optionalDataFuture = dataSource.getPlayerData(uuid, lock); return optionalDataFuture.thenCompose(optionalUser -> { if (optionalUser.isEmpty()) { // locked @@ -252,6 +252,7 @@ public class StorageManagerImpl implements StorageManager, Listener { if (optionalData.isPresent()) { putDataInCache(player, optionalData.get()); task.cancel(); + if (CFConfig.lockData) dataSource.lockPlayerData(uuid, true); } }); } @@ -264,7 +265,7 @@ public class StorageManagerImpl implements StorageManager, Listener { var player = Bukkit.getPlayer(uuid); if (player == null || !player.isOnline() || times > 3) return; - this.dataSource.getPlayerData(uuid, false).thenAccept(optionalData -> { + this.dataSource.getPlayerData(uuid, CFConfig.lockData).thenAccept(optionalData -> { // should not be empty if (optionalData.isEmpty()) return; diff --git a/plugin/src/main/java/net/momirealms/customfishing/storage/method/AbstractStorage.java b/plugin/src/main/java/net/momirealms/customfishing/storage/method/AbstractStorage.java index def38b0e..682ee77b 100644 --- a/plugin/src/main/java/net/momirealms/customfishing/storage/method/AbstractStorage.java +++ b/plugin/src/main/java/net/momirealms/customfishing/storage/method/AbstractStorage.java @@ -23,6 +23,7 @@ import net.momirealms.customfishing.api.data.user.OfflineUser; import java.time.Instant; import java.util.Collection; +import java.util.UUID; public abstract class AbstractStorage implements DataStorageInterface { @@ -52,4 +53,8 @@ public abstract class AbstractStorage implements DataStorageInterface { this.savePlayerData(user.getUUID(), user.getPlayerData(), unlock); } } + + public void lockPlayerData(UUID uuid, boolean lock) { + + } } diff --git a/plugin/src/main/java/net/momirealms/customfishing/storage/method/database/nosql/MongoDBImpl.java b/plugin/src/main/java/net/momirealms/customfishing/storage/method/database/nosql/MongoDBImpl.java index 4d2ba237..a21ad815 100644 --- a/plugin/src/main/java/net/momirealms/customfishing/storage/method/database/nosql/MongoDBImpl.java +++ b/plugin/src/main/java/net/momirealms/customfishing/storage/method/database/nosql/MongoDBImpl.java @@ -18,25 +18,20 @@ package net.momirealms.customfishing.storage.method.database.nosql; import com.mongodb.*; -import com.mongodb.bulk.BulkWriteResult; -import com.mongodb.client.MongoClient; -import com.mongodb.client.MongoClients; -import com.mongodb.client.MongoCollection; -import com.mongodb.client.MongoDatabase; +import com.mongodb.client.*; import com.mongodb.client.model.*; -import com.mongodb.client.result.InsertOneResult; import com.mongodb.client.result.UpdateResult; import net.momirealms.customfishing.api.CustomFishingPlugin; import net.momirealms.customfishing.api.data.PlayerData; import net.momirealms.customfishing.api.data.StorageType; import net.momirealms.customfishing.api.data.user.OfflineUser; import net.momirealms.customfishing.api.util.LogUtils; +import net.momirealms.customfishing.setting.CFConfig; import net.momirealms.customfishing.storage.method.AbstractStorage; import org.bson.Document; import org.bson.UuidRepresentation; import org.bson.conversions.Bson; import org.bson.types.Binary; -import org.bson.types.ObjectId; import org.bukkit.Bukkit; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.YamlConfiguration; @@ -109,23 +104,25 @@ public class MongoDBImpl extends AbstractStorage { } @Override - public CompletableFuture> getPlayerData(UUID uuid, boolean force) { + public CompletableFuture> getPlayerData(UUID uuid, boolean lock) { var future = new CompletableFuture>(); plugin.getScheduler().runTaskAsync(() -> { MongoCollection collection = database.getCollection(getCollectionName("data")); Document doc = collection.find(Filters.eq("uuid", uuid)).first(); if (doc == null) { if (Bukkit.getPlayer(uuid) != null) { + if (lock) lockPlayerData(uuid, true); future.complete(Optional.of(PlayerData.empty())); } else { future.complete(Optional.empty()); } } else { - if (!force && doc.getInteger("lock") != 0) { + if (doc.getInteger("lock") != 0 && getCurrentSeconds() - CFConfig.dataSaveInterval <= doc.getInteger("lock")) { future.complete(Optional.of(PlayerData.LOCKED)); return; } Binary binary = (Binary) doc.get("data"); + if (lock) lockPlayerData(uuid, true); future.complete(Optional.of(plugin.getStorageManager().fromBytes(binary.getData()))); } }); @@ -172,4 +169,35 @@ public class MongoDBImpl extends AbstractStorage { LogUtils.warn("Failed to update data for online players", e); } } + + @Override + public void lockPlayerData(UUID uuid, boolean lock) { + MongoCollection collection = database.getCollection(getCollectionName("data")); + try { + Document query = new Document("uuid", uuid); + Bson updates = Updates.combine(Updates.set("lock", !lock ? 0 : getCurrentSeconds())); + UpdateOptions options = new UpdateOptions().upsert(true); + collection.updateOne(query, updates, options); + } catch (MongoException e) { + LogUtils.warn("Failed to lock data for " + uuid, e); + } + } + + @Override + public Set getUniqueUsers(boolean legacy) { + // no legacy files + Set uuids = new HashSet<>(); + MongoCollection collection = database.getCollection(getCollectionName("data")); + try { + Bson projectionFields = Projections.fields(Projections.include("uuid")); + try (MongoCursor cursor = collection.find().projection(projectionFields).iterator()) { + while (cursor.hasNext()) { + uuids.add(cursor.next().get("uuid", UUID.class)); + } + } + } catch (MongoException e) { + LogUtils.warn("Failed to get unique data.", e); + } + return uuids; + } } diff --git a/plugin/src/main/java/net/momirealms/customfishing/storage/method/database/nosql/RedisManager.java b/plugin/src/main/java/net/momirealms/customfishing/storage/method/database/nosql/RedisManager.java index a2688ff5..d2594adf 100644 --- a/plugin/src/main/java/net/momirealms/customfishing/storage/method/database/nosql/RedisManager.java +++ b/plugin/src/main/java/net/momirealms/customfishing/storage/method/database/nosql/RedisManager.java @@ -31,7 +31,9 @@ import redis.clients.jedis.resps.Tuple; import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.util.HashSet; import java.util.Optional; +import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -226,7 +228,7 @@ public class RedisManager extends AbstractStorage { } @Override - public CompletableFuture> getPlayerData(UUID uuid, boolean ignore) { + public CompletableFuture> getPlayerData(UUID uuid, boolean lock) { var future = new CompletableFuture>(); plugin.getScheduler().runTaskAsync(() -> { try (Jedis jedis = jedisPool.getResource()) { @@ -268,6 +270,11 @@ public class RedisManager extends AbstractStorage { return future; } + @Override + public Set getUniqueUsers(boolean legacy) { + return new HashSet<>(); + } + private byte[] getRedisKey(String key, @NotNull UUID uuid) { return (key + ":" + uuid).getBytes(StandardCharsets.UTF_8); } diff --git a/plugin/src/main/java/net/momirealms/customfishing/storage/method/database/sql/AbstractHikariDatabase.java b/plugin/src/main/java/net/momirealms/customfishing/storage/method/database/sql/AbstractHikariDatabase.java index 2f5b8225..ba2ec575 100644 --- a/plugin/src/main/java/net/momirealms/customfishing/storage/method/database/sql/AbstractHikariDatabase.java +++ b/plugin/src/main/java/net/momirealms/customfishing/storage/method/database/sql/AbstractHikariDatabase.java @@ -19,18 +19,20 @@ package net.momirealms.customfishing.storage.method.database.sql; import com.zaxxer.hikari.HikariDataSource; import net.momirealms.customfishing.api.CustomFishingPlugin; -import net.momirealms.customfishing.api.data.StorageType; +import net.momirealms.customfishing.api.data.*; import net.momirealms.customfishing.api.util.LogUtils; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.YamlConfiguration; import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; import java.sql.SQLException; -import java.util.Locale; -import java.util.Map; -import java.util.Properties; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; -public abstract class AbstractHikariDatabase extends AbstractSQLDatabase { +public abstract class AbstractHikariDatabase extends AbstractSQLDatabase implements LegacyDataStorageInterface { private HikariDataSource dataSource; private final String driverClass; @@ -117,4 +119,56 @@ public abstract class AbstractHikariDatabase extends AbstractSQLDatabase { public Connection getConnection() throws SQLException { return dataSource.getConnection(); } + + @Override + public CompletableFuture> getLegacyPlayerData(UUID uuid) { + var future = new CompletableFuture>(); + plugin.getScheduler().runTaskAsync(() -> { + try ( + Connection connection = getConnection() + ) { + var builder = new PlayerData.Builder().setName(""); + PreparedStatement statementOne = connection.prepareStatement(String.format(SqlConstants.SQL_SELECT_BY_UUID, getTableName("fishingbag"))); + statementOne.setString(1, uuid.toString()); + ResultSet rsOne = statementOne.executeQuery(); + if (rsOne.next()) { + int size = rsOne.getInt("size"); + String contents = rsOne.getString("contents"); + builder.setBagData(new InventoryData(contents, size)); + } else { + builder.setBagData(InventoryData.empty()); + } + + PreparedStatement statementTwo = connection.prepareStatement(String.format(SqlConstants.SQL_SELECT_BY_UUID, getTableName("selldata"))); + statementTwo.setString(1, uuid.toString()); + ResultSet rsTwo = statementTwo.executeQuery(); + if (rsTwo.next()) { + int date = rsTwo.getInt("date"); + double money = rsTwo.getInt("money"); + builder.setEarningData(new EarningData(money, date)); + } else { + builder.setEarningData(EarningData.empty()); + } + + PreparedStatement statementThree = connection.prepareStatement(String.format(SqlConstants.SQL_SELECT_BY_UUID, getTableName("statistics"))); + statementThree.setString(1, uuid.toString()); + ResultSet rsThree = statementThree.executeQuery(); + if (rsThree.next()) { + String stats = rsThree.getString("stats"); + var amountMap = (Map) Arrays.stream(stats.split(";")) + .map(element -> element.split(":")) + .filter(pair -> pair.length == 2) + .collect(Collectors.toMap(pair -> pair[0], pair -> Integer.parseInt(pair[1]))); + builder.setStats(new StatisticData(amountMap)); + } else { + builder.setStats(StatisticData.empty()); + } + future.complete(Optional.of(builder.build())); + } catch (SQLException e) { + LogUtils.warn("Failed to get " + uuid + "'s data.", e); + future.completeExceptionally(e); + } + }); + return future; + } } diff --git a/plugin/src/main/java/net/momirealms/customfishing/storage/method/database/sql/AbstractSQLDatabase.java b/plugin/src/main/java/net/momirealms/customfishing/storage/method/database/sql/AbstractSQLDatabase.java index ada144ea..87329914 100644 --- a/plugin/src/main/java/net/momirealms/customfishing/storage/method/database/sql/AbstractSQLDatabase.java +++ b/plugin/src/main/java/net/momirealms/customfishing/storage/method/database/sql/AbstractSQLDatabase.java @@ -79,7 +79,7 @@ public abstract class AbstractSQLDatabase extends AbstractStorage { @SuppressWarnings("DuplicatedCode") @Override - public CompletableFuture> getPlayerData(UUID uuid, boolean force) { + public CompletableFuture> getPlayerData(UUID uuid, boolean lock) { var future = new CompletableFuture>(); plugin.getScheduler().runTaskAsync(() -> { try ( @@ -89,10 +89,8 @@ public abstract class AbstractSQLDatabase extends AbstractStorage { statement.setString(1, uuid.toString()); ResultSet rs = statement.executeQuery(); if (rs.next()) { - int lock = rs.getInt(2); - if (!force && (lock != 0 && getCurrentSeconds() - CFConfig.dataSaveInterval <= lock)) { - statement.close(); - rs.close(); + int lockValue = rs.getInt(2); + if (lockValue != 0 && getCurrentSeconds() - CFConfig.dataSaveInterval <= lockValue) { connection.close(); future.complete(Optional.of(PlayerData.LOCKED)); return; @@ -100,11 +98,11 @@ public abstract class AbstractSQLDatabase extends AbstractStorage { final Blob blob = rs.getBlob("data"); final byte[] dataByteArray = blob.getBytes(1, (int) blob.length()); blob.free(); - lockPlayerData(uuid); + if (lock) lockPlayerData(uuid, true); future.complete(Optional.of(plugin.getStorageManager().fromBytes(dataByteArray))); } else if (Bukkit.getPlayer(uuid) != null) { var data = PlayerData.empty(); - insertPlayerData(uuid, data); + insertPlayerData(uuid, data, lock); future.complete(Optional.of(data)); } else { future.complete(Optional.empty()); @@ -162,13 +160,13 @@ public abstract class AbstractSQLDatabase extends AbstractStorage { } } - public void insertPlayerData(UUID uuid, PlayerData playerData) { + public void insertPlayerData(UUID uuid, PlayerData playerData, boolean lock) { try ( Connection connection = getConnection(); PreparedStatement statement = connection.prepareStatement(String.format(SqlConstants.SQL_INSERT_DATA_BY_UUID, getTableName("data"))) ) { statement.setString(1, uuid.toString()); - statement.setInt(2, getCurrentSeconds()); + statement.setInt(2, lock ? getCurrentSeconds() : 0); statement.setBlob(3, new ByteArrayInputStream(plugin.getStorageManager().toBytes(playerData))); statement.execute(); } catch (SQLException e) { @@ -176,12 +174,13 @@ public abstract class AbstractSQLDatabase extends AbstractStorage { } } - public void lockPlayerData(UUID uuid) { + @Override + public void lockPlayerData(UUID uuid, boolean lock) { try ( Connection connection = getConnection(); PreparedStatement statement = connection.prepareStatement(String.format(SqlConstants.SQL_LOCK_BY_UUID, getTableName("data"))) ) { - statement.setInt(1, getCurrentSeconds()); + statement.setInt(1, lock ? getCurrentSeconds() : 0); statement.setString(2, uuid.toString()); statement.execute(); } catch (SQLException e) { @@ -189,8 +188,26 @@ public abstract class AbstractSQLDatabase extends AbstractStorage { } } + @Override + public Set getUniqueUsers(boolean legacy) { + Set uuids = new HashSet<>(); + try (Connection connection = getConnection(); + PreparedStatement statement = connection.prepareStatement(String.format(SqlConstants.SQL_SELECT_ALL_UUID, legacy ? getTableName("fishingbag") : getTableName("data")))) { + try (ResultSet rs = statement.executeQuery()) { + while (rs.next()) { + UUID uuid = UUID.fromString(rs.getString("uuid")); + uuids.add(uuid); + } + } + } catch (SQLException e) { + LogUtils.warn("Failed to get unique data.", e); + } + return uuids; + } + public static class SqlConstants { public static final String SQL_SELECT_BY_UUID = "SELECT * FROM `%s` WHERE `uuid` = ?"; + public static final String SQL_SELECT_ALL_UUID = "SELECT uuid FROM `%s`"; public static final String SQL_UPDATE_BY_UUID = "UPDATE `%s` SET `lock` = ?, `data` = ? WHERE `uuid` = ?"; public static final String SQL_LOCK_BY_UUID = "UPDATE `%s` SET `lock` = ? WHERE `uuid` = ?"; public static final String SQL_INSERT_DATA_BY_UUID = "INSERT INTO `%s`(`uuid`, `lock`, `data`) VALUES(?, ?, ?)"; diff --git a/plugin/src/main/java/net/momirealms/customfishing/storage/method/database/sql/SQLiteImpl.java b/plugin/src/main/java/net/momirealms/customfishing/storage/method/database/sql/SQLiteImpl.java index 03d79f60..943f8d78 100644 --- a/plugin/src/main/java/net/momirealms/customfishing/storage/method/database/sql/SQLiteImpl.java +++ b/plugin/src/main/java/net/momirealms/customfishing/storage/method/database/sql/SQLiteImpl.java @@ -77,7 +77,7 @@ public class SQLiteImpl extends AbstractSQLDatabase { @SuppressWarnings("DuplicatedCode") @Override - public CompletableFuture> getPlayerData(UUID uuid, boolean force) { + public CompletableFuture> getPlayerData(UUID uuid, boolean lock) { var future = new CompletableFuture>(); plugin.getScheduler().runTaskAsync(() -> { try ( @@ -87,20 +87,18 @@ public class SQLiteImpl extends AbstractSQLDatabase { statement.setString(1, uuid.toString()); ResultSet rs = statement.executeQuery(); if (rs.next()) { - int lock = rs.getInt(2); - if (!force && (lock != 0 && getCurrentSeconds() - CFConfig.dataSaveInterval <= lock)) { - statement.close(); - rs.close(); + int lockValue = rs.getInt(2); + if (lockValue != 0 && getCurrentSeconds() - CFConfig.dataSaveInterval <= lockValue) { connection.close(); future.complete(Optional.of(PlayerData.LOCKED)); return; } final byte[] dataByteArray = rs.getBytes("data"); - lockPlayerData(uuid); + if (lock) lockPlayerData(uuid, true); future.complete(Optional.of(plugin.getStorageManager().fromBytes(dataByteArray))); } else if (Bukkit.getPlayer(uuid) != null) { var data = PlayerData.empty(); - insertPlayerData(uuid, data); + insertPlayerData(uuid, data, lock); future.complete(Optional.of(data)); } else { future.complete(Optional.empty()); @@ -158,13 +156,13 @@ public class SQLiteImpl extends AbstractSQLDatabase { } @Override - public void insertPlayerData(UUID uuid, PlayerData playerData) { + public void insertPlayerData(UUID uuid, PlayerData playerData, boolean lock) { try ( Connection connection = getConnection(); PreparedStatement statement = connection.prepareStatement(String.format(SqlConstants.SQL_INSERT_DATA_BY_UUID, getTableName("data"))) ) { statement.setString(1, uuid.toString()); - statement.setInt(2, getCurrentSeconds()); + statement.setInt(2, lock ? getCurrentSeconds() : 0); statement.setBytes(3, plugin.getStorageManager().toBytes(playerData)); statement.execute(); } catch (SQLException e) { diff --git a/plugin/src/main/java/net/momirealms/customfishing/storage/method/file/JsonImpl.java b/plugin/src/main/java/net/momirealms/customfishing/storage/method/file/JsonImpl.java index acedc071..a38e2bb8 100644 --- a/plugin/src/main/java/net/momirealms/customfishing/storage/method/file/JsonImpl.java +++ b/plugin/src/main/java/net/momirealms/customfishing/storage/method/file/JsonImpl.java @@ -29,12 +29,15 @@ import java.io.FileInputStream; import java.io.FileWriter; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.HashSet; import java.util.Optional; +import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; public class JsonImpl extends AbstractStorage { + @SuppressWarnings("ResultOfMethodCallIgnored") public JsonImpl(CustomFishingPlugin plugin) { super(plugin); File folder = new File(plugin.getDataFolder(), "data"); @@ -47,7 +50,7 @@ public class JsonImpl extends AbstractStorage { } @Override - public CompletableFuture> getPlayerData(UUID uuid, boolean ignore) { + public CompletableFuture> getPlayerData(UUID uuid, boolean lock) { File file = getPlayerDataFile(uuid); PlayerData playerData; if (file.exists()) { @@ -94,4 +97,20 @@ public class JsonImpl extends AbstractStorage { } return fileBytes; } + + @Override + public Set getUniqueUsers(boolean legacy) { + // No legacy files + File folder = new File(plugin.getDataFolder(), "data"); + Set uuids = new HashSet<>(); + if (folder.exists()) { + File[] files = folder.listFiles(); + if (files != null) { + for (File file : files) { + uuids.add(UUID.fromString(file.getName().substring(file.getName().length() - 5))); + } + } + } + return uuids; + } } diff --git a/plugin/src/main/java/net/momirealms/customfishing/storage/method/file/YAMLImpl.java b/plugin/src/main/java/net/momirealms/customfishing/storage/method/file/YAMLImpl.java index af90882e..b4199514 100644 --- a/plugin/src/main/java/net/momirealms/customfishing/storage/method/file/YAMLImpl.java +++ b/plugin/src/main/java/net/momirealms/customfishing/storage/method/file/YAMLImpl.java @@ -28,14 +28,12 @@ import org.bukkit.configuration.file.YamlConfiguration; import java.io.File; import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; +import java.util.*; import java.util.concurrent.CompletableFuture; -public class YAMLImpl extends AbstractStorage { +public class YAMLImpl extends AbstractStorage implements LegacyDataStorageInterface { + @SuppressWarnings("ResultOfMethodCallIgnored") public YAMLImpl(CustomFishingPlugin plugin) { super(plugin); File folder = new File(plugin.getDataFolder(), "data"); @@ -52,7 +50,7 @@ public class YAMLImpl extends AbstractStorage { } @Override - public CompletableFuture> getPlayerData(UUID uuid, boolean ignore) { + public CompletableFuture> getPlayerData(UUID uuid, boolean lock) { File dataFile = getPlayerDataFile(uuid); if (!dataFile.exists()) { if (Bukkit.getPlayer(uuid) != null) { @@ -91,6 +89,26 @@ public class YAMLImpl extends AbstractStorage { return CompletableFuture.completedFuture(true); } + @Override + public Set getUniqueUsers(boolean legacy) { + File folder; + if (legacy) { + folder = new File(plugin.getDataFolder(), "data/fishingbag"); + } else { + folder = new File(plugin.getDataFolder(), "data"); + } + Set uuids = new HashSet<>(); + if (folder.exists()) { + File[] files = folder.listFiles(); + if (files != null) { + for (File file : files) { + uuids.add(UUID.fromString(file.getName().substring(0, file.getName().length() - 4))); + } + } + } + return uuids; + } + public StatisticData getStatistics(ConfigurationSection section) { if (section == null) return StatisticData.empty(); @@ -102,4 +120,42 @@ public class YAMLImpl extends AbstractStorage { return new StatisticData(map); } } + + @Override + public CompletableFuture> getLegacyPlayerData(UUID uuid) { + var builder = new PlayerData.Builder().setName(""); + File bagFile = new File(plugin.getDataFolder(), "data/fishingbag/" + uuid + ".yml"); + if (bagFile.exists()) { + YamlConfiguration yaml = YamlConfiguration.loadConfiguration(bagFile); + String contents = yaml.getString("contents", ""); + int size = yaml.getInt("size", 9); + builder.setBagData(new InventoryData(contents, size)); + } else { + builder.setBagData(InventoryData.empty()); + } + + File statFile = new File(plugin.getDataFolder(), "data/statistics/" + uuid + ".yml"); + if (statFile.exists()) { + YamlConfiguration yaml = YamlConfiguration.loadConfiguration(statFile); + HashMap map = new HashMap<>(); + for (Map.Entry entry : yaml.getValues(false).entrySet()) { + if (entry.getValue() instanceof Integer integer) { + map.put(entry.getKey(), integer); + } + } + builder.setStats(new StatisticData(map)); + } else { + builder.setStats(StatisticData.empty()); + } + + File sellFile = new File(plugin.getDataFolder(), "data/sell/" + uuid + ".yml"); + if (sellFile.exists()) { + YamlConfiguration yaml = YamlConfiguration.loadConfiguration(sellFile); + builder.setEarningData(new EarningData(yaml.getDouble("earnings"), yaml.getInt("date"))); + } else { + builder.setEarningData(EarningData.empty()); + } + + return CompletableFuture.completedFuture(Optional.of(builder.build())); + } } diff --git a/plugin/src/main/java/net/momirealms/customfishing/util/CompletableFutures.java b/plugin/src/main/java/net/momirealms/customfishing/util/CompletableFutures.java new file mode 100644 index 00000000..8d21cbf1 --- /dev/null +++ b/plugin/src/main/java/net/momirealms/customfishing/util/CompletableFutures.java @@ -0,0 +1,31 @@ +package net.momirealms.customfishing.util; + +import com.google.common.collect.ImmutableList; + +import java.util.Collection; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collector; +import java.util.stream.Stream; + +public final class CompletableFutures { + private CompletableFutures() {} + + public static > Collector, CompletableFuture> collector() { + return Collector.of( + ImmutableList.Builder::new, + ImmutableList.Builder::add, + (l, r) -> l.addAll(r.build()), + builder -> allOf(builder.build()) + ); + } + + public static CompletableFuture allOf(Stream> futures) { + CompletableFuture[] arr = futures.toArray(CompletableFuture[]::new); + return CompletableFuture.allOf(arr); + } + + public static CompletableFuture allOf(Collection> futures) { + CompletableFuture[] arr = futures.toArray(new CompletableFuture[0]); + return CompletableFuture.allOf(arr); + } +} \ No newline at end of file diff --git a/plugin/src/main/resources/config.yml b/plugin/src/main/resources/config.yml index abf8ac7f..ab361f23 100644 --- a/plugin/src/main/resources/config.yml +++ b/plugin/src/main/resources/config.yml @@ -149,6 +149,10 @@ other-settings: # set to -1 to disable data-saving-interval: 600 + # Lock player's data if a player is playing on a server that connected to database + # If you can ensure low database link latency and fast processing, you can consider disabling this option to save some performance + lock-data: true + # Requires PlaceholderAPI to work placeholder-register: '{date}': '%server_time_yyyy-MM-dd-HH:mm:ss%'