diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..085fb981 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +charset = utf-8 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.java] +indent_style = space +indent_size = 4 + +[*.yml] +indent_style = space +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..1adafa6e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.java text +*.txt text +*.yml text +*.xml text +*.md text +LICENSE text \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 89122a1d..b2270fd7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: java jdk: - - oraclejdk7 + - openjdk8 notifications: email: false diff --git a/LogBlockQuestioner.jar b/LogBlockQuestioner.jar deleted file mode 100644 index aa16a489..00000000 Binary files a/LogBlockQuestioner.jar and /dev/null differ diff --git a/README.md b/README.md index 31a32700..9b17fdee 100644 --- a/README.md +++ b/README.md @@ -4,4 +4,4 @@ LogBlock This plugin logs block changes such as breaking, placing, modifying, or burning to a MySQL Database. It can be used as an anti-griefing tool to find out who made a particular edit, or even roll back changes by certain players. Originally written by bootswithdefer, for hMod, ported to Bukkit by me, because of the inability to identfy griefers. BigBrother also did't work, so I was forced to do it myself. The honor belongs to bootswithdefer for the sourcecode, I only spent about 8 hours to transcribe. All functions except sign text logging shold work as in hMod. The use of permissions plugin is possible, but not necessary. -Questioner: http://git.io/u2MxKQ +You can download development builds [from our Jenkins server](https://www.iani.de/jenkins/job/LogBlock/). diff --git a/pom.xml b/pom.xml index fe6a40c0..5c5d8265 100644 --- a/pom.xml +++ b/pom.xml @@ -1,10 +1,10 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 de.diddiz logblock - 1.11-SNAPSHOT + 1.20.0.0-SNAPSHOT jar LogBlock @@ -29,70 +29,61 @@ - md_5-releases - http://repo.md-5.net/content/repositories/releases/ + nexus + Releases + https://www.iani.de/nexus/content/repositories/releases/ - md_5-snapshots - http://repo.md-5.net/content/repositories/snapshots/ + nexus + Snapshot + https://www.iani.de/nexus/content/repositories/snapshots/ - org.bukkit - bukkit - 1.11-R0.1-SNAPSHOT + org.spigotmc + spigot-api + 1.21.5-R0.1-SNAPSHOT provided - ${project.groupId} - questioner - ${project.version} - system - ${project.basedir}/LogBlockQuestioner.jar + com.sk89q.worldedit + worldedit-bukkit + 7.3.0 + provided - com.sk89q - worldedit - 6.0.0-SNAPSHOT + com.sk89q.worldguard + worldguard-bukkit + 7.1.0-SNAPSHOT provided junit junit - 4.11 + 4.13.2 test com.zaxxer HikariCP - 2.4.1 - compile - - - org.slf4j - slf4j-jdk14 - 1.7.10 + 5.1.0 compile - repobo-snap - http://repo.bukkit.org/content/groups/public + spigot-repo + https://hub.spigotmc.org/nexus/content/repositories/snapshots/ sk89q-repo - http://maven.sk89q.com/repo/ + https://maven.enginehub.org/repo/ - kitteh-repo - http://repo.kitteh.org/content/groups/public - - - spigot-repo - https://hub.spigotmc.org/nexus/content/repositories/snapshots/ + brokkonaut-repo + https://www.iani.de/nexus/content/groups/public/ @@ -127,23 +118,22 @@ true - ${project.basedir}/src/main/resources + src/main/resources org.apache.maven.plugins maven-compiler-plugin - 3.2 + 3.13.0 - 1.7 - 1.7 + 21 org.codehaus.mojo build-helper-maven-plugin - 1.9.1 + 3.5.0 regex-property @@ -163,7 +153,7 @@ org.apache.maven.plugins maven-shade-plugin - 2.3 + 3.5.3 @@ -172,6 +162,14 @@ shade + + + + com.zaxxer.hikari + de.diddiz.lib.com.zaxxer.hikari + + + diff --git a/src/main/java/de/diddiz/LogBlock/Actor.java b/src/main/java/de/diddiz/LogBlock/Actor.java index b739739f..b13a4640 100644 --- a/src/main/java/de/diddiz/LogBlock/Actor.java +++ b/src/main/java/de/diddiz/LogBlock/Actor.java @@ -1,130 +1,182 @@ -package de.diddiz.LogBlock; - -import org.bukkit.entity.Entity; -import org.bukkit.entity.EntityType; -import org.bukkit.entity.Player; -import org.bukkit.projectiles.BlockProjectileSource; -import org.bukkit.projectiles.ProjectileSource; - -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.Collection; - -import static de.diddiz.util.BukkitUtils.entityName; -import org.bukkit.Bukkit; - -public class Actor { - - @Override - public int hashCode() { - int hash = 5; - hash = 79 * hash + (this.UUID != null ? this.UUID.hashCode() : 0); - return hash; - } - - @Override - public boolean equals(Object obj) { - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - final Actor other = (Actor) obj; - return ((this.UUID == null && other.UUID == null) || this.UUID.equals(other.UUID)); - } - - final String name; - final String UUID; - - public Actor(String name, String UUID) { - this.name = name; - this.UUID = UUID; - - } - - public Actor(String name, java.util.UUID UUID) { - this.name = name; - this.UUID = UUID.toString(); - - } - - public Actor(String name) { - this(name, generateUUID(name)); - } - - public Actor(ResultSet rs) throws SQLException { - this(rs.getString("playername"), rs.getString("UUID")); - } - - public String getName() { - return name; - } - - public String getUUID() { - return UUID; - } - - public static Actor actorFromEntity(Entity entity) { - if (entity instanceof Player) { - return new Actor(entityName(entity), entity.getUniqueId()); - } else { - return new Actor(entityName(entity)); - } - } - - public static Actor actorFromEntity(EntityType entity) { - return new Actor(entity.getName()); - } - - public static Actor actorFromProjectileSource(ProjectileSource psource) { - if (psource instanceof Entity) { - return actorFromEntity((Entity) psource); - } - if (psource instanceof BlockProjectileSource) { - return new Actor(((BlockProjectileSource) psource).getBlock().getType().toString()); - } else { - return new Actor(psource.toString()); - } - - } -/** - * Generate an Actor object from a String name, trying to guess if it's an online player - * and if so, setting the UUID accordingly. This only checks against currently online - * players and is a "best effort" attempt for use with the pre-UUID API - *

- * If you know something is an entity (player or otherwise) use the {@link #actorFromEntity(org.bukkit.entity.Entity) } - * or {@link #actorFromEntity(org.bukkit.entity.EntityType) } methods - *

- * If you know something is a server effect (like gravity) use {@link #Actor(java.lang.String)} - * @deprecated Only use this if you have a String of unknown origin - * - * @param actorName String of unknown origin - * @return - */ - public static Actor actorFromString(String actorName) { - Collection players = Bukkit.getServer().getOnlinePlayers(); - for (Player p : players) { - if (p.getName().equalsIgnoreCase(actorName)) { - return actorFromEntity(p); - } - } - // No player found online with that name, assuming non-player entity/effect - return new Actor(actorName); - } - - public static boolean isValidUUID(String uuid) { - try { - java.util.UUID.fromString(uuid); - return true; - } catch (IllegalArgumentException e) { - return false; - } - } - - public static String generateUUID(String name) { - return "log_" + name; - - } - -} +package de.diddiz.LogBlock; + +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.entity.Projectile; +import org.bukkit.projectiles.BlockProjectileSource; +import org.bukkit.projectiles.ProjectileSource; + +import static de.diddiz.LogBlock.util.BukkitUtils.entityName; + +import java.sql.ResultSet; +import java.sql.SQLException; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.block.Block; + +public class Actor { + + @Override + public int hashCode() { + return this.UUID != null ? this.UUID.hashCode() : 0; + } + + @Override + public boolean equals(Object obj) { + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final Actor other = (Actor) obj; + return (this.UUID == null) ? (other.UUID == null) : this.UUID.equals(other.UUID); + } + + final String name; + final String UUID; + final Location blockLocation; + final Entity entity; + + public Actor(String name, String UUID) { + this.name = name; + this.UUID = UUID == null ? "unknown" : (UUID.length() > 36 ? UUID.substring(0, 36) : UUID); + this.blockLocation = null; + this.entity = null; + } + + public Actor(String name, String UUID, Block block) { + this.name = name; + this.UUID = UUID == null ? "unknown" : (UUID.length() > 36 ? UUID.substring(0, 36) : UUID); + this.blockLocation = block == null ? null : block.getLocation(); + this.entity = null; + } + + public Actor(String name, java.util.UUID UUID) { + this.name = name; + this.UUID = UUID.toString(); + this.blockLocation = null; + this.entity = null; + } + + public Actor(String name, java.util.UUID UUID, Block block) { + this.name = name; + this.UUID = UUID.toString(); + this.blockLocation = block == null ? null : block.getLocation(); + this.entity = null; + } + + public Actor(String name, java.util.UUID UUID, Entity entity) { + this.name = name; + this.UUID = UUID.toString(); + this.blockLocation = null; + this.entity = entity; + } + + public Actor(String name) { + this(name, generateUUID(name)); + } + + public Actor(String name, Block block) { + this(name, generateUUID(name), block); + } + + public Actor(String name, Entity entity) { + this.name = name; + this.UUID = generateUUID(name); + this.blockLocation = null; + this.entity = entity; + } + + public Actor(ResultSet rs) throws SQLException { + this(rs.getString("playername"), rs.getString("UUID")); + } + + public String getName() { + return name; + } + + public String getUUID() { + return UUID; + } + + public Location getBlockLocation() { + return blockLocation; + } + + /** + * The acting entity object (if known) + */ + public Entity getEntity() { + return entity; + } + + public static Actor actorFromEntity(Entity entity) { + if (entity instanceof Player) { + return new Actor(entityName(entity), entity.getUniqueId(), entity); + } + if (entity instanceof Projectile) { + ProjectileSource shooter = ((Projectile) entity).getShooter(); + if (shooter != null) { + return actorFromProjectileSource(shooter); + } + } + return new Actor(entityName(entity), entity); + } + + @Deprecated + public static Actor actorFromEntity(EntityType entity) { + return new Actor(entity.name()); + } + + public static Actor actorFromProjectileSource(ProjectileSource psource) { + if (psource instanceof Entity) { + return actorFromEntity((Entity) psource); + } + if (psource instanceof BlockProjectileSource) { + return new Actor(((BlockProjectileSource) psource).getBlock().getType().toString()); + } else { + return new Actor(psource.toString()); + } + + } + + /** + * Generate an Actor object from a String name, trying to guess if it's an online player + * and if so, setting the UUID accordingly. This only checks against currently online + * players and is a "best effort" attempt for use with the pre-UUID API + *

+ * If you know something is an entity (player or otherwise) use the {@link #actorFromEntity(org.bukkit.entity.Entity) } + * or {@link #actorFromEntity(org.bukkit.entity.EntityType) } methods + *

+ * If you know something is a server effect (like gravity) use {@link #Actor(java.lang.String)} + * + * @deprecated Only use this if you have a String of unknown origin + * + * @param actorName + * String of unknown origin + * @return + */ + @Deprecated + public static Actor actorFromString(String actorName) { + Player p = Bukkit.getServer().getPlayerExact(actorName); + if (p != null) { + return actorFromEntity(p); + } + // No player found online with that name, assuming non-player entity/effect + return new Actor(actorName); + } + + public static boolean isValidUUID(String uuid) { + try { + java.util.UUID.fromString(uuid); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + public static String generateUUID(String name) { + return "log_" + name; + + } + +} diff --git a/src/main/java/de/diddiz/LogBlock/AutoClearLog.java b/src/main/java/de/diddiz/LogBlock/AutoClearLog.java index 0173ec8d..c7c28cf9 100644 --- a/src/main/java/de/diddiz/LogBlock/AutoClearLog.java +++ b/src/main/java/de/diddiz/LogBlock/AutoClearLog.java @@ -1,28 +1,32 @@ -package de.diddiz.LogBlock; - -import java.util.Arrays; -import java.util.logging.Level; - -import static de.diddiz.LogBlock.config.Config.autoClearLog; -import static org.bukkit.Bukkit.*; - -public class AutoClearLog implements Runnable { - private final LogBlock logblock; - - AutoClearLog(LogBlock logblock) { - this.logblock = logblock; - } - - @Override - public void run() { - final CommandsHandler handler = logblock.getCommandsHandler(); - for (final String paramStr : autoClearLog) { - try { - final QueryParams params = new QueryParams(logblock, getConsoleSender(), Arrays.asList(paramStr.split(" "))); - handler.new CommandClearLog(getServer().getConsoleSender(), params, false); - } catch (final Exception ex) { - getLogger().log(Level.SEVERE, "Failed to schedule auto ClearLog: ", ex); - } - } - } -} +package de.diddiz.LogBlock; + +import java.util.Arrays; +import java.util.logging.Level; + +import static de.diddiz.LogBlock.config.Config.autoClearLog; +import static org.bukkit.Bukkit.*; + +public class AutoClearLog implements Runnable { + private final LogBlock logblock; + + AutoClearLog(LogBlock logblock) { + this.logblock = logblock; + } + + @Override + public void run() { + final CommandsHandler handler = logblock.getCommandsHandler(); + for (final String paramStr : autoClearLog) { + if (!logblock.isCompletelyEnabled()) { + return; // do not try when plugin is disabled + } + try { + final QueryParams params = new QueryParams(logblock, getConsoleSender(), Arrays.asList(paramStr.split(" "))); + params.noForcedLimit = true; + handler.new CommandClearLog(getServer().getConsoleSender(), params, false); + } catch (final Exception ex) { + getLogger().log(Level.SEVERE, "Failed to schedule auto ClearLog: ", ex); + } + } + } +} diff --git a/src/main/java/de/diddiz/LogBlock/BlockChange.java b/src/main/java/de/diddiz/LogBlock/BlockChange.java index a5803df4..8cc33285 100644 --- a/src/main/java/de/diddiz/LogBlock/BlockChange.java +++ b/src/main/java/de/diddiz/LogBlock/BlockChange.java @@ -1,128 +1,272 @@ -package de.diddiz.LogBlock; - -import de.diddiz.LogBlock.config.Config; -import de.diddiz.util.BukkitUtils; -import org.bukkit.Location; -import org.bukkit.Material; - -import java.sql.ResultSet; -import java.sql.SQLException; - -import static de.diddiz.util.LoggingUtil.checkText; -import static de.diddiz.util.MaterialName.materialName; - -public class BlockChange implements LookupCacheElement { - public final long id, date; - public final Location loc; - public final Actor actor; - public final String playerName; - public final int replaced, type; - public final byte data; - public final String signtext; - public final ChestAccess ca; - - public BlockChange(long date, Location loc, Actor actor, int replaced, int type, byte data, String signtext, ChestAccess ca) { - id = 0; - this.date = date; - this.loc = loc; - this.actor = actor; - this.replaced = replaced; - this.type = type; - this.data = data; - this.signtext = checkText(signtext); - this.ca = ca; - this.playerName = actor == null ? null : actor.getName(); - } - - public BlockChange(ResultSet rs, QueryParams p) throws SQLException { - id = p.needId ? rs.getInt("id") : 0; - date = p.needDate ? rs.getTimestamp("date").getTime() : 0; - loc = p.needCoords ? new Location(p.world, rs.getInt("x"), rs.getInt("y"), rs.getInt("z")) : null; - actor = p.needPlayer ? new Actor(rs) : null; - playerName = p.needPlayer ? rs.getString("playername") : null; - replaced = p.needType ? rs.getInt("replaced") : 0; - type = p.needType ? rs.getInt("type") : 0; - data = p.needData ? rs.getByte("data") : (byte) 0; - signtext = p.needSignText ? rs.getString("signtext") : null; - ca = p.needChestAccess && rs.getShort("itemtype") != 0 && rs.getShort("itemamount") != 0 ? new ChestAccess(rs.getShort("itemtype"), rs.getShort("itemamount"), rs.getShort("itemdata")) : null; - } - - @Override - public String toString() { - final StringBuilder msg = new StringBuilder(); - if (date > 0) { - msg.append(Config.formatter.format(date)).append(" "); - } - if (actor != null) { - msg.append(actor.getName()).append(" "); - } - if (signtext != null) { - final String action = type == 0 ? "destroyed " : "created "; - if (!signtext.contains("\0")) { - msg.append(action).append(signtext); - } else { - msg.append(action).append(materialName(type != 0 ? type : replaced)).append(" [").append(signtext.replace("\0", "] [")).append("]"); - } - } else if (type == replaced) { - if (type == 0) { - msg.append("did an unspecified action"); - } else if (ca != null) { - if (ca.itemType == 0 || ca.itemAmount == 0) { - msg.append("looked inside ").append(materialName(type)); - } else if (ca.itemAmount < 0) { - msg.append("took ").append(-ca.itemAmount).append("x ").append(materialName(ca.itemType, ca.itemData)).append(" from ").append(materialName(type)); - } else { - msg.append("put ").append(ca.itemAmount).append("x ").append(materialName(ca.itemType, ca.itemData)).append(" into ").append(materialName(type)); - } - } else if (BukkitUtils.getContainerBlocks().contains(Material.getMaterial(type))) { - msg.append("opened ").append(materialName(type)); - } else if (type == 64 || type == 71) - // This is a problem that will have to be addressed in LB 2, - // there is no way to tell from the top half of the block if - // the door is opened or closed. - { - msg.append("moved ").append(materialName(type)); - } - // Trapdoor - else if (type == 96) { - msg.append((data < 8 || data > 11) ? "opened" : "closed").append(" ").append(materialName(type)); - } - // Fence gate - else if (type == 107) { - msg.append(data > 3 ? "opened" : "closed").append(" ").append(materialName(type)); - } else if (type == 69) { - msg.append("switched ").append(materialName(type)); - } else if (type == 77 || type == 143) { - msg.append("pressed ").append(materialName(type)); - } else if (type == 92) { - msg.append("ate a piece of ").append(materialName(type)); - } else if (type == 25 || type == 93 || type == 94 || type == 149 || type == 150) { - msg.append("changed ").append(materialName(type)); - } else if (type == 70 || type == 72 || type == 147 || type == 148) { - msg.append("stepped on ").append(materialName(type)); - } else if (type == 132) { - msg.append("ran into ").append(materialName(type)); - } - } else if (type == 0) { - msg.append("destroyed ").append(materialName(replaced, data)); - } else if (replaced == 0) { - msg.append("created ").append(materialName(type, data)); - } else { - msg.append("replaced ").append(materialName(replaced, (byte) 0)).append(" with ").append(materialName(type, data)); - } - if (loc != null) { - msg.append(" at ").append(loc.getBlockX()).append(":").append(loc.getBlockY()).append(":").append(loc.getBlockZ()); - } - return msg.toString(); - } - - @Override - public Location getLocation() { - return loc; - } - - @Override - public String getMessage() { - return toString(); - } -} +package de.diddiz.LogBlock; + +import static de.diddiz.LogBlock.util.ActionColor.CREATE; +import static de.diddiz.LogBlock.util.ActionColor.DESTROY; +import static de.diddiz.LogBlock.util.ActionColor.INTERACT; +import static de.diddiz.LogBlock.util.MessagingUtil.createTextComponentWithColor; +import static de.diddiz.LogBlock.util.MessagingUtil.prettyDate; +import static de.diddiz.LogBlock.util.MessagingUtil.prettyLocation; +import static de.diddiz.LogBlock.util.MessagingUtil.prettyMaterial; +import static de.diddiz.LogBlock.util.MessagingUtil.prettyState; +import static de.diddiz.LogBlock.util.TypeColor.DEFAULT; + +import de.diddiz.LogBlock.blockstate.BlockStateCodecs; +import de.diddiz.LogBlock.util.BukkitUtils; +import de.diddiz.LogBlock.util.ItemStackAndAmount; +import de.diddiz.LogBlock.util.Utils; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.logging.Level; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.TextComponent; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Note; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.Lightable; +import org.bukkit.block.data.Openable; +import org.bukkit.block.data.Powerable; +import org.bukkit.block.data.Waterlogged; +import org.bukkit.block.data.type.Candle; +import org.bukkit.block.data.type.Comparator; +import org.bukkit.block.data.type.DaylightDetector; +import org.bukkit.block.data.type.Lectern; +import org.bukkit.block.data.type.NoteBlock; +import org.bukkit.block.data.type.Repeater; +import org.bukkit.block.data.type.Sign; +import org.bukkit.block.data.type.Switch; +import org.bukkit.block.data.type.WallSign; + +public class BlockChange implements LookupCacheElement { + public final long id, date; + public final Location loc; + public final Actor actor; + public final String playerName; + public final int replacedMaterial, replacedData, typeMaterial, typeData; + public final byte[] replacedState, typeState; + public final ChestAccess ca; + + public BlockChange(long date, Location loc, Actor actor, int replaced, int replacedData, byte[] replacedState, int type, int typeData, byte[] typeState, ChestAccess ca) { + id = 0; + this.date = date; + this.loc = loc; + this.actor = actor; + this.replacedMaterial = replaced; + this.replacedData = replacedData; + this.replacedState = replacedState; + this.typeMaterial = type; + this.typeData = typeData; + this.typeState = typeState; + this.ca = ca; + this.playerName = actor == null ? null : actor.getName(); + } + + public BlockChange(ResultSet rs, QueryParams p) throws SQLException { + id = p.needId ? rs.getLong("id") : 0; + date = p.needDate ? rs.getTimestamp("date").getTime() : 0; + loc = p.needCoords ? new Location(p.world, rs.getInt("x"), rs.getInt("y"), rs.getInt("z")) : null; + actor = p.needPlayer ? new Actor(rs) : null; + playerName = p.needPlayer ? rs.getString("playername") : null; + replacedMaterial = p.needType ? rs.getInt("replaced") : 0; + replacedData = p.needType ? rs.getInt("replacedData") : -1; + typeMaterial = p.needType ? rs.getInt("type") : 0; + typeData = p.needType ? rs.getInt("typeData") : -1; + replacedState = p.needType ? rs.getBytes("replacedState") : null; + typeState = p.needType ? rs.getBytes("typeState") : null; + ChestAccess catemp = null; + if (p.needChestAccess) { + ItemStackAndAmount stack = Utils.loadItemStack(rs.getBytes("item")); + if (stack != null) { + catemp = new ChestAccess(stack, rs.getBoolean("itemremove"), rs.getInt("itemtype")); + } + } + ca = catemp; + } + + private BaseComponent getTypeDetails(BlockData type, byte[] typeState) { + return getTypeDetails(type, typeState, null, null); + } + + private BaseComponent getTypeDetails(BlockData type, byte[] typeState, BlockData oldType, byte[] oldTypeState) { + BaseComponent typeDetails = null; + + if (BlockStateCodecs.hasCodec(type.getMaterial())) { + try { + typeDetails = BlockStateCodecs.getChangesAsComponent(type.getMaterial(), Utils.deserializeYamlConfiguration(typeState), type.equals(oldType) ? Utils.deserializeYamlConfiguration(oldTypeState) : null); + } catch (Exception e) { + LogBlock.getInstance().getLogger().log(Level.SEVERE, "Could not parse BlockState for " + type.getMaterial(), e); + } + } + + if (typeDetails == null) { + return new TextComponent(""); + } else { + TextComponent component = new TextComponent(" "); + component.addExtra(typeDetails); + return component; + } + } + + @Override + public String toString() { + return BaseComponent.toPlainText(getLogMessage(-1)); + } + + @Override + public BaseComponent getLogMessage(int entry) { + TextComponent msg = new TextComponent(); + if (date > 0) { + msg.addExtra(prettyDate(date)); + msg.addExtra(" "); + } + if (actor != null) { + msg.addExtra(actor.getName()); + msg.addExtra(" "); + } + BlockData type = getBlockSet(); + BlockData replaced = getBlockReplaced(); + if (type == null || replaced == null) { + msg.addExtra("did an unknown block modification"); + return msg; + } + + // Process type details once for later use. + BaseComponent typeDetails = getTypeDetails(type, typeState, replaced, replacedState); + BaseComponent replacedDetails = getTypeDetails(replaced, replacedState); + + if (type.getMaterial().equals(replaced.getMaterial()) || (type.getMaterial() == Material.CAKE && BukkitUtils.isCandleCake(replaced.getMaterial()))) { + if (BukkitUtils.isEmpty(type.getMaterial())) { + msg.addExtra(createTextComponentWithColor("did an unspecified action", INTERACT.getColor())); + } else if (ca != null) { + if (ca.itemStack == null) { + msg.addExtra(createTextComponentWithColor("looked inside ", INTERACT.getColor())); + msg.addExtra(prettyMaterial(type)); + } else if (ca.remove) { + msg.addExtra(createTextComponentWithColor("took ", DESTROY.getColor())); + msg.addExtra(BukkitUtils.toString(ca.itemStack)); + msg.addExtra(createTextComponentWithColor(" from ", DESTROY.getColor())); + msg.addExtra(prettyMaterial(type)); + } else { + msg.addExtra(createTextComponentWithColor("put ", CREATE.getColor())); + msg.addExtra(BukkitUtils.toString(ca.itemStack)); + msg.addExtra(createTextComponentWithColor(" into ", CREATE.getColor())); + msg.addExtra(prettyMaterial(type)); + } + } else if (type instanceof Waterlogged && ((Waterlogged) type).isWaterlogged() != ((Waterlogged) replaced).isWaterlogged()) { + if (((Waterlogged) type).isWaterlogged()) { + msg.addExtra(createTextComponentWithColor("waterlogged ", CREATE.getColor())); + msg.addExtra(prettyMaterial(type)); + } else { + msg.addExtra(createTextComponentWithColor("dried ", DESTROY.getColor())); + msg.addExtra(prettyMaterial(type)); + } + } else if (BukkitUtils.isContainerBlock(type.getMaterial())) { + msg.addExtra(createTextComponentWithColor("opened ", INTERACT.getColor())); + msg.addExtra(prettyMaterial(type)); + } else if (type instanceof Openable && ((Openable) type).isOpen() != ((Openable) replaced).isOpen()) { + // Door, Trapdoor, Fence gate + msg.addExtra(createTextComponentWithColor(((Openable) type).isOpen() ? "opened " : "closed ", INTERACT.getColor())); + msg.addExtra(prettyMaterial(type)); + } else if (type.getMaterial() == Material.LEVER && ((Switch) type).isPowered() != ((Switch) replaced).isPowered()) { + msg.addExtra(createTextComponentWithColor("switched ", INTERACT.getColor())); + msg.addExtra(prettyMaterial(type)); + msg.addExtra(prettyState(((Switch) type).isPowered() ? " on" : " off")); + } else if (type instanceof Switch && ((Switch) type).isPowered() != ((Switch) replaced).isPowered()) { + msg.addExtra(createTextComponentWithColor("pressed ", INTERACT.getColor())); + msg.addExtra(prettyMaterial(type)); + } else if (type.getMaterial() == Material.CAKE) { + msg.addExtra(createTextComponentWithColor("ate a piece of ", DESTROY.getColor())); + msg.addExtra(prettyMaterial(type)); + } else if (type.getMaterial() == Material.NOTE_BLOCK) { + Note note = ((NoteBlock) type).getNote(); + msg.addExtra(createTextComponentWithColor("set ", INTERACT.getColor())); + msg.addExtra(prettyMaterial(type)); + msg.addExtra(" to "); + msg.addExtra(prettyState(note.getTone().name() + (note.isSharped() ? "#" : ""))); + } else if (type.getMaterial() == Material.REPEATER) { + msg.addExtra(createTextComponentWithColor("set ", INTERACT.getColor())); + msg.addExtra(prettyMaterial(type)); + msg.addExtra(" to "); + msg.addExtra(prettyState(((Repeater) type).getDelay())); + msg.addExtra(createTextComponentWithColor(" ticks delay", DEFAULT.getColor())); + } else if (type.getMaterial() == Material.COMPARATOR) { + msg.addExtra(createTextComponentWithColor("set ", INTERACT.getColor())); + msg.addExtra(prettyMaterial(type)); + msg.addExtra(" to "); + msg.addExtra(prettyState(((Comparator) type).getMode())); + } else if (type.getMaterial() == Material.DAYLIGHT_DETECTOR) { + msg.addExtra(createTextComponentWithColor("set ", INTERACT.getColor())); + msg.addExtra(prettyMaterial(type)); + msg.addExtra(" to "); + msg.addExtra(prettyState(((DaylightDetector) type).isInverted() ? "inverted" : "normal")); + } else if (type instanceof Lectern) { + msg.addExtra(createTextComponentWithColor("changed the book on a ", INTERACT.getColor())); + msg.addExtra(prettyMaterial(type)); + msg.addExtra(" to"); + msg.addExtra(prettyState(typeDetails)); + } else if (type instanceof Powerable) { + msg.addExtra(createTextComponentWithColor("stepped on ", INTERACT.getColor())); + msg.addExtra(prettyMaterial(type)); + } else if (type.getMaterial() == Material.TRIPWIRE) { + msg.addExtra(createTextComponentWithColor("ran into ", INTERACT.getColor())); + msg.addExtra(prettyMaterial(type)); + } else if (type instanceof Sign || type instanceof WallSign) { + msg.addExtra(createTextComponentWithColor("edited a ", CREATE.getColor())); + msg.addExtra(prettyMaterial(type)); + msg.addExtra(createTextComponentWithColor(" to", CREATE.getColor())); + msg.addExtra(prettyState(typeDetails)); + } else if (type instanceof Candle && ((Candle) type).getCandles() != ((Candle) replaced).getCandles()) { + msg.addExtra(createTextComponentWithColor("added a candle to ", CREATE.getColor())); + msg.addExtra(prettyMaterial(type)); + } else if ((type instanceof Candle || BukkitUtils.isCandleCake(type.getMaterial())) && ((Lightable) type).isLit() != ((Lightable) replaced).isLit()) { + if (((Lightable) type).isLit()) { + msg.addExtra(createTextComponentWithColor("lit a ", CREATE.getColor())); + msg.addExtra(prettyMaterial(type)); + } else { + msg.addExtra(createTextComponentWithColor("extinguished a ", CREATE.getColor())); + msg.addExtra(prettyMaterial(type)); + } + } else { + msg.addExtra(createTextComponentWithColor("replaced ", CREATE.getColor())); + msg.addExtra(prettyMaterial(replaced)); + msg.addExtra(prettyState(replacedDetails)); + msg.addExtra(createTextComponentWithColor(" with ", CREATE.getColor())); + msg.addExtra(prettyMaterial(type)); + msg.addExtra(prettyState(typeDetails)); + } + } else if (BukkitUtils.isEmpty(type.getMaterial())) { + msg.addExtra(createTextComponentWithColor("destroyed ", DESTROY.getColor())); + msg.addExtra(prettyMaterial(replaced)); + msg.addExtra(prettyState(replacedDetails)); + } else if (BukkitUtils.isEmpty(replaced.getMaterial())) { + msg.addExtra(createTextComponentWithColor("created ", CREATE.getColor())); + msg.addExtra(prettyMaterial(type)); + msg.addExtra(prettyState(typeDetails)); + } else { + msg.addExtra(createTextComponentWithColor("replaced ", CREATE.getColor())); + msg.addExtra(prettyMaterial(replaced)); + msg.addExtra(prettyState(replacedDetails)); + msg.addExtra(createTextComponentWithColor(" with ", CREATE.getColor())); + msg.addExtra(prettyMaterial(type)); + msg.addExtra(prettyState(typeDetails)); + } + if (loc != null) { + msg.addExtra(" at "); + msg.addExtra(prettyLocation(loc, entry)); + } + return msg; + } + + public BlockData getBlockReplaced() { + return MaterialConverter.getBlockData(replacedMaterial, replacedData); + } + + public BlockData getBlockSet() { + return MaterialConverter.getBlockData(typeMaterial, typeData); + } + + @Override + public Location getLocation() { + return loc; + } +} diff --git a/src/main/java/de/diddiz/LogBlock/ChatMessage.java b/src/main/java/de/diddiz/LogBlock/ChatMessage.java index 097f56bb..e7ccc1c7 100644 --- a/src/main/java/de/diddiz/LogBlock/ChatMessage.java +++ b/src/main/java/de/diddiz/LogBlock/ChatMessage.java @@ -1,40 +1,57 @@ -package de.diddiz.LogBlock; - -import org.bukkit.Location; - -import java.sql.ResultSet; -import java.sql.SQLException; - -import static de.diddiz.util.LoggingUtil.checkText; - -public class ChatMessage implements LookupCacheElement { - final long id, date; - final String playerName, message; - final Actor player; - - public ChatMessage(Actor player, String message) { - id = 0; - date = System.currentTimeMillis() / 1000; - this.player = player; - this.message = checkText(message); - this.playerName = player == null ? null : player.getName(); - } - - public ChatMessage(ResultSet rs, QueryParams p) throws SQLException { - id = p.needId ? rs.getInt("id") : 0; - date = p.needDate ? rs.getTimestamp("date").getTime() : 0; - player = p.needPlayer ? new Actor(rs) : null; - playerName = p.needPlayer ? rs.getString("playername") : null; - message = p.needMessage ? rs.getString("message") : null; - } - - @Override - public Location getLocation() { - return null; - } - - @Override - public String getMessage() { - return (player != null ? "<" + player.getName() + "> " : "") + (message != null ? message : ""); - } -} +package de.diddiz.LogBlock; + +import static de.diddiz.LogBlock.util.LoggingUtil.checkText; +import static de.diddiz.LogBlock.util.MessagingUtil.brackets; +import static de.diddiz.LogBlock.util.MessagingUtil.prettyDate; + +import de.diddiz.LogBlock.util.MessagingUtil; +import de.diddiz.LogBlock.util.MessagingUtil.BracketType; +import java.sql.ResultSet; +import java.sql.SQLException; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.TextComponent; +import org.bukkit.Location; + +public class ChatMessage implements LookupCacheElement { + final long id, date; + final String playerName, message; + final Actor player; + + public ChatMessage(Actor player, String message) { + id = 0; + date = System.currentTimeMillis() / 1000; + this.player = player; + this.message = checkText(message); + this.playerName = player == null ? null : player.getName(); + } + + public ChatMessage(ResultSet rs, QueryParams p) throws SQLException { + id = p.needId ? rs.getLong("id") : 0; + date = p.needDate ? rs.getTimestamp("date").getTime() : 0; + player = p.needPlayer ? new Actor(rs) : null; + playerName = p.needPlayer ? rs.getString("playername") : null; + message = p.needMessage ? rs.getString("message") : null; + } + + @Override + public Location getLocation() { + return null; + } + + @Override + public BaseComponent getLogMessage(int entry) { + TextComponent msg = new TextComponent(); + if (date > 0) { + msg.addExtra(prettyDate(date)); + msg.addExtra(" "); + } + if (playerName != null) { + msg.addExtra(brackets(BracketType.ANGLE, MessagingUtil.createTextComponentWithColor(playerName, net.md_5.bungee.api.ChatColor.WHITE))); + msg.addExtra(" "); + } + if (message != null) { + msg.addExtra(TextComponent.fromLegacy(message)); + } + return msg; + } +} diff --git a/src/main/java/de/diddiz/LogBlock/ChestAccess.java b/src/main/java/de/diddiz/LogBlock/ChestAccess.java index 10260e5b..92480b16 100644 --- a/src/main/java/de/diddiz/LogBlock/ChestAccess.java +++ b/src/main/java/de/diddiz/LogBlock/ChestAccess.java @@ -1,11 +1,15 @@ -package de.diddiz.LogBlock; - -public class ChestAccess { - final short itemType, itemAmount, itemData; - - public ChestAccess(short itemType, short itemAmount, short itemData) { - this.itemType = itemType; - this.itemAmount = itemAmount; - this.itemData = itemData >= 0 ? itemData : 0; - } -} +package de.diddiz.LogBlock; + +import de.diddiz.LogBlock.util.ItemStackAndAmount; + +public class ChestAccess { + public final ItemStackAndAmount itemStack; + public final boolean remove; + public final int itemType; + + public ChestAccess(ItemStackAndAmount itemStack, boolean remove, int itemType) { + this.itemStack = itemStack; + this.remove = remove; + this.itemType = itemType; + } +} diff --git a/src/main/java/de/diddiz/LogBlock/CommandsHandler.java b/src/main/java/de/diddiz/LogBlock/CommandsHandler.java index 78bbf2ac..2a57c5ae 100755 --- a/src/main/java/de/diddiz/LogBlock/CommandsHandler.java +++ b/src/main/java/de/diddiz/LogBlock/CommandsHandler.java @@ -1,864 +1,1067 @@ -package de.diddiz.LogBlock; - -import de.diddiz.LogBlock.QueryParams.BlockChangeType; -import de.diddiz.LogBlock.QueryParams.Order; -import de.diddiz.LogBlock.QueryParams.SummarizationMode; -import de.diddiz.LogBlock.config.Config; -import de.diddiz.LogBlock.config.WorldConfig; -import de.diddiz.LogBlockQuestioner.LogBlockQuestioner; -import de.diddiz.util.Block; -import org.bukkit.ChatColor; -import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; -import org.bukkit.scheduler.BukkitScheduler; - -import java.io.Closeable; -import java.io.File; -import java.io.FileWriter; -import java.sql.Connection; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.logging.Level; - -import static de.diddiz.LogBlock.Session.getSession; -import static de.diddiz.LogBlock.config.Config.*; -import static de.diddiz.util.BukkitUtils.giveTool; -import static de.diddiz.util.BukkitUtils.saveSpawnHeight; -import static de.diddiz.util.Utils.isInt; -import static de.diddiz.util.Utils.listing; -import static org.bukkit.Bukkit.getLogger; -import static org.bukkit.Bukkit.getServer; - -public class CommandsHandler implements CommandExecutor { - private final LogBlock logblock; - private final BukkitScheduler scheduler; - private final LogBlockQuestioner questioner; - - CommandsHandler(LogBlock logblock) { - this.logblock = logblock; - scheduler = logblock.getServer().getScheduler(); - questioner = (LogBlockQuestioner) logblock.getServer().getPluginManager().getPlugin("LogBlockQuestioner"); - } - - @Override - public boolean onCommand(CommandSender sender, Command cmd, String commandLabel, String[] args) { - try { - if (args.length == 0) { - sender.sendMessage(ChatColor.LIGHT_PURPLE + "LogBlock v" + logblock.getDescription().getVersion() + " by DiddiZ"); - sender.sendMessage(ChatColor.LIGHT_PURPLE + "Type /lb help for help"); - } else { - final String command = args[0].toLowerCase(); - if (command.equals("help")) { - sender.sendMessage(ChatColor.DARK_AQUA + "LogBlock Help:"); - sender.sendMessage(ChatColor.GOLD + "For the commands list type '/lb commands'"); - sender.sendMessage(ChatColor.GOLD + "For the parameters list type '/lb params'"); - sender.sendMessage(ChatColor.GOLD + "For the list of permissions you got type '/lb permissions'"); - } else if (command.equals("commands")) { - sender.sendMessage(ChatColor.DARK_AQUA + "LogBlock Commands:"); - sender.sendMessage(ChatColor.GOLD + "/lb tool -- Gives you the lb tool"); - sender.sendMessage(ChatColor.GOLD + "/lb tool [on|off] -- Enables/Disables tool"); - sender.sendMessage(ChatColor.GOLD + "/lb tool [params] -- Sets the tool lookup query"); - sender.sendMessage(ChatColor.GOLD + "/lb tool default -- Sets the tool lookup query to default"); - sender.sendMessage(ChatColor.GOLD + "/lb toolblock -- Analog to tool"); - sender.sendMessage(ChatColor.GOLD + "/lb hide -- Hides you from log"); - sender.sendMessage(ChatColor.GOLD + "/lb rollback [params] -- Rollback"); - sender.sendMessage(ChatColor.GOLD + "/lb redo [params] -- Redo"); - sender.sendMessage(ChatColor.GOLD + "/lb tp [params] -- Teleports you to the location of griefing"); - sender.sendMessage(ChatColor.GOLD + "/lb writelogfile [params] -- Writes a log file"); - sender.sendMessage(ChatColor.GOLD + "/lb lookup [params] -- Lookup"); - sender.sendMessage(ChatColor.GOLD + "/lb prev|next -- Browse lookup result pages"); - sender.sendMessage(ChatColor.GOLD + "/lb page -- Shows a specific lookup result page"); - sender.sendMessage(ChatColor.GOLD + "/lb me -- Displays your stats"); - sender.sendMessage(ChatColor.GOLD + "Look at github.com/LogBlock/LogBlock/wiki/Commands for the full commands reference"); - } else if (command.equals("params")) { - sender.sendMessage(ChatColor.DARK_AQUA + "LogBlock Query Parameters:"); - sender.sendMessage(ChatColor.GOLD + "Use doublequotes to escape a keyword: world \"world\""); - sender.sendMessage(ChatColor.GOLD + "player [name1] -- List of players"); - sender.sendMessage(ChatColor.GOLD + "block [type1] -- List of block types"); - sender.sendMessage(ChatColor.GOLD + "created, destroyed -- Show only created/destroyed blocks"); - sender.sendMessage(ChatColor.GOLD + "chestaccess -- Show only chest accesses"); - sender.sendMessage(ChatColor.GOLD + "area -- Area around you"); - sender.sendMessage(ChatColor.GOLD + "selection, sel -- Inside current WorldEdit selection"); - sender.sendMessage(ChatColor.GOLD + "world [worldname] -- Changes the world"); - sender.sendMessage(ChatColor.GOLD + "time [number] [minutes|hours|days] -- Limits time"); - sender.sendMessage(ChatColor.GOLD + "since -- Limits time to a fixed point"); - sender.sendMessage(ChatColor.GOLD + "before -- Affects only blocks before a fixed time"); - sender.sendMessage(ChatColor.GOLD + "limit -- Limits the result to count of rows"); - sender.sendMessage(ChatColor.GOLD + "sum [none|blocks|players] -- Sums the result"); - sender.sendMessage(ChatColor.GOLD + "asc, desc -- Changes the order of the displayed log"); - sender.sendMessage(ChatColor.GOLD + "coords -- Shows coordinates for each block"); - sender.sendMessage(ChatColor.GOLD + "silent -- Displays lesser messages"); - } else if (command.equals("permissions")) { - sender.sendMessage(ChatColor.DARK_AQUA + "You've got the following permissions:"); - for (final String permission : new String[]{"me", "lookup", "tp", "rollback", "clearlog", "hide", "ignoreRestrictions", "spawnTools"}) { - if (logblock.hasPermission(sender, "logblock." + permission)) { - sender.sendMessage(ChatColor.GOLD + "logblock." + permission); - } - } - for (final Tool tool : toolsByType.values()) { - if (logblock.hasPermission(sender, "logblock.tools." + tool.name)) { - sender.sendMessage(ChatColor.GOLD + "logblock.tools." + tool.name); - } - } - } else if (command.equals("logging")) { - if (logblock.hasPermission(sender, "logblock.lookup")) { - World world = null; - if (args.length > 1) { - world = getServer().getWorld(args[1]); - } else if (sender instanceof Player) { - world = ((Player) sender).getWorld(); - } - if (world != null) { - final WorldConfig wcfg = getWorldConfig(world.getName()); - if (wcfg != null) { - sender.sendMessage(ChatColor.DARK_AQUA + "Currently logging in " + world.getName() + ":"); - final List logging = new ArrayList(); - for (final Logging l : Logging.values()) { - if (wcfg.isLogging(l)) { - logging.add(l.toString()); - } - } - sender.sendMessage(ChatColor.GOLD + listing(logging, ", ", " and ")); - } else { - sender.sendMessage(ChatColor.RED + "World not logged: '" + world.getName() + "'"); - sender.sendMessage(ChatColor.LIGHT_PURPLE + "Make the world name is listed at loggedWorlds in config. World names are case sensitive and must contains the path (if any), exactly like in the message above."); - } - } else { - sender.sendMessage(ChatColor.RED + "No world specified"); - } - } else { - sender.sendMessage(ChatColor.RED + "You aren't allowed to do this."); - } - } else if (toolsByName.get(command) != null) { - final Tool tool = toolsByName.get(command); - if (logblock.hasPermission(sender, "logblock.tools." + tool.name)) { - if (sender instanceof Player) { - final Player player = (Player) sender; - final Session session = Session.getSession(player.getName()); - final ToolData toolData = session.toolData.get(tool); - if (args.length == 1) { - if (logblock.hasPermission(player, "logblock.spawnTools")) { - giveTool(player, tool.item); - session.toolData.get(tool).enabled = true; - } else { - sender.sendMessage(ChatColor.RED + "You aren't allowed to do this."); - } - } else if (args[1].equalsIgnoreCase("enable") || args[1].equalsIgnoreCase("on")) { - toolData.enabled = true; - player.sendMessage(ChatColor.GREEN + "Tool enabled."); - } else if (args[1].equalsIgnoreCase("disable") || args[1].equalsIgnoreCase("off")) { - toolData.enabled = false; - player.getInventory().removeItem(new ItemStack(tool.item, 1)); - player.sendMessage(ChatColor.GREEN + "Tool disabled."); - } else if (args[1].equalsIgnoreCase("mode")) { - if (args.length == 3) { - final ToolMode mode; - try { - mode = ToolMode.valueOf(args[2].toUpperCase()); - } catch (final IllegalArgumentException ex) { - sender.sendMessage(ChatColor.RED + "Can't find mode " + args[2]); - return true; - } - if (logblock.hasPermission(player, mode.getPermission())) { - toolData.mode = mode; - sender.sendMessage(ChatColor.GREEN + "Tool mode set to " + args[2]); - } else { - sender.sendMessage(ChatColor.RED + "You aren't allowed to use mode " + args[2]); - } - } else { - player.sendMessage(ChatColor.RED + "No mode specified"); - } - } else if (args[1].equalsIgnoreCase("default")) { - toolData.params = tool.params.clone(); - toolData.mode = tool.mode; - sender.sendMessage(ChatColor.GREEN + "Tool set to default."); - } else if (logblock.hasPermission(player, "logblock.lookup")) { - try { - final QueryParams params = tool.params.clone(); - params.parseArgs(sender, argsToList(args, 1)); - toolData.params = params; - sender.sendMessage(ChatColor.GREEN + "Set tool query to: " + params.getTitle()); - } catch (final Exception ex) { - sender.sendMessage(ChatColor.RED + ex.getMessage()); - } - } else { - sender.sendMessage(ChatColor.RED + "You aren't allowed to do this."); - } - } else { - sender.sendMessage(ChatColor.RED + "You have to be a player."); - } - } else { - sender.sendMessage(ChatColor.RED + "You aren't allowed to do this."); - } - } else if (command.equals("hide")) { - if (sender instanceof Player) { - if (logblock.hasPermission(sender, "logblock.hide")) { - if (args.length == 2) { - if (args[1].equalsIgnoreCase("on")) { - Consumer.hide((Player) sender); - sender.sendMessage(ChatColor.GREEN + "You are now hidden and aren't logged. Type /lb hide to unhide."); - } else if (args[1].equalsIgnoreCase("off")) { - Consumer.unHide((Player) sender); - sender.sendMessage(ChatColor.GREEN + "You aren't hidden any longer."); - } - } else { - if (Consumer.toggleHide((Player) sender)) { - sender.sendMessage(ChatColor.GREEN + "You are now hidden and aren't logged. Type '/lb hide' again to unhide."); - } else { - sender.sendMessage(ChatColor.GREEN + "You aren't hidden any longer."); - } - } - } else { - sender.sendMessage(ChatColor.RED + "You aren't allowed to do this."); - } - } else { - sender.sendMessage(ChatColor.RED + "You have to be a player."); - } - } else if (command.equals("page")) { - if (args.length == 2 && isInt(args[1])) { - showPage(sender, Integer.valueOf(args[1])); - } else { - sender.sendMessage(ChatColor.RED + "You have to specify a page"); - } - } else if (command.equals("next") || command.equals("+")) { - showPage(sender, getSession(sender).page + 1); - } else if (command.equals("prev") || command.equals("-")) { - showPage(sender, getSession(sender).page - 1); - } else if (args[0].equalsIgnoreCase("savequeue")) { - if (logblock.hasPermission(sender, "logblock.rollback")) { - new CommandSaveQueue(sender, null, true); - } else { - sender.sendMessage(ChatColor.RED + "You aren't allowed to do this."); - } - } else if (args[0].equalsIgnoreCase("queuesize")) { - if (logblock.hasPermission(sender, "logblock.rollback")) { - sender.sendMessage("Current queue size: " + logblock.getConsumer().getQueueSize()); - } else { - sender.sendMessage(ChatColor.RED + "You aren't allowed to do this."); - } - } else if (command.equals("rollback") || command.equals("undo") || command.equals("rb")) { - if (logblock.hasPermission(sender, "logblock.rollback")) { - final QueryParams params = new QueryParams(logblock); - params.since = defaultTime; - params.bct = BlockChangeType.ALL; - params.parseArgs(sender, argsToList(args, 1)); - new CommandRollback(sender, params, true); - } else { - sender.sendMessage(ChatColor.RED + "You aren't allowed to do this."); - } - } else if (command.equals("redo")) { - if (logblock.hasPermission(sender, "logblock.rollback")) { - final QueryParams params = new QueryParams(logblock); - params.since = defaultTime; - params.bct = BlockChangeType.ALL; - params.parseArgs(sender, argsToList(args, 1)); - new CommandRedo(sender, params, true); - } else { - sender.sendMessage(ChatColor.RED + "You aren't allowed to do this."); - } - } else if (command.equals("me")) { - if (sender instanceof Player) { - if (logblock.hasPermission(sender, "logblock.me")) { - final Player player = (Player) sender; - if (Config.isLogged(player.getWorld())) { - final QueryParams params = new QueryParams(logblock); - params.setPlayer(player.getName()); - params.world = player.getWorld(); - player.sendMessage("Total block changes: " + logblock.getCount(params)); - params.sum = SummarizationMode.TYPES; - new CommandLookup(sender, params, true); - } else { - sender.sendMessage(ChatColor.RED + "This world isn't logged"); - } - } else { - sender.sendMessage(ChatColor.RED + "You aren't allowed to do this."); - } - } else { - sender.sendMessage(ChatColor.RED + "You have to be a player."); - } - } else if (command.equals("writelogfile")) { - if (logblock.hasPermission(sender, "logblock.rollback")) { - final QueryParams params = new QueryParams(logblock); - params.limit = -1; - params.bct = BlockChangeType.ALL; - params.parseArgs(sender, argsToList(args, 1)); - new CommandWriteLogFile(sender, params, true); - } else { - sender.sendMessage(ChatColor.RED + "You aren't allowed to do this"); - } - } else if (command.equals("clearlog")) { - if (logblock.hasPermission(sender, "logblock.clearlog")) { - final QueryParams params = new QueryParams(logblock, sender, argsToList(args, 1)); - params.bct = BlockChangeType.ALL; - params.limit = -1; - new CommandClearLog(sender, params, true); - } else { - sender.sendMessage(ChatColor.RED + "You aren't allowed to do this"); - } - } else if (command.equals("tp")) { - if (sender instanceof Player) { - if (logblock.hasPermission(sender, "logblock.tp")) { - if (args.length == 2 || isInt(args[1])) { - final int pos = Integer.parseInt(args[1]) - 1; - final Player player = (Player) sender; - final Session session = getSession(player); - if (session.lookupCache != null) { - if (pos >= 0 && pos < session.lookupCache.length) { - final Location loc = session.lookupCache[pos].getLocation(); - if (loc != null) { - player.teleport(new Location(loc.getWorld(), loc.getX() + 0.5, saveSpawnHeight(loc), loc.getZ() + 0.5, player.getLocation().getYaw(), 90)); - player.sendMessage(ChatColor.LIGHT_PURPLE + "Teleported to " + loc.getBlockX() + ":" + loc.getBlockY() + ":" + loc.getBlockZ()); - } else { - sender.sendMessage(ChatColor.RED + "There is no location associated with that. Did you forget coords parameter?"); - } - } else { - sender.sendMessage(ChatColor.RED + "'" + args[1] + " is out of range"); - } - } else { - sender.sendMessage(ChatColor.RED + "You havn't done a lookup yet"); - } - } else { - new CommandTeleport(sender, new QueryParams(logblock, sender, argsToList(args, 1)), true); - } - } else { - sender.sendMessage(ChatColor.RED + "You aren't allowed to do this"); - } - } else { - sender.sendMessage(ChatColor.RED + "You have to be a player."); - } - } else if (command.equals("lookup") || QueryParams.isKeyWord(args[0])) { - if (logblock.hasPermission(sender, "logblock.lookup")) { - final List argsList = new ArrayList(Arrays.asList(args)); - if (command.equals("lookup")) { - argsList.remove(0); - } - new CommandLookup(sender, new QueryParams(logblock, sender, argsList), true); - } else { - sender.sendMessage(ChatColor.RED + "You aren't allowed to do this"); - } - } else { - sender.sendMessage(ChatColor.RED + "Unknown command '" + args[0] + "'"); - } - } - } catch (final IllegalArgumentException ex) { - sender.sendMessage(ChatColor.RED + ex.getMessage()); - } catch (final ArrayIndexOutOfBoundsException ex) { - sender.sendMessage(ChatColor.RED + "Not enough arguments given"); - } catch (final Exception ex) { - sender.sendMessage(ChatColor.RED + "Error, check server.log"); - getLogger().log(Level.WARNING, "Exception in commands handler: ", ex); - } - return true; - } - - private static void showPage(CommandSender sender, int page) { - final Session session = getSession(sender); - if (session.lookupCache != null && session.lookupCache.length > 0) { - final int startpos = (page - 1) * linesPerPage; - if (page > 0 && startpos <= session.lookupCache.length - 1) { - final int stoppos = startpos + linesPerPage >= session.lookupCache.length ? session.lookupCache.length - 1 : startpos + linesPerPage - 1; - final int numberOfPages = (int) Math.ceil(session.lookupCache.length / (double) linesPerPage); - if (numberOfPages != 1) { - sender.sendMessage(ChatColor.DARK_AQUA + "Page " + page + "/" + numberOfPages); - } - for (int i = startpos; i <= stoppos; i++) { - sender.sendMessage(ChatColor.GOLD + (session.lookupCache[i].getLocation() != null ? "(" + (i + 1) + ") " : "") + session.lookupCache[i].getMessage()); - } - session.page = page; - } else { - sender.sendMessage(ChatColor.RED + "There isn't a page '" + page + "'"); - } - } else { - sender.sendMessage(ChatColor.RED + "No blocks in lookup cache"); - } - } - - private boolean checkRestrictions(CommandSender sender, QueryParams params) { - if (sender.isOp() || logblock.hasPermission(sender, "logblock.ignoreRestrictions")) { - return true; - } - if (rollbackMaxTime > 0 && (params.before > 0 || params.since > rollbackMaxTime)) { - sender.sendMessage(ChatColor.RED + "You are not allowed to rollback more than " + rollbackMaxTime + " minutes"); - return false; - } - if (rollbackMaxArea > 0 && (params.sel == null && params.loc == null || params.radius > rollbackMaxArea || params.sel != null && (params.sel.getSelection().getLength() > rollbackMaxArea || params.sel.getSelection().getWidth() > rollbackMaxArea))) { - sender.sendMessage(ChatColor.RED + "You are not allowed to rollback an area larger than " + rollbackMaxArea + " blocks"); - return false; - } - return true; - } - - public abstract class AbstractCommand implements Runnable, Closeable { - protected CommandSender sender; - protected QueryParams params; - protected Connection conn = null; - protected Statement state = null; - protected ResultSet rs = null; - - protected AbstractCommand(CommandSender sender, QueryParams params, boolean async) throws Exception { - this.sender = sender; - this.params = params; - if (async) { - if (scheduler.scheduleAsyncDelayedTask(logblock, this) == -1) { - throw new Exception("Failed to schedule the command"); - } - } else { - run(); - } - } - - @Override - public final void close() { - try { - if (conn != null) { - conn.close(); - } - if (state != null) { - state.close(); - } - if (rs != null) { - rs.close(); - } - } catch (final SQLException ex) { - getLogger().log(Level.SEVERE, "[CommandsHandler] SQL exception on close", ex); - } - } - } - - public class CommandLookup extends AbstractCommand { - public CommandLookup(CommandSender sender, QueryParams params, boolean async) throws Exception { - super(sender, params, async); - } - - @Override - public void run() { - try { - if (params.bct == BlockChangeType.CHAT) { - params.needDate = true; - params.needPlayer = true; - params.needMessage = true; - } else if (params.bct == BlockChangeType.KILLS) { - params.needDate = true; - params.needPlayer = true; - params.needKiller = true; - params.needVictim = true; - params.needWeapon = true; - } else { - params.needDate = true; - params.needType = true; - params.needData = true; - params.needPlayer = true; - if (params.types.isEmpty() || Block.inList(params.types, 63) || Block.inList(params.types, 68)) { - params.needSignText = true; - } - if (params.bct == BlockChangeType.CHESTACCESS || params.bct == BlockChangeType.ALL) { - params.needChestAccess = true; - } - } - conn = logblock.getConnection(); - if (conn == null) { - sender.sendMessage(ChatColor.RED + "MySQL connection lost"); - return; - } - state = conn.createStatement(); - rs = executeQuery(state, params.getQuery()); - sender.sendMessage(ChatColor.DARK_AQUA + params.getTitle() + ":"); - if (rs.next()) { - rs.beforeFirst(); - final List blockchanges = new ArrayList(); - final LookupCacheElementFactory factory = new LookupCacheElementFactory(params, sender instanceof Player ? 2 / 3f : 1); - while (rs.next()) { - blockchanges.add(factory.getLookupCacheElement(rs)); - } - getSession(sender).lookupCache = blockchanges.toArray(new LookupCacheElement[blockchanges.size()]); - if (blockchanges.size() > linesPerPage) { - sender.sendMessage(ChatColor.DARK_AQUA.toString() + blockchanges.size() + " changes found." + (blockchanges.size() == linesLimit ? " Use 'limit -1' to see all changes." : "")); - } - if (params.sum != SummarizationMode.NONE) { - if (params.bct == BlockChangeType.KILLS && params.sum == SummarizationMode.PLAYERS) { - sender.sendMessage(ChatColor.GOLD + "Kills - Killed - Player"); - } else { - sender.sendMessage(ChatColor.GOLD + "Created - Destroyed - " + (params.sum == SummarizationMode.TYPES ? "Block" : "Player")); - } - } - showPage(sender, 1); - } else { - sender.sendMessage(ChatColor.DARK_AQUA + "No results found."); - getSession(sender).lookupCache = null; - } - } catch (final Exception ex) { - sender.sendMessage(ChatColor.RED + "Exception, check error log"); - getLogger().log(Level.SEVERE, "[Lookup] " + params.getQuery() + ": ", ex); - } finally { - close(); - } - } - } - - public class CommandWriteLogFile extends AbstractCommand { - public CommandWriteLogFile(CommandSender sender, QueryParams params, boolean async) throws Exception { - super(sender, params, async); - } - - @Override - public void run() { - File file = null; - try { - if (params.bct == BlockChangeType.CHAT) { - params.needDate = true; - params.needPlayer = true; - params.needMessage = true; - } else { - params.needDate = true; - params.needType = true; - params.needData = true; - params.needPlayer = true; - if (params.types.isEmpty() || Block.inList(params.types, 63) || Block.inList(params.types, 68)) { - params.needSignText = true; - } - if (params.bct == BlockChangeType.CHESTACCESS || params.bct == BlockChangeType.ALL) { - params.needChestAccess = true; - } - } - conn = logblock.getConnection(); - if (conn == null) { - sender.sendMessage(ChatColor.RED + "MySQL connection lost"); - return; - } - state = conn.createStatement(); - file = new File("plugins/LogBlock/log/" + params.getTitle().replace(":", ".") + ".log"); - sender.sendMessage(ChatColor.GREEN + "Creating " + file.getName()); - rs = executeQuery(state, params.getQuery()); - file.getParentFile().mkdirs(); - file.createNewFile(); - final FileWriter writer = new FileWriter(file); - final String newline = System.getProperty("line.separator"); - file.getParentFile().mkdirs(); - int counter = 0; - if (params.sum != SummarizationMode.NONE) { - writer.write("Created - Destroyed - " + (params.sum == SummarizationMode.TYPES ? "Block" : "Player") + newline); - } - final LookupCacheElementFactory factory = new LookupCacheElementFactory(params, sender instanceof Player ? 2 / 3f : 1); - while (rs.next()) { - writer.write(factory.getLookupCacheElement(rs).getMessage() + newline); - counter++; - } - writer.close(); - sender.sendMessage(ChatColor.GREEN + "Wrote " + counter + " lines."); - } catch (final Exception ex) { - sender.sendMessage(ChatColor.RED + "Exception, check error log"); - getLogger().log(Level.SEVERE, "[WriteLogFile] " + params.getQuery() + " (file was " + file.getAbsolutePath() + "): ", ex); - } finally { - close(); - } - } - } - - public class CommandSaveQueue extends AbstractCommand { - public CommandSaveQueue(CommandSender sender, QueryParams params, boolean async) throws Exception { - super(sender, params, async); - } - - @Override - public void run() { - final Consumer consumer = logblock.getConsumer(); - if (consumer.getQueueSize() > 0) { - sender.sendMessage(ChatColor.DARK_AQUA + "Current queue size: " + consumer.getQueueSize()); - int lastSize = -1, fails = 0; - while (consumer.getQueueSize() > 0) { - fails = lastSize == consumer.getQueueSize() ? fails + 1 : 0; - if (fails > 10) { - sender.sendMessage(ChatColor.RED + "Unable to save queue"); - return; - } - lastSize = consumer.getQueueSize(); - consumer.run(); - } - sender.sendMessage(ChatColor.GREEN + "Queue saved successfully"); - } - } - } - - public class CommandTeleport extends AbstractCommand { - public CommandTeleport(CommandSender sender, QueryParams params, boolean async) throws Exception { - super(sender, params, async); - } - - @Override - public void run() { - try { - params.needCoords = true; - if (params.bct == BlockChangeType.CHESTACCESS || params.bct == BlockChangeType.ALL) { - params.needChestAccess = true; - } - params.limit = 1; - params.sum = SummarizationMode.NONE; - conn = logblock.getConnection(); - if (conn == null) { - sender.sendMessage(ChatColor.RED + "MySQL connection lost"); - return; - } - state = conn.createStatement(); - rs = executeQuery(state, params.getQuery()); - if (rs.next()) { - final Player player = (Player) sender; - final int y = rs.getInt("y"); - final Location loc = new Location(params.world, rs.getInt("x") + 0.5, y, rs.getInt("z") + 0.5, player.getLocation().getYaw(), 90); - - // Teleport the player sync because omg thread safety - logblock.getServer().getScheduler().scheduleSyncDelayedTask(logblock, new Runnable() { - @Override - public void run() { - final int y2 = saveSpawnHeight(loc); - loc.setY(y2); - player.teleport(loc); - sender.sendMessage(ChatColor.GREEN + "You were teleported " + Math.abs(y2 - y) + " blocks " + (y2 - y > 0 ? "above" : "below")); - } - }); - } else { - sender.sendMessage(ChatColor.RED + "No block change found to teleport to"); - } - } catch (final Exception ex) { - sender.sendMessage(ChatColor.RED + "Exception, check error log"); - getLogger().log(Level.SEVERE, "[Teleport] " + params.getQuery() + ": ", ex); - } finally { - close(); - } - } - } - - public class CommandRollback extends AbstractCommand { - public CommandRollback(CommandSender sender, QueryParams params, boolean async) throws Exception { - super(sender, params, async); - } - - @Override - public void run() { - try { - params.needCoords = true; - params.needType = true; - params.needData = true; - params.needSignText = true; - params.needChestAccess = true; - params.order = Order.DESC; - params.sum = SummarizationMode.NONE; - conn = logblock.getConnection(); - if (conn == null) { - sender.sendMessage(ChatColor.RED + "MySQL connection lost"); - return; - } - state = conn.createStatement(); - if (!checkRestrictions(sender, params)) { - return; - } - if (logblock.getConsumer().getQueueSize() > 0) { - new CommandSaveQueue(sender, null, false); - } - if (!params.silent) { - sender.sendMessage(ChatColor.DARK_AQUA + "Searching " + params.getTitle() + ":"); - } - rs = executeQuery(state, params.getQuery()); - final WorldEditor editor = new WorldEditor(logblock, params.world); - - while (rs.next()) { - editor.queueEdit(rs.getInt("x"), rs.getInt("y"), rs.getInt("z"), rs.getInt("replaced"), rs.getInt("type"), rs.getByte("data"), rs.getString("signtext"), rs.getShort("itemtype"), rs.getShort("itemamount"), rs.getShort("itemdata")); - } - final int changes = editor.getSize(); - if (changes > 10000) { - editor.setSender(sender); - } - if (!params.silent) { - sender.sendMessage(ChatColor.GREEN.toString() + changes + " blocks found."); - } - if (changes == 0) { - if (!params.silent) { - sender.sendMessage(ChatColor.RED + "Rollback aborted"); - } - return; - } - if (!params.silent && askRollbacks && questioner != null && sender instanceof Player && !questioner.ask((Player) sender, "Are you sure you want to continue?", "yes", "no").equals("yes")) { - sender.sendMessage(ChatColor.RED + "Rollback aborted"); - return; - } - editor.start(); - getSession(sender).lookupCache = editor.errors; - sender.sendMessage(ChatColor.GREEN + "Rollback finished successfully (" + editor.getElapsedTime() + " ms, " + editor.getSuccesses() + "/" + changes + " blocks" + (editor.getErrors() > 0 ? ", " + ChatColor.RED + editor.getErrors() + " errors" + ChatColor.GREEN : "") + (editor.getBlacklistCollisions() > 0 ? ", " + editor.getBlacklistCollisions() + " blacklist collisions" : "") + ")"); - if (!params.silent && askClearLogAfterRollback && logblock.hasPermission(sender, "logblock.clearlog") && questioner != null && sender instanceof Player) { - Thread.sleep(1000); - if (questioner.ask((Player) sender, "Do you want to delete the rollbacked log?", "yes", "no").equals("yes")) { - params.silent = true; - new CommandClearLog(sender, params, false); - } else { - sender.sendMessage(ChatColor.LIGHT_PURPLE + "Clearlog cancelled"); - } - } - } catch (final Exception ex) { - sender.sendMessage(ChatColor.RED + "Exception, check error log"); - getLogger().log(Level.SEVERE, "[Rollback] " + params.getQuery() + ": ", ex); - } finally { - close(); - } - } - } - - public class CommandRedo extends AbstractCommand { - public CommandRedo(CommandSender sender, QueryParams params, boolean async) throws Exception { - super(sender, params, async); - } - - @Override - public void run() { - try { - params.needCoords = true; - params.needType = true; - params.needData = true; - params.needSignText = true; - params.needChestAccess = true; - params.order = Order.ASC; - params.sum = SummarizationMode.NONE; - conn = logblock.getConnection(); - if (conn == null) { - sender.sendMessage(ChatColor.RED + "MySQL connection lost"); - return; - } - state = conn.createStatement(); - if (!checkRestrictions(sender, params)) { - return; - } - rs = executeQuery(state, params.getQuery()); - if (!params.silent) { - sender.sendMessage(ChatColor.DARK_AQUA + "Searching " + params.getTitle() + ":"); - } - final WorldEditor editor = new WorldEditor(logblock, params.world); - while (rs.next()) { - editor.queueEdit(rs.getInt("x"), rs.getInt("y"), rs.getInt("z"), rs.getInt("type"), rs.getInt("replaced"), rs.getByte("data"), rs.getString("signtext"), rs.getShort("itemtype"), (short) -rs.getShort("itemamount"), rs.getShort("itemdata")); - } - final int changes = editor.getSize(); - if (!params.silent) { - sender.sendMessage(ChatColor.GREEN.toString() + changes + " blocks found."); - } - if (changes == 0) { - if (!params.silent) { - sender.sendMessage(ChatColor.RED + "Redo aborted"); - } - return; - } - if (!params.silent && askRedos && questioner != null && sender instanceof Player && !questioner.ask((Player) sender, "Are you sure you want to continue?", "yes", "no").equals("yes")) { - sender.sendMessage(ChatColor.RED + "Redo aborted"); - return; - } - editor.start(); - sender.sendMessage(ChatColor.GREEN + "Redo finished successfully (" + editor.getElapsedTime() + " ms, " + editor.getSuccesses() + "/" + changes + " blocks" + (editor.getErrors() > 0 ? ", " + ChatColor.RED + editor.getErrors() + " errors" + ChatColor.GREEN : "") + (editor.getBlacklistCollisions() > 0 ? ", " + editor.getBlacklistCollisions() + " blacklist collisions" : "") + ")"); - } catch (final Exception ex) { - sender.sendMessage(ChatColor.RED + "Exception, check error log"); - getLogger().log(Level.SEVERE, "[Redo] " + params.getQuery() + ": ", ex); - } finally { - close(); - } - } - } - - public class CommandClearLog extends AbstractCommand { - public CommandClearLog(CommandSender sender, QueryParams params, boolean async) throws Exception { - super(sender, params, async); - } - - @Override - public void run() { - try { - conn = logblock.getConnection(); - state = conn.createStatement(); - if (conn == null) { - sender.sendMessage(ChatColor.RED + "MySQL connection lost"); - return; - } - if (!checkRestrictions(sender, params)) { - return; - } - final File dumpFolder = new File(logblock.getDataFolder(), "dump"); - if (!dumpFolder.exists()) { - dumpFolder.mkdirs(); - } - final String time = new SimpleDateFormat("yyMMddHHmmss").format(System.currentTimeMillis()); - int deleted; - final String table = params.getTable(); - final String join = params.players.size() > 0 ? "INNER JOIN `lb-players` USING (playerid) " : ""; - rs = state.executeQuery("SELECT count(*) FROM `" + table + "` " + join + params.getWhere()); - rs.next(); - if ((deleted = rs.getInt(1)) > 0) { - if (!params.silent && askClearLogs && sender instanceof Player && questioner != null) { - sender.sendMessage(ChatColor.DARK_AQUA + "Searching " + params.getTitle() + ":"); - sender.sendMessage(ChatColor.GREEN.toString() + deleted + " blocks found."); - if (!questioner.ask((Player) sender, "Are you sure you want to continue?", "yes", "no").equals("yes")) { - sender.sendMessage(ChatColor.RED + "ClearLog aborted"); - return; - } - } - if (dumpDeletedLog) { - try { - state.execute("SELECT * FROM `" + table + "` " + join + params.getWhere() + "INTO OUTFILE '" + new File(dumpFolder, time + " " + table + " " + params.getTitle().replace(":", ".") + ".csv").getAbsolutePath().replace("\\", "\\\\") + "' FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' LINES TERMINATED BY '\n'"); - } catch (final SQLException ex) { - sender.sendMessage(ChatColor.RED + "Error while dumping log. Make sure your MySQL user has access to the LogBlock folder, or disable clearlog.dumpDeletedLog"); - getLogger().log(Level.SEVERE, "[ClearLog] Exception while dumping log: ", ex); - return; - } - } - state.execute("DELETE `" + table + "` FROM `" + table + "` " + join + params.getWhere()); - sender.sendMessage(ChatColor.GREEN + "Cleared out table " + table + ". Deleted " + deleted + " entries."); - } - rs = state.executeQuery("SELECT COUNT(*) FROM `" + table + "-sign` LEFT JOIN `" + table + "` USING (id) WHERE `" + table + "`.id IS NULL"); - rs.next(); - if ((deleted = rs.getInt(1)) > 0) { - if (dumpDeletedLog) { - state.execute("SELECT id, signtext FROM `" + table + "-sign` LEFT JOIN `" + table + "` USING (id) WHERE `" + table + "`.id IS NULL INTO OUTFILE '" + new File(dumpFolder, time + " " + table + "-sign " + params.getTitle() + ".csv").getAbsolutePath().replace("\\", "\\\\") + "' FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' LINES TERMINATED BY '\n'"); - } - state.execute("DELETE `" + table + "-sign` FROM `" + table + "-sign` LEFT JOIN `" + table + "` USING (id) WHERE `" + table + "`.id IS NULL;"); - sender.sendMessage(ChatColor.GREEN + "Cleared out table " + table + "-sign. Deleted " + deleted + " entries."); - } - rs = state.executeQuery("SELECT COUNT(*) FROM `" + table + "-chest` LEFT JOIN `" + table + "` USING (id) WHERE `" + table + "`.id IS NULL"); - rs.next(); - if ((deleted = rs.getInt(1)) > 0) { - if (dumpDeletedLog) { - state.execute("SELECT id, itemtype, itemamount, itemdata FROM `" + table + "-chest` LEFT JOIN `" + table + "` USING (id) WHERE `" + table + "`.id IS NULL INTO OUTFILE '" + new File(dumpFolder, time + " " + table + "-chest " + params.getTitle() + ".csv").getAbsolutePath().replace("\\", "\\\\") + "' FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' LINES TERMINATED BY '\n'"); - } - state.execute("DELETE `" + table + "-chest` FROM `" + table + "-chest` LEFT JOIN `" + table + "` USING (id) WHERE `" + table + "`.id IS NULL;"); - sender.sendMessage(ChatColor.GREEN + "Cleared out table " + table + "-chest. Deleted " + deleted + " entries."); - } - } catch (final Exception ex) { - sender.sendMessage(ChatColor.RED + "Exception, check error log"); - getLogger().log(Level.SEVERE, "[ClearLog] Exception: ", ex); - } finally { - close(); - } - } - } - - private static ResultSet executeQuery(Statement state, String query) throws SQLException { - if (Config.debug) { - long startTime = System.currentTimeMillis(); - ResultSet rs = state.executeQuery(query); - getLogger().log(Level.INFO, "[LogBlock Debug] Time Taken: " + (System.currentTimeMillis() - startTime) + " milliseconds. Query: " + query); - return rs; - } else { - return state.executeQuery(query); - } - } - - private static List argsToList(String[] arr, int offset) { - final List list = new ArrayList(Arrays.asList(arr)); - for (int i = 0; i < offset; i++) { - list.remove(0); - } - return list; - } -} +package de.diddiz.LogBlock; + +import static de.diddiz.LogBlock.Session.getSession; +import static de.diddiz.LogBlock.config.Config.askClearLogAfterRollback; +import static de.diddiz.LogBlock.config.Config.askClearLogs; +import static de.diddiz.LogBlock.config.Config.askRedos; +import static de.diddiz.LogBlock.config.Config.askRollbacks; +import static de.diddiz.LogBlock.config.Config.defaultTime; +import static de.diddiz.LogBlock.config.Config.dumpDeletedLog; +import static de.diddiz.LogBlock.config.Config.getWorldConfig; +import static de.diddiz.LogBlock.config.Config.linesLimit; +import static de.diddiz.LogBlock.config.Config.linesPerPage; +import static de.diddiz.LogBlock.config.Config.rollbackMaxArea; +import static de.diddiz.LogBlock.config.Config.rollbackMaxTime; +import static de.diddiz.LogBlock.config.Config.toolsByName; +import static de.diddiz.LogBlock.config.Config.toolsByType; +import static de.diddiz.LogBlock.util.BukkitUtils.giveTool; +import static de.diddiz.LogBlock.util.BukkitUtils.safeSpawnHeight; +import static de.diddiz.LogBlock.util.TypeColor.DEFAULT; +import static de.diddiz.LogBlock.util.TypeColor.ERROR; +import static de.diddiz.LogBlock.util.TypeColor.HEADER; +import static de.diddiz.LogBlock.util.Utils.isInt; +import static de.diddiz.LogBlock.util.Utils.listing; + +import de.diddiz.LogBlock.QueryParams.BlockChangeType; +import de.diddiz.LogBlock.QueryParams.Order; +import de.diddiz.LogBlock.QueryParams.SummarizationMode; +import de.diddiz.LogBlock.config.Config; +import de.diddiz.LogBlock.config.WorldConfig; +import de.diddiz.LogBlock.util.MessagingUtil; +import de.diddiz.LogBlock.util.Utils; +import java.io.BufferedOutputStream; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.OutputStreamWriter; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.logging.Level; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.ClickEvent; +import net.md_5.bungee.api.chat.TextComponent; +import org.bukkit.ChatColor; +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.scheduler.BukkitScheduler; + +public class CommandsHandler implements CommandExecutor { + private final LogBlock logblock; + private final BukkitScheduler scheduler; + + CommandsHandler(LogBlock logblock) { + this.logblock = logblock; + scheduler = logblock.getServer().getScheduler(); + } + + @Override + public boolean onCommand(CommandSender sender, Command cmd, String commandLabel, String[] args) { + try { + if (args.length == 0) { + sender.sendMessage(ChatColor.YELLOW + "------------------[ " + ChatColor.WHITE + "LogBlock" + ChatColor.YELLOW + " ]-------------------"); + sender.sendMessage(ChatColor.GOLD + "LogBlock " + ChatColor.WHITE + "v" + logblock.getDescription().getVersion() + ChatColor.GOLD + " by DiddiZ"); + TextComponent message = MessagingUtil.createTextComponentWithColor("Type ", net.md_5.bungee.api.ChatColor.GOLD); + TextComponent clickable = MessagingUtil.createTextComponentWithColor("/lb help", net.md_5.bungee.api.ChatColor.WHITE); + clickable.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/lb help")); + message.addExtra(clickable); + message.addExtra(" for help"); + sender.spigot().sendMessage(message); + } else { + final String command = args[0].toLowerCase(); + if (command.equals("help")) { + sender.sendMessage(ChatColor.YELLOW + "----------------[ " + ChatColor.WHITE + "LogBlock Help" + ChatColor.YELLOW + " ]----------------"); + + TextComponent message = MessagingUtil.createTextComponentWithColor("For the commands list type ", net.md_5.bungee.api.ChatColor.GOLD); + TextComponent clickable = MessagingUtil.createTextComponentWithColor("/lb commands", net.md_5.bungee.api.ChatColor.WHITE); + clickable.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/lb commands")); + message.addExtra(clickable); + sender.spigot().sendMessage(message); + + message = MessagingUtil.createTextComponentWithColor("For the parameters list type ", net.md_5.bungee.api.ChatColor.GOLD); + clickable = MessagingUtil.createTextComponentWithColor("/lb params", net.md_5.bungee.api.ChatColor.WHITE); + clickable.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/lb params")); + message.addExtra(clickable); + sender.spigot().sendMessage(message); + + message = MessagingUtil.createTextComponentWithColor("For the list of permissions you got type ", net.md_5.bungee.api.ChatColor.GOLD); + clickable = MessagingUtil.createTextComponentWithColor("/lb permissions", net.md_5.bungee.api.ChatColor.WHITE); + clickable.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/lb permissions")); + message.addExtra(clickable); + sender.spigot().sendMessage(message); + } else if (command.equals("commands")) { + sender.sendMessage(ChatColor.YELLOW + "--------------[ " + ChatColor.WHITE + "LogBlock Commands" + ChatColor.YELLOW + " ]--------------"); + sender.sendMessage(ChatColor.GOLD + "/lb tool " + ChatColor.WHITE + "-- Gives you the lb tool"); + sender.sendMessage(ChatColor.GOLD + "/lb tool [on|off] " + ChatColor.WHITE + "-- Enables/Disables tool"); + sender.sendMessage(ChatColor.GOLD + "/lb tool [params] " + ChatColor.WHITE + "-- Sets the tool lookup query"); + sender.sendMessage(ChatColor.GOLD + "/lb tool default " + ChatColor.WHITE + "-- Sets the tool lookup query to default"); + sender.sendMessage(ChatColor.GOLD + "/lb toolblock " + ChatColor.WHITE + "-- Analog to tool"); + sender.sendMessage(ChatColor.GOLD + "/lb hide " + ChatColor.WHITE + "-- Hides you from log"); + sender.sendMessage(ChatColor.GOLD + "/lb rollback [params] " + ChatColor.WHITE + "-- Rollback"); + sender.sendMessage(ChatColor.GOLD + "/lb redo [params] " + ChatColor.WHITE + "-- Redo"); + sender.sendMessage(ChatColor.GOLD + "/lb tp [params] " + ChatColor.WHITE + "-- Teleports you to the location of griefing"); + sender.sendMessage(ChatColor.GOLD + "/lb writelogfile [params] " + ChatColor.WHITE + "-- Writes a log file"); + sender.sendMessage(ChatColor.GOLD + "/lb lookup [params] " + ChatColor.WHITE + "-- Lookup"); + sender.sendMessage(ChatColor.GOLD + "/lb prev|next " + ChatColor.WHITE + "-- Browse lookup result pages"); + sender.sendMessage(ChatColor.GOLD + "/lb page " + ChatColor.WHITE + "-- Shows a specific lookup result page"); + sender.sendMessage(ChatColor.GOLD + "/lb me " + ChatColor.WHITE + "-- Displays your stats"); + sender.sendMessage(""); + sender.sendMessage(ChatColor.GOLD + "Look at " + ChatColor.WHITE + "github.com/LogBlock/LogBlock/wiki/Commands" + ChatColor.GOLD + " for the full commands reference"); + } else if (command.equals("params")) { + sender.sendMessage(ChatColor.YELLOW + "----------[ " + ChatColor.WHITE + "LogBlock Query Parameters" + ChatColor.YELLOW + " ]----------"); + sender.sendMessage(ChatColor.GOLD + "Use doublequotes to escape a keyword: world \"world\""); + sender.sendMessage(ChatColor.GOLD + "player [name1] " + ChatColor.WHITE + "-- List of players"); + sender.sendMessage(ChatColor.GOLD + "block [type1] " + ChatColor.WHITE + "-- List of block types"); + sender.sendMessage(ChatColor.GOLD + "created, destroyed " + ChatColor.WHITE + "-- Show only created/destroyed blocks"); + sender.sendMessage(ChatColor.GOLD + "chestaccess " + ChatColor.WHITE + "-- Show only chest accesses"); + sender.sendMessage(ChatColor.GOLD + "entities [type1] " + ChatColor.WHITE + "-- List of entity types; can not be combined with blocks"); + sender.sendMessage(ChatColor.GOLD + "area " + ChatColor.WHITE + "-- Area around you"); + sender.sendMessage(ChatColor.GOLD + "selection, sel " + ChatColor.WHITE + "-- Inside current WorldEdit selection"); + sender.sendMessage(ChatColor.GOLD + "world [worldname] " + ChatColor.WHITE + "-- Changes the world"); + sender.sendMessage(ChatColor.GOLD + "time [number] [minutes|hours|days] " + ChatColor.WHITE + "-- Limits time"); + sender.sendMessage(ChatColor.GOLD + "since " + ChatColor.WHITE + "-- Limits time to a fixed point"); + sender.sendMessage(ChatColor.GOLD + "before " + ChatColor.WHITE + "-- Affects only blocks before a fixed time"); + sender.sendMessage(ChatColor.GOLD + "force " + ChatColor.WHITE + "-- Forces replacing not matching blocks"); + sender.sendMessage(ChatColor.GOLD + "limit " + ChatColor.WHITE + "-- Limits the result to count of rows"); + sender.sendMessage(ChatColor.GOLD + "sum [none|blocks|players] " + ChatColor.WHITE + "-- Sums the result"); + sender.sendMessage(ChatColor.GOLD + "asc, desc " + ChatColor.WHITE + "-- Changes the order of the displayed log"); + sender.sendMessage(ChatColor.GOLD + "coords " + ChatColor.WHITE + "-- Shows coordinates for each block"); + sender.sendMessage(ChatColor.GOLD + "nocache " + ChatColor.WHITE + "-- Don't set the lookup cache"); + sender.sendMessage(ChatColor.GOLD + "silent " + ChatColor.WHITE + "-- Displays lesser messages"); + } else if (command.equals("permissions")) { + sender.sendMessage(ChatColor.DARK_AQUA + "You've got the following permissions:"); + for (final String permission : new String[] { "me", "lookup", "tp", "rollback", "clearlog", "hide", "ignoreRestrictions", "spawnTools" }) { + if (logblock.hasPermission(sender, "logblock." + permission)) { + sender.sendMessage(ChatColor.GOLD + "logblock." + permission); + } + } + for (final Tool tool : toolsByType.values()) { + if (logblock.hasPermission(sender, "logblock.tools." + tool.name)) { + sender.sendMessage(ChatColor.GOLD + "logblock.tools." + tool.name); + } + } + } else if (command.equals("logging")) { + if (logblock.hasPermission(sender, "logblock.lookup")) { + World world = null; + if (args.length > 1) { + world = logblock.getServer().getWorld(args[1]); + } else if (sender instanceof Player) { + world = ((Player) sender).getWorld(); + } + if (world != null) { + final WorldConfig wcfg = getWorldConfig(world.getName()); + if (wcfg != null) { + sender.sendMessage(ChatColor.DARK_AQUA + "Currently logging in " + world.getName() + ":"); + final List logging = new ArrayList<>(); + for (final Logging l : Logging.values()) { + if (wcfg.isLogging(l)) { + logging.add(l.toString()); + } + } + sender.sendMessage(ChatColor.GOLD + listing(logging, ", ", " and ")); + } else { + sender.sendMessage(ChatColor.RED + "World not logged: '" + world.getName() + "'"); + sender.sendMessage(ChatColor.LIGHT_PURPLE + "Make the world name is listed at loggedWorlds in config. World names are case sensitive and must contains the path (if any), exactly like in the message above."); + } + } else { + sender.sendMessage(ChatColor.RED + "No world specified"); + } + } else { + sender.sendMessage(ChatColor.RED + "You aren't allowed to do this."); + } + } else if (toolsByName.get(command) != null) { + final Tool tool = toolsByName.get(command); + if (logblock.hasPermission(sender, "logblock.tools." + tool.name)) { + if (sender instanceof Player) { + final Player player = (Player) sender; + final Session session = Session.getSession(player); + final ToolData toolData = session.toolData.get(tool); + if (args.length == 1) { + if (logblock.hasPermission(player, "logblock.spawnTools")) { + giveTool(player, tool.item); + toolData.enabled = true; + } else { + sender.sendMessage(ChatColor.RED + "You aren't allowed to do this."); + } + } else if (args[1].equalsIgnoreCase("enable") || args[1].equalsIgnoreCase("on")) { + toolData.enabled = true; + player.sendMessage(ChatColor.GREEN + "Tool enabled."); + } else if (args[1].equalsIgnoreCase("disable") || args[1].equalsIgnoreCase("off")) { + toolData.enabled = false; + if (tool.removeOnDisable && logblock.hasPermission(player, "logblock.spawnTools")) { + if (!player.getInventory().removeItem(new ItemStack(tool.item, 1)).isEmpty()) { + // remove the tool from the offhand if it cannot be found in the main hand + ItemStack offhandItem = player.getInventory().getItemInOffHand(); + if (offhandItem != null && offhandItem.isSimilar(new ItemStack(tool.item, 1))) { + if (offhandItem.getAmount() <= 1) { + player.getInventory().setItemInOffHand(null); + } else { + offhandItem.setAmount(offhandItem.getAmount() - 1); + player.getInventory().setItemInOffHand(offhandItem); + } + } + } + } + player.sendMessage(ChatColor.GREEN + "Tool disabled."); + } else if (args[1].equalsIgnoreCase("mode")) { + if (args.length == 3) { + final ToolMode mode; + try { + mode = ToolMode.valueOf(args[2].toUpperCase()); + } catch (final IllegalArgumentException ex) { + sender.sendMessage(ChatColor.RED + "Can't find mode " + args[2]); + return true; + } + if (logblock.hasPermission(player, mode.getPermission())) { + toolData.mode = mode; + sender.sendMessage(ChatColor.GREEN + "Tool mode set to " + args[2]); + } else { + sender.sendMessage(ChatColor.RED + "You aren't allowed to use mode " + args[2]); + } + } else { + player.sendMessage(ChatColor.RED + "No mode specified"); + } + } else if (args[1].equalsIgnoreCase("default")) { + toolData.params = tool.params.clone(); + toolData.mode = tool.mode; + sender.sendMessage(ChatColor.GREEN + "Tool set to default."); + } else if (logblock.hasPermission(player, "logblock.lookup")) { + try { + final QueryParams params = tool.params.clone(); + params.parseArgs(sender, argsToList(args, 1)); + toolData.params = params; + sender.sendMessage(ChatColor.GREEN + "Set tool query to: " + params.getTitle()); + } catch (final Exception ex) { + sender.sendMessage(ChatColor.RED + ex.getMessage()); + } + } else { + sender.sendMessage(ChatColor.RED + "You aren't allowed to do this."); + } + } else { + sender.sendMessage(ChatColor.RED + "You have to be a player."); + } + } else { + sender.sendMessage(ChatColor.RED + "You aren't allowed to do this."); + } + } else if (command.equals("hide")) { + if (sender instanceof Player) { + if (logblock.hasPermission(sender, "logblock.hide")) { + if (args.length == 2) { + if (args[1].equalsIgnoreCase("on")) { + Consumer.hide((Player) sender); + sender.sendMessage(ChatColor.GREEN + "You are now hidden and aren't logged. Type /lb hide to unhide."); + } else if (args[1].equalsIgnoreCase("off")) { + Consumer.unHide((Player) sender); + sender.sendMessage(ChatColor.GREEN + "You aren't hidden any longer."); + } + } else { + if (Consumer.toggleHide((Player) sender)) { + sender.sendMessage(ChatColor.GREEN + "You are now hidden and aren't logged. Type '/lb hide' again to unhide."); + } else { + sender.sendMessage(ChatColor.GREEN + "You aren't hidden any longer."); + } + } + } else { + sender.sendMessage(ChatColor.RED + "You aren't allowed to do this."); + } + } else { + sender.sendMessage(ChatColor.RED + "You have to be a player."); + } + } else if (command.equals("page")) { + if (args.length == 2 && isInt(args[1])) { + sender.sendMessage(""); + showPage(sender, Integer.valueOf(args[1])); + } else { + sender.sendMessage(ChatColor.RED + "You have to specify a page"); + } + } else if (command.equals("next") || command.equals("+")) { + sender.sendMessage(""); + showPage(sender, getSession(sender).page + 1); + } else if (command.equals("prev") || command.equals("-")) { + sender.sendMessage(""); + showPage(sender, getSession(sender).page - 1); + } else if (args[0].equalsIgnoreCase("savequeue")) { + if (logblock.hasPermission(sender, "logblock.rollback")) { + new CommandSaveQueue(sender, null, true); + } else { + sender.sendMessage(ChatColor.RED + "You aren't allowed to do this."); + } + } else if (args[0].equalsIgnoreCase("queuesize")) { + if (logblock.hasPermission(sender, "logblock.rollback")) { + sender.sendMessage("Current queue size: " + logblock.getConsumer().getQueueSize()); + } else { + sender.sendMessage(ChatColor.RED + "You aren't allowed to do this."); + } + } else if (command.equals("rollback") || command.equals("undo") || command.equals("rb")) { + if (logblock.hasPermission(sender, "logblock.rollback")) { + final QueryParams params = new QueryParams(logblock); + params.since = defaultTime; + params.bct = BlockChangeType.ALL; + params.parseArgs(sender, argsToList(args, 1)); + new CommandRollback(sender, params, true); + } else { + sender.sendMessage(ChatColor.RED + "You aren't allowed to do this."); + } + } else if (command.equals("redo")) { + if (logblock.hasPermission(sender, "logblock.rollback")) { + final QueryParams params = new QueryParams(logblock); + params.since = defaultTime; + params.bct = BlockChangeType.ALL; + params.parseArgs(sender, argsToList(args, 1)); + new CommandRedo(sender, params, true); + } else { + sender.sendMessage(ChatColor.RED + "You aren't allowed to do this."); + } + } else if (command.equals("me")) { + if (sender instanceof Player) { + if (logblock.hasPermission(sender, "logblock.me")) { + final Player player = (Player) sender; + if (Config.isLogged(player.getWorld())) { + final QueryParams params = new QueryParams(logblock); + params.setPlayer(player.getName()); + params.world = player.getWorld(); + params.sum = SummarizationMode.TYPES; + new CommandLookup(sender, params, true); + } else { + sender.sendMessage(ChatColor.RED + "This world isn't logged"); + } + } else { + sender.sendMessage(ChatColor.RED + "You aren't allowed to do this."); + } + } else { + sender.sendMessage(ChatColor.RED + "You have to be a player."); + } + } else if (command.equals("writelogfile")) { + if (logblock.hasPermission(sender, "logblock.rollback")) { + final QueryParams params = new QueryParams(logblock); + params.limit = -1; + params.bct = BlockChangeType.ALL; + params.parseArgs(sender, argsToList(args, 1)); + new CommandWriteLogFile(sender, params, true); + } else { + sender.sendMessage(ChatColor.RED + "You aren't allowed to do this"); + } + } else if (command.equals("clearlog")) { + if (logblock.hasPermission(sender, "logblock.clearlog")) { + final QueryParams params = new QueryParams(logblock); + params.limit = -1; + params.bct = BlockChangeType.ALL; + params.parseArgs(sender, argsToList(args, 1)); + new CommandClearLog(sender, params, true); + } else { + sender.sendMessage(ChatColor.RED + "You aren't allowed to do this"); + } + } else if (command.equals("tp")) { + if (sender instanceof Player) { + if (logblock.hasPermission(sender, "logblock.tp")) { + if (args.length == 2 || isInt(args[1])) { + final int pos = Integer.parseInt(args[1]) - 1; + final Player player = (Player) sender; + final Session session = getSession(player); + if (session.lookupCache != null) { + if (pos >= 0 && pos < session.lookupCache.length) { + final Location loc = session.lookupCache[pos].getLocation(); + if (loc != null) { + player.teleport(new Location(loc.getWorld(), loc.getX() + 0.5, player.getGameMode() != GameMode.SPECTATOR ? safeSpawnHeight(loc) : loc.getY(), loc.getZ() + 0.5, player.getLocation().getYaw(), 90)); + player.sendMessage(ChatColor.LIGHT_PURPLE + "Teleported to " + loc.getBlockX() + ":" + loc.getBlockY() + ":" + loc.getBlockZ()); + } else { + sender.sendMessage(ChatColor.RED + "There is no location associated with that. Did you forget coords parameter?"); + } + } else { + sender.sendMessage(ChatColor.RED + "'" + args[1] + " is out of range"); + } + } else { + sender.sendMessage(ChatColor.RED + "You havn't done a lookup yet"); + } + } else { + new CommandTeleport(sender, new QueryParams(logblock, sender, argsToList(args, 1)), true); + } + } else { + sender.sendMessage(ChatColor.RED + "You aren't allowed to do this"); + } + } else { + sender.sendMessage(ChatColor.RED + "You have to be a player."); + } + } else if (command.equals("lookup") || QueryParams.isKeyWord(args[0])) { + if (logblock.hasPermission(sender, "logblock.lookup")) { + final List argsList = new ArrayList<>(Arrays.asList(args)); + if (command.equals("lookup")) { + argsList.remove(0); + } + final QueryParams params = new QueryParams(logblock); + params.since = defaultTime; + params.bct = BlockChangeType.ALL; + params.limit = Config.linesLimit; + params.parseArgs(sender, argsList); + new CommandLookup(sender, params, true); + } else { + sender.sendMessage(ChatColor.RED + "You aren't allowed to do this"); + } + } else { + sender.sendMessage(ChatColor.RED + "Unknown command '" + args[0] + "'"); + } + } + } catch (final IllegalArgumentException ex) { + sender.sendMessage(ChatColor.RED + ex.getMessage()); + } catch (final ArrayIndexOutOfBoundsException ex) { + sender.sendMessage(ChatColor.RED + "Not enough arguments given"); + } catch (final Exception ex) { + sender.sendMessage(ChatColor.RED + "Error, check server.log"); + logblock.getLogger().log(Level.WARNING, "Exception in commands handler: ", ex); + } + return true; + } + + private static void showPage(CommandSender sender, int page) { + showPage(sender, page, getSession(sender).lookupCache, true); + } + + private static void showPage(CommandSender sender, int page, LookupCacheElement[] lookupElements, boolean setSessionPage) { + if (lookupElements != null && lookupElements.length > 0) { + final int startpos = (page - 1) * linesPerPage; + if (page > 0 && startpos <= lookupElements.length - 1) { + final int stoppos = startpos + linesPerPage >= lookupElements.length ? lookupElements.length - 1 : startpos + linesPerPage - 1; + final int numberOfPages = (int) Math.ceil(lookupElements.length / (double) linesPerPage); + if (numberOfPages != 1) { + sender.sendMessage(HEADER + "Page " + page + "/" + numberOfPages); + } + for (int i = startpos; i <= stoppos; i++) { + TextComponent message = new TextComponent(); + message.setColor(DEFAULT.getColor()); + if (lookupElements[i].getLocation() != null) { + message.addExtra(new TextComponent("(" + (i + 1) + ") ")); + } + message.addExtra(lookupElements[i].getLogMessage(i + 1)); + sender.spigot().sendMessage(message); + } + if (setSessionPage) { + getSession(sender).page = page; + } + } else { + sender.sendMessage(ERROR + "There isn't a page '" + page + "'"); + } + } else { + sender.sendMessage(ERROR + "No blocks in lookup cache"); + } + } + + private boolean checkRestrictions(CommandSender sender, QueryParams params) { + if (sender.isOp() || logblock.hasPermission(sender, "logblock.ignoreRestrictions")) { + return true; + } + if (rollbackMaxTime > 0 && (params.since <= 0 || params.since > rollbackMaxTime)) { + sender.sendMessage(ChatColor.RED + "You are not allowed to rollback more than " + rollbackMaxTime + " minutes"); + return false; + } + if (rollbackMaxArea > 0 && (params.sel == null && params.loc == null || params.radius > rollbackMaxArea || params.sel != null && (params.sel.getSizeX() > rollbackMaxArea || params.sel.getSizeZ() > rollbackMaxArea))) { + sender.sendMessage(ChatColor.RED + "You are not allowed to rollback an area larger than " + rollbackMaxArea + " blocks"); + return false; + } + return true; + } + + public abstract class AbstractCommand implements Runnable { + protected CommandSender sender; + protected QueryParams params; + protected Connection conn = null; + protected Statement state = null; + protected ResultSet rs = null; + + protected AbstractCommand(CommandSender sender, QueryParams params, boolean async) throws Exception { + this.sender = sender; + this.params = params == null ? null : params.clone(); + if (async) { + scheduler.runTaskAsynchronously(logblock, this); + } else { + run(); + } + } + + public final void close() { + try { + if (conn != null) { + conn.close(); + } + if (state != null) { + state.close(); + } + if (rs != null) { + rs.close(); + } + } catch (final SQLException ex) { + if (logblock.isCompletelyEnabled()) { + logblock.getLogger().log(Level.SEVERE, "[CommandsHandler] SQL exception on close", ex); + } + } + } + } + + public class CommandLookup extends AbstractCommand { + public CommandLookup(CommandSender sender, QueryParams params, boolean async) throws Exception { + super(sender, params, async); + } + + @Override + public void run() { + try { + if (params.bct == BlockChangeType.CHAT) { + params.needDate = true; + params.needPlayer = true; + params.needMessage = true; + } else if (params.bct == BlockChangeType.KILLS) { + params.needDate = true; + params.needPlayer = true; + params.needKiller = true; + params.needVictim = true; + params.needWeapon = true; + } else { + params.needDate = true; + params.needType = true; + params.needData = true; + params.needPlayer = true; + if (params.bct == BlockChangeType.CHESTACCESS || params.bct == BlockChangeType.ALL) { + params.needChestAccess = true; + } + } + conn = logblock.getConnection(); + if (conn == null) { + sender.sendMessage(ChatColor.RED + "MySQL connection lost"); + return; + } + state = conn.createStatement(); + rs = executeQuery(state, params.getQuery()); + sender.sendMessage(""); + sender.sendMessage(ChatColor.DARK_AQUA + params.getTitle() + ":"); + final List blockchanges = new ArrayList<>(); + final LookupCacheElementFactory factory = new LookupCacheElementFactory(params, sender instanceof Player ? 2 / 3f : 1); + while (rs.next()) { + blockchanges.add(factory.getLookupCacheElement(rs)); + } + if (!blockchanges.isEmpty()) { + LookupCacheElement[] blockChangeArray = blockchanges.toArray(new LookupCacheElement[blockchanges.size()]); + if (!params.noCache) { + getSession(sender).lookupCache = blockChangeArray; + } + if (params.sum == SummarizationMode.NONE) { + if (blockchanges.size() > linesPerPage) { + sender.sendMessage(ChatColor.DARK_AQUA.toString() + blockchanges.size() + " changes found." + (blockchanges.size() == linesLimit ? " Use 'limit -1' to see all changes." : "")); + } + } else { + int totalChanges = 0; + for (LookupCacheElement element : blockchanges) { + totalChanges += element.getNumChanges(); + } + sender.sendMessage(ChatColor.DARK_AQUA.toString() + totalChanges + " changes found."); + if (params.bct == BlockChangeType.KILLS && params.sum == SummarizationMode.PLAYERS) { + sender.sendMessage(ChatColor.DARK_AQUA.toString() + blockchanges.size() + " distinct players found."); + sender.sendMessage(ChatColor.GOLD + "Kills - Killed - Player"); + } else { + sender.sendMessage(ChatColor.DARK_AQUA.toString() + blockchanges.size() + " distinct " + (params.sum == SummarizationMode.TYPES ? (params.bct == BlockChangeType.ENTITIES ? "entities" : "blocks") : "players") + " found."); + sender.sendMessage(ChatColor.GOLD + "Created - Destroyed - " + (params.sum == SummarizationMode.TYPES ? (params.bct == BlockChangeType.ENTITIES ? "Entity" : "Block") : "Player")); + } + } + if (!params.noCache) { + showPage(sender, 1); + } else { + showPage(sender, 1, blockChangeArray, false); + } + } else { + sender.sendMessage(ChatColor.DARK_AQUA + "No results found."); + if (!params.noCache) { + getSession(sender).lookupCache = null; + } + } + } catch (final Exception ex) { + if (logblock.isCompletelyEnabled() || !(ex instanceof SQLException)) { + sender.sendMessage(ChatColor.RED + "Exception, check error log"); + logblock.getLogger().log(Level.SEVERE, "[Lookup] " + params.getQuery() + ": ", ex); + } + } finally { + close(); + } + } + } + + public class CommandWriteLogFile extends AbstractCommand { + public CommandWriteLogFile(CommandSender sender, QueryParams params, boolean async) throws Exception { + super(sender, params, async); + } + + @Override + public void run() { + File file = null; + try { + if (params.bct == BlockChangeType.CHAT) { + params.needDate = true; + params.needPlayer = true; + params.needMessage = true; + } else { + params.needDate = true; + params.needType = true; + params.needData = true; + params.needPlayer = true; + if (params.bct == BlockChangeType.CHESTACCESS || params.bct == BlockChangeType.ALL) { + params.needChestAccess = true; + } + } + conn = logblock.getConnection(); + if (conn == null) { + sender.sendMessage(ChatColor.RED + "MySQL connection lost"); + return; + } + state = conn.createStatement(); + final File dumpFolder = new File(logblock.getDataFolder(), "log"); + if (!dumpFolder.exists()) { + dumpFolder.mkdirs(); + } + file = new File(dumpFolder, params.getTitle().replace(":", ".").replace("/", "_").replace("\\", "_") + ".log"); + sender.sendMessage(ChatColor.GREEN + "Creating " + file.getName()); + rs = executeQuery(state, params.getQuery()); + file.getParentFile().mkdirs(); + file.createNewFile(); + final FileWriter writer = new FileWriter(file); + final String newline = System.getProperty("line.separator"); + file.getParentFile().mkdirs(); + int counter = 0; + if (params.sum != SummarizationMode.NONE) { + writer.write("Created - Destroyed - " + (params.sum == SummarizationMode.TYPES ? (params.bct == BlockChangeType.ENTITIES ? "Entity" : "Block") : "Player") + newline); + } + final LookupCacheElementFactory factory = new LookupCacheElementFactory(params, sender instanceof Player ? 2 / 3f : 1); + while (rs.next()) { + writer.write(BaseComponent.toPlainText(factory.getLookupCacheElement(rs).getLogMessage()) + newline); + counter++; + } + writer.close(); + sender.sendMessage(ChatColor.GREEN + "Wrote " + counter + " lines."); + } catch (final Exception ex) { + if (logblock.isCompletelyEnabled() || !(ex instanceof SQLException)) { + sender.sendMessage(ChatColor.RED + "Exception, check error log"); + logblock.getLogger().log(Level.SEVERE, "[WriteLogFile] " + params.getQuery() + " (file was " + file.getAbsolutePath() + "): ", ex); + } + } finally { + close(); + } + } + } + + public class CommandSaveQueue extends AbstractCommand { + public CommandSaveQueue(CommandSender sender, QueryParams params, boolean async) throws Exception { + super(sender, params, async); + } + + @Override + public void run() { + final Consumer consumer = logblock.getConsumer(); + sender.sendMessage(ChatColor.DARK_AQUA + "Current queue size: " + consumer.getQueueSize()); + } + } + + public class CommandTeleport extends AbstractCommand { + public CommandTeleport(CommandSender sender, QueryParams params, boolean async) throws Exception { + super(sender, params, async); + } + + @Override + public void run() { + try { + params.needCoords = true; + if (params.bct == BlockChangeType.CHESTACCESS || params.bct == BlockChangeType.ALL) { + params.needChestAccess = true; + } + params.limit = 1; + params.sum = SummarizationMode.NONE; + conn = logblock.getConnection(); + if (conn == null) { + sender.sendMessage(ChatColor.RED + "MySQL connection lost"); + return; + } + state = conn.createStatement(); + rs = executeQuery(state, params.getQuery()); + if (rs.next()) { + final Player player = (Player) sender; + final int y = rs.getInt("y"); + final Location loc = new Location(params.world, rs.getInt("x") + 0.5, y, rs.getInt("z") + 0.5, player.getLocation().getYaw(), 90); + + // Teleport the player sync because omg thread safety + logblock.getServer().getScheduler().scheduleSyncDelayedTask(logblock, new Runnable() { + @Override + public void run() { + if (player.getGameMode() != GameMode.SPECTATOR) { + final int y2 = safeSpawnHeight(loc); + loc.setY(y2); + sender.sendMessage(ChatColor.GREEN + "You were teleported " + Math.abs(y2 - y) + " blocks " + (y2 - y > 0 ? "above" : "below")); + } + player.teleport(loc); + } + }); + } else { + sender.sendMessage(ChatColor.RED + "No block change found to teleport to"); + } + } catch (final Exception ex) { + if (logblock.isCompletelyEnabled() || !(ex instanceof SQLException)) { + sender.sendMessage(ChatColor.RED + "Exception, check error log"); + logblock.getLogger().log(Level.SEVERE, "[Teleport] " + params.getQuery() + ": ", ex); + } + } finally { + close(); + } + } + } + + public class CommandRollback extends AbstractCommand { + public CommandRollback(CommandSender sender, QueryParams params, boolean async) throws Exception { + super(sender, params, async); + } + + @Override + public void run() { + try { + if (params.bct == BlockChangeType.CHAT) { + sender.sendMessage(ChatColor.RED + "Chat cannot be rolled back"); + return; + } + if (params.bct == BlockChangeType.KILLS) { + sender.sendMessage(ChatColor.RED + "Kills cannot be rolled back"); + return; + } + if (params.sum != SummarizationMode.NONE) { + sender.sendMessage(ChatColor.RED + "Cannot rollback summarized changes"); + return; + } + params.needDate = true; + params.needCoords = true; + params.needType = true; + params.needData = true; + params.needChestAccess = true; + params.sum = SummarizationMode.NONE; + conn = logblock.getConnection(); + if (conn == null) { + sender.sendMessage(ChatColor.RED + "MySQL connection lost"); + return; + } + state = conn.createStatement(); + if (!checkRestrictions(sender, params)) { + return; + } + if (!params.silent) { + sender.sendMessage(ChatColor.DARK_AQUA + "Searching " + params.getTitle() + ":"); + } + rs = executeQuery(state, params.getQuery()); + final WorldEditor editor = new WorldEditor(logblock, params.world, params.forceReplace); + WorldEditorEditFactory editFactory = new WorldEditorEditFactory(editor, params, true); + while (rs.next()) { + editFactory.processRow(rs); + } + if (params.order == Order.DESC) { + editor.reverseRowOrder(); + } + editor.sortRows(Order.DESC); + final int changes = editor.getSize(); + if (changes > 10000) { + editor.setSender(sender); + } + if (!params.silent) { + sender.sendMessage(ChatColor.GREEN.toString() + changes + " " + (params.bct == BlockChangeType.ENTITIES ? "entities" : "blocks") + " found."); + } + if (changes == 0) { + if (!params.silent) { + sender.sendMessage(ChatColor.RED + "Rollback aborted"); + } + return; + } + if (!params.silent && askRollbacks && sender instanceof Player && !logblock.getQuestioner().ask((Player) sender, "Are you sure you want to continue?", "yes", "no").equals("yes")) { + sender.sendMessage(ChatColor.RED + "Rollback aborted"); + return; + } + editor.start(); + if (!params.noCache) { + getSession(sender).lookupCache = editor.errors; + } + sender.sendMessage(ChatColor.GREEN + "Rollback finished successfully (" + editor.getElapsedTime() + " ms, " + editor.getSuccesses() + "/" + changes + " blocks" + (editor.getErrors() > 0 ? ", " + ChatColor.RED + editor.getErrors() + " errors" + ChatColor.GREEN : "") + (editor.getBlacklistCollisions() > 0 ? ", " + editor.getBlacklistCollisions() + " blacklist collisions" : "") + ")"); + if (!params.silent && askClearLogAfterRollback && logblock.hasPermission(sender, "logblock.clearlog") && sender instanceof Player) { + Thread.sleep(1000); + if (logblock.getQuestioner().ask((Player) sender, "Do you want to delete the rollbacked log?", "yes", "no").equals("yes")) { + params.silent = true; + new CommandClearLog(sender, params, false); + } else { + sender.sendMessage(ChatColor.LIGHT_PURPLE + "Clearlog cancelled"); + } + } + } catch (final Exception ex) { + if (logblock.isCompletelyEnabled() || !(ex instanceof SQLException)) { + sender.sendMessage(ChatColor.RED + "Exception, check error log"); + logblock.getLogger().log(Level.SEVERE, "[Rollback] " + params.getQuery() + ": ", ex); + } + } finally { + close(); + } + } + } + + public class CommandRedo extends AbstractCommand { + public CommandRedo(CommandSender sender, QueryParams params, boolean async) throws Exception { + super(sender, params, async); + } + + @Override + public void run() { + try { + if (params.bct == BlockChangeType.CHAT) { + sender.sendMessage(ChatColor.RED + "Chat cannot be redone"); + return; + } + if (params.bct == BlockChangeType.KILLS) { + sender.sendMessage(ChatColor.RED + "Kills cannot be redone"); + return; + } + if (params.sum != SummarizationMode.NONE) { + sender.sendMessage(ChatColor.RED + "Cannot redo summarized changes"); + return; + } + params.needDate = true; + params.needCoords = true; + params.needType = true; + params.needData = true; + params.needChestAccess = true; + params.sum = SummarizationMode.NONE; + conn = logblock.getConnection(); + if (conn == null) { + sender.sendMessage(ChatColor.RED + "MySQL connection lost"); + return; + } + state = conn.createStatement(); + if (!checkRestrictions(sender, params)) { + return; + } + rs = executeQuery(state, params.getQuery()); + if (!params.silent) { + sender.sendMessage(ChatColor.DARK_AQUA + "Searching " + params.getTitle() + ":"); + } + final WorldEditor editor = new WorldEditor(logblock, params.world, params.forceReplace); + WorldEditorEditFactory editFactory = new WorldEditorEditFactory(editor, params, false); + while (rs.next()) { + editFactory.processRow(rs); + } + if (params.order == Order.ASC) { + editor.reverseRowOrder(); + } + editor.sortRows(Order.ASC); + final int changes = editor.getSize(); + if (!params.silent) { + sender.sendMessage(ChatColor.GREEN.toString() + changes + " " + (params.bct == BlockChangeType.ENTITIES ? "entities" : "blocks") + " found."); + } + if (changes == 0) { + if (!params.silent) { + sender.sendMessage(ChatColor.RED + "Redo aborted"); + } + return; + } + if (!params.silent && askRedos && sender instanceof Player && !logblock.getQuestioner().ask((Player) sender, "Are you sure you want to continue?", "yes", "no").equals("yes")) { + sender.sendMessage(ChatColor.RED + "Redo aborted"); + return; + } + editor.start(); + sender.sendMessage(ChatColor.GREEN + "Redo finished successfully (" + editor.getElapsedTime() + " ms, " + editor.getSuccesses() + "/" + changes + " blocks" + (editor.getErrors() > 0 ? ", " + ChatColor.RED + editor.getErrors() + " errors" + ChatColor.GREEN : "") + (editor.getBlacklistCollisions() > 0 ? ", " + editor.getBlacklistCollisions() + " blacklist collisions" : "") + ")"); + } catch (final Exception ex) { + if (logblock.isCompletelyEnabled() || !(ex instanceof SQLException)) { + sender.sendMessage(ChatColor.RED + "Exception, check error log"); + logblock.getLogger().log(Level.SEVERE, "[Redo] " + params.getQuery() + ": ", ex); + } + } finally { + close(); + } + } + } + + public class CommandClearLog extends AbstractCommand { + public CommandClearLog(CommandSender sender, QueryParams params, boolean async) throws Exception { + super(sender, params, async); + } + + @Override + public void run() { + try { + conn = logblock.getConnection(); + conn.setAutoCommit(true); + state = conn.createStatement(); + if (conn == null) { + sender.sendMessage(ChatColor.RED + "MySQL connection lost"); + return; + } + if (!checkRestrictions(sender, params)) { + return; + } + if (params.sum != SummarizationMode.NONE) { + sender.sendMessage(ChatColor.RED + "Cannot summarize on ClearLog"); + return; + } + String tableBase; + String deleteFromTables; + String tableName; + params.needId = true; + params.needDate = true; + params.needPlayerId = true; + if (params.bct == BlockChangeType.CHAT) { + params.needMessage = true; + tableBase = "lb-chat"; + deleteFromTables = "`lb-chat` "; + tableName = "lb-chat"; + } else if (params.bct == BlockChangeType.KILLS) { + params.needWeapon = true; + params.needCoords = true; + tableBase = params.getTable(); + deleteFromTables = "`" + tableBase + "-kills` "; + tableName = tableBase + "-kills"; + } else if (params.bct == BlockChangeType.ENTITIES || params.bct == BlockChangeType.ENTITIES_CREATED || params.bct == BlockChangeType.ENTITIES_KILLED) { + params.needType = true; + params.needCoords = true; + params.needData = true; + tableBase = params.getTable(); + deleteFromTables = "`" + tableBase + "-entities` "; + tableName = tableBase + "-entities"; + } else { + params.needType = true; + params.needCoords = true; + params.needData = true; + params.needChestAccess = true; + tableBase = params.getTable(); + deleteFromTables = "`" + tableBase + "-blocks`, `" + tableBase + "-state`, `" + tableBase + "-chestdata` "; + tableName = tableBase + "-blocks"; + } + final File dumpFolder = new File(logblock.getDataFolder(), "dump"); + if (!dumpFolder.exists()) { + dumpFolder.mkdirs(); + } + rs = state.executeQuery("SELECT count(*) " + params.getFrom() + params.getWhere()); + int deleted = rs.next() ? rs.getInt(1) : 0; + rs.close(); + if (!params.silent && askClearLogs && sender instanceof Player) { + sender.sendMessage(ChatColor.DARK_AQUA + "Searching " + params.getTitle() + ":"); + sender.sendMessage(ChatColor.GREEN.toString() + deleted + " entries found."); + if (deleted == 0 || !logblock.getQuestioner().ask((Player) sender, "Are you sure you want to continue?", "yes", "no").equals("yes")) { + sender.sendMessage(ChatColor.RED + "ClearLog aborted"); + return; + } + } + if (deleted > 0 && dumpDeletedLog) { + final String time = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss").format(System.currentTimeMillis()); + try { + File outFile = new File(dumpFolder, (time + " " + tableName + " " + params.getTitle() + ".sql").replace(':', '.').replace('/', '_').replace('\\', '_')); + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new BufferedOutputStream(new FileOutputStream(outFile)), "UTF-8")); + try { + rs = state.executeQuery("SELECT " + params.getFields() + params.getFrom() + params.getWhere()); + while (rs.next()) { + StringBuilder sb = new StringBuilder(); + if (params.bct == BlockChangeType.CHAT) { + sb.append("INSERT INTO `lb-chat` (`id`, `date`, `playerid`, `message`) VALUES ("); + sb.append(rs.getLong("id")).append(", FROM_UNIXTIME("); + sb.append(rs.getTimestamp("date").getTime() / 1000).append("), "); + sb.append(rs.getInt("playerid")).append(", '"); + sb.append(Utils.mysqlTextEscape(rs.getString("message"))); + sb.append("');\n"); + } else if (params.bct == BlockChangeType.KILLS) { + sb.append("INSERT INTO `").append(tableBase).append("-kills` (`id`, `date`, `killer`, `victim`, `weapon`, `x`, `y`, `z`) VALUES ("); + sb.append(rs.getLong("id")).append(", FROM_UNIXTIME("); + sb.append(rs.getTimestamp("date").getTime() / 1000).append("), "); + sb.append(rs.getInt("killerid")).append(", "); + sb.append(rs.getInt("victimid")).append(", "); + sb.append(rs.getInt("weapon")).append(", "); + sb.append(rs.getInt("x")).append(", "); + sb.append(rs.getInt("y")).append(", "); + sb.append(rs.getInt("z")); + sb.append(");\n"); + } else if (params.bct == BlockChangeType.ENTITIES || params.bct == BlockChangeType.ENTITIES_CREATED || params.bct == BlockChangeType.ENTITIES_KILLED) { + + } else { + sb.append("INSERT INTO `").append(tableBase).append("-blocks` (`id`, `date`, `playerid`, `replaced`, `replacedData`, `type`, `typeData`, `x`, `y`, `z`) VALUES ("); + sb.append(rs.getLong("id")).append(", FROM_UNIXTIME("); + sb.append(rs.getTimestamp("date").getTime() / 1000).append("), "); + sb.append(rs.getInt("playerid")).append(", "); + sb.append(rs.getInt("replaced")).append(", "); + sb.append(rs.getInt("replacedData")).append(", "); + sb.append(rs.getInt("type")).append(", "); + sb.append(rs.getInt("typeData")).append(", "); + sb.append(rs.getInt("x")).append(", "); + sb.append(rs.getInt("y")).append(", "); + sb.append(rs.getInt("z")); + sb.append(");\n"); + byte[] replacedState = rs.getBytes("replacedState"); + byte[] typeState = rs.getBytes("typeState"); + if (replacedState != null || typeState != null) { + sb.append("INSERT INTO `").append(tableBase).append("-state` (`id`, `replacedState`, `typeState`) VALUES ("); + sb.append(rs.getLong("id")).append(", "); + sb.append(Utils.mysqlPrepareBytesForInsertAllowNull(replacedState)).append(", "); + sb.append(Utils.mysqlPrepareBytesForInsertAllowNull(typeState)); + sb.append(");\n"); + } + byte[] item = rs.getBytes("item"); + if (item != null) { + sb.append("INSERT INTO `").append(tableBase).append("-chestdata` (`id`, `item`, `itemremove`, `itemtype`) VALUES ("); + sb.append(rs.getLong("id")).append(", "); + sb.append(Utils.mysqlPrepareBytesForInsertAllowNull(item)).append(", "); + sb.append(rs.getInt("itemremove")).append(", "); + sb.append(rs.getInt("itemtype")); + sb.append(");\n"); + } + } + writer.write(sb.toString()); + } + rs.close(); + } finally { + writer.close(); + } + } catch (final SQLException ex) { + sender.sendMessage(ChatColor.RED + "Error while dumping log."); + logblock.getLogger().log(Level.SEVERE, "[ClearLog] Exception while dumping log: ", ex); + return; + } + } + if (deleted > 0) { + state.executeUpdate("DELETE " + deleteFromTables + params.getFrom() + params.getWhere()); + if (params.bct == BlockChangeType.ENTITIES || params.bct == BlockChangeType.ENTITIES_CREATED || params.bct == BlockChangeType.ENTITIES_KILLED) { + state.executeUpdate("DELETE `" + tableBase + "-entityids` FROM `" + tableBase + "-entityids` LEFT JOIN `" + tableBase + "-entities` USING (entityid) WHERE `" + tableBase + "-entities`.entityid IS NULL"); + } + } + sender.sendMessage(ChatColor.GREEN + "Cleared out table " + tableName + ". Deleted " + deleted + " entries."); + } catch (final Exception ex) { + if (logblock.isCompletelyEnabled() || !(ex instanceof SQLException)) { + sender.sendMessage(ChatColor.RED + "Exception, check error log"); + logblock.getLogger().log(Level.SEVERE, "[ClearLog] Exception: ", ex); + } + } finally { + close(); + } + } + } + + private ResultSet executeQuery(Statement state, String query) throws SQLException { + if (Config.debug) { + long startTime = System.currentTimeMillis(); + ResultSet rs = state.executeQuery(query); + logblock.getLogger().log(Level.INFO, "[LogBlock Debug] Time Taken: " + (System.currentTimeMillis() - startTime) + " milliseconds. Query: " + query); + return rs; + } else { + return state.executeQuery(query); + } + } + + private static List argsToList(String[] arr, int offset) { + final List list = new ArrayList<>(Arrays.asList(arr)); + for (int i = 0; i < offset; i++) { + list.remove(0); + } + return list; + } +} diff --git a/src/main/java/de/diddiz/LogBlock/Consumer.java b/src/main/java/de/diddiz/LogBlock/Consumer.java index aa84004e..b9b00049 100644 --- a/src/main/java/de/diddiz/LogBlock/Consumer.java +++ b/src/main/java/de/diddiz/LogBlock/Consumer.java @@ -1,1190 +1,1223 @@ -package de.diddiz.LogBlock; - -import static de.diddiz.LogBlock.Actor.actorFromString; -import de.diddiz.LogBlock.config.Config; -import de.diddiz.LogBlock.events.BlockChangePreLogEvent; -import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.block.BlockState; -import org.bukkit.block.Sign; -import org.bukkit.entity.Entity; -import org.bukkit.entity.Player; -import org.bukkit.entity.Projectile; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.InventoryHolder; -import org.bukkit.inventory.ItemStack; -import org.bukkit.projectiles.ProjectileSource; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.PrintWriter; -import java.sql.*; -import java.util.*; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; -import java.util.logging.Level; - -import static de.diddiz.LogBlock.config.Config.*; -import static de.diddiz.util.Utils.mysqlTextEscape; -import static de.diddiz.util.BukkitUtils.*; -import static org.bukkit.Bukkit.getLogger; - -public class Consumer extends TimerTask { - private final Queue queue = new LinkedBlockingQueue(); - private final Set failedPlayers = new HashSet(); - private final LogBlock logblock; - private final Map playerIds = new HashMap(); - private final Lock lock = new ReentrantLock(); - - Consumer(LogBlock logblock) { - this.logblock = logblock; - try { - Class.forName("PlayerLeaveRow"); - } catch (final ClassNotFoundException ex) { - } - } - - /** - * Logs any block change. Don't try to combine broken and placed blocks. Queue two block changes or use the queueBLockReplace methods. - * - * @param actor Actor responsible for making the change - * @param loc Location of the block change - * @param typeBefore Type of the block before the change - * @param typeAfter Type of the block after the change - * @param data Data of the block after the change - */ - public void queueBlock(Actor actor, Location loc, int typeBefore, int typeAfter, byte data) { - queueBlock(actor, loc, typeBefore, typeAfter, data, null, null); - } - - /** - * Logs a block break. The type afterwards is assumed to be 0 (air). - * - * @param actor Actor responsible for breaking the block - * @param before Blockstate of the block before actually being destroyed. - */ - public void queueBlockBreak(Actor actor, BlockState before) { - queueBlockBreak(actor, new Location(before.getWorld(), before.getX(), before.getY(), before.getZ()), before.getTypeId(), before.getRawData()); - } - - /** - * Logs a block break. The block type afterwards is assumed to be 0 (air). - * - * @param actor Actor responsible for the block break - * @param loc Location of the broken block - * @param typeBefore Type of the block before the break - * @param dataBefore Data of the block before the break - */ - public void queueBlockBreak(Actor actor, Location loc, int typeBefore, byte dataBefore) { - queueBlock(actor, loc, typeBefore, 0, dataBefore); - } - - /** - * Logs a block place. The block type before is assumed to be 0 (air). - * - * @param actor Actor responsible for placing the block - * @param after Blockstate of the block after actually being placed. - */ - public void queueBlockPlace(Actor actor, BlockState after) { - queueBlockPlace(actor, new Location(after.getWorld(), after.getX(), after.getY(), after.getZ()), after.getBlock().getTypeId(), after.getBlock().getData()); - } - - /** - * Logs a block place. The block type before is assumed to be 0 (air). - * - * @param actor Actor responsible for placing the block - * @param loc Location of the placed block - * @param type Type of the placed block - * @param data Data of the placed block - */ - public void queueBlockPlace(Actor actor, Location loc, int type, byte data) { - queueBlock(actor, loc, 0, type, data); - } - - /** - * Logs a block being replaced from the before and after {@link org.bukkit.block.BlockState}s - * - * @param actor Actor responsible for replacing the block - * @param before Blockstate of the block before actually being destroyed. - * @param after Blockstate of the block after actually being placed. - */ - public void queueBlockReplace(Actor actor, BlockState before, BlockState after) { - queueBlockReplace(actor, new Location(before.getWorld(), before.getX(), before.getY(), before.getZ()), before.getTypeId(), before.getRawData(), after.getTypeId(), after.getRawData()); - } - - /** - * Logs a block being replaced from the before {@link org.bukkit.block.BlockState} and the type and data after - * - * @param actor Actor responsible for replacing the block - * @param before Blockstate of the block before being replaced. - * @param typeAfter Type of the block after being replaced - * @param dataAfter Data of the block after being replaced - */ - public void queueBlockReplace(Actor actor, BlockState before, int typeAfter, byte dataAfter) { - queueBlockReplace(actor, new Location(before.getWorld(), before.getX(), before.getY(), before.getZ()), before.getTypeId(), before.getRawData(), typeAfter, dataAfter); - } - - /** - * Logs a block being replaced from the type and data before and the {@link org.bukkit.block.BlockState} after - * - * @param actor Actor responsible for replacing the block - * @param typeBefore Type of the block before being replaced - * @param dataBefore Data of the block before being replaced - * @param after Blockstate of the block after actually being placed. - */ - public void queueBlockReplace(Actor actor, int typeBefore, byte dataBefore, BlockState after) { - queueBlockReplace(actor, new Location(after.getWorld(), after.getX(), after.getY(), after.getZ()), typeBefore, dataBefore, after.getTypeId(), after.getRawData()); - } - - public void queueBlockReplace(Actor actor, Location loc, int typeBefore, byte dataBefore, int typeAfter, byte dataAfter) { - if (dataBefore == 0 && (typeBefore != typeAfter)) { - queueBlock(actor, loc, typeBefore, typeAfter, dataAfter); - } else { - queueBlockBreak(actor, loc, typeBefore, dataBefore); - queueBlockPlace(actor, loc, typeAfter, dataAfter); - } - } - - /** - * Logs an actor interacting with a container block's inventory - * - * @param actor The actor interacting with the container - * @param container The respective container. Must be an instance of an InventoryHolder. - * @param itemType Type of the item taken/stored - * @param itemAmount Amount of the item taken/stored - * @param itemData Data of the item taken/stored - */ - public void queueChestAccess(Actor actor, BlockState container, short itemType, short itemAmount, short itemData) { - if (!(container instanceof InventoryHolder)) { - throw new IllegalArgumentException("Container must be instanceof InventoryHolder"); - } - queueChestAccess(actor, new Location(container.getWorld(), container.getX(), container.getY(), container.getZ()), container.getTypeId(), itemType, itemAmount, itemData); - } - - /** - * Logs an actor interacting with a container block's inventory - * - * @param actor The actor interacting with the container - * @param loc The location of the container block - * @param type Type id of the container. - * @param itemType Type of the item taken/stored - * @param itemAmount Amount of the item taken/stored - * @param itemData Data of the item taken/stored - */ - public void queueChestAccess(Actor actor, Location loc, int type, short itemType, short itemAmount, short itemData) { - queueBlock(actor, loc, type, type, (byte) 0, null, new ChestAccess(itemType, itemAmount, itemData)); - } - - /** - * Logs a container block break. The block type before is assumed to be o (air). All content is assumed to be taken. - * - * @param actor The actor breaking the container - * @param container Must be an instance of InventoryHolder - */ - public void queueContainerBreak(Actor actor, BlockState container) { - if (!(container instanceof InventoryHolder)) { - return; - } - queueContainerBreak(actor, new Location(container.getWorld(), container.getX(), container.getY(), container.getZ()), container.getTypeId(), container.getRawData(), ((InventoryHolder) container).getInventory()); - } - - /** - * Logs a container block break. The block type before is assumed to be o (air). All content is assumed to be taken. - * - * @param actor The actor responsible for breaking the container - * @param loc The location of the inventory block - * @param type The type of the container block - * @param data The data of the container block - * @param inv The inventory of the container block - */ - public void queueContainerBreak(Actor actor, Location loc, int type, byte data, Inventory inv) { - final ItemStack[] items = compressInventory(inv.getContents()); - for (final ItemStack item : items) { - queueChestAccess(actor, loc, type, (short) item.getTypeId(), (short) (item.getAmount() * -1), rawData(item)); - } - queueBlockBreak(actor, loc, type, data); - } - - /** - * @param killer Can't be null - * @param victim Can't be null - */ - public void queueKill(Entity killer, Entity victim) { - if (killer == null || victim == null) { - return; - } - int weapon = 0; - Actor killerActor = Actor.actorFromEntity(killer); - // If it's a projectile kill we want to manually assign the weapon, so check for player before converting a projectile to its source - if (killer instanceof Player && ((Player) killer).getItemInHand() != null) { - weapon = ((Player) killer).getItemInHand().getTypeId(); - } - if (killer instanceof Projectile) { - weapon = itemIDfromProjectileEntity(killer); - ProjectileSource ps = ((Projectile) killer).getShooter(); - if (ps == null) { - killerActor = Actor.actorFromEntity(killer); - } else { - killerActor = Actor.actorFromProjectileSource(ps); - } - } - - queueKill(victim.getLocation(), killerActor, Actor.actorFromEntity(victim), weapon); - } - - /** - * This form should only be used when the killer is not an entity e.g. for fall or suffocation damage - * - * @param killer Can't be null - * @param victim Can't be null - */ - public void queueKill(Actor killer, Entity victim) { - if (killer == null || victim == null) { - return; - } - queueKill(victim.getLocation(), killer, Actor.actorFromEntity(victim), 0); - } - - /** - * @param world World the victim was inside. - * @param killer Name of the killer. Can be null. - * @param victim Name of the victim. Can't be null. - * @param weapon Item id of the weapon. 0 for no weapon. - * @deprecated Use {@link #queueKill(org.bukkit.Location, de.diddiz.LogBlock.Actor, de.diddiz.LogBlock.Actor, int)} - * instead - */ - @Deprecated - public void queueKill(World world, Actor killer, Actor victim, int weapon) { - queueKill(new Location(world, 0, 0, 0), killer, victim, weapon); - } - - /** - * @param location Location of the victim. - * @param killer Killer Actor. Can be null. - * @param victim Victim Actor. Can't be null. - * @param weapon Item id of the weapon. 0 for no weapon. - */ - public void queueKill(Location location, Actor killer, Actor victim, int weapon) { - if (victim == null || !isLogged(location.getWorld())) { - return; - } - queue.add(new KillRow(location, killer == null ? null : killer, victim, weapon)); - } - - /** - * Logs an actor breaking a sign along with its contents - * - * @param actor Actor responsible for breaking the sign - * @param loc Location of the broken sign - * @param type Type of the sign. Must be 63 or 68. - * @param data Data of the sign being broken - * @param lines The four lines on the sign. - */ - public void queueSignBreak(Actor actor, Location loc, int type, byte data, String[] lines) { - if (type != 63 && type != 68 || lines == null || lines.length != 4) { - return; - } - queueBlock(actor, loc, type, 0, data, lines[0] + "\0" + lines[1] + "\0" + lines[2] + "\0" + lines[3], null); - } - - /** - * Logs an actor breaking a sign along with its contents - * - * @param actor Actor responsible for breaking the sign - * @param sign The sign being broken - */ - public void queueSignBreak(Actor actor, Sign sign) { - queueSignBreak(actor, new Location(sign.getWorld(), sign.getX(), sign.getY(), sign.getZ()), sign.getTypeId(), sign.getRawData(), sign.getLines()); - } - - /** - * Logs an actor placing a sign along with its contents - * - * @param actor Actor placing the sign - * @param loc Location of the placed sign - * @param type Type of the sign. Must be 63 or 68. - * @param data Data of the placed sign block - * @param lines The four lines on the sign. - */ - public void queueSignPlace(Actor actor, Location loc, int type, byte data, String[] lines) { - if (type != 63 && type != 68 || lines == null || lines.length != 4) { - return; - } - queueBlock(actor, loc, 0, type, data, lines[0] + "\0" + lines[1] + "\0" + lines[2] + "\0" + lines[3], null); - } - - /** - * Logs an actor placing a sign along with its contents - * - * @param actor Actor placing the sign - * @param sign The palced sign object - */ - public void queueSignPlace(Actor actor, Sign sign) { - queueSignPlace(actor, new Location(sign.getWorld(), sign.getX(), sign.getY(), sign.getZ()), sign.getTypeId(), sign.getRawData(), sign.getLines()); - } - - public void queueChat(Actor player, String message) { - for (String ignored : Config.ignoredChat) { - if (message.startsWith(ignored)) { - return; - } - } - if (hiddenPlayers.contains(player.getName().toLowerCase())) { - return; - } - queue.add(new ChatRow(player, message)); - } - - public void queueJoin(Player player) { - queue.add(new PlayerJoinRow(player)); - } - - public void queueLeave(Player player) { - queue.add(new PlayerLeaveRow(player)); - } - - // Deprecated methods re-added for API compatability - - /** - * Logs any block change. Don't try to combine broken and placed blocks. - * Queue two block changes or use the queueBLockReplace methods. - * - * @deprecated Use - * {@link #queueBlock(de.diddiz.LogBlock.Actor, org.bukkit.Location, int, int, byte)} - * which supports UUIDs - */ - public void queueBlock(String playerName, Location loc, int typeBefore, int typeAfter, byte data) { - queueBlock(actorFromString(playerName), loc, typeBefore, typeAfter, data); - } - - /** - * Logs a block break. The type afterwards is assumed to be 0 (air). - * - * @param before Blockstate of the block before actually being destroyed. - * @deprecated Use - * {@link #queueBlockBreak(de.diddiz.LogBlock.Actor, org.bukkit.block.BlockState)} - * which supports UUIDs - */ - public void queueBlockBreak(String playerName, BlockState before) { - queueBlockBreak(actorFromString(playerName), before); - - } - - /** - * Logs a block break. The block type afterwards is assumed to be 0 (air). - * - * @deprecated Use {@link #queueBlockBreak(de.diddiz.LogBlock.Actor, org.bukkit.Location, int, byte)} - * which supports UUIDs - */ - public void queueBlockBreak(String playerName, Location loc, int typeBefore, byte dataBefore) { - queueBlockBreak(actorFromString(playerName), loc, typeBefore, dataBefore); - } - - /** - * Logs a block place. The block type before is assumed to be 0 (air). - * - * @param after Blockstate of the block after actually being placed. - * @depracated Use {@link #queueBlockPlace(de.diddiz.LogBlock.Actor, org.bukkit.block.BlockState)} - * which supports UUIDs - */ - public void queueBlockPlace(String playerName, BlockState after) { - queueBlockPlace(actorFromString(playerName), after); - } - - /** - * Logs a block place. The block type before is assumed to be 0 (air). - * @deprecated Use {@link #queueBlockPlace(de.diddiz.LogBlock.Actor, org.bukkit.Location, int, byte)} - * which supports UUIDs - */ - public void queueBlockPlace(String playerName, Location loc, int type, byte data) { - queueBlockPlace(actorFromString(playerName), loc, type, data); - } - - /** - * @param before Blockstate of the block before actually being destroyed. - * @param after Blockstate of the block after actually being placed. - * @deprecated Use {@link #queueBlockReplace(de.diddiz.LogBlock.Actor, org.bukkit.block.BlockState, org.bukkit.block.BlockState)} - * which supports UUIDs - */ - public void queueBlockReplace(String playerName, BlockState before, BlockState after) { - queueBlockReplace(actorFromString(playerName), before, after); - } - - /** - * @param before Blockstate of the block before actually being destroyed. - * @deprecated Use {@link #queueBlockReplace(de.diddiz.LogBlock.Actor, org.bukkit.block.BlockState, int, byte)} - * which supports UUIDs - */ - public void queueBlockReplace(String playerName, BlockState before, int typeAfter, byte dataAfter) { - queueBlockReplace(actorFromString(playerName), before, typeAfter, dataAfter); - } - - /** - * @param after Blockstate of the block after actually being placed. - * @deprecated {@link #queueBlockReplace(de.diddiz.LogBlock.Actor, int, byte, org.bukkit.block.BlockState)} - * which supports UUIDs - */ - public void queueBlockReplace(String playerName, int typeBefore, byte dataBefore, BlockState after) { - queueBlockReplace(actorFromString(playerName), typeBefore, dataBefore, after); - } - - /** - * @deprecated use {@link #queueBlockReplace(de.diddiz.LogBlock.Actor, org.bukkit.Location, int, byte, int, byte)} - * which supports UUIDs - */ - public void queueBlockReplace(String playerName, Location loc, int typeBefore, byte dataBefore, int typeAfter, byte dataAfter) { - queueBlockReplace(actorFromString(playerName),loc,typeBefore,dataBefore,typeAfter,dataAfter); - } - - /** - * @param container The respective container. Must be an instance of an - * InventoryHolder. - * @deprecated Use {@link #queueChestAccess(de.diddiz.LogBlock.Actor, org.bukkit.block.BlockState, short, short, short)} - * which supports UUIDs - */ - public void queueChestAccess(String playerName, BlockState container, short itemType, short itemAmount, short itemData) { - queueChestAccess(actorFromString(playerName),container,itemType,itemAmount,itemData); - } - - /** - * @param type Type id of the container. - * @deprecated Use {@link #queueChestAccess(de.diddiz.LogBlock.Actor, org.bukkit.Location, int, short, short, short)} - * which supports UUIDs - */ - public void queueChestAccess(String playerName, Location loc, int type, short itemType, short itemAmount, short itemData) { - queueChestAccess(actorFromString(playerName), loc, type, itemType, itemAmount, itemData); - } - - /** - * Logs a container block break. The block type before is assumed to be o - * (air). All content is assumed to be taken. - * - * @param container Must be an instance of InventoryHolder - * @deprecated Use {@link #queueContainerBreak(de.diddiz.LogBlock.Actor, org.bukkit.block.BlockState)} - * which supports UUIDs - */ - public void queueContainerBreak(String playerName, BlockState container) { - queueContainerBreak(actorFromString(playerName), container); - } - - /** - * Logs a container block break. The block type before is assumed to be o - * (air). All content is assumed to be taken. - * @deprecated Use {@link #queueContainerBreak(de.diddiz.LogBlock.Actor, org.bukkit.Location, int, byte, org.bukkit.inventory.Inventory)} - * which supports UUIDs - */ - public void queueContainerBreak(String playerName, Location loc, int type, byte data, Inventory inv) { - queueContainerBreak(actorFromString(playerName),loc,type,data,inv); - } - - /** - * This form should only be used when the killer is not an entity e.g. for - * fall or suffocation damage - * - * @param killer Can't be null - * @param victim Can't be null - * @deprecated Use {@link #queueKill(de.diddiz.LogBlock.Actor, org.bukkit.entity.Entity)} - * which supports UUIDs - */ - public void queueKill(String killer, Entity victim) { - queueKill(actorFromString(killer),victim); - } - - /** - * @param world World the victim was inside. - * @param killerName Name of the killer. Can be null. - * @param victimName Name of the victim. Can't be null. - * @param weapon Item id of the weapon. 0 for no weapon. - * @deprecated Use {@link #queueKill(org.bukkit.Location, de.diddiz.LogBlock.Actor, de.diddiz.LogBlock.Actor, int)} instead - */ - @Deprecated - public void queueKill(World world, String killerName, String victimName, int weapon) { - queueKill(world,actorFromString(killerName),actorFromString(victimName),weapon); - } - - /** - * @param location Location of the victim. - * @param killerName Name of the killer. Can be null. - * @param victimName Name of the victim. Can't be null. - * @param weapon Item id of the weapon. 0 for no weapon. - * @deprecated Use {@link #queueKill(org.bukkit.Location, de.diddiz.LogBlock.Actor, de.diddiz.LogBlock.Actor, int)} - * which supports UUIDs - */ - public void queueKill(Location location, String killerName, String victimName, int weapon) { - queueKill(location,actorFromString(killerName),actorFromString(victimName),weapon); - } - - /** - * @param type Type of the sign. Must be 63 or 68. - * @param lines The four lines on the sign. - * @deprecated Use {@link #queueSignBreak(de.diddiz.LogBlock.Actor, org.bukkit.Location, int, byte, java.lang.String[])} - * which supports UUIDs - */ - public void queueSignBreak(String playerName, Location loc, int type, byte data, String[] lines) { - queueSignBreak(actorFromString(playerName),loc,type,data,lines); - } - - /** - * @deprecated Use {@link #queueSignBreak(de.diddiz.LogBlock.Actor, org.bukkit.block.Sign)} - * which supports UUIDs - */ - public void queueSignBreak(String playerName, Sign sign) { - queueSignBreak(actorFromString(playerName),sign); - } - - /** - * @param type Type of the sign. Must be 63 or 68. - * @param lines The four lines on the sign. - * @deprecated Use {@link #queueSignPlace(de.diddiz.LogBlock.Actor, org.bukkit.Location, int, byte, java.lang.String[])} - * which supports UUIDs - */ - public void queueSignPlace(String playerName, Location loc, int type, byte data, String[] lines) { - queueSignPlace(actorFromString(playerName),loc,type,data,lines); - } - - /** - * @deprecated Use {@link #queueSignPlace(de.diddiz.LogBlock.Actor, org.bukkit.block.Sign)} - * which supports UUIDs - */ - public void queueSignPlace(String playerName, Sign sign) { - queueSignPlace(actorFromString(playerName),sign); - } -/** - * @deprecated Use {@link #queueChat(de.diddiz.LogBlock.Actor, java.lang.String)} - * which supports UUIDs - */ - public void queueChat(String player, String message) { - queueChat(actorFromString(player),message); - } - - @Override - public synchronized void run() { - if (queue.isEmpty() || !lock.tryLock()) { - return; - } - long startTime = System.currentTimeMillis(); - int startSize = queue.size(); - - final Connection conn = logblock.getConnection(); - Statement state = null; - if (Config.queueWarningSize > 0 && queue.size() >= Config.queueWarningSize) { - getLogger().info("[Consumer] Queue overloaded. Size: " + getQueueSize()); - } - - int count = 0; - - try { - if (conn == null) { - return; - } - conn.setAutoCommit(false); - state = conn.createStatement(); - final long start = System.currentTimeMillis(); - process: - while (!queue.isEmpty() && (System.currentTimeMillis() - start < timePerRun || count < forceToProcessAtLeast)) { - final Row r = queue.poll(); - if (r == null) { - continue; - } - for (final Actor actor : r.getActors()) { - if (!playerIds.containsKey(actor)) { - if (!addPlayer(state, actor)) { - if (!failedPlayers.contains(actor)) { - failedPlayers.add(actor); - getLogger().warning("[Consumer] Failed to add player " + actor.getName()); - } - continue process; - } - } - } - if (r instanceof PreparedStatementRow) { - PreparedStatementRow PSRow = (PreparedStatementRow) r; - if (r instanceof MergeableRow) { - int batchCount = count; - // if we've reached our row target but not exceeded our time target, allow merging of up to 50% of our row limit more rows - if (count > forceToProcessAtLeast) { - batchCount = forceToProcessAtLeast / 2; - } - while (!queue.isEmpty()) { - MergeableRow mRow = (MergeableRow) PSRow; - Row s = queue.peek(); - if (s == null) { - break; - } - if (!(s instanceof MergeableRow)) { - break; - } - MergeableRow mRow2 = (MergeableRow) s; - if (mRow.canMerge(mRow2)) { - PSRow = mRow.merge((MergeableRow) queue.poll()); - count++; - batchCount++; - if (batchCount > forceToProcessAtLeast) { - break; - } - } else { - break; - } - } - } - PSRow.setConnection(conn); - try { - PSRow.executeStatements(); - } catch (final SQLException ex) { - getLogger().log(Level.SEVERE, "[Consumer] SQL exception on insertion: ", ex); - break; - } - } else { - for (final String insert : r.getInserts()) { - try { - state.execute(insert); - } catch (final SQLException ex) { - getLogger().log(Level.SEVERE, "[Consumer] SQL exception on " + insert + ": ", ex); - break process; - } - } - } - - count++; - } - conn.commit(); - } catch (final SQLException ex) { - getLogger().log(Level.SEVERE, "[Consumer] SQL exception", ex); - } finally { - try { - if (state != null) { - state.close(); - } - if (conn != null) { - conn.close(); - } - } catch (final SQLException ex) { - getLogger().log(Level.SEVERE, "[Consumer] SQL exception on close", ex); - } - lock.unlock(); - - if (debug) { - long timeElapsed = System.currentTimeMillis() - startTime; - float rowPerTime = count / timeElapsed; - getLogger().log(Level.INFO, "[Consumer] Finished consumer cycle in " + timeElapsed + " milliseconds."); - getLogger().log(Level.INFO, "[Consumer] Total rows processed: " + count + ". row/time: " + String.format("%.4f", rowPerTime)); - } - } - } - - public void writeToFile() throws FileNotFoundException { - final long time = System.currentTimeMillis(); - final Set insertedPlayers = new HashSet(); - int counter = 0; - new File("plugins/LogBlock/import/").mkdirs(); - PrintWriter writer = new PrintWriter(new File("plugins/LogBlock/import/queue-" + time + "-0.sql")); - while (!queue.isEmpty()) { - final Row r = queue.poll(); - if (r == null) { - continue; - } - for (final Actor actor : r.getActors()) { - if (!playerIds.containsKey(actor) && !insertedPlayers.contains(actor)) { - // Odd query contruction is to work around innodb auto increment behaviour - bug #492 - writer.println("INSERT IGNORE INTO `lb-players` (playername,UUID) SELECT '" + mysqlTextEscape(actor.getName()) + "','" + actor.getUUID() + "' FROM `lb-players` WHERE NOT EXISTS (SELECT NULL FROM `lb-players` WHERE UUID = '" + actor.getUUID() + "') LIMIT 1;"); - insertedPlayers.add(actor); - } - } - for (final String insert : r.getInserts()) { - writer.println(insert); - } - counter++; - if (counter % 1000 == 0) { - writer.close(); - writer = new PrintWriter(new File("plugins/LogBlock/import/queue-" + time + "-" + counter / 1000 + ".sql")); - } - } - writer.close(); - } - - int getQueueSize() { - return queue.size(); - } - - static void hide(Player player) { - hiddenPlayers.add(player.getName().toLowerCase()); - } - - static void unHide(Player player) { - hiddenPlayers.remove(player.getName().toLowerCase()); - } - - static boolean toggleHide(Player player) { - final String playerName = player.getName().toLowerCase(); - if (hiddenPlayers.contains(playerName)) { - hiddenPlayers.remove(playerName); - return false; - } - hiddenPlayers.add(playerName); - return true; - } - - private boolean addPlayer(Statement state, Actor actor) throws SQLException { - // Odd query contruction is to work around innodb auto increment behaviour - bug #492 - String name = actor.getName(); - String uuid = actor.getUUID(); - state.execute("INSERT IGNORE INTO `lb-players` (playername,UUID) SELECT '" + mysqlTextEscape(name) + "','" + uuid + "' FROM `lb-players` WHERE NOT EXISTS (SELECT NULL FROM `lb-players` WHERE UUID = '" + uuid + "') LIMIT 1;"); - final ResultSet rs = state.executeQuery("SELECT playerid FROM `lb-players` WHERE UUID = '" + uuid + "'"); - if (rs.next()) { - playerIds.put(actor, rs.getInt(1)); - } - rs.close(); - return playerIds.containsKey(actor); - } - - private void queueBlock(Actor actor, Location loc, int typeBefore, int typeAfter, byte data, String signtext, ChestAccess ca) { - - if (Config.fireCustomEvents) { - // Create and call the event - BlockChangePreLogEvent event = new BlockChangePreLogEvent(actor, loc, typeBefore, typeAfter, data, signtext, ca); - logblock.getServer().getPluginManager().callEvent(event); - if (event.isCancelled()) { - return; - } - - // Update variables - actor = event.getOwnerActor(); - loc = event.getLocation(); - typeBefore = event.getTypeBefore(); - typeAfter = event.getTypeAfter(); - data = event.getData(); - signtext = event.getSignText(); - ca = event.getChestAccess(); - } - // Do this last so LogBlock still has final say in what is being added - if (actor == null || loc == null || typeBefore < 0 || typeAfter < 0 || (Config.safetyIdCheck && (typeBefore > 255 || typeAfter > 255)) || hiddenPlayers.contains(actor.getName().toLowerCase()) || !isLogged(loc.getWorld()) || typeBefore != typeAfter && hiddenBlocks.contains(typeBefore) && hiddenBlocks.contains(typeAfter)) { - return; - } - queue.add(new BlockRow(loc, actor, typeBefore, typeAfter, data, signtext, ca)); - } - - private String playerID(Actor actor) { - if (actor == null) { - return "NULL"; - } - final Integer id = playerIds.get(actor); - if (id != null) { - return id.toString(); - } - return "(SELECT playerid FROM `lb-players` WHERE UUID = '" + actor.getUUID() + "')"; - } - - private Integer playerIDAsInt(Actor actor) { - if (actor == null) { - return null; - } - return playerIds.get(actor); - } - - private static interface Row { - String[] getInserts(); - - /** - * @deprecated - Names are not guaranteed to be unique. Use {@link #getActors() } - */ - String[] getPlayers(); - - Actor[] getActors(); - } - - private interface PreparedStatementRow extends Row { - - abstract void setConnection(Connection connection); - - abstract void executeStatements() throws SQLException; - } - - private interface MergeableRow extends PreparedStatementRow { - abstract boolean isUnique(); - - abstract boolean canMerge(MergeableRow row); - - abstract MergeableRow merge(MergeableRow second); - } - - private class BlockRow extends BlockChange implements MergeableRow { - private Connection connection; - - public BlockRow(Location loc, Actor actor, int replaced, int type, byte data, String signtext, ChestAccess ca) { - super(System.currentTimeMillis() / 1000, loc, actor, replaced, type, data, signtext, ca); - } - - @Override - public String[] getInserts() { - final String table = getWorldConfig(loc.getWorld()).table; - final String[] inserts = new String[ca != null || signtext != null ? 2 : 1]; - inserts[0] = "INSERT INTO `" + table + "` (date, playerid, replaced, type, data, x, y, z) VALUES (FROM_UNIXTIME(" + date + "), " + playerID(actor) + ", " + replaced + ", " + type + ", " + data + ", '" + loc.getBlockX() + "', " + safeY(loc) + ", '" + loc.getBlockZ() + "');"; - if (signtext != null) { - inserts[1] = "INSERT INTO `" + table + "-sign` (id, signtext) values (LAST_INSERT_ID(), '" + mysqlTextEscape(signtext) + "');"; - } else if (ca != null) { - inserts[1] = "INSERT INTO `" + table + "-chest` (id, itemtype, itemamount, itemdata) values (LAST_INSERT_ID(), " + ca.itemType + ", " + ca.itemAmount + ", " + ca.itemData + ");"; - } - return inserts; - } - - @Override - public String[] getPlayers() { - return new String[]{actor.getName()}; - } - - @Override - public Actor[] getActors() { - return new Actor[]{actor}; - } - - @Override - public void setConnection(Connection connection) { - this.connection = connection; - } - - @Override - public void executeStatements() throws SQLException { - final String table = getWorldConfig(loc.getWorld()).table; - - PreparedStatement ps1 = null; - PreparedStatement ps = null; - try { - ps1 = connection.prepareStatement("INSERT INTO `" + table + "` (date, playerid, replaced, type, data, x, y, z) VALUES(FROM_UNIXTIME(?), " + playerID(actor) + ", ?, ?, ?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS); - ps1.setLong(1, date); - ps1.setInt(2, replaced); - ps1.setInt(3, type); - ps1.setInt(4, data); - ps1.setInt(5, loc.getBlockX()); - ps1.setInt(6, safeY(loc)); - ps1.setInt(7, loc.getBlockZ()); - ps1.executeUpdate(); - - int id; - ResultSet rs = ps1.getGeneratedKeys(); - rs.next(); - id = rs.getInt(1); - - if (signtext != null) { - ps = connection.prepareStatement("INSERT INTO `" + table + "-sign` (signtext, id) VALUES(?, ?)"); - ps.setString(1, signtext); - ps.setInt(2, id); - ps.executeUpdate(); - } else if (ca != null) { - ps = connection.prepareStatement("INSERT INTO `" + table + "-chest` (itemtype, itemamount, itemdata, id) values (?, ?, ?, ?)"); - ps.setInt(1, ca.itemType); - ps.setInt(2, ca.itemAmount); - ps.setInt(3, ca.itemData); - ps.setInt(4, id); - ps.executeUpdate(); - } - } catch (final SQLException ex) { - if (ps1 != null) { - getLogger().log(Level.SEVERE, "[Consumer] Troublesome query: " + ps1.toString()); - } - if (ps != null) { - getLogger().log(Level.SEVERE, "[Consumer] Troublesome query: " + ps.toString()); - } - throw ex; - } finally { - // individual try/catch here, though ugly, prevents resource leaks - if (ps1 != null) { - try { - ps1.close(); - } catch (SQLException e) { - // ideally should log to logger, none is available in this class - // at the time of this writing, so I'll leave that to the plugin - // maintainers to integrate if they wish - e.printStackTrace(); - } - } - - if (ps != null) { - try { - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - } - } - - @Override - public boolean isUnique() { - return !(signtext == null && ca == null && playerIds.containsKey(actor)); - } - - @Override - public boolean canMerge(MergeableRow row) { - return !this.isUnique() && !row.isUnique() && row instanceof BlockRow && getWorldConfig(loc.getWorld()).table.equals(getWorldConfig(((BlockRow) row).loc.getWorld()).table); - } - - @Override - public MergeableRow merge(MergeableRow singleRow) { - return new MultiBlockChangeRow(this, (BlockRow) singleRow); - } - } - - private class MultiBlockChangeRow implements MergeableRow { - private List rows = new ArrayList(); - private Connection connection; - private Set players = new HashSet(); - private Set actors = new HashSet(); - private String table; - - MultiBlockChangeRow(BlockRow first, BlockRow second) { - if (first.isUnique() || second.isUnique()) { - throw new IllegalArgumentException("Can't merge a unique row"); - } - rows.add(first); - rows.add(second); - actors.addAll(Arrays.asList(first.getActors())); - actors.addAll(Arrays.asList(second.getActors())); - players.addAll(Arrays.asList(first.getPlayers())); - players.addAll(Arrays.asList(second.getPlayers())); - table = getWorldConfig(first.loc.getWorld()).table; - } - - @Override - public void setConnection(Connection connection) { - this.connection = connection; - } - - @Override - public void executeStatements() throws SQLException { - PreparedStatement ps = null; - try { - ps = connection.prepareStatement("INSERT INTO `" + table + "` (date, playerid, replaced, type, data, x, y, z) VALUES(FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?)"); - for (BlockRow row : rows) { - ps.setLong(1, row.date); - ps.setInt(2, playerIds.get(row.actor)); - ps.setInt(3, row.replaced); - ps.setInt(4, row.type); - ps.setInt(5, row.data); - ps.setInt(6, row.loc.getBlockX()); - ps.setInt(7, safeY(row.loc)); - ps.setInt(8, row.loc.getBlockZ()); - ps.addBatch(); - } - ps.executeBatch(); - } catch (final SQLException ex) { - if (ps != null) { - getLogger().log(Level.SEVERE, "[Consumer] Troublesome query: " + ps.toString()); - } - throw ex; - } finally { - // individual try/catch here, though ugly, prevents resource leaks - if (ps != null) { - try { - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - } - } - - @Override - public boolean isUnique() { - return true; - } - - @Override - public boolean canMerge(MergeableRow row) { - return !row.isUnique() && row instanceof BlockRow && table.equals(getWorldConfig(((BlockRow) row).loc.getWorld()).table); - } - - @Override - public MergeableRow merge(MergeableRow second) { - if (second.isUnique()) { - throw new IllegalArgumentException("Can't merge a unique row"); - } - rows.add((BlockRow) second); - actors.addAll(Arrays.asList(second.getActors())); - players.addAll(Arrays.asList(second.getPlayers())); - return this; - } - - @Override - public String[] getInserts() { - List l = new ArrayList(); - for (BlockRow row : rows) { - l.addAll(Arrays.asList(row.getInserts())); - } - return (String[]) l.toArray(); - } - - @Override - public String[] getPlayers() { - return (String[]) players.toArray(); - } - - @Override - public Actor[] getActors() { - return (Actor[]) actors.toArray(); - } - } - - private class KillRow implements Row { - final long date; - final Actor killer, victim; - final int weapon; - final Location loc; - - KillRow(Location loc, Actor attacker, Actor defender, int weapon) { - date = System.currentTimeMillis() / 1000; - this.loc = loc; - killer = attacker; - victim = defender; - this.weapon = weapon; - } - - @Override - public String[] getInserts() { - return new String[]{"INSERT INTO `" + getWorldConfig(loc.getWorld()).table + "-kills` (date, killer, victim, weapon, x, y, z) VALUES (FROM_UNIXTIME(" + date + "), " + playerID(killer) + ", " + playerID(victim) + ", " + weapon + ", " + loc.getBlockX() + ", " + safeY(loc) + ", " + loc.getBlockZ() + ");"}; - } - - @Override - public String[] getPlayers() { - return new String[]{killer.getName(), victim.getName()}; - } - - @Override - public Actor[] getActors() { - return new Actor[]{killer, victim}; - } - } - - private class ChatRow extends ChatMessage implements PreparedStatementRow { - private Connection connection; - - ChatRow(Actor player, String message) { - super(player, message); - } - - @Override - public String[] getInserts() { - return new String[]{"INSERT INTO `lb-chat` (date, playerid, message) VALUES (FROM_UNIXTIME(" + date + "), " + playerID(player) + ", '" + mysqlTextEscape(message) + "');"}; - } - - @Override - public String[] getPlayers() { - return new String[]{player.getName()}; - } - - @Override - public Actor[] getActors() { - return new Actor[]{player}; - } - - @Override - public void setConnection(Connection connection) { - this.connection = connection; - } - - @Override - public void executeStatements() throws SQLException { - boolean noID = false; - Integer id; - - String sql = "INSERT INTO `lb-chat` (date, playerid, message) VALUES (FROM_UNIXTIME(?), "; - if ((id = playerIDAsInt(player)) == null) { - noID = true; - sql += playerID(player) + ", "; - } else { - sql += "?, "; - } - sql += "?)"; - - PreparedStatement ps = null; - try { - ps = connection.prepareStatement(sql); - ps.setLong(1, date); - if (!noID) { - ps.setInt(2, id); - ps.setString(3, message); - } else { - ps.setString(2, message); - } - ps.execute(); - } - // we intentionally do not catch SQLException, it is thrown to the caller - finally { - if (ps != null) { - try { - ps.close(); - } catch (SQLException e) { - // should print to a Logger instead if one is ever added to this class - e.printStackTrace(); - } - } - } - } - } - - private class PlayerJoinRow implements Row { - private final Actor player; - private final long lastLogin; - private final String ip; - - PlayerJoinRow(Player player) { - this.player = Actor.actorFromEntity(player); - lastLogin = System.currentTimeMillis() / 1000; - ip = player.getAddress().toString().replace("'", "\\'"); - } - - @Override - public String[] getInserts() { - if (logPlayerInfo) { - return new String[]{"UPDATE `lb-players` SET lastlogin = FROM_UNIXTIME(" + lastLogin + "), firstlogin = IF(firstlogin = 0, FROM_UNIXTIME(" + lastLogin + "), firstlogin), ip = '" + ip + "', playername = '" + mysqlTextEscape(player.getName()) + "' WHERE UUID = '" + player.getUUID() + "';"}; - } - return new String[]{"UPDATE `lb-players` SET playername = '" + mysqlTextEscape(player.getName()) + "' WHERE UUID = '" + player.getUUID() + "';"}; - } - - @Override - public String[] getPlayers() { - return new String[]{player.getName()}; - } - - @Override - public Actor[] getActors() { - return new Actor[]{player}; - } - } - - private class PlayerLeaveRow implements Row { - ; - private final long leaveTime; - private final Actor actor; - - PlayerLeaveRow(Player player) { - leaveTime = System.currentTimeMillis() / 1000; - actor = Actor.actorFromEntity(player); - } - - @Override - public String[] getInserts() { - if (logPlayerInfo) { - return new String[]{"UPDATE `lb-players` SET onlinetime = onlinetime + TIMESTAMPDIFF(SECOND, lastlogin, FROM_UNIXTIME('" + leaveTime + "')), playername = '" + mysqlTextEscape(actor.getName()) + "' WHERE lastlogin > 0 && UUID = '" + actor.getUUID() + "';"}; - } - return new String[]{"UPDATE `lb-players` SET playername = '" + mysqlTextEscape(actor.getName()) + "' WHERE UUID = '" + actor.getUUID() + "';"}; - } - - @Override - public String[] getPlayers() { - return new String[]{actor.getName()}; - } - - @Override - public Actor[] getActors() { - return new Actor[]{actor}; - } - } - - private int safeY(Location loc) { - int safeY = loc.getBlockY(); - if (safeY<0) safeY = 0; - if (safeY>65535) safeY=65535; - return safeY; - } -} +package de.diddiz.LogBlock; + +import static de.diddiz.LogBlock.config.Config.getWorldConfig; +import static de.diddiz.LogBlock.config.Config.hiddenBlocks; +import static de.diddiz.LogBlock.config.Config.hiddenPlayers; +import static de.diddiz.LogBlock.config.Config.isLogged; +import static de.diddiz.LogBlock.config.Config.logPlayerInfo; +import static de.diddiz.LogBlock.util.BukkitUtils.compressInventory; +import static de.diddiz.LogBlock.util.BukkitUtils.itemIDfromProjectileEntity; +import static de.diddiz.LogBlock.util.Utils.mysqlTextEscape; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.UUID; +import java.util.logging.Level; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.BlockState; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.Waterlogged; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.entity.Projectile; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.ItemStack; +import org.bukkit.projectiles.ProjectileSource; + +import de.diddiz.LogBlock.EntityChange.EntityChangeType; +import de.diddiz.LogBlock.blockstate.BlockStateCodecs; +import de.diddiz.LogBlock.config.Config; +import de.diddiz.LogBlock.events.BlockChangePreLogEvent; +import de.diddiz.LogBlock.events.EntityChangePreLogEvent; +import de.diddiz.LogBlock.util.BukkitUtils; +import de.diddiz.LogBlock.util.ItemStackAndAmount; +import de.diddiz.LogBlock.util.Utils; + +public class Consumer extends Thread { + private static final int MAX_SHUTDOWN_TIME_MILLIS = 20000; + private static final int WAIT_FOR_CONNECTION_TIME_MILLIS = 10000; + private static final int RETURN_IDLE_CONNECTION_TIME_MILLIS = 120000; + private static final int RETRIES_ON_UNKNOWN_CONNECTION_ERROR = 2; + + private final Deque queue = new ArrayDeque<>(); + private final LogBlock logblock; + private final Map playerIds = new HashMap<>(); + private final Map uncommitedPlayerIds = new HashMap<>(); + private final Map> uncommitedEntityIds = new HashMap<>(); + + private long addEntryCounter; + private long nextWarnCounter; + + private boolean shutdown; + private long shutdownInitialized; + + Consumer(LogBlock logblock) { + this.logblock = logblock; + PlayerLeaveRow.class.getName(); // preload this class + setName("Logblock-Consumer"); + } + + public LogBlock getLogblock() { + return logblock; + } + + /** + * Logs any block change. Don't try to combine broken and placed blocks. Queue two block changes or use the queueBLockReplace methods. + * + * @param actor + * Actor responsible for making the change + * @param loc + * Location of the block change + * @param typeBefore + * BlockData of the block before the change + * @param typeAfter + * BlockData of the block after the change + */ + public void queueBlock(Actor actor, Location loc, BlockData typeBefore, BlockData typeAfter) { + queueBlock(actor, loc, typeBefore, typeAfter, null, null, null); + } + + /** + * Logs a block break. The type afterwards is assumed to be air. + * + * @param actor + * Actor responsible for breaking the block + * @param before + * BlockState of the block before actually being destroyed. + */ + public void queueBlockBreak(Actor actor, BlockState before) { + queueBlock(actor, new Location(before.getWorld(), before.getX(), before.getY(), before.getZ()), before.getBlockData(), null, BlockStateCodecs.serialize(before), null, null); + } + + /** + * Logs a block break. The block type afterwards is assumed to be air. + * + * @param actor + * Actor responsible for the block break + * @param loc + * Location of the broken block + * @param typeBefore + * BlockData of the block before the break + */ + public void queueBlockBreak(Actor actor, Location loc, BlockData typeBefore) { + queueBlock(actor, loc, typeBefore, null); + } + + /** + * Logs a block place. The block type before is assumed to be air. + * + * @param actor + * Actor responsible for placing the block + * @param after + * BlockState of the block after actually being placed. + */ + public void queueBlockPlace(Actor actor, BlockState after) { + queueBlock(actor, new Location(after.getWorld(), after.getX(), after.getY(), after.getZ()), null, after.getBlockData(), null, BlockStateCodecs.serialize(after), null); + } + + /** + * Logs a block place. The block type before is assumed to be air. + * + * @param actor + * Actor responsible for placing the block + * @param loc + * Location of the placed block + * @param type + * BlockData of the placed block + */ + public void queueBlockPlace(Actor actor, Location loc, BlockData type) { + queueBlock(actor, loc, null, type); + } + + /** + * Logs a block being replaced from the before and after {@link org.bukkit.block.BlockState}s + * + * @param actor + * Actor responsible for replacing the block + * @param before + * BlockState of the block before actually being destroyed. + * @param after + * BlockState of the block after actually being placed. + */ + public void queueBlockReplace(Actor actor, BlockState before, BlockState after) { + queueBlock(actor, new Location(before.getWorld(), before.getX(), before.getY(), before.getZ()), before.getBlockData(), after.getBlockData(), BlockStateCodecs.serialize(before), BlockStateCodecs.serialize(after), null); + } + + /** + * Logs a block being replaced from the before {@link org.bukkit.block.BlockState} and the type and data after + * + * @param actor + * Actor responsible for replacing the block + * @param before + * BlockState of the block before being replaced. + * @param typeAfter + * BlockData of the block after being replaced + */ + public void queueBlockReplace(Actor actor, BlockState before, BlockData typeAfter) { + queueBlock(actor, new Location(before.getWorld(), before.getX(), before.getY(), before.getZ()), before.getBlockData(), typeAfter, BlockStateCodecs.serialize(before), null, null); + } + + /** + * Logs a block being replaced from the type and data before and the {@link org.bukkit.block.BlockState} after + * + * @param actor + * Actor responsible for replacing the block + * @param typeBefore + * BlockData of the block before being replaced + * @param after + * BlockState of the block after actually being placed. + */ + public void queueBlockReplace(Actor actor, BlockData typeBefore, BlockState after) { + queueBlock(actor, new Location(after.getWorld(), after.getX(), after.getY(), after.getZ()), typeBefore, after.getBlockData(), null, BlockStateCodecs.serialize(after), null); + } + + public void queueBlockReplace(Actor actor, Location loc, BlockData typeBefore, BlockData typeAfter) { + queueBlock(actor, loc, typeBefore, typeAfter, null, null, null); + } + + /** + * Logs an actor interacting with a container block's inventory + * + * @param actor + * The actor interacting with the container + * @param container + * The respective container. Must be an instance of an InventoryHolder. + * @param itemStack + * Item taken/stored, including amount + * @param remove + * true if the item was removed + */ + public void queueChestAccess(Actor actor, BlockState container, ItemStackAndAmount itemStack, boolean remove) { + if (!(container instanceof InventoryHolder)) { + throw new IllegalArgumentException("Container must be instanceof InventoryHolder"); + } + queueChestAccess(actor, new Location(container.getWorld(), container.getX(), container.getY(), container.getZ()), container.getBlockData(), itemStack, remove); + } + + /** + * Logs an actor interacting with a container block's inventory + * + * @param actor + * The actor interacting with the container + * @param loc + * The location of the container block + * @param type + * BlockData of the container. + * @param itemStack + * Item taken/stored, including amount + * @param remove + * true if the item was removed + */ + public void queueChestAccess(Actor actor, Location loc, BlockData type, ItemStackAndAmount itemStack, boolean remove) { + queueBlock(actor, loc, type, type, null, null, new ChestAccess(itemStack, remove, MaterialConverter.getOrAddMaterialId(itemStack.stack().getType()))); + } + + /** + * Logs a container block break. The block type before is assumed to be air. All content is assumed to be taken. + * + * @param actor + * The actor breaking the container + * @param container + * Must be an instance of InventoryHolder + */ + public void queueContainerBreak(Actor actor, BlockState container) { + if (!(container instanceof InventoryHolder)) { + return; + } + queueContainerBreak(actor, new Location(container.getWorld(), container.getX(), container.getY(), container.getZ()), container.getBlockData(), ((InventoryHolder) container).getInventory()); + } + + /** + * Logs a container block break. The block type before is assumed to be air. All content is assumed to be taken. + * + * @param actor + * The actor responsible for breaking the container + * @param loc + * The location of the inventory block + * @param type + * BlockData of the container block + * @param inv + * The inventory of the container block + */ + public void queueContainerBreak(Actor actor, Location loc, BlockData type, Inventory inv) { + final Collection items = compressInventory(inv.getContents()); + for (final ItemStackAndAmount item : items) { + queueChestAccess(actor, loc, type, item, true); + } + queueBlockBreak(actor, loc, type); + } + + /** + * @param killer + * Can't be null + * @param victim + * Can't be null + */ + public void queueKill(Entity killer, Entity victim) { + if (killer == null || victim == null) { + return; + } + ItemStack weapon = null; + Actor killerActor = Actor.actorFromEntity(killer); + // If it's a projectile kill we want to manually assign the weapon, so check for player before converting a projectile to its source + if (killer instanceof Player && ((Player) killer).getInventory().getItemInMainHand() != null) { + weapon = ((Player) killer).getInventory().getItemInMainHand(); + } + if (killer instanceof Projectile) { + Material projectileMaterial = itemIDfromProjectileEntity(killer); + weapon = projectileMaterial == null ? null : new ItemStack(projectileMaterial); + ProjectileSource ps = ((Projectile) killer).getShooter(); + if (ps == null) { + killerActor = Actor.actorFromEntity(killer); + } else { + killerActor = Actor.actorFromProjectileSource(ps); + } + } + + queueKill(victim.getLocation(), killerActor, Actor.actorFromEntity(victim), weapon); + } + + /** + * This form should only be used when the killer is not an entity e.g. for fall or suffocation damage + * + * @param killer + * Can't be null + * @param victim + * Can't be null + */ + public void queueKill(Actor killer, Entity victim) { + if (killer == null || victim == null) { + return; + } + queueKill(victim.getLocation(), killer, Actor.actorFromEntity(victim), null); + } + + /** + * @param location + * Location of the victim. + * @param killer + * Killer Actor. Can be null. + * @param victim + * Victim Actor. Can't be null. + * @param weapon + * Item of the weapon. null for no weapon. + */ + public void queueKill(Location location, Actor killer, Actor victim, ItemStack weapon) { + if (victim == null || !isLogged(location.getWorld())) { + return; + } + addQueueLast(new KillRow(location, killer == null ? null : killer, victim, weapon == null ? 0 : MaterialConverter.getOrAddMaterialId(weapon.getType()))); + } + + public void queueChat(Actor player, String message) { + if (!Config.ignoredChat.isEmpty()) { + String lowerCaseMessage = message.toLowerCase(); + for (String ignored : Config.ignoredChat) { + if (lowerCaseMessage.startsWith(ignored)) { + return; + } + } + } + if (hiddenPlayers.contains(player.getName().toLowerCase())) { + return; + } + while (message.length() > 256) { + addQueueLast(new ChatRow(player, message.substring(0, 256))); + message = message.substring(256); + } + addQueueLast(new ChatRow(player, message)); + } + + public void queueJoin(Player player) { + addQueueLast(new PlayerJoinRow(player)); + } + + public void queueLeave(Player player, long onlineTime) { + addQueueLast(new PlayerLeaveRow(player, onlineTime)); + } + + public void shutdown() { + synchronized (queue) { + shutdown = true; + shutdownInitialized = System.currentTimeMillis(); + queue.notifyAll(); + } + while (isAlive()) { + try { + join(); + } catch (InterruptedException e) { + // ignore + } + } + } + + @Override + public void run() { + ArrayList currentRows = new ArrayList<>(); + Connection conn = null; + BatchHelper batchHelper = new BatchHelper(); + int lastCommitsFailed = 0; + while (true) { + try { + if (conn == null) { + batchHelper.reset(); + conn = logblock.getConnection(); + if (conn != null) { + // initialize connection + conn.setAutoCommit(false); + } else { + // we did not get a connection + boolean wantsShutdown; + synchronized (queue) { + wantsShutdown = shutdown; + } + if (wantsShutdown) { + // lets give up + break; + } + // wait for a connection + logblock.getLogger().severe("[Consumer] Could not connect to the database!"); + try { + Thread.sleep(WAIT_FOR_CONNECTION_TIME_MILLIS); + } catch (InterruptedException e) { + // ignore + } + continue; + } + } + Row r; + boolean processBatch = false; + synchronized (queue) { + if (shutdown) { + // Give this thread some time to process the remaining entries + if (queue.isEmpty() || System.currentTimeMillis() - shutdownInitialized > MAX_SHUTDOWN_TIME_MILLIS) { + if (currentRows.isEmpty()) { + break; + } else { + processBatch = true; + } + } + } + r = queue.pollFirst(); + if (r == null) { + try { + if (currentRows.isEmpty() && !shutdown) { + // nothing to do for us + // wait some time before closing the connection + queue.wait(RETURN_IDLE_CONNECTION_TIME_MILLIS); + // if there is still nothing to do, close the connection and go to sleep + if (queue.isEmpty() && !shutdown) { + try { + conn.close(); + } catch (Exception e) { + // ignored + } + conn = null; + queue.wait(); + } + } else { + processBatch = true; + } + } catch (InterruptedException e) { + // ignore + } + } + } + if (r != null) { + boolean failOnActors = false; + for (final Actor actor : r.getActors()) { + if (playerIDAsIntIncludeUncommited(actor) == null) { + if (!addPlayer(conn, actor)) { + failOnActors = true; // skip this row + } + } + } + if (!failOnActors) { + currentRows.add(r); + r.process(conn, batchHelper); + } + } + if (currentRows.size() >= Math.max((processBatch ? 1 : (Config.forceToProcessAtLeast * 10)), 1)) { + batchHelper.processStatements(conn); + conn.commit(); + currentRows.clear(); + playerIds.putAll(uncommitedPlayerIds); + uncommitedPlayerIds.clear(); + uncommitedEntityIds.clear(); + lastCommitsFailed = 0; + } + } catch (Exception e) { + boolean retry = lastCommitsFailed < RETRIES_ON_UNKNOWN_CONNECTION_ERROR; + String state = "unknown"; + if (e instanceof SQLException) { + // Retry on network errors: SQLSTATE = 08S01 08001 08004 HY000 40001 + state = ((SQLException) e).getSQLState(); + retry = retry || (state != null && (state.equals("08S01") || state.equals("08001") || state.equals("08004") || state.equals("HY000") || state.equals("40001"))); + } + lastCommitsFailed += 1; + if (retry) { + logblock.getLogger().log(Level.WARNING, "[Consumer] Database connection lost, reconnecting! SQLState: " + state); + // readd rows to the queue + synchronized (queue) { + while (!currentRows.isEmpty()) { + queue.addFirst(currentRows.remove(currentRows.size() - 1)); + } + } + } else { + logblock.getLogger().log(Level.SEVERE, "[Consumer] Could not insert entries! SQLState: " + state, e); + } + currentRows.clear(); + batchHelper.reset(); + uncommitedPlayerIds.clear(); + uncommitedEntityIds.clear(); + if (conn != null) { + try { + conn.close(); + } catch (SQLException e1) { + // ignore + } + } + conn = null; + } + } + if (conn != null) { + try { + conn.close(); + } catch (SQLException e1) { + // ignore + } + } + + // readd to queue - this can be saved later + synchronized (queue) { + while (!currentRows.isEmpty()) { + queue.addFirst(currentRows.remove(currentRows.size() - 1)); + } + } + } + + public void writeToFile() throws FileNotFoundException { + final long time = System.currentTimeMillis(); + final Set insertedPlayers = new HashSet<>(); + int counter = 0; + final File importDir = new File(logblock.getDataFolder(), "import"); + importDir.mkdirs(); + PrintWriter writer = new PrintWriter(new File(importDir, "queue-" + time + "-0.sql")); + while (!isQueueEmpty()) { + final Row r = pollQueueFirst(); + if (r == null) { + continue; + } + for (final Actor actor : r.getActors()) { + if (!playerIds.containsKey(actor) && !insertedPlayers.contains(actor)) { + // Odd query contruction is to work around innodb auto increment behaviour - bug #492 + writer.println("INSERT IGNORE INTO `lb-players` (playername,UUID) SELECT '" + mysqlTextEscape(actor.getName()) + "','" + mysqlTextEscape(actor.getUUID()) + "' FROM `lb-players` WHERE NOT EXISTS (SELECT NULL FROM `lb-players` WHERE UUID = '" + mysqlTextEscape(actor.getUUID()) + "') LIMIT 1;"); + insertedPlayers.add(actor); + } + } + for (final String insert : r.getInserts()) { + writer.println(insert); + } + counter++; + if (counter % 1000 == 0) { + writer.close(); + writer = new PrintWriter(new File(importDir, "queue-" + time + "-" + counter / 1000 + ".sql")); + } + } + writer.close(); + } + + int getQueueSize() { + synchronized (queue) { + return queue.size(); + } + } + + private boolean isQueueEmpty() { + synchronized (queue) { + return queue.isEmpty(); + } + } + + private void addQueueLast(Row row) { + synchronized (queue) { + boolean wasEmpty = queue.isEmpty(); + queue.addLast(row); + addEntryCounter++; + if (Config.queueWarningSize > 0 && queue.size() >= Config.queueWarningSize && addEntryCounter >= nextWarnCounter) { + logblock.getLogger().warning("[Consumer] Queue overloaded. Size: " + queue.size()); + nextWarnCounter = addEntryCounter + 1000; + } + if (wasEmpty) { + queue.notifyAll(); + } + } + } + + private Row pollQueueFirst() { + synchronized (queue) { + return queue.pollFirst(); + } + } + + static void hide(Player player) { + hiddenPlayers.add(player.getName().toLowerCase()); + } + + static void unHide(Player player) { + hiddenPlayers.remove(player.getName().toLowerCase()); + } + + static boolean toggleHide(Player player) { + final String playerName = player.getName().toLowerCase(); + if (hiddenPlayers.contains(playerName)) { + hiddenPlayers.remove(playerName); + return false; + } + hiddenPlayers.add(playerName); + return true; + } + + private boolean addPlayer(Connection conn, Actor actor) throws SQLException { + // Odd query contruction is to work around innodb auto increment behaviour - bug #492 + String name = actor.getName(); + String uuid = actor.getUUID(); + Statement state = conn.createStatement(); + String q1 = "INSERT IGNORE INTO `lb-players` (playername,UUID) SELECT '" + mysqlTextEscape(name) + "','" + mysqlTextEscape(uuid) + "' FROM `lb-players` WHERE NOT EXISTS (SELECT NULL FROM `lb-players` WHERE UUID = '" + mysqlTextEscape(uuid) + "') LIMIT 1"; + String q2 = "SELECT playerid FROM `lb-players` WHERE UUID = '" + mysqlTextEscape(uuid) + "'"; + int q1Result = state.executeUpdate(q1); + ResultSet rs = state.executeQuery(q2); + if (rs.next()) { + uncommitedPlayerIds.put(actor, rs.getInt(1)); + } + rs.close(); + if (!uncommitedPlayerIds.containsKey(actor)) { + state.executeUpdate("INSERT IGNORE INTO `lb-players` (playername,UUID) VALUES ('" + mysqlTextEscape(name) + "','" + mysqlTextEscape(uuid) + "')"); + rs = state.executeQuery(q2); + if (rs.next()) { + uncommitedPlayerIds.put(actor, rs.getInt(1)); + } else { + logblock.getLogger().warning("[Consumer] Failed to add player " + actor.getName()); + logblock.getLogger().warning("[Consumer-Debug] Query 1: " + q1); + logblock.getLogger().warning("[Consumer-Debug] Query 1 - Result: " + q1Result); + logblock.getLogger().warning("[Consumer-Debug] Query 2: " + q2); + } + rs.close(); + } + state.close(); + return uncommitedPlayerIds.containsKey(actor); + } + + private long getEntityUUID(Connection conn, World world, UUID uuid) throws SQLException { + Map uncommitedEntityIdsHere = uncommitedEntityIds.get(world); + if (uncommitedEntityIdsHere == null) { + uncommitedEntityIdsHere = new HashMap<>(); + uncommitedEntityIds.put(world, uncommitedEntityIdsHere); + } + Long existing = uncommitedEntityIdsHere.get(uuid); + if (existing != null) { + return existing; + } + + // Odd query contruction is to work around innodb auto increment behaviour - bug #492 + final String table = getWorldConfig(world).table; + String uuidString = uuid.toString(); + Statement state = conn.createStatement(); + String q1 = "INSERT IGNORE INTO `" + table + "-entityids` (entityuuid) SELECT '" + mysqlTextEscape(uuidString) + "' FROM `" + table + "-entityids` WHERE NOT EXISTS (SELECT NULL FROM `" + table + "-entityids` WHERE entityuuid = '" + mysqlTextEscape(uuidString) + "') LIMIT 1"; + String q2 = "SELECT entityid FROM `" + table + "-entityids` WHERE entityuuid = '" + mysqlTextEscape(uuidString) + "'"; + int q1Result = state.executeUpdate(q1); + ResultSet rs = state.executeQuery(q2); + if (rs.next()) { + uncommitedEntityIdsHere.put(uuid, rs.getLong(1)); + } + rs.close(); + // if there was not any row in the table the query above does not work, so we need to try this one + if (!uncommitedEntityIdsHere.containsKey(uuid)) { + state.executeUpdate("INSERT IGNORE INTO `" + table + "-entityids` (entityuuid) VALUES ('" + mysqlTextEscape(uuidString) + "')"); + rs = state.executeQuery(q2); + if (rs.next()) { + uncommitedEntityIdsHere.put(uuid, rs.getLong(1)); + } else { + logblock.getLogger().warning("[Consumer] Failed to add entity uuid " + uuidString.toString()); + logblock.getLogger().warning("[Consumer-Debug] World: " + world.getName()); + logblock.getLogger().warning("[Consumer-Debug] Query 1: " + q1); + logblock.getLogger().warning("[Consumer-Debug] Query 1 - Result: " + q1Result); + logblock.getLogger().warning("[Consumer-Debug] Query 2: " + q2); + } + rs.close(); + } + state.close(); + return uncommitedEntityIdsHere.get(uuid); + } + + private void queueBlock(Actor actor, Location loc, BlockData typeBefore, BlockData typeAfter, YamlConfiguration stateBefore, YamlConfiguration stateAfter, ChestAccess ca) { + if (typeBefore == null || typeBefore.getMaterial() == Material.CAVE_AIR || typeBefore.getMaterial() == Material.VOID_AIR) { + typeBefore = Bukkit.createBlockData(Material.AIR); + } + if (typeAfter == null && ((typeBefore instanceof Waterlogged && ((Waterlogged) typeBefore).isWaterlogged()) || BukkitUtils.isAlwaysWaterlogged(typeBefore.getMaterial()))) { + typeAfter = Bukkit.createBlockData(Material.WATER); + } + if (typeAfter == null || typeAfter.getMaterial() == Material.CAVE_AIR || typeAfter.getMaterial() == Material.VOID_AIR) { + typeAfter = Bukkit.createBlockData(Material.AIR); + } + if (BlockChangePreLogEvent.getHandlerList().getRegisteredListeners().length > 0) { + // Create and call the event + BlockChangePreLogEvent event = new BlockChangePreLogEvent(actor, loc, typeBefore, typeAfter, stateBefore, stateAfter, ca); + logblock.getServer().getPluginManager().callEvent(event); + if (event.isCancelled()) { + return; + } + + // Update variables + actor = event.getOwnerActor(); + loc = event.getLocation(); + typeBefore = event.getTypeBefore(); + typeAfter = event.getTypeAfter(); + stateBefore = event.getStateBefore(); + stateAfter = event.getStateAfter(); + ca = event.getChestAccess(); + } + // Do this last so LogBlock still has final say in what is being added + if (actor == null || loc == null || typeBefore == null || typeAfter == null || hiddenPlayers.contains(actor.getName().toLowerCase()) || !isLogged(loc.getWorld()) || typeBefore != typeAfter && hiddenBlocks.contains(typeBefore.getMaterial()) && hiddenBlocks.contains(typeAfter.getMaterial())) { + return; + } + + int replacedMaterialId = MaterialConverter.getOrAddMaterialId(typeBefore); + int replacedStateId = MaterialConverter.getOrAddBlockStateId(typeBefore); + int typeMaterialId = MaterialConverter.getOrAddMaterialId(typeAfter); + int typeStateId = MaterialConverter.getOrAddBlockStateId(typeAfter); + + addQueueLast(new BlockRow(loc, actor, replacedMaterialId, replacedStateId, Utils.serializeYamlConfiguration(stateBefore), typeMaterialId, typeStateId, Utils.serializeYamlConfiguration(stateAfter), ca)); + } + + public void queueEntityModification(Actor actor, Entity entity, EntityChangeType changeType, YamlConfiguration data) { + if (actor == null || changeType == null || entity == null || hiddenPlayers.contains(actor.getName().toLowerCase()) || !isLogged(entity.getWorld())) { + return; + } + UUID entityId = entity.getUniqueId(); + EntityType entityType = entity.getType(); + Location loc = entity.getLocation(); + + if (EntityChangePreLogEvent.getHandlerList().getRegisteredListeners().length > 0) { + // Create and call the event + EntityChangePreLogEvent event = new EntityChangePreLogEvent(actor, loc, entity, changeType, data); + logblock.getServer().getPluginManager().callEvent(event); + if (event.isCancelled()) { + return; + } + + // Update variables + actor = event.getOwnerActor(); + loc = event.getLocation(); + } + // Do this last so LogBlock still has final say in what is being added + if (actor == null || loc == null || hiddenPlayers.contains(actor.getName().toLowerCase()) || !isLogged(loc.getWorld())) { + return; + } + + 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"; + } + final Integer id = playerIds.get(actor); + if (id != null) { + return id.toString(); + } + return "(SELECT playerid FROM `lb-players` WHERE UUID = '" + mysqlTextEscape(actor.getUUID()) + "')"; + } + + private Integer playerIDAsIntIncludeUncommited(Actor actor) { + if (actor == null) { + return null; + } + Integer id = playerIds.get(actor); + if (id != null) { + return id; + } + return uncommitedPlayerIds.get(actor); + } + + private static interface Row { + String[] getInserts(); + + void process(Connection conn, BatchHelper batchHelper) throws SQLException; + + Actor[] getActors(); + } + + private class BlockRow extends BlockChange implements Row { + final String statementString; + final String selectActorIdStatementString; + + public BlockRow(Location loc, Actor actor, int replaced, int replacedData, byte[] replacedState, int type, int typeData, byte[] typeState, ChestAccess ca) { + super(System.currentTimeMillis() / 1000, loc, actor, replaced, replacedData, replacedState, type, typeData, typeState, ca); + + statementString = getWorldConfig(loc.getWorld()).insertBlockStatementString; + selectActorIdStatementString = getWorldConfig(loc.getWorld()).selectBlockActorIdStatementString; + } + + @Override + public String[] getInserts() { + final String table = getWorldConfig(loc.getWorld()).table; + final String[] inserts = new String[ca != null || replacedState != null || typeState != null ? 2 : 1]; + + inserts[0] = "INSERT INTO `" + table + "-blocks` (date, playerid, replaced, replaceddata, type, typedata, x, y, z) VALUES (FROM_UNIXTIME(" + date + "), " + playerID(actor) + ", " + replacedMaterial + ", " + replacedData + ", " + typeMaterial + ", " + typeData + ", '" + loc.getBlockX() + + "', " + safeY(loc) + ", '" + loc.getBlockZ() + "');"; + if (replacedState != null || typeState != null) { + inserts[1] = "INSERT INTO `" + table + "-state` (replacedState, typeState, id) VALUES(" + Utils.mysqlPrepareBytesForInsertAllowNull(replacedState) + ", " + Utils.mysqlPrepareBytesForInsertAllowNull(typeState) + ", LAST_INSERT_ID());"; + } else if (ca != null) { + try { + inserts[1] = "INSERT INTO `" + table + "-chestdata` (id, item, itemremove, itemtype) values (LAST_INSERT_ID(), '" + Utils.mysqlEscapeBytes(Utils.saveItemStack(ca.itemStack)) + "', " + (ca.remove ? 1 : 0) + ", " + ca.itemType + ");"; + } catch (Exception e) { + LogBlock.getInstance().getLogger().log(Level.SEVERE, "Could not serialize ItemStack " + e.getMessage(), e); + LogBlock.getInstance().getLogger().log(Level.SEVERE, "Problematic row: " + toString()); + return new String[0]; + } + } + return inserts; + } + + @Override + public Actor[] getActors() { + return new Actor[] { actor }; + } + + @Override + public void process(Connection conn, BatchHelper batchHelper) throws SQLException { + byte[] serializedItemStack = null; + if (ca != null) { + try { + serializedItemStack = Utils.saveItemStack(ca.itemStack); + } catch (Exception e) { + LogBlock.getInstance().getLogger().log(Level.SEVERE, "Could not serialize ItemStack " + e.getMessage(), e); + LogBlock.getInstance().getLogger().log(Level.SEVERE, "Problematic row: " + toString()); + return; + } + } + final byte[] finalSerializedItemStack = serializedItemStack; + int sourceActor = playerIDAsIntIncludeUncommited(actor); + Location actorBlockLocation = actor.getBlockLocation(); + if (actorBlockLocation != null) { + Integer tempSourceActor = batchHelper.getUncommitedBlockActor(actorBlockLocation); + if (tempSourceActor != null) { + sourceActor = tempSourceActor; + } else { + PreparedStatement smt = batchHelper.getOrPrepareStatement(conn, selectActorIdStatementString, Statement.NO_GENERATED_KEYS); + smt.setInt(1, actorBlockLocation.getBlockX()); + smt.setInt(2, safeY(actorBlockLocation)); + smt.setInt(3, actorBlockLocation.getBlockZ()); + ResultSet rs = smt.executeQuery(); + if (rs.next()) { + sourceActor = rs.getInt(1); + } + rs.close(); + } + } + PreparedStatement smt = batchHelper.getOrPrepareStatement(conn, statementString, Statement.RETURN_GENERATED_KEYS); + smt.setLong(1, date); + smt.setInt(2, sourceActor); + smt.setInt(3, replacedMaterial); + smt.setInt(4, replacedData); + smt.setInt(5, typeMaterial); + smt.setInt(6, typeData); + smt.setInt(7, loc.getBlockX()); + smt.setInt(8, safeY(loc)); + smt.setInt(9, loc.getBlockZ()); + batchHelper.addUncommitedBlockActorId(loc, sourceActor); + batchHelper.addBatch(smt, new LongCallback() { + @Override + public void call(long id) throws SQLException { + PreparedStatement ps; + if (typeState != null || replacedState != null) { + ps = batchHelper.getOrPrepareStatement(conn, getWorldConfig(loc.getWorld()).insertBlockStateStatementString, Statement.NO_GENERATED_KEYS); + ps.setBytes(1, replacedState); + ps.setBytes(2, typeState); + ps.setLong(3, id); + batchHelper.addBatch(ps, null); + } + if (ca != null) { + ps = batchHelper.getOrPrepareStatement(conn, getWorldConfig(loc.getWorld()).insertBlockChestDataStatementString, Statement.NO_GENERATED_KEYS); + ps.setBytes(1, finalSerializedItemStack); + ps.setInt(2, ca.remove ? 1 : 0); + ps.setLong(3, id); + ps.setInt(4, ca.itemType); + batchHelper.addBatch(ps, null); + } + } + }); + } + } + + private class KillRow implements Row { + final long date; + final Actor killer, victim; + final int weapon; + final Location loc; + final String statementString; + + KillRow(Location loc, Actor attacker, Actor defender, int weapon) { + date = System.currentTimeMillis() / 1000; + this.loc = loc; + killer = attacker; + victim = defender; + this.weapon = weapon; + + statementString = "INSERT INTO `" + getWorldConfig(loc.getWorld()).table + "-kills` (date, killer, victim, weapon, x, y, z) VALUES (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?)"; + } + + @Override + public String[] getInserts() { + return new String[] { "INSERT INTO `" + getWorldConfig(loc.getWorld()).table + "-kills` (date, killer, victim, weapon, x, y, z) VALUES (FROM_UNIXTIME(" + date + "), " + playerID(killer) + ", " + playerID(victim) + ", " + weapon + ", " + loc.getBlockX() + ", " + safeY(loc) + ", " + + loc.getBlockZ() + ");" }; + } + + @Override + public Actor[] getActors() { + return new Actor[] { killer, victim }; + } + + @Override + public void process(Connection conn, BatchHelper batchHelper) throws SQLException { + PreparedStatement smt = batchHelper.getOrPrepareStatement(conn, statementString, Statement.NO_GENERATED_KEYS); + smt.setLong(1, date); + smt.setInt(2, playerIDAsIntIncludeUncommited(killer)); + smt.setInt(3, playerIDAsIntIncludeUncommited(victim)); + smt.setInt(4, weapon); + smt.setInt(5, loc.getBlockX()); + smt.setInt(6, safeY(loc)); + smt.setInt(7, loc.getBlockZ()); + batchHelper.addBatch(smt, null); + } + } + + private class ChatRow extends ChatMessage implements Row { + private String statementString; + + ChatRow(Actor player, String message) { + super(player, message); + + statementString = "INSERT INTO `lb-chat` (date, playerid, message) VALUES (FROM_UNIXTIME(?), ?, ?)"; + } + + @Override + public String[] getInserts() { + return new String[] { "INSERT INTO `lb-chat` (date, playerid, message) VALUES (FROM_UNIXTIME(" + date + "), " + playerID(player) + ", '" + mysqlTextEscape(message) + "');" }; + } + + @Override + public Actor[] getActors() { + return new Actor[] { player }; + } + + @Override + public void process(Connection conn, BatchHelper batchHelper) throws SQLException { + PreparedStatement smt = batchHelper.getOrPrepareStatement(conn, statementString, Statement.NO_GENERATED_KEYS); + smt.setLong(1, date); + smt.setInt(2, playerIDAsIntIncludeUncommited(player)); + smt.setString(3, message); + batchHelper.addBatch(smt, null); + } + } + + private class PlayerJoinRow implements Row { + private final Actor player; + private final long lastLogin; + private final String ip; + private String statementString; + + PlayerJoinRow(Player player) { + this.player = Actor.actorFromEntity(player); + lastLogin = System.currentTimeMillis() / 1000; + ip = player.getAddress().toString().replace("'", "\\'"); + + if (logPlayerInfo) { + statementString = "UPDATE `lb-players` SET lastlogin = FROM_UNIXTIME(?), firstlogin = IF(firstlogin = 0, FROM_UNIXTIME(?), firstlogin), ip = ?, playername = ? WHERE UUID = ?"; + } else { + statementString = "UPDATE `lb-players` SET playername = ? WHERE UUID = ?"; + } + } + + @Override + public String[] getInserts() { + if (logPlayerInfo) { + return new String[] { + "UPDATE `lb-players` SET lastlogin = FROM_UNIXTIME(" + lastLogin + "), firstlogin = IF(firstlogin = 0, FROM_UNIXTIME(" + lastLogin + "), firstlogin), ip = '" + ip + "', playername = '" + mysqlTextEscape(player.getName()) + "' WHERE UUID = '" + player.getUUID() + "';" }; + } + return new String[] { "UPDATE `lb-players` SET playername = '" + mysqlTextEscape(player.getName()) + "' WHERE UUID = '" + mysqlTextEscape(player.getUUID()) + "';" }; + } + + @Override + public Actor[] getActors() { + return new Actor[] { player }; + } + + @Override + public void process(Connection conn, BatchHelper batchHelper) throws SQLException { + PreparedStatement smt = batchHelper.getOrPrepareStatement(conn, statementString, Statement.NO_GENERATED_KEYS); + if (logPlayerInfo) { + smt.setLong(1, lastLogin); + smt.setLong(2, lastLogin); + smt.setString(3, ip); + smt.setString(4, player.getName()); + smt.setString(5, player.getUUID()); + } else { + smt.setString(1, player.getName()); + smt.setString(2, player.getUUID()); + } + batchHelper.addBatch(smt, null); + } + } + + private class PlayerLeaveRow implements Row { + private final long onlineTime; + private final Actor actor; + private String statementString; + + PlayerLeaveRow(Player player, long onlineTime) { + this.onlineTime = onlineTime; + actor = Actor.actorFromEntity(player); + statementString = "UPDATE `lb-players` SET onlinetime = onlinetime + ? WHERE lastlogin > 0 && UUID = ?"; + } + + @Override + public String[] getInserts() { + if (logPlayerInfo) { + return new String[] { "UPDATE `lb-players` SET onlinetime = onlinetime + " + onlineTime + " WHERE lastlogin > 0 && UUID = '" + mysqlTextEscape(actor.getUUID()) + "';" }; + } + return new String[0]; + } + + @Override + public Actor[] getActors() { + return new Actor[] { actor }; + } + + @Override + public void process(Connection conn, BatchHelper batchHelper) throws SQLException { + PreparedStatement smt = batchHelper.getOrPrepareStatement(conn, statementString, Statement.NO_GENERATED_KEYS); + smt.setLong(1, onlineTime); + smt.setString(2, actor.getUUID()); + batchHelper.addBatch(smt, null); + } + } + + private class EntityRow extends EntityChange implements Row { + final String statementString; + final String selectActorIdStatementString; + + public EntityRow(Location loc, Actor actor, EntityType type, UUID entityid, EntityChangeType changeType, byte[] data) { + super(System.currentTimeMillis() / 1000, loc, actor, type, entityid, changeType, data); + statementString = getWorldConfig(loc.getWorld()).insertEntityStatementString; + selectActorIdStatementString = getWorldConfig(loc.getWorld()).selectBlockActorIdStatementString; + } + + @Override + public String[] getInserts() { + final String table = getWorldConfig(loc.getWorld()).table; + final String[] inserts = new String[2]; + + 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(entityUUID.toString()) + "')" + + ", " + entityTypeId + ", '" + loc.getBlockX() + "', " + safeY(loc) + ", '" + loc.getBlockZ() + "', " + changeType.ordinal() + ", " + Utils.mysqlPrepareBytesForInsertAllowNull(data) + ");"; + return inserts; + } + + @Override + public Actor[] getActors() { + return new Actor[] { actor }; + } + + @Override + public void process(Connection conn, BatchHelper batchHelper) throws SQLException { + int sourceActor = playerIDAsIntIncludeUncommited(actor); + Location actorBlockLocation = actor.getBlockLocation(); + if (actorBlockLocation != null) { + Integer tempSourceActor = batchHelper.getUncommitedBlockActor(actorBlockLocation); + if (tempSourceActor != null) { + sourceActor = tempSourceActor; + } else { + PreparedStatement smt = batchHelper.getOrPrepareStatement(conn, selectActorIdStatementString, Statement.NO_GENERATED_KEYS); + smt.setInt(1, actorBlockLocation.getBlockX()); + smt.setInt(2, safeY(actorBlockLocation)); + smt.setInt(3, actorBlockLocation.getBlockZ()); + ResultSet rs = smt.executeQuery(); + if (rs.next()) { + sourceActor = rs.getInt(1); + } + rs.close(); + } + } + PreparedStatement smt = batchHelper.getOrPrepareStatement(conn, statementString, Statement.NO_GENERATED_KEYS); + smt.setLong(1, date); + smt.setInt(2, sourceActor); + smt.setLong(3, getEntityUUID(conn, loc.getWorld(), entityUUID)); + smt.setInt(4, EntityTypeConverter.getOrAddEntityTypeId(type)); + smt.setInt(5, loc.getBlockX()); + smt.setInt(6, safeY(loc)); + smt.setInt(7, loc.getBlockZ()); + smt.setInt(8, changeType.ordinal()); + smt.setBytes(9, data); + batchHelper.addBatch(smt, null); + } + } + + private class EntityUUIDChange implements Row { + private final World world; + private final long entityId; + private final UUID entityUUID; + final String updateEntityUUIDString; + + public EntityUUIDChange(World world, long 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.setLong(2, entityId); + smt.executeUpdate(); + } + } + + private int safeY(Location loc) { + int safeY = loc.getBlockY(); + if (safeY < Short.MIN_VALUE) { + safeY = Short.MIN_VALUE; + } + if (safeY > Short.MAX_VALUE) { + safeY = Short.MAX_VALUE; + } + return safeY; + } + + private class BatchHelper { + private HashMap preparedStatements = new HashMap<>(); + private HashSet preparedStatementsWithGeneratedKeys = new HashSet<>(); + private LinkedHashMap> generatedKeyHandler = new LinkedHashMap<>(); + private HashMap uncommitedBlockActors = new HashMap<>(); + + public void reset() { + preparedStatements.clear(); + preparedStatementsWithGeneratedKeys.clear(); + generatedKeyHandler.clear(); + uncommitedBlockActors.clear(); + } + + public void addUncommitedBlockActorId(Location loc, int actorId) { + uncommitedBlockActors.put(loc, actorId); + } + + public Integer getUncommitedBlockActor(Location loc) { + return uncommitedBlockActors.get(loc); + } + + public void processStatements(Connection conn) throws SQLException { + while (!generatedKeyHandler.isEmpty()) { + Entry> entry = generatedKeyHandler.entrySet().iterator().next(); + PreparedStatement smt = entry.getKey(); + ArrayList callbackList = entry.getValue(); + generatedKeyHandler.remove(smt); + smt.executeBatch(); + if (preparedStatementsWithGeneratedKeys.contains(smt)) { + ResultSet keys = smt.getGeneratedKeys(); + long[] results = new long[callbackList.size()]; + int pos = 0; + while (keys.next() && pos < results.length) { + results[pos++] = keys.getLong(1); + } + keys.close(); + for (int i = 0; i < results.length; i++) { + LongCallback callback = callbackList.get(i); + if (callback != null) { + callback.call(results[i]); + } + } + } + } + uncommitedBlockActors.clear(); + } + + public PreparedStatement getOrPrepareStatement(Connection conn, String sql, int autoGeneratedKeys) throws SQLException { + PreparedStatement smt = preparedStatements.get(sql); + if (smt == null) { + smt = conn.prepareStatement(sql, autoGeneratedKeys); + preparedStatements.put(sql, smt); + if (autoGeneratedKeys == Statement.RETURN_GENERATED_KEYS) { + preparedStatementsWithGeneratedKeys.add(smt); + } + } + return smt; + } + + public void addBatch(PreparedStatement smt, LongCallback generatedKeysCallback) throws SQLException { + smt.addBatch(); + ArrayList callbackList = generatedKeyHandler.get(smt); + if (callbackList == null) { + callbackList = new ArrayList<>(); + generatedKeyHandler.put(smt, callbackList); + } + callbackList.add(generatedKeysCallback); + } + } + + protected interface LongCallback { + public void call(long value) throws SQLException; + } +} diff --git a/src/main/java/de/diddiz/LogBlock/DumpedLogImporter.java b/src/main/java/de/diddiz/LogBlock/DumpedLogImporter.java index 85bc6a73..0a0d9d29 100644 --- a/src/main/java/de/diddiz/LogBlock/DumpedLogImporter.java +++ b/src/main/java/de/diddiz/LogBlock/DumpedLogImporter.java @@ -1,70 +1,147 @@ -package de.diddiz.LogBlock; - -import de.diddiz.util.Utils.ExtensionFilenameFilter; - -import java.io.*; -import java.sql.Connection; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.logging.Level; - -import static de.diddiz.util.Utils.newline; -import static org.bukkit.Bukkit.getLogger; - -public class DumpedLogImporter implements Runnable { - private final LogBlock logblock; - - DumpedLogImporter(LogBlock logblock) { - this.logblock = logblock; - } - - @Override - public void run() { - final File[] imports = new File("plugins/LogBlock/import/").listFiles(new ExtensionFilenameFilter("sql")); - if (imports != null && imports.length > 0) { - getLogger().info("Found " + imports.length + " imports."); - Connection conn = null; - try { - conn = logblock.getConnection(); - if (conn == null) { - return; - } - conn.setAutoCommit(false); - final Statement st = conn.createStatement(); - final BufferedWriter writer = new BufferedWriter(new FileWriter(new File(logblock.getDataFolder(), "import/failed.txt"))); - int successes = 0, errors = 0; - for (final File sqlFile : imports) { - getLogger().info("Trying to import " + sqlFile.getName() + " ..."); - final BufferedReader reader = new BufferedReader(new FileReader(sqlFile)); - String line; - while ((line = reader.readLine()) != null) { - try { - st.execute(line); - successes++; - } catch (final Exception ex) { - getLogger().warning("Error while importing: '" + line + "': " + ex.getMessage()); - writer.write(line + newline); - errors++; - } - } - conn.commit(); - reader.close(); - sqlFile.delete(); - getLogger().info("Successfully imported " + sqlFile.getName() + "."); - } - writer.close(); - st.close(); - getLogger().info("Successfully imported stored queue. (" + successes + " rows imported, " + errors + " errors)"); - } catch (final Exception ex) { - getLogger().log(Level.WARNING, "Error while importing: ", ex); - } finally { - if (conn != null) { - try { - conn.close(); - } catch (final SQLException ex) { - } - } - } - } - } -} +package de.diddiz.LogBlock; + +import static de.diddiz.LogBlock.util.Utils.newline; + +import de.diddiz.LogBlock.util.Utils.ExtensionFilenameFilter; +import java.io.*; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Arrays; +import java.util.Comparator; +import java.util.logging.Level; +import java.util.regex.Pattern; + +public class DumpedLogImporter implements Runnable { + private final LogBlock logblock; + + DumpedLogImporter(LogBlock logblock) { + this.logblock = logblock; + } + + @Override + public void run() { + final File[] imports = new File(logblock.getDataFolder(), "import").listFiles(new ExtensionFilenameFilter("sql")); + if (imports != null && imports.length > 0) { + logblock.getLogger().info("Found " + imports.length + " imports."); + Arrays.sort(imports, new ImportsComparator()); + Connection conn = null; + try { + conn = logblock.getConnection(); + if (conn == null) { + return; + } + conn.setAutoCommit(false); + final Statement st = conn.createStatement(); + final BufferedWriter writer = new BufferedWriter(new FileWriter(new File(logblock.getDataFolder(), "import/failed.txt"))); + int successes = 0, errors = 0; + try { + for (final File sqlFile : imports) { + String line = null; + try { + logblock.getLogger().info("Trying to import " + sqlFile.getName() + " ..."); + // first try batch import the whole file + final BufferedReader reader = new BufferedReader(new FileReader(sqlFile)); + int statements = 0; + while ((line = reader.readLine()) != null) { + if (line.endsWith(";")) { + line = line.substring(0, line.length() - 1); + } + if (!line.isEmpty()) { + statements++; + st.addBatch(line); + } + } + st.executeBatch(); + conn.commit(); + reader.close(); + sqlFile.delete(); + successes += statements; + logblock.getLogger().info("Successfully imported " + sqlFile.getName() + "."); + } catch (final Exception ignored) { + // if the batch import did not work, retry line by line + try { + final BufferedReader reader = new BufferedReader(new FileReader(sqlFile)); + while ((line = reader.readLine()) != null) { + if (line.endsWith(";")) { + line = line.substring(0, line.length() - 1); + } + if (!line.isEmpty()) { + try { + st.execute(line); + successes++; + } catch (final SQLException ex) { + logblock.getLogger().severe("Error while importing: '" + line + "': " + ex.getMessage()); + writer.write(line + newline); + errors++; + } + } + } + conn.commit(); + reader.close(); + sqlFile.delete(); + logblock.getLogger().info("Successfully imported " + sqlFile.getName() + "."); + } catch (final Exception ex) { + logblock.getLogger().severe("Error while importing " + sqlFile.getName() + ": " + ex.getMessage()); + errors++; + } + } + } + } finally { + writer.close(); + } + st.close(); + logblock.getLogger().info("Successfully imported stored queue. (" + successes + " rows imported, " + errors + " errors)"); + } catch (final Exception ex) { + logblock.getLogger().log(Level.WARNING, "Error while importing: ", ex); + } finally { + if (conn != null) { + try { + conn.close(); + } catch (final SQLException ex) { + } + } + } + } + } + + private static class ImportsComparator implements Comparator { + private final Pattern splitPattern = Pattern.compile("[\\-\\.]"); + + @Override + public int compare(File o1, File o2) { + String[] name1 = splitPattern.split(o1.getName()); + String[] name2 = splitPattern.split(o2.getName()); + if (name1.length > name2.length) { + return 1; + } else if (name1.length < name2.length) { + return -1; + } + for (int i = 0; i < name1.length; i++) { + String part1 = name1[i]; + String part2 = name2[i]; + if (part1.length() > 0 && part2.length() > 0) { + char first1 = part1.charAt(0); + char first2 = part2.charAt(0); + if (first1 >= '0' && first1 <= '9' && first2 >= '0' && first2 <= '9') { + try { + long long1 = Long.parseLong(part1); + long long2 = Long.parseLong(part2); + if (long1 == long2) { + continue; + } + return long1 > long2 ? 1 : -1; + } catch (NumberFormatException e) { + // fallthrough to string compare + } + } + } + int compareString = part1.compareTo(part2); + if (compareString != 0) { + return compareString; + } + } + return 0; + } + } +} diff --git a/src/main/java/de/diddiz/LogBlock/EntityChange.java b/src/main/java/de/diddiz/LogBlock/EntityChange.java new file mode 100644 index 00000000..385b9ba2 --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/EntityChange.java @@ -0,0 +1,138 @@ +package de.diddiz.LogBlock; + +import static de.diddiz.LogBlock.util.ActionColor.CREATE; +import static de.diddiz.LogBlock.util.ActionColor.DESTROY; +import static de.diddiz.LogBlock.util.ActionColor.INTERACT; +import static de.diddiz.LogBlock.util.MessagingUtil.createTextComponentWithColor; +import static de.diddiz.LogBlock.util.MessagingUtil.prettyDate; +import static de.diddiz.LogBlock.util.MessagingUtil.prettyEntityType; +import static de.diddiz.LogBlock.util.MessagingUtil.prettyLocation; +import static de.diddiz.LogBlock.util.MessagingUtil.prettyMaterial; + +import de.diddiz.LogBlock.util.Utils; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.UUID; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.TextComponent; +import org.bukkit.Location; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.ArmorStand; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.LivingEntity; +import org.bukkit.inventory.ItemStack; + +public class EntityChange implements LookupCacheElement { + public static enum EntityChangeType { + CREATE, + KILL, + MODIFY, + ADDEQUIP, + REMOVEEQUIP, + GET_STUNG; + + private static EntityChangeType[] values = values(); + + public static EntityChangeType valueOf(int ordinal) { + return values[ordinal]; + } + } + + public final long id, date; + public final Location loc; + public final Actor actor; + public final EntityType type; + public final int entityId; + public final UUID entityUUID; + public final EntityChangeType changeType; + public final byte[] data; + + public EntityChange(long date, Location loc, Actor actor, EntityType type, UUID entityid, EntityChangeType changeType, byte[] data) { + id = 0; + this.date = date; + this.loc = loc; + this.actor = actor; + this.type = type; + this.entityId = -1; + this.entityUUID = entityid; + this.changeType = changeType; + this.data = data; + } + + public EntityChange(ResultSet rs, QueryParams p) throws SQLException { + id = p.needId ? rs.getLong("id") : 0; + date = p.needDate ? rs.getTimestamp("date").getTime() : 0; + 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 ? 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; + } + + @Override + public String toString() { + return BaseComponent.toPlainText(getLogMessage()); + } + + @Override + public BaseComponent getLogMessage(int entry) { + TextComponent msg = new TextComponent(); + if (date > 0) { + msg.addExtra(prettyDate(date)); + msg.addExtra(" "); + } + if (actor != null) { + msg.addExtra(actor.getName()); + msg.addExtra(" "); + } + if (changeType == EntityChangeType.CREATE) { + msg.addExtra(createTextComponentWithColor("created ", CREATE.getColor())); + } else if (changeType == EntityChangeType.KILL) { + boolean living = type != null && LivingEntity.class.isAssignableFrom(type.getEntityClass()) && !ArmorStand.class.isAssignableFrom(type.getDeclaringClass()); + msg.addExtra(createTextComponentWithColor(living ? "killed " : "destroyed ", DESTROY.getColor())); + } else if (changeType == EntityChangeType.ADDEQUIP) { + YamlConfiguration conf = Utils.deserializeYamlConfiguration(data); + ItemStack stack = conf == null ? null : conf.getItemStack("item"); + if (stack == null) { + msg.addExtra(createTextComponentWithColor("added an item to ", CREATE.getColor())); + } else { + msg.addExtra(createTextComponentWithColor("added ", CREATE.getColor())); + msg.addExtra(prettyMaterial(stack.getType())); + msg.addExtra(" to "); + } + } else if (changeType == EntityChangeType.REMOVEEQUIP) { + YamlConfiguration conf = Utils.deserializeYamlConfiguration(data); + ItemStack stack = conf == null ? null : conf.getItemStack("item"); + if (stack == null) { + msg.addExtra(createTextComponentWithColor("removed an item from ", DESTROY.getColor())); + } else { + msg.addExtra(createTextComponentWithColor("removed ", DESTROY.getColor())); + msg.addExtra(prettyMaterial(stack.getType())); + msg.addExtra(" from "); + } + } else if (changeType == EntityChangeType.MODIFY) { + msg.addExtra(createTextComponentWithColor("modified ", INTERACT.getColor())); + } else if (changeType == EntityChangeType.GET_STUNG) { + msg.addExtra(createTextComponentWithColor("got stung by ", DESTROY.getColor())); + } else { + msg.addExtra(createTextComponentWithColor("did an unknown action to ", INTERACT.getColor())); + } + if (type != null) { + msg.addExtra(prettyEntityType(type)); + } else { + msg.addExtra(prettyMaterial("an unknown entity")); + } + if (loc != null) { + msg.addExtra(" at "); + msg.addExtra(prettyLocation(loc, entry)); + } + return msg; + } + + @Override + public Location getLocation() { + return loc; + } +} diff --git a/src/main/java/de/diddiz/LogBlock/EntityTypeConverter.java b/src/main/java/de/diddiz/LogBlock/EntityTypeConverter.java new file mode 100644 index 00000000..c1fcc597 --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/EntityTypeConverter.java @@ -0,0 +1,114 @@ +package de.diddiz.LogBlock; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Arrays; +import java.util.HashMap; +import java.util.logging.Level; + +import org.bukkit.entity.EntityType; + +public class EntityTypeConverter { + private static EntityType[] idToEntityType = new EntityType[10]; + private static HashMap entityTypeToId = new HashMap<>(); + private static int nextEntityTypeId; + + public synchronized static Integer getExistingEntityTypeId(EntityType entityType) { + return entityType == null ? null : entityTypeToId.get(entityType); + } + + public synchronized static int getOrAddEntityTypeId(EntityType entityType) { + Integer key = entityTypeToId.get(entityType); + int tries = 0; + while (key == null && tries < 10) { + tries++; + key = nextEntityTypeId; + Connection conn = LogBlock.getInstance().getConnection(); + try { + conn.setAutoCommit(false); + PreparedStatement smt = conn.prepareStatement("INSERT IGNORE INTO `lb-entitytypes` (id, name) VALUES (?, ?)"); + smt.setInt(1, key); + smt.setString(2, entityType.name()); + boolean couldAdd = smt.executeUpdate() > 0; + conn.commit(); + smt.close(); + if (couldAdd) { + internalAddEntityType(key, entityType); + } else { + initializeEntityTypes(conn); + } + } catch (Exception e) { + LogBlock.getInstance().getLogger().log(Level.SEVERE, "Could not update lb-entitytypes", e); + reinitializeEntityTypesCatchException(); + if (tries == 10) { + throw new RuntimeException(e); + } + } finally { + try { + conn.close(); + } catch (SQLException e) { + // ignored + } + } + key = entityTypeToId.get(entityType); + } + return key.intValue(); + } + + public synchronized static EntityType getEntityType(int entityTypeId) { + return entityTypeId >= 0 && entityTypeId < idToEntityType.length ? idToEntityType[entityTypeId] : null; + } + + private static void reinitializeEntityTypesCatchException() { + Connection conn = LogBlock.getInstance().getConnection(); + try { + initializeEntityTypes(conn); + } catch (Exception e) { + LogBlock.getInstance().getLogger().log(Level.SEVERE, "Could not reinitialize lb-entitytypes", e); + } finally { + try { + conn.close(); + } catch (Exception e) { + // ignored + } + } + } + + protected synchronized static void initializeEntityTypes(Connection connection) throws SQLException { + Statement smt = connection.createStatement(); + ResultSet rs = smt.executeQuery("SELECT id, name FROM `lb-entitytypes`"); + while (rs.next()) { + int key = rs.getInt(1); + try { + EntityType entityType = EntityType.valueOf(rs.getString(2)); + internalAddEntityType(key, entityType); + } catch (IllegalArgumentException ignored) { + // the key is used, but not available in this version + if (nextEntityTypeId <= key) { + nextEntityTypeId = key + 1; + } + } + } + rs.close(); + smt.close(); + connection.close(); + } + + private static void internalAddEntityType(int key, EntityType entityType) { + entityTypeToId.put(entityType, key); + int length = idToEntityType.length; + while (length <= key) { + length = (length * 3 / 2) + 5; + } + if (length > idToEntityType.length) { + idToEntityType = Arrays.copyOf(idToEntityType, length); + } + idToEntityType[key] = entityType; + if (nextEntityTypeId <= key) { + nextEntityTypeId = key + 1; + } + } +} diff --git a/src/main/java/de/diddiz/LogBlock/Kill.java b/src/main/java/de/diddiz/LogBlock/Kill.java index 3e58631f..ff6287eb 100755 --- a/src/main/java/de/diddiz/LogBlock/Kill.java +++ b/src/main/java/de/diddiz/LogBlock/Kill.java @@ -1,11 +1,18 @@ package de.diddiz.LogBlock; -import de.diddiz.LogBlock.config.Config; -import org.bukkit.Location; -import org.bukkit.inventory.ItemStack; +import static de.diddiz.LogBlock.util.ActionColor.DESTROY; +import static de.diddiz.LogBlock.util.MessagingUtil.prettyDate; +import static de.diddiz.LogBlock.util.MessagingUtil.prettyLocation; +import static de.diddiz.LogBlock.util.MessagingUtil.prettyMaterial; +import de.diddiz.LogBlock.util.BukkitUtils; +import de.diddiz.LogBlock.util.MessagingUtil; import java.sql.ResultSet; import java.sql.SQLException; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.TextComponent; +import org.bukkit.Location; +import org.bukkit.Material; public class Kill implements LookupCacheElement { final long id, date; @@ -23,7 +30,7 @@ public Kill(String killerName, String victimName, int weapon, Location loc) { } public Kill(ResultSet rs, QueryParams p) throws SQLException { - id = p.needId ? rs.getInt("id") : 0; + id = p.needId ? rs.getLong("id") : 0; date = p.needDate ? rs.getTimestamp("date").getTime() : 0; loc = p.needCoords ? new Location(p.world, rs.getInt("x"), rs.getInt("y"), rs.getInt("z")) : null; killerName = p.needKiller ? rs.getString("killer") : null; @@ -33,17 +40,7 @@ public Kill(ResultSet rs, QueryParams p) throws SQLException { @Override public String toString() { - final StringBuilder msg = new StringBuilder(); - if (date > 0) { - msg.append(Config.formatter.format(date)).append(" "); - } - msg.append(killerName).append(" killed ").append(victimName); - if (loc != null) { - msg.append(" at ").append(loc.getBlockX()).append(":").append(loc.getBlockY()).append(":").append(loc.getBlockZ()); - } - String weaponName = prettyItemName(new ItemStack(weapon)); - msg.append(" with " + weaponName); // + ("aeiou".contains(weaponName.substring(0, 1)) ? "an " : "a " ) - return msg.toString(); + return BaseComponent.toPlainText(getLogMessage()); } @Override @@ -52,15 +49,29 @@ public Location getLocation() { } @Override - public String getMessage() { - return toString(); + public BaseComponent getLogMessage(int entry) { + TextComponent msg = new TextComponent(); + if (date > 0) { + msg.addExtra(prettyDate(date)); + msg.addExtra(" "); + } + msg.addExtra(MessagingUtil.createTextComponentWithColor(killerName + " killed ", DESTROY.getColor())); + msg.addExtra(new TextComponent(victimName)); + if (loc != null) { + msg.addExtra(" at "); + msg.addExtra(prettyLocation(loc, entry)); + } + if (weapon != 0) { + msg.addExtra(" with "); + msg.addExtra(prettyItemName(MaterialConverter.getMaterial(weapon))); + } + return msg; } - public String prettyItemName(ItemStack i) { - String item = i.getType().toString().replace('_', ' ').toLowerCase(); - if (item.equals("air")) { - item = "fist"; + public TextComponent prettyItemName(Material t) { + if (t == null || BukkitUtils.isEmpty(t)) { + return prettyMaterial("fist"); } - return item; + return prettyMaterial(t.toString().replace('_', ' ')); } } diff --git a/src/main/java/de/diddiz/LogBlock/LogBlock.java b/src/main/java/de/diddiz/LogBlock/LogBlock.java index d1a319b9..504ad8c4 100644 --- a/src/main/java/de/diddiz/LogBlock/LogBlock.java +++ b/src/main/java/de/diddiz/LogBlock/LogBlock.java @@ -1,336 +1,375 @@ -package de.diddiz.LogBlock; - -import de.diddiz.LogBlock.config.Config; -import de.diddiz.LogBlock.listeners.*; -import de.diddiz.util.MySQLConnectionPool; -import de.diddiz.worldedit.WorldEditLoggingHook; -import org.bukkit.ChatColor; -import org.bukkit.command.Command; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.bukkit.permissions.Permission; -import org.bukkit.plugin.PluginManager; -import org.bukkit.plugin.java.JavaPlugin; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.sql.Connection; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.ArrayList; -import java.util.List; -import java.util.Timer; -import java.util.logging.Level; - -import static de.diddiz.LogBlock.config.Config.*; -import static de.diddiz.util.MaterialName.materialName; -import static org.bukkit.Bukkit.getPluginManager; - -public class LogBlock extends JavaPlugin { - private static LogBlock logblock = null; - private MySQLConnectionPool pool; - private Consumer consumer = null; - private CommandsHandler commandsHandler; - private Updater updater = null; - private Timer timer = null; - private boolean errorAtLoading = false, noDb = false, connected = true; - - public static LogBlock getInstance() { - return logblock; - } - - public Consumer getConsumer() { - return consumer; - } - - public CommandsHandler getCommandsHandler() { - return commandsHandler; - } - - Updater getUpdater() { - return updater; - } - - @Override - public void onLoad() { - logblock = this; - try { - updater = new Updater(this); - Config.load(this); - getLogger().info("Connecting to " + user + "@" + url + "..."); - pool = new MySQLConnectionPool(url, user, password); - final Connection conn = getConnection(); - if (conn == null) { - noDb = true; - return; - } - final Statement st = conn.createStatement(); - final ResultSet rs = st.executeQuery("SHOW CHARACTER SET where charset='utf8mb4';"); - if (rs.next()) { - Config.mb4 = true; - // Allegedly JDBC driver since 2010 hasn't needed this. I did. - st.executeQuery("SET NAMES utf8mb4;"); - } - conn.close(); - if (updater.update()) { - load(this); - } - updater.checkTables(); - } catch (final NullPointerException ex) { - getLogger().log(Level.SEVERE, "Error while loading: ", ex); - } catch (final Exception ex) { - getLogger().severe("Error while loading: " + ex.getMessage()); - errorAtLoading = true; - return; - } - consumer = new Consumer(this); - } - - @Override - public void onEnable() { - materialName(0); // Force static code to run - final PluginManager pm = getPluginManager(); - if (errorAtLoading) { - pm.disablePlugin(this); - return; - } - if (noDb) { - return; - } - if (pm.getPlugin("WorldEdit") != null) { - if (Integer.parseInt(pm.getPlugin("WorldEdit").getDescription().getVersion().substring(0, 1)) > 5) { - new WorldEditLoggingHook(this).hook(); - } else { - getLogger().warning("Failed to hook into WorldEdit. Your WorldEdit version seems to be outdated, please make sure WorldEdit is at least version 6."); - } - } - commandsHandler = new CommandsHandler(this); - getCommand("lb").setExecutor(commandsHandler); - if (enableAutoClearLog && autoClearLogDelay > 0) { - getServer().getScheduler().runTaskTimerAsynchronously(this, new AutoClearLog(this), 6000, autoClearLogDelay * 60 * 20); - } - getServer().getScheduler().runTaskAsynchronously(this, new DumpedLogImporter(this)); - registerEvents(); - if (useBukkitScheduler) { - if (getServer().getScheduler().runTaskTimerAsynchronously(this, consumer, delayBetweenRuns < 20 ? 20 : delayBetweenRuns, delayBetweenRuns).getTaskId() > 0) { - getLogger().info("Scheduled consumer with bukkit scheduler."); - } else { - getLogger().warning("Failed to schedule consumer with bukkit scheduler. Now trying schedule with timer."); - timer = new Timer(); - timer.schedule(consumer, delayBetweenRuns < 20 ? 1000 : delayBetweenRuns * 50, delayBetweenRuns * 50); - } - } else { - timer = new Timer(); - timer.schedule(consumer, delayBetweenRuns < 20 ? 1000 : delayBetweenRuns * 50, delayBetweenRuns * 50); - getLogger().info("Scheduled consumer with timer."); - } - getServer().getScheduler().runTaskAsynchronously(this, new Updater.PlayerCountChecker(this)); - for (final Tool tool : toolsByType.values()) { - if (pm.getPermission("logblock.tools." + tool.name) == null) { - final Permission perm = new Permission("logblock.tools." + tool.name, tool.permissionDefault); - pm.addPermission(perm); - } - } - try { - Metrics metrics = new Metrics(this); - metrics.start(); - } catch (IOException ex) { - getLogger().info("Could not start metrics: " + ex.getMessage()); - } - } - - private void registerEvents() { - final PluginManager pm = getPluginManager(); - pm.registerEvents(new ToolListener(this), this); - pm.registerEvents(new PlayerInfoLogging(this), this); - if (askRollbackAfterBan) { - pm.registerEvents(new BanListener(this), this); - } - if (isLogging(Logging.BLOCKPLACE)) { - pm.registerEvents(new BlockPlaceLogging(this), this); - } - if (isLogging(Logging.BLOCKPLACE) || isLogging(Logging.LAVAFLOW) || isLogging(Logging.WATERFLOW)) { - pm.registerEvents(new FluidFlowLogging(this), this); - } - if (isLogging(Logging.BLOCKBREAK)) { - pm.registerEvents(new BlockBreakLogging(this), this); - } - if (isLogging(Logging.SIGNTEXT)) { - pm.registerEvents(new SignChangeLogging(this), this); - } - if (isLogging(Logging.FIRE)) { - pm.registerEvents(new BlockBurnLogging(this), this); - } - if (isLogging(Logging.SNOWFORM)) { - pm.registerEvents(new SnowFormLogging(this), this); - } - if (isLogging(Logging.SNOWFADE)) { - pm.registerEvents(new SnowFadeLogging(this), this); - } - if (isLogging(Logging.CREEPEREXPLOSION) || isLogging(Logging.TNTEXPLOSION) || isLogging(Logging.GHASTFIREBALLEXPLOSION) || isLogging(Logging.ENDERDRAGON) || isLogging(Logging.MISCEXPLOSION)) { - pm.registerEvents(new ExplosionLogging(this), this); - } - if (isLogging(Logging.LEAVESDECAY)) { - pm.registerEvents(new LeavesDecayLogging(this), this); - } - if (isLogging(Logging.CHESTACCESS)) { - pm.registerEvents(new ChestAccessLogging(this), this); - } - if (isLogging(Logging.SWITCHINTERACT) || isLogging(Logging.DOORINTERACT) || isLogging(Logging.CAKEEAT) || isLogging(Logging.DIODEINTERACT) || isLogging(Logging.COMPARATORINTERACT) || isLogging(Logging.NOTEBLOCKINTERACT) || isLogging(Logging.PRESUREPLATEINTERACT) || isLogging(Logging.TRIPWIREINTERACT) || isLogging(Logging.CROPTRAMPLE)) { - pm.registerEvents(new InteractLogging(this), this); - } - if (isLogging(Logging.CREATURECROPTRAMPLE)) { - pm.registerEvents(new CreatureInteractLogging(this), this); - } - if (isLogging(Logging.KILL)) { - pm.registerEvents(new KillLogging(this), this); - } - if (isLogging(Logging.CHAT)) { - pm.registerEvents(new ChatLogging(this), this); - } - if (isLogging(Logging.ENDERMEN)) { - pm.registerEvents(new EndermenLogging(this), this); - } - if (isLogging(Logging.WITHER)) { - pm.registerEvents(new WitherLogging(this), this); - } - if (isLogging(Logging.NATURALSTRUCTUREGROW) || isLogging(Logging.BONEMEALSTRUCTUREGROW)) { - pm.registerEvents(new StructureGrowLogging(this), this); - } - if (isLogging(Logging.GRASSGROWTH) || isLogging(Logging.MYCELIUMSPREAD) || isLogging(Logging.VINEGROWTH) || isLogging(Logging.MUSHROOMSPREAD)) { - pm.registerEvents(new BlockSpreadLogging(this), this); - } - if (isLogging(Logging.LOCKEDCHESTDECAY)) { - pm.registerEvents(new LockedChestDecayLogging(this), this); - } - } - - @Override - public void onDisable() { - if (timer != null) { - timer.cancel(); - } - getServer().getScheduler().cancelTasks(this); - if (consumer != null) { - if (logPlayerInfo && getServer().getOnlinePlayers() != null) { - for (final Player player : getServer().getOnlinePlayers()) { - consumer.queueLeave(player); - } - } - getLogger().info("Waiting for consumer ..."); - consumer.run(); - if (consumer.getQueueSize() > 0) { - int tries = 9; - while (consumer.getQueueSize() > 0) { - getLogger().info("Remaining queue size: " + consumer.getQueueSize()); - if (tries > 0) { - getLogger().info("Remaining tries: " + tries); - } else { - getLogger().info("Unable to save queue to database. Trying to write to a local file."); - try { - consumer.writeToFile(); - getLogger().info("Successfully dumped queue."); - } catch (final FileNotFoundException ex) { - getLogger().info("Failed to write. Given up."); - break; - } - } - consumer.run(); - tries--; - } - } - } - if (pool != null) { - pool.close(); - } - } - - @Override - public boolean onCommand(CommandSender sender, Command cmd, String commandLabel, String[] args) { - if (noDb) { - sender.sendMessage(ChatColor.RED + "No database connected. Check your MySQL user/pw and database for typos. Start/restart your MySQL server."); - } - return true; - } - - public boolean hasPermission(CommandSender sender, String permission) { - return sender.hasPermission(permission); - } - - public Connection getConnection() { - try { - final Connection conn = pool.getConnection(); - if (!connected) { - getLogger().info("MySQL connection rebuild"); - connected = true; - } - return conn; - } catch (final Exception ex) { - if (connected) { - getLogger().log(Level.SEVERE, "Error while fetching connection: ", ex); - connected = false; - } else { - getLogger().severe("MySQL connection lost"); - } - return null; - } - } - - /** - * Returns a list of block changes based on the given query parameters, the query parameters - * are essentially programmatic versions of the parameters a player would pass - * to the logblock lookup command i.e /lb lookup query-parameters - * - * Note: this method directly calls a SQL query and is hence a slow blocking function, avoid running - * it on the main game thread - * - * @param params QueryParams that contains the needed columns (all other will be filled with default values) and the params. World is required. - * @return Returns a list of block changes based on the given query parameters - * @throws SQLException if a sql exception occurs while looking up the block changes - */ - public List getBlockChanges(QueryParams params) throws SQLException { - final Connection conn = getConnection(); - Statement state = null; - if (conn == null) { - throw new SQLException("No connection"); - } - try { - state = conn.createStatement(); - final ResultSet rs = state.executeQuery(params.getQuery()); - final List blockchanges = new ArrayList(); - while (rs.next()) { - blockchanges.add(new BlockChange(rs, params)); - } - return blockchanges; - } finally { - if (state != null) { - state.close(); - } - conn.close(); - } - } - - public int getCount(QueryParams params) throws SQLException { - final Connection conn = getConnection(); - Statement state = null; - if (conn == null) { - throw new SQLException("No connection"); - } - try { - state = conn.createStatement(); - final QueryParams p = params.clone(); - p.needCount = true; - final ResultSet rs = state.executeQuery(p.getQuery()); - if (!rs.next()) { - return 0; - } - return rs.getInt(1); - } finally { - if (state != null) { - state.close(); - } - conn.close(); - } - } -} +package de.diddiz.LogBlock; + +import de.diddiz.LogBlock.addons.worldguard.WorldGuardLoggingFlagsAddon; +import de.diddiz.LogBlock.config.Config; +import de.diddiz.LogBlock.listeners.*; +import de.diddiz.LogBlock.questioner.Questioner; +import de.diddiz.LogBlock.util.BukkitUtils; +import de.diddiz.LogBlock.util.MySQLConnectionPool; +import de.diddiz.LogBlock.worldedit.WorldEditHelper; +import de.diddiz.LogBlock.worldedit.WorldEditLoggingHook; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.permissions.Permission; +import org.bukkit.plugin.PluginManager; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.File; +import java.io.FileNotFoundException; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; + +import static de.diddiz.LogBlock.config.Config.*; +import static org.bukkit.Bukkit.getPluginManager; + +public class LogBlock extends JavaPlugin { + private static LogBlock logblock = null; + private MySQLConnectionPool pool; + private Consumer consumer = null; + private CommandsHandler commandsHandler; + private boolean noDb = false, connected = true; + private PlayerInfoLogging playerInfoLogging; + private ScaffoldingLogging scaffoldingLogging; + private Questioner questioner; + private WorldGuardLoggingFlagsAddon worldGuardLoggingFlagsAddon; + private boolean isConfigLoaded; + private volatile boolean isCompletelyEnabled; + + public static LogBlock getInstance() { + return logblock; + } + + public boolean isCompletelyEnabled() { + return isCompletelyEnabled; + } + + public Consumer getConsumer() { + return consumer; + } + + public CommandsHandler getCommandsHandler() { + return commandsHandler; + } + + @Override + public void onLoad() { + logblock = this; + BukkitUtils.isDoublePlant(Material.AIR); // Force static code to run + try { + Config.load(this); + isConfigLoaded = true; + } catch (final Exception ex) { + getLogger().log(Level.SEVERE, "Could not load LogBlock config! " + ex.getMessage(), ex); + } + if (Config.worldGuardLoggingFlags) { + if (getServer().getPluginManager().getPlugin("WorldGuard") == null) { + getLogger().log(Level.SEVERE, "Invalid config! addons.worldguardLoggingFlags is set to true, but WorldGuard is not loaded."); + } else { + worldGuardLoggingFlagsAddon = new WorldGuardLoggingFlagsAddon(this); + worldGuardLoggingFlagsAddon.onPluginLoad(); + } + } + } + + @Override + public void onEnable() { + final PluginManager pm = getPluginManager(); + if (!isConfigLoaded) { + pm.disablePlugin(this); + return; + } + consumer = new Consumer(this); + try { + getLogger().info("Connecting to " + user + "@" + url + "..."); + try { + Class.forName("com.mysql.cj.jdbc.Driver"); + } catch (ClassNotFoundException ignored) { + Class.forName("com.mysql.jdbc.Driver"); + } + pool = new MySQLConnectionPool(url, user, password, mysqlUseSSL, mysqlRequireSSL); + final Connection conn = getConnection(true); + if (conn == null) { + noDb = true; + return; + } + final Statement st = conn.createStatement(); + final ResultSet rs = st.executeQuery("SHOW CHARACTER SET where charset='utf8mb4';"); + if (rs.next()) { + Config.mb4 = true; + // Allegedly JDBC driver since 2010 hasn't needed this. I did. + st.executeUpdate("SET NAMES utf8mb4;"); + } + conn.close(); + Updater updater = new Updater(this); + updater.checkTables(); + MaterialConverter.initializeMaterials(getConnection()); + MaterialConverter.getOrAddMaterialId(Material.AIR); // AIR must be the first entry + EntityTypeConverter.initializeEntityTypes(getConnection()); + if (updater.update()) { + load(this); + } + } catch (final NullPointerException ex) { + getLogger().log(Level.SEVERE, "Error while loading: ", ex); + } catch (final Exception ex) { + getLogger().log(Level.SEVERE, "Error while loading: " + ex.getMessage(), ex); + pm.disablePlugin(this); + return; + } + + if (WorldEditHelper.hasWorldEdit()) { + new WorldEditLoggingHook(this).hook(); + } + commandsHandler = new CommandsHandler(this); + getCommand("lb").setExecutor(commandsHandler); + if (enableAutoClearLog && autoClearLogDelay > 0) { + getServer().getScheduler().runTaskTimerAsynchronously(this, new AutoClearLog(this), 6000, autoClearLogDelay * 60 * 20); + } + new DumpedLogImporter(this).run(); + registerEvents(); + consumer.start(); + for (final Tool tool : toolsByType.values()) { + if (pm.getPermission("logblock.tools." + tool.name) == null) { + final Permission perm = new Permission("logblock.tools." + tool.name, tool.permissionDefault); + pm.addPermission(perm); + } + } + questioner = new Questioner(this); + if (worldGuardLoggingFlagsAddon != null) { + worldGuardLoggingFlagsAddon.onPluginEnable(); + } + isCompletelyEnabled = true; + getServer().getScheduler().runTaskAsynchronously(this, new Updater.PlayerCountChecker(this)); + } + + private void registerEvents() { + final PluginManager pm = getPluginManager(); + pm.registerEvents(new ToolListener(this), this); + pm.registerEvents(playerInfoLogging = new PlayerInfoLogging(this), this); + if (askRollbackAfterBan) { + pm.registerEvents(new BanListener(this), this); + } + if (isLogging(Logging.BLOCKPLACE)) { + pm.registerEvents(new BlockPlaceLogging(this), this); + } + if (isLogging(Logging.LAVAFLOW) || isLogging(Logging.WATERFLOW)) { + pm.registerEvents(new FluidFlowLogging(this), this); + } + if (isLogging(Logging.BLOCKBREAK)) { + pm.registerEvents(new BlockBreakLogging(this), this); + } + if (isLogging(Logging.SIGNTEXT)) { + pm.registerEvents(new SignChangeLogging(this), this); + } + if (isLogging(Logging.FIRE)) { + pm.registerEvents(new BlockBurnLogging(this), this); + } + if (isLogging(Logging.SNOWFORM)) { + pm.registerEvents(new SnowFormLogging(this), this); + } + if (isLogging(Logging.SNOWFADE)) { + pm.registerEvents(new SnowFadeLogging(this), this); + } + if (isLogging(Logging.SCAFFOLDING)) { + pm.registerEvents(scaffoldingLogging = new ScaffoldingLogging(this), this); + } + if (isLogging(Logging.CAULDRONINTERACT)) { + pm.registerEvents(new CauldronLogging(this), this); + } + if (isLogging(Logging.CREEPEREXPLOSION) || isLogging(Logging.TNTEXPLOSION) || isLogging(Logging.GHASTFIREBALLEXPLOSION) || isLogging(Logging.ENDERDRAGON) || isLogging(Logging.MISCEXPLOSION)) { + pm.registerEvents(new ExplosionLogging(this), this); + } + if (isLogging(Logging.LEAVESDECAY)) { + pm.registerEvents(new LeavesDecayLogging(this), this); + } + if (isLogging(Logging.CHESTACCESS)) { + pm.registerEvents(new ChestAccessLogging(this), this); + } + if (isLogging(Logging.BLOCKBREAK) || isLogging(Logging.BLOCKPLACE) || isLogging(Logging.SWITCHINTERACT) || isLogging(Logging.DOORINTERACT) || isLogging(Logging.CAKEEAT) || isLogging(Logging.DIODEINTERACT) || isLogging(Logging.COMPARATORINTERACT) || isLogging(Logging.NOTEBLOCKINTERACT) + || isLogging(Logging.PRESUREPLATEINTERACT) || isLogging(Logging.TRIPWIREINTERACT) || isLogging(Logging.CROPTRAMPLE)) { + pm.registerEvents(new InteractLogging(this), this); + } + if (isLogging(Logging.CREATURECROPTRAMPLE)) { + pm.registerEvents(new CreatureInteractLogging(this), this); + } + if (isLogging(Logging.KILL)) { + pm.registerEvents(new KillLogging(this), this); + } + if (isLogging(Logging.CHAT) || isLogging(Logging.PLAYER_COMMANDS) || isLogging(Logging.CONSOLE_COMMANDS) || isLogging(Logging.COMMANDBLOCK_COMMANDS)) { + pm.registerEvents(new ChatLogging(this), this); + } + if (isLogging(Logging.WITHER) || isLogging(Logging.ENDERMEN)) { + pm.registerEvents(new EntityChangeBlockLogging(this), this); + } + if (isLogging(Logging.NATURALSTRUCTUREGROW)) { + pm.registerEvents(new StructureGrowLogging(this), this); + } + if (isLogging(Logging.BONEMEALSTRUCTUREGROW)) { + pm.registerEvents(new BlockFertilizeLogging(this), this); + } + if (isLogging(Logging.GRASSGROWTH) || isLogging(Logging.MYCELIUMSPREAD) || isLogging(Logging.VINEGROWTH) || isLogging(Logging.MUSHROOMSPREAD) || isLogging(Logging.BAMBOOGROWTH) || isLogging(Logging.DRIPSTONEGROWTH) || isLogging(Logging.SCULKSPREAD)) { + pm.registerEvents(new BlockSpreadLogging(this), this); + } + if (isLogging(Logging.DRAGONEGGTELEPORT)) { + pm.registerEvents(new DragonEggLogging(this), this); + } + if (isLogging(Logging.LECTERNBOOKCHANGE)) { + pm.registerEvents(new LecternLogging(this), this); + } + if (isLogging(Logging.OXIDIZATION)) { + pm.registerEvents(new OxidizationLogging(this), this); + } + if (Config.isLoggingAnyEntities()) { + if (!WorldEditHelper.hasFullWorldEdit()) { + getLogger().severe("No compatible WorldEdit found, entity logging will not work!"); + } else { + pm.registerEvents(new AdvancedEntityLogging(this), this); + getLogger().info("Entity logging enabled!"); + } + } + } + + @Override + public void onDisable() { + isCompletelyEnabled = false; + getServer().getScheduler().cancelTasks(this); + if (consumer != null) { + if (logPlayerInfo && playerInfoLogging != null) { + for (final Player player : getServer().getOnlinePlayers()) { + playerInfoLogging.onPlayerQuit(player); + } + } + getLogger().info("Waiting for consumer ..."); + consumer.shutdown(); + if (consumer.getQueueSize() > 0) { + getLogger().info("Remaining queue size: " + consumer.getQueueSize() + ". Trying to write to a local file."); + try { + consumer.writeToFile(); + getLogger().info("Successfully dumped queue."); + } catch (final FileNotFoundException ex) { + getLogger().info("Failed to write. Given up."); + } + } + } + if (pool != null) { + pool.close(); + } + } + + @Override + public boolean onCommand(CommandSender sender, Command cmd, String commandLabel, String[] args) { + if (noDb) { + sender.sendMessage(ChatColor.RED + "No database connected. Check your MySQL user/pw and database for typos. Start/restart your MySQL server."); + } + return true; + } + + public boolean hasPermission(CommandSender sender, String permission) { + return sender.hasPermission(permission); + } + + public Connection getConnection() { + return getConnection(false); + } + + public Connection getConnection(boolean testConnection) { + try { + final Connection conn = pool.getConnection(); + if (!connected) { + getLogger().info("MySQL connection rebuild"); + connected = true; + } + return conn; + } catch (final Exception ex) { + if (testConnection) { + getLogger().log(Level.SEVERE, "Could not connect to the Database! Please check your config! " + ex.getMessage()); + } else if (connected) { + getLogger().log(Level.SEVERE, "Error while fetching connection: ", ex); + connected = false; + } else { + getLogger().log(Level.SEVERE, "MySQL connection lost", ex); + } + return null; + } + } + + /** + * Returns a list of block changes based on the given query parameters, the query parameters + * are essentially programmatic versions of the parameters a player would pass + * to the logblock lookup command i.e /lb lookup query-parameters + * + * Note: this method directly calls a SQL query and is hence a slow blocking function, avoid running + * it on the main game thread + * + * @param params QueryParams that contains the needed columns (all other will be filled with default values) and the params. World is required. + * @return Returns a list of block changes based on the given query parameters + * @throws SQLException if a sql exception occurs while looking up the block changes + */ + public List getBlockChanges(QueryParams params) throws SQLException { + final Connection conn = getConnection(); + Statement state = null; + if (conn == null) { + throw new SQLException("No connection"); + } + try { + state = conn.createStatement(); + final ResultSet rs = state.executeQuery(params.getQuery()); + final List blockchanges = new ArrayList<>(); + while (rs.next()) { + blockchanges.add(new BlockChange(rs, params)); + } + return blockchanges; + } finally { + if (state != null) { + state.close(); + } + conn.close(); + } + } + + public int getCount(QueryParams params) throws SQLException { + if (params == null || params.world == null || !Config.isLogged(params.world)) { + throw new IllegalArgumentException("World is not logged: " + ((params == null || params.world == null) ? "null" : params.world.getName())); + } + final Connection conn = getConnection(); + Statement state = null; + if (conn == null) { + throw new SQLException("No connection"); + } + try { + state = conn.createStatement(); + final QueryParams p = params.clone(); + p.needCount = true; + final ResultSet rs = state.executeQuery(p.getQuery()); + if (!rs.next()) { + return 0; + } + return rs.getInt(1); + } finally { + if (state != null) { + state.close(); + } + conn.close(); + } + } + + @Override + public File getFile() { + return super.getFile(); + } + + public Questioner getQuestioner() { + return questioner; + } + + public ScaffoldingLogging getScaffoldingLogging() { + return scaffoldingLogging; + } +} diff --git a/src/main/java/de/diddiz/LogBlock/Logging.java b/src/main/java/de/diddiz/LogBlock/Logging.java index 77765181..5be67a48 100644 --- a/src/main/java/de/diddiz/LogBlock/Logging.java +++ b/src/main/java/de/diddiz/LogBlock/Logging.java @@ -1,27 +1,75 @@ -package de.diddiz.LogBlock; - -public enum Logging { - BLOCKPLACE(true), BLOCKBREAK(true), SIGNTEXT, TNTEXPLOSION(true), CREEPEREXPLOSION(true), - GHASTFIREBALLEXPLOSION(true), ENDERDRAGON(true), MISCEXPLOSION, FIRE(true), LEAVESDECAY, - LAVAFLOW, WATERFLOW, CHESTACCESS, KILL, CHAT, SNOWFORM, SNOWFADE, DOORINTERACT, - SWITCHINTERACT, CAKEEAT, ENDERMEN, NOTEBLOCKINTERACT, DIODEINTERACT, COMPARATORINTERACT, - PRESUREPLATEINTERACT, TRIPWIREINTERACT, CREATURECROPTRAMPLE, CROPTRAMPLE, - NATURALSTRUCTUREGROW, GRASSGROWTH, MYCELIUMSPREAD, VINEGROWTH, MUSHROOMSPREAD, - WITHER(true), WITHER_SKULL(true), BONEMEALSTRUCTUREGROW, - WORLDEDIT, TNTMINECARTEXPLOSION(true), LOCKEDCHESTDECAY; - - public static final int length = Logging.values().length; - private final boolean defaultEnabled; - - private Logging() { - this(false); - } - - private Logging(boolean defaultEnabled) { - this.defaultEnabled = defaultEnabled; - } - - public boolean isDefaultEnabled() { - return defaultEnabled; - } -} +package de.diddiz.LogBlock; + +public enum Logging { + BLOCKPLACE(true), + BLOCKBREAK(true), + SIGNTEXT(true), + TNTEXPLOSION(true), + CREEPEREXPLOSION(true), + GHASTFIREBALLEXPLOSION(true), + ENDERDRAGON(true), + MISCEXPLOSION(true), + FIRE(true), + LEAVESDECAY, + LAVAFLOW, + WATERFLOW, + CHESTACCESS, + KILL, + CHAT, + SNOWFORM, + SNOWFADE, + DOORINTERACT, + SWITCHINTERACT, + CAKEEAT, + ENDERMEN, + NOTEBLOCKINTERACT, + DIODEINTERACT, + COMPARATORINTERACT, + PRESUREPLATEINTERACT, + TRIPWIREINTERACT, + CAULDRONINTERACT(true), + CREATURECROPTRAMPLE, + CROPTRAMPLE, + NATURALSTRUCTUREGROW, + GRASSGROWTH, + MYCELIUMSPREAD, + VINEGROWTH, + DRIPSTONEGROWTH, + MUSHROOMSPREAD, + BAMBOOGROWTH, + WITHER(true), + WITHER_SKULL(true), + GRASS_EAT, + MISCENTITYCHANGEBLOCK(true), + BONEMEALSTRUCTUREGROW, + WORLDEDIT, + TNTMINECARTEXPLOSION(true), + ENDERCRYSTALEXPLOSION(true), + BEDEXPLOSION(true), + DRAGONEGGTELEPORT(true), + DAYLIGHTDETECTORINTERACT, + LECTERNBOOKCHANGE(true), + SCAFFOLDING(true), + OXIDIZATION, + SCULKSPREAD(true), + SHULKER_BOX_CONTENT, + RESPAWNANCHOREXPLOSION(true), + PLAYER_COMMANDS, + COMMANDBLOCK_COMMANDS, + CONSOLE_COMMANDS; + + public static final int length = Logging.values().length; + private final boolean defaultEnabled; + + private Logging() { + this(false); + } + + private Logging(boolean defaultEnabled) { + this.defaultEnabled = defaultEnabled; + } + + public boolean isDefaultEnabled() { + return defaultEnabled; + } +} diff --git a/src/main/java/de/diddiz/LogBlock/LookupCacheElement.java b/src/main/java/de/diddiz/LogBlock/LookupCacheElement.java index 595a2e39..88fc1c25 100644 --- a/src/main/java/de/diddiz/LogBlock/LookupCacheElement.java +++ b/src/main/java/de/diddiz/LogBlock/LookupCacheElement.java @@ -1,9 +1,18 @@ -package de.diddiz.LogBlock; - -import org.bukkit.Location; - -public interface LookupCacheElement { - public Location getLocation(); - - public String getMessage(); -} +package de.diddiz.LogBlock; + +import net.md_5.bungee.api.chat.BaseComponent; +import org.bukkit.Location; + +public interface LookupCacheElement { + public Location getLocation(); + + public default BaseComponent getLogMessage() { + return getLogMessage(-1); + } + + public BaseComponent getLogMessage(int entry); + + public default int getNumChanges() { + return 1; + } +} diff --git a/src/main/java/de/diddiz/LogBlock/LookupCacheElementFactory.java b/src/main/java/de/diddiz/LogBlock/LookupCacheElementFactory.java index 6a1af349..6bbbc456 100755 --- a/src/main/java/de/diddiz/LogBlock/LookupCacheElementFactory.java +++ b/src/main/java/de/diddiz/LogBlock/LookupCacheElementFactory.java @@ -1,34 +1,40 @@ -package de.diddiz.LogBlock; - -import de.diddiz.LogBlock.QueryParams.BlockChangeType; -import de.diddiz.LogBlock.QueryParams.SummarizationMode; - -import java.sql.ResultSet; -import java.sql.SQLException; - -public class LookupCacheElementFactory { - private final QueryParams params; - private final float spaceFactor; - - public LookupCacheElementFactory(QueryParams params, float spaceFactor) { - this.params = params; - this.spaceFactor = spaceFactor; - } - - public LookupCacheElement getLookupCacheElement(ResultSet rs) throws SQLException { - if (params.bct == BlockChangeType.CHAT) { - return new ChatMessage(rs, params); - } - if (params.bct == BlockChangeType.KILLS) { - if (params.sum == SummarizationMode.NONE) { - return new Kill(rs, params); - } else if (params.sum == SummarizationMode.PLAYERS) { - return new SummedKills(rs, params, spaceFactor); - } - } - if (params.sum == SummarizationMode.NONE) { - return new BlockChange(rs, params); - } - return new SummedBlockChanges(rs, params, spaceFactor); - } -} +package de.diddiz.LogBlock; + +import de.diddiz.LogBlock.QueryParams.BlockChangeType; +import de.diddiz.LogBlock.QueryParams.SummarizationMode; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class LookupCacheElementFactory { + private final QueryParams params; + private final float spaceFactor; + + public LookupCacheElementFactory(QueryParams params, float spaceFactor) { + this.params = params; + this.spaceFactor = spaceFactor; + } + + public LookupCacheElement getLookupCacheElement(ResultSet rs) throws SQLException { + if (params.bct == BlockChangeType.CHAT) { + return new ChatMessage(rs, params); + } + if (params.bct == BlockChangeType.KILLS) { + if (params.sum == SummarizationMode.NONE) { + return new Kill(rs, params); + } else if (params.sum == SummarizationMode.PLAYERS) { + return new SummedKills(rs, params, spaceFactor); + } + } + if (params.bct == BlockChangeType.ENTITIES || params.bct == BlockChangeType.ENTITIES_CREATED || params.bct == BlockChangeType.ENTITIES_KILLED) { + if (params.sum == SummarizationMode.NONE) { + return new EntityChange(rs, params); + } + return new SummedEntityChanges(rs, params, spaceFactor); + } + if (params.sum == SummarizationMode.NONE) { + return new BlockChange(rs, params); + } + return new SummedBlockChanges(rs, params, spaceFactor); + } +} diff --git a/src/main/java/de/diddiz/LogBlock/MaterialConverter.java b/src/main/java/de/diddiz/LogBlock/MaterialConverter.java new file mode 100644 index 00000000..3d0988cb --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/MaterialConverter.java @@ -0,0 +1,256 @@ +package de.diddiz.LogBlock; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Arrays; +import java.util.HashMap; +import java.util.logging.Level; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.block.data.BlockData; + +public class MaterialConverter { + private static String[] idToMaterial = new String[10]; + private static HashMap materialToID = new HashMap<>(); + private static int nextMaterialId; + + private static String[] idToBlockState = new String[10]; + private static HashMap blockStateToID = new HashMap<>(); + private static int nextBlockStateId; + + private static HashMap materialKeyToMaterial = new HashMap<>(); + + static { + for (Material m : Material.values()) { + materialKeyToMaterial.put(m.getKey().toString(), m); + } + } + + public synchronized static Integer getExistingMaterialId(BlockData blockData) { + return blockData == null ? null : getExistingMaterialId(blockData.getMaterial()); + } + + public synchronized static Integer getExistingMaterialId(Material material) { + if (material == null) { + return null; + } + String materialString = material.getKey().toString(); + return materialToID.get(materialString); + } + + public synchronized static int getOrAddMaterialId(BlockData blockData) { + return getOrAddMaterialId(blockData == null ? Material.AIR : blockData.getMaterial()); + } + + public synchronized static int getOrAddMaterialId(Material material) { + if (material == null) { + material = Material.AIR; + } + String materialString = material.getKey().toString(); + Integer key = materialToID.get(materialString); + int tries = 0; + while (key == null && tries < 10) { + tries++; + key = nextMaterialId; + Connection conn = LogBlock.getInstance().getConnection(); + try { + conn.setAutoCommit(false); + PreparedStatement smt = conn.prepareStatement("INSERT IGNORE INTO `lb-materials` (id, name) VALUES (?, ?)"); + smt.setInt(1, key); + smt.setString(2, materialString); + boolean couldAdd = smt.executeUpdate() > 0; + conn.commit(); + smt.close(); + if (couldAdd) { + internalAddMaterial(key, materialString); + } else { + initializeMaterials(conn); + } + } catch (Exception e) { + LogBlock.getInstance().getLogger().log(Level.SEVERE, "Could not update lb-materials", e); + reinitializeMaterialsCatchException(); + if (tries == 10) { + throw new RuntimeException(e); + } + } finally { + try { + conn.close(); + } catch (SQLException e) { + // ignored + } + } + key = materialToID.get(materialString); + } + return key.intValue(); + } + + public synchronized static Integer getExistingBlockStateId(BlockData blockData) { + if (blockData == null) { + return -1; + } + String blockDataString = blockData.getAsString(); + int dataPart = blockDataString.indexOf("["); + if (dataPart < 0) { + return -1; + } + String materialString = blockDataString.substring(dataPart); + return blockStateToID.get(materialString); + } + + public synchronized static int getOrAddBlockStateId(BlockData blockData) { + if (blockData == null) { + return -1; + } + String blockDataString = blockData.getAsString(); + int dataPart = blockDataString.indexOf("["); + if (dataPart < 0) { + return -1; + } + String materialString = blockDataString.substring(dataPart); + Integer key = blockStateToID.get(materialString); + int tries = 0; + while (key == null && tries < 10) { + tries++; + key = nextBlockStateId; + Connection conn = LogBlock.getInstance().getConnection(); + try { + conn.setAutoCommit(false); + PreparedStatement smt = conn.prepareStatement("INSERT IGNORE INTO `lb-blockstates` (id, name) VALUES (?, ?)"); + smt.setInt(1, key); + smt.setString(2, materialString); + boolean couldAdd = smt.executeUpdate() > 0; + conn.commit(); + smt.close(); + if (couldAdd) { + internalAddBlockState(key, materialString); + } else { + initializeMaterials(conn); + } + } catch (Exception e) { + LogBlock.getInstance().getLogger().log(Level.SEVERE, "Could not update lb-blockstates", e); + reinitializeMaterialsCatchException(); + if (tries == 10) { + throw new RuntimeException(e); + } + } finally { + try { + conn.close(); + } catch (SQLException e) { + // ignored + } + } + key = blockStateToID.get(materialString); + } + return key.intValue(); + } + + public synchronized static BlockData getBlockData(int materialId, int blockStateId) { + String material = materialId >= 0 && materialId < idToMaterial.length ? idToMaterial[materialId] : null; + if (material == null) { + return null; + } + if (blockStateId >= 0 && blockStateId < idToBlockState.length && idToBlockState[blockStateId] != null) { + material = material + updateBlockState(material, idToBlockState[blockStateId]); + } + try { + return Bukkit.createBlockData(material); + } catch (IllegalArgumentException ignored) { + // fall back to create the default block data for the material + try { + return Bukkit.createBlockData(idToMaterial[materialId]); + } catch (IllegalArgumentException ignored2) { + return null; + } + } + } + + public synchronized static Material getMaterial(int materialId) { + return materialId >= 0 && materialId < idToMaterial.length ? materialKeyToMaterial.get(idToMaterial[materialId]) : null; + } + + private static void reinitializeMaterialsCatchException() { + Connection conn = LogBlock.getInstance().getConnection(); + try { + initializeMaterials(conn); + } catch (Exception e) { + LogBlock.getInstance().getLogger().log(Level.SEVERE, "Could not reinitialize lb-materials", e); + } finally { + try { + conn.close(); + } catch (Exception e) { + // ignored + } + } + } + + protected synchronized static void initializeMaterials(Connection connection) throws SQLException { + Statement smt = connection.createStatement(); + ResultSet rs = smt.executeQuery("SELECT id, name FROM `lb-materials`"); + while (rs.next()) { + int key = rs.getInt(1); + String materialString = rs.getString(2); + internalAddMaterial(key, materialString); + } + rs.close(); + rs = smt.executeQuery("SELECT id, name FROM `lb-blockstates`"); + while (rs.next()) { + int key = rs.getInt(1); + String materialString = rs.getString(2); + internalAddBlockState(key, materialString); + } + rs.close(); + smt.close(); + connection.close(); + } + + private static void internalAddMaterial(int key, String materialString) { + materialToID.put(materialString, key); + int length = idToMaterial.length; + while (length <= key) { + length = (length * 3 / 2) + 5; + } + if (length > idToMaterial.length) { + idToMaterial = Arrays.copyOf(idToMaterial, length); + } + idToMaterial[key] = materialString; + if (nextMaterialId <= key) { + nextMaterialId = key + 1; + } + } + + private static void internalAddBlockState(int key, String materialString) { + blockStateToID.put(materialString, key); + int length = idToBlockState.length; + while (length <= key) { + length = (length * 3 / 2) + 5; + } + if (length > idToBlockState.length) { + idToBlockState = Arrays.copyOf(idToBlockState, length); + } + idToBlockState[key] = materialString; + if (nextBlockStateId <= key) { + nextBlockStateId = key + 1; + } + } + + private static String updateBlockState(String material, String blockState) { + // since 1.16 + if (material.endsWith("_wall")) { + if (blockState.contains("east=false") || blockState.contains("east=true")) { + blockState = blockState.replace("east=false", "east=none"); + blockState = blockState.replace("west=false", "west=none"); + blockState = blockState.replace("north=false", "north=none"); + blockState = blockState.replace("south=false", "south=none"); + blockState = blockState.replace("east=true", "east=low"); + blockState = blockState.replace("west=true", "west=low"); + blockState = blockState.replace("north=true", "north=low"); + blockState = blockState.replace("south=true", "south=low"); + } + } + return blockState; + } +} diff --git a/src/main/java/de/diddiz/LogBlock/Metrics.java b/src/main/java/de/diddiz/LogBlock/Metrics.java deleted file mode 100644 index 556efc7c..00000000 --- a/src/main/java/de/diddiz/LogBlock/Metrics.java +++ /dev/null @@ -1,750 +0,0 @@ -/* - * Copyright 2011-2013 Tyler Blair. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ''AS IS'' AND ANY EXPRESS OR IMPLIED - * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND - * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * The views and conclusions contained in the software and documentation are those of the - * authors and contributors and should not be interpreted as representing official policies, - * either expressed or implied, of anybody else. - */ -package de.diddiz.LogBlock; - -import org.bukkit.Bukkit; -import org.bukkit.configuration.InvalidConfigurationException; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.plugin.Plugin; -import org.bukkit.plugin.PluginDescriptionFile; -import org.bukkit.scheduler.BukkitTask; - -import java.io.*; -import java.net.Proxy; -import java.net.URL; -import java.net.URLConnection; -import java.net.URLEncoder; -import java.util.*; -import java.util.logging.Level; -import java.util.zip.GZIPOutputStream; - -public class Metrics { - - /** - * The current revision number - */ - private final static int REVISION = 7; - - /** - * The base url of the metrics domain - */ - private static final String BASE_URL = "http://report.mcstats.org"; - - /** - * The url used to report a server's status - */ - private static final String REPORT_URL = "/plugin/%s"; - - /** - * Interval of time to ping (in minutes) - */ - private static final int PING_INTERVAL = 15; - - /** - * The plugin this metrics submits for - */ - private final Plugin plugin; - - /** - * All of the custom graphs to submit to metrics - */ - private final Set graphs = Collections.synchronizedSet(new HashSet()); - - /** - * The plugin configuration file - */ - private final YamlConfiguration configuration; - - /** - * The plugin configuration file - */ - private final File configurationFile; - - /** - * Unique server id - */ - private final String guid; - - /** - * Debug mode - */ - private final boolean debug; - - /** - * Lock for synchronization - */ - private final Object optOutLock = new Object(); - - /** - * The scheduled task - */ - private volatile BukkitTask task = null; - - public Metrics(final Plugin plugin) throws IOException { - if (plugin == null) { - throw new IllegalArgumentException("Plugin cannot be null"); - } - - this.plugin = plugin; - - // load the config - configurationFile = getConfigFile(); - configuration = YamlConfiguration.loadConfiguration(configurationFile); - - // add some defaults - configuration.addDefault("opt-out", false); - configuration.addDefault("guid", UUID.randomUUID().toString()); - configuration.addDefault("debug", false); - - // Do we need to create the file? - if (configuration.get("guid", null) == null) { - configuration.options().header("http://mcstats.org").copyDefaults(true); - configuration.save(configurationFile); - } - - // Load the guid then - guid = configuration.getString("guid"); - debug = configuration.getBoolean("debug", false); - } - - /** - * Construct and create a Graph that can be used to separate specific plotters to their own graphs on the metrics - * website. Plotters can be added to the graph object returned. - * - * @param name The name of the graph - * @return Graph object created. Will never return NULL under normal circumstances unless bad parameters are given - */ - public Graph createGraph(final String name) { - if (name == null) { - throw new IllegalArgumentException("Graph name cannot be null"); - } - - // Construct the graph object - final Graph graph = new Graph(name); - - // Now we can add our graph - graphs.add(graph); - - // and return back - return graph; - } - - /** - * Add a Graph object to BukkitMetrics that represents data for the plugin that should be sent to the backend - * - * @param graph The name of the graph - */ - public void addGraph(final Graph graph) { - if (graph == null) { - throw new IllegalArgumentException("Graph cannot be null"); - } - - graphs.add(graph); - } - - /** - * Start measuring statistics. This will immediately create an async repeating task as the plugin and send the - * initial data to the metrics backend, and then after that it will post in increments of PING_INTERVAL * 1200 - * ticks. - * - * @return True if statistics measuring is running, otherwise false. - */ - public boolean start() { - synchronized (optOutLock) { - // Did we opt out? - if (isOptOut()) { - return false; - } - - // Is metrics already running? - if (task != null) { - return true; - } - - // Begin hitting the server with glorious data - task = plugin.getServer().getScheduler().runTaskTimerAsynchronously(plugin, new Runnable() { - - private boolean firstPost = true; - - public void run() { - try { - // This has to be synchronized or it can collide with the disable method. - synchronized (optOutLock) { - // Disable Task, if it is running and the server owner decided to opt-out - if (isOptOut() && task != null) { - task.cancel(); - task = null; - // Tell all plotters to stop gathering information. - for (Graph graph : graphs) { - graph.onOptOut(); - } - } - } - - // We use the inverse of firstPost because if it is the first time we are posting, - // it is not a interval ping, so it evaluates to FALSE - // Each time thereafter it will evaluate to TRUE, i.e PING! - postPlugin(!firstPost); - - // After the first post we set firstPost to false - // Each post thereafter will be a ping - firstPost = false; - } catch (IOException e) { - if (debug) { - Bukkit.getLogger().log(Level.INFO, "[Metrics] " + e.getMessage()); - } - } - } - }, 0, PING_INTERVAL * 1200); - - return true; - } - } - - /** - * Has the server owner denied plugin metrics? - * - * @return true if metrics should be opted out of it - */ - public boolean isOptOut() { - synchronized (optOutLock) { - try { - // Reload the metrics file - configuration.load(getConfigFile()); - } catch (IOException ex) { - if (debug) { - Bukkit.getLogger().log(Level.INFO, "[Metrics] " + ex.getMessage()); - } - return true; - } catch (InvalidConfigurationException ex) { - if (debug) { - Bukkit.getLogger().log(Level.INFO, "[Metrics] " + ex.getMessage()); - } - return true; - } - return configuration.getBoolean("opt-out", false); - } - } - - /** - * Enables metrics for the server by setting "opt-out" to false in the config file and starting the metrics task. - * - * @throws java.io.IOException - */ - public void enable() throws IOException { - // This has to be synchronized or it can collide with the check in the task. - synchronized (optOutLock) { - // Check if the server owner has already set opt-out, if not, set it. - if (isOptOut()) { - configuration.set("opt-out", false); - configuration.save(configurationFile); - } - - // Enable Task, if it is not running - if (task == null) { - start(); - } - } - } - - /** - * Disables metrics for the server by setting "opt-out" to true in the config file and canceling the metrics task. - * - * @throws java.io.IOException - */ - public void disable() throws IOException { - // This has to be synchronized or it can collide with the check in the task. - synchronized (optOutLock) { - // Check if the server owner has already set opt-out, if not, set it. - if (!isOptOut()) { - configuration.set("opt-out", true); - configuration.save(configurationFile); - } - - // Disable Task, if it is running - if (task != null) { - task.cancel(); - task = null; - } - } - } - - /** - * Gets the File object of the config file that should be used to store data such as the GUID and opt-out status - * - * @return the File object for the config file - */ - public File getConfigFile() { - // I believe the easiest way to get the base folder (e.g craftbukkit set via -P) for plugins to use - // is to abuse the plugin object we already have - // plugin.getDataFolder() => base/plugins/PluginA/ - // pluginsFolder => base/plugins/ - // The base is not necessarily relative to the startup directory. - File pluginsFolder = plugin.getDataFolder().getParentFile(); - - // return => base/plugins/PluginMetrics/config.yml - return new File(new File(pluginsFolder, "PluginMetrics"), "config.yml"); - } - - /** - * Generic method that posts a plugin to the metrics website - */ - private void postPlugin(final boolean isPing) throws IOException { - // Server software specific section - PluginDescriptionFile description = plugin.getDescription(); - String pluginName = description.getName(); - boolean onlineMode = Bukkit.getServer().getOnlineMode(); // TRUE if online mode is enabled - String pluginVersion = description.getVersion(); - String serverVersion = Bukkit.getVersion(); - int playersOnline = Bukkit.getServer().getOnlinePlayers().size(); - - // END server software specific section -- all code below does not use any code outside of this class / Java - - // Construct the post data - StringBuilder json = new StringBuilder(1024); - json.append('{'); - - // The plugin's description file containg all of the plugin data such as name, version, author, etc - appendJSONPair(json, "guid", guid); - appendJSONPair(json, "plugin_version", pluginVersion); - appendJSONPair(json, "server_version", serverVersion); - appendJSONPair(json, "players_online", Integer.toString(playersOnline)); - - // New data as of R6 - String osname = System.getProperty("os.name"); - String osarch = System.getProperty("os.arch"); - String osversion = System.getProperty("os.version"); - String java_version = System.getProperty("java.version"); - int coreCount = Runtime.getRuntime().availableProcessors(); - - // normalize os arch .. amd64 -> x86_64 - if (osarch.equals("amd64")) { - osarch = "x86_64"; - } - - appendJSONPair(json, "osname", osname); - appendJSONPair(json, "osarch", osarch); - appendJSONPair(json, "osversion", osversion); - appendJSONPair(json, "cores", Integer.toString(coreCount)); - appendJSONPair(json, "auth_mode", onlineMode ? "1" : "0"); - appendJSONPair(json, "java_version", java_version); - - // If we're pinging, append it - if (isPing) { - appendJSONPair(json, "ping", "1"); - } - - if (graphs.size() > 0) { - synchronized (graphs) { - json.append(','); - json.append('"'); - json.append("graphs"); - json.append('"'); - json.append(':'); - json.append('{'); - - boolean firstGraph = true; - - final Iterator iter = graphs.iterator(); - - while (iter.hasNext()) { - Graph graph = iter.next(); - - StringBuilder graphJson = new StringBuilder(); - graphJson.append('{'); - - for (Plotter plotter : graph.getPlotters()) { - appendJSONPair(graphJson, plotter.getColumnName(), Integer.toString(plotter.getValue())); - } - - graphJson.append('}'); - - if (!firstGraph) { - json.append(','); - } - - json.append(escapeJSON(graph.getName())); - json.append(':'); - json.append(graphJson); - - firstGraph = false; - } - - json.append('}'); - } - } - - // close json - json.append('}'); - - // Create the url - URL url = new URL(BASE_URL + String.format(REPORT_URL, urlEncode(pluginName))); - - // Connect to the website - URLConnection connection; - - // Mineshafter creates a socks proxy, so we can safely bypass it - // It does not reroute POST requests so we need to go around it - if (isMineshafterPresent()) { - connection = url.openConnection(Proxy.NO_PROXY); - } else { - connection = url.openConnection(); - } - - - byte[] uncompressed = json.toString().getBytes(); - byte[] compressed = gzip(json.toString()); - - // Headers - connection.addRequestProperty("User-Agent", "MCStats/" + REVISION); - connection.addRequestProperty("Content-Type", "application/json"); - connection.addRequestProperty("Content-Encoding", "gzip"); - connection.addRequestProperty("Content-Length", Integer.toString(compressed.length)); - connection.addRequestProperty("Accept", "application/json"); - connection.addRequestProperty("Connection", "close"); - - connection.setDoOutput(true); - - if (debug) { - System.out.println("[Metrics] Prepared request for " + pluginName + " uncompressed=" + uncompressed.length + " compressed=" + compressed.length); - } - - // Write the data - OutputStream os = connection.getOutputStream(); - os.write(compressed); - os.flush(); - - // Now read the response - final BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); - String response = reader.readLine(); - - // close resources - os.close(); - reader.close(); - - if (response == null || response.startsWith("ERR") || response.startsWith("7")) { - if (response == null) { - response = "null"; - } else if (response.startsWith("7")) { - response = response.substring(response.startsWith("7,") ? 2 : 1); - } - - throw new IOException(response); - } else { - // Is this the first update this hour? - if (response.equals("1") || response.contains("This is your first update this hour")) { - synchronized (graphs) { - final Iterator iter = graphs.iterator(); - - while (iter.hasNext()) { - final Graph graph = iter.next(); - - for (Plotter plotter : graph.getPlotters()) { - plotter.reset(); - } - } - } - } - } - } - - /** - * GZip compress a string of bytes - * - * @param input - * @return - */ - public static byte[] gzip(String input) { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - GZIPOutputStream gzos = null; - - try { - gzos = new GZIPOutputStream(baos); - gzos.write(input.getBytes("UTF-8")); - } catch (IOException e) { - e.printStackTrace(); - } finally { - if (gzos != null) { - try { - gzos.close(); - } catch (IOException ignore) { - } - } - } - - return baos.toByteArray(); - } - - /** - * Check if mineshafter is present. If it is, we need to bypass it to send POST requests - * - * @return true if mineshafter is installed on the server - */ - private boolean isMineshafterPresent() { - try { - Class.forName("mineshafter.MineServer"); - return true; - } catch (Exception e) { - return false; - } - } - - /** - * Appends a json encoded key/value pair to the given string builder. - * - * @param json - * @param key - * @param value - * @throws UnsupportedEncodingException - */ - private static void appendJSONPair(StringBuilder json, String key, String value) throws UnsupportedEncodingException { - boolean isValueNumeric = false; - - try { - if (value.equals("0") || !value.endsWith("0")) { - Double.parseDouble(value); - isValueNumeric = true; - } - } catch (NumberFormatException e) { - isValueNumeric = false; - } - - if (json.charAt(json.length() - 1) != '{') { - json.append(','); - } - - json.append(escapeJSON(key)); - json.append(':'); - - if (isValueNumeric) { - json.append(value); - } else { - json.append(escapeJSON(value)); - } - } - - /** - * Escape a string to create a valid JSON string - * - * @param text - * @return - */ - private static String escapeJSON(String text) { - StringBuilder builder = new StringBuilder(); - - builder.append('"'); - for (int index = 0; index < text.length(); index++) { - char chr = text.charAt(index); - - switch (chr) { - case '"': - case '\\': - builder.append('\\'); - builder.append(chr); - break; - case '\b': - builder.append("\\b"); - break; - case '\t': - builder.append("\\t"); - break; - case '\n': - builder.append("\\n"); - break; - case '\r': - builder.append("\\r"); - break; - default: - if (chr < ' ') { - String t = "000" + Integer.toHexString(chr); - builder.append("\\u" + t.substring(t.length() - 4)); - } else { - builder.append(chr); - } - break; - } - } - builder.append('"'); - - return builder.toString(); - } - - /** - * Encode text as UTF-8 - * - * @param text the text to encode - * @return the encoded text, as UTF-8 - */ - private static String urlEncode(final String text) throws UnsupportedEncodingException { - return URLEncoder.encode(text, "UTF-8"); - } - - /** - * Represents a custom graph on the website - */ - public static class Graph { - - /** - * The graph's name, alphanumeric and spaces only :) If it does not comply to the above when submitted, it is - * rejected - */ - private final String name; - - /** - * The set of plotters that are contained within this graph - */ - private final Set plotters = new LinkedHashSet(); - - private Graph(final String name) { - this.name = name; - } - - /** - * Gets the graph's name - * - * @return the Graph's name - */ - public String getName() { - return name; - } - - /** - * Add a plotter to the graph, which will be used to plot entries - * - * @param plotter the plotter to add to the graph - */ - public void addPlotter(final Plotter plotter) { - plotters.add(plotter); - } - - /** - * Remove a plotter from the graph - * - * @param plotter the plotter to remove from the graph - */ - public void removePlotter(final Plotter plotter) { - plotters.remove(plotter); - } - - /** - * Gets an unmodifiable set of the plotter objects in the graph - * - * @return an unmodifiable {@link java.util.Set} of the plotter objects - */ - public Set getPlotters() { - return Collections.unmodifiableSet(plotters); - } - - @Override - public int hashCode() { - return name.hashCode(); - } - - @Override - public boolean equals(final Object object) { - if (!(object instanceof Graph)) { - return false; - } - - final Graph graph = (Graph) object; - return graph.name.equals(name); - } - - /** - * Called when the server owner decides to opt-out of BukkitMetrics while the server is running. - */ - protected void onOptOut() { - } - } - - /** - * Interface used to collect custom data for a plugin - */ - public static abstract class Plotter { - - /** - * The plot's name - */ - private final String name; - - /** - * Construct a plotter with the default plot name - */ - public Plotter() { - this("Default"); - } - - /** - * Construct a plotter with a specific plot name - * - * @param name the name of the plotter to use, which will show up on the website - */ - public Plotter(final String name) { - this.name = name; - } - - /** - * Get the current value for the plotted point. Since this function defers to an external function it may or may - * not return immediately thus cannot be guaranteed to be thread friendly or safe. This function can be called - * from any thread so care should be taken when accessing resources that need to be synchronized. - * - * @return the current value for the point to be plotted. - */ - public abstract int getValue(); - - /** - * Get the column name for the plotted point - * - * @return the plotted point's column name - */ - public String getColumnName() { - return name; - } - - /** - * Called after the website graphs have been updated - */ - public void reset() { - } - - @Override - public int hashCode() { - return getColumnName().hashCode(); - } - - @Override - public boolean equals(final Object object) { - if (!(object instanceof Plotter)) { - return false; - } - - final Plotter plotter = (Plotter) object; - return plotter.name.equals(name) && plotter.getValue() == getValue(); - } - } -} \ No newline at end of file diff --git a/src/main/java/de/diddiz/LogBlock/QueryParams.java b/src/main/java/de/diddiz/LogBlock/QueryParams.java index d72ae77c..b0bd875c 100644 --- a/src/main/java/de/diddiz/LogBlock/QueryParams.java +++ b/src/main/java/de/diddiz/LogBlock/QueryParams.java @@ -1,911 +1,1140 @@ -package de.diddiz.LogBlock; - -import de.diddiz.util.Block; -import de.diddiz.util.Utils; -import de.diddiz.worldedit.RegionContainer; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.World; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.bukkit.plugin.Plugin; - -import java.util.*; - -import static de.diddiz.LogBlock.Session.getSession; -import static de.diddiz.LogBlock.config.Config.*; -import static de.diddiz.util.BukkitUtils.friendlyWorldname; -import static de.diddiz.util.BukkitUtils.getBlockEquivalents; -import static de.diddiz.util.MaterialName.materialName; -import static de.diddiz.util.MaterialName.typeFromName; -import static de.diddiz.util.Utils.*; - -public final class QueryParams implements Cloneable { - private static final Set keywords = new HashSet(Arrays.asList("player".hashCode(), "area".hashCode(), "selection".hashCode(), "sel".hashCode(), "block".hashCode(), "type".hashCode(), "sum".hashCode(), "destroyed".hashCode(), "created".hashCode(), "chestaccess".hashCode(), "all".hashCode(), "time".hashCode(), "since".hashCode(), "before".hashCode(), "limit".hashCode(), "world".hashCode(), "asc".hashCode(), "desc".hashCode(), "last".hashCode(), "coords".hashCode(), "silent".hashCode(), "chat".hashCode(), "search".hashCode(), "match".hashCode(), "loc".hashCode(), "location".hashCode(), "kills".hashCode(), "killer".hashCode(), "victim".hashCode(), "both".hashCode())); - public BlockChangeType bct = BlockChangeType.BOTH; - public int limit = -1, before = 0, since = 0, radius = -1; - public Location loc = null; - public Order order = Order.DESC; - public List players = new ArrayList(); - public List killers = new ArrayList(); - public List victims = new ArrayList(); - public boolean excludePlayersMode = false, excludeKillersMode = false, excludeVictimsMode = false, excludeBlocksMode = false, prepareToolQuery = false, silent = false; - public RegionContainer sel = null; - public SummarizationMode sum = SummarizationMode.NONE; - public List types = new ArrayList(); - public World world = null; - public String match = null; - public boolean needCount = false, needId = false, needDate = false, needType = false, needData = false, needPlayer = false, needCoords = false, needSignText = false, needChestAccess = false, needMessage = false, needKiller = false, needVictim = false, needWeapon = false; - private final LogBlock logblock; - - public QueryParams(LogBlock logblock) { - this.logblock = logblock; - } - - public QueryParams(LogBlock logblock, CommandSender sender, List args) throws IllegalArgumentException { - this.logblock = logblock; - parseArgs(sender, args); - } - - public static boolean isKeyWord(String param) { - return keywords.contains(param.toLowerCase().hashCode()); - } - - public String getLimit() { - return limit > 0 ? "LIMIT " + limit : ""; - } - - public String getQuery() { - if (bct == BlockChangeType.CHAT) { - String select = "SELECT "; - if (needCount) { - select += "COUNT(*) AS count"; - } else { - if (needId) { - select += "id, "; - } - if (needDate) { - select += "date, "; - } - if (needPlayer) { - select += "playername, UUID,"; - } - if (needMessage) { - select += "message, "; - } - select = select.substring(0, select.length() - 2); - } - String from = "FROM `lb-chat` "; - - if (needPlayer || players.size() > 0) { - from += "INNER JOIN `lb-players` USING (playerid) "; - } - return select + " " + from + getWhere() + "ORDER BY date " + order + ", id " + order + " " + getLimit(); - } - if (bct == BlockChangeType.KILLS) { - if (sum == SummarizationMode.NONE) { - String select = "SELECT "; - if (needCount) { - select += "COUNT(*) AS count"; - } else { - if (needId) { - select += "id, "; - } - if (needDate) { - select += "date, "; - } - if (needPlayer || needKiller) { - select += "killers.playername as killer, "; - } - if (needPlayer || needVictim) { - select += "victims.playername as victim, "; - } - if (needWeapon) { - select += "weapon, "; - } - if (needCoords) { - select += "x, y, z, "; - } - select = select.substring(0, select.length() - 2); - } - String from = "FROM `" + getTable() + "-kills` "; - - if (needPlayer || needKiller || killers.size() > 0) { - from += "INNER JOIN `lb-players` as killers ON (killer=killers.playerid) "; - } - - if (needPlayer || needVictim || victims.size() > 0) { - from += "INNER JOIN `lb-players` as victims ON (victim=victims.playerid) "; - } - - return select + " " + from + getWhere() + "ORDER BY date " + order + ", id " + order + " " + getLimit(); - } else if (sum == SummarizationMode.PLAYERS) { - return "SELECT playername, UUID, SUM(kills) AS kills, SUM(killed) AS killed FROM ((SELECT killer AS playerid, count(*) AS kills, 0 as killed FROM `" + getTable() + "-kills` INNER JOIN `lb-players` as killers ON (killer=killers.playerid) INNER JOIN `lb-players` as victims ON (victim=victims.playerid) " + getWhere(BlockChangeType.KILLS) + "GROUP BY killer) UNION (SELECT victim AS playerid, 0 as kills, count(*) AS killed FROM `" + getTable() + "-kills` INNER JOIN `lb-players` as killers ON (killer=killers.playerid) INNER JOIN `lb-players` as victims ON (victim=victims.playerid) " + getWhere(BlockChangeType.KILLS) + "GROUP BY victim)) AS t INNER JOIN `lb-players` USING (playerid) GROUP BY playerid ORDER BY SUM(kills) + SUM(killed) " + order + " " + getLimit(); - } - } - if (sum == SummarizationMode.NONE) { - String select = "SELECT "; - if (needCount) { - select += "COUNT(*) AS count"; - } else { - if (needId) { - select += "`" + getTable() + "`.id, "; - } - if (needDate) { - select += "date, "; - } - if (needType) { - select += "replaced, type, "; - } - if (needData) { - select += "data, "; - } - if (needPlayer) { - select += "playername, UUID, "; - } - if (needCoords) { - select += "x, y, z, "; - } - if (needSignText) { - select += "signtext, "; - } - if (needChestAccess) { - select += "itemtype, itemamount, itemdata, "; - } - select = select.substring(0, select.length() - 2); - } - String from = "FROM `" + getTable() + "` "; - if (needPlayer || players.size() > 0) { - from += "INNER JOIN `lb-players` USING (playerid) "; - } - if (needSignText) { - from += "LEFT JOIN `" + getTable() + "-sign` USING (id) "; - } - if (needChestAccess) - // If BlockChangeType is CHESTACCESS, we can use more efficient query - { - if (bct == BlockChangeType.CHESTACCESS) { - from += "RIGHT JOIN `" + getTable() + "-chest` USING (id) "; - } else { - from += "LEFT JOIN `" + getTable() + "-chest` USING (id) "; - } - } - return select + " " + from + getWhere() + "ORDER BY date " + order + ", id " + order + " " + getLimit(); - } else if (sum == SummarizationMode.TYPES) { - return "SELECT type, SUM(created) AS created, SUM(destroyed) AS destroyed FROM ((SELECT type, count(*) AS created, 0 AS destroyed FROM `" + getTable() + "` INNER JOIN `lb-players` USING (playerid) " + getWhere(BlockChangeType.CREATED) + "GROUP BY type) UNION (SELECT replaced AS type, 0 AS created, count(*) AS destroyed FROM `" + getTable() + "` INNER JOIN `lb-players` USING (playerid) " + getWhere(BlockChangeType.DESTROYED) + "GROUP BY replaced)) AS t GROUP BY type ORDER BY SUM(created) + SUM(destroyed) " + order + " " + getLimit(); - } else { - return "SELECT playername, UUID, SUM(created) AS created, SUM(destroyed) AS destroyed FROM ((SELECT playerid, count(*) AS created, 0 AS destroyed FROM `" + getTable() + "` " + getWhere(BlockChangeType.CREATED) + "GROUP BY playerid) UNION (SELECT playerid, 0 AS created, count(*) AS destroyed FROM `" + getTable() + "` " + getWhere(BlockChangeType.DESTROYED) + "GROUP BY playerid)) AS t INNER JOIN `lb-players` USING (playerid) GROUP BY playerid ORDER BY SUM(created) + SUM(destroyed) " + order + " " + getLimit(); - } - } - - public String getTable() { - return getWorldConfig(world).table; - } - - public String getTitle() { - final StringBuilder title = new StringBuilder(); - if (bct == BlockChangeType.CHESTACCESS) { - title.append("chest accesses "); - } else if (bct == BlockChangeType.CHAT) { - title.append("chat messages "); - } else if (bct == BlockChangeType.KILLS) { - title.append("kills "); - } else { - if (!types.isEmpty()) { - if (excludeBlocksMode) { - title.append("all blocks except "); - } - final String[] blocknames = new String[types.size()]; - for (int i = 0; i < types.size(); i++) { - blocknames[i] = materialName(types.get(i).getBlock()); - } - title.append(listing(blocknames, ", ", " and ")).append(" "); - } else { - title.append("block "); - } - if (bct == BlockChangeType.CREATED) { - title.append("creations "); - } else if (bct == BlockChangeType.DESTROYED) { - title.append("destructions "); - } else { - title.append("changes "); - } - } - if (killers.size() > 10) { - title.append(excludeKillersMode ? "without" : "from").append(" many killers "); - } else if (!killers.isEmpty()) { - title.append(excludeKillersMode ? "without" : "from").append(" ").append(listing(killers.toArray(new String[killers.size()]), ", ", " and ")).append(" "); - } - if (victims.size() > 10) { - title.append(excludeVictimsMode ? "without" : "of").append(" many victims "); - } else if (!victims.isEmpty()) { - title.append(excludeVictimsMode ? "without" : "of").append(" victim").append(victims.size() != 1 ? "s" : "").append(" ").append(listing(victims.toArray(new String[victims.size()]), ", ", " and ")).append(" "); - } - if (players.size() > 10) { - title.append(excludePlayersMode ? "without" : "from").append(" many players "); - } else if (!players.isEmpty()) { - title.append(excludePlayersMode ? "without" : "from").append(" player").append(players.size() != 1 ? "s" : "").append(" ").append(listing(players.toArray(new String[players.size()]), ", ", " and ")).append(" "); - } - if (match != null && match.length() > 0) { - title.append("matching '").append(match).append("' "); - } - if (before > 0 && since > 0) { - title.append("between ").append(since).append(" and ").append(before).append(" minutes ago "); - } else if (since > 0) { - title.append("in the last ").append(since).append(" minutes "); - } else if (before > 0) { - title.append("more than ").append(before * -1).append(" minutes ago "); - } - if (loc != null) { - if (radius > 0) { - title.append("within ").append(radius).append(" blocks of ").append(prepareToolQuery ? "clicked block" : "location").append(" "); - } else if (radius == 0) { - title.append("at ").append(loc.getBlockX()).append(":").append(loc.getBlockY()).append(":").append(loc.getBlockZ()).append(" "); - } - } else if (sel != null) { - title.append(prepareToolQuery ? "at double chest " : "inside selection "); - } else if (prepareToolQuery) { - if (radius > 0) { - title.append("within ").append(radius).append(" blocks of clicked block "); - } else if (radius == 0) { - title.append("at clicked block "); - } - } - if (world != null && !(sel != null && prepareToolQuery)) { - title.append("in ").append(friendlyWorldname(world.getName())).append(" "); - } - if (sum != SummarizationMode.NONE) { - title.append("summed up by ").append(sum == SummarizationMode.TYPES ? "blocks" : "players").append(" "); - } - title.deleteCharAt(title.length() - 1); - title.setCharAt(0, String.valueOf(title.charAt(0)).toUpperCase().toCharArray()[0]); - return title.toString(); - } - - public String getWhere() { - return getWhere(bct); - } - - public String getWhere(BlockChangeType blockChangeType) { - final StringBuilder where = new StringBuilder("WHERE "); - if (blockChangeType == BlockChangeType.CHAT) { - if (match != null && match.length() > 0) { - final boolean unlike = match.startsWith("-"); - if (match.length() > 3 && !unlike || match.length() > 4) { - where.append("MATCH (message) AGAINST ('").append(match).append("' IN BOOLEAN MODE) AND "); - } else { - where.append("message ").append(unlike ? "NOT " : "").append("LIKE '%").append(unlike ? match.substring(1) : match).append("%' AND "); - } - } - } else if (blockChangeType == BlockChangeType.KILLS) { - if (!players.isEmpty()) { - if (!excludePlayersMode) { - where.append('('); - for (final String killerName : players) { - where.append("killers.playername = '").append(killerName).append("' OR "); - } - for (final String victimName : players) { - where.append("victims.playername = '").append(victimName).append("' OR "); - } - where.delete(where.length() - 4, where.length()); - where.append(") AND "); - } else { - for (final String killerName : players) { - where.append("killers.playername != '").append(killerName).append("' AND "); - } - for (final String victimName : players) { - where.append("victims.playername != '").append(victimName).append("' AND "); - } - } - } - - if (!killers.isEmpty()) { - if (!excludeKillersMode) { - where.append('('); - for (final String killerName : killers) { - where.append("killers.playername = '").append(killerName).append("' OR "); - } - where.delete(where.length() - 4, where.length()); - where.append(") AND "); - } else { - for (final String killerName : killers) { - where.append("killers.playername != '").append(killerName).append("' AND "); - } - } - } - - if (!victims.isEmpty()) { - if (!excludeVictimsMode) { - where.append('('); - for (final String victimName : victims) { - where.append("victims.playername = '").append(victimName).append("' OR "); - } - where.delete(where.length() - 4, where.length()); - where.append(") AND "); - } else { - for (final String victimName : victims) { - where.append("victims.playername != '").append(victimName).append("' AND "); - } - } - } - - if (loc != null) { - if (radius == 0) { - compileLocationQuery( - where, - loc.getBlockX(), loc.getBlockX(), - loc.getBlockY(), loc.getBlockY(), - loc.getBlockZ(), loc.getBlockZ() - ); - } else if (radius > 0) { - compileLocationQuery( - where, - loc.getBlockX() - radius + 1, loc.getBlockX() + radius - 1, - loc.getBlockY() - radius + 1, loc.getBlockY() + radius - 1, - loc.getBlockZ() - radius + 1, loc.getBlockZ() + radius - 1 - ); - } - - } else if (sel != null) { - compileLocationQuery( - where, - sel.getSelection().getMinimumPoint().getBlockX(), sel.getSelection().getMaximumPoint().getBlockX(), - sel.getSelection().getMinimumPoint().getBlockY(), sel.getSelection().getMaximumPoint().getBlockY(), - sel.getSelection().getMinimumPoint().getBlockZ(), sel.getSelection().getMaximumPoint().getBlockZ() - ); - } - - } else { - switch (blockChangeType) { - case ALL: - if (!types.isEmpty()) { - if (excludeBlocksMode) { - where.append("NOT "); - } - where.append('('); - for (final Block block : types) { - where.append("((type = ").append(block.getBlock()).append(" OR replaced = ").append(block.getBlock()); - if (block.getData() != -1) { - where.append(") AND data = ").append(block.getData()); - } else { - where.append(")"); - } - where.append(") OR "); - } - where.delete(where.length() - 4, where.length() - 1); - where.append(") AND "); - } - break; - case BOTH: - if (!types.isEmpty()) { - if (excludeBlocksMode) { - where.append("NOT "); - } - where.append('('); - for (final Block block : types) { - where.append("((type = ").append(block.getBlock()).append(" OR replaced = ").append(block.getBlock()); - if (block.getData() != -1) { - where.append(") AND data = ").append(block.getData()); - } else { - where.append(")"); - } - where.append(") OR "); - } - where.delete(where.length() - 4, where.length()); - where.append(") AND "); - } - where.append("type != replaced AND "); - break; - case CREATED: - if (!types.isEmpty()) { - if (excludeBlocksMode) { - where.append("NOT "); - } - where.append('('); - for (final Block block : types) { - where.append("((type = ").append(block.getBlock()); - if (block.getData() != -1) { - where.append(") AND data = ").append(block.getData()); - } else { - where.append(")"); - } - where.append(") OR "); - } - where.delete(where.length() - 4, where.length()); - where.append(") AND "); - } - where.append("type != 0 AND type != replaced AND "); - break; - case DESTROYED: - if (!types.isEmpty()) { - if (excludeBlocksMode) { - where.append("NOT "); - } - where.append('('); - for (final Block block : types) { - where.append("((replaced = ").append(block.getBlock()); - if (block.getData() != -1) { - where.append(") AND data = ").append(block.getData()); - } else { - where.append(")"); - } - where.append(") OR "); - } - where.delete(where.length() - 4, where.length()); - where.append(") AND "); - } - where.append("replaced != 0 AND type != replaced AND "); - break; - case CHESTACCESS: - if (!types.isEmpty()) { - if (excludeBlocksMode) { - where.append("NOT "); - } - where.append('('); - for (final Block block : types) { - where.append("((itemtype = ").append(block.getBlock()); - if (block.getData() != -1) { - where.append(") AND itemdata = ").append(block.getData()); - } else { - where.append(")"); - } - where.append(") OR "); - } - where.delete(where.length() - 4, where.length()); - where.append(") AND "); - } - break; - } - if (loc != null) { - if (radius == 0) { - compileLocationQuery( - where, - loc.getBlockX(), loc.getBlockX(), - loc.getBlockY(), loc.getBlockY(), - loc.getBlockZ(), loc.getBlockZ() - ); - } else if (radius > 0) { - compileLocationQuery( - where, - loc.getBlockX() - radius + 1, loc.getBlockX() + radius - 1, - loc.getBlockY() - radius + 1, loc.getBlockY() + radius - 1, - loc.getBlockZ() - radius + 1, loc.getBlockZ() + radius - 1 - ); - } - - } else if (sel != null) { - compileLocationQuery( - where, - sel.getSelection().getMinimumPoint().getBlockX(), sel.getSelection().getMaximumPoint().getBlockX(), - sel.getSelection().getMinimumPoint().getBlockY(), sel.getSelection().getMaximumPoint().getBlockY(), - sel.getSelection().getMinimumPoint().getBlockZ(), sel.getSelection().getMaximumPoint().getBlockZ() - ); - } - - } - if (!players.isEmpty() && sum != SummarizationMode.PLAYERS && blockChangeType != BlockChangeType.KILLS) { - if (!excludePlayersMode) { - where.append('('); - for (final String playerName : players) { - where.append("playername = '").append(playerName).append("' OR "); - } - where.delete(where.length() - 4, where.length()); - where.append(") AND "); - } else { - for (final String playerName : players) { - where.append("playername != '").append(playerName).append("' AND "); - } - } - } - if (since > 0) { - where.append("date > date_sub(now(), INTERVAL ").append(since).append(" MINUTE) AND "); - } - if (before > 0) { - where.append("date < date_sub(now(), INTERVAL ").append(before).append(" MINUTE) AND "); - } - if (where.length() > 6) { - where.delete(where.length() - 4, where.length()); - } else { - where.delete(0, where.length()); - } - return where.toString(); - } - - private void compileLocationQuery(StringBuilder where, int blockX, int blockX2, int blockY, int blockY2, int blockZ, int blockZ2) { - compileLocationQueryPart(where, "x", blockX, blockX2); - where.append(" AND "); - compileLocationQueryPart(where, "y", blockY, blockY2); - where.append(" AND "); - compileLocationQueryPart(where, "z", blockZ, blockZ2); - where.append(" AND "); - } - - private void compileLocationQueryPart(StringBuilder where, String locValue, int loc, int loc2) { - int min = Math.min(loc, loc2); - int max = Math.max(loc2, loc); - - if (min == max) { - where.append(locValue).append(" = ").append(min); - } else if (max - min > 50) { - where.append(locValue).append(" >= ").append(min).append(" AND ").append(locValue).append(" <= ").append(max); - } else { - where.append(locValue).append(" in ("); - for (int c = min; c < max; c++) { - where.append(c).append(","); - } - where.append(max); - where.append(")"); - } - } - - public void parseArgs(CommandSender sender, List args) throws IllegalArgumentException { - if (args == null || args.isEmpty()) { - throw new IllegalArgumentException("No parameters specified."); - } - args = Utils.parseQuotes(args); - final Player player = sender instanceof Player ? (Player) sender : null; - final Session session = prepareToolQuery ? null : getSession(sender); - if (player != null && world == null) { - world = player.getWorld(); - } - for (int i = 0; i < args.size(); i++) { - final String param = args.get(i).toLowerCase(); - final String[] values = getValues(args, i + 1); - if (param.equals("last")) { - if (session.lastQuery == null) { - throw new IllegalArgumentException("This is your first command, you can't use last."); - } - merge(session.lastQuery); - } else if (param.equals("player")) { - if (values.length < 1) { - throw new IllegalArgumentException("No or wrong count of arguments for '" + param + "'"); - } - for (final String playerName : values) { - if (playerName.length() > 0) { - if (playerName.contains("!")) { - excludePlayersMode = true; - } - if (playerName.contains("\"")) { - players.add(playerName.replaceAll("[^a-zA-Z0-9_]", "")); - } else { - final List matches = logblock.getServer().matchPlayer(playerName); - if (matches.size() > 1) { - throw new IllegalArgumentException("Ambiguous playername '" + param + "'"); - } - players.add(matches.size() == 1 ? matches.get(0).getName() : playerName.replaceAll("[^a-zA-Z0-9_]", "")); - } - } - } - needPlayer = true; - } else if (param.equals("killer")) { - if (values.length < 1) { - throw new IllegalArgumentException("No or wrong count of arguments for '" + param + "'"); - } - for (final String killerName : values) { - if (killerName.length() > 0) { - if (killerName.contains("!")) { - excludeVictimsMode = true; - } - if (killerName.contains("\"")) { - killers.add(killerName.replaceAll("[^a-zA-Z0-9_]", "")); - } else { - final List matches = logblock.getServer().matchPlayer(killerName); - if (matches.size() > 1) { - throw new IllegalArgumentException("Ambiguous victimname '" + param + "'"); - } - killers.add(matches.size() == 1 ? matches.get(0).getName() : killerName.replaceAll("[^a-zA-Z0-9_]", "")); - } - } - } - needKiller = true; - } else if (param.equals("victim")) { - if (values.length < 1) { - throw new IllegalArgumentException("No or wrong count of arguments for '" + param + "'"); - } - for (final String victimName : values) { - if (victimName.length() > 0) { - if (victimName.contains("!")) { - excludeVictimsMode = true; - } - if (victimName.contains("\"")) { - victims.add(victimName.replaceAll("[^a-zA-Z0-9_]", "")); - } else { - final List matches = logblock.getServer().matchPlayer(victimName); - if (matches.size() > 1) { - throw new IllegalArgumentException("Ambiguous victimname '" + param + "'"); - } - victims.add(matches.size() == 1 ? matches.get(0).getName() : victimName.replaceAll("[^a-zA-Z0-9_]", "")); - } - } - } - needVictim = true; - } else if (param.equals("weapon")) { - if (values.length < 1) { - throw new IllegalArgumentException("No or wrong count of arguments for '" + param + "'"); - } - for (final String weaponName : values) { - Material mat = Material.matchMaterial(weaponName); - if (mat == null) { - try { - mat = Material.getMaterial(Integer.parseInt(weaponName)); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Data type not a valid number: '" + weaponName + "'"); - } - } - if (mat == null) { - throw new IllegalArgumentException("No material matching: '" + weaponName + "'"); - } - types.add(new Block(mat.getId(), -1)); - } - needWeapon = true; - } else if (param.equals("block") || param.equals("type")) { - if (values.length < 1) { - throw new IllegalArgumentException("No or wrong count of arguments for '" + param + "'"); - } - for (String blockName : values) { - if (blockName.startsWith("!")) { - excludeBlocksMode = true; - blockName = blockName.substring(1); - } - if (blockName.contains(":")) { - String[] blockNameSplit = blockName.split(":"); - if (blockNameSplit.length > 2) { - throw new IllegalArgumentException("No material matching: '" + blockName + "'"); - } - final int data; - try { - data = Integer.parseInt(blockNameSplit[1]); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Data type not a valid number: '" + blockNameSplit[1] + "'"); - } - if (data > 255 || data < 0) { - throw new IllegalArgumentException("Data type out of range (0-255): '" + data + "'"); - } - final Material mat = Material.matchMaterial(blockNameSplit[0]); - if (mat == null) { - throw new IllegalArgumentException("No material matching: '" + blockName + "'"); - } - types.add(new Block(mat.getId(), data)); - } else { - final Material mat = Material.matchMaterial(blockName); - types.add(new Block(typeFromName(blockName), -1)); - } - } - } else if (param.equals("area")) { - if (player == null && !prepareToolQuery && loc == null) { - throw new IllegalArgumentException("You have to be a player to use area, or specify a location first"); - } - if (values.length == 0) { - radius = defaultDist; - if (!prepareToolQuery && loc == null) { - loc = player.getLocation(); - } - } else { - if (!isInt(values[0])) { - throw new IllegalArgumentException("Not a number: '" + values[0] + "'"); - } - radius = Integer.parseInt(values[0]); - if (!prepareToolQuery && loc == null) { - loc = player.getLocation(); - } - } - } else if (param.equals("selection") || param.equals("sel")) { - if (player == null) { - throw new IllegalArgumentException("You have to ba a player to use selection"); - } - final Plugin we = player.getServer().getPluginManager().getPlugin("WorldEdit"); - if (we != null) { - setSelection(RegionContainer.fromPlayerSelection(player, we)); - } else { - throw new IllegalArgumentException("WorldEdit not found!"); - } - } else if (param.equals("time") || param.equals("since")) { - since = values.length > 0 ? parseTimeSpec(values) : defaultTime; - if (since == -1) { - throw new IllegalArgumentException("Failed to parse time spec for '" + param + "'"); - } - } else if (param.equals("before")) { - before = values.length > 0 ? parseTimeSpec(values) : defaultTime; - if (before == -1) { - throw new IllegalArgumentException("Faile to parse time spec for '" + param + "'"); - } - } else if (param.equals("sum")) { - if (values.length != 1) { - throw new IllegalArgumentException("No or wrong count of arguments for '" + param + "'"); - } - if (values[0].startsWith("p")) { - sum = SummarizationMode.PLAYERS; - } else if (values[0].startsWith("b")) { - sum = SummarizationMode.TYPES; - } else if (values[0].startsWith("n")) { - sum = SummarizationMode.NONE; - } else { - throw new IllegalArgumentException("Wrong summarization mode"); - } - } else if (param.equals("created")) { - bct = BlockChangeType.CREATED; - } else if (param.equals("destroyed")) { - bct = BlockChangeType.DESTROYED; - } else if (param.equals("both")) { - bct = BlockChangeType.BOTH; - } else if (param.equals("chestaccess")) { - bct = BlockChangeType.CHESTACCESS; - } else if (param.equals("chat")) { - bct = BlockChangeType.CHAT; - } else if (param.equals("kills")) { - bct = BlockChangeType.KILLS; - } else if (param.equals("all")) { - bct = BlockChangeType.ALL; - } else if (param.equals("limit")) { - if (values.length != 1) { - throw new IllegalArgumentException("Wrong count of arguments for '" + param + "'"); - } - if (!isInt(values[0])) { - throw new IllegalArgumentException("Not a number: '" + values[0] + "'"); - } - limit = Integer.parseInt(values[0]); - } else if (param.equals("world")) { - if (values.length != 1) { - throw new IllegalArgumentException("Wrong count of arguments for '" + param + "'"); - } - final World w = sender.getServer().getWorld(values[0].replace("\"", "")); - if (w == null) { - throw new IllegalArgumentException("There is no world called '" + values[0] + "'"); - } - world = w; - } else if (param.equals("asc")) { - order = Order.ASC; - } else if (param.equals("desc")) { - order = Order.DESC; - } else if (param.equals("coords")) { - needCoords = true; - } else if (param.equals("silent")) { - silent = true; - } else if (param.equals("search") || param.equals("match")) { - if (values.length == 0) { - throw new IllegalArgumentException("No arguments for '" + param + "'"); - } - match = mysqlTextEscape(join(values, " ")); - } else if (param.equals("loc") || param.equals("location")) { - final String[] vectors = values.length == 1 ? values[0].split(":") : values; - if (vectors.length != 3) { - throw new IllegalArgumentException("Wrong count arguments for '" + param + "'"); - } - for (final String vec : vectors) { - if (!isInt(vec)) { - throw new IllegalArgumentException("Not a number: '" + vec + "'"); - } - } - loc = new Location(null, Integer.valueOf(vectors[0]), Integer.valueOf(vectors[1]), Integer.valueOf(vectors[2])); - radius = 0; - } else { - throw new IllegalArgumentException("Not a valid argument: '" + param + "'"); - } - i += values.length; - } - if (bct == BlockChangeType.KILLS) { - if (world == null) { - throw new IllegalArgumentException("No world specified"); - } - if (!getWorldConfig(world).isLogging(Logging.KILL)) { - throw new IllegalArgumentException("Kill logging not enabled for world '" + world.getName() + "'"); - } - } - if (types.size() > 0) { - for (final Set equivalent : getBlockEquivalents()) { - boolean found = false; - for (final Block block : types) { - if (equivalent.contains(block.getBlock())) { - found = true; - break; - } - } - if (found) { - for (final Integer type : equivalent) { - if (!Block.inList(types, type)) { - types.add(new Block(type, -1)); - } - } - } - } - } - if (!prepareToolQuery && bct != BlockChangeType.CHAT) { - if (world == null) { - throw new IllegalArgumentException("No world specified"); - } - if (!isLogged(world)) { - throw new IllegalArgumentException("This world ('" + world.getName() + "') isn't logged"); - } - } - if (session != null) { - session.lastQuery = clone(); - } - } - - public void setLocation(Location loc) { - this.loc = loc; - world = loc.getWorld(); - } - - public void setSelection(RegionContainer container) { - this.sel = container; - world = sel.getSelection().getWorld(); - } - - public void setPlayer(String playerName) { - players.clear(); - players.add(playerName); - } - - @Override - protected QueryParams clone() { - try { - final QueryParams params = (QueryParams) super.clone(); - params.players = new ArrayList(players); - params.types = new ArrayList(types); - return params; - } catch (final CloneNotSupportedException ex) { - } - return null; - } - - private static String[] getValues(List args, int offset) { - // The variable i will store the last value's index - int i; - // Iterate over the all the values from the offset up till the end - for (i = offset; i < args.size(); i++) { - // We found a keyword, break here since anything after this isn't a value. - if (isKeyWord(args.get(i))) { - break; - } - } - // If there are no values, i.e there is a keyword immediately after the offset - // return an empty string array - if (i == offset) { - return new String[0]; - } - - final String[] values = new String[i - offset]; - for (int j = offset; j < i; j++) { - String value = args.get(j); - - // If the value is encapsulated in quotes, strip them - if (value.startsWith("\"") && value.endsWith("\"")) { - value = value.substring(1, value.length() - 1); - } - values[j - offset] = value; - } - return values; - } - - public void merge(QueryParams p) { - players = p.players; - excludePlayersMode = p.excludePlayersMode; - types = p.types; - loc = p.loc; - radius = p.radius; - sel = p.sel; - if (p.since != 0 || since != defaultTime) { - since = p.since; - } - before = p.before; - sum = p.sum; - bct = p.bct; - limit = p.limit; - world = p.world; - order = p.order; - match = p.match; - } - - public static enum BlockChangeType { - ALL, BOTH, CHESTACCESS, CREATED, DESTROYED, CHAT, KILLS - } - - public static enum Order { - ASC, DESC - } - - public static enum SummarizationMode { - NONE, PLAYERS, TYPES - } -} +package de.diddiz.LogBlock; + +import de.diddiz.LogBlock.config.Config; +import de.diddiz.LogBlock.util.BukkitUtils; +import de.diddiz.LogBlock.util.CuboidRegion; +import de.diddiz.LogBlock.util.SqlUtil; +import de.diddiz.LogBlock.util.Utils; +import de.diddiz.LogBlock.worldedit.WorldEditHelper; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Tag; +import org.bukkit.World; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; + +import java.util.*; +import java.util.regex.Pattern; + +import static de.diddiz.LogBlock.Session.getSession; +import static de.diddiz.LogBlock.config.Config.*; +import static de.diddiz.LogBlock.util.BukkitUtils.friendlyWorldname; +import static de.diddiz.LogBlock.util.Utils.*; + +public final class QueryParams implements Cloneable { + private static final HashMap keywords = new HashMap<>(); + static { + keywords.put("player", 1); + keywords.put("area", 0); + keywords.put("selection", 0); + keywords.put("sel", 0); + keywords.put("block", 1); + keywords.put("type", 1); + keywords.put("sum", 1); + keywords.put("destroyed", 0); + keywords.put("created", 0); + keywords.put("chestaccess", 0); + keywords.put("all", 0); + keywords.put("time", 0); + keywords.put("since", 0); + keywords.put("before", 0); + keywords.put("limit", 1); + keywords.put("world", 1); + keywords.put("asc", 0); + keywords.put("desc", 0); + keywords.put("last", 0); + keywords.put("coords", 0); + keywords.put("silent", 0); + keywords.put("chat", 0); + keywords.put("search", 1); + keywords.put("match", 1); + keywords.put("loc", 1); + keywords.put("location", 1); + keywords.put("kills", 0); + keywords.put("killer", 1); + keywords.put("victim", 1); + keywords.put("both", 0); + keywords.put("force", 0); + keywords.put("nocache", 0); + keywords.put("entities", 0); + keywords.put("entity", 0); + } + public BlockChangeType bct = BlockChangeType.BOTH; + public int limit = -1, before = 0, since = 0, radius = -1; + public Location loc = null; + public Order order = Order.DESC; + public List players = new ArrayList<>(); + public List killers = new ArrayList<>(); + public List victims = new ArrayList<>(); + public boolean excludePlayersMode = false, excludeKillersMode = false, excludeVictimsMode = false, excludeBlocksEntitiesMode = false, prepareToolQuery = false, silent = false, noForcedLimit = false; + public boolean forceReplace = false, noCache = false; + public CuboidRegion sel = null; + public SummarizationMode sum = SummarizationMode.NONE; + public List types = new ArrayList<>(); + public List> typeTags = new ArrayList<>(); + public List entityTypes = new ArrayList<>(); + public World world = null; + public String match = null; + public boolean needCount = false, needId = false, needDate = false, needType = false, needData = false, needPlayerId = false, needPlayer = false, needCoords = false, needChestAccess = false, needMessage = false, needKiller = false, needVictim = false, needWeapon = false; + private final LogBlock logblock; + + public QueryParams(LogBlock logblock) { + this.logblock = logblock; + } + + public QueryParams(LogBlock logblock, CommandSender sender, List args) throws IllegalArgumentException { + this.logblock = logblock; + parseArgs(sender, args); + } + + public static boolean isKeyWord(String param) { + return keywords.containsKey(param.toLowerCase()); + } + + public static int getKeyWordMinArguments(String param) { + Integer minArgs = keywords.get(param.toLowerCase()); + return minArgs == null ? 0 : minArgs; + } + + public String getLimit() { + if (noForcedLimit || Config.hardLinesLimit <= 0 || (limit > 0 && limit <= Config.hardLinesLimit)) { + return limit > 0 ? "LIMIT " + limit : ""; + } + return "LIMIT " + Config.hardLinesLimit; + } + + public String getFrom() { + if (sum != SummarizationMode.NONE) { + throw new IllegalStateException("Not implemented for summarization"); + } + if (bct == BlockChangeType.CHAT) { + String from = "FROM `lb-chat` "; + + if (needPlayer || players.size() > 0) { + from += "INNER JOIN `lb-players` USING (playerid) "; + } + return from; + } + if (bct == BlockChangeType.KILLS) { + String from = "FROM `" + getTable() + "-kills` "; + + if (needPlayer || needKiller || killers.size() > 0) { + from += "INNER JOIN `lb-players` as killers ON (killer=killers.playerid) "; + } + + if (needPlayer || needVictim || victims.size() > 0) { + from += "INNER JOIN `lb-players` as victims ON (victim=victims.playerid) "; + } + return from; + } + if (bct == BlockChangeType.ENTITIES || bct == BlockChangeType.ENTITIES_CREATED || bct == BlockChangeType.ENTITIES_KILLED) { + String from = "FROM `" + getTable() + "-entities` "; + + if (needPlayer || players.size() > 0) { + from += "INNER JOIN `lb-players` USING (playerid) "; + } + if (!needCount && needData) { + from += "LEFT JOIN `" + getTable() + "-entityids` USING (entityid) "; + } + return from; + } + + String from = "FROM `" + getTable() + "-blocks` "; + + if (needPlayer || players.size() > 0) { + from += "INNER JOIN `lb-players` USING (playerid) "; + } + if (!needCount && needData) { + from += "LEFT JOIN `" + getTable() + "-state` USING (id) "; + } + if (needChestAccess) + // If BlockChangeType is CHESTACCESS, we can use more efficient query + { + if (bct == BlockChangeType.CHESTACCESS) { + from += "RIGHT JOIN `" + getTable() + "-chestdata` USING (id) "; + } else { + from += "LEFT JOIN `" + getTable() + "-chestdata` USING (id) "; + } + } + return from; + } + + public String getOrder() { + if (sum != SummarizationMode.NONE) { + throw new IllegalStateException("Not implemented for summarization"); + } + + // heuristics, for small time spans this might be faster + final int twoDaysInSeconds = 60 * 24 * 2; + if (since > 0 && since <= twoDaysInSeconds) { + return "ORDER BY date " + order + ", id " + order + " "; + } + + return "ORDER BY id " + order + " "; + } + + public String getFields() { + if (sum != SummarizationMode.NONE) { + throw new IllegalStateException("Not implemented for summarization"); + } + if (bct == BlockChangeType.CHAT) { + String select = ""; + if (needCount) { + select += "COUNT(*) AS count "; + } else { + if (needId) { + select += "id, "; + } + if (needDate) { + select += "date, "; + } + if (needPlayer) { + select += "playername, UUID, "; + } + if (needPlayerId) { + select += "playerid, "; + } + if (needMessage) { + select += "message, "; + } + select = select.substring(0, select.length() - 2) + " "; + } + return select; + } + if (bct == BlockChangeType.KILLS) { + String select = ""; + if (needCount) { + select += "COUNT(*) AS count "; + } else { + if (needId) { + select += "id, "; + } + if (needDate) { + select += "date, "; + } + if (needPlayer || needKiller) { + select += "killers.playername as killer, "; + } + if (needPlayer || needVictim) { + select += "victims.playername as victim, "; + } + if (needPlayerId) { + select += "killer as killerid, victim as victimid, "; + } + if (needWeapon) { + select += "weapon, "; + } + if (needCoords) { + select += "x, y, z, "; + } + select = select.substring(0, select.length() - 2) + " "; + } + return select; + } + String select = ""; + if (needCount) { + select += "COUNT(*) AS count "; + } else { + if (needId) { + if (bct != BlockChangeType.ENTITIES && bct != BlockChangeType.ENTITIES_CREATED && bct != BlockChangeType.ENTITIES_KILLED) { + select += "`" + getTable() + "-blocks`.id, "; + } else { + select += "`" + getTable() + "-entities`.id, "; + } + } + if (needDate) { + select += "date, "; + } + if (bct != BlockChangeType.ENTITIES && bct != BlockChangeType.ENTITIES_CREATED && bct != BlockChangeType.ENTITIES_KILLED) { + if (needType) { + select += "replaced, type, "; + } + if (needData) { + select += "replacedData, typeData, "; + } + } + if (needPlayer) { + select += "playername, UUID, "; + } + if (needPlayerId) { + select += "playerid, "; + } + if (needCoords) { + select += "x, y, z, "; + } + if (bct != BlockChangeType.ENTITIES && bct != BlockChangeType.ENTITIES_CREATED && bct != BlockChangeType.ENTITIES_KILLED) { + if (needData) { + select += "replacedState, typeState, "; + } + if (needChestAccess) { + select += "item, itemremove, itemtype, "; + } + } else { + if (needType) { + select += "entitytypeid, action, "; + } + if (needData) { + select += "entityid, entityuuid, data, "; + } + } + select = select.substring(0, select.length() - 2) + " "; + } + return select; + } + + public String getQuery() { + if (sum == SummarizationMode.NONE) { + return "SELECT " + getFields() + getFrom() + getWhere() + getOrder() + getLimit(); + } + if (bct == BlockChangeType.CHAT) { + throw new IllegalStateException("Invalid summarization for chat"); + } + if (bct == BlockChangeType.KILLS) { + if (sum == SummarizationMode.PLAYERS) { + return "SELECT playername, UUID, SUM(kills) AS kills, SUM(killed) AS killed FROM ((SELECT killer AS playerid, count(*) AS kills, 0 as killed FROM `" + getTable() + "-kills` INNER JOIN `lb-players` as killers ON (killer=killers.playerid) INNER JOIN `lb-players` as victims ON (victim=victims.playerid) " + getWhere(BlockChangeType.KILLS) + "GROUP BY killer) UNION (SELECT victim AS playerid, 0 as kills, count(*) AS killed FROM `" + getTable() + + "-kills` INNER JOIN `lb-players` as killers ON (killer=killers.playerid) INNER JOIN `lb-players` as victims ON (victim=victims.playerid) " + getWhere(BlockChangeType.KILLS) + "GROUP BY victim)) AS t INNER JOIN `lb-players` USING (playerid) GROUP BY playerid ORDER BY SUM(kills) + SUM(killed) " + order + " " + getLimit(); + } + throw new IllegalStateException("Invalid summarization for kills"); + } + if (bct == BlockChangeType.ENTITIES || bct == BlockChangeType.ENTITIES_CREATED || bct == BlockChangeType.ENTITIES_KILLED) { + if (sum == SummarizationMode.TYPES) { + return "SELECT entitytypeid, SUM(created) AS created, SUM(destroyed) AS destroyed FROM ((SELECT entitytypeid, count(*) AS created, 0 AS destroyed FROM `" + getTable() + "-entities` INNER JOIN `lb-players` USING (playerid) " + getWhere(BlockChangeType.ENTITIES_CREATED) + "GROUP BY entitytypeid) UNION (SELECT entitytypeid, 0 AS created, count(*) AS destroyed FROM `" + getTable() + "-entities` INNER JOIN `lb-players` USING (playerid) " + getWhere(BlockChangeType.ENTITIES_KILLED) + + "GROUP BY entitytypeid)) AS t GROUP BY entitytypeid ORDER BY SUM(created) + SUM(destroyed) " + order + " " + getLimit(); + } else { + return "SELECT playername, UUID, SUM(created) AS created, SUM(destroyed) AS destroyed FROM ((SELECT playerid, count(*) AS created, 0 AS destroyed FROM `" + getTable() + "-entities` " + getWhere(BlockChangeType.ENTITIES_CREATED) + "GROUP BY playerid) UNION (SELECT playerid, 0 AS created, count(*) AS destroyed FROM `" + getTable() + "-entities` " + getWhere(BlockChangeType.ENTITIES_KILLED) + + "GROUP BY playerid)) AS t INNER JOIN `lb-players` USING (playerid) GROUP BY playerid ORDER BY SUM(created) + SUM(destroyed) " + order + " " + getLimit(); + } + } + if (sum == SummarizationMode.TYPES) { + return "SELECT type, SUM(created) AS created, SUM(destroyed) AS destroyed FROM ((SELECT type, count(*) AS created, 0 AS destroyed FROM `" + getTable() + "-blocks` INNER JOIN `lb-players` USING (playerid) " + getWhere(BlockChangeType.CREATED) + "GROUP BY type) UNION (SELECT replaced AS type, 0 AS created, count(*) AS destroyed FROM `" + getTable() + "-blocks` INNER JOIN `lb-players` USING (playerid) " + getWhere(BlockChangeType.DESTROYED) + + "GROUP BY replaced)) AS t GROUP BY type ORDER BY SUM(created) + SUM(destroyed) " + order + " " + getLimit(); + } else { + return "SELECT playername, UUID, SUM(created) AS created, SUM(destroyed) AS destroyed FROM ((SELECT playerid, count(*) AS created, 0 AS destroyed FROM `" + getTable() + "-blocks` " + getWhere(BlockChangeType.CREATED) + "GROUP BY playerid) UNION (SELECT playerid, 0 AS created, count(*) AS destroyed FROM `" + getTable() + "-blocks` " + getWhere(BlockChangeType.DESTROYED) + + "GROUP BY playerid)) AS t INNER JOIN `lb-players` USING (playerid) GROUP BY playerid ORDER BY SUM(created) + SUM(destroyed) " + order + " " + getLimit(); + } + } + + public String getTable() { + return getWorldConfig(world).table; + } + + public String getTitle() { + final StringBuilder title = new StringBuilder(); + if (bct == BlockChangeType.CHESTACCESS) { + title.append("chest accesses "); + } else if (bct == BlockChangeType.CHAT) { + title.append("chat messages "); + } else if (bct == BlockChangeType.KILLS) { + title.append("kills "); + } else if (bct == BlockChangeType.ENTITIES || bct == BlockChangeType.ENTITIES_CREATED || bct == BlockChangeType.ENTITIES_KILLED) { + if (!entityTypes.isEmpty()) { + if (excludeBlocksEntitiesMode) { + title.append("all entities except "); + } + final String[] entityTypeNames = new String[entityTypes.size()]; + for (int i = 0; i < entityTypes.size(); i++) { + entityTypeNames[i] = entityTypes.get(i).name(); + } + title.append(listing(entityTypeNames, ", ", " and ")).append(" "); + } else { + title.append("entity changes "); + } + } else { + if (!types.isEmpty() || !typeTags.isEmpty()) { + if (excludeBlocksEntitiesMode) { + title.append("all blocks except "); + } + final String[] blocknames = new String[types.size() + typeTags.size()]; + for (int i = 0; i < types.size(); i++) { + blocknames[i] = types.get(i).name(); + } + for (int i = 0; i < typeTags.size(); i++) { + blocknames[i + types.size()] = "#" + typeTags.get(i).getKey().getKey().toUpperCase(); + } + title.append(listing(blocknames, ", ", " and ")).append(" "); + } else { + title.append("block "); + } + if (bct == BlockChangeType.CREATED) { + title.append("creations "); + } else if (bct == BlockChangeType.DESTROYED) { + title.append("destructions "); + } else { + title.append("changes "); + } + } + if (killers.size() > 10) { + title.append(excludeKillersMode ? "without" : "from").append(" many killers "); + } else if (!killers.isEmpty()) { + title.append(excludeKillersMode ? "without" : "from").append(" ").append(listing(killers.toArray(new String[killers.size()]), ", ", " and ")).append(" "); + } + if (victims.size() > 10) { + title.append(excludeVictimsMode ? "without" : "of").append(" many victims "); + } else if (!victims.isEmpty()) { + title.append(excludeVictimsMode ? "without" : "of").append(" victim").append(victims.size() != 1 ? "s" : "").append(" ").append(listing(victims.toArray(new String[victims.size()]), ", ", " and ")).append(" "); + } + if (players.size() > 10) { + title.append(excludePlayersMode ? "without" : "from").append(" many players "); + } else if (!players.isEmpty()) { + title.append(excludePlayersMode ? "without" : "from").append(" player").append(players.size() != 1 ? "s" : "").append(" ").append(listing(players.toArray(new String[players.size()]), ", ", " and ")).append(" "); + } + if (match != null && match.length() > 0) { + title.append("matching '").append(match).append("' "); + } + if (before > 0 && since > 0) { + title.append("between ").append(since).append(" and ").append(before).append(" minutes ago "); + } else if (since > 0) { + title.append("in the last ").append(since).append(" minutes "); + } else if (before > 0) { + title.append("more than ").append(before).append(" minutes ago "); + } + if (loc != null) { + if (radius > 0) { + title.append("within ").append(radius).append(" blocks of ").append(prepareToolQuery ? "clicked block" : "location").append(" "); + } else if (radius == 0) { + title.append("at ").append(loc.getBlockX()).append(":").append(loc.getBlockY()).append(":").append(loc.getBlockZ()).append(" "); + } + } else if (sel != null) { + title.append(prepareToolQuery ? "at double chest " : "inside selection "); + } else if (prepareToolQuery) { + if (radius > 0) { + title.append("within ").append(radius).append(" blocks of clicked block "); + } else if (radius == 0) { + title.append("at clicked block "); + } + } + if (world != null && !(sel != null && prepareToolQuery)) { + title.append("in ").append(friendlyWorldname(world.getName())).append(" "); + } + if (sum != SummarizationMode.NONE) { + title.append("summed up by ").append(sum == SummarizationMode.TYPES ? ((bct == BlockChangeType.ENTITIES || bct == BlockChangeType.ENTITIES_CREATED || bct == BlockChangeType.ENTITIES_KILLED) ? "entities" : "blocks") : "players").append(" "); + } + title.deleteCharAt(title.length() - 1); + title.setCharAt(0, String.valueOf(title.charAt(0)).toUpperCase().toCharArray()[0]); + return title.toString(); + } + + public String getWhere() { + return getWhere(bct); + } + + public String getWhere(BlockChangeType blockChangeType) { + final StringBuilder where = new StringBuilder("WHERE "); + if (blockChangeType == BlockChangeType.CHAT) { + if (match != null && match.length() > 0) { + final boolean unlike = match.startsWith("-"); + if (match.length() > 3 && !unlike || match.length() > 4) { + where.append("MATCH (message) AGAINST ('").append(SqlUtil.escapeString(match)).append("' IN BOOLEAN MODE) AND "); + } else { + where.append("message ").append(unlike ? "NOT " : "").append("LIKE '%").append(SqlUtil.escapeString(unlike ? match.substring(1) : match, true)).append("%' AND "); + } + } + } else if (blockChangeType == BlockChangeType.KILLS) { + if (!players.isEmpty()) { + if (!excludePlayersMode) { + where.append('('); + for (final String killerName : players) { + where.append("killers.playername = '").append(SqlUtil.escapeString(killerName)).append("' OR "); + } + for (final String victimName : players) { + where.append("victims.playername = '").append(SqlUtil.escapeString(victimName)).append("' OR "); + } + where.delete(where.length() - 4, where.length()); + where.append(") AND "); + } else { + for (final String killerName : players) { + where.append("killers.playername != '").append(SqlUtil.escapeString(killerName)).append("' AND "); + } + for (final String victimName : players) { + where.append("victims.playername != '").append(SqlUtil.escapeString(victimName)).append("' AND "); + } + } + } + + if (!killers.isEmpty()) { + if (!excludeKillersMode) { + where.append('('); + for (final String killerName : killers) { + where.append("killers.playername = '").append(SqlUtil.escapeString(killerName)).append("' OR "); + } + where.delete(where.length() - 4, where.length()); + where.append(") AND "); + } else { + for (final String killerName : killers) { + where.append("killers.playername != '").append(SqlUtil.escapeString(killerName)).append("' AND "); + } + } + } + + if (!victims.isEmpty()) { + if (!excludeVictimsMode) { + where.append('('); + for (final String victimName : victims) { + where.append("victims.playername = '").append(victimName).append("' OR "); + } + where.delete(where.length() - 4, where.length()); + where.append(") AND "); + } else { + for (final String victimName : victims) { + where.append("victims.playername != '").append(victimName).append("' AND "); + } + } + } + } else if (blockChangeType == BlockChangeType.ENTITIES || blockChangeType == BlockChangeType.ENTITIES_CREATED || blockChangeType == BlockChangeType.ENTITIES_KILLED) { + Set entityTypeIds = getEntityTypeIds(); + if (!entityTypeIds.isEmpty()) { + if (excludeBlocksEntitiesMode) { + where.append("NOT "); + } + where.append('('); + for (final Integer entityType : entityTypeIds) { + where.append("(entitytypeid = ").append(entityType); + where.append(") OR "); + } + where.delete(where.length() - 4, where.length() - 1); + where.append(") AND "); + } + if (blockChangeType == BlockChangeType.ENTITIES_CREATED) { + where.append("action = " + EntityChange.EntityChangeType.CREATE.ordinal() + " AND "); + } else if (blockChangeType == BlockChangeType.ENTITIES_KILLED) { + where.append("action = " + EntityChange.EntityChangeType.KILL.ordinal() + " AND "); + } + } else { + Set typeIds = getTypeIds(); + switch (blockChangeType) { + case ALL: + if (!typeIds.isEmpty()) { + if (excludeBlocksEntitiesMode) { + where.append("NOT "); + } + where.append('('); + for (final Integer block : typeIds) { + where.append("((type = ").append(block).append(" OR replaced = ").append(block); + where.append(")"); + where.append(") OR "); + } + where.delete(where.length() - 4, where.length() - 1); + where.append(") AND "); + } + break; + case BOTH: + if (!typeIds.isEmpty()) { + if (excludeBlocksEntitiesMode) { + where.append("NOT "); + } + where.append('('); + for (final Integer block : typeIds) { + where.append("((type = ").append(block).append(" OR replaced = ").append(block); + where.append(")"); + where.append(") OR "); + } + where.delete(where.length() - 4, where.length()); + where.append(") AND "); + } + where.append("type != replaced AND "); + break; + case CREATED: + if (!typeIds.isEmpty()) { + if (excludeBlocksEntitiesMode) { + where.append("NOT "); + } + where.append('('); + for (final Integer block : typeIds) { + where.append("((type = ").append(block); + where.append(")"); + where.append(") OR "); + } + where.delete(where.length() - 4, where.length()); + where.append(") AND "); + } + where.append("type != 0 AND type != replaced AND "); + break; + case DESTROYED: + if (!typeIds.isEmpty()) { + if (excludeBlocksEntitiesMode) { + where.append("NOT "); + } + where.append('('); + for (final Integer block : typeIds) { + where.append("((replaced = ").append(block); + where.append(")"); + where.append(") OR "); + } + where.delete(where.length() - 4, where.length()); + where.append(") AND "); + } + where.append("replaced != 0 AND type != replaced AND "); + break; + case CHESTACCESS: + if (!typeIds.isEmpty()) { + if (excludeBlocksEntitiesMode) { + where.append("NOT "); + } + where.append('('); + for (final Integer block : typeIds) { + where.append("((itemtype = ").append(block); + where.append(")"); + where.append(") OR "); + } + where.delete(where.length() - 4, where.length()); + where.append(") AND "); + } + break; + default: + break; + } + } + if (blockChangeType != BlockChangeType.CHAT) { + if (loc != null) { + if (radius == 0) { + compileLocationQuery( + where, + loc.getBlockX(), loc.getBlockX(), + loc.getBlockY(), loc.getBlockY(), + loc.getBlockZ(), loc.getBlockZ()); + } else if (radius > 0) { + compileLocationQuery( + where, + loc.getBlockX() - radius + 1, loc.getBlockX() + radius - 1, + loc.getBlockY() - radius + 1, loc.getBlockY() + radius - 1, + loc.getBlockZ() - radius + 1, loc.getBlockZ() + radius - 1); + } + + } else if (sel != null) { + compileLocationQuery( + where, + sel.getMinimumPoint().getBlockX(), sel.getMaximumPoint().getBlockX(), + sel.getMinimumPoint().getBlockY(), sel.getMaximumPoint().getBlockY(), + sel.getMinimumPoint().getBlockZ(), sel.getMaximumPoint().getBlockZ()); + } + } + if (!players.isEmpty() && sum != SummarizationMode.PLAYERS && blockChangeType != BlockChangeType.KILLS) { + if (!excludePlayersMode) { + where.append('('); + for (final String playerName : players) { + where.append("playername = '").append(SqlUtil.escapeString(playerName)).append("' OR "); + } + where.delete(where.length() - 4, where.length()); + where.append(") AND "); + } else { + for (final String playerName : players) { + where.append("playername != '").append(SqlUtil.escapeString(playerName)).append("' AND "); + } + } + } + if (since > 0) { + where.append("date > date_sub(now(), INTERVAL ").append(since).append(" MINUTE) AND "); + } + if (before > 0) { + where.append("date < date_sub(now(), INTERVAL ").append(before).append(" MINUTE) AND "); + } + if (where.length() > 6) { + where.delete(where.length() - 4, where.length()); + } else { + where.delete(0, where.length()); + } + return where.toString(); + } + + private void compileLocationQuery(StringBuilder where, int blockX, int blockX2, int blockY, int blockY2, int blockZ, int blockZ2) { + compileLocationQueryPart(where, "x", blockX, blockX2); + where.append(" AND "); + compileLocationQueryPart(where, "y", blockY, blockY2); + where.append(" AND "); + compileLocationQueryPart(where, "z", blockZ, blockZ2); + where.append(" AND "); + } + + private void compileLocationQueryPart(StringBuilder where, String locValue, int loc, int loc2) { + int min = Math.min(loc, loc2); + int max = Math.max(loc2, loc); + + if (min == max) { + where.append(locValue).append(" = ").append(min); + } else if (max - min > 50) { + where.append(locValue).append(" >= ").append(min).append(" AND ").append(locValue).append(" <= ").append(max); + } else { + where.append(locValue).append(" in ("); + for (int c = min; c < max; c++) { + where.append(c).append(","); + } + where.append(max); + where.append(")"); + } + } + + public void parseArgs(CommandSender sender, List args) throws IllegalArgumentException { + parseArgs(sender, args, true); + } + + public void parseArgs(CommandSender sender, List args, boolean validate) throws IllegalArgumentException { + if (args == null || args.isEmpty()) { + throw new IllegalArgumentException("No parameters specified."); + } + args = Utils.parseQuotes(args); + final Player player = sender instanceof Player ? (Player) sender : null; + final Session session = prepareToolQuery ? null : getSession(sender); + if (player != null && world == null) { + world = player.getWorld(); + } + for (int i = 0; i < args.size(); i++) { + final String param = args.get(i).toLowerCase(); + final String[] values = getValues(args, i + 1, getKeyWordMinArguments(param)); + if (param.equals("last")) { + if (session == null || session.lastQuery == null) { + throw new IllegalArgumentException("This is your first command, you can't use last."); + } + merge(session.lastQuery); + } else if (param.equals("player")) { + if (values.length < 1) { + throw new IllegalArgumentException("No or wrong count of arguments for '" + param + "'"); + } + for (String playerName : values) { + if (playerName.length() > 0) { + if (playerName.startsWith("!")) { + playerName = playerName.substring(1); + excludePlayersMode = true; + if (playerName.isEmpty()) { + continue; + } + } + if (playerName.contains("\"")) { + players.add(playerName.replace("\"", "")); + } else { + final Player matches = logblock.getServer().getPlayerExact(playerName); + players.add(matches != null ? matches.getName() : playerName.replace("\\\"", "")); + } + } + } + needPlayer = true; + } else if (param.equals("killer")) { + if (values.length < 1) { + throw new IllegalArgumentException("No or wrong count of arguments for '" + param + "'"); + } + for (final String killerName : values) { + if (killerName.length() > 0) { + if (killerName.contains("!")) { + excludeVictimsMode = true; + } + if (killerName.contains("\"")) { + killers.add(killerName.replaceAll("[^a-zA-Z0-9_]", "")); + } else { + final List matches = logblock.getServer().matchPlayer(killerName); + if (matches.size() > 1) { + throw new IllegalArgumentException("Ambiguous victimname '" + param + "'"); + } + killers.add(matches.size() == 1 ? matches.get(0).getName() : killerName.replaceAll("[^a-zA-Z0-9_]", "")); + } + } + } + needKiller = true; + } else if (param.equals("victim")) { + if (values.length < 1) { + throw new IllegalArgumentException("No or wrong count of arguments for '" + param + "'"); + } + for (final String victimName : values) { + if (victimName.length() > 0) { + if (victimName.contains("!")) { + excludeVictimsMode = true; + } + if (victimName.contains("\"")) { + victims.add(victimName.replaceAll("[^a-zA-Z0-9_]", "")); + } else { + final List matches = logblock.getServer().matchPlayer(victimName); + if (matches.size() > 1) { + throw new IllegalArgumentException("Ambiguous victimname '" + param + "'"); + } + victims.add(matches.size() == 1 ? matches.get(0).getName() : victimName.replaceAll("[^a-zA-Z0-9_]", "")); + } + } + } + needVictim = true; + } else if (param.equals("weapon")) { + if (values.length < 1) { + throw new IllegalArgumentException("No or wrong count of arguments for '" + param + "'"); + } + for (final String weaponName : values) { + Material mat = weaponName.equalsIgnoreCase("fist") ? Material.AIR : Material.matchMaterial(weaponName); + if (mat == null) { + throw new IllegalArgumentException("No material matching: '" + weaponName + "'"); + } + types.add(mat); + } + needWeapon = true; + } else if (param.equals("block") || param.equals("type")) { + if (values.length < 1) { + throw new IllegalArgumentException("No or wrong count of arguments for '" + param + "'"); + } + for (String blockName : values) { + if (blockName.startsWith("!")) { + excludeBlocksEntitiesMode = true; + blockName = blockName.substring(1); + } + if (blockName.startsWith("#")) { + String tagName = blockName.substring(1).toLowerCase(); + Tag tag = logblock.getServer().getTag(Tag.REGISTRY_BLOCKS, NamespacedKey.minecraft(tagName), Material.class); + if (tag == null || tag.getValues().isEmpty()) { + tag = logblock.getServer().getTag(Tag.REGISTRY_ITEMS, NamespacedKey.minecraft(tagName), Material.class); + if (tag == null || tag.getValues().isEmpty()) { + throw new IllegalArgumentException("No block tag matching: '" + blockName + "'"); + } + } + typeTags.add(tag); + } else if (blockName.contains("*")) { + StringBuilder sb = new StringBuilder(); + for (int ci = 0; ci < blockName.length(); ci++) { + char c = blockName.charAt(ci); + if (!Character.isAlphabetic(c)) { + if (c == '*') { + sb.append('.'); + } else { + sb.append('\\'); + } + } + sb.append(c); + } + String blockNamePattern = sb.toString(); + Pattern pattern = Pattern.compile(blockNamePattern, Pattern.CASE_INSENSITIVE); + ArrayList matched = new ArrayList<>(); + for (Material mat : Material.values()) { + if (pattern.matcher(mat.name()).matches()) { + matched.add(mat); + } + } + if (matched.isEmpty()) { + throw new IllegalArgumentException("No material matching: '" + blockName + "'"); + } + for (Material mat : matched) { + types.add(mat); + } + } else { + final Material mat = Material.matchMaterial(blockName); + if (mat == null) { + throw new IllegalArgumentException("No material matching: '" + blockName + "'"); + } + types.add(mat); + } + } + } else if (param.equals("area")) { + if (player == null && !prepareToolQuery && loc == null) { + throw new IllegalArgumentException("You have to be a player to use area, or specify a location first"); + } + if (values.length == 0) { + radius = defaultDist; + if (!prepareToolQuery && loc == null) { + loc = player.getLocation(); + } + } else { + if (!isInt(values[0])) { + throw new IllegalArgumentException("Not a number: '" + values[0] + "'"); + } + radius = Integer.parseInt(values[0]); + if (!prepareToolQuery && loc == null) { + loc = player.getLocation(); + } + } + } else if (param.equals("selection") || param.equals("sel")) { + if (player == null) { + throw new IllegalArgumentException("You have to be a player to use selection"); + } + setSelection(WorldEditHelper.getSelectedRegion(player)); + } else if (param.equals("time") || param.equals("since")) { + since = values.length > 0 ? parseTimeSpec(values) : defaultTime; + if (since == -1) { + throw new IllegalArgumentException("Failed to parse time spec for '" + param + "'"); + } + } else if (param.equals("before")) { + before = values.length > 0 ? parseTimeSpec(values) : defaultTime; + if (before == -1) { + throw new IllegalArgumentException("Faile to parse time spec for '" + param + "'"); + } + } else if (param.equals("sum")) { + if (values.length != 1) { + throw new IllegalArgumentException("No or wrong count of arguments for '" + param + "'"); + } + if (values[0].startsWith("p")) { + sum = SummarizationMode.PLAYERS; + } else if (values[0].startsWith("b") || values[0].startsWith("e")) { + if (values[0].startsWith("e")) { + bct = BlockChangeType.ENTITIES; + } + sum = SummarizationMode.TYPES; + } else if (values[0].startsWith("n")) { + sum = SummarizationMode.NONE; + } else { + throw new IllegalArgumentException("Wrong summarization mode"); + } + } else if (param.equals("created")) { + bct = BlockChangeType.CREATED; + } else if (param.equals("destroyed")) { + bct = BlockChangeType.DESTROYED; + } else if (param.equals("both")) { + bct = BlockChangeType.BOTH; + } else if (param.equals("chestaccess")) { + bct = BlockChangeType.CHESTACCESS; + } else if (param.equals("chat")) { + bct = BlockChangeType.CHAT; + } else if (param.equals("kills")) { + bct = BlockChangeType.KILLS; + } else if (param.equals("entities") || param.equals("entity")) { + bct = BlockChangeType.ENTITIES; + if (values.length > 0) { + for (String entityTypeName : values) { + if (entityTypeName.startsWith("!")) { + excludeBlocksEntitiesMode = true; + entityTypeName = entityTypeName.substring(1); + } + + final EntityType entityType = BukkitUtils.matchEntityType(entityTypeName); + if (entityType == null) { + throw new IllegalArgumentException("No entity type matching: '" + entityTypeName + "'"); + } + entityTypes.add(entityType); + } + } + } else if (param.equals("all")) { + bct = BlockChangeType.ALL; + } else if (param.equals("limit")) { + if (values.length != 1) { + throw new IllegalArgumentException("Wrong count of arguments for '" + param + "'"); + } + if (!isInt(values[0])) { + throw new IllegalArgumentException("Not a number: '" + values[0] + "'"); + } + limit = Integer.parseInt(values[0]); + } else if (param.equals("world")) { + if (values.length != 1) { + throw new IllegalArgumentException("Wrong count of arguments for '" + param + "'"); + } + final World w = sender.getServer().getWorld(values[0].replace("\"", "")); + if (w == null) { + throw new IllegalArgumentException("There is no world called '" + values[0] + "'"); + } + world = w; + } else if (param.equals("asc")) { + order = Order.ASC; + } else if (param.equals("desc")) { + order = Order.DESC; + } else if (param.equals("coords")) { + needCoords = true; + } else if (param.equals("silent")) { + silent = true; + } else if (param.equals("force")) { + forceReplace = true; + } else if (param.equals("nocache")) { + noCache = true; + } else if (param.equals("search") || param.equals("match")) { + if (values.length == 0) { + throw new IllegalArgumentException("No arguments for '" + param + "'"); + } + match = join(values, " "); + } else if (param.equals("loc") || param.equals("location")) { + final String[] vectors = values.length == 1 ? values[0].split(":") : values; + if (vectors.length != 3) { + throw new IllegalArgumentException("Wrong count arguments for '" + param + "'"); + } + for (final String vec : vectors) { + if (!isInt(vec)) { + throw new IllegalArgumentException("Not a number: '" + vec + "'"); + } + } + loc = new Location(null, Integer.valueOf(vectors[0]), Integer.valueOf(vectors[1]), Integer.valueOf(vectors[2])); + radius = 0; + } else { + throw new IllegalArgumentException("Not a valid argument: '" + param + "'"); + } + i += values.length; + } + if (validate) { + validate(); + } + if (session != null && !noCache) { + session.lastQuery = clone(); + } + } + + public void validate() { + if (bct == BlockChangeType.KILLS) { + if (world == null) { + throw new IllegalArgumentException("No world specified"); + } + if (!getWorldConfig(world).isLogging(Logging.KILL)) { + throw new IllegalArgumentException("Kill logging not enabled for world '" + world.getName() + "'"); + } + if (sum != SummarizationMode.NONE && sum != SummarizationMode.PLAYERS) { + throw new IllegalArgumentException("Invalid summarization for kills"); + } + } + if (!prepareToolQuery && bct != BlockChangeType.CHAT) { + if (world == null) { + throw new IllegalArgumentException("No world specified"); + } + if (!isLogged(world)) { + throw new IllegalArgumentException("This world ('" + world.getName() + "') isn't logged"); + } + } + if (bct == BlockChangeType.CHAT) { + if (!(isLogging(Logging.CHAT) || isLogging(Logging.PLAYER_COMMANDS) || isLogging(Logging.CONSOLE_COMMANDS) || isLogging(Logging.COMMANDBLOCK_COMMANDS))) { + throw new IllegalArgumentException("Chat is not logged"); + } + if (sum != SummarizationMode.NONE) { + throw new IllegalArgumentException("Invalid summarization for chat"); + } + } + } + + public void setLocation(Location loc) { + this.loc = loc; + this.sel = null; + world = loc.getWorld(); + } + + public void setSelection(CuboidRegion container) { + this.sel = container; + this.loc = null; + world = sel.getWorld(); + } + + public void setPlayer(String playerName) { + players.clear(); + players.add(playerName); + } + + @Override + public QueryParams clone() { + try { + final QueryParams params = (QueryParams) super.clone(); + params.players = new ArrayList<>(players); + params.killers = new ArrayList<>(killers); + params.victims = new ArrayList<>(victims); + params.types = new ArrayList<>(types); + params.typeTags = new ArrayList<>(typeTags); + params.entityTypes = new ArrayList<>(entityTypes); + params.loc = loc == null ? null : loc.clone(); + params.sel = sel == null ? null : sel.clone(); + return params; + } catch (final CloneNotSupportedException ex) { + throw new Error("QueryParams should be cloneable", ex); + } + } + + private Set getTypeIds() { + HashSet typeIds = new HashSet<>(); + for (Material type : types) { + if (type != null) { + Integer id = MaterialConverter.getExistingMaterialId(type); + if (id != null) { + typeIds.add(id); + } + } + } + for (Tag tag : typeTags) { + if (tag != null) { + for (Material type : tag.getValues()) { + Integer id = MaterialConverter.getExistingMaterialId(type); + if (id != null) { + typeIds.add(id); + } + } + } + } + // add invalid id, so the type list is not ignored + if ((!types.isEmpty() || !typeTags.isEmpty()) && typeIds.isEmpty()) { + typeIds.add(-1); + } + return typeIds; + } + + private Set getEntityTypeIds() { + HashSet typeIds = new HashSet<>(); + for (EntityType type : entityTypes) { + Integer id = EntityTypeConverter.getExistingEntityTypeId(type); + if (id != null) { + typeIds.add(id); + } + } + // add invalid id, so the type list is not ignored + if (!entityTypes.isEmpty() && typeIds.isEmpty()) { + typeIds.add(-1); + } + return typeIds; + } + + private static String[] getValues(List args, int offset, int minParams) { + // The variable i will store the last value's index + int i; + // Iterate over the all the values from the offset up till the end + for (i = offset; i < args.size(); i++) { + // We found a keyword, break here since anything after this isn't a value. + if (i >= offset + minParams && isKeyWord(args.get(i))) { + break; + } + } + // If there are no values, i.e there is a keyword immediately after the offset + // return an empty string array + if (i == offset) { + return new String[0]; + } + + final String[] values = new String[i - offset]; + for (int j = offset; j < i; j++) { + String value = args.get(j); + + // If the value is encapsulated in quotes, strip them + if (value.startsWith("\"") && value.endsWith("\"")) { + value = value.substring(1, value.length() - 1); + } + values[j - offset] = value; + } + return values; + } + + public void merge(QueryParams p) { + players.addAll(p.players); + killers.addAll(p.killers); + victims.addAll(p.victims); + excludePlayersMode = p.excludePlayersMode; + types.addAll(p.types); + typeTags.addAll(p.typeTags); + entityTypes.addAll(p.entityTypes); + loc = p.loc == null ? null : p.loc.clone(); + radius = p.radius; + sel = p.sel == null ? null : p.sel.clone(); + if (p.since != 0 || since != defaultTime) { + since = p.since; + } + before = p.before; + sum = p.sum; + bct = p.bct; + limit = p.limit; + world = p.world; + order = p.order; + match = p.match; + } + + public static enum BlockChangeType { + ALL, + BOTH, + CHESTACCESS, + CREATED, + DESTROYED, + CHAT, + KILLS, + ENTITIES, + ENTITIES_CREATED, + ENTITIES_KILLED, + } + + public static enum Order { + ASC, + DESC + } + + public static enum SummarizationMode { + NONE, + PLAYERS, + TYPES + } +} diff --git a/src/main/java/de/diddiz/LogBlock/Session.java b/src/main/java/de/diddiz/LogBlock/Session.java index 3e533557..b145d4e9 100644 --- a/src/main/java/de/diddiz/LogBlock/Session.java +++ b/src/main/java/de/diddiz/LogBlock/Session.java @@ -1,49 +1,49 @@ -package de.diddiz.LogBlock; - -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; - -import java.util.HashMap; -import java.util.Map; - -import static de.diddiz.LogBlock.config.Config.toolsByType; -import static org.bukkit.Bukkit.getServer; - -public class Session { - private static final Map sessions = new HashMap(); - public QueryParams lastQuery = null; - public LookupCacheElement[] lookupCache = null; - public int page = 1; - public Map toolData; - - private Session(Player player) { - toolData = new HashMap(); - final LogBlock logblock = LogBlock.getInstance(); - if (player != null) { - for (final Tool tool : toolsByType.values()) { - toolData.put(tool, new ToolData(tool, logblock, player)); - } - } - } - - public static boolean hasSession(CommandSender sender) { - return sessions.containsKey(sender.getName().toLowerCase()); - } - - public static boolean hasSession(String playerName) { - return sessions.containsKey(playerName.toLowerCase()); - } - - public static Session getSession(CommandSender sender) { - return getSession(sender.getName()); - } - - public static Session getSession(String playerName) { - Session session = sessions.get(playerName.toLowerCase()); - if (session == null) { - session = new Session(getServer().getPlayer(playerName)); - sessions.put(playerName.toLowerCase(), session); - } - return session; - } -} +package de.diddiz.LogBlock; + +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.HashMap; +import java.util.Map; + +import static de.diddiz.LogBlock.config.Config.toolsByType; +import static org.bukkit.Bukkit.getServer; + +public class Session { + private static final Map sessions = new HashMap<>(); + public QueryParams lastQuery = null; + public LookupCacheElement[] lookupCache = null; + public int page = 1; + public Map toolData; + + private Session(Player player) { + toolData = new HashMap<>(); + final LogBlock logblock = LogBlock.getInstance(); + if (player != null) { + for (final Tool tool : toolsByType.values()) { + toolData.put(tool, new ToolData(tool, logblock, player)); + } + } + } + + public static boolean hasSession(CommandSender sender) { + return sessions.containsKey(sender.getName().toLowerCase()); + } + + public static boolean hasSession(String playerName) { + return sessions.containsKey(playerName.toLowerCase()); + } + + public static Session getSession(CommandSender sender) { + return getSession(sender.getName()); + } + + public static Session getSession(String playerName) { + Session session = sessions.get(playerName.toLowerCase()); + if (session == null) { + session = new Session(getServer().getPlayer(playerName)); + sessions.put(playerName.toLowerCase(), session); + } + return session; + } +} diff --git a/src/main/java/de/diddiz/LogBlock/SummedBlockChanges.java b/src/main/java/de/diddiz/LogBlock/SummedBlockChanges.java index f6dfa0db..08dfe8dc 100644 --- a/src/main/java/de/diddiz/LogBlock/SummedBlockChanges.java +++ b/src/main/java/de/diddiz/LogBlock/SummedBlockChanges.java @@ -1,36 +1,43 @@ -package de.diddiz.LogBlock; - -import de.diddiz.LogBlock.QueryParams.SummarizationMode; -import org.bukkit.Location; - -import java.sql.ResultSet; -import java.sql.SQLException; - -import static de.diddiz.util.MaterialName.materialName; -import static de.diddiz.util.Utils.spaces; - -public class SummedBlockChanges implements LookupCacheElement { - private final String group; - private final int created, destroyed; - private final float spaceFactor; - private final Actor actor; - - public SummedBlockChanges(ResultSet rs, QueryParams p, float spaceFactor) throws SQLException { - // Actor currently useless here as we don't yet output UUID in results anywhere - actor = p.sum == SummarizationMode.PLAYERS ? new Actor(rs) : null; - group = actor == null ? materialName(rs.getInt("type")) : actor.getName(); - created = rs.getInt("created"); - destroyed = rs.getInt("destroyed"); - this.spaceFactor = spaceFactor; - } - - @Override - public Location getLocation() { - return null; - } - - @Override - public String getMessage() { - return created + spaces((int) ((10 - String.valueOf(created).length()) / spaceFactor)) + destroyed + spaces((int) ((10 - String.valueOf(destroyed).length()) / spaceFactor)) + group; - } -} +package de.diddiz.LogBlock; + +import static de.diddiz.LogBlock.util.MessagingUtil.prettyMaterial; + +import de.diddiz.LogBlock.QueryParams.SummarizationMode; +import de.diddiz.LogBlock.util.MessagingUtil; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Objects; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.TextComponent; +import org.bukkit.Location; + +public class SummedBlockChanges implements LookupCacheElement { + private final int type; + private final int created, destroyed; + private final float spaceFactor; + private final Actor actor; + + public SummedBlockChanges(ResultSet rs, QueryParams p, float spaceFactor) throws SQLException { + // Actor currently useless here as we don't yet output UUID in results anywhere + actor = p.sum == SummarizationMode.PLAYERS ? new Actor(rs) : null; + type = p.sum == SummarizationMode.TYPES ? rs.getInt("type") : 0; + created = rs.getInt("created"); + destroyed = rs.getInt("destroyed"); + this.spaceFactor = spaceFactor; + } + + @Override + public Location getLocation() { + return null; + } + + @Override + public BaseComponent getLogMessage(int entry) { + return MessagingUtil.formatSummarizedChanges(created, destroyed, actor != null ? new TextComponent(actor.getName()) : prettyMaterial(Objects.toString(MaterialConverter.getMaterial(type))), 10, 10, spaceFactor); + } + + @Override + public int getNumChanges() { + return created + destroyed; + } +} diff --git a/src/main/java/de/diddiz/LogBlock/SummedEntityChanges.java b/src/main/java/de/diddiz/LogBlock/SummedEntityChanges.java new file mode 100644 index 00000000..015978ca --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/SummedEntityChanges.java @@ -0,0 +1,44 @@ +package de.diddiz.LogBlock; + +import static de.diddiz.LogBlock.util.MessagingUtil.prettyMaterial; + +import de.diddiz.LogBlock.QueryParams.SummarizationMode; +import de.diddiz.LogBlock.util.MessagingUtil; +import org.bukkit.Location; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Objects; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.TextComponent; + +public class SummedEntityChanges implements LookupCacheElement { + private final int type; + private final int created, destroyed; + private final float spaceFactor; + private final Actor actor; + + public SummedEntityChanges(ResultSet rs, QueryParams p, float spaceFactor) throws SQLException { + // Actor currently useless here as we don't yet output UUID in results anywhere + actor = p.sum == SummarizationMode.PLAYERS ? new Actor(rs) : null; + type = p.sum == SummarizationMode.TYPES ? rs.getInt("entitytypeid") : 0; + created = rs.getInt("created"); + destroyed = rs.getInt("destroyed"); + this.spaceFactor = spaceFactor; + } + + @Override + public Location getLocation() { + return null; + } + + @Override + public BaseComponent getLogMessage(int entry) { + return MessagingUtil.formatSummarizedChanges(created, destroyed, actor != null ? new TextComponent(actor.getName()) : prettyMaterial(Objects.toString(EntityTypeConverter.getEntityType(type))), 10, 10, spaceFactor); + } + + @Override + public int getNumChanges() { + return created + destroyed; + } +} diff --git a/src/main/java/de/diddiz/LogBlock/SummedKills.java b/src/main/java/de/diddiz/LogBlock/SummedKills.java index 3db77814..2e366e04 100644 --- a/src/main/java/de/diddiz/LogBlock/SummedKills.java +++ b/src/main/java/de/diddiz/LogBlock/SummedKills.java @@ -1,11 +1,11 @@ package de.diddiz.LogBlock; -import org.bukkit.Location; - +import de.diddiz.LogBlock.util.MessagingUtil; import java.sql.ResultSet; import java.sql.SQLException; - -import static de.diddiz.util.Utils.spaces; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.TextComponent; +import org.bukkit.Location; public class SummedKills implements LookupCacheElement { private final Actor player; @@ -25,7 +25,12 @@ public Location getLocation() { } @Override - public String getMessage() { - return kills + spaces((int) ((6 - String.valueOf(kills).length()) / spaceFactor)) + killed + spaces((int) ((7 - String.valueOf(killed).length()) / spaceFactor)) + player.getName(); + public BaseComponent getLogMessage(int entry) { + return MessagingUtil.formatSummarizedChanges(kills, killed, new TextComponent(player.getName()), 6, 7, spaceFactor); + } + + @Override + public int getNumChanges() { + return kills + killed; } } diff --git a/src/main/java/de/diddiz/LogBlock/Tool.java b/src/main/java/de/diddiz/LogBlock/Tool.java index e84c9824..51234091 100644 --- a/src/main/java/de/diddiz/LogBlock/Tool.java +++ b/src/main/java/de/diddiz/LogBlock/Tool.java @@ -1,30 +1,35 @@ -package de.diddiz.LogBlock; - -import org.bukkit.permissions.PermissionDefault; - -import java.util.List; - -public class Tool { - public final String name; - public final List aliases; - public final ToolBehavior leftClickBehavior, rightClickBehavior; - public final boolean defaultEnabled; - public final int item; - public final boolean canDrop; - public final QueryParams params; - public final ToolMode mode; - public final PermissionDefault permissionDefault; - - public Tool(String name, List aliases, ToolBehavior leftClickBehavior, ToolBehavior rightClickBehavior, boolean defaultEnabled, int item, boolean canDrop, QueryParams params, ToolMode mode, PermissionDefault permissionDefault) { - this.name = name; - this.aliases = aliases; - this.leftClickBehavior = leftClickBehavior; - this.rightClickBehavior = rightClickBehavior; - this.defaultEnabled = defaultEnabled; - this.item = item; - this.canDrop = canDrop; - this.params = params; - this.mode = mode; - this.permissionDefault = permissionDefault; - } -} +package de.diddiz.LogBlock; + +import org.bukkit.Material; +import org.bukkit.permissions.PermissionDefault; + +import java.util.List; + +public class Tool { + public final String name; + public final List aliases; + public final ToolBehavior leftClickBehavior, rightClickBehavior; + public final boolean defaultEnabled; + public final Material item; + public final boolean canDrop; + public final QueryParams params; + public final ToolMode mode; + public final PermissionDefault permissionDefault; + public final boolean removeOnDisable; + public final boolean dropToDisable; + + public Tool(String name, List aliases, ToolBehavior leftClickBehavior, ToolBehavior rightClickBehavior, boolean defaultEnabled, Material item, boolean canDrop, QueryParams params, ToolMode mode, PermissionDefault permissionDefault, boolean removeOnDisable, boolean dropToDisable) { + this.name = name; + this.aliases = aliases; + this.leftClickBehavior = leftClickBehavior; + this.rightClickBehavior = rightClickBehavior; + this.defaultEnabled = defaultEnabled; + this.item = item; + this.canDrop = canDrop; + this.params = params; + this.mode = mode; + this.permissionDefault = permissionDefault; + this.removeOnDisable = removeOnDisable; + this.dropToDisable = dropToDisable; + } +} diff --git a/src/main/java/de/diddiz/LogBlock/ToolBehavior.java b/src/main/java/de/diddiz/LogBlock/ToolBehavior.java index e51e7b51..5c75895d 100644 --- a/src/main/java/de/diddiz/LogBlock/ToolBehavior.java +++ b/src/main/java/de/diddiz/LogBlock/ToolBehavior.java @@ -1,5 +1,7 @@ -package de.diddiz.LogBlock; - -public enum ToolBehavior { - TOOL, BLOCK, NONE -} +package de.diddiz.LogBlock; + +public enum ToolBehavior { + TOOL, + BLOCK, + NONE +} diff --git a/src/main/java/de/diddiz/LogBlock/ToolData.java b/src/main/java/de/diddiz/LogBlock/ToolData.java index f5243937..ee75dcc0 100644 --- a/src/main/java/de/diddiz/LogBlock/ToolData.java +++ b/src/main/java/de/diddiz/LogBlock/ToolData.java @@ -1,15 +1,15 @@ -package de.diddiz.LogBlock; - -import org.bukkit.entity.Player; - -public class ToolData { - public boolean enabled; - public QueryParams params; - public ToolMode mode; - - public ToolData(Tool tool, LogBlock logblock, Player player) { - enabled = tool.defaultEnabled && logblock.hasPermission(player, "logblock.tools." + tool.name); - params = tool.params.clone(); - mode = tool.mode; - } -} +package de.diddiz.LogBlock; + +import org.bukkit.entity.Player; + +public class ToolData { + public boolean enabled; + public QueryParams params; + public ToolMode mode; + + public ToolData(Tool tool, LogBlock logblock, Player player) { + enabled = tool.defaultEnabled && logblock.hasPermission(player, "logblock.tools." + tool.name); + params = tool.params.clone(); + mode = tool.mode; + } +} diff --git a/src/main/java/de/diddiz/LogBlock/ToolMode.java b/src/main/java/de/diddiz/LogBlock/ToolMode.java index bea69c0a..3a901375 100644 --- a/src/main/java/de/diddiz/LogBlock/ToolMode.java +++ b/src/main/java/de/diddiz/LogBlock/ToolMode.java @@ -1,14 +1,18 @@ -package de.diddiz.LogBlock; - -public enum ToolMode { - CLEARLOG("logblock.clearlog"), LOOKUP("logblock.lookup"), REDO("logblock.rollback"), ROLLBACK("logblock.rollback"), WRITELOGFILE("logblock.rollback"); - private final String permission; - - private ToolMode(String permission) { - this.permission = permission; - } - - public String getPermission() { - return permission; - } -} +package de.diddiz.LogBlock; + +public enum ToolMode { + CLEARLOG("logblock.clearlog"), + LOOKUP("logblock.lookup"), + REDO("logblock.rollback"), + ROLLBACK("logblock.rollback"), + WRITELOGFILE("logblock.rollback"); + private final String permission; + + private ToolMode(String permission) { + this.permission = permission; + } + + public String getPermission() { + return permission; + } +} diff --git a/src/main/java/de/diddiz/LogBlock/Updater.java b/src/main/java/de/diddiz/LogBlock/Updater.java index f0767aee..51e5a920 100644 --- a/src/main/java/de/diddiz/LogBlock/Updater.java +++ b/src/main/java/de/diddiz/LogBlock/Updater.java @@ -1,477 +1,1166 @@ -package de.diddiz.LogBlock; - -import de.diddiz.LogBlock.config.Config; -import de.diddiz.LogBlock.config.WorldConfig; -import de.diddiz.util.UUIDFetcher; -import org.bukkit.Bukkit; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.file.YamlConfiguration; - -import java.io.File; -import java.io.IOException; -import java.sql.*; -import java.util.*; -import java.util.logging.Level; - -import static de.diddiz.LogBlock.config.Config.getLoggedWorlds; -import static de.diddiz.LogBlock.config.Config.isLogging; -import static de.diddiz.util.BukkitUtils.friendlyWorldname; -import de.diddiz.util.ComparableVersion; -import java.util.regex.Pattern; -import static org.bukkit.Bukkit.getLogger; - -class Updater { - private final LogBlock logblock; - final int UUID_CONVERT_BATCH_SIZE = 100; - - Updater(LogBlock logblock) { - this.logblock = logblock; - } - - boolean update() { - final ConfigurationSection config = logblock.getConfig(); - String versionString = config.getString("version"); - if (Pattern.matches("1\\.\\d{2}",versionString)) { - versionString = "1." + versionString.charAt(2) + "." + versionString.charAt(3); - config.set("version",versionString); - logblock.saveConfig(); - } - ComparableVersion configVersion = new ComparableVersion(versionString); - if (configVersion.compareTo(new ComparableVersion(logblock.getDescription().getVersion())) >= 0) { - return false; - } - if (configVersion.compareTo(new ComparableVersion("1.2.7")) < 0) { - getLogger().info("Updating tables to 1.2.7 ..."); - if (isLogging(Logging.CHAT)) { - final Connection conn = logblock.getConnection(); - try { - conn.setAutoCommit(true); - final Statement st = conn.createStatement(); - st.execute("ALTER TABLE `lb-chat` ENGINE = MyISAM, ADD FULLTEXT message (message)"); - st.close(); - conn.close(); - } catch (final SQLException ex) { - Bukkit.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); - return false; - } - } - config.set("version", "1.2.7"); - } - if (configVersion.compareTo(new ComparableVersion("1.3")) < 0) { - getLogger().info("Updating config to 1.3.0 ..."); - for (final String tool : config.getConfigurationSection("tools").getKeys(false)) { - if (config.get("tools." + tool + ".permissionDefault") == null) { - config.set("tools." + tool + ".permissionDefault", "OP"); - } - } - config.set("version", "1.3.0"); - } - if (configVersion.compareTo(new ComparableVersion("1.3.1")) < 0) { - getLogger().info("Updating tables to 1.3.1 ..."); - final Connection conn = logblock.getConnection(); - try { - conn.setAutoCommit(true); - final Statement st = conn.createStatement(); - st.execute("ALTER TABLE `lb-players` ADD COLUMN lastlogin DATETIME NOT NULL, ADD COLUMN onlinetime TIME NOT NULL, ADD COLUMN ip VARCHAR(255) NOT NULL"); - st.close(); - conn.close(); - } catch (final SQLException ex) { - Bukkit.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); - return false; - } - config.set("version", "1.3.1"); - } - if (configVersion.compareTo(new ComparableVersion("1.3.2")) < 0) { - getLogger().info("Updating tables to 1.3.2 ..."); - final Connection conn = logblock.getConnection(); - try { - conn.setAutoCommit(true); - final Statement st = conn.createStatement(); - st.execute("ALTER TABLE `lb-players` ADD COLUMN firstlogin DATETIME NOT NULL AFTER playername"); - st.close(); - conn.close(); - } catch (final SQLException ex) { - Bukkit.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); - return false; - } - config.set("version", "1.3.2"); - } - if (configVersion.compareTo(new ComparableVersion("1.4")) < 0) { - getLogger().info("Updating config to 1.4.0 ..."); - config.set("clearlog.keepLogDays", null); - config.set("version", "1.4.0"); - } - if (configVersion.compareTo(new ComparableVersion("1.4.2")) < 0) { - getLogger().info("Updating config to 1.4.2 ..."); - for (final String world : config.getStringList("loggedWorlds")) { - final File file = new File(logblock.getDataFolder(), friendlyWorldname(world) + ".yml"); - final YamlConfiguration wcfg = YamlConfiguration.loadConfiguration(file); - if (wcfg.contains("logBlockCreations")) { - wcfg.set("logging.BLOCKPLACE", wcfg.getBoolean("logBlockCreations")); - } - if (wcfg.contains("logBlockDestroyings")) { - wcfg.set("logging.BLOCKBREAK", wcfg.getBoolean("logBlockDestroyings")); - } - if (wcfg.contains("logSignTexts")) { - wcfg.set("logging.SIGNTEXT", wcfg.getBoolean("logSignTexts")); - } - if (wcfg.contains("logFire")) { - wcfg.set("logging.FIRE", wcfg.getBoolean("logFire")); - } - if (wcfg.contains("logLeavesDecay")) { - wcfg.set("logging.LEAVESDECAY", wcfg.getBoolean("logLeavesDecay")); - } - if (wcfg.contains("logLavaFlow")) { - wcfg.set("logging.LAVAFLOW", wcfg.getBoolean("logLavaFlow")); - } - if (wcfg.contains("logWaterFlow")) { - wcfg.set("logging.WATERFLOW", wcfg.getBoolean("logWaterFlow")); - } - if (wcfg.contains("logChestAccess")) { - wcfg.set("logging.CHESTACCESS", wcfg.getBoolean("logChestAccess")); - } - if (wcfg.contains("logButtonsAndLevers")) { - wcfg.set("logging.SWITCHINTERACT", wcfg.getBoolean("logButtonsAndLevers")); - } - if (wcfg.contains("logKills")) { - wcfg.set("logging.KILL", wcfg.getBoolean("logKills")); - } - if (wcfg.contains("logChat")) { - wcfg.set("logging.CHAT", wcfg.getBoolean("logChat")); - } - if (wcfg.contains("logSnowForm")) { - wcfg.set("logging.SNOWFORM", wcfg.getBoolean("logSnowForm")); - } - if (wcfg.contains("logSnowFade")) { - wcfg.set("logging.SNOWFADE", wcfg.getBoolean("logSnowFade")); - } - if (wcfg.contains("logDoors")) { - wcfg.set("logging.DOORINTERACT", wcfg.getBoolean("logDoors")); - } - if (wcfg.contains("logCakes")) { - wcfg.set("logging.CAKEEAT", wcfg.getBoolean("logCakes")); - } - if (wcfg.contains("logEndermen")) { - wcfg.set("logging.ENDERMEN", wcfg.getBoolean("logEndermen")); - } - if (wcfg.contains("logExplosions")) { - final boolean logExplosions = wcfg.getBoolean("logExplosions"); - wcfg.set("logging.TNTEXPLOSION", logExplosions); - wcfg.set("logging.MISCEXPLOSION", logExplosions); - wcfg.set("logging.CREEPEREXPLOSION", logExplosions); - wcfg.set("logging.GHASTFIREBALLEXPLOSION", logExplosions); - } - wcfg.set("logBlockCreations", null); - wcfg.set("logBlockDestroyings", null); - wcfg.set("logSignTexts", null); - wcfg.set("logExplosions", null); - wcfg.set("logFire", null); - wcfg.set("logLeavesDecay", null); - wcfg.set("logLavaFlow", null); - wcfg.set("logWaterFlow", null); - wcfg.set("logChestAccess", null); - wcfg.set("logButtonsAndLevers", null); - wcfg.set("logKills", null); - wcfg.set("logChat", null); - wcfg.set("logSnowForm", null); - wcfg.set("logSnowFade", null); - wcfg.set("logDoors", null); - wcfg.set("logCakes", null); - wcfg.set("logEndermen", null); - try { - wcfg.save(file); - } catch (final IOException ex) { - Bukkit.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); - } - } - config.set("clearlog.keepLogDays", null); - config.set("version", "1.4.2"); - } - if (configVersion.compareTo(new ComparableVersion("1.5.1")) < 0) { - getLogger().info("Updating tables to 1.5.1 ..."); - final Connection conn = logblock.getConnection(); - try { - conn.setAutoCommit(true); - final Statement st = conn.createStatement(); - for (final WorldConfig wcfg : getLoggedWorlds()) { - if (wcfg.isLogging(Logging.KILL)) { - st.execute("ALTER TABLE `" + wcfg.table + "-kills` ADD (x MEDIUMINT NOT NULL DEFAULT 0, y SMALLINT NOT NULL DEFAULT 0, z MEDIUMINT NOT NULL DEFAULT 0)"); - } - } - st.close(); - conn.close(); - } catch (final SQLException ex) { - Bukkit.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); - return false; - } - config.set("version", "1.5.1"); - } - if (configVersion.compareTo(new ComparableVersion("1.5.2")) < 0) { - getLogger().info("Updating tables to 1.5.2 ..."); - final Connection conn = logblock.getConnection(); - try { - conn.setAutoCommit(true); - final Statement st = conn.createStatement(); - final ResultSet rs = st.executeQuery("SHOW COLUMNS FROM `lb-players` WHERE field = 'onlinetime'"); - if (rs.next() && rs.getString("Type").equalsIgnoreCase("time")) { - st.execute("ALTER TABLE `lb-players` ADD onlinetime2 INT UNSIGNED NOT NULL"); - st.execute("UPDATE `lb-players` SET onlinetime2 = HOUR(onlinetime) * 3600 + MINUTE(onlinetime) * 60 + SECOND(onlinetime)"); - st.execute("ALTER TABLE `lb-players` DROP onlinetime"); - st.execute("ALTER TABLE `lb-players` CHANGE onlinetime2 onlinetime INT UNSIGNED NOT NULL"); - } else { - getLogger().info("Column lb-players was already modified, skipping it."); - } - st.close(); - conn.close(); - } catch (final SQLException ex) { - Bukkit.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); - return false; - } - config.set("version", "1.5.2"); - } - if (configVersion.compareTo(new ComparableVersion("1.8.1")) < 0) { - getLogger().info("Updating tables to 1.8.1 ..."); - final Connection conn = logblock.getConnection(); - try { - conn.setAutoCommit(true); - final Statement st = conn.createStatement(); - for (final WorldConfig wcfg : getLoggedWorlds()) { - if (wcfg.isLogging(Logging.CHESTACCESS)) { - st.execute("ALTER TABLE `" + wcfg.table + "-chest` CHANGE itemdata itemdata SMALLINT NOT NULL"); - getLogger().info("Table " + wcfg.table + "-chest modified"); - } - } - st.close(); - conn.close(); - } catch (final SQLException ex) { - Bukkit.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); - return false; - } - config.set("version", "1.8.1"); - } - - if (configVersion.compareTo(new ComparableVersion("1.9")) < 0) { - getLogger().info("Updating tables to 1.9.0 ..."); - getLogger().info("Importing UUIDs for large databases may take some time"); - final Connection conn = logblock.getConnection(); - try { - conn.setAutoCommit(true); - final Statement st = conn.createStatement(); - st.execute("ALTER TABLE `lb-players` ADD `UUID` VARCHAR(36) NOT NULL"); - } catch (final SQLException ex) { - // Error 1060 is MySQL error "column already exists". We want to continue with import if we get that error - if (ex.getErrorCode() != 1060) { - Bukkit.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); - return false; - } - } - try { - String unimportedPrefix = "noimport_"; - ResultSet rs; - conn.setAutoCommit(true); - final Statement st = conn.createStatement(); - if (config.getBoolean("logging.logPlayerInfo")) { - // Start by assuming anything with no onlinetime is not a player - st.execute("UPDATE `lb-players` SET UUID = CONCAT ('log_',playername) WHERE onlinetime=0 AND LENGTH(UUID) = 0"); - } else { - // If we can't assume that, we must assume anything we can't look up is not a player - unimportedPrefix = "log_"; - } - // Tell people how many are needing converted - rs = st.executeQuery("SELECT COUNT(playername) FROM `lb-players` WHERE LENGTH(UUID)=0"); - rs.next(); - String total = Integer.toString(rs.getInt(1)); - getLogger().info(total + " players to convert"); - int done = 0; - - conn.setAutoCommit(false); - Map players = new HashMap(); - List names = new ArrayList(UUID_CONVERT_BATCH_SIZE); - Map response; - rs = st.executeQuery("SELECT playerid,playername FROM `lb-players` WHERE LENGTH(UUID)=0 LIMIT " + Integer.toString(UUID_CONVERT_BATCH_SIZE)); - while (rs.next()) { - do { - players.put(rs.getString(2), rs.getInt(1)); - names.add(rs.getString(2)); - } while (rs.next()); - if (names.size() > 0) { - String theUUID; - response = UUIDFetcher.getUUIDs(names); - for (Map.Entry entry : players.entrySet()) { - if (response.get(entry.getKey()) == null) { - theUUID = unimportedPrefix + entry.getKey(); - getLogger().warning(entry.getKey() + " not found - giving UUID of " + theUUID); - } else { - theUUID = response.get(entry.getKey()).toString(); - } - String thePID = entry.getValue().toString(); - st.execute("UPDATE `lb-players` SET UUID = '" + theUUID + "' WHERE playerid = " + thePID); - done++; - } - conn.commit(); - players.clear(); - names.clear(); - getLogger().info("Processed " + Integer.toString(done) + " out of " + total); - rs = st.executeQuery("SELECT playerid,playername FROM `lb-players` WHERE LENGTH(UUID)=0 LIMIT " + Integer.toString(UUID_CONVERT_BATCH_SIZE)); - } - } - st.close(); - conn.close(); - - } catch (final SQLException ex) { - Bukkit.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); - return false; - } catch (Exception ex) { - Bukkit.getLogger().log(Level.SEVERE, "[UUID importer]", ex); - return false; - } - config.set("version", "1.9.0"); - } - if (configVersion.compareTo(new ComparableVersion("1.9.4")) < 0) { - getLogger().info("Updating tables to 1.9.4 ..."); - final Connection conn = logblock.getConnection(); - try { - conn.setAutoCommit(true); - final Statement st = conn.createStatement(); - // Need to wrap both these next two inside individual try/catch statements in case index does not exist - try { - st.execute("DROP INDEX UUID ON `lb-players`"); - } catch (final SQLException ex) { - if (ex.getErrorCode() != 1091) { - Bukkit.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); - return false; - } - } - try { - st.execute("DROP INDEX playername ON `lb-players`"); - } catch (final SQLException ex) { - if (ex.getErrorCode() != 1091) { - Bukkit.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); - return false; - } - } - st.execute("CREATE INDEX UUID ON `lb-players` (UUID);"); - st.execute("CREATE INDEX playername ON `lb-players` (playername);"); - st.close(); - conn.close(); - } catch (final SQLException ex) { - Bukkit.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); - return false; - } - config.set("version", "1.9.4"); - } - // Ensure charset for free-text fields is UTF-8, or UTF8-mb4 if possible - // As this may be an expensive operation and the database default may already be this, check on a table-by-table basis before converting - if (configVersion.compareTo(new ComparableVersion("1.10.0")) < 0) { - getLogger().info("Updating tables to 1.10.0 ..."); - final Connection conn = logblock.getConnection(); - try { - conn.setAutoCommit(true); - final Statement st = conn.createStatement(); - checkCharset("lb-players","name",st); - if (isLogging(Logging.CHAT)) { - checkCharset("lb-chat","message", st); - } - for (final WorldConfig wcfg : getLoggedWorlds()) { - if (wcfg.isLogging(Logging.SIGNTEXT)) { - checkCharset(wcfg.table + "-sign","signtext",st); - } - } - st.close(); - conn.close(); - } catch (final SQLException ex) { - Bukkit.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); - return false; - } - config.set("version", "1.10.0"); - } - - logblock.saveConfig(); - return true; - } - - void checkCharset(String table, String column, Statement st) throws SQLException { - final ResultSet rs = st.executeQuery("SHOW FULL COLUMNS FROM `" + table + "` WHERE field = '" + column + "'"); - String charset = "utf8"; - if (Config.mb4) { - charset = "utf8mb4"; - } - if (rs.next() && !rs.getString("Collation").substring(0, charset.length()).equalsIgnoreCase(charset)) { - st.execute("ALTER TABLE `" + table + "` CONVERT TO CHARSET " + charset); - getLogger().info("Table " + table + " modified"); - } else { - getLogger().info("Table " + table + " already fine, skipping it"); - } - } - - void checkTables() throws SQLException { - String charset = "utf8"; - if (Config.mb4) { - charset = "utf8mb4"; - } - final Connection conn = logblock.getConnection(); - if (conn == null) { - throw new SQLException("No connection"); - } - final Statement state = conn.createStatement(); - final DatabaseMetaData dbm = conn.getMetaData(); - conn.setAutoCommit(true); - createTable(dbm, state, "lb-players", "(playerid INT UNSIGNED NOT NULL AUTO_INCREMENT, UUID varchar(36) NOT NULL, playername varchar(32) NOT NULL, firstlogin DATETIME NOT NULL, lastlogin DATETIME NOT NULL, onlinetime INT UNSIGNED NOT NULL, ip varchar(255) NOT NULL, PRIMARY KEY (playerid), INDEX (UUID), INDEX (playername)) DEFAULT CHARSET " + charset); - // Players table must not be empty or inserts won't work - bug #492 - final ResultSet rs = state.executeQuery("SELECT NULL FROM `lb-players` LIMIT 1;"); - if (!rs.next()) { - state.execute("INSERT IGNORE INTO `lb-players` (UUID,playername) VALUES ('log_dummy_record','dummy_record')"); - } - if (isLogging(Logging.CHAT)) { - createTable(dbm, state, "lb-chat", "(id INT UNSIGNED NOT NULL AUTO_INCREMENT, date DATETIME NOT NULL, playerid INT UNSIGNED NOT NULL, message VARCHAR(255) NOT NULL, PRIMARY KEY (id), KEY playerid (playerid), FULLTEXT message (message)) ENGINE=MyISAM DEFAULT CHARSET " + charset); - } - for (final WorldConfig wcfg : getLoggedWorlds()) { - createTable(dbm, state, wcfg.table, "(id INT UNSIGNED NOT NULL AUTO_INCREMENT, date DATETIME NOT NULL, playerid INT UNSIGNED NOT NULL, replaced TINYINT UNSIGNED NOT NULL, type TINYINT UNSIGNED NOT NULL, data TINYINT UNSIGNED NOT NULL, x MEDIUMINT NOT NULL, y SMALLINT UNSIGNED NOT NULL, z MEDIUMINT NOT NULL, PRIMARY KEY (id), KEY coords (x, z, y), KEY date (date), KEY playerid (playerid))"); - createTable(dbm, state, wcfg.table + "-sign", "(id INT UNSIGNED NOT NULL, signtext VARCHAR(255) NOT NULL, PRIMARY KEY (id)) DEFAULT CHARSET " + charset); - createTable(dbm, state, wcfg.table + "-chest", "(id INT UNSIGNED NOT NULL, itemtype SMALLINT UNSIGNED NOT NULL, itemamount SMALLINT NOT NULL, itemdata SMALLINT NOT NULL, PRIMARY KEY (id))"); - if (wcfg.isLogging(Logging.KILL)) { - createTable(dbm, state, wcfg.table + "-kills", "(id INT UNSIGNED NOT NULL AUTO_INCREMENT, date DATETIME NOT NULL, killer INT UNSIGNED, victim INT UNSIGNED NOT NULL, weapon SMALLINT UNSIGNED NOT NULL, x MEDIUMINT NOT NULL, y SMALLINT NOT NULL, z MEDIUMINT NOT NULL, PRIMARY KEY (id))"); - } - } - state.close(); - conn.close(); - } - - private static void createTable(DatabaseMetaData dbm, Statement state, String table, String query) throws SQLException { - if (!dbm.getTables(null, null, table, null).next()) { - getLogger().log(Level.INFO, "Creating table " + table + "."); - state.execute("CREATE TABLE `" + table + "` " + query); - if (!dbm.getTables(null, null, table, null).next()) { - throw new SQLException("Table " + table + " not found and failed to create"); - } - } - } - - public static class PlayerCountChecker implements Runnable { - - private LogBlock logblock; - - public PlayerCountChecker(LogBlock logblock) { - this.logblock = logblock; - } - - @Override - public void run() { - final Connection conn = logblock.getConnection(); - try { - conn.setAutoCommit(true); - final Statement st = conn.createStatement(); - ResultSet rs = st.executeQuery("SELECT auto_increment FROM information_schema.columns AS col join information_schema.tables AS tab ON (col.table_schema=tab.table_schema AND col.table_name=tab.table_name) WHERE col.table_name = 'lb-players' AND col.column_name = 'playerid' AND col.data_type = 'smallint' AND col.table_schema = DATABASE() AND auto_increment > 65000;"); - if (rs.next()) { - for (int i = 0; i < 6; i++) { - logblock.getLogger().warning("Your server reached 65000 players. You should soon update your database table schema - see FAQ: https://github.com/LogBlock/LogBlock/wiki/FAQ#logblock-your-server-reached-65000-players-"); - } - } - st.close(); - conn.close(); - } catch (final SQLException ex) { - logblock.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); - } - } - } -} +package de.diddiz.LogBlock; + +import de.diddiz.LogBlock.blockstate.BlockStateCodecSign; +import de.diddiz.LogBlock.config.Config; +import de.diddiz.LogBlock.config.WorldConfig; +import de.diddiz.LogBlock.util.ComparableVersion; +import de.diddiz.LogBlock.util.ItemStackAndAmount; +import de.diddiz.LogBlock.util.UUIDFetcher; +import de.diddiz.LogBlock.util.Utils; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.sign.Side; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.inventory.ItemStack; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.sql.*; +import java.util.*; +import java.util.jar.JarFile; +import java.util.logging.Level; + +import static de.diddiz.LogBlock.config.Config.getLoggedWorlds; +import static de.diddiz.LogBlock.config.Config.isLogging; +import static de.diddiz.LogBlock.util.BukkitUtils.friendlyWorldname; + +class Updater { + private final LogBlock logblock; + final int UUID_CONVERT_BATCH_SIZE = 100; + final int BLOCKS_CONVERT_BATCH_SIZE = 100000; + final int OTHER_CONVERT_BATCH_SIZE = 20000; + + Updater(LogBlock logblock) { + this.logblock = logblock; + } + + boolean update() { + final ConfigurationSection config = logblock.getConfig(); + String versionString = config.getString("version"); + ComparableVersion configVersion = new ComparableVersion(versionString); + // if (configVersion.compareTo(new ComparableVersion(logblock.getDescription().getVersion().replace(" (manually compiled)", ""))) >= 0) { + // return false; + // } + if (configVersion.compareTo(new ComparableVersion("1.2.7")) < 0) { + logblock.getLogger().info("Updating tables to 1.2.7 ..."); + if (isLogging(Logging.CHAT) || isLogging(Logging.PLAYER_COMMANDS) || isLogging(Logging.CONSOLE_COMMANDS) || isLogging(Logging.COMMANDBLOCK_COMMANDS)) { + final Connection conn = logblock.getConnection(); + try { + conn.setAutoCommit(true); + final Statement st = conn.createStatement(); + st.execute("ALTER TABLE `lb-chat` ADD FULLTEXT message (message)"); + st.close(); + conn.close(); + } catch (final SQLException ex) { + logblock.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); + return false; + } + } + config.set("version", "1.2.7"); + } + if (configVersion.compareTo(new ComparableVersion("1.3")) < 0) { + logblock.getLogger().info("Updating config to 1.3.0 ..."); + for (final String tool : config.getConfigurationSection("tools").getKeys(false)) { + if (config.get("tools." + tool + ".permissionDefault") == null) { + config.set("tools." + tool + ".permissionDefault", "OP"); + } + } + config.set("version", "1.3.0"); + } + if (configVersion.compareTo(new ComparableVersion("1.3.1")) < 0) { + logblock.getLogger().info("Updating tables to 1.3.1 ..."); + final Connection conn = logblock.getConnection(); + try { + conn.setAutoCommit(true); + final Statement st = conn.createStatement(); + st.execute("ALTER TABLE `lb-players` ADD COLUMN lastlogin DATETIME NOT NULL, ADD COLUMN onlinetime TIME NOT NULL, ADD COLUMN ip VARCHAR(255) NOT NULL"); + st.close(); + conn.close(); + } catch (final SQLException ex) { + logblock.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); + return false; + } + config.set("version", "1.3.1"); + } + if (configVersion.compareTo(new ComparableVersion("1.3.2")) < 0) { + logblock.getLogger().info("Updating tables to 1.3.2 ..."); + final Connection conn = logblock.getConnection(); + try { + conn.setAutoCommit(true); + final Statement st = conn.createStatement(); + st.execute("ALTER TABLE `lb-players` ADD COLUMN firstlogin DATETIME NOT NULL AFTER playername"); + st.close(); + conn.close(); + } catch (final SQLException ex) { + logblock.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); + return false; + } + config.set("version", "1.3.2"); + } + if (configVersion.compareTo(new ComparableVersion("1.4")) < 0) { + logblock.getLogger().info("Updating config to 1.4.0 ..."); + config.set("clearlog.keepLogDays", null); + config.set("version", "1.4.0"); + } + if (configVersion.compareTo(new ComparableVersion("1.4.2")) < 0) { + logblock.getLogger().info("Updating config to 1.4.2 ..."); + for (final String world : config.getStringList("loggedWorlds")) { + final File file = new File(logblock.getDataFolder(), friendlyWorldname(world) + ".yml"); + final YamlConfiguration wcfg = YamlConfiguration.loadConfiguration(file); + if (wcfg.contains("logBlockCreations")) { + wcfg.set("logging.BLOCKPLACE", wcfg.getBoolean("logBlockCreations")); + } + if (wcfg.contains("logBlockDestroyings")) { + wcfg.set("logging.BLOCKBREAK", wcfg.getBoolean("logBlockDestroyings")); + } + if (wcfg.contains("logSignTexts")) { + wcfg.set("logging.SIGNTEXT", wcfg.getBoolean("logSignTexts")); + } + if (wcfg.contains("logFire")) { + wcfg.set("logging.FIRE", wcfg.getBoolean("logFire")); + } + if (wcfg.contains("logLeavesDecay")) { + wcfg.set("logging.LEAVESDECAY", wcfg.getBoolean("logLeavesDecay")); + } + if (wcfg.contains("logLavaFlow")) { + wcfg.set("logging.LAVAFLOW", wcfg.getBoolean("logLavaFlow")); + } + if (wcfg.contains("logWaterFlow")) { + wcfg.set("logging.WATERFLOW", wcfg.getBoolean("logWaterFlow")); + } + if (wcfg.contains("logChestAccess")) { + wcfg.set("logging.CHESTACCESS", wcfg.getBoolean("logChestAccess")); + } + if (wcfg.contains("logButtonsAndLevers")) { + wcfg.set("logging.SWITCHINTERACT", wcfg.getBoolean("logButtonsAndLevers")); + } + if (wcfg.contains("logKills")) { + wcfg.set("logging.KILL", wcfg.getBoolean("logKills")); + } + if (wcfg.contains("logChat")) { + wcfg.set("logging.CHAT", wcfg.getBoolean("logChat")); + } + if (wcfg.contains("logSnowForm")) { + wcfg.set("logging.SNOWFORM", wcfg.getBoolean("logSnowForm")); + } + if (wcfg.contains("logSnowFade")) { + wcfg.set("logging.SNOWFADE", wcfg.getBoolean("logSnowFade")); + } + if (wcfg.contains("logDoors")) { + wcfg.set("logging.DOORINTERACT", wcfg.getBoolean("logDoors")); + } + if (wcfg.contains("logCakes")) { + wcfg.set("logging.CAKEEAT", wcfg.getBoolean("logCakes")); + } + if (wcfg.contains("logEndermen")) { + wcfg.set("logging.ENDERMEN", wcfg.getBoolean("logEndermen")); + } + if (wcfg.contains("logExplosions")) { + final boolean logExplosions = wcfg.getBoolean("logExplosions"); + wcfg.set("logging.TNTEXPLOSION", logExplosions); + wcfg.set("logging.MISCEXPLOSION", logExplosions); + wcfg.set("logging.CREEPEREXPLOSION", logExplosions); + wcfg.set("logging.GHASTFIREBALLEXPLOSION", logExplosions); + } + wcfg.set("logBlockCreations", null); + wcfg.set("logBlockDestroyings", null); + wcfg.set("logSignTexts", null); + wcfg.set("logExplosions", null); + wcfg.set("logFire", null); + wcfg.set("logLeavesDecay", null); + wcfg.set("logLavaFlow", null); + wcfg.set("logWaterFlow", null); + wcfg.set("logChestAccess", null); + wcfg.set("logButtonsAndLevers", null); + wcfg.set("logKills", null); + wcfg.set("logChat", null); + wcfg.set("logSnowForm", null); + wcfg.set("logSnowFade", null); + wcfg.set("logDoors", null); + wcfg.set("logCakes", null); + wcfg.set("logEndermen", null); + try { + wcfg.save(file); + } catch (final IOException ex) { + logblock.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); + } + } + config.set("clearlog.keepLogDays", null); + config.set("version", "1.4.2"); + } + if (configVersion.compareTo(new ComparableVersion("1.5.1")) < 0) { + logblock.getLogger().info("Updating tables to 1.5.1 ..."); + final Connection conn = logblock.getConnection(); + try { + conn.setAutoCommit(true); + final Statement st = conn.createStatement(); + for (final WorldConfig wcfg : getLoggedWorlds()) { + if (wcfg.isLogging(Logging.KILL)) { + st.execute("ALTER TABLE `" + wcfg.table + "-kills` ADD (x MEDIUMINT NOT NULL DEFAULT 0, y SMALLINT NOT NULL DEFAULT 0, z MEDIUMINT NOT NULL DEFAULT 0)"); + } + } + st.close(); + conn.close(); + } catch (final SQLException ex) { + logblock.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); + return false; + } + config.set("version", "1.5.1"); + } + if (configVersion.compareTo(new ComparableVersion("1.5.2")) < 0) { + logblock.getLogger().info("Updating tables to 1.5.2 ..."); + final Connection conn = logblock.getConnection(); + try { + conn.setAutoCommit(true); + final Statement st = conn.createStatement(); + final ResultSet rs = st.executeQuery("SHOW COLUMNS FROM `lb-players` WHERE field = 'onlinetime'"); + if (rs.next() && rs.getString("Type").equalsIgnoreCase("time")) { + st.execute("ALTER TABLE `lb-players` ADD onlinetime2 INT UNSIGNED NOT NULL"); + st.execute("UPDATE `lb-players` SET onlinetime2 = HOUR(onlinetime) * 3600 + MINUTE(onlinetime) * 60 + SECOND(onlinetime)"); + st.execute("ALTER TABLE `lb-players` DROP onlinetime"); + st.execute("ALTER TABLE `lb-players` CHANGE onlinetime2 onlinetime INT UNSIGNED NOT NULL"); + } else { + logblock.getLogger().info("Column lb-players was already modified, skipping it."); + } + st.close(); + conn.close(); + } catch (final SQLException ex) { + logblock.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); + return false; + } + config.set("version", "1.5.2"); + } + if (configVersion.compareTo(new ComparableVersion("1.8.1")) < 0) { + logblock.getLogger().info("Updating tables to 1.8.1 ..."); + final Connection conn = logblock.getConnection(); + try { + conn.setAutoCommit(true); + final Statement st = conn.createStatement(); + for (final WorldConfig wcfg : getLoggedWorlds()) { + if (wcfg.isLogging(Logging.CHESTACCESS)) { + st.execute("ALTER TABLE `" + wcfg.table + "-chest` CHANGE itemdata itemdata SMALLINT NOT NULL"); + logblock.getLogger().info("Table " + wcfg.table + "-chest modified"); + } + } + st.close(); + conn.close(); + } catch (final SQLException ex) { + logblock.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); + return false; + } + config.set("version", "1.8.1"); + } + + if (configVersion.compareTo(new ComparableVersion("1.9")) < 0) { + logblock.getLogger().info("Updating tables to 1.9.0 ..."); + logblock.getLogger().info("Importing UUIDs for large databases may take some time"); + final Connection conn = logblock.getConnection(); + try { + conn.setAutoCommit(true); + final Statement st = conn.createStatement(); + st.execute("ALTER TABLE `lb-players` ADD `UUID` VARCHAR(36) NOT NULL"); + } catch (final SQLException ex) { + // Error 1060 is MySQL error "column already exists". We want to continue with import if we get that error + if (ex.getErrorCode() != 1060) { + logblock.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); + return false; + } + } + try { + String unimportedPrefix = "noimport_"; + ResultSet rs; + conn.setAutoCommit(true); + final Statement st = conn.createStatement(); + if (config.getBoolean("logging.logPlayerInfo")) { + // Start by assuming anything with no onlinetime is not a player + st.execute("UPDATE `lb-players` SET UUID = CONCAT ('log_',playername) WHERE onlinetime=0 AND LENGTH(UUID) = 0"); + } else { + // If we can't assume that, we must assume anything we can't look up is not a player + unimportedPrefix = "log_"; + } + // Tell people how many are needing converted + rs = st.executeQuery("SELECT COUNT(playername) FROM `lb-players` WHERE LENGTH(UUID)=0"); + rs.next(); + String total = Integer.toString(rs.getInt(1)); + logblock.getLogger().info(total + " players to convert"); + int done = 0; + + conn.setAutoCommit(false); + Map players = new HashMap<>(); + List names = new ArrayList<>(UUID_CONVERT_BATCH_SIZE); + Map response; + rs = st.executeQuery("SELECT playerid,playername FROM `lb-players` WHERE LENGTH(UUID)=0 LIMIT " + Integer.toString(UUID_CONVERT_BATCH_SIZE)); + while (rs.next()) { + do { + players.put(rs.getString(2), rs.getInt(1)); + names.add(rs.getString(2)); + } while (rs.next()); + if (names.size() > 0) { + String theUUID; + response = UUIDFetcher.getUUIDs(names); + for (Map.Entry entry : players.entrySet()) { + if (response.get(entry.getKey()) == null) { + theUUID = unimportedPrefix + entry.getKey(); + logblock.getLogger().warning(entry.getKey() + " not found - giving UUID of " + theUUID); + } else { + theUUID = response.get(entry.getKey()).toString(); + } + String thePID = entry.getValue().toString(); + st.execute("UPDATE `lb-players` SET UUID = '" + theUUID + "' WHERE playerid = " + thePID); + done++; + } + conn.commit(); + players.clear(); + names.clear(); + logblock.getLogger().info("Processed " + Integer.toString(done) + " out of " + total); + rs.close(); + rs = st.executeQuery("SELECT playerid,playername FROM `lb-players` WHERE LENGTH(UUID)=0 LIMIT " + Integer.toString(UUID_CONVERT_BATCH_SIZE)); + } + } + rs.close(); + st.close(); + conn.close(); + + } catch (final SQLException ex) { + logblock.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); + return false; + } catch (Exception ex) { + logblock.getLogger().log(Level.SEVERE, "[UUID importer]", ex); + return false; + } + config.set("version", "1.9.0"); + } + if (configVersion.compareTo(new ComparableVersion("1.9.4")) < 0) { + logblock.getLogger().info("Updating tables to 1.9.4 ..."); + final Connection conn = logblock.getConnection(); + try { + conn.setAutoCommit(true); + final Statement st = conn.createStatement(); + // Need to wrap both these next two inside individual try/catch statements in case index does not exist + try { + st.execute("DROP INDEX UUID ON `lb-players`"); + } catch (final SQLException ex) { + if (ex.getErrorCode() != 1091) { + logblock.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); + return false; + } + } + try { + st.execute("DROP INDEX playername ON `lb-players`"); + } catch (final SQLException ex) { + if (ex.getErrorCode() != 1091) { + logblock.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); + return false; + } + } + st.execute("CREATE INDEX UUID ON `lb-players` (UUID);"); + st.execute("CREATE INDEX playername ON `lb-players` (playername);"); + st.close(); + conn.close(); + } catch (final SQLException ex) { + logblock.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); + return false; + } + config.set("version", "1.9.4"); + } + // Ensure charset for free-text fields is UTF-8, or UTF8-mb4 if possible + // As this may be an expensive operation and the database default may already be this, check on a table-by-table basis before converting + if (configVersion.compareTo(new ComparableVersion("1.10.0")) < 0) { + logblock.getLogger().info("Updating tables to 1.10.0 ..."); + final Connection conn = logblock.getConnection(); + try { + conn.setAutoCommit(true); + final Statement st = conn.createStatement(); + checkCharset("lb-players", "name", st, false); + if (isLogging(Logging.CHAT) || isLogging(Logging.PLAYER_COMMANDS) || isLogging(Logging.CONSOLE_COMMANDS) || isLogging(Logging.COMMANDBLOCK_COMMANDS)) { + checkCharset("lb-chat", "message", st, false); + } + for (final WorldConfig wcfg : getLoggedWorlds()) { + if (wcfg.isLogging(Logging.SIGNTEXT)) { + // checkCharset(wcfg.table + "-sign","signtext",st); + } + } + st.close(); + conn.close(); + } catch (final SQLException ex) { + logblock.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); + return false; + } + config.set("version", "1.10.0"); + } + + if (configVersion.compareTo(new ComparableVersion("1.12.0")) < 0) { + logblock.getLogger().info("Updating tables to 1.12.0 ..."); + if (isLogging(Logging.CHAT) || isLogging(Logging.PLAYER_COMMANDS) || isLogging(Logging.CONSOLE_COMMANDS) || isLogging(Logging.COMMANDBLOCK_COMMANDS)) { + final Connection conn = logblock.getConnection(); + try { + conn.setAutoCommit(true); + final Statement st = conn.createStatement(); + st.execute("ALTER TABLE `lb-chat` MODIFY COLUMN `message` VARCHAR(256) NOT NULL"); + st.close(); + conn.close(); + } catch (final SQLException ex) { + logblock.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); + return false; + } + } + config.set("version", "1.12.0"); + } + if (configVersion.compareTo(new ComparableVersion("1.13.0")) < 0) { + logblock.getLogger().info("Updating tables to 1.13.0 ..."); + try { + MaterialUpdater1_13 materialUpdater = new MaterialUpdater1_13(logblock); + logblock.getLogger().info("Convertig BlockId to BlockData. This can take a while ..."); + final Connection conn = logblock.getConnection(); + conn.setAutoCommit(false); + final Statement st = conn.createStatement(); + for (final WorldConfig wcfg : getLoggedWorlds()) { + logblock.getLogger().info("Processing world " + wcfg.world + "..."); + logblock.getLogger().info("Processing block changes..."); + boolean hadRow = true; + long rowsToConvert = 0; + long done = 0; + try { + ResultSet rs = st.executeQuery("SELECT count(*) as rowcount FROM `" + wcfg.table + "`"); + if (rs.next()) { + rowsToConvert = rs.getLong(1); + logblock.getLogger().info("Converting " + rowsToConvert + " entries in " + wcfg.table); + } + rs.close(); + + PreparedStatement deleteStatement = conn.prepareStatement("DELETE FROM `" + wcfg.table + "` WHERE id = ?"); + PreparedStatement insertStatement = conn.prepareStatement("INSERT INTO `" + wcfg.table + "-blocks` (id, date, playerid, replaced, replacedData, type, typeData, x, y, z) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS); + + while (hadRow) { + hadRow = false; + ResultSet entries = st.executeQuery("SELECT id, date, playerid, replaced, type, data, x, y, z FROM `" + wcfg.table + "` ORDER BY id ASC LIMIT " + BLOCKS_CONVERT_BATCH_SIZE); + while (entries.next()) { + hadRow = true; + long id = entries.getLong("id"); + Timestamp date = entries.getTimestamp("date"); + int playerid = entries.getInt("playerid"); + int replaced = entries.getInt("replaced"); + int type = entries.getInt("type"); + int data = entries.getInt("data"); + int x = entries.getInt("x"); + int y = entries.getInt("y"); + int z = entries.getInt("z"); + if (data == 16) { + data = 0; + } + + try { + BlockData replacedBlockData = materialUpdater.getBlockData(replaced, data); + BlockData setBlockData = materialUpdater.getBlockData(type, data); + + int newReplacedId = MaterialConverter.getOrAddMaterialId(replacedBlockData); + int newReplacedData = MaterialConverter.getOrAddBlockStateId(replacedBlockData); + + int newSetId = MaterialConverter.getOrAddMaterialId(setBlockData); + int newSetData = MaterialConverter.getOrAddBlockStateId(setBlockData); + + insertStatement.setLong(1, id); + insertStatement.setTimestamp(2, date); + insertStatement.setInt(3, playerid); + insertStatement.setInt(4, newReplacedId); + insertStatement.setInt(5, newReplacedData); + insertStatement.setInt(6, newSetId); + insertStatement.setInt(7, newSetData); + insertStatement.setInt(8, x); + insertStatement.setInt(9, y); + insertStatement.setInt(10, z); + insertStatement.addBatch(); + } catch (Exception e) { + logblock.getLogger().info("Exception in entry " + id + " (" + replaced + ":" + data + "->" + type + ":" + data + "): " + e.getMessage()); + } + deleteStatement.setLong(1, id); + deleteStatement.addBatch(); + + done++; + } + entries.close(); + int failedRows = 0; + if (hadRow) { + try { + insertStatement.executeBatch(); + } catch (BatchUpdateException e) { + for (int result : e.getUpdateCounts()) { + if (result == Statement.EXECUTE_FAILED) { + failedRows++; + } + } + } + deleteStatement.executeBatch(); + } + conn.commit(); + logblock.getLogger().info("Done: " + done + "/" + rowsToConvert + " " + (failedRows > 0 ? "Duplicates: " + failedRows + " " : "") + "(" + (rowsToConvert > 0 ? (done * 100 / rowsToConvert) : 100) + "%)"); + } + insertStatement.close(); + deleteStatement.close(); + } catch (SQLException e) { + logblock.getLogger().info("Could not convert " + wcfg.table + ": " + e.getMessage()); + } + + logblock.getLogger().info("Processing chests..."); + rowsToConvert = 0; + done = 0; + try { + ResultSet rs = st.executeQuery("SELECT count(*) as rowcount FROM `" + wcfg.table + "-chest`"); + if (rs.next()) { + rowsToConvert = rs.getLong(1); + logblock.getLogger().info("Converting " + rowsToConvert + " entries in " + wcfg.table + "-chest"); + } + rs.close(); + + PreparedStatement insertChestData = conn.prepareStatement("INSERT INTO `" + wcfg.table + "-chestdata` (id, item, itemremove, itemtype) VALUES (?, ?, ?, ?)"); + PreparedStatement deleteChest = conn.prepareStatement("DELETE FROM `" + wcfg.table + "-chest` WHERE id = ?"); + while (true) { + rs = st.executeQuery("SELECT id, itemtype, itemamount, itemdata FROM `" + wcfg.table + "-chest` ORDER BY id ASC LIMIT " + OTHER_CONVERT_BATCH_SIZE); + boolean anyRow = false; + while (rs.next()) { + anyRow = true; + long id = rs.getLong("id"); + int itemtype = rs.getInt("itemtype"); + int itemdata = rs.getInt("itemdata"); + int amount = rs.getInt("itemamount"); + Material weaponMaterial = materialUpdater.getMaterial(itemtype, itemdata); + if (weaponMaterial == null) { + weaponMaterial = Material.AIR; + } + @SuppressWarnings("deprecation") + ItemStack stack = weaponMaterial.getMaxDurability() > 0 ? new ItemStack(weaponMaterial, Math.abs(amount), (short) itemdata) : new ItemStack(weaponMaterial, Math.abs(amount)); + insertChestData.setLong(1, id); + insertChestData.setBytes(2, Utils.saveItemStack(ItemStackAndAmount.fromStack(stack))); + insertChestData.setInt(3, amount >= 0 ? 0 : 1); + insertChestData.setInt(4, MaterialConverter.getOrAddMaterialId(weaponMaterial)); + insertChestData.addBatch(); + + deleteChest.setLong(1, id); + deleteChest.addBatch(); + done++; + } + rs.close(); + if (!anyRow) { + break; + } + int failedRows = 0; + try { + insertChestData.executeBatch(); + } catch (BatchUpdateException e) { + for (int result : e.getUpdateCounts()) { + if (result == Statement.EXECUTE_FAILED) { + failedRows++; + } + } + } + deleteChest.executeBatch(); + conn.commit(); + logblock.getLogger().info("Done: " + done + "/" + rowsToConvert + " " + (failedRows > 0 ? "Duplicates: " + failedRows + " " : "") + "(" + (rowsToConvert > 0 ? (done * 100 / rowsToConvert) : 100) + "%)"); + } + insertChestData.close(); + deleteChest.close(); + } catch (SQLException e) { + logblock.getLogger().info("Could not convert " + wcfg.table + "-chest: " + e.getMessage()); + } + + if (wcfg.isLogging(Logging.KILL)) { + logblock.getLogger().info("Processing kills..."); + rowsToConvert = 0; + done = 0; + try { + ResultSet rs = st.executeQuery("SELECT count(*) as rowcount FROM `" + wcfg.table + "-kills`"); + if (rs.next()) { + rowsToConvert = rs.getLong(1); + logblock.getLogger().info("Converting " + rowsToConvert + " entries in " + wcfg.table + "-kills"); + } + rs.close(); + + PreparedStatement updateWeaponStatement = conn.prepareStatement("UPDATE `" + wcfg.table + "-kills` SET weapon = ? WHERE id = ?"); + for (int start = 0;; start += OTHER_CONVERT_BATCH_SIZE) { + rs = st.executeQuery("SELECT id, weapon FROM `" + wcfg.table + "-kills` ORDER BY id ASC LIMIT " + start + "," + OTHER_CONVERT_BATCH_SIZE); + boolean anyUpdate = false; + boolean anyRow = false; + while (rs.next()) { + anyRow = true; + long id = rs.getLong("id"); + int weapon = rs.getInt("weapon"); + Material weaponMaterial = materialUpdater.getMaterial(weapon, 0); + if (weaponMaterial == null) { + weaponMaterial = Material.AIR; + } + int newWeapon = MaterialConverter.getOrAddMaterialId(weaponMaterial); + if (newWeapon != weapon) { + anyUpdate = true; + updateWeaponStatement.setInt(1, newWeapon); + updateWeaponStatement.setLong(2, id); + updateWeaponStatement.addBatch(); + } + done++; + } + rs.close(); + if (anyUpdate) { + updateWeaponStatement.executeBatch(); + conn.commit(); + } + logblock.getLogger().info("Done: " + done + "/" + rowsToConvert + " (" + (rowsToConvert > 0 ? (done * 100 / rowsToConvert) : 100) + "%)"); + if (!anyRow) { + break; + } + } + updateWeaponStatement.close(); + } catch (SQLException e) { + logblock.getLogger().info("Could not convert " + wcfg.table + "-kills: " + e.getMessage()); + } + } + } + st.close(); + conn.close(); + + logblock.getLogger().info("Updating config to 1.13.0 ..."); + config.set("logging.hiddenBlocks", materialUpdater.convertMaterials(config.getStringList("logging.hiddenBlocks"))); + config.set("rollback.dontRollback", materialUpdater.convertMaterials(config.getStringList("rollback.dontRollback"))); + config.set("rollback.replaceAnyway", materialUpdater.convertMaterials(config.getStringList("rollback.replaceAnyway"))); + final ConfigurationSection toolsSec = config.getConfigurationSection("tools"); + for (final String toolName : toolsSec.getKeys(false)) { + final ConfigurationSection tSec = toolsSec.getConfigurationSection(toolName); + tSec.set("item", materialUpdater.convertMaterial(tSec.getString("item", "OAK_LOG"))); + } + } catch (final SQLException | IOException ex) { + logblock.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); + return false; + } + config.set("version", "1.13.0"); + } + + if (configVersion.compareTo(new ComparableVersion("1.13.1")) < 0) { + logblock.getLogger().info("Updating tables to 1.13.1 ..."); + BlockStateCodecSign signCodec = new BlockStateCodecSign(); + try (Connection conn = logblock.getConnection()) { + conn.setAutoCommit(false); + final Statement st = conn.createStatement(); + for (final WorldConfig wcfg : getLoggedWorlds()) { + logblock.getLogger().info("Processing world " + wcfg.world + "..."); + ResultSet rsCol = st.executeQuery("SHOW COLUMNS FROM `" + wcfg.table + "-chestdata` LIKE 'itemtype'"); + if (!rsCol.next()) { + st.execute("ALTER TABLE `" + wcfg.table + "-chestdata` ADD COLUMN `itemtype` SMALLINT NOT NULL DEFAULT '0'"); + } + rsCol.close(); + conn.commit(); + if (wcfg.isLogging(Logging.SIGNTEXT)) { + long rowsToConvert = 0; + long done = 0; + try { + ResultSet rs = st.executeQuery("SELECT count(*) as rowcount FROM `" + wcfg.table + "-sign`"); + if (rs.next()) { + rowsToConvert = rs.getLong(1); + logblock.getLogger().info("Converting " + rowsToConvert + " entries in " + wcfg.table + "-sign"); + } + rs.close(); + + PreparedStatement insertSignState = conn.prepareStatement("INSERT INTO `" + wcfg.table + "-state` (id, replacedState, typeState) VALUES (?, ?, ?)"); + PreparedStatement deleteSign = conn.prepareStatement("DELETE FROM `" + wcfg.table + "-sign` WHERE id = ?"); + while (true) { + rs = st.executeQuery("SELECT id, signtext, replaced, type FROM `" + wcfg.table + "-sign` LEFT JOIN `" + wcfg.table + "-blocks` USING (id) ORDER BY id ASC LIMIT " + OTHER_CONVERT_BATCH_SIZE); + boolean anyRow = false; + while (rs.next()) { + anyRow = true; + long id = rs.getLong("id"); + String signText = rs.getString("signtext"); + int replaced = rs.getInt("replaced"); + boolean nullBlock = rs.wasNull(); + int type = rs.getInt("type"); + + if (!nullBlock && signText != null) { + String[] lines = signText.split("\0", 4); + byte[] bytes = Utils.serializeYamlConfiguration(signCodec.serialize(null, Side.FRONT, lines)); + + Material replacedMaterial = MaterialConverter.getBlockData(replaced, -1).getMaterial(); + Material typeMaterial = MaterialConverter.getBlockData(type, -1).getMaterial(); + boolean wasSign = replacedMaterial == Material.OAK_SIGN || replacedMaterial == Material.OAK_WALL_SIGN; + boolean isSign = typeMaterial == Material.OAK_SIGN || typeMaterial == Material.OAK_WALL_SIGN; + + insertSignState.setLong(1, id); + insertSignState.setBytes(2, wasSign ? bytes : null); + insertSignState.setBytes(3, isSign ? bytes : null); + insertSignState.addBatch(); + } + + deleteSign.setLong(1, id); + deleteSign.addBatch(); + done++; + } + rs.close(); + if (!anyRow) { + break; + } + int failedRows = 0; + try { + insertSignState.executeBatch(); + } catch (BatchUpdateException e) { + for (int result : e.getUpdateCounts()) { + if (result == Statement.EXECUTE_FAILED) { + failedRows++; + } + } + } + deleteSign.executeBatch(); + conn.commit(); + logblock.getLogger().info("Done: " + done + "/" + rowsToConvert + " " + (failedRows > 0 ? "Duplicates: " + failedRows + " " : "") + "(" + (rowsToConvert > 0 ? (done * 100 / rowsToConvert) : 100) + "%)"); + } + insertSignState.close(); + deleteSign.close(); + } catch (SQLException e) { + logblock.getLogger().info("Could not convert " + wcfg.table + "-sign: " + e.getMessage()); + } + } + } + + st.close(); + } catch (final SQLException ex) { + logblock.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); + return false; + } + + config.set("version", "1.13.1"); + } + + if (configVersion.compareTo(new ComparableVersion("1.16.0")) < 0) { + logblock.getLogger().info("Updating tables to 1.16.0 ..."); + try (Connection conn = logblock.getConnection()) { + conn.setAutoCommit(true); + final Statement st = conn.createStatement(); + for (final WorldConfig wcfg : getLoggedWorlds()) { + createIndexIfDoesNotExist(wcfg.table + "-entities", "entityid", "KEY `entityid` (entityid)", st, false); + } + st.close(); + } catch (final SQLException ex) { + logblock.getLogger().log(Level.SEVERE, "[Updater] Warning: Could not add index", ex); + } + config.set("version", "1.16.0"); + } + if (configVersion.compareTo(new ComparableVersion("1.17.0")) < 0) { + logblock.getLogger().info("Updating tables to 1.17.0 ..."); + logblock.getLogger().warning("The updating process might take several minutes if you have a huge log table! Please do not shutdown your server until it is completed."); + try (Connection conn = logblock.getConnection()) { + conn.setAutoCommit(true); + final Statement st = conn.createStatement(); + for (final WorldConfig wcfg : getLoggedWorlds()) { + st.executeUpdate("ALTER TABLE `" + wcfg.table + "-blocks` CHANGE `y` `y` SMALLINT(5) NOT NULL"); + } + st.close(); + } catch (final SQLException ex) { + logblock.getLogger().log(Level.SEVERE, "[Updater] Warning: Could not alter table", ex); + } + logblock.getLogger().info("Update to 1.17.0 completed."); + config.set("version", "1.17.0"); + } + + if (configVersion.compareTo(new ComparableVersion(Config.CURRENT_CONFIG_VERSION)) < 0) { + config.set("version", Config.CURRENT_CONFIG_VERSION); + } + + // this can always be checked + try (Connection conn = logblock.getConnection()) { + conn.setAutoCommit(true); + final Statement st = conn.createStatement(); + checkCharset("lb-players", "name", st, true); + if (isLogging(Logging.CHAT) || isLogging(Logging.PLAYER_COMMANDS) || isLogging(Logging.CONSOLE_COMMANDS) || isLogging(Logging.COMMANDBLOCK_COMMANDS)) { + checkCharset("lb-chat", "message", st, true); + } + createIndexIfDoesNotExist("lb-materials", "name", "UNIQUE KEY `name` (`name`(150))", st, true); + createIndexIfDoesNotExist("lb-blockstates", "name", "UNIQUE KEY `name` (`name`(150))", st, true); + + st.close(); + } catch (final SQLException ex) { + logblock.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); + return false; + } + try (Connection conn = logblock.getConnection()) { + conn.setAutoCommit(true); + final Statement st = conn.createStatement(); + PreparedStatement stSelectColumnType = conn.prepareStatement("SELECT `TABLE_NAME`, `COLUMN_TYPE` FROM information_schema.`COLUMNS` WHERE `TABLE_SCHEMA` = ? AND `COLUMN_NAME` = ?"); + stSelectColumnType.setString(1, Config.mysqlDatabase); + stSelectColumnType.setString(2, "y"); + HashMap tablesAndYColumnType = new HashMap<>(); + try (ResultSet rs = stSelectColumnType.executeQuery()) { + while (rs.next()) { + String table = rs.getString("TABLE_NAME").toLowerCase(); + String type = rs.getString("COLUMN_TYPE").toLowerCase(); + tablesAndYColumnType.put(table, type); + } + } + for (final WorldConfig wcfg : getLoggedWorlds()) { + String type = tablesAndYColumnType.get((wcfg.table + "-blocks").toLowerCase()); + if (type != null) { + if (type.contains("tinyint") || type.contains("unsigned")) { + logblock.getLogger().info("Fixing y column type for table " + wcfg.table + "-blocks ..."); + logblock.getLogger().warning("The updating process might take several minutes if you have a huge log table! Please do not shutdown your server until it is completed."); + st.executeUpdate("ALTER TABLE `" + wcfg.table + "-blocks` CHANGE `y` `y` SMALLINT(5) NOT NULL"); + } + } + type = tablesAndYColumnType.get((wcfg.table + "-entities").toLowerCase()); + if (type != null) { + if (type.contains("tinyint") || type.contains("unsigned")) { + logblock.getLogger().info("Fixing y column type for table " + wcfg.table + "-entities ..."); + logblock.getLogger().warning("The updating process might take several minutes if you have a huge log table! Please do not shutdown your server until it is completed."); + st.executeUpdate("ALTER TABLE `" + wcfg.table + "-entities` CHANGE `y` `y` SMALLINT(5) NOT NULL"); + } + } + type = tablesAndYColumnType.get((wcfg.table + "-kills").toLowerCase()); + if (type != null) { + if (type.contains("tinyint") || type.contains("unsigned")) { + logblock.getLogger().info("Fixing y column type for table " + wcfg.table + "-kills ..."); + logblock.getLogger().warning("The updating process might take several minutes if you have a huge log table! Please do not shutdown your server until it is completed."); + st.executeUpdate("ALTER TABLE `" + wcfg.table + "-kills` CHANGE `y` `y` SMALLINT(5) NOT NULL"); + } + } + } + st.close(); + conn.close(); + } catch (final SQLException ex) { + logblock.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); + return false; + } + + updateMaterialsPost1_13(); + + logblock.saveConfig(); + return true; + } + + void createIndexIfDoesNotExist(String table, String indexName, String definition, Statement st, boolean silent) throws SQLException { + final ResultSet rs = st.executeQuery("SHOW INDEX FROM `" + table + "` WHERE Key_name = '" + indexName + "'"); + if (!rs.next()) { + st.execute("ALTER TABLE `" + table + "` ADD " + definition); + logblock.getLogger().info("Add index " + indexName + " to table " + table + ": Table modified"); + } else if (!silent) { + logblock.getLogger().info("Add index " + indexName + " to table " + table + ": Already fine, skipping it"); + } + rs.close(); + } + + void checkCharset(String table, String column, Statement st, boolean silent) throws SQLException { + final ResultSet rs = st.executeQuery("SHOW FULL COLUMNS FROM `" + table + "` WHERE field = '" + column + "'"); + String charset = "utf8"; + if (Config.mb4) { + charset = "utf8mb4"; + } + if (rs.next() && !rs.getString("Collation").substring(0, charset.length()).equalsIgnoreCase(charset)) { + st.execute("ALTER TABLE `" + table + "` CONVERT TO CHARSET " + charset); + logblock.getLogger().info("Table " + table + " modified"); + } else if (!silent) { + logblock.getLogger().info("Table " + table + " already fine, skipping it"); + } + rs.close(); + } + + void checkTables() throws SQLException { + String charset = "utf8"; + if (Config.mb4) { + charset = "utf8mb4"; + } + final Connection conn = logblock.getConnection(); + if (conn == null) { + throw new SQLException("No connection"); + } + final Statement state = conn.createStatement(); + conn.setAutoCommit(true); + createTable(state, "lb-players", "(playerid INT UNSIGNED NOT NULL AUTO_INCREMENT, UUID varchar(36) NOT NULL, playername varchar(32) NOT NULL, firstlogin DATETIME NOT NULL, lastlogin DATETIME NOT NULL, onlinetime INT UNSIGNED NOT NULL, ip varchar(255) NOT NULL, PRIMARY KEY (playerid), INDEX (UUID), INDEX (playername)) DEFAULT CHARSET " + charset); + // Players table must not be empty or inserts won't work - bug #492 + final ResultSet rs = state.executeQuery("SELECT NULL FROM `lb-players` LIMIT 1;"); + if (!rs.next()) { + state.execute("INSERT IGNORE INTO `lb-players` (UUID,playername) VALUES ('log_dummy_record','dummy_record')"); + } + if (isLogging(Logging.CHAT) || isLogging(Logging.PLAYER_COMMANDS) || isLogging(Logging.CONSOLE_COMMANDS) || isLogging(Logging.COMMANDBLOCK_COMMANDS)) { + try { + createTable(state, "lb-chat", "(id INT UNSIGNED NOT NULL AUTO_INCREMENT, date DATETIME NOT NULL, playerid INT UNSIGNED NOT NULL, message VARCHAR(256) NOT NULL, PRIMARY KEY (id), KEY playerid (playerid), FULLTEXT message (message)) DEFAULT CHARSET " + charset); + } catch (SQLException e) { + createTable(state, "lb-chat", "(id INT UNSIGNED NOT NULL AUTO_INCREMENT, date DATETIME NOT NULL, playerid INT UNSIGNED NOT NULL, message VARCHAR(256) NOT NULL, PRIMARY KEY (id), KEY playerid (playerid)) DEFAULT CHARSET " + charset); + } + } + createTable(state, "lb-materials", "(id INT UNSIGNED NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY (id)) DEFAULT CHARSET " + charset); + createTable(state, "lb-blockstates", "(id INT UNSIGNED NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY (id)) DEFAULT CHARSET " + charset); + createTable(state, "lb-entitytypes", "(id INT UNSIGNED NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY (id)) DEFAULT CHARSET " + charset); + + for (final WorldConfig wcfg : getLoggedWorlds()) { + createTable(state, wcfg.table + "-blocks", "(id INT UNSIGNED NOT NULL AUTO_INCREMENT, date DATETIME NOT NULL, playerid INT UNSIGNED NOT NULL, replaced SMALLINT UNSIGNED NOT NULL, replacedData SMALLINT NOT NULL, type SMALLINT UNSIGNED NOT NULL, typeData SMALLINT NOT NULL, x MEDIUMINT NOT NULL, y SMALLINT NOT NULL, z MEDIUMINT NOT NULL, PRIMARY KEY (id), KEY coords (x, z, y), KEY date (date), KEY playerid (playerid))"); + createTable(state, wcfg.table + "-chestdata", "(id INT UNSIGNED NOT NULL, item MEDIUMBLOB, itemremove TINYINT, itemtype SMALLINT NOT NULL DEFAULT '0', PRIMARY KEY (id))"); + createTable(state, wcfg.table + "-state", "(id INT UNSIGNED NOT NULL, replacedState MEDIUMBLOB NULL, typeState MEDIUMBLOB NULL, PRIMARY KEY (id))"); + if (wcfg.isLogging(Logging.KILL)) { + createTable(state, wcfg.table + "-kills", "(id INT UNSIGNED NOT NULL AUTO_INCREMENT, date DATETIME NOT NULL, killer INT UNSIGNED, victim INT UNSIGNED NOT NULL, weapon SMALLINT UNSIGNED NOT NULL, x MEDIUMINT NOT NULL, y SMALLINT NOT NULL, z MEDIUMINT NOT NULL, PRIMARY KEY (id))"); + } + createTable(state, wcfg.table + "-entityids", "(entityid INT UNSIGNED NOT NULL AUTO_INCREMENT, entityuuid VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, PRIMARY KEY (entityid), UNIQUE KEY (entityuuid))"); + createTable(state, wcfg.table + "-entities", "(id INT UNSIGNED NOT NULL AUTO_INCREMENT, date DATETIME NOT NULL, playerid INT UNSIGNED NOT NULL, entityid INT UNSIGNED NOT NULL, entitytypeid INT UNSIGNED NOT NULL, x MEDIUMINT NOT NULL, y SMALLINT NOT NULL, z MEDIUMINT NOT NULL, action TINYINT UNSIGNED NOT NULL, data MEDIUMBLOB NULL, PRIMARY KEY (id), KEY coords (x, z, y), KEY date (date), KEY playerid (playerid), KEY entityid (entityid))"); + } + state.close(); + conn.close(); + } + + private void createTable(Statement state, String table, String query) throws SQLException { + try (ResultSet tableResult = state.executeQuery("SHOW TABLES LIKE '" + table + "'")) { + if (!tableResult.next()) { + logblock.getLogger().log(Level.INFO, "Creating table " + table + "."); + state.execute("CREATE TABLE `" + table + "` " + query); + try (ResultSet tableResultNew = state.executeQuery("SHOW TABLES LIKE '" + table + "'")) { + if (!tableResultNew.next()) { + throw new SQLException("Table " + table + " not found and failed to create"); + } + } + } + } + } + + /** + * Update materials that were renamed + */ + private void updateMaterialsPost1_13() { + final ConfigurationSection config = logblock.getConfig(); + String previousMinecraftVersion = config.getString("previousMinecraftVersion"); + if (previousMinecraftVersion == null) { + previousMinecraftVersion = "1.13"; + } + ComparableVersion comparablePreviousMinecraftVersion = new ComparableVersion(previousMinecraftVersion); + String currentMinecraftVersion = logblock.getServer().getVersion(); + currentMinecraftVersion = currentMinecraftVersion.substring(currentMinecraftVersion.indexOf("(MC: ") + 5); + int currentVersionEnd = currentMinecraftVersion.indexOf(" "); + int currentVersionEnd2 = currentMinecraftVersion.indexOf(")"); + if (currentVersionEnd2 >= 0 && (currentVersionEnd < 0 || currentVersionEnd2 < currentVersionEnd)) { + currentVersionEnd = currentVersionEnd2; + } + currentMinecraftVersion = currentMinecraftVersion.substring(0, currentVersionEnd); + logblock.getLogger().info("[Updater] Current Minecraft Version: '" + currentMinecraftVersion + "'"); + ComparableVersion comparableCurrentMinecraftVersion = new ComparableVersion(currentMinecraftVersion); + + if (comparablePreviousMinecraftVersion.compareTo("1.14") < 0 && comparableCurrentMinecraftVersion.compareTo("1.14") >= 0) { + logblock.getLogger().info("[Updater] Upgrading Materials to 1.14"); + renameMaterial("minecraft:sign", Material.OAK_SIGN); + renameMaterial("minecraft:wall_sign", Material.OAK_WALL_SIGN); + renameMaterial("minecraft:stone_slab", Material.SMOOTH_STONE_SLAB); + renameMaterial("minecraft:rose_red", Material.RED_DYE); + renameMaterial("minecraft:dandelion_yellow", Material.YELLOW_DYE); + renameMaterial("minecraft:cactus_green", Material.GREEN_DYE); + } + + if (comparablePreviousMinecraftVersion.compareTo("1.17") < 0 && comparableCurrentMinecraftVersion.compareTo("1.17") >= 0) { + logblock.getLogger().info("[Updater] Upgrading Materials to 1.17"); + renameMaterial("minecraft:grass_path", Material.DIRT_PATH); + } + + if (comparablePreviousMinecraftVersion.compareTo("1.20.4") < 0 && comparableCurrentMinecraftVersion.compareTo("1.20.4") >= 0) { + logblock.getLogger().info("[Updater] Upgrading Materials to 1.20.4"); + renameMaterial("minecraft:grass", Material.SHORT_GRASS); + } + + config.set("previousMinecraftVersion", currentMinecraftVersion); + logblock.saveConfig(); + } + + private void renameMaterial(String oldName, Material newName) { + final Connection conn = logblock.getConnection(); + try { + conn.setAutoCommit(false); + PreparedStatement stSelectMaterial = conn.prepareStatement("SELECT id FROM `lb-materials` WHERE name = ?"); + stSelectMaterial.setString(1, oldName); + ResultSet rs = stSelectMaterial.executeQuery(); + if (rs.next()) { + logblock.getLogger().info("[Updater] Updating " + oldName + " to " + newName); + int oldId = rs.getInt(1); + int newId = MaterialConverter.getOrAddMaterialId(newName); + + Statement st = conn.createStatement(); + int rows = 0; + for (final WorldConfig wcfg : getLoggedWorlds()) { + rows += st.executeUpdate("UPDATE `" + wcfg.table + "-blocks` SET replaced = " + newId + " WHERE replaced = " + oldId); + rows += st.executeUpdate("UPDATE `" + wcfg.table + "-blocks` SET type = " + newId + " WHERE type = " + oldId); + rows += st.executeUpdate("UPDATE `" + wcfg.table + "-chestdata` SET itemtype = " + newId + " WHERE itemtype = " + oldId); + if (wcfg.isLogging(Logging.KILL)) { + rows += st.executeUpdate("UPDATE `" + wcfg.table + "-kills` SET weapon = " + newId + " WHERE weapon = " + oldId); + } + } + st.close(); + if (rows > 0) { + logblock.getLogger().info("[Updater] Successfully updated " + rows + " entries.."); + } + } + stSelectMaterial.close(); + conn.commit(); + conn.close(); + } catch (final SQLException ex) { + logblock.getLogger().log(Level.SEVERE, "[Updater] Error: " + ex.getMessage(), ex); + } + } + + public static class PlayerCountChecker implements Runnable { + + private LogBlock logblock; + + public PlayerCountChecker(LogBlock logblock) { + this.logblock = logblock; + } + + @Override + public void run() { + final Connection conn = logblock.getConnection(); + try { + conn.setAutoCommit(true); + final Statement st = conn.createStatement(); + ResultSet rs = st.executeQuery("SELECT auto_increment FROM information_schema.columns AS col join information_schema.tables AS tab ON (col.table_schema=tab.table_schema AND col.table_name=tab.table_name) WHERE col.table_name = 'lb-players' AND col.column_name = 'playerid' AND col.data_type = 'smallint' AND col.table_schema = DATABASE() AND auto_increment > 65000;"); + if (rs.next()) { + for (int i = 0; i < 6; i++) { + logblock.getLogger().warning("Your server reached 65000 players. You should soon update your database table schema - see FAQ: https://github.com/LogBlock/LogBlock/wiki/FAQ#logblock-your-server-reached-65000-players-"); + } + } + st.close(); + conn.close(); + } catch (final SQLException ex) { + if (logblock.isCompletelyEnabled()) { + logblock.getLogger().log(Level.SEVERE, "[Updater] Error: ", ex); + } + } + } + } + + public static class MaterialUpdater1_13 { + BlockData[][] blockDataMapping; + Material[][] itemMapping = new Material[10][]; + + public MaterialUpdater1_13(LogBlock plugin) throws IOException { + blockDataMapping = new BlockData[256][16]; + try (JarFile file = new JarFile(plugin.getFile())) { + BufferedReader reader = new BufferedReader(new InputStreamReader(new BufferedInputStream(file.getInputStream(file.getJarEntry("blockdata.txt"))), "UTF-8")); + while (true) { + String line = reader.readLine(); + if (line == null) { + break; + } + int splitter1 = line.indexOf(":"); + int splitter2 = line.indexOf(","); + if (splitter1 >= 0 && splitter2 >= 0) { + int blockid = Integer.parseInt(line.substring(0, splitter1)); + int blockdata = Integer.parseInt(line.substring(splitter1 + 1, splitter2)); + BlockData newBlockData = Bukkit.createBlockData(line.substring(splitter2 + 1)); + + if (blockdata == 0) { + for (int i = 0; i < 16; i++) { + if (blockDataMapping[blockid][i] == null) { + blockDataMapping[blockid][i] = newBlockData; + } + } + } else { + blockDataMapping[blockid][blockdata] = newBlockData; + } + } + } + reader.close(); + + HashMap materialKeysToMaterial = new HashMap<>(); + for (Material material : Material.values()) { + materialKeysToMaterial.put(material.getKey().toString(), material); + } + + reader = new BufferedReader(new InputStreamReader(new BufferedInputStream(file.getInputStream(file.getJarEntry("itemdata.txt"))), "UTF-8")); + while (true) { + String line = reader.readLine(); + if (line == null) { + break; + } + int splitter1 = line.indexOf(":"); + int splitter2 = line.indexOf(","); + if (splitter1 >= 0 && splitter2 >= 0) { + int itemid = Integer.parseInt(line.substring(0, splitter1)); + int itemdata = Integer.parseInt(line.substring(splitter1 + 1, splitter2)); + Material newMaterial = materialKeysToMaterial.get(line.substring(splitter2 + 1)); + if (newMaterial == null) { + throw new IOException("Unknown item: " + line.substring(splitter2 + 1)); + } + if (itemid >= itemMapping.length) { + itemMapping = Arrays.copyOf(itemMapping, Math.max(itemMapping.length * 3 / 2, itemid + 1)); + } + + Material[] itemValues = itemMapping[itemid]; + if (itemValues == null) { + itemValues = new Material[itemdata + 1]; + itemMapping[itemid] = itemValues; + } else if (itemValues.length <= itemdata) { + itemValues = Arrays.copyOf(itemValues, itemdata + 1); + itemMapping[itemid] = itemValues; + } + itemValues[itemdata] = newMaterial; + } + } + reader.close(); + } + } + + public BlockData getBlockData(int id, int data) { + return id >= 0 && id < 256 && data >= 0 && data < 16 ? blockDataMapping[id][data] : null; + } + + public Material getMaterial(int id, int data) { + Material[] materials = id >= 0 && id < itemMapping.length ? itemMapping[id] : null; + if (materials != null && materials.length > 0) { + if (materials[0] != null && materials[0].getMaxDurability() == 0 && data >= 0 && data < materials.length && materials[data] != null) { + return materials[data]; + } + return materials[0]; + } + return null; + } + + public Material getMaterial(String id) { + int item = 0; + int data = 0; + int seperator = id.indexOf(':'); + if (seperator < 0) { + item = Integer.parseInt(id); + } else { + item = Integer.parseInt(id.substring(0, seperator)); + data = Integer.parseInt(id.substring(seperator + 1)); + } + return getMaterial(item, data); + } + + public String convertMaterial(String oldEntry) { + if (oldEntry == null) { + return null; + } + try { + Material newMaterial = getMaterial(oldEntry); + if (newMaterial != null) { + return newMaterial.name(); + } + } catch (Exception e) { + Material newMaterial = Material.matchMaterial(oldEntry, true); + if (newMaterial != null) { + return newMaterial.name(); + } else { + newMaterial = Material.matchMaterial(oldEntry); + if (newMaterial != null) { + return newMaterial.name(); + } + } + } + return null; + } + + public List convertMaterials(Collection oldEntries) { + Set newEntries = new LinkedHashSet<>(); + for (String oldEntry : oldEntries) { + String newEntry = convertMaterial(oldEntry); + if (newEntry != null) { + newEntries.add(newEntry); + if (newEntry.equals(Material.AIR.name())) { + newEntries.add(Material.CAVE_AIR.name()); + newEntries.add(Material.VOID_AIR.name()); + } + } + } + return new ArrayList<>(newEntries); + } + } +} diff --git a/src/main/java/de/diddiz/LogBlock/WorldEditor.java b/src/main/java/de/diddiz/LogBlock/WorldEditor.java index 1c54adc3..8bb41a2c 100644 --- a/src/main/java/de/diddiz/LogBlock/WorldEditor.java +++ b/src/main/java/de/diddiz/LogBlock/WorldEditor.java @@ -1,269 +1,500 @@ -package de.diddiz.LogBlock; - -import org.bukkit.ChatColor; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.World; -import org.bukkit.block.Block; -import org.bukkit.block.BlockFace; -import org.bukkit.block.BlockState; -import org.bukkit.block.Sign; -import org.bukkit.command.CommandSender; -import org.bukkit.inventory.InventoryHolder; -import org.bukkit.inventory.ItemStack; -import org.bukkit.material.Bed; -import org.bukkit.material.PistonBaseMaterial; -import org.bukkit.material.PistonExtensionMaterial; - -import java.io.File; -import java.io.PrintWriter; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.List; -import java.util.Queue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.logging.Level; - -import static de.diddiz.LogBlock.config.Config.dontRollback; -import static de.diddiz.LogBlock.config.Config.replaceAnyway; -import static de.diddiz.util.BukkitUtils.*; -import static de.diddiz.util.MaterialName.materialName; -import static org.bukkit.Bukkit.getLogger; - -public class WorldEditor implements Runnable { - private final LogBlock logblock; - private final Queue edits = new LinkedBlockingQueue(); - private final World world; - - /** - * The player responsible for editing the world, used to report progress - */ - private CommandSender sender; - private int taskID; - private int successes = 0, blacklistCollisions = 0; - private long elapsedTime = 0; - public LookupCacheElement[] errors; - - public WorldEditor(LogBlock logblock, World world) { - this.logblock = logblock; - this.world = world; - } - - public int getSize() { - return edits.size(); - } - - public int getSuccesses() { - return successes; - } - - public int getErrors() { - return errors.length; - } - - public int getBlacklistCollisions() { - return blacklistCollisions; - } - - - public void setSender(CommandSender sender) { - this.sender = sender; - } - - public void queueEdit(int x, int y, int z, int replaced, int type, byte data, String signtext, short itemType, short itemAmount, short itemData) { - edits.add(new Edit(0, new Location(world, x, y, z), null, replaced, type, data, signtext, new ChestAccess(itemType, itemAmount, itemData))); - } - - public long getElapsedTime() { - return elapsedTime; - } - - synchronized public void start() throws Exception { - final long start = System.currentTimeMillis(); - taskID = logblock.getServer().getScheduler().scheduleSyncRepeatingTask(logblock, this, 0, 1); - if (taskID == -1) { - throw new Exception("Failed to schedule task"); - } - try { - this.wait(); - } catch (final InterruptedException ex) { - throw new Exception("Interrupted"); - } - elapsedTime = System.currentTimeMillis() - start; - } - - @Override - public synchronized void run() { - final List errorList = new ArrayList(); - int counter = 0; - float size = edits.size(); - while (!edits.isEmpty() && counter < 100) { - try { - switch (edits.poll().perform()) { - case SUCCESS: - successes++; - break; - case BLACKLISTED: - blacklistCollisions++; - break; - } - } catch (final WorldEditorException ex) { - errorList.add(ex); - } catch (final Exception ex) { - getLogger().log(Level.WARNING, "[WorldEditor] Exeption: ", ex); - } - counter++; - if (sender != null) { - float percentage = ((size - edits.size()) / size) * 100.0F; - if (percentage % 20 == 0) { - sender.sendMessage(ChatColor.GOLD + "[LogBlock]" + ChatColor.YELLOW + " Rollback progress: " + percentage + "%" + - " Blocks edited: " + counter); - } - } - } - if (edits.isEmpty()) { - logblock.getServer().getScheduler().cancelTask(taskID); - if (errorList.size() > 0) { - try { - final File file = new File("plugins/LogBlock/error/WorldEditor-" + new SimpleDateFormat("yy-MM-dd-HH-mm-ss").format(System.currentTimeMillis()) + ".log"); - file.getParentFile().mkdirs(); - final PrintWriter writer = new PrintWriter(file); - for (final LookupCacheElement err : errorList) { - writer.println(err.getMessage()); - } - writer.close(); - } catch (final Exception ex) { - } - } - errors = errorList.toArray(new WorldEditorException[errorList.size()]); - notify(); - } - } - - private static enum PerformResult { - SUCCESS, BLACKLISTED, NO_ACTION - } - - private class Edit extends BlockChange { - public Edit(long time, Location loc, Actor actor, int replaced, int type, byte data, String signtext, ChestAccess ca) { - super(time, loc, actor, replaced, type, data, signtext, ca); - } - - PerformResult perform() throws WorldEditorException { - if (dontRollback.contains(replaced)) { - return PerformResult.BLACKLISTED; - } - final Block block = loc.getBlock(); - if (replaced == 0 && block.getTypeId() == 0) { - return PerformResult.NO_ACTION; - } - final BlockState state = block.getState(); - if (!world.isChunkLoaded(block.getChunk())) { - world.loadChunk(block.getChunk()); - } - if (type == replaced) { - if (type == 0) { - if (!block.setTypeId(0)) { - throw new WorldEditorException(block.getTypeId(), 0, block.getLocation()); - } - } else if (ca != null) { - if (getContainerBlocks().contains(Material.getMaterial(type))) { - int leftover; - try { - leftover = modifyContainer(state, new ItemStack(ca.itemType, -ca.itemAmount, ca.itemData)); - // Special-case blocks which might be double chests - if (leftover > 0 && (type == 54 || type == 146)) { - for (final BlockFace face : new BlockFace[]{BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST}) { - if (block.getRelative(face).getTypeId() == type) { - leftover = modifyContainer(block.getRelative(face).getState(), new ItemStack(ca.itemType, ca.itemAmount < 0 ? leftover : -leftover, ca.itemData)); - } - } - } - } catch (final Exception ex) { - throw new WorldEditorException(ex.getMessage(), block.getLocation()); - } - if (!state.update()) { - throw new WorldEditorException("Failed to update inventory of " + materialName(block.getTypeId()), block.getLocation()); - } - if (leftover > 0 && ca.itemAmount < 0) { - throw new WorldEditorException("Not enough space left in " + materialName(block.getTypeId()), block.getLocation()); - } - } - } else { - return PerformResult.NO_ACTION; - } - return PerformResult.SUCCESS; - } - if (!(equalTypes(block.getTypeId(), type) || replaceAnyway.contains(block.getTypeId()))) { - return PerformResult.NO_ACTION; - } - if (state instanceof InventoryHolder) { - ((InventoryHolder) state).getInventory().clear(); - state.update(); - } - if (block.getTypeId() == replaced) { - if (block.getData() != (type == 0 ? data : (byte) 0)) { - block.setData(type == 0 ? data : (byte) 0, true); - } else { - return PerformResult.NO_ACTION; - } - } else if (!block.setTypeIdAndData(replaced, type == 0 ? data : (byte) 0, true)) { - throw new WorldEditorException(block.getTypeId(), replaced, block.getLocation()); - } - final int curtype = block.getTypeId(); - if (signtext != null && (curtype == 63 || curtype == 68)) { - final Sign sign = (Sign) block.getState(); - final String[] lines = signtext.split("\0", 4); - if (lines.length < 4) { - return PerformResult.NO_ACTION; - } - for (int i = 0; i < 4; i++) { - sign.setLine(i, lines[i]); - } - if (!sign.update()) { - throw new WorldEditorException("Failed to update signtext of " + materialName(block.getTypeId()), block.getLocation()); - } - } else if (curtype == 26) { - final Bed bed = (Bed) block.getState().getData(); - final Block secBlock = bed.isHeadOfBed() ? block.getRelative(bed.getFacing().getOppositeFace()) : block.getRelative(bed.getFacing()); - if (secBlock.getTypeId() == 0 && !secBlock.setTypeIdAndData(26, (byte) (bed.getData() | 8), true)) { - throw new WorldEditorException(secBlock.getTypeId(), 26, secBlock.getLocation()); - } - } else if ((curtype == 29 || curtype == 33) && (block.getData() & 8) > 0) { - final PistonBaseMaterial piston = (PistonBaseMaterial) block.getState().getData(); - final Block secBlock = block.getRelative(piston.getFacing()); - if (secBlock.getTypeId() == 0 && !secBlock.setTypeIdAndData(34, curtype == 29 ? (byte) (block.getData() | 8) : (byte) (block.getData() & ~8), true)) { - throw new WorldEditorException(secBlock.getTypeId(), 34, secBlock.getLocation()); - } - } else if (curtype == 34) { - final PistonExtensionMaterial piston = (PistonExtensionMaterial) block.getState().getData(); - final Block secBlock = block.getRelative(piston.getFacing().getOppositeFace()); - if (secBlock.getTypeId() == 0 && !secBlock.setTypeIdAndData(piston.isSticky() ? 29 : 33, (byte) (block.getData() | 8), true)) { - throw new WorldEditorException(secBlock.getTypeId(), piston.isSticky() ? 29 : 33, secBlock.getLocation()); - } - } else if (curtype == 18 && (block.getData() & 8) > 0) { - block.setData((byte) (block.getData() & 0xF7)); - } - return PerformResult.SUCCESS; - } - } - - @SuppressWarnings("serial") - public static class WorldEditorException extends Exception implements LookupCacheElement { - private final Location loc; - - public WorldEditorException(int typeBefore, int typeAfter, Location loc) { - this("Failed to replace " + materialName(typeBefore) + " with " + materialName(typeAfter), loc); - } - - public WorldEditorException(String msg, Location loc) { - super(msg + " at " + loc.getWorld().getName() + ":" + loc.getBlockX() + ":" + loc.getBlockY() + ":" + loc.getBlockZ()); - this.loc = loc; - } - - @Override - public Location getLocation() { - return loc; - } - } -} +package de.diddiz.LogBlock; + +import org.bukkit.ChatColor; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.BlockState; +import org.bukkit.block.Container; +import org.bukkit.block.data.Bisected.Half; +import org.bukkit.block.data.Bisected; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.type.Bed; +import org.bukkit.block.data.type.Chest; +import org.bukkit.block.data.type.Bed.Part; +import org.bukkit.block.data.type.Piston; +import org.bukkit.block.data.type.PistonHead; +import org.bukkit.block.data.type.TechnicalPiston.Type; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.ArmorStand; +import org.bukkit.entity.Bee; +import org.bukkit.entity.Entity; +import org.bukkit.entity.ItemFrame; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.ItemStack; + +import de.diddiz.LogBlock.QueryParams.Order; +import de.diddiz.LogBlock.blockstate.BlockStateCodecs; +import de.diddiz.LogBlock.util.BukkitUtils; +import de.diddiz.LogBlock.util.Utils; +import de.diddiz.LogBlock.worldedit.WorldEditHelper; +import java.io.File; +import java.io.PrintWriter; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.text.NumberFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; +import java.util.logging.Level; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.TextComponent; + +import static de.diddiz.LogBlock.config.Config.dontRollback; +import static de.diddiz.LogBlock.config.Config.replaceAnyway; +import static de.diddiz.LogBlock.util.BukkitUtils.*; + +public class WorldEditor implements Runnable { + private final LogBlock logblock; + private final ArrayList edits = new ArrayList<>(); + private int rowsCompleted; + private int totalRows; + private final World world; + + /** + * The player responsible for editing the world, used to report progress + */ + private CommandSender sender; + private int taskID; + private int successes = 0, blacklistCollisions = 0; + private long elapsedTime = 0; + public LookupCacheElement[] errors; + private boolean forceReplace; + private HashMap uuidReplacements = new HashMap<>(); + private boolean started = false; + + public WorldEditor(LogBlock logblock, World world) { + this(logblock, world, false); + } + + public WorldEditor(LogBlock logblock, World world, boolean forceReplace) { + this.logblock = logblock; + this.world = world; + this.forceReplace = forceReplace; + } + + public int getSize() { + return edits.size(); + } + + public int getSuccesses() { + return successes; + } + + public int getErrors() { + return errors.length; + } + + public int getBlacklistCollisions() { + return blacklistCollisions; + } + + public void setSender(CommandSender sender) { + this.sender = sender; + } + + public void queueBlockEdit(long time, int x, int y, int z, int replaced, int replaceData, byte[] replacedState, int type, int typeData, byte[] typeState, ChestAccess item) { + if (started) { + throw new IllegalStateException("Already started"); + } + edits.add(new BlockEdit(time, new Location(world, x, y, z), null, replaced, replaceData, replacedState, type, typeData, typeState, item)); + } + + public void queueEntityEdit(ResultSet rs, QueryParams p, boolean rollback) throws SQLException { + if (started) { + throw new IllegalStateException("Already started"); + } + edits.add(new EntityEdit(rs, p, rollback)); + } + + public void reverseRowOrder() { + if (started) { + throw new IllegalStateException("Already started"); + } + Collections.reverse(edits); + } + + public void sortRows(QueryParams.Order order) { + if (started) { + throw new IllegalStateException("Already started"); + } + edits.sort(new EditComparator(order)); + } + + public long getElapsedTime() { + return elapsedTime; + } + + synchronized public void start() throws Exception { + if (started) { + throw new IllegalStateException("Already started"); + } + started = true; + final long start = System.currentTimeMillis(); + totalRows = edits.size(); + taskID = logblock.getServer().getScheduler().scheduleSyncRepeatingTask(logblock, this, 0, 1); + if (taskID == -1) { + throw new Exception("Failed to schedule task"); + } + try { + this.wait(); + } catch (final InterruptedException ex) { + throw new Exception("Interrupted"); + } + elapsedTime = System.currentTimeMillis() - start; + } + + @Override + public synchronized void run() { + final List errorList = new ArrayList<>(); + int counter = 0; + long t0 = System.nanoTime(); + long maxEditTime = 5_000_000; // 5 ms + while (!edits.isEmpty() && counter < 10000 && (counter < 100 || counter % 10 != 0 || System.nanoTime() - t0 < maxEditTime)) { + try { + switch (edits.remove(edits.size() - 1).perform()) { + case SUCCESS: + successes++; + break; + case BLACKLISTED: + blacklistCollisions++; + break; + case NO_ACTION: + break; + } + } catch (final WorldEditorException ex) { + errorList.add(ex); + } catch (final Exception ex) { + logblock.getLogger().log(Level.WARNING, "[WorldEditor] Exeption: ", ex); + } + rowsCompleted++; + counter++; + if (sender != null) { + float percentage = rowsCompleted * 100.0f / totalRows; + if (rowsCompleted % 10000 == 0) { + sender.sendMessage(ChatColor.GOLD + "[LogBlock]" + ChatColor.YELLOW + " Rollback progress: " + NumberFormat.getNumberInstance().format(percentage) + "%" + + " Blocks edited: " + counter); + } + } + } + if (edits.isEmpty()) { + logblock.getServer().getScheduler().cancelTask(taskID); + if (errorList.size() > 0) { + try { + final File errorDir = new File(logblock.getDataFolder(), "error"); + errorDir.mkdirs(); + final File file = new File(errorDir, "WorldEditor-" + new SimpleDateFormat("yy-MM-dd-HH-mm-ss").format(System.currentTimeMillis()) + ".log"); + final PrintWriter writer = new PrintWriter(file); + for (final WorldEditorException err : errorList) { + writer.println(BaseComponent.toPlainText(err.getLogMessage())); + err.printStackTrace(writer); + writer.println(); + writer.println(); + } + writer.close(); + } catch (final Exception ex) { + } + } + errors = errorList.toArray(new WorldEditorException[errorList.size()]); + notify(); + } + } + + 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 + } + + public interface Edit { + PerformResult perform() throws WorldEditorException; + + public long getTime(); + } + + public class EntityEdit extends EntityChange implements Edit { + private boolean rollback; + + public EntityEdit(ResultSet rs, QueryParams p, boolean rollback) throws SQLException { + super(rs, p); + this.rollback = rollback; + } + + @Override + public long getTime() { + return date; + } + + @Override + public PerformResult perform() throws WorldEditorException { + if (type == null) { + throw new WorldEditorException("Unkown entity type for entity " + entityUUID, loc); + } + 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"); + 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 = BukkitUtils.loadEntityAround(location.getChunk(), uuid); + if (existing != null) { + return PerformResult.NO_ACTION; + } + byte[] serializedWorldEditEntity = (byte[]) deserialized.get("worldedit"); + if (serializedWorldEditEntity != null) { + 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 = BukkitUtils.loadEntityAround(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 + } else if (changeType == (rollback ? EntityChangeType.REMOVEEQUIP : EntityChangeType.ADDEQUIP)) { + // set equip + UUID uuid = getReplacedUUID(entityId, entityUUID); + Entity existing = BukkitUtils.loadEntityAround(loc.getChunk(), uuid); + if (existing != null) { + YamlConfiguration deserialized = Utils.deserializeYamlConfiguration(data); + ItemStack item = deserialized.getItemStack("item"); + if (item != null && existing instanceof ItemFrame) { + ItemStack old = ((ItemFrame) existing).getItem(); + if (old == null || old.getType() == Material.AIR) { + ((ItemFrame) existing).setItem(item); + return PerformResult.SUCCESS; + } + } else if (item != null && existing instanceof ArmorStand) { + EquipmentSlot slot = EquipmentSlot.valueOf(deserialized.getString("slot")); + ArmorStand stand = (ArmorStand) existing; + ItemStack old = BukkitUtils.getItemInSlot(stand, slot); + if (old == null || old.getType() == Material.AIR) { + BukkitUtils.setItemInSlot(stand, slot, item); + return PerformResult.SUCCESS; + } + } + } + return PerformResult.NO_ACTION; // the entity is not there, or equip does not match + } else if (changeType == (rollback ? EntityChangeType.ADDEQUIP : EntityChangeType.REMOVEEQUIP)) { + // remove equip + UUID uuid = getReplacedUUID(entityId, entityUUID); + Entity existing = BukkitUtils.loadEntityAround(loc.getChunk(), uuid); + if (existing != null) { + YamlConfiguration deserialized = Utils.deserializeYamlConfiguration(data); + ItemStack item = deserialized.getItemStack("item"); + if (item != null && existing instanceof ItemFrame) { + ItemStack old = ((ItemFrame) existing).getItem(); + if (old != null && old.isSimilar(item)) { + ((ItemFrame) existing).setItem(null); + return PerformResult.SUCCESS; + } + } else if (item != null && existing instanceof ArmorStand) { + EquipmentSlot slot = EquipmentSlot.valueOf(deserialized.getString("slot")); + ArmorStand stand = (ArmorStand) existing; + ItemStack old = BukkitUtils.getItemInSlot(stand, slot); + if (old != null && old.isSimilar(item)) { + BukkitUtils.setItemInSlot(stand, slot, null); + return PerformResult.SUCCESS; + } + } + } + return PerformResult.NO_ACTION; // the entity is not there, or equip does not match + } else if (changeType == EntityChangeType.GET_STUNG) { + UUID uuid = getReplacedUUID(entityId, entityUUID); + Entity existing = BukkitUtils.loadEntityAround(loc.getChunk(), uuid); + if (existing != null && existing instanceof Bee) { + ((Bee) existing).setHasStung(!rollback); + } + } + return PerformResult.NO_ACTION; + } + } + + public class BlockEdit extends BlockChange implements Edit { + public BlockEdit(long time, Location loc, Actor actor, int replaced, int replaceData, byte[] replacedState, int type, int typeData, byte[] typeState, ChestAccess ca) { + super(time, loc, actor, replaced, replaceData, replacedState, type, typeData, typeState, ca); + } + + @Override + public long getTime() { + return date; + } + + @Override + public PerformResult perform() throws WorldEditorException { + BlockData replacedBlock = getBlockReplaced(); + BlockData setBlock = getBlockSet(); + if (replacedBlock == null || setBlock == null) { + throw new WorldEditorException("Could not parse the material", loc.clone()); + } + // action: set to replaced + + if (dontRollback.contains(replacedBlock.getMaterial())) { + return PerformResult.BLACKLISTED; + } + final Block block = loc.getBlock(); + if (BukkitUtils.isEmpty(replacedBlock.getMaterial()) && BukkitUtils.isEmpty(block.getType())) { + return PerformResult.NO_ACTION; + } + BlockState state = block.getState(); + if (setBlock.equals(replacedBlock)) { + if (ca != null) { + if (state instanceof InventoryHolder && state.getType() == replacedBlock.getMaterial()) { + int leftover; + try { + leftover = modifyContainer(state, ca.itemStack, !ca.remove); + } catch (final Exception ex) { + throw new WorldEditorException(ex.getMessage(), block.getLocation()); + } + if (leftover > 0 && ca.remove) { + throw new WorldEditorException("Not enough space left in " + block.getType(), block.getLocation()); + } + return PerformResult.SUCCESS; + } + return PerformResult.NO_ACTION; + } + } + if (!forceReplace && !BukkitUtils.isSimilarForRollback(setBlock.getMaterial(), block.getType()) && !block.isEmpty() && !replaceAnyway.contains(block.getType())) { + return PerformResult.NO_ACTION; + } + if (state instanceof Container && replacedBlock.getMaterial() != block.getType()) { + ((Container) state).getSnapshotInventory().clear(); + state.update(); + } + block.setBlockData(replacedBlock); + BlockData newData = block.getBlockData(); + if (BlockStateCodecs.hasCodec(replacedBlock.getMaterial())) { + state = block.getState(); + try { + BlockStateCodecs.deserialize(state, Utils.deserializeYamlConfiguration(replacedState)); + state.update(); + } catch (Exception e) { + throw new WorldEditorException("Failed to restore blockstate of " + block.getType() + ": " + e, block.getLocation()); + } + } + + final Material curtype = block.getType(); + if (newData instanceof Bed) { + final Bed bed = (Bed) newData; + final Block secBlock = bed.getPart() == Part.HEAD ? block.getRelative(bed.getFacing().getOppositeFace()) : block.getRelative(bed.getFacing()); + if (secBlock.isEmpty()) { + Bed bed2 = (Bed) bed.clone(); + bed2.setPart(bed.getPart() == Part.HEAD ? Part.FOOT : Part.HEAD); + secBlock.setBlockData(bed2); + } + } else if (curtype == Material.IRON_DOOR || BukkitUtils.isWoodenDoor(curtype) || BukkitUtils.isDoublePlant(curtype)) { + final Bisected firstPart = (Bisected) newData; + final Block secBlock = block.getRelative(firstPart.getHalf() == Half.TOP ? BlockFace.DOWN : BlockFace.UP); + if (secBlock.isEmpty()) { + Bisected secondPart = (Bisected) firstPart.clone(); + secondPart.setHalf(firstPart.getHalf() == Half.TOP ? Half.BOTTOM : Half.TOP); + secBlock.setBlockData(secondPart); + } + } else if ((curtype == Material.PISTON || curtype == Material.STICKY_PISTON)) { + Piston piston = (Piston) newData; + if (piston.isExtended()) { + final Block secBlock = block.getRelative(piston.getFacing()); + if (secBlock.isEmpty()) { + PistonHead head = (PistonHead) Material.PISTON_HEAD.createBlockData(); + head.setFacing(piston.getFacing()); + head.setType(curtype == Material.PISTON ? Type.NORMAL : Type.STICKY); + secBlock.setBlockData(head); + } + } + } else if (curtype == Material.PISTON_HEAD) { + PistonHead head = (PistonHead) newData; + final Block secBlock = block.getRelative(head.getFacing().getOppositeFace()); + if (secBlock.isEmpty()) { + Piston piston = (Piston) (head.getType() == Type.NORMAL ? Material.PISTON : Material.STICKY_PISTON).createBlockData(); + piston.setFacing(head.getFacing()); + piston.setExtended(true); + secBlock.setBlockData(piston); + } + } else if (newData instanceof Chest) { + Chest chest = (Chest) newData; + if (chest.getType() != org.bukkit.block.data.type.Chest.Type.SINGLE) { + if (getConnectedChest(block) == null) { + chest.setType(org.bukkit.block.data.type.Chest.Type.SINGLE); + block.setBlockData(chest); + } + } + } + return PerformResult.SUCCESS; + } + } + + public static class EditComparator implements Comparator { + private final int mult; + + public EditComparator(QueryParams.Order order) { + mult = order == Order.DESC ? 1 : -1; + } + + @Override + public int compare(Edit edit1, Edit edit2) { + long time1 = edit1.getTime(); + long time2 = edit2.getTime(); + return time1 > time2 ? mult : time1 < time2 ? -mult : 0; + } + } + + @SuppressWarnings("serial") + public static class WorldEditorException extends Exception implements LookupCacheElement { + private final Location loc; + + public WorldEditorException(Material typeBefore, Material typeAfter, Location loc) { + this("Failed to replace " + typeBefore.name() + " with " + typeAfter.name(), loc); + } + + public WorldEditorException(String msg, Location loc) { + super(msg + " at " + loc.getWorld().getName() + ":" + loc.getBlockX() + ":" + loc.getBlockY() + ":" + loc.getBlockZ()); + this.loc = loc; + } + + @Override + public Location getLocation() { + return loc; + } + + @Override + public BaseComponent getLogMessage(int entry) { + return TextComponent.fromLegacy(getMessage()); + } + } +} 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 00000000..cc2b6d1d --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/WorldEditorEditFactory.java @@ -0,0 +1,37 @@ +package de.diddiz.LogBlock; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import de.diddiz.LogBlock.QueryParams.BlockChangeType; +import de.diddiz.LogBlock.util.ItemStackAndAmount; +import de.diddiz.LogBlock.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; + ItemStackAndAmount 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.getTimestamp("date").getTime(), 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.getTimestamp("date").getTime(), 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/addons/worldguard/WorldGuardLoggingFlagsAddon.java b/src/main/java/de/diddiz/LogBlock/addons/worldguard/WorldGuardLoggingFlagsAddon.java new file mode 100644 index 00000000..92f726c3 --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/addons/worldguard/WorldGuardLoggingFlagsAddon.java @@ -0,0 +1,80 @@ +package de.diddiz.LogBlock.addons.worldguard; + +import com.sk89q.worldedit.bukkit.BukkitAdapter; +import com.sk89q.worldguard.WorldGuard; +import com.sk89q.worldguard.bukkit.WorldGuardPlugin; +import com.sk89q.worldguard.protection.ApplicableRegionSet; +import com.sk89q.worldguard.protection.association.RegionAssociable; +import com.sk89q.worldguard.protection.flags.StateFlag; +import com.sk89q.worldguard.protection.flags.registry.FlagConflictException; +import com.sk89q.worldguard.protection.flags.registry.FlagRegistry; +import de.diddiz.LogBlock.LogBlock; +import de.diddiz.LogBlock.events.BlockChangePreLogEvent; +import de.diddiz.LogBlock.events.EntityChangePreLogEvent; +import java.util.logging.Level; +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; + +public class WorldGuardLoggingFlagsAddon { + private final StateFlag LOGBLOCK_LOG_BLOCKS = new StateFlag("logblock-log-blocks", true); + private final StateFlag LOGBLOCK_LOG_ENTITIES = new StateFlag("logblock-log-entities", true); + + private LogBlock plugin; + private WorldGuardPlugin worldGuard; + + public WorldGuardLoggingFlagsAddon(LogBlock plugin) { + this.plugin = plugin; + } + + public void onPluginLoad() { + registerFlags(); + } + + public void onPluginEnable() { + worldGuard = (WorldGuardPlugin) plugin.getServer().getPluginManager().getPlugin("WorldGuard"); + plugin.getServer().getPluginManager().registerEvents(new LoggingListener(), plugin); + } + + private void registerFlags() { + try { + FlagRegistry registry = WorldGuard.getInstance().getFlagRegistry(); + registry.register(LOGBLOCK_LOG_BLOCKS); + registry.register(LOGBLOCK_LOG_ENTITIES); + } catch (FlagConflictException e) { + plugin.getLogger().log(Level.SEVERE, "Could not initialize Flags", e); + } + } + + private class LoggingListener implements Listener { + @EventHandler(ignoreCancelled = true) + public void onBlockChangePreLog(BlockChangePreLogEvent event) { + RegionAssociable regionAssociable = null; + Location location = event.getLocation(); + Entity actorEntity = event.getOwnerActor().getEntity(); + if (actorEntity instanceof Player) { + regionAssociable = worldGuard.wrapPlayer((Player) actorEntity); + } + ApplicableRegionSet set = WorldGuard.getInstance().getPlatform().getRegionContainer().createQuery().getApplicableRegions(BukkitAdapter.adapt(location)); + if (!set.testState(regionAssociable, LOGBLOCK_LOG_BLOCKS)) { + event.setCancelled(true); + } + } + + @EventHandler(ignoreCancelled = true) + public void onEntityChangePreLog(EntityChangePreLogEvent event) { + RegionAssociable regionAssociable = null; + Location location = event.getLocation(); + Entity actorEntity = event.getOwnerActor().getEntity(); + if (actorEntity instanceof Player) { + regionAssociable = worldGuard.wrapPlayer((Player) actorEntity); + } + ApplicableRegionSet set = WorldGuard.getInstance().getPlatform().getRegionContainer().createQuery().getApplicableRegions(BukkitAdapter.adapt(location)); + if (!set.testState(regionAssociable, LOGBLOCK_LOG_ENTITIES)) { + event.setCancelled(true); + } + } + } +} diff --git a/src/main/java/de/diddiz/LogBlock/blockstate/BlockStateCodec.java b/src/main/java/de/diddiz/LogBlock/blockstate/BlockStateCodec.java new file mode 100644 index 00000000..65ed64b4 --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/blockstate/BlockStateCodec.java @@ -0,0 +1,16 @@ +package de.diddiz.LogBlock.blockstate; + +import net.md_5.bungee.api.chat.BaseComponent; +import org.bukkit.Material; +import org.bukkit.block.BlockState; +import org.bukkit.configuration.file.YamlConfiguration; + +public interface BlockStateCodec { + Material[] getApplicableMaterials(); + + YamlConfiguration serialize(BlockState state); + + void deserialize(BlockState state, YamlConfiguration conf); + + BaseComponent getChangesAsComponent(YamlConfiguration conf, YamlConfiguration oldState); +} diff --git a/src/main/java/de/diddiz/LogBlock/blockstate/BlockStateCodecBanner.java b/src/main/java/de/diddiz/LogBlock/blockstate/BlockStateCodecBanner.java new file mode 100644 index 00000000..890c34d1 --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/blockstate/BlockStateCodecBanner.java @@ -0,0 +1,81 @@ +package de.diddiz.LogBlock.blockstate; + +import java.util.List; +import java.util.Locale; +import net.md_5.bungee.api.chat.BaseComponent; +import org.bukkit.DyeColor; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.block.Banner; +import org.bukkit.block.BlockState; +import org.bukkit.block.banner.Pattern; +import org.bukkit.block.banner.PatternType; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; + +public class BlockStateCodecBanner implements BlockStateCodec { + @Override + public Material[] getApplicableMaterials() { + return new Material[] { Material.WHITE_BANNER, Material.ORANGE_BANNER, Material.MAGENTA_BANNER, Material.LIGHT_BLUE_BANNER, Material.YELLOW_BANNER, Material.LIME_BANNER, Material.PINK_BANNER, Material.GRAY_BANNER, Material.LIGHT_GRAY_BANNER, Material.CYAN_BANNER, Material.PURPLE_BANNER, + Material.BLUE_BANNER, Material.BROWN_BANNER, Material.GREEN_BANNER, Material.RED_BANNER, Material.BLACK_BANNER, Material.WHITE_WALL_BANNER, Material.ORANGE_WALL_BANNER, Material.MAGENTA_WALL_BANNER, Material.LIGHT_BLUE_WALL_BANNER, Material.YELLOW_WALL_BANNER, + Material.LIME_WALL_BANNER, Material.PINK_WALL_BANNER, Material.GRAY_WALL_BANNER, Material.LIGHT_GRAY_WALL_BANNER, Material.CYAN_WALL_BANNER, Material.PURPLE_WALL_BANNER, Material.BLUE_WALL_BANNER, Material.BROWN_WALL_BANNER, Material.GREEN_WALL_BANNER, Material.RED_WALL_BANNER, + Material.BLACK_WALL_BANNER }; + } + + @Override + public YamlConfiguration serialize(BlockState state) { + if (state instanceof Banner) { + Banner banner = (Banner) state; + int nr = 0; + List patterns = banner.getPatterns(); + if (!patterns.isEmpty()) { + YamlConfiguration conf = new YamlConfiguration(); + ConfigurationSection patternsSection = conf.createSection("patterns"); + for (Pattern pattern : patterns) { + NamespacedKey key = pattern.getPattern().getKey(); + if (key != null) { + ConfigurationSection section = patternsSection.createSection(Integer.toString(nr)); + section.set("color", pattern.getColor().name()); + section.set("pattern", key.toString()); + nr++; + } + } + return conf; + } + } + return null; + } + + @Override + public void deserialize(BlockState state, YamlConfiguration conf) { + if (state instanceof Banner) { + Banner banner = (Banner) state; + int oldPatterns = banner.getPatterns().size(); + for (int i = 0; i < oldPatterns; i++) { + banner.removePattern(0); + } + ConfigurationSection patternsSection = conf == null ? null : conf.getConfigurationSection("patterns"); + if (patternsSection != null) { + for (String key : patternsSection.getKeys(false)) { + ConfigurationSection section = patternsSection.getConfigurationSection(key); + if (section != null) { + DyeColor color = DyeColor.valueOf(section.getString("color")); + NamespacedKey patternKey = NamespacedKey.fromString(section.getString("pattern").toLowerCase(Locale.ROOT)); + if (patternKey != null) { + PatternType type = Registry.BANNER_PATTERN.get(patternKey); + if (type != null) { + banner.addPattern(new Pattern(color, type)); + } + } + } + } + } + } + } + + @Override + public BaseComponent getChangesAsComponent(YamlConfiguration conf, YamlConfiguration oldState) { + return null; + } +} diff --git a/src/main/java/de/diddiz/LogBlock/blockstate/BlockStateCodecLectern.java b/src/main/java/de/diddiz/LogBlock/blockstate/BlockStateCodecLectern.java new file mode 100644 index 00000000..6c897bc4 --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/blockstate/BlockStateCodecLectern.java @@ -0,0 +1,54 @@ +package de.diddiz.LogBlock.blockstate; + +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.TextComponent; +import org.bukkit.Material; +import org.bukkit.block.BlockState; +import org.bukkit.block.Lectern; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.inventory.ItemStack; + +public class BlockStateCodecLectern implements BlockStateCodec { + @Override + public Material[] getApplicableMaterials() { + return new Material[] { Material.LECTERN }; + } + + @Override + public YamlConfiguration serialize(BlockState state) { + if (state instanceof Lectern) { + Lectern lectern = (Lectern) state; + ItemStack book = lectern.getSnapshotInventory().getItem(0); + if (book != null && book.getType() != Material.AIR) { + YamlConfiguration conf = new YamlConfiguration(); + conf.set("book", book); + return conf; + } + } + return null; + } + + @Override + public void deserialize(BlockState state, YamlConfiguration conf) { + if (state instanceof Lectern) { + Lectern lectern = (Lectern) state; + ItemStack book = null; + if (conf != null) { + book = conf.getItemStack("book"); + } + try { + lectern.getSnapshotInventory().setItem(0, book); + } catch (NullPointerException e) { + //ignored + } + } + } + + @Override + public BaseComponent getChangesAsComponent(YamlConfiguration conf, YamlConfiguration oldState) { + if (conf != null) { + return new TextComponent("[book]"); + } + return new TextComponent("empty"); + } +} diff --git a/src/main/java/de/diddiz/LogBlock/blockstate/BlockStateCodecShulkerBox.java b/src/main/java/de/diddiz/LogBlock/blockstate/BlockStateCodecShulkerBox.java new file mode 100644 index 00000000..1325a862 --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/blockstate/BlockStateCodecShulkerBox.java @@ -0,0 +1,87 @@ +package de.diddiz.LogBlock.blockstate; + +import static de.diddiz.LogBlock.config.Config.getWorldConfig; + +import de.diddiz.LogBlock.Logging; +import de.diddiz.LogBlock.config.WorldConfig; +import de.diddiz.LogBlock.util.BukkitUtils; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.TextComponent; +import org.bukkit.Material; +import org.bukkit.block.BlockState; +import org.bukkit.block.ShulkerBox; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.inventory.ItemStack; + +public class BlockStateCodecShulkerBox implements BlockStateCodec { + @Override + public Material[] getApplicableMaterials() { + return BukkitUtils.getShulkerBoxBlocks().toArray(Material[]::new); + } + + @Override + public YamlConfiguration serialize(BlockState state) { + WorldConfig wcfg = getWorldConfig(state.getWorld()); + if (wcfg == null || !wcfg.isLogging(Logging.SHULKER_BOX_CONTENT)) { + return null; + } + if (state instanceof ShulkerBox) { + ShulkerBox shulkerBox = (ShulkerBox) state; + ItemStack[] content = shulkerBox.getSnapshotInventory().getStorageContents(); + YamlConfiguration conf = new YamlConfiguration(); + boolean anySlot = false; + for (int i = 0; i < content.length; i++) { + ItemStack stack = content[i]; + if (stack != null && stack.getType() != Material.AIR) { + conf.set("slot" + i, stack); + anySlot = true; + } + } + if (anySlot) { + return conf; + } + } + return null; + } + + @Override + public void deserialize(BlockState state, YamlConfiguration conf) { + if (state instanceof ShulkerBox) { + ShulkerBox shulkerBox = (ShulkerBox) state; + if (conf != null) { + ItemStack[] content = shulkerBox.getSnapshotInventory().getStorageContents(); + for (int i = 0; i < content.length; i++) { + ItemStack stack = conf.getItemStack("slot" + i); + if (stack != null && stack.getType() != Material.AIR) { + content[i] = stack; + } + } + shulkerBox.getSnapshotInventory().setContents(content); + } + } + } + + @Override + public BaseComponent getChangesAsComponent(YamlConfiguration conf, YamlConfiguration oldState) { + if (conf != null) { + StringBuilder sb = new StringBuilder(); + sb.append("["); + boolean anySlot = false; + for (String key : conf.getKeys(false)) { + if (key.startsWith("slot")) { + ItemStack stack = conf.getItemStack(key); + if (stack != null && stack.getType() != Material.AIR) { + if (anySlot) { + sb.append(","); + } + anySlot = true; + sb.append(stack.getAmount()).append("x").append(stack.getType()); + } + } + } + sb.append("]"); + return anySlot ? new TextComponent(sb.toString()) : null; + } + return null; + } +} diff --git a/src/main/java/de/diddiz/LogBlock/blockstate/BlockStateCodecSign.java b/src/main/java/de/diddiz/LogBlock/blockstate/BlockStateCodecSign.java new file mode 100644 index 00000000..442ffcb5 --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/blockstate/BlockStateCodecSign.java @@ -0,0 +1,227 @@ +package de.diddiz.LogBlock.blockstate; + +import de.diddiz.LogBlock.util.BukkitUtils; +import java.awt.Color; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.TextComponent; +import org.bukkit.DyeColor; +import org.bukkit.Material; +import org.bukkit.block.BlockState; +import org.bukkit.block.Sign; +import org.bukkit.block.sign.Side; +import org.bukkit.block.sign.SignSide; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; + +public class BlockStateCodecSign implements BlockStateCodec { + @Override + public Material[] getApplicableMaterials() { + return BukkitUtils.getAllSignMaterials().toArray(Material[]::new); + } + + @Override + public YamlConfiguration serialize(BlockState state) { + YamlConfiguration conf = null; + if (state instanceof Sign sign) { + boolean waxed = sign.isWaxed(); + if (waxed) { + conf = new YamlConfiguration(); + conf.set("waxed", waxed); + } + for (Side side : Side.values()) { + SignSide signSide = sign.getSide(side); + String[] lines = signSide.getLines(); + boolean hasText = false; + for (int i = 0; i < lines.length; i++) { + if (lines[i] != null && lines[i].length() > 0) { + hasText = true; + break; + } + } + DyeColor signColor = signSide.getColor(); + if (signColor == null) { + signColor = DyeColor.BLACK; + } + boolean glowing = signSide.isGlowingText(); + if (hasText || signColor != DyeColor.BLACK || glowing) { + if (conf == null) { + conf = new YamlConfiguration(); + } + ConfigurationSection sideSection = side == Side.FRONT ? conf : conf.createSection(side.name().toLowerCase()); + if (hasText) { + sideSection.set("lines", Arrays.asList(lines)); + } + if (signColor != DyeColor.BLACK) { + sideSection.set("color", signColor.name()); + } + if (glowing) { + sideSection.set("glowing", true); + } + } + } + } + return conf; + } + + /** + * This is required for the SignChangeEvent, because we have no updated BlockState there. + * @param state + */ + public YamlConfiguration serialize(BlockState state, Side side, String[] lines) { + YamlConfiguration conf = state == null ? null : serialize(state); + if (lines != null) { + if (conf == null) { + conf = new YamlConfiguration(); + } + ConfigurationSection sideSection = side == Side.FRONT ? conf : conf.getConfigurationSection(side.name().toLowerCase()); + if (sideSection == null) { + sideSection = conf.createSection(side.name().toLowerCase()); + } + sideSection.set("lines", Arrays.asList(lines)); + } + return conf; + } + + @Override + public void deserialize(BlockState state, YamlConfiguration conf) { + if (state instanceof Sign) { + Sign sign = (Sign) state; + if (conf != null) { + sign.setWaxed(conf.getBoolean("waxed")); + for (Side side : Side.values()) { + ConfigurationSection sideSection = side == Side.FRONT ? conf : conf.getConfigurationSection(side.name().toLowerCase()); + DyeColor signColor = DyeColor.BLACK; + boolean glowing = false; + List lines = Collections.emptyList(); + if (sideSection != null) { + if (sideSection.contains("lines")) { + lines = sideSection.getStringList("lines"); + } + if (sideSection.contains("color")) { + try { + signColor = DyeColor.valueOf(sideSection.getString("color")); + } catch (IllegalArgumentException | NullPointerException e) { + // ignored + } + } + glowing = sideSection.getBoolean("glowing", false); + } + SignSide signSide = sign.getSide(side); + for (int i = 0; i < 4; i++) { + String line = lines.size() > i && lines.get(i) != null ? lines.get(i) : ""; + signSide.setLine(i, line); + } + signSide.setColor(signColor); + signSide.setGlowingText(glowing); + } + } else { + sign.setWaxed(false); + for (Side side : Side.values()) { + SignSide signSide = sign.getSide(side); + for (int i = 0; i < 4; i++) { + signSide.setLine(i, ""); + } + signSide.setColor(DyeColor.BLACK); + signSide.setGlowingText(false); + } + } + } + } + + @Override + public BaseComponent getChangesAsComponent(YamlConfiguration state, YamlConfiguration oldState) { + if (state != null) { + TextComponent tc = new TextComponent(); + // StringBuilder sb = new StringBuilder(); + boolean isWaxed = state.getBoolean("waxed"); + boolean oldWaxed = oldState != null && oldState.getBoolean("waxed"); + if (isWaxed != oldWaxed) { + tc.addExtra(isWaxed ? "(waxed)" : "(not waxed)"); + } + for (Side side : Side.values()) { + boolean sideHeaderAdded = false; + ConfigurationSection sideSection = side == Side.FRONT ? state : state.getConfigurationSection(side.name().toLowerCase()); + + List lines = sideSection == null ? Collections.emptyList() : sideSection.getStringList("lines"); + List oldLines = Collections.emptyList(); + DyeColor signColor = DyeColor.BLACK; + if (sideSection != null && sideSection.contains("color")) { + try { + signColor = DyeColor.valueOf(sideSection.getString("color")); + } catch (IllegalArgumentException | NullPointerException e) { + // ignored + } + } + DyeColor oldSignColor = DyeColor.BLACK; + boolean glowing = sideSection != null && sideSection.getBoolean("glowing", false); + boolean oldGlowing = false; + if (oldState != null) { + ConfigurationSection oldSideSection = side == Side.FRONT ? oldState : oldState.getConfigurationSection(side.name().toLowerCase()); + if (oldSideSection != null) { + oldLines = oldSideSection.getStringList("lines"); + if (oldSideSection.contains("color")) { + try { + oldSignColor = DyeColor.valueOf(oldSideSection.getString("color")); + } catch (IllegalArgumentException | NullPointerException e) { + // ignored + } + } + oldGlowing = oldSideSection.getBoolean("glowing", false); + } + } + + if (!lines.equals(oldLines)) { + sideHeaderAdded = addSideHeaderText(tc, side, sideHeaderAdded); + for (String line : lines) { + if (tc.getExtra() != null && !tc.getExtra().isEmpty()) { + tc.addExtra(" "); + } + tc.addExtra("["); + if (line != null && !line.isEmpty()) { + tc.addExtra(TextComponent.fromLegacy(line)); + } + tc.addExtra("]"); + } + } + if (signColor != oldSignColor) { + sideHeaderAdded = addSideHeaderText(tc, side, sideHeaderAdded); + if (tc.getExtra() != null && !tc.getExtra().isEmpty()) { + tc.addExtra(" "); + } + tc.addExtra("(color: "); + TextComponent colorText = new TextComponent(signColor.name().toLowerCase()); + colorText.setColor(ChatColor.of(new Color(signColor.getColor().asARGB()))); + tc.addExtra(colorText); + tc.addExtra(")"); + } + if (glowing != oldGlowing) { + sideHeaderAdded = addSideHeaderText(tc, side, sideHeaderAdded); + if (tc.getExtra() != null && !tc.getExtra().isEmpty()) { + tc.addExtra(" "); + } + if (glowing) { + tc.addExtra("(glowing)"); + } else { + tc.addExtra("(not glowing)"); + } + } + } + return tc; + } + return null; + } + + private static boolean addSideHeaderText(TextComponent tc, Side side, boolean wasAdded) { + if (!wasAdded) { + if (tc.getExtra() != null && !tc.getExtra().isEmpty()) { + tc.addExtra(" "); + } + tc.addExtra(side.name() + ":"); + } + return true; + } +} diff --git a/src/main/java/de/diddiz/LogBlock/blockstate/BlockStateCodecSkull.java b/src/main/java/de/diddiz/LogBlock/blockstate/BlockStateCodecSkull.java new file mode 100644 index 00000000..310a72e6 --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/blockstate/BlockStateCodecSkull.java @@ -0,0 +1,90 @@ +package de.diddiz.LogBlock.blockstate; + +import java.util.UUID; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.HoverEvent; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.chat.hover.content.Text; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.OfflinePlayer; +import org.bukkit.block.BlockState; +import org.bukkit.block.Skull; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.profile.PlayerProfile; + +public class BlockStateCodecSkull implements BlockStateCodec { + private static final boolean HAS_PROFILE_API; + static { + boolean hasProfileApi = false; + try { + Skull.class.getMethod("getOwnerProfile"); + hasProfileApi = true; + } catch (NoSuchMethodException ignored) { + } + HAS_PROFILE_API = hasProfileApi; + } + + @Override + public Material[] getApplicableMaterials() { + return new Material[] { Material.PLAYER_WALL_HEAD, Material.PLAYER_HEAD }; + } + + @Override + public YamlConfiguration serialize(BlockState state) { + if (state instanceof Skull) { + Skull skull = (Skull) state; + OfflinePlayer owner = skull.hasOwner() ? skull.getOwningPlayer() : null; + PlayerProfile profile = HAS_PROFILE_API ? skull.getOwnerProfile() : null; + if (owner != null || profile != null) { + YamlConfiguration conf = new YamlConfiguration(); + if (profile != null) { + conf.set("profile", profile); + } else if (owner != null) { + conf.set("owner", owner.getUniqueId().toString()); + } + return conf; + } + } + return null; + } + + @Override + public void deserialize(BlockState state, YamlConfiguration conf) { + if (state instanceof Skull) { + Skull skull = (Skull) state; + PlayerProfile profile = conf == null || !HAS_PROFILE_API ? null : (PlayerProfile) conf.get("profile"); + if (profile != null) { + skull.setOwnerProfile(profile); + } else { + UUID ownerId = conf == null ? null : UUID.fromString(conf.getString("owner")); + if (ownerId == null) { + skull.setOwningPlayer(null); + } else { + skull.setOwningPlayer(Bukkit.getOfflinePlayer(ownerId)); + } + } + } + } + + @Override + public BaseComponent getChangesAsComponent(YamlConfiguration conf, YamlConfiguration oldState) { + if (HAS_PROFILE_API && conf != null) { + PlayerProfile profile = (PlayerProfile) conf.get("profile"); + if (profile != null) { + TextComponent tc = new TextComponent("[" + (profile.getName() != null ? profile.getName() : (profile.getUniqueId() != null ? profile.getUniqueId().toString() : "~unknown~")) + "]"); + if (profile.getName() != null && profile.getUniqueId() != null) { + tc.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("UUID: " + profile.getUniqueId().toString()))); + } + return tc; + } + } + String ownerIdString = conf == null ? null : conf.getString("owner"); + UUID ownerId = ownerIdString == null ? null : UUID.fromString(ownerIdString); + if (ownerId != null) { + OfflinePlayer owner = Bukkit.getOfflinePlayer(ownerId); + return new TextComponent("[" + (owner.getName() != null ? owner.getName() : owner.getUniqueId().toString()) + "]"); + } + return null; + } +} diff --git a/src/main/java/de/diddiz/LogBlock/blockstate/BlockStateCodecSpawner.java b/src/main/java/de/diddiz/LogBlock/blockstate/BlockStateCodecSpawner.java new file mode 100644 index 00000000..db6d42e5 --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/blockstate/BlockStateCodecSpawner.java @@ -0,0 +1,74 @@ +package de.diddiz.LogBlock.blockstate; + +import de.diddiz.LogBlock.LogBlock; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.TextComponent; +import org.bukkit.Material; +import org.bukkit.block.BlockState; +import org.bukkit.block.CreatureSpawner; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.EntityType; + +public class BlockStateCodecSpawner implements BlockStateCodec { + @Override + public Material[] getApplicableMaterials() { + return new Material[] { Material.SPAWNER }; + } + + @Override + public YamlConfiguration serialize(BlockState state) { + if (state instanceof CreatureSpawner) { + CreatureSpawner spawner = (CreatureSpawner) state; + YamlConfiguration conf = new YamlConfiguration(); + conf.set("delay", spawner.getDelay()); + conf.set("maxNearbyEntities", spawner.getMaxNearbyEntities()); + conf.set("maxSpawnDelay", spawner.getMaxSpawnDelay()); + conf.set("minSpawnDelay", spawner.getMinSpawnDelay()); + conf.set("requiredPlayerRange", spawner.getRequiredPlayerRange()); + conf.set("spawnCount", spawner.getSpawnCount()); + if (spawner.getSpawnedType() != null) { + conf.set("spawnedType", spawner.getSpawnedType().name()); + } + conf.set("spawnRange", spawner.getSpawnRange()); + return conf; + } + return null; + } + + @Override + public void deserialize(BlockState state, YamlConfiguration conf) { + if (state instanceof CreatureSpawner) { + CreatureSpawner spawner = (CreatureSpawner) state; + if (conf != null) { + spawner.setDelay(conf.getInt("delay")); + spawner.setMaxNearbyEntities(conf.getInt("maxNearbyEntities")); + spawner.setMaxSpawnDelay(conf.getInt("maxSpawnDelay")); + spawner.setMinSpawnDelay(conf.getInt("minSpawnDelay")); + spawner.setRequiredPlayerRange(conf.getInt("requiredPlayerRange")); + spawner.setSpawnCount(conf.getInt("spawnCount")); + EntityType spawnedType = null; + String spawnedTypeString = conf.getString("spawnedType"); + if (spawnedTypeString != null) { + try { + spawnedType = EntityType.valueOf(spawnedTypeString); + } catch (IllegalArgumentException ignored) { + LogBlock.getInstance().getLogger().warning("Could not find spawner spawned type: " + spawnedTypeString); + } + } + spawner.setSpawnedType(spawnedType); + spawner.setSpawnRange(conf.getInt("spawnRange")); + } + } + } + + @Override + public BaseComponent getChangesAsComponent(YamlConfiguration conf, YamlConfiguration oldState) { + if (conf != null) { + EntityType entity = EntityType.valueOf(conf.getString("spawnedType")); + if (entity != null) { + return new TextComponent("[" + entity.getKey().getKey() + "]"); + } + } + return null; + } +} diff --git a/src/main/java/de/diddiz/LogBlock/blockstate/BlockStateCodecs.java b/src/main/java/de/diddiz/LogBlock/blockstate/BlockStateCodecs.java new file mode 100644 index 00000000..4f1ed7c8 --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/blockstate/BlockStateCodecs.java @@ -0,0 +1,61 @@ +package de.diddiz.LogBlock.blockstate; + +import java.util.HashMap; +import java.util.Map; +import net.md_5.bungee.api.chat.BaseComponent; +import org.bukkit.Material; +import org.bukkit.block.BlockState; +import org.bukkit.configuration.file.YamlConfiguration; + +public class BlockStateCodecs { + private static Map codecs = new HashMap<>(); + + public static void registerCodec(BlockStateCodec codec) { + Material[] materials = codec.getApplicableMaterials(); + for (Material material : materials) { + if (codecs.containsKey(material)) { + throw new IllegalArgumentException("BlockStateCodec for " + material + " already registered!"); + } + codecs.put(material, codec); + } + } + + static { + registerCodec(new BlockStateCodecSign()); + registerCodec(new BlockStateCodecSkull()); + registerCodec(new BlockStateCodecBanner()); + registerCodec(new BlockStateCodecSpawner()); + registerCodec(new BlockStateCodecLectern()); + registerCodec(new BlockStateCodecShulkerBox()); + } + + public static boolean hasCodec(Material material) { + return codecs.containsKey(material); + } + + public static YamlConfiguration serialize(BlockState state) { + BlockStateCodec codec = codecs.get(state.getType()); + if (codec != null) { + YamlConfiguration serialized = codec.serialize(state); + if (serialized != null && !serialized.getKeys(false).isEmpty()) { + return serialized; + } + } + return null; + } + + public static void deserialize(BlockState block, YamlConfiguration state) { + BlockStateCodec codec = codecs.get(block.getType()); + if (codec != null) { + codec.deserialize(block, state); + } + } + + public static BaseComponent getChangesAsComponent(Material material, YamlConfiguration state, YamlConfiguration oldState) { + BlockStateCodec codec = codecs.get(material); + if (codec != null) { + return codec.getChangesAsComponent(state, oldState); + } + return null; + } +} diff --git a/src/main/java/de/diddiz/LogBlock/config/Config.java b/src/main/java/de/diddiz/LogBlock/config/Config.java index ed6cbaea..924afc25 100644 --- a/src/main/java/de/diddiz/LogBlock/config/Config.java +++ b/src/main/java/de/diddiz/LogBlock/config/Config.java @@ -1,293 +1,383 @@ -package de.diddiz.LogBlock.config; - -import de.diddiz.LogBlock.*; -import org.bukkit.Material; -import org.bukkit.World; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.permissions.PermissionDefault; - -import java.io.File; -import java.io.IOException; -import java.text.SimpleDateFormat; -import java.util.*; -import java.util.Map.Entry; -import java.util.logging.Level; -import java.util.zip.DataFormatException; - -import static de.diddiz.util.BukkitUtils.friendlyWorldname; -import static de.diddiz.util.Utils.parseTimeSpec; -import static org.bukkit.Bukkit.*; - -public class Config { - private static LoggingEnabledMapping superWorldConfig; - private static Map worldConfigs; - public static String url, user, password; - public static int delayBetweenRuns, forceToProcessAtLeast, timePerRun; - public static boolean fireCustomEvents; - public static boolean useBukkitScheduler; - public static int queueWarningSize; - public static boolean enableAutoClearLog; - public static List autoClearLog; - public static int autoClearLogDelay; - public static boolean dumpDeletedLog; - public static boolean logCreeperExplosionsAsPlayerWhoTriggeredThese, logPlayerInfo; - public static LogKillsLevel logKillsLevel; - public static Set dontRollback, replaceAnyway; - public static int rollbackMaxTime, rollbackMaxArea; - public static Map toolsByName; - public static Map toolsByType; - public static int defaultDist, defaultTime; - public static int linesPerPage, linesLimit; - public static boolean askRollbacks, askRedos, askClearLogs, askClearLogAfterRollback, askRollbackAfterBan; - public static String banPermission; - public static Set hiddenBlocks; - public static Set hiddenPlayers; - public static Set ignoredChat; - public static SimpleDateFormat formatter; - public static boolean safetyIdCheck; - public static boolean debug; - public static boolean logEnvironmentalKills; - // Not loaded from config - checked at runtime - public static boolean mb4 = false; - - public static enum LogKillsLevel { - PLAYERS, MONSTERS, ANIMALS; - } - - public static void load(LogBlock logblock) throws DataFormatException, IOException { - final ConfigurationSection config = logblock.getConfig(); - final Map def = new HashMap(); - def.put("version", logblock.getDescription().getVersion()); - final List worldNames = new ArrayList(); - for (final World world : getWorlds()) { - worldNames.add(world.getName()); - } - if (worldNames.isEmpty()) { - worldNames.add("world"); - worldNames.add("world_nether"); - worldNames.add("world_the_end"); - } - def.put("loggedWorlds", worldNames); - def.put("mysql.host", "localhost"); - def.put("mysql.port", 3306); - def.put("mysql.database", "minecraft"); - def.put("mysql.user", "username"); - def.put("mysql.password", "pass"); - def.put("consumer.delayBetweenRuns", 2); - def.put("consumer.forceToProcessAtLeast", 200); - def.put("consumer.timePerRun", 1000); - def.put("consumer.fireCustomEvents", false); - def.put("consumer.useBukkitScheduler", true); - def.put("consumer.queueWarningSize", 1000); - def.put("clearlog.dumpDeletedLog", false); - def.put("clearlog.enableAutoClearLog", false); - def.put("clearlog.auto", Arrays.asList("world \"world\" before 365 days all", "world \"world\" player lavaflow waterflow leavesdecay before 7 days all", "world world_nether before 365 days all", "world world_nether player lavaflow before 7 days all")); - def.put("clearlog.autoClearLogDelay", "6h"); - def.put("logging.logCreeperExplosionsAsPlayerWhoTriggeredThese", false); - def.put("logging.logKillsLevel", "PLAYERS"); - def.put("logging.logEnvironmentalKills", false); - def.put("logging.logPlayerInfo", false); - def.put("logging.hiddenPlayers", new ArrayList()); - def.put("logging.hiddenBlocks", Arrays.asList(0)); - def.put("logging.ignoredChat", Arrays.asList("/register", "/login")); - def.put("rollback.dontRollback", Arrays.asList(10, 11, 46, 51)); - def.put("rollback.replaceAnyway", Arrays.asList(8, 9, 10, 11, 51)); - def.put("rollback.maxTime", "2 days"); - def.put("rollback.maxArea", 50); - def.put("lookup.defaultDist", 20); - def.put("lookup.defaultTime", "30 minutes"); - def.put("lookup.linesPerPage", 15); - def.put("lookup.linesLimit", 1500); - try { - formatter = new SimpleDateFormat(config.getString("lookup.dateFormat", "MM-dd HH:mm:ss")); - } catch (IllegalArgumentException e) { - throw new DataFormatException("Invalid specification for date format, please see http://docs.oracle.com/javase/1.4.2/docs/api/java/text/SimpleDateFormat.html : " + e.getMessage()); - } - def.put("lookup.dateFormat", "MM-dd HH:mm:ss"); - def.put("questioner.askRollbacks", true); - def.put("questioner.askRedos", true); - def.put("questioner.askClearLogs", true); - def.put("questioner.askClearLogAfterRollback", true); - def.put("questioner.askRollbackAfterBan", false); - def.put("questioner.banPermission", "mcbans.ban.local"); - def.put("tools.tool.aliases", Arrays.asList("t")); - def.put("tools.tool.leftClickBehavior", "NONE"); - def.put("tools.tool.rightClickBehavior", "TOOL"); - def.put("tools.tool.defaultEnabled", true); - def.put("tools.tool.item", 270); - def.put("tools.tool.canDrop", true); - def.put("tools.tool.params", "area 0 all sum none limit 15 desc silent"); - def.put("tools.tool.mode", "LOOKUP"); - def.put("tools.tool.permissionDefault", "OP"); - def.put("tools.toolblock.aliases", Arrays.asList("tb")); - def.put("tools.toolblock.leftClickBehavior", "TOOL"); - def.put("tools.toolblock.rightClickBehavior", "BLOCK"); - def.put("tools.toolblock.defaultEnabled", true); - def.put("tools.toolblock.item", 7); - def.put("tools.toolblock.canDrop", false); - def.put("tools.toolblock.params", "area 0 all sum none limit 15 desc silent"); - def.put("tools.toolblock.mode", "LOOKUP"); - def.put("tools.toolblock.permissionDefault", "OP"); - def.put("safety.id.check", true); - def.put("debug", false); - for (final Entry e : def.entrySet()) { - if (!config.contains(e.getKey())) { - config.set(e.getKey(), e.getValue()); - } - } - logblock.saveConfig(); - url = "jdbc:mysql://" + config.getString("mysql.host") + ":" + config.getInt("mysql.port") + "/" + getStringIncludingInts(config, "mysql.database"); - user = getStringIncludingInts(config, "mysql.user"); - password = getStringIncludingInts(config, "mysql.password"); - delayBetweenRuns = config.getInt("consumer.delayBetweenRuns", 2); - forceToProcessAtLeast = config.getInt("consumer.forceToProcessAtLeast", 0); - timePerRun = config.getInt("consumer.timePerRun", 1000); - fireCustomEvents = config.getBoolean("consumer.fireCustomEvents", false); - useBukkitScheduler = config.getBoolean("consumer.useBukkitScheduler", true); - queueWarningSize = config.getInt("consumer.queueWarningSize", 1000); - enableAutoClearLog = config.getBoolean("clearlog.enableAutoClearLog"); - autoClearLog = config.getStringList("clearlog.auto"); - dumpDeletedLog = config.getBoolean("clearlog.dumpDeletedLog", false); - autoClearLogDelay = parseTimeSpec(config.getString("clearlog.autoClearLogDelay").split(" ")); - logCreeperExplosionsAsPlayerWhoTriggeredThese = config.getBoolean("logging.logCreeperExplosionsAsPlayerWhoTriggeredThese", false); - logPlayerInfo = config.getBoolean("logging.logPlayerInfo", true); - try { - logKillsLevel = LogKillsLevel.valueOf(config.getString("logging.logKillsLevel").toUpperCase()); - } catch (final IllegalArgumentException ex) { - throw new DataFormatException("logging.logKillsLevel doesn't appear to be a valid log level. Allowed are 'PLAYERS', 'MONSTERS' and 'ANIMALS'"); - } - logEnvironmentalKills = config.getBoolean("logging.logEnvironmentalKills", false); - hiddenPlayers = new HashSet(); - for (final String playerName : config.getStringList("logging.hiddenPlayers")) { - hiddenPlayers.add(playerName.toLowerCase().trim()); - } - hiddenBlocks = new HashSet(); - for (final Object blocktype : config.getList("logging.hiddenBlocks")) { - final Material mat = Material.matchMaterial(String.valueOf(blocktype)); - if (mat != null) { - hiddenBlocks.add(mat.getId()); - } else { - throw new DataFormatException("Not a valid material: '" + blocktype + "'"); - } - } - ignoredChat = new HashSet(); - for (String chatCommand : config.getStringList("logging.ignoredChat")) { - ignoredChat.add(chatCommand); - } - dontRollback = new HashSet(config.getIntegerList("rollback.dontRollback")); - replaceAnyway = new HashSet(config.getIntegerList("rollback.replaceAnyway")); - rollbackMaxTime = parseTimeSpec(config.getString("rollback.maxTime").split(" ")); - rollbackMaxArea = config.getInt("rollback.maxArea", 50); - defaultDist = config.getInt("lookup.defaultDist", 20); - defaultTime = parseTimeSpec(config.getString("lookup.defaultTime").split(" ")); - linesPerPage = config.getInt("lookup.linesPerPage", 15); - linesLimit = config.getInt("lookup.linesLimit", 1500); - askRollbacks = config.getBoolean("questioner.askRollbacks", true); - askRedos = config.getBoolean("questioner.askRedos", true); - askClearLogs = config.getBoolean("questioner.askClearLogs", true); - askClearLogAfterRollback = config.getBoolean("questioner.askClearLogAfterRollback", true); - askRollbackAfterBan = config.getBoolean("questioner.askRollbackAfterBan", false); - safetyIdCheck = config.getBoolean("safety.id.check", true); - debug = config.getBoolean("debug", false); - banPermission = config.getString("questioner.banPermission"); - final List tools = new ArrayList(); - final ConfigurationSection toolsSec = config.getConfigurationSection("tools"); - for (final String toolName : toolsSec.getKeys(false)) { - try { - final ConfigurationSection tSec = toolsSec.getConfigurationSection(toolName); - final List aliases = tSec.getStringList("aliases"); - final ToolBehavior leftClickBehavior = ToolBehavior.valueOf(tSec.getString("leftClickBehavior").toUpperCase()); - final ToolBehavior rightClickBehavior = ToolBehavior.valueOf(tSec.getString("rightClickBehavior").toUpperCase()); - final boolean defaultEnabled = tSec.getBoolean("defaultEnabled", false); - final int item = tSec.getInt("item", 0); - final boolean canDrop = tSec.getBoolean("canDrop", false); - final QueryParams params = new QueryParams(logblock); - params.prepareToolQuery = true; - params.parseArgs(getConsoleSender(), Arrays.asList(tSec.getString("params").split(" "))); - final ToolMode mode = ToolMode.valueOf(tSec.getString("mode").toUpperCase()); - final PermissionDefault pdef = PermissionDefault.valueOf(tSec.getString("permissionDefault").toUpperCase()); - tools.add(new Tool(toolName, aliases, leftClickBehavior, rightClickBehavior, defaultEnabled, item, canDrop, params, mode, pdef)); - } catch (final Exception ex) { - getLogger().log(Level.WARNING, "Error at parsing tool '" + toolName + "': ", ex); - } - } - toolsByName = new HashMap(); - toolsByType = new HashMap(); - for (final Tool tool : tools) { - toolsByType.put(tool.item, tool); - toolsByName.put(tool.name.toLowerCase(), tool); - for (final String alias : tool.aliases) { - toolsByName.put(alias, tool); - } - } - final List loggedWorlds = config.getStringList("loggedWorlds"); - worldConfigs = new HashMap(); - if (loggedWorlds.isEmpty()) { - throw new DataFormatException("No worlds configured"); - } - for (final String world : loggedWorlds) { - worldConfigs.put(world, new WorldConfig(new File(logblock.getDataFolder(), friendlyWorldname(world) + ".yml"))); - } - superWorldConfig = new LoggingEnabledMapping(); - for (final WorldConfig wcfg : worldConfigs.values()) { - for (final Logging l : Logging.values()) { - if (wcfg.isLogging(l)) { - superWorldConfig.setLogging(l, true); - } - } - } - } - - private static String getStringIncludingInts(ConfigurationSection cfg, String key) { - String str = cfg.getString(key); - if (str == null) { - str = String.valueOf(cfg.getInt(key)); - } - if (str == null) { - str = "No value set for '" + key + "'"; - } - return str; - } - - public static boolean isLogging(World world, Logging l) { - final WorldConfig wcfg = worldConfigs.get(world.getName()); - return wcfg != null && wcfg.isLogging(l); - } - - public static boolean isLogging(String worldName, Logging l) { - final WorldConfig wcfg = worldConfigs.get(worldName); - return wcfg != null && wcfg.isLogging(l); - } - - public static boolean isLogged(World world) { - return worldConfigs.containsKey(world.getName()); - } - - public static WorldConfig getWorldConfig(World world) { - return worldConfigs.get(world.getName()); - } - - public static WorldConfig getWorldConfig(String world) { - return worldConfigs.get(world); - } - - public static boolean isLogging(Logging l) { - return superWorldConfig.isLogging(l); - } - - public static Collection getLoggedWorlds() { - return worldConfigs.values(); - } -} - -class LoggingEnabledMapping { - private final boolean[] logging = new boolean[Logging.length]; - - public void setLogging(Logging l, boolean enabled) { - logging[l.ordinal()] = enabled; - } - - public boolean isLogging(Logging l) { - return logging[l.ordinal()]; - } -} +package de.diddiz.LogBlock.config; + +import de.diddiz.LogBlock.*; +import de.diddiz.LogBlock.util.ComparableVersion; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.Entity; +import org.bukkit.permissions.PermissionDefault; + +import java.io.File; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.Map.Entry; +import java.util.logging.Level; +import java.util.zip.DataFormatException; + +import static de.diddiz.LogBlock.util.BukkitUtils.friendlyWorldname; +import static de.diddiz.LogBlock.util.Utils.parseTimeSpec; +import static org.bukkit.Bukkit.*; + +public class Config { + private static LoggingEnabledMapping superWorldConfig; + private static Map worldConfigs; + public static String url, user, password; + public static String mysqlDatabase; + public static boolean mysqlUseSSL; + public static boolean mysqlRequireSSL; + public static int delayBetweenRuns, forceToProcessAtLeast, timePerRun; + public static boolean useBukkitScheduler; + public static int queueWarningSize; + public static boolean enableAutoClearLog; + public static List autoClearLog; + public static int autoClearLogDelay; + public static boolean dumpDeletedLog; + public static boolean logBedExplosionsAsPlayerWhoTriggeredThese; + public static boolean logCreeperExplosionsAsPlayerWhoTriggeredThese, logPlayerInfo; + public static boolean logFireSpreadAsPlayerWhoCreatedIt; + public static boolean logFluidFlowAsPlayerWhoTriggeredIt; + public static LogKillsLevel logKillsLevel; + public static Set dontRollback, replaceAnyway; + public static int rollbackMaxTime, rollbackMaxArea; + public static Map toolsByName; + public static Map toolsByType; + public static int defaultDist, defaultTime; + public static int linesPerPage, linesLimit, hardLinesLimit; + public static boolean askRollbacks, askRedos, askClearLogs, askClearLogAfterRollback, askRollbackAfterBan; + public static String banPermission; + public static Set hiddenBlocks; + public static Set hiddenPlayers; + public static List ignoredChat; + public static SimpleDateFormat formatter; + public static SimpleDateFormat formatterShort; + public static boolean debug; + public static boolean logEnvironmentalKills; + // addons + public static boolean worldGuardLoggingFlags; + // Not loaded from config - checked at runtime + public static boolean mb4 = false; + + public static final String CURRENT_CONFIG_VERSION = "1.19.0"; + + public static enum LogKillsLevel { + PLAYERS, + MONSTERS, + ANIMALS; + } + + public static void load(LogBlock logblock) throws DataFormatException, IOException { + final ConfigurationSection config = logblock.getConfig(); + final Map def = new HashMap<>(); + def.put("version", CURRENT_CONFIG_VERSION); + final List worldNames = new ArrayList<>(); + for (final World world : getWorlds()) { + worldNames.add(world.getName()); + } + if (worldNames.isEmpty()) { + worldNames.add("world"); + worldNames.add("world_nether"); + worldNames.add("world_the_end"); + } + def.put("loggedWorlds", worldNames); + def.put("mysql.protocol", "mysql"); + def.put("mysql.host", "localhost"); + def.put("mysql.port", 3306); + def.put("mysql.database", "minecraft"); + def.put("mysql.user", "username"); + def.put("mysql.password", "pass"); + def.put("mysql.useSSL", true); + def.put("mysql.requireSSL", false); + def.put("consumer.delayBetweenRuns", 2); + def.put("consumer.forceToProcessAtLeast", 200); + def.put("consumer.timePerRun", 1000); + def.put("consumer.useBukkitScheduler", true); + def.put("consumer.queueWarningSize", 1000); + def.put("clearlog.dumpDeletedLog", false); + def.put("clearlog.enableAutoClearLog", false); + final List autoClearlog = new ArrayList<>(); + for (final String world : worldNames) { + autoClearlog.add("world \"" + world + "\" before 365 days all"); + autoClearlog.add("world \"" + world + "\" player lavaflow waterflow leavesdecay before 7 days all"); + autoClearlog.add("world \"" + world + "\" entities before 365 days"); + } + def.put("clearlog.auto", autoClearlog); + def.put("clearlog.autoClearLogDelay", "6h"); + def.put("logging.logBedExplosionsAsPlayerWhoTriggeredThese", true); + def.put("logging.logCreeperExplosionsAsPlayerWhoTriggeredThese", false); + def.put("logging.logFireSpreadAsPlayerWhoCreatedIt", true); + def.put("logging.logFluidFlowAsPlayerWhoTriggeredIt", false); + def.put("logging.logKillsLevel", "PLAYERS"); + def.put("logging.logEnvironmentalKills", false); + def.put("logging.logPlayerInfo", false); + def.put("logging.hiddenPlayers", new ArrayList()); + def.put("logging.hiddenBlocks", Arrays.asList(Material.AIR.name(), Material.CAVE_AIR.name(), Material.VOID_AIR.name())); + def.put("logging.ignoredChat", Arrays.asList("/register", "/login")); + def.put("rollback.dontRollback", Arrays.asList(Material.LAVA.name(), Material.TNT.name(), Material.FIRE.name())); + def.put("rollback.replaceAnyway", Arrays.asList(Material.LAVA.name(), Material.WATER.name(), Material.FIRE.name(), Material.GRASS_BLOCK.name())); + def.put("rollback.maxTime", "2 days"); + def.put("rollback.maxArea", 50); + def.put("lookup.defaultDist", 20); + def.put("lookup.defaultTime", "30 minutes"); + def.put("lookup.linesPerPage", 15); + def.put("lookup.linesLimit", 1500); + def.put("lookup.hardLinesLimit", 100000); + try { + formatter = new SimpleDateFormat(config.getString("lookup.dateFormat", "yyyy-MM-dd HH:mm:ss")); + } catch (IllegalArgumentException e) { + throw new DataFormatException("Invalid specification for date format, please see http://docs.oracle.com/javase/1.4.2/docs/api/java/text/SimpleDateFormat.html : " + e.getMessage()); + } + def.put("lookup.dateFormat", "yyyy-MM-dd HH:mm:ss"); + try { + formatterShort = new SimpleDateFormat(config.getString("lookup.dateFormatShort", "MM-dd HH:mm")); + } catch (IllegalArgumentException e) { + throw new DataFormatException("Invalid specification for date format, please see http://docs.oracle.com/javase/1.4.2/docs/api/java/text/SimpleDateFormat.html : " + e.getMessage()); + } + def.put("lookup.dateFormatShort", "MM-dd HH:mm"); + def.put("questioner.askRollbacks", true); + def.put("questioner.askRedos", true); + def.put("questioner.askClearLogs", true); + def.put("questioner.askClearLogAfterRollback", true); + def.put("questioner.askRollbackAfterBan", false); + def.put("questioner.banPermission", "mcbans.ban.local"); + def.put("tools.tool.aliases", Arrays.asList("t")); + def.put("tools.tool.leftClickBehavior", "NONE"); + def.put("tools.tool.rightClickBehavior", "TOOL"); + def.put("tools.tool.defaultEnabled", true); + def.put("tools.tool.item", Material.WOODEN_PICKAXE.name()); + def.put("tools.tool.canDrop", true); + def.put("tools.tool.removeOnDisable", true); + def.put("tools.tool.dropToDisable", false); + def.put("tools.tool.params", "area 0 all sum none limit 15 desc since 60d silent"); + def.put("tools.tool.mode", "LOOKUP"); + def.put("tools.tool.permissionDefault", "OP"); + def.put("tools.toolblock.aliases", Arrays.asList("tb")); + def.put("tools.toolblock.leftClickBehavior", "TOOL"); + def.put("tools.toolblock.rightClickBehavior", "BLOCK"); + def.put("tools.toolblock.defaultEnabled", true); + def.put("tools.toolblock.item", Material.BEDROCK.name()); + def.put("tools.toolblock.canDrop", false); + def.put("tools.toolblock.removeOnDisable", true); + def.put("tools.toolblock.dropToDisable", false); + def.put("tools.toolblock.params", "area 0 all sum none limit 15 desc since 60d silent"); + def.put("tools.toolblock.mode", "LOOKUP"); + def.put("tools.toolblock.permissionDefault", "OP"); + def.put("safety.id.check", true); + def.put("addons.worldguardLoggingFlags", false); + def.put("debug", false); + for (final Entry e : def.entrySet()) { + if (!config.contains(e.getKey())) { + config.set(e.getKey(), e.getValue()); + } + } + if (config.contains("consumer.fireCustomEvents")) { + config.set("consumer.fireCustomEvents", null); + } + logblock.saveConfig(); + + ComparableVersion configVersion = new ComparableVersion(config.getString("version")); + boolean oldConfig = configVersion.compareTo(new ComparableVersion(CURRENT_CONFIG_VERSION)) < 0; + + mysqlDatabase = getStringIncludingInts(config, "mysql.database"); + url = "jdbc:" + config.getString("mysql.protocol") + "://" + config.getString("mysql.host") + ":" + config.getInt("mysql.port") + "/" + mysqlDatabase; + user = getStringIncludingInts(config, "mysql.user"); + password = getStringIncludingInts(config, "mysql.password"); + mysqlUseSSL = config.getBoolean("mysql.useSSL", true); + mysqlRequireSSL = config.getBoolean("mysql.requireSSL", false); + delayBetweenRuns = config.getInt("consumer.delayBetweenRuns", 2); + forceToProcessAtLeast = config.getInt("consumer.forceToProcessAtLeast", 0); + timePerRun = config.getInt("consumer.timePerRun", 1000); + useBukkitScheduler = config.getBoolean("consumer.useBukkitScheduler", true); + queueWarningSize = config.getInt("consumer.queueWarningSize", 1000); + enableAutoClearLog = config.getBoolean("clearlog.enableAutoClearLog"); + autoClearLog = config.getStringList("clearlog.auto"); + dumpDeletedLog = config.getBoolean("clearlog.dumpDeletedLog", false); + autoClearLogDelay = parseTimeSpec(config.getString("clearlog.autoClearLogDelay").split(" ")); + logBedExplosionsAsPlayerWhoTriggeredThese = config.getBoolean("logging.logBedExplosionsAsPlayerWhoTriggeredThese", true); + logCreeperExplosionsAsPlayerWhoTriggeredThese = config.getBoolean("logging.logCreeperExplosionsAsPlayerWhoTriggeredThese", false); + logFireSpreadAsPlayerWhoCreatedIt = config.getBoolean("logging.logFireSpreadAsPlayerWhoCreatedIt", true); + logFluidFlowAsPlayerWhoTriggeredIt = config.getBoolean("logging.logFluidFlowAsPlayerWhoTriggeredIt", false); + logPlayerInfo = config.getBoolean("logging.logPlayerInfo", true); + try { + logKillsLevel = LogKillsLevel.valueOf(config.getString("logging.logKillsLevel").toUpperCase()); + } catch (final IllegalArgumentException ex) { + throw new DataFormatException("logging.logKillsLevel doesn't appear to be a valid log level. Allowed are 'PLAYERS', 'MONSTERS' and 'ANIMALS'"); + } + logEnvironmentalKills = config.getBoolean("logging.logEnvironmentalKills", false); + hiddenPlayers = new HashSet<>(); + for (final String playerName : config.getStringList("logging.hiddenPlayers")) { + hiddenPlayers.add(playerName.toLowerCase().trim()); + } + hiddenBlocks = new HashSet<>(); + for (final String blocktype : config.getStringList("logging.hiddenBlocks")) { + final Material mat = Material.matchMaterial(blocktype); + if (mat != null) { + hiddenBlocks.add(mat); + } else if (!oldConfig) { + throw new DataFormatException("Not a valid material in hiddenBlocks: '" + blocktype + "'"); + } + } + ignoredChat = new ArrayList<>(); + for (String chatCommand : config.getStringList("logging.ignoredChat")) { + ignoredChat.add(chatCommand.toLowerCase()); + } + dontRollback = new HashSet<>(); + for (String e : config.getStringList("rollback.dontRollback")) { + Material mat = Material.matchMaterial(e); + if (mat != null) { + dontRollback.add(mat); + } else if (!oldConfig) { + throw new DataFormatException("Not a valid material in dontRollback: '" + e + "'"); + } + } + replaceAnyway = new HashSet<>(); + for (String e : config.getStringList("rollback.replaceAnyway")) { + Material mat = Material.matchMaterial(e); + if (mat != null) { + replaceAnyway.add(mat); + } else if (!oldConfig) { + throw new DataFormatException("Not a valid material in replaceAnyway: '" + e + "'"); + } + } + rollbackMaxTime = parseTimeSpec(config.getString("rollback.maxTime").split(" ")); + rollbackMaxArea = config.getInt("rollback.maxArea", 50); + defaultDist = config.getInt("lookup.defaultDist", 20); + defaultTime = parseTimeSpec(config.getString("lookup.defaultTime").split(" ")); + linesPerPage = config.getInt("lookup.linesPerPage", 15); + linesLimit = config.getInt("lookup.linesLimit", 1500); + hardLinesLimit = config.getInt("lookup.hardLinesLimit", 100000); + askRollbacks = config.getBoolean("questioner.askRollbacks", true); + askRedos = config.getBoolean("questioner.askRedos", true); + askClearLogs = config.getBoolean("questioner.askClearLogs", true); + askClearLogAfterRollback = config.getBoolean("questioner.askClearLogAfterRollback", true); + askRollbackAfterBan = config.getBoolean("questioner.askRollbackAfterBan", false); + debug = config.getBoolean("debug", false); + banPermission = config.getString("questioner.banPermission"); + final List tools = new ArrayList<>(); + final ConfigurationSection toolsSec = config.getConfigurationSection("tools"); + for (final String toolName : toolsSec.getKeys(false)) { + try { + final ConfigurationSection tSec = toolsSec.getConfigurationSection(toolName); + final List aliases = tSec.getStringList("aliases"); + final ToolBehavior leftClickBehavior = ToolBehavior.valueOf(tSec.getString("leftClickBehavior").toUpperCase()); + final ToolBehavior rightClickBehavior = ToolBehavior.valueOf(tSec.getString("rightClickBehavior").toUpperCase()); + final boolean defaultEnabled = tSec.getBoolean("defaultEnabled", false); + final Material item = Material.matchMaterial(tSec.getString("item", "OAK_LOG")); + final boolean canDrop = tSec.getBoolean("canDrop", false); + final boolean removeOnDisable = tSec.getBoolean("removeOnDisable", true); + final boolean dropToDisable = tSec.getBoolean("dropToDisable", false); + final QueryParams params = new QueryParams(logblock); + params.prepareToolQuery = true; + params.parseArgs(getConsoleSender(), Arrays.asList(tSec.getString("params").split(" ")), false); + final ToolMode mode = ToolMode.valueOf(tSec.getString("mode").toUpperCase()); + final PermissionDefault pdef = PermissionDefault.valueOf(tSec.getString("permissionDefault").toUpperCase()); + tools.add(new Tool(toolName, aliases, leftClickBehavior, rightClickBehavior, defaultEnabled, item, canDrop, params, mode, pdef, removeOnDisable, dropToDisable)); + } catch (final Exception ex) { + getLogger().log(Level.WARNING, "Error at parsing tool '" + toolName + "': ", ex); + } + } + toolsByName = new HashMap<>(); + toolsByType = new HashMap<>(); + for (final Tool tool : tools) { + toolsByType.put(tool.item, tool); + toolsByName.put(tool.name.toLowerCase(), tool); + for (final String alias : tool.aliases) { + toolsByName.put(alias, tool); + } + } + worldGuardLoggingFlags = config.getBoolean("addons.worldguardLoggingFlags"); + final List loggedWorlds = config.getStringList("loggedWorlds"); + worldConfigs = new HashMap<>(); + if (loggedWorlds.isEmpty()) { + throw new DataFormatException("No worlds configured"); + } + for (final String world : loggedWorlds) { + worldConfigs.put(world, new WorldConfig(world, new File(logblock.getDataFolder(), friendlyWorldname(world) + ".yml"))); + } + superWorldConfig = new LoggingEnabledMapping(); + for (final WorldConfig wcfg : worldConfigs.values()) { + for (final Logging l : Logging.values()) { + if (wcfg.isLogging(l)) { + superWorldConfig.setLogging(l, true); + } + } + } + } + + private static String getStringIncludingInts(ConfigurationSection cfg, String key) { + String str = cfg.getString(key); + if (str == null) { + str = String.valueOf(cfg.getInt(key)); + } + if (str == null) { + str = "No value set for '" + key + "'"; + } + return str; + } + + public static boolean isLogging(World world, Logging l) { + final WorldConfig wcfg = worldConfigs.get(world.getName()); + return wcfg != null && wcfg.isLogging(l); + } + + public static boolean isLogging(String worldName, Logging l) { + final WorldConfig wcfg = worldConfigs.get(worldName); + return wcfg != null && wcfg.isLogging(l); + } + + public static boolean isLogged(World world) { + return worldConfigs.containsKey(world.getName()); + } + + public static WorldConfig getWorldConfig(World world) { + return worldConfigs.get(world.getName()); + } + + public static WorldConfig getWorldConfig(String world) { + return worldConfigs.get(world); + } + + public static boolean isLogging(Logging l) { + return superWorldConfig.isLogging(l); + } + + public static Collection getLoggedWorlds() { + return worldConfigs.values(); + } + + public static boolean isLogging(World world, EntityLogging logging, Entity entity) { + final WorldConfig wcfg = worldConfigs.get(world.getName()); + return wcfg != null && wcfg.isLogging(logging, entity); + } + + public static boolean isLoggingAnyEntities() { + for (WorldConfig worldConfig : worldConfigs.values()) { + if (worldConfig.isLoggingAnyEntities()) { + return true; + } + } + return false; + } + + public static boolean isLoggingNatualSpawns(World world) { + final WorldConfig wcfg = worldConfigs.get(world.getName()); + return wcfg != null && wcfg.logNaturalEntitySpawns; + } +} + +class LoggingEnabledMapping { + private final EnumSet logging = EnumSet.noneOf(Logging.class); + + public void setLogging(Logging l, boolean enabled) { + if (enabled) { + logging.add(l); + } else { + logging.remove(l); + } + } + + public boolean isLogging(Logging l) { + return logging.contains(l); + } +} diff --git a/src/main/java/de/diddiz/LogBlock/config/EntityLogging.java b/src/main/java/de/diddiz/LogBlock/config/EntityLogging.java new file mode 100644 index 00000000..f2569a4c --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/config/EntityLogging.java @@ -0,0 +1,28 @@ +package de.diddiz.LogBlock.config; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.bukkit.entity.EntityType; + +public enum EntityLogging { + SPAWN(new String[] { EntityType.ARMOR_STAND.name(), EntityType.ITEM_FRAME.name(), EntityType.SNOW_GOLEM.name() }), + DESTROY(new String[] { EntityType.ARMOR_STAND.name(), EntityType.ITEM_FRAME.name(), EntityType.VILLAGER.name(), EntityType.SNOW_GOLEM.name(), "ANIMAL" }), + MODIFY(new String[] { "ALL" }); + + public static final int length = EntityLogging.values().length; + private final List defaultEnabled; + + private EntityLogging() { + this(null); + } + + private EntityLogging(String[] defaultEnabled) { + this.defaultEnabled = defaultEnabled == null ? Collections.emptyList() : Collections.unmodifiableList(Arrays.asList(defaultEnabled)); + } + + public List getDefaultEnabled() { + return defaultEnabled; + } +} diff --git a/src/main/java/de/diddiz/LogBlock/config/WorldConfig.java b/src/main/java/de/diddiz/LogBlock/config/WorldConfig.java index 4a1878be..06a6e90b 100644 --- a/src/main/java/de/diddiz/LogBlock/config/WorldConfig.java +++ b/src/main/java/de/diddiz/LogBlock/config/WorldConfig.java @@ -1,35 +1,171 @@ -package de.diddiz.LogBlock.config; - -import de.diddiz.LogBlock.Logging; -import org.bukkit.configuration.file.YamlConfiguration; - -import java.io.File; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; - -public class WorldConfig extends LoggingEnabledMapping { - public final String table; - - public WorldConfig(File file) throws IOException { - final Map def = new HashMap(); - // "Before MySQL 5.1.6, database and table names cannot contain "/", "\", ".", or characters that are not permitted in file names" - MySQL manual - // They _can_ contain spaces, but replace them as well - def.put("table", "lb-" + file.getName().substring(0, file.getName().length() - 4).replaceAll("[ ./\\\\]", "_")); - for (final Logging l : Logging.values()) { - def.put("logging." + l.toString(), l.isDefaultEnabled()); - } - final YamlConfiguration config = YamlConfiguration.loadConfiguration(file); - for (final Entry e : def.entrySet()) { - if (config.get(e.getKey()) == null) { - config.set(e.getKey(), e.getValue()); - } - } - config.save(file); - table = config.getString("table"); - for (final Logging l : Logging.values()) { - setLogging(l, config.getBoolean("logging." + l.toString())); - } - } -} +package de.diddiz.LogBlock.config; + +import de.diddiz.LogBlock.LogBlock; +import de.diddiz.LogBlock.Logging; +import de.diddiz.LogBlock.util.BukkitUtils; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Animals; +import org.bukkit.entity.ArmorStand; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Monster; +import org.bukkit.entity.Player; +import org.bukkit.entity.WaterMob; + +import java.io.File; +import java.io.IOException; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.logging.Level; + +public class WorldConfig extends LoggingEnabledMapping { + public final String world; + public final String table; + public final String insertBlockStatementString; + public final String selectBlockActorIdStatementString; + public final String insertBlockStateStatementString; + public final String insertBlockChestDataStatementString; + public final String insertEntityStatementString; + public final String updateEntityUUIDString; + + private final EnumMap entityLogging = new EnumMap<>(EntityLogging.class); + public final boolean logNaturalEntitySpawns; + public final boolean logAllNamedEntityKills; + + public WorldConfig(String world, File file) throws IOException { + this.world = world; + final Map def = new HashMap<>(); + // "Before MySQL 5.1.6, database and table names cannot contain "/", "\", ".", or characters that are not permitted in file names" - MySQL manual + // They _can_ contain spaces, but replace them as well + def.put("table", "lb-" + file.getName().substring(0, file.getName().length() - 4).replaceAll("[ ./\\\\]", "_")); + for (final Logging l : Logging.values()) { + def.put("logging." + l.toString(), l.isDefaultEnabled()); + } + final YamlConfiguration config = YamlConfiguration.loadConfiguration(file); + for (final Entry e : def.entrySet()) { + if (config.get(e.getKey()) == null) { + config.set(e.getKey(), e.getValue()); + } + } + for (EntityLogging el : EntityLogging.values()) { + if (!(config.get("entity." + el.name().toLowerCase()) instanceof List)) { + config.set("entity." + el.name().toLowerCase(), el.getDefaultEnabled()); + } + entityLogging.put(el, new EntityLoggingList(el, config.getStringList("entity." + el.name().toLowerCase()))); + } + if (!config.isBoolean("entity.logNaturalSpawns")) { + config.set("entity.logNaturalSpawns", false); + } + logNaturalEntitySpawns = config.getBoolean("entity.logNaturalSpawns"); + + if (!config.isBoolean("entity.logAllNamedEntityKills")) { + config.set("entity.logAllNamedEntityKills", true); + } + logAllNamedEntityKills = config.getBoolean("entity.logAllNamedEntityKills"); + + config.save(file); + table = config.getString("table"); + for (final Logging l : Logging.values()) { + setLogging(l, config.getBoolean("logging." + l.toString())); + } + + insertBlockStatementString = "INSERT INTO `" + table + "-blocks` (date, playerid, replaced, replaceddata, type, typedata, x, y, z) VALUES (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?)"; + selectBlockActorIdStatementString = "SELECT playerid FROM `" + table + "-blocks` WHERE x = ? AND y = ? AND z = ? ORDER BY date DESC LIMIT 1"; + 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 = ?"; + } + + public boolean isLogging(EntityLogging logging, Entity entity) { + return entityLogging.get(logging).isLogging(entity); + } + + public boolean isLoggingAnyEntities() { + for (EntityLoggingList list : entityLogging.values()) { + if (list.isLoggingAnyEntities()) { + return true; + } + } + return false; + } + + private class EntityLoggingList { + private final EntityLogging entityAction; + private final HashSet logged = new HashSet<>(); + private final boolean logAll; + private final boolean logAnimals; + private final boolean logWateranimals; + private final boolean logMonsters; + private final boolean logLiving; + + public EntityLoggingList(EntityLogging entityAction, List types) { + this.entityAction = entityAction; + boolean all = false; + boolean animals = false; + boolean wateranimals = false; + boolean monsters = false; + boolean living = false; + for (String type : types) { + EntityType et = BukkitUtils.matchEntityType(type); + if (et != null) { + logged.add(et); + } else { + if (type.equalsIgnoreCase("all")) { + all = true; + } else if (type.equalsIgnoreCase("animal") || type.equalsIgnoreCase("animals")) { + animals = true; + } else if (type.equalsIgnoreCase("wateranimal") || type.equalsIgnoreCase("wateranimals")) { + wateranimals = true; + } else if (type.equalsIgnoreCase("monster") || type.equalsIgnoreCase("monsters")) { + monsters = true; + } else if (type.equalsIgnoreCase("living")) { + living = true; + } else { + LogBlock.getInstance().getLogger().log(Level.WARNING, "Unkown entity type in config for " + world + ": " + type); + } + } + } + logAll = all; + logAnimals = animals; + logWateranimals = wateranimals; + logMonsters = monsters; + logLiving = living; + } + + public boolean isLogging(Entity entity) { + if (entity == null || (entity instanceof Player)) { + return false; + } + EntityType type = entity.getType(); + if (logAll || logged.contains(type)) { + return true; + } + if (logLiving && LivingEntity.class.isAssignableFrom(entity.getClass()) && !(entity instanceof ArmorStand)) { + return true; + } + if (logAnimals && Animals.class.isAssignableFrom(entity.getClass())) { + return true; + } + if (logWateranimals && WaterMob.class.isAssignableFrom(entity.getClass())) { + return true; + } + if (logMonsters && (Monster.class.isAssignableFrom(entity.getClass()) || entity.getType() == EntityType.SLIME || entity.getType() == EntityType.WITHER || entity.getType() == EntityType.ENDER_DRAGON || entity.getType() == EntityType.SHULKER || entity.getType() == EntityType.GHAST)) { + return true; + } + if (entityAction == EntityLogging.DESTROY && logAllNamedEntityKills && entity.getCustomName() != null) { + return true; + } + return false; + } + + public boolean isLoggingAnyEntities() { + return logAll || logAnimals || logLiving || logMonsters || !logged.isEmpty() || (entityAction == EntityLogging.DESTROY && logAllNamedEntityKills); + } + } +} diff --git a/src/main/java/de/diddiz/LogBlock/events/BlockChangePreLogEvent.java b/src/main/java/de/diddiz/LogBlock/events/BlockChangePreLogEvent.java index 5decd897..efdf0266 100644 --- a/src/main/java/de/diddiz/LogBlock/events/BlockChangePreLogEvent.java +++ b/src/main/java/de/diddiz/LogBlock/events/BlockChangePreLogEvent.java @@ -2,123 +2,93 @@ import de.diddiz.LogBlock.Actor; import de.diddiz.LogBlock.ChestAccess; -import org.apache.commons.lang.Validate; + +import org.bukkit.Bukkit; import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.data.BlockData; +import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.event.HandlerList; public class BlockChangePreLogEvent extends PreLogEvent { private static final HandlerList handlers = new HandlerList(); private Location location; - private int typeBefore, typeAfter; - private byte data; - private String signText; + private BlockData typeBefore, typeAfter; private ChestAccess chestAccess; + private YamlConfiguration stateBefore; + private YamlConfiguration stateAfter; - public BlockChangePreLogEvent(Actor owner, Location location, int typeBefore, int typeAfter, byte data, - String signText, ChestAccess chestAccess) { - + public BlockChangePreLogEvent(Actor owner, Location location, BlockData typeBefore, BlockData typeAfter, YamlConfiguration stateBefore, YamlConfiguration stateAfter, ChestAccess chestAccess) { super(owner); this.location = location; this.typeBefore = typeBefore; this.typeAfter = typeAfter; - this.data = data; - this.signText = signText; + this.stateBefore = stateBefore; + this.stateAfter = stateAfter; this.chestAccess = chestAccess; } public Location getLocation() { - return location; } public void setLocation(Location location) { - this.location = location; } - public int getTypeBefore() { - + public BlockData getTypeBefore() { return typeBefore; } - public void setTypeBefore(int typeBefore) { - + public void setTypeBefore(BlockData typeBefore) { + if (typeBefore == null) { + typeBefore = Bukkit.createBlockData(Material.AIR); + } this.typeBefore = typeBefore; } - public int getTypeAfter() { - + public BlockData getTypeAfter() { return typeAfter; } - public void setTypeAfter(int typeAfter) { - + public void setTypeAfter(BlockData typeAfter) { + if (typeAfter == null) { + typeAfter = Bukkit.createBlockData(Material.AIR); + } this.typeAfter = typeAfter; } - public byte getData() { - - return data; + public YamlConfiguration getStateBefore() { + return stateBefore; } - public void setData(byte data) { - - this.data = data; + public YamlConfiguration getStateAfter() { + return stateAfter; } - public String getSignText() { - - return signText; + public void setStateBefore(YamlConfiguration stateBefore) { + this.stateBefore = stateBefore; } - public void setSignText(String[] signText) { - - if (signText != null) { - // Check for block - Validate.isTrue(isValidSign(), "Must be valid sign block"); - - // Check for problems - Validate.noNullElements(signText, "No null lines"); - Validate.isTrue(signText.length == 4, "Sign text must be 4 strings"); - - this.signText = signText[0] + "\0" + signText[1] + "\0" + signText[2] + "\0" + signText[3]; - } else { - this.signText = null; - } - } - - private boolean isValidSign() { - - if ((typeAfter == 63 || typeAfter == 68) && typeBefore == 0) { - return true; - } - if ((typeBefore == 63 || typeBefore == 68) && typeAfter == 0) { - return true; - } - if ((typeAfter == 63 || typeAfter == 68) && typeBefore == typeAfter) { - return true; - } - return false; + public void setStateAfter(YamlConfiguration stateAfter) { + this.stateAfter = stateAfter; } public ChestAccess getChestAccess() { - return chestAccess; } public void setChestAccess(ChestAccess chestAccess) { - this.chestAccess = chestAccess; } + @Override public HandlerList getHandlers() { - return handlers; } public static HandlerList getHandlerList() { - return handlers; } } diff --git a/src/main/java/de/diddiz/LogBlock/events/EntityChangePreLogEvent.java b/src/main/java/de/diddiz/LogBlock/events/EntityChangePreLogEvent.java new file mode 100644 index 00000000..061aaa9e --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/events/EntityChangePreLogEvent.java @@ -0,0 +1,54 @@ +package de.diddiz.LogBlock.events; + +import de.diddiz.LogBlock.Actor; +import de.diddiz.LogBlock.EntityChange.EntityChangeType; +import org.bukkit.Location; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Entity; +import org.bukkit.event.HandlerList; + +public class EntityChangePreLogEvent extends PreLogEvent { + + private static final HandlerList handlers = new HandlerList(); + private Location location; + private Entity entity; + private EntityChangeType changeType; + private YamlConfiguration changeData; + + public EntityChangePreLogEvent(Actor owner, Location location, Entity entity, EntityChangeType changeType, YamlConfiguration changeData) { + super(owner); + this.location = location; + this.entity = entity; + this.changeType = changeType; + this.changeData = changeData; + } + + public Location getLocation() { + return location; + } + + public void setLocation(Location location) { + this.location = location; + } + + public Entity getEntity() { + return entity; + } + + public EntityChangeType getChangeType() { + return changeType; + } + + public YamlConfiguration getChangeData() { + return changeData; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } +} diff --git a/src/main/java/de/diddiz/LogBlock/events/PreLogEvent.java b/src/main/java/de/diddiz/LogBlock/events/PreLogEvent.java index 51a7a228..109aed59 100644 --- a/src/main/java/de/diddiz/LogBlock/events/PreLogEvent.java +++ b/src/main/java/de/diddiz/LogBlock/events/PreLogEvent.java @@ -10,7 +10,6 @@ public abstract class PreLogEvent extends Event implements Cancellable { protected Actor owner; public PreLogEvent(Actor owner) { - this.owner = owner; } @@ -21,8 +20,8 @@ public PreLogEvent(Actor owner) { * @deprecated {@link #getOwnerActor() } returns an object encapsulating * name and uuid. Names are not guaranteed to be unique. */ + @Deprecated public String getOwner() { - return owner.getName(); } @@ -41,17 +40,16 @@ public Actor getOwnerActor() { * @param owner The player/monster/cause who is involved in this event */ public void setOwner(Actor owner) { - this.owner = owner; } + @Override public boolean isCancelled() { - return cancelled; } + @Override public void setCancelled(boolean cancelled) { - this.cancelled = cancelled; } } diff --git a/src/main/java/de/diddiz/LogBlock/events/ToolUseEvent.java b/src/main/java/de/diddiz/LogBlock/events/ToolUseEvent.java new file mode 100644 index 00000000..cb6c0833 --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/events/ToolUseEvent.java @@ -0,0 +1,59 @@ +package de.diddiz.LogBlock.events; + +import de.diddiz.LogBlock.QueryParams; +import de.diddiz.LogBlock.Tool; +import de.diddiz.LogBlock.ToolBehavior; +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.HandlerList; +import org.bukkit.event.player.PlayerEvent; + +/** + * Fired whether a tool is about to be used by a player. + */ +public class ToolUseEvent extends PlayerEvent implements Cancellable { + + private static final HandlerList handlers = new HandlerList(); + private boolean cancel; + private final Tool tool; + private final ToolBehavior behavior; + private final QueryParams params; + + public ToolUseEvent(Player who, Tool tool, ToolBehavior behavior, QueryParams params) { + super(who); + this.tool = tool; + this.behavior = behavior; + this.params = params; + } + + @Override + public boolean isCancelled() { + return cancel; + } + + @Override + public void setCancelled(boolean cancel) { + this.cancel = cancel; + } + + public Tool getTool() { + return tool; + } + + public ToolBehavior getBehavior() { + return behavior; + } + + public QueryParams getParams() { + return params; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } +} diff --git a/src/main/java/de/diddiz/LogBlock/listeners/AdvancedEntityLogging.java b/src/main/java/de/diddiz/LogBlock/listeners/AdvancedEntityLogging.java new file mode 100644 index 00000000..0b85194f --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/listeners/AdvancedEntityLogging.java @@ -0,0 +1,327 @@ +package de.diddiz.LogBlock.listeners; + +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.block.BlockFace; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.ArmorStand; +import org.bukkit.entity.Bee; +import org.bukkit.entity.EnderCrystal; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Hanging; +import org.bukkit.entity.IronGolem; +import org.bukkit.entity.ItemFrame; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.entity.Snowman; +import org.bukkit.entity.Wither; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.block.Action; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.entity.CreatureSpawnEvent; +import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.event.entity.EntityPlaceEvent; +import org.bukkit.event.hanging.HangingBreakByEntityEvent; +import org.bukkit.event.hanging.HangingBreakEvent; +import org.bukkit.event.hanging.HangingPlaceEvent; +import org.bukkit.event.player.PlayerArmorStandManipulateEvent; +import org.bukkit.event.player.PlayerInteractEntityEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.ItemStack; +import org.bukkit.scheduler.BukkitRunnable; + +import de.diddiz.LogBlock.Actor; +import de.diddiz.LogBlock.EntityChange; +import de.diddiz.LogBlock.EntityChange.EntityChangeType; +import de.diddiz.LogBlock.LogBlock; +import de.diddiz.LogBlock.config.Config; +import de.diddiz.LogBlock.config.EntityLogging; +import de.diddiz.LogBlock.util.LoggingUtil; +import de.diddiz.LogBlock.worldedit.WorldEditHelper; +import java.util.UUID; + +public class AdvancedEntityLogging extends LoggingListener { + + private Player lastSpawner; + private Class lastSpawning; + private boolean lastSpawnerEgg; + + // serialize them before the death event + private UUID lastEntityDamagedForDeathUUID; + private byte[] lastEntityDamagedForDeathSerialized; + private Entity lastEntityDamagedForDeathDamager; + + public AdvancedEntityLogging(LogBlock lb) { + super(lb); + new BukkitRunnable() { + @Override + public void run() { + resetOnTick(); + } + }.runTaskTimer(lb, 1, 1); + } + + private void resetOnTick() { + lastSpawner = null; + lastSpawning = null; + lastSpawnerEgg = false; + lastEntityDamagedForDeathUUID = null; + lastEntityDamagedForDeathSerialized = null; + lastEntityDamagedForDeathDamager = null; + } + + private void setLastSpawner(Player player, Class spawning, boolean spawnEgg) { + lastSpawner = player; + lastSpawning = spawning; + lastSpawnerEgg = spawnEgg; + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerBlockPlace(BlockPlaceEvent event) { + Material placed = event.getBlock().getType(); + if (placed == Material.WITHER_SKELETON_SKULL) { + setLastSpawner(event.getPlayer(), Wither.class, false); + } else if (placed == Material.CARVED_PUMPKIN) { + Material below = event.getBlock().getRelative(BlockFace.DOWN).getType(); + if (below == Material.SNOW_BLOCK) { + setLastSpawner(event.getPlayer(), Snowman.class, false); + } else if (below == Material.IRON_BLOCK) { + setLastSpawner(event.getPlayer(), IronGolem.class, false); + } + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerInteract(PlayerInteractEvent event) { + if (event.getAction() == Action.RIGHT_CLICK_BLOCK) { + ItemStack inHand = event.getItem(); + if (inHand != null) { + Material mat = inHand.getType(); + if (mat == Material.ARMOR_STAND) { + setLastSpawner(event.getPlayer(), ArmorStand.class, false); + } else if (mat.name().endsWith("_SPAWN_EGG")) { + setLastSpawner(event.getPlayer(), null, true); + } + } + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerInteractEntity(PlayerInteractEntityEvent event) { + ItemStack inHand = event.getHand() == EquipmentSlot.HAND ? event.getPlayer().getInventory().getItemInMainHand() : event.getPlayer().getInventory().getItemInOffHand(); + if (inHand != null && inHand.getType() != Material.AIR) { + Material mat = inHand.getType(); + if (mat.name().endsWith("_SPAWN_EGG")) { + setLastSpawner(event.getPlayer(), null, true); + } + + Entity entity = event.getRightClicked(); + if (entity instanceof ItemFrame) { + ItemStack oldItem = ((ItemFrame) entity).getItem(); + if (oldItem == null || oldItem.getType() == Material.AIR) { + if (Config.isLogging(entity.getWorld(), EntityLogging.MODIFY, entity)) { + Actor actor = Actor.actorFromEntity(event.getPlayer()); + YamlConfiguration data = new YamlConfiguration(); + inHand = inHand.clone(); + inHand.setAmount(1); + data.set("item", inHand); + consumer.queueEntityModification(actor, entity, EntityChange.EntityChangeType.ADDEQUIP, data); + } + } + } + } + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onEntitySpawn(CreatureSpawnEvent event) { + if (!event.isCancelled()) { + if (event.getSpawnReason() == SpawnReason.CUSTOM || event.getSpawnReason() == SpawnReason.BEEHIVE) { + return; + } + if (event.getEntityType() == EntityType.ARMOR_STAND) { + resetOnTick(); + return; // logged in the method below + } + LivingEntity entity = event.getEntity(); + if (Config.isLogging(entity.getWorld(), EntityLogging.SPAWN, entity)) { + Actor actor = null; + if (lastSpawner != null && lastSpawner.getWorld() == entity.getWorld() && lastSpawner.getLocation().distance(entity.getLocation()) < 10) { + if (lastSpawnerEgg && event.getSpawnReason() == SpawnReason.SPAWNER_EGG) { + actor = Actor.actorFromEntity(lastSpawner); + } else if (lastSpawning != null && lastSpawning.isAssignableFrom(entity.getClass())) { + actor = Actor.actorFromEntity(lastSpawner); + } + } + if (actor == null) { + if (event.getSpawnReason() == SpawnReason.NATURAL && !Config.isLoggingNatualSpawns(entity.getWorld())) { + return; + } + actor = new Actor(event.getSpawnReason().toString()); + } + queueEntitySpawnOrKill(entity, actor, EntityChange.EntityChangeType.CREATE); + } + } + resetOnTick(); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onEntityPlace(EntityPlaceEvent event) { + if (!event.isCancelled()) { + Entity entity = event.getEntity(); + if (Config.isLogging(entity.getWorld(), EntityLogging.SPAWN, entity)) { + Actor actor = null; + if (event.getPlayer() != null) { + actor = Actor.actorFromEntity(event.getPlayer()); + } + if (actor == null) { + actor = new Actor("UNKNOWN"); + } + queueEntitySpawnOrKill(entity, actor, EntityChange.EntityChangeType.CREATE); + } + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onEntityDeath(EntityDeathEvent event) { + LivingEntity entity = event.getEntity(); + if (Config.isLogging(entity.getWorld(), EntityLogging.DESTROY, entity)) { + Actor actor = null; + Entity cause = event.getDamageSource().getCausingEntity(); + Entity damager = LoggingUtil.getRealDamager(cause); + if (damager != null) { + actor = Actor.actorFromEntity(damager); + } + if (actor == null && entity.getKiller() != null) { + actor = Actor.actorFromEntity(entity.getKiller()); + } + if (actor == null) { + NamespacedKey key = event.getDamageSource().getDamageType().getKey(); + actor = new Actor(key == null ? "unknown" : key.getKey().toUpperCase()); + } + queueEntitySpawnOrKill(entity, actor, EntityChange.EntityChangeType.KILL); + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onHangingPlace(HangingPlaceEvent event) { + Hanging entity = event.getEntity(); + if (Config.isLogging(entity.getWorld(), EntityLogging.SPAWN, entity)) { + Actor actor = Actor.actorFromEntity(event.getPlayer()); + queueEntitySpawnOrKill(entity, actor, EntityChange.EntityChangeType.CREATE); + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onHangingBreak(HangingBreakEvent event) { + Entity entity = event.getEntity(); + if (Config.isLogging(entity.getWorld(), EntityLogging.DESTROY, entity)) { + Actor actor; + if (event instanceof HangingBreakByEntityEvent) { + Entity damager = LoggingUtil.getRealDamager(((HangingBreakByEntityEvent) event).getRemover()); + actor = Actor.actorFromEntity(damager); + } else { + actor = new Actor(event.getCause().toString()); + } + queueEntitySpawnOrKill(entity, actor, EntityChange.EntityChangeType.KILL); + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onEntityDamage(EntityDamageEvent event) { + Entity entity = event.getEntity(); + if (entity instanceof ItemFrame) { + ItemStack oldItem = ((ItemFrame) entity).getItem(); + if (oldItem != null && oldItem.getType() != Material.AIR) { + if (Config.isLogging(entity.getWorld(), EntityLogging.MODIFY, entity)) { + Actor actor; + if (event instanceof EntityDamageByEntityEvent) { + Entity damager = LoggingUtil.getRealDamager(((EntityDamageByEntityEvent) event).getDamager()); + actor = Actor.actorFromEntity(damager); + } else { + actor = new Actor(event.getCause().toString()); + } + YamlConfiguration data = new YamlConfiguration(); + data.set("item", oldItem); + consumer.queueEntityModification(actor, entity, EntityChange.EntityChangeType.REMOVEEQUIP, data); + } + } + } + if (Config.isLogging(entity.getWorld(), EntityLogging.DESTROY, entity)) { + lastEntityDamagedForDeathUUID = entity.getUniqueId(); + lastEntityDamagedForDeathSerialized = WorldEditHelper.serializeEntity(entity); + } + if (entity instanceof EnderCrystal) { + if (Config.isLogging(entity.getWorld(), EntityLogging.DESTROY, entity)) { + if (event instanceof EntityDamageByEntityEvent) { + Entity damager = LoggingUtil.getRealDamager(((EntityDamageByEntityEvent) event).getDamager()); + if (lastEntityDamagedForDeathDamager == null || !(damager instanceof EnderCrystal)) { + lastEntityDamagedForDeathDamager = damager; + } + } + Actor actor = lastEntityDamagedForDeathDamager != null ? Actor.actorFromEntity(lastEntityDamagedForDeathDamager) : new Actor(event.getCause().toString()); + queueEntitySpawnOrKill(entity, actor, EntityChange.EntityChangeType.KILL); + } + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onEntityDamageByEntity(EntityDamageByEntityEvent event) { + Entity damager = event.getDamager(); + if (damager instanceof Bee && !((Bee) damager).hasStung()) { + if (Config.isLogging(damager.getWorld(), EntityLogging.MODIFY, damager)) { + Actor actor = Actor.actorFromEntity(event.getEntity()); + consumer.queueEntityModification(actor, damager, EntityChange.EntityChangeType.GET_STUNG, null); + } + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerArmorStandManipulate(PlayerArmorStandManipulateEvent event) { + ArmorStand entity = event.getRightClicked(); + ItemStack oldItem = event.getArmorStandItem(); + ItemStack newItem = event.getPlayerItem(); + boolean oldEmpty = oldItem == null || oldItem.getType() == Material.AIR; + boolean newEmpty = newItem == null || newItem.getType() == Material.AIR; + if ((!oldEmpty || !newEmpty) && Config.isLogging(entity.getWorld(), EntityLogging.MODIFY, entity)) { + Actor actor = Actor.actorFromEntity(event.getPlayer()); + if (!oldEmpty && !newEmpty && newItem.getAmount() > 1) { + return; + } + if (!oldEmpty) { + YamlConfiguration data = new YamlConfiguration(); + data.set("item", oldItem); + data.set("slot", event.getSlot().name()); + consumer.queueEntityModification(actor, entity, EntityChange.EntityChangeType.REMOVEEQUIP, data); + } + if (!newEmpty) { + YamlConfiguration data = new YamlConfiguration(); + data.set("item", newItem); + data.set("slot", event.getSlot().name()); + consumer.queueEntityModification(actor, entity, EntityChange.EntityChangeType.ADDEQUIP, data); + } + } + } + + protected void queueEntitySpawnOrKill(Entity entity, Actor actor, EntityChange.EntityChangeType changeType) { + 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()); + if (changeType == EntityChangeType.KILL && entity.getUniqueId().equals(lastEntityDamagedForDeathUUID)) { + data.set("worldedit", lastEntityDamagedForDeathSerialized); + } else { + data.set("worldedit", WorldEditHelper.serializeEntity(entity)); + } + consumer.queueEntityModification(actor, entity, changeType, data); + } +} diff --git a/src/main/java/de/diddiz/LogBlock/listeners/BanListener.java b/src/main/java/de/diddiz/LogBlock/listeners/BanListener.java index f7c29693..63ab4acd 100644 --- a/src/main/java/de/diddiz/LogBlock/listeners/BanListener.java +++ b/src/main/java/de/diddiz/LogBlock/listeners/BanListener.java @@ -1,48 +1,48 @@ -package de.diddiz.LogBlock.listeners; - -import de.diddiz.LogBlock.CommandsHandler; -import de.diddiz.LogBlock.LogBlock; -import de.diddiz.LogBlock.QueryParams; -import org.bukkit.World; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerCommandPreprocessEvent; - -import static de.diddiz.LogBlock.config.Config.banPermission; -import static de.diddiz.LogBlock.config.Config.isLogged; -import static org.bukkit.Bukkit.getScheduler; - -public class BanListener implements Listener { - private final CommandsHandler handler; - private final LogBlock logblock; - - public BanListener(LogBlock logblock) { - this.logblock = logblock; - handler = logblock.getCommandsHandler(); - } - - @EventHandler - public void onPlayerCommandPreprocess(final PlayerCommandPreprocessEvent event) { - final String[] split = event.getMessage().split(" "); - if (split.length > 1 && split[0].equalsIgnoreCase("/ban") && logblock.hasPermission(event.getPlayer(), banPermission)) { - final QueryParams p = new QueryParams(logblock); - p.setPlayer(split[1].equalsIgnoreCase("g") ? split[2] : split[1]); - p.since = 0; - p.silent = false; - getScheduler().scheduleAsyncDelayedTask(logblock, new Runnable() { - @Override - public void run() { - for (final World world : logblock.getServer().getWorlds()) { - if (isLogged(world)) { - p.world = world; - try { - handler.new CommandRollback(event.getPlayer(), p, false); - } catch (final Exception ex) { - } - } - } - } - }); - } - } -} +package de.diddiz.LogBlock.listeners; + +import de.diddiz.LogBlock.CommandsHandler; +import de.diddiz.LogBlock.LogBlock; +import de.diddiz.LogBlock.QueryParams; +import org.bukkit.World; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerCommandPreprocessEvent; + +import static de.diddiz.LogBlock.config.Config.banPermission; +import static de.diddiz.LogBlock.config.Config.isLogged; +import static org.bukkit.Bukkit.getScheduler; + +public class BanListener implements Listener { + private final CommandsHandler handler; + private final LogBlock logblock; + + public BanListener(LogBlock logblock) { + this.logblock = logblock; + handler = logblock.getCommandsHandler(); + } + + @EventHandler + public void onPlayerCommandPreprocess(final PlayerCommandPreprocessEvent event) { + final String[] split = event.getMessage().split(" "); + if (split.length > 1 && split[0].equalsIgnoreCase("/ban") && logblock.hasPermission(event.getPlayer(), banPermission)) { + final QueryParams p = new QueryParams(logblock); + p.setPlayer(split[1].equalsIgnoreCase("g") ? split[2] : split[1]); + p.since = 0; + p.silent = false; + getScheduler().runTaskAsynchronously(logblock, new Runnable() { + @Override + public void run() { + for (final World world : logblock.getServer().getWorlds()) { + if (isLogged(world)) { + p.world = world; + try { + handler.new CommandRollback(event.getPlayer(), p, false); + } catch (final Exception ex) { + } + } + } + } + }); + } + } +} diff --git a/src/main/java/de/diddiz/LogBlock/listeners/BlockBreakLogging.java b/src/main/java/de/diddiz/LogBlock/listeners/BlockBreakLogging.java index b27db533..018cffa8 100644 --- a/src/main/java/de/diddiz/LogBlock/listeners/BlockBreakLogging.java +++ b/src/main/java/de/diddiz/LogBlock/listeners/BlockBreakLogging.java @@ -1,64 +1,88 @@ -package de.diddiz.LogBlock.listeners; - -import de.diddiz.LogBlock.Actor; -import de.diddiz.LogBlock.LogBlock; -import de.diddiz.LogBlock.Logging; -import de.diddiz.LogBlock.config.WorldConfig; -import de.diddiz.util.BukkitUtils; -import org.bukkit.GameMode; -import org.bukkit.Material; -import org.bukkit.block.Block; -import org.bukkit.block.Sign; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.block.BlockBreakEvent; -import org.bukkit.event.player.PlayerBucketFillEvent; - -import static de.diddiz.LogBlock.config.Config.getWorldConfig; -import static de.diddiz.LogBlock.config.Config.isLogging; -import static de.diddiz.util.LoggingUtil.smartLogBlockBreak; -import static de.diddiz.util.LoggingUtil.smartLogFallables; - -public class BlockBreakLogging extends LoggingListener { - public BlockBreakLogging(LogBlock lb) { - super(lb); - } - - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onBlockBreak(BlockBreakEvent event) { - if (isLogging(event.getBlock().getWorld(), Logging.BLOCKBREAK)) { - WorldConfig wcfg = getWorldConfig(event.getBlock().getWorld()); - if (wcfg == null) { - return; - } - - final Actor actor = Actor.actorFromEntity(event.getPlayer()); - final Block origin = event.getBlock(); - final int typeId = origin.getTypeId(); - final Material type = origin.getType(); - - if (wcfg.isLogging(Logging.SIGNTEXT) && (typeId == 63 || typeId == 68)) { - consumer.queueSignBreak(actor, (Sign) origin.getState()); - } else if (wcfg.isLogging(Logging.CHESTACCESS) && BukkitUtils.getContainerBlocks().contains(type)) { - consumer.queueContainerBreak(actor, origin.getState()); - } else if (type == Material.ICE) { - // When in creative mode ice doesn't form water - if (event.getPlayer().getGameMode().equals(GameMode.CREATIVE)) { - consumer.queueBlockBreak(actor, origin.getState()); - } else { - consumer.queueBlockReplace(actor, origin.getState(), 9, (byte) 0); - } - } else { - smartLogBlockBreak(consumer, actor, origin); - } - smartLogFallables(consumer, actor, origin); - } - } - - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onPlayerBucketFill(PlayerBucketFillEvent event) { - if (isLogging(event.getBlockClicked().getWorld(), Logging.BLOCKBREAK)) { - consumer.queueBlockBreak(Actor.actorFromEntity(event.getPlayer()), event.getBlockClicked().getState()); - } - } -} +package de.diddiz.LogBlock.listeners; + +import de.diddiz.LogBlock.Actor; +import de.diddiz.LogBlock.LogBlock; +import de.diddiz.LogBlock.Logging; +import de.diddiz.LogBlock.config.WorldConfig; +import de.diddiz.LogBlock.util.BukkitUtils; +import org.bukkit.Bukkit; +import org.bukkit.GameMode; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.Waterlogged; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockDropItemEvent; +import org.bukkit.event.player.PlayerBucketFillEvent; + +import static de.diddiz.LogBlock.config.Config.getWorldConfig; +import static de.diddiz.LogBlock.config.Config.isLogging; +import static de.diddiz.LogBlock.util.LoggingUtil.smartLogBlockBreak; +import static de.diddiz.LogBlock.util.LoggingUtil.smartLogBlockReplace; +import static de.diddiz.LogBlock.util.LoggingUtil.smartLogFallables; + +public class BlockBreakLogging extends LoggingListener { + public BlockBreakLogging(LogBlock lb) { + super(lb); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockBreak(BlockBreakEvent event) { + if (isLogging(event.getBlock().getWorld(), Logging.BLOCKBREAK)) { + WorldConfig wcfg = getWorldConfig(event.getBlock().getWorld()); + if (wcfg == null) { + return; + } + + final Actor actor = Actor.actorFromEntity(event.getPlayer()); + final Block origin = event.getBlock(); + final Material type = origin.getType(); + + if (wcfg.isLogging(Logging.CHESTACCESS) && BukkitUtils.isContainerBlock(type) && !BukkitUtils.isShulkerBoxBlock(type)) { + consumer.queueContainerBreak(actor, origin.getState()); + } else if (type == Material.ICE) { + // When in creative mode ice doesn't form water + if (event.getPlayer().getGameMode().equals(GameMode.CREATIVE)) { + smartLogBlockBreak(consumer, actor, origin); + } else { + smartLogBlockReplace(consumer, actor, origin, Bukkit.createBlockData(Material.WATER)); + } + } else { + smartLogBlockBreak(consumer, actor, origin); + } + smartLogFallables(consumer, actor, origin); + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerBucketFill(PlayerBucketFillEvent event) { + if (isLogging(event.getBlockClicked().getWorld(), Logging.BLOCKBREAK)) { + BlockData clickedBlockData = event.getBlockClicked().getBlockData(); + if (clickedBlockData instanceof Waterlogged) { + Waterlogged clickedWaterlogged = (Waterlogged) clickedBlockData; + if (clickedWaterlogged.isWaterlogged()) { + Waterlogged clickedWaterloggedWithoutWater = (Waterlogged) clickedWaterlogged.clone(); + clickedWaterloggedWithoutWater.setWaterlogged(false); + consumer.queueBlockReplace(Actor.actorFromEntity(event.getPlayer()), event.getBlockClicked().getLocation(), clickedWaterlogged, clickedWaterloggedWithoutWater); + } + } else { + consumer.queueBlockBreak(Actor.actorFromEntity(event.getPlayer()), event.getBlockClicked().getState()); + } + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockDropItem(BlockDropItemEvent event) { + if (isLogging(event.getBlock().getWorld(), Logging.BLOCKBREAK)) { + Material type = event.getBlock().getType(); + if (type == Material.SUSPICIOUS_GRAVEL || type == Material.SUSPICIOUS_SAND) { + Material simplyBroken = type == Material.SUSPICIOUS_SAND ? Material.SAND : Material.GRAVEL; + if (event.getItems().size() != 1 || event.getItems().get(0).getItemStack().getType() != simplyBroken) { + consumer.queueBlockReplace(Actor.actorFromEntity(event.getPlayer()), event.getBlockState(), simplyBroken.createBlockData()); + } + } + } + } +} diff --git a/src/main/java/de/diddiz/LogBlock/listeners/BlockBurnLogging.java b/src/main/java/de/diddiz/LogBlock/listeners/BlockBurnLogging.java index b0571cde..1ee7ed1f 100644 --- a/src/main/java/de/diddiz/LogBlock/listeners/BlockBurnLogging.java +++ b/src/main/java/de/diddiz/LogBlock/listeners/BlockBurnLogging.java @@ -1,44 +1,72 @@ -package de.diddiz.LogBlock.listeners; - -import de.diddiz.LogBlock.Actor; -import de.diddiz.LogBlock.LogBlock; -import de.diddiz.LogBlock.Logging; -import org.bukkit.Material; -import org.bukkit.block.Block; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.block.Action; -import org.bukkit.event.block.BlockBurnEvent; -import org.bukkit.event.player.PlayerInteractEvent; - -import static de.diddiz.LogBlock.config.Config.isLogging; -import static de.diddiz.util.LoggingUtil.smartLogBlockBreak; -import static de.diddiz.util.LoggingUtil.smartLogFallables; - -public class BlockBurnLogging extends LoggingListener { - public BlockBurnLogging(LogBlock lb) { - super(lb); - } - - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onBlockBurn(BlockBurnEvent event) { - if (isLogging(event.getBlock().getWorld(), Logging.FIRE)) { - smartLogBlockBreak(consumer, new Actor("Fire"), event.getBlock()); - smartLogFallables(consumer, new Actor("Fire"), event.getBlock()); - } - } - - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onExtinguish(PlayerInteractEvent event) { - if (event.getAction().equals(Action.LEFT_CLICK_BLOCK)) { - Player player = event.getPlayer(); - Block block = event.getClickedBlock().getRelative(event.getBlockFace()); - if (block.getType().equals(Material.FIRE) && isLogging(player.getWorld(), Logging.FIRE)) { - Actor actor = Actor.actorFromEntity(player); - smartLogBlockBreak(consumer, actor, block); - smartLogFallables(consumer, actor, block); - } - } - } -} +package de.diddiz.LogBlock.listeners; + +import de.diddiz.LogBlock.Actor; +import de.diddiz.LogBlock.LogBlock; +import de.diddiz.LogBlock.Logging; +import de.diddiz.LogBlock.config.Config; + +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.block.Action; +import org.bukkit.event.block.BlockBurnEvent; +import org.bukkit.event.block.BlockIgniteEvent; +import org.bukkit.event.block.BlockIgniteEvent.IgniteCause; +import org.bukkit.event.player.PlayerInteractEvent; + +import static de.diddiz.LogBlock.config.Config.isLogging; +import static de.diddiz.LogBlock.util.LoggingUtil.smartLogBlockBreak; +import static de.diddiz.LogBlock.util.LoggingUtil.smartLogBlockReplace; +import static de.diddiz.LogBlock.util.LoggingUtil.smartLogFallables; + +public class BlockBurnLogging extends LoggingListener { + public BlockBurnLogging(LogBlock lb) { + super(lb); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockBurn(BlockBurnEvent event) { + if (isLogging(event.getBlock().getWorld(), Logging.FIRE)) { + smartLogBlockReplace(consumer, new Actor("Fire", Config.logFireSpreadAsPlayerWhoCreatedIt ? event.getIgnitingBlock() : null), event.getBlock(), Material.FIRE.createBlockData()); + smartLogFallables(consumer, new Actor("Fire", Config.logFireSpreadAsPlayerWhoCreatedIt ? event.getIgnitingBlock() : null), event.getBlock()); + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockIgnite(BlockIgniteEvent event) { + Actor actor = new Actor("Fire", Config.logFireSpreadAsPlayerWhoCreatedIt ? event.getIgnitingBlock() : null); + if (event.getCause() == IgniteCause.FLINT_AND_STEEL) { + if (event.getIgnitingEntity() != null) { + return; // handled in block place + } else { + actor = new Actor("Dispenser"); + } + } else if (event.getCause() == IgniteCause.LIGHTNING) { + actor = new Actor("Lightning"); + } else if (event.getCause() == IgniteCause.EXPLOSION) { + actor = new Actor("Explosion"); + } else if (event.getCause() == IgniteCause.LAVA) { + actor = new Actor("Lava"); + } else if (event.getCause() == IgniteCause.ENDER_CRYSTAL) { + actor = new Actor("EnderCrystal"); + } + if (isLogging(event.getBlock().getWorld(), Logging.FIRE)) { + consumer.queueBlockPlace(actor, event.getBlock().getLocation(), Material.FIRE.createBlockData()); + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onExtinguish(PlayerInteractEvent event) { + if (event.getAction().equals(Action.LEFT_CLICK_BLOCK)) { + Player player = event.getPlayer(); + Block block = event.getClickedBlock().getRelative(event.getBlockFace()); + if (block.getType().equals(Material.FIRE) && isLogging(player.getWorld(), Logging.FIRE)) { + Actor actor = Actor.actorFromEntity(player); + smartLogBlockBreak(consumer, actor, block); + smartLogFallables(consumer, actor, block); + } + } + } +} diff --git a/src/main/java/de/diddiz/LogBlock/listeners/BlockFertilizeLogging.java b/src/main/java/de/diddiz/LogBlock/listeners/BlockFertilizeLogging.java new file mode 100644 index 00000000..b77ebcdf --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/listeners/BlockFertilizeLogging.java @@ -0,0 +1,36 @@ +package de.diddiz.LogBlock.listeners; + +import de.diddiz.LogBlock.Actor; +import de.diddiz.LogBlock.LogBlock; +import de.diddiz.LogBlock.Logging; +import de.diddiz.LogBlock.config.WorldConfig; +import org.bukkit.block.BlockState; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.block.BlockFertilizeEvent; +import static de.diddiz.LogBlock.config.Config.getWorldConfig; + +public class BlockFertilizeLogging extends LoggingListener { + public BlockFertilizeLogging(LogBlock lb) { + super(lb); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockFertilize(BlockFertilizeEvent event) { + final WorldConfig wcfg = getWorldConfig(event.getBlock().getLocation().getWorld()); + if (wcfg != null) { + if (!wcfg.isLogging(Logging.BONEMEALSTRUCTUREGROW)) { + return; + } + final Actor actor; + if (event.getPlayer() != null) { + actor = Actor.actorFromEntity(event.getPlayer()); + } else { + actor = new Actor("Dispenser"); + } + for (final BlockState state : event.getBlocks()) { + consumer.queueBlockReplace(actor, state.getBlock().getState(), state); + } + } + } +} diff --git a/src/main/java/de/diddiz/LogBlock/listeners/BlockPlaceLogging.java b/src/main/java/de/diddiz/LogBlock/listeners/BlockPlaceLogging.java index c5325379..e6f72610 100644 --- a/src/main/java/de/diddiz/LogBlock/listeners/BlockPlaceLogging.java +++ b/src/main/java/de/diddiz/LogBlock/listeners/BlockPlaceLogging.java @@ -1,92 +1,67 @@ -package de.diddiz.LogBlock.listeners; - -import de.diddiz.LogBlock.Actor; -import de.diddiz.LogBlock.LogBlock; -import de.diddiz.LogBlock.Logging; -import de.diddiz.LogBlock.config.WorldConfig; -import de.diddiz.util.BukkitUtils; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.block.BlockFace; -import org.bukkit.block.BlockState; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.block.BlockPlaceEvent; -import org.bukkit.event.player.PlayerBucketEmptyEvent; - -import static de.diddiz.LogBlock.config.Config.getWorldConfig; -import static de.diddiz.LogBlock.config.Config.isLogging; - -public class BlockPlaceLogging extends LoggingListener { - public BlockPlaceLogging(LogBlock lb) { - super(lb); - } - - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onBlockPlace(BlockPlaceEvent event) { - final WorldConfig wcfg = getWorldConfig(event.getBlock().getWorld()); - if (wcfg != null && wcfg.isLogging(Logging.BLOCKPLACE)) { - final Material type = event.getBlock().getType(); - final BlockState before = event.getBlockReplacedState(); - final BlockState after = event.getBlockPlaced().getState(); - final Actor actor = Actor.actorFromEntity(event.getPlayer()); - - //Handle falling blocks - if (BukkitUtils.getRelativeTopFallables().contains(type)) { - - // Catch placed blocks overwriting something - if (before.getType() != Material.AIR) { - consumer.queueBlockBreak(actor, before); - } - - Location loc = event.getBlock().getLocation(); - int x = loc.getBlockX(); - int y = loc.getBlockY(); - int z = loc.getBlockZ(); - // Blocks only fall if they have a chance to start a velocity - if (event.getBlock().getRelative(BlockFace.DOWN).getType() == Material.AIR) { - while (y > 0 && BukkitUtils.canFall(loc.getWorld(), x, (y - 1), z)) { - y--; - } - } - // If y is 0 then the sand block fell out of the world :( - if (y != 0) { - Location finalLoc = new Location(loc.getWorld(), x, y, z); - // Run this check to avoid false positives - if (!BukkitUtils.getFallingEntityKillers().contains(finalLoc.getBlock().getType())) { - if (finalLoc.getBlock().getType() == Material.AIR || finalLoc.equals(event.getBlock().getLocation())) { - consumer.queueBlockPlace(actor, finalLoc, type.getId(), event.getBlock().getData()); - } else { - consumer.queueBlockReplace(actor, finalLoc, finalLoc.getBlock().getTypeId(), finalLoc.getBlock().getData(), type.getId(), event.getBlock().getData()); - } - } - } - return; - } - - //Sign logging is handled elsewhere - if (wcfg.isLogging(Logging.SIGNTEXT) && (type.getId() == 63 || type.getId() == 68)) { - return; - } - - //Delay queuing by one tick to allow data to be updated - LogBlock.getInstance().getServer().getScheduler().scheduleSyncDelayedTask(LogBlock.getInstance(), new Runnable() { - @Override - public void run() { - if (before.getTypeId() == 0) { - consumer.queueBlockPlace(actor, after); - } else { - consumer.queueBlockReplace(actor, before, after); - } - } - }, 1L); - } - } - - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onPlayerBucketEmpty(PlayerBucketEmptyEvent event) { - if (isLogging(event.getPlayer().getWorld(), Logging.BLOCKPLACE)) { - consumer.queueBlockPlace(Actor.actorFromEntity(event.getPlayer()), event.getBlockClicked().getRelative(event.getBlockFace()).getLocation(), event.getBucket() == Material.WATER_BUCKET ? 9 : 11, (byte) 0); - } - } -} +package de.diddiz.LogBlock.listeners; + +import de.diddiz.LogBlock.Actor; +import de.diddiz.LogBlock.LogBlock; +import de.diddiz.LogBlock.Logging; +import de.diddiz.LogBlock.config.Config; +import de.diddiz.LogBlock.util.LoggingUtil; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.Waterlogged; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.player.PlayerBucketEmptyEvent; + +import static de.diddiz.LogBlock.config.Config.isLogging; + +public class BlockPlaceLogging extends LoggingListener { + public BlockPlaceLogging(LogBlock lb) { + super(lb); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockPlace(BlockPlaceEvent event) { + if (Config.isLogging(event.getBlock().getWorld(), Logging.BLOCKPLACE)) { + final BlockState before = event.getBlockReplacedState(); + final BlockState after = event.getBlockPlaced().getState(); + final Actor actor = Actor.actorFromEntity(event.getPlayer()); + if (before.getType() == Material.LECTERN && after.getType() == Material.LECTERN) { + return; + } + LoggingUtil.smartLogBlockPlace(consumer, actor, before, after); + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerBucketEmpty(PlayerBucketEmptyEvent event) { + if (isLogging(event.getPlayer().getWorld(), Logging.BLOCKPLACE)) { + Material placedMaterial = event.getBucket() == Material.LAVA_BUCKET ? Material.LAVA : Material.WATER; + BlockData clickedBlockData = event.getBlockClicked().getBlockData(); + if (placedMaterial == Material.WATER && clickedBlockData instanceof Waterlogged) { + Waterlogged clickedWaterlogged = (Waterlogged) clickedBlockData; + if (!clickedWaterlogged.isWaterlogged()) { + Waterlogged clickedWaterloggedWithWater = (Waterlogged) clickedWaterlogged.clone(); + clickedWaterloggedWithWater.setWaterlogged(true); + consumer.queueBlockReplace(Actor.actorFromEntity(event.getPlayer()), event.getBlockClicked().getLocation(), clickedWaterlogged, clickedWaterloggedWithWater); + return; + } + } + Block placedAt = event.getBlockClicked().getRelative(event.getBlockFace()); + if (placedAt.isEmpty()) { + consumer.queueBlockPlace(Actor.actorFromEntity(event.getPlayer()), placedAt.getLocation(), placedMaterial.createBlockData()); + } else { + BlockData placedAtBlock = placedAt.getBlockData(); + if (placedAtBlock instanceof Waterlogged && !(((Waterlogged) placedAtBlock).isWaterlogged())) { + Waterlogged clickedWaterloggedWithWater = (Waterlogged) placedAtBlock.clone(); + clickedWaterloggedWithWater.setWaterlogged(true); + consumer.queueBlockReplace(Actor.actorFromEntity(event.getPlayer()), placedAt.getLocation(), placedAtBlock, clickedWaterloggedWithWater); + } else { + consumer.queueBlockReplace(Actor.actorFromEntity(event.getPlayer()), placedAt.getLocation(), placedAtBlock, placedMaterial.createBlockData()); + } + } + } + } +} diff --git a/src/main/java/de/diddiz/LogBlock/listeners/BlockSpreadLogging.java b/src/main/java/de/diddiz/LogBlock/listeners/BlockSpreadLogging.java index ca3d9232..c2a12552 100644 --- a/src/main/java/de/diddiz/LogBlock/listeners/BlockSpreadLogging.java +++ b/src/main/java/de/diddiz/LogBlock/listeners/BlockSpreadLogging.java @@ -5,6 +5,10 @@ import de.diddiz.LogBlock.Logging; import org.bukkit.Material; import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.data.type.PointedDripstone; +import org.bukkit.block.data.type.PointedDripstone.Thickness; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.block.BlockSpreadEvent; @@ -22,37 +26,74 @@ public void onBlockSpread(BlockSpreadEvent event) { String name; - World world = event.getBlock().getWorld(); - Material type = event.getSource().getType(); + World world = event.getNewState().getWorld(); + Material type = event.getNewState().getType(); - switch (type) { - case GRASS: - if (!isLogging(world, Logging.GRASSGROWTH)) { - return; - } - name = "GrassGrowth"; - break; - case MYCEL: - if (!isLogging(world, Logging.MYCELIUMSPREAD)) { - return; - } - name = "MyceliumSpread"; - break; - case VINE: - if (!isLogging(world, Logging.VINEGROWTH)) { - return; - } - name = "VineGrowth"; - break; - case RED_MUSHROOM: - case BROWN_MUSHROOM: - if (!isLogging(world, Logging.MUSHROOMSPREAD)) { - return; + if (type == Material.SHORT_GRASS) { + if (!isLogging(world, Logging.GRASSGROWTH)) { + return; + } + name = "GrassGrowth"; + } else if (type == Material.MYCELIUM) { + if (!isLogging(world, Logging.MYCELIUMSPREAD)) { + return; + } + name = "MyceliumSpread"; + } else if (type == Material.VINE || type == Material.CAVE_VINES || type == Material.CAVE_VINES_PLANT || type == Material.WEEPING_VINES || type == Material.WEEPING_VINES_PLANT || type == Material.TWISTING_VINES || type == Material.TWISTING_VINES_PLANT) { + if (!isLogging(world, Logging.VINEGROWTH)) { + return; + } + name = "VineGrowth"; + } else if (type == Material.RED_MUSHROOM || type == Material.BROWN_MUSHROOM) { + if (!isLogging(world, Logging.MUSHROOMSPREAD)) { + return; + } + name = "MushroomSpread"; + } else if (type == Material.BAMBOO || type == Material.BAMBOO_SAPLING) { + if (!isLogging(world, Logging.BAMBOOGROWTH)) { + return; + } + name = "BambooGrowth"; + if (type == Material.BAMBOO_SAPLING) { + // bamboo sapling gets replaced by bamboo + consumer.queueBlockReplace(new Actor(name), event.getSource().getState(), Material.BAMBOO.createBlockData()); + } + } else if (type == Material.POINTED_DRIPSTONE) { + if (!isLogging(world, Logging.DRIPSTONEGROWTH)) { + return; + } + name = "DripstoneGrowth"; + PointedDripstone pointed = (PointedDripstone) event.getNewState().getBlockData(); + if (pointed.getThickness() != Thickness.TIP_MERGE) { + BlockFace direction = pointed.getVerticalDirection(); + Block previousPart = event.getBlock().getRelative(direction.getOppositeFace()); + if (previousPart.getType() == Material.POINTED_DRIPSTONE) { + PointedDripstone newBelow = (PointedDripstone) previousPart.getBlockData(); + newBelow.setThickness(Thickness.FRUSTUM); + consumer.queueBlockReplace(new Actor(name), previousPart.getState(), newBelow); + + previousPart = previousPart.getRelative(direction.getOppositeFace()); + if (previousPart.getType() == Material.POINTED_DRIPSTONE) { + Block evenMorePrevious = previousPart.getRelative(direction.getOppositeFace()); + newBelow = (PointedDripstone) previousPart.getBlockData(); + newBelow.setThickness(evenMorePrevious.getType() == Material.POINTED_DRIPSTONE ? Thickness.MIDDLE : Thickness.BASE); + consumer.queueBlockReplace(new Actor(name), previousPart.getState(), newBelow); + } } - name = "MushroomSpread"; - break; - default: + } else { + // special case because the old state is already changed (for one half) + PointedDripstone oldState = (PointedDripstone) event.getNewState().getBlockData(); + oldState.setThickness(Thickness.TIP); + consumer.queueBlockReplace(new Actor(name), oldState, event.getNewState()); + return; + } + } else if (type == Material.SCULK || type == Material.SCULK_VEIN || type == Material.SCULK_CATALYST || type == Material.SCULK_SENSOR || type == Material.SCULK_SHRIEKER) { + if (!isLogging(world, Logging.SCULKSPREAD)) { return; + } + name = "SculkSpread"; + } else { + return; } consumer.queueBlockReplace(new Actor(name), event.getBlock().getState(), event.getNewState()); diff --git a/src/main/java/de/diddiz/LogBlock/listeners/CauldronLogging.java b/src/main/java/de/diddiz/LogBlock/listeners/CauldronLogging.java new file mode 100644 index 00000000..5676adb7 --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/listeners/CauldronLogging.java @@ -0,0 +1,27 @@ +package de.diddiz.LogBlock.listeners; + +import de.diddiz.LogBlock.Actor; +import de.diddiz.LogBlock.LogBlock; +import de.diddiz.LogBlock.Logging; +import de.diddiz.LogBlock.config.Config; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.block.CauldronLevelChangeEvent; + +public class CauldronLogging extends LoggingListener { + public CauldronLogging(LogBlock lb) { + super(lb); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onCauldronLevelChange(CauldronLevelChangeEvent event) { + if (Config.isLogging(event.getBlock().getWorld(), Logging.CAULDRONINTERACT)) { + Entity causingEntity = event.getEntity(); + if (causingEntity instanceof Player) { + consumer.queueBlockReplace(Actor.actorFromEntity(causingEntity), event.getBlock().getBlockData(), event.getNewState()); + } + } + } +} diff --git a/src/main/java/de/diddiz/LogBlock/listeners/ChatLogging.java b/src/main/java/de/diddiz/LogBlock/listeners/ChatLogging.java index e8985f20..11b1a75c 100644 --- a/src/main/java/de/diddiz/LogBlock/listeners/ChatLogging.java +++ b/src/main/java/de/diddiz/LogBlock/listeners/ChatLogging.java @@ -1,37 +1,58 @@ -package de.diddiz.LogBlock.listeners; - -import de.diddiz.LogBlock.Actor; -import de.diddiz.LogBlock.LogBlock; -import de.diddiz.LogBlock.Logging; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.player.AsyncPlayerChatEvent; -import org.bukkit.event.player.PlayerCommandPreprocessEvent; -import org.bukkit.event.server.ServerCommandEvent; - -import static de.diddiz.LogBlock.config.Config.isLogging; - -public class ChatLogging extends LoggingListener { - public ChatLogging(LogBlock lb) { - super(lb); - } - - @EventHandler(priority = EventPriority.MONITOR) - public void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event) { - if (isLogging(event.getPlayer().getWorld(), Logging.CHAT)) { - consumer.queueChat(Actor.actorFromEntity(event.getPlayer()), event.getMessage()); - } - } - - @EventHandler(priority = EventPriority.MONITOR) - public void onPlayerChat(AsyncPlayerChatEvent event) { - if (isLogging(event.getPlayer().getWorld(), Logging.CHAT)) { - consumer.queueChat(Actor.actorFromEntity(event.getPlayer()), event.getMessage()); - } - } - - @EventHandler(priority = EventPriority.MONITOR) - public void onServerCommand(ServerCommandEvent event) { - consumer.queueChat(new Actor("Console"), "/" + event.getCommand()); - } -} +package de.diddiz.LogBlock.listeners; + +import de.diddiz.LogBlock.Actor; +import de.diddiz.LogBlock.LogBlock; +import de.diddiz.LogBlock.Logging; +import org.bukkit.command.BlockCommandSender; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.minecart.CommandMinecart; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.event.player.PlayerCommandPreprocessEvent; +import org.bukkit.event.server.ServerCommandEvent; + +import static de.diddiz.LogBlock.config.Config.isLogging; + +public class ChatLogging extends LoggingListener { + public ChatLogging(LogBlock lb) { + super(lb); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event) { + if (isLogging(event.getPlayer().getWorld(), Logging.PLAYER_COMMANDS)) { + consumer.queueChat(Actor.actorFromEntity(event.getPlayer()), event.getMessage()); + } + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onPlayerChat(AsyncPlayerChatEvent event) { + if (isLogging(event.getPlayer().getWorld(), Logging.CHAT)) { + consumer.queueChat(Actor.actorFromEntity(event.getPlayer()), event.getMessage()); + } + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onServerCommand(ServerCommandEvent event) { + CommandSender sender = event.getSender(); + Actor actor; + if (sender instanceof BlockCommandSender) { + if (!isLogging(((BlockCommandSender) sender).getBlock().getWorld(), Logging.COMMANDBLOCK_COMMANDS)) { + return; + } + actor = new Actor("CommandBlock"); + } else if (sender instanceof CommandMinecart) { + if (!isLogging(((CommandMinecart) sender).getWorld(), Logging.COMMANDBLOCK_COMMANDS)) { + return; + } + actor = new Actor("CommandMinecart"); + } else { + if (!isLogging(Logging.CONSOLE_COMMANDS)) { + return; + } + actor = new Actor("Console"); + } + consumer.queueChat(actor, "/" + event.getCommand()); + } +} diff --git a/src/main/java/de/diddiz/LogBlock/listeners/ChestAccessLogging.java b/src/main/java/de/diddiz/LogBlock/listeners/ChestAccessLogging.java index 3ada3b90..cb986000 100644 --- a/src/main/java/de/diddiz/LogBlock/listeners/ChestAccessLogging.java +++ b/src/main/java/de/diddiz/LogBlock/listeners/ChestAccessLogging.java @@ -1,67 +1,372 @@ -package de.diddiz.LogBlock.listeners; - -import de.diddiz.LogBlock.Actor; -import de.diddiz.LogBlock.LogBlock; -import de.diddiz.LogBlock.Logging; -import org.bukkit.Location; -import org.bukkit.block.BlockState; -import org.bukkit.block.DoubleChest; -import org.bukkit.entity.HumanEntity; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.inventory.InventoryCloseEvent; -import org.bukkit.event.inventory.InventoryOpenEvent; -import org.bukkit.inventory.InventoryHolder; -import org.bukkit.inventory.ItemStack; - -import java.util.HashMap; -import java.util.Map; - -import static de.diddiz.LogBlock.config.Config.isLogging; -import static de.diddiz.util.BukkitUtils.*; - -public class ChestAccessLogging extends LoggingListener { - private final Map containers = new HashMap(); - - public ChestAccessLogging(LogBlock lb) { - super(lb); - } - - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onInventoryClose(InventoryCloseEvent event) { - - if (!isLogging(event.getPlayer().getWorld(), Logging.CHESTACCESS)) { - return; - } - InventoryHolder holder = event.getInventory().getHolder(); - if (holder instanceof BlockState || holder instanceof DoubleChest) { - final HumanEntity player = event.getPlayer(); - final ItemStack[] before = containers.get(player); - if (before != null) { - final ItemStack[] after = compressInventory(event.getInventory().getContents()); - final ItemStack[] diff = compareInventories(before, after); - final Location loc = getInventoryHolderLocation(holder); - for (final ItemStack item : diff) { - consumer.queueChestAccess(Actor.actorFromEntity(player), loc, loc.getWorld().getBlockTypeIdAt(loc), (short) item.getTypeId(), (short) item.getAmount(), rawData(item)); - } - containers.remove(player); - } - } - } - - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onInventoryOpen(InventoryOpenEvent event) { - - if (!isLogging(event.getPlayer().getWorld(), Logging.CHESTACCESS)) { - return; - } - if (event.getInventory() != null) { - InventoryHolder holder = event.getInventory().getHolder(); - if (holder instanceof BlockState || holder instanceof DoubleChest) { - if (getInventoryHolderType(holder) != 58) { - containers.put(event.getPlayer(), compressInventory(event.getInventory().getContents())); - } - } - } - } -} +package de.diddiz.LogBlock.listeners; + +import de.diddiz.LogBlock.Actor; +import de.diddiz.LogBlock.LogBlock; +import de.diddiz.LogBlock.Logging; +import de.diddiz.LogBlock.util.ItemStackAndAmount; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Tag; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.bukkit.block.DecoratedPot; +import org.bukkit.block.DoubleChest; +import org.bukkit.block.data.type.ChiseledBookshelf; +import org.bukkit.entity.HumanEntity; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.block.Action; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.event.inventory.InventoryDragEvent; +import org.bukkit.event.inventory.InventoryOpenEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.ItemStack; +import org.bukkit.util.Vector; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import static de.diddiz.LogBlock.config.Config.isLogging; +import static de.diddiz.LogBlock.util.BukkitUtils.*; + +public class ChestAccessLogging extends LoggingListener { + private class PlayerActiveInventoryModifications { + private final HumanEntity actor; + private final Location location; + private final HashMap modifications; + + public PlayerActiveInventoryModifications(HumanEntity actor, Location location) { + this.actor = actor; + this.location = location; + this.modifications = new HashMap<>(); + } + + public void addModification(ItemStack stack, int amount) { + if (amount == 0) { + return; + } + // if we have other viewers, we have to flush their changes + ArrayList allViewers = containersByLocation.get(location); + if (allViewers.size() > 1) { + for (PlayerActiveInventoryModifications other : allViewers) { + if (other != this) { + other.flush(); + } + } + } + + // consumer.getLogblock().getLogger().info("Modify container: " + stack + " change: " + amount); + stack = new ItemStack(stack); + stack.setAmount(1); + Integer existing = modifications.get(stack); + int newTotal = amount + (existing == null ? 0 : existing); + if (newTotal == 0) { + modifications.remove(stack); + } else { + modifications.put(stack, newTotal); + } + } + + public void flush() { + if (!modifications.isEmpty()) { + for (Entry e : modifications.entrySet()) { + ItemStack stack = e.getKey(); + int amount = e.getValue(); + if (amount != 0) { + // consumer.getLogblock().getLogger().info("Store container: " + stack + " take: " + (amount < 0)); + consumer.queueChestAccess(Actor.actorFromEntity(actor), location, location.getWorld().getBlockAt(location).getBlockData(), new ItemStackAndAmount(stack, Math.abs(amount)), amount < 0); + } + } + modifications.clear(); + } + } + + public HumanEntity getActor() { + return actor; + } + + public Location getLocation() { + return location; + } + } + + private final Map containersByOwner = new HashMap<>(); + private final Map> containersByLocation = new HashMap<>(); + + public ChestAccessLogging(LogBlock lb) { + super(lb); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onInventoryClose(InventoryCloseEvent event) { + final HumanEntity player = event.getPlayer(); + if (!isLogging(player.getWorld(), Logging.CHESTACCESS)) { + return; + } + InventoryHolder holder = event.getInventory().getHolder(); + if (holder instanceof BlockState || holder instanceof DoubleChest) { + final PlayerActiveInventoryModifications modifications = containersByOwner.remove(player); + if (modifications != null) { + final Location loc = modifications.getLocation(); + ArrayList atLocation = containersByLocation.get(loc); + atLocation.remove(modifications); + if (atLocation.isEmpty()) { + containersByLocation.remove(loc); + } + modifications.flush(); + } + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onInventoryOpen(InventoryOpenEvent event) { + final HumanEntity player = event.getPlayer(); + if (!isLogging(player.getWorld(), Logging.CHESTACCESS)) { + return; + } + if (event.getInventory() != null) { + InventoryHolder holder = event.getInventory().getHolder(); + if (holder instanceof BlockState || holder instanceof DoubleChest) { + if (getInventoryHolderType(holder) != Material.CRAFTING_TABLE) { + PlayerActiveInventoryModifications modifications = new PlayerActiveInventoryModifications(event.getPlayer(), getInventoryHolderLocation(holder)); + containersByOwner.put(modifications.getActor(), modifications); + containersByLocation.compute(modifications.getLocation(), (k, v) -> { + if (v == null) { + v = new ArrayList<>(); + } + v.add(modifications); + return v; + }); + } + } + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onInventoryClick(InventoryClickEvent event) { + final HumanEntity player = event.getWhoClicked(); + if (!isLogging(player.getWorld(), Logging.CHESTACCESS)) { + return; + } + InventoryHolder holder = event.getInventory().getHolder(); + if (holder instanceof BlockState || holder instanceof DoubleChest) { + final PlayerActiveInventoryModifications modifications = containersByOwner.get(player); + if (modifications != null) { + switch (event.getAction()) { + case PICKUP_ONE: + case DROP_ONE_SLOT: + if (event.getRawSlot() < event.getView().getTopInventory().getSize()) { + modifications.addModification(event.getCurrentItem(), -1); + } + break; + case PICKUP_HALF: + // server behaviour: round up + if (event.getRawSlot() < event.getView().getTopInventory().getSize()) { + modifications.addModification(event.getCurrentItem(), -(event.getCurrentItem().getAmount() + 1) / 2); + } + break; + case PICKUP_SOME: // oversized stack - can not take all when clicking + // server behaviour: leave a full stack in the slot, take everything else + if (event.getRawSlot() < event.getView().getTopInventory().getSize()) { + int taken = event.getCurrentItem().getAmount() - event.getCurrentItem().getMaxStackSize(); + modifications.addModification(event.getCursor(), -taken); + } + break; + case PICKUP_ALL: + case DROP_ALL_SLOT: + if (event.getRawSlot() < event.getView().getTopInventory().getSize()) { + modifications.addModification(event.getCurrentItem(), -event.getCurrentItem().getAmount()); + } + break; + case PLACE_ONE: + if (event.getRawSlot() < event.getView().getTopInventory().getSize()) { + modifications.addModification(event.getCursor(), 1); + } + break; + case PLACE_SOME: // not enough free place in target slot + // server behaviour: place as much as possible + if (event.getRawSlot() < event.getView().getTopInventory().getSize()) { + int placeable = event.getCurrentItem().getMaxStackSize() - event.getCurrentItem().getAmount(); + modifications.addModification(event.getCursor(), placeable); + } + break; + case PLACE_ALL: + if (event.getRawSlot() < event.getView().getTopInventory().getSize()) { + modifications.addModification(event.getCursor(), event.getCursor().getAmount()); + } + break; + case SWAP_WITH_CURSOR: + if (event.getRawSlot() < event.getView().getTopInventory().getSize()) { + modifications.addModification(event.getCursor(), event.getCursor().getAmount()); + modifications.addModification(event.getCurrentItem(), -event.getCurrentItem().getAmount()); + } + break; + case MOVE_TO_OTHER_INVENTORY: // shift + click + boolean removed = event.getRawSlot() < event.getView().getTopInventory().getSize(); + modifications.addModification(event.getCurrentItem(), event.getCurrentItem().getAmount() * (removed ? -1 : 1)); + break; + case COLLECT_TO_CURSOR: // double click + // server behaviour: first collect all with an amount != maxstacksize, then others, starting from slot 0 (container) + ItemStack cursor = event.getCursor(); + if (cursor == null) { + return; + } + int toPickUp = cursor.getMaxStackSize() - cursor.getAmount(); + int takenFromContainer = 0; + boolean takeFromFullStacks = false; + Inventory top = event.getView().getTopInventory(); + Inventory bottom = event.getView().getBottomInventory(); + while (toPickUp > 0) { + for (ItemStack stack : top.getStorageContents()) { + if (cursor.isSimilar(stack)) { + if (takeFromFullStacks == (stack.getAmount() == stack.getMaxStackSize())) { + int take = Math.min(toPickUp, stack.getAmount()); + toPickUp -= take; + takenFromContainer += take; + if (toPickUp <= 0) { + break; + } + } + } + } + if (toPickUp <= 0) { + break; + } + for (ItemStack stack : bottom.getStorageContents()) { + if (cursor.isSimilar(stack)) { + if (takeFromFullStacks == (stack.getAmount() == stack.getMaxStackSize())) { + int take = Math.min(toPickUp, stack.getAmount()); + toPickUp -= take; + if (toPickUp <= 0) { + break; + } + } + } + } + if (takeFromFullStacks) { + break; + } else { + takeFromFullStacks = true; + } + } + if (takenFromContainer > 0) { + modifications.addModification(event.getCursor(), -takenFromContainer); + } + break; + case HOTBAR_SWAP: // number key or offhand key + case HOTBAR_MOVE_AND_READD: // something was in the other slot + if (event.getRawSlot() < event.getView().getTopInventory().getSize()) { + ItemStack otherSlot = (event.getClick() == ClickType.SWAP_OFFHAND) ? event.getWhoClicked().getInventory().getItemInOffHand() : event.getWhoClicked().getInventory().getItem(event.getHotbarButton()); + if (event.getCurrentItem() != null && event.getCurrentItem().getType() != Material.AIR) { + modifications.addModification(event.getCurrentItem(), -event.getCurrentItem().getAmount()); + } + if (otherSlot != null && otherSlot.getType() != Material.AIR) { + modifications.addModification(otherSlot, otherSlot.getAmount()); + } + } + break; + case DROP_ALL_CURSOR: + case DROP_ONE_CURSOR: + case CLONE_STACK: + case NOTHING: + // only the cursor or nothing (but not the inventory) was modified + break; + case UNKNOWN: + default: + // unable to log something we don't know + consumer.getLogblock().getLogger().warning("Unknown inventory action by " + event.getWhoClicked().getName() + ": " + event.getAction() + " Slot: " + event.getSlot() + " Slot type: " + event.getSlotType()); + break; + } + } + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onInventoryDrag(InventoryDragEvent event) { + final HumanEntity player = event.getWhoClicked(); + if (!isLogging(player.getWorld(), Logging.CHESTACCESS)) { + return; + } + InventoryHolder holder = event.getInventory().getHolder(); + if (holder instanceof BlockState || holder instanceof DoubleChest) { + final PlayerActiveInventoryModifications modifications = containersByOwner.get(player); + if (modifications != null) { + Inventory container = event.getView().getTopInventory(); + int containerSize = container.getSize(); + for (Entry e : event.getNewItems().entrySet()) { + int slot = e.getKey(); + if (slot < containerSize) { + ItemStack old = container.getItem(slot); + int oldAmount = (old == null || old.getType() == Material.AIR) ? 0 : old.getAmount(); + modifications.addModification(e.getValue(), e.getValue().getAmount() - oldAmount); + } + } + } + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerInteract(PlayerInteractEvent event) { + final Block clicked = event.getClickedBlock(); + if (event.getAction() != Action.RIGHT_CLICK_BLOCK || event.getHand() != EquipmentSlot.HAND || !event.hasBlock() || clicked == null) { + return; + } + final Player player = event.getPlayer(); + if (!isLogging(player.getWorld(), Logging.CHESTACCESS)) { + return; + } + final Material type = clicked.getType(); + if (type == Material.DECORATED_POT) { + ItemStack mainHand = player.getInventory().getItemInMainHand(); + if (mainHand != null && mainHand.getType() != Material.AIR && clicked.getState() instanceof DecoratedPot pot) { + ItemStack currentInPot = pot.getSnapshotInventory().getItem(); + if (currentInPot == null || currentInPot.getType() == Material.AIR || currentInPot.isSimilar(mainHand) && currentInPot.getAmount() < currentInPot.getMaxStackSize()) { + ItemStack stack = mainHand.clone(); + stack.setAmount(1); + consumer.queueChestAccess(Actor.actorFromEntity(player), clicked.getLocation(), clicked.getBlockData(), ItemStackAndAmount.fromStack(stack), false); + } + } + } else if (type == Material.CHISELED_BOOKSHELF) { + if (clicked.getBlockData() instanceof ChiseledBookshelf blockData && blockData.getFacing() == event.getBlockFace() && clicked.getState() instanceof org.bukkit.block.ChiseledBookshelf bookshelf) { + // calculate the slot the same way as minecraft does it + Vector pos = event.getClickedPosition(); + if (pos == null) { + return; // some plugins create this event without a clicked pos + } + double clickx = switch (blockData.getFacing()) { + case NORTH -> 1 - pos.getX(); + case SOUTH -> pos.getX(); + case EAST -> 1 - pos.getZ(); + case WEST -> pos.getZ(); + default -> throw new IllegalArgumentException("Unexpected facing for chiseled bookshelf: " + blockData.getFacing()); + }; + int col = clickx < 0.375 ? 0 : (clickx < 0.6875 ? 1 : 2); // 6/16 ; 11/16 + int row = pos.getY() >= 0.5 ? 0 : 1; + int slot = col + row * 3; + + ItemStack currentInSlot = bookshelf.getSnapshotInventory().getItem(slot); + if (blockData.isSlotOccupied(slot)) { + // not empty: always take + if (currentInSlot != null && currentInSlot.getType() != Material.AIR) { + consumer.queueChestAccess(Actor.actorFromEntity(player), clicked.getLocation(), clicked.getBlockData(), ItemStackAndAmount.fromStack(currentInSlot), true); + } + } else { + // empty: put if has tag BOOKSHELF_BOOKS + ItemStack mainHand = player.getInventory().getItemInMainHand(); + if (mainHand != null && mainHand.getType() != Material.AIR && Tag.ITEMS_BOOKSHELF_BOOKS.isTagged(mainHand.getType())) { + ItemStack stack = mainHand.clone(); + stack.setAmount(1); + consumer.queueChestAccess(Actor.actorFromEntity(player), clicked.getLocation(), clicked.getBlockData(), ItemStackAndAmount.fromStack(stack), false); + } + } + } + } + } +} diff --git a/src/main/java/de/diddiz/LogBlock/listeners/CreatureInteractLogging.java b/src/main/java/de/diddiz/LogBlock/listeners/CreatureInteractLogging.java index ea0cab00..7066b357 100644 --- a/src/main/java/de/diddiz/LogBlock/listeners/CreatureInteractLogging.java +++ b/src/main/java/de/diddiz/LogBlock/listeners/CreatureInteractLogging.java @@ -4,12 +4,12 @@ import de.diddiz.LogBlock.LogBlock; import de.diddiz.LogBlock.Logging; import de.diddiz.LogBlock.config.WorldConfig; -import de.diddiz.util.BukkitUtils; +import de.diddiz.LogBlock.util.BukkitUtils; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; -import org.bukkit.entity.EntityType; +import org.bukkit.block.data.type.TurtleEgg; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; @@ -26,34 +26,39 @@ public CreatureInteractLogging(LogBlock lb) { public void onEntityInteract(EntityInteractEvent event) { final WorldConfig wcfg = getWorldConfig(event.getEntity().getWorld()); - final EntityType entityType = event.getEntityType(); - // Mobs only - if (event.getEntity() instanceof Player || entityType == null) { + if (event.getEntity() instanceof Player) { return; } if (wcfg != null) { final Block clicked = event.getBlock(); final Material type = clicked.getType(); - final int typeId = type.getId(); - final byte blockData = clicked.getData(); final Location loc = clicked.getLocation(); - switch (type) { - case SOIL: - if (wcfg.isLogging(Logging.CREATURECROPTRAMPLE)) { - // 3 = Dirt ID - consumer.queueBlock(Actor.actorFromEntity(entityType), loc, typeId, 3, blockData); - // Log the crop on top as being broken - Block trampledCrop = clicked.getRelative(BlockFace.UP); - if (BukkitUtils.getCropBlocks().contains(trampledCrop.getType())) { - consumer.queueBlockBreak(new Actor("CreatureTrample"), trampledCrop.getState()); - } + if (type == Material.FARMLAND) { + if (wcfg.isLogging(Logging.CREATURECROPTRAMPLE)) { + // 3 = Dirt ID + consumer.queueBlock(new Actor("CreatureTrample"), loc, type.createBlockData(), Material.DIRT.createBlockData()); + // Log the crop on top as being broken + Block trampledCrop = clicked.getRelative(BlockFace.UP); + if (BukkitUtils.isCropBlock(trampledCrop.getType())) { + consumer.queueBlockBreak(new Actor("CreatureTrample"), trampledCrop.getState()); + } + } + } else if (type == Material.TURTLE_EGG) { + if (wcfg.isLogging(Logging.CREATURECROPTRAMPLE)) { + TurtleEgg turtleEggData = (TurtleEgg) clicked.getBlockData(); + int eggs = turtleEggData.getEggs(); + if (eggs > 1) { + TurtleEgg turtleEggData2 = (TurtleEgg) turtleEggData.clone(); + turtleEggData2.setEggs(eggs - 1); + consumer.queueBlock(new Actor("CreatureTrample"), loc, turtleEggData, turtleEggData2); + } else { + consumer.queueBlock(new Actor("CreatureTrample"), loc, turtleEggData, Material.AIR.createBlockData()); } - break; + } } } } } - diff --git a/src/main/java/de/diddiz/LogBlock/listeners/DragonEggLogging.java b/src/main/java/de/diddiz/LogBlock/listeners/DragonEggLogging.java new file mode 100644 index 00000000..657ae9ed --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/listeners/DragonEggLogging.java @@ -0,0 +1,73 @@ +package de.diddiz.LogBlock.listeners; + +import java.util.UUID; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.bukkit.block.data.BlockData; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.block.Action; +import org.bukkit.event.block.BlockFromToEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.scheduler.BukkitRunnable; + +import de.diddiz.LogBlock.Actor; +import de.diddiz.LogBlock.LogBlock; +import de.diddiz.LogBlock.Logging; +import de.diddiz.LogBlock.config.Config; +import de.diddiz.LogBlock.util.LoggingUtil; + +public class DragonEggLogging extends LoggingListener { + + private UUID lastDragonEggInteractionPlayer; + private Location lastDragonEggInteractionLocation; + + public DragonEggLogging(LogBlock lb) { + super(lb); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerInteract(PlayerInteractEvent event) { + if (event.getAction() == Action.RIGHT_CLICK_BLOCK && event.hasBlock() && event.getClickedBlock().getType() == Material.DRAGON_EGG) { + Block block = event.getClickedBlock(); + if (!Config.isLogging(block.getWorld(), Logging.DRAGONEGGTELEPORT)) { + return; + } + lastDragonEggInteractionPlayer = event.getPlayer().getUniqueId(); + lastDragonEggInteractionLocation = block.getLocation(); + new BukkitRunnable() { + @Override + public void run() { + lastDragonEggInteractionPlayer = null; + lastDragonEggInteractionLocation = null; + } + }.runTask(LogBlock.getInstance()); + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onDragonEggTeleport(BlockFromToEvent event) { + Block block = event.getBlock(); + Player teleportCause = null; + if (lastDragonEggInteractionPlayer != null && lastDragonEggInteractionLocation != null && lastDragonEggInteractionLocation.equals(block.getLocation())) { + teleportCause = Bukkit.getPlayer(lastDragonEggInteractionPlayer); + } + + if (block.getType() == Material.DRAGON_EGG && Config.isLogging(block.getWorld(), Logging.DRAGONEGGTELEPORT)) { + Actor actor = new Actor("DragonEgg"); + if (teleportCause != null) { + actor = Actor.actorFromEntity(teleportCause); + } + BlockData data = block.getBlockData(); + consumer.queueBlockBreak(actor, block.getLocation(), data); + BlockState finalState = event.getToBlock().getState(); + finalState.setBlockData(data); + LoggingUtil.smartLogBlockPlace(consumer, actor, event.getToBlock().getState(), finalState); + } + } +} diff --git a/src/main/java/de/diddiz/LogBlock/listeners/EndermenLogging.java b/src/main/java/de/diddiz/LogBlock/listeners/EndermenLogging.java deleted file mode 100644 index 9fc14baa..00000000 --- a/src/main/java/de/diddiz/LogBlock/listeners/EndermenLogging.java +++ /dev/null @@ -1,24 +0,0 @@ -package de.diddiz.LogBlock.listeners; - -import de.diddiz.LogBlock.Actor; -import de.diddiz.LogBlock.LogBlock; -import de.diddiz.LogBlock.Logging; -import org.bukkit.entity.Enderman; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.entity.EntityChangeBlockEvent; - -import static de.diddiz.LogBlock.config.Config.isLogging; - -public class EndermenLogging extends LoggingListener { - public EndermenLogging(LogBlock lb) { - super(lb); - } - - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onEntityChangeBlock(EntityChangeBlockEvent event) { - if (event.getEntity() instanceof Enderman && isLogging(event.getBlock().getWorld(), Logging.ENDERMEN)) { - consumer.queueBlockReplace(new Actor("Enderman"), event.getBlock().getState(), event.getTo().getId(), (byte) 0); // Figure out how to get the data of the placed block; - } - } -} diff --git a/src/main/java/de/diddiz/LogBlock/listeners/EntityChangeBlockLogging.java b/src/main/java/de/diddiz/LogBlock/listeners/EntityChangeBlockLogging.java new file mode 100644 index 00000000..e546c934 --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/listeners/EntityChangeBlockLogging.java @@ -0,0 +1,46 @@ +package de.diddiz.LogBlock.listeners; + +import de.diddiz.LogBlock.Actor; +import de.diddiz.LogBlock.LogBlock; +import de.diddiz.LogBlock.Logging; +import org.bukkit.Material; +import org.bukkit.entity.Enderman; +import org.bukkit.entity.Player; +import org.bukkit.entity.Sheep; +import org.bukkit.entity.Wither; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.entity.EntityChangeBlockEvent; + +import static de.diddiz.LogBlock.config.Config.isLogging; + +public class EntityChangeBlockLogging extends LoggingListener { + public EntityChangeBlockLogging(LogBlock lb) { + super(lb); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onEntityChangeBlock(EntityChangeBlockEvent event) { + Material oldType = event.getBlock().getType(); + if ((oldType == Material.REDSTONE_ORE || oldType == Material.DEEPSLATE_REDSTONE_ORE) && event.getBlockData().getMaterial() == oldType) { + return; // ignore redstone ore activation by stepping on it + } + if (event.getEntity() instanceof Wither) { + if (isLogging(event.getBlock().getWorld(), Logging.WITHER)) { + consumer.queueBlockReplace(Actor.actorFromEntity(event.getEntity()), event.getBlock().getState(), event.getBlockData()); + } + } else if (event.getEntity() instanceof Enderman) { + if (isLogging(event.getBlock().getWorld(), Logging.ENDERMEN)) { + consumer.queueBlockReplace(Actor.actorFromEntity(event.getEntity()), event.getBlock().getState(), event.getBlockData()); + } + } else if (event.getEntity() instanceof Sheep) { + if (isLogging(event.getBlock().getWorld(), Logging.GRASS_EAT)) { + consumer.queueBlockReplace(Actor.actorFromEntity(event.getEntity()), event.getBlock().getState(), event.getBlockData()); + } + } else { + if (isLogging(event.getBlock().getWorld(), event.getEntity() instanceof Player ? Logging.BLOCKPLACE : Logging.MISCENTITYCHANGEBLOCK)) { + consumer.queueBlockReplace(Actor.actorFromEntity(event.getEntity()), event.getBlock().getState(), event.getBlockData()); + } + } + } +} diff --git a/src/main/java/de/diddiz/LogBlock/listeners/ExplosionLogging.java b/src/main/java/de/diddiz/LogBlock/listeners/ExplosionLogging.java index 070b7174..0caedf97 100644 --- a/src/main/java/de/diddiz/LogBlock/listeners/ExplosionLogging.java +++ b/src/main/java/de/diddiz/LogBlock/listeners/ExplosionLogging.java @@ -1,105 +1,235 @@ -package de.diddiz.LogBlock.listeners; - -import de.diddiz.LogBlock.Actor; -import de.diddiz.LogBlock.LogBlock; -import de.diddiz.LogBlock.Logging; -import de.diddiz.LogBlock.config.WorldConfig; -import org.bukkit.Material; -import org.bukkit.block.Block; -import org.bukkit.block.Sign; -import org.bukkit.entity.*; -import org.bukkit.entity.minecart.ExplosiveMinecart; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.entity.EntityExplodeEvent; -import org.bukkit.projectiles.ProjectileSource; - -import static de.diddiz.LogBlock.config.Config.getWorldConfig; -import static de.diddiz.LogBlock.config.Config.logCreeperExplosionsAsPlayerWhoTriggeredThese; -import static de.diddiz.util.BukkitUtils.getContainerBlocks; - -public class ExplosionLogging extends LoggingListener { - public ExplosionLogging(LogBlock lb) { - super(lb); - } - - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onEntityExplode(EntityExplodeEvent event) { - final WorldConfig wcfg = getWorldConfig(event.getLocation().getWorld()); - if (wcfg != null) { - Actor actor = new Actor("Explosion"); - Entity source = event.getEntity(); - if (source == null) { - if (!wcfg.isLogging(Logging.MISCEXPLOSION)) { - return; - } - } else if (source instanceof TNTPrimed) { - if (!wcfg.isLogging(Logging.TNTEXPLOSION)) { - return; - } - actor = new Actor("TNT"); - } else if (source instanceof ExplosiveMinecart) { - if (!wcfg.isLogging(Logging.TNTMINECARTEXPLOSION)) { - return; - } - actor = new Actor("TNTMinecart"); - } else if (source instanceof Creeper) { - if (!wcfg.isLogging(Logging.CREEPEREXPLOSION)) { - return; - } - if (logCreeperExplosionsAsPlayerWhoTriggeredThese) { - final Entity target = ((Creeper) source).getTarget(); - actor = target instanceof Player ? Actor.actorFromEntity(target) : new Actor("Creeper"); - } else { - new Actor("Creeper"); - } - } else if (source instanceof Fireball) { - Fireball fireball = (Fireball) source; - ProjectileSource shooter = fireball.getShooter(); - if (shooter == null) { - return; - } - if (shooter instanceof Ghast) { - if (!wcfg.isLogging(Logging.GHASTFIREBALLEXPLOSION)) { - return; - } - actor = Actor.actorFromProjectileSource(shooter); - } else if (shooter instanceof Wither) { - if (!wcfg.isLogging(Logging.WITHER)) { - return; - } - actor = Actor.actorFromProjectileSource(shooter); - } - } else if (source instanceof EnderDragon) { - if (!wcfg.isLogging(Logging.ENDERDRAGON)) { - return; - } - actor = Actor.actorFromEntity(source); - } else if (source instanceof Wither) { - if (!wcfg.isLogging(Logging.WITHER)) { - return; - } - actor = Actor.actorFromEntity(source); - } else if (source instanceof WitherSkull) { - if (!wcfg.isLogging(Logging.WITHER_SKULL)) { - return; - } - actor = Actor.actorFromEntity(source); - } else { - if (!wcfg.isLogging(Logging.MISCEXPLOSION)) { - return; - } - } - for (final Block block : event.blockList()) { - final int type = block.getTypeId(); - if (wcfg.isLogging(Logging.SIGNTEXT) & (type == 63 || type == 68)) { - consumer.queueSignBreak(actor, (Sign) block.getState()); - } else if (wcfg.isLogging(Logging.CHESTACCESS) && (getContainerBlocks().contains(Material.getMaterial(type)))) { - consumer.queueContainerBreak(actor, block.getState()); - } else { - consumer.queueBlockBreak(actor, block.getState()); - } - } - } - } -} +package de.diddiz.LogBlock.listeners; + +import de.diddiz.LogBlock.Actor; +import de.diddiz.LogBlock.LogBlock; +import de.diddiz.LogBlock.Logging; +import de.diddiz.LogBlock.config.Config; +import de.diddiz.LogBlock.config.WorldConfig; +import de.diddiz.LogBlock.util.BukkitUtils; +import org.bukkit.Bukkit; +import org.bukkit.ExplosionResult; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.data.type.RespawnAnchor; +import org.bukkit.entity.*; +import org.bukkit.entity.minecart.ExplosiveMinecart; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.block.Action; +import org.bukkit.event.block.BlockExplodeEvent; +import org.bukkit.event.entity.EntityExplodeEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.projectiles.ProjectileSource; +import org.bukkit.scheduler.BukkitRunnable; + +import static de.diddiz.LogBlock.config.Config.getWorldConfig; +import static de.diddiz.LogBlock.config.Config.logCreeperExplosionsAsPlayerWhoTriggeredThese; + +import java.util.UUID; + +public class ExplosionLogging extends LoggingListener { + + private UUID lastBedInteractionPlayer; + private Location lastBedInteractionLocation; + private UUID lastRespawnAnchorInteractionPlayer; + private Location lastRespawnAnchorInteractionLocation; + + public ExplosionLogging(LogBlock lb) { + super(lb); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onEntityExplode(EntityExplodeEvent event) { + if (event.getExplosionResult() == ExplosionResult.KEEP || event.getExplosionResult() == ExplosionResult.TRIGGER_BLOCK) { + return; + } + final WorldConfig wcfg = getWorldConfig(event.getLocation().getWorld()); + if (wcfg != null) { + Actor actor = new Actor("Explosion"); + Entity source = event.getEntity(); + if (source == null) { + if (!wcfg.isLogging(Logging.MISCEXPLOSION)) { + return; + } + } else if (source instanceof TNTPrimed) { + if (!wcfg.isLogging(Logging.TNTEXPLOSION)) { + return; + } + actor = new Actor("TNT"); + } else if (source instanceof ExplosiveMinecart) { + if (!wcfg.isLogging(Logging.TNTMINECARTEXPLOSION)) { + return; + } + actor = new Actor("TNTMinecart"); + } else if (source instanceof Creeper) { + if (!wcfg.isLogging(Logging.CREEPEREXPLOSION)) { + return; + } + if (logCreeperExplosionsAsPlayerWhoTriggeredThese) { + final Entity target = ((Creeper) source).getTarget(); + actor = target instanceof Player ? Actor.actorFromEntity(target) : new Actor("Creeper"); + } else { + actor = new Actor("Creeper"); + } + } else if (source instanceof Wither) { + if (!wcfg.isLogging(Logging.WITHER)) { + return; + } + actor = Actor.actorFromEntity(source); + } else if (source instanceof WitherSkull) { + if (!wcfg.isLogging(Logging.WITHER_SKULL)) { + return; + } + actor = Actor.actorFromEntity(source); + } else if (source instanceof Fireball) { + Fireball fireball = (Fireball) source; + ProjectileSource shooter = fireball.getShooter(); + if (shooter == null) { + if (!wcfg.isLogging(Logging.MISCEXPLOSION)) { + return; + } + actor = Actor.actorFromEntity(source); + } else if (shooter instanceof Ghast) { + if (!wcfg.isLogging(Logging.GHASTFIREBALLEXPLOSION)) { + return; + } + actor = Actor.actorFromProjectileSource(shooter); + } else if (shooter instanceof Wither) { + if (!wcfg.isLogging(Logging.WITHER)) { + return; + } + actor = Actor.actorFromProjectileSource(shooter); + } + } else if (source instanceof EnderDragon) { + if (!wcfg.isLogging(Logging.ENDERDRAGON)) { + return; + } + actor = Actor.actorFromEntity(source); + } else if (source instanceof EnderCrystal) { + if (!wcfg.isLogging(Logging.ENDERCRYSTALEXPLOSION)) { + return; + } + actor = Actor.actorFromEntity(source); + + } else { + if (!wcfg.isLogging(Logging.MISCEXPLOSION)) { + return; + } + } + for (final Block block : event.blockList()) { + final Material type = block.getType(); + if (wcfg.isLogging(Logging.CHESTACCESS) && BukkitUtils.isContainerBlock(type) && !BukkitUtils.isShulkerBoxBlock(type)) { + consumer.queueContainerBreak(actor, block.getState()); + } else { + consumer.queueBlockBreak(actor, block.getState()); + } + } + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerInteract(PlayerInteractEvent event) { + if (event.getAction() == Action.RIGHT_CLICK_BLOCK && event.hasBlock()) { + Block block = event.getClickedBlock(); + if (BukkitUtils.isBed(block.getType()) && !block.getWorld().isBedWorks()) { + if (!Config.isLogging(block.getWorld(), Logging.BEDEXPLOSION)) { + return; + } + lastBedInteractionPlayer = event.getPlayer().getUniqueId(); + lastBedInteractionLocation = block.getLocation(); + new BukkitRunnable() { + @Override + public void run() { + lastBedInteractionPlayer = null; + lastBedInteractionLocation = null; + } + }.runTask(LogBlock.getInstance()); + } else if (block.getType() == Material.RESPAWN_ANCHOR && block.getBlockData() instanceof RespawnAnchor data) { + if (!Config.isLogging(block.getWorld(), Logging.RESPAWNANCHOREXPLOSION)) { + return; + } + ItemStack inHand = event.getItem(); + int charges = data.getCharges(); + if (charges < data.getMaximumCharges() && inHand != null && inHand.getType() == Material.GLOWSTONE) { + // charge + Actor actor = Actor.actorFromEntity(event.getPlayer()); + RespawnAnchor blockNew = (RespawnAnchor) data.clone(); + blockNew.setCharges(charges + 1); + consumer.queueBlockReplace(actor, block.getState(), blockNew); + } else if (charges > 0 && !block.getWorld().isRespawnAnchorWorks()) { + // explode + Actor actor = Actor.actorFromEntity(event.getPlayer()); + consumer.queueBlockBreak(actor, block.getState()); + lastRespawnAnchorInteractionPlayer = event.getPlayer().getUniqueId(); + lastRespawnAnchorInteractionLocation = block.getLocation(); + new BukkitRunnable() { + @Override + public void run() { + lastRespawnAnchorInteractionPlayer = null; + lastRespawnAnchorInteractionLocation = null; + } + }.runTask(LogBlock.getInstance()); + } + } + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockExplode(BlockExplodeEvent event) { + if (event.getExplosionResult() == ExplosionResult.KEEP || event.getExplosionResult() == ExplosionResult.TRIGGER_BLOCK) { + return; + } + Player bedCause = null; + if (lastBedInteractionPlayer != null && lastBedInteractionLocation != null) { + Location block = event.getBlock().getLocation(); + if (lastBedInteractionLocation.getWorld() == block.getWorld() && block.distanceSquared(lastBedInteractionLocation) <= 1) { + bedCause = Bukkit.getPlayer(lastBedInteractionPlayer); + } + } + Player respawnAnchorCause = null; + if (lastRespawnAnchorInteractionPlayer != null && lastRespawnAnchorInteractionLocation != null) { + Location block = event.getBlock().getLocation(); + if (lastRespawnAnchorInteractionLocation.equals(block)) { + respawnAnchorCause = Bukkit.getPlayer(lastRespawnAnchorInteractionPlayer); + } + } + + for (final Block block : event.blockList()) { + final WorldConfig wcfg = getWorldConfig(block.getLocation().getWorld()); + + if (wcfg != null) { + Actor actor = new Actor("Explosion"); + if (bedCause != null) { + if (!wcfg.isLogging(Logging.BEDEXPLOSION)) { + return; + } + if (Config.logBedExplosionsAsPlayerWhoTriggeredThese) { + actor = Actor.actorFromEntity(bedCause); + } else { + actor = new Actor("BedExplosion"); + } + } else if (respawnAnchorCause != null) { + if (!wcfg.isLogging(Logging.RESPAWNANCHOREXPLOSION)) { + return; + } + if (Config.logBedExplosionsAsPlayerWhoTriggeredThese) { + actor = Actor.actorFromEntity(respawnAnchorCause); + } else { + actor = new Actor("RespawnAnchorExplosion"); + } + } else if (!wcfg.isLogging(Logging.MISCEXPLOSION)) { + return; + } + + final Material type = block.getType(); + if (wcfg.isLogging(Logging.CHESTACCESS) && BukkitUtils.isContainerBlock(type) && !BukkitUtils.isShulkerBoxBlock(type)) { + consumer.queueContainerBreak(actor, block.getState()); + } else { + consumer.queueBlockBreak(actor, block.getState()); + } + } + } + } +} diff --git a/src/main/java/de/diddiz/LogBlock/listeners/FluidFlowLogging.java b/src/main/java/de/diddiz/LogBlock/listeners/FluidFlowLogging.java index ec1efc51..ca607c63 100644 --- a/src/main/java/de/diddiz/LogBlock/listeners/FluidFlowLogging.java +++ b/src/main/java/de/diddiz/LogBlock/listeners/FluidFlowLogging.java @@ -1,83 +1,127 @@ -package de.diddiz.LogBlock.listeners; - -import de.diddiz.LogBlock.Actor; -import de.diddiz.LogBlock.LogBlock; -import de.diddiz.LogBlock.Logging; -import de.diddiz.LogBlock.config.WorldConfig; -import org.bukkit.block.Block; -import org.bukkit.block.BlockFace; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.block.BlockFromToEvent; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -import static de.diddiz.LogBlock.config.Config.getWorldConfig; - -public class FluidFlowLogging extends LoggingListener { - private static final Set nonFluidProofBlocks = new HashSet(Arrays.asList(27, 28, 31, 32, 37, 38, 39, 40, 50, 51, 55, 59, 66, 69, 70, 75, 76, 78, 93, 94, 104, 105, 106)); - - public FluidFlowLogging(LogBlock lb) { - super(lb); - } - - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onBlockFromTo(BlockFromToEvent event) { - final WorldConfig wcfg = getWorldConfig(event.getBlock().getWorld()); - if (wcfg != null) { - final Block to = event.getToBlock(); - final int typeFrom = event.getBlock().getTypeId(); - final int typeTo = to.getTypeId(); - final boolean canFlow = typeTo == 0 || nonFluidProofBlocks.contains(typeTo); - if (typeFrom == 10 || typeFrom == 11) { - if (canFlow && wcfg.isLogging(Logging.LAVAFLOW)) { - if (isSurroundedByWater(to) && event.getBlock().getData() <= 2) { - consumer.queueBlockReplace(new Actor("LavaFlow"), to.getState(), 4, (byte) 0); - } else if (typeTo == 0) { - consumer.queueBlockPlace(new Actor("LavaFlow"), to.getLocation(), 10, (byte) (event.getBlock().getData() + 1)); - } else { - consumer.queueBlockReplace(new Actor("LavaFlow"), to.getState(), 10, (byte) (event.getBlock().getData() + 1)); - } - } else if (typeTo == 8 || typeTo == 9) { - if (event.getFace() == BlockFace.DOWN) { - consumer.queueBlockReplace(new Actor("LavaFlow"), to.getState(), 1, (byte) 0); - } else { - consumer.queueBlockReplace(new Actor("LavaFlow"), to.getState(), 4, (byte) 0); - } - } - } else if ((typeFrom == 8 || typeFrom == 9) && wcfg.isLogging(Logging.WATERFLOW)) { - if (typeTo == 0) { - consumer.queueBlockPlace(new Actor("WaterFlow"), to.getLocation(), 8, (byte) (event.getBlock().getData() + 1)); - } else if (nonFluidProofBlocks.contains(typeTo)) { - consumer.queueBlockReplace(new Actor("WaterFlow"), to.getState(), 8, (byte) (event.getBlock().getData() + 1)); - } else if (typeTo == 10 || typeTo == 11) { - if (to.getData() == 0) { - consumer.queueBlockReplace(new Actor("WaterFlow"), to.getState(), 49, (byte) 0); - } else if (event.getFace() == BlockFace.DOWN) { - consumer.queueBlockReplace(new Actor("LavaFlow"), to.getState(), 1, (byte) 0); - } - } - if (typeTo == 0 || nonFluidProofBlocks.contains(typeTo)) { - for (final BlockFace face : new BlockFace[]{BlockFace.DOWN, BlockFace.NORTH, BlockFace.WEST, BlockFace.EAST, BlockFace.SOUTH}) { - final Block lower = to.getRelative(face); - if (lower.getTypeId() == 10 || lower.getTypeId() == 11) { - consumer.queueBlockReplace(new Actor("WaterFlow"), lower.getState(), lower.getData() == 0 ? 49 : 4, (byte) 0); - } - } - } - } - } - } - - private static boolean isSurroundedByWater(Block block) { - for (final BlockFace face : new BlockFace[]{BlockFace.NORTH, BlockFace.WEST, BlockFace.EAST, BlockFace.SOUTH}) { - final int type = block.getRelative(face).getTypeId(); - if (type == 8 || type == 9) { - return true; - } - } - return false; - } -} +package de.diddiz.LogBlock.listeners; + +import de.diddiz.LogBlock.Actor; +import de.diddiz.LogBlock.LogBlock; +import de.diddiz.LogBlock.Logging; +import de.diddiz.LogBlock.config.Config; +import de.diddiz.LogBlock.config.WorldConfig; +import de.diddiz.LogBlock.util.BukkitUtils; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.Levelled; +import org.bukkit.block.data.Waterlogged; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.block.BlockFormEvent; +import org.bukkit.event.block.BlockFromToEvent; + +import static de.diddiz.LogBlock.config.Config.getWorldConfig; + +public class FluidFlowLogging extends LoggingListener { + + public FluidFlowLogging(LogBlock lb) { + super(lb); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockFromTo(BlockFromToEvent event) { + final WorldConfig wcfg = getWorldConfig(event.getBlock().getWorld()); + if (wcfg != null && (wcfg.isLogging(Logging.WATERFLOW) || wcfg.isLogging(Logging.LAVAFLOW))) { + final BlockData blockDataFrom = event.getBlock().getBlockData(); + Material typeFrom = blockDataFrom.getMaterial(); + boolean fromWaterlogged = false; + if (blockDataFrom instanceof Waterlogged) { + typeFrom = Material.WATER; + fromWaterlogged = true; + } + if (typeFrom == Material.SEAGRASS || typeFrom == Material.KELP_PLANT || typeFrom == Material.KELP) { + typeFrom = Material.WATER; + fromWaterlogged = true; + } + + Block source = Config.logFluidFlowAsPlayerWhoTriggeredIt ? event.getBlock() : null; + final Block to = event.getToBlock(); + final Material typeTo = to.getType(); + boolean down = event.getFace() == BlockFace.DOWN; + final boolean canFlow = BukkitUtils.isEmpty(typeTo) || BukkitUtils.isNonFluidProofBlock(typeTo); + if (typeFrom == Material.LAVA && wcfg.isLogging(Logging.LAVAFLOW)) { + Levelled levelledFrom = (Levelled) blockDataFrom; + if (canFlow) { + if (isSurroundedByWater(to) && levelledFrom.getLevel() <= 2) { + consumer.queueBlockReplace(new Actor("LavaFlow", source), to.getState(), Material.COBBLESTONE.createBlockData()); + } else { + Levelled newBlock = (Levelled) blockDataFrom.clone(); + newBlock.setLevel(down ? 1 : Math.min(levelledFrom.getLevel() + 1, levelledFrom.getMaximumLevel())); + if (BukkitUtils.isEmpty(typeTo)) { + consumer.queueBlockPlace(new Actor("LavaFlow", source), to.getLocation(), newBlock); + } else { + consumer.queueBlockReplace(new Actor("LavaFlow", source), to.getState(), newBlock); + } + } + } else if (typeTo == Material.WATER) { + if (down) { + consumer.queueBlockReplace(new Actor("LavaFlow", source), to.getState(), Material.STONE.createBlockData()); + } else { + consumer.queueBlockReplace(new Actor("LavaFlow", source), to.getState(), Material.COBBLESTONE.createBlockData()); + } + } + } else if ((typeFrom == Material.WATER) && wcfg.isLogging(Logging.WATERFLOW)) { + Levelled levelledFrom = fromWaterlogged ? null : (Levelled) blockDataFrom; + Levelled newBlock = (Levelled) Material.WATER.createBlockData(); + newBlock.setLevel(fromWaterlogged || down ? 1 : Math.min(levelledFrom.getLevel() + 1, levelledFrom.getMaximumLevel())); + if (BukkitUtils.isEmpty(typeTo)) { + consumer.queueBlockPlace(new Actor("WaterFlow", source), to.getLocation(), newBlock); + } else if (BukkitUtils.isNonFluidProofBlock(typeTo)) { + consumer.queueBlockReplace(new Actor("WaterFlow", source), to.getState(), newBlock); + } else if (typeTo == Material.LAVA) { + int toLevel = ((Levelled) to.getBlockData()).getLevel(); + if (toLevel == 0) { + consumer.queueBlockReplace(new Actor("WaterFlow", source), to.getState(), Material.OBSIDIAN.createBlockData()); + } else if (event.getFace() == BlockFace.DOWN) { + consumer.queueBlockReplace(new Actor("WaterFlow", source), to.getState(), Material.STONE.createBlockData()); + } + } + if (BukkitUtils.isEmpty(typeTo) || BukkitUtils.isNonFluidProofBlock(typeTo)) { + for (final BlockFace face : new BlockFace[] { BlockFace.DOWN, BlockFace.NORTH, BlockFace.WEST, BlockFace.EAST, BlockFace.SOUTH }) { + final Block lower = to.getRelative(face); + if (lower.getType() == Material.LAVA) { + int toLevel = ((Levelled) lower.getBlockData()).getLevel(); + if (toLevel == 0) { + consumer.queueBlockReplace(new Actor("WaterFlow", source), lower.getState(), Material.OBSIDIAN.createBlockData()); + } else if (event.getFace() == BlockFace.DOWN) { + consumer.queueBlockReplace(new Actor("WaterFlow", source), lower.getState(), Material.STONE.createBlockData()); + } + } + } + } + } + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockForm(BlockFormEvent event) { + final WorldConfig wcfg = getWorldConfig(event.getBlock().getWorld()); + if (wcfg != null && (wcfg.isLogging(Logging.WATERFLOW) || wcfg.isLogging(Logging.LAVAFLOW))) { + if (wcfg.isLogging(Logging.LAVAFLOW) && event.getBlock().getType() == Material.WATER && event.getNewState().getType() == Material.COBBLESTONE) { + consumer.queueBlockReplace(new Actor("LavaFlow"), event.getBlock().getBlockData(), event.getNewState()); + } + if (wcfg.isLogging(Logging.WATERFLOW) && event.getBlock().getType() == Material.LAVA) { + consumer.queueBlockReplace(new Actor("WaterFlow"), event.getBlock().getBlockData(), event.getNewState()); + } + if (wcfg.isLogging(Logging.WATERFLOW) && BukkitUtils.isConcreteBlock(event.getNewState().getType())) { + consumer.queueBlockReplace(new Actor("WaterFlow"), event.getBlock().getBlockData(), event.getNewState()); + } + } + } + + private static boolean isSurroundedByWater(Block block) { + for (final BlockFace face : new BlockFace[] { BlockFace.NORTH, BlockFace.WEST, BlockFace.EAST, BlockFace.SOUTH }) { + if (block.getRelative(face).getType() == Material.WATER) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/de/diddiz/LogBlock/listeners/InteractLogging.java b/src/main/java/de/diddiz/LogBlock/listeners/InteractLogging.java index dd3e350f..4942d707 100644 --- a/src/main/java/de/diddiz/LogBlock/listeners/InteractLogging.java +++ b/src/main/java/de/diddiz/LogBlock/listeners/InteractLogging.java @@ -1,103 +1,274 @@ -package de.diddiz.LogBlock.listeners; - -import de.diddiz.LogBlock.Actor; -import de.diddiz.LogBlock.LogBlock; -import de.diddiz.LogBlock.Logging; -import de.diddiz.LogBlock.config.WorldConfig; -import de.diddiz.util.BukkitUtils; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.block.Block; -import org.bukkit.block.BlockFace; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.block.Action; -import org.bukkit.event.player.PlayerInteractEvent; - -import static de.diddiz.LogBlock.config.Config.getWorldConfig; - -public class InteractLogging extends LoggingListener { - public InteractLogging(LogBlock lb) { - super(lb); - } - - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onPlayerInteract(PlayerInteractEvent event) { - final WorldConfig wcfg = getWorldConfig(event.getPlayer().getWorld()); - if (wcfg != null) { - final Block clicked = event.getClickedBlock(); - if (clicked == null) { - return; - } - final Material type = clicked.getType(); - final int typeId = type.getId(); - final byte blockData = clicked.getData(); - final Player player = event.getPlayer(); - final Location loc = clicked.getLocation(); - - switch (type) { - case LEVER: - case WOOD_BUTTON: - case STONE_BUTTON: - if (wcfg.isLogging(Logging.SWITCHINTERACT) && event.getAction() == Action.RIGHT_CLICK_BLOCK) { - consumer.queueBlock(Actor.actorFromEntity(player), loc, typeId, typeId, blockData); - } - break; - case FENCE_GATE: - case WOODEN_DOOR: - case TRAP_DOOR: - if (wcfg.isLogging(Logging.DOORINTERACT) && event.getAction() == Action.RIGHT_CLICK_BLOCK) { - consumer.queueBlock(Actor.actorFromEntity(player), loc, typeId, typeId, blockData); - } - break; - case CAKE_BLOCK: - if (wcfg.isLogging(Logging.CAKEEAT) && event.getAction() == Action.RIGHT_CLICK_BLOCK && player.getFoodLevel() < 20) { - consumer.queueBlock(Actor.actorFromEntity(player), loc, typeId, typeId, blockData); - } - break; - case NOTE_BLOCK: - if (wcfg.isLogging(Logging.NOTEBLOCKINTERACT) && event.getAction() == Action.RIGHT_CLICK_BLOCK) { - consumer.queueBlock(Actor.actorFromEntity(player), loc, typeId, typeId, blockData); - } - break; - case DIODE_BLOCK_OFF: - case DIODE_BLOCK_ON: - if (wcfg.isLogging(Logging.DIODEINTERACT) && event.getAction() == Action.RIGHT_CLICK_BLOCK) { - consumer.queueBlock(Actor.actorFromEntity(player), loc, typeId, typeId, blockData); - } - break; - case REDSTONE_COMPARATOR_OFF: - case REDSTONE_COMPARATOR_ON: - if (wcfg.isLogging(Logging.COMPARATORINTERACT) && event.getAction() == Action.RIGHT_CLICK_BLOCK) { - consumer.queueBlock(Actor.actorFromEntity(player), loc, typeId, typeId, blockData); - } - break; - case WOOD_PLATE: - case STONE_PLATE: - case IRON_PLATE: - case GOLD_PLATE: - if (wcfg.isLogging(Logging.PRESUREPLATEINTERACT) && event.getAction() == Action.PHYSICAL) { - consumer.queueBlock(Actor.actorFromEntity(player), loc, typeId, typeId, blockData); - } - break; - case TRIPWIRE: - if (wcfg.isLogging(Logging.TRIPWIREINTERACT) && event.getAction() == Action.PHYSICAL) { - consumer.queueBlock(Actor.actorFromEntity(player), loc, typeId, typeId, blockData); - } - break; - case SOIL: - if (wcfg.isLogging(Logging.CROPTRAMPLE) && event.getAction() == Action.PHYSICAL) { - // 3 = Dirt ID - consumer.queueBlock(Actor.actorFromEntity(player), loc, typeId, 3, blockData); - // Log the crop on top as being broken - Block trampledCrop = clicked.getRelative(BlockFace.UP); - if (BukkitUtils.getCropBlocks().contains(trampledCrop.getType())) { - consumer.queueBlockBreak(Actor.actorFromEntity(player), trampledCrop.getState()); - } - } - break; - } - } - } -} +package de.diddiz.LogBlock.listeners; + +import de.diddiz.LogBlock.Actor; +import de.diddiz.LogBlock.LogBlock; +import de.diddiz.LogBlock.Logging; +import de.diddiz.LogBlock.config.WorldConfig; +import de.diddiz.LogBlock.util.BukkitUtils; +import java.util.UUID; +import org.bukkit.DyeColor; +import org.bukkit.GameEvent; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Note; +import org.bukkit.Note.Tone; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.BlockState; +import org.bukkit.block.Sign; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.Directional; +import org.bukkit.block.data.Lightable; +import org.bukkit.block.data.Openable; +import org.bukkit.block.data.type.Cake; +import org.bukkit.block.data.type.Candle; +import org.bukkit.block.data.type.Comparator; +import org.bukkit.block.data.type.Comparator.Mode; +import org.bukkit.block.data.type.DaylightDetector; +import org.bukkit.block.data.type.Door; +import org.bukkit.block.data.type.NoteBlock; +import org.bukkit.block.data.type.Repeater; +import org.bukkit.block.data.type.Switch; +import org.bukkit.block.data.type.TurtleEgg; +import org.bukkit.block.sign.Side; +import org.bukkit.block.sign.SignSide; +import org.bukkit.entity.Player; +import org.bukkit.event.Event.Result; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.block.Action; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.world.GenericGameEvent; +import org.bukkit.inventory.ItemStack; + +import static de.diddiz.LogBlock.config.Config.getWorldConfig; + +public class InteractLogging extends LoggingListener { + public InteractLogging(LogBlock lb) { + super(lb); + } + + private UUID lastInteractionPlayer; + private BlockData lastInteractionBlockData; + private Location lastInteractionLocation; + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerInteract(PlayerInteractEvent event) { + final WorldConfig wcfg = getWorldConfig(event.getPlayer().getWorld()); + if (wcfg != null) { + final Block clicked = event.getClickedBlock(); + if (clicked == null) { + return; + } + final BlockData blockData = clicked.getBlockData(); + final Material type = blockData.getMaterial(); + final Player player = event.getPlayer(); + final Location loc = clicked.getLocation(); + lastInteractionPlayer = player.getUniqueId(); + lastInteractionBlockData = blockData; + lastInteractionLocation = loc; + + if (BukkitUtils.isFenceGate(type) || BukkitUtils.isWoodenTrapdoor(type)) { + if (wcfg.isLogging(Logging.DOORINTERACT) && event.getAction() == Action.RIGHT_CLICK_BLOCK) { + Openable newBlockData = (Openable) blockData.clone(); + newBlockData.setOpen(!newBlockData.isOpen()); + consumer.queueBlock(Actor.actorFromEntity(player), loc, blockData, newBlockData); + } + } else if (BukkitUtils.isPressurePlate(type)) { + if (wcfg.isLogging(Logging.PRESUREPLATEINTERACT) && event.getAction() == Action.PHYSICAL) { + consumer.queueBlock(Actor.actorFromEntity(player), loc, blockData, blockData); + } + } else if (BukkitUtils.isWoodenDoor(type)) { + if (wcfg.isLogging(Logging.DOORINTERACT) && event.getAction() == Action.RIGHT_CLICK_BLOCK) { + Door newBlockData = (Door) blockData.clone(); + newBlockData.setOpen(!newBlockData.isOpen()); + consumer.queueBlock(Actor.actorFromEntity(player), loc, blockData, newBlockData); + } + } else if (BukkitUtils.isButton(type) || type == Material.LEVER) { + if (wcfg.isLogging(Logging.SWITCHINTERACT) && event.getAction() == Action.RIGHT_CLICK_BLOCK) { + Switch newBlockData = (Switch) blockData.clone(); + if (!newBlockData.isPowered() || type == Material.LEVER) { + newBlockData.setPowered(!newBlockData.isPowered()); + } + consumer.queueBlock(Actor.actorFromEntity(player), loc, blockData, newBlockData); + } + } else if (BukkitUtils.isSign(type)) { + if (wcfg.isLogging(Logging.SIGNTEXT) && event.getAction() == Action.RIGHT_CLICK_BLOCK && event.getItem() != null) { + Material itemType = event.getItem().getType(); + if (BukkitUtils.isDye(itemType) || itemType == Material.GLOW_INK_SAC || itemType == Material.INK_SAC || itemType == Material.HONEYCOMB) { + final BlockState before = event.getClickedBlock().getState(); + if (before instanceof Sign signBefore) { + if (!signBefore.isWaxed()) { + final Sign signAfter = (Sign) event.getClickedBlock().getState(); + Side side = BukkitUtils.getFacingSignSide(player, clicked); + SignSide signSideBefore = signBefore.getSide(side); + SignSide signSideAfter = signAfter.getSide(side); + if (itemType == Material.GLOW_INK_SAC) { + if (!signSideBefore.isGlowingText() && hasText(signSideBefore)) { + signSideAfter.setGlowingText(true); + consumer.queueBlockReplace(Actor.actorFromEntity(player), signBefore, signAfter); + } + } else if (itemType == Material.INK_SAC) { + if (signSideBefore.isGlowingText() && hasText(signSideBefore)) { + signSideAfter.setGlowingText(false); + consumer.queueBlockReplace(Actor.actorFromEntity(player), signBefore, signAfter); + } + } else if (itemType == Material.HONEYCOMB) { + signAfter.setWaxed(true); + consumer.queueBlockReplace(Actor.actorFromEntity(player), signBefore, signAfter); + } else if (BukkitUtils.isDye(itemType) && hasText(signSideBefore)) { + DyeColor newColor = BukkitUtils.dyeToDyeColor(itemType); + if (newColor != null && signSideBefore.getColor() != newColor) { + signSideAfter.setColor(newColor); + consumer.queueBlockReplace(Actor.actorFromEntity(player), signBefore, signAfter); + } + } + } + } + } + } + } else if (type == Material.CAKE) { + if (event.hasItem() && BukkitUtils.isCandle(event.getItem().getType()) && event.useItemInHand() != Result.DENY) { + BlockData newBlockData = Material.valueOf(event.getItem().getType().name() + "_CAKE").createBlockData(); + consumer.queueBlock(Actor.actorFromEntity(player), loc, blockData, newBlockData); + } else if (wcfg.isLogging(Logging.CAKEEAT) && event.getAction() == Action.RIGHT_CLICK_BLOCK && player.getFoodLevel() < 20) { + Cake newBlockData = (Cake) blockData.clone(); + if (newBlockData.getBites() < 6) { + newBlockData.setBites(newBlockData.getBites() + 1); + consumer.queueBlock(Actor.actorFromEntity(player), loc, blockData, newBlockData); + } else { + consumer.queueBlock(Actor.actorFromEntity(player), loc, blockData, Material.AIR.createBlockData()); + } + } + } else if (type == Material.NOTE_BLOCK) { + if (wcfg.isLogging(Logging.NOTEBLOCKINTERACT) && event.getAction() == Action.RIGHT_CLICK_BLOCK) { + NoteBlock newBlockData = (NoteBlock) blockData.clone(); + if (newBlockData.getNote().getOctave() == 2) { + newBlockData.setNote(new Note(0, Tone.F, true)); + } else { + newBlockData.setNote(newBlockData.getNote().sharped()); + } + consumer.queueBlock(Actor.actorFromEntity(player), loc, blockData, newBlockData); + } + } else if (type == Material.REPEATER) { + if (wcfg.isLogging(Logging.DIODEINTERACT) && event.getAction() == Action.RIGHT_CLICK_BLOCK) { + Repeater newBlockData = (Repeater) blockData.clone(); + newBlockData.setDelay((newBlockData.getDelay() % 4) + 1); + consumer.queueBlock(Actor.actorFromEntity(player), loc, blockData, newBlockData); + } + } else if (type == Material.COMPARATOR) { + if (wcfg.isLogging(Logging.COMPARATORINTERACT) && event.getAction() == Action.RIGHT_CLICK_BLOCK) { + Comparator newBlockData = (Comparator) blockData.clone(); + newBlockData.setMode(newBlockData.getMode() == Mode.COMPARE ? Mode.SUBTRACT : Mode.COMPARE); + consumer.queueBlock(Actor.actorFromEntity(player), loc, blockData, newBlockData); + } + } else if (type == Material.DAYLIGHT_DETECTOR) { + if (wcfg.isLogging(Logging.DAYLIGHTDETECTORINTERACT) && event.getAction() == Action.RIGHT_CLICK_BLOCK) { + DaylightDetector newBlockData = (DaylightDetector) blockData.clone(); + newBlockData.setInverted(!newBlockData.isInverted()); + consumer.queueBlock(Actor.actorFromEntity(player), loc, blockData, newBlockData); + } + } else if (type == Material.TRIPWIRE) { + if (wcfg.isLogging(Logging.TRIPWIREINTERACT) && event.getAction() == Action.PHYSICAL) { + consumer.queueBlock(Actor.actorFromEntity(player), loc, blockData, blockData); + } + } else if (type == Material.FARMLAND) { + if (wcfg.isLogging(Logging.CROPTRAMPLE) && event.getAction() == Action.PHYSICAL) { + // 3 = Dirt ID + consumer.queueBlock(Actor.actorFromEntity(player), loc, blockData, Material.DIRT.createBlockData()); + // Log the crop on top as being broken + Block trampledCrop = clicked.getRelative(BlockFace.UP); + if (BukkitUtils.isCropBlock(trampledCrop.getType())) { + consumer.queueBlockBreak(Actor.actorFromEntity(player), trampledCrop.getState()); + } + } + } else if (type == Material.TURTLE_EGG) { + if (wcfg.isLogging(Logging.BLOCKBREAK) && event.getAction() == Action.PHYSICAL) { + TurtleEgg turtleEggData = (TurtleEgg) blockData; + int eggs = turtleEggData.getEggs(); + if (eggs > 1) { + TurtleEgg turtleEggData2 = (TurtleEgg) turtleEggData.clone(); + turtleEggData2.setEggs(eggs - 1); + consumer.queueBlock(Actor.actorFromEntity(player), loc, turtleEggData, turtleEggData2); + } else { + consumer.queueBlock(Actor.actorFromEntity(player), loc, turtleEggData, Material.AIR.createBlockData()); + } + } + } else if (type == Material.PUMPKIN) { + if ((wcfg.isLogging(Logging.BLOCKBREAK) || wcfg.isLogging(Logging.BLOCKPLACE)) && event.getAction() == Action.RIGHT_CLICK_BLOCK) { + ItemStack inHand = event.getItem(); + if (inHand != null && inHand.getType() == Material.SHEARS) { + BlockFace clickedFace = event.getBlockFace(); + Directional newBlockData = (Directional) Material.CARVED_PUMPKIN.createBlockData(); + if (clickedFace == BlockFace.NORTH || clickedFace == BlockFace.SOUTH || clickedFace == BlockFace.EAST || clickedFace == BlockFace.WEST) { + newBlockData.setFacing(clickedFace); + } else { + // use player distance to calculate the facing + Location playerLoc = player.getLocation(); + playerLoc.subtract(0.5, 0, 0.5); + double dx = playerLoc.getX() - loc.getX(); + double dz = playerLoc.getZ() - loc.getZ(); + if (Math.abs(dx) > Math.abs(dz)) { + newBlockData.setFacing(dx > 0 ? BlockFace.EAST : BlockFace.WEST); + } else { + newBlockData.setFacing(dz > 0 ? BlockFace.SOUTH : BlockFace.NORTH); + } + } + consumer.queueBlock(Actor.actorFromEntity(player), loc, blockData, newBlockData); + } + } + } + } + } + + private boolean hasText(SignSide signSide) { + for (int i = 0; i < 4; i++) { + if (!signSide.getLine(i).isEmpty()) { + return true; + } + } + return false; + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onGenericGameEvent(GenericGameEvent event) { + if (lastInteractionPlayer != null && event.getEntity() != null && event.getEntity().getUniqueId().equals(lastInteractionPlayer) && lastInteractionLocation != null && event.getLocation().equals(lastInteractionLocation)) { + if (lastInteractionBlockData instanceof Candle) { + Candle previousCandle = (Candle) lastInteractionBlockData; + if (previousCandle.isLit()) { + BlockData newData = lastInteractionLocation.getBlock().getBlockData(); + if (newData instanceof Candle) { + Candle newCandle = (Candle) newData; + if (!newCandle.isLit() && !newCandle.isWaterlogged()) { + // log candle extinguish + consumer.queueBlockReplace(Actor.actorFromEntity(event.getEntity()), lastInteractionLocation, lastInteractionBlockData, newData); + } + } + } + } else if (lastInteractionBlockData instanceof Lightable && BukkitUtils.isCandleCake(lastInteractionBlockData.getMaterial())) { + Lightable previousLightable = (Lightable) lastInteractionBlockData; + BlockData newData = lastInteractionLocation.getBlock().getBlockData(); + if (event.getEvent().equals(GameEvent.EAT)) { + final WorldConfig wcfg = getWorldConfig(event.getLocation().getWorld()); + if (wcfg.isLogging(Logging.CAKEEAT)) { + // nom nom (don't know why newData is incorrect here) + newData = Material.CAKE.createBlockData(); + ((Cake) newData).setBites(1); + consumer.queueBlockReplace(Actor.actorFromEntity(event.getEntity()), lastInteractionLocation, lastInteractionBlockData, newData); + } + } else if (previousLightable.isLit()) { + if (newData instanceof Lightable) { + Lightable newLightable = (Lightable) newData; + if (!newLightable.isLit()) { + // log cake extinguish + consumer.queueBlockReplace(Actor.actorFromEntity(event.getEntity()), lastInteractionLocation, lastInteractionBlockData, newData); + } + } + } + } + } + lastInteractionPlayer = null; + lastInteractionBlockData = null; + lastInteractionLocation = null; + } +} diff --git a/src/main/java/de/diddiz/LogBlock/listeners/KillLogging.java b/src/main/java/de/diddiz/LogBlock/listeners/KillLogging.java index 49c8c76e..c6aff64e 100644 --- a/src/main/java/de/diddiz/LogBlock/listeners/KillLogging.java +++ b/src/main/java/de/diddiz/LogBlock/listeners/KillLogging.java @@ -1,50 +1,49 @@ -package de.diddiz.LogBlock.listeners; - -import de.diddiz.LogBlock.Actor; -import de.diddiz.LogBlock.LogBlock; -import de.diddiz.LogBlock.Logging; -import de.diddiz.LogBlock.config.Config.*; -import org.bukkit.entity.Entity; -import org.bukkit.entity.LivingEntity; -import org.bukkit.entity.Monster; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.entity.EntityDamageByEntityEvent; -import org.bukkit.event.entity.EntityDamageEvent; -import org.bukkit.event.entity.EntityDeathEvent; - -import static de.diddiz.LogBlock.config.Config.*; - - -public class KillLogging extends LoggingListener { - - public KillLogging(LogBlock lb) { - super(lb); - } - - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onEntityDeath(EntityDeathEvent deathEvent) { - EntityDamageEvent event = deathEvent.getEntity().getLastDamageCause(); - // For a death event, there should always be a damage event and it should not be cancelled. Check anyway. - if (event != null && event.isCancelled() == false && isLogging(event.getEntity().getWorld(), Logging.KILL) && event.getEntity() instanceof LivingEntity) { - final LivingEntity victim = (LivingEntity) event.getEntity(); - if (event instanceof EntityDamageByEntityEvent) { - final Entity killer = ((EntityDamageByEntityEvent) event).getDamager(); - if (logKillsLevel == LogKillsLevel.PLAYERS && !(victim instanceof Player && killer instanceof Player)) { - return; - } else if (logKillsLevel == LogKillsLevel.MONSTERS && !((victim instanceof Player || victim instanceof Monster) && killer instanceof Player || killer instanceof Monster)) { - return; - } - consumer.queueKill(killer, victim); - } else if (logEnvironmentalKills) { - if (logKillsLevel == LogKillsLevel.PLAYERS && !(victim instanceof Player)) { - return; - } else if (logKillsLevel == LogKillsLevel.MONSTERS && !((victim instanceof Player || victim instanceof Monster))) { - return; - } - consumer.queueKill(new Actor(event.getCause().toString()), victim); - } - } - } -} +package de.diddiz.LogBlock.listeners; + +import de.diddiz.LogBlock.Actor; +import de.diddiz.LogBlock.LogBlock; +import de.diddiz.LogBlock.Logging; +import de.diddiz.LogBlock.config.Config.*; +import org.bukkit.NamespacedKey; +import org.bukkit.entity.Entity; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Monster; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.entity.EntityDeathEvent; + +import static de.diddiz.LogBlock.config.Config.*; + +public class KillLogging extends LoggingListener { + + public KillLogging(LogBlock lb) { + super(lb); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onEntityDeath(EntityDeathEvent deathEvent) { + if (isLogging(deathEvent.getEntity().getWorld(), Logging.KILL)) { + LivingEntity victim = deathEvent.getEntity(); + Entity killer = deathEvent.getDamageSource().getCausingEntity(); + if (killer != null) { + if (logKillsLevel == LogKillsLevel.PLAYERS && !(victim instanceof Player && killer instanceof Player)) { + return; + } else if (logKillsLevel == LogKillsLevel.MONSTERS && !((victim instanceof Player || victim instanceof Monster) && killer instanceof Player || killer instanceof Monster)) { + return; + } + consumer.queueKill(killer, victim); + } else if (logEnvironmentalKills) { + if (logKillsLevel == LogKillsLevel.PLAYERS && !(victim instanceof Player)) { + return; + } else if (logKillsLevel == LogKillsLevel.MONSTERS && !((victim instanceof Player || victim instanceof Monster))) { + return; + } + NamespacedKey key = deathEvent.getDamageSource().getDamageType().getKey(); + Actor actor = new Actor(key == null ? "unknown" : key.getKey().toUpperCase()); + + consumer.queueKill(actor, victim); + } + } + } +} diff --git a/src/main/java/de/diddiz/LogBlock/listeners/LeavesDecayLogging.java b/src/main/java/de/diddiz/LogBlock/listeners/LeavesDecayLogging.java index 055974ea..c75ea06f 100644 --- a/src/main/java/de/diddiz/LogBlock/listeners/LeavesDecayLogging.java +++ b/src/main/java/de/diddiz/LogBlock/listeners/LeavesDecayLogging.java @@ -1,26 +1,26 @@ -package de.diddiz.LogBlock.listeners; - -import de.diddiz.LogBlock.Actor; -import de.diddiz.LogBlock.LogBlock; -import de.diddiz.LogBlock.Logging; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.block.LeavesDecayEvent; - -import static de.diddiz.LogBlock.config.Config.isLogging; -import static de.diddiz.util.LoggingUtil.smartLogBlockBreak; -import static de.diddiz.util.LoggingUtil.smartLogFallables; - -public class LeavesDecayLogging extends LoggingListener { - public LeavesDecayLogging(LogBlock lb) { - super(lb); - } - - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onLeavesDecay(LeavesDecayEvent event) { - if (isLogging(event.getBlock().getWorld(), Logging.LEAVESDECAY)) { - smartLogBlockBreak(consumer, new Actor("LeavesDecay"), event.getBlock()); - smartLogFallables(consumer, new Actor("LeavesDecay"), event.getBlock()); - } - } -} +package de.diddiz.LogBlock.listeners; + +import de.diddiz.LogBlock.Actor; +import de.diddiz.LogBlock.LogBlock; +import de.diddiz.LogBlock.Logging; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.block.LeavesDecayEvent; + +import static de.diddiz.LogBlock.config.Config.isLogging; +import static de.diddiz.LogBlock.util.LoggingUtil.smartLogBlockBreak; +import static de.diddiz.LogBlock.util.LoggingUtil.smartLogFallables; + +public class LeavesDecayLogging extends LoggingListener { + public LeavesDecayLogging(LogBlock lb) { + super(lb); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onLeavesDecay(LeavesDecayEvent event) { + if (isLogging(event.getBlock().getWorld(), Logging.LEAVESDECAY)) { + smartLogBlockBreak(consumer, new Actor("LeavesDecay"), event.getBlock()); + smartLogFallables(consumer, new Actor("LeavesDecay"), event.getBlock()); + } + } +} diff --git a/src/main/java/de/diddiz/LogBlock/listeners/LecternLogging.java b/src/main/java/de/diddiz/LogBlock/listeners/LecternLogging.java new file mode 100644 index 00000000..df26b2b7 --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/listeners/LecternLogging.java @@ -0,0 +1,79 @@ +package de.diddiz.LogBlock.listeners; + +import static de.diddiz.LogBlock.config.Config.getWorldConfig; +import static de.diddiz.LogBlock.config.Config.isLogging; + +import de.diddiz.LogBlock.Actor; +import de.diddiz.LogBlock.LogBlock; +import de.diddiz.LogBlock.Logging; +import de.diddiz.LogBlock.config.WorldConfig; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.Tag; +import org.bukkit.block.Block; +import org.bukkit.block.Lectern; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.block.Action; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.player.PlayerTakeLecternBookEvent; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.ItemStack; + +public class LecternLogging extends LoggingListener { + public LecternLogging(LogBlock lb) { + super(lb); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerInteract(PlayerInteractEvent event) { + final Block clicked = event.getClickedBlock(); + if (event.getAction() != Action.RIGHT_CLICK_BLOCK || event.getHand() != EquipmentSlot.HAND || !event.hasBlock() || clicked == null) { + return; + } + final Player player = event.getPlayer(); + if (!isLogging(player.getWorld(), Logging.LECTERNBOOKCHANGE)) { + return; + } + final Material type = clicked.getType(); + if (type == Material.LECTERN) { + ItemStack mainHand = player.getInventory().getItemInMainHand(); + if (mainHand != null && mainHand.getType() != Material.AIR && Tag.ITEMS_LECTERN_BOOKS.isTagged(mainHand.getType()) && clicked.getState() instanceof Lectern lectern) { + ItemStack currentInLectern = lectern.getSnapshotInventory().getItem(0); + if (currentInLectern == null || currentInLectern.getType() == Material.AIR) { + ItemStack stack = mainHand.clone(); + stack.setAmount(1); + Lectern newLectern = (Lectern) clicked.getState(); + newLectern.getSnapshotInventory().setItem(0, stack); + org.bukkit.block.data.type.Lectern blockDataOld = (org.bukkit.block.data.type.Lectern) newLectern.getBlockData(); + org.bukkit.block.data.type.Lectern blockDataWithBook = (org.bukkit.block.data.type.Lectern) Bukkit.createBlockData("lectern[has_book=true]"); + blockDataWithBook.setFacing(blockDataOld.getFacing()); + blockDataWithBook.setPowered(blockDataOld.isPowered()); + newLectern.setBlockData(blockDataWithBook); + consumer.queueBlockReplace(Actor.actorFromEntity(event.getPlayer()), lectern, newLectern); + } + } + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerTakeLecternBook(PlayerTakeLecternBookEvent event) { + final WorldConfig wcfg = getWorldConfig(event.getPlayer().getWorld()); + if (wcfg != null && wcfg.isLogging(Logging.LECTERNBOOKCHANGE)) { + Lectern oldState = event.getLectern(); + Lectern newState = (Lectern) oldState.getBlock().getState(); + try { + newState.getSnapshotInventory().setItem(0, null); + } catch (NullPointerException e) { + //ignored + } + org.bukkit.block.data.type.Lectern oldBlockData = (org.bukkit.block.data.type.Lectern) oldState.getBlockData(); + org.bukkit.block.data.type.Lectern blockData = (org.bukkit.block.data.type.Lectern) Material.LECTERN.createBlockData(); + blockData.setFacing(oldBlockData.getFacing()); + blockData.setPowered(oldBlockData.isPowered()); + newState.setBlockData(blockData); + consumer.queueBlockReplace(Actor.actorFromEntity(event.getPlayer()), oldState, newState); + } + } +} diff --git a/src/main/java/de/diddiz/LogBlock/listeners/LockedChestDecayLogging.java b/src/main/java/de/diddiz/LogBlock/listeners/LockedChestDecayLogging.java deleted file mode 100644 index 159d12b3..00000000 --- a/src/main/java/de/diddiz/LogBlock/listeners/LockedChestDecayLogging.java +++ /dev/null @@ -1,26 +0,0 @@ -package de.diddiz.LogBlock.listeners; - -import de.diddiz.LogBlock.Actor; -import de.diddiz.LogBlock.LogBlock; -import de.diddiz.LogBlock.Logging; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.block.BlockFadeEvent; - -import static de.diddiz.LogBlock.config.Config.isLogging; - -public class LockedChestDecayLogging extends LoggingListener { - public LockedChestDecayLogging(LogBlock lb) { - super(lb); - } - - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onBlockFade(BlockFadeEvent event) { - if (isLogging(event.getBlock().getWorld(), Logging.LOCKEDCHESTDECAY)) { - final int type = event.getBlock().getTypeId(); - if (type == 95) { - consumer.queueBlockReplace(new Actor("LockedChestDecay"), event.getBlock().getState(), event.getNewState()); - } - } - } -} diff --git a/src/main/java/de/diddiz/LogBlock/listeners/LoggingListener.java b/src/main/java/de/diddiz/LogBlock/listeners/LoggingListener.java index 4f910892..ca5db00a 100644 --- a/src/main/java/de/diddiz/LogBlock/listeners/LoggingListener.java +++ b/src/main/java/de/diddiz/LogBlock/listeners/LoggingListener.java @@ -1,13 +1,13 @@ -package de.diddiz.LogBlock.listeners; - -import de.diddiz.LogBlock.Consumer; -import de.diddiz.LogBlock.LogBlock; -import org.bukkit.event.Listener; - -public class LoggingListener implements Listener { - protected final Consumer consumer; - - public LoggingListener(LogBlock lb) { - consumer = lb.getConsumer(); - } -} +package de.diddiz.LogBlock.listeners; + +import de.diddiz.LogBlock.Consumer; +import de.diddiz.LogBlock.LogBlock; +import org.bukkit.event.Listener; + +public class LoggingListener implements Listener { + protected final Consumer consumer; + + public LoggingListener(LogBlock lb) { + consumer = lb.getConsumer(); + } +} diff --git a/src/main/java/de/diddiz/LogBlock/listeners/OxidizationLogging.java b/src/main/java/de/diddiz/LogBlock/listeners/OxidizationLogging.java new file mode 100644 index 00000000..fcdfd673 --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/listeners/OxidizationLogging.java @@ -0,0 +1,28 @@ +package de.diddiz.LogBlock.listeners; + +import static de.diddiz.LogBlock.config.Config.isLogging; + +import de.diddiz.LogBlock.Actor; +import de.diddiz.LogBlock.LogBlock; +import de.diddiz.LogBlock.Logging; +import org.bukkit.Material; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.block.BlockFormEvent; + +public class OxidizationLogging extends LoggingListener { + public OxidizationLogging(LogBlock lb) { + super(lb); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockPhysics(BlockFormEvent event) { + if (isLogging(event.getBlock().getWorld(), Logging.OXIDIZATION)) { + final Material type = event.getNewState().getType(); + if (type.name().contains("COPPER")) { + consumer.queueBlockReplace(new Actor("NaturalOxidization"), event.getBlock().getState(), event.getNewState()); + } + } + } + +} diff --git a/src/main/java/de/diddiz/LogBlock/listeners/PlayerInfoLogging.java b/src/main/java/de/diddiz/LogBlock/listeners/PlayerInfoLogging.java index dad35039..f10d65d6 100644 --- a/src/main/java/de/diddiz/LogBlock/listeners/PlayerInfoLogging.java +++ b/src/main/java/de/diddiz/LogBlock/listeners/PlayerInfoLogging.java @@ -1,23 +1,43 @@ -package de.diddiz.LogBlock.listeners; - -import de.diddiz.LogBlock.LogBlock; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.event.player.PlayerQuitEvent; - -public class PlayerInfoLogging extends LoggingListener { - public PlayerInfoLogging(LogBlock lb) { - super(lb); - } - - @EventHandler(priority = EventPriority.MONITOR) - public void onPlayerJoin(PlayerJoinEvent event) { - consumer.queueJoin(event.getPlayer()); - } - - @EventHandler(priority = EventPriority.MONITOR) - public void onPlayerQuit(PlayerQuitEvent event) { - consumer.queueLeave(event.getPlayer()); - } -} +package de.diddiz.LogBlock.listeners; + +import de.diddiz.LogBlock.LogBlock; +import de.diddiz.LogBlock.config.Config; + +import java.util.HashMap; +import java.util.UUID; + +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; + +public class PlayerInfoLogging extends LoggingListener { + + private final HashMap playerLogins = new HashMap<>(); + + public PlayerInfoLogging(LogBlock lb) { + super(lb); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onPlayerJoin(PlayerJoinEvent event) { + playerLogins.put(event.getPlayer().getUniqueId(), System.currentTimeMillis()); + consumer.queueJoin(event.getPlayer()); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onPlayerQuit(PlayerQuitEvent event) { + onPlayerQuit(event.getPlayer()); + } + + public void onPlayerQuit(Player player) { + Long joinTime = playerLogins.remove(player.getUniqueId()); + if (Config.logPlayerInfo && joinTime != null) { + long onlineTime = (System.currentTimeMillis() - joinTime) / 1000; + if (onlineTime > 0) { + consumer.queueLeave(player, onlineTime); + } + } + } +} diff --git a/src/main/java/de/diddiz/LogBlock/listeners/ScaffoldingLogging.java b/src/main/java/de/diddiz/LogBlock/listeners/ScaffoldingLogging.java new file mode 100644 index 00000000..032c7e94 --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/listeners/ScaffoldingLogging.java @@ -0,0 +1,171 @@ +package de.diddiz.LogBlock.listeners; + +import de.diddiz.LogBlock.Actor; +import de.diddiz.LogBlock.LogBlock; +import de.diddiz.LogBlock.Logging; +import java.util.ArrayDeque; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockFadeEvent; +import org.bukkit.event.block.BlockPlaceEvent; + +import static de.diddiz.LogBlock.config.Config.isLogging; +import static de.diddiz.LogBlock.util.LoggingUtil.smartLogFallables; + +public class ScaffoldingLogging extends LoggingListener { + private final static long MAX_SCAFFOLDING_LOG_TIME_MS = 2000; + private final static EnumSet NEIGHBOURS_SIDES_AND_UP = EnumSet.of(BlockFace.UP, BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST); + private final static EnumSet NEIGHBOURS_SIDES_AND_BELOW = EnumSet.of(BlockFace.DOWN, BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST); + + private final ArrayDeque scaffoldingBreakersList = new ArrayDeque<>(); + private final HashMap scaffoldingBreakersByLocation = new HashMap<>(); + private final HashMap scaffoldingPlacersByLocation = new HashMap<>(); + + public ScaffoldingLogging(LogBlock lb) { + super(lb); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockFade(BlockFadeEvent event) { + Block block = event.getBlock(); + if (isLogging(block.getWorld(), Logging.SCAFFOLDING)) { + final Material type = block.getType(); + if (type == Material.SCAFFOLDING) { + Actor actor = scaffoldingPlacersByLocation.get(block.getLocation()); // get placer before cleanupScaffoldingBreakers + cleanupScaffoldingBreakers(); + if (actor == null) { + actor = getScaffoldingBreaker(block); + if (actor != null) { + for (BlockFace dir : NEIGHBOURS_SIDES_AND_UP) { + Block otherBlock = block.getRelative(dir); + if (otherBlock.getType() == Material.SCAFFOLDING) { + addScaffoldingBreaker(actor, otherBlock); + } + } + } else { + actor = new Actor("ScaffoldingFall"); + } + } + consumer.queueBlockReplace(actor, block.getState(), event.getNewState()); + smartLogFallables(consumer, actor, block); + } + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockBreak(BlockBreakEvent event) { + Block block = event.getBlock(); + if (isLogging(block.getWorld(), Logging.SCAFFOLDING)) { + cleanupScaffoldingBreakers(); + Block otherBlock; + if (block.getType() == Material.SCAFFOLDING) { + for (BlockFace dir : NEIGHBOURS_SIDES_AND_UP) { + otherBlock = block.getRelative(dir); + if (otherBlock.getType() == Material.SCAFFOLDING) { + addScaffoldingBreaker(Actor.actorFromEntity(event.getPlayer()), otherBlock); + } + } + } else if ((otherBlock = block.getRelative(BlockFace.UP)).getType() == Material.SCAFFOLDING) { + addScaffoldingBreaker(Actor.actorFromEntity(event.getPlayer()), otherBlock); + } + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockPlace(BlockPlaceEvent event) { + Block block = event.getBlock(); + if (isLogging(block.getWorld(), Logging.SCAFFOLDING)) { + cleanupScaffoldingBreakers(); + if (block.getType() == Material.SCAFFOLDING) { + scaffoldingPlacersByLocation.put(block.getLocation(), Actor.actorFromEntity(event.getPlayer())); + } + } + } + + public void addScaffoldingBreaker(Actor actor, Block block) { + ScaffoldingBreaker breaker = new ScaffoldingBreaker(actor, block.getLocation()); + scaffoldingBreakersList.addLast(breaker); + scaffoldingBreakersByLocation.put(breaker.getLocation(), breaker); + + } + + private void cleanupScaffoldingBreakers() { + if (!scaffoldingPlacersByLocation.isEmpty()) { + scaffoldingPlacersByLocation.clear(); + } + if (!scaffoldingBreakersList.isEmpty()) { + long time = System.currentTimeMillis() - MAX_SCAFFOLDING_LOG_TIME_MS; + while (!scaffoldingBreakersList.isEmpty() && scaffoldingBreakersList.getFirst().getTime() < time) { + ScaffoldingBreaker breaker = scaffoldingBreakersList.removeFirst(); + scaffoldingBreakersByLocation.remove(breaker.getLocation(), breaker); + } + } + } + + private Actor getScaffoldingBreaker(Block block) { + if (scaffoldingBreakersList.isEmpty()) { + return null; + } + + ScaffoldingBreaker breaker = scaffoldingBreakersByLocation.get(block.getLocation()); + if (breaker != null) { + return breaker.getActor(); + } + + // Search all connected scaffoldings + ArrayDeque front = new ArrayDeque<>(); + HashSet frontAndDone = new HashSet<>(); + front.addLast(block); + frontAndDone.add(block); + while (!front.isEmpty()) { + Block current = front.removeFirst(); + Location loc = current.getLocation(); + + breaker = scaffoldingBreakersByLocation.get(loc); + if (breaker != null) { + return breaker.getActor(); + } + + for (BlockFace dir : NEIGHBOURS_SIDES_AND_BELOW) { + Block otherBlock = current.getRelative(dir); + if (!frontAndDone.contains(otherBlock) && otherBlock.getType() == Material.SCAFFOLDING) { + front.addLast(otherBlock); + frontAndDone.add(otherBlock); + } + } + } + return null; + } + + class ScaffoldingBreaker { + protected final Actor actor; + protected final long time; + protected final Location location; + + public ScaffoldingBreaker(Actor actor, Location location) { + this.actor = actor; + this.location = location; + this.time = System.currentTimeMillis(); + } + + public Actor getActor() { + return actor; + } + + public Location getLocation() { + return location; + } + + public long getTime() { + return time; + } + } +} diff --git a/src/main/java/de/diddiz/LogBlock/listeners/SignChangeLogging.java b/src/main/java/de/diddiz/LogBlock/listeners/SignChangeLogging.java index 4d808dd4..466883ac 100644 --- a/src/main/java/de/diddiz/LogBlock/listeners/SignChangeLogging.java +++ b/src/main/java/de/diddiz/LogBlock/listeners/SignChangeLogging.java @@ -1,23 +1,40 @@ -package de.diddiz.LogBlock.listeners; - -import de.diddiz.LogBlock.Actor; -import de.diddiz.LogBlock.LogBlock; -import de.diddiz.LogBlock.Logging; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.block.SignChangeEvent; - -import static de.diddiz.LogBlock.config.Config.isLogging; - -public class SignChangeLogging extends LoggingListener { - public SignChangeLogging(LogBlock lb) { - super(lb); - } - - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onSignChange(SignChangeEvent event) { - if (isLogging(event.getBlock().getWorld(), Logging.SIGNTEXT)) { - consumer.queueSignPlace(Actor.actorFromEntity(event.getPlayer()), event.getBlock().getLocation(), event.getBlock().getTypeId(), event.getBlock().getData(), event.getLines()); - } - } -} +package de.diddiz.LogBlock.listeners; + +import de.diddiz.LogBlock.Actor; +import de.diddiz.LogBlock.LogBlock; +import de.diddiz.LogBlock.Logging; +import java.util.Objects; +import org.bukkit.block.BlockState; +import org.bukkit.block.Sign; +import org.bukkit.block.sign.SignSide; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.block.SignChangeEvent; + +import static de.diddiz.LogBlock.config.Config.isLogging; + +public class SignChangeLogging extends LoggingListener { + public SignChangeLogging(LogBlock lb) { + super(lb); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onSignChange(SignChangeEvent event) { + if (isLogging(event.getBlock().getWorld(), Logging.SIGNTEXT)) { + BlockState newState = event.getBlock().getState(); + if (newState instanceof Sign sign) { + SignSide signSide = sign.getSide(event.getSide()); + boolean changed = false; + for (int i = 0; i < 4; i++) { + if (!Objects.equals(signSide.getLine(i), event.getLine(i))) { + signSide.setLine(i, event.getLine(i)); + changed = true; + } + } + if (changed) { + consumer.queueBlockReplace(Actor.actorFromEntity(event.getPlayer()), event.getBlock().getState(), newState); + } + } + } + } +} diff --git a/src/main/java/de/diddiz/LogBlock/listeners/SnowFadeLogging.java b/src/main/java/de/diddiz/LogBlock/listeners/SnowFadeLogging.java index 8acc3eb0..9c411ba9 100644 --- a/src/main/java/de/diddiz/LogBlock/listeners/SnowFadeLogging.java +++ b/src/main/java/de/diddiz/LogBlock/listeners/SnowFadeLogging.java @@ -1,26 +1,28 @@ -package de.diddiz.LogBlock.listeners; - -import de.diddiz.LogBlock.Actor; -import de.diddiz.LogBlock.LogBlock; -import de.diddiz.LogBlock.Logging; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.block.BlockFadeEvent; - -import static de.diddiz.LogBlock.config.Config.isLogging; - -public class SnowFadeLogging extends LoggingListener { - public SnowFadeLogging(LogBlock lb) { - super(lb); - } - - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onBlockFade(BlockFadeEvent event) { - if (isLogging(event.getBlock().getWorld(), Logging.SNOWFADE)) { - final int type = event.getBlock().getTypeId(); - if (type == 78 || type == 79) { - consumer.queueBlockReplace(new Actor("SnowFade"), event.getBlock().getState(), event.getNewState()); - } - } - } -} +package de.diddiz.LogBlock.listeners; + +import de.diddiz.LogBlock.Actor; +import de.diddiz.LogBlock.LogBlock; +import de.diddiz.LogBlock.Logging; + +import org.bukkit.Material; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.block.BlockFadeEvent; + +import static de.diddiz.LogBlock.config.Config.isLogging; + +public class SnowFadeLogging extends LoggingListener { + public SnowFadeLogging(LogBlock lb) { + super(lb); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockFade(BlockFadeEvent event) { + if (isLogging(event.getBlock().getWorld(), Logging.SNOWFADE)) { + final Material type = event.getBlock().getType(); + if (type == Material.SNOW || type == Material.ICE) { + consumer.queueBlockReplace(new Actor("SnowFade"), event.getBlock().getState(), event.getNewState()); + } + } + } +} diff --git a/src/main/java/de/diddiz/LogBlock/listeners/SnowFormLogging.java b/src/main/java/de/diddiz/LogBlock/listeners/SnowFormLogging.java index d3c4146b..6d4a2479 100644 --- a/src/main/java/de/diddiz/LogBlock/listeners/SnowFormLogging.java +++ b/src/main/java/de/diddiz/LogBlock/listeners/SnowFormLogging.java @@ -1,32 +1,28 @@ -package de.diddiz.LogBlock.listeners; - -import de.diddiz.LogBlock.Actor; -import de.diddiz.LogBlock.LogBlock; -import de.diddiz.LogBlock.Logging; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.block.BlockFormEvent; - -import static de.diddiz.LogBlock.config.Config.isLogging; - -public class SnowFormLogging extends LoggingListener { - public SnowFormLogging(LogBlock lb) { - super(lb); - } - -// @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) -// public void onLeavesDecay(LeavesDecayEvent event) { -// if (isLogging(event.getBlock().getWorld(), Logging.SNOWFORM)) -// consumer.queueBlockBreak("LeavesDecay", event.getBlock().getState()); -// } - - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onBlockForm(BlockFormEvent event) { - if (isLogging(event.getBlock().getWorld(), Logging.SNOWFORM)) { - final int type = event.getNewState().getTypeId(); - if (type == 78 || type == 79) { - consumer.queueBlockReplace(new Actor("SnowForm"), event.getBlock().getState(), event.getNewState()); - } - } - } -} +package de.diddiz.LogBlock.listeners; + +import de.diddiz.LogBlock.Actor; +import de.diddiz.LogBlock.LogBlock; +import de.diddiz.LogBlock.Logging; + +import org.bukkit.Material; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.block.BlockFormEvent; + +import static de.diddiz.LogBlock.config.Config.isLogging; + +public class SnowFormLogging extends LoggingListener { + public SnowFormLogging(LogBlock lb) { + super(lb); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockForm(BlockFormEvent event) { + if (isLogging(event.getBlock().getWorld(), Logging.SNOWFORM)) { + final Material type = event.getNewState().getType(); + if (type == Material.SNOW || type == Material.ICE) { + consumer.queueBlockReplace(new Actor("SnowForm"), event.getBlock().getState(), event.getNewState()); + } + } + } +} diff --git a/src/main/java/de/diddiz/LogBlock/listeners/StructureGrowLogging.java b/src/main/java/de/diddiz/LogBlock/listeners/StructureGrowLogging.java index c5730d26..93b395f8 100644 --- a/src/main/java/de/diddiz/LogBlock/listeners/StructureGrowLogging.java +++ b/src/main/java/de/diddiz/LogBlock/listeners/StructureGrowLogging.java @@ -1,40 +1,34 @@ -package de.diddiz.LogBlock.listeners; - -import de.diddiz.LogBlock.Actor; -import de.diddiz.LogBlock.LogBlock; -import de.diddiz.LogBlock.Logging; -import de.diddiz.LogBlock.config.WorldConfig; -import org.bukkit.block.BlockState; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.world.StructureGrowEvent; - -import static de.diddiz.LogBlock.config.Config.getWorldConfig; - -public class StructureGrowLogging extends LoggingListener { - public StructureGrowLogging(LogBlock lb) { - super(lb); - } - - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onStructureGrow(StructureGrowEvent event) { - final WorldConfig wcfg = getWorldConfig(event.getWorld()); - if (wcfg != null) { - final Actor actor; - if (event.getPlayer() != null) { - if (!wcfg.isLogging(Logging.BONEMEALSTRUCTUREGROW)) { - return; - } - actor = Actor.actorFromEntity(event.getPlayer()); - } else { - if (!wcfg.isLogging(Logging.NATURALSTRUCTUREGROW)) { - return; - } - actor = new Actor("NaturalGrow"); - } - for (final BlockState state : event.getBlocks()) { - consumer.queueBlockReplace(actor, state.getBlock().getState(), state); - } - } - } -} +package de.diddiz.LogBlock.listeners; + +import de.diddiz.LogBlock.Actor; +import de.diddiz.LogBlock.LogBlock; +import de.diddiz.LogBlock.Logging; +import de.diddiz.LogBlock.config.WorldConfig; +import org.bukkit.block.BlockState; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.world.StructureGrowEvent; + +import static de.diddiz.LogBlock.config.Config.getWorldConfig; + +public class StructureGrowLogging extends LoggingListener { + public StructureGrowLogging(LogBlock lb) { + super(lb); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onStructureGrow(StructureGrowEvent event) { + final WorldConfig wcfg = getWorldConfig(event.getWorld()); + if (wcfg != null) { + if (!wcfg.isLogging(Logging.NATURALSTRUCTUREGROW)) { + return; + } + if (!event.isFromBonemeal()) { + final Actor actor = new Actor("NaturalGrow"); + for (final BlockState state : event.getBlocks()) { + consumer.queueBlockReplace(actor, state.getBlock().getState(), state); + } + } + } + } +} diff --git a/src/main/java/de/diddiz/LogBlock/listeners/ToolListener.java b/src/main/java/de/diddiz/LogBlock/listeners/ToolListener.java index 06dcab83..9e468676 100644 --- a/src/main/java/de/diddiz/LogBlock/listeners/ToolListener.java +++ b/src/main/java/de/diddiz/LogBlock/listeners/ToolListener.java @@ -1,124 +1,151 @@ -package de.diddiz.LogBlock.listeners; - -import de.diddiz.LogBlock.*; -import de.diddiz.worldedit.RegionContainer; -import org.bukkit.ChatColor; -import org.bukkit.block.Block; -import org.bukkit.block.BlockFace; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.block.Action; -import org.bukkit.event.player.PlayerChangedWorldEvent; -import org.bukkit.event.player.PlayerDropItemEvent; -import org.bukkit.event.player.PlayerInteractEvent; -import org.bukkit.inventory.ItemStack; - -import java.util.Map.Entry; - -import static de.diddiz.LogBlock.Session.getSession; -import static de.diddiz.LogBlock.Session.hasSession; -import static de.diddiz.LogBlock.config.Config.isLogged; -import static de.diddiz.LogBlock.config.Config.toolsByType; - -public class ToolListener implements Listener { - private final CommandsHandler handler; - private final LogBlock logblock; - - public ToolListener(LogBlock logblock) { - this.logblock = logblock; - handler = logblock.getCommandsHandler(); - } - - @EventHandler(ignoreCancelled = true) - public void onPlayerInteract(PlayerInteractEvent event) { - if (event.getMaterial() != null) { - final Action action = event.getAction(); - final int type = event.getMaterial().getId(); - final Tool tool = toolsByType.get(type); - final Player player = event.getPlayer(); - if (tool != null && (action == Action.RIGHT_CLICK_BLOCK || action == Action.LEFT_CLICK_BLOCK) && logblock.hasPermission(player, "logblock.tools." + tool.name)) { - final ToolBehavior behavior = action == Action.RIGHT_CLICK_BLOCK ? tool.rightClickBehavior : tool.leftClickBehavior; - final ToolData toolData = getSession(player).toolData.get(tool); - if (behavior != ToolBehavior.NONE && toolData.enabled) { - if (!isLogged(player.getWorld())) { - player.sendMessage(ChatColor.RED + "This world is not currently logged."); - event.setCancelled(true); - return; - } - final Block block = event.getClickedBlock(); - final QueryParams params = toolData.params; - params.loc = null; - params.sel = null; - if (behavior == ToolBehavior.BLOCK) { - params.setLocation(block.getRelative(event.getBlockFace()).getLocation()); - } else if ((block.getTypeId() != 54 && block.getTypeId() != 146) || tool.params.radius != 0) { - params.setLocation(block.getLocation()); - } else { - if (logblock.getServer().getPluginManager().isPluginEnabled("WorldEdit")) { - for (final BlockFace face : new BlockFace[]{BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST}) { - if (block.getRelative(face).getTypeId() == block.getTypeId()) { - params.setSelection(RegionContainer.fromCorners(event.getPlayer().getWorld(), - block.getLocation(), block.getRelative(face).getLocation())); - } - } - } - if (params.sel == null) { - params.setLocation(block.getLocation()); - } - } - try { - if (toolData.mode == ToolMode.ROLLBACK) { - handler.new CommandRollback(player, params, true); - } else if (toolData.mode == ToolMode.REDO) { - handler.new CommandRedo(player, params, true); - } else if (toolData.mode == ToolMode.CLEARLOG) { - handler.new CommandClearLog(player, params, true); - } else if (toolData.mode == ToolMode.WRITELOGFILE) { - handler.new CommandWriteLogFile(player, params, true); - } else { - handler.new CommandLookup(player, params, true); - } - } catch (final Exception ex) { - player.sendMessage(ChatColor.RED + ex.getMessage()); - } - event.setCancelled(true); - } - } - } - } - - @EventHandler - public void onPlayerChangedWorld(PlayerChangedWorldEvent event) { - final Player player = event.getPlayer(); - if (hasSession(player)) { - final Session session = getSession(player); - for (final Entry entry : session.toolData.entrySet()) { - final Tool tool = entry.getKey(); - final ToolData toolData = entry.getValue(); - if (toolData.enabled && !logblock.hasPermission(player, "logblock.tools." + tool.name)) { - toolData.enabled = false; - player.getInventory().removeItem(new ItemStack(tool.item, 1)); - player.sendMessage(ChatColor.GREEN + "Tool disabled."); - } - } - } - } - - @EventHandler - public void onPlayerDropItem(PlayerDropItemEvent event) { - final Player player = event.getPlayer(); - if (hasSession(player)) { - final Session session = getSession(player); - for (final Entry entry : session.toolData.entrySet()) { - final Tool tool = entry.getKey(); - final ToolData toolData = entry.getValue(); - final int item = event.getItemDrop().getItemStack().getTypeId(); - if (item == tool.item && toolData.enabled && !tool.canDrop) { - player.sendMessage(ChatColor.RED + "You cannot drop this tool."); - event.setCancelled(true); - } - } - } - } -} +package de.diddiz.LogBlock.listeners; + +import de.diddiz.LogBlock.*; +import de.diddiz.LogBlock.events.ToolUseEvent; +import de.diddiz.LogBlock.util.BukkitUtils; +import de.diddiz.LogBlock.util.CuboidRegion; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.player.PlayerChangedWorldEvent; +import org.bukkit.event.player.PlayerDropItemEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.ItemStack; + +import java.util.Map.Entry; + +import static de.diddiz.LogBlock.Session.getSession; +import static de.diddiz.LogBlock.Session.hasSession; +import static de.diddiz.LogBlock.config.Config.isLogged; +import static de.diddiz.LogBlock.config.Config.toolsByType; + +public class ToolListener implements Listener { + private final CommandsHandler handler; + private final LogBlock logblock; + + public ToolListener(LogBlock logblock) { + this.logblock = logblock; + handler = logblock.getCommandsHandler(); + } + + @EventHandler + public void onPlayerInteract(PlayerInteractEvent event) { + if (event.getMaterial() != null) { + final Action action = event.getAction(); + final Material type = event.getMaterial(); + final Tool tool = toolsByType.get(type); + final Player player = event.getPlayer(); + if (tool != null && (action == Action.RIGHT_CLICK_BLOCK || action == Action.LEFT_CLICK_BLOCK) && logblock.hasPermission(player, "logblock.tools." + tool.name)) { + final ToolBehavior behavior = action == Action.RIGHT_CLICK_BLOCK ? tool.rightClickBehavior : tool.leftClickBehavior; + final ToolData toolData = getSession(player).toolData.get(tool); + if (behavior != ToolBehavior.NONE && toolData.enabled) { + if (!isLogged(player.getWorld())) { + player.sendMessage(ChatColor.RED + "This world is not currently logged."); + event.setCancelled(true); + return; + } + final Block block = event.getClickedBlock(); + final QueryParams params = toolData.params.clone(); + params.loc = null; + params.sel = null; + if (behavior == ToolBehavior.BLOCK) { + params.setLocation(block.getRelative(event.getBlockFace()).getLocation()); + } else if (tool.params.radius != 0) { + params.setLocation(block.getLocation()); + } else { + Block otherHalfChest = BukkitUtils.getConnectedChest(block); + if (otherHalfChest == null) { + params.setLocation(block.getLocation()); + } else { + params.setSelection(CuboidRegion.fromCorners(block.getLocation().getWorld(), block.getLocation(), otherHalfChest.getLocation())); + } + } + try { + params.validate(); + if (this.callToolUseEvent(new ToolUseEvent(player, tool, behavior, params))) { + return; + } + if (toolData.mode == ToolMode.ROLLBACK) { + handler.new CommandRollback(player, params, true); + } else if (toolData.mode == ToolMode.REDO) { + handler.new CommandRedo(player, params, true); + } else if (toolData.mode == ToolMode.CLEARLOG) { + handler.new CommandClearLog(player, params, true); + } else if (toolData.mode == ToolMode.WRITELOGFILE) { + handler.new CommandWriteLogFile(player, params, true); + } else { + handler.new CommandLookup(player, params, true); + } + } catch (final Exception ex) { + player.sendMessage(ChatColor.RED + ex.getMessage()); + } + event.setCancelled(true); + } + } + } + } + + private boolean callToolUseEvent(ToolUseEvent event) { + this.logblock.getServer().getPluginManager().callEvent(event); + return event.isCancelled(); + } + + @EventHandler + public void onPlayerChangedWorld(PlayerChangedWorldEvent event) { + final Player player = event.getPlayer(); + if (hasSession(player)) { + final Session session = getSession(player); + for (final Entry entry : session.toolData.entrySet()) { + final Tool tool = entry.getKey(); + final ToolData toolData = entry.getValue(); + if (toolData.enabled && !logblock.hasPermission(player, "logblock.tools." + tool.name)) { + toolData.enabled = false; + if (tool.removeOnDisable && logblock.hasPermission(player, "logblock.spawnTools")) { + player.getInventory().removeItem(new ItemStack(tool.item, 1)); + } + player.sendMessage(ChatColor.GREEN + "Tool disabled."); + } + } + } + } + + @EventHandler + public void onPlayerDropItem(PlayerDropItemEvent event) { + final Player player = event.getPlayer(); + if (hasSession(player)) { + final Session session = getSession(player); + for (final Entry entry : session.toolData.entrySet()) { + final Tool tool = entry.getKey(); + final ToolData toolData = entry.getValue(); + final Material item = event.getItemDrop().getItemStack().getType(); + if (item == tool.item && toolData.enabled) { + if (tool.dropToDisable) { + toolData.enabled = false; + ItemStack stack = event.getItemDrop().getItemStack(); + if (tool.removeOnDisable && logblock.hasPermission(player, "logblock.spawnTools")) { + if (stack.isSimilar(new ItemStack(item))) { + if (stack.getAmount() > 1) { + stack.setAmount(stack.getAmount() - 1); + event.getItemDrop().setItemStack(stack); + } else { + event.getItemDrop().remove(); + } + } + } + if (BukkitUtils.hasInventoryStorageSpaceFor(player.getInventory(), stack)) { + event.setCancelled(true); + } + player.sendMessage(ChatColor.GREEN + "Tool disabled."); + } else if (!tool.canDrop) { + player.sendMessage(ChatColor.RED + "You cannot drop this tool."); + event.setCancelled(true); + } + } + } + } + } +} diff --git a/src/main/java/de/diddiz/LogBlock/listeners/WitherLogging.java b/src/main/java/de/diddiz/LogBlock/listeners/WitherLogging.java deleted file mode 100644 index fa74c934..00000000 --- a/src/main/java/de/diddiz/LogBlock/listeners/WitherLogging.java +++ /dev/null @@ -1,24 +0,0 @@ -package de.diddiz.LogBlock.listeners; - -import de.diddiz.LogBlock.Actor; -import de.diddiz.LogBlock.LogBlock; -import de.diddiz.LogBlock.Logging; -import org.bukkit.entity.Wither; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.entity.EntityChangeBlockEvent; - -import static de.diddiz.LogBlock.config.Config.isLogging; - -public class WitherLogging extends LoggingListener { - public WitherLogging(LogBlock lb) { - super(lb); - } - - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onEntityChangeBlock(EntityChangeBlockEvent event) { - if (event.getEntity() instanceof Wither && isLogging(event.getBlock().getWorld(), Logging.WITHER)) { - consumer.queueBlockReplace(Actor.actorFromEntity(event.getEntity()), event.getBlock().getState(), event.getTo().getId(), event.getData()); // Wither walked through a block. - } - } -} diff --git a/src/main/java/de/diddiz/LogBlock/questioner/Question.java b/src/main/java/de/diddiz/LogBlock/questioner/Question.java new file mode 100644 index 00000000..1c63c746 --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/questioner/Question.java @@ -0,0 +1,73 @@ +package de.diddiz.LogBlock.questioner; + +import org.bukkit.entity.Player; + +public class Question { + private String answer; + private final String[] answers; + private final String questionMessage; + private final Player respondent; + private final long start; + + public Question(Player respondent, String questionMessage, String[] answers) { + this.start = System.currentTimeMillis(); + this.respondent = respondent; + this.questionMessage = questionMessage; + this.answers = answers; + } + + public synchronized String ask() { + StringBuilder options = new StringBuilder(); + for (String ans : this.answers) { + options.append("/" + ans + ", "); + } + options.delete(options.length() - 2, options.length()); + this.respondent.sendMessage(this.questionMessage); + this.respondent.sendMessage("- " + options + "?"); + while (answer == null) { + try { + wait(); + } catch (InterruptedException ex) { + if (answer == null) { + answer = "interrupted"; + } + } + } + return this.answer; + } + + public synchronized boolean isExpired(boolean forceExpire) { + if (forceExpire || System.currentTimeMillis() - this.start > 120000L || this.answer != null) { + if (answer == null) { + answer = "timed out"; + } + notifyAll(); + return true; + } + return false; + } + + public boolean returnAnswer(String answer) { + return returnAnswer(answer, false); + } + + public synchronized boolean returnAnswer(String answer, boolean forceReturn) { + if (forceReturn) { + if (this.answer == null) { + this.answer = answer; + } + notifyAll(); + return true; + } + for (String s : answers) { + if (s.equalsIgnoreCase(answer)) { + if (this.answer == null) { + this.answer = s; + } + notifyAll(); + return true; + } + } + return false; + } +} diff --git a/src/main/java/de/diddiz/LogBlock/questioner/Questioner.java b/src/main/java/de/diddiz/LogBlock/questioner/Questioner.java new file mode 100644 index 00000000..399f3589 --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/questioner/Questioner.java @@ -0,0 +1,76 @@ +package de.diddiz.LogBlock.questioner; + +import java.util.Iterator; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.Map.Entry; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerCommandPreprocessEvent; + +import de.diddiz.LogBlock.LogBlock; + +public class Questioner { + private final LogBlock logBlock; + private final ConcurrentHashMap questions = new ConcurrentHashMap<>(); + + public Questioner(LogBlock logBlock) { + this.logBlock = logBlock; + logBlock.getServer().getPluginManager().registerEvents(new QuestionerListener(), logBlock); + logBlock.getServer().getScheduler().scheduleSyncRepeatingTask(logBlock, new QuestionsReaper(), 600, 600); + } + + public String ask(Player respondent, String questionMessage, String... answers) { + if (Bukkit.isPrimaryThread()) { + throw new IllegalStateException("This method may not be called from the primary thread"); + } + Question question = new Question(respondent, questionMessage, answers); + Question oldQuestion = this.questions.put(respondent.getUniqueId(), question); + if (oldQuestion != null) { + oldQuestion.returnAnswer("no", true); + // wait a little time to let the other thread continue + try { + Thread.sleep(10); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + return question.ask(); + } + + private class QuestionsReaper implements Runnable { + @Override + public void run() { + if (questions.isEmpty()) { + return; + } + Iterator> it = questions.entrySet().iterator(); + while (it.hasNext()) { + Entry e = it.next(); + Question question = e.getValue(); + if (question.isExpired(logBlock.getServer().getPlayer(e.getKey()) == null)) { + it.remove(); + } + } + } + } + + private class QuestionerListener implements Listener { + @EventHandler(ignoreCancelled = true) + public void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event) { + UUID player = event.getPlayer().getUniqueId(); + Question question; + question = questions.get(player); + if (question != null) { + String answer = event.getMessage().substring(1).toLowerCase(); + if (question.returnAnswer(answer)) { + questions.remove(player, question); + event.setCancelled(true); + } + } + } + } +} diff --git a/src/main/java/de/diddiz/LogBlock/util/ActionColor.java b/src/main/java/de/diddiz/LogBlock/util/ActionColor.java new file mode 100644 index 00000000..fa69d50a --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/util/ActionColor.java @@ -0,0 +1,24 @@ +package de.diddiz.LogBlock.util; + +import net.md_5.bungee.api.ChatColor; + +public enum ActionColor { + DESTROY(ChatColor.RED), + CREATE(ChatColor.DARK_GREEN), + INTERACT(ChatColor.GRAY); + + private final ChatColor color; + + ActionColor(ChatColor color) { + this.color = color; + } + + public ChatColor getColor() { + return color; + } + + @Override + public String toString() { + return color.toString(); + } +} diff --git a/src/main/java/de/diddiz/LogBlock/util/BukkitUtils.java b/src/main/java/de/diddiz/LogBlock/util/BukkitUtils.java new file mode 100644 index 00000000..2e25bbbb --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/util/BukkitUtils.java @@ -0,0 +1,1065 @@ +package de.diddiz.LogBlock.util; + +import static de.diddiz.LogBlock.util.MessagingUtil.prettyMaterial; + +import de.diddiz.LogBlock.LogBlock; +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.logging.Level; +import java.util.Set; +import java.util.UUID; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.HoverEvent; +import net.md_5.bungee.api.chat.HoverEvent.Action; +import net.md_5.bungee.api.chat.ItemTag; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.chat.hover.content.Item; +import net.md_5.bungee.api.chat.hover.content.Text; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Chunk; +import org.bukkit.DyeColor; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Tag; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.BlockState; +import org.bukkit.block.DoubleChest; +import org.bukkit.block.data.Bisected; +import org.bukkit.block.data.Bisected.Half; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.Directional; +import org.bukkit.block.data.Rotatable; +import org.bukkit.block.data.type.HangingSign; +import org.bukkit.block.data.type.Sign; +import org.bukkit.block.data.type.Slab; +import org.bukkit.block.data.type.Slab.Type; +import org.bukkit.block.data.type.Stairs; +import org.bukkit.block.data.type.WallHangingSign; +import org.bukkit.block.data.type.WallSign; +import org.bukkit.block.sign.Side; +import org.bukkit.entity.ArmorStand; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.entity.TNTPrimed; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.ItemStack; + +public class BukkitUtils { + + private static final Map dyes; + private static final Map projectileItems; + + private static final Set alwaysWaterlogged; + private static final Set concreteBlocks; + private static final Set containerBlocks; + private static final Set doublePlants; + private static final Set fallingEntityKillers; + private static final Set nonFluidProofBlocks; + private static final Set relativeBreakable; + private static final Set relativeTopBreakable; + private static final Set singleBlockPlants; + + private static final Tag allSigns; + private static final Tag bedBlocks; + private static final Tag buttons; + private static final Tag candleCakes; + private static final Tag candles; + private static final Tag cropBlocks; + private static final Tag fenceGates; + private static final Tag hangingSigns; + private static final Tag pressurePlates; + private static final Tag shulkerBoxBlocks; + private static final Tag slabs; + private static final Tag woodenDoors; + private static final Tag woodenTrapdoors; + + static { + // Global Tags + + // https://minecraft.fandom.com/wiki/Tag#blocks_fence_gates + fenceGates = Tag.FENCE_GATES; + + // https://minecraft.fandom.com/wiki/Tag#blocks_wooden_trapdoors + woodenTrapdoors = Tag.WOODEN_TRAPDOORS; + + // https://minecraft.fandom.com/wiki/Tag#blocks_pressure_plates + pressurePlates = Tag.PRESSURE_PLATES; + + // https://minecraft.fandom.com/wiki/Tag#blocks_wooden_doors + woodenDoors = Tag.WOODEN_DOORS; + + // https://minecraft.fandom.com/wiki/Tag#blocks_slabs + slabs = Tag.SLABS; + + // https://minecraft.fandom.com/wiki/Tag#blocks_buttons + buttons = Tag.BUTTONS; + + // https://minecraft.fandom.com/wiki/Tag#blocks_ceiling_hanging_signs + hangingSigns = Tag.CEILING_HANGING_SIGNS; + + // https://minecraft.fandom.com/wiki/Tag#blocks_all_signs + allSigns = Tag.ALL_SIGNS; + + // https://minecraft.fandom.com/wiki/Tag#blocks_candles + candles = Tag.CANDLES; + + // https://minecraft.fandom.com/wiki/Tag#blocks_candle_cakes + candleCakes = Tag.CANDLE_CAKES; + + // https://minecraft.fandom.com/wiki/Tag#blocks_crops + cropBlocks = Tag.CROPS; + + // https://minecraft.fandom.com/wiki/Tag#blocks_shulker_boxes + shulkerBoxBlocks = Tag.SHULKER_BOXES; + + // https://minecraft.fandom.com/wiki/Tag#blocks_beds + bedBlocks = Tag.BEDS; + + // Local Tags + + // https://minecraft.fandom.com/wiki/Tag#blocks_standing_signs + Set signs = Tag.STANDING_SIGNS.getValues(); + + // https://minecraft.fandom.com/wiki/Tag#blocks_wall_signs + Set wallSigns = Tag.WALL_SIGNS.getValues(); + + // https://minecraft.fandom.com/wiki/Tag#blocks_wool_carpets + Set carpets = Tag.WOOL_CARPETS.getValues(); + + // https://minecraft.fandom.com/wiki/Tag#blocks_flower_pots + Set flowerPots = Tag.FLOWER_POTS.getValues(); + + // https://minecraft.fandom.com/wiki/Tag#blocks_saplings + Set saplings = Tag.SAPLINGS.getValues(); + + // https://minecraft.fandom.com/wiki/Tag#blocks_small_flowers + Set smallFlowers = Tag.SMALL_FLOWERS.getValues(); + + Set tallFlowers = Set.of(Material.SUNFLOWER, + Material.LILAC, + Material.PEONY, + Material.ROSE_BUSH, + Material.PITCHER_PLANT); + + Set bannerStanding = Set.of(Material.WHITE_BANNER, + Material.ORANGE_BANNER, + Material.MAGENTA_BANNER, + Material.LIGHT_BLUE_BANNER, + Material.YELLOW_BANNER, + Material.LIME_BANNER, + Material.PINK_BANNER, + Material.GRAY_BANNER, + Material.LIGHT_GRAY_BANNER, + Material.CYAN_BANNER, + Material.PURPLE_BANNER, + Material.BLUE_BANNER, + Material.BROWN_BANNER, + Material.GREEN_BANNER, + Material.RED_BANNER, + Material.BLACK_BANNER); + + Set bannerWall = Set.of(Material.WHITE_WALL_BANNER, + Material.ORANGE_WALL_BANNER, + Material.MAGENTA_WALL_BANNER, + Material.LIGHT_BLUE_WALL_BANNER, + Material.YELLOW_WALL_BANNER, + Material.LIME_WALL_BANNER, + Material.PINK_WALL_BANNER, + Material.GRAY_WALL_BANNER, + Material.LIGHT_GRAY_WALL_BANNER, + Material.CYAN_WALL_BANNER, + Material.PURPLE_WALL_BANNER, + Material.BLUE_WALL_BANNER, + Material.BROWN_WALL_BANNER, + Material.GREEN_WALL_BANNER, + Material.RED_WALL_BANNER, + Material.BLACK_WALL_BANNER); + + Set bannerAll = Tag.BANNERS.getValues(); + + Set headAndSkulls = Set.of(Material.PLAYER_HEAD, + Material.PLAYER_WALL_HEAD, + Material.CREEPER_HEAD, + Material.CREEPER_WALL_HEAD, + Material.DRAGON_HEAD, + Material.DRAGON_WALL_HEAD, + Material.ZOMBIE_HEAD, + Material.ZOMBIE_WALL_HEAD, + Material.SKELETON_SKULL, + Material.SKELETON_WALL_SKULL, + Material.WITHER_SKELETON_SKULL, + Material.WITHER_SKELETON_WALL_SKULL, + Material.PIGLIN_HEAD, + Material.PIGLIN_WALL_HEAD); + + Set standingTorch = Set.of(Material.TORCH, + Material.SOUL_TORCH, + Material.REDSTONE_TORCH); + + Set wallTorch = Set.of(Material.WALL_TORCH, + Material.SOUL_WALL_TORCH, + Material.REDSTONE_WALL_TORCH); + + singleBlockPlants = new HashSet<>(); + singleBlockPlants.addAll(smallFlowers); + singleBlockPlants.add(Material.SHORT_GRASS); + singleBlockPlants.add(Material.FERN); + singleBlockPlants.add(Material.DEAD_BUSH); + singleBlockPlants.add(Material.BROWN_MUSHROOM); + singleBlockPlants.add(Material.RED_MUSHROOM); + singleBlockPlants.add(Material.SWEET_BERRY_BUSH); + singleBlockPlants.add(Material.CRIMSON_FUNGUS); + singleBlockPlants.add(Material.WARPED_FUNGUS); + singleBlockPlants.add(Material.CRIMSON_ROOTS); + singleBlockPlants.add(Material.WARPED_ROOTS); + singleBlockPlants.add(Material.NETHER_SPROUTS); + singleBlockPlants.add(Material.AZALEA); + singleBlockPlants.add(Material.FLOWERING_AZALEA); + singleBlockPlants.add(Material.PINK_PETALS); + singleBlockPlants.add(Material.PITCHER_CROP); + + doublePlants = new HashSet<>(); + doublePlants.addAll(tallFlowers); + doublePlants.add(Material.TALL_GRASS); + doublePlants.add(Material.LARGE_FERN); + doublePlants.add(Material.TALL_SEAGRASS); + doublePlants.add(Material.SMALL_DRIPLEAF); + + // Blocks that break when they are attached to a block + relativeBreakable = new HashSet<>(); + relativeBreakable.addAll(bannerWall); + relativeBreakable.addAll(buttons.getValues()); + relativeBreakable.addAll(wallSigns); + relativeBreakable.addAll(wallTorch); + relativeBreakable.add(Material.LADDER); + relativeBreakable.add(Material.LEVER); + relativeBreakable.add(Material.TRIPWIRE_HOOK); + relativeBreakable.add(Material.COCOA); + relativeBreakable.add(Material.BELL); + relativeBreakable.add(Material.AMETHYST_CLUSTER); + relativeBreakable.add(Material.SMALL_AMETHYST_BUD); + relativeBreakable.add(Material.MEDIUM_AMETHYST_BUD); + relativeBreakable.add(Material.LARGE_AMETHYST_BUD); + + // Blocks that break when they are on top of a block + relativeTopBreakable = new HashSet<>(); + relativeTopBreakable.addAll(bannerStanding); + relativeTopBreakable.addAll(candleCakes.getValues()); + relativeTopBreakable.addAll(candles.getValues()); + relativeTopBreakable.addAll(carpets); + relativeTopBreakable.addAll(cropBlocks.getValues()); + relativeTopBreakable.addAll(doublePlants); + relativeTopBreakable.addAll(flowerPots); + relativeTopBreakable.addAll(pressurePlates.getValues()); + relativeTopBreakable.addAll(saplings); + relativeTopBreakable.addAll(signs); + relativeTopBreakable.addAll(singleBlockPlants); + relativeTopBreakable.addAll(standingTorch); + relativeTopBreakable.addAll(woodenDoors.getValues()); + relativeTopBreakable.add(Material.LILY_PAD); + relativeTopBreakable.add(Material.CACTUS); + relativeTopBreakable.add(Material.SUGAR_CANE); + relativeTopBreakable.add(Material.FLOWER_POT); + relativeTopBreakable.add(Material.POWERED_RAIL); + relativeTopBreakable.add(Material.DETECTOR_RAIL); + relativeTopBreakable.add(Material.ACTIVATOR_RAIL); + relativeTopBreakable.add(Material.RAIL); + relativeTopBreakable.add(Material.REDSTONE_WIRE); + relativeTopBreakable.add(Material.SNOW); + relativeTopBreakable.add(Material.REPEATER); + relativeTopBreakable.add(Material.COMPARATOR); + relativeTopBreakable.add(Material.IRON_DOOR); + relativeTopBreakable.add(Material.BAMBOO); + relativeTopBreakable.add(Material.BAMBOO_SAPLING); + relativeTopBreakable.add(Material.TWISTING_VINES); + relativeTopBreakable.add(Material.TWISTING_VINES_PLANT); + relativeTopBreakable.add(Material.BIG_DRIPLEAF); + relativeTopBreakable.add(Material.BIG_DRIPLEAF_STEM); + + // Blocks that break falling entities + fallingEntityKillers = new HashSet<>(); + fallingEntityKillers.addAll(bannerAll); + fallingEntityKillers.addAll(candleCakes.getValues()); + fallingEntityKillers.addAll(candles.getValues()); + fallingEntityKillers.addAll(carpets); + fallingEntityKillers.addAll(cropBlocks.getValues()); + fallingEntityKillers.addAll(doublePlants); + fallingEntityKillers.addAll(pressurePlates.getValues()); + fallingEntityKillers.addAll(saplings); + fallingEntityKillers.addAll(signs); + fallingEntityKillers.addAll(singleBlockPlants); + fallingEntityKillers.addAll(headAndSkulls); + fallingEntityKillers.addAll(slabs.getValues()); + fallingEntityKillers.addAll(standingTorch); + fallingEntityKillers.addAll(wallSigns); + fallingEntityKillers.addAll(wallTorch); + fallingEntityKillers.add(Material.NETHER_WART); + fallingEntityKillers.add(Material.COCOA); + fallingEntityKillers.add(Material.FLOWER_POT); + fallingEntityKillers.add(Material.POWERED_RAIL); + fallingEntityKillers.add(Material.DETECTOR_RAIL); + fallingEntityKillers.add(Material.ACTIVATOR_RAIL); + fallingEntityKillers.add(Material.RAIL); + fallingEntityKillers.add(Material.LEVER); + fallingEntityKillers.add(Material.REDSTONE_WIRE); + fallingEntityKillers.add(Material.REPEATER); + fallingEntityKillers.add(Material.COMPARATOR); + fallingEntityKillers.add(Material.DAYLIGHT_DETECTOR); + fallingEntityKillers.remove(Material.SHORT_GRASS); + fallingEntityKillers.remove(Material.NETHER_SPROUTS); + + // Container Blocks + containerBlocks = new HashSet<>(); + containerBlocks.addAll(shulkerBoxBlocks.getValues()); + containerBlocks.add(Material.CHEST); + containerBlocks.add(Material.TRAPPED_CHEST); + containerBlocks.add(Material.DISPENSER); + containerBlocks.add(Material.DROPPER); + containerBlocks.add(Material.HOPPER); + containerBlocks.add(Material.BREWING_STAND); + containerBlocks.add(Material.FURNACE); + containerBlocks.add(Material.BARREL); + containerBlocks.add(Material.BLAST_FURNACE); + containerBlocks.add(Material.SMOKER); + containerBlocks.add(Material.CHISELED_BOOKSHELF); + containerBlocks.add(Material.DECORATED_POT); + // Doesn't actually have a block inventory + // containerBlocks.add(Material.ENDER_CHEST); + + // It doesn't seem like you could injure people with some of these, but they exist, so.... + projectileItems = new HashMap<>(); + projectileItems.put(EntityType.ARROW, Material.ARROW); + projectileItems.put(EntityType.EGG, Material.EGG); + projectileItems.put(EntityType.ENDER_PEARL, Material.ENDER_PEARL); + projectileItems.put(EntityType.SMALL_FIREBALL, Material.FIRE_CHARGE); // Fire charge + projectileItems.put(EntityType.FIREBALL, Material.FIRE_CHARGE); // Fire charge + projectileItems.put(EntityType.FISHING_BOBBER, Material.FISHING_ROD); + projectileItems.put(EntityType.SNOWBALL, Material.SNOWBALL); + projectileItems.put(EntityType.SPLASH_POTION, Material.SPLASH_POTION); + projectileItems.put(EntityType.LINGERING_POTION, Material.LINGERING_POTION); + projectileItems.put(EntityType.EXPERIENCE_BOTTLE, Material.EXPERIENCE_BOTTLE); + projectileItems.put(EntityType.WITHER_SKULL, Material.WITHER_SKELETON_SKULL); + projectileItems.put(EntityType.FIREWORK_ROCKET, Material.FIREWORK_ROCKET); + + nonFluidProofBlocks = new HashSet<>(); + nonFluidProofBlocks.addAll(carpets); + nonFluidProofBlocks.addAll(cropBlocks.getValues()); + nonFluidProofBlocks.addAll(doublePlants); + nonFluidProofBlocks.addAll(headAndSkulls); + nonFluidProofBlocks.addAll(pressurePlates.getValues()); + nonFluidProofBlocks.addAll(saplings); + nonFluidProofBlocks.addAll(singleBlockPlants); + nonFluidProofBlocks.addAll(standingTorch); + nonFluidProofBlocks.addAll(wallTorch); + nonFluidProofBlocks.add(Material.LEVER); + nonFluidProofBlocks.add(Material.TRIPWIRE_HOOK); + nonFluidProofBlocks.add(Material.COCOA); + nonFluidProofBlocks.add(Material.NETHER_WART); + nonFluidProofBlocks.add(Material.FLOWER_POT); + // nonFluidProofBlocks.add(Material.POWERED_RAIL); + // nonFluidProofBlocks.add(Material.DETECTOR_RAIL); + // nonFluidProofBlocks.add(Material.ACTIVATOR_RAIL); + // nonFluidProofBlocks.add(Material.RAIL); + nonFluidProofBlocks.add(Material.LEVER); + nonFluidProofBlocks.add(Material.REDSTONE_WIRE); + nonFluidProofBlocks.add(Material.REPEATER); + nonFluidProofBlocks.add(Material.COMPARATOR); + nonFluidProofBlocks.add(Material.DAYLIGHT_DETECTOR); + + alwaysWaterlogged = Set.of(Material.SEAGRASS, + Material.TALL_SEAGRASS, + Material.KELP, + Material.KELP_PLANT); + + concreteBlocks = Set.of(Material.BLACK_CONCRETE, + Material.BLUE_CONCRETE, + Material.LIGHT_GRAY_CONCRETE, + Material.BROWN_CONCRETE, + Material.CYAN_CONCRETE, + Material.GRAY_CONCRETE, + Material.GREEN_CONCRETE, + Material.LIGHT_BLUE_CONCRETE, + Material.MAGENTA_CONCRETE, + Material.LIME_CONCRETE, + Material.ORANGE_CONCRETE, + Material.PINK_CONCRETE, + Material.PURPLE_CONCRETE, + Material.RED_CONCRETE, + Material.WHITE_CONCRETE, + Material.YELLOW_CONCRETE); + + dyes = new HashMap<>(); + dyes.put(Material.BLACK_DYE, DyeColor.BLACK); + dyes.put(Material.BLUE_DYE, DyeColor.BLUE); + dyes.put(Material.LIGHT_GRAY_DYE, DyeColor.LIGHT_GRAY); + dyes.put(Material.BROWN_DYE, DyeColor.BROWN); + dyes.put(Material.CYAN_DYE, DyeColor.CYAN); + dyes.put(Material.GRAY_DYE, DyeColor.GRAY); + dyes.put(Material.GREEN_DYE, DyeColor.GREEN); + dyes.put(Material.LIGHT_BLUE_DYE, DyeColor.LIGHT_BLUE); + dyes.put(Material.MAGENTA_DYE, DyeColor.MAGENTA); + dyes.put(Material.LIME_DYE, DyeColor.LIME); + dyes.put(Material.ORANGE_DYE, DyeColor.ORANGE); + dyes.put(Material.PINK_DYE, DyeColor.PINK); + dyes.put(Material.PURPLE_DYE, DyeColor.PURPLE); + dyes.put(Material.RED_DYE, DyeColor.RED); + dyes.put(Material.WHITE_DYE, DyeColor.WHITE); + dyes.put(Material.YELLOW_DYE, DyeColor.YELLOW); + } + + private static final BlockFace[] relativeBlockFaces = new BlockFace[] { + BlockFace.EAST, BlockFace.WEST, BlockFace.NORTH, BlockFace.SOUTH, BlockFace.UP, BlockFace.DOWN + }; + + /** + * Returns a list of block locations around the block that are of the type specified by the integer list parameter + * + * @param block The central block to get the blocks around + * @param type The type of blocks around the center block to return + * @return List of block locations around the block that are of the type specified by the integer list parameter + */ + public static List getBlocksNearby(org.bukkit.block.Block block, Set type) { + ArrayList blocks = new ArrayList<>(); + for (BlockFace blockFace : relativeBlockFaces) { + if (type.contains(block.getRelative(blockFace).getType())) { + blocks.add(block.getRelative(blockFace).getLocation()); + } + } + return blocks; + } + + public static boolean isTop(BlockData data) { + if (data instanceof Bisected && !(data instanceof Stairs)) { + return ((Bisected) data).getHalf() == Half.TOP; + } + return false; + } + + public static Material getInventoryHolderType(InventoryHolder holder) { + if (holder instanceof DoubleChest) { + return getInventoryHolderType(((DoubleChest) holder).getLeftSide()); + } else if (holder instanceof BlockState) { + return ((BlockState) holder).getType(); + } else { + return null; + } + } + + public static Location getInventoryHolderLocation(InventoryHolder holder) { + if (holder instanceof DoubleChest) { + return getInventoryHolderLocation(((DoubleChest) holder).getLeftSide()); + } else if (holder instanceof BlockState) { + return ((BlockState) holder).getLocation(); + } else { + return null; + } + } + + public static ItemStack[] compareInventories(ItemStack[] items1, ItemStack[] items2) { + final ArrayList diff = new ArrayList<>(); + for (ItemStack current : items2) { + try { + diff.add(new ItemStack(current)); + } catch (NullPointerException e) { + LogBlock.getInstance().getLogger().log(Level.SEVERE, "Could not clone ItemStack, probably Spigot bug SPIGOT-6025", e); // SPIGOT-6025 + } + } + for (ItemStack previous : items1) { + boolean found = false; + for (ItemStack current : diff) { + if (current.isSimilar(previous)) { + int newAmount = current.getAmount() - previous.getAmount(); + if (newAmount == 0) { + diff.remove(current); + } else { + current.setAmount(newAmount); + } + found = true; + break; + } + } + if (!found) { + try { + ItemStack subtracted = new ItemStack(previous); + subtracted.setAmount(-subtracted.getAmount()); + diff.add(subtracted); + } catch (NullPointerException e) { + LogBlock.getInstance().getLogger().log(Level.SEVERE, "Could not clone ItemStack, probably Spigot bug SPIGOT-6025", e); // SPIGOT-6025 + } + } + } + return diff.toArray(new ItemStack[diff.size()]); + } + + public static Collection compressInventory(ItemStack[] items) { + final HashMap compressed = new HashMap<>(); + for (final ItemStack item : items) { + if (item != null && item.getType() != Material.AIR && item.getAmount() > 0) { + int amount = item.getAmount(); + ItemStack stack = item.clone(); + stack.setAmount(1); + Integer old = compressed.get(stack); + compressed.put(stack, (old == null ? 0 : old) + amount); + } + } + ArrayList result = new ArrayList<>(); + for (Entry e : compressed.entrySet()) { + result.add(new ItemStackAndAmount(e.getKey(), e.getValue())); + } + return result; + } + + public static String friendlyWorldname(String worldName) { + return new File(worldName).getName(); + } + + public static Set getRelativeBreakables() { + return Collections.unmodifiableSet(relativeBreakable); + } + + public static boolean isRelativeTopBreakable(Material type) { + return relativeTopBreakable.contains(type); + } + + public static boolean isFallingEntityKiller(Material type) { + return fallingEntityKillers.contains(type); + } + + public static boolean isNonFluidProofBlock(Material type) { + return nonFluidProofBlocks.contains(type); + } + + public static boolean isCropBlock(Material type) { + return cropBlocks.isTagged(type); + } + + public static boolean isContainerBlock(Material type) { + return containerBlocks.contains(type); + } + + public static Set getShulkerBoxBlocks() { + return shulkerBoxBlocks.getValues(); // Already an unmodifiable Set + } + + public static boolean isShulkerBoxBlock(Material type) { + return shulkerBoxBlocks.isTagged(type); + } + + public static boolean isConcreteBlock(Material m) { + return concreteBlocks.contains(m); + } + + public static String entityName(Entity entity) { + if (entity instanceof Player player) { + return player.getName(); + } + if (entity instanceof TNTPrimed) { + return "TNT"; + } + return entity.getClass().getSimpleName().substring(5); + } + + public static void giveTool(Player player, Material type) { + final Inventory inv = player.getInventory(); + if (inv.contains(type)) { + player.sendMessage(ChatColor.RED + "You have already a " + type.name()); + } else { + final int free = inv.firstEmpty(); + if (free >= 0) { + if (player.getInventory().getItemInMainHand() != null && player.getInventory().getItemInMainHand().getType() != Material.AIR) { + inv.setItem(free, player.getInventory().getItemInMainHand()); + } + player.getInventory().setItemInMainHand(new ItemStack(type)); + player.sendMessage(ChatColor.GREEN + "Here's your " + type.name()); + } else { + player.sendMessage(ChatColor.RED + "You have no empty slot in your inventory"); + } + } + } + + public static int safeSpawnHeight(Location loc) { + final World world = loc.getWorld(); + world.getChunkAt(loc); + final int x = loc.getBlockX(), z = loc.getBlockZ(); + int y = loc.getBlockY(); + boolean lower = world.getBlockAt(x, y, z).isEmpty(), upper = world.getBlockAt(x, y + 1, z).isEmpty(); + while ((!lower || !upper) && y != world.getMaxHeight()) { + lower = upper; + upper = world.getBlockAt(x, ++y, z).isEmpty(); + } + while (world.getBlockAt(x, y - 1, z).isEmpty() && y != world.getMinHeight()) { + y--; + } + return y; + } + + public static int modifyContainer(BlockState b, ItemStackAndAmount item, boolean remove) { + if (item.amount() > 0 && b instanceof InventoryHolder c) { + final Inventory inv = c.getInventory(); + if (remove) { + return InventoryUtils.removeFromInventory(inv, item); + } else { + return InventoryUtils.addToInventory(inv, item); + } + } + return 0; + } + + public static boolean canFallIn(World world, int x, int y, int z) { + Block block = world.getBlockAt(x, y, z); + Material mat = block.getType(); + if (canDirectlyFallIn(mat)) { + return true; + } else if (isFallingEntityKiller(mat) || singleBlockPlants.contains(mat) || mat == Material.VINE) { + if (slabs.isTagged(mat)) { + if (((Slab) block.getBlockData()).getType() != Type.BOTTOM) { + return false; + } + } + return true; + } + return false; + } + + public static boolean canDirectlyFallIn(Material m) { + return isEmpty(m) || m == Material.WATER || m == Material.LAVA || m == Material.FIRE; + } + + public static Material itemIDfromProjectileEntity(Entity e) { + return projectileItems.get(e.getType()); + } + + public static boolean isDoublePlant(Material m) { + return doublePlants.contains(m); + } + + public static boolean isWoodenDoor(Material m) { + return woodenDoors.isTagged(m); + } + + public static boolean isButton(Material m) { + return buttons.isTagged(m); + } + + public static boolean isEmpty(Material m) { + return m == Material.AIR || m == Material.CAVE_AIR || m == Material.VOID_AIR; + } + + public static TextComponent toString(ItemStackAndAmount stack) { + if (stack == null || stack.stack() == null || stack.amount() == 0 || isEmpty(stack.stack().getType())) { + return prettyMaterial("nothing"); + } + TextComponent msg = MessagingUtil.createTextComponentWithColor(stack.amount() + "x ", TypeColor.DEFAULT.getColor()); + msg.addExtra(prettyMaterial(stack.stack().getType())); + + try { + String itemTag = stack.stack().getItemMeta().getAsString(); + msg.setHoverEvent(new HoverEvent(Action.SHOW_ITEM, new Item(stack.stack().getType().getKey().toString(), 1, itemTag != null ? ItemTag.ofNbt(itemTag) : null))); + } catch (Exception e) { + LogBlock.getInstance().getLogger().log(Level.SEVERE, "Failed to convert Itemstack to JSON", e); + msg.setHoverEvent(new HoverEvent(Action.SHOW_TEXT, new Text(new BaseComponent[] { MessagingUtil.createTextComponentWithColor("Error", TypeColor.ERROR.getColor()) }))); + } + + return msg; + } + + public static String formatMinecraftKey(String s) { + char[] cap = s.toCharArray(); + boolean lastSpace = true; + for (int i = 0; i < cap.length; i++) { + char c = cap[i]; + if (c == '_') { + c = ' '; + lastSpace = true; + } else if (c >= '0' && c <= '9' || c == '(' || c == ')') { + lastSpace = true; + } else { + if (lastSpace) { + c = Character.toUpperCase(c); + } else { + c = Character.toLowerCase(c); + } + lastSpace = false; + } + cap[i] = c; + } + return new String(cap); + } + + public static boolean isBed(Material type) { + return bedBlocks.isTagged(type); + } + + public static boolean isDye(Material type) { + return dyes.containsKey(type); + } + + public static DyeColor dyeToDyeColor(Material type) { + return dyes.get(type); + } + + public static Block getConnectedChest(Block chestBlock) { + // is this a chest? + BlockData blockData = chestBlock.getBlockData(); + if (!(blockData instanceof org.bukkit.block.data.type.Chest)) { + return null; + } + // so check if is should have a neighbour + org.bukkit.block.data.type.Chest chestData = (org.bukkit.block.data.type.Chest) blockData; + org.bukkit.block.data.type.Chest.Type chestType = chestData.getType(); + if (chestType != org.bukkit.block.data.type.Chest.Type.SINGLE) { + // check if the neighbour exists + BlockFace chestFace = chestData.getFacing(); + BlockFace faceToSecondChest; + if (chestFace == BlockFace.WEST) { + faceToSecondChest = BlockFace.NORTH; + } else if (chestFace == BlockFace.NORTH) { + faceToSecondChest = BlockFace.EAST; + } else if (chestFace == BlockFace.EAST) { + faceToSecondChest = BlockFace.SOUTH; + } else if (chestFace == BlockFace.SOUTH) { + faceToSecondChest = BlockFace.WEST; + } else { + return null; + } + org.bukkit.block.data.type.Chest.Type wantedChestType = org.bukkit.block.data.type.Chest.Type.RIGHT; + if (chestType == org.bukkit.block.data.type.Chest.Type.RIGHT) { + faceToSecondChest = faceToSecondChest.getOppositeFace(); + wantedChestType = org.bukkit.block.data.type.Chest.Type.LEFT; + } + Block face = chestBlock.getRelative(faceToSecondChest); + if (face.getType() == chestBlock.getType()) { + // check is the neighbour connects to this chest + org.bukkit.block.data.type.Chest otherChestData = (org.bukkit.block.data.type.Chest) face.getBlockData(); + if (otherChestData.getType() != wantedChestType || otherChestData.getFacing() != chestFace) { + return null; + } + return face; + } + } + return null; + } + + public static Entity loadEntityAround(Chunk chunk, UUID uuid) { + Entity e = Bukkit.getEntity(uuid); + if (e != null) { + return e; + } + if (!chunk.isLoaded()) { + chunk.getWorld().getChunkAt(chunk.getX(), chunk.getZ()); + 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); + if (!chunk.getWorld().isChunkLoaded(x, z)) { + chunk.getWorld().getChunkAt(x, z); + e = Bukkit.getEntity(uuid); + if (e != null) { + return e; + } + } + } + return null; + } + + private static final HashMap types = new HashMap<>(); + static { + for (EntityType t : EntityType.values()) { + if (t != EntityType.UNKNOWN) { + types.put(t.name().toLowerCase(), t); + @SuppressWarnings("deprecation") + String typeName = t.getName(); + if (typeName != null) { + types.put(typeName.toLowerCase(), t); + } + Class ec = t.getEntityClass(); + if (ec != null) { + types.put(ec.getSimpleName().toLowerCase(), t); + } + types.put(t.getKey().getKey(), t); + types.put(t.getKey().toString(), t); + } + } + } + + public static EntityType matchEntityType(String typeName) { + return types.get(typeName.toLowerCase()); + } + + public static ItemStack getItemInSlot(ArmorStand stand, EquipmentSlot slot) { + if (slot == EquipmentSlot.HAND) { + return stand.getEquipment().getItemInMainHand(); + } else if (slot == EquipmentSlot.OFF_HAND) { + return stand.getEquipment().getItemInOffHand(); + } else if (slot == EquipmentSlot.FEET) { + return stand.getEquipment().getBoots(); + } else if (slot == EquipmentSlot.LEGS) { + return stand.getEquipment().getLeggings(); + } else if (slot == EquipmentSlot.CHEST) { + return stand.getEquipment().getChestplate(); + } else if (slot == EquipmentSlot.HEAD) { + return stand.getEquipment().getHelmet(); + } + return null; + } + + public static void setItemInSlot(ArmorStand stand, EquipmentSlot slot, ItemStack stack) { + if (slot == EquipmentSlot.HAND) { + stand.getEquipment().setItemInMainHand(stack); + } else if (slot == EquipmentSlot.OFF_HAND) { + stand.getEquipment().setItemInOffHand(stack); + } else if (slot == EquipmentSlot.FEET) { + stand.getEquipment().setBoots(stack); + } else if (slot == EquipmentSlot.LEGS) { + stand.getEquipment().setLeggings(stack); + } else if (slot == EquipmentSlot.CHEST) { + stand.getEquipment().setChestplate(stack); + } else if (slot == EquipmentSlot.HEAD) { + stand.getEquipment().setHelmet(stack); + } + } + + public static ItemStack[] deepCopy(ItemStack[] of) { + ItemStack[] result = new ItemStack[of.length]; + for (int i = 0; i < result.length; i++) { + result[i] = of[i] == null ? null : new ItemStack(of[i]); + } + return result; + } + + private static int getFirstPartialItemStack(ItemStack item, ItemStack[] contents, int start) { + for (int i = start; i < contents.length; i++) { + ItemStack content = contents[i]; + if (content != null && content.isSimilar(item) && content.getAmount() < content.getMaxStackSize()) { + return i; + } + } + return -1; + } + + private static int getFirstFreeItemStack(ItemStack[] contents, int start) { + for (int i = start; i < contents.length; i++) { + ItemStack content = contents[i]; + if (content == null || content.getAmount() == 0 || content.getType() == Material.AIR) { + return i; + } + } + return -1; + } + + public static boolean hasInventoryStorageSpaceFor(Inventory inv, ItemStack... items) { + ItemStack[] contents = deepCopy(inv.getStorageContents()); + for (ItemStack item : items) { + if (item != null && item.getType() != Material.AIR) { + int remaining = item.getAmount(); + // fill partial stacks + int firstPartial = -1; + while (remaining > 0) { + firstPartial = getFirstPartialItemStack(item, contents, firstPartial + 1); + if (firstPartial < 0) { + break; + } + ItemStack content = contents[firstPartial]; + int add = Math.min(content.getMaxStackSize() - content.getAmount(), remaining); + content.setAmount(content.getAmount() + add); + remaining -= add; + } + // create new stacks + int firstFree = -1; + while (remaining > 0) { + firstFree = getFirstFreeItemStack(contents, firstFree + 1); + if (firstFree < 0) { + return false; // no free place found + } + ItemStack content = new ItemStack(item); + contents[firstFree] = content; + // max stack size might return -1, in this case assume 1 + int add = Math.min(Math.max(content.getMaxStackSize(), 1), remaining); + content.setAmount(add); + remaining -= add; + } + } + } + return true; + } + + public static boolean isSimilarForRollback(Material expected, Material found) { + if (expected == found) { + return true; + } + if (expected == Material.DIRT || expected == Material.MYCELIUM || expected == Material.FARMLAND || expected == Material.GRASS_BLOCK || expected == Material.PODZOL || expected == Material.DIRT_PATH) { + return found == Material.DIRT || found == Material.MYCELIUM || found == Material.FARMLAND || found == Material.GRASS_BLOCK || found == Material.PODZOL || found == Material.DIRT_PATH; + } + if (expected == Material.BAMBOO || expected == Material.BAMBOO_SAPLING) { + return found == Material.BAMBOO || found == Material.BAMBOO_SAPLING; + } + if (expected == Material.SPONGE || expected == Material.WET_SPONGE) { + return found == Material.SPONGE || found == Material.WET_SPONGE; + } + if (expected == Material.MELON_STEM || expected == Material.ATTACHED_MELON_STEM) { + return found == Material.MELON_STEM || found == Material.ATTACHED_MELON_STEM; + } + if (expected == Material.PUMPKIN_STEM || expected == Material.ATTACHED_PUMPKIN_STEM) { + return found == Material.PUMPKIN_STEM || found == Material.ATTACHED_PUMPKIN_STEM; + } + if (expected == Material.TWISTING_VINES || expected == Material.TWISTING_VINES_PLANT) { + return found == Material.TWISTING_VINES || found == Material.TWISTING_VINES_PLANT; + } + if (expected == Material.WEEPING_VINES || expected == Material.WEEPING_VINES_PLANT) { + return found == Material.WEEPING_VINES || found == Material.WEEPING_VINES_PLANT; + } + if (expected == Material.CAVE_VINES || expected == Material.CAVE_VINES_PLANT) { + return found == Material.CAVE_VINES || found == Material.CAVE_VINES_PLANT; + } + if (expected == Material.BIG_DRIPLEAF || expected == Material.BIG_DRIPLEAF_STEM) { + return found == Material.BIG_DRIPLEAF || found == Material.BIG_DRIPLEAF_STEM; + } + if (expected == Material.COPPER_BLOCK || expected == Material.EXPOSED_COPPER || expected == Material.WEATHERED_COPPER || expected == Material.OXIDIZED_COPPER) { + return found == Material.COPPER_BLOCK || found == Material.EXPOSED_COPPER || found == Material.WEATHERED_COPPER || found == Material.OXIDIZED_COPPER; + } + if (expected == Material.CUT_COPPER || expected == Material.EXPOSED_CUT_COPPER || expected == Material.WEATHERED_CUT_COPPER || expected == Material.OXIDIZED_CUT_COPPER) { + return found == Material.CUT_COPPER || found == Material.EXPOSED_CUT_COPPER || found == Material.WEATHERED_CUT_COPPER || found == Material.OXIDIZED_CUT_COPPER; + } + if (expected == Material.CUT_COPPER_STAIRS || expected == Material.EXPOSED_CUT_COPPER_STAIRS || expected == Material.WEATHERED_CUT_COPPER_STAIRS || expected == Material.OXIDIZED_CUT_COPPER_STAIRS) { + return found == Material.CUT_COPPER_STAIRS || found == Material.EXPOSED_CUT_COPPER_STAIRS || found == Material.WEATHERED_CUT_COPPER_STAIRS || found == Material.OXIDIZED_CUT_COPPER_STAIRS; + } + if (expected == Material.CUT_COPPER_SLAB || expected == Material.EXPOSED_CUT_COPPER_SLAB || expected == Material.WEATHERED_CUT_COPPER_SLAB || expected == Material.OXIDIZED_CUT_COPPER_SLAB) { + return found == Material.CUT_COPPER_SLAB || found == Material.EXPOSED_CUT_COPPER_SLAB || found == Material.WEATHERED_CUT_COPPER_SLAB || found == Material.OXIDIZED_CUT_COPPER_SLAB; + } + return false; + } + + public static Set getAllSignMaterials() { + return allSigns.getValues(); + } + + public static boolean isAlwaysWaterlogged(Material m) { + return alwaysWaterlogged.contains(m); + } + + public static boolean isCandle(Material m) { + return candles.isTagged(m); + } + + public static boolean isCandleCake(Material m) { + return candleCakes.isTagged(m); + } + + public static boolean isHangingSign(Material m) { + return hangingSigns.isTagged(m); + } + + public static boolean isFenceGate(Material m) { + return fenceGates.isTagged(m); + } + + public static boolean isWoodenTrapdoor(Material m) { + return woodenTrapdoors.isTagged(m); + } + + public static boolean isPressurePlate(Material m) { + return pressurePlates.isTagged(m); + } + + public static boolean isSign(Material m) { + return allSigns.isTagged(m); + } + + public static Side getFacingSignSide(Entity entity, Block sign) { + BlockData data = sign.getBlockData(); + Material type = data.getMaterial(); + BlockFace signFace = null; + double centerx = 0.5; + double centerz = 0.5; + double yRotationDegree = 0; + if (type.data == Sign.class || type.data == HangingSign.class) { + Rotatable rotatableData = (Rotatable) data; + signFace = rotatableData.getRotation(); + if (signFace == BlockFace.SOUTH) { + yRotationDegree = 360 * 0.0 / 16.0; + } else if (signFace == BlockFace.SOUTH_SOUTH_WEST) { + yRotationDegree = 360 * 1.0 / 16.0; + } else if (signFace == BlockFace.SOUTH_WEST) { + yRotationDegree = 360 * 2.0 / 16.0; + } else if (signFace == BlockFace.WEST_SOUTH_WEST) { + yRotationDegree = 360 * 3.0 / 16.0; + } else if (signFace == BlockFace.WEST) { + yRotationDegree = 360 * 4.0 / 16.0; + } else if (signFace == BlockFace.WEST_NORTH_WEST) { + yRotationDegree = 360 * 5.0 / 16.0; + } else if (signFace == BlockFace.NORTH_WEST) { + yRotationDegree = 360 * 6.0 / 16.0; + } else if (signFace == BlockFace.NORTH_NORTH_WEST) { + yRotationDegree = 360 * 7.0 / 16.0; + } else if (signFace == BlockFace.NORTH) { + yRotationDegree = 360 * 8.0 / 16.0; + } else if (signFace == BlockFace.NORTH_NORTH_EAST) { + yRotationDegree = 360 * 9.0 / 16.0; + } else if (signFace == BlockFace.NORTH_EAST) { + yRotationDegree = 360 * 10.0 / 16.0; + } else if (signFace == BlockFace.EAST_NORTH_EAST) { + yRotationDegree = 360 * 11.0 / 16.0; + } else if (signFace == BlockFace.EAST) { + yRotationDegree = 360 * 12.0 / 16.0; + } else if (signFace == BlockFace.EAST_SOUTH_EAST) { + yRotationDegree = 360 * 13.0 / 16.0; + } else if (signFace == BlockFace.SOUTH_EAST) { + yRotationDegree = 360 * 14.0 / 16.0; + } else if (signFace == BlockFace.SOUTH_SOUTH_EAST) { + yRotationDegree = 360 * 15.0 / 16.0; + } + } else if (type.data == WallSign.class || type.data == WallHangingSign.class) { + Directional directionalData = (Directional) data; + signFace = directionalData.getFacing(); + if (signFace == BlockFace.SOUTH) { + yRotationDegree = 0; + } else if (signFace == BlockFace.WEST) { + yRotationDegree = 90; + } else if (signFace == BlockFace.NORTH) { + yRotationDegree = 180; + } else if (signFace == BlockFace.EAST) { + yRotationDegree = 270; + } + // wall signs are not centered on the block (but hanging wall signs are) + if (type.data == WallSign.class) { + if (signFace == BlockFace.NORTH) { + centerz = 15.0 / 16.0; + } else if (signFace == BlockFace.SOUTH) { + centerz = 1.0 / 16.0; + } else if (signFace == BlockFace.WEST) { + centerx = 15.0 / 16.0; + } else if (signFace == BlockFace.EAST) { + centerx = 1.0 / 16.0; + } + } + } else { + throw new IllegalArgumentException("block is not a sign"); + } + + Location entityLoc = entity.getLocation(); + double relativeX = entityLoc.getX() - (sign.getX() + centerx); + double relativeZ = entityLoc.getZ() - (sign.getZ() + centerz); + double f = Math.atan2(relativeZ, relativeX) * 180.0 / Math.PI - 90.0; + + return Math.abs(Utils.warpDegrees(f - yRotationDegree)) <= 90.0 ? Side.FRONT : Side.BACK; + } +} diff --git a/src/main/java/de/diddiz/util/ComparableVersion.java b/src/main/java/de/diddiz/LogBlock/util/ComparableVersion.java similarity index 54% rename from src/main/java/de/diddiz/util/ComparableVersion.java rename to src/main/java/de/diddiz/LogBlock/util/ComparableVersion.java index 46ac027c..5bdec9a1 100644 --- a/src/main/java/de/diddiz/util/ComparableVersion.java +++ b/src/main/java/de/diddiz/LogBlock/util/ComparableVersion.java @@ -1,471 +1,424 @@ -package de.diddiz.util; - -// Taken from maven-artifact at -// http://grepcode.com/file_/repo1.maven.org/maven2/org.apache.maven/maven-artifact/3.2.3/org/apache/maven/artifact/versioning/ComparableVersion.java/?v=source - -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import java.math.BigInteger; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; -import java.util.ListIterator; -import java.util.Locale; -import java.util.Properties; -import java.util.Stack; - -/** - * Generic implementation of version comparison. - * - *

Features: - *

    - *
  • mixing of '-' (dash) and '.' (dot) separators,
  • - *
  • transition between characters and digits also constitutes a separator: - * 1.0alpha1 => [1, 0, alpha, 1]
  • - *
  • unlimited number of version components,
  • - *
  • version components in the text can be digits or strings,
  • - *
  • strings are checked for well-known qualifiers and the qualifier ordering is used for version ordering. - * Well-known qualifiers (case insensitive) are:
      - *
    • alpha or a
    • - *
    • beta or b
    • - *
    • milestone or m
    • - *
    • rc or cr
    • - *
    • snapshot
    • - *
    • (the empty string) or ga or final
    • - *
    • sp
    • - *
    - * Unknown qualifiers are considered after known qualifiers, with lexical order (always case insensitive), - *
  • - *
  • a dash usually precedes a qualifier, and is always less important than something preceded with a dot.
  • - *

- * - * @see "Versioning" on Maven Wiki - * @author Kenney Westerhof - * @author Hervé Boutemy - */ -public class ComparableVersion - implements Comparable -{ - private String value; - - private String canonical; - - private ListItem items; - - private interface Item - { - int INTEGER_ITEM = 0; - int STRING_ITEM = 1; - int LIST_ITEM = 2; - - int compareTo( Item item ); - - int getType(); - - boolean isNull(); - } - - /** - * Represents a numeric item in the version item list. - */ - private static class IntegerItem - implements Item - { - private static final BigInteger BIG_INTEGER_ZERO = new BigInteger( "0" ); - - private final BigInteger value; - - public static final IntegerItem ZERO = new IntegerItem(); - - private IntegerItem() - { - this.value = BIG_INTEGER_ZERO; - } - - public IntegerItem( String str ) - { - this.value = new BigInteger( str ); - } - - public int getType() - { - return INTEGER_ITEM; - } - - public boolean isNull() - { - return BIG_INTEGER_ZERO.equals( value ); - } - - public int compareTo( Item item ) - { - if ( item == null ) - { - return BIG_INTEGER_ZERO.equals( value ) ? 0 : 1; // 1.0 == 1, 1.1 > 1 - } - - switch ( item.getType() ) - { - case INTEGER_ITEM: - return value.compareTo( ( (IntegerItem) item ).value ); - - case STRING_ITEM: - return 1; // 1.1 > 1-sp - - case LIST_ITEM: - return 1; // 1.1 > 1-1 - - default: - throw new RuntimeException( "invalid item: " + item.getClass() ); - } - } - - public String toString() - { - return value.toString(); - } - } - - /** - * Represents a string in the version item list, usually a qualifier. - */ - private static class StringItem - implements Item - { - private static final String[] QUALIFIERS = { "alpha", "beta", "milestone", "rc", "snapshot", "", "sp" }; - - private static final List _QUALIFIERS = Arrays.asList( QUALIFIERS ); - - private static final Properties ALIASES = new Properties(); - static - { - ALIASES.put( "ga", "" ); - ALIASES.put( "final", "" ); - ALIASES.put( "cr", "rc" ); - } - - /** - * A comparable value for the empty-string qualifier. This one is used to determine if a given qualifier makes - * the version older than one without a qualifier, or more recent. - */ - private static final String RELEASE_VERSION_INDEX = String.valueOf( _QUALIFIERS.indexOf( "" ) ); - - private String value; - - public StringItem( String value, boolean followedByDigit ) - { - if ( followedByDigit && value.length() == 1 ) - { - // a1 = alpha-1, b1 = beta-1, m1 = milestone-1 - switch ( value.charAt( 0 ) ) - { - case 'a': - value = "alpha"; - break; - case 'b': - value = "beta"; - break; - case 'm': - value = "milestone"; - break; - } - } - this.value = ALIASES.getProperty( value , value ); - } - - public int getType() - { - return STRING_ITEM; - } - - public boolean isNull() - { - return ( comparableQualifier( value ).compareTo( RELEASE_VERSION_INDEX ) == 0 ); - } - - /** - * Returns a comparable value for a qualifier. - * - * This method takes into account the ordering of known qualifiers then unknown qualifiers with lexical ordering. - * - * just returning an Integer with the index here is faster, but requires a lot of if/then/else to check for -1 - * or QUALIFIERS.size and then resort to lexical ordering. Most comparisons are decided by the first character, - * so this is still fast. If more characters are needed then it requires a lexical sort anyway. - * - * @param qualifier - * @return an equivalent value that can be used with lexical comparison - */ - public static String comparableQualifier( String qualifier ) - { - int i = _QUALIFIERS.indexOf( qualifier ); - - return i == -1 ? ( _QUALIFIERS.size() + "-" + qualifier ) : String.valueOf( i ); - } - - public int compareTo( Item item ) - { - if ( item == null ) - { - // 1-rc < 1, 1-ga > 1 - return comparableQualifier( value ).compareTo( RELEASE_VERSION_INDEX ); - } - switch ( item.getType() ) - { - case INTEGER_ITEM: - return -1; // 1.any < 1.1 ? - - case STRING_ITEM: - return comparableQualifier( value ).compareTo( comparableQualifier( ( (StringItem) item ).value ) ); - - case LIST_ITEM: - return -1; // 1.any < 1-1 - - default: - throw new RuntimeException( "invalid item: " + item.getClass() ); - } - } - - public String toString() - { - return value; - } - } - - /** - * Represents a version list item. This class is used both for the global item list and for sub-lists (which start - * with '-(number)' in the version specification). - */ - private static class ListItem - extends ArrayList - implements Item - { - public int getType() - { - return LIST_ITEM; - } - - public boolean isNull() - { - return ( size() == 0 ); - } - - void normalize() - { - for ( ListIterator iterator = listIterator( size() ); iterator.hasPrevious(); ) - { - Item item = iterator.previous(); - if ( item.isNull() ) - { - iterator.remove(); // remove null trailing items: 0, "", empty list - } - else - { - break; - } - } - } - - public int compareTo( Item item ) - { - if ( item == null ) - { - if ( size() == 0 ) - { - return 0; // 1-0 = 1- (normalize) = 1 - } - Item first = get( 0 ); - return first.compareTo( null ); - } - switch ( item.getType() ) - { - case INTEGER_ITEM: - return -1; // 1-1 < 1.0.x - - case STRING_ITEM: - return 1; // 1-1 > 1-sp - - case LIST_ITEM: - Iterator left = iterator(); - Iterator right = ( (ListItem) item ).iterator(); - - while ( left.hasNext() || right.hasNext() ) - { - Item l = left.hasNext() ? left.next() : null; - Item r = right.hasNext() ? right.next() : null; - - // if this is shorter, then invert the compare and mul with -1 - int result = l == null ? ( r == null ? 0 : -1 * r.compareTo( l ) ) : l.compareTo( r ); - - if ( result != 0 ) - { - return result; - } - } - - return 0; - - default: - throw new RuntimeException( "invalid item: " + item.getClass() ); - } - } - - public String toString() - { - StringBuilder buffer = new StringBuilder( "(" ); - for ( Iterator iter = iterator(); iter.hasNext(); ) - { - buffer.append( iter.next() ); - if ( iter.hasNext() ) - { - buffer.append( ',' ); - } - } - buffer.append( ')' ); - return buffer.toString(); - } - } - - public ComparableVersion( String version ) - { - parseVersion( version ); - } - - public final void parseVersion( String version ) - { - this.value = version; - - items = new ListItem(); - - version = version.toLowerCase( Locale.ENGLISH ); - - ListItem list = items; - - Stack stack = new Stack(); - stack.push( list ); - - boolean isDigit = false; - - int startIndex = 0; - - for ( int i = 0; i < version.length(); i++ ) - { - char c = version.charAt( i ); - - if ( c == '.' ) - { - if ( i == startIndex ) - { - list.add( IntegerItem.ZERO ); - } - else - { - list.add( parseItem( isDigit, version.substring( startIndex, i ) ) ); - } - startIndex = i + 1; - } - else if ( c == '-' ) - { - if ( i == startIndex ) - { - list.add( IntegerItem.ZERO ); - } - else - { - list.add( parseItem( isDigit, version.substring( startIndex, i ) ) ); - } - startIndex = i + 1; - - if ( isDigit ) - { - list.normalize(); // 1.0-* = 1-* - - if ( ( i + 1 < version.length() ) && Character.isDigit( version.charAt( i + 1 ) ) ) - { - // new ListItem only if previous were digits and new char is a digit, - // ie need to differentiate only 1.1 from 1-1 - list.add( list = new ListItem() ); - - stack.push( list ); - } - } - } - else if ( Character.isDigit( c ) ) - { - if ( !isDigit && i > startIndex ) - { - list.add( new StringItem( version.substring( startIndex, i ), true ) ); - startIndex = i; - } - - isDigit = true; - } - else - { - if ( isDigit && i > startIndex ) - { - list.add( parseItem( true, version.substring( startIndex, i ) ) ); - startIndex = i; - } - - isDigit = false; - } - } - - if ( version.length() > startIndex ) - { - list.add( parseItem( isDigit, version.substring( startIndex ) ) ); - } - - while ( !stack.isEmpty() ) - { - list = (ListItem) stack.pop(); - list.normalize(); - } - - canonical = items.toString(); - } - - private static Item parseItem( boolean isDigit, String buf ) - { - return isDigit ? new IntegerItem( buf ) : new StringItem( buf, false ); - } - - public int compareTo( ComparableVersion o ) - { - return items.compareTo( o.items ); - } - - public String toString() - { - return value; - } - - public boolean equals( Object o ) - { - return ( o instanceof ComparableVersion ) && canonical.equals( ( (ComparableVersion) o ).canonical ); - } - - public int hashCode() - { - return canonical.hashCode(); - } -} - +package de.diddiz.LogBlock.util; + +// Taken from maven-artifact at +// http://grepcode.com/file_/repo1.maven.org/maven2/org.apache.maven/maven-artifact/3.2.3/org/apache/maven/artifact/versioning/ComparableVersion.java/?v=source + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Locale; +import java.util.Properties; +import java.util.Stack; + +/** + * Generic implementation of version comparison. + * + *

Features: + *

    + *
  • mixing of '-' (dash) and '.' (dot) separators,
  • + *
  • transition between characters and digits also constitutes a separator: + * 1.0alpha1 => [1, 0, alpha, 1]
  • + *
  • unlimited number of version components,
  • + *
  • version components in the text can be digits or strings,
  • + *
  • strings are checked for well-known qualifiers and the qualifier ordering is used for version ordering. + * Well-known qualifiers (case insensitive) are:
      + *
    • alpha or a
    • + *
    • beta or b
    • + *
    • milestone or m
    • + *
    • rc or cr
    • + *
    • snapshot
    • + *
    • (the empty string) or ga or final
    • + *
    • sp
    • + *
    + * Unknown qualifiers are considered after known qualifiers, with lexical order (always case insensitive), + *
  • + *
  • a dash usually precedes a qualifier, and is always less important than something preceded with a dot.
  • + *

+ * + * @see "Versioning" on Maven Wiki + * @author Kenney Westerhof + * @author Hervé Boutemy + */ +public class ComparableVersion implements Comparable { + private String value; + + private String canonical; + + private ListItem items; + + private interface Item { + int INTEGER_ITEM = 0; + int STRING_ITEM = 1; + int LIST_ITEM = 2; + + int compareTo(Item item); + + int getType(); + + boolean isNull(); + } + + /** + * Represents a numeric item in the version item list. + */ + private static class IntegerItem implements Item { + private static final BigInteger BIG_INTEGER_ZERO = new BigInteger("0"); + + private final BigInteger value; + + public static final IntegerItem ZERO = new IntegerItem(); + + private IntegerItem() { + this.value = BIG_INTEGER_ZERO; + } + + public IntegerItem(String str) { + this.value = new BigInteger(str); + } + + @Override + public int getType() { + return INTEGER_ITEM; + } + + @Override + public boolean isNull() { + return BIG_INTEGER_ZERO.equals(value); + } + + @Override + public int compareTo(Item item) { + if (item == null) { + return BIG_INTEGER_ZERO.equals(value) ? 0 : 1; // 1.0 == 1, 1.1 > 1 + } + + switch (item.getType()) { + case INTEGER_ITEM: + return value.compareTo(((IntegerItem) item).value); + + case STRING_ITEM: + return 1; // 1.1 > 1-sp + + case LIST_ITEM: + return 1; // 1.1 > 1-1 + + default: + throw new RuntimeException("invalid item: " + item.getClass()); + } + } + + @Override + public String toString() { + return value.toString(); + } + } + + /** + * Represents a string in the version item list, usually a qualifier. + */ + private static class StringItem implements Item { + private static final String[] QUALIFIERS = { "alpha", "beta", "milestone", "rc", "snapshot", "", "sp" }; + + private static final List _QUALIFIERS = Arrays.asList(QUALIFIERS); + + private static final Properties ALIASES = new Properties(); + static { + ALIASES.put("ga", ""); + ALIASES.put("final", ""); + ALIASES.put("cr", "rc"); + } + + /** + * A comparable value for the empty-string qualifier. This one is used to determine if a given qualifier makes + * the version older than one without a qualifier, or more recent. + */ + private static final String RELEASE_VERSION_INDEX = String.valueOf(_QUALIFIERS.indexOf("")); + + private String value; + + public StringItem(String value, boolean followedByDigit) { + if (followedByDigit && value.length() == 1) { + // a1 = alpha-1, b1 = beta-1, m1 = milestone-1 + switch (value.charAt(0)) { + case 'a': + value = "alpha"; + break; + case 'b': + value = "beta"; + break; + case 'm': + value = "milestone"; + break; + } + } + this.value = ALIASES.getProperty(value, value); + } + + @Override + public int getType() { + return STRING_ITEM; + } + + @Override + public boolean isNull() { + return (comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX) == 0); + } + + /** + * Returns a comparable value for a qualifier. + * + * This method takes into account the ordering of known qualifiers then unknown qualifiers with lexical ordering. + * + * just returning an Integer with the index here is faster, but requires a lot of if/then/else to check for -1 + * or QUALIFIERS.size and then resort to lexical ordering. Most comparisons are decided by the first character, + * so this is still fast. If more characters are needed then it requires a lexical sort anyway. + * + * @param qualifier + * @return an equivalent value that can be used with lexical comparison + */ + public static String comparableQualifier(String qualifier) { + int i = _QUALIFIERS.indexOf(qualifier); + + return i == -1 ? (_QUALIFIERS.size() + "-" + qualifier) : String.valueOf(i); + } + + @Override + public int compareTo(Item item) { + if (item == null) { + // 1-rc < 1, 1-ga > 1 + return comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX); + } + switch (item.getType()) { + case INTEGER_ITEM: + return -1; // 1.any < 1.1 ? + + case STRING_ITEM: + return comparableQualifier(value).compareTo(comparableQualifier(((StringItem) item).value)); + + case LIST_ITEM: + return -1; // 1.any < 1-1 + + default: + throw new RuntimeException("invalid item: " + item.getClass()); + } + } + + @Override + public String toString() { + return value; + } + } + + /** + * Represents a version list item. This class is used both for the global item list and for sub-lists (which start + * with '-(number)' in the version specification). + */ + private static class ListItem extends ArrayList implements Item { + private static final long serialVersionUID = 5914575811857700009L; + + @Override + public int getType() { + return LIST_ITEM; + } + + @Override + public boolean isNull() { + return (size() == 0); + } + + void normalize() { + for (ListIterator iterator = listIterator(size()); iterator.hasPrevious();) { + Item item = iterator.previous(); + if (item.isNull()) { + iterator.remove(); // remove null trailing items: 0, "", empty list + } else { + break; + } + } + } + + @Override + public int compareTo(Item item) { + if (item == null) { + if (size() == 0) { + return 0; // 1-0 = 1- (normalize) = 1 + } + Item first = get(0); + return first.compareTo(null); + } + switch (item.getType()) { + case INTEGER_ITEM: + return -1; // 1-1 < 1.0.x + + case STRING_ITEM: + return 1; // 1-1 > 1-sp + + case LIST_ITEM: + Iterator left = iterator(); + Iterator right = ((ListItem) item).iterator(); + + while (left.hasNext() || right.hasNext()) { + Item l = left.hasNext() ? left.next() : null; + Item r = right.hasNext() ? right.next() : null; + + // if this is shorter, then invert the compare and mul with -1 + int result = l == null ? (r == null ? 0 : -1 * r.compareTo(l)) : l.compareTo(r); + + if (result != 0) { + return result; + } + } + + return 0; + + default: + throw new RuntimeException("invalid item: " + item.getClass()); + } + } + + @Override + public String toString() { + StringBuilder buffer = new StringBuilder("("); + for (Iterator iter = iterator(); iter.hasNext();) { + buffer.append(iter.next()); + if (iter.hasNext()) { + buffer.append(','); + } + } + buffer.append(')'); + return buffer.toString(); + } + } + + public ComparableVersion(String version) { + parseVersion(version); + } + + public final void parseVersion(String version) { + this.value = version; + + items = new ListItem(); + + version = version.toLowerCase(Locale.ENGLISH); + + ListItem list = items; + + Stack stack = new Stack<>(); + stack.push(list); + + boolean isDigit = false; + + int startIndex = 0; + + for (int i = 0; i < version.length(); i++) { + char c = version.charAt(i); + + if (c == '.') { + if (i == startIndex) { + list.add(IntegerItem.ZERO); + } else { + list.add(parseItem(isDigit, version.substring(startIndex, i))); + } + startIndex = i + 1; + } else if (c == '-') { + if (i == startIndex) { + list.add(IntegerItem.ZERO); + } else { + list.add(parseItem(isDigit, version.substring(startIndex, i))); + } + startIndex = i + 1; + + if (isDigit) { + list.normalize(); // 1.0-* = 1-* + + if ((i + 1 < version.length()) && Character.isDigit(version.charAt(i + 1))) { + // new ListItem only if previous were digits and new char is a digit, + // ie need to differentiate only 1.1 from 1-1 + list.add(list = new ListItem()); + + stack.push(list); + } + } + } else if (Character.isDigit(c)) { + if (!isDigit && i > startIndex) { + list.add(new StringItem(version.substring(startIndex, i), true)); + startIndex = i; + } + + isDigit = true; + } else { + if (isDigit && i > startIndex) { + list.add(parseItem(true, version.substring(startIndex, i))); + startIndex = i; + } + + isDigit = false; + } + } + + if (version.length() > startIndex) { + list.add(parseItem(isDigit, version.substring(startIndex))); + } + + while (!stack.isEmpty()) { + list = (ListItem) stack.pop(); + list.normalize(); + } + + canonical = items.toString(); + } + + private static Item parseItem(boolean isDigit, String buf) { + return isDigit ? new IntegerItem(buf) : new StringItem(buf, false); + } + + @Override + public int compareTo(ComparableVersion o) { + return items.compareTo(o.items); + } + + public int compareTo(String version) { + return compareTo(new ComparableVersion(version)); + } + + @Override + public String toString() { + return value; + } + + public String toCanonicalString() { + return canonical; + } + + @Override + public boolean equals(Object o) { + return (o instanceof ComparableVersion) && canonical.equals(((ComparableVersion) o).canonical); + } + + @Override + public int hashCode() { + return canonical.hashCode(); + } +} diff --git a/src/main/java/de/diddiz/LogBlock/util/CuboidRegion.java b/src/main/java/de/diddiz/LogBlock/util/CuboidRegion.java new file mode 100644 index 00000000..7247706c --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/util/CuboidRegion.java @@ -0,0 +1,58 @@ +package de.diddiz.LogBlock.util; + +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.util.BlockVector; + +public class CuboidRegion implements Cloneable { + + private World world; + private BlockVector min = new BlockVector(); + private BlockVector max = new BlockVector(); + + public CuboidRegion(World world, BlockVector first, BlockVector second) { + this.world = world; + this.min.setX(Math.min(first.getBlockX(), second.getBlockX())); + this.min.setY(Math.min(first.getBlockY(), second.getBlockY())); + this.min.setZ(Math.min(first.getBlockZ(), second.getBlockZ())); + this.max.setX(Math.max(first.getBlockX(), second.getBlockX())); + this.max.setY(Math.max(first.getBlockY(), second.getBlockY())); + this.max.setZ(Math.max(first.getBlockZ(), second.getBlockZ())); + } + + public static CuboidRegion fromCorners(World world, Location first, Location second) { + return new CuboidRegion(world, new BlockVector(first.getBlockX(), first.getBlockY(), first.getBlockZ()), new BlockVector(second.getBlockX(), second.getBlockY(), second.getBlockZ())); + } + + public World getWorld() { + return world; + } + + public BlockVector getMinimumPoint() { + return min; + } + + public BlockVector getMaximumPoint() { + return max; + } + + public int getSizeX() { + return max.getBlockX() - min.getBlockX() + 1; + } + + public int getSizeZ() { + return max.getBlockZ() - min.getBlockZ() + 1; + } + + @Override + public CuboidRegion clone() { + try { + CuboidRegion clone = (CuboidRegion) super.clone(); + clone.min = min.clone(); + clone.max = max.clone(); + return clone; + } catch (final CloneNotSupportedException ex) { + throw new Error("CuboidRegion should be cloneable", ex); + } + } +} diff --git a/src/main/java/de/diddiz/LogBlock/util/InventoryUtils.java b/src/main/java/de/diddiz/LogBlock/util/InventoryUtils.java new file mode 100644 index 00000000..269be74b --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/util/InventoryUtils.java @@ -0,0 +1,113 @@ +package de.diddiz.LogBlock.util; + +import org.bukkit.Material; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +public class InventoryUtils { + + public static int addToInventory(Inventory inventory, ItemStackAndAmount item) { + if (item == null || item.stack() == null || item.stack().getType() == Material.AIR) { + return 0; + } + int maxStackSize = Math.max(Math.min(inventory.getMaxStackSize(), item.stack().getMaxStackSize()), 1); + + ItemStack[] contents = inventory.getStorageContents(); + + int remaining = item.amount(); + int initialRemaining = remaining; + + // fill partial stacks + int firstPartial = -1; + while (remaining > 0) { + firstPartial = getFirstPartial(item.stack(), maxStackSize, contents, firstPartial + 1); + if (firstPartial < 0) { + break; + } + ItemStack content = contents[firstPartial]; + int add = Math.min(maxStackSize - content.getAmount(), remaining); + content.setAmount(content.getAmount() + add); + remaining -= add; + } + // create new stacks + int firstFree = -1; + while (remaining > 0) { + firstFree = getFirstFree(contents, firstFree + 1); + if (firstFree < 0) { + break; + } + ItemStack content = item.stack().clone(); + contents[firstFree] = content; + int add = Math.min(maxStackSize, remaining); + content.setAmount(add); + remaining -= add; + } + + if (remaining < initialRemaining) { + inventory.setStorageContents(contents); + } + return remaining; + } + + public static int removeFromInventory(Inventory inventory, ItemStackAndAmount item) { + if (item == null || item.stack() == null || item.stack().getType() == Material.AIR) { + return 0; + } + + ItemStack[] contents = inventory.getStorageContents(); + int remaining = item.amount(); + int initialRemaining = remaining; + + int firstSimilar = -1; + while (remaining > 0) { + firstSimilar = getFirstSimilar(item.stack(), contents, firstSimilar + 1); + if (firstSimilar < 0) { + break; + } + ItemStack content = contents[firstSimilar]; + int here = content.getAmount(); + if (here > remaining) { + content.setAmount(here - remaining); + remaining = 0; + } else { + contents[firstSimilar] = null; + remaining -= here; + } + } + + if (remaining < initialRemaining) { + inventory.setStorageContents(contents); + } + return remaining; + } + + private static int getFirstSimilar(ItemStack item, ItemStack[] contents, int start) { + for (int i = start; i < contents.length; i++) { + ItemStack content = contents[i]; + if (content != null && content.isSimilar(item)) { + return i; + } + } + return -1; + } + + private static int getFirstPartial(ItemStack item, int maxStackSize, ItemStack[] contents, int start) { + for (int i = start; i < contents.length; i++) { + ItemStack content = contents[i]; + if (content != null && content.isSimilar(item) && content.getAmount() < maxStackSize) { + return i; + } + } + return -1; + } + + private static int getFirstFree(ItemStack[] contents, int start) { + for (int i = start; i < contents.length; i++) { + ItemStack content = contents[i]; + if (content == null || content.getAmount() == 0 || content.getType() == Material.AIR) { + return i; + } + } + return -1; + } +} diff --git a/src/main/java/de/diddiz/LogBlock/util/ItemStackAndAmount.java b/src/main/java/de/diddiz/LogBlock/util/ItemStackAndAmount.java new file mode 100644 index 00000000..1737a4f0 --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/util/ItemStackAndAmount.java @@ -0,0 +1,15 @@ +package de.diddiz.LogBlock.util; + +import org.bukkit.inventory.ItemStack; + +public record ItemStackAndAmount(ItemStack stack, int amount) { + + public static ItemStackAndAmount fromStack(ItemStack stack) { + int amount = stack.getAmount(); + if (amount > 1) { + stack = stack.clone(); + stack.setAmount(1); + } + return new ItemStackAndAmount(stack, amount); + } +} diff --git a/src/main/java/de/diddiz/LogBlock/util/LoggingUtil.java b/src/main/java/de/diddiz/LogBlock/util/LoggingUtil.java new file mode 100644 index 00000000..8322f765 --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/util/LoggingUtil.java @@ -0,0 +1,361 @@ +package de.diddiz.LogBlock.util; + +import de.diddiz.LogBlock.Actor; +import de.diddiz.LogBlock.Consumer; +import de.diddiz.LogBlock.Logging; +import de.diddiz.LogBlock.config.WorldConfig; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.BlockState; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.Directional; +import org.bukkit.block.data.type.Bell; +import org.bukkit.block.data.type.Bell.Attachment; +import org.bukkit.block.data.type.Lantern; +import org.bukkit.block.data.type.PointedDripstone; +import org.bukkit.block.data.type.PointedDripstone.Thickness; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Projectile; +import org.bukkit.entity.TNTPrimed; +import org.bukkit.projectiles.ProjectileSource; +import java.util.List; + +import static de.diddiz.LogBlock.config.Config.getWorldConfig; +import static de.diddiz.LogBlock.config.Config.mb4; + +public class LoggingUtil { + + public static void smartLogBlockPlace(Consumer consumer, Actor actor, BlockState replaced, BlockState placed) { + Location loc = replaced.getLocation(); + Material placedType = placed.getType(); + if (!placedType.hasGravity() || !BukkitUtils.canDirectlyFallIn(replaced.getBlock().getRelative(BlockFace.DOWN).getType())) { + if (placedType == Material.TWISTING_VINES) { + Block below = placed.getBlock().getRelative(BlockFace.DOWN); + if (below.getType() == Material.TWISTING_VINES) { + consumer.queueBlockReplace(actor, below.getState(), Material.TWISTING_VINES_PLANT.createBlockData()); + } + } + if (placedType == Material.WEEPING_VINES) { + Block above = placed.getBlock().getRelative(BlockFace.UP); + if (above.getType() == Material.WEEPING_VINES) { + consumer.queueBlockReplace(actor, above.getState(), Material.WEEPING_VINES_PLANT.createBlockData()); + } + } + if (BukkitUtils.isEmpty(replaced.getType())) { + consumer.queueBlockPlace(actor, placed); + } else { + consumer.queueBlockReplace(actor, replaced, placed); + } + return; + } + int x = loc.getBlockX(); + int initialy = loc.getBlockY(); + int y = initialy; + int z = loc.getBlockZ(); + while (y > loc.getWorld().getMinHeight() && BukkitUtils.canFallIn(loc.getWorld(), x, (y - 1), z)) { + y--; + } + if (initialy != y && !BukkitUtils.isEmpty(replaced.getType())) { + // this is not the final location but the block got removed (vines etc) + consumer.queueBlockBreak(actor, replaced); + } + // If y is minHeight then the block fell out of the world :( + if (y > loc.getWorld().getMinHeight()) { + // Run this check to avoid false positives + Location finalLoc = new Location(loc.getWorld(), x, y, z); + if (y == initialy || !BukkitUtils.isFallingEntityKiller(finalLoc.getBlock().getType())) { + if (BukkitUtils.isEmpty(finalLoc.getBlock().getType())) { + consumer.queueBlockPlace(actor, finalLoc, placed.getBlockData()); + } else { + consumer.queueBlockReplace(actor, finalLoc.getBlock().getState(), placed.getBlockData()); + } + } + } + } + + public static void smartLogFallables(Consumer consumer, Actor actor, Block origin) { + + WorldConfig wcfg = getWorldConfig(origin.getWorld()); + if (wcfg == null) { + return; + } + + //Handle falling blocks + Block checkBlock = origin.getRelative(BlockFace.UP); + int up = 0; + final int highestBlock = checkBlock.getWorld().getHighestBlockYAt(checkBlock.getLocation()); + while (checkBlock.getType().hasGravity()) { + + // Record this block as falling + consumer.queueBlockBreak(actor, checkBlock.getState()); + + // Guess where the block is going (This could be thrown of by explosions, but it is better than nothing) + Location loc = origin.getLocation(); + int x = loc.getBlockX(); + int y = loc.getBlockY(); + int z = loc.getBlockZ(); + while (y > loc.getWorld().getMinHeight() && BukkitUtils.canFallIn(loc.getWorld(), x, (y - 1), z)) { + y--; + } + // If y is minHeight then the sand block fell out of the world :( + if (y > loc.getWorld().getMinHeight()) { + Location finalLoc = new Location(loc.getWorld(), x, y, z); + // Run this check to avoid false positives + if (!BukkitUtils.isFallingEntityKiller(finalLoc.getBlock().getType())) { + finalLoc.add(0, up, 0); // Add this here after checking for block breakers + if (BukkitUtils.isEmpty(finalLoc.getBlock().getType())) { + consumer.queueBlockPlace(actor, finalLoc, checkBlock.getBlockData()); + } else { + consumer.queueBlockReplace(actor, finalLoc, finalLoc.getBlock().getBlockData(), checkBlock.getBlockData()); + } + up++; + } + } + if (checkBlock.getY() >= highestBlock) { + break; + } + checkBlock = checkBlock.getRelative(BlockFace.UP); + } + if (wcfg.isLogging(Logging.SCAFFOLDING) && checkBlock.getType() == Material.SCAFFOLDING && consumer.getLogblock().getScaffoldingLogging() != null) { + consumer.getLogblock().getScaffoldingLogging().addScaffoldingBreaker(actor, checkBlock); + } + } + + public static void smartLogBlockBreak(Consumer consumer, Actor actor, Block origin) { + smartLogBlockReplace(consumer, actor, origin, null); + } + + public static void smartLogBlockReplace(Consumer consumer, Actor actor, Block origin, BlockData replacedWith) { + + WorldConfig wcfg = getWorldConfig(origin.getWorld()); + if (wcfg == null) { + return; + } + Material replacedType = origin.getType(); + if (replacedType == Material.TWISTING_VINES || replacedType == Material.TWISTING_VINES_PLANT) { + Block below = origin.getRelative(BlockFace.DOWN); + if (below.getType() == Material.TWISTING_VINES_PLANT) { + consumer.queueBlockReplace(actor, below.getState(), Material.TWISTING_VINES.createBlockData()); + } + } + if (replacedType == Material.WEEPING_VINES || replacedType == Material.WEEPING_VINES_PLANT) { + Block above = origin.getRelative(BlockFace.UP); + if (above.getType() == Material.WEEPING_VINES_PLANT) { + consumer.queueBlockReplace(actor, above.getState(), Material.WEEPING_VINES.createBlockData()); + } + } + if (replacedType == Material.CAVE_VINES || replacedType == Material.CAVE_VINES_PLANT) { + Block above = origin.getRelative(BlockFace.UP); + if (above.getType() == Material.CAVE_VINES_PLANT) { + consumer.queueBlockReplace(actor, above.getState(), Material.CAVE_VINES.createBlockData()); + } + } + + Block checkBlock = origin.getRelative(BlockFace.UP); + Material typeAbove = checkBlock.getType(); + if (BukkitUtils.isRelativeTopBreakable(typeAbove)) { + if (typeAbove == Material.IRON_DOOR || BukkitUtils.isWoodenDoor(typeAbove)) { + Block doorBlock = checkBlock; + // If the doorBlock is the top half a door the player simply punched a door + // this will be handled later. + if (!BukkitUtils.isTop(doorBlock.getBlockData())) { + doorBlock = doorBlock.getRelative(BlockFace.UP); + // Fall back check just in case the top half wasn't a door + if (doorBlock.getType() == Material.IRON_DOOR || BukkitUtils.isWoodenDoor(doorBlock.getType())) { + consumer.queueBlockBreak(actor, doorBlock.getState()); + } + consumer.queueBlockBreak(actor, checkBlock.getState()); + } + } else if (BukkitUtils.isDoublePlant(typeAbove)) { + Block plantBlock = checkBlock; + // If the plantBlock is the top half of a double plant the player simply + // punched the plant this will be handled later. + if (!BukkitUtils.isTop(plantBlock.getBlockData())) { + plantBlock = plantBlock.getRelative(BlockFace.UP); + // Fall back check just in case the top half wasn't a plant + if (BukkitUtils.isDoublePlant(plantBlock.getType())) { + consumer.queueBlockBreak(actor, plantBlock.getState()); + } + consumer.queueBlockBreak(actor, checkBlock.getState()); + } + } else { + consumer.queueBlockBreak(actor, checkBlock.getState()); + // check next blocks above + checkBlock = checkBlock.getRelative(BlockFace.UP); + typeAbove = checkBlock.getType(); + while (BukkitUtils.isRelativeTopBreakable(typeAbove)) { + consumer.queueBlockBreak(actor, checkBlock.getState()); + checkBlock = checkBlock.getRelative(BlockFace.UP); + typeAbove = checkBlock.getType(); + } + } + } else if (typeAbove == Material.LANTERN) { + Lantern lantern = (Lantern) checkBlock.getBlockData(); + if (!lantern.isHanging()) { + consumer.queueBlockBreak(actor, checkBlock.getState()); + } + } else if (typeAbove == Material.BELL) { + Bell bell = (Bell) checkBlock.getBlockData(); + if (bell.getAttachment() == Attachment.FLOOR) { + consumer.queueBlockBreak(actor, checkBlock.getState()); + } + } else if (typeAbove == Material.POINTED_DRIPSTONE) { + Block dripStoneBlock = checkBlock; + while (true) { + if (dripStoneBlock.getType() != Material.POINTED_DRIPSTONE) { + break; + } + PointedDripstone dripstone = (PointedDripstone) dripStoneBlock.getBlockData(); + if (dripstone.getVerticalDirection() != BlockFace.UP) { + if (dripstone.getThickness() == Thickness.TIP_MERGE) { + PointedDripstone newDripstone = (PointedDripstone) dripstone.clone(); + newDripstone.setThickness(Thickness.TIP); + consumer.queueBlockReplace(actor, dripStoneBlock.getState(), newDripstone); + } + break; + } + consumer.queueBlockBreak(actor, dripStoneBlock.getState()); + dripStoneBlock = dripStoneBlock.getRelative(BlockFace.UP); + } + } + + checkBlock = origin.getRelative(BlockFace.DOWN); + Material typeBelow = checkBlock.getType(); + if (typeBelow == Material.LANTERN) { + Lantern lantern = (Lantern) checkBlock.getBlockData(); + if (lantern.isHanging()) { + consumer.queueBlockBreak(actor, checkBlock.getState()); + } + } else if (BukkitUtils.isHangingSign(typeBelow)) { + consumer.queueBlockBreak(actor, checkBlock.getState()); + } else if (typeBelow == Material.BELL) { + Bell bell = (Bell) checkBlock.getBlockData(); + if (bell.getAttachment() == Attachment.CEILING) { + consumer.queueBlockBreak(actor, checkBlock.getState()); + } + } else if (typeBelow == Material.WEEPING_VINES || typeBelow == Material.WEEPING_VINES_PLANT || typeBelow == Material.CAVE_VINES || typeBelow == Material.CAVE_VINES_PLANT) { + consumer.queueBlockBreak(actor, checkBlock.getState()); + // check next blocks below + checkBlock = checkBlock.getRelative(BlockFace.DOWN); + typeBelow = checkBlock.getType(); + while (typeBelow == Material.WEEPING_VINES || typeBelow == Material.WEEPING_VINES_PLANT || typeBelow == Material.CAVE_VINES || typeBelow == Material.CAVE_VINES_PLANT) { + consumer.queueBlockBreak(actor, checkBlock.getState()); + checkBlock = checkBlock.getRelative(BlockFace.DOWN); + typeBelow = checkBlock.getType(); + } + } else if ((replacedType == Material.BIG_DRIPLEAF || replacedType == Material.BIG_DRIPLEAF_STEM) && (typeBelow == Material.BIG_DRIPLEAF || typeBelow == Material.BIG_DRIPLEAF_STEM)) { + consumer.queueBlockBreak(actor, checkBlock.getState()); + // check next blocks below + checkBlock = checkBlock.getRelative(BlockFace.DOWN); + typeBelow = checkBlock.getType(); + while (typeBelow == Material.BIG_DRIPLEAF || typeBelow == Material.BIG_DRIPLEAF_STEM) { + consumer.queueBlockBreak(actor, checkBlock.getState()); + checkBlock = checkBlock.getRelative(BlockFace.DOWN); + typeBelow = checkBlock.getType(); + } + } else if (typeBelow == Material.POINTED_DRIPSTONE) { + Block dripStoneBlock = checkBlock; + while (true) { + if (dripStoneBlock.getType() != Material.POINTED_DRIPSTONE) { + break; + } + PointedDripstone dripstone = (PointedDripstone) dripStoneBlock.getBlockData(); + if (dripstone.getVerticalDirection() != BlockFace.DOWN) { + if (dripstone.getThickness() == Thickness.TIP_MERGE) { + PointedDripstone newDripstone = (PointedDripstone) dripstone.clone(); + newDripstone.setThickness(Thickness.TIP); + consumer.queueBlockReplace(actor, dripStoneBlock.getState(), newDripstone); + } + break; + } + consumer.queueBlockBreak(actor, dripStoneBlock.getState()); + dripStoneBlock = dripStoneBlock.getRelative(BlockFace.DOWN); + } + } + + List relativeBreakables = BukkitUtils.getBlocksNearby(origin, BukkitUtils.getRelativeBreakables()); + if (!relativeBreakables.isEmpty()) { + for (Location location : relativeBreakables) { + Block block = location.getBlock(); + BlockData blockData = block.getBlockData(); + if (blockData instanceof Directional) { + if (blockData.getMaterial() == Material.BELL) { + if (((Bell) blockData).getAttachment() == Attachment.SINGLE_WALL) { + if (block.getRelative(((Bell) blockData).getFacing()).equals(origin)) { + consumer.queueBlockBreak(actor, block.getState()); + } + } + } else { + if (block.getRelative(((Directional) blockData).getFacing().getOppositeFace()).equals(origin)) { + consumer.queueBlockBreak(actor, block.getState()); + } + } + } + } + } + + // Special door check + if (replacedType == Material.IRON_DOOR || BukkitUtils.isWoodenDoor(replacedType)) { + Block doorBlock = origin; + + // Up or down? + if (!BukkitUtils.isTop(doorBlock.getBlockData())) { + doorBlock = doorBlock.getRelative(BlockFace.UP); + } else { + doorBlock = doorBlock.getRelative(BlockFace.DOWN); + } + + if (doorBlock.getType() == Material.IRON_DOOR || BukkitUtils.isWoodenDoor(doorBlock.getType())) { + consumer.queueBlockBreak(actor, doorBlock.getState()); + } + } else if (BukkitUtils.isDoublePlant(replacedType)) { // Special double plant check + Block plantBlock = origin; + + // Up or down? + if (!BukkitUtils.isTop(origin.getBlockData())) { + plantBlock = plantBlock.getRelative(BlockFace.UP); + } else { + plantBlock = plantBlock.getRelative(BlockFace.DOWN); + } + + if (BukkitUtils.isDoublePlant(plantBlock.getType())) { + consumer.queueBlockBreak(actor, plantBlock.getState()); + } + } + + // Do this down here so that the block is added after blocks sitting on it + if (replacedWith == null) { + consumer.queueBlockBreak(actor, origin.getState()); + } else { + consumer.queueBlockReplace(actor, origin.getState(), replacedWith); + } + } + + public static String checkText(String text) { + if (text == null) { + return text; + } + if (mb4) { + return text; + } + return text.replaceAll("[^\\u0000-\\uFFFF]", "?"); + } + + public static Entity getRealDamager(Entity damager) { + if (damager instanceof Projectile) { + ProjectileSource realDamager = ((Projectile) damager).getShooter(); + if (realDamager instanceof Entity) { + damager = (Entity) realDamager; + } + } + if (damager instanceof TNTPrimed) { + Entity realRemover = ((TNTPrimed) damager).getSource(); + if (realRemover != null) { + damager = realRemover; + } + } + return damager; + } +} diff --git a/src/main/java/de/diddiz/LogBlock/util/MessagingUtil.java b/src/main/java/de/diddiz/LogBlock/util/MessagingUtil.java new file mode 100644 index 00000000..cbef43bb --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/util/MessagingUtil.java @@ -0,0 +1,136 @@ +package de.diddiz.LogBlock.util; + +import static de.diddiz.LogBlock.util.ActionColor.CREATE; +import static de.diddiz.LogBlock.util.ActionColor.DESTROY; +import static de.diddiz.LogBlock.util.TypeColor.DEFAULT; +import static de.diddiz.LogBlock.util.Utils.spaces; + +import de.diddiz.LogBlock.config.Config; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.ClickEvent; +import net.md_5.bungee.api.chat.HoverEvent; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.chat.hover.content.Text; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.data.BlockData; +import org.bukkit.entity.EntityType; + +public class MessagingUtil { + public static BaseComponent formatSummarizedChanges(int created, int destroyed, BaseComponent actor, int createdWidth, int destroyedWidth, float spaceFactor) { + TextComponent textCreated = createTextComponentWithColor(created + spaces((int) ((10 - String.valueOf(created).length()) / spaceFactor)), CREATE.getColor()); + TextComponent textDestroyed = createTextComponentWithColor(destroyed + spaces((int) ((10 - String.valueOf(destroyed).length()) / spaceFactor)), DESTROY.getColor()); + TextComponent result = new TextComponent(); + result.addExtra(textCreated); + result.addExtra(textDestroyed); + result.addExtra(actor); + return result; + } + + public static TextComponent createTextComponentWithColor(String text, ChatColor color) { + TextComponent tc = new TextComponent(text); + tc.setColor(color); + return tc; + } + + public static TextComponent brackets(BracketType type, BaseComponent... content) { + TextComponent tc = createTextComponentWithColor(type.getStarting(), TypeColor.BRACKETS.getColor()); + for (BaseComponent c : content) { + tc.addExtra(c); + } + tc.addExtra(new TextComponent(type.getEnding())); + return tc; + } + + public static TextComponent prettyDate(long date) { + TextComponent tc = brackets(BracketType.STANDARD, createTextComponentWithColor(Config.formatterShort.format(date), TypeColor.DATE.getColor())); + tc.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(Config.formatter.format(date)))); + return tc; + } + + public static TextComponent prettyState(String stateName) { + return createTextComponentWithColor(stateName, TypeColor.STATE.getColor()); + } + + public static TextComponent prettyState(BaseComponent stateName) { + TextComponent tc = new TextComponent(); + tc.setColor(TypeColor.STATE.getColor()); + if (stateName != null) { + tc.addExtra(stateName); + } + return tc; + } + + public static TextComponent prettyState(int stateValue) { + return prettyState(Integer.toString(stateValue)); + } + + public static > TextComponent prettyState(E enumerator) { + return prettyState(enumerator.toString()); + } + + public static TextComponent prettyMaterial(String materialName) { + return createTextComponentWithColor(materialName.toUpperCase(), TypeColor.MATERIAL.getColor()); + } + + public static TextComponent prettyMaterial(Material material) { + return prettyMaterial(material.name()); + } + + public static TextComponent prettyMaterial(BlockData material) { + TextComponent tc = prettyMaterial(material.getMaterial()); + String bdString = material.getAsString(); + int bracket = bdString.indexOf("["); + if (bracket >= 0) { + int bracket2 = bdString.indexOf("]", bracket); + if (bracket2 >= 0) { + String state = bdString.substring(bracket + 1, bracket2).replace(',', '\n'); + tc.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(state))); + } + } + return tc; + } + + public static TextComponent prettyEntityType(EntityType type) { + return prettyMaterial(type.name()); + } + + public static TextComponent prettyLocation(Location loc, int entryId) { + return prettyLocation(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ(), entryId); + } + + public static TextComponent prettyLocation(int x, int y, int z, int entryId) { + TextComponent tc = createTextComponentWithColor("", DEFAULT.getColor()); + tc.addExtra(createTextComponentWithColor(Integer.toString(x), TypeColor.COORDINATE.getColor())); + tc.addExtra(createTextComponentWithColor(", ", DEFAULT.getColor())); + tc.addExtra(createTextComponentWithColor(Integer.toString(y), TypeColor.COORDINATE.getColor())); + tc.addExtra(createTextComponentWithColor(", ", DEFAULT.getColor())); + tc.addExtra(createTextComponentWithColor(Integer.toString(z), TypeColor.COORDINATE.getColor())); + if (entryId > 0) { + tc.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/lb tp " + entryId)); + tc.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("Teleport here"))); + } + return tc; + } + + public enum BracketType { + STANDARD("[", "]"), + ANGLE("<", ">"); + + private String starting, ending; + + BracketType(String starting, String ending) { + this.starting = starting; + this.ending = ending; + } + + public String getStarting() { + return starting; + } + + public String getEnding() { + return ending; + } + } +} diff --git a/src/main/java/de/diddiz/util/MySQLConnectionPool.java b/src/main/java/de/diddiz/LogBlock/util/MySQLConnectionPool.java similarity index 71% rename from src/main/java/de/diddiz/util/MySQLConnectionPool.java rename to src/main/java/de/diddiz/LogBlock/util/MySQLConnectionPool.java index 0f7c976f..d2b8337a 100644 --- a/src/main/java/de/diddiz/util/MySQLConnectionPool.java +++ b/src/main/java/de/diddiz/LogBlock/util/MySQLConnectionPool.java @@ -1,45 +1,51 @@ -package de.diddiz.util; - -import com.zaxxer.hikari.HikariDataSource; -import de.diddiz.LogBlock.config.Config; - -import java.io.Closeable; -import java.sql.Connection; -import java.sql.SQLException; - -public class MySQLConnectionPool implements Closeable { - - private final HikariDataSource ds; - - public MySQLConnectionPool(String url, String user, String password) throws ClassNotFoundException { - this.ds = new HikariDataSource(); - ds.setJdbcUrl(url); - ds.setUsername(user); - ds.setPassword(password); - - ds.setMinimumIdle(2); - ds.setPoolName("LogBlock-Connection-Pool"); - - ds.addDataSourceProperty("useUnicode", "true"); - ds.addDataSourceProperty("characterEncoding", "utf-8"); - ds.addDataSourceProperty("rewriteBatchedStatements", "true"); - - ds.addDataSourceProperty("cachePrepStmts", "true"); - ds.addDataSourceProperty("prepStmtCacheSize", "250"); - ds.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); - } - - @Override - public void close() { - ds.close(); - } - - public Connection getConnection() throws SQLException { - Connection connection = ds.getConnection(); - if (Config.mb4) { - connection.createStatement().executeQuery("SET NAMES utf8mb4"); - } - return connection; - } - -} +package de.diddiz.LogBlock.util; + +import com.zaxxer.hikari.HikariDataSource; +import de.diddiz.LogBlock.config.Config; + +import java.io.Closeable; +import java.sql.Connection; +import java.sql.SQLException; + +public class MySQLConnectionPool implements Closeable { + + private final HikariDataSource ds; + + public MySQLConnectionPool(String url, String user, String password, boolean useSSL, boolean requireSSL) { + this.ds = new HikariDataSource(); + ds.setJdbcUrl(url); + ds.setUsername(user); + ds.setPassword(password); + + ds.setMinimumIdle(2); + ds.setMaximumPoolSize(15); + ds.setPoolName("LogBlock-Connection-Pool"); + + ds.addDataSourceProperty("useUnicode", "true"); + ds.addDataSourceProperty("characterEncoding", "utf-8"); + ds.addDataSourceProperty("rewriteBatchedStatements", "true"); + + ds.addDataSourceProperty("cachePrepStmts", "true"); + ds.addDataSourceProperty("prepStmtCacheSize", "250"); + ds.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); + ds.addDataSourceProperty("useServerPrepStmts", "true"); + + ds.addDataSourceProperty("useSSL", Boolean.toString(useSSL)); + ds.addDataSourceProperty("requireSSL", Boolean.toString(requireSSL)); + ds.addDataSourceProperty("verifyServerCertificate", "false"); + } + + @Override + public void close() { + ds.close(); + } + + public Connection getConnection() throws SQLException { + Connection connection = ds.getConnection(); + if (Config.mb4) { + connection.createStatement().executeUpdate("SET NAMES utf8mb4"); + } + return connection; + } + +} diff --git a/src/main/java/de/diddiz/LogBlock/util/ReflectionUtil.java b/src/main/java/de/diddiz/LogBlock/util/ReflectionUtil.java new file mode 100644 index 00000000..5a797708 --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/util/ReflectionUtil.java @@ -0,0 +1,27 @@ +package de.diddiz.LogBlock.util; + +import org.bukkit.Bukkit; + +public class ReflectionUtil { + + private static String versionString; + + public static String getVersion() { + if (versionString == null) { + String name = Bukkit.getServer().getClass().getPackage().getName(); + versionString = name.substring(name.lastIndexOf('.') + 1); + } + + return versionString; + } + + public static Class getMinecraftClass(String minecraftClassName) throws ClassNotFoundException { + String clazzName = "net.minecraft." + minecraftClassName; + return Class.forName(clazzName); + } + + public static Class getCraftBukkitClass(String craftBukkitClassName) throws ClassNotFoundException { + String clazzName = "org.bukkit.craftbukkit." + getVersion() + "." + craftBukkitClassName; + return Class.forName(clazzName); + } +} diff --git a/src/main/java/de/diddiz/LogBlock/util/SqlUtil.java b/src/main/java/de/diddiz/LogBlock/util/SqlUtil.java new file mode 100644 index 00000000..a21c2c40 --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/util/SqlUtil.java @@ -0,0 +1,24 @@ +package de.diddiz.LogBlock.util; + +public class SqlUtil { + public static String escapeString(String s) { + return escapeString(s, false); + } + + public static String escapeString(String s, boolean escapeMatcher) { + s = s.replace("\u0000", "\\0"); + s = s.replace("\u0026", "\\Z"); + s = s.replace("\\", "\\\\"); + s = s.replace("'", "\\'"); + s = s.replace("\"", "\\\""); + s = s.replace("\b", "\\b"); + s = s.replace("\n", "\\n"); + s = s.replace("\r", "\\r"); + s = s.replace("\t", "\\t"); + if (escapeMatcher) { + s = s.replace("%", "\\%"); + s = s.replace("_", "\\_"); + } + return s; + } +} diff --git a/src/main/java/de/diddiz/LogBlock/util/TypeColor.java b/src/main/java/de/diddiz/LogBlock/util/TypeColor.java new file mode 100644 index 00000000..db62b567 --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/util/TypeColor.java @@ -0,0 +1,29 @@ +package de.diddiz.LogBlock.util; + +import net.md_5.bungee.api.ChatColor; + +public enum TypeColor { + DEFAULT(ChatColor.YELLOW), + MATERIAL(ChatColor.BLUE), + STATE(ChatColor.BLUE), + DATE(ChatColor.DARK_AQUA), + BRACKETS(ChatColor.DARK_GRAY), + COORDINATE(ChatColor.WHITE), + HEADER(ChatColor.GOLD), + ERROR(ChatColor.RED); + + private final ChatColor color; + + TypeColor(ChatColor color) { + this.color = color; + } + + public ChatColor getColor() { + return color; + } + + @Override + public String toString() { + return color.toString(); + } +} diff --git a/src/main/java/de/diddiz/util/UUIDFetcher.java b/src/main/java/de/diddiz/LogBlock/util/UUIDFetcher.java similarity index 50% rename from src/main/java/de/diddiz/util/UUIDFetcher.java rename to src/main/java/de/diddiz/LogBlock/util/UUIDFetcher.java index 2b4d66a8..6decf6c3 100644 --- a/src/main/java/de/diddiz/util/UUIDFetcher.java +++ b/src/main/java/de/diddiz/LogBlock/util/UUIDFetcher.java @@ -1,79 +1,62 @@ -package de.diddiz.util; - -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; -import org.json.simple.parser.JSONParser; - -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.ByteBuffer; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -// Adapted from https://gist.github.com/evilmidget38/26d70114b834f71fb3b4 - -public class UUIDFetcher { - - private static final String PROFILE_URL = "https://api.mojang.com/profiles/minecraft"; - private static final JSONParser jsonParser = new JSONParser(); - - public static Map getUUIDs(List names) throws Exception { - Map uuidMap = new HashMap(); - HttpURLConnection connection = createConnection(); - String body = JSONArray.toJSONString(names); - writeBody(connection, body); - JSONArray array = (JSONArray) jsonParser.parse(new InputStreamReader(connection.getInputStream())); - for (Object profile : array) { - JSONObject jsonProfile = (JSONObject) profile; - String id = (String) jsonProfile.get("id"); - String name = (String) jsonProfile.get("name"); - UUID uuid = UUIDFetcher.getUUID(id); - uuidMap.put(name, uuid); - } - return uuidMap; - } - - private static void writeBody(HttpURLConnection connection, String body) throws Exception { - OutputStream stream = connection.getOutputStream(); - stream.write(body.getBytes()); - stream.flush(); - stream.close(); - } - - private static HttpURLConnection createConnection() throws Exception { - URL url = new URL(PROFILE_URL); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod("POST"); - connection.setRequestProperty("Content-Type", "application/json"); - connection.setUseCaches(false); - connection.setDoInput(true); - connection.setDoOutput(true); - return connection; - } - - private static UUID getUUID(String id) { - return UUID.fromString(id.substring(0, 8) + "-" + id.substring(8, 12) + "-" + id.substring(12, 16) + "-" + id.substring(16, 20) + "-" + id.substring(20, 32)); - } - - public static byte[] toBytes(UUID uuid) { - ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[16]); - byteBuffer.putLong(uuid.getMostSignificantBits()); - byteBuffer.putLong(uuid.getLeastSignificantBits()); - return byteBuffer.array(); - } - - public static UUID fromBytes(byte[] array) { - if (array.length != 16) { - throw new IllegalArgumentException("Illegal byte array length: " + array.length); - } - ByteBuffer byteBuffer = ByteBuffer.wrap(array); - long mostSignificant = byteBuffer.getLong(); - long leastSignificant = byteBuffer.getLong(); - return new UUID(mostSignificant, leastSignificant); - } - -} +package de.diddiz.LogBlock.util; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +// Adapted from https://gist.github.com/evilmidget38/26d70114b834f71fb3b4 + +public class UUIDFetcher { + + private static final String PROFILE_URL = "https://api.mojang.com/profiles/minecraft"; + private static final Gson gson = new GsonBuilder().setLenient().create(); + + public static Map getUUIDs(List names) throws Exception { + Map uuidMap = new HashMap<>(); + HttpURLConnection connection = createConnection(); + String body = gson.toJson(names); + writeBody(connection, body); + JsonArray array = gson.fromJson(new InputStreamReader(connection.getInputStream()), JsonArray.class); + for (JsonElement profile : array) { + JsonObject jsonProfile = (JsonObject) profile; + String id = jsonProfile.get("id").getAsString(); + String name = jsonProfile.get("name").getAsString(); + UUID uuid = getUUID(id); + uuidMap.put(name, uuid); + } + return uuidMap; + } + + private static void writeBody(HttpURLConnection connection, String body) throws Exception { + OutputStream stream = connection.getOutputStream(); + stream.write(body.getBytes()); + stream.flush(); + stream.close(); + } + + private static HttpURLConnection createConnection() throws Exception { + URL url = new URI(PROFILE_URL).toURL(); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setUseCaches(false); + connection.setDoInput(true); + connection.setDoOutput(true); + return connection; + } + + private static UUID getUUID(String id) { + return UUID.fromString(id.substring(0, 8) + "-" + id.substring(8, 12) + "-" + id.substring(12, 16) + "-" + id.substring(16, 20) + "-" + id.substring(20, 32)); + } +} diff --git a/src/main/java/de/diddiz/util/Utils.java b/src/main/java/de/diddiz/LogBlock/util/Utils.java similarity index 61% rename from src/main/java/de/diddiz/util/Utils.java rename to src/main/java/de/diddiz/LogBlock/util/Utils.java index 12349af0..dd4296d2 100644 --- a/src/main/java/de/diddiz/util/Utils.java +++ b/src/main/java/de/diddiz/LogBlock/util/Utils.java @@ -1,199 +1,313 @@ -package de.diddiz.util; - -import java.io.File; -import java.io.FilenameFilter; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class Utils { - public static String newline = System.getProperty("line.separator"); - - public static boolean isInt(String str) { - try { - Integer.parseInt(str); - return true; - } catch (final NumberFormatException ex) { - } - return false; - } - - public static boolean isShort(String str) { - try { - Short.parseShort(str); - return true; - } catch (final NumberFormatException ex) { - } - return false; - } - - public static boolean isByte(String str) { - try { - Byte.parseByte(str); - return true; - } catch (final NumberFormatException ex) { - } - return false; - } - - public static String listing(String[] entries, String delimiter, String finalDelimiter) { - final int len = entries.length; - if (len == 0) { - return ""; - } - if (len == 1) { - return entries[0]; - } - final StringBuilder builder = new StringBuilder(entries[0]); - for (int i = 1; i < len - 1; i++) { - builder.append(delimiter).append(entries[i]); - } - builder.append(finalDelimiter).append(entries[len - 1]); - return builder.toString(); - } - - public static String listing(List entries, String delimiter, String finalDelimiter) { - final int len = entries.size(); - if (len == 0) { - return ""; - } - if (len == 1) { - return entries.get(0).toString(); - } - final StringBuilder builder = new StringBuilder(entries.get(0).toString()); - for (int i = 1; i < len - 1; i++) { - builder.append(delimiter).append(entries.get(i).toString()); - } - builder.append(finalDelimiter).append(entries.get(len - 1).toString()); - return builder.toString(); - } - - public static int parseTimeSpec(String[] spec) { - if (spec == null || spec.length < 1 || spec.length > 2) { - return -1; - } - if (spec.length == 1 && isInt(spec[0])) { - return Integer.valueOf(spec[0]); - } - if (!spec[0].contains(":") && !spec[0].contains(".")) { - if (spec.length == 2) { - if (!isInt(spec[0])) { - return -1; - } - int min = Integer.parseInt(spec[0]); - if (spec[1].startsWith("h")) { - min *= 60; - } else if (spec[1].startsWith("d")) { - min *= 1440; - } - return min; - } else if (spec.length == 1) { - int days = 0, hours = 0, minutes = 0; - int lastIndex = 0, currIndex = 1; - while (currIndex <= spec[0].length()) { - while (currIndex <= spec[0].length() && isInt(spec[0].substring(lastIndex, currIndex))) { - currIndex++; - } - if (currIndex - 1 != lastIndex) { - if (currIndex > spec[0].length()) { - return -1; - } - final String param = spec[0].substring(currIndex - 1, currIndex).toLowerCase(); - if (param.equals("d")) { - days = Integer.parseInt(spec[0].substring(lastIndex, currIndex - 1)); - } else if (param.equals("h")) { - hours = Integer.parseInt(spec[0].substring(lastIndex, currIndex - 1)); - } else if (param.equals("m")) { - minutes = Integer.parseInt(spec[0].substring(lastIndex, currIndex - 1)); - } - } - lastIndex = currIndex; - currIndex++; - } - if (days == 0 && hours == 0 && minutes == 0) { - return -1; - } - return minutes + hours * 60 + days * 1440; - } else { - return -1; - } - } - final String timestamp; - if (spec.length == 1) { - if (spec[0].contains(":")) { - timestamp = new SimpleDateFormat("dd.MM.yyyy").format(System.currentTimeMillis()) + " " + spec[0]; - } else { - timestamp = spec[0] + " 00:00:00"; - } - } else { - timestamp = spec[0] + " " + spec[1]; - } - try { - return (int) ((System.currentTimeMillis() - new SimpleDateFormat("dd.MM.yyyy HH:mm:ss").parse(timestamp).getTime()) / 60000); - } catch (final ParseException ex) { - return -1; - } - } - - public static String spaces(int count) { - final StringBuilder filled = new StringBuilder(count); - for (int i = 0; i < count; i++) { - filled.append(' '); - } - return filled.toString(); - } - - public static String join(String[] s, String delimiter) { - if (s == null || s.length == 0) { - return ""; - } - final int len = s.length; - final StringBuilder builder = new StringBuilder(s[0]); - for (int i = 1; i < len; i++) { - builder.append(delimiter).append(s[i]); - } - return builder.toString(); - } - - /** - * Converts a list of arguments e.g ['lb', 'clearlog', 'world', '"my', 'world', 'of', 'swag"'] - * into a list of arguments with any text encapsulated by quotes treated as one word - * For this particular example: ['lb', 'clearlog', 'world', '"my world of swag"'] - * - * @param args The list of arguments - * @return A new list with the quoted arguments parsed to single values - */ - public static List parseQuotes(List args) { - List newArguments = new ArrayList(); - String subjectString = join(args.toArray(new String[args.size()]), " "); - - Pattern regex = Pattern.compile("[^\\s\"']+|\"[^\"]*\"|'[^']*'"); - Matcher regexMatcher = regex.matcher(subjectString); - while (regexMatcher.find()) { - newArguments.add(regexMatcher.group()); - } - - return newArguments; - } - - public static class ExtensionFilenameFilter implements FilenameFilter { - private final String ext; - - public ExtensionFilenameFilter(String ext) { - this.ext = ext; - } - - @Override - public boolean accept(File dir, String name) { - return name.toLowerCase().endsWith(ext); - } - } - - public static String mysqlTextEscape(String untrusted) { - return untrusted.replace("\\", "\\\\").replace("'", "\\'"); - } - -} +package de.diddiz.LogBlock.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; +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.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.inventory.ItemStack; + +import de.diddiz.LogBlock.LogBlock; + +public class Utils { + public static String newline = System.getProperty("line.separator"); + + public static boolean isInt(String str) { + try { + Integer.parseInt(str); + return true; + } catch (final NumberFormatException ex) { + } + return false; + } + + public static boolean isShort(String str) { + try { + Short.parseShort(str); + return true; + } catch (final NumberFormatException ex) { + } + return false; + } + + public static boolean isByte(String str) { + try { + Byte.parseByte(str); + return true; + } catch (final NumberFormatException ex) { + } + return false; + } + + public static String listing(String[] entries, String delimiter, String finalDelimiter) { + final int len = entries.length; + if (len == 0) { + return ""; + } + if (len == 1) { + return entries[0]; + } + final StringBuilder builder = new StringBuilder(entries[0]); + for (int i = 1; i < len - 1; i++) { + builder.append(delimiter).append(entries[i]); + } + builder.append(finalDelimiter).append(entries[len - 1]); + return builder.toString(); + } + + public static String listing(List entries, String delimiter, String finalDelimiter) { + final int len = entries.size(); + if (len == 0) { + return ""; + } + if (len == 1) { + return entries.get(0).toString(); + } + final StringBuilder builder = new StringBuilder(entries.get(0).toString()); + for (int i = 1; i < len - 1; i++) { + builder.append(delimiter).append(entries.get(i).toString()); + } + builder.append(finalDelimiter).append(entries.get(len - 1).toString()); + return builder.toString(); + } + + public static int parseTimeSpec(String[] spec) { + if (spec == null || spec.length < 1 || spec.length > 2) { + return -1; + } + if (spec.length == 1 && isInt(spec[0])) { + return Integer.valueOf(spec[0]); + } + if (!spec[0].contains(":") && !spec[0].contains(".")) { + if (spec.length == 2) { + if (!isInt(spec[0])) { + return -1; + } + int min = Integer.parseInt(spec[0]); + if (spec[1].startsWith("h")) { + min *= 60; + } else if (spec[1].startsWith("d")) { + min *= 1440; + } + return min; + } else if (spec.length == 1) { + int days = 0, hours = 0, minutes = 0; + int lastIndex = 0, currIndex = 1; + while (currIndex <= spec[0].length()) { + while (currIndex <= spec[0].length() && isInt(spec[0].substring(lastIndex, currIndex))) { + currIndex++; + } + if (currIndex - 1 != lastIndex) { + if (currIndex > spec[0].length()) { + return -1; + } + final String param = spec[0].substring(currIndex - 1, currIndex).toLowerCase(); + if (param.equals("d")) { + days = Integer.parseInt(spec[0].substring(lastIndex, currIndex - 1)); + } else if (param.equals("h")) { + hours = Integer.parseInt(spec[0].substring(lastIndex, currIndex - 1)); + } else if (param.equals("m")) { + minutes = Integer.parseInt(spec[0].substring(lastIndex, currIndex - 1)); + } + } + lastIndex = currIndex; + currIndex++; + } + if (days == 0 && hours == 0 && minutes == 0) { + return -1; + } + return minutes + hours * 60 + days * 1440; + } else { + return -1; + } + } + final String timestamp; + if (spec.length == 1) { + if (spec[0].contains(":")) { + timestamp = new SimpleDateFormat("dd.MM.yyyy").format(System.currentTimeMillis()) + " " + spec[0]; + } else { + timestamp = spec[0] + " 00:00:00"; + } + } else { + timestamp = spec[0] + " " + spec[1]; + } + try { + return (int) ((System.currentTimeMillis() - new SimpleDateFormat("dd.MM.yyyy HH:mm:ss").parse(timestamp).getTime()) / 60000); + } catch (final ParseException ex) { + return -1; + } + } + + public static String spaces(int count) { + final StringBuilder filled = new StringBuilder(count); + for (int i = 0; i < count; i++) { + filled.append(' '); + } + return filled.toString(); + } + + public static String join(String[] s, String delimiter) { + if (s == null || s.length == 0) { + return ""; + } + final int len = s.length; + final StringBuilder builder = new StringBuilder(s[0]); + for (int i = 1; i < len; i++) { + builder.append(delimiter).append(s[i]); + } + return builder.toString(); + } + + /** + * Converts a list of arguments e.g ['lb', 'clearlog', 'world', '"my', 'world', 'of', 'swag"'] + * into a list of arguments with any text encapsulated by quotes treated as one word + * For this particular example: ['lb', 'clearlog', 'world', '"my world of swag"'] + * + * @param args The list of arguments + * @return A new list with the quoted arguments parsed to single values + */ + public static List parseQuotes(List args) { + List newArguments = new ArrayList<>(); + String subjectString = join(args.toArray(new String[args.size()]), " "); + + Pattern regex = Pattern.compile("[^\\s\"']+|\"[^\"]*\"|'[^']*'"); + Matcher regexMatcher = regex.matcher(subjectString); + while (regexMatcher.find()) { + newArguments.add(regexMatcher.group()); + } + + return newArguments; + } + + public static class ExtensionFilenameFilter implements FilenameFilter { + private final String ext; + + public ExtensionFilenameFilter(String ext) { + this.ext = "." + ext; + } + + @Override + public boolean accept(File dir, String name) { + return name.toLowerCase().endsWith(ext); + } + } + + private final static char[] hexArray = "0123456789ABCDEF".toCharArray(); + + public static String mysqlEscapeBytes(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2 + 2]; + hexChars[0] = '0'; + hexChars[1] = 'x'; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2 + 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 3] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } + + public static String mysqlPrepareBytesForInsertAllowNull(byte[] bytes) { + if (bytes == null) { + return "null"; + } + return "'" + mysqlEscapeBytes(bytes) + "'"; + } + + public static String mysqlTextEscape(String untrusted) { + return untrusted.replace("\\", "\\\\").replace("'", "\\'"); + } + + public static ItemStackAndAmount loadItemStack(byte[] data) { + if (data == null || data.length == 0) { + return null; + } + YamlConfiguration conf = deserializeYamlConfiguration(data); + if (conf == null) { + return null; + } + ItemStack stack = conf.getItemStack("stack"); + if (stack == null) { + return null; + } + int amount = conf.contains("amount") ? conf.getInt("amount") : stack.getAmount(); + stack.setAmount(1); + return new ItemStackAndAmount(stack, amount); + } + + public static byte[] saveItemStack(ItemStackAndAmount stack) { + if (stack == null || stack.stack() == null || BukkitUtils.isEmpty(stack.stack().getType())) { + return null; + } + YamlConfiguration conf = new YamlConfiguration(); + ItemStack itemStack = stack.stack(); + if (itemStack.getAmount() > 1) { + itemStack = itemStack.clone(); + itemStack.setAmount(1); + } + conf.set("stack", itemStack); + conf.set("amount", stack.amount()); + return serializeYamlConfiguration(conf); + } + + public static YamlConfiguration deserializeYamlConfiguration(byte[] data) { + if (data == null || data.length == 0) { + return null; + } + YamlConfiguration conf = new YamlConfiguration(); + try { + InputStreamReader reader = new InputStreamReader(new GZIPInputStream(new ByteArrayInputStream(data)), "UTF-8"); + conf.load(reader); + reader.close(); + return conf; + } catch (ZipException | InvalidConfigurationException e) { + LogBlock.getInstance().getLogger().warning("Could not deserialize YamlConfiguration: " + e.getMessage()); + return conf; + } catch (IOException e) { + throw new RuntimeException("IOException should be impossible for ByteArrayInputStream", e); + } + } + + public static byte[] serializeYamlConfiguration(YamlConfiguration conf) { + if (conf == null || conf.getKeys(false).isEmpty()) { + return null; + } + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + OutputStreamWriter writer = new OutputStreamWriter(new GZIPOutputStream(baos), "UTF-8"); + writer.write(conf.saveToString()); + writer.close(); + return baos.toByteArray(); + } catch (IOException e) { + throw new RuntimeException("IOException should be impossible for ByteArrayOutputStream", e); + } + } + + public static String serializeForSQL(YamlConfiguration conf) { + return mysqlPrepareBytesForInsertAllowNull(serializeYamlConfiguration(conf)); + } + + public static double warpDegrees(double degrees) { + double d = degrees % 360.0; + if (d >= 180.0) { + d -= 360.0; + } + if (d < -180.0) { + d += 360.0; + } + return d; + } +} diff --git a/src/main/java/de/diddiz/LogBlock/worldedit/WorldEditHelper.java b/src/main/java/de/diddiz/LogBlock/worldedit/WorldEditHelper.java new file mode 100644 index 00000000..446c635a --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/worldedit/WorldEditHelper.java @@ -0,0 +1,188 @@ +package de.diddiz.LogBlock.worldedit; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.UUID; +import java.util.logging.Level; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.bukkit.util.BlockVector; +import org.enginehub.linbus.stream.LinBinaryIO; +import org.enginehub.linbus.stream.LinStream; +import org.enginehub.linbus.tree.LinCompoundTag; +import org.enginehub.linbus.tree.LinDoubleTag; +import org.enginehub.linbus.tree.LinIntArrayTag; +import org.enginehub.linbus.tree.LinListTag; +import org.enginehub.linbus.tree.LinLongTag; +import org.enginehub.linbus.tree.LinRootEntry; +import org.enginehub.linbus.tree.LinTagType; +import com.sk89q.worldedit.IncompleteRegionException; +import com.sk89q.worldedit.LocalSession; +import com.sk89q.worldedit.bukkit.BukkitAdapter; +import com.sk89q.worldedit.bukkit.WorldEditPlugin; +import com.sk89q.worldedit.entity.BaseEntity; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.regions.Region; +import com.sk89q.worldedit.util.concurrency.LazyReference; +import de.diddiz.LogBlock.LogBlock; +import de.diddiz.LogBlock.util.CuboidRegion; + +public class WorldEditHelper { + private static boolean checkedForWorldEdit; + private static boolean hasWorldEdit; + + public static boolean hasWorldEdit() { + if (!checkedForWorldEdit) { + checkedForWorldEdit = true; + Plugin worldEdit = Bukkit.getPluginManager().getPlugin("WorldEdit"); + hasWorldEdit = worldEdit != null; + if (worldEdit != null) { + Internal.setWorldEdit(worldEdit); + } + } + return hasWorldEdit; + } + + public static boolean hasFullWorldEdit() { + return hasWorldEdit && Internal.hasBukkitImplAdapter(); + } + + public static byte[] serializeEntity(Entity entity) { + if (!hasWorldEdit()) { + return null; + } + return Internal.serializeEntity(entity); + } + + public static Entity restoreEntity(Location location, EntityType type, byte[] serialized) { + if (!hasWorldEdit()) { + return null; + } + return Internal.restoreEntity(location, type, serialized); + } + + public static CuboidRegion getSelectedRegion(Player player) throws IllegalArgumentException { + if (!hasWorldEdit()) { + throw new IllegalArgumentException("WorldEdit not found!"); + } + return Internal.getSelectedRegion(player); + } + + private static class Internal { + private static WorldEditPlugin worldEdit; + private static Method getBukkitImplAdapter; + + public static void setWorldEdit(Plugin worldEdit) { + Internal.worldEdit = (WorldEditPlugin) worldEdit; + } + + public static boolean hasBukkitImplAdapter() { + if (getBukkitImplAdapter == null) { + try { + getBukkitImplAdapter = WorldEditPlugin.class.getDeclaredMethod("getBukkitImplAdapter"); + getBukkitImplAdapter.setAccessible(true); + } catch (Exception e) { + LogBlock.getInstance().getLogger().log(Level.SEVERE, "Exception while checking for BukkitImplAdapter", e); + return false; + } + } + try { + return getBukkitImplAdapter.invoke(worldEdit) != null; + } catch (Exception e) { + LogBlock.getInstance().getLogger().log(Level.SEVERE, "Exception while checking for BukkitImplAdapter", e); + return false; + } + } + + public static Entity restoreEntity(Location location, EntityType type, byte[] serialized) { + com.sk89q.worldedit.world.entity.EntityType weType = BukkitAdapter.adapt(type); + com.sk89q.worldedit.util.Location weLocation = BukkitAdapter.adapt(location); + try { + LinStream stream = LinBinaryIO.read(new DataInputStream(new ByteArrayInputStream(serialized))); + LinRootEntry namedTag = LinRootEntry.readFrom(stream); + UUID newUUID = null; + if (namedTag.name().equals("entity")) { + LinCompoundTag serializedState = namedTag.value(); + BaseEntity state = new BaseEntity(weType, LazyReference.computed(serializedState)); + com.sk89q.worldedit.entity.Entity weEntity = weLocation.getExtent().createEntity(weLocation, state); + if (weEntity != null) { + LinCompoundTag newNbt = weEntity.getState().getNbt(); + LinIntArrayTag uuidTag = newNbt.findTag("UUID", LinTagType.intArrayTag()); + int[] uuidInts = uuidTag == null ? null : uuidTag.value(); + if (uuidInts != null && uuidInts.length >= 4) { + newUUID = new UUID(((long) uuidInts[0] << 32) | (uuidInts[1] & 0xFFFFFFFFL), ((long) uuidInts[2] << 32) | (uuidInts[3] & 0xFFFFFFFFL)); + } else { + LinLongTag uuidMostTag = newNbt.findTag("UUIDMost", LinTagType.longTag()); + LinLongTag uuidLeastTag = newNbt.findTag("UUIDLeast", LinTagType.longTag()); + if (uuidMostTag != null && uuidLeastTag != null) { + newUUID = new UUID(uuidMostTag.valueAsLong(), uuidLeastTag.valueAsLong()); // pre 1.16 + } + } + } + } + return newUUID == null ? null : Bukkit.getEntity(newUUID); + } catch (IOException e) { + throw new RuntimeException("This IOException should be impossible", e); + } + } + + public static byte[] serializeEntity(Entity entity) { + com.sk89q.worldedit.entity.Entity weEntity = BukkitAdapter.adapt(entity); + BaseEntity state = weEntity.getState(); + if (state != null) { + try { + LinCompoundTag.Builder nbt = state.getNbt().toBuilder(); + nbt.putFloat("Health", 20.0f); + nbt.put("Motion", LinListTag.builder(LinTagType.doubleTag()).add(LinDoubleTag.of(0)).add(LinDoubleTag.of(0)).add(LinDoubleTag.of(0)).build()); + nbt.putShort("Fire", (short) -20); + nbt.putShort("HurtTime", (short) 0); + + LinRootEntry root = new LinRootEntry("entity", nbt.build()); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (DataOutputStream dos = new DataOutputStream(baos)) { + LinBinaryIO.write(dos, root); + } + return baos.toByteArray(); + } catch (IOException e) { + throw new RuntimeException("This IOException should be impossible", e); + } + } + return null; + } + + public static CuboidRegion getSelectedRegion(Player player) throws IllegalArgumentException { + LocalSession session = worldEdit.getSession(player); + World world = player.getWorld(); + com.sk89q.worldedit.world.World weWorld = BukkitAdapter.adapt(world); + if (!weWorld.equals(session.getSelectionWorld())) { + throw new IllegalArgumentException("No selection defined"); + } + Region selection; + try { + selection = session.getSelection(weWorld); + } catch (IncompleteRegionException e) { + throw new IllegalArgumentException("No selection defined"); + } + if (selection == null) { + throw new IllegalArgumentException("No selection defined"); + } + if (!(selection instanceof com.sk89q.worldedit.regions.CuboidRegion)) { + throw new IllegalArgumentException("You have to define a cuboid selection"); + } + BlockVector3 min = selection.getMinimumPoint(); + BlockVector3 max = selection.getMaximumPoint(); + return new CuboidRegion(world, new BlockVector(min.x(), min.y(), min.z()), new BlockVector(max.x(), max.y(), max.z())); + } + } +} diff --git a/src/main/java/de/diddiz/LogBlock/worldedit/WorldEditLoggingHook.java b/src/main/java/de/diddiz/LogBlock/worldedit/WorldEditLoggingHook.java new file mode 100644 index 00000000..613405f6 --- /dev/null +++ b/src/main/java/de/diddiz/LogBlock/worldedit/WorldEditLoggingHook.java @@ -0,0 +1,106 @@ +package de.diddiz.LogBlock.worldedit; + +import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.WorldEditException; +import com.sk89q.worldedit.bukkit.BukkitAdapter; +import com.sk89q.worldedit.event.extent.EditSessionEvent; +import com.sk89q.worldedit.extension.platform.Actor; +import com.sk89q.worldedit.extent.AbstractDelegateExtent; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.util.eventbus.Subscribe; +import com.sk89q.worldedit.world.block.BlockStateHolder; + +import de.diddiz.LogBlock.LogBlock; +import de.diddiz.LogBlock.Logging; +import de.diddiz.LogBlock.blockstate.BlockStateCodecs; +import de.diddiz.LogBlock.config.Config; +import de.diddiz.LogBlock.util.BukkitUtils; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; + +import java.util.logging.Level; + +//...so they ALSO have a class called Actor... need to fully-qualify when we use ours + +public class WorldEditLoggingHook { + + private LogBlock plugin; + + public WorldEditLoggingHook(LogBlock plugin) { + this.plugin = plugin; + } + + // Convert WE Actor to LB Actor + private de.diddiz.LogBlock.Actor AtoA(Actor weActor) { + if (weActor.isPlayer()) { + return new de.diddiz.LogBlock.Actor(weActor.getName(), weActor.getUniqueId()); + } + return new de.diddiz.LogBlock.Actor(weActor.getName()); + } + + public void hook() { + WorldEdit.getInstance().getEventBus().register(new Object() { + @Subscribe + public void wrapForLogging(final EditSessionEvent event) { + final Actor actor = event.getActor(); + if (actor == null) { + return; + } + final de.diddiz.LogBlock.Actor lbActor = AtoA(actor); + + // Check to ensure the world should be logged + final World world; + try { + world = BukkitAdapter.adapt(event.getWorld()); + } catch (RuntimeException ex) { + plugin.getLogger().warning("Failed to register logging for WorldEdit!"); + plugin.getLogger().log(Level.WARNING, ex.getMessage(), ex); + return; + } + + // If config becomes reloadable, this check should be moved + if (!(Config.isLogging(world, Logging.WORLDEDIT))) { + return; + } + + event.setExtent(new AbstractDelegateExtent(event.getExtent()) { + @Override + public final > boolean setBlock(BlockVector3 position, B block) throws WorldEditException { + onBlockChange(position, block); + return super.setBlock(position, block); + } + + protected > void onBlockChange(BlockVector3 pt, B block) { + + if (event.getStage() != EditSession.Stage.BEFORE_CHANGE) { + return; + } + + Location location = BukkitAdapter.adapt(world, pt); + Block blockBefore = location.getBlock(); + BlockData blockDataBefore = blockBefore.getBlockData(); + Material typeBefore = blockDataBefore.getMaterial(); + + BlockData blockDataNew = BukkitAdapter.adapt(block); + + if (!blockDataBefore.equals(blockDataNew)) { + // Check to see if we've broken a sign + if (BlockStateCodecs.hasCodec(typeBefore)) { + plugin.getConsumer().queueBlockBreak(lbActor, blockBefore.getState()); + } else if (!BukkitUtils.isEmpty(typeBefore)) { + plugin.getConsumer().queueBlockBreak(lbActor, location, blockDataBefore); + } + if (!BukkitUtils.isEmpty(blockDataNew.getMaterial())) { + plugin.getConsumer().queueBlockPlace(lbActor, location, blockDataNew); + } + } + } + }); + } + }); + } +} diff --git a/src/main/java/de/diddiz/util/Block.java b/src/main/java/de/diddiz/util/Block.java deleted file mode 100644 index e59fce71..00000000 --- a/src/main/java/de/diddiz/util/Block.java +++ /dev/null @@ -1,34 +0,0 @@ -package de.diddiz.util; - -import java.util.List; - -public class Block { - private int block; - private int data; - - /** - * @param block The id of the block - * @param data The data for the block, -1 for any data - */ - public Block(int block, int data) { - this.block = block; - this.data = data; - } - - public int getBlock() { - return this.block; - } - - public int getData() { - return this.data; - } - - public static boolean inList(List types, int blockID) { - for (Block block : types) { - if (block.getBlock() == blockID) { - return true; - } - } - return false; - } -} diff --git a/src/main/java/de/diddiz/util/BukkitUtils.java b/src/main/java/de/diddiz/util/BukkitUtils.java deleted file mode 100644 index 02f4b730..00000000 --- a/src/main/java/de/diddiz/util/BukkitUtils.java +++ /dev/null @@ -1,451 +0,0 @@ -package de.diddiz.util; - -import org.bukkit.*; -import org.bukkit.block.BlockFace; -import org.bukkit.block.BlockState; -import org.bukkit.block.DoubleChest; -import org.bukkit.entity.Entity; -import org.bukkit.entity.EntityType; -import org.bukkit.entity.Player; -import org.bukkit.entity.TNTPrimed; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.InventoryHolder; -import org.bukkit.inventory.ItemStack; - -import java.io.File; -import java.util.*; - -import static de.diddiz.util.MaterialName.materialName; - -public class BukkitUtils { - private static final Set> blockEquivalents; - private static final Set relativeBreakable; - private static final Set relativeTopBreakable; - private static final Set relativeTopFallables; - private static final Set fallingEntityKillers; - - private static final Set cropBlocks; - private static final Set containerBlocks; - - private static final Map projectileItems; - - static { - blockEquivalents = new HashSet>(7); - blockEquivalents.add(new HashSet(Arrays.asList(2, 3, 60))); - blockEquivalents.add(new HashSet(Arrays.asList(8, 9, 79))); - blockEquivalents.add(new HashSet(Arrays.asList(10, 11))); - blockEquivalents.add(new HashSet(Arrays.asList(61, 62))); - blockEquivalents.add(new HashSet(Arrays.asList(73, 74))); - blockEquivalents.add(new HashSet(Arrays.asList(75, 76))); - blockEquivalents.add(new HashSet(Arrays.asList(93, 94))); - - // Blocks that break when they are attached to a block - relativeBreakable = new HashSet(11); - relativeBreakable.add(Material.WALL_SIGN); - relativeBreakable.add(Material.LADDER); - relativeBreakable.add(Material.STONE_BUTTON); - relativeBreakable.add(Material.WOOD_BUTTON); - relativeBreakable.add(Material.REDSTONE_TORCH_ON); - relativeBreakable.add(Material.REDSTONE_TORCH_OFF); - relativeBreakable.add(Material.LEVER); - relativeBreakable.add(Material.TORCH); - relativeBreakable.add(Material.TRAP_DOOR); - relativeBreakable.add(Material.TRIPWIRE_HOOK); - relativeBreakable.add(Material.COCOA); - - // Blocks that break when they are on top of a block - relativeTopBreakable = new HashSet(33); - relativeTopBreakable.add(Material.SAPLING); - relativeTopBreakable.add(Material.LONG_GRASS); - relativeTopBreakable.add(Material.DEAD_BUSH); - relativeTopBreakable.add(Material.YELLOW_FLOWER); - relativeTopBreakable.add(Material.RED_ROSE); - relativeTopBreakable.add(Material.BROWN_MUSHROOM); - relativeTopBreakable.add(Material.RED_MUSHROOM); - relativeTopBreakable.add(Material.CROPS); - relativeTopBreakable.add(Material.POTATO); - relativeTopBreakable.add(Material.CARROT); - relativeTopBreakable.add(Material.WATER_LILY); - relativeTopBreakable.add(Material.CACTUS); - relativeTopBreakable.add(Material.SUGAR_CANE_BLOCK); - relativeTopBreakable.add(Material.FLOWER_POT); - relativeTopBreakable.add(Material.POWERED_RAIL); - relativeTopBreakable.add(Material.DETECTOR_RAIL); - relativeTopBreakable.add(Material.ACTIVATOR_RAIL); - relativeTopBreakable.add(Material.RAILS); - relativeTopBreakable.add(Material.REDSTONE_WIRE); - relativeTopBreakable.add(Material.SIGN_POST); - relativeTopBreakable.add(Material.STONE_PLATE); - relativeTopBreakable.add(Material.WOOD_PLATE); - relativeTopBreakable.add(Material.IRON_PLATE); - relativeTopBreakable.add(Material.GOLD_PLATE); - relativeTopBreakable.add(Material.SNOW); - relativeTopBreakable.add(Material.DIODE_BLOCK_ON); - relativeTopBreakable.add(Material.DIODE_BLOCK_OFF); - relativeTopBreakable.add(Material.REDSTONE_COMPARATOR_ON); - relativeTopBreakable.add(Material.REDSTONE_COMPARATOR_OFF); - relativeTopBreakable.add(Material.WOODEN_DOOR); - relativeTopBreakable.add(Material.IRON_DOOR_BLOCK); - relativeTopBreakable.add(Material.CARPET); - relativeTopBreakable.add(Material.DOUBLE_PLANT); - - // Blocks that fall - relativeTopFallables = new HashSet(4); - relativeTopFallables.add(Material.SAND); - relativeTopFallables.add(Material.GRAVEL); - relativeTopFallables.add(Material.DRAGON_EGG); - relativeTopFallables.add(Material.ANVIL); - - // Blocks that break falling entities - fallingEntityKillers = new HashSet(32); - fallingEntityKillers.add(Material.SIGN_POST); - fallingEntityKillers.add(Material.WALL_SIGN); - fallingEntityKillers.add(Material.STONE_PLATE); - fallingEntityKillers.add(Material.WOOD_PLATE); - fallingEntityKillers.add(Material.IRON_PLATE); - fallingEntityKillers.add(Material.GOLD_PLATE); - fallingEntityKillers.add(Material.SAPLING); - fallingEntityKillers.add(Material.YELLOW_FLOWER); - fallingEntityKillers.add(Material.RED_ROSE); - fallingEntityKillers.add(Material.CROPS); - fallingEntityKillers.add(Material.CARROT); - fallingEntityKillers.add(Material.POTATO); - fallingEntityKillers.add(Material.RED_MUSHROOM); - fallingEntityKillers.add(Material.BROWN_MUSHROOM); - fallingEntityKillers.add(Material.STEP); - fallingEntityKillers.add(Material.WOOD_STEP); - fallingEntityKillers.add(Material.TORCH); - fallingEntityKillers.add(Material.FLOWER_POT); - fallingEntityKillers.add(Material.POWERED_RAIL); - fallingEntityKillers.add(Material.DETECTOR_RAIL); - fallingEntityKillers.add(Material.ACTIVATOR_RAIL); - fallingEntityKillers.add(Material.RAILS); - fallingEntityKillers.add(Material.LEVER); - fallingEntityKillers.add(Material.REDSTONE_WIRE); - fallingEntityKillers.add(Material.REDSTONE_TORCH_ON); - fallingEntityKillers.add(Material.REDSTONE_TORCH_OFF); - fallingEntityKillers.add(Material.DIODE_BLOCK_ON); - fallingEntityKillers.add(Material.DIODE_BLOCK_OFF); - fallingEntityKillers.add(Material.REDSTONE_COMPARATOR_ON); - fallingEntityKillers.add(Material.REDSTONE_COMPARATOR_OFF); - fallingEntityKillers.add(Material.DAYLIGHT_DETECTOR); - fallingEntityKillers.add(Material.CARPET); - - // Crop Blocks - cropBlocks = new HashSet(5); - cropBlocks.add(Material.CROPS); - cropBlocks.add(Material.MELON_STEM); - cropBlocks.add(Material.PUMPKIN_STEM); - cropBlocks.add(Material.CARROT); - cropBlocks.add(Material.POTATO); - - // Container Blocks - containerBlocks = new HashSet(6); - containerBlocks.add(Material.CHEST); - containerBlocks.add(Material.TRAPPED_CHEST); - containerBlocks.add(Material.DISPENSER); - containerBlocks.add(Material.DROPPER); - containerBlocks.add(Material.HOPPER); - containerBlocks.add(Material.BREWING_STAND); - containerBlocks.add(Material.FURNACE); - containerBlocks.add(Material.BURNING_FURNACE); - containerBlocks.add(Material.BEACON); - containerBlocks.add(Material.BLACK_SHULKER_BOX); - containerBlocks.add(Material.BLUE_SHULKER_BOX); - containerBlocks.add(Material.SILVER_SHULKER_BOX); - containerBlocks.add(Material.BROWN_SHULKER_BOX); - containerBlocks.add(Material.CYAN_SHULKER_BOX); - containerBlocks.add(Material.GRAY_SHULKER_BOX); - containerBlocks.add(Material.GREEN_SHULKER_BOX); - containerBlocks.add(Material.LIGHT_BLUE_SHULKER_BOX); - containerBlocks.add(Material.MAGENTA_SHULKER_BOX); - containerBlocks.add(Material.LIME_SHULKER_BOX); - containerBlocks.add(Material.ORANGE_SHULKER_BOX); - containerBlocks.add(Material.PINK_SHULKER_BOX); - containerBlocks.add(Material.PURPLE_SHULKER_BOX); - containerBlocks.add(Material.RED_SHULKER_BOX); - containerBlocks.add(Material.WHITE_SHULKER_BOX); - containerBlocks.add(Material.YELLOW_SHULKER_BOX); - // Doesn't actually have a block inventory - // containerBlocks.add(Material.ENDER_CHEST); - - // It doesn't seem like you could injure people with some of these, but they exist, so.... - projectileItems = new EnumMap(EntityType.class); - projectileItems.put(EntityType.ARROW, 262); - projectileItems.put(EntityType.EGG, 344); - projectileItems.put(EntityType.ENDER_PEARL, 368); - projectileItems.put(EntityType.SMALL_FIREBALL, 385); // Fire charge - projectileItems.put(EntityType.FIREBALL, 385); // Fire charge - projectileItems.put(EntityType.FISHING_HOOK, 346); - projectileItems.put(EntityType.SNOWBALL, 332); - projectileItems.put(EntityType.SPLASH_POTION, 373); - projectileItems.put(EntityType.THROWN_EXP_BOTTLE, 384); - projectileItems.put(EntityType.WITHER_SKULL, 397); - - } - - private static final BlockFace[] relativeBlockFaces = new BlockFace[]{ - BlockFace.EAST, BlockFace.WEST, BlockFace.NORTH, BlockFace.SOUTH, BlockFace.UP, BlockFace.DOWN - }; - - /** - * Returns a list of block locations around the block that are of the type specified by the integer list parameter - * - * @param block The central block to get the blocks around - * @param type The type of blocks around the center block to return - * @return List of block locations around the block that are of the type specified by the integer list parameter - */ - public static List getBlocksNearby(org.bukkit.block.Block block, Set type) { - ArrayList blocks = new ArrayList(); - for (BlockFace blockFace : relativeBlockFaces) { - if (type.contains(block.getRelative(blockFace).getType())) { - blocks.add(block.getRelative(blockFace).getLocation()); - } - } - return blocks; - } - - public static boolean isTop(Material mat, byte data) { - - switch (mat) { - case DOUBLE_PLANT: - return data > 5; - case IRON_DOOR_BLOCK: - case WOODEN_DOOR: - return data == 8 || data == 9; - default: - return false; - } - } - - public static int getInventoryHolderType(InventoryHolder holder) { - if (holder instanceof DoubleChest) { - return getInventoryHolderType(((DoubleChest) holder).getLeftSide()); - } else if (holder instanceof BlockState) { - return ((BlockState) holder).getTypeId(); - } else { - return -1; - } - } - - public static Location getInventoryHolderLocation(InventoryHolder holder) { - if (holder instanceof DoubleChest) { - return getInventoryHolderLocation(((DoubleChest) holder).getLeftSide()); - } else if (holder instanceof BlockState) { - return ((BlockState) holder).getLocation(); - } else { - return null; - } - } - - public static ItemStack[] compareInventories(ItemStack[] items1, ItemStack[] items2) { - final ItemStackComparator comperator = new ItemStackComparator(); - final ArrayList diff = new ArrayList(); - final int l1 = items1.length, l2 = items2.length; - int c1 = 0, c2 = 0; - while (c1 < l1 || c2 < l2) { - if (c1 >= l1) { - diff.add(items2[c2]); - c2++; - continue; - } - if (c2 >= l2) { - items1[c1].setAmount(items1[c1].getAmount() * -1); - diff.add(items1[c1]); - c1++; - continue; - } - final int comp = comperator.compare(items1[c1], items2[c2]); - if (comp < 0) { - items1[c1].setAmount(items1[c1].getAmount() * -1); - diff.add(items1[c1]); - c1++; - } else if (comp > 0) { - diff.add(items2[c2]); - c2++; - } else { - final int amount = items2[c2].getAmount() - items1[c1].getAmount(); - if (amount != 0) { - items1[c1].setAmount(amount); - diff.add(items1[c1]); - } - c1++; - c2++; - } - } - return diff.toArray(new ItemStack[diff.size()]); - } - - public static ItemStack[] compressInventory(ItemStack[] items) { - final ArrayList compressed = new ArrayList(); - for (final ItemStack item : items) { - if (item != null) { - final int type = item.getTypeId(); - final short data = rawData(item); - boolean found = false; - for (final ItemStack item2 : compressed) { - if (type == item2.getTypeId() && data == rawData(item2)) { - item2.setAmount(item2.getAmount() + item.getAmount()); - found = true; - break; - } - } - if (!found) { - compressed.add(new ItemStack(type, item.getAmount(), data)); - } - } - } - Collections.sort(compressed, new ItemStackComparator()); - return compressed.toArray(new ItemStack[compressed.size()]); - } - - public static boolean equalTypes(int type1, int type2) { - if (type1 == type2) { - return true; - } - for (final Set equivalent : blockEquivalents) { - if (equivalent.contains(type1) && equivalent.contains(type2)) { - return true; - } - } - return false; - } - - public static String friendlyWorldname(String worldName) { - return new File(worldName).getName(); - } - - public static Set> getBlockEquivalents() { - return blockEquivalents; - } - - public static Set getRelativeBreakables() { - return relativeBreakable; - } - - public static Set getRelativeTopBreakabls() { - return relativeTopBreakable; - } - - public static Set getRelativeTopFallables() { - return relativeTopFallables; - } - - public static Set getFallingEntityKillers() { - return fallingEntityKillers; - } - - public static Set getCropBlocks() { - return cropBlocks; - } - - public static Set getContainerBlocks() { - return containerBlocks; - } - - public static String entityName(Entity entity) { - if (entity instanceof Player) { - return ((Player) entity).getName(); - } - if (entity instanceof TNTPrimed) { - return "TNT"; - } - return entity.getClass().getSimpleName().substring(5); - } - - public static void giveTool(Player player, int type) { - final Inventory inv = player.getInventory(); - if (inv.contains(type)) { - player.sendMessage(ChatColor.RED + "You have already a " + materialName(type)); - } else { - final int free = inv.firstEmpty(); - if (free >= 0) { - if (player.getItemInHand() != null && player.getItemInHand().getTypeId() != 0) { - inv.setItem(free, player.getItemInHand()); - } - player.setItemInHand(new ItemStack(type, 1)); - player.sendMessage(ChatColor.GREEN + "Here's your " + materialName(type)); - } else { - player.sendMessage(ChatColor.RED + "You have no empty slot in your inventory"); - } - } - } - - public static short rawData(ItemStack item) { - return item.getType() != null ? item.getData() != null ? item.getDurability() : 0 : 0; - } - - public static int saveSpawnHeight(Location loc) { - final World world = loc.getWorld(); - final Chunk chunk = world.getChunkAt(loc); - if (!world.isChunkLoaded(chunk)) { - world.loadChunk(chunk); - } - final int x = loc.getBlockX(), z = loc.getBlockZ(); - int y = loc.getBlockY(); - boolean lower = world.getBlockTypeIdAt(x, y, z) == 0, upper = world.getBlockTypeIdAt(x, y + 1, z) == 0; - while ((!lower || !upper) && y != 127) { - lower = upper; - upper = world.getBlockTypeIdAt(x, ++y, z) == 0; - } - while (world.getBlockTypeIdAt(x, y - 1, z) == 0 && y != 0) { - y--; - } - return y; - } - - public static int modifyContainer(BlockState b, ItemStack item) { - if (b instanceof InventoryHolder) { - final Inventory inv = ((InventoryHolder) b).getInventory(); - if (item.getAmount() < 0) { - item.setAmount(-item.getAmount()); - final ItemStack tmp = inv.removeItem(item).get(0); - return tmp != null ? tmp.getAmount() : 0; - } else if (item.getAmount() > 0) { - final ItemStack tmp = inv.addItem(item).get(0); - return tmp != null ? tmp.getAmount() : 0; - } - } - return 0; - } - - public static boolean canFall(World world, int x, int y, int z) { - Material mat = world.getBlockAt(x, y, z).getType(); - - // Air - if (mat == Material.AIR) { - return true; - } else if (mat == Material.WATER || mat == Material.STATIONARY_WATER || mat == Material.LAVA || mat == Material.STATIONARY_LAVA) { // Fluids - return true; - } else if (getFallingEntityKillers().contains(mat) || mat == Material.FIRE || mat == Material.VINE || mat == Material.LONG_GRASS || mat == Material.DEAD_BUSH) { // Misc. - return true; - } - return false; - } - - public static class ItemStackComparator implements Comparator { - @Override - public int compare(ItemStack a, ItemStack b) { - final int aType = a.getTypeId(), bType = b.getTypeId(); - if (aType < bType) { - return -1; - } - if (aType > bType) { - return 1; - } - final short aData = rawData(a), bData = rawData(b); - if (aData < bData) { - return -1; - } - if (aData > bData) { - return 1; - } - return 0; - } - } - - public static int itemIDfromProjectileEntity(Entity e) { - Integer i = projectileItems.get(e.getType()); - return (i == null) ? 0 : i; - } -} diff --git a/src/main/java/de/diddiz/util/LoggingUtil.java b/src/main/java/de/diddiz/util/LoggingUtil.java deleted file mode 100644 index 51a0e448..00000000 --- a/src/main/java/de/diddiz/util/LoggingUtil.java +++ /dev/null @@ -1,214 +0,0 @@ -package de.diddiz.util; - -import de.diddiz.LogBlock.Actor; -import de.diddiz.LogBlock.Consumer; -import de.diddiz.LogBlock.Logging; -import de.diddiz.LogBlock.config.WorldConfig; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.block.Block; -import org.bukkit.block.BlockFace; -import org.bukkit.block.BlockState; -import org.bukkit.block.Sign; -import org.bukkit.material.*; - -import java.util.List; - -import static de.diddiz.LogBlock.config.Config.getWorldConfig; -import static de.diddiz.LogBlock.config.Config.mb4; - -public class LoggingUtil { - - public static void smartLogFallables(Consumer consumer, Actor actor, Block origin) { - - WorldConfig wcfg = getWorldConfig(origin.getWorld()); - if (wcfg == null) { - return; - } - - //Handle falling blocks - Block checkBlock = origin.getRelative(BlockFace.UP); - int up = 0; - final int highestBlock = checkBlock.getWorld().getHighestBlockYAt(checkBlock.getLocation()); - while (BukkitUtils.getRelativeTopFallables().contains(checkBlock.getType())) { - - // Record this block as falling - consumer.queueBlockBreak(actor, checkBlock.getState()); - - // Guess where the block is going (This could be thrown of by explosions, but it is better than nothing) - Location loc = origin.getLocation(); - int x = loc.getBlockX(); - int y = loc.getBlockY(); - int z = loc.getBlockZ(); - while (y > 0 && BukkitUtils.canFall(loc.getWorld(), x, (y - 1), z)) { - y--; - } - // If y is 0 then the sand block fell out of the world :( - if (y != 0) { - Location finalLoc = new Location(loc.getWorld(), x, y, z); - // Run this check to avoid false positives - if (!BukkitUtils.getFallingEntityKillers().contains(finalLoc.getBlock().getType())) { - finalLoc.add(0, up, 0); // Add this here after checking for block breakers - if (finalLoc.getBlock().getType() == Material.AIR || BukkitUtils.getRelativeTopFallables().contains(finalLoc.getBlock().getType())) { - consumer.queueBlockPlace(actor, finalLoc, checkBlock.getTypeId(), checkBlock.getData()); - } else { - consumer.queueBlockReplace(actor, finalLoc, finalLoc.getBlock().getTypeId(), finalLoc.getBlock().getData(), checkBlock.getTypeId(), checkBlock.getData()); - } - up++; - } - } - if (checkBlock.getY() >= highestBlock) { - break; - } - checkBlock = checkBlock.getRelative(BlockFace.UP); - } - } - - public static void smartLogBlockBreak(Consumer consumer, Actor actor, Block origin) { - - WorldConfig wcfg = getWorldConfig(origin.getWorld()); - if (wcfg == null) { - return; - } - - Block checkBlock = origin.getRelative(BlockFace.UP); - if (BukkitUtils.getRelativeTopBreakabls().contains(checkBlock.getType())) { - if (wcfg.isLogging(Logging.SIGNTEXT) && checkBlock.getType() == Material.SIGN_POST) { - consumer.queueSignBreak(actor, (Sign) checkBlock.getState()); - } else if (checkBlock.getType() == Material.IRON_DOOR_BLOCK || checkBlock.getType() == Material.WOODEN_DOOR) { - Block doorBlock = checkBlock; - // If the doorBlock is the top half a door the player simply punched a door - // this will be handled later. - if (!BukkitUtils.isTop(doorBlock.getType(), doorBlock.getData())) { - doorBlock = doorBlock.getRelative(BlockFace.UP); - // Fall back check just in case the top half wasn't a door - if (doorBlock.getType() == Material.IRON_DOOR_BLOCK || doorBlock.getType() == Material.WOODEN_DOOR) { - consumer.queueBlockBreak(actor, doorBlock.getState()); - } - consumer.queueBlockBreak(actor, checkBlock.getState()); - } - } else if (checkBlock.getType() == Material.DOUBLE_PLANT) { - Block plantBlock = checkBlock; - // If the plantBlock is the top half of a double plant the player simply - // punched the plant this will be handled later. - if (!BukkitUtils.isTop(plantBlock.getType(), plantBlock.getData())) { - plantBlock = plantBlock.getRelative(BlockFace.UP); - // Fall back check just in case the top half wasn't a plant - if (plantBlock.getType() == Material.DOUBLE_PLANT) { - consumer.queueBlockBreak(actor, plantBlock.getState()); - } - consumer.queueBlockBreak(actor, checkBlock.getState()); - } - } else { - consumer.queueBlockBreak(actor, checkBlock.getState()); - } - } - - List relativeBreakables = BukkitUtils.getBlocksNearby(origin, BukkitUtils.getRelativeBreakables()); - if (relativeBreakables.size() != 0) { - for (Location location : relativeBreakables) { - final Material blockType = location.getBlock().getType(); - final BlockState blockState = location.getBlock().getState(); - final MaterialData data = blockState.getData(); - switch (blockType) { - case REDSTONE_TORCH_ON: - case REDSTONE_TORCH_OFF: - if (blockState.getBlock().getRelative(((RedstoneTorch) data).getAttachedFace()).equals(origin)) { - consumer.queueBlockBreak(actor, blockState); - } - break; - case TORCH: - if (blockState.getBlock().getRelative(((Torch) data).getAttachedFace()).equals(origin)) { - consumer.queueBlockBreak(actor, blockState); - } - break; - case COCOA: - if (blockState.getBlock().getRelative(((CocoaPlant) data).getAttachedFace().getOppositeFace()).equals(origin)) { - consumer.queueBlockBreak(actor, blockState); - } - break; - case LADDER: - if (blockState.getBlock().getRelative(((Ladder) data).getAttachedFace()).equals(origin)) { - consumer.queueBlockBreak(actor, blockState); - } - break; - case LEVER: - if (blockState.getBlock().getRelative(((Lever) data).getAttachedFace()).equals(origin)) { - consumer.queueBlockBreak(actor, blockState); - } - break; - case TRIPWIRE_HOOK: - if (blockState.getBlock().getRelative(((TripwireHook) data).getAttachedFace()).equals(origin)) { - consumer.queueBlockBreak(actor, blockState); - } - break; - case WOOD_BUTTON: - case STONE_BUTTON: - if (blockState.getBlock().getRelative(((Button) data).getAttachedFace()).equals(origin)) { - consumer.queueBlockBreak(actor, blockState); - } - break; - case WALL_SIGN: - if (blockState.getBlock().getRelative(((org.bukkit.material.Sign) data).getAttachedFace()).equals(origin)) { - if (wcfg.isLogging(Logging.SIGNTEXT)) { - consumer.queueSignBreak(actor, (Sign) blockState); - } else { - consumer.queueBlockBreak(actor, blockState); - } - } - break; - case TRAP_DOOR: - if (blockState.getBlock().getRelative(((TrapDoor) data).getAttachedFace()).equals(origin)) { - consumer.queueBlockBreak(actor, blockState); - } - break; - default: - consumer.queueBlockBreak(actor, blockState); - break; - } - } - } - - // Special door check - if (origin.getType() == Material.IRON_DOOR_BLOCK || origin.getType() == Material.WOODEN_DOOR) { - Block doorBlock = origin; - - // Up or down? - if (!BukkitUtils.isTop(doorBlock.getType(), doorBlock.getData())) { - doorBlock = doorBlock.getRelative(BlockFace.UP); - } else { - doorBlock = doorBlock.getRelative(BlockFace.DOWN); - } - - if (doorBlock.getType() == Material.IRON_DOOR_BLOCK || doorBlock.getType() == Material.WOODEN_DOOR) { - consumer.queueBlockBreak(actor, doorBlock.getState()); - } - } else if (origin.getType() == Material.DOUBLE_PLANT) { // Special double plant check - Block plantBlock = origin; - - // Up or down? - if (!BukkitUtils.isTop(origin.getType(), origin.getData())) { - plantBlock = plantBlock.getRelative(BlockFace.UP); - } else { - plantBlock = plantBlock.getRelative(BlockFace.DOWN); - } - - if (plantBlock.getType() == Material.DOUBLE_PLANT) { - consumer.queueBlockBreak(actor, plantBlock.getState()); - } - } - - // Do this down here so that the block is added after blocks sitting on it - consumer.queueBlockBreak(actor, origin.getState()); - } - - public static String checkText(String text) { - if (text == null) { - return text; - } - if (mb4) { - return text; - } - return text.replaceAll("[^\\u0000-\\uFFFF]", "?"); - } -} diff --git a/src/main/java/de/diddiz/util/MaterialName.java b/src/main/java/de/diddiz/util/MaterialName.java deleted file mode 100644 index 9c763bdb..00000000 --- a/src/main/java/de/diddiz/util/MaterialName.java +++ /dev/null @@ -1,277 +0,0 @@ -package de.diddiz.util; - -import org.bukkit.Material; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.material.MaterialData; - -import java.io.File; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import java.util.logging.Level; - -import static de.diddiz.util.Utils.isInt; -import static de.diddiz.util.Utils.isShort; -import static org.bukkit.Bukkit.getLogger; - -public class MaterialName { - private static final String[] COLORS = {"white", "orange", "magenta", "light blue", "yellow", "lime", "pink", "gray", "silver", "cyan", "purple", "blue", "brown", "green", "red", "black"}; - private static final Map materialNames = new HashMap(); - private static final Map> materialDataNames = new HashMap>(); - private static final Map nameTypes = new HashMap(); - - static { - // Add all known materials - for (final Material mat : Material.values()) { - materialNames.put(mat.getId(), mat.toString().replace('_', ' ').toLowerCase()); - } - // Load config - final File file = new File("plugins/LogBlock/materials.yml"); - final YamlConfiguration cfg = YamlConfiguration.loadConfiguration(file); - if (cfg.getKeys(false).isEmpty()) { - // Generate defaults - cfg.options().header("Add block or item names you want to be overridden or also names for custom blocks"); - cfg.set("1.1", "granite"); - cfg.set("1.2", "polished granite"); - cfg.set("1.3", "diorite"); - cfg.set("1.4", "polished diorite"); - cfg.set("1.5", "andesite"); - cfg.set("1.6", "polished andesite"); - cfg.set("5.0", "oak wood"); - cfg.set("5.1", "spruce wood"); - cfg.set("5.2", "birch wood"); - cfg.set("5.3", "jungle wood"); - cfg.set("5.4", "acacia wood"); - cfg.set("5.5", "dark oak wood"); - cfg.set("3.1", "coarse dirt"); - cfg.set("3.2", "podzol"); - cfg.set("6.1", "redwood sapling"); - cfg.set("6.2", "birch sapling"); - cfg.set("6.3", "jungle sapling"); - cfg.set("6.4", "acacia sapling"); - cfg.set("6.5", "dark oak sapling"); - cfg.set("9", "water"); - cfg.set("11", "lava"); - cfg.set("12.1", "red sand"); - cfg.set("17.0", "oak log"); - cfg.set("17.1", "spruce log"); - cfg.set("17.2", "birch log"); - cfg.set("17.3", "jungle log"); - cfg.set("17.4", "oak log"); - cfg.set("17.5", "spruce log"); - cfg.set("17.6", "birch log"); - cfg.set("17.7", "jungle log"); - cfg.set("17.8", "oak log"); - cfg.set("17.9", "spruce log"); - cfg.set("17.10", "birch log"); - cfg.set("17.11", "jungle log"); - cfg.set("17.12", "oak log"); - cfg.set("17.13", "spruce log"); - cfg.set("17.14", "birch log"); - cfg.set("17.15", "jungle log"); - cfg.set("18.1", "spruce leaves"); - cfg.set("18.2", "birch leaves"); - cfg.set("18.3", "jungle leaves"); - cfg.set("18.4", "oak leaves"); - cfg.set("18.5", "spruce leaves"); - cfg.set("18.6", "birch leaves"); - cfg.set("18.7", "jungle leaves"); - cfg.set("18.8", "oak leaves"); - cfg.set("18.9", "spruce leaves"); - cfg.set("18.10", "birch leaves"); - cfg.set("18.11", "jungle leaves"); - cfg.set("18.12", "oak leaves"); - cfg.set("18.13", "spruce leaves"); - cfg.set("18.14", "birch leaves"); - cfg.set("18.15", "jungle leaves"); - cfg.set("19.1", "wet sponge"); - cfg.set("37.0", "dandelion"); - cfg.set("38.0", "poppy"); - cfg.set("38.1", "blue orchid"); - cfg.set("38.2", "allium"); - cfg.set("38.3", "azure bluet"); - cfg.set("38.4", "red tulip"); - cfg.set("38.5", "orange tulip"); - cfg.set("38.6", "white tulip"); - cfg.set("38.7", "pink tulip"); - cfg.set("38.8", "oxeye daisy"); - cfg.set("24.1", "chiseled sandstone"); - cfg.set("24.2", "smooth sandstone"); - cfg.set("31.0", "dead bush"); - cfg.set("31.1", "tall grass"); - cfg.set("31.2", "fern"); - cfg.set("98.0", "stone brick"); - cfg.set("98.1", "mossy stone brick"); - cfg.set("98.2", "cracked stone brick"); - cfg.set("98.3", "chiseled stone brick"); - cfg.set("125.0", "oak double step"); - cfg.set("125.1", "spruce double step"); - cfg.set("125.2", "birch double step"); - cfg.set("125.3", "jungle double step"); - cfg.set("125.4", "acacia double step"); - cfg.set("125.5", "dark oak double step"); - cfg.set("126.0", "oak step"); - cfg.set("126.1", "spruce step"); - cfg.set("126.2", "birch step"); - cfg.set("126.3", "jungle step"); - cfg.set("126.4", "acacia step"); - cfg.set("126.5", "dark oak step"); - cfg.set("126.8", "oak step"); - cfg.set("126.9", "spruce step"); - cfg.set("126.10", "birch step"); - cfg.set("126.11", "jungle step"); - cfg.set("126.12", "acacia step"); - cfg.set("126.13", "dark oak step"); - cfg.set("139.1", "mossy cobble wall"); - cfg.set("155.1", "chiseled quartz block"); - cfg.set("155.2", "pillar quartz block"); - cfg.set("155.3", "pillar quartz block"); - cfg.set("155.4", "pillar quartz block"); - cfg.set("161.0", "acacia leaves"); - cfg.set("161.1", "dark oak leaves"); - cfg.set("161.4", "acacia leaves"); - cfg.set("161.5", "dark oak leaves"); - cfg.set("161.8", "acacia leaves"); - cfg.set("161.9", "dark oak leaves"); - cfg.set("161.12", "acacia leaves"); - cfg.set("161.13", "dark oak leaves"); - cfg.set("162.0", "acacia log"); - cfg.set("162.1", "dark oak log"); - cfg.set("162.4", "acacia log"); - cfg.set("162.5", "dark oak log"); - cfg.set("162.8", "acacia log"); - cfg.set("162.9", "dark oak log"); - cfg.set("162.12", "acacia log"); - cfg.set("162.13", "dark oak log"); - cfg.set("168.1", "prismarine brick"); - cfg.set("168.2", "dark prismarine"); - cfg.set("181.0", "red sandstone double step"); - cfg.set("181.8", "smooth red sandstone double step"); - cfg.set("162.13", "dark oak log"); - cfg.set("175.0", "sunflower"); - cfg.set("175.1", "lilac"); - cfg.set("175.2", "double tall grass"); - cfg.set("175.3", "large fern"); - cfg.set("175.4", "rose bush"); - cfg.set("175.5", "peony"); - cfg.set("175.8", "sunflower"); - cfg.set("175.9", "lilac"); - cfg.set("175.10", "double tall grass"); - cfg.set("175.11", "large fern"); - cfg.set("175.12", "rose bush"); - cfg.set("175.13", "peony"); - cfg.set("179.1", "chiseled sandstone"); - cfg.set("179.2", "smooth sandstone"); - cfg.set("263.1", "charcoal"); - for (byte i = 0; i < 10; i++) { - cfg.set("43." + i, toReadable(Material.DOUBLE_STEP.getNewData(i))); - } - cfg.set("43.8", "stone double step"); - cfg.set("43.9", "sandstone double step"); - cfg.set("43.15", "quartz double step"); - for (byte i = 0; i < 8; i++) { - cfg.set("44." + i, toReadable(Material.STEP.getNewData(i))); - // The second half of this data list should read the same as the first half - cfg.set("44." + (i + 7), toReadable(Material.STEP.getNewData(i))); - } - for (byte i = 0; i < 16; i++) { - cfg.set("351." + i, toReadable(Material.INK_SACK.getNewData(i))); - cfg.set("35." + i, COLORS[i] + " wool"); - cfg.set("159." + i, COLORS[i] + " stained clay"); - cfg.set("95." + i, COLORS[i] + " stained glass"); - cfg.set("160." + i, COLORS[i] + " stained glass pane"); - cfg.set("171." + i, COLORS[i] + " carpet"); - } - for (byte i = 0; i < 6; i++) { - cfg.set("125." + i, toReadable(Material.WOOD_DOUBLE_STEP.getNewData(i))); - cfg.set("126." + i, toReadable(Material.WOOD_STEP.getNewData(i))); - cfg.set("126." + i + 8, toReadable(Material.WOOD_STEP.getNewData(i))); - } - try { - cfg.save(file); - } catch (final IOException ex) { - getLogger().log(Level.WARNING, "Unable to save material.yml: ", ex); - } - } - if (cfg.getString("263.1") == null) { - getLogger().info("[Logblock-names] Logblock's default materials.yml file has been updated with more names"); - getLogger().info("[Logblock-names] Consider deleting your current materials.yml file to allow it to be recreated"); - } - for (final String entry : cfg.getKeys(false)) { - if (isInt(entry)) { - if (cfg.isString(entry)) { - materialNames.put(Integer.valueOf(entry), cfg.getString(entry)); - nameTypes.put(cfg.getString(entry), Integer.valueOf(entry)); - } else if (cfg.isConfigurationSection(entry)) { - final Map dataNames = new HashMap(); - materialDataNames.put(Integer.valueOf(entry), dataNames); - final ConfigurationSection sec = cfg.getConfigurationSection(entry); - for (final String data : sec.getKeys(false)) { - if (isShort(data)) { - if (sec.isString(data)) { - dataNames.put(Short.valueOf(data), sec.getString(data)); - nameTypes.put(sec.getString(data), Integer.valueOf(entry)); - } else { - getLogger().warning("Parsing materials.yml: '" + data + "' is not a string."); - } - } else { - getLogger().warning("Parsing materials.yml: '" + data + "' is no valid material data"); - } - } - } else { - getLogger().warning("Parsing materials.yml: '" + entry + "' is neither a string nor a section."); - } - } else { - getLogger().warning("Parsing materials.yml: '" + entry + "' is no valid material id"); - } - } - } - - /** - * Returns the name of a material based on its id - * - * @param type The type of the material - * @return Name of the material, or if it's unknown, the id. - */ - public static String materialName(int type) { - return materialNames.containsKey(type) ? materialNames.get(type) : String.valueOf(type); - } - - /** - * Returns the name of a material based on its id and data - * - * @param type The type of the material - * @param data The data of the material - * @return Name of the material regarding it's data, or if it's unknown, the basic name. - */ - public static String materialName(int type, short data) { - final Map dataNames = materialDataNames.get(type); - if (dataNames != null) { - if (dataNames.containsKey(data)) { - return dataNames.get(data); - } - } - return materialName(type); - } - - public static Integer typeFromName(String name) { - Integer answer = nameTypes.get(toReadable(name)); - if (answer != null) { - return answer; - } - final Material mat = Material.matchMaterial(name); - if (mat == null) { - throw new IllegalArgumentException("No material matching: '" + name + "'"); - } - return mat.getId(); - } - - private static String toReadable(MaterialData matData) { - return matData.toString().toLowerCase().replace('_', ' ').replaceAll("[^a-z ]", ""); - } - - private static String toReadable(String matData) { - return matData.toLowerCase().replace('_', ' ').replaceAll("[^a-z ]", ""); - } -} diff --git a/src/main/java/de/diddiz/worldedit/RegionContainer.java b/src/main/java/de/diddiz/worldedit/RegionContainer.java deleted file mode 100644 index 3258434d..00000000 --- a/src/main/java/de/diddiz/worldedit/RegionContainer.java +++ /dev/null @@ -1,41 +0,0 @@ -package de.diddiz.worldedit; - -import com.sk89q.worldedit.bukkit.WorldEditPlugin; -import com.sk89q.worldedit.bukkit.selections.CuboidSelection; -import com.sk89q.worldedit.bukkit.selections.Selection; -import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.entity.Player; -import org.bukkit.plugin.Plugin; - -public class RegionContainer { - - private Selection selection; - - public RegionContainer(Selection sel) { - this.selection = sel; - } - - public static RegionContainer fromPlayerSelection(Player player, Plugin plugin) { - final Selection selection = ((WorldEditPlugin) plugin).getSelection(player); - if (selection == null) { - throw new IllegalArgumentException("No selection defined"); - } - if (!(selection instanceof CuboidSelection)) { - throw new IllegalArgumentException("You have to define a cuboid selection"); - } - return new RegionContainer(selection); - } - - public static RegionContainer fromCorners(World world, Location first, Location second) { - return new RegionContainer(new CuboidSelection(world, first, second)); - } - - public Selection getSelection() { - return selection; - } - - public void setSelection(Selection selection) { - this.selection = selection; - } -} diff --git a/src/main/java/de/diddiz/worldedit/WorldEditLoggingHook.java b/src/main/java/de/diddiz/worldedit/WorldEditLoggingHook.java deleted file mode 100644 index 0bdef234..00000000 --- a/src/main/java/de/diddiz/worldedit/WorldEditLoggingHook.java +++ /dev/null @@ -1,122 +0,0 @@ -package de.diddiz.worldedit; - -import com.sk89q.worldedit.EditSession; -import com.sk89q.worldedit.Vector; -import com.sk89q.worldedit.WorldEdit; -import com.sk89q.worldedit.blocks.BaseBlock; -import com.sk89q.worldedit.bukkit.BukkitWorld; -import com.sk89q.worldedit.event.extent.EditSessionEvent; -import com.sk89q.worldedit.extension.platform.Actor; -import com.sk89q.worldedit.extent.logging.AbstractLoggingExtent; -import com.sk89q.worldedit.util.eventbus.Subscribe; -import de.diddiz.LogBlock.LogBlock; -import de.diddiz.LogBlock.Logging; -import de.diddiz.LogBlock.config.Config; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.World; -import org.bukkit.block.Block; -import org.bukkit.block.BlockState; -import org.bukkit.block.Sign; - -import java.util.logging.Level; - -//...so they ALSO have a class called Actor... need to fully-qualify when we use ours - -public class WorldEditLoggingHook { - - private LogBlock plugin; - - public WorldEditLoggingHook(LogBlock plugin) { - this.plugin = plugin; - } - - // Convert WE Actor to LB Actor - private de.diddiz.LogBlock.Actor AtoA(Actor weActor) { - if (weActor.isPlayer()) { - return new de.diddiz.LogBlock.Actor(weActor.getName(), weActor.getUniqueId()); - } - return new de.diddiz.LogBlock.Actor(weActor.getName()); - } - - private World adapt(com.sk89q.worldedit.world.World weWorld) { - if (weWorld == null) { - throw new NullPointerException("[Logblock-Worldedit] The provided world was null."); - } - if (weWorld instanceof BukkitWorld) { - return ((BukkitWorld) weWorld).getWorld(); - } - World world = Bukkit.getServer().getWorld(weWorld.getName()); - if (world == null) { - throw new IllegalArgumentException("Can't find a Bukkit world for " + weWorld); - } - return world; - } - - public void hook() { - WorldEdit.getInstance().getEventBus().register(new Object() { - @Subscribe - public void wrapForLogging(final EditSessionEvent event) { - final Actor actor = event.getActor(); - if (actor == null) { - return; - } - final de.diddiz.LogBlock.Actor lbActor = AtoA(actor); - - // Check to ensure the world should be logged - final World world; - final com.sk89q.worldedit.world.World k = event.getWorld(); - try { - world = adapt(k); - } catch (RuntimeException ex) { - plugin.getLogger().warning("Failed to register logging for WorldEdit!"); - plugin.getLogger().log(Level.WARNING, ex.getMessage(), ex); - return; - } - - // If config becomes reloadable, this check should be moved - if (!(Config.isLogging(world, Logging.WORLDEDIT))) { - return; - } - - event.setExtent(new AbstractLoggingExtent(event.getExtent()) { - @Override - protected void onBlockChange(Vector pt, BaseBlock block) { - - if (event.getStage() != EditSession.Stage.BEFORE_CHANGE) { - return; - } - - Location location = new Location(world, pt.getBlockX(), pt.getBlockY(), pt.getBlockZ()); - Block origin = location.getBlock(); - int typeBefore = origin.getTypeId(); - byte dataBefore = origin.getData(); - // If we're dealing with a sign, store the block state to read the text off - BlockState stateBefore = null; - if (typeBefore == Material.SIGN_POST.getId() || typeBefore == Material.SIGN.getId()) { - stateBefore = origin.getState(); - } - - // Check to see if we've broken a sign - if (Config.isLogging(location.getWorld().getName(), Logging.SIGNTEXT) && (typeBefore == Material.SIGN_POST.getId() || typeBefore == Material.SIGN.getId())) { - plugin.getConsumer().queueSignBreak(lbActor, (Sign) stateBefore); - if (block.getType() != Material.AIR.getId()) { - plugin.getConsumer().queueBlockPlace(lbActor, location, block.getType(), (byte) block.getData()); - } - } else { - if (dataBefore != 0) { - plugin.getConsumer().queueBlockBreak(lbActor, location, typeBefore, dataBefore); - if (block.getType() != Material.AIR.getId()) { - plugin.getConsumer().queueBlockPlace(lbActor, location, block.getType(), (byte) block.getData()); - } - } else { - plugin.getConsumer().queueBlock(lbActor, location, typeBefore, block.getType(), (byte) block.getData()); - } - } - } - }); - } - }); - } -} diff --git a/src/main/resources/blockdata.txt b/src/main/resources/blockdata.txt new file mode 100644 index 00000000..97da67d7 --- /dev/null +++ b/src/main/resources/blockdata.txt @@ -0,0 +1,2357 @@ +0:0,minecraft:air +1:0,minecraft:stone +1:1,minecraft:granite +1:2,minecraft:polished_granite +1:3,minecraft:diorite +1:4,minecraft:polished_diorite +1:5,minecraft:andesite +1:6,minecraft:polished_andesite +2:0,minecraft:grass_block[snowy=false] +3:0,minecraft:dirt +3:1,minecraft:coarse_dirt +3:2,minecraft:podzol[snowy=false] +4:0,minecraft:cobblestone +5:0,minecraft:oak_planks +5:1,minecraft:spruce_planks +5:2,minecraft:birch_planks +5:3,minecraft:jungle_planks +5:4,minecraft:acacia_planks +5:5,minecraft:dark_oak_planks +6:0,minecraft:oak_sapling[stage=0] +6:1,minecraft:spruce_sapling[stage=0] +6:2,minecraft:birch_sapling[stage=0] +6:3,minecraft:jungle_sapling[stage=0] +6:4,minecraft:acacia_sapling[stage=0] +6:5,minecraft:dark_oak_sapling[stage=0] +6:8,minecraft:oak_sapling[stage=1] +6:9,minecraft:spruce_sapling[stage=1] +6:10,minecraft:birch_sapling[stage=1] +6:11,minecraft:jungle_sapling[stage=1] +6:12,minecraft:acacia_sapling[stage=1] +6:13,minecraft:dark_oak_sapling[stage=1] +6:14,minecraft:oak_sapling[stage=1] +6:15,minecraft:oak_sapling[stage=1] +7:0,minecraft:bedrock +8:0,minecraft:water[level=0] +8:1,minecraft:water[level=1] +8:2,minecraft:water[level=2] +8:3,minecraft:water[level=3] +8:4,minecraft:water[level=4] +8:5,minecraft:water[level=5] +8:6,minecraft:water[level=6] +8:7,minecraft:water[level=7] +8:8,minecraft:water[level=8] +8:9,minecraft:water[level=9] +8:10,minecraft:water[level=10] +8:11,minecraft:water[level=11] +8:12,minecraft:water[level=12] +8:13,minecraft:water[level=13] +8:14,minecraft:water[level=14] +8:15,minecraft:water[level=15] +9:0,minecraft:water[level=0] +9:1,minecraft:water[level=1] +9:2,minecraft:water[level=2] +9:3,minecraft:water[level=3] +9:4,minecraft:water[level=4] +9:5,minecraft:water[level=5] +9:6,minecraft:water[level=6] +9:7,minecraft:water[level=7] +9:8,minecraft:water[level=8] +9:9,minecraft:water[level=9] +9:10,minecraft:water[level=10] +9:11,minecraft:water[level=11] +9:12,minecraft:water[level=12] +9:13,minecraft:water[level=13] +9:14,minecraft:water[level=14] +9:15,minecraft:water[level=15] +10:0,minecraft:lava[level=0] +10:1,minecraft:lava[level=1] +10:2,minecraft:lava[level=2] +10:3,minecraft:lava[level=3] +10:4,minecraft:lava[level=4] +10:5,minecraft:lava[level=5] +10:6,minecraft:lava[level=6] +10:7,minecraft:lava[level=7] +10:8,minecraft:lava[level=8] +10:9,minecraft:lava[level=9] +10:10,minecraft:lava[level=10] +10:11,minecraft:lava[level=11] +10:12,minecraft:lava[level=12] +10:13,minecraft:lava[level=13] +10:14,minecraft:lava[level=14] +10:15,minecraft:lava[level=15] +11:0,minecraft:lava[level=0] +11:1,minecraft:lava[level=1] +11:2,minecraft:lava[level=2] +11:3,minecraft:lava[level=3] +11:4,minecraft:lava[level=4] +11:5,minecraft:lava[level=5] +11:6,minecraft:lava[level=6] +11:7,minecraft:lava[level=7] +11:8,minecraft:lava[level=8] +11:9,minecraft:lava[level=9] +11:10,minecraft:lava[level=10] +11:11,minecraft:lava[level=11] +11:12,minecraft:lava[level=12] +11:13,minecraft:lava[level=13] +11:14,minecraft:lava[level=14] +11:15,minecraft:lava[level=15] +12:0,minecraft:sand +12:1,minecraft:red_sand +13:0,minecraft:gravel +14:0,minecraft:gold_ore +15:0,minecraft:iron_ore +16:0,minecraft:coal_ore +17:0,minecraft:oak_log[axis=y] +17:1,minecraft:spruce_log[axis=y] +17:2,minecraft:birch_log[axis=y] +17:3,minecraft:jungle_log[axis=y] +17:4,minecraft:oak_log[axis=x] +17:5,minecraft:spruce_log[axis=x] +17:6,minecraft:birch_log[axis=x] +17:7,minecraft:jungle_log[axis=x] +17:8,minecraft:oak_log[axis=z] +17:9,minecraft:spruce_log[axis=z] +17:10,minecraft:birch_log[axis=z] +17:11,minecraft:jungle_log[axis=z] +17:12,minecraft:oak_wood[axis=y] +17:13,minecraft:spruce_wood[axis=y] +17:14,minecraft:birch_wood[axis=y] +17:15,minecraft:jungle_wood[axis=y] +18:0,minecraft:oak_leaves[distance=7,persistent=false] +18:1,minecraft:spruce_leaves[distance=7,persistent=false] +18:2,minecraft:birch_leaves[distance=7,persistent=false] +18:3,minecraft:jungle_leaves[distance=7,persistent=false] +18:4,minecraft:oak_leaves[distance=7,persistent=true] +18:5,minecraft:spruce_leaves[distance=7,persistent=true] +18:6,minecraft:birch_leaves[distance=7,persistent=true] +18:7,minecraft:jungle_leaves[distance=7,persistent=true] +18:9,minecraft:spruce_leaves[distance=7,persistent=false] +18:10,minecraft:birch_leaves[distance=7,persistent=false] +18:11,minecraft:jungle_leaves[distance=7,persistent=false] +18:12,minecraft:oak_leaves[distance=7,persistent=true] +18:13,minecraft:spruce_leaves[distance=7,persistent=true] +18:14,minecraft:birch_leaves[distance=7,persistent=true] +18:15,minecraft:jungle_leaves[distance=7,persistent=true] +19:0,minecraft:sponge +19:1,minecraft:wet_sponge +19:3,minecraft:wet_sponge +19:5,minecraft:wet_sponge +19:7,minecraft:wet_sponge +19:9,minecraft:wet_sponge +19:11,minecraft:wet_sponge +19:13,minecraft:wet_sponge +19:15,minecraft:wet_sponge +20:0,minecraft:glass +21:0,minecraft:lapis_ore +22:0,minecraft:lapis_block +23:0,minecraft:dispenser[facing=down,triggered=false] +23:1,minecraft:dispenser[facing=up,triggered=false] +23:2,minecraft:dispenser[facing=north,triggered=false] +23:3,minecraft:dispenser[facing=south,triggered=false] +23:4,minecraft:dispenser[facing=west,triggered=false] +23:5,minecraft:dispenser[facing=east,triggered=false] +23:7,minecraft:dispenser[facing=up,triggered=false] +23:8,minecraft:dispenser[facing=down,triggered=true] +23:9,minecraft:dispenser[facing=up,triggered=true] +23:10,minecraft:dispenser[facing=north,triggered=true] +23:11,minecraft:dispenser[facing=south,triggered=true] +23:12,minecraft:dispenser[facing=west,triggered=true] +23:13,minecraft:dispenser[facing=east,triggered=true] +23:14,minecraft:dispenser[facing=down,triggered=true] +23:15,minecraft:dispenser[facing=up,triggered=true] +24:0,minecraft:sandstone +24:1,minecraft:chiseled_sandstone +24:2,minecraft:cut_sandstone +25:0,minecraft:note_block[instrument=harp,note=0,powered=false] +26:0,minecraft:red_bed[facing=south,occupied=false,part=foot] +26:1,minecraft:red_bed[facing=west,occupied=false,part=foot] +26:2,minecraft:red_bed[facing=north,occupied=false,part=foot] +26:3,minecraft:red_bed[facing=east,occupied=false,part=foot] +26:5,minecraft:red_bed[facing=west,occupied=false,part=foot] +26:6,minecraft:red_bed[facing=north,occupied=false,part=foot] +26:7,minecraft:red_bed[facing=east,occupied=false,part=foot] +26:8,minecraft:red_bed[facing=south,occupied=false,part=head] +26:9,minecraft:red_bed[facing=west,occupied=false,part=head] +26:10,minecraft:red_bed[facing=north,occupied=false,part=head] +26:11,minecraft:red_bed[facing=east,occupied=false,part=head] +26:12,minecraft:red_bed[facing=south,occupied=true,part=head] +26:13,minecraft:red_bed[facing=west,occupied=true,part=head] +26:14,minecraft:red_bed[facing=north,occupied=true,part=head] +26:15,minecraft:red_bed[facing=east,occupied=true,part=head] +27:0,minecraft:powered_rail[powered=false,shape=north_south] +27:6,minecraft:air +27:7,minecraft:air +27:14,minecraft:air +27:15,minecraft:air +28:0,minecraft:detector_rail[powered=false,shape=north_south] +28:6,minecraft:air +28:7,minecraft:air +28:9,minecraft:detector_rail[powered=false,shape=east_west] +28:10,minecraft:detector_rail[powered=false,shape=ascending_east] +28:11,minecraft:detector_rail[powered=false,shape=ascending_west] +28:12,minecraft:detector_rail[powered=false,shape=ascending_north] +28:13,minecraft:detector_rail[powered=false,shape=ascending_south] +28:14,minecraft:air +28:15,minecraft:air +29:0,minecraft:sticky_piston[extended=false,facing=down] +29:1,minecraft:sticky_piston[extended=false,facing=up] +29:2,minecraft:sticky_piston[extended=false,facing=north] +29:3,minecraft:sticky_piston[extended=false,facing=south] +29:4,minecraft:sticky_piston[extended=false,facing=west] +29:5,minecraft:sticky_piston[extended=false,facing=east] +29:6,minecraft:air +29:7,minecraft:air +29:8,minecraft:moving_piston[facing=down,type=sticky] +29:9,minecraft:moving_piston[facing=up,type=sticky] +29:10,minecraft:moving_piston[facing=north,type=sticky] +29:11,minecraft:moving_piston[facing=south,type=sticky] +29:12,minecraft:moving_piston[facing=west,type=sticky] +29:13,minecraft:moving_piston[facing=east,type=sticky] +29:14,minecraft:air +29:15,minecraft:air +30:0,minecraft:cobweb +31:0,minecraft:dead_bush +31:1,minecraft:grass +31:2,minecraft:fern +32:0,minecraft:dead_bush +33:0,minecraft:piston[extended=false,facing=down] +33:1,minecraft:piston[extended=false,facing=up] +33:2,minecraft:piston[extended=false,facing=north] +33:3,minecraft:piston[extended=false,facing=south] +33:4,minecraft:piston[extended=false,facing=west] +33:5,minecraft:piston[extended=false,facing=east] +33:6,minecraft:air +33:7,minecraft:air +33:8,minecraft:moving_piston[facing=down,type=normal] +33:9,minecraft:moving_piston[facing=up,type=normal] +33:10,minecraft:moving_piston[facing=north,type=normal] +33:11,minecraft:moving_piston[facing=south,type=normal] +33:12,minecraft:moving_piston[facing=west,type=normal] +33:13,minecraft:moving_piston[facing=east,type=normal] +33:14,minecraft:air +33:15,minecraft:air +34:0,minecraft:piston_head[facing=down,short=false,type=normal] +34:1,minecraft:piston_head[facing=up,short=false,type=normal] +34:2,minecraft:piston_head[facing=north,short=false,type=normal] +34:3,minecraft:piston_head[facing=south,short=false,type=normal] +34:4,minecraft:piston_head[facing=west,short=false,type=normal] +34:5,minecraft:piston_head[facing=east,short=false,type=normal] +34:6,minecraft:air +34:7,minecraft:air +34:8,minecraft:piston_head[facing=down,short=false,type=sticky] +34:9,minecraft:piston_head[facing=up,short=false,type=sticky] +34:10,minecraft:piston_head[facing=north,short=false,type=sticky] +34:11,minecraft:piston_head[facing=south,short=false,type=sticky] +34:12,minecraft:piston_head[facing=west,short=false,type=sticky] +34:13,minecraft:piston_head[facing=east,short=false,type=sticky] +34:14,minecraft:air +34:15,minecraft:air +35:0,minecraft:white_wool +35:1,minecraft:orange_wool +35:2,minecraft:magenta_wool +35:3,minecraft:light_blue_wool +35:4,minecraft:yellow_wool +35:5,minecraft:lime_wool +35:6,minecraft:pink_wool +35:7,minecraft:gray_wool +35:8,minecraft:light_gray_wool +35:9,minecraft:cyan_wool +35:10,minecraft:purple_wool +35:11,minecraft:blue_wool +35:12,minecraft:brown_wool +35:13,minecraft:green_wool +35:14,minecraft:red_wool +35:15,minecraft:black_wool +36:0,minecraft:moving_piston[facing=down,type=normal] +36:1,minecraft:moving_piston[facing=up,type=normal] +36:2,minecraft:moving_piston[facing=north,type=normal] +36:3,minecraft:moving_piston[facing=south,type=normal] +36:4,minecraft:moving_piston[facing=west,type=normal] +36:5,minecraft:moving_piston[facing=east,type=normal] +36:6,minecraft:air +36:7,minecraft:air +36:8,minecraft:moving_piston[facing=down,type=sticky] +36:9,minecraft:moving_piston[facing=up,type=sticky] +36:10,minecraft:moving_piston[facing=north,type=sticky] +36:11,minecraft:moving_piston[facing=south,type=sticky] +36:12,minecraft:moving_piston[facing=west,type=sticky] +36:13,minecraft:moving_piston[facing=east,type=sticky] +36:14,minecraft:air +36:15,minecraft:air +37:0,minecraft:dandelion +38:0,minecraft:poppy +38:1,minecraft:blue_orchid +38:2,minecraft:allium +38:3,minecraft:azure_bluet +38:4,minecraft:red_tulip +38:5,minecraft:orange_tulip +38:6,minecraft:white_tulip +38:7,minecraft:pink_tulip +38:8,minecraft:oxeye_daisy +39:0,minecraft:brown_mushroom +40:0,minecraft:red_mushroom +41:0,minecraft:gold_block +42:0,minecraft:iron_block +43:0,minecraft:stone_slab[type=double,waterlogged=false] +43:1,minecraft:sandstone_slab[type=double,waterlogged=false] +43:2,minecraft:petrified_oak_slab[type=double,waterlogged=false] +43:3,minecraft:cobblestone_slab[type=double,waterlogged=false] +43:4,minecraft:brick_slab[type=double,waterlogged=false] +43:5,minecraft:stone_brick_slab[type=double,waterlogged=false] +43:6,minecraft:nether_brick_slab[type=double,waterlogged=false] +43:7,minecraft:quartz_slab[type=double,waterlogged=false] +43:8,minecraft:smooth_stone +43:9,minecraft:smooth_sandstone +43:10,minecraft:petrified_oak_slab[type=double,waterlogged=false] +43:11,minecraft:cobblestone_slab[type=double,waterlogged=false] +43:12,minecraft:brick_slab[type=double,waterlogged=false] +43:13,minecraft:stone_brick_slab[type=double,waterlogged=false] +43:14,minecraft:nether_brick_slab[type=double,waterlogged=false] +43:15,minecraft:smooth_quartz +44:0,minecraft:smooth_stone_slab[type=bottom,waterlogged=false] +44:1,minecraft:sandstone_slab[type=bottom,waterlogged=false] +44:2,minecraft:petrified_oak_slab[type=bottom,waterlogged=false] +44:3,minecraft:cobblestone_slab[type=bottom,waterlogged=false] +44:4,minecraft:brick_slab[type=bottom,waterlogged=false] +44:5,minecraft:stone_brick_slab[type=bottom,waterlogged=false] +44:6,minecraft:nether_brick_slab[type=bottom,waterlogged=false] +44:7,minecraft:quartz_slab[type=bottom,waterlogged=false] +44:8,minecraft:smooth_stone_slab[type=top,waterlogged=false] +44:9,minecraft:sandstone_slab[type=top,waterlogged=false] +44:10,minecraft:petrified_oak_slab[type=top,waterlogged=false] +44:11,minecraft:cobblestone_slab[type=top,waterlogged=false] +44:12,minecraft:brick_slab[type=top,waterlogged=false] +44:13,minecraft:stone_brick_slab[type=top,waterlogged=false] +44:14,minecraft:nether_brick_slab[type=top,waterlogged=false] +44:15,minecraft:quartz_slab[type=top,waterlogged=false] +45:0,minecraft:bricks +46:0,minecraft:tnt +47:0,minecraft:bookshelf +48:0,minecraft:mossy_cobblestone +49:0,minecraft:obsidian +50:0,minecraft:torch +50:1,minecraft:air +50:2,minecraft:air +50:3,minecraft:air +50:4,minecraft:air +51:0,minecraft:fire[age=0,east=false,north=false,south=false,up=false,west=false] +51:1,minecraft:fire[age=1,east=false,north=false,south=false,up=false,west=false] +51:2,minecraft:fire[age=2,east=false,north=false,south=false,up=false,west=false] +51:3,minecraft:fire[age=3,east=false,north=false,south=false,up=false,west=false] +51:4,minecraft:fire[age=4,east=false,north=false,south=false,up=false,west=false] +51:5,minecraft:fire[age=5,east=false,north=false,south=false,up=false,west=false] +51:6,minecraft:fire[age=6,east=false,north=false,south=false,up=false,west=false] +51:7,minecraft:fire[age=7,east=false,north=false,south=false,up=false,west=false] +51:8,minecraft:fire[age=8,east=false,north=false,south=false,up=false,west=false] +51:9,minecraft:fire[age=9,east=false,north=false,south=false,up=false,west=false] +51:10,minecraft:fire[age=10,east=false,north=false,south=false,up=false,west=false] +51:11,minecraft:fire[age=11,east=false,north=false,south=false,up=false,west=false] +51:12,minecraft:fire[age=12,east=false,north=false,south=false,up=false,west=false] +51:13,minecraft:fire[age=13,east=false,north=false,south=false,up=false,west=false] +51:14,minecraft:fire[age=14,east=false,north=false,south=false,up=false,west=false] +51:15,minecraft:fire[age=15,east=false,north=false,south=false,up=false,west=false] +52:0,minecraft:spawner +53:0,minecraft:oak_stairs[facing=east,half=bottom,shape=straight,waterlogged=false] +53:1,minecraft:oak_stairs[facing=west,half=bottom,shape=straight,waterlogged=false] +53:2,minecraft:oak_stairs[facing=south,half=bottom,shape=straight,waterlogged=false] +53:3,minecraft:oak_stairs[facing=north,half=bottom,shape=straight,waterlogged=false] +53:4,minecraft:oak_stairs[facing=east,half=top,shape=straight,waterlogged=false] +53:5,minecraft:oak_stairs[facing=west,half=top,shape=straight,waterlogged=false] +53:6,minecraft:oak_stairs[facing=south,half=top,shape=straight,waterlogged=false] +53:7,minecraft:oak_stairs[facing=north,half=top,shape=straight,waterlogged=false] +53:9,minecraft:oak_stairs[facing=west,half=bottom,shape=straight,waterlogged=false] +53:10,minecraft:oak_stairs[facing=south,half=bottom,shape=straight,waterlogged=false] +53:11,minecraft:oak_stairs[facing=north,half=bottom,shape=straight,waterlogged=false] +53:12,minecraft:oak_stairs[facing=east,half=top,shape=straight,waterlogged=false] +53:13,minecraft:oak_stairs[facing=west,half=top,shape=straight,waterlogged=false] +53:14,minecraft:oak_stairs[facing=south,half=top,shape=straight,waterlogged=false] +53:15,minecraft:oak_stairs[facing=north,half=top,shape=straight,waterlogged=false] +54:0,minecraft:chest[facing=north,type=single,waterlogged=false] +54:3,minecraft:chest[facing=south,type=single,waterlogged=false] +54:4,minecraft:chest[facing=west,type=single,waterlogged=false] +54:5,minecraft:chest[facing=east,type=single,waterlogged=false] +54:9,minecraft:chest[facing=south,type=single,waterlogged=false] +54:10,minecraft:chest[facing=west,type=single,waterlogged=false] +54:11,minecraft:chest[facing=east,type=single,waterlogged=false] +54:15,minecraft:chest[facing=south,type=single,waterlogged=false] +55:0,minecraft:redstone_wire[east=none,north=none,power=0,south=none,west=none] +55:2,minecraft:redstone_wire[east=none,north=none,power=1,south=none,west=none] +55:3,minecraft:redstone_wire[east=none,north=none,power=2,south=none,west=none] +55:4,minecraft:redstone_wire[east=none,north=none,power=3,south=none,west=none] +55:5,minecraft:redstone_wire[east=none,north=none,power=4,south=none,west=none] +55:6,minecraft:redstone_wire[east=none,north=none,power=5,south=none,west=none] +55:7,minecraft:redstone_wire[east=none,north=none,power=6,south=none,west=none] +55:8,minecraft:redstone_wire[east=none,north=none,power=7,south=none,west=none] +55:9,minecraft:redstone_wire[east=none,north=none,power=8,south=none,west=none] +55:10,minecraft:redstone_wire[east=none,north=none,power=9,south=none,west=none] +55:11,minecraft:redstone_wire[east=none,north=none,power=10,south=none,west=none] +55:12,minecraft:redstone_wire[east=none,north=none,power=11,south=none,west=none] +55:13,minecraft:redstone_wire[east=none,north=none,power=12,south=none,west=none] +55:14,minecraft:redstone_wire[east=none,north=none,power=13,south=none,west=none] +55:15,minecraft:redstone_wire[east=none,north=none,power=14,south=none,west=none] +56:0,minecraft:diamond_ore +57:0,minecraft:diamond_block +58:0,minecraft:crafting_table +59:0,minecraft:wheat[age=0] +59:1,minecraft:wheat[age=1] +59:2,minecraft:wheat[age=2] +59:3,minecraft:wheat[age=3] +59:4,minecraft:wheat[age=4] +59:5,minecraft:wheat[age=5] +59:6,minecraft:wheat[age=6] +59:7,minecraft:wheat[age=7] +59:8,minecraft:air +59:9,minecraft:air +59:10,minecraft:air +59:11,minecraft:air +59:12,minecraft:air +59:13,minecraft:air +59:14,minecraft:air +59:15,minecraft:air +60:0,minecraft:farmland[moisture=0] +60:1,minecraft:farmland[moisture=1] +60:2,minecraft:farmland[moisture=2] +60:3,minecraft:farmland[moisture=3] +60:4,minecraft:farmland[moisture=4] +60:5,minecraft:farmland[moisture=5] +60:6,minecraft:farmland[moisture=6] +60:7,minecraft:farmland[moisture=7] +60:9,minecraft:farmland[moisture=1] +60:10,minecraft:farmland[moisture=2] +60:11,minecraft:farmland[moisture=3] +60:12,minecraft:farmland[moisture=4] +60:13,minecraft:farmland[moisture=5] +60:14,minecraft:farmland[moisture=6] +60:15,minecraft:farmland[moisture=7] +61:0,minecraft:furnace[facing=north,lit=false] +61:3,minecraft:furnace[facing=south,lit=false] +61:4,minecraft:furnace[facing=west,lit=false] +61:5,minecraft:furnace[facing=east,lit=false] +61:9,minecraft:furnace[facing=south,lit=false] +61:10,minecraft:furnace[facing=west,lit=false] +61:11,minecraft:furnace[facing=east,lit=false] +61:15,minecraft:furnace[facing=south,lit=false] +62:0,minecraft:furnace[facing=north,lit=false] +62:3,minecraft:furnace[facing=south,lit=false] +62:4,minecraft:furnace[facing=west,lit=false] +62:5,minecraft:furnace[facing=east,lit=false] +62:9,minecraft:furnace[facing=south,lit=false] +62:10,minecraft:furnace[facing=west,lit=false] +62:11,minecraft:furnace[facing=east,lit=false] +62:15,minecraft:furnace[facing=south,lit=false] +63:0,minecraft:oak_sign[rotation=0,waterlogged=false] +63:1,minecraft:oak_sign[rotation=1,waterlogged=false] +63:2,minecraft:oak_sign[rotation=2,waterlogged=false] +63:3,minecraft:oak_sign[rotation=3,waterlogged=false] +63:4,minecraft:oak_sign[rotation=4,waterlogged=false] +63:5,minecraft:oak_sign[rotation=5,waterlogged=false] +63:6,minecraft:oak_sign[rotation=6,waterlogged=false] +63:7,minecraft:oak_sign[rotation=7,waterlogged=false] +63:8,minecraft:oak_sign[rotation=8,waterlogged=false] +63:9,minecraft:oak_sign[rotation=9,waterlogged=false] +63:10,minecraft:oak_sign[rotation=10,waterlogged=false] +63:11,minecraft:oak_sign[rotation=11,waterlogged=false] +63:12,minecraft:oak_sign[rotation=12,waterlogged=false] +63:13,minecraft:oak_sign[rotation=13,waterlogged=false] +63:14,minecraft:oak_sign[rotation=14,waterlogged=false] +63:15,minecraft:oak_sign[rotation=15,waterlogged=false] +64:0,minecraft:oak_door[facing=east,half=lower,hinge=right,open=false,powered=false] +64:1,minecraft:oak_door[facing=south,half=lower,hinge=right,open=false,powered=false] +64:2,minecraft:oak_door[facing=west,half=lower,hinge=right,open=false,powered=false] +64:3,minecraft:oak_door[facing=north,half=lower,hinge=right,open=false,powered=false] +64:4,minecraft:oak_door[facing=east,half=lower,hinge=right,open=true,powered=false] +64:5,minecraft:oak_door[facing=south,half=lower,hinge=right,open=true,powered=false] +64:6,minecraft:oak_door[facing=west,half=lower,hinge=right,open=true,powered=false] +64:7,minecraft:oak_door[facing=north,half=lower,hinge=right,open=true,powered=false] +64:8,minecraft:oak_door[facing=east,half=upper,hinge=left,open=false,powered=false] +64:9,minecraft:oak_door[facing=east,half=upper,hinge=right,open=false,powered=false] +64:10,minecraft:oak_door[facing=east,half=upper,hinge=left,open=false,powered=true] +64:11,minecraft:oak_door[facing=east,half=upper,hinge=right,open=false,powered=true] +64:12,minecraft:oak_door[facing=east,half=upper,hinge=left,open=false,powered=false] +64:13,minecraft:oak_door[facing=east,half=upper,hinge=right,open=false,powered=false] +64:14,minecraft:oak_door[facing=east,half=upper,hinge=left,open=false,powered=true] +64:15,minecraft:oak_door[facing=east,half=upper,hinge=right,open=false,powered=true] +65:0,minecraft:ladder[facing=north,waterlogged=false] +65:3,minecraft:ladder[facing=south,waterlogged=false] +65:4,minecraft:ladder[facing=west,waterlogged=false] +65:5,minecraft:ladder[facing=east,waterlogged=false] +65:9,minecraft:ladder[facing=south,waterlogged=false] +65:10,minecraft:ladder[facing=west,waterlogged=false] +65:11,minecraft:ladder[facing=east,waterlogged=false] +65:15,minecraft:ladder[facing=south,waterlogged=false] +66:0,minecraft:rail[shape=north_south] +67:0,minecraft:cobblestone_stairs[facing=east,half=bottom,shape=straight,waterlogged=false] +67:1,minecraft:cobblestone_stairs[facing=west,half=bottom,shape=straight,waterlogged=false] +67:2,minecraft:cobblestone_stairs[facing=south,half=bottom,shape=straight,waterlogged=false] +67:3,minecraft:cobblestone_stairs[facing=north,half=bottom,shape=straight,waterlogged=false] +67:4,minecraft:cobblestone_stairs[facing=east,half=top,shape=straight,waterlogged=false] +67:5,minecraft:cobblestone_stairs[facing=west,half=top,shape=straight,waterlogged=false] +67:6,minecraft:cobblestone_stairs[facing=south,half=top,shape=straight,waterlogged=false] +67:7,minecraft:cobblestone_stairs[facing=north,half=top,shape=straight,waterlogged=false] +67:9,minecraft:cobblestone_stairs[facing=west,half=bottom,shape=straight,waterlogged=false] +67:10,minecraft:cobblestone_stairs[facing=south,half=bottom,shape=straight,waterlogged=false] +67:11,minecraft:cobblestone_stairs[facing=north,half=bottom,shape=straight,waterlogged=false] +67:12,minecraft:cobblestone_stairs[facing=east,half=top,shape=straight,waterlogged=false] +67:13,minecraft:cobblestone_stairs[facing=west,half=top,shape=straight,waterlogged=false] +67:14,minecraft:cobblestone_stairs[facing=south,half=top,shape=straight,waterlogged=false] +67:15,minecraft:cobblestone_stairs[facing=north,half=top,shape=straight,waterlogged=false] +68:0,minecraft:oak_wall_sign[facing=north,waterlogged=false] +68:3,minecraft:oak_wall_sign[facing=south,waterlogged=false] +68:4,minecraft:oak_wall_sign[facing=west,waterlogged=false] +68:5,minecraft:oak_wall_sign[facing=east,waterlogged=false] +68:9,minecraft:oak_wall_sign[facing=south,waterlogged=false] +68:10,minecraft:oak_wall_sign[facing=west,waterlogged=false] +68:11,minecraft:oak_wall_sign[facing=east,waterlogged=false] +68:15,minecraft:oak_wall_sign[facing=south,waterlogged=false] +69:0,minecraft:lever[face=ceiling,facing=west,powered=false] +69:1,minecraft:lever[face=wall,facing=east,powered=false] +69:2,minecraft:lever[face=wall,facing=west,powered=false] +69:3,minecraft:lever[face=wall,facing=south,powered=false] +69:4,minecraft:lever[face=wall,facing=north,powered=false] +69:5,minecraft:lever[face=floor,facing=north,powered=false] +69:6,minecraft:lever[face=floor,facing=west,powered=false] +69:7,minecraft:lever[face=ceiling,facing=north,powered=false] +69:8,minecraft:lever[face=ceiling,facing=west,powered=true] +69:9,minecraft:lever[face=wall,facing=east,powered=true] +69:10,minecraft:lever[face=wall,facing=west,powered=true] +69:11,minecraft:lever[face=wall,facing=south,powered=true] +69:12,minecraft:lever[face=wall,facing=north,powered=true] +69:13,minecraft:lever[face=floor,facing=north,powered=true] +69:14,minecraft:lever[face=floor,facing=west,powered=true] +69:15,minecraft:lever[face=ceiling,facing=north,powered=true] +70:0,minecraft:stone_pressure_plate[powered=false] +70:1,minecraft:stone_pressure_plate[powered=true] +71:0,minecraft:iron_door[facing=east,half=lower,hinge=right,open=false,powered=false] +71:1,minecraft:iron_door[facing=south,half=lower,hinge=right,open=false,powered=false] +71:2,minecraft:iron_door[facing=west,half=lower,hinge=right,open=false,powered=false] +71:3,minecraft:iron_door[facing=north,half=lower,hinge=right,open=false,powered=false] +71:4,minecraft:iron_door[facing=east,half=lower,hinge=right,open=true,powered=false] +71:5,minecraft:iron_door[facing=south,half=lower,hinge=right,open=true,powered=false] +71:6,minecraft:iron_door[facing=west,half=lower,hinge=right,open=true,powered=false] +71:7,minecraft:iron_door[facing=north,half=lower,hinge=right,open=true,powered=false] +71:8,minecraft:iron_door[facing=east,half=upper,hinge=left,open=false,powered=false] +71:9,minecraft:iron_door[facing=east,half=upper,hinge=right,open=false,powered=false] +71:10,minecraft:iron_door[facing=east,half=upper,hinge=left,open=false,powered=true] +71:11,minecraft:iron_door[facing=east,half=upper,hinge=right,open=false,powered=true] +71:12,minecraft:iron_door[facing=east,half=upper,hinge=left,open=false,powered=false] +71:13,minecraft:iron_door[facing=east,half=upper,hinge=right,open=false,powered=false] +71:14,minecraft:iron_door[facing=east,half=upper,hinge=left,open=false,powered=true] +71:15,minecraft:iron_door[facing=east,half=upper,hinge=right,open=false,powered=true] +72:0,minecraft:oak_pressure_plate[powered=false] +72:1,minecraft:oak_pressure_plate[powered=true] +73:0,minecraft:redstone_ore[lit=false] +74:0,minecraft:redstone_ore[lit=true] +75:0,minecraft:redstone_torch[lit=false] +75:1,minecraft:redstone_wall_torch[facing=east,lit=false] +75:2,minecraft:redstone_wall_torch[facing=west,lit=false] +75:3,minecraft:redstone_wall_torch[facing=south,lit=false] +75:4,minecraft:redstone_wall_torch[facing=north,lit=false] +76:0,minecraft:redstone_torch[lit=true] +76:1,minecraft:redstone_wall_torch[facing=east,lit=true] +76:2,minecraft:redstone_wall_torch[facing=west,lit=true] +76:3,minecraft:redstone_wall_torch[facing=south,lit=true] +76:4,minecraft:redstone_wall_torch[facing=north,lit=true] +77:0,minecraft:stone_button[face=ceiling,facing=north,powered=false] +77:1,minecraft:stone_button[face=wall,facing=east,powered=false] +77:2,minecraft:stone_button[face=wall,facing=west,powered=false] +77:3,minecraft:stone_button[face=wall,facing=south,powered=false] +77:4,minecraft:stone_button[face=wall,facing=north,powered=false] +77:5,minecraft:stone_button[face=floor,facing=north,powered=false] +77:6,minecraft:stone_button[face=floor,facing=north,powered=false] +77:7,minecraft:stone_button[face=floor,facing=north,powered=false] +77:8,minecraft:stone_button[face=ceiling,facing=north,powered=true] +77:9,minecraft:stone_button[face=wall,facing=east,powered=true] +77:10,minecraft:stone_button[face=wall,facing=west,powered=true] +77:11,minecraft:stone_button[face=wall,facing=south,powered=true] +77:12,minecraft:stone_button[face=wall,facing=north,powered=true] +77:13,minecraft:stone_button[face=floor,facing=north,powered=true] +77:14,minecraft:stone_button[face=floor,facing=north,powered=true] +77:15,minecraft:stone_button[face=floor,facing=north,powered=true] +78:0,minecraft:snow[layers=1] +78:1,minecraft:snow[layers=2] +78:2,minecraft:snow[layers=3] +78:3,minecraft:snow[layers=4] +78:4,minecraft:snow[layers=5] +78:5,minecraft:snow[layers=6] +78:6,minecraft:snow[layers=7] +78:7,minecraft:snow[layers=8] +78:9,minecraft:snow[layers=2] +78:10,minecraft:snow[layers=3] +78:11,minecraft:snow[layers=4] +78:12,minecraft:snow[layers=5] +78:13,minecraft:snow[layers=6] +78:14,minecraft:snow[layers=7] +78:15,minecraft:snow[layers=8] +79:0,minecraft:ice +80:0,minecraft:snow_block +81:0,minecraft:cactus[age=0] +81:1,minecraft:cactus[age=1] +81:2,minecraft:cactus[age=2] +81:3,minecraft:cactus[age=3] +81:4,minecraft:cactus[age=4] +81:5,minecraft:cactus[age=5] +81:6,minecraft:cactus[age=6] +81:7,minecraft:cactus[age=7] +81:8,minecraft:cactus[age=8] +81:9,minecraft:cactus[age=9] +81:10,minecraft:cactus[age=10] +81:11,minecraft:cactus[age=11] +81:12,minecraft:cactus[age=12] +81:13,minecraft:cactus[age=13] +81:14,minecraft:cactus[age=14] +81:15,minecraft:cactus[age=15] +82:0,minecraft:clay +83:0,minecraft:sugar_cane[age=0] +83:1,minecraft:sugar_cane[age=1] +83:2,minecraft:sugar_cane[age=2] +83:3,minecraft:sugar_cane[age=3] +83:4,minecraft:sugar_cane[age=4] +83:5,minecraft:sugar_cane[age=5] +83:6,minecraft:sugar_cane[age=6] +83:7,minecraft:sugar_cane[age=7] +83:8,minecraft:sugar_cane[age=8] +83:9,minecraft:sugar_cane[age=9] +83:10,minecraft:sugar_cane[age=10] +83:11,minecraft:sugar_cane[age=11] +83:12,minecraft:sugar_cane[age=12] +83:13,minecraft:sugar_cane[age=13] +83:14,minecraft:sugar_cane[age=14] +83:15,minecraft:sugar_cane[age=15] +84:0,minecraft:jukebox[has_record=false] +84:1,minecraft:jukebox[has_record=true] +84:2,minecraft:jukebox[has_record=true] +84:3,minecraft:jukebox[has_record=true] +84:4,minecraft:jukebox[has_record=true] +84:5,minecraft:jukebox[has_record=true] +84:6,minecraft:jukebox[has_record=true] +84:7,minecraft:jukebox[has_record=true] +84:8,minecraft:jukebox[has_record=true] +84:9,minecraft:jukebox[has_record=true] +84:10,minecraft:jukebox[has_record=true] +84:11,minecraft:jukebox[has_record=true] +84:12,minecraft:jukebox[has_record=true] +84:13,minecraft:jukebox[has_record=true] +84:14,minecraft:jukebox[has_record=true] +84:15,minecraft:jukebox[has_record=true] +85:0,minecraft:oak_fence[east=false,north=false,south=false,waterlogged=false,west=false] +86:0,minecraft:carved_pumpkin[facing=south] +86:1,minecraft:carved_pumpkin[facing=west] +86:2,minecraft:carved_pumpkin[facing=north] +86:3,minecraft:carved_pumpkin[facing=east] +86:5,minecraft:carved_pumpkin[facing=west] +86:6,minecraft:carved_pumpkin[facing=north] +86:7,minecraft:carved_pumpkin[facing=east] +86:9,minecraft:carved_pumpkin[facing=west] +86:10,minecraft:carved_pumpkin[facing=north] +86:11,minecraft:carved_pumpkin[facing=east] +86:13,minecraft:carved_pumpkin[facing=west] +86:14,minecraft:carved_pumpkin[facing=north] +86:15,minecraft:carved_pumpkin[facing=east] +87:0,minecraft:netherrack +88:0,minecraft:soul_sand +89:0,minecraft:glowstone +90:0,minecraft:nether_portal[axis=x] +90:2,minecraft:nether_portal[axis=z] +90:6,minecraft:nether_portal[axis=z] +90:10,minecraft:nether_portal[axis=z] +90:14,minecraft:nether_portal[axis=z] +91:0,minecraft:jack_o_lantern[facing=south] +91:1,minecraft:jack_o_lantern[facing=west] +91:2,minecraft:jack_o_lantern[facing=north] +91:3,minecraft:jack_o_lantern[facing=east] +91:5,minecraft:jack_o_lantern[facing=west] +91:6,minecraft:jack_o_lantern[facing=north] +91:7,minecraft:jack_o_lantern[facing=east] +91:9,minecraft:jack_o_lantern[facing=west] +91:10,minecraft:jack_o_lantern[facing=north] +91:11,minecraft:jack_o_lantern[facing=east] +91:13,minecraft:jack_o_lantern[facing=west] +91:14,minecraft:jack_o_lantern[facing=north] +91:15,minecraft:jack_o_lantern[facing=east] +92:0,minecraft:cake[bites=0] +92:1,minecraft:cake[bites=1] +92:2,minecraft:cake[bites=2] +92:3,minecraft:cake[bites=3] +92:4,minecraft:cake[bites=4] +92:5,minecraft:cake[bites=5] +92:6,minecraft:cake[bites=6] +92:7,minecraft:air +92:8,minecraft:air +92:9,minecraft:air +92:10,minecraft:air +92:11,minecraft:air +92:12,minecraft:air +92:13,minecraft:air +92:14,minecraft:air +92:15,minecraft:air +93:0,minecraft:repeater[delay=1,facing=south,locked=false,powered=false] +93:1,minecraft:repeater[delay=1,facing=west,locked=false,powered=false] +93:2,minecraft:repeater[delay=1,facing=north,locked=false,powered=false] +93:3,minecraft:repeater[delay=1,facing=east,locked=false,powered=false] +93:4,minecraft:repeater[delay=2,facing=south,locked=false,powered=false] +93:5,minecraft:repeater[delay=2,facing=west,locked=false,powered=false] +93:6,minecraft:repeater[delay=2,facing=north,locked=false,powered=false] +93:7,minecraft:repeater[delay=2,facing=east,locked=false,powered=false] +93:8,minecraft:repeater[delay=3,facing=south,locked=false,powered=false] +93:9,minecraft:repeater[delay=3,facing=west,locked=false,powered=false] +93:10,minecraft:repeater[delay=3,facing=north,locked=false,powered=false] +93:11,minecraft:repeater[delay=3,facing=east,locked=false,powered=false] +93:12,minecraft:repeater[delay=4,facing=south,locked=false,powered=false] +93:13,minecraft:repeater[delay=4,facing=west,locked=false,powered=false] +93:14,minecraft:repeater[delay=4,facing=north,locked=false,powered=false] +93:15,minecraft:repeater[delay=4,facing=east,locked=false,powered=false] +94:0,minecraft:repeater[delay=1,facing=south,locked=false,powered=true] +94:1,minecraft:repeater[delay=1,facing=west,locked=false,powered=true] +94:2,minecraft:repeater[delay=1,facing=north,locked=false,powered=true] +94:3,minecraft:repeater[delay=1,facing=east,locked=false,powered=true] +94:4,minecraft:repeater[delay=2,facing=south,locked=false,powered=true] +94:5,minecraft:repeater[delay=2,facing=west,locked=false,powered=true] +94:6,minecraft:repeater[delay=2,facing=north,locked=false,powered=true] +94:7,minecraft:repeater[delay=2,facing=east,locked=false,powered=true] +94:8,minecraft:repeater[delay=3,facing=south,locked=false,powered=true] +94:9,minecraft:repeater[delay=3,facing=west,locked=false,powered=true] +94:10,minecraft:repeater[delay=3,facing=north,locked=false,powered=true] +94:11,minecraft:repeater[delay=3,facing=east,locked=false,powered=true] +94:12,minecraft:repeater[delay=4,facing=south,locked=false,powered=true] +94:13,minecraft:repeater[delay=4,facing=west,locked=false,powered=true] +94:14,minecraft:repeater[delay=4,facing=north,locked=false,powered=true] +94:15,minecraft:repeater[delay=4,facing=east,locked=false,powered=true] +95:0,minecraft:white_stained_glass +95:1,minecraft:orange_stained_glass +95:2,minecraft:magenta_stained_glass +95:3,minecraft:light_blue_stained_glass +95:4,minecraft:yellow_stained_glass +95:5,minecraft:lime_stained_glass +95:6,minecraft:pink_stained_glass +95:7,minecraft:gray_stained_glass +95:8,minecraft:light_gray_stained_glass +95:9,minecraft:cyan_stained_glass +95:10,minecraft:purple_stained_glass +95:11,minecraft:blue_stained_glass +95:12,minecraft:brown_stained_glass +95:13,minecraft:green_stained_glass +95:14,minecraft:red_stained_glass +95:15,minecraft:black_stained_glass +96:0,minecraft:oak_trapdoor[facing=north,half=bottom,open=false,powered=false,waterlogged=false] +96:1,minecraft:oak_trapdoor[facing=south,half=bottom,open=false,powered=false,waterlogged=false] +96:2,minecraft:oak_trapdoor[facing=west,half=bottom,open=false,powered=false,waterlogged=false] +96:3,minecraft:oak_trapdoor[facing=east,half=bottom,open=false,powered=false,waterlogged=false] +96:4,minecraft:oak_trapdoor[facing=north,half=bottom,open=true,powered=false,waterlogged=false] +96:5,minecraft:oak_trapdoor[facing=south,half=bottom,open=true,powered=false,waterlogged=false] +96:6,minecraft:oak_trapdoor[facing=west,half=bottom,open=true,powered=false,waterlogged=false] +96:7,minecraft:oak_trapdoor[facing=east,half=bottom,open=true,powered=false,waterlogged=false] +96:8,minecraft:oak_trapdoor[facing=north,half=top,open=false,powered=false,waterlogged=false] +96:9,minecraft:oak_trapdoor[facing=south,half=top,open=false,powered=false,waterlogged=false] +96:10,minecraft:oak_trapdoor[facing=west,half=top,open=false,powered=false,waterlogged=false] +96:11,minecraft:oak_trapdoor[facing=east,half=top,open=false,powered=false,waterlogged=false] +96:12,minecraft:oak_trapdoor[facing=north,half=top,open=true,powered=false,waterlogged=false] +96:13,minecraft:oak_trapdoor[facing=south,half=top,open=true,powered=false,waterlogged=false] +96:14,minecraft:oak_trapdoor[facing=west,half=top,open=true,powered=false,waterlogged=false] +96:15,minecraft:oak_trapdoor[facing=east,half=top,open=true,powered=false,waterlogged=false] +97:0,minecraft:infested_stone +97:1,minecraft:infested_cobblestone +97:2,minecraft:infested_stone_bricks +97:3,minecraft:infested_mossy_stone_bricks +97:4,minecraft:infested_cracked_stone_bricks +97:5,minecraft:infested_chiseled_stone_bricks +98:0,minecraft:stone_bricks +98:1,minecraft:mossy_stone_bricks +98:2,minecraft:cracked_stone_bricks +98:3,minecraft:chiseled_stone_bricks +99:0,minecraft:brown_mushroom_block[down=false,east=false,north=false,south=false,up=false,west=false] +99:1,minecraft:brown_mushroom_block[down=false,east=false,north=true,south=false,up=true,west=true] +99:2,minecraft:brown_mushroom_block[down=false,east=false,north=true,south=false,up=true,west=false] +99:3,minecraft:brown_mushroom_block[down=false,east=true,north=true,south=false,up=true,west=false] +99:4,minecraft:brown_mushroom_block[down=false,east=false,north=false,south=false,up=true,west=true] +99:5,minecraft:brown_mushroom_block[down=false,east=false,north=false,south=false,up=true,west=false] +99:6,minecraft:brown_mushroom_block[down=false,east=true,north=false,south=false,up=true,west=false] +99:7,minecraft:brown_mushroom_block[down=false,east=false,north=false,south=true,up=true,west=true] +99:8,minecraft:brown_mushroom_block[down=false,east=false,north=false,south=true,up=true,west=false] +99:9,minecraft:brown_mushroom_block[down=false,east=true,north=false,south=true,up=true,west=false] +99:10,minecraft:mushroom_stem[down=false,east=true,north=true,south=true,up=false,west=true] +99:14,minecraft:brown_mushroom_block[down=true,east=true,north=true,south=true,up=true,west=true] +99:15,minecraft:mushroom_stem[down=true,east=true,north=true,south=true,up=true,west=true] +100:0,minecraft:red_mushroom_block[down=false,east=false,north=false,south=false,up=false,west=false] +100:1,minecraft:red_mushroom_block[down=false,east=false,north=true,south=false,up=true,west=true] +100:2,minecraft:red_mushroom_block[down=false,east=false,north=true,south=false,up=true,west=false] +100:3,minecraft:red_mushroom_block[down=false,east=true,north=true,south=false,up=true,west=false] +100:4,minecraft:red_mushroom_block[down=false,east=false,north=false,south=false,up=true,west=true] +100:5,minecraft:red_mushroom_block[down=false,east=false,north=false,south=false,up=true,west=false] +100:6,minecraft:red_mushroom_block[down=false,east=true,north=false,south=false,up=true,west=false] +100:7,minecraft:red_mushroom_block[down=false,east=false,north=false,south=true,up=true,west=true] +100:8,minecraft:red_mushroom_block[down=false,east=false,north=false,south=true,up=true,west=false] +100:9,minecraft:red_mushroom_block[down=false,east=true,north=false,south=true,up=true,west=false] +100:10,minecraft:mushroom_stem[down=false,east=true,north=true,south=true,up=false,west=true] +100:14,minecraft:red_mushroom_block[down=true,east=true,north=true,south=true,up=true,west=true] +100:15,minecraft:mushroom_stem[down=true,east=true,north=true,south=true,up=true,west=true] +101:0,minecraft:iron_bars[east=false,north=false,south=false,waterlogged=false,west=false] +102:0,minecraft:glass_pane[east=false,north=false,south=false,waterlogged=false,west=false] +103:0,minecraft:melon +104:0,minecraft:pumpkin_stem[age=0] +104:1,minecraft:pumpkin_stem[age=1] +104:2,minecraft:pumpkin_stem[age=2] +104:3,minecraft:pumpkin_stem[age=3] +104:4,minecraft:pumpkin_stem[age=4] +104:5,minecraft:pumpkin_stem[age=5] +104:6,minecraft:pumpkin_stem[age=6] +104:7,minecraft:pumpkin_stem[age=7] +104:8,minecraft:air +104:9,minecraft:air +104:10,minecraft:air +104:11,minecraft:air +104:12,minecraft:air +104:13,minecraft:air +104:14,minecraft:air +104:15,minecraft:air +105:0,minecraft:melon_stem[age=0] +105:1,minecraft:melon_stem[age=1] +105:2,minecraft:melon_stem[age=2] +105:3,minecraft:melon_stem[age=3] +105:4,minecraft:melon_stem[age=4] +105:5,minecraft:melon_stem[age=5] +105:6,minecraft:melon_stem[age=6] +105:7,minecraft:melon_stem[age=7] +105:8,minecraft:air +105:9,minecraft:air +105:10,minecraft:air +105:11,minecraft:air +105:12,minecraft:air +105:13,minecraft:air +105:14,minecraft:air +105:15,minecraft:air +106:0,minecraft:vine[east=false,north=false,south=false,up=true,west=false] +106:1,minecraft:vine[east=false,north=false,south=true,up=true,west=false] +106:2,minecraft:vine[east=false,north=false,south=false,up=true,west=true] +106:3,minecraft:vine[east=false,north=false,south=true,up=true,west=true] +106:4,minecraft:vine[east=false,north=true,south=false,up=true,west=false] +106:5,minecraft:vine[east=false,north=true,south=true,up=true,west=false] +106:6,minecraft:vine[east=false,north=true,south=false,up=true,west=true] +106:7,minecraft:vine[east=false,north=true,south=true,up=true,west=true] +106:8,minecraft:vine[east=true,north=false,south=false,up=true,west=false] +106:9,minecraft:vine[east=true,north=false,south=true,up=true,west=false] +106:10,minecraft:vine[east=true,north=false,south=false,up=true,west=true] +106:11,minecraft:vine[east=true,north=false,south=true,up=true,west=true] +106:12,minecraft:vine[east=true,north=true,south=false,up=true,west=false] +106:13,minecraft:vine[east=true,north=true,south=true,up=true,west=false] +106:14,minecraft:vine[east=true,north=true,south=false,up=true,west=true] +106:15,minecraft:vine[east=true,north=true,south=true,up=true,west=true] +107:0,minecraft:oak_fence_gate[facing=south,in_wall=false,open=false,powered=false] +107:1,minecraft:oak_fence_gate[facing=west,in_wall=false,open=false,powered=false] +107:2,minecraft:oak_fence_gate[facing=north,in_wall=false,open=false,powered=false] +107:3,minecraft:oak_fence_gate[facing=east,in_wall=false,open=false,powered=false] +107:4,minecraft:oak_fence_gate[facing=south,in_wall=false,open=true,powered=false] +107:5,minecraft:oak_fence_gate[facing=west,in_wall=false,open=true,powered=false] +107:6,minecraft:oak_fence_gate[facing=north,in_wall=false,open=true,powered=false] +107:7,minecraft:oak_fence_gate[facing=east,in_wall=false,open=true,powered=false] +107:8,minecraft:oak_fence_gate[facing=south,in_wall=false,open=false,powered=true] +107:9,minecraft:oak_fence_gate[facing=west,in_wall=false,open=false,powered=true] +107:10,minecraft:oak_fence_gate[facing=north,in_wall=false,open=false,powered=true] +107:11,minecraft:oak_fence_gate[facing=east,in_wall=false,open=false,powered=true] +107:12,minecraft:oak_fence_gate[facing=south,in_wall=false,open=true,powered=true] +107:13,minecraft:oak_fence_gate[facing=west,in_wall=false,open=true,powered=true] +107:14,minecraft:oak_fence_gate[facing=north,in_wall=false,open=true,powered=true] +107:15,minecraft:oak_fence_gate[facing=east,in_wall=false,open=true,powered=true] +108:0,minecraft:brick_stairs[facing=east,half=bottom,shape=straight,waterlogged=false] +108:1,minecraft:brick_stairs[facing=west,half=bottom,shape=straight,waterlogged=false] +108:2,minecraft:brick_stairs[facing=south,half=bottom,shape=straight,waterlogged=false] +108:3,minecraft:brick_stairs[facing=north,half=bottom,shape=straight,waterlogged=false] +108:4,minecraft:brick_stairs[facing=east,half=top,shape=straight,waterlogged=false] +108:5,minecraft:brick_stairs[facing=west,half=top,shape=straight,waterlogged=false] +108:6,minecraft:brick_stairs[facing=south,half=top,shape=straight,waterlogged=false] +108:7,minecraft:brick_stairs[facing=north,half=top,shape=straight,waterlogged=false] +108:9,minecraft:brick_stairs[facing=west,half=bottom,shape=straight,waterlogged=false] +108:10,minecraft:brick_stairs[facing=south,half=bottom,shape=straight,waterlogged=false] +108:11,minecraft:brick_stairs[facing=north,half=bottom,shape=straight,waterlogged=false] +108:12,minecraft:brick_stairs[facing=east,half=top,shape=straight,waterlogged=false] +108:13,minecraft:brick_stairs[facing=west,half=top,shape=straight,waterlogged=false] +108:14,minecraft:brick_stairs[facing=south,half=top,shape=straight,waterlogged=false] +108:15,minecraft:brick_stairs[facing=north,half=top,shape=straight,waterlogged=false] +109:0,minecraft:stone_brick_stairs[facing=east,half=bottom,shape=straight,waterlogged=false] +109:1,minecraft:stone_brick_stairs[facing=west,half=bottom,shape=straight,waterlogged=false] +109:2,minecraft:stone_brick_stairs[facing=south,half=bottom,shape=straight,waterlogged=false] +109:3,minecraft:stone_brick_stairs[facing=north,half=bottom,shape=straight,waterlogged=false] +109:4,minecraft:stone_brick_stairs[facing=east,half=top,shape=straight,waterlogged=false] +109:5,minecraft:stone_brick_stairs[facing=west,half=top,shape=straight,waterlogged=false] +109:6,minecraft:stone_brick_stairs[facing=south,half=top,shape=straight,waterlogged=false] +109:7,minecraft:stone_brick_stairs[facing=north,half=top,shape=straight,waterlogged=false] +109:9,minecraft:stone_brick_stairs[facing=west,half=bottom,shape=straight,waterlogged=false] +109:10,minecraft:stone_brick_stairs[facing=south,half=bottom,shape=straight,waterlogged=false] +109:11,minecraft:stone_brick_stairs[facing=north,half=bottom,shape=straight,waterlogged=false] +109:12,minecraft:stone_brick_stairs[facing=east,half=top,shape=straight,waterlogged=false] +109:13,minecraft:stone_brick_stairs[facing=west,half=top,shape=straight,waterlogged=false] +109:14,minecraft:stone_brick_stairs[facing=south,half=top,shape=straight,waterlogged=false] +109:15,minecraft:stone_brick_stairs[facing=north,half=top,shape=straight,waterlogged=false] +110:0,minecraft:mycelium[snowy=false] +111:0,minecraft:lily_pad +112:0,minecraft:nether_bricks +113:0,minecraft:nether_brick_fence[east=false,north=false,south=false,waterlogged=false,west=false] +114:0,minecraft:nether_brick_stairs[facing=east,half=bottom,shape=straight,waterlogged=false] +114:1,minecraft:nether_brick_stairs[facing=west,half=bottom,shape=straight,waterlogged=false] +114:2,minecraft:nether_brick_stairs[facing=south,half=bottom,shape=straight,waterlogged=false] +114:3,minecraft:nether_brick_stairs[facing=north,half=bottom,shape=straight,waterlogged=false] +114:4,minecraft:nether_brick_stairs[facing=east,half=top,shape=straight,waterlogged=false] +114:5,minecraft:nether_brick_stairs[facing=west,half=top,shape=straight,waterlogged=false] +114:6,minecraft:nether_brick_stairs[facing=south,half=top,shape=straight,waterlogged=false] +114:7,minecraft:nether_brick_stairs[facing=north,half=top,shape=straight,waterlogged=false] +114:9,minecraft:nether_brick_stairs[facing=west,half=bottom,shape=straight,waterlogged=false] +114:10,minecraft:nether_brick_stairs[facing=south,half=bottom,shape=straight,waterlogged=false] +114:11,minecraft:nether_brick_stairs[facing=north,half=bottom,shape=straight,waterlogged=false] +114:12,minecraft:nether_brick_stairs[facing=east,half=top,shape=straight,waterlogged=false] +114:13,minecraft:nether_brick_stairs[facing=west,half=top,shape=straight,waterlogged=false] +114:14,minecraft:nether_brick_stairs[facing=south,half=top,shape=straight,waterlogged=false] +114:15,minecraft:nether_brick_stairs[facing=north,half=top,shape=straight,waterlogged=false] +115:0,minecraft:nether_wart[age=0] +115:1,minecraft:nether_wart[age=1] +115:2,minecraft:nether_wart[age=2] +115:3,minecraft:nether_wart[age=3] +115:4,minecraft:air +115:5,minecraft:air +115:6,minecraft:air +115:7,minecraft:air +115:8,minecraft:air +115:9,minecraft:air +115:10,minecraft:air +115:11,minecraft:air +115:12,minecraft:air +115:13,minecraft:air +115:14,minecraft:air +115:15,minecraft:air +116:0,minecraft:enchanting_table +117:0,minecraft:brewing_stand[has_bottle_0=false,has_bottle_1=false,has_bottle_2=false] +118:0,minecraft:cauldron +118:1,minecraft:cauldron +118:2,minecraft:cauldron +118:3,minecraft:cauldron +118:4,minecraft:air +118:5,minecraft:air +118:6,minecraft:air +118:7,minecraft:air +118:8,minecraft:air +118:9,minecraft:air +118:10,minecraft:air +118:11,minecraft:air +118:12,minecraft:air +118:13,minecraft:air +118:14,minecraft:air +118:15,minecraft:air +119:0,minecraft:end_portal +120:0,minecraft:end_portal_frame[eye=false,facing=south] +120:1,minecraft:end_portal_frame[eye=false,facing=west] +120:2,minecraft:end_portal_frame[eye=false,facing=north] +120:3,minecraft:end_portal_frame[eye=false,facing=east] +120:4,minecraft:end_portal_frame[eye=true,facing=south] +120:5,minecraft:end_portal_frame[eye=true,facing=west] +120:6,minecraft:end_portal_frame[eye=true,facing=north] +120:7,minecraft:end_portal_frame[eye=true,facing=east] +120:9,minecraft:end_portal_frame[eye=false,facing=west] +120:10,minecraft:end_portal_frame[eye=false,facing=north] +120:11,minecraft:end_portal_frame[eye=false,facing=east] +120:12,minecraft:end_portal_frame[eye=true,facing=south] +120:13,minecraft:end_portal_frame[eye=true,facing=west] +120:14,minecraft:end_portal_frame[eye=true,facing=north] +120:15,minecraft:end_portal_frame[eye=true,facing=east] +121:0,minecraft:end_stone +122:0,minecraft:dragon_egg +123:0,minecraft:redstone_lamp[lit=false] +124:0,minecraft:redstone_lamp[lit=false] +125:0,minecraft:oak_slab[type=double,waterlogged=false] +125:1,minecraft:spruce_slab[type=double,waterlogged=false] +125:2,minecraft:birch_slab[type=double,waterlogged=false] +125:3,minecraft:jungle_slab[type=double,waterlogged=false] +125:4,minecraft:acacia_slab[type=double,waterlogged=false] +125:5,minecraft:dark_oak_slab[type=double,waterlogged=false] +125:9,minecraft:spruce_slab[type=double,waterlogged=false] +125:10,minecraft:birch_slab[type=double,waterlogged=false] +125:11,minecraft:jungle_slab[type=double,waterlogged=false] +125:12,minecraft:acacia_slab[type=double,waterlogged=false] +125:13,minecraft:dark_oak_slab[type=double,waterlogged=false] +126:0,minecraft:oak_slab[type=bottom,waterlogged=false] +126:1,minecraft:spruce_slab[type=bottom,waterlogged=false] +126:2,minecraft:birch_slab[type=bottom,waterlogged=false] +126:3,minecraft:jungle_slab[type=bottom,waterlogged=false] +126:4,minecraft:acacia_slab[type=bottom,waterlogged=false] +126:5,minecraft:dark_oak_slab[type=bottom,waterlogged=false] +126:8,minecraft:oak_slab[type=top,waterlogged=false] +126:9,minecraft:spruce_slab[type=top,waterlogged=false] +126:10,minecraft:birch_slab[type=top,waterlogged=false] +126:11,minecraft:jungle_slab[type=top,waterlogged=false] +126:12,minecraft:acacia_slab[type=top,waterlogged=false] +126:13,minecraft:dark_oak_slab[type=top,waterlogged=false] +126:14,minecraft:oak_slab[type=top,waterlogged=false] +126:15,minecraft:oak_slab[type=top,waterlogged=false] +127:0,minecraft:cocoa[age=0,facing=south] +127:1,minecraft:cocoa[age=0,facing=west] +127:2,minecraft:cocoa[age=0,facing=north] +127:3,minecraft:cocoa[age=0,facing=east] +127:4,minecraft:cocoa[age=1,facing=south] +127:5,minecraft:cocoa[age=1,facing=west] +127:6,minecraft:cocoa[age=1,facing=north] +127:7,minecraft:cocoa[age=1,facing=east] +127:8,minecraft:cocoa[age=2,facing=south] +127:9,minecraft:cocoa[age=2,facing=west] +127:10,minecraft:cocoa[age=2,facing=north] +127:11,minecraft:cocoa[age=2,facing=east] +127:12,minecraft:air +127:13,minecraft:air +127:14,minecraft:air +127:15,minecraft:air +128:0,minecraft:sandstone_stairs[facing=east,half=bottom,shape=straight,waterlogged=false] +128:1,minecraft:sandstone_stairs[facing=west,half=bottom,shape=straight,waterlogged=false] +128:2,minecraft:sandstone_stairs[facing=south,half=bottom,shape=straight,waterlogged=false] +128:3,minecraft:sandstone_stairs[facing=north,half=bottom,shape=straight,waterlogged=false] +128:4,minecraft:sandstone_stairs[facing=east,half=top,shape=straight,waterlogged=false] +128:5,minecraft:sandstone_stairs[facing=west,half=top,shape=straight,waterlogged=false] +128:6,minecraft:sandstone_stairs[facing=south,half=top,shape=straight,waterlogged=false] +128:7,minecraft:sandstone_stairs[facing=north,half=top,shape=straight,waterlogged=false] +128:9,minecraft:sandstone_stairs[facing=west,half=bottom,shape=straight,waterlogged=false] +128:10,minecraft:sandstone_stairs[facing=south,half=bottom,shape=straight,waterlogged=false] +128:11,minecraft:sandstone_stairs[facing=north,half=bottom,shape=straight,waterlogged=false] +128:12,minecraft:sandstone_stairs[facing=east,half=top,shape=straight,waterlogged=false] +128:13,minecraft:sandstone_stairs[facing=west,half=top,shape=straight,waterlogged=false] +128:14,minecraft:sandstone_stairs[facing=south,half=top,shape=straight,waterlogged=false] +128:15,minecraft:sandstone_stairs[facing=north,half=top,shape=straight,waterlogged=false] +129:0,minecraft:emerald_ore +130:0,minecraft:ender_chest[facing=north,waterlogged=false] +130:3,minecraft:ender_chest[facing=south,waterlogged=false] +130:4,minecraft:ender_chest[facing=west,waterlogged=false] +130:5,minecraft:ender_chest[facing=east,waterlogged=false] +130:9,minecraft:ender_chest[facing=south,waterlogged=false] +130:10,minecraft:ender_chest[facing=west,waterlogged=false] +130:11,minecraft:ender_chest[facing=east,waterlogged=false] +130:15,minecraft:ender_chest[facing=south,waterlogged=false] +131:0,minecraft:tripwire_hook[attached=false,facing=south,powered=false] +131:1,minecraft:tripwire_hook[attached=false,facing=west,powered=false] +131:2,minecraft:tripwire_hook[attached=false,facing=north,powered=false] +131:3,minecraft:tripwire_hook[attached=false,facing=east,powered=false] +131:4,minecraft:tripwire_hook[attached=true,facing=south,powered=false] +131:5,minecraft:tripwire_hook[attached=true,facing=west,powered=false] +131:6,minecraft:tripwire_hook[attached=true,facing=north,powered=false] +131:7,minecraft:tripwire_hook[attached=true,facing=east,powered=false] +131:8,minecraft:tripwire_hook[attached=false,facing=south,powered=true] +131:9,minecraft:tripwire_hook[attached=false,facing=west,powered=true] +131:10,minecraft:tripwire_hook[attached=false,facing=north,powered=true] +131:11,minecraft:tripwire_hook[attached=false,facing=east,powered=true] +131:12,minecraft:tripwire_hook[attached=true,facing=south,powered=true] +131:13,minecraft:tripwire_hook[attached=true,facing=west,powered=true] +131:14,minecraft:tripwire_hook[attached=true,facing=north,powered=true] +131:15,minecraft:tripwire_hook[attached=true,facing=east,powered=true] +132:0,minecraft:tripwire[attached=false,disarmed=false,east=false,north=false,powered=false,south=false,west=false] +132:1,minecraft:tripwire[attached=false,disarmed=false,east=false,north=false,powered=true,south=false,west=false] +132:3,minecraft:tripwire[attached=false,disarmed=false,east=false,north=false,powered=true,south=false,west=false] +132:4,minecraft:tripwire[attached=true,disarmed=false,east=false,north=false,powered=false,south=false,west=false] +132:5,minecraft:tripwire[attached=true,disarmed=false,east=false,north=false,powered=true,south=false,west=false] +132:6,minecraft:tripwire[attached=true,disarmed=false,east=false,north=false,powered=false,south=false,west=false] +132:7,minecraft:tripwire[attached=true,disarmed=false,east=false,north=false,powered=true,south=false,west=false] +132:8,minecraft:tripwire[attached=false,disarmed=true,east=false,north=false,powered=false,south=false,west=false] +132:9,minecraft:tripwire[attached=false,disarmed=true,east=false,north=false,powered=true,south=false,west=false] +132:10,minecraft:tripwire[attached=false,disarmed=true,east=false,north=false,powered=false,south=false,west=false] +132:11,minecraft:tripwire[attached=false,disarmed=true,east=false,north=false,powered=true,south=false,west=false] +132:12,minecraft:tripwire[attached=true,disarmed=true,east=false,north=false,powered=false,south=false,west=false] +132:13,minecraft:tripwire[attached=true,disarmed=true,east=false,north=false,powered=true,south=false,west=false] +132:14,minecraft:tripwire[attached=true,disarmed=true,east=false,north=false,powered=false,south=false,west=false] +132:15,minecraft:tripwire[attached=true,disarmed=true,east=false,north=false,powered=true,south=false,west=false] +133:0,minecraft:emerald_block +134:0,minecraft:spruce_stairs[facing=east,half=bottom,shape=straight,waterlogged=false] +134:1,minecraft:spruce_stairs[facing=west,half=bottom,shape=straight,waterlogged=false] +134:2,minecraft:spruce_stairs[facing=south,half=bottom,shape=straight,waterlogged=false] +134:3,minecraft:spruce_stairs[facing=north,half=bottom,shape=straight,waterlogged=false] +134:4,minecraft:spruce_stairs[facing=east,half=top,shape=straight,waterlogged=false] +134:5,minecraft:spruce_stairs[facing=west,half=top,shape=straight,waterlogged=false] +134:6,minecraft:spruce_stairs[facing=south,half=top,shape=straight,waterlogged=false] +134:7,minecraft:spruce_stairs[facing=north,half=top,shape=straight,waterlogged=false] +134:9,minecraft:spruce_stairs[facing=west,half=bottom,shape=straight,waterlogged=false] +134:10,minecraft:spruce_stairs[facing=south,half=bottom,shape=straight,waterlogged=false] +134:11,minecraft:spruce_stairs[facing=north,half=bottom,shape=straight,waterlogged=false] +134:12,minecraft:spruce_stairs[facing=east,half=top,shape=straight,waterlogged=false] +134:13,minecraft:spruce_stairs[facing=west,half=top,shape=straight,waterlogged=false] +134:14,minecraft:spruce_stairs[facing=south,half=top,shape=straight,waterlogged=false] +134:15,minecraft:spruce_stairs[facing=north,half=top,shape=straight,waterlogged=false] +135:0,minecraft:birch_stairs[facing=east,half=bottom,shape=straight,waterlogged=false] +135:1,minecraft:birch_stairs[facing=west,half=bottom,shape=straight,waterlogged=false] +135:2,minecraft:birch_stairs[facing=south,half=bottom,shape=straight,waterlogged=false] +135:3,minecraft:birch_stairs[facing=north,half=bottom,shape=straight,waterlogged=false] +135:4,minecraft:birch_stairs[facing=east,half=top,shape=straight,waterlogged=false] +135:5,minecraft:birch_stairs[facing=west,half=top,shape=straight,waterlogged=false] +135:6,minecraft:birch_stairs[facing=south,half=top,shape=straight,waterlogged=false] +135:7,minecraft:birch_stairs[facing=north,half=top,shape=straight,waterlogged=false] +135:9,minecraft:birch_stairs[facing=west,half=bottom,shape=straight,waterlogged=false] +135:10,minecraft:birch_stairs[facing=south,half=bottom,shape=straight,waterlogged=false] +135:11,minecraft:birch_stairs[facing=north,half=bottom,shape=straight,waterlogged=false] +135:12,minecraft:birch_stairs[facing=east,half=top,shape=straight,waterlogged=false] +135:13,minecraft:birch_stairs[facing=west,half=top,shape=straight,waterlogged=false] +135:14,minecraft:birch_stairs[facing=south,half=top,shape=straight,waterlogged=false] +135:15,minecraft:birch_stairs[facing=north,half=top,shape=straight,waterlogged=false] +136:0,minecraft:jungle_stairs[facing=east,half=bottom,shape=straight,waterlogged=false] +136:1,minecraft:jungle_stairs[facing=west,half=bottom,shape=straight,waterlogged=false] +136:2,minecraft:jungle_stairs[facing=south,half=bottom,shape=straight,waterlogged=false] +136:3,minecraft:jungle_stairs[facing=north,half=bottom,shape=straight,waterlogged=false] +136:4,minecraft:jungle_stairs[facing=east,half=top,shape=straight,waterlogged=false] +136:5,minecraft:jungle_stairs[facing=west,half=top,shape=straight,waterlogged=false] +136:6,minecraft:jungle_stairs[facing=south,half=top,shape=straight,waterlogged=false] +136:7,minecraft:jungle_stairs[facing=north,half=top,shape=straight,waterlogged=false] +136:9,minecraft:jungle_stairs[facing=west,half=bottom,shape=straight,waterlogged=false] +136:10,minecraft:jungle_stairs[facing=south,half=bottom,shape=straight,waterlogged=false] +136:11,minecraft:jungle_stairs[facing=north,half=bottom,shape=straight,waterlogged=false] +136:12,minecraft:jungle_stairs[facing=east,half=top,shape=straight,waterlogged=false] +136:13,minecraft:jungle_stairs[facing=west,half=top,shape=straight,waterlogged=false] +136:14,minecraft:jungle_stairs[facing=south,half=top,shape=straight,waterlogged=false] +136:15,minecraft:jungle_stairs[facing=north,half=top,shape=straight,waterlogged=false] +137:0,minecraft:command_block[conditional=false,facing=down] +137:1,minecraft:command_block[conditional=false,facing=up] +137:2,minecraft:command_block[conditional=false,facing=north] +137:3,minecraft:command_block[conditional=false,facing=south] +137:4,minecraft:command_block[conditional=false,facing=west] +137:5,minecraft:command_block[conditional=false,facing=east] +137:7,minecraft:command_block[conditional=false,facing=up] +137:8,minecraft:command_block[conditional=true,facing=down] +137:9,minecraft:command_block[conditional=true,facing=up] +137:10,minecraft:command_block[conditional=true,facing=north] +137:11,minecraft:command_block[conditional=true,facing=south] +137:12,minecraft:command_block[conditional=true,facing=west] +137:13,minecraft:command_block[conditional=true,facing=east] +137:14,minecraft:command_block[conditional=true,facing=down] +137:15,minecraft:command_block[conditional=true,facing=up] +138:0,minecraft:beacon +139:0,minecraft:cobblestone_wall[east=none,north=none,south=none,up=false,waterlogged=false,west=none] +139:1,minecraft:mossy_cobblestone_wall[east=none,north=none,south=none,up=false,waterlogged=false,west=none] +140:0,minecraft:flower_pot +141:0,minecraft:carrots[age=0] +141:1,minecraft:carrots[age=1] +141:2,minecraft:carrots[age=2] +141:3,minecraft:carrots[age=3] +141:4,minecraft:carrots[age=4] +141:5,minecraft:carrots[age=5] +141:6,minecraft:carrots[age=6] +141:7,minecraft:carrots[age=7] +141:8,minecraft:air +141:9,minecraft:air +141:10,minecraft:air +141:11,minecraft:air +141:12,minecraft:air +141:13,minecraft:air +141:14,minecraft:air +141:15,minecraft:air +142:0,minecraft:potatoes[age=0] +142:1,minecraft:potatoes[age=1] +142:2,minecraft:potatoes[age=2] +142:3,minecraft:potatoes[age=3] +142:4,minecraft:potatoes[age=4] +142:5,minecraft:potatoes[age=5] +142:6,minecraft:potatoes[age=6] +142:7,minecraft:potatoes[age=7] +142:8,minecraft:air +142:9,minecraft:air +142:10,minecraft:air +142:11,minecraft:air +142:12,minecraft:air +142:13,minecraft:air +142:14,minecraft:air +142:15,minecraft:air +143:0,minecraft:oak_button[face=ceiling,facing=north,powered=false] +143:1,minecraft:oak_button[face=wall,facing=east,powered=false] +143:2,minecraft:oak_button[face=wall,facing=west,powered=false] +143:3,minecraft:oak_button[face=wall,facing=south,powered=false] +143:4,minecraft:oak_button[face=wall,facing=north,powered=false] +143:5,minecraft:oak_button[face=floor,facing=north,powered=false] +143:6,minecraft:oak_button[face=floor,facing=north,powered=false] +143:7,minecraft:oak_button[face=floor,facing=north,powered=false] +143:8,minecraft:oak_button[face=ceiling,facing=north,powered=true] +143:9,minecraft:oak_button[face=wall,facing=east,powered=true] +143:10,minecraft:oak_button[face=wall,facing=west,powered=true] +143:11,minecraft:oak_button[face=wall,facing=south,powered=true] +143:12,minecraft:oak_button[face=wall,facing=north,powered=true] +143:13,minecraft:oak_button[face=floor,facing=north,powered=true] +143:14,minecraft:oak_button[face=floor,facing=north,powered=true] +143:15,minecraft:oak_button[face=floor,facing=north,powered=true] +144:0,minecraft:skeleton_skull[rotation=0] +144:2,minecraft:skeleton_wall_skull[facing=north] +144:3,minecraft:skeleton_wall_skull[facing=south] +144:4,minecraft:skeleton_wall_skull[facing=west] +144:5,minecraft:skeleton_wall_skull[facing=east] +144:10,minecraft:skeleton_wall_skull[facing=north] +144:11,minecraft:skeleton_wall_skull[facing=south] +144:12,minecraft:skeleton_wall_skull[facing=west] +144:13,minecraft:skeleton_wall_skull[facing=east] +145:0,minecraft:anvil[facing=south] +145:1,minecraft:anvil[facing=west] +145:2,minecraft:anvil[facing=north] +145:3,minecraft:anvil[facing=east] +145:4,minecraft:chipped_anvil[facing=south] +145:5,minecraft:chipped_anvil[facing=west] +145:6,minecraft:chipped_anvil[facing=north] +145:7,minecraft:chipped_anvil[facing=east] +145:8,minecraft:damaged_anvil[facing=south] +145:9,minecraft:damaged_anvil[facing=west] +145:10,minecraft:damaged_anvil[facing=north] +145:11,minecraft:damaged_anvil[facing=east] +145:12,minecraft:air +145:13,minecraft:air +145:14,minecraft:air +145:15,minecraft:air +146:0,minecraft:trapped_chest[facing=north,type=single,waterlogged=false] +146:3,minecraft:trapped_chest[facing=south,type=single,waterlogged=false] +146:4,minecraft:trapped_chest[facing=west,type=single,waterlogged=false] +146:5,minecraft:trapped_chest[facing=east,type=single,waterlogged=false] +146:9,minecraft:trapped_chest[facing=south,type=single,waterlogged=false] +146:10,minecraft:trapped_chest[facing=west,type=single,waterlogged=false] +146:11,minecraft:trapped_chest[facing=east,type=single,waterlogged=false] +146:15,minecraft:trapped_chest[facing=south,type=single,waterlogged=false] +147:0,minecraft:light_weighted_pressure_plate[power=0] +147:1,minecraft:light_weighted_pressure_plate[power=1] +147:2,minecraft:light_weighted_pressure_plate[power=2] +147:3,minecraft:light_weighted_pressure_plate[power=3] +147:4,minecraft:light_weighted_pressure_plate[power=4] +147:5,minecraft:light_weighted_pressure_plate[power=5] +147:6,minecraft:light_weighted_pressure_plate[power=6] +147:7,minecraft:light_weighted_pressure_plate[power=7] +147:8,minecraft:light_weighted_pressure_plate[power=8] +147:9,minecraft:light_weighted_pressure_plate[power=9] +147:10,minecraft:light_weighted_pressure_plate[power=10] +147:11,minecraft:light_weighted_pressure_plate[power=11] +147:12,minecraft:light_weighted_pressure_plate[power=12] +147:13,minecraft:light_weighted_pressure_plate[power=13] +147:14,minecraft:light_weighted_pressure_plate[power=14] +147:15,minecraft:light_weighted_pressure_plate[power=15] +148:0,minecraft:heavy_weighted_pressure_plate[power=0] +148:1,minecraft:heavy_weighted_pressure_plate[power=1] +148:2,minecraft:heavy_weighted_pressure_plate[power=2] +148:3,minecraft:heavy_weighted_pressure_plate[power=3] +148:4,minecraft:heavy_weighted_pressure_plate[power=4] +148:5,minecraft:heavy_weighted_pressure_plate[power=5] +148:6,minecraft:heavy_weighted_pressure_plate[power=6] +148:7,minecraft:heavy_weighted_pressure_plate[power=7] +148:8,minecraft:heavy_weighted_pressure_plate[power=8] +148:9,minecraft:heavy_weighted_pressure_plate[power=9] +148:10,minecraft:heavy_weighted_pressure_plate[power=10] +148:11,minecraft:heavy_weighted_pressure_plate[power=11] +148:12,minecraft:heavy_weighted_pressure_plate[power=12] +148:13,minecraft:heavy_weighted_pressure_plate[power=13] +148:14,minecraft:heavy_weighted_pressure_plate[power=14] +148:15,minecraft:heavy_weighted_pressure_plate[power=15] +149:0,minecraft:comparator[facing=south,mode=compare,powered=false] +149:1,minecraft:comparator[facing=west,mode=compare,powered=false] +149:2,minecraft:comparator[facing=north,mode=compare,powered=false] +149:3,minecraft:comparator[facing=east,mode=compare,powered=false] +149:4,minecraft:comparator[facing=south,mode=subtract,powered=false] +149:5,minecraft:comparator[facing=west,mode=subtract,powered=false] +149:6,minecraft:comparator[facing=north,mode=subtract,powered=false] +149:7,minecraft:comparator[facing=east,mode=subtract,powered=false] +149:8,minecraft:comparator[facing=south,mode=compare,powered=true] +149:9,minecraft:comparator[facing=west,mode=compare,powered=true] +149:10,minecraft:comparator[facing=north,mode=compare,powered=true] +149:11,minecraft:comparator[facing=east,mode=compare,powered=true] +149:12,minecraft:comparator[facing=south,mode=subtract,powered=true] +149:13,minecraft:comparator[facing=west,mode=subtract,powered=true] +149:14,minecraft:comparator[facing=north,mode=subtract,powered=true] +149:15,minecraft:comparator[facing=east,mode=subtract,powered=true] +150:0,minecraft:comparator[facing=south,mode=compare,powered=false] +150:1,minecraft:comparator[facing=west,mode=compare,powered=false] +150:2,minecraft:comparator[facing=north,mode=compare,powered=false] +150:3,minecraft:comparator[facing=east,mode=compare,powered=false] +150:4,minecraft:comparator[facing=south,mode=subtract,powered=false] +150:5,minecraft:comparator[facing=west,mode=subtract,powered=false] +150:6,minecraft:comparator[facing=north,mode=subtract,powered=false] +150:7,minecraft:comparator[facing=east,mode=subtract,powered=false] +150:8,minecraft:comparator[facing=south,mode=compare,powered=true] +150:9,minecraft:comparator[facing=west,mode=compare,powered=true] +150:10,minecraft:comparator[facing=north,mode=compare,powered=true] +150:11,minecraft:comparator[facing=east,mode=compare,powered=true] +150:12,minecraft:comparator[facing=south,mode=subtract,powered=true] +150:13,minecraft:comparator[facing=west,mode=subtract,powered=true] +150:14,minecraft:comparator[facing=north,mode=subtract,powered=true] +150:15,minecraft:comparator[facing=east,mode=subtract,powered=true] +151:0,minecraft:daylight_detector[inverted=false,power=0] +151:1,minecraft:daylight_detector[inverted=false,power=1] +151:2,minecraft:daylight_detector[inverted=false,power=2] +151:3,minecraft:daylight_detector[inverted=false,power=3] +151:4,minecraft:daylight_detector[inverted=false,power=4] +151:5,minecraft:daylight_detector[inverted=false,power=5] +151:6,minecraft:daylight_detector[inverted=false,power=6] +151:7,minecraft:daylight_detector[inverted=false,power=7] +151:8,minecraft:daylight_detector[inverted=false,power=8] +151:9,minecraft:daylight_detector[inverted=false,power=9] +151:10,minecraft:daylight_detector[inverted=false,power=10] +151:11,minecraft:daylight_detector[inverted=false,power=11] +151:12,minecraft:daylight_detector[inverted=false,power=12] +151:13,minecraft:daylight_detector[inverted=false,power=13] +151:14,minecraft:daylight_detector[inverted=false,power=14] +151:15,minecraft:daylight_detector[inverted=false,power=15] +152:0,minecraft:redstone_block +153:0,minecraft:nether_quartz_ore +154:0,minecraft:hopper[enabled=true,facing=down] +154:1,minecraft:air +154:2,minecraft:hopper[enabled=true,facing=north] +154:3,minecraft:hopper[enabled=true,facing=south] +154:4,minecraft:hopper[enabled=true,facing=west] +154:5,minecraft:hopper[enabled=true,facing=east] +154:7,minecraft:air +154:9,minecraft:air +154:10,minecraft:hopper[enabled=true,facing=north] +154:11,minecraft:hopper[enabled=true,facing=south] +154:12,minecraft:hopper[enabled=true,facing=west] +154:13,minecraft:hopper[enabled=true,facing=east] +154:15,minecraft:air +155:0,minecraft:quartz_block +155:1,minecraft:chiseled_quartz_block +155:2,minecraft:quartz_pillar[axis=y] +155:3,minecraft:quartz_pillar[axis=x] +155:4,minecraft:quartz_pillar[axis=z] +156:0,minecraft:quartz_stairs[facing=east,half=bottom,shape=straight,waterlogged=false] +156:1,minecraft:quartz_stairs[facing=west,half=bottom,shape=straight,waterlogged=false] +156:2,minecraft:quartz_stairs[facing=south,half=bottom,shape=straight,waterlogged=false] +156:3,minecraft:quartz_stairs[facing=north,half=bottom,shape=straight,waterlogged=false] +156:4,minecraft:quartz_stairs[facing=east,half=top,shape=straight,waterlogged=false] +156:5,minecraft:quartz_stairs[facing=west,half=top,shape=straight,waterlogged=false] +156:6,minecraft:quartz_stairs[facing=south,half=top,shape=straight,waterlogged=false] +156:7,minecraft:quartz_stairs[facing=north,half=top,shape=straight,waterlogged=false] +156:9,minecraft:quartz_stairs[facing=west,half=bottom,shape=straight,waterlogged=false] +156:10,minecraft:quartz_stairs[facing=south,half=bottom,shape=straight,waterlogged=false] +156:11,minecraft:quartz_stairs[facing=north,half=bottom,shape=straight,waterlogged=false] +156:12,minecraft:quartz_stairs[facing=east,half=top,shape=straight,waterlogged=false] +156:13,minecraft:quartz_stairs[facing=west,half=top,shape=straight,waterlogged=false] +156:14,minecraft:quartz_stairs[facing=south,half=top,shape=straight,waterlogged=false] +156:15,minecraft:quartz_stairs[facing=north,half=top,shape=straight,waterlogged=false] +157:0,minecraft:activator_rail[powered=false,shape=north_south] +157:6,minecraft:air +157:7,minecraft:air +157:14,minecraft:air +157:15,minecraft:air +158:0,minecraft:dropper[facing=down,triggered=false] +158:1,minecraft:dropper[facing=up,triggered=false] +158:2,minecraft:dropper[facing=north,triggered=false] +158:3,minecraft:dropper[facing=south,triggered=false] +158:4,minecraft:dropper[facing=west,triggered=false] +158:5,minecraft:dropper[facing=east,triggered=false] +158:7,minecraft:dropper[facing=up,triggered=false] +158:8,minecraft:dropper[facing=down,triggered=true] +158:9,minecraft:dropper[facing=up,triggered=true] +158:10,minecraft:dropper[facing=north,triggered=true] +158:11,minecraft:dropper[facing=south,triggered=true] +158:12,minecraft:dropper[facing=west,triggered=true] +158:13,minecraft:dropper[facing=east,triggered=true] +158:14,minecraft:dropper[facing=down,triggered=true] +158:15,minecraft:dropper[facing=up,triggered=true] +159:0,minecraft:white_terracotta +159:1,minecraft:orange_terracotta +159:2,minecraft:magenta_terracotta +159:3,minecraft:light_blue_terracotta +159:4,minecraft:yellow_terracotta +159:5,minecraft:lime_terracotta +159:6,minecraft:pink_terracotta +159:7,minecraft:gray_terracotta +159:8,minecraft:light_gray_terracotta +159:9,minecraft:cyan_terracotta +159:10,minecraft:purple_terracotta +159:11,minecraft:blue_terracotta +159:12,minecraft:brown_terracotta +159:13,minecraft:green_terracotta +159:14,minecraft:red_terracotta +159:15,minecraft:black_terracotta +160:0,minecraft:white_stained_glass_pane[east=false,north=false,south=false,waterlogged=false,west=false] +160:1,minecraft:orange_stained_glass_pane[east=false,north=false,south=false,waterlogged=false,west=false] +160:2,minecraft:magenta_stained_glass_pane[east=false,north=false,south=false,waterlogged=false,west=false] +160:3,minecraft:light_blue_stained_glass_pane[east=false,north=false,south=false,waterlogged=false,west=false] +160:4,minecraft:yellow_stained_glass_pane[east=false,north=false,south=false,waterlogged=false,west=false] +160:5,minecraft:lime_stained_glass_pane[east=false,north=false,south=false,waterlogged=false,west=false] +160:6,minecraft:pink_stained_glass_pane[east=false,north=false,south=false,waterlogged=false,west=false] +160:7,minecraft:gray_stained_glass_pane[east=false,north=false,south=false,waterlogged=false,west=false] +160:8,minecraft:light_gray_stained_glass_pane[east=false,north=false,south=false,waterlogged=false,west=false] +160:9,minecraft:cyan_stained_glass_pane[east=false,north=false,south=false,waterlogged=false,west=false] +160:10,minecraft:purple_stained_glass_pane[east=false,north=false,south=false,waterlogged=false,west=false] +160:11,minecraft:blue_stained_glass_pane[east=false,north=false,south=false,waterlogged=false,west=false] +160:12,minecraft:brown_stained_glass_pane[east=false,north=false,south=false,waterlogged=false,west=false] +160:13,minecraft:green_stained_glass_pane[east=false,north=false,south=false,waterlogged=false,west=false] +160:14,minecraft:red_stained_glass_pane[east=false,north=false,south=false,waterlogged=false,west=false] +160:15,minecraft:black_stained_glass_pane[east=false,north=false,south=false,waterlogged=false,west=false] +161:0,minecraft:acacia_leaves[distance=7,persistent=false] +161:1,minecraft:dark_oak_leaves[distance=7,persistent=false] +161:2,minecraft:air +161:3,minecraft:air +161:4,minecraft:acacia_leaves[distance=7,persistent=true] +161:5,minecraft:dark_oak_leaves[distance=7,persistent=true] +161:6,minecraft:air +161:7,minecraft:air +161:9,minecraft:dark_oak_leaves[distance=7,persistent=false] +161:10,minecraft:air +161:11,minecraft:air +161:12,minecraft:acacia_leaves[distance=7,persistent=true] +161:13,minecraft:dark_oak_leaves[distance=7,persistent=true] +161:14,minecraft:air +161:15,minecraft:air +162:0,minecraft:acacia_log[axis=y] +162:1,minecraft:dark_oak_log[axis=y] +162:2,minecraft:air +162:3,minecraft:air +162:4,minecraft:acacia_log[axis=x] +162:5,minecraft:dark_oak_log[axis=x] +162:6,minecraft:air +162:7,minecraft:air +162:8,minecraft:acacia_log[axis=z] +162:9,minecraft:dark_oak_log[axis=z] +162:10,minecraft:air +162:11,minecraft:air +162:12,minecraft:acacia_wood[axis=y] +162:13,minecraft:dark_oak_wood[axis=y] +162:14,minecraft:air +162:15,minecraft:air +163:0,minecraft:acacia_stairs[facing=east,half=bottom,shape=straight,waterlogged=false] +163:1,minecraft:acacia_stairs[facing=west,half=bottom,shape=straight,waterlogged=false] +163:2,minecraft:acacia_stairs[facing=south,half=bottom,shape=straight,waterlogged=false] +163:3,minecraft:acacia_stairs[facing=north,half=bottom,shape=straight,waterlogged=false] +163:4,minecraft:acacia_stairs[facing=east,half=top,shape=straight,waterlogged=false] +163:5,minecraft:acacia_stairs[facing=west,half=top,shape=straight,waterlogged=false] +163:6,minecraft:acacia_stairs[facing=south,half=top,shape=straight,waterlogged=false] +163:7,minecraft:acacia_stairs[facing=north,half=top,shape=straight,waterlogged=false] +163:9,minecraft:acacia_stairs[facing=west,half=bottom,shape=straight,waterlogged=false] +163:10,minecraft:acacia_stairs[facing=south,half=bottom,shape=straight,waterlogged=false] +163:11,minecraft:acacia_stairs[facing=north,half=bottom,shape=straight,waterlogged=false] +163:12,minecraft:acacia_stairs[facing=east,half=top,shape=straight,waterlogged=false] +163:13,minecraft:acacia_stairs[facing=west,half=top,shape=straight,waterlogged=false] +163:14,minecraft:acacia_stairs[facing=south,half=top,shape=straight,waterlogged=false] +163:15,minecraft:acacia_stairs[facing=north,half=top,shape=straight,waterlogged=false] +164:0,minecraft:dark_oak_stairs[facing=east,half=bottom,shape=straight,waterlogged=false] +164:1,minecraft:dark_oak_stairs[facing=west,half=bottom,shape=straight,waterlogged=false] +164:2,minecraft:dark_oak_stairs[facing=south,half=bottom,shape=straight,waterlogged=false] +164:3,minecraft:dark_oak_stairs[facing=north,half=bottom,shape=straight,waterlogged=false] +164:4,minecraft:dark_oak_stairs[facing=east,half=top,shape=straight,waterlogged=false] +164:5,minecraft:dark_oak_stairs[facing=west,half=top,shape=straight,waterlogged=false] +164:6,minecraft:dark_oak_stairs[facing=south,half=top,shape=straight,waterlogged=false] +164:7,minecraft:dark_oak_stairs[facing=north,half=top,shape=straight,waterlogged=false] +164:9,minecraft:dark_oak_stairs[facing=west,half=bottom,shape=straight,waterlogged=false] +164:10,minecraft:dark_oak_stairs[facing=south,half=bottom,shape=straight,waterlogged=false] +164:11,minecraft:dark_oak_stairs[facing=north,half=bottom,shape=straight,waterlogged=false] +164:12,minecraft:dark_oak_stairs[facing=east,half=top,shape=straight,waterlogged=false] +164:13,minecraft:dark_oak_stairs[facing=west,half=top,shape=straight,waterlogged=false] +164:14,minecraft:dark_oak_stairs[facing=south,half=top,shape=straight,waterlogged=false] +164:15,minecraft:dark_oak_stairs[facing=north,half=top,shape=straight,waterlogged=false] +165:0,minecraft:slime_block +166:0,minecraft:barrier +167:0,minecraft:iron_trapdoor[facing=north,half=bottom,open=false,powered=false,waterlogged=false] +167:1,minecraft:iron_trapdoor[facing=south,half=bottom,open=false,powered=false,waterlogged=false] +167:2,minecraft:iron_trapdoor[facing=west,half=bottom,open=false,powered=false,waterlogged=false] +167:3,minecraft:iron_trapdoor[facing=east,half=bottom,open=false,powered=false,waterlogged=false] +167:4,minecraft:iron_trapdoor[facing=north,half=bottom,open=true,powered=false,waterlogged=false] +167:5,minecraft:iron_trapdoor[facing=south,half=bottom,open=true,powered=false,waterlogged=false] +167:6,minecraft:iron_trapdoor[facing=west,half=bottom,open=true,powered=false,waterlogged=false] +167:7,minecraft:iron_trapdoor[facing=east,half=bottom,open=true,powered=false,waterlogged=false] +167:8,minecraft:iron_trapdoor[facing=north,half=top,open=false,powered=false,waterlogged=false] +167:9,minecraft:iron_trapdoor[facing=south,half=top,open=false,powered=false,waterlogged=false] +167:10,minecraft:iron_trapdoor[facing=west,half=top,open=false,powered=false,waterlogged=false] +167:11,minecraft:iron_trapdoor[facing=east,half=top,open=false,powered=false,waterlogged=false] +167:12,minecraft:iron_trapdoor[facing=north,half=top,open=true,powered=false,waterlogged=false] +167:13,minecraft:iron_trapdoor[facing=south,half=top,open=true,powered=false,waterlogged=false] +167:14,minecraft:iron_trapdoor[facing=west,half=top,open=true,powered=false,waterlogged=false] +167:15,minecraft:iron_trapdoor[facing=east,half=top,open=true,powered=false,waterlogged=false] +168:0,minecraft:prismarine +168:1,minecraft:prismarine_bricks +168:2,minecraft:dark_prismarine +169:0,minecraft:sea_lantern +170:0,minecraft:hay_block[axis=y] +170:4,minecraft:hay_block[axis=x] +170:5,minecraft:hay_block[axis=x] +170:6,minecraft:hay_block[axis=x] +170:7,minecraft:hay_block[axis=x] +170:8,minecraft:hay_block[axis=z] +170:9,minecraft:hay_block[axis=z] +170:10,minecraft:hay_block[axis=z] +170:11,minecraft:hay_block[axis=z] +171:0,minecraft:white_carpet +171:1,minecraft:orange_carpet +171:2,minecraft:magenta_carpet +171:3,minecraft:light_blue_carpet +171:4,minecraft:yellow_carpet +171:5,minecraft:lime_carpet +171:6,minecraft:pink_carpet +171:7,minecraft:gray_carpet +171:8,minecraft:light_gray_carpet +171:9,minecraft:cyan_carpet +171:10,minecraft:purple_carpet +171:11,minecraft:blue_carpet +171:12,minecraft:brown_carpet +171:13,minecraft:green_carpet +171:14,minecraft:red_carpet +171:15,minecraft:black_carpet +172:0,minecraft:terracotta +173:0,minecraft:coal_block +174:0,minecraft:packed_ice +175:0,minecraft:sunflower[half=lower] +175:1,minecraft:lilac[half=lower] +175:2,minecraft:tall_grass[half=lower] +175:3,minecraft:large_fern[half=lower] +175:4,minecraft:rose_bush[half=lower] +175:5,minecraft:peony[half=lower] +175:8,minecraft:peony[half=upper] +175:9,minecraft:peony[half=upper] +175:10,minecraft:peony[half=upper] +175:11,minecraft:peony[half=upper] +175:12,minecraft:peony[half=upper] +175:13,minecraft:peony[half=upper] +175:14,minecraft:peony[half=upper] +175:15,minecraft:peony[half=upper] +176:0,minecraft:black_banner[rotation=0] +176:1,minecraft:black_banner[rotation=1] +176:2,minecraft:black_banner[rotation=2] +176:3,minecraft:black_banner[rotation=3] +176:4,minecraft:black_banner[rotation=4] +176:5,minecraft:black_banner[rotation=5] +176:6,minecraft:black_banner[rotation=6] +176:7,minecraft:black_banner[rotation=7] +176:8,minecraft:black_banner[rotation=8] +176:9,minecraft:black_banner[rotation=9] +176:10,minecraft:black_banner[rotation=10] +176:11,minecraft:black_banner[rotation=11] +176:12,minecraft:black_banner[rotation=12] +176:13,minecraft:black_banner[rotation=13] +176:14,minecraft:black_banner[rotation=14] +176:15,minecraft:black_banner[rotation=15] +177:0,minecraft:black_wall_banner[facing=north] +177:3,minecraft:black_wall_banner[facing=south] +177:4,minecraft:black_wall_banner[facing=west] +177:5,minecraft:black_wall_banner[facing=east] +177:9,minecraft:black_wall_banner[facing=south] +177:10,minecraft:black_wall_banner[facing=west] +177:11,minecraft:black_wall_banner[facing=east] +177:15,minecraft:black_wall_banner[facing=south] +178:0,minecraft:daylight_detector[inverted=true,power=0] +178:1,minecraft:daylight_detector[inverted=true,power=1] +178:2,minecraft:daylight_detector[inverted=true,power=2] +178:3,minecraft:daylight_detector[inverted=true,power=3] +178:4,minecraft:daylight_detector[inverted=true,power=4] +178:5,minecraft:daylight_detector[inverted=true,power=5] +178:6,minecraft:daylight_detector[inverted=true,power=6] +178:7,minecraft:daylight_detector[inverted=true,power=7] +178:8,minecraft:daylight_detector[inverted=true,power=8] +178:9,minecraft:daylight_detector[inverted=true,power=9] +178:10,minecraft:daylight_detector[inverted=true,power=10] +178:11,minecraft:daylight_detector[inverted=true,power=11] +178:12,minecraft:daylight_detector[inverted=true,power=12] +178:13,minecraft:daylight_detector[inverted=true,power=13] +178:14,minecraft:daylight_detector[inverted=true,power=14] +178:15,minecraft:daylight_detector[inverted=true,power=15] +179:0,minecraft:red_sandstone +179:1,minecraft:chiseled_red_sandstone +179:2,minecraft:cut_red_sandstone +180:0,minecraft:red_sandstone_stairs[facing=east,half=bottom,shape=straight,waterlogged=false] +180:1,minecraft:red_sandstone_stairs[facing=west,half=bottom,shape=straight,waterlogged=false] +180:2,minecraft:red_sandstone_stairs[facing=south,half=bottom,shape=straight,waterlogged=false] +180:3,minecraft:red_sandstone_stairs[facing=north,half=bottom,shape=straight,waterlogged=false] +180:4,minecraft:red_sandstone_stairs[facing=east,half=top,shape=straight,waterlogged=false] +180:5,minecraft:red_sandstone_stairs[facing=west,half=top,shape=straight,waterlogged=false] +180:6,minecraft:red_sandstone_stairs[facing=south,half=top,shape=straight,waterlogged=false] +180:7,minecraft:red_sandstone_stairs[facing=north,half=top,shape=straight,waterlogged=false] +180:9,minecraft:red_sandstone_stairs[facing=west,half=bottom,shape=straight,waterlogged=false] +180:10,minecraft:red_sandstone_stairs[facing=south,half=bottom,shape=straight,waterlogged=false] +180:11,minecraft:red_sandstone_stairs[facing=north,half=bottom,shape=straight,waterlogged=false] +180:12,minecraft:red_sandstone_stairs[facing=east,half=top,shape=straight,waterlogged=false] +180:13,minecraft:red_sandstone_stairs[facing=west,half=top,shape=straight,waterlogged=false] +180:14,minecraft:red_sandstone_stairs[facing=south,half=top,shape=straight,waterlogged=false] +180:15,minecraft:red_sandstone_stairs[facing=north,half=top,shape=straight,waterlogged=false] +181:0,minecraft:red_sandstone_slab[type=double,waterlogged=false] +181:8,minecraft:smooth_red_sandstone +181:9,minecraft:smooth_red_sandstone +181:10,minecraft:smooth_red_sandstone +181:11,minecraft:smooth_red_sandstone +181:12,minecraft:smooth_red_sandstone +181:13,minecraft:smooth_red_sandstone +181:14,minecraft:smooth_red_sandstone +181:15,minecraft:smooth_red_sandstone +182:0,minecraft:red_sandstone_slab[type=bottom,waterlogged=false] +182:8,minecraft:red_sandstone_slab[type=top,waterlogged=false] +182:9,minecraft:red_sandstone_slab[type=top,waterlogged=false] +182:10,minecraft:red_sandstone_slab[type=top,waterlogged=false] +182:11,minecraft:red_sandstone_slab[type=top,waterlogged=false] +182:12,minecraft:red_sandstone_slab[type=top,waterlogged=false] +182:13,minecraft:red_sandstone_slab[type=top,waterlogged=false] +182:14,minecraft:red_sandstone_slab[type=top,waterlogged=false] +182:15,minecraft:red_sandstone_slab[type=top,waterlogged=false] +183:0,minecraft:spruce_fence_gate[facing=south,in_wall=false,open=false,powered=false] +183:1,minecraft:spruce_fence_gate[facing=west,in_wall=false,open=false,powered=false] +183:2,minecraft:spruce_fence_gate[facing=north,in_wall=false,open=false,powered=false] +183:3,minecraft:spruce_fence_gate[facing=east,in_wall=false,open=false,powered=false] +183:4,minecraft:spruce_fence_gate[facing=south,in_wall=false,open=true,powered=false] +183:5,minecraft:spruce_fence_gate[facing=west,in_wall=false,open=true,powered=false] +183:6,minecraft:spruce_fence_gate[facing=north,in_wall=false,open=true,powered=false] +183:7,minecraft:spruce_fence_gate[facing=east,in_wall=false,open=true,powered=false] +183:8,minecraft:spruce_fence_gate[facing=south,in_wall=false,open=false,powered=true] +183:9,minecraft:spruce_fence_gate[facing=west,in_wall=false,open=false,powered=true] +183:10,minecraft:spruce_fence_gate[facing=north,in_wall=false,open=false,powered=true] +183:11,minecraft:spruce_fence_gate[facing=east,in_wall=false,open=false,powered=true] +183:12,minecraft:spruce_fence_gate[facing=south,in_wall=false,open=true,powered=true] +183:13,minecraft:spruce_fence_gate[facing=west,in_wall=false,open=true,powered=true] +183:14,minecraft:spruce_fence_gate[facing=north,in_wall=false,open=true,powered=true] +183:15,minecraft:spruce_fence_gate[facing=east,in_wall=false,open=true,powered=true] +184:0,minecraft:birch_fence_gate[facing=south,in_wall=false,open=false,powered=false] +184:1,minecraft:birch_fence_gate[facing=west,in_wall=false,open=false,powered=false] +184:2,minecraft:birch_fence_gate[facing=north,in_wall=false,open=false,powered=false] +184:3,minecraft:birch_fence_gate[facing=east,in_wall=false,open=false,powered=false] +184:4,minecraft:birch_fence_gate[facing=south,in_wall=false,open=true,powered=false] +184:5,minecraft:birch_fence_gate[facing=west,in_wall=false,open=true,powered=false] +184:6,minecraft:birch_fence_gate[facing=north,in_wall=false,open=true,powered=false] +184:7,minecraft:birch_fence_gate[facing=east,in_wall=false,open=true,powered=false] +184:8,minecraft:birch_fence_gate[facing=south,in_wall=false,open=false,powered=true] +184:9,minecraft:birch_fence_gate[facing=west,in_wall=false,open=false,powered=true] +184:10,minecraft:birch_fence_gate[facing=north,in_wall=false,open=false,powered=true] +184:11,minecraft:birch_fence_gate[facing=east,in_wall=false,open=false,powered=true] +184:12,minecraft:birch_fence_gate[facing=south,in_wall=false,open=true,powered=true] +184:13,minecraft:birch_fence_gate[facing=west,in_wall=false,open=true,powered=true] +184:14,minecraft:birch_fence_gate[facing=north,in_wall=false,open=true,powered=true] +184:15,minecraft:birch_fence_gate[facing=east,in_wall=false,open=true,powered=true] +185:0,minecraft:jungle_fence_gate[facing=south,in_wall=false,open=false,powered=false] +185:1,minecraft:jungle_fence_gate[facing=west,in_wall=false,open=false,powered=false] +185:2,minecraft:jungle_fence_gate[facing=north,in_wall=false,open=false,powered=false] +185:3,minecraft:jungle_fence_gate[facing=east,in_wall=false,open=false,powered=false] +185:4,minecraft:jungle_fence_gate[facing=south,in_wall=false,open=true,powered=false] +185:5,minecraft:jungle_fence_gate[facing=west,in_wall=false,open=true,powered=false] +185:6,minecraft:jungle_fence_gate[facing=north,in_wall=false,open=true,powered=false] +185:7,minecraft:jungle_fence_gate[facing=east,in_wall=false,open=true,powered=false] +185:8,minecraft:jungle_fence_gate[facing=south,in_wall=false,open=false,powered=true] +185:9,minecraft:jungle_fence_gate[facing=west,in_wall=false,open=false,powered=true] +185:10,minecraft:jungle_fence_gate[facing=north,in_wall=false,open=false,powered=true] +185:11,minecraft:jungle_fence_gate[facing=east,in_wall=false,open=false,powered=true] +185:12,minecraft:jungle_fence_gate[facing=south,in_wall=false,open=true,powered=true] +185:13,minecraft:jungle_fence_gate[facing=west,in_wall=false,open=true,powered=true] +185:14,minecraft:jungle_fence_gate[facing=north,in_wall=false,open=true,powered=true] +185:15,minecraft:jungle_fence_gate[facing=east,in_wall=false,open=true,powered=true] +186:0,minecraft:dark_oak_fence_gate[facing=south,in_wall=false,open=false,powered=false] +186:1,minecraft:dark_oak_fence_gate[facing=west,in_wall=false,open=false,powered=false] +186:2,minecraft:dark_oak_fence_gate[facing=north,in_wall=false,open=false,powered=false] +186:3,minecraft:dark_oak_fence_gate[facing=east,in_wall=false,open=false,powered=false] +186:4,minecraft:dark_oak_fence_gate[facing=south,in_wall=false,open=true,powered=false] +186:5,minecraft:dark_oak_fence_gate[facing=west,in_wall=false,open=true,powered=false] +186:6,minecraft:dark_oak_fence_gate[facing=north,in_wall=false,open=true,powered=false] +186:7,minecraft:dark_oak_fence_gate[facing=east,in_wall=false,open=true,powered=false] +186:8,minecraft:dark_oak_fence_gate[facing=south,in_wall=false,open=false,powered=true] +186:9,minecraft:dark_oak_fence_gate[facing=west,in_wall=false,open=false,powered=true] +186:10,minecraft:dark_oak_fence_gate[facing=north,in_wall=false,open=false,powered=true] +186:11,minecraft:dark_oak_fence_gate[facing=east,in_wall=false,open=false,powered=true] +186:12,minecraft:dark_oak_fence_gate[facing=south,in_wall=false,open=true,powered=true] +186:13,minecraft:dark_oak_fence_gate[facing=west,in_wall=false,open=true,powered=true] +186:14,minecraft:dark_oak_fence_gate[facing=north,in_wall=false,open=true,powered=true] +186:15,minecraft:dark_oak_fence_gate[facing=east,in_wall=false,open=true,powered=true] +187:0,minecraft:acacia_fence_gate[facing=south,in_wall=false,open=false,powered=false] +187:1,minecraft:acacia_fence_gate[facing=west,in_wall=false,open=false,powered=false] +187:2,minecraft:acacia_fence_gate[facing=north,in_wall=false,open=false,powered=false] +187:3,minecraft:acacia_fence_gate[facing=east,in_wall=false,open=false,powered=false] +187:4,minecraft:acacia_fence_gate[facing=south,in_wall=false,open=true,powered=false] +187:5,minecraft:acacia_fence_gate[facing=west,in_wall=false,open=true,powered=false] +187:6,minecraft:acacia_fence_gate[facing=north,in_wall=false,open=true,powered=false] +187:7,minecraft:acacia_fence_gate[facing=east,in_wall=false,open=true,powered=false] +187:8,minecraft:acacia_fence_gate[facing=south,in_wall=false,open=false,powered=true] +187:9,minecraft:acacia_fence_gate[facing=west,in_wall=false,open=false,powered=true] +187:10,minecraft:acacia_fence_gate[facing=north,in_wall=false,open=false,powered=true] +187:11,minecraft:acacia_fence_gate[facing=east,in_wall=false,open=false,powered=true] +187:12,minecraft:acacia_fence_gate[facing=south,in_wall=false,open=true,powered=true] +187:13,minecraft:acacia_fence_gate[facing=west,in_wall=false,open=true,powered=true] +187:14,minecraft:acacia_fence_gate[facing=north,in_wall=false,open=true,powered=true] +187:15,minecraft:acacia_fence_gate[facing=east,in_wall=false,open=true,powered=true] +188:0,minecraft:spruce_fence[east=false,north=false,south=false,waterlogged=false,west=false] +189:0,minecraft:birch_fence[east=false,north=false,south=false,waterlogged=false,west=false] +190:0,minecraft:jungle_fence[east=false,north=false,south=false,waterlogged=false,west=false] +191:0,minecraft:dark_oak_fence[east=false,north=false,south=false,waterlogged=false,west=false] +192:0,minecraft:acacia_fence[east=false,north=false,south=false,waterlogged=false,west=false] +193:0,minecraft:spruce_door[facing=east,half=lower,hinge=right,open=false,powered=false] +193:1,minecraft:spruce_door[facing=south,half=lower,hinge=right,open=false,powered=false] +193:2,minecraft:spruce_door[facing=west,half=lower,hinge=right,open=false,powered=false] +193:3,minecraft:spruce_door[facing=north,half=lower,hinge=right,open=false,powered=false] +193:4,minecraft:spruce_door[facing=east,half=lower,hinge=right,open=true,powered=false] +193:5,minecraft:spruce_door[facing=south,half=lower,hinge=right,open=true,powered=false] +193:6,minecraft:spruce_door[facing=west,half=lower,hinge=right,open=true,powered=false] +193:7,minecraft:spruce_door[facing=north,half=lower,hinge=right,open=true,powered=false] +193:8,minecraft:spruce_door[facing=east,half=upper,hinge=left,open=false,powered=false] +193:9,minecraft:spruce_door[facing=east,half=upper,hinge=right,open=false,powered=false] +193:10,minecraft:spruce_door[facing=east,half=upper,hinge=left,open=false,powered=true] +193:11,minecraft:spruce_door[facing=east,half=upper,hinge=right,open=false,powered=true] +193:12,minecraft:spruce_door[facing=east,half=upper,hinge=left,open=false,powered=false] +193:13,minecraft:spruce_door[facing=east,half=upper,hinge=right,open=false,powered=false] +193:14,minecraft:spruce_door[facing=east,half=upper,hinge=left,open=false,powered=true] +193:15,minecraft:spruce_door[facing=east,half=upper,hinge=right,open=false,powered=true] +194:0,minecraft:birch_door[facing=east,half=lower,hinge=right,open=false,powered=false] +194:1,minecraft:birch_door[facing=south,half=lower,hinge=right,open=false,powered=false] +194:2,minecraft:birch_door[facing=west,half=lower,hinge=right,open=false,powered=false] +194:3,minecraft:birch_door[facing=north,half=lower,hinge=right,open=false,powered=false] +194:4,minecraft:birch_door[facing=east,half=lower,hinge=right,open=true,powered=false] +194:5,minecraft:birch_door[facing=south,half=lower,hinge=right,open=true,powered=false] +194:6,minecraft:birch_door[facing=west,half=lower,hinge=right,open=true,powered=false] +194:7,minecraft:birch_door[facing=north,half=lower,hinge=right,open=true,powered=false] +194:8,minecraft:birch_door[facing=east,half=upper,hinge=left,open=false,powered=false] +194:9,minecraft:birch_door[facing=east,half=upper,hinge=right,open=false,powered=false] +194:10,minecraft:birch_door[facing=east,half=upper,hinge=left,open=false,powered=true] +194:11,minecraft:birch_door[facing=east,half=upper,hinge=right,open=false,powered=true] +194:12,minecraft:birch_door[facing=east,half=upper,hinge=left,open=false,powered=false] +194:13,minecraft:birch_door[facing=east,half=upper,hinge=right,open=false,powered=false] +194:14,minecraft:birch_door[facing=east,half=upper,hinge=left,open=false,powered=true] +194:15,minecraft:birch_door[facing=east,half=upper,hinge=right,open=false,powered=true] +195:0,minecraft:jungle_door[facing=east,half=lower,hinge=right,open=false,powered=false] +195:1,minecraft:jungle_door[facing=south,half=lower,hinge=right,open=false,powered=false] +195:2,minecraft:jungle_door[facing=west,half=lower,hinge=right,open=false,powered=false] +195:3,minecraft:jungle_door[facing=north,half=lower,hinge=right,open=false,powered=false] +195:4,minecraft:jungle_door[facing=east,half=lower,hinge=right,open=true,powered=false] +195:5,minecraft:jungle_door[facing=south,half=lower,hinge=right,open=true,powered=false] +195:6,minecraft:jungle_door[facing=west,half=lower,hinge=right,open=true,powered=false] +195:7,minecraft:jungle_door[facing=north,half=lower,hinge=right,open=true,powered=false] +195:8,minecraft:jungle_door[facing=east,half=upper,hinge=left,open=false,powered=false] +195:9,minecraft:jungle_door[facing=east,half=upper,hinge=right,open=false,powered=false] +195:10,minecraft:jungle_door[facing=east,half=upper,hinge=left,open=false,powered=true] +195:11,minecraft:jungle_door[facing=east,half=upper,hinge=right,open=false,powered=true] +195:12,minecraft:jungle_door[facing=east,half=upper,hinge=left,open=false,powered=false] +195:13,minecraft:jungle_door[facing=east,half=upper,hinge=right,open=false,powered=false] +195:14,minecraft:jungle_door[facing=east,half=upper,hinge=left,open=false,powered=true] +195:15,minecraft:jungle_door[facing=east,half=upper,hinge=right,open=false,powered=true] +196:0,minecraft:acacia_door[facing=east,half=lower,hinge=right,open=false,powered=false] +196:1,minecraft:acacia_door[facing=south,half=lower,hinge=right,open=false,powered=false] +196:2,minecraft:acacia_door[facing=west,half=lower,hinge=right,open=false,powered=false] +196:3,minecraft:acacia_door[facing=north,half=lower,hinge=right,open=false,powered=false] +196:4,minecraft:acacia_door[facing=east,half=lower,hinge=right,open=true,powered=false] +196:5,minecraft:acacia_door[facing=south,half=lower,hinge=right,open=true,powered=false] +196:6,minecraft:acacia_door[facing=west,half=lower,hinge=right,open=true,powered=false] +196:7,minecraft:acacia_door[facing=north,half=lower,hinge=right,open=true,powered=false] +196:8,minecraft:acacia_door[facing=east,half=upper,hinge=left,open=false,powered=false] +196:9,minecraft:acacia_door[facing=east,half=upper,hinge=right,open=false,powered=false] +196:10,minecraft:acacia_door[facing=east,half=upper,hinge=left,open=false,powered=true] +196:11,minecraft:acacia_door[facing=east,half=upper,hinge=right,open=false,powered=true] +196:12,minecraft:acacia_door[facing=east,half=upper,hinge=left,open=false,powered=false] +196:13,minecraft:acacia_door[facing=east,half=upper,hinge=right,open=false,powered=false] +196:14,minecraft:acacia_door[facing=east,half=upper,hinge=left,open=false,powered=true] +196:15,minecraft:acacia_door[facing=east,half=upper,hinge=right,open=false,powered=true] +197:0,minecraft:dark_oak_door[facing=east,half=lower,hinge=right,open=false,powered=false] +197:1,minecraft:dark_oak_door[facing=south,half=lower,hinge=right,open=false,powered=false] +197:2,minecraft:dark_oak_door[facing=west,half=lower,hinge=right,open=false,powered=false] +197:3,minecraft:dark_oak_door[facing=north,half=lower,hinge=right,open=false,powered=false] +197:4,minecraft:dark_oak_door[facing=east,half=lower,hinge=right,open=true,powered=false] +197:5,minecraft:dark_oak_door[facing=south,half=lower,hinge=right,open=true,powered=false] +197:6,minecraft:dark_oak_door[facing=west,half=lower,hinge=right,open=true,powered=false] +197:7,minecraft:dark_oak_door[facing=north,half=lower,hinge=right,open=true,powered=false] +197:8,minecraft:dark_oak_door[facing=east,half=upper,hinge=left,open=false,powered=false] +197:9,minecraft:dark_oak_door[facing=east,half=upper,hinge=right,open=false,powered=false] +197:10,minecraft:dark_oak_door[facing=east,half=upper,hinge=left,open=false,powered=true] +197:11,minecraft:dark_oak_door[facing=east,half=upper,hinge=right,open=false,powered=true] +197:12,minecraft:dark_oak_door[facing=east,half=upper,hinge=left,open=false,powered=false] +197:13,minecraft:dark_oak_door[facing=east,half=upper,hinge=right,open=false,powered=false] +197:14,minecraft:dark_oak_door[facing=east,half=upper,hinge=left,open=false,powered=true] +197:15,minecraft:dark_oak_door[facing=east,half=upper,hinge=right,open=false,powered=true] +198:0,minecraft:end_rod[facing=down] +198:1,minecraft:end_rod[facing=up] +198:2,minecraft:end_rod[facing=north] +198:3,minecraft:end_rod[facing=south] +198:4,minecraft:end_rod[facing=west] +198:5,minecraft:end_rod[facing=east] +198:7,minecraft:end_rod[facing=up] +198:8,minecraft:end_rod[facing=north] +198:9,minecraft:end_rod[facing=south] +198:10,minecraft:end_rod[facing=west] +198:11,minecraft:end_rod[facing=east] +198:13,minecraft:end_rod[facing=up] +198:14,minecraft:end_rod[facing=north] +198:15,minecraft:end_rod[facing=south] +199:0,minecraft:chorus_plant[down=false,east=false,north=false,south=false,up=false,west=false] +200:0,minecraft:chorus_flower[age=0] +200:1,minecraft:chorus_flower[age=1] +200:2,minecraft:chorus_flower[age=2] +200:3,minecraft:chorus_flower[age=3] +200:4,minecraft:chorus_flower[age=4] +200:5,minecraft:chorus_flower[age=5] +200:6,minecraft:air +200:7,minecraft:air +200:8,minecraft:air +200:9,minecraft:air +200:10,minecraft:air +200:11,minecraft:air +200:12,minecraft:air +200:13,minecraft:air +200:14,minecraft:air +200:15,minecraft:air +201:0,minecraft:purpur_block +202:0,minecraft:purpur_pillar[axis=y] +202:4,minecraft:purpur_pillar[axis=x] +202:5,minecraft:purpur_pillar[axis=x] +202:6,minecraft:purpur_pillar[axis=x] +202:7,minecraft:purpur_pillar[axis=x] +202:8,minecraft:purpur_pillar[axis=z] +202:9,minecraft:purpur_pillar[axis=z] +202:10,minecraft:purpur_pillar[axis=z] +202:11,minecraft:purpur_pillar[axis=z] +203:0,minecraft:purpur_stairs[facing=east,half=bottom,shape=straight,waterlogged=false] +203:1,minecraft:purpur_stairs[facing=west,half=bottom,shape=straight,waterlogged=false] +203:2,minecraft:purpur_stairs[facing=south,half=bottom,shape=straight,waterlogged=false] +203:3,minecraft:purpur_stairs[facing=north,half=bottom,shape=straight,waterlogged=false] +203:4,minecraft:purpur_stairs[facing=east,half=top,shape=straight,waterlogged=false] +203:5,minecraft:purpur_stairs[facing=west,half=top,shape=straight,waterlogged=false] +203:6,minecraft:purpur_stairs[facing=south,half=top,shape=straight,waterlogged=false] +203:7,minecraft:purpur_stairs[facing=north,half=top,shape=straight,waterlogged=false] +203:9,minecraft:purpur_stairs[facing=west,half=bottom,shape=straight,waterlogged=false] +203:10,minecraft:purpur_stairs[facing=south,half=bottom,shape=straight,waterlogged=false] +203:11,minecraft:purpur_stairs[facing=north,half=bottom,shape=straight,waterlogged=false] +203:12,minecraft:purpur_stairs[facing=east,half=top,shape=straight,waterlogged=false] +203:13,minecraft:purpur_stairs[facing=west,half=top,shape=straight,waterlogged=false] +203:14,minecraft:purpur_stairs[facing=south,half=top,shape=straight,waterlogged=false] +203:15,minecraft:purpur_stairs[facing=north,half=top,shape=straight,waterlogged=false] +204:0,minecraft:purpur_slab[type=double,waterlogged=false] +205:0,minecraft:purpur_slab[type=bottom,waterlogged=false] +205:8,minecraft:purpur_slab[type=top,waterlogged=false] +205:9,minecraft:purpur_slab[type=top,waterlogged=false] +205:10,minecraft:purpur_slab[type=top,waterlogged=false] +205:11,minecraft:purpur_slab[type=top,waterlogged=false] +205:12,minecraft:purpur_slab[type=top,waterlogged=false] +205:13,minecraft:purpur_slab[type=top,waterlogged=false] +205:14,minecraft:purpur_slab[type=top,waterlogged=false] +205:15,minecraft:purpur_slab[type=top,waterlogged=false] +206:0,minecraft:end_stone_bricks +207:0,minecraft:beetroots[age=0] +207:1,minecraft:beetroots[age=1] +207:2,minecraft:beetroots[age=2] +207:3,minecraft:beetroots[age=3] +207:4,minecraft:air +207:5,minecraft:air +207:6,minecraft:air +207:7,minecraft:air +207:8,minecraft:air +207:9,minecraft:air +207:10,minecraft:air +207:11,minecraft:air +207:12,minecraft:air +207:13,minecraft:air +207:14,minecraft:air +207:15,minecraft:air +208:0,minecraft:dirt_path +209:0,minecraft:end_gateway +210:0,minecraft:repeating_command_block[conditional=false,facing=down] +210:1,minecraft:repeating_command_block[conditional=false,facing=up] +210:2,minecraft:repeating_command_block[conditional=false,facing=north] +210:3,minecraft:repeating_command_block[conditional=false,facing=south] +210:4,minecraft:repeating_command_block[conditional=false,facing=west] +210:5,minecraft:repeating_command_block[conditional=false,facing=east] +210:7,minecraft:repeating_command_block[conditional=false,facing=up] +210:8,minecraft:repeating_command_block[conditional=true,facing=down] +210:9,minecraft:repeating_command_block[conditional=true,facing=up] +210:10,minecraft:repeating_command_block[conditional=true,facing=north] +210:11,minecraft:repeating_command_block[conditional=true,facing=south] +210:12,minecraft:repeating_command_block[conditional=true,facing=west] +210:13,minecraft:repeating_command_block[conditional=true,facing=east] +210:14,minecraft:repeating_command_block[conditional=true,facing=down] +210:15,minecraft:repeating_command_block[conditional=true,facing=up] +211:0,minecraft:chain_command_block[conditional=false,facing=down] +211:1,minecraft:chain_command_block[conditional=false,facing=up] +211:2,minecraft:chain_command_block[conditional=false,facing=north] +211:3,minecraft:chain_command_block[conditional=false,facing=south] +211:4,minecraft:chain_command_block[conditional=false,facing=west] +211:5,minecraft:chain_command_block[conditional=false,facing=east] +211:7,minecraft:chain_command_block[conditional=false,facing=up] +211:8,minecraft:chain_command_block[conditional=true,facing=down] +211:9,minecraft:chain_command_block[conditional=true,facing=up] +211:10,minecraft:chain_command_block[conditional=true,facing=north] +211:11,minecraft:chain_command_block[conditional=true,facing=south] +211:12,minecraft:chain_command_block[conditional=true,facing=west] +211:13,minecraft:chain_command_block[conditional=true,facing=east] +211:14,minecraft:chain_command_block[conditional=true,facing=down] +211:15,minecraft:chain_command_block[conditional=true,facing=up] +212:0,minecraft:frosted_ice[age=0] +212:1,minecraft:frosted_ice[age=1] +212:2,minecraft:frosted_ice[age=2] +212:3,minecraft:frosted_ice[age=3] +212:4,minecraft:frosted_ice[age=3] +212:5,minecraft:frosted_ice[age=3] +212:6,minecraft:frosted_ice[age=3] +212:7,minecraft:frosted_ice[age=3] +212:8,minecraft:frosted_ice[age=3] +212:9,minecraft:frosted_ice[age=3] +212:10,minecraft:frosted_ice[age=3] +212:11,minecraft:frosted_ice[age=3] +212:12,minecraft:frosted_ice[age=3] +212:13,minecraft:frosted_ice[age=3] +212:14,minecraft:frosted_ice[age=3] +212:15,minecraft:frosted_ice[age=3] +213:0,minecraft:magma_block +214:0,minecraft:nether_wart_block +215:0,minecraft:red_nether_bricks +216:0,minecraft:bone_block[axis=y] +216:4,minecraft:bone_block[axis=x] +216:5,minecraft:bone_block[axis=x] +216:6,minecraft:bone_block[axis=x] +216:7,minecraft:bone_block[axis=x] +216:8,minecraft:bone_block[axis=z] +216:9,minecraft:bone_block[axis=z] +216:10,minecraft:bone_block[axis=z] +216:11,minecraft:bone_block[axis=z] +217:0,minecraft:structure_void +218:0,minecraft:observer[facing=down,powered=false] +218:1,minecraft:observer[facing=up,powered=false] +218:2,minecraft:observer[facing=north,powered=false] +218:3,minecraft:observer[facing=south,powered=false] +218:4,minecraft:observer[facing=west,powered=false] +218:5,minecraft:observer[facing=east,powered=false] +218:7,minecraft:observer[facing=up,powered=false] +218:9,minecraft:observer[facing=up,powered=false] +218:10,minecraft:observer[facing=north,powered=false] +218:11,minecraft:observer[facing=south,powered=false] +218:12,minecraft:observer[facing=west,powered=false] +218:13,minecraft:observer[facing=east,powered=false] +218:15,minecraft:observer[facing=up,powered=false] +219:0,minecraft:white_shulker_box[facing=down] +219:1,minecraft:white_shulker_box[facing=up] +219:2,minecraft:white_shulker_box[facing=north] +219:3,minecraft:white_shulker_box[facing=south] +219:4,minecraft:white_shulker_box[facing=west] +219:5,minecraft:white_shulker_box[facing=east] +219:7,minecraft:white_shulker_box[facing=up] +219:8,minecraft:white_shulker_box[facing=north] +219:9,minecraft:white_shulker_box[facing=south] +219:10,minecraft:white_shulker_box[facing=west] +219:11,minecraft:white_shulker_box[facing=east] +219:13,minecraft:white_shulker_box[facing=up] +219:14,minecraft:white_shulker_box[facing=north] +219:15,minecraft:white_shulker_box[facing=south] +220:0,minecraft:orange_shulker_box[facing=down] +220:1,minecraft:orange_shulker_box[facing=up] +220:2,minecraft:orange_shulker_box[facing=north] +220:3,minecraft:orange_shulker_box[facing=south] +220:4,minecraft:orange_shulker_box[facing=west] +220:5,minecraft:orange_shulker_box[facing=east] +220:7,minecraft:orange_shulker_box[facing=up] +220:8,minecraft:orange_shulker_box[facing=north] +220:9,minecraft:orange_shulker_box[facing=south] +220:10,minecraft:orange_shulker_box[facing=west] +220:11,minecraft:orange_shulker_box[facing=east] +220:13,minecraft:orange_shulker_box[facing=up] +220:14,minecraft:orange_shulker_box[facing=north] +220:15,minecraft:orange_shulker_box[facing=south] +221:0,minecraft:magenta_shulker_box[facing=down] +221:1,minecraft:magenta_shulker_box[facing=up] +221:2,minecraft:magenta_shulker_box[facing=north] +221:3,minecraft:magenta_shulker_box[facing=south] +221:4,minecraft:magenta_shulker_box[facing=west] +221:5,minecraft:magenta_shulker_box[facing=east] +221:7,minecraft:magenta_shulker_box[facing=up] +221:8,minecraft:magenta_shulker_box[facing=north] +221:9,minecraft:magenta_shulker_box[facing=south] +221:10,minecraft:magenta_shulker_box[facing=west] +221:11,minecraft:magenta_shulker_box[facing=east] +221:13,minecraft:magenta_shulker_box[facing=up] +221:14,minecraft:magenta_shulker_box[facing=north] +221:15,minecraft:magenta_shulker_box[facing=south] +222:0,minecraft:light_blue_shulker_box[facing=down] +222:1,minecraft:light_blue_shulker_box[facing=up] +222:2,minecraft:light_blue_shulker_box[facing=north] +222:3,minecraft:light_blue_shulker_box[facing=south] +222:4,minecraft:light_blue_shulker_box[facing=west] +222:5,minecraft:light_blue_shulker_box[facing=east] +222:7,minecraft:light_blue_shulker_box[facing=up] +222:8,minecraft:light_blue_shulker_box[facing=north] +222:9,minecraft:light_blue_shulker_box[facing=south] +222:10,minecraft:light_blue_shulker_box[facing=west] +222:11,minecraft:light_blue_shulker_box[facing=east] +222:13,minecraft:light_blue_shulker_box[facing=up] +222:14,minecraft:light_blue_shulker_box[facing=north] +222:15,minecraft:light_blue_shulker_box[facing=south] +223:0,minecraft:yellow_shulker_box[facing=down] +223:1,minecraft:yellow_shulker_box[facing=up] +223:2,minecraft:yellow_shulker_box[facing=north] +223:3,minecraft:yellow_shulker_box[facing=south] +223:4,minecraft:yellow_shulker_box[facing=west] +223:5,minecraft:yellow_shulker_box[facing=east] +223:7,minecraft:yellow_shulker_box[facing=up] +223:8,minecraft:yellow_shulker_box[facing=north] +223:9,minecraft:yellow_shulker_box[facing=south] +223:10,minecraft:yellow_shulker_box[facing=west] +223:11,minecraft:yellow_shulker_box[facing=east] +223:13,minecraft:yellow_shulker_box[facing=up] +223:14,minecraft:yellow_shulker_box[facing=north] +223:15,minecraft:yellow_shulker_box[facing=south] +224:0,minecraft:lime_shulker_box[facing=down] +224:1,minecraft:lime_shulker_box[facing=up] +224:2,minecraft:lime_shulker_box[facing=north] +224:3,minecraft:lime_shulker_box[facing=south] +224:4,minecraft:lime_shulker_box[facing=west] +224:5,minecraft:lime_shulker_box[facing=east] +224:7,minecraft:lime_shulker_box[facing=up] +224:8,minecraft:lime_shulker_box[facing=north] +224:9,minecraft:lime_shulker_box[facing=south] +224:10,minecraft:lime_shulker_box[facing=west] +224:11,minecraft:lime_shulker_box[facing=east] +224:13,minecraft:lime_shulker_box[facing=up] +224:14,minecraft:lime_shulker_box[facing=north] +224:15,minecraft:lime_shulker_box[facing=south] +225:0,minecraft:pink_shulker_box[facing=down] +225:1,minecraft:pink_shulker_box[facing=up] +225:2,minecraft:pink_shulker_box[facing=north] +225:3,minecraft:pink_shulker_box[facing=south] +225:4,minecraft:pink_shulker_box[facing=west] +225:5,minecraft:pink_shulker_box[facing=east] +225:7,minecraft:pink_shulker_box[facing=up] +225:8,minecraft:pink_shulker_box[facing=north] +225:9,minecraft:pink_shulker_box[facing=south] +225:10,minecraft:pink_shulker_box[facing=west] +225:11,minecraft:pink_shulker_box[facing=east] +225:13,minecraft:pink_shulker_box[facing=up] +225:14,minecraft:pink_shulker_box[facing=north] +225:15,minecraft:pink_shulker_box[facing=south] +226:0,minecraft:gray_shulker_box[facing=down] +226:1,minecraft:gray_shulker_box[facing=up] +226:2,minecraft:gray_shulker_box[facing=north] +226:3,minecraft:gray_shulker_box[facing=south] +226:4,minecraft:gray_shulker_box[facing=west] +226:5,minecraft:gray_shulker_box[facing=east] +226:7,minecraft:gray_shulker_box[facing=up] +226:8,minecraft:gray_shulker_box[facing=north] +226:9,minecraft:gray_shulker_box[facing=south] +226:10,minecraft:gray_shulker_box[facing=west] +226:11,minecraft:gray_shulker_box[facing=east] +226:13,minecraft:gray_shulker_box[facing=up] +226:14,minecraft:gray_shulker_box[facing=north] +226:15,minecraft:gray_shulker_box[facing=south] +227:0,minecraft:light_gray_shulker_box[facing=down] +227:1,minecraft:light_gray_shulker_box[facing=up] +227:2,minecraft:light_gray_shulker_box[facing=north] +227:3,minecraft:light_gray_shulker_box[facing=south] +227:4,minecraft:light_gray_shulker_box[facing=west] +227:5,minecraft:light_gray_shulker_box[facing=east] +227:7,minecraft:light_gray_shulker_box[facing=up] +227:8,minecraft:light_gray_shulker_box[facing=north] +227:9,minecraft:light_gray_shulker_box[facing=south] +227:10,minecraft:light_gray_shulker_box[facing=west] +227:11,minecraft:light_gray_shulker_box[facing=east] +227:13,minecraft:light_gray_shulker_box[facing=up] +227:14,minecraft:light_gray_shulker_box[facing=north] +227:15,minecraft:light_gray_shulker_box[facing=south] +228:0,minecraft:cyan_shulker_box[facing=down] +228:1,minecraft:cyan_shulker_box[facing=up] +228:2,minecraft:cyan_shulker_box[facing=north] +228:3,minecraft:cyan_shulker_box[facing=south] +228:4,minecraft:cyan_shulker_box[facing=west] +228:5,minecraft:cyan_shulker_box[facing=east] +228:7,minecraft:cyan_shulker_box[facing=up] +228:8,minecraft:cyan_shulker_box[facing=north] +228:9,minecraft:cyan_shulker_box[facing=south] +228:10,minecraft:cyan_shulker_box[facing=west] +228:11,minecraft:cyan_shulker_box[facing=east] +228:13,minecraft:cyan_shulker_box[facing=up] +228:14,minecraft:cyan_shulker_box[facing=north] +228:15,minecraft:cyan_shulker_box[facing=south] +229:0,minecraft:shulker_box[facing=down] +229:1,minecraft:shulker_box[facing=up] +229:2,minecraft:shulker_box[facing=north] +229:3,minecraft:shulker_box[facing=south] +229:4,minecraft:shulker_box[facing=west] +229:5,minecraft:shulker_box[facing=east] +229:7,minecraft:shulker_box[facing=up] +229:8,minecraft:shulker_box[facing=north] +229:9,minecraft:shulker_box[facing=south] +229:10,minecraft:shulker_box[facing=west] +229:11,minecraft:shulker_box[facing=east] +229:13,minecraft:shulker_box[facing=up] +229:14,minecraft:shulker_box[facing=north] +229:15,minecraft:shulker_box[facing=south] +230:0,minecraft:blue_shulker_box[facing=down] +230:1,minecraft:blue_shulker_box[facing=up] +230:2,minecraft:blue_shulker_box[facing=north] +230:3,minecraft:blue_shulker_box[facing=south] +230:4,minecraft:blue_shulker_box[facing=west] +230:5,minecraft:blue_shulker_box[facing=east] +230:7,minecraft:blue_shulker_box[facing=up] +230:8,minecraft:blue_shulker_box[facing=north] +230:9,minecraft:blue_shulker_box[facing=south] +230:10,minecraft:blue_shulker_box[facing=west] +230:11,minecraft:blue_shulker_box[facing=east] +230:13,minecraft:blue_shulker_box[facing=up] +230:14,minecraft:blue_shulker_box[facing=north] +230:15,minecraft:blue_shulker_box[facing=south] +231:0,minecraft:brown_shulker_box[facing=down] +231:1,minecraft:brown_shulker_box[facing=up] +231:2,minecraft:brown_shulker_box[facing=north] +231:3,minecraft:brown_shulker_box[facing=south] +231:4,minecraft:brown_shulker_box[facing=west] +231:5,minecraft:brown_shulker_box[facing=east] +231:7,minecraft:brown_shulker_box[facing=up] +231:8,minecraft:brown_shulker_box[facing=north] +231:9,minecraft:brown_shulker_box[facing=south] +231:10,minecraft:brown_shulker_box[facing=west] +231:11,minecraft:brown_shulker_box[facing=east] +231:13,minecraft:brown_shulker_box[facing=up] +231:14,minecraft:brown_shulker_box[facing=north] +231:15,minecraft:brown_shulker_box[facing=south] +232:0,minecraft:green_shulker_box[facing=down] +232:1,minecraft:green_shulker_box[facing=up] +232:2,minecraft:green_shulker_box[facing=north] +232:3,minecraft:green_shulker_box[facing=south] +232:4,minecraft:green_shulker_box[facing=west] +232:5,minecraft:green_shulker_box[facing=east] +232:7,minecraft:green_shulker_box[facing=up] +232:8,minecraft:green_shulker_box[facing=north] +232:9,minecraft:green_shulker_box[facing=south] +232:10,minecraft:green_shulker_box[facing=west] +232:11,minecraft:green_shulker_box[facing=east] +232:13,minecraft:green_shulker_box[facing=up] +232:14,minecraft:green_shulker_box[facing=north] +232:15,minecraft:green_shulker_box[facing=south] +233:0,minecraft:red_shulker_box[facing=down] +233:1,minecraft:red_shulker_box[facing=up] +233:2,minecraft:red_shulker_box[facing=north] +233:3,minecraft:red_shulker_box[facing=south] +233:4,minecraft:red_shulker_box[facing=west] +233:5,minecraft:red_shulker_box[facing=east] +233:7,minecraft:red_shulker_box[facing=up] +233:8,minecraft:red_shulker_box[facing=north] +233:9,minecraft:red_shulker_box[facing=south] +233:10,minecraft:red_shulker_box[facing=west] +233:11,minecraft:red_shulker_box[facing=east] +233:13,minecraft:red_shulker_box[facing=up] +233:14,minecraft:red_shulker_box[facing=north] +233:15,minecraft:red_shulker_box[facing=south] +234:0,minecraft:black_shulker_box[facing=down] +234:1,minecraft:black_shulker_box[facing=up] +234:2,minecraft:black_shulker_box[facing=north] +234:3,minecraft:black_shulker_box[facing=south] +234:4,minecraft:black_shulker_box[facing=west] +234:5,minecraft:black_shulker_box[facing=east] +234:7,minecraft:black_shulker_box[facing=up] +234:8,minecraft:black_shulker_box[facing=north] +234:9,minecraft:black_shulker_box[facing=south] +234:10,minecraft:black_shulker_box[facing=west] +234:11,minecraft:black_shulker_box[facing=east] +234:13,minecraft:black_shulker_box[facing=up] +234:14,minecraft:black_shulker_box[facing=north] +234:15,minecraft:black_shulker_box[facing=south] +235:0,minecraft:white_glazed_terracotta[facing=south] +235:1,minecraft:white_glazed_terracotta[facing=west] +235:2,minecraft:white_glazed_terracotta[facing=north] +235:3,minecraft:white_glazed_terracotta[facing=east] +235:5,minecraft:white_glazed_terracotta[facing=west] +235:6,minecraft:white_glazed_terracotta[facing=north] +235:7,minecraft:white_glazed_terracotta[facing=east] +235:9,minecraft:white_glazed_terracotta[facing=west] +235:10,minecraft:white_glazed_terracotta[facing=north] +235:11,minecraft:white_glazed_terracotta[facing=east] +235:13,minecraft:white_glazed_terracotta[facing=west] +235:14,minecraft:white_glazed_terracotta[facing=north] +235:15,minecraft:white_glazed_terracotta[facing=east] +236:0,minecraft:orange_glazed_terracotta[facing=south] +236:1,minecraft:orange_glazed_terracotta[facing=west] +236:2,minecraft:orange_glazed_terracotta[facing=north] +236:3,minecraft:orange_glazed_terracotta[facing=east] +236:5,minecraft:orange_glazed_terracotta[facing=west] +236:6,minecraft:orange_glazed_terracotta[facing=north] +236:7,minecraft:orange_glazed_terracotta[facing=east] +236:9,minecraft:orange_glazed_terracotta[facing=west] +236:10,minecraft:orange_glazed_terracotta[facing=north] +236:11,minecraft:orange_glazed_terracotta[facing=east] +236:13,minecraft:orange_glazed_terracotta[facing=west] +236:14,minecraft:orange_glazed_terracotta[facing=north] +236:15,minecraft:orange_glazed_terracotta[facing=east] +237:0,minecraft:magenta_glazed_terracotta[facing=south] +237:1,minecraft:magenta_glazed_terracotta[facing=west] +237:2,minecraft:magenta_glazed_terracotta[facing=north] +237:3,minecraft:magenta_glazed_terracotta[facing=east] +237:5,minecraft:magenta_glazed_terracotta[facing=west] +237:6,minecraft:magenta_glazed_terracotta[facing=north] +237:7,minecraft:magenta_glazed_terracotta[facing=east] +237:9,minecraft:magenta_glazed_terracotta[facing=west] +237:10,minecraft:magenta_glazed_terracotta[facing=north] +237:11,minecraft:magenta_glazed_terracotta[facing=east] +237:13,minecraft:magenta_glazed_terracotta[facing=west] +237:14,minecraft:magenta_glazed_terracotta[facing=north] +237:15,minecraft:magenta_glazed_terracotta[facing=east] +238:0,minecraft:light_blue_glazed_terracotta[facing=south] +238:1,minecraft:light_blue_glazed_terracotta[facing=west] +238:2,minecraft:light_blue_glazed_terracotta[facing=north] +238:3,minecraft:light_blue_glazed_terracotta[facing=east] +238:5,minecraft:light_blue_glazed_terracotta[facing=west] +238:6,minecraft:light_blue_glazed_terracotta[facing=north] +238:7,minecraft:light_blue_glazed_terracotta[facing=east] +238:9,minecraft:light_blue_glazed_terracotta[facing=west] +238:10,minecraft:light_blue_glazed_terracotta[facing=north] +238:11,minecraft:light_blue_glazed_terracotta[facing=east] +238:13,minecraft:light_blue_glazed_terracotta[facing=west] +238:14,minecraft:light_blue_glazed_terracotta[facing=north] +238:15,minecraft:light_blue_glazed_terracotta[facing=east] +239:0,minecraft:yellow_glazed_terracotta[facing=south] +239:1,minecraft:yellow_glazed_terracotta[facing=west] +239:2,minecraft:yellow_glazed_terracotta[facing=north] +239:3,minecraft:yellow_glazed_terracotta[facing=east] +239:5,minecraft:yellow_glazed_terracotta[facing=west] +239:6,minecraft:yellow_glazed_terracotta[facing=north] +239:7,minecraft:yellow_glazed_terracotta[facing=east] +239:9,minecraft:yellow_glazed_terracotta[facing=west] +239:10,minecraft:yellow_glazed_terracotta[facing=north] +239:11,minecraft:yellow_glazed_terracotta[facing=east] +239:13,minecraft:yellow_glazed_terracotta[facing=west] +239:14,minecraft:yellow_glazed_terracotta[facing=north] +239:15,minecraft:yellow_glazed_terracotta[facing=east] +240:0,minecraft:lime_glazed_terracotta[facing=south] +240:1,minecraft:lime_glazed_terracotta[facing=west] +240:2,minecraft:lime_glazed_terracotta[facing=north] +240:3,minecraft:lime_glazed_terracotta[facing=east] +240:5,minecraft:lime_glazed_terracotta[facing=west] +240:6,minecraft:lime_glazed_terracotta[facing=north] +240:7,minecraft:lime_glazed_terracotta[facing=east] +240:9,minecraft:lime_glazed_terracotta[facing=west] +240:10,minecraft:lime_glazed_terracotta[facing=north] +240:11,minecraft:lime_glazed_terracotta[facing=east] +240:13,minecraft:lime_glazed_terracotta[facing=west] +240:14,minecraft:lime_glazed_terracotta[facing=north] +240:15,minecraft:lime_glazed_terracotta[facing=east] +241:0,minecraft:pink_glazed_terracotta[facing=south] +241:1,minecraft:pink_glazed_terracotta[facing=west] +241:2,minecraft:pink_glazed_terracotta[facing=north] +241:3,minecraft:pink_glazed_terracotta[facing=east] +241:5,minecraft:pink_glazed_terracotta[facing=west] +241:6,minecraft:pink_glazed_terracotta[facing=north] +241:7,minecraft:pink_glazed_terracotta[facing=east] +241:9,minecraft:pink_glazed_terracotta[facing=west] +241:10,minecraft:pink_glazed_terracotta[facing=north] +241:11,minecraft:pink_glazed_terracotta[facing=east] +241:13,minecraft:pink_glazed_terracotta[facing=west] +241:14,minecraft:pink_glazed_terracotta[facing=north] +241:15,minecraft:pink_glazed_terracotta[facing=east] +242:0,minecraft:gray_glazed_terracotta[facing=south] +242:1,minecraft:gray_glazed_terracotta[facing=west] +242:2,minecraft:gray_glazed_terracotta[facing=north] +242:3,minecraft:gray_glazed_terracotta[facing=east] +242:5,minecraft:gray_glazed_terracotta[facing=west] +242:6,minecraft:gray_glazed_terracotta[facing=north] +242:7,minecraft:gray_glazed_terracotta[facing=east] +242:9,minecraft:gray_glazed_terracotta[facing=west] +242:10,minecraft:gray_glazed_terracotta[facing=north] +242:11,minecraft:gray_glazed_terracotta[facing=east] +242:13,minecraft:gray_glazed_terracotta[facing=west] +242:14,minecraft:gray_glazed_terracotta[facing=north] +242:15,minecraft:gray_glazed_terracotta[facing=east] +243:0,minecraft:light_gray_glazed_terracotta[facing=south] +243:1,minecraft:light_gray_glazed_terracotta[facing=west] +243:2,minecraft:light_gray_glazed_terracotta[facing=north] +243:3,minecraft:light_gray_glazed_terracotta[facing=east] +243:5,minecraft:light_gray_glazed_terracotta[facing=west] +243:6,minecraft:light_gray_glazed_terracotta[facing=north] +243:7,minecraft:light_gray_glazed_terracotta[facing=east] +243:9,minecraft:light_gray_glazed_terracotta[facing=west] +243:10,minecraft:light_gray_glazed_terracotta[facing=north] +243:11,minecraft:light_gray_glazed_terracotta[facing=east] +243:13,minecraft:light_gray_glazed_terracotta[facing=west] +243:14,minecraft:light_gray_glazed_terracotta[facing=north] +243:15,minecraft:light_gray_glazed_terracotta[facing=east] +244:0,minecraft:cyan_glazed_terracotta[facing=south] +244:1,minecraft:cyan_glazed_terracotta[facing=west] +244:2,minecraft:cyan_glazed_terracotta[facing=north] +244:3,minecraft:cyan_glazed_terracotta[facing=east] +244:5,minecraft:cyan_glazed_terracotta[facing=west] +244:6,minecraft:cyan_glazed_terracotta[facing=north] +244:7,minecraft:cyan_glazed_terracotta[facing=east] +244:9,minecraft:cyan_glazed_terracotta[facing=west] +244:10,minecraft:cyan_glazed_terracotta[facing=north] +244:11,minecraft:cyan_glazed_terracotta[facing=east] +244:13,minecraft:cyan_glazed_terracotta[facing=west] +244:14,minecraft:cyan_glazed_terracotta[facing=north] +244:15,minecraft:cyan_glazed_terracotta[facing=east] +245:0,minecraft:purple_glazed_terracotta[facing=south] +245:1,minecraft:purple_glazed_terracotta[facing=west] +245:2,minecraft:purple_glazed_terracotta[facing=north] +245:3,minecraft:purple_glazed_terracotta[facing=east] +245:5,minecraft:purple_glazed_terracotta[facing=west] +245:6,minecraft:purple_glazed_terracotta[facing=north] +245:7,minecraft:purple_glazed_terracotta[facing=east] +245:9,minecraft:purple_glazed_terracotta[facing=west] +245:10,minecraft:purple_glazed_terracotta[facing=north] +245:11,minecraft:purple_glazed_terracotta[facing=east] +245:13,minecraft:purple_glazed_terracotta[facing=west] +245:14,minecraft:purple_glazed_terracotta[facing=north] +245:15,minecraft:purple_glazed_terracotta[facing=east] +246:0,minecraft:blue_glazed_terracotta[facing=south] +246:1,minecraft:blue_glazed_terracotta[facing=west] +246:2,minecraft:blue_glazed_terracotta[facing=north] +246:3,minecraft:blue_glazed_terracotta[facing=east] +246:5,minecraft:blue_glazed_terracotta[facing=west] +246:6,minecraft:blue_glazed_terracotta[facing=north] +246:7,minecraft:blue_glazed_terracotta[facing=east] +246:9,minecraft:blue_glazed_terracotta[facing=west] +246:10,minecraft:blue_glazed_terracotta[facing=north] +246:11,minecraft:blue_glazed_terracotta[facing=east] +246:13,minecraft:blue_glazed_terracotta[facing=west] +246:14,minecraft:blue_glazed_terracotta[facing=north] +246:15,minecraft:blue_glazed_terracotta[facing=east] +247:0,minecraft:brown_glazed_terracotta[facing=south] +247:1,minecraft:brown_glazed_terracotta[facing=west] +247:2,minecraft:brown_glazed_terracotta[facing=north] +247:3,minecraft:brown_glazed_terracotta[facing=east] +247:5,minecraft:brown_glazed_terracotta[facing=west] +247:6,minecraft:brown_glazed_terracotta[facing=north] +247:7,minecraft:brown_glazed_terracotta[facing=east] +247:9,minecraft:brown_glazed_terracotta[facing=west] +247:10,minecraft:brown_glazed_terracotta[facing=north] +247:11,minecraft:brown_glazed_terracotta[facing=east] +247:13,minecraft:brown_glazed_terracotta[facing=west] +247:14,minecraft:brown_glazed_terracotta[facing=north] +247:15,minecraft:brown_glazed_terracotta[facing=east] +248:0,minecraft:green_glazed_terracotta[facing=south] +248:1,minecraft:green_glazed_terracotta[facing=west] +248:2,minecraft:green_glazed_terracotta[facing=north] +248:3,minecraft:green_glazed_terracotta[facing=east] +248:5,minecraft:green_glazed_terracotta[facing=west] +248:6,minecraft:green_glazed_terracotta[facing=north] +248:7,minecraft:green_glazed_terracotta[facing=east] +248:9,minecraft:green_glazed_terracotta[facing=west] +248:10,minecraft:green_glazed_terracotta[facing=north] +248:11,minecraft:green_glazed_terracotta[facing=east] +248:13,minecraft:green_glazed_terracotta[facing=west] +248:14,minecraft:green_glazed_terracotta[facing=north] +248:15,minecraft:green_glazed_terracotta[facing=east] +249:0,minecraft:red_glazed_terracotta[facing=south] +249:1,minecraft:red_glazed_terracotta[facing=west] +249:2,minecraft:red_glazed_terracotta[facing=north] +249:3,minecraft:red_glazed_terracotta[facing=east] +249:5,minecraft:red_glazed_terracotta[facing=west] +249:6,minecraft:red_glazed_terracotta[facing=north] +249:7,minecraft:red_glazed_terracotta[facing=east] +249:9,minecraft:red_glazed_terracotta[facing=west] +249:10,minecraft:red_glazed_terracotta[facing=north] +249:11,minecraft:red_glazed_terracotta[facing=east] +249:13,minecraft:red_glazed_terracotta[facing=west] +249:14,minecraft:red_glazed_terracotta[facing=north] +249:15,minecraft:red_glazed_terracotta[facing=east] +250:0,minecraft:black_glazed_terracotta[facing=south] +250:1,minecraft:black_glazed_terracotta[facing=west] +250:2,minecraft:black_glazed_terracotta[facing=north] +250:3,minecraft:black_glazed_terracotta[facing=east] +250:5,minecraft:black_glazed_terracotta[facing=west] +250:6,minecraft:black_glazed_terracotta[facing=north] +250:7,minecraft:black_glazed_terracotta[facing=east] +250:9,minecraft:black_glazed_terracotta[facing=west] +250:10,minecraft:black_glazed_terracotta[facing=north] +250:11,minecraft:black_glazed_terracotta[facing=east] +250:13,minecraft:black_glazed_terracotta[facing=west] +250:14,minecraft:black_glazed_terracotta[facing=north] +250:15,minecraft:black_glazed_terracotta[facing=east] +251:0,minecraft:white_concrete +251:1,minecraft:orange_concrete +251:2,minecraft:magenta_concrete +251:3,minecraft:light_blue_concrete +251:4,minecraft:yellow_concrete +251:5,minecraft:lime_concrete +251:6,minecraft:pink_concrete +251:7,minecraft:gray_concrete +251:8,minecraft:light_gray_concrete +251:9,minecraft:cyan_concrete +251:10,minecraft:purple_concrete +251:11,minecraft:blue_concrete +251:12,minecraft:brown_concrete +251:13,minecraft:green_concrete +251:14,minecraft:red_concrete +251:15,minecraft:black_concrete +252:0,minecraft:white_concrete_powder +252:1,minecraft:orange_concrete_powder +252:2,minecraft:magenta_concrete_powder +252:3,minecraft:light_blue_concrete_powder +252:4,minecraft:yellow_concrete_powder +252:5,minecraft:lime_concrete_powder +252:6,minecraft:pink_concrete_powder +252:7,minecraft:gray_concrete_powder +252:8,minecraft:light_gray_concrete_powder +252:9,minecraft:cyan_concrete_powder +252:10,minecraft:purple_concrete_powder +252:11,minecraft:blue_concrete_powder +252:12,minecraft:brown_concrete_powder +252:13,minecraft:green_concrete_powder +252:14,minecraft:red_concrete_powder +252:15,minecraft:black_concrete_powder +255:0,minecraft:structure_block[mode=save] +255:1,minecraft:structure_block[mode=load] +255:2,minecraft:structure_block[mode=corner] +255:3,minecraft:structure_block[mode=data] diff --git a/src/main/resources/itemdata.txt b/src/main/resources/itemdata.txt new file mode 100644 index 00000000..25223ce3 --- /dev/null +++ b/src/main/resources/itemdata.txt @@ -0,0 +1,839 @@ +0:0,minecraft:air +1:0,minecraft:stone +1:1,minecraft:granite +1:2,minecraft:polished_granite +1:3,minecraft:diorite +1:4,minecraft:polished_diorite +1:5,minecraft:andesite +1:6,minecraft:polished_andesite +2:0,minecraft:grass_block +3:0,minecraft:dirt +3:1,minecraft:coarse_dirt +3:2,minecraft:podzol +4:0,minecraft:cobblestone +5:0,minecraft:oak_planks +5:1,minecraft:spruce_planks +5:2,minecraft:birch_planks +5:3,minecraft:jungle_planks +5:4,minecraft:acacia_planks +5:5,minecraft:dark_oak_planks +6:0,minecraft:oak_sapling +6:1,minecraft:spruce_sapling +6:2,minecraft:birch_sapling +6:3,minecraft:jungle_sapling +6:4,minecraft:acacia_sapling +6:5,minecraft:dark_oak_sapling +6:9,minecraft:spruce_sapling +6:10,minecraft:birch_sapling +6:11,minecraft:jungle_sapling +6:12,minecraft:acacia_sapling +6:13,minecraft:dark_oak_sapling +7:0,minecraft:bedrock +8:0,minecraft:water +9:0,minecraft:water +10:0,minecraft:lava +11:0,minecraft:lava +12:0,minecraft:sand +12:1,minecraft:red_sand +13:0,minecraft:gravel +14:0,minecraft:gold_ore +15:0,minecraft:iron_ore +16:0,minecraft:coal_ore +17:0,minecraft:oak_log +17:1,minecraft:spruce_log +17:2,minecraft:birch_log +17:3,minecraft:jungle_log +17:5,minecraft:spruce_log +17:6,minecraft:birch_log +17:7,minecraft:jungle_log +17:9,minecraft:spruce_log +17:10,minecraft:birch_log +17:11,minecraft:jungle_log +18:0,minecraft:oak_leaves +18:1,minecraft:spruce_leaves +18:2,minecraft:birch_leaves +18:3,minecraft:jungle_leaves +18:5,minecraft:spruce_leaves +18:6,minecraft:birch_leaves +18:7,minecraft:jungle_leaves +18:9,minecraft:spruce_leaves +18:10,minecraft:birch_leaves +18:11,minecraft:jungle_leaves +18:13,minecraft:spruce_leaves +18:14,minecraft:birch_leaves +18:15,minecraft:jungle_leaves +19:0,minecraft:sponge +19:1,minecraft:wet_sponge +20:0,minecraft:glass +21:0,minecraft:lapis_ore +22:0,minecraft:lapis_block +23:0,minecraft:dispenser +24:0,minecraft:sandstone +24:1,minecraft:chiseled_sandstone +24:2,minecraft:cut_sandstone +25:0,minecraft:note_block +26:0,minecraft:red_bed +27:0,minecraft:powered_rail +28:0,minecraft:detector_rail +29:0,minecraft:sticky_piston +30:0,minecraft:cobweb +31:0,minecraft:dead_bush +31:1,minecraft:grass +31:2,minecraft:fern +32:0,minecraft:dead_bush +33:0,minecraft:piston +34:0,minecraft:piston_head +35:0,minecraft:white_wool +35:1,minecraft:orange_wool +35:2,minecraft:magenta_wool +35:3,minecraft:light_blue_wool +35:4,minecraft:yellow_wool +35:5,minecraft:lime_wool +35:6,minecraft:pink_wool +35:7,minecraft:gray_wool +35:8,minecraft:light_gray_wool +35:9,minecraft:cyan_wool +35:10,minecraft:purple_wool +35:11,minecraft:blue_wool +35:12,minecraft:brown_wool +35:13,minecraft:green_wool +35:14,minecraft:red_wool +35:15,minecraft:black_wool +36:0,minecraft:moving_piston +37:0,minecraft:dandelion +38:0,minecraft:poppy +38:1,minecraft:blue_orchid +38:2,minecraft:allium +38:3,minecraft:azure_bluet +38:4,minecraft:red_tulip +38:5,minecraft:orange_tulip +38:6,minecraft:white_tulip +38:7,minecraft:pink_tulip +38:8,minecraft:oxeye_daisy +39:0,minecraft:brown_mushroom +40:0,minecraft:red_mushroom +41:0,minecraft:gold_block +42:0,minecraft:iron_block +43:0,minecraft:smooth_stone_slab +43:1,minecraft:sandstone_slab +43:2,minecraft:petrified_oak_slab +43:3,minecraft:cobblestone_slab +43:4,minecraft:brick_slab +43:5,minecraft:stone_brick_slab +43:6,minecraft:nether_brick_slab +43:7,minecraft:quartz_slab +43:8,minecraft:smooth_stone +43:9,minecraft:smooth_sandstone +43:10,minecraft:petrified_oak_slab +43:11,minecraft:cobblestone_slab +43:12,minecraft:brick_slab +43:13,minecraft:stone_brick_slab +43:14,minecraft:nether_brick_slab +43:15,minecraft:smooth_quartz +44:0,minecraft:stone_slab +44:1,minecraft:sandstone_slab +44:2,minecraft:petrified_oak_slab +44:3,minecraft:cobblestone_slab +44:4,minecraft:brick_slab +44:5,minecraft:stone_brick_slab +44:6,minecraft:nether_brick_slab +44:7,minecraft:quartz_slab +44:9,minecraft:sandstone_slab +44:10,minecraft:petrified_oak_slab +44:11,minecraft:cobblestone_slab +44:12,minecraft:brick_slab +44:13,minecraft:stone_brick_slab +44:14,minecraft:nether_brick_slab +44:15,minecraft:quartz_slab +45:0,minecraft:bricks +46:0,minecraft:tnt +47:0,minecraft:bookshelf +48:0,minecraft:mossy_cobblestone +49:0,minecraft:obsidian +50:0,minecraft:torch +50:1,minecraft:wall_torch +50:2,minecraft:wall_torch +50:3,minecraft:wall_torch +50:4,minecraft:wall_torch +51:0,minecraft:fire +52:0,minecraft:spawner +53:0,minecraft:oak_stairs +54:0,minecraft:chest +55:0,minecraft:redstone_wire +56:0,minecraft:diamond_ore +57:0,minecraft:diamond_block +58:0,minecraft:crafting_table +59:0,minecraft:wheat +60:0,minecraft:farmland +61:0,minecraft:furnace +62:0,minecraft:air +62:2,minecraft:furnace +62:3,minecraft:furnace +62:4,minecraft:furnace +62:5,minecraft:furnace +63:0,minecraft:oak_sign +64:0,minecraft:oak_door +65:0,minecraft:ladder +66:0,minecraft:rail +67:0,minecraft:cobblestone_stairs +68:0,minecraft:air +68:2,minecraft:oak_wall_sign +68:3,minecraft:oak_wall_sign +68:4,minecraft:oak_wall_sign +68:5,minecraft:oak_wall_sign +69:0,minecraft:lever +70:0,minecraft:stone_pressure_plate +71:0,minecraft:iron_door +72:0,minecraft:oak_pressure_plate +73:0,minecraft:redstone_ore +74:0,minecraft:redstone_ore +75:0,minecraft:air +75:1,minecraft:redstone_wall_torch +75:2,minecraft:redstone_wall_torch +75:3,minecraft:redstone_wall_torch +75:4,minecraft:redstone_wall_torch +75:5,minecraft:redstone_torch +76:0,minecraft:redstone_torch +76:1,minecraft:redstone_wall_torch +76:2,minecraft:redstone_wall_torch +76:3,minecraft:redstone_wall_torch +76:4,minecraft:redstone_wall_torch +77:0,minecraft:stone_button +78:0,minecraft:snow +79:0,minecraft:ice +80:0,minecraft:snow_block +81:0,minecraft:cactus +82:0,minecraft:clay +83:0,minecraft:sugar_cane +84:0,minecraft:jukebox +85:0,minecraft:oak_fence +86:0,minecraft:carved_pumpkin +87:0,minecraft:netherrack +88:0,minecraft:soul_sand +89:0,minecraft:glowstone +90:0,minecraft:nether_portal +91:0,minecraft:jack_o_lantern +92:0,minecraft:cake +93:0,minecraft:repeater +94:0,minecraft:repeater +95:0,minecraft:white_stained_glass +95:1,minecraft:orange_stained_glass +95:2,minecraft:magenta_stained_glass +95:3,minecraft:light_blue_stained_glass +95:4,minecraft:yellow_stained_glass +95:5,minecraft:lime_stained_glass +95:6,minecraft:pink_stained_glass +95:7,minecraft:gray_stained_glass +95:8,minecraft:light_gray_stained_glass +95:9,minecraft:cyan_stained_glass +95:10,minecraft:purple_stained_glass +95:11,minecraft:blue_stained_glass +95:12,minecraft:brown_stained_glass +95:13,minecraft:green_stained_glass +95:14,minecraft:red_stained_glass +95:15,minecraft:black_stained_glass +96:0,minecraft:oak_trapdoor +97:0,minecraft:infested_stone +97:1,minecraft:infested_cobblestone +97:2,minecraft:infested_stone_bricks +97:3,minecraft:infested_mossy_stone_bricks +97:4,minecraft:infested_cracked_stone_bricks +97:5,minecraft:infested_chiseled_stone_bricks +98:0,minecraft:stone_bricks +98:1,minecraft:mossy_stone_bricks +98:2,minecraft:cracked_stone_bricks +98:3,minecraft:chiseled_stone_bricks +99:0,minecraft:brown_mushroom_block +99:10,minecraft:mushroom_stem +99:15,minecraft:mushroom_stem +100:0,minecraft:red_mushroom_block +100:10,minecraft:mushroom_stem +100:15,minecraft:mushroom_stem +101:0,minecraft:iron_bars +102:0,minecraft:glass_pane +103:0,minecraft:melon +104:0,minecraft:pumpkin_stem +105:0,minecraft:melon_stem +106:0,minecraft:vine +107:0,minecraft:oak_fence_gate +108:0,minecraft:brick_stairs +109:0,minecraft:stone_brick_stairs +110:0,minecraft:mycelium +111:0,minecraft:lily_pad +112:0,minecraft:nether_bricks +113:0,minecraft:nether_brick_fence +114:0,minecraft:nether_brick_stairs +115:0,minecraft:nether_wart +116:0,minecraft:enchanting_table +117:0,minecraft:brewing_stand +118:0,minecraft:cauldron +119:0,minecraft:end_portal +120:0,minecraft:end_portal_frame +121:0,minecraft:end_stone +122:0,minecraft:dragon_egg +123:0,minecraft:redstone_lamp +124:0,minecraft:redstone_lamp +125:0,minecraft:oak_slab +125:1,minecraft:spruce_slab +125:2,minecraft:birch_slab +125:3,minecraft:jungle_slab +125:4,minecraft:acacia_slab +125:5,minecraft:dark_oak_slab +126:0,minecraft:oak_slab +126:1,minecraft:spruce_slab +126:2,minecraft:birch_slab +126:3,minecraft:jungle_slab +126:4,minecraft:acacia_slab +126:5,minecraft:dark_oak_slab +126:9,minecraft:spruce_slab +126:10,minecraft:birch_slab +126:11,minecraft:jungle_slab +126:12,minecraft:acacia_slab +126:13,minecraft:dark_oak_slab +127:0,minecraft:cocoa +128:0,minecraft:sandstone_stairs +129:0,minecraft:emerald_ore +130:0,minecraft:ender_chest +131:0,minecraft:tripwire_hook +132:0,minecraft:tripwire +133:0,minecraft:emerald_block +134:0,minecraft:spruce_stairs +135:0,minecraft:birch_stairs +136:0,minecraft:jungle_stairs +137:0,minecraft:command_block +138:0,minecraft:beacon +139:0,minecraft:cobblestone_wall +139:1,minecraft:mossy_cobblestone_wall +140:0,minecraft:potted_cactus +141:0,minecraft:carrots +142:0,minecraft:potatoes +143:0,minecraft:oak_button +144:0,minecraft:air +145:0,minecraft:anvil +145:4,minecraft:chipped_anvil +145:5,minecraft:chipped_anvil +145:6,minecraft:chipped_anvil +145:7,minecraft:chipped_anvil +145:8,minecraft:damaged_anvil +145:9,minecraft:damaged_anvil +145:10,minecraft:damaged_anvil +145:11,minecraft:damaged_anvil +146:0,minecraft:trapped_chest +147:0,minecraft:light_weighted_pressure_plate +148:0,minecraft:heavy_weighted_pressure_plate +149:0,minecraft:comparator +150:0,minecraft:comparator +151:0,minecraft:daylight_detector +152:0,minecraft:redstone_block +153:0,minecraft:nether_quartz_ore +154:0,minecraft:hopper +155:0,minecraft:quartz_block +155:1,minecraft:chiseled_quartz_block +155:2,minecraft:quartz_pillar +155:3,minecraft:quartz_pillar +155:4,minecraft:quartz_pillar +156:0,minecraft:quartz_stairs +157:0,minecraft:activator_rail +158:0,minecraft:dropper +159:0,minecraft:white_terracotta +159:1,minecraft:orange_terracotta +159:2,minecraft:magenta_terracotta +159:3,minecraft:light_blue_terracotta +159:4,minecraft:yellow_terracotta +159:5,minecraft:lime_terracotta +159:6,minecraft:pink_terracotta +159:7,minecraft:gray_terracotta +159:8,minecraft:light_gray_terracotta +159:9,minecraft:cyan_terracotta +159:10,minecraft:purple_terracotta +159:11,minecraft:blue_terracotta +159:12,minecraft:brown_terracotta +159:13,minecraft:green_terracotta +159:14,minecraft:red_terracotta +159:15,minecraft:black_terracotta +160:0,minecraft:white_stained_glass_pane +160:1,minecraft:orange_stained_glass_pane +160:2,minecraft:magenta_stained_glass_pane +160:3,minecraft:light_blue_stained_glass_pane +160:4,minecraft:yellow_stained_glass_pane +160:5,minecraft:lime_stained_glass_pane +160:6,minecraft:pink_stained_glass_pane +160:7,minecraft:gray_stained_glass_pane +160:8,minecraft:light_gray_stained_glass_pane +160:9,minecraft:cyan_stained_glass_pane +160:10,minecraft:purple_stained_glass_pane +160:11,minecraft:blue_stained_glass_pane +160:12,minecraft:brown_stained_glass_pane +160:13,minecraft:green_stained_glass_pane +160:14,minecraft:red_stained_glass_pane +160:15,minecraft:black_stained_glass_pane +161:0,minecraft:acacia_leaves +161:1,minecraft:dark_oak_leaves +161:5,minecraft:dark_oak_leaves +161:9,minecraft:dark_oak_leaves +161:13,minecraft:dark_oak_leaves +162:0,minecraft:acacia_log +162:1,minecraft:dark_oak_log +162:5,minecraft:dark_oak_log +162:9,minecraft:dark_oak_log +163:0,minecraft:acacia_stairs +164:0,minecraft:dark_oak_stairs +165:0,minecraft:slime_block +166:0,minecraft:barrier +167:0,minecraft:iron_trapdoor +168:0,minecraft:prismarine +168:1,minecraft:prismarine_bricks +168:2,minecraft:dark_prismarine +169:0,minecraft:sea_lantern +170:0,minecraft:hay_block +171:0,minecraft:white_carpet +171:1,minecraft:orange_carpet +171:2,minecraft:magenta_carpet +171:3,minecraft:light_blue_carpet +171:4,minecraft:yellow_carpet +171:5,minecraft:lime_carpet +171:6,minecraft:pink_carpet +171:7,minecraft:gray_carpet +171:8,minecraft:light_gray_carpet +171:9,minecraft:cyan_carpet +171:10,minecraft:purple_carpet +171:11,minecraft:blue_carpet +171:12,minecraft:brown_carpet +171:13,minecraft:green_carpet +171:14,minecraft:red_carpet +171:15,minecraft:black_carpet +172:0,minecraft:terracotta +173:0,minecraft:coal_block +174:0,minecraft:packed_ice +175:0,minecraft:sunflower +175:1,minecraft:lilac +175:2,minecraft:tall_grass +175:3,minecraft:large_fern +175:4,minecraft:rose_bush +175:5,minecraft:peony +175:8,minecraft:peony +175:9,minecraft:peony +175:10,minecraft:peony +175:11,minecraft:peony +176:0,minecraft:white_banner +177:0,minecraft:air +177:2,minecraft:white_wall_banner +177:3,minecraft:white_wall_banner +177:4,minecraft:white_wall_banner +177:5,minecraft:white_wall_banner +178:0,minecraft:daylight_detector +179:0,minecraft:red_sandstone +179:1,minecraft:chiseled_red_sandstone +179:2,minecraft:cut_red_sandstone +180:0,minecraft:red_sandstone_stairs +181:0,minecraft:red_sandstone_slab +181:8,minecraft:smooth_red_sandstone +182:0,minecraft:red_sandstone_slab +183:0,minecraft:spruce_fence_gate +184:0,minecraft:birch_fence_gate +185:0,minecraft:jungle_fence_gate +186:0,minecraft:dark_oak_fence_gate +187:0,minecraft:acacia_fence_gate +188:0,minecraft:spruce_fence +189:0,minecraft:birch_fence +190:0,minecraft:jungle_fence +191:0,minecraft:dark_oak_fence +192:0,minecraft:acacia_fence +193:0,minecraft:spruce_door +194:0,minecraft:birch_door +195:0,minecraft:jungle_door +196:0,minecraft:acacia_door +197:0,minecraft:dark_oak_door +198:0,minecraft:end_rod +199:0,minecraft:chorus_plant +200:0,minecraft:chorus_flower +201:0,minecraft:purpur_block +202:0,minecraft:purpur_pillar +203:0,minecraft:purpur_stairs +204:0,minecraft:purpur_slab +205:0,minecraft:purpur_slab +206:0,minecraft:end_stone_bricks +207:0,minecraft:beetroots +208:0,minecraft:dirt_path +209:0,minecraft:end_gateway +210:0,minecraft:repeating_command_block +211:0,minecraft:chain_command_block +212:0,minecraft:frosted_ice +213:0,minecraft:magma_block +214:0,minecraft:nether_wart_block +215:0,minecraft:red_nether_bricks +216:0,minecraft:bone_block +217:0,minecraft:structure_void +218:0,minecraft:observer +219:0,minecraft:white_shulker_box +220:0,minecraft:orange_shulker_box +221:0,minecraft:magenta_shulker_box +222:0,minecraft:light_blue_shulker_box +223:0,minecraft:yellow_shulker_box +224:0,minecraft:lime_shulker_box +225:0,minecraft:pink_shulker_box +226:0,minecraft:gray_shulker_box +227:0,minecraft:light_gray_shulker_box +228:0,minecraft:cyan_shulker_box +229:0,minecraft:purple_shulker_box +230:0,minecraft:blue_shulker_box +231:0,minecraft:brown_shulker_box +232:0,minecraft:green_shulker_box +233:0,minecraft:red_shulker_box +234:0,minecraft:black_shulker_box +235:0,minecraft:white_glazed_terracotta +236:0,minecraft:orange_glazed_terracotta +237:0,minecraft:magenta_glazed_terracotta +238:0,minecraft:light_blue_glazed_terracotta +239:0,minecraft:yellow_glazed_terracotta +240:0,minecraft:lime_glazed_terracotta +241:0,minecraft:pink_glazed_terracotta +242:0,minecraft:gray_glazed_terracotta +243:0,minecraft:light_gray_glazed_terracotta +244:0,minecraft:cyan_glazed_terracotta +245:0,minecraft:purple_glazed_terracotta +246:0,minecraft:blue_glazed_terracotta +247:0,minecraft:brown_glazed_terracotta +248:0,minecraft:green_glazed_terracotta +249:0,minecraft:red_glazed_terracotta +250:0,minecraft:black_glazed_terracotta +251:0,minecraft:white_concrete +251:1,minecraft:orange_concrete +251:2,minecraft:magenta_concrete +251:3,minecraft:light_blue_concrete +251:4,minecraft:yellow_concrete +251:5,minecraft:lime_concrete +251:6,minecraft:pink_concrete +251:7,minecraft:gray_concrete +251:8,minecraft:light_gray_concrete +251:9,minecraft:cyan_concrete +251:10,minecraft:purple_concrete +251:11,minecraft:blue_concrete +251:12,minecraft:brown_concrete +251:13,minecraft:green_concrete +251:14,minecraft:red_concrete +251:15,minecraft:black_concrete +252:0,minecraft:white_concrete_powder +252:1,minecraft:orange_concrete_powder +252:2,minecraft:magenta_concrete_powder +252:3,minecraft:light_blue_concrete_powder +252:4,minecraft:yellow_concrete_powder +252:5,minecraft:lime_concrete_powder +252:6,minecraft:pink_concrete_powder +252:7,minecraft:gray_concrete_powder +252:8,minecraft:light_gray_concrete_powder +252:9,minecraft:cyan_concrete_powder +252:10,minecraft:purple_concrete_powder +252:11,minecraft:blue_concrete_powder +252:12,minecraft:brown_concrete_powder +252:13,minecraft:green_concrete_powder +252:14,minecraft:red_concrete_powder +252:15,minecraft:black_concrete_powder +255:0,minecraft:structure_block +256:0,minecraft:iron_shovel +257:0,minecraft:iron_pickaxe +258:0,minecraft:iron_axe +259:0,minecraft:flint_and_steel +260:0,minecraft:apple +261:0,minecraft:bow +262:0,minecraft:arrow +263:0,minecraft:coal +263:1,minecraft:charcoal +264:0,minecraft:diamond +265:0,minecraft:iron_ingot +266:0,minecraft:gold_ingot +267:0,minecraft:iron_sword +268:0,minecraft:wooden_sword +269:0,minecraft:wooden_shovel +270:0,minecraft:wooden_pickaxe +271:0,minecraft:wooden_axe +272:0,minecraft:stone_sword +273:0,minecraft:stone_shovel +274:0,minecraft:stone_pickaxe +275:0,minecraft:stone_axe +276:0,minecraft:diamond_sword +277:0,minecraft:diamond_shovel +278:0,minecraft:diamond_pickaxe +279:0,minecraft:diamond_axe +280:0,minecraft:stick +281:0,minecraft:bowl +282:0,minecraft:mushroom_stew +283:0,minecraft:golden_sword +284:0,minecraft:golden_shovel +285:0,minecraft:golden_pickaxe +286:0,minecraft:golden_axe +287:0,minecraft:string +288:0,minecraft:feather +289:0,minecraft:gunpowder +290:0,minecraft:wooden_hoe +291:0,minecraft:stone_hoe +292:0,minecraft:iron_hoe +293:0,minecraft:diamond_hoe +294:0,minecraft:golden_hoe +295:0,minecraft:wheat_seeds +296:0,minecraft:wheat +297:0,minecraft:bread +298:0,minecraft:leather_helmet +299:0,minecraft:leather_chestplate +300:0,minecraft:leather_leggings +301:0,minecraft:leather_boots +302:0,minecraft:chainmail_helmet +303:0,minecraft:chainmail_chestplate +304:0,minecraft:chainmail_leggings +305:0,minecraft:chainmail_boots +306:0,minecraft:iron_helmet +307:0,minecraft:iron_chestplate +308:0,minecraft:iron_leggings +309:0,minecraft:iron_boots +310:0,minecraft:diamond_helmet +311:0,minecraft:diamond_chestplate +312:0,minecraft:diamond_leggings +313:0,minecraft:diamond_boots +314:0,minecraft:golden_helmet +315:0,minecraft:golden_chestplate +316:0,minecraft:golden_leggings +317:0,minecraft:golden_boots +318:0,minecraft:flint +319:0,minecraft:porkchop +320:0,minecraft:cooked_porkchop +321:0,minecraft:painting +322:0,minecraft:golden_apple +322:1,minecraft:enchanted_golden_apple +323:0,minecraft:oak_sign +324:0,minecraft:oak_door +325:0,minecraft:bucket +326:0,minecraft:water_bucket +327:0,minecraft:lava_bucket +328:0,minecraft:minecart +329:0,minecraft:saddle +330:0,minecraft:iron_door +331:0,minecraft:redstone +332:0,minecraft:snowball +333:0,minecraft:oak_boat +334:0,minecraft:leather +335:0,minecraft:milk_bucket +336:0,minecraft:brick +337:0,minecraft:clay_ball +338:0,minecraft:sugar_cane +339:0,minecraft:paper +340:0,minecraft:book +341:0,minecraft:slime_ball +342:0,minecraft:chest_minecart +343:0,minecraft:furnace_minecart +344:0,minecraft:egg +345:0,minecraft:compass +346:0,minecraft:fishing_rod +347:0,minecraft:clock +348:0,minecraft:glowstone_dust +349:0,minecraft:cod +349:1,minecraft:salmon +349:2,minecraft:tropical_fish +349:3,minecraft:pufferfish +350:0,minecraft:cooked_cod +350:1,minecraft:cooked_salmon +351:0,minecraft:ink_sac +351:1,minecraft:red_dye +351:2,minecraft:green_dye +351:3,minecraft:cocoa_beans +351:4,minecraft:lapis_lazuli +351:5,minecraft:purple_dye +351:6,minecraft:cyan_dye +351:7,minecraft:light_gray_dye +351:8,minecraft:gray_dye +351:9,minecraft:pink_dye +351:10,minecraft:lime_dye +351:11,minecraft:yellow_dye +351:12,minecraft:light_blue_dye +351:13,minecraft:magenta_dye +351:14,minecraft:orange_dye +351:15,minecraft:bone_meal +352:0,minecraft:bone +353:0,minecraft:sugar +354:0,minecraft:cake +355:0,minecraft:red_bed +355:1,minecraft:orange_bed +355:2,minecraft:magenta_bed +355:3,minecraft:light_blue_bed +355:4,minecraft:yellow_bed +355:5,minecraft:lime_bed +355:6,minecraft:pink_bed +355:7,minecraft:gray_bed +355:8,minecraft:light_gray_bed +355:9,minecraft:cyan_bed +355:10,minecraft:purple_bed +355:11,minecraft:blue_bed +355:12,minecraft:brown_bed +355:13,minecraft:green_bed +355:15,minecraft:black_bed +356:0,minecraft:repeater +357:0,minecraft:cookie +358:0,minecraft:filled_map +359:0,minecraft:shears +360:0,minecraft:melon_slice +361:0,minecraft:pumpkin_seeds +362:0,minecraft:melon_seeds +363:0,minecraft:beef +364:0,minecraft:cooked_beef +365:0,minecraft:chicken +366:0,minecraft:cooked_chicken +367:0,minecraft:rotten_flesh +368:0,minecraft:ender_pearl +369:0,minecraft:blaze_rod +370:0,minecraft:ghast_tear +371:0,minecraft:gold_nugget +372:0,minecraft:nether_wart +373:0,minecraft:potion +374:0,minecraft:glass_bottle +375:0,minecraft:spider_eye +376:0,minecraft:fermented_spider_eye +377:0,minecraft:blaze_powder +378:0,minecraft:magma_cream +379:0,minecraft:brewing_stand +380:0,minecraft:cauldron +381:0,minecraft:ender_eye +382:0,minecraft:glistering_melon_slice +383:0,minecraft:pig_spawn_egg +383:4,minecraft:elder_guardian_spawn_egg +383:5,minecraft:wither_skeleton_spawn_egg +383:6,minecraft:stray_spawn_egg +383:23,minecraft:husk_spawn_egg +383:27,minecraft:zombie_villager_spawn_egg +383:28,minecraft:skeleton_horse_spawn_egg +383:29,minecraft:zombie_horse_spawn_egg +383:31,minecraft:donkey_spawn_egg +383:32,minecraft:mule_spawn_egg +383:34,minecraft:evoker_spawn_egg +383:35,minecraft:vex_spawn_egg +383:36,minecraft:vindicator_spawn_egg +383:50,minecraft:creeper_spawn_egg +383:51,minecraft:skeleton_spawn_egg +383:52,minecraft:spider_spawn_egg +383:54,minecraft:zombie_spawn_egg +383:55,minecraft:slime_spawn_egg +383:56,minecraft:ghast_spawn_egg +383:57,minecraft:zombie_pigman_spawn_egg +383:58,minecraft:enderman_spawn_egg +383:59,minecraft:cave_spider_spawn_egg +383:60,minecraft:silverfish_spawn_egg +383:61,minecraft:blaze_spawn_egg +383:62,minecraft:magma_cube_spawn_egg +383:65,minecraft:bat_spawn_egg +383:66,minecraft:witch_spawn_egg +383:67,minecraft:endermite_spawn_egg +383:68,minecraft:guardian_spawn_egg +383:69,minecraft:shulker_spawn_egg +383:91,minecraft:sheep_spawn_egg +383:92,minecraft:cow_spawn_egg +383:93,minecraft:chicken_spawn_egg +383:94,minecraft:squid_spawn_egg +383:95,minecraft:wolf_spawn_egg +383:96,minecraft:mooshroom_spawn_egg +383:98,minecraft:ocelot_spawn_egg +383:100,minecraft:horse_spawn_egg +383:101,minecraft:rabbit_spawn_egg +383:102,minecraft:polar_bear_spawn_egg +383:103,minecraft:llama_spawn_egg +383:105,minecraft:parrot_spawn_egg +383:120,minecraft:villager_spawn_egg +383:255,minecraft:turtle_spawn_egg +384:0,minecraft:experience_bottle +385:0,minecraft:fire_charge +386:0,minecraft:writable_book +387:0,minecraft:written_book +388:0,minecraft:emerald +389:0,minecraft:item_frame +390:0,minecraft:flower_pot +391:0,minecraft:carrot +392:0,minecraft:potato +393:0,minecraft:baked_potato +394:0,minecraft:poisonous_potato +395:0,minecraft:map +396:0,minecraft:golden_carrot +397:0,minecraft:skeleton_skull +397:1,minecraft:wither_skeleton_skull +397:2,minecraft:zombie_head +397:3,minecraft:player_head +397:4,minecraft:creeper_head +397:5,minecraft:dragon_head +398:0,minecraft:carrot_on_a_stick +399:0,minecraft:nether_star +400:0,minecraft:pumpkin_pie +401:0,minecraft:firework_rocket +402:0,minecraft:firework_star +403:0,minecraft:enchanted_book +404:0,minecraft:comparator +405:0,minecraft:nether_brick +406:0,minecraft:quartz +407:0,minecraft:tnt_minecart +408:0,minecraft:hopper_minecart +409:0,minecraft:prismarine_shard +410:0,minecraft:prismarine_crystals +411:0,minecraft:rabbit +412:0,minecraft:cooked_rabbit +413:0,minecraft:rabbit_stew +414:0,minecraft:rabbit_foot +415:0,minecraft:rabbit_hide +416:0,minecraft:armor_stand +417:0,minecraft:iron_horse_armor +418:0,minecraft:golden_horse_armor +419:0,minecraft:diamond_horse_armor +420:0,minecraft:lead +421:0,minecraft:name_tag +422:0,minecraft:command_block_minecart +423:0,minecraft:mutton +424:0,minecraft:cooked_mutton +425:0,minecraft:black_banner +425:1,minecraft:red_banner +425:2,minecraft:green_banner +425:3,minecraft:brown_banner +425:4,minecraft:blue_banner +425:5,minecraft:purple_banner +425:6,minecraft:cyan_banner +425:7,minecraft:light_gray_banner +425:8,minecraft:gray_banner +425:9,minecraft:pink_banner +425:10,minecraft:lime_banner +425:11,minecraft:yellow_banner +425:12,minecraft:light_blue_banner +425:13,minecraft:magenta_banner +425:14,minecraft:orange_banner +425:15,minecraft:white_banner +426:0,minecraft:end_crystal +427:0,minecraft:spruce_door +428:0,minecraft:birch_door +429:0,minecraft:jungle_door +430:0,minecraft:acacia_door +431:0,minecraft:dark_oak_door +432:0,minecraft:chorus_fruit +433:0,minecraft:popped_chorus_fruit +434:0,minecraft:beetroot +435:0,minecraft:beetroot_seeds +436:0,minecraft:beetroot_soup +437:0,minecraft:dragon_breath +438:0,minecraft:splash_potion +439:0,minecraft:spectral_arrow +440:0,minecraft:tipped_arrow +441:0,minecraft:lingering_potion +442:0,minecraft:shield +443:0,minecraft:elytra +444:0,minecraft:spruce_boat +445:0,minecraft:birch_boat +446:0,minecraft:jungle_boat +447:0,minecraft:acacia_boat +448:0,minecraft:dark_oak_boat +449:0,minecraft:totem_of_undying +450:0,minecraft:shulker_shell +452:0,minecraft:iron_nugget +453:0,minecraft:knowledge_book +2256:0,minecraft:music_disc_13 +2257:0,minecraft:music_disc_cat +2258:0,minecraft:music_disc_blocks +2259:0,minecraft:music_disc_chirp +2260:0,minecraft:music_disc_far +2261:0,minecraft:music_disc_mall +2262:0,minecraft:music_disc_mellohi +2263:0,minecraft:music_disc_stal +2264:0,minecraft:music_disc_strad +2265:0,minecraft:music_disc_ward +2266:0,minecraft:music_disc_11 +2267:0,minecraft:music_disc_wait diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 7b908bbf..9c9a7930 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -5,7 +5,8 @@ authors: [md_5, ammar2, frymaster] website: http://dev.bukkit.org/server-mods/logblock/ main: de.diddiz.LogBlock.LogBlock description: ${project.description} -softdepend: [LogBlockQuestioner, WorldEdit] +softdepend: [WorldEdit, WorldGuard] +api-version: 1.20 commands: lb: description: 'LogBlock plugin commands' diff --git a/src/test/java/de/diddiz/LogBlock/QueryParsingTest.java b/src/test/java/de/diddiz/LogBlock/QueryParsingTest.java index 30e8ee60..751aa4c0 100644 --- a/src/test/java/de/diddiz/LogBlock/QueryParsingTest.java +++ b/src/test/java/de/diddiz/LogBlock/QueryParsingTest.java @@ -1,10 +1,8 @@ package de.diddiz.LogBlock; - -import de.diddiz.util.Utils; import org.junit.Assert; import org.junit.Test; - +import de.diddiz.LogBlock.util.Utils; import java.util.Arrays; import java.util.List;