From c8fc5f4c81d8a2566a6858f6ba570f3473fe39e6 Mon Sep 17 00:00:00 2001 From: zontreck Date: Tue, 23 Apr 2024 18:39:02 -0700 Subject: [PATCH] Finish implementation 1 of Block Snapshotting --- gradle.properties | 2 +- .../dev/zontreck/libzontreck/LibZontreck.java | 12 ++ .../events/RegisterMigrationsEvent.java | 22 +++ .../memory/world/BlockRestoreQueue.java | 70 ++++++++- .../memory/world/DatabaseMigrations.java | 148 ++++++++++++++++++ .../memory/world/DatabaseWrapper.java | 75 ++++++++- 6 files changed, 320 insertions(+), 9 deletions(-) create mode 100644 src/main/java/dev/zontreck/libzontreck/events/RegisterMigrationsEvent.java create mode 100644 src/main/java/dev/zontreck/libzontreck/memory/world/DatabaseMigrations.java diff --git a/gradle.properties b/gradle.properties index 290d77a..754a4a2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -53,7 +53,7 @@ mod_name=Zontreck's Library Mod # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. mod_license=GPLv3 # The mod version. See https://semver.org/ -mod_version=1201.13.041224.0832 +mod_version=1201.13.042324.1837 # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # This should match the base package used for the mod sources. # See https://maven.apache.org/guides/mini/guide-naming-conventions.html diff --git a/src/main/java/dev/zontreck/libzontreck/LibZontreck.java b/src/main/java/dev/zontreck/libzontreck/LibZontreck.java index af3c290..982e02d 100644 --- a/src/main/java/dev/zontreck/libzontreck/LibZontreck.java +++ b/src/main/java/dev/zontreck/libzontreck/LibZontreck.java @@ -3,6 +3,7 @@ package dev.zontreck.libzontreck; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.sql.SQLException; import java.util.HashMap; import java.util.Iterator; import java.util.Map; @@ -16,6 +17,8 @@ import dev.zontreck.libzontreck.events.BlockRestoreQueueRegistrationEvent; import dev.zontreck.libzontreck.items.ModItems; import dev.zontreck.libzontreck.memory.world.BlockRestoreQueue; import dev.zontreck.libzontreck.memory.world.BlockRestoreQueueRegistry; +import dev.zontreck.libzontreck.memory.world.DatabaseMigrations; +import dev.zontreck.libzontreck.memory.world.DatabaseWrapper; import dev.zontreck.libzontreck.menus.ChestGUIScreen; import dev.zontreck.libzontreck.types.ModMenuTypes; import dev.zontreck.libzontreck.networking.NetworkEvents; @@ -112,6 +115,8 @@ public class LibZontreck { ALIVE=true; ServerConfig.init(); + DatabaseWrapper.start(); + CURRENT_SIDE = LogicalSide.SERVER; MinecraftForge.EVENT_BUS.post(new BlockRestoreQueueRegistrationEvent()); @@ -119,6 +124,13 @@ public class LibZontreck { for(ServerLevel level : event.getServer().getAllLevels()) { // Queues have been registered, but we now need to initialize the queue's data from saveddata + BlockRestoreQueueRegistry.init(level); + } + + try { + DatabaseMigrations.initMigrations(); + } catch (SQLException e) { + e.printStackTrace(); } } diff --git a/src/main/java/dev/zontreck/libzontreck/events/RegisterMigrationsEvent.java b/src/main/java/dev/zontreck/libzontreck/events/RegisterMigrationsEvent.java new file mode 100644 index 0000000..ee5fd93 --- /dev/null +++ b/src/main/java/dev/zontreck/libzontreck/events/RegisterMigrationsEvent.java @@ -0,0 +1,22 @@ +package dev.zontreck.libzontreck.events; + +import dev.zontreck.libzontreck.memory.world.DatabaseMigrations; +import net.minecraftforge.eventbus.api.Event; + +import java.util.ArrayList; +import java.util.List; + +public class RegisterMigrationsEvent extends Event +{ + private List migrations = new ArrayList<>(); + + public void register(DatabaseMigrations.Migration migration) + { + migrations.add(migration); + } + + public DatabaseMigrations.Migration[] getMigrations() + { + return (DatabaseMigrations.Migration[]) migrations.toArray(); + } +} diff --git a/src/main/java/dev/zontreck/libzontreck/memory/world/BlockRestoreQueue.java b/src/main/java/dev/zontreck/libzontreck/memory/world/BlockRestoreQueue.java index 46612a3..a98a854 100644 --- a/src/main/java/dev/zontreck/libzontreck/memory/world/BlockRestoreQueue.java +++ b/src/main/java/dev/zontreck/libzontreck/memory/world/BlockRestoreQueue.java @@ -1,8 +1,10 @@ package dev.zontreck.libzontreck.memory.world; +import dev.zontreck.libzontreck.vectors.WorldPosition; import net.minecraft.core.BlockPos; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.NbtUtils; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.level.block.Block; import net.minecraftforge.common.MinecraftForge; @@ -10,8 +12,9 @@ import net.minecraftforge.event.TickEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.common.Mod; -import java.io.File; -import java.io.IOException; +import java.io.*; +import java.sql.PreparedStatement; +import java.sql.SQLException; import java.util.ArrayList; import java.util.List; @@ -26,6 +29,15 @@ public abstract class BlockRestoreQueue RUNNER = new BlockRestoreRunner(this); } + /** + * When true, uses the database to store blocks. The blocks stored in the database will not be loaded into memory at runtime + * @return + */ + public boolean usesDatabase() + { + return false; + } + /** * Returns the restore queue name * @return Name of the restore queue @@ -55,11 +67,57 @@ public abstract class BlockRestoreQueue */ public void enqueueBlock(PrimitiveBlock block) { + if(usesDatabase()) + { + databaseUpdate(block); + return; + } BLOCK_QUEUE.add(block); notifyDirtyQueue(true); } + /** + * Called when enqueuing a block, this is a special handler to insert the block to the database. Custom queues should override this to put into a different table, or add additional data + * @param block + */ + public void databaseUpdate(PrimitiveBlock block) + { + + PreparedStatement pstmt = null; + try { + pstmt = DatabaseWrapper.get().prepareStatement("REPLACE INTO `blocks` (queueName, posX, posY, posZ, state, entity, dimension, snapshotID) VALUES (?, ?, ?, ?, ?, ?, ?, ?);"); + pstmt.setString(0, getRestoreQueueName()); + pstmt.setInt(1, block.position.getX()); + pstmt.setInt(2, block.position.getY()); + pstmt.setInt(3, block.position.getZ()); + ByteArrayOutputStream blockState = new ByteArrayOutputStream(); + DataOutputStream dos0 = new DataOutputStream(blockState); + NbtIo.write(NbtUtils.writeBlockState(block.blockState), dos0); + pstmt.setBytes(4, blockState.toByteArray()); + ByteArrayOutputStream blockEntity = new ByteArrayOutputStream(); + + if(block.blockEntity == null) + { + pstmt.setObject(5, null); + } else { + dos0 = new DataOutputStream(blockEntity); + NbtIo.write(block.blockEntity, dos0); + pstmt.setBytes(5, blockEntity.toByteArray()); + } + + pstmt.setString(6, WorldPosition.getDim(block.level)); + pstmt.setInt(7, 0); + + DatabaseWrapper.get().executePreparedStatement(pstmt); + + } catch (SQLException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + /** * Executed when the queue is modified. * @param blockAdded Whether a block was added or removed from the queue @@ -138,8 +196,16 @@ public abstract class BlockRestoreQueue * @throws IOException On failure to read a file */ public void initialize(ServerLevel level) throws IOException { + if(usesDatabase()) + { + return; + } var file = SaveDataFactory.builder().withDimension(level).withQueueID(this).withPosition(null).build(); + if(!file.getSaveDataPath().toFile().exists()) + { + return; + } CompoundTag tag = NbtIo.read(file.getSaveDataPath().toFile()); SaveDataFactory.SaveDataManifest manifest = SaveDataFactory.SaveDataManifest.deserialize(tag); diff --git a/src/main/java/dev/zontreck/libzontreck/memory/world/DatabaseMigrations.java b/src/main/java/dev/zontreck/libzontreck/memory/world/DatabaseMigrations.java new file mode 100644 index 0000000..a0175ae --- /dev/null +++ b/src/main/java/dev/zontreck/libzontreck/memory/world/DatabaseMigrations.java @@ -0,0 +1,148 @@ +package dev.zontreck.libzontreck.memory.world; + +import com.google.common.collect.Lists; +import dev.zontreck.libzontreck.events.RegisterMigrationsEvent; +import net.minecraftforge.common.MinecraftForge; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class DatabaseMigrations +{ + public static class Migration + { + String tableID; + int version; + List migrationActions = new ArrayList<>(); + + private Migration(){ + tableID = ""; + version = 0; + } + + /** + * Builder pattern function - Sets the table ID for the migration + * @param tableID + * @return + */ + public Migration withTableID(String tableID) + { + this.tableID = tableID; + return this; + } + + /** + * Builder pattern function - Sets the table version for the migration + * @param version + * @return + */ + public Migration withVersion(int version) + { + this.version = version; + return this; + } + + /** + * Builder pattern function - Adds the action to be executed. The list will operate as FILO. + * @param pstat + * @return + */ + public Migration withMigrationAction(PreparedStatement pstat) + { + migrationActions.add(pstat); + return this; + } + + /** + * Executes the migration as defined by the builder pattern. + */ + public void execute() + { + for(PreparedStatement pstmt : migrationActions) + { + try { + DatabaseWrapper.get().executePreparedStatement(pstmt); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + try { + + PreparedStatement pstat = DatabaseWrapper.get().prepareStatement("REPLACE INTO `migrations` (tableID, version) VALUES (?,?);"); + pstat.setString(0, tableID); + pstat.setInt(1, version); + } catch (SQLException ex) + { + ex.printStackTrace(); + } + + + } + + } + private static List migrations = new ArrayList<>(); + + public static void initMigrations() throws SQLException { + Migration migrationsTable = builder() + .withVersion(0) + .withTableID("migrations"); + + PreparedStatement statement = DatabaseWrapper.get().prepareStatement("CREATE TABLE `migrations` (" + + " `tableID` varchar(255) NOT NULL," + + " `version` int(11) NOT NULL," + + " PRIMARY KEY (`tableID`)," + + " UNIQUE KEY `tableID` (`tableID`)" + + ") ;"); + migrations.add(migrationsTable.withMigrationAction(statement)); + + Migration blocksTable = builder() + .withTableID("blocks") + .withVersion(0); + + PreparedStatement makeBlocksTable = DatabaseWrapper.get().prepareStatement("CREATE TABLE `blocks` (" + + " `time` timestamp NOT NULL DEFAULT current_timestamp()," + + " `queueName` varchar(255) NOT NULL," + + " `posX` int(11) NOT NULL," + + " `posY` int(11) NOT NULL," + + " `posZ` int(11) NOT NULL," + + " `state` blob NOT NULL," + + " `entity` blob," + + " `dimension` varchar(255) NOT NULL," + + " `snapshotID` int(11) NOT NULL DEFAULT 0 COMMENT 'Enables multiple blocks existing at the same position'," + + " PRIMARY KEY (`time`)," + + " UNIQUE KEY `posX` (`posX`)," + + " UNIQUE KEY `posY` (`posY`)," + + " UNIQUE KEY `posZ` (`posZ`)" + + "); "); + + migrations.add(blocksTable.withMigrationAction(makeBlocksTable)); + + RegisterMigrationsEvent rme = new RegisterMigrationsEvent(); + MinecraftForge.EVENT_BUS.post(rme); + + + migrations.addAll(Lists.reverse(List.of(rme.getMigrations()))); + + + executeMigrations(); + } + + private static void executeMigrations() + { + + List migration = Lists.reverse(migrations); + + for(Migration m : migration) + { + m.execute(); + } + } + + public static Migration builder() + { + return new Migration(); + } +} diff --git a/src/main/java/dev/zontreck/libzontreck/memory/world/DatabaseWrapper.java b/src/main/java/dev/zontreck/libzontreck/memory/world/DatabaseWrapper.java index 18ace42..dc7b3ca 100644 --- a/src/main/java/dev/zontreck/libzontreck/memory/world/DatabaseWrapper.java +++ b/src/main/java/dev/zontreck/libzontreck/memory/world/DatabaseWrapper.java @@ -1,15 +1,54 @@ package dev.zontreck.libzontreck.memory.world; +import dev.zontreck.libzontreck.LibZontreck; +import dev.zontreck.libzontreck.config.ServerConfig; + import java.sql.*; public class DatabaseWrapper { private Connection connection; + private static DatabaseWrapper instance; + private static boolean hasDatabase=false; + + private static void setHasDatabase(boolean value) + { + hasDatabase = value; + } + + public static DatabaseWrapper get() + { + if(instance==null) + start(); + return instance; + } + + /** + * This function will return true if the database drivers are available. + * @return + */ + public static boolean databaseIsAvailable() + { + return hasDatabase; + } + public DatabaseWrapper() { connection = null; } + public static void start() + { + instance = new DatabaseWrapper(); + try { + instance.connect(ServerConfig.database.host, ServerConfig.database.user, ServerConfig.database.password); + setHasDatabase(true); + } catch (SQLException e) { + setHasDatabase(false); + throw new RuntimeException(e); + } + } + public void connect(String url, String username, String password) throws SQLException { try { // Try MariaDB JDBC driver @@ -17,16 +56,28 @@ public class DatabaseWrapper { connection = DriverManager.getConnection("jdbc:mariadb://" + url, username, password); } catch (ClassNotFoundException | SQLException e) { // MariaDB not found or failed to connect, try MySQL + LibZontreck.LOGGER.warn("Failed to connect via MariaDB: " + e.getMessage() + "; Attempting to fall back to mysql"); try { Class.forName("com.mysql.cj.jdbc.Driver"); connection = DriverManager.getConnection("jdbc:mysql://" + url, username, password); } catch (ClassNotFoundException | SQLException ex) { // MySQL not found or failed to connect, try SQLite try { - Class.forName("org.sqlite.JDBC"); - connection = DriverManager.getConnection("jdbc:sqlite:" + url); - } catch (ClassNotFoundException | SQLException exc) { - throw new SQLException("Failed to connect to database: " + exc.getMessage()); + + Class.forName("com.mysql.jdbc.Driver"); + connection = DriverManager.getConnection("jdbc:mysql://" + url, username, password); + }catch (ClassNotFoundException | SQLException ex1) + { + + LibZontreck.LOGGER.warn("Failed to connect via MySQL: " + e.getMessage() + "; " + ex1.getMessage()+ "; Attempting to fall back to sqlite"); + + try { + Class.forName("org.sqlite.JDBC"); + connection = DriverManager.getConnection("jdbc:sqlite:" + url); + } catch (ClassNotFoundException | SQLException exc) { + LibZontreck.LOGGER.warn("Failed to connect via SQLite: " + e.getMessage() + "; If you require the use of the block queues, please check the above warnings for explanation on cause. It could be that you do not have the relevant JDBC installed in your server mods list."); + throw new SQLException("Failed to connect to database: " + exc.getMessage()); + } } } } @@ -44,8 +95,7 @@ public class DatabaseWrapper { if (connection == null) { throw new SQLException("Connection not established."); } - Statement stmt = connection.createStatement(); - return stmt.executeUpdate(statement); + return connection.createStatement().executeUpdate(statement); } public void disconnect() throws SQLException { @@ -54,4 +104,17 @@ public class DatabaseWrapper { } } + public int executePreparedStatement(PreparedStatement preparedStatement) throws SQLException { + if (connection == null) { + throw new SQLException("Connection not established."); + } + return preparedStatement.executeUpdate(); + } + + public PreparedStatement prepareStatement(String query) throws SQLException { + if (connection == null) { + throw new SQLException("Connection not established."); + } + return connection.prepareStatement(query); + } }