Finish implementation 1 of Block Snapshotting
This commit is contained in:
parent
40ce8774fb
commit
c8fc5f4c81
6 changed files with 320 additions and 9 deletions
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<DatabaseMigrations.Migration> migrations = new ArrayList<>();
|
||||
|
||||
public void register(DatabaseMigrations.Migration migration)
|
||||
{
|
||||
migrations.add(migration);
|
||||
}
|
||||
|
||||
public DatabaseMigrations.Migration[] getMigrations()
|
||||
{
|
||||
return (DatabaseMigrations.Migration[]) migrations.toArray();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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<PreparedStatement> 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<Migration> 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> migration = Lists.reverse(migrations);
|
||||
|
||||
for(Migration m : migration)
|
||||
{
|
||||
m.execute();
|
||||
}
|
||||
}
|
||||
|
||||
public static Migration builder()
|
||||
{
|
||||
return new Migration();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Reference in a new issue