package com.matt.forgehax.mods; import static com.matt.forgehax.Helper.getFileManager; import static com.matt.forgehax.Helper.getLocalPlayer; import static com.matt.forgehax.Helper.printWarning; import com.google.common.collect.Lists; import com.matt.forgehax.util.SafeConverter; import com.matt.forgehax.util.command.Setting; import com.matt.forgehax.util.console.ConsoleIO; import com.matt.forgehax.util.mod.Category; import com.matt.forgehax.util.mod.ToggleMod; import com.matt.forgehax.util.mod.loader.RegisterMod; import io.netty.buffer.Unpooled; import java.io.BufferedWriter; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.Collection; import java.util.Scanner; import java.util.function.Consumer; import net.minecraft.client.gui.GuiScreenBook; import net.minecraft.entity.player.InventoryPlayer; import net.minecraft.item.ItemStack; import net.minecraft.item.ItemWritableBook; import net.minecraft.nbt.NBTTagList; import net.minecraft.nbt.NBTTagString; import net.minecraft.network.PacketBuffer; import net.minecraft.network.play.client.CPacketCustomPayload; import net.minecraft.util.EnumHand; /** * Created on 12/17/2017 by fr1kin */ @RegisterMod public class BookBot extends ToggleMod { private static final int MAX_CHARACTERS_PER_PAGE = 256; private static final int MAX_PAGES = 50; public static final String NUMBER_TOKEN = "\\{NUMBER\\}"; public static final String NEW_PAGE = ":PAGE:"; private final Setting<String> name = getCommandStub() .builders() .<String>newSettingBuilder() .name("name") .description("Name of the book, use {NUMBER} for the number") .defaultTo("Book #{NUMBER}") .changed( cb -> { // 3 digits seems like a reasonable upper limit String str = cb.getTo().replaceAll(NUMBER_TOKEN, "XXX"); if (str.length() > 32) { printWarning( "Final book names longer than 32 letters will cause crashes! Current length (assuming 3 digits): %d", str.length()); } }) .build(); private final Setting<String> file = getCommandStub() .builders() .<String>newSettingBuilder() .name("file") .description("Name of the file inside the forgehax directory to use") .defaultTo("") .build(); private final Setting<Boolean> prettify = getCommandStub() .builders() .<Boolean>newSettingBuilder() .name("prettify") .description("Enables word wrapping. Can cause book size to increase dramatically") .defaultTo(true) .build(); private final Setting<Long> sleep = getCommandStub() .builders() .<Long>newSettingBuilder() .name("sleep") .description("Sleep time in ms") .defaultTo(300L) .build(); private Thread writerThread = null; private BookWriter writer = null; public BookBot() { super(Category.MISC, "BookBot", false, "Automatically write books"); } private static final Collection<Character> CHARS_NO_REPEATING = Lists.newArrayList(' ', '\n', '\t', '\r'); private static String parseText(String text, boolean wrap) { text = text.replace('\r', '\n').replace('\t', ' ').replace("\0", ""); StringBuilder builder = new StringBuilder(); char next = '\0', last; int ls = -1; // last space index for (int i = 0, p = i; i < text.length(); i++, p++, p %= MAX_CHARACTERS_PER_PAGE) { // previous character last = next; // next character next = text.charAt(i); // start a new page at the initial position if (p == 0) { builder.append(NEW_PAGE); } // if this index contains a space, save the index if (next == ' ') { ls = i; } // prevent annoying repeating characters if (CHARS_NO_REPEATING.contains(next) && CHARS_NO_REPEATING.contains(last)) { // do not append, go back 1 position to act as if this was never processed p--; continue; } // word wrapping logic if (wrap && ls != -1 && last == ' ') { // next space index int ns = text.indexOf(' ', i); // distance from next space to last space int d = ns - ls; // if the word (distance between two spaces) is less than the max chars allowed (to prevent // words greater than it from causing an infinite loop), and // the word will not fit onto the current page. if (d < MAX_CHARACTERS_PER_PAGE && (p + d) > MAX_CHARACTERS_PER_PAGE) { // insert new page builder.append(NEW_PAGE); // start at position 0 p = 0; } } builder.append(next); } return builder.toString(); } private BookWriter loadFile() throws RuntimeException { if (file.get().isEmpty()) { throw new RuntimeException("No file name set"); } Path data = getFileManager().getBaseResolve(file.get()); if (!Files.exists(data)) { throw new RuntimeException("File not found"); } if (!Files.isRegularFile(data)) { throw new RuntimeException("Not a file type"); } String text; try { text = new String(Files.readAllBytes(data), StandardCharsets.UTF_8); } catch (IOException e) { throw new RuntimeException("Failed to read file"); } String name = data.getFileName().toString(); if (name.endsWith(".txt") || name.endsWith(".book")) { return new BookWriter(this, name.endsWith(".txt") ? parseText(text, prettify.get()) : text); } else { throw new RuntimeException("File is not a .txt or .book type"); } } @Override public String getDisplayText() { return this.writer == null ? super.getDisplayText() : super.getDisplayText() + "[" + this.writer.toString() + "]"; } @Override protected void onLoad() { getCommandStub() .builders() .newCommandBuilder() .name("start") .description("Start book bot. Can optionally set the starting position") .processor( data -> { if (writerThread != null) { throw new RuntimeException("BookBot thread already running!"); } Integer page = SafeConverter.toInteger(data.getArgument(0), 0); if (writer == null) { writer = loadFile(); data.write(String.format("BookBot file \"%s\" loaded successfully", file.get())); } writer.setPage(page); writerThread = new Thread(writer); writer.start(); writerThread.start(); data.write("BookBot task started"); }) .build(); getCommandStub() .builders() .newCommandBuilder() .name("reset") .description("Stop the BookBot task") .processor( data -> { if (writer != null) { writer.setFinalListener( o -> ConsoleIO.write("BookBot task stopped at page " + writer.getPage())); writer.stop(); writerThread = null; data.write("Stopping BookBot"); } else { data.write("No writer present"); } }) .build(); getCommandStub() .builders() .newCommandBuilder() .name("resume") .description("Resume the BookBot task") .processor( data -> { if (writer != null) { writerThread = new Thread(writer); writer.start(); writerThread.start(); } else { data.write("No writer present"); } }) .build(); getCommandStub() .builders() .newCommandBuilder() .name("delete") .description("Delete the writer bot instance") .processor( data -> { if (writer != null) { writer.setFinalListener( o -> ConsoleIO.write("BookBot task stopped at page " + writer.getPage())); writer.stop(); writer = null; writerThread = null; data.write("Shutting down BookBot instance"); } else { data.write("No writer present"); } }) .build(); getCommandStub() .builders() .newCommandBuilder() .name("load") .description("Load the file into memory") .processor( data -> { writer = loadFile(); data.write(String.format("BookBot file \"%s\" loaded successfully", file.get())); }) .build(); getCommandStub() .builders() .newCommandBuilder() .name("save") .description("Save the contents to a .book file in the forgehax folder") .processor( data -> { String fname = data.getArgument(0); // optional argument, if not given use name from file variable and rename the // extension to .book if (fname == null || fname.isEmpty()) { fname = file.get(); if (!fname.endsWith(".book")) { fname = fname.substring(0, fname.lastIndexOf('.')); } } if (!fname.endsWith(".book")) { fname += ".book"; // append extension type } if (writer != null) { try (BufferedWriter out = Files.newBufferedWriter( getFileManager().getBaseResolve(fname), StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.WRITE)) { out.write(writer.contents); data.write("Successfully saved book data"); } catch (IOException e) { data.write("Failed to write file"); } } else { data.write("No writer present"); } }) .build(); } private static class BookWriter implements Runnable { public enum Status { INITIALIZED, FINISHED, ERROR, STOPPED, AWAITING_GUI_CLOSE, NEED_EMPTY_BOOKS_IN_HOTBAR, CHANGING_HELD_ITEM, OPENING_BOOK, CLOSING_BOOK, WRITING_BOOK, } private final BookBot parent; private final String contents; private final int totalPages; private volatile Status status = Status.INITIALIZED; private volatile boolean stopped = false; private Scanner parser; private int page = 0; private Consumer<BookWriter> finalListener = null; public BookWriter(BookBot parent, String contents) { this.parent = parent; this.contents = contents; Scanner scanner = newScanner(); int c = 0; while (scanner.hasNext()) { scanner.next(); c++; } this.totalPages = c; } private Scanner newScanner() { return new Scanner(contents).useDelimiter(NEW_PAGE); } public int getTotalPages() { return totalPages; } public int getTotalBooks() { return totalPages > 0 ? (int) Math.ceil((double) (totalPages) / (double) (MAX_PAGES)) : 0; } public Status getStatus() { return status; } public int getPage() { return page; } public void setPage(int page) { if (parser != null) { throw new RuntimeException("Cannot set position while task is running or stopped"); } this.page = page; } public int getBook() { return page > 0 ? (int) Math.ceil((double) (page) / (double) (MAX_PAGES)) : 0; } public void setFinalListener(Consumer<BookWriter> finalListener) { this.finalListener = finalListener; } public boolean isStopped() { return stopped; } public void start() { if (parser == null) { parser = newScanner(); // skip pages for (int i = 0; i < page && parser.hasNext(); i++) { parser.next(); } } stopped = false; finalListener = null; } public void stop() { stopped = true; } private void sendBook(ItemStack stack) { NBTTagList pages = new NBTTagList(); // page tag list // copy pages into NBT for (int i = 0; i < MAX_PAGES && parser.hasNext(); i++) { pages.appendTag(new NBTTagString(parser.next().trim())); page++; } // set our client side book if (stack.hasTagCompound()) { stack.getTagCompound().setTag("pages", pages); } else { stack.setTagInfo("pages", pages); } // publish the book stack.setTagInfo("author", new NBTTagString(getLocalPlayer().getName())); stack.setTagInfo( "title", new NBTTagString(parent.name.get().replaceAll(NUMBER_TOKEN, "" + getBook()).trim())); PacketBuffer buff = new PacketBuffer(Unpooled.buffer()); buff.writeItemStack(stack); MC.getConnection().sendPacket(new CPacketCustomPayload("MC|BSign", buff)); } @Override public void run() { try { while (!stopped) { // check to see if we've finished the book if (!parser.hasNext()) { this.status = Status.FINISHED; break; } sleep(); // wait for screen if (MC.currentScreen != null) { this.status = Status.AWAITING_GUI_CLOSE; continue; } // search for empty book int slot = -1; ItemStack selected = null; for (int i = 0; i < InventoryPlayer.getHotbarSize(); i++) { ItemStack stack = getLocalPlayer().inventory.getStackInSlot(i); if (stack != null && !stack.equals(ItemStack.EMPTY) && stack.getItem() instanceof ItemWritableBook) { slot = i; selected = stack; break; } } // make sure we found a book if (slot == -1) { this.status = Status.NEED_EMPTY_BOOKS_IN_HOTBAR; continue; } // set selected item to that slot while (getLocalPlayer().inventory.currentItem != slot) { getLocalPlayer().inventory.currentItem = slot; this.status = Status.CHANGING_HELD_ITEM; sleep(); } final ItemStack item = selected; // open the book gui screen this.status = Status.OPENING_BOOK; MC.addScheduledTask(() -> getLocalPlayer().openBook(item, EnumHand.MAIN_HAND)); // wait for gui to open while (!(MC.currentScreen instanceof GuiScreenBook)) { sleep(); } // send book to server this.status = Status.WRITING_BOOK; MC.addScheduledTask( () -> { sendBook(item); MC.displayGuiScreen(null); }); // wait for screen to close while (MC.currentScreen != null) { sleep(); } } } catch (Throwable t) { this.status = Status.ERROR; } finally { if (finalListener != null) { finalListener.accept(this); finalListener = null; } // set stopped to true this.stopped = true; if (!this.status.equals(Status.FINISHED) && !this.status.equals(Status.ERROR)) { this.status = Status.STOPPED; } } } @Override public String toString() { return String.format( "Status=%s,P/T=%d/%d,B/T=%d/%d", status.name(), page, getTotalPages(), getBook(), getTotalBooks()); } private void sleep() throws InterruptedException { Thread.sleep(parent.sleep.get()); if (stopped) { throw new RuntimeException("Thread stopped"); } } } }