Improve entity logging/rollbacks

- Allow UUID changes on respawn
- Log Spawn
- Implement rollback and redo for entities
This commit is contained in:
Brokkonaut
2018-11-09 00:40:36 +01:00
parent 3a2c1d8d6f
commit 9d1baee2e2
9 changed files with 198 additions and 21 deletions

View File

@@ -753,6 +753,18 @@ public class Consumer extends Thread {
addQueueLast(new EntityRow(loc, actor, entityType, entityId, changeType, Utils.serializeYamlConfiguration(data)));
}
/**
* Change the UUID that is stored for an entity in the database. This is needed when an entity is respawned
* and now has a different UUID.
*
* @param world the world that contains the entity
* @param entityId the database id of the entity
* @param entityUUID the new UUID of the entity
*/
public void queueEntityUUIDChange(World world, int entityId, UUID entityUUID) {
addQueueLast(new EntityUUIDChange(world, entityId, entityUUID));
}
private String playerID(Actor actor) {
if (actor == null) {
return "NULL";
@@ -1037,9 +1049,9 @@ public class Consumer extends Thread {
final String table = getWorldConfig(loc.getWorld()).table;
final String[] inserts = new String[2];
inserts[0] = "INSERT IGNORE INTO `" + table + "-entityids` (entityuuid) SELECT '" + mysqlTextEscape(entityid.toString()) + "' FROM `" + table + "-entityids` WHERE NOT EXISTS (SELECT NULL FROM `" + table + "-entityids` WHERE entityuuid = '" + mysqlTextEscape(entityid.toString()) + "') LIMIT 1";
inserts[0] = "INSERT IGNORE INTO `" + table + "-entityids` (entityuuid) SELECT '" + mysqlTextEscape(entityUUID.toString()) + "' FROM `" + table + "-entityids` WHERE NOT EXISTS (SELECT NULL FROM `" + table + "-entityids` WHERE entityuuid = '" + mysqlTextEscape(entityUUID.toString()) + "') LIMIT 1";
int entityTypeId = EntityTypeConverter.getOrAddEntityTypeId(type);
inserts[1] = "INSERT INTO `" + table + "-entities` (date, playerid, entityid, entitytypeid, x, y, z, action, data) VALUES (FROM_UNIXTIME(" + date + "), " + playerID(actor) + ", " + "(SELECT entityid FROM `" + table + "-entityids` WHERE entityuuid = '" + mysqlTextEscape(entityid.toString()) + "')"
inserts[1] = "INSERT INTO `" + table + "-entities` (date, playerid, entityid, entitytypeid, x, y, z, action, data) VALUES (FROM_UNIXTIME(" + date + "), " + playerID(actor) + ", " + "(SELECT entityid FROM `" + table + "-entityids` WHERE entityuuid = '" + mysqlTextEscape(entityUUID.toString()) + "')"
+ ", " + entityTypeId + ", '" + loc.getBlockX() + "', " + safeY(loc) + ", '" + loc.getBlockZ() + "', " + changeType.ordinal() + ", " + Utils.mysqlPrepareBytesForInsertAllowNull(data) + ");";
return inserts;
}
@@ -1069,10 +1081,10 @@ public class Consumer extends Thread {
rs.close();
}
}
PreparedStatement smt = batchHelper.getOrPrepareStatement(conn, statementString, Statement.RETURN_GENERATED_KEYS);
PreparedStatement smt = batchHelper.getOrPrepareStatement(conn, statementString, Statement.NO_GENERATED_KEYS);
smt.setLong(1, date);
smt.setInt(2, sourceActor);
smt.setInt(3, getEntityUUID(conn, loc.getWorld(), entityid));
smt.setInt(3, getEntityUUID(conn, loc.getWorld(), entityUUID));
smt.setInt(4, EntityTypeConverter.getOrAddEntityTypeId(type));
smt.setInt(5, loc.getBlockX());
smt.setInt(6, safeY(loc));
@@ -1083,6 +1095,42 @@ public class Consumer extends Thread {
}
}
private class EntityUUIDChange implements Row {
private final World world;
private final int entityId;
private final UUID entityUUID;
final String updateEntityUUIDString;
public EntityUUIDChange(World world, int entityId, UUID entityUUID) {
this.world = world;
this.entityId = entityId;
this.entityUUID = entityUUID;
updateEntityUUIDString = getWorldConfig(world).updateEntityUUIDString;
}
@Override
public String[] getInserts() {
final String table = getWorldConfig(world).table;
final String[] inserts = new String[1];
inserts[0] = "UPDATE `" + table + "-entityids` SET entityuuid = '" + mysqlTextEscape(entityUUID.toString()) + "' WHERE entityid = " + entityId;
return inserts;
}
@Override
public Actor[] getActors() {
return new Actor[0];
}
@Override
public void process(Connection conn, BatchHelper batchHelper) throws SQLException {
PreparedStatement smt = batchHelper.getOrPrepareStatement(conn, updateEntityUUIDString, Statement.NO_GENERATED_KEYS);
smt.setString(1, entityUUID.toString());
smt.setInt(2, entityId);
smt.executeUpdate();
}
}
private int safeY(Location loc) {
int safeY = loc.getBlockY();
if (safeY < 0)

View File

@@ -30,7 +30,8 @@ public class EntityChange implements LookupCacheElement {
public final Location loc;
public final Actor actor;
public final EntityType type;
public final UUID entityid;
public final int entityId;
public final UUID entityUUID;
public final EntityChangeType changeType;
public final byte[] data;
@@ -40,7 +41,8 @@ public class EntityChange implements LookupCacheElement {
this.loc = loc;
this.actor = actor;
this.type = type;
this.entityid = entityid;
this.entityId = -1;
this.entityUUID = entityid;
this.changeType = changeType;
this.data = data;
}
@@ -51,7 +53,8 @@ public class EntityChange implements LookupCacheElement {
loc = p.needCoords ? new Location(p.world, rs.getInt("x"), rs.getInt("y"), rs.getInt("z")) : null;
actor = p.needPlayer ? new Actor(rs) : null;
type = p.needType ? EntityTypeConverter.getEntityType(rs.getInt("entitytypeid")) : null;
entityid = p.needData ? UUID.fromString(rs.getString("entityuuid")) : null;
entityId = p.needData ? rs.getInt("entityid") : 0;
entityUUID = p.needData ? UUID.fromString(rs.getString("entityuuid")) : null;
changeType = p.needType ? EntityChangeType.valueOf(rs.getInt("action")) : null;
data = p.needData ? rs.getBytes("data") : null;
}

View File

@@ -270,7 +270,7 @@ public final class QueryParams implements Cloneable {
select += "entitytypeid, action, ";
}
if(needData) {
select += "entityuuid, data, ";
select += "entityid, entityuuid, data, ";
}
}
select = select.substring(0, select.length() - 2) + " ";

View File

@@ -33,8 +33,10 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.logging.Level;
@@ -56,6 +58,7 @@ public class WorldEditor implements Runnable {
private long elapsedTime = 0;
public LookupCacheElement[] errors;
private boolean forceReplace;
private HashMap<Integer, UUID> uuidReplacements = new HashMap<>();
public WorldEditor(LogBlock logblock, World world) {
this(logblock, world, false);
@@ -164,6 +167,11 @@ public class WorldEditor implements Runnable {
}
}
protected UUID getReplacedUUID(int entityid, UUID unreplaced) {
UUID replaced = uuidReplacements.get(entityid);
return replaced != null ? replaced : unreplaced;
}
public static enum PerformResult {
SUCCESS, BLACKLISTED, NO_ACTION
}
@@ -182,7 +190,10 @@ public class WorldEditor implements Runnable {
@Override
public PerformResult perform() throws WorldEditorException {
if (changeType == EntityChangeType.KILL && rollback) {
if (changeType == (rollback ? EntityChangeType.KILL : EntityChangeType.CREATE)) {
// spawn entity
UUID uuid = getReplacedUUID(entityId, entityUUID);
Entity result = null;
YamlConfiguration deserialized = Utils.deserializeYamlConfiguration(data);
double x = deserialized.getDouble("x");
double y = deserialized.getDouble("y");
@@ -190,14 +201,39 @@ public class WorldEditor implements Runnable {
float yaw = (float) deserialized.getDouble("yaw");
float pitch = (float) deserialized.getDouble("pitch");
Location location = new Location(world, x, y, z, yaw, pitch);
Entity existing = Utils.loadChunksForEntity(location.getChunk(), uuid);
if (existing != null) {
return PerformResult.NO_ACTION;
}
byte[] serializedWorldEditEntity = (byte[]) deserialized.get("worldedit");
if (serializedWorldEditEntity != null) {
Entity result = WorldEditHelper.restoreEntity(location, type, serializedWorldEditEntity);
if (result == null) {
throw new WorldEditorException("Could not restore " + type, location);
result = WorldEditHelper.restoreEntity(location, type, serializedWorldEditEntity);
}
if (result == null) {
throw new WorldEditorException("Could not restore " + type, location);
} else {
if (!result.getUniqueId().equals(uuid)) {
logblock.getConsumer().queueEntityUUIDChange(world, entityId, result.getUniqueId());
uuidReplacements.put(entityId, result.getUniqueId());
}
}
return PerformResult.SUCCESS;
} else if (changeType == (rollback ? EntityChangeType.CREATE : EntityChangeType.KILL)) {
// kill entity
UUID uuid = getReplacedUUID(entityId, entityUUID);
YamlConfiguration deserialized = Utils.deserializeYamlConfiguration(data);
double x = deserialized.getDouble("x");
double y = deserialized.getDouble("y");
double z = deserialized.getDouble("z");
float yaw = (float) deserialized.getDouble("yaw");
float pitch = (float) deserialized.getDouble("pitch");
Location location = new Location(world, x, y, z, yaw, pitch);
Entity existing = Utils.loadChunksForEntity(location.getChunk(), uuid);
if (existing != null) {
existing.remove();
return PerformResult.SUCCESS;
}
return PerformResult.NO_ACTION; // the entity is not there, so we cannot do anything
}
return PerformResult.NO_ACTION;
}

View File

@@ -0,0 +1,38 @@
package de.diddiz.LogBlock;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.bukkit.inventory.ItemStack;
import de.diddiz.LogBlock.QueryParams.BlockChangeType;
import de.diddiz.util.Utils;
public class WorldEditorEditFactory {
private final WorldEditor editor;
private final boolean rollback;
private final QueryParams params;
public WorldEditorEditFactory(WorldEditor editor, QueryParams params, boolean rollback) {
this.editor = editor;
this.params = params;
this.rollback = rollback;
}
public void processRow(ResultSet rs) throws SQLException {
if (params.bct == BlockChangeType.ENTITIES) {
editor.queueEntityEdit(rs, params, rollback);
return;
}
ChestAccess chestaccess = null;
ItemStack stack = Utils.loadItemStack(rs.getBytes("item"));
if (stack != null) {
chestaccess = new ChestAccess(stack, rs.getBoolean("itemremove") == rollback, rs.getInt("itemtype"));
}
if (rollback) {
editor.queueBlockEdit(rs.getInt("x"), rs.getInt("y"), rs.getInt("z"), rs.getInt("replaced"), rs.getInt("replacedData"), rs.getBytes("replacedState"), rs.getInt("type"), rs.getInt("typeData"), rs.getBytes("typeState"), chestaccess);
} else {
editor.queueBlockEdit(rs.getInt("x"), rs.getInt("y"), rs.getInt("z"), rs.getInt("type"), rs.getInt("typeData"), rs.getBytes("typeState"), rs.getInt("replaced"), rs.getInt("replacedData"), rs.getBytes("replacedState"), chestaccess);
}
}
}

View File

@@ -17,6 +17,7 @@ public class WorldConfig extends LoggingEnabledMapping {
public final String insertBlockStateStatementString;
public final String insertBlockChestDataStatementString;
public final String insertEntityStatementString;
public final String updateEntityUUIDString;
public WorldConfig(String world, File file) throws IOException {
this.world = world;
@@ -44,5 +45,6 @@ public class WorldConfig extends LoggingEnabledMapping {
insertBlockStateStatementString = "INSERT INTO `" + table + "-state` (replacedState, typeState, id) VALUES(?, ?, ?)";
insertBlockChestDataStatementString = "INSERT INTO `" + table + "-chestdata` (item, itemremove, id, itemtype) values (?, ?, ?, ?)";
insertEntityStatementString = "INSERT INTO `" + table + "-entities` (date, playerid, entityid, entitytypeid, x, y, z, action, data) VALUES (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?)";
updateEntityUUIDString = "UPDATE `" + table + "-entityids` SET entityuuid = ? WHERE entityid = ?";
}
}

View File

@@ -11,14 +11,18 @@ import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import java.util.zip.ZipException;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Entity;
import org.bukkit.inventory.ItemStack;
import de.diddiz.LogBlock.LogBlock;
@@ -284,4 +288,28 @@ public class Utils {
public static String serializeForSQL(YamlConfiguration conf) {
return mysqlPrepareBytesForInsertAllowNull(serializeYamlConfiguration(conf));
}
public static Entity loadChunksForEntity(Chunk chunk, UUID uuid) {
Entity e = Bukkit.getEntity(uuid);
if (e != null) {
return e;
}
chunk.load();
e = Bukkit.getEntity(uuid);
if (e != null) {
return e;
}
int chunkx = chunk.getX();
int chunkz = chunk.getZ();
for (int i = 0; i < 8; i++) {
int x = i < 3 ? chunkx - 1 : (i < 5 ? chunkx : chunkx + 1);
int z = i == 0 || i == 3 || i == 5 ? chunkz - 1 : (i == 1 || i == 6 ? chunkz : chunkz + 1);
chunk.getWorld().loadChunk(x, z);
e = Bukkit.getEntity(uuid);
if (e != null) {
return e;
}
}
return null;
}
}

View File

@@ -7,9 +7,12 @@ import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.event.entity.EntitySpawnEvent;
import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason;
import de.diddiz.LogBlock.Actor;
import de.diddiz.LogBlock.EntityChange;
@@ -28,12 +31,12 @@ public class AdvancedKillLogging extends LoggingListener {
if (!(entity instanceof Animals) && !(entity instanceof Villager)) {
return;
}
Actor killer;
Actor actor;
EntityDamageEvent lastDamage = entity.getLastDamageCause();
if (lastDamage instanceof EntityDamageByEntityEvent) {
killer = Actor.actorFromEntity(((EntityDamageByEntityEvent) lastDamage).getDamager());
actor = Actor.actorFromEntity(((EntityDamageByEntityEvent) lastDamage).getDamager());
} else {
killer = new Actor(lastDamage.getCause().toString());
actor = new Actor(lastDamage.getCause().toString());
}
Location location = entity.getLocation();
YamlConfiguration data = new YamlConfiguration();
@@ -44,7 +47,30 @@ public class AdvancedKillLogging extends LoggingListener {
data.set("pitch", location.getPitch());
data.set("worldedit", WorldEditHelper.serializeEntity(entity));
consumer.queueEntityModification(killer, entity.getUniqueId(), entity.getType(), location, EntityChange.EntityChangeType.KILL, data);
consumer.queueEntityModification(actor, entity.getUniqueId(), entity.getType(), location, EntityChange.EntityChangeType.KILL, data);
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onEntitySpawn(CreatureSpawnEvent event) {
if (event.getSpawnReason() == SpawnReason.CUSTOM) {
return;
}
LivingEntity entity = event.getEntity();
if (!(entity instanceof Animals) && !(entity instanceof Villager)) {
return;
}
Actor actor = new Actor(event.getSpawnReason().toString());
Location location = entity.getLocation();
YamlConfiguration data = new YamlConfiguration();
data.set("x", location.getX());
data.set("y", location.getY());
data.set("z", location.getZ());
data.set("yaw", location.getYaw());
data.set("pitch", location.getPitch());
data.set("worldedit", WorldEditHelper.serializeEntity(entity));
consumer.queueEntityModification(actor, entity.getUniqueId(), entity.getType(), location, EntityChange.EntityChangeType.CREATE, data);
}
}

View File

@@ -68,14 +68,10 @@ public class WorldEditHelper {
if (namedTag.getName().equals("entity") && namedTag.getTag() instanceof CompoundTag) {
CompoundTag serializedState = (CompoundTag) namedTag.getTag();
BaseEntity state = new BaseEntity(weType, serializedState);
CompoundTag oldNbt = state.getNbtData();
UUID oldUUID = new UUID(oldNbt.getLong("UUIDMost"), oldNbt.getLong("UUIDLeast"));
com.sk89q.worldedit.entity.Entity weEntity = weLocation.getExtent().createEntity(weLocation, state);
if (weEntity != null) {
CompoundTag newNbt = weEntity.getState().getNbtData();
newUUID = new UUID(newNbt.getLong("UUIDMost"), newNbt.getLong("UUIDLeast"));
System.out.println("Old UUID: " + oldUUID);
System.out.println("New UUID: " + newUUID);
}
}
return newUUID == null ? null : Bukkit.getEntity(newUUID);