diff --git a/src/main/java/de/diddiz/LogBlock/Consumer.java b/src/main/java/de/diddiz/LogBlock/Consumer.java index d96daa1..c5e40f0 100644 --- a/src/main/java/de/diddiz/LogBlock/Consumer.java +++ b/src/main/java/de/diddiz/LogBlock/Consumer.java @@ -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) diff --git a/src/main/java/de/diddiz/LogBlock/EntityChange.java b/src/main/java/de/diddiz/LogBlock/EntityChange.java index 35cf6c3..2230789 100644 --- a/src/main/java/de/diddiz/LogBlock/EntityChange.java +++ b/src/main/java/de/diddiz/LogBlock/EntityChange.java @@ -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; } diff --git a/src/main/java/de/diddiz/LogBlock/QueryParams.java b/src/main/java/de/diddiz/LogBlock/QueryParams.java index 94b87a7..afceadd 100644 --- a/src/main/java/de/diddiz/LogBlock/QueryParams.java +++ b/src/main/java/de/diddiz/LogBlock/QueryParams.java @@ -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) + " "; diff --git a/src/main/java/de/diddiz/LogBlock/WorldEditor.java b/src/main/java/de/diddiz/LogBlock/WorldEditor.java index 16f5c89..d03c58f 100644 --- a/src/main/java/de/diddiz/LogBlock/WorldEditor.java +++ b/src/main/java/de/diddiz/LogBlock/WorldEditor.java @@ -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 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; } diff --git a/src/main/java/de/diddiz/LogBlock/WorldEditorEditFactory.java b/src/main/java/de/diddiz/LogBlock/WorldEditorEditFactory.java new file mode 100644 index 0000000..e23dc6b --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/WorldEditorEditFactory.java @@ -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); + } + } +} diff --git a/src/main/java/de/diddiz/LogBlock/config/WorldConfig.java b/src/main/java/de/diddiz/LogBlock/config/WorldConfig.java index 8b647d3..b8542b9 100644 --- a/src/main/java/de/diddiz/LogBlock/config/WorldConfig.java +++ b/src/main/java/de/diddiz/LogBlock/config/WorldConfig.java @@ -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 = ?"; } } diff --git a/src/main/java/de/diddiz/util/Utils.java b/src/main/java/de/diddiz/util/Utils.java index c4ea0f9..ee16bc4 100644 --- a/src/main/java/de/diddiz/util/Utils.java +++ b/src/main/java/de/diddiz/util/Utils.java @@ -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; + } } diff --git a/src/main/java/de/diddiz/worldedit/AdvancedKillLogging.java b/src/main/java/de/diddiz/worldedit/AdvancedKillLogging.java index 5cbd20f..ed2a94a 100644 --- a/src/main/java/de/diddiz/worldedit/AdvancedKillLogging.java +++ b/src/main/java/de/diddiz/worldedit/AdvancedKillLogging.java @@ -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); } } diff --git a/src/main/java/de/diddiz/worldedit/WorldEditHelper.java b/src/main/java/de/diddiz/worldedit/WorldEditHelper.java index 5bfcd33..69285fd 100644 --- a/src/main/java/de/diddiz/worldedit/WorldEditHelper.java +++ b/src/main/java/de/diddiz/worldedit/WorldEditHelper.java @@ -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);