├── gradlew ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── settings.gradle ├── src └── main │ ├── java │ └── com │ │ └── terriblefriends │ │ └── booktrolling │ │ ├── ItemSizeResults.java │ │ ├── BookTrolling.java │ │ ├── ToggleButton.java │ │ ├── mixins │ │ ├── ItemEntityMixin.java │ │ ├── GameMenuScreenMixin.java │ │ ├── BookSigningScreenMixin.java │ │ ├── LecternScreenMixin.java │ │ ├── BookEditScreenMixin.java │ │ └── ItemStackMixin.java │ │ ├── Config.java │ │ └── LongCountingDataOutputStream.java │ └── resources │ ├── booktrolling.mixins.json │ └── fabric.mod.json ├── gradle.properties ├── LICENSE ├── .gitignore ├── README-pre-24w09a.md ├── README.md └── gradlew.bat /gradlew: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Captain-S0L0/booktrolling/HEAD/gradlew -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Captain-S0L0/booktrolling/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven { 4 | name = 'Fabric' 5 | url = 'https://maven.fabricmc.net/' 6 | } 7 | gradlePluginPortal() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/terriblefriends/booktrolling/ItemSizeResults.java: -------------------------------------------------------------------------------- 1 | package com.terriblefriends.booktrolling; 2 | 3 | public record ItemSizeResults(boolean error, long diskSize, long diskSizeCompressed, int packetSize, int packetSizeCompressed) { 4 | 5 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/main/java/com/terriblefriends/booktrolling/BookTrolling.java: -------------------------------------------------------------------------------- 1 | package com.terriblefriends.booktrolling; 2 | 3 | import net.fabricmc.api.ClientModInitializer; 4 | 5 | public class BookTrolling implements ClientModInitializer { 6 | 7 | @Override 8 | public void onInitializeClient() { 9 | Config.load(); 10 | 11 | Runtime.getRuntime().addShutdownHook(new Thread(Config::save)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Done to increase the memory available to gradle. 2 | org.gradle.jvmargs=-Xmx1G 3 | # Fabric Properties 4 | # check these on https://fabricmc.net/develop/ 5 | 6 | minecraft_version=1.21.9 7 | yarn_mappings=1.21.9+build.1 8 | loader_version=0.17.2 9 | loom_version=1.11-SNAPSHOT 10 | 11 | # Mod Properties 12 | mod_version=1.6.0-1.21.9 13 | maven_group=com.terriblefriends 14 | archives_base_name=booktrolling 15 | 16 | -------------------------------------------------------------------------------- /src/main/resources/booktrolling.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "minVersion": "0.8", 4 | "package": "com.terriblefriends.booktrolling.mixins", 5 | "compatibilityLevel": "JAVA_17", 6 | "client": [ 7 | "BookEditScreenMixin", 8 | "BookSigningScreenMixin", 9 | "GameMenuScreenMixin", 10 | "ItemEntityMixin", 11 | "ItemStackMixin", 12 | "LecternScreenMixin" 13 | ], 14 | "injectors": { 15 | "defaultRequire": 1 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/resources/fabric.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "id": "booktrolling", 4 | "version": "${version}", 5 | "name": "Booktrolling", 6 | "description": "", 7 | "authors": [], 8 | "contact": {}, 9 | "license": "MIT", 10 | "icon": "assets/booktrolling/icon.png", 11 | "environment": "*", 12 | "entrypoints": { 13 | "client": [ 14 | "com.terriblefriends.booktrolling.BookTrolling" 15 | ] 16 | }, 17 | "mixins": [ 18 | "booktrolling.mixins.json" 19 | ], 20 | "depends": { 21 | "fabricloader": ">=0.14.10", 22 | "minecraft": "1.21.X" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/terriblefriends/booktrolling/ToggleButton.java: -------------------------------------------------------------------------------- 1 | package com.terriblefriends.booktrolling; 2 | 3 | import net.minecraft.client.gui.Click; 4 | import net.minecraft.client.gui.widget.ButtonWidget; 5 | import net.minecraft.text.Text; 6 | 7 | public class ToggleButton extends ButtonWidget { 8 | public ToggleButton(int x, int y, int width, int height, Text message, PressAction onPress, boolean active) { 9 | super(x, y, width, height, message, onPress, ButtonWidget.DEFAULT_NARRATION_SUPPLIER); 10 | this.active = active; 11 | } 12 | 13 | public boolean isInteractable() { 14 | return this.visible; 15 | } 16 | 17 | public void onClick(Click click, boolean doubled) { 18 | this.active = !this.active; 19 | super.onPress(click); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/java/com/terriblefriends/booktrolling/mixins/ItemEntityMixin.java: -------------------------------------------------------------------------------- 1 | package com.terriblefriends.booktrolling.mixins; 2 | 3 | import net.minecraft.entity.Entity; 4 | import net.minecraft.entity.EntityType; 5 | import net.minecraft.entity.ItemEntity; 6 | import net.minecraft.entity.data.DataTracker; 7 | import net.minecraft.entity.data.TrackedData; 8 | import net.minecraft.item.ItemStack; 9 | import net.minecraft.item.Items; 10 | import net.minecraft.world.World; 11 | import org.spongepowered.asm.mixin.Final; 12 | import org.spongepowered.asm.mixin.Mixin; 13 | import org.spongepowered.asm.mixin.Shadow; 14 | import org.spongepowered.asm.mixin.injection.At; 15 | import org.spongepowered.asm.mixin.injection.Inject; 16 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 17 | 18 | @Mixin(ItemEntity.class) 19 | public abstract class ItemEntityMixin extends Entity { 20 | @Shadow @Final private static TrackedData STACK; 21 | 22 | public ItemEntityMixin(EntityType type, World world) { 23 | super(type, world); 24 | } 25 | 26 | @Inject(at=@At("HEAD"),method = "initDataTracker",cancellable = true) 27 | private void bookTrolling$preventLargeInvisibleItems(DataTracker.Builder builder, CallbackInfo ci) { 28 | builder.add(STACK, new ItemStack(Items.BARRIER)); 29 | ci.cancel(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/terriblefriends/booktrolling/mixins/GameMenuScreenMixin.java: -------------------------------------------------------------------------------- 1 | package com.terriblefriends.booktrolling.mixins; 2 | 3 | import com.terriblefriends.booktrolling.Config; 4 | import com.terriblefriends.booktrolling.ToggleButton; 5 | import net.minecraft.client.gui.screen.GameMenuScreen; 6 | import net.minecraft.client.gui.screen.Screen; 7 | import net.minecraft.text.Text; 8 | import org.spongepowered.asm.mixin.Mixin; 9 | import org.spongepowered.asm.mixin.injection.At; 10 | import org.spongepowered.asm.mixin.injection.Inject; 11 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 12 | 13 | @Mixin(GameMenuScreen.class) 14 | public class GameMenuScreenMixin extends Screen { 15 | protected GameMenuScreenMixin(Text title) { 16 | super(title); 17 | } 18 | 19 | @Inject(at=@At("TAIL"),method="Lnet/minecraft/client/gui/screen/GameMenuScreen;initWidgets()V") 20 | private void booktrolling$addGuiButtons(CallbackInfo ci) { 21 | this.addDrawableChild(new ToggleButton(0, this.height-20, 98, 20, Text.literal("Item Size Debug"), (button) -> { 22 | Config.get().itemSizeDebug = !Config.get().itemSizeDebug; 23 | }, Config.get().itemSizeDebug)); 24 | this.addDrawableChild(new ToggleButton(0, this.height-40, 98, 20, Text.literal("Raw Sizes"), (button) -> { 25 | Config.get().itemSizeDebugRawSizes = !Config.get().itemSizeDebugRawSizes; 26 | }, Config.get().itemSizeDebugRawSizes)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/terriblefriends/booktrolling/Config.java: -------------------------------------------------------------------------------- 1 | package com.terriblefriends.booktrolling; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | import com.mojang.logging.LogUtils; 6 | import net.fabricmc.loader.api.FabricLoader; 7 | import org.slf4j.Logger; 8 | 9 | import java.io.*; 10 | 11 | public class Config { 12 | private static final Logger LOGGER = LogUtils.getLogger(); 13 | private static final File file = new File(FabricLoader.getInstance().getConfigDir().toFile(), "booktrolling.json"); 14 | private static Config INSTANCE = new Config(); 15 | 16 | public boolean itemSizeDebugRawSizes = false; 17 | public boolean itemSizeDebug = false; 18 | public boolean autoSign = false; 19 | public boolean autoDrop = false; 20 | public boolean randomizeCharacters = false; 21 | 22 | public static void load() { 23 | Gson gson = new Gson(); 24 | 25 | try (Reader reader = new FileReader(file)) { 26 | INSTANCE = gson.fromJson(reader, Config.class); 27 | } catch (Exception e) { 28 | if (file.exists()) { 29 | LOGGER.error("BookTrolling could not load the config! Using defaults.", e); 30 | } else { 31 | LOGGER.info("BookTrolling could not find a config, so we're creating a new one."); 32 | } 33 | } 34 | } 35 | 36 | public static void save() { 37 | Gson gson = new GsonBuilder().setPrettyPrinting().create(); 38 | 39 | try (FileWriter writer = new FileWriter(file)) { 40 | 41 | gson.toJson(INSTANCE, writer); 42 | } catch (IOException e) { 43 | LOGGER.error("BookTrolling failed to save the config!", e); 44 | } 45 | } 46 | 47 | public static Config get() { 48 | return INSTANCE; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/terriblefriends/booktrolling/mixins/BookSigningScreenMixin.java: -------------------------------------------------------------------------------- 1 | package com.terriblefriends.booktrolling.mixins; 2 | 3 | import net.minecraft.client.gui.screen.Screen; 4 | import net.minecraft.client.gui.screen.ingame.BookSigningScreen; 5 | import net.minecraft.client.gui.widget.TextFieldWidget; 6 | import net.minecraft.entity.player.PlayerEntity; 7 | import net.minecraft.network.packet.c2s.play.BookUpdateC2SPacket; 8 | import net.minecraft.text.Text; 9 | import net.minecraft.util.Hand; 10 | import org.spongepowered.asm.mixin.Final; 11 | import org.spongepowered.asm.mixin.Mixin; 12 | import org.spongepowered.asm.mixin.Shadow; 13 | import org.spongepowered.asm.mixin.injection.At; 14 | import org.spongepowered.asm.mixin.injection.Inject; 15 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 16 | 17 | import java.util.List; 18 | import java.util.Optional; 19 | 20 | @Mixin(BookSigningScreen.class) 21 | public abstract class BookSigningScreenMixin extends Screen { 22 | 23 | @Shadow private TextFieldWidget bookTitleTextField; 24 | @Shadow @Final private PlayerEntity player; 25 | @Shadow @Final private List pages; 26 | @Shadow @Final private Hand hand; 27 | 28 | protected BookSigningScreenMixin(Text title) { 29 | super(title); 30 | } 31 | 32 | @Inject(method = "init", at = @At("TAIL")) 33 | private void booktrolling$initBookSigningScreen(CallbackInfo ci) { 34 | this.bookTitleTextField.setMaxLength(32); // set max title length to the max the component allows 35 | } 36 | 37 | @Override 38 | public void close() { 39 | // update contents but don't sign if screen was closed 40 | int i = this.hand == Hand.MAIN_HAND ? this.player.getInventory().getSelectedSlot() : 40; 41 | this.client.getNetworkHandler().sendPacket(new BookUpdateC2SPacket(i, this.pages, Optional.empty())); 42 | super.close(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/terriblefriends/booktrolling/mixins/LecternScreenMixin.java: -------------------------------------------------------------------------------- 1 | package com.terriblefriends.booktrolling.mixins; 2 | 3 | import net.minecraft.client.gui.screen.Screen; 4 | import net.minecraft.client.gui.screen.ingame.LecternScreen; 5 | import net.minecraft.client.gui.widget.ButtonWidget; 6 | import net.minecraft.screen.slot.SlotActionType; 7 | import net.minecraft.text.Text; 8 | import net.minecraft.util.Formatting; 9 | import org.spongepowered.asm.mixin.Mixin; 10 | import org.spongepowered.asm.mixin.injection.At; 11 | import org.spongepowered.asm.mixin.injection.Inject; 12 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 13 | 14 | @Mixin(LecternScreen.class) 15 | public abstract class LecternScreenMixin extends Screen { 16 | protected LecternScreenMixin(Text title) { 17 | super(title); 18 | } 19 | 20 | @Inject(method="init", at=@At("HEAD")) 21 | private void booktrolling$addGuiButtons(CallbackInfo ci) { 22 | this.addDrawableChild(ButtonWidget.builder(Text.literal("Shadow Book"), (button) -> { 23 | if (this.client == null || this.client.player == null || this.client.interactionManager == null) { 24 | return; 25 | } 26 | 27 | int slotToFill = this.client.player.getInventory().getEmptySlot(); 28 | if (slotToFill != -1) { 29 | this.client.interactionManager.clickSlot(this.client.player.currentScreenHandler.syncId, 0, slotToFill, SlotActionType.SWAP, this.client.player); 30 | this.client.inGameHud.getChatHud().addMessage(Text.literal(" Shadowed book...").formatted(Formatting.RED)); 31 | } 32 | else { 33 | this.client.inGameHud.getChatHud().addMessage(Text.literal(" Error! No room in inventory!").formatted(Formatting.RED)); 34 | } 35 | }).dimensions(this.width - 98, this.height - 40, 98, 20).build()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/ 3 | 4 | *.iml 5 | *.ipr 6 | *.iws 7 | 8 | # IntelliJ 9 | out/ 10 | # mpeltonen/sbt-idea plugin 11 | .idea_modules/ 12 | 13 | # JIRA plugin 14 | atlassian-ide-plugin.xml 15 | 16 | # Compiled class file 17 | *.class 18 | 19 | # Log file 20 | *.log 21 | 22 | # BlueJ files 23 | *.ctxt 24 | 25 | # Package Files # 26 | *.jar 27 | *.war 28 | *.nar 29 | *.ear 30 | *.zip 31 | *.tar.gz 32 | *.rar 33 | 34 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 35 | hs_err_pid* 36 | 37 | *~ 38 | 39 | # temporary files which can be created if a process still has a handle open of a deleted file 40 | .fuse_hidden* 41 | 42 | # KDE directory preferences 43 | .directory 44 | 45 | # Linux trash folder which might appear on any partition or disk 46 | .Trash-* 47 | 48 | # .nfs files are created when an open file is removed but is still being accessed 49 | .nfs* 50 | 51 | # General 52 | .DS_Store 53 | .AppleDouble 54 | .LSOverride 55 | 56 | # Icon must end with two \r 57 | Icon 58 | 59 | # Thumbnails 60 | ._* 61 | 62 | # Files that might appear in the root of a volume 63 | .DocumentRevisions-V100 64 | .fseventsd 65 | .Spotlight-V100 66 | .TemporaryItems 67 | .Trashes 68 | .VolumeIcon.icns 69 | .com.apple.timemachine.donotpresent 70 | 71 | # Directories potentially created on remote AFP share 72 | .AppleDB 73 | .AppleDesktop 74 | Network Trash Folder 75 | Temporary Items 76 | .apdisk 77 | 78 | # Windows thumbnail cache files 79 | Thumbs.db 80 | Thumbs.db:encryptable 81 | ehthumbs.db 82 | ehthumbs_vista.db 83 | 84 | # Dump file 85 | *.stackdump 86 | 87 | # Folder config file 88 | [Dd]esktop.ini 89 | 90 | # Recycle Bin used on file shares 91 | $RECYCLE.BIN/ 92 | 93 | # Windows Installer files 94 | *.cab 95 | *.msi 96 | *.msix 97 | *.msm 98 | *.msp 99 | 100 | # Windows shortcuts 101 | *.lnk 102 | 103 | .gradle 104 | build/ 105 | 106 | # Ignore Gradle GUI config 107 | gradle-app.setting 108 | 109 | # Cache of project 110 | .gradletasknamecache 111 | 112 | **/build/ 113 | 114 | # Common working directory 115 | run/ 116 | 117 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 118 | !gradle-wrapper.jar 119 | -------------------------------------------------------------------------------- /README-pre-24w09a.md: -------------------------------------------------------------------------------- 1 | # booktrolling 2 | 3 | A collection of tools for ~~exploiting~~ working with written books in Minecraft. 4 | 5 | This is a FABRIC mod. 6 | 7 | 8 | **Changes provided:** 9 | - Allows up to 8,192 characters / page in multiplayer (limit of BookUpdateC2SPacket.write()), or 32,767 in singleplayer (limit of WritableBookItem.isValid()) 10 | - Allows titles up to 128 characters in length in multiplayer (limit of BookUpdateC2SPacket.write()), or 65,535 in singleplayer (limit of NbtString.write()) 11 | - Allows written books with titles > 31 characters in length to be parsed (in vanilla, books with titles longer than 31 characters are treated as "invalid" and will not display contents on book screen) 12 | - Prevents client-side kicks as a result of NbtTagSizeTracker tracking more than 2,097,152 bytes 13 | - Toggleable item size debug information in hover tooltip (toggleable on in-game pause menu, default false) 14 | 15 | 16 | **Book Presets:** 17 | - Vanilla: 100 pages each with 1,023 3 byte characters (this is achieveable in vanilla by utilizing a resource pack to edit the width of characters, then limited by a hardcoded < 1024 character check in the book edit GUI) 18 | - Singleplayer: 100 pages each with 21,837 3 byte characters (singleplayer only, limit of NbtString.write()) 19 | - Multiplayer: 100 pages each with 8,192 3 byte characters (limit of BookUpdateC2SPacket.write()) 20 | - Paper: 100 pages each with 320 3 byte characters (respects limits of PaperMC servers and its forks) 21 | - Clear: removes all contents of a book 22 | - AutoSign: automatically sign book when using presets (toggleable, default false) 23 | - RandomizeChars: generate random characters, or use a single one (toggleable, default true) 24 | 25 | 26 | **Item Size Debug**: 27 | 28 | The item size debug tooltip can help provide approximates for relavent size information. It is not expected to be exact. 29 | - BYTES: bytes written by PacketByteBuf.writeItemStack. A decent approximation of what is utilized in RAM. Important to consider as if > 8388608, creates server-side kicks 30 | - NBT: bytes counted by NbtTagSizeTracker during NbtIo.read. Important to consider as if > 2,097,152, creates client-side kicks 31 | - COMPRESS: bytes written by compressing PacketByteBuf.writeItemStack with java.util.zip.Deflater. A decent approximation of what is written to disk. Important to consider if resulting item packets are incompressible (PacketByteBuf.getVarIntLength(buf.readableBytes()) > 3), creates server-side kicks 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # booktrolling 2 | 3 | A collection of tools for ~~exploiting~~ working with written books in Minecraft. 4 | 5 | This is a FABRIC mod. 6 | 7 | 8 | **Changes provided:** 9 | - Allows up to 1,024 characters / page regardless of character width (limit of WritableBookContentComponent) 10 | - Allows titles up to 32 characters in length (limit of WrittenBookContentComponent) 11 | - Toggleable item size debug information in hover tooltip (toggleable on in-game pause menu, default false) 12 | 13 | **Book Presets:** 14 | - Vanilla: 100 pages each with 1,023 3 byte characters (this is achievable in vanilla by utilizing a resource pack to edit the 15 | width of characters, then limited by a hardcoded < 1024 character check in the book edit GUI) 16 | - Max: 100 pages each with 1,024 3 byte characters (limit of WritableBookContentComponent) 17 | - Paper: 100 pages each with 320 3 byte characters (respects default limits of PaperMC servers and its forks) 18 | - Clear: removes all contents of a book 19 | - Auto Sign: automatically sign book when using presets (toggleable, default false) 20 | - Randomize Chars: generate random characters, or use a single one (toggleable, default false) 21 | - Drop: automatically drop book when using presets (toggleable, default false) 22 | 23 | 24 | **Item Size Debug**: 25 | 26 | The item size debug tooltip can help provide approximates for relevant size information. It is not expected to be exact. 27 | 28 | Two statistics are provided: disk size and packet size, each with a raw and a compressed value. 29 | 30 | Raw disk size is a decent approximation as to what is utilized in RAM. Useful for OutOfMemory suppression or similar. 31 | 32 | If the compressed disk size of a chunk is more than the 32-bit integer limit of bytes (~2.147 GB), then the chunk will never 33 | be able to save as the process to save a chunk to disk includes creating a byte array with the compressed data. Arrays cannot 34 | exceed the 32-bit integer limit of elements in practically all JVM implementations (plus or minus a few for header stuff). 35 | 36 | Raw packet size cannot exceed 8,388,608 bytes, or the server will kick any player who would receive such a packet. 37 | 38 | Compressed packet size cannot exceed 2,097,152 bytes, or the server will kick any player who would receive such a packet. 39 | 40 | **Old Versions**: 41 | This readme contains relevant information for Minecraft versions 1.20.5 (24w09a) and newer. For 1.20.4 (24w07a) and lower, please see 42 | the old readme at [README-pre-24w09a.md](https://github.com/Captain-S0L0/booktrolling/blob/master/README-pre-24w09a.md). -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /src/main/java/com/terriblefriends/booktrolling/LongCountingDataOutputStream.java: -------------------------------------------------------------------------------- 1 | package com.terriblefriends.booktrolling; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.io.*; 6 | 7 | // counting logic from java.io.DataOutputStream 8 | public class LongCountingDataOutputStream extends OutputStream implements DataOutput { 9 | private long written; 10 | private final DataOutputStream out; 11 | 12 | public LongCountingDataOutputStream(OutputStream out) { 13 | this.out = new DataOutputStream(out); 14 | } 15 | 16 | private void incCount(long value) { 17 | long temp = written + value; 18 | if (temp < 0) { 19 | temp = Long.MAX_VALUE; 20 | } 21 | written = temp; 22 | } 23 | 24 | public synchronized void write(int b) throws IOException { 25 | out.write(b); 26 | incCount(1); 27 | } 28 | 29 | @Override 30 | public void write(byte @NotNull [] b) throws IOException { 31 | out.write(b); 32 | incCount(b.length); 33 | } 34 | 35 | public synchronized void write(byte @NotNull [] b, int off, int len) 36 | throws IOException 37 | { 38 | out.write(b, off, len); 39 | incCount(len); 40 | } 41 | 42 | public void flush() throws IOException { 43 | out.flush(); 44 | } 45 | 46 | public final void writeBoolean(boolean v) throws IOException { 47 | out.writeBoolean(v); 48 | incCount(1); 49 | } 50 | 51 | public final void writeByte(int v) throws IOException { 52 | out.writeByte(v); 53 | incCount(1); 54 | } 55 | 56 | public final void writeShort(int v) throws IOException { 57 | out.writeShort(v); 58 | incCount(2); 59 | } 60 | 61 | public final void writeChar(int v) throws IOException { 62 | out.writeChar(v); 63 | incCount(2); 64 | } 65 | 66 | public final void writeInt(int v) throws IOException { 67 | out.writeInt(v); 68 | incCount(4); 69 | } 70 | 71 | public final void writeLong(long v) throws IOException { 72 | out.writeLong(v); 73 | incCount(8); 74 | } 75 | 76 | public final void writeFloat(float v) throws IOException { 77 | out.writeFloat(v); 78 | incCount(4); 79 | } 80 | 81 | public final void writeDouble(double v) throws IOException { 82 | out.writeDouble(v); 83 | incCount(8); 84 | } 85 | 86 | public final void writeBytes(@NotNull String s) throws IOException { 87 | out.writeBytes(s); 88 | incCount(s.length()); 89 | } 90 | 91 | public final void writeChars(@NotNull String s) throws IOException { 92 | out.writeChars(s); 93 | incCount(s.length() * 2L); 94 | } 95 | 96 | public final void writeUTF(@NotNull String str) throws IOException { 97 | out.writeUTF(str); 98 | 99 | final int strlen = str.length(); 100 | int utflen = strlen; // optimized for ASCII 101 | 102 | for (int i = 0; i < strlen; i++) { 103 | int c = str.charAt(i); 104 | if (c >= 0x80 || c == 0) 105 | utflen += (c >= 0x800) ? 2 : 1; 106 | } 107 | incCount(utflen + 2); 108 | } 109 | 110 | public final long size() { 111 | return written; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/com/terriblefriends/booktrolling/mixins/BookEditScreenMixin.java: -------------------------------------------------------------------------------- 1 | package com.terriblefriends.booktrolling.mixins; 2 | 3 | import com.mojang.logging.LogUtils; 4 | import com.terriblefriends.booktrolling.Config; 5 | import com.terriblefriends.booktrolling.ToggleButton; 6 | import net.minecraft.client.gui.screen.Screen; 7 | import net.minecraft.client.gui.screen.ingame.BookEditScreen; 8 | import net.minecraft.client.gui.widget.ButtonWidget; 9 | import net.minecraft.client.gui.widget.EditBoxWidget; 10 | import net.minecraft.entity.player.PlayerEntity; 11 | import net.minecraft.network.packet.c2s.play.BookUpdateC2SPacket; 12 | import net.minecraft.text.Text; 13 | import net.minecraft.util.Formatting; 14 | import net.minecraft.util.Hand; 15 | import org.slf4j.Logger; 16 | import org.spongepowered.asm.mixin.*; 17 | import org.spongepowered.asm.mixin.injection.At; 18 | import org.spongepowered.asm.mixin.injection.Inject; 19 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 20 | 21 | import java.util.List; 22 | import java.util.Optional; 23 | import java.util.Random; 24 | import java.util.concurrent.Callable; 25 | 26 | @Mixin(BookEditScreen.class) 27 | public abstract class BookEditScreenMixin extends Screen { 28 | // static finals, magic numbers, constants, etc 29 | @Unique 30 | private static final Logger LOGGER = LogUtils.getLogger(); 31 | @Unique 32 | private static final Random RANDOM = new Random(); 33 | @Unique 34 | private static final Callable RANDOM_CHAR_PROVIDER = () -> (char)RANDOM.nextInt(2048, 65536); 35 | @Unique 36 | private static final Callable STATIC_CHAR_PROVIDER = () -> (char)2048; 37 | @Unique 38 | private static final String BRANDING = "BookTrolling™ by Captain_S0L0"; 39 | 40 | // accessors 41 | @Shadow @Final private PlayerEntity player; 42 | @Shadow @Final private Hand hand; 43 | @Shadow @Final private List pages; 44 | @Shadow private int currentPage; 45 | @Shadow private EditBoxWidget editBox; 46 | 47 | @Shadow protected abstract void finalizeBook(); 48 | @Shadow protected abstract void updatePage(); 49 | 50 | protected BookEditScreenMixin(Text title) { 51 | super(title); 52 | } 53 | 54 | @Override 55 | public void close() { 56 | // update contents but don't sign if screen was closed 57 | this.finalizeBook(); 58 | super.close(); 59 | } 60 | 61 | @Inject(method="init", at=@At("TAIL")) 62 | private void booktrolling$initBookEditScreen(CallbackInfo ci) { 63 | this.editBox.setMaxLines(Integer.MAX_VALUE); // disable checking character width 64 | 65 | int y = 0; 66 | this.addDrawableChild(ButtonWidget.builder(Text.literal("1023"), (button) -> { 67 | this.createBook(100, Config.get().autoSign, Config.get().autoDrop, () -> { 68 | StringBuilder builder = new StringBuilder(); 69 | Callable charProvider = getCharProvider(); 70 | for (int i = 0; i < 1023; i++) { 71 | builder.append(charProvider.call()); 72 | } 73 | return builder.toString(); 74 | }, () -> BRANDING); 75 | }).dimensions(0, y, 98, 20).build()); 76 | y+=20; 77 | this.addDrawableChild(ButtonWidget.builder(Text.literal("Max"), (button) -> { 78 | this.createBook(100, Config.get().autoSign, Config.get().autoDrop, () -> { 79 | StringBuilder builder = new StringBuilder(); 80 | Callable charProvider = getCharProvider(); 81 | for (int i = 0; i < 1024; i++) { 82 | builder.append(charProvider.call()); 83 | } 84 | return builder.toString(); 85 | }, () -> BRANDING); 86 | }).dimensions(0, y, 98, 20).build()); 87 | y+=20; 88 | /*this.addDrawableChild(ButtonWidget.builder(Text.literal("max signed"), (button) -> { 89 | this.sign(100, true, drop, () -> { 90 | StringBuilder builder = new StringBuilder(); 91 | Callable charProvider = getCharProvider(); 92 | for (int i = 0; i < 8192; i++) { 93 | builder.append(charProvider.call()); 94 | } 95 | return builder.toString(); 96 | }, () -> BRANDING); 97 | }).dimensions(0, y, 98, 20).build()); 98 | y+=20;*/ 99 | this.addDrawableChild(ButtonWidget.builder(Text.literal("Paper"), (button) -> { 100 | this.createBook(100, Config.get().autoSign, Config.get().autoDrop, () -> { 101 | StringBuilder builder = new StringBuilder(); 102 | Callable charProvider = getCharProvider(); 103 | for (int i = 0; i < 320; i++) { 104 | builder.append(charProvider.call()); 105 | } 106 | return builder.toString(); 107 | }, () -> BRANDING); 108 | }).dimensions(0, y, 98, 20).build()); 109 | y+=20; 110 | /*this.addDrawableChild(ButtonWidget.builder(Text.literal("unsaveable"), (button) -> { 111 | this.sign(1, true, drop, () -> "", () -> "123456789012345678901234567890123"); 112 | }).dimensions(0, y, 98, 20).build()); 113 | y+=20;*/ 114 | this.addDrawableChild(ButtonWidget.builder(Text.literal("Clear"), (button) -> { 115 | this.createBook(1, false, Config.get().autoDrop, () -> "", () -> BRANDING); 116 | }).dimensions(0, y, 98, 20).build()); 117 | y+=20; 118 | 119 | this.addDrawableChild(new ToggleButton(0, this.height-20, 98, 20, Text.literal("Auto Sign"), (button) -> { 120 | Config.get().autoSign = !Config.get().autoSign; 121 | }, Config.get().autoSign)); 122 | this.addDrawableChild(new ToggleButton(0, this.height-40, 98, 20, Text.literal("Randomize Chars"), (button) -> { 123 | Config.get().randomizeCharacters = !Config.get().randomizeCharacters; 124 | }, Config.get().randomizeCharacters)); 125 | this.addDrawableChild(new ToggleButton(0, this.height-60, 98, 20, Text.literal("Auto Drop"), (button) -> { 126 | Config.get().autoDrop = !Config.get().autoDrop; 127 | }, Config.get().autoDrop)); 128 | } 129 | 130 | @Unique 131 | private void createBook(int pageCount, boolean signing, boolean drop, Callable pageGenerator, Callable titleGenerator) { 132 | try { 133 | this.pages.clear(); 134 | 135 | for (int i = 0; i < pageCount; i++) { 136 | this.pages.add(pageGenerator.call()); 137 | } 138 | 139 | this.currentPage = 0; 140 | this.updatePage(); 141 | 142 | Optional title = Optional.empty(); 143 | 144 | if (signing) { 145 | title = Optional.of(titleGenerator.call()); 146 | } 147 | 148 | if (drop || signing) { 149 | int i = this.hand == Hand.MAIN_HAND ? this.player.getInventory().getSelectedSlot() : 40; 150 | this.client.getNetworkHandler().sendPacket(new BookUpdateC2SPacket(i, this.pages, title)); 151 | this.client.setScreen(null); 152 | } 153 | 154 | if (drop) { 155 | this.client.player.dropSelectedItem(true); 156 | } 157 | } 158 | catch (Exception e) { 159 | LOGGER.error("BookTrolling failed to generate book!", e); 160 | this.client.inGameHud.getChatHud().addMessage(Text.literal(" Error generating book! See logs!").formatted(Formatting.DARK_RED)); 161 | } 162 | } 163 | 164 | @Unique 165 | private static Callable getCharProvider() { 166 | return Config.get().randomizeCharacters ? RANDOM_CHAR_PROVIDER : STATIC_CHAR_PROVIDER; 167 | } 168 | } -------------------------------------------------------------------------------- /src/main/java/com/terriblefriends/booktrolling/mixins/ItemStackMixin.java: -------------------------------------------------------------------------------- 1 | package com.terriblefriends.booktrolling.mixins; 2 | 3 | import com.mojang.logging.LogUtils; 4 | import com.terriblefriends.booktrolling.Config; 5 | import com.terriblefriends.booktrolling.ItemSizeResults; 6 | import com.terriblefriends.booktrolling.LongCountingDataOutputStream; 7 | import io.netty.buffer.Unpooled; 8 | import net.minecraft.client.MinecraftClient; 9 | import net.minecraft.component.type.TooltipDisplayComponent; 10 | import net.minecraft.entity.player.PlayerEntity; 11 | import net.minecraft.item.Item; 12 | import net.minecraft.item.ItemStack; 13 | import net.minecraft.item.tooltip.TooltipType; 14 | import net.minecraft.nbt.NbtCompound; 15 | import net.minecraft.nbt.NbtOps; 16 | import net.minecraft.network.PacketByteBuf; 17 | import net.minecraft.network.RegistryByteBuf; 18 | import net.minecraft.network.encoding.VarInts; 19 | import net.minecraft.text.MutableText; 20 | import net.minecraft.text.Text; 21 | import net.minecraft.util.Formatting; 22 | import org.slf4j.Logger; 23 | import org.spongepowered.asm.mixin.Mixin; 24 | import org.spongepowered.asm.mixin.Unique; 25 | import org.spongepowered.asm.mixin.injection.At; 26 | import org.spongepowered.asm.mixin.injection.Inject; 27 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 28 | 29 | import java.io.OutputStream; 30 | import java.util.concurrent.ExecutionException; 31 | import java.util.concurrent.ExecutorService; 32 | import java.util.concurrent.Executors; 33 | import java.util.concurrent.Future; 34 | import java.util.function.Consumer; 35 | import java.util.zip.Deflater; 36 | import java.util.zip.GZIPOutputStream; 37 | 38 | @Mixin(ItemStack.class) 39 | public class ItemStackMixin { 40 | // magic numbers and constants 41 | @Unique 42 | private static final Text WARNING_TEXT = Text.literal(" (WARNING)").formatted(Formatting.GOLD); 43 | @Unique 44 | private static final Text OVERSIZED_TEXT = Text.literal(" (OVERSIZED)").formatted(Formatting.DARK_RED); 45 | @Unique 46 | private static final Text GENERIC_ERROR_TEXT = Text.literal("ERROR CALCULATING ITEM SIZE! SEE LOGS!").formatted(Formatting.DARK_RED); 47 | 48 | // static finals 49 | @Unique 50 | private static final ExecutorService threadPool = Executors.newFixedThreadPool(1); 51 | @Unique 52 | private static final Logger LOGGER = LogUtils.getLogger(); 53 | 54 | // statics 55 | @Unique 56 | private static ItemStack lastCalculatedStack = null; 57 | @Unique 58 | private static ItemSizeResults lastResults = null; 59 | @Unique 60 | private static Future currentTask = null; 61 | 62 | // this accessor 63 | @Unique 64 | private final ItemStack instance = (ItemStack) (Object) this; 65 | 66 | @Inject(method = "appendTooltip", at = @At("TAIL")) 67 | private void booktrolling$handleItemSizeDebug(Item.TooltipContext context, TooltipDisplayComponent displayComponent, PlayerEntity player, TooltipType type, Consumer textConsumer, CallbackInfo ci) { 68 | if (!Config.get().itemSizeDebug || this.instance.isEmpty()) { 69 | return; 70 | } 71 | 72 | boolean taskStackEqual = lastCalculatedStack != null && (this.instance == lastCalculatedStack || ItemStack.areItemsAndComponentsEqual(this.instance, lastCalculatedStack)); 73 | 74 | if (taskStackEqual) { 75 | // check if current task is done 76 | if (currentTask != null && currentTask.isDone()) { 77 | try { 78 | lastResults = currentTask.get(); 79 | } 80 | catch (InterruptedException | ExecutionException e) { 81 | LOGGER.error("Failed to get result from future when it should have been done! Reason:", e); 82 | textConsumer.accept(GENERIC_ERROR_TEXT); 83 | return; 84 | } 85 | finally { 86 | currentTask = null; 87 | } 88 | } 89 | 90 | // if lastResults is null then the thread is still calculating, so hold on 91 | if (lastResults == null) { 92 | textConsumer.accept(Text.literal("Calculating...").formatted(Formatting.RED)); 93 | return; 94 | } 95 | 96 | appendData(textConsumer, lastResults); 97 | return; 98 | } 99 | 100 | // if the requested stack is different then terminate the current task and start a new one 101 | lastResults = null; 102 | lastCalculatedStack = this.instance; 103 | if (currentTask != null) { 104 | currentTask.cancel(true); 105 | } 106 | currentTask = threadPool.submit(() -> { 107 | MinecraftClient minecraftClient = MinecraftClient.getInstance(); 108 | if (minecraftClient == null || minecraftClient.world == null) { 109 | LOGGER.error("Failed to calculate item size! Reason: MinecraftClient or MinecraftClient.world was null!"); 110 | return new ItemSizeResults(true, -1, -1, -1, -1); 111 | } 112 | 113 | long rawDiskBytes = -1; 114 | long compressedDiskBytes = -1; 115 | int rawPacketBytes = -1; 116 | int compressedPacketBytes = -1; 117 | 118 | try { 119 | // calculate disk sizes 120 | NbtCompound itemTag = (NbtCompound)ItemStack.CODEC.encode(this.instance, minecraftClient.world.getRegistryManager().getOps(NbtOps.INSTANCE), new NbtCompound()).getOrThrow(); 121 | 122 | LongCountingDataOutputStream compressedDataOutputStream = new LongCountingDataOutputStream(OutputStream.nullOutputStream()); 123 | GZIPOutputStream gzipOutputStream = new GZIPOutputStream(compressedDataOutputStream); 124 | LongCountingDataOutputStream rawDataOutputStream = new LongCountingDataOutputStream(gzipOutputStream); 125 | itemTag.write(rawDataOutputStream); 126 | gzipOutputStream.finish(); 127 | // allow garbage collection of item tag 128 | itemTag = null; 129 | 130 | rawDiskBytes = rawDataOutputStream.size(); 131 | compressedDiskBytes = compressedDataOutputStream.size(); 132 | 133 | // calculate packet sizes 134 | RegistryByteBuf packetBuf = new RegistryByteBuf(Unpooled.buffer(), minecraftClient.world.getRegistryManager()); 135 | 136 | ItemStack.PACKET_CODEC.encode(packetBuf, this.instance); 137 | rawPacketBytes = packetBuf.readableBytes(); 138 | 139 | // 256 bytes is the default compression threshold 140 | if (rawPacketBytes < 256) { 141 | compressedPacketBytes = rawPacketBytes + 1; 142 | } else { 143 | PacketByteBuf compressionBuf = new PacketByteBuf(Unpooled.buffer()); 144 | byte[] deflateBuffer = new byte[8192]; 145 | Deflater deflater = new Deflater(); 146 | 147 | byte[] bs = new byte[rawPacketBytes]; 148 | packetBuf.readBytes(bs); 149 | VarInts.write(compressionBuf, bs.length); 150 | 151 | deflater.setInput(bs, 0, bs.length); 152 | deflater.finish(); 153 | 154 | while(!deflater.finished()) { 155 | int j = deflater.deflate(deflateBuffer); 156 | compressionBuf.writeBytes(deflateBuffer, 0, j); 157 | } 158 | 159 | compressedPacketBytes = compressionBuf.readableBytes(); 160 | } 161 | 162 | return new ItemSizeResults(false, rawDiskBytes, compressedDiskBytes, rawPacketBytes, compressedPacketBytes); 163 | } catch (Throwable t) { 164 | LOGGER.error("Failed to calculate item size! Reason:", t); 165 | return new ItemSizeResults(true, -1, -1, -1, -1); 166 | } 167 | }); 168 | } 169 | 170 | @Unique 171 | private static void optionalAppendLabel(MutableText text, long value, long max, long warningThreshold) { 172 | if (value >= max) { 173 | text.append(OVERSIZED_TEXT); 174 | } 175 | else if (value >= max - warningThreshold) { 176 | text.append(WARNING_TEXT); 177 | } 178 | } 179 | 180 | @Unique 181 | private static void appendData(Consumer textConsumer, ItemSizeResults results) { 182 | if (results.error()) { 183 | textConsumer.accept(GENERIC_ERROR_TEXT); 184 | return; 185 | } 186 | 187 | MutableText line; 188 | 189 | textConsumer.accept(Text.literal("DISK SIZES").formatted(Formatting.RED, Formatting.UNDERLINE)); 190 | textConsumer.accept(Text.literal("Raw: " + toReadableNumber(results.diskSize())).formatted(Formatting.RED)); 191 | 192 | line = Text.literal("Compressed: " + toReadableNumber(results.diskSizeCompressed())).formatted(Formatting.RED); 193 | // chunks cannot save if they contain more than the array limit of bytes after compression as in the 194 | // chunk saving process, the bytes are sent to a byte array before being flushed to disk 195 | // arrays are limited to approximately Integer.MAX_VALUE - 8 elements, JVM dependent. 196 | 197 | // a warning of 10 MB should be fine 198 | optionalAppendLabel(line, results.diskSizeCompressed(), Integer.MAX_VALUE - 8, 10485760); 199 | textConsumer.accept(line); 200 | 201 | textConsumer.accept(Text.literal("PACKET SIZES").formatted(Formatting.RED, Formatting.UNDERLINE)); 202 | 203 | line = Text.literal("Raw: " + toReadableNumber(results.packetSize())).formatted(Formatting.RED); 204 | // packets cannot have more than 8388608 bytes of raw data (net.minecraft.network.handler.PacketDeflater) 205 | 206 | // a warning of 128 KB should be fine 207 | optionalAppendLabel(line, results.packetSize(), 8388608, 128000); 208 | textConsumer.accept(line); 209 | 210 | line = Text.literal("Compressed: " + toReadableNumber(results.packetSizeCompressed())).formatted(Formatting.RED); 211 | // packets cannot have more than 2097152 bytes of compressed data (net.minecraft.network.handler.SizePrepender) 212 | 213 | // a warning of 128 KB should be fine 214 | optionalAppendLabel(line, results.packetSizeCompressed(), 2097152, 128000); 215 | textConsumer.accept(line); 216 | } 217 | 218 | @Unique 219 | private static String toReadableNumber(long value) { 220 | if (Config.get().itemSizeDebugRawSizes) { 221 | return value + " bytes"; 222 | } 223 | 224 | if (value >= 1073741824) { 225 | return String.format("%.2f GB", value / 1073741824D); 226 | } 227 | if (value >= 1048576) { 228 | return String.format("%.2f MB", value / 1048576D); 229 | } 230 | if (value >= 1024) { 231 | return String.format("%.2f KB", value / 1024D); 232 | } 233 | return value + " bytes"; 234 | } 235 | } 236 | --------------------------------------------------------------------------------