Finish implementation 1 of Block Snapshotting

This commit is contained in:
zontreck 2024-04-23 18:39:02 -07:00
parent 40ce8774fb
commit c8fc5f4c81
6 changed files with 320 additions and 9 deletions

View file

@ -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

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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);

View file

@ -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();
}
}

View file

@ -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,20 +56,32 @@ 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("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());
}
}
}
}
}
public ResultSet executeQuery(String query) throws SQLException {
if (connection == null) {
@ -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);
}
}