diff --git a/.gitignore b/.gitignore index 5459b6f0..0290202d 100644 --- a/.gitignore +++ b/.gitignore @@ -48,4 +48,8 @@ bin .settings .classpath .project -.idea \ No newline at end of file +.idea +*.code-workspace + +# Generated resources +src/main/resources/generated/* diff --git a/build.gradle b/build.gradle index ebff8ea2..bae0d8a0 100644 --- a/build.gradle +++ b/build.gradle @@ -22,13 +22,12 @@ license { header = project.file('LICENSE_HEADER') ext.year = Calendar.getInstance().get(Calendar.YEAR) mapping("java", "SLASHSTAR_STYLE") + exclude "**/*.json" // Exclude JSON to keep the font width data valid } tasks.withType(JavaCompile) { options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" << "-XDignore.symbol.file" } -// Run the license formatter before compiling the source code. -tasks.compileJava.dependsOn licenseFormatMain, licenseFormatTest configurations { jaxDoclet @@ -42,9 +41,24 @@ repositories { maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } maven { url 'https://papermc.io/repo/repository/maven-public/' } + + // Define a Ivy repo for the font width data (that way we don't need another plugin!) + def ddd_mc_font = ivy { + url 'https://dumbdogdiner.github.io/' + patternLayout { artifact '/[module]/[revision]/[classifier].[ext]'} + metadataSources { artifact() } + } + // Only use the Ivy repo for font width data - speeds up dependency resolution + exclusiveContent { + forRepositories(ddd_mc_font) + filter { includeGroup("dumbdogdiner") } + } } dependencies { + // Font width data (see above) + compile 'dumbdogdiner:mc-font-extractor:main:mojangles_width_data@json' + compileOnly 'org.projectlombok:lombok:1.18.16' annotationProcessor 'org.projectlombok:lombok:1.18.16' @@ -92,7 +106,6 @@ task sources(type: Jar, dependsOn: classes) { // Some environments (such as the builder image) do not use UTF-8 as the default encoding! // This sets UTF-8 as the encoding for the following tasks: delombok, compileJava, compileTestJava and javadoc. delombok.encoding = "UTF-8" - compileJava.options.encoding = "UTF-8" compileTestJava.options.encoding = "UTF-8" javadoc.options.encoding = "UTF-8" @@ -129,10 +142,23 @@ task processSourceTokens(type: Sync) { // Use the filter task as the input for compileJava compileJava.source = processSourceTokens.outputs - -tasks.publish.dependsOn build, sources +// Font Width Info +task copyMCFontExtractor(type: Copy) { + def path = project.configurations.compile.find {it.name.startsWith("mc-font-extractor") } + println path + from file(path) + // into file("src/main/resources") + // - Please keep this comment for future reference. + // - This is how we would do this if we weren't also adding build info (see processSourceTokens) + destinationDir file("src/main/resources/generated/") + rename 'mc-font-extractor-main-mojangles_width_data.json', 'mojangles_width_data.json' +} +// Run the license formatter and font data copier before compiling the source code. +tasks.compileJava.dependsOn licenseFormatMain, licenseFormatTest, copyMCFontExtractor +tasks.build.dependsOn sources +tasks.publish.dependsOn build publishing { diff --git a/src/main/java/com/dumbdogdiner/stickyapi/bukkit/item/BookAndQuillBuilder.java b/src/main/java/com/dumbdogdiner/stickyapi/bukkit/item/BookAndQuillBuilder.java new file mode 100644 index 00000000..40fb01d0 --- /dev/null +++ b/src/main/java/com/dumbdogdiner/stickyapi/bukkit/item/BookAndQuillBuilder.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +package com.dumbdogdiner.stickyapi.bukkit.item; + +import com.dumbdogdiner.stickyapi.common.nbt.NbtCompoundTag; +import com.dumbdogdiner.stickyapi.common.nbt.NbtJsonTag; +import com.dumbdogdiner.stickyapi.common.nbt.NbtListTag; +import com.dumbdogdiner.stickyapi.common.nbt.NbtStringTag; +import com.dumbdogdiner.stickyapi.common.util.TextUtil; +import com.dumbdogdiner.stickyapi.common.util.StringUtil; +import com.google.common.base.Preconditions; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.VisibleForTesting; + +import java.awt.print.Book; +import java.util.Collections; +import java.util.StringJoiner; + +/** + * This class allows for easy construction of a writable book, which is unfortunately much + * simpler than a written/signed book, either manually in code, or by loading a JSON object + * + * @since TBA + */ + +@SuppressWarnings({"UnusedReturnValue", "unused"}) +@Accessors(chain = true) +@NoArgsConstructor +public class BookAndQuillBuilder { + private @NotNull JsonArray pages = new JsonArray(); + + @Setter + @Getter + private @Nullable String displayName; + + private JsonArray lore = new JsonArray(); + + /** + * Creates a new BookGenerator from a given JsonObject, representing a book, which can + * be obtained in whatever way you want. + * + * @param bookObject A JSON object containing a JsonArray of valid Pages; If a {@link Material#WRITTEN_BOOK} is desired, it must contain a title, author, and optionally, a Generation (an integer with the value of 0, 1, 2, or 3, with 0 being the default) + * @return a {@link BookAndQuillBuilder} with the specified pages; if a title and author are both specified, it will be a {@link Material#WRITTEN_BOOK}, otherwise it will be a {@link Material#WRITABLE_BOOK} + */ + public static @NotNull BookAndQuillBuilder fromJson(@NotNull JsonObject bookObject) { + BookAndQuillBuilder bookBuilder = new BookAndQuillBuilder(); + JsonArray pages = bookObject.get("pages").getAsJsonArray(); + Preconditions.checkNotNull(pages); + bookBuilder.pages = pages; + if(bookObject.has("lore") && bookObject.get("lore").isJsonArray()){ + bookBuilder.lore = bookObject.get("lore").getAsJsonArray(); + } + return bookBuilder; + } + + /** + * Add some pages to the book. May fail with {@link IllegalStateException} if the book is full or fills up. + * + * @param pages Pages to add + * @return This object, for chaining + */ + public @NotNull BookAndQuillBuilder addPages(String @NotNull ... pages) { + for (String page : pages) { + addPage(page); + } + return this; + } + + /** + * Add a page to the book. May fail with {@link IllegalStateException} if the book is full. + * + * @param page The page to add + * @return This object, for chaining + */ + public @NotNull BookAndQuillBuilder addPage(@NotNull String page) { + Preconditions.checkState(!isFull(), "Cannot add page, the book is overfilled!"); + pages.add(page); + return this; + } + + /** + * Build a book and quill from this generator. + * + * @return an {@link ItemStack} of the book, with pages and all other data + */ + @SuppressWarnings("deprecation") + public @NotNull ItemStack toItemStack() { + Preconditions.checkState(pages.size() > 0, "Cannot generate book with no pages"); + Preconditions.checkState(pages.size() < TextUtil.PAGES_PER_BOOK, "Cannot generate book with an invalid number of pages (must be less than " + TextUtil.PAGES_PER_BOOK + ")"); + ItemStack stack = new ItemStack(Material.WRITABLE_BOOK, 1); + + stack = Bukkit.getUnsafe().modifyItemStack(stack, generateNBT()); + + return stack; + } + + /** + * @return The percentage of pages allowed that are used by this book. + * TODO: Does not account for characters per page or packet size at this time. + */ + public float percentFull() { + return (float) pages.size() / (float) TextUtil.PAGES_PER_BOOK; + } + + /** + * TODO: Does not account for characters per page or packet size at this time. + * + * @return True if the book is full. + */ + public boolean isFull() { + return percentFull() >= 1.0f; + } + + /** + * Adds a line of lore (formatted JSON text stuffs) to the book + */ + public @NotNull BookAndQuillBuilder addLoreLine(JsonObject lore){ + this.lore.add(lore); + return this; + } + + public @NotNull BookAndQuillBuilder addLoreLines(JsonObject ... lores){ + for(JsonObject lore : lores){ + addLoreLine(lore); + } + return this; + } + + /** + * Uses a {@link StringJoiner} to convert pages JsonArray to the weird NBT list + * + * @return {@link String} with NBT of the pages + */ + @VisibleForTesting + public @NotNull String generateNBT() { + NbtCompoundTag bookNBT = new NbtCompoundTag(); + bookNBT.put("pages", NbtListTag.fromJsonArray(this.pages)); + NbtCompoundTag display = new NbtCompoundTag(); + if(displayName != null) + display.put("Name", displayName); + if(lore.size() > 0){ + display.put("Lore", NbtListTag.fromJsonArrayQuoted(lore)); + } + if(!display.isEmpty()) + bookNBT.put("display", display); + return bookNBT.toNbtString(); + } +} diff --git a/src/main/java/com/dumbdogdiner/stickyapi/bukkit/item/WrittenBookBuilder.java b/src/main/java/com/dumbdogdiner/stickyapi/bukkit/item/WrittenBookBuilder.java new file mode 100644 index 00000000..31aae34d --- /dev/null +++ b/src/main/java/com/dumbdogdiner/stickyapi/bukkit/item/WrittenBookBuilder.java @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +package com.dumbdogdiner.stickyapi.bukkit.item; + +import com.dumbdogdiner.stickyapi.common.nbt.NbtCompoundTag; +import com.dumbdogdiner.stickyapi.common.nbt.NbtListTag; +import com.dumbdogdiner.stickyapi.common.nbt.NbtStringTag; +import com.dumbdogdiner.stickyapi.common.util.TextUtil; +import com.dumbdogdiner.stickyapi.common.util.StringUtil; +import com.google.common.base.Preconditions; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.BookMeta; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.VisibleForTesting; + +import java.util.Collections; +import java.util.StringJoiner; + +/** + * This class allows for easy construction of a Written Book, whether through JSON objects and manual + * creation, or by loading a full JSON object from a file or other source + * + * @since TBA + */ +@SuppressWarnings({"UnusedReturnValue", "unused"}) +@Accessors(chain = true) +@NoArgsConstructor +public class WrittenBookBuilder { + private @NotNull JsonArray pages = new JsonArray(); + /** + * The generation of the book. + */ + @Getter + @Setter + private @NonNull BookMeta.@NotNull Generation generation = BookMeta.Generation.ORIGINAL; + + /** + * The author of the book. Must not be null if the book is written. + */ + @Getter + @Setter + private @NotNull String author = StringUtil.randomObfuscatedString(1, 17, 4); + + /** + * The title of the book, can be formatted using color codes. + */ + @Getter + @Setter + private @NotNull String title = StringUtil.randomObfuscatedString(1, 17, 6); + + /** + * Lore/hover text for the item + */ + private @NotNull JsonArray lore = new JsonArray(); + + /** + * The book display name (In inventory) + */ + @Getter + @Setter + private @Nullable String displayName; + + /** + * Creates a new BookGenerator from a given JsonObject, representing a book, which can + * be obtained in whatever way you want. + * + * @param bookObject A JSON object containing a JsonArray of valid Pages; If a {@link Material#WRITTEN_BOOK} is desired, it must contain a title, author, and optionally, a Generation (an integer with the value of 0, 1, 2, or 3, with 0 being the default) + * @return a {@link WrittenBookBuilder} with the specified pages; if a title and author are both specified, it will be a {@link Material#WRITTEN_BOOK}, otherwise it will be a {@link Material#WRITABLE_BOOK} + */ + public static @NotNull WrittenBookBuilder fromJson(@NotNull JsonObject bookObject) { + Preconditions.checkArgument(bookObject.has("pages") && bookObject.get("pages").isJsonArray(), "The provided JSON object must have a pages array!"); + WrittenBookBuilder bookBuilder = new WrittenBookBuilder(); + if (bookObject.has("author")) + bookBuilder.author = bookObject.get("author").getAsString(); + if (bookObject.has("title")) + bookBuilder.title = bookObject.get("title").getAsString(); + if (bookObject.has("generation")) + switch (bookObject.get("generation").getAsInt()) { + case 1: + bookBuilder.generation = BookMeta.Generation.COPY_OF_ORIGINAL; + break; + case 2: + bookBuilder.generation = BookMeta.Generation.COPY_OF_COPY; + break; + case 3: + bookBuilder .generation = BookMeta.Generation.TATTERED; + break; + case 0: + default: + bookBuilder.generation = BookMeta.Generation.ORIGINAL; + } + if(bookObject.has("lore") && bookObject.get("lore").isJsonArray()) + bookBuilder.lore = bookObject.get("lore").getAsJsonArray(); + + bookBuilder.pages = bookObject.get("pages").getAsJsonArray(); + return bookBuilder; + } + + /** + * Adds a line of lore (formatted JSON text stuffs) to the book + */ + public @NotNull WrittenBookBuilder addLoreLine(JsonObject lore){ + this.lore.add(lore); + return this; + } + + public @NotNull WrittenBookBuilder addLoreLines(JsonObject ... lores){ + for(JsonObject lore : lores){ + addLoreLine(lore); + } + return this; + } + + /** + * Adds a line of lore (formatted JSON text stuffs) to the book + */ + public @NotNull WrittenBookBuilder addLoreLine(NbtStringTag lore){ + this.lore.add(lore.toJson()); + return this; + } + + public @NotNull WrittenBookBuilder addLoreLines(NbtStringTag ... lores){ + for(NbtStringTag lore : lores){ + addLoreLine(lore); + } + return this; + } + + /** + * Add some pages to the book. May fail with {@link IllegalStateException} if the book is full or fills up. + * + * @param pages Pages to add + * @return This object, for chaining + */ + public @NotNull WrittenBookBuilder addPages(JsonObject @NotNull ... pages) { + for (JsonObject page : pages) { + addPage(page); + } + return this; + } + + /** + * Add a page to the book. May fail with {@link IllegalStateException} if the book is full. + * + * @param page The page to add + * @return This object, for chaining + */ + public @NotNull WrittenBookBuilder addPage(@NotNull JsonObject page) { + Preconditions.checkState(!isFull(), "Cannot add page, the book is overfilled!"); + pages.add(page); + return this; + } + + /** + * Build a book from this generator. + * + * @param quantity Quantity of the item stack. + * @return an {@link ItemStack} of the book, with pages and all other data + */ + @SuppressWarnings("deprecation") + public @NotNull ItemStack toItemStack(int quantity) { + Preconditions.checkArgument(quantity > 0 && quantity <= 16, "Invalid quantity specified, quantity should be greater than 0 and less than or equal to 16, but was " + quantity); + Preconditions.checkState(pages.size() > 0, "Cannot generate book with no pages"); + Preconditions.checkState(pages.size() < TextUtil.PAGES_PER_BOOK, "Cannot generate book with an invalid number of pages (must be less than " + TextUtil.PAGES_PER_BOOK + ")"); + ItemStack stack = new ItemStack(Material.WRITTEN_BOOK, quantity); + + stack = Bukkit.getUnsafe().modifyItemStack(stack, generateNBT()); + + return stack; + } + + /** + * @return The percentage of pages allowed that are used by this book. + * TODO: Does not account for characters per page or packet size at this time. + */ + public float percentFull() { + return (float) pages.size() / (float) TextUtil.PAGES_PER_BOOK; + } + + /** + * TODO: Does not account for characters per page or packet size at this time. + * + * @return True if the book is full. + */ + public boolean isFull() { + return percentFull() >= 1.0f; + } + + /** + * Uses a {@link StringJoiner} to convert pages JsonArray to the weird NBT list + * + * @return {@link String} with NBT of the pages + */ + @VisibleForTesting + public @NotNull String generateNBT() { + NbtCompoundTag bookNBT = new NbtCompoundTag(); + bookNBT.put("author", author); + bookNBT.put("generation", getGenerationInt()); + bookNBT.put("title", title); + NbtCompoundTag display = new NbtCompoundTag(); + if(displayName != null) + display.put("Name", displayName); + if(lore.size() > 0){ + display.put("Lore", NbtListTag.fromJsonArrayQuoted(lore)); + } + if(!display.isEmpty()) + bookNBT.put("display", display); + bookNBT.put("pages", NbtListTag.fromJsonArrayQuoted(pages)); + return bookNBT.toNbtString(); + } + + /** + * Gets the generation as an int + */ + private int getGenerationInt() { + switch (generation){ + case ORIGINAL: + return 0; + case COPY_OF_ORIGINAL: + return 1; + case COPY_OF_COPY: + return 2; + case TATTERED: + return 3; + default: + throw new IllegalStateException(); + } + } +} diff --git a/src/main/java/com/dumbdogdiner/stickyapi/bukkit/item/package-info.java b/src/main/java/com/dumbdogdiner/stickyapi/bukkit/item/package-info.java new file mode 100644 index 00000000..1f5149b8 --- /dev/null +++ b/src/main/java/com/dumbdogdiner/stickyapi/bukkit/item/package-info.java @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +/** + * Classes related to item generation or manipulation + */ + +package com.dumbdogdiner.stickyapi.bukkit.item; diff --git a/src/main/java/com/dumbdogdiner/stickyapi/common/util/MathUtil.java b/src/main/java/com/dumbdogdiner/stickyapi/common/util/MathUtil.java index c5c66f72..f65d3b7f 100644 --- a/src/main/java/com/dumbdogdiner/stickyapi/common/util/MathUtil.java +++ b/src/main/java/com/dumbdogdiner/stickyapi/common/util/MathUtil.java @@ -4,9 +4,14 @@ */ package com.dumbdogdiner.stickyapi.common.util; +import com.google.common.primitives.Chars; +import com.google.common.primitives.Ints; +import org.jetbrains.annotations.NotNull; + import java.text.CharacterIterator; import java.text.StringCharacterIterator; import java.util.List; +import java.util.Objects; import java.util.Random; /** @@ -15,6 +20,8 @@ *

*/ public class MathUtil { + private MathUtil() { + } private static final Random random = new Random(); @@ -35,13 +42,15 @@ public static int randomInt(int max) { * Get a random number within a range *

* - * @param min minimum value - * @param max maximum value + * @param min minimum value (inclusive) + * @param max maximum value (inclusive) * @return a random integer within the specified range * @throws IllegalArgumentException when min is greater than max */ public static int randomInt(int min, int max) { - if (min >= max) + if (min == max) + return min; + if (min > max) throw new IllegalArgumentException("Min may not be greater than max!"); return min + randomInt(1 + max - min); } @@ -82,8 +91,8 @@ public static double randomDouble(double min, double max) { * @param array the array that should be used * @return a random element from the specified array */ - public static T randomElement(T[] array) { - if(array.length < 1) return null; + public static T randomElement(@NotNull T @NotNull [] array) { + if (array.length < 1) return null; return array[randomInt(array.length)]; } @@ -95,8 +104,8 @@ public static T randomElement(T[] array) { * @param list the list that should be used * @return a random element from the specified list */ - public static T randomElement(List list) { - if(list.size() < 1) return null; + public static T randomElement(@NotNull List list) { + if (list.size() < 1) return null; return list.get(randomInt(list.size())); } @@ -105,7 +114,7 @@ public static T randomElement(List list) { * Round a double value *

* - * @param value the value that should be rounded + * @param value the value that should be rounded * @param places amount of decimal places * @return {@link Double} */ @@ -145,12 +154,25 @@ public static String bytesToReadable(long bytes) { *

* * @param number the number that should be tested - * @param min minimum value - * @param max maximum value + * @param min minimum value + * @param max maximum value * @return true if given number is in range */ public static boolean inRange(int number, int min, int max) { return number >= min && number <= max; } -} \ No newline at end of file + /** + * @see #randomElement(List) + */ + public static char randomElement(char @NotNull [] choices) { + return Objects.requireNonNull(randomElement(Chars.asList(choices))); + } + + /** + * @see #randomElement(List) + */ + public static int randomElement(int @NotNull [] choices) { + return Objects.requireNonNull(randomElement(Ints.asList(choices))); + } +} diff --git a/src/main/java/com/dumbdogdiner/stickyapi/common/util/StringUtil.java b/src/main/java/com/dumbdogdiner/stickyapi/common/util/StringUtil.java index 5a5dddfa..b289e5c5 100644 --- a/src/main/java/com/dumbdogdiner/stickyapi/common/util/StringUtil.java +++ b/src/main/java/com/dumbdogdiner/stickyapi/common/util/StringUtil.java @@ -9,6 +9,8 @@ import java.util.Map; import java.util.UUID; +import com.google.common.base.Preconditions; + import net.md_5.bungee.api.ChatColor; import org.jetbrains.annotations.NotNull; @@ -269,6 +271,21 @@ public static boolean startsWithIgnoreCase(@NotNull final String string, @NotNul return string.regionMatches(true, 0, prefix, 0, prefix.length()); } + /** + * This method uses a region to check case-sensitive equality. + * + * @param string String to check + * @param prefix Prefix of string to compare + * @return {@link Boolean} + * @throws NullPointerException if prefix or string is null + */ + public static boolean startsWith(@NotNull final String string, @NotNull final String prefix) { + if (string.length() < prefix.length()) { + return false; + } + return string.regionMatches(false, 0, prefix, 0, prefix.length()); + } + /** * Put hyphens into a uuid *

@@ -277,7 +294,7 @@ public static boolean startsWithIgnoreCase(@NotNull final String string, @NotNul * * @param uuid to hyphenate * @return {@link UUID} - * @throws NullPointerException if uuid string is null + * @throws NullPointerException if uuid string is null * @throws IllegalArgumentException if uuid is not 32 characters and is invalid */ public static UUID hyphenateUUID(@NotNull String uuid) { @@ -291,13 +308,63 @@ public static UUID hyphenateUUID(@NotNull String uuid) { } /** - * Replaces & followed by any valid minecraft format code (matching the regex

(?=([a-f]|[0-9]|[klmnor]))
with § + * Creates a random string of "magic" characters of characters + * + * @param min the minimum length of the string (inclusive) + * @param max the maximum length of the string (inclusive) + * @param minRunBeforeSpace Minimum number of characters before a space can + * appear, set to 0 to disable space + * @return A string of random characters, where the length + */ + public static String randomObfuscatedString(int min, int max, int minRunBeforeSpace) { + Preconditions.checkArgument(max >= min, "Max cannot be less than min"); + // Placeholder chars by width + char[] choices = new char[] { + // Space is boring + // ' ', // 1 px + 'i', // 2 px + 'l', // 3 px + 't', // 4 px + 'f', // 5 px + 'a', // 6 px + '@' // 7 px + }; + StringBuilder obfuscated = new StringBuilder(); + + int len = MathUtil.randomInt(min, max); + int charsSinceSpace = 0; + while (obfuscated.length() < len) { + if (minRunBeforeSpace > 0 && charsSinceSpace > minRunBeforeSpace && + // Set a 5% probability of the character being a space + MathUtil.randomInt(1, 100) <= 5) { + obfuscated.append(' '); + charsSinceSpace = 0; + } else { + obfuscated.append(MathUtil.randomElement(choices)); + charsSinceSpace++; + } + } + obfuscated.insert(0, ChatColor.MAGIC); + obfuscated.append(ChatColor.RESET); + return obfuscated.toString(); + } + + /** + * Replaces & followed by any valid minecraft format code (matching the + * regex + * + *
+     * (?=([a-f]|[0-9]|[klmnor]))
+     * 
+ * + * with § * * @param input The input string - * @return A string where the relevant ampersands are replaced with section symbols + * @return A string where the relevant ampersands are replaced with section + * symbols */ public static String formatChatCodes(String input) { return input.replaceAll("&(?=([a-f]|[0-9]|[klmnor]))", Character.toString(ChatColor.COLOR_CHAR)); } -} +} \ No newline at end of file diff --git a/src/main/java/com/dumbdogdiner/stickyapi/common/util/TextUtil.java b/src/main/java/com/dumbdogdiner/stickyapi/common/util/TextUtil.java index 9bdee829..2409e091 100644 --- a/src/main/java/com/dumbdogdiner/stickyapi/common/util/TextUtil.java +++ b/src/main/java/com/dumbdogdiner/stickyapi/common/util/TextUtil.java @@ -4,63 +4,150 @@ */ package com.dumbdogdiner.stickyapi.common.util; -import java.util.HashMap; - +import com.dumbdogdiner.stickyapi.StickyAPI; +import com.google.common.base.Preconditions; +import com.google.gson.Gson; +import lombok.Data; +import lombok.NonNull; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import net.md_5.bungee.api.ChatColor; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.HashMap; +import java.util.Set; +/** + * Utilities for text, such as in chat, books, and signs + * + * @since TBA (rewritten) + */ public class TextUtil { - static HashMap characterWidths = new HashMap<>() { - { - put('!', 1); - put(',', 1); - put('\'', 1); - put('.', 1); - put(':', 1); - put(';', 1); - put('i', 1); - put('|', 1); - put('!', 1); - - put('`', 2); - put('l', 2); - - put(' ', 3); - put('(', 3); - put(')', 3); - put('*', 3); - put('I', 3); - put('[', 3); - put(']', 3); - put('t', 3); - put('{', 3); - put('}', 3); - - put('<', 4); - put('>', 4); - put('f', 4); - put('k', 4); - - put('@', 6); - put('~', 6); + private TextUtil() { + } + + /** Offset for bold (in each direction) */ + public static final float BOLD_OFFSET = 0.5f; + /** Offset for shadows (in each direction) */ + public static final float SHADOW_OFFSET = 0.5f; + + /** Number of pixels per line of a book */ + public static final int PIXELS_PER_BOOK_LINE = 113; + /** Number of half-pixels per book line */ + public static final int HALF_PIXELS_PER_BOOK_LINE = PIXELS_PER_BOOK_LINE * 2; + + /** Number of lines in a page of a book */ + public static final int LINES_PER_PAGE = 14; + /** Number of pages in a book */ + public static final int PAGES_PER_BOOK = 50; + + /** Number of pixels per line of a sign */ + // TODO verify via minecraft source code + public static final int PIXELS_PER_SIGN_LINE = 96; + /** Number of lines per sign */ + public static final int LINES_PER_SIGN = 4; + + private static final HashMap widths = new HashMap<>(); + + /** + * Internal class to parse the width data json file + */ + @Data + private static class WidthEntry { + private @NotNull String uid; + private @Nullable Integer id; + private @NotNull String val; + private int width; + } + + static { + Gson gson = new Gson(); + try (InputStream input = ClassLoader.getSystemResource("generated/mojangles_width_data.json").openStream()) { + WidthEntry[] entries = gson.fromJson(new InputStreamReader(input), WidthEntry[].class); + for (WidthEntry entry : entries) { + if (entry.getId() == null) entry.setId(0); + widths.put((char) (entry.getId() != null ? entry.getId() : '\0'), entry.width); + } + } catch (Exception e) { + StickyAPI.getLogger().severe(e.getMessage()); + // fallback to a minimal configuration, in case anything fails + for (char c = 32; c <= 126; ++c) { + widths.put(c, 12); + } + // Extra fallback data to load in in case anything fails + // Minecraft font data is formatted in a very particular way + // so we are forced to use unfriendly code to load in extra fallback data. + "\0 ".chars().forEach(c -> widths.put((char) c, 2)); + "!',.:;i|".chars().forEach(c -> widths.put((char) c, 4)); + "`l".chars().forEach(c -> widths.put((char) c, 6)); + // Some characters are represented by their Unicode code point to ensure they + // load correctly + "\"()*I[]t{}\u2022".chars().forEach(c -> widths.put((char) c, 8)); + "<>fk\u00b7".chars().forEach(c -> widths.put((char) c, 10)); + "@~".chars().forEach(c -> widths.put((char) c, 14)); } - }; + } - // Uses info from: /** - * Get the width of a character (https://minecraft.gamepedia.com/Language#Font) + * Check if the given character is supported in-game. * - * @param c - * @return + * @param c The character to check + * @return A boolean representing if the character is supported or not */ - public static int getCharacterWidth(@NotNull char c) { - if (c < 32 || c > 126) { - // Not presently implemented, would require rendering TTF - return -1; - } + public static boolean isCharacterSupported(char c) { + return widths.containsKey(c); + } - if (characterWidths.containsKey(c)) { - return characterWidths.get(c); + + /** + * Returns a {@link Set} of {@link Character}of the supported characters + */ + public static Set getSupportedCharacters() { + return widths.keySet(); + } + + /** + * @param c The character to measure + * @return The width of the character in half-pixels + * @throws IllegalArgumentException if the character is out of range + */ + public static int getCharacterWidth(char c) throws IllegalArgumentException{ + Preconditions.checkArgument(widths.containsKey(c), "Unsupported character: " + c + " code: " + String.format("%04x", (int) c)); + + return widths.get(c); + } + + /** + * Measure the width of a string, assuming it is in the default Minecraft font. + * + * @param text The string to measure + * @param isBold If the string is bold + * @return The width of the string, in half-pixels + */ + public static int getStringWidth(@NonNull String text, boolean isBold) { + text = ChatColor.stripColor(text); + int width = 0; + + for (char c : text.toCharArray()) { + width += TextUtil.getCharacterWidth(c); } - return 5; + + if (isBold) + width += 2 * text.length(); + + return width; + } + + /** + Measure the width of a string, assuming it is in the default Minecraft font. + * + * @param text The string to measure + * @return The width of the string, in half-pixels + * @see #getStringWidth(String, boolean) + */ + public static int getStringWidth(@NonNull String text) { + return getStringWidth(text, false); } } diff --git a/src/main/java/com/dumbdogdiner/stickyapi/common/util/reflection/package-info.java b/src/main/java/com/dumbdogdiner/stickyapi/common/util/reflection/package-info.java new file mode 100644 index 00000000..5b9b0d6f --- /dev/null +++ b/src/main/java/com/dumbdogdiner/stickyapi/common/util/reflection/package-info.java @@ -0,0 +1,8 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +/** + * Classes that make reflection a tad easier + */ +package com.dumbdogdiner.stickyapi.common.util.reflection; diff --git a/src/main/java/com/dumbdogdiner/stickyapi/common/util/url/URLPair.java b/src/main/java/com/dumbdogdiner/stickyapi/common/util/url/URLPair.java index 3b400fbd..06d29054 100644 --- a/src/main/java/com/dumbdogdiner/stickyapi/common/util/url/URLPair.java +++ b/src/main/java/com/dumbdogdiner/stickyapi/common/util/url/URLPair.java @@ -4,24 +4,18 @@ */ package com.dumbdogdiner.stickyapi.common.util.url; +import lombok.Data; + /** * Sub-class for easier URL formatting between methods. */ +@Data public class URLPair { - String fullPath; - String shortened; + private String fullPath; + private String shortened; public URLPair(String fullUrl, String shortenedUrl) { this.fullPath = fullUrl; this.shortened = shortenedUrl; } - - public String getShortened() { - return shortened; - } - - public String getFullPath() { - return fullPath; - } - } diff --git a/src/main/java/com/dumbdogdiner/stickyapi/common/util/url/package-info.java b/src/main/java/com/dumbdogdiner/stickyapi/common/util/url/package-info.java new file mode 100644 index 00000000..5cab60b2 --- /dev/null +++ b/src/main/java/com/dumbdogdiner/stickyapi/common/util/url/package-info.java @@ -0,0 +1,8 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +/** + * Utility classes for URLs + */ +package com.dumbdogdiner.stickyapi.common.util.url; diff --git a/src/main/java/com/dumbdogdiner/stickyapi/package-info.java b/src/main/java/com/dumbdogdiner/stickyapi/package-info.java new file mode 100644 index 00000000..827f5776 --- /dev/null +++ b/src/main/java/com/dumbdogdiner/stickyapi/package-info.java @@ -0,0 +1,11 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +/** + *

StickyAPI

Utility methods, classes and potentially + * code-dupe-annihilating code for DDD plugins. + * + * @author DumbDogDiner (dumbdogdiner.com) + */ +package com.dumbdogdiner.stickyapi; diff --git a/src/test/java/com/dumbdogdiner/stickyapi/bukkit/item/BookBuilderTest.java b/src/test/java/com/dumbdogdiner/stickyapi/bukkit/item/BookBuilderTest.java new file mode 100644 index 00000000..6884d01a --- /dev/null +++ b/src/test/java/com/dumbdogdiner/stickyapi/bukkit/item/BookBuilderTest.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +package com.dumbdogdiner.stickyapi.bukkit.item; + +import org.junit.jupiter.api.Test; + +public class BookBuilderTest { + @Test + public void testJsonBuild(){ + BookAndQuillBuilder bb = new BookAndQuillBuilder(); + bb.addPage("Page1 is just a page\\n"); + bb.addPages("And another page", "and yet another"); + //noinspection RedundantArrayCreation + bb.addPages(new String [] {"yet more", "and more still"}); + + System.out.println("Pages NBT:"); + System.out.println(bb.generateNBT()); + + + System.out.println("Please paste the following line into a command block, and activate it"); + System.out.println("give @p minecraft:writable_book" + bb.generateNBT()); + } +} diff --git a/src/test/java/com/dumbdogdiner/stickyapi/bukkit/item/WrittenBookBuilderTest.java b/src/test/java/com/dumbdogdiner/stickyapi/bukkit/item/WrittenBookBuilderTest.java new file mode 100644 index 00000000..0b4c012e --- /dev/null +++ b/src/test/java/com/dumbdogdiner/stickyapi/bukkit/item/WrittenBookBuilderTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +package com.dumbdogdiner.stickyapi.bukkit.item; + +import com.dumbdogdiner.stickyapi.common.nbt.NbtCompoundTag; +import com.dumbdogdiner.stickyapi.common.nbt.NbtJsonTag; +import com.dumbdogdiner.stickyapi.common.nbt.NbtListTag; +import com.dumbdogdiner.stickyapi.common.nbt.NbtStringTag; +import com.dumbdogdiner.stickyapi.common.util.StringUtil; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import org.junit.jupiter.api.*; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.text.MessageFormat; + +public class WrittenBookBuilderTest { + private static Reader bookReader; + + private static Gson G; + + + @BeforeAll + static void setUp(){ + G = new GsonBuilder().create(); + } + + + @AfterEach + void tearDown() throws IOException { + bookReader.close(); + System.gc(); + } + + @AfterAll + static void tearDownEnd() { + G = null; + System.gc(); + } + + + @Test + public void testSimple() { + testJSON("/book1.json"); + } + + @Test + public void testProdRulebook() { + testJSON("/rulebook.json"); + } + + @Test + void testNbtStuff() { + bookReader = new InputStreamReader(WrittenBookBuilderTest.class.getResourceAsStream("/rulebook.json")); + JsonObject bookObject = JsonParser.parseReader(bookReader).getAsJsonObject(); + + + + NbtCompoundTag test123 = new NbtCompoundTag(); + test123.put("title", new NbtStringTag(bookObject.get("title").getAsString())); + test123.put("author", NbtStringTag.fromPrimitive(bookObject.get("author").getAsJsonPrimitive())); + NbtListTag pages = new NbtListTag(); + for(JsonElement element : bookObject.get("pages").getAsJsonArray()){ + pages.add(new NbtJsonTag(element)); + } + test123.put("pages", pages); + System.out.println("give @p minecraft:written_book" + test123.toNbtString()); + } + + public void testJSON(String filename) { + bookReader = new InputStreamReader(WrittenBookBuilderTest.class.getResourceAsStream(filename)); + JsonObject bookObject = JsonParser.parseReader(bookReader).getAsJsonObject(); + WrittenBookBuilder bb = WrittenBookBuilder.fromJson(bookObject); + System.out.println("Testing BookBuilder, please make sure the following NBT string is valid (It does not yet contain the title or author):"); + bb.addLoreLines(new NbtStringTag("The rules owo")); + System.out.println(bb.generateNBT()); + + System.out.println("Please paste the following line into a command block, and activate it"); + System.out.println("give @p minecraft:written_book" + bb.generateNBT()); + } +} diff --git a/src/test/java/com/dumbdogdiner/stickyapi/common/util/StringUtilTest.java b/src/test/java/com/dumbdogdiner/stickyapi/common/util/StringUtilTest.java index 2e01cb0b..bfb6db78 100644 --- a/src/test/java/com/dumbdogdiner/stickyapi/common/util/StringUtilTest.java +++ b/src/test/java/com/dumbdogdiner/stickyapi/common/util/StringUtilTest.java @@ -4,8 +4,11 @@ */ package com.dumbdogdiner.stickyapi.common.util; +import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; +import net.md_5.bungee.api.ChatColor; + import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -219,4 +222,17 @@ public void testHyphenateUUIDNull() { StringUtil.hyphenateUUID(null); }); } + + @RepeatedTest(100) + public void testRandomObfuscatedString() { + String rand = StringUtil.randomObfuscatedString(3,3, 0); + assertTrue(StringUtil.startsWith(rand, ChatColor.MAGIC.toString())); + System.out.println(rand); + assertEquals(3, ChatColor.stripColor(rand).length()); + + rand = StringUtil.randomObfuscatedString(1, 100, 0); + assertTrue(ChatColor.stripColor(rand).length() >= 1 && ChatColor.stripColor(rand).length() <= 100); + assertEquals(ChatColor.stripColor(rand).length(), rand.length() - 4); + System.out.println(rand); + } } diff --git a/src/test/java/com/dumbdogdiner/stickyapi/common/util/TextUtilTest.java b/src/test/java/com/dumbdogdiner/stickyapi/common/util/TextUtilTest.java new file mode 100644 index 00000000..09bd8dce --- /dev/null +++ b/src/test/java/com/dumbdogdiner/stickyapi/common/util/TextUtilTest.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2020-2021 DumbDogDiner . All rights reserved. + * Licensed under the MIT license, see LICENSE for more information... + */ +package com.dumbdogdiner.stickyapi.common.util; + +import com.google.common.io.CharStreams; +import org.junit.jupiter.api.Test; + +import java.text.MessageFormat; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +public class TextUtilTest { + @Test + public void testIsCharacterSupportedTrueBasicPlaneUtf16() { + assertTrue(TextUtil.isCharacterSupported((char) 0x0041)); + } + + @Test + public void testIsCharacterSupportedTrueBasicPlaneDecimal() { + assertTrue(TextUtil.isCharacterSupported((char) 65)); + } + + @Test + public void testIsCharacterSupportedTrueEmojiUtf16() { + // 0x1F5E1 | "Dagger Knife" Emoji 🗡️ + assertTrue(TextUtil.isCharacterSupported((char) 0x1F5E1)); + } + + @Test + public void testIsCharacterSupportedFalseEmojiUtf16() { + // 0x1F98A | "Fox Face" Emoji 🦊 + assertFalse(TextUtil.isCharacterSupported((char) 0x1F98A)); + } + + @Test + public void testAllSupportedCharsAreSupported() { + for(char c : TextUtil.getSupportedCharacters()){ + assertTrue(TextUtil.isCharacterSupported(c)); + } + } + + @Test + public void testUnsupportedCharsNotSupported() { + Set unsupported = IntStream.range(Character.MIN_VALUE, Character.MAX_VALUE) + .boxed() + .map(i -> (char) (int) i) + .collect(Collectors.toSet()); + unsupported.removeAll(TextUtil.getSupportedCharacters()); + for (Character c : unsupported) { + try { + int x = TextUtil.getCharacterWidth(c); + fail(MessageFormat.format("Should have failed, instead got a width of {0} for char {1}", x, c)); + } catch (IllegalArgumentException e) { + // expected + assertFalse(TextUtil.isCharacterSupported(c)); + } + } + } +} diff --git a/src/test/resources/book1.json b/src/test/resources/book1.json new file mode 100644 index 00000000..68e28d2f --- /dev/null +++ b/src/test/resources/book1.json @@ -0,0 +1,25 @@ +{ + "pages": [ + [ + { + "text": "Page 1\\nI'm text in " + }, + { + "text": "bold", + "bold": true + }, + { + "text": " owo", + "color": "reset" + } + ], + [ + { + "text": "red text in page 2", + "color": "dark_red" + } + ] + ], + "title": "a booky book", + "author": "rodwuffy" +} diff --git a/src/test/resources/rulebook.json b/src/test/resources/rulebook.json new file mode 100644 index 00000000..2dbc04f8 --- /dev/null +++ b/src/test/resources/rulebook.json @@ -0,0 +1,458 @@ +{ + "pages": [ + [ + { + "text": "Dumb Dog Diner MC\\nSurvival Handbook", + "bold": true, + "italic": true, + "color": "dark_purple" + }, + { + "text": "\\n\\n", + "color": "reset" + }, + { + "text": "Table of Contents:", + "bold": true, + "italic": true, + "underlined": true, + "color": "dark_red" + }, + { + "text": "\\n", + "color": "reset" + }, + { + "text": "Page 2: ", + "color": "red" + }, + { + "text": "Rules\\n", + "color": "reset" + }, + { + "text": "Pg. 3: ", + "color": "red" + }, + { + "text": "Gameplay\\n", + "color": "reset" + }, + { + "text": "Pg. 5: ", + "color": "red" + }, + { + "text": "Trading\\n", + "color": "reset" + }, + { + "text": "Pg. 7: ", + "color": "red" + }, + { + "text": "Commands\\n", + "color": "reset" + }, + { + "text": "Pg. 9: ", + "color": "red" + }, + { + "text": "Trading\\nCommands\\n", + "color": "reset" + }, + { + "text": "Pg. 11: ", + "color": "red" + }, + { + "text": "Extras\\n", + "color": "reset" + }, + { + "text": "Pg. 12: ", + "color": "red" + }, + { + "text": "Getting Help\\n", + "color": "reset" + }, + { + "text": "Pg. 14: ", + "color": "red" + }, + { + "text": "Credits", + "color": "reset" + } + ], + [ + { + "text": "The Rules:", + "bold": true, + "italic": true, + "underlined": true, + "color": "red" + }, + { + "text": "\\n1. Absolutely no\\ngriefing, theft, or\\nintentional distress!\\n2. No cheating/hacking\\nallowed. No exploits.\\n3. No floating trees.\\n4. No fully-automatic\\nredstone farms.\\n5. No harassment,\\nbullying, or abuse.\\n6. No AFK fishing.", + "color": "reset" + } + ], + [ + { + "text": "Gameplay:", + "bold": true, + "italic": true, + "underlined": true, + "color": "dark_green" + }, + { + "text": "\\nThe world border for all worlds is 8,000\\nblocks from the\\ncenter. Because of\\nnow ", + "color": "reset" + }, + { + "text": "Nether Portal", + "color": "dark_red" + }, + { + "text": "\\ntravel works, you will\\nnot be able to light\\nthem past 1,000 blocks\\nfrom the center.\\nYou may build wherever you like! Try ", + "color": "reset" + }, + { + "text": "/rtp", + "color": "blue" + }, + { + "text": " to help find a spot!", + "color": "reset" + } + ], + [ + { + "text": "This server uses a " + }, + { + "text": "Death Chest ", + "color": "red" + }, + { + "text": "system. Upon death, a campfire with your items will spawn at your death location. It will last\\nfor 10 minutes before\\nyour items drop on \\nthe ground!\\n\\nYou can have up to 3\\n", + "color": "reset" + }, + { + "text": "Death Chests", + "color": "red" + }, + { + "text": " at a time.", + "color": "reset" + } + ], + [ + { + "text": "Trading:", + "bold": true, + "italic": true, + "underlined": true, + "color": "dark_aqua" + }, + { + "text": "\\nWe use chest chops\\nfor player to trade\\ngood for ", + "color": "reset" + }, + { + "text": "Coins", + "color": "gold" + }, + { + "text": ".\\n\\nCoins may be earned through selling goods\\nto other players,\\npaying/receiving\\nmoney for a service,\\nor by selling goods to\\nthe server itself.", + "color": "reset" + } + ], + { + "text": "Chest shops can buy\\nor sell items!\\n\\nTo make a shop, place a chest and\\nshift-click with the\\nitem you want to sell\\nor buy.\\n\\nRegular users may have up to 6 shops\\nat a time." + }, + [ + { + "text": "Commands:", + "bold": true, + "italic": true, + "underlined": true, + "color": "red" + }, + { + "text": "\\nTab-complete is set\\nup to only show commands you have access to. Here are some useful ones:\\n\\n", + "color": "reset" + }, + { + "text": "/helpmenu ", + "color": "blue" + }, + { + "text": "- Opens the\\nhelp menu\\n", + "color": "reset" + }, + { + "text": "/servers ", + "color": "blue" + }, + { + "text": "- Opens the\\nservers menu\\n", + "color": "reset" + }, + { + "text": "/rules", + "color": "blue" + }, + { + "text": " - Grants this\\nbook", + "color": "reset" + } + ], + [ + { + "text": "/options ", + "color": "blue" + }, + { + "text": "- Opens the player options menu\\n", + "color": "reset" + }, + { + "text": "/tpa ", + "color": "blue" + }, + { + "text": "- Sends a\\nteleport request to another player\\n", + "color": "reset" + }, + { + "text": "/rtp ", + "color": "blue" + }, + { + "text": "- Start a\\nrandom teleport\\n", + "color": "reset" + }, + { + "text": "/pw ", + "color": "blue" + }, + { + "text": "- Opens the playerwarps personal\\nhome menu\\n", + "color": "reset" + }, + { + "text": "/helpme ", + "color": "blue" + }, + { + "text": "- Pings an online staff member\\nfor assistance", + "color": "reset" + } + ], + [ + { + "text": "Trading Commands:", + "bold": true, + "italic": true, + "underlined": true, + "color": "dark_purple" + }, + { + "text": "\\n", + "color": "reset" + }, + { + "text": "/bal ", + "color": "blue" + }, + { + "text": "- Check your\\nbalance\\n", + "color": "reset" + }, + { + "text": "/pay ", + "color": "blue" + }, + { + "text": "- Pay another player\\n", + "color": "reset" + }, + { + "text": "/sell ", + "color": "blue" + }, + { + "text": "- Sell the item in your hand to the server\\n", + "color": "reset" + }, + { + "text": "/worth ", + "color": "blue" + }, + { + "text": "- Shows the\\nitem price when selling\\nto the server", + "color": "reset" + } + ], + [ + { + "text": "/value", + "color": "blue" + }, + { + "text": " - Shows the\\naverage selling price\\nfor an item in\\nplayer shops", + "color": "reset" + } + ], + [ + { + "text": "Extras:", + "bold": true, + "italic": true, + "underlined": true, + "color": "dark_aqua" + }, + { + "text": "\\n", + "color": "reset" + }, + { + "text": "/donate, /vip ", + "color": "blue" + }, + { + "text": "- Gives link to the server Patreon page\\n", + "color": "reset" + }, + { + "text": "/twitter, /twitch, /discord, /merch ", + "color": "blue" + }, + { + "text": "\\n- Gives various social media links\\n", + "color": "reset" + }, + { + "text": "/afk ", + "color": "blue" + }, + { + "text": "- Set your status to AFK\\n", + "color": "reset" + }, + { + "text": "/tamer tool ", + "color": "blue" + }, + { + "text": "- Gives\\ntool to protect pets\\n ", + "color": "reset" + } + ], + [ + { + "text": "Getting Help:", + "bold": true, + "italic": true, + "underlined": true, + "color": "light_purple" + }, + { + "text": "\\nIf you ever need help\\ngetting your way\\naround the server,\\nplease ask any staff!\\n", + "color": "reset" + }, + { + "text": "/helpme", + "color": "blue" + }, + { + "text": "\\n\\nPlease join the\\nDiscord server to get\\ntechnical help!\\n", + "color": "reset" + }, + { + "text": "/discord", + "color": "blue" + } + ], + [ + { + "text": "Please report any\\nbugs, griefing, etc.\\nthat you come across\\nor are a victim of!", + "bold": true, + "underlined": true + }, + { + "text": "\\n\\nUse ", + "color": "reset" + }, + { + "text": "/report \u003cplayer\u003e", + "color": "blue" + }, + { + "text": "\\nto report another\\nplayer to staff!\\n\\nThe help menu has shortcuts that make getting help easy!\\n", + "color": "reset" + }, + { + "text": "/help", + "color": "blue" + } + ], + [ + { + "text": "Credits:", + "bold": true, + "italic": true, + "underlined": true, + "color": "gold" + }, + { + "text": "\\n", + "color": "reset" + }, + { + "text": "dddMC ", + "bold": true, + "color": "dark_purple" + }, + { + "text": "is fully funded\\nthrough player donations. Please consider helping support us on Patreon!\\n", + "color": "reset" + }, + { + "text": "/donate /patreon", + "color": "blue" + }, + { + "text": "\\n\\nPatrons will receive a\\nVIP rank. More ranks\\nwill be added in the\\nfuture.", + "color": "reset" + } + ], + [ + { + "text": "We hope you enjoy\\nyour time playing\\non the server!\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n- Stixil, " + }, + { + "text": "dddMC 2020", + "bold": true, + "color": "dark_purple" + } + ] + ], + "title": "\u0026ddddMC Survival Handbook v1.0", + "author": "Stixil", + "lore" : [ + { + "text": "dddMC 2020", + "bold": true, + "color": "dark_purple" + }, + { + "text" : "", + "color": "reset" + } + ] +} \ No newline at end of file diff --git a/src/test/resources/rulebook.md b/src/test/resources/rulebook.md new file mode 100644 index 00000000..16354a8b --- /dev/null +++ b/src/test/resources/rulebook.md @@ -0,0 +1,97 @@ +# Dumb Dog Diner MC Survival Handbook + +## $c The Rules$ + +1. Absolutely no griefing, theft, or intentional distress! +2. No cheating/hacking allowed. Optifine and similar are okay. +3. No floating trees. +4. No fully-automatic redstone farms. +5. No harassment, bullying, or abuse. +6. No AFK fishing. + +--- + +## $2 Gameplay$ + +The world border for all worlds is 8,000 blocks from the center. Because of how +$dark_red Nether Portal$ travel works, you will not be able to light them past 1,000 blocks +from the center. You may build wherever you like! Try /rtp to help find a spot! + +This server uses a $red Death Chest$ system. Upon death, a campfire with your items +will spawn at your death location. It will last for 10 minutes before your items +drop on the ground! You can have up to 3 $red Death Chests$ at a time. + +--- + +## $3 Trading$ + +We use chest chops for player to trade good for $gold Coins$. Coins may be +earned through selling goods to other players, paying/receiving money for a +service, or by selling goods to the server itself. + +Chest shops can buy or sell items! To make a shop, place a chest and shift-click +with the item you want to sell or buy. Regular users may have up to 6 shops at a +time. + +--- + +## $red Commands$ + +Tab-complete is set up to only show commands you have access to. Here are some +useful ones: + +- $blue /help$ - Opens the help menu +- $blue /servers$ - Opens the server menu +- $blue /rules$ - Grants this book +- $blue /options$ - Opens the player options menu +- $blue /tpa$ - Sends a teleport request to another player +- $blue /rtp$ - Start a random teleport +- $blue /pw$ - Opens the playerwarps personal home menu +- $blue /helpme$ - Pings an online staff member for assistance + +--- + +## $dark_purple Trading Commands$ + +- $blue /bal$ - Check your balance +- $blue /pay$ - Pay another player +- $blue /sell$ - Sell the item in your hand to the server +- $blue /worth$ - Shows the item price when selling to the server +- $blue /value$ - Shows the average selling price for an item in player shops + +--- + +## $dark_aqua Extra Commands$ + +- $blue /donate, /vip$ - Gives link to the server Patreon page +- $blue /discord$ - Gives link to the server Discord +- $blue /twitter, /twitch$ - Gives social media links +- $blue /merch$ - Gives merch link +- $blue /afk$ - Set your status to AFK + +--- + +## $light_purple Getting Help$ + +If you ever need help getting your way around the server, please ask any staff! +**$blue /helpme$** + +Please join the Discord server to get technical help! **$blue /discord$** + +Please report any bugs, griefing, etc. that you come across or are a victim of. +A staff member will be happy to assist you. The help menu has shortcuts that +make getting help easy! **$blue /help$** + +--- + +## $gold Credits$ + +$dark_purple **dddMC**$ is fully funded through player donations. Please consider helping +support us on Patreon! $blue **/donate /patreon**$ + +Patrons will receive a VIP rank. More +ranks will be added in the future. + +We hope you enjoy your time playing on the server! + +\- Stixil, $dark_purple **dddMC 2020**$