diff --git a/build.gradle b/build.gradle index 6310f69..79ce0a0 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,7 @@ targetCompatibility = 1.8 group = "io.github.lxgaming" archivesBaseName = "Sledgehammer" -version = "1.12.2-1.2.17" +version = "1.12.2-1.2.18" minecraft { version = "1.12.2-14.23.5.2768" diff --git a/src/main/java/io/github/lxgaming/sledgehammer/Sledgehammer.java b/src/main/java/io/github/lxgaming/sledgehammer/Sledgehammer.java index 1275113..87f3b36 100644 --- a/src/main/java/io/github/lxgaming/sledgehammer/Sledgehammer.java +++ b/src/main/java/io/github/lxgaming/sledgehammer/Sledgehammer.java @@ -76,8 +76,12 @@ private void registerMappings() { getMixinMappings().put("io.github.lxgaming.sledgehammer.mixin.core.network.MixinNetHandlerPlayServer_Event", MixinCategory::isInteractEvents); getMixinMappings().put("io.github.lxgaming.sledgehammer.mixin.core.network.MixinNetworkManager", MixinCategory::isFlushNetworkOnTick); getMixinMappings().put("io.github.lxgaming.sledgehammer.mixin.core.network.MixinNetworkSystem", MixinCategory::isNetworkSystem); - getMixinMappings().put("io.github.lxgaming.sledgehammer.mixin.core.server.MixinDedicatedServer", (module) -> true); + getMixinMappings().put("io.github.lxgaming.sledgehammer.mixin.core.server.MixinDedicatedServer", category -> true); getMixinMappings().put("io.github.lxgaming.sledgehammer.mixin.core.world.biome.MixinBiomeProvider", MixinCategory::isBiomeProvider); + getMixinMappings().put("io.github.lxgaming.sledgehammer.mixin.core.world.chunk.storage.MixinAnvilChunkLoader", category -> + category.isChunkSaveAlert() || category.isChunkSavePurgeBlacklist() || category.isChunkSavePurgeAll()); + getMixinMappings().put("io.github.lxgaming.sledgehammer.mixin.core.world.chunk.storage.MixinRegionFileChunkBuffer", category -> + category.isChunkSaveAlert() || category.isChunkSavePurgeBlacklist() || category.isChunkSavePurgeAll()); // Mixin Forge getMixinMappings().put("io.github.lxgaming.sledgehammer.mixin.forge.common.MixinForgeHooks_Advancement", MixinCategory::isAdvancementStacktrace); diff --git a/src/main/java/io/github/lxgaming/sledgehammer/configuration/category/MessageCategory.java b/src/main/java/io/github/lxgaming/sledgehammer/configuration/category/MessageCategory.java index b3fb251..6eccd3e 100644 --- a/src/main/java/io/github/lxgaming/sledgehammer/configuration/category/MessageCategory.java +++ b/src/main/java/io/github/lxgaming/sledgehammer/configuration/category/MessageCategory.java @@ -22,17 +22,24 @@ @ConfigSerializable public class MessageCategory { - @Setting(value = "move-outside-border", comment = "Sent to the player when attempting to move outside the world border") - private String moveOutsideBorder = "&cCannot move outside of the world border"; + @Setting(value = "chunk-save", comment = "Sent to the player when a chunk fails to save") + private String chunkSave = "&fChunk ([X], [Z]) &cfailed to save"; @Setting(value = "item-teleport", comment = "Sent to the player when a thrown item gets deleted") private String itemTeleport = "&f[ID] &cwas lost in time and space"; - public String getMoveOutsideBorder() { - return moveOutsideBorder; + @Setting(value = "move-outside-border", comment = "Sent to the player when attempting to move outside the world border") + private String moveOutsideBorder = "&cCannot move outside of the world border"; + + public String getChunkSave() { + return chunkSave; } public String getItemTeleport() { return itemTeleport; } + + public String getMoveOutsideBorder() { + return moveOutsideBorder; + } } \ No newline at end of file diff --git a/src/main/java/io/github/lxgaming/sledgehammer/configuration/category/MixinCategory.java b/src/main/java/io/github/lxgaming/sledgehammer/configuration/category/MixinCategory.java index 96230da..820cddf 100644 --- a/src/main/java/io/github/lxgaming/sledgehammer/configuration/category/MixinCategory.java +++ b/src/main/java/io/github/lxgaming/sledgehammer/configuration/category/MixinCategory.java @@ -40,6 +40,18 @@ public class MixinCategory { @Setting(value = "ceremony-rain", comment = "Prevents Totemic from changing the weather") private boolean ceremonyRain = false; + @Setting(value = "chunk-save-alert", comment = "Alerts players with permission (sledgehammer.broadcast.chunksave) when a chunk fails to save") + private boolean chunkSaveAlert = false; + + @Setting(value = "chunk-save-blacklist", comment = "Items to removed from chunks") + private List chunkSaveBlacklist = Lists.newArrayList("minecraft:writable_book", "minecraft:written_book"); + + @Setting(value = "chunk-save-purge-blacklist", comment = "Removes all blacklisted items from chunks that fail to save") + private boolean chunkSavePurgeBlacklist = false; + + @Setting(value = "chunk-save-purge-all", comment = "Removes all Entities and TileEntities from chunks that fail to save") + private boolean chunkSavePurgeAll = false; + @Setting(value = "flush-network-on-tick", comment = "Reduce Network usage by postponing flush") private boolean flushNetworkOnTick = false; @@ -90,6 +102,22 @@ public boolean isCeremonyRain() { return ceremonyRain; } + public boolean isChunkSaveAlert() { + return chunkSaveAlert; + } + + public List getChunkSaveBlacklist() { + return chunkSaveBlacklist; + } + + public boolean isChunkSavePurgeBlacklist() { + return chunkSavePurgeBlacklist; + } + + public boolean isChunkSavePurgeAll() { + return chunkSavePurgeAll; + } + public boolean isFlushNetworkOnTick() { return flushNetworkOnTick; } diff --git a/src/main/java/io/github/lxgaming/sledgehammer/exception/ChunkSaveException.java b/src/main/java/io/github/lxgaming/sledgehammer/exception/ChunkSaveException.java new file mode 100644 index 0000000..9c07300 --- /dev/null +++ b/src/main/java/io/github/lxgaming/sledgehammer/exception/ChunkSaveException.java @@ -0,0 +1,37 @@ +/* + * Copyright 2019 Alex Thomson + * + * Licensed 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. + */ + +package io.github.lxgaming.sledgehammer.exception; + +import java.io.IOException; + +public class ChunkSaveException extends IOException { + + public ChunkSaveException() { + } + + public ChunkSaveException(String message) { + super(message); + } + + public ChunkSaveException(String message, Throwable cause) { + super(message, cause); + } + + public ChunkSaveException(Throwable cause) { + super(cause); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/lxgaming/sledgehammer/mixin/core/world/chunk/storage/MixinAnvilChunkLoader.java b/src/main/java/io/github/lxgaming/sledgehammer/mixin/core/world/chunk/storage/MixinAnvilChunkLoader.java new file mode 100644 index 0000000..927a448 --- /dev/null +++ b/src/main/java/io/github/lxgaming/sledgehammer/mixin/core/world/chunk/storage/MixinAnvilChunkLoader.java @@ -0,0 +1,265 @@ +/* + * Copyright 2019 Alex Thomson + * + * Licensed 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. + */ + +package io.github.lxgaming.sledgehammer.mixin.core.world.chunk.storage; + +import com.google.common.collect.Lists; +import io.github.lxgaming.sledgehammer.Sledgehammer; +import io.github.lxgaming.sledgehammer.configuration.Config; +import io.github.lxgaming.sledgehammer.configuration.category.MessageCategory; +import io.github.lxgaming.sledgehammer.configuration.category.MixinCategory; +import io.github.lxgaming.sledgehammer.exception.ChunkSaveException; +import io.github.lxgaming.sledgehammer.util.Broadcast; +import io.github.lxgaming.sledgehammer.util.Reference; +import io.github.lxgaming.sledgehammer.util.Toolbox; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.world.chunk.storage.AnvilChunkLoader; +import org.apache.commons.lang3.StringUtils; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.function.Predicate; + +@Mixin(value = AnvilChunkLoader.class, priority = 1337) +public abstract class MixinAnvilChunkLoader { + + @Shadow + protected abstract void writeChunkData(ChunkPos pos, NBTTagCompound compound) throws IOException; + + @Redirect(method = "writeNextIO", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/chunk/storage/AnvilChunkLoader;writeChunkData(Lnet/minecraft/util/math/ChunkPos;Lnet/minecraft/nbt/NBTTagCompound;)V" + ) + ) + private void onWriteChunkData(AnvilChunkLoader chunkLoader, ChunkPos pos, NBTTagCompound compound) throws IOException { + if (sledgehammer$writeChunk(pos, compound)) { + return; + } + + // TAG_COMPOUND + if (!compound.hasKey("Level", 10)) { + Sledgehammer.getInstance().getLogger().error("Chunk ({}, {}) is missing its Level tag", pos.x, pos.z); + return; + } + + NBTTagCompound level = compound.getCompoundTag("Level"); + boolean saveRequired; + + // Remove Blacklisted items from Entities and TileEntities + if (Sledgehammer.getInstance().getConfig().map(Config::getMixinCategory).map(MixinCategory::isChunkSavePurgeBlacklist).orElse(false)) { + List blacklist = Sledgehammer.getInstance().getConfig().map(Config::getMixinCategory).map(MixinCategory::getChunkSaveBlacklist).orElseGet(Lists::newArrayList); + saveRequired = sledgehammer$removeEntityItems(level, item -> sledgehammer$checkItem(item, blacklist::contains)); + saveRequired |= sledgehammer$removeTileEntityItems(level, item -> sledgehammer$checkItem(item, blacklist::contains)); + if (saveRequired && sledgehammer$writeChunk(pos, compound)) { + Sledgehammer.getInstance().getLogger().info("Chunk ({}, {}) saved after removing Blacklisted Items from Entities and TileEntities", pos.x, pos.z); + return; + } + } + + // Remove all Entities and TileEntities + if (Sledgehammer.getInstance().getConfig().map(Config::getMixinCategory).map(MixinCategory::isChunkSavePurgeAll).orElse(false)) { + saveRequired = sledgehammer$removeEntities(level, entity -> true); + saveRequired |= sledgehammer$removeTileEntities(level, tileEntity -> true); + if (saveRequired && sledgehammer$writeChunk(pos, compound)) { + Sledgehammer.getInstance().getLogger().info("Chunk ({}, {}) saved after removing all Entities and TileEntities", pos.x, pos.z); + return; + } + } + + // Broadcast + if (Sledgehammer.getInstance().getConfig().map(Config::getMixinCategory).map(MixinCategory::isChunkSaveAlert).orElse(false)) { + Sledgehammer.getInstance().getConfig().map(Config::getMessageCategory).map(MessageCategory::getChunkSave).filter(StringUtils::isNotBlank).ifPresent(message -> { + Broadcast.builder() + .message(Toolbox.convertColor(message.replace("[X]", String.valueOf(pos.x)).replace("[Z]", String.valueOf(pos.z)))) + .permission(Reference.ID + ".broadcast.chunksave") + .type(Broadcast.Type.CHAT) + .build() + .sendMessage(); + }); + } + } + + private boolean sledgehammer$writeChunk(ChunkPos pos, NBTTagCompound compound) throws IOException { + try { + this.writeChunkData(pos, compound); + return true; + } catch (ChunkSaveException ex) { + return false; + } + } + + private boolean sledgehammer$removeEntities(NBTTagCompound level, Predicate predicate) { + AtomicInteger removed = new AtomicInteger(0); + sledgehammer$forEachEntity(level, entity -> { + if (predicate.test(entity)) { + removed.getAndIncrement(); + return true; + } + + return false; + }); + + if (removed.get() > 0) { + Sledgehammer.getInstance().getLogger().info("Removed {} {}", removed.get(), Toolbox.formatUnit(removed.get(), "Entity", "Entities")); + return true; + } + + return false; + } + + private boolean sledgehammer$removeTileEntities(NBTTagCompound level, Predicate predicate) { + AtomicInteger removed = new AtomicInteger(0); + sledgehammer$forEachTileEntity(level, tileEntity -> { + if (predicate.test(tileEntity)) { + removed.getAndIncrement(); + return true; + } + + return false; + }); + + if (removed.get() > 0) { + Sledgehammer.getInstance().getLogger().info("Removed {} {}", removed.get(), Toolbox.formatUnit(removed.get(), "TileEntity", "TileEntities")); + return true; + } + + return false; + } + + private boolean sledgehammer$removeEntityItems(NBTTagCompound level, Predicate predicate) { + AtomicInteger removedEntities = new AtomicInteger(0); + AtomicInteger removedItems = new AtomicInteger(0); + sledgehammer$forEachEntity(level, entity -> { + // TAG_COMPOUND + if (entity.hasKey("Item", 10)) { + NBTTagCompound item = entity.getCompoundTag("Item"); + if (predicate.test(item)) { + removedEntities.getAndIncrement(); + return true; + } + + // TAG_COMPOUND + if (item.hasKey("tag", 10)) { + NBTTagCompound tag = item.getCompoundTag("tag"); + // TAG_COMPOUND + if (tag.hasKey("BlockEntityTag", 10)) { + removedItems.getAndAdd(sledgehammer$removeItems(tag.getCompoundTag("BlockEntityTag"), predicate)); + } + } + } + + return false; + }); + + boolean saveRequired = false; + if (removedItems.get() > 0) { + Sledgehammer.getInstance().getLogger().info("Removed {} {} from Entities", removedItems.get(), Toolbox.formatUnit(removedItems.get(), "Item", "Items")); + saveRequired = true; + } + + if (removedEntities.get() > 0) { + Sledgehammer.getInstance().getLogger().info("Removed {} {}", removedEntities.get(), Toolbox.formatUnit(removedEntities.get(), "Entity", "Entities")); + saveRequired = true; + } + + return saveRequired; + } + + private boolean sledgehammer$removeTileEntityItems(NBTTagCompound level, Predicate predicate) { + AtomicInteger removed = new AtomicInteger(0); + sledgehammer$forEachTileEntity(level, tileEntity -> { + removed.getAndAdd(sledgehammer$removeItems(tileEntity, predicate)); + return false; + }); + + if (removed.get() > 0) { + Sledgehammer.getInstance().getLogger().info("Removed {} {} from TileEntities", removed.get(), Toolbox.formatUnit(removed.get(), "Item", "Items")); + return true; + } + + return false; + } + + private int sledgehammer$removeItems(NBTTagCompound compound, Predicate predicate) { + int removed = 0; + // TAG_LIST + if (compound.hasKey("Items", 9)) { + // TAG_COMPOUND + NBTTagList items = compound.getTagList("Items", 10); + for (int index = 0; index < items.tagCount(); index++) { + NBTTagCompound item = items.getCompoundTagAt(index); + if (predicate.test(item)) { + items.removeTag(index); + index--; + removed++; + continue; + } + + // TAG_COMPOUND + if (item.hasKey("tag", 10)) { + NBTTagCompound tag = item.getCompoundTag("tag"); + // TAG_COMPOUND + if (tag.hasKey("BlockEntityTag", 10)) { + removed += sledgehammer$removeItems(tag.getCompoundTag("BlockEntityTag"), predicate); + } + } + } + } + + return removed; + } + + private boolean sledgehammer$checkItem(NBTTagCompound item, Predicate predicate) { + // TAG_STRING + return item.hasKey("id", 8) && predicate.test(item.getString("id")); + } + + private void sledgehammer$forEachEntity(NBTTagCompound level, Function function) { + // TAG_LIST + if (level.hasKey("Entities", 9)) { + // TAG_COMPOUND + NBTTagList entities = level.getTagList("Entities", 10); + for (int index = 0; index < entities.tagCount(); index++) { + if (function.apply(entities.getCompoundTagAt(index))) { + entities.removeTag(index); + index--; + } + } + } + } + + private void sledgehammer$forEachTileEntity(NBTTagCompound level, Function function) { + // TAG_LIST + if (level.hasKey("TileEntities", 9)) { + // TAG_COMPOUND + NBTTagList tileEntities = level.getTagList("TileEntities", 10); + for (int index = 0; index < tileEntities.tagCount(); index++) { + if (function.apply(tileEntities.getCompoundTagAt(index))) { + tileEntities.removeTag(index); + index--; + } + } + } + } +} \ No newline at end of file diff --git a/src/main/java/io/github/lxgaming/sledgehammer/mixin/core/world/chunk/storage/MixinRegionFileChunkBuffer.java b/src/main/java/io/github/lxgaming/sledgehammer/mixin/core/world/chunk/storage/MixinRegionFileChunkBuffer.java new file mode 100644 index 0000000..a0a03ae --- /dev/null +++ b/src/main/java/io/github/lxgaming/sledgehammer/mixin/core/world/chunk/storage/MixinRegionFileChunkBuffer.java @@ -0,0 +1,48 @@ +/* + * Copyright 2019 Alex Thomson + * + * Licensed 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. + */ + +package io.github.lxgaming.sledgehammer.mixin.core.world.chunk.storage; + +import io.github.lxgaming.sledgehammer.exception.ChunkSaveException; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +@Mixin(targets = "net/minecraft/world/chunk/storage/RegionFile$ChunkBuffer", priority = 1337) +public abstract class MixinRegionFileChunkBuffer extends ByteArrayOutputStream { + + @Shadow + @Final + private int chunkX; + + @Shadow + @Final + private int chunkZ; + + @Inject(method = "close", at = @At(value = "HEAD"), remap = false) + private void onClose(CallbackInfo callbackInfo) throws IOException { + int length = (this.count + 5) / 4096 + 1; + if (length >= 256) { + throw new ChunkSaveException("Chunk (" + this.chunkX + ", " + this.chunkZ + ") too big (" + length + " >= 256)"); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/github/lxgaming/sledgehammer/util/Reference.java b/src/main/java/io/github/lxgaming/sledgehammer/util/Reference.java index 3775b81..98306fa 100644 --- a/src/main/java/io/github/lxgaming/sledgehammer/util/Reference.java +++ b/src/main/java/io/github/lxgaming/sledgehammer/util/Reference.java @@ -20,7 +20,7 @@ public class Reference { public static final String ID = "sledgehammer"; public static final String NAME = "Sledgehammer"; - public static final String VERSION = "1.12.2-1.2.17"; + public static final String VERSION = "1.12.2-1.2.18"; public static final String DESCRIPTION = "Smashes the stupid out of the server."; public static final String AUTHORS = "LX_Gaming"; public static final String SOURCE = "https://github.com/LXGaming/Sledgehammer/"; diff --git a/src/main/resources/mixins.sledgehammer.core.json b/src/main/resources/mixins.sledgehammer.core.json index 0d0abdf..2ea6032 100644 --- a/src/main/resources/mixins.sledgehammer.core.json +++ b/src/main/resources/mixins.sledgehammer.core.json @@ -18,7 +18,9 @@ "network.MixinNetworkManager", "network.MixinNetworkSystem", "server.MixinDedicatedServer", - "world.biome.MixinBiomeProvider" + "world.biome.MixinBiomeProvider", + "world.chunk.storage.MixinAnvilChunkLoader", + "world.chunk.storage.MixinRegionFileChunkBuffer" ], "injectors": { "defaultRequire": 1