├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main ├── java │ └── io │ │ └── wispforest │ │ └── lavender │ │ ├── Lavender.java │ │ ├── LavenderClientRecipeCache.java │ │ ├── LavenderCommands.java │ │ ├── book │ │ ├── Book.java │ │ ├── BookContentLoader.java │ │ ├── BookLoader.java │ │ ├── Category.java │ │ ├── ClientNewEntriesUnlockedCallback.java │ │ ├── Entry.java │ │ ├── LavenderBookItem.java │ │ ├── LavenderClientStorage.java │ │ └── StructureComponent.java │ │ ├── client │ │ ├── AlphanumComparator.java │ │ ├── AssociatedEntryTooltipComponent.java │ │ ├── BasicVertexConsumerProvider.java │ │ ├── BlitAlphaProgram.java │ │ ├── BlitCutoutProgram.java │ │ ├── BookBakedModel.java │ │ ├── LavenderBookScreen.java │ │ ├── LavenderClient.java │ │ ├── NewEntriesToast.java │ │ ├── OffhandBookRenderer.java │ │ ├── StructureOverlayRenderer.java │ │ ├── UnbakedBookModel.java │ │ └── UnreadNotificationComponent.java │ │ ├── md │ │ ├── ItemListComponent.java │ │ ├── compiler │ │ │ └── BookCompiler.java │ │ └── features │ │ │ ├── AlternativesExtension.java.disabled │ │ │ ├── ItemTagFeature.java │ │ │ ├── OwoUIModelFeature.java │ │ │ ├── PageBreakFeature.java │ │ │ ├── RecipeFeature.java │ │ │ └── StructureFeature.java │ │ ├── mixin │ │ ├── ClientAdvancementManagerMixin.java │ │ ├── CreativeInventoryScreenMixin.java │ │ ├── DrawContextMixin.java │ │ ├── FramebufferMixin.java │ │ ├── HeldItemRendererMixin.java │ │ ├── LifecycledResourceManagerMixin.java │ │ ├── MinecraftClientMixin.java │ │ ├── MouseMixin.java │ │ ├── RenderPhaseMixin.java │ │ ├── ScreenMixin.java │ │ ├── SimpleResourceReloadMixin.java │ │ ├── TextureUtilMixin.java │ │ └── access │ │ │ ├── ClientAdvancementManagerAccessor.java │ │ │ └── RegistryOpsAccessor.java │ │ ├── pond │ │ ├── LavenderFramebufferExtension.java │ │ └── LavenderLifecycledResourceManagerExtension.java │ │ └── structure │ │ ├── BlockStatePredicate.java │ │ ├── LavenderStructures.java │ │ └── StructureTemplate.java └── resources │ ├── assets │ └── lavender │ │ ├── icon.png │ │ ├── items │ │ └── dynamic_book.json │ │ ├── lang │ │ ├── de_de.json │ │ └── en_us.json │ │ ├── models │ │ └── item │ │ │ ├── brown_book.json │ │ │ └── red_book.json │ │ ├── owo_ui │ │ ├── book.xml │ │ └── book_components.xml │ │ ├── shaders │ │ └── core │ │ │ ├── blit_alpha.fsh │ │ │ ├── blit_alpha.json │ │ │ ├── blit_cutout.fsh │ │ │ └── blit_cutout.json │ │ ├── sounds.json │ │ ├── sounds │ │ └── item │ │ │ ├── book_open_1.ogg │ │ │ ├── book_open_2.ogg │ │ │ └── book_open_3.ogg │ │ └── textures │ │ ├── gui │ │ ├── brown_book.png │ │ ├── purple_book.png │ │ ├── red_book.png │ │ ├── sprites │ │ │ └── new_entries_toast.png │ │ └── structure_overlay_bars.png │ │ └── item │ │ ├── brown_book.png │ │ └── red_book.png │ ├── fabric.mod.json │ ├── lavender.accesswidener │ └── lavender.mixins.json └── testmod ├── java └── io │ └── wispforest │ └── lavenderflower │ └── LavenderFlower.java └── resources ├── assets ├── lavender-bud │ ├── lavender │ │ ├── books │ │ │ └── book_from_another_world.json │ │ └── entries │ │ │ └── book_from_another_world │ │ │ ├── de_de │ │ │ └── entry_from_another_world.md │ │ │ └── entry_from_another_world.md │ ├── models │ │ └── item │ │ │ └── wispen_testament.json │ └── textures │ │ └── item │ │ └── wispen_testament.png ├── lavender-flower │ ├── items │ │ ├── marketing_book.json │ │ └── the_book.json │ ├── lang │ │ ├── de_de.json │ │ └── en_us.json │ ├── lavender │ │ ├── books │ │ │ ├── marketing.json │ │ │ ├── more_book.json │ │ │ └── the_book.json │ │ ├── categories │ │ │ ├── marketing │ │ │ │ ├── category_1.md │ │ │ │ ├── category_2.md │ │ │ │ ├── category_3.md │ │ │ │ ├── category_4.md │ │ │ │ ├── category_5.md │ │ │ │ ├── category_6.md │ │ │ │ └── category_7.md │ │ │ ├── more_book │ │ │ │ ├── de_de │ │ │ │ │ └── epic_category.md │ │ │ │ └── epic_category.md │ │ │ └── the_book │ │ │ │ ├── a_category.md │ │ │ │ ├── another_category.md │ │ │ │ ├── b_category.md │ │ │ │ └── de_de │ │ │ │ └── a_category.md │ │ ├── entries │ │ │ ├── marketing │ │ │ │ ├── entry_1.md │ │ │ │ ├── entry_2.md │ │ │ │ ├── entry_3.md │ │ │ │ ├── entry_33.md │ │ │ │ ├── entry_4.md │ │ │ │ ├── entry_44.md │ │ │ │ ├── entry_5.md │ │ │ │ ├── entry_55.md │ │ │ │ ├── entry_6.md │ │ │ │ ├── entry_66.md │ │ │ │ ├── landing_page.md │ │ │ │ └── lone_entry.md │ │ │ ├── more_book │ │ │ │ └── page.md │ │ │ └── the_book │ │ │ │ ├── de_de │ │ │ │ └── landing_page.md │ │ │ │ ├── landing_page.md │ │ │ │ ├── profound_page.md │ │ │ │ └── profounder_pagee.md │ │ └── structures │ │ │ ├── iron_golem.json │ │ │ ├── nether_portal.json │ │ │ └── well.json │ └── owo_ui │ │ └── ritual_basics.xml └── mymod │ └── lavender │ └── categories │ └── my_book │ └── a_category.md └── fabric.mod.json /.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 | 120 | # generated sources 121 | /src/*/generated/ 122 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![The landing page of a Lavender book](https://cdn.modrinth.com/data/D5h9NKNI/images/ed7daa42a82c61305de1d4ba5e19c7627adaad01.png) 2 | 3 | Lavender is tool allowing modders, modpack makers and everybody else to create player guides and other in-game documentation in the form of easy-to-read, intuitive guide books. 4 | 5 | Everything is written using easy to learn, familiar and, most importantly, readable markdown with a bunch of [special syntax](https://docs.wispforest.io/lavender/markdown-syntax/) to seamlessly integrate with the game. This is augmented with support for powerful macros and the entirety of the [owo-ui framework](https://docs.wispforest.io/owo/ui/) at your fingertips 6 | 7 | Further, for the first time in any guidebook mod (we believe), all books can be viewed in-game when placed in the offhand - extremely useful for referencing an entry while carrying out its instructions 8 | 9 | ## Features 10 | - Fully data-driven book authoring with near-instant hot reloading 11 | - Optional grouping of entries into categories for easy discovery 12 | - Deep linking between entries and categories 13 | - Structure/Multiblock visualization and building assistance 14 | - Gradual content unlocking, backed by the game's advancement system 15 | - Built-in support for visualizing all vanilla recipe types and an easy framework for implementing custom ones 16 | - Localization support 17 | - Individual bookmarks for each save 18 | - Support for extending another mod's books, extremely useful for seamlessly integrating your addon's documentation 19 | 20 | ## Getting started 21 | To start making your own Lavender book right now, complete the [setup](https://docs.wispforest.io/lavender/setup/) and then follow [Getting Started](https://docs.wispforest.io/lavender/getting-started/) in the comprehensive documentation over at https://docs.wispforest.io. 22 | 23 | For reference, it might be helpful to check out Affinity's [Wispen Testament](https://github.com/wisp-forest/affinity/tree/main/src/main/resources/assets/affinity/lavender) as a solid example book made using Lavender 24 | 25 | ## Credits 26 | - [The Patchouli team](https://modrinth.com/mod/patchouli) for making the original mod that inspired Lavender 27 | - [chyzman](https://modrinth.com/user/chyzman) for inspiring a range of features, notably offhand viewing -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'fabric-loom' version '1.8-SNAPSHOT' 3 | id 'maven-publish' 4 | } 5 | 6 | version = "${project.mod_version}+${project.minecraft_base_version}" 7 | group = project.maven_group 8 | 9 | sourceSets { 10 | testmod { 11 | runtimeClasspath += main.runtimeClasspath 12 | compileClasspath += main.compileClasspath 13 | } 14 | } 15 | 16 | loom { 17 | accessWidenerPath = project.file("src/main/resources/lavender.accesswidener") 18 | 19 | runs { 20 | testmodClient { 21 | client() 22 | ideConfigGenerated true 23 | name "Testmod Client" 24 | source sourceSets.testmod 25 | } 26 | 27 | testmodServer { 28 | server() 29 | ideConfigGenerated true 30 | name "Testmod Server" 31 | source sourceSets.testmod 32 | } 33 | } 34 | } 35 | 36 | repositories { 37 | maven { url "https://maven.wispforest.io/releases/" } 38 | 39 | // rei 40 | maven { url "https://maven.shedaniel.me/" } 41 | 42 | // modmenu 43 | maven { url "https://maven.terraformersmc.com/releases/" } 44 | } 45 | 46 | dependencies { 47 | // To change the versions see the gradle.properties file 48 | minecraft "com.mojang:minecraft:${project.minecraft_version}" 49 | mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" 50 | modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" 51 | 52 | // Fabric API. This is technically optional, but you probably want it anyway. 53 | modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" 54 | 55 | modImplementation "io.wispforest:owo-lib:${project.owo_version}" 56 | include "io.wispforest:owo-sentinel:${project.owo_version}" 57 | 58 | // modLocalRuntime "me.shedaniel:RoughlyEnoughItems-fabric:${project.rei_version}" 59 | modCompileOnly "me.shedaniel:RoughlyEnoughItems-api-fabric:${project.rei_version}" 60 | 61 | include modApi("io.wispforest.lavender-md:core:${project.lavender_md_version}") 62 | include modApi("io.wispforest.lavender-md:owo-ui:${project.lavender_md_version}") 63 | 64 | modLocalRuntime "com.terraformersmc:modmenu:${project.modmenu_version}" 65 | 66 | // --- testmod --- 67 | 68 | testmodImplementation sourceSets.main.output 69 | } 70 | 71 | base { 72 | archivesName = project.archives_base_name 73 | } 74 | 75 | processResources { 76 | inputs.property "version", project.version 77 | 78 | filesMatching("fabric.mod.json") { 79 | expand "version": project.version 80 | } 81 | } 82 | 83 | //tasks.withType(JavaCompile).configureEach { 84 | // // Minecraft 1.18 (1.18-pre2) upwards uses Java 17. 85 | // it.options.release = 17 86 | //} 87 | 88 | java { 89 | // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task 90 | // if it is present. 91 | // If you remove this line, sources will not be generated. 92 | withSourcesJar() 93 | 94 | sourceCompatibility = JavaVersion.VERSION_21 95 | targetCompatibility = JavaVersion.VERSION_21 96 | } 97 | 98 | jar { 99 | from("LICENSE") { 100 | rename { "${it}_${base.archivesName.get()}"} 101 | } 102 | } 103 | 104 | def ENV = System.getenv() 105 | publishing { 106 | publications { 107 | mavenJava(MavenPublication) { 108 | from components.java 109 | } 110 | } 111 | 112 | repositories { 113 | maven { 114 | url ENV.MAVEN_URL 115 | credentials { 116 | username ENV.MAVEN_USER 117 | password ENV.MAVEN_PASSWORD 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Done to increase the memory available to gradle. 2 | org.gradle.jvmargs=-Xmx1G 3 | 4 | # Fabric Properties 5 | # check these on https://fabricmc.net/develop 6 | minecraft_base_version=1.21.4 7 | minecraft_version=1.21.4 8 | yarn_mappings=1.21.4+build.2 9 | loader_version=0.16.9 10 | 11 | # Mod Properties 12 | mod_version = 0.1.15 13 | maven_group = io.wispforest 14 | archives_base_name = lavender 15 | 16 | # Dependencies 17 | # check this on https://fabricmc.net/develop 18 | fabric_version=0.112.0+1.21.4 19 | 20 | # https://maven.shedaniel.me/me/shedaniel/RoughlyEnoughItems-fabric/ 21 | rei_version=18.0.796 22 | 23 | # https://maven.wispforest.io/#/releases/io/wispforest/owo-lib/ 24 | owo_version=0.12.20+1.21.4 25 | 26 | # https://maven.terraformersmc.com/releases/com/terraformersmc/modmenu 27 | modmenu_version=13.0.0-beta.1 28 | 29 | # https://maven.wispforest.io/#/releases/io/wispforest/lavender-md/core/ 30 | lavender_md_version=0.1.2+1.21.2 31 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisp-forest/lavender/96ff335fe8194aa36e1b75c0eb039c7e69fc081c/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/io/wispforest/lavender/Lavender.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender; 2 | 3 | import com.mojang.logging.LogUtils; 4 | import io.wispforest.endec.Endec; 5 | import io.wispforest.endec.impl.BuiltInEndecs; 6 | import io.wispforest.endec.impl.StructEndecBuilder; 7 | import io.wispforest.lavender.book.LavenderBookItem; 8 | import io.wispforest.owo.network.OwoNetChannel; 9 | import io.wispforest.owo.serialization.CodecUtils; 10 | import net.fabricmc.api.ModInitializer; 11 | import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; 12 | import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; 13 | import net.minecraft.datafixer.DataFixTypes; 14 | import net.minecraft.nbt.NbtCompound; 15 | import net.minecraft.nbt.NbtElement; 16 | import net.minecraft.network.packet.CustomPayload; 17 | import net.minecraft.registry.Registries; 18 | import net.minecraft.registry.Registry; 19 | import net.minecraft.registry.RegistryWrapper; 20 | import net.minecraft.sound.SoundEvent; 21 | import net.minecraft.util.Identifier; 22 | import net.minecraft.world.PersistentState; 23 | import org.slf4j.Logger; 24 | 25 | import java.util.UUID; 26 | 27 | public class Lavender implements ModInitializer { 28 | 29 | public static final Logger LOGGER = LogUtils.getLogger(); 30 | public static final String MOD_ID = "lavender"; 31 | public static final SoundEvent ITEM_BOOK_OPEN = SoundEvent.of(id("item.book.open")); 32 | 33 | public static final OwoNetChannel CHANNEL = OwoNetChannel.create(Lavender.id("main")); 34 | 35 | @Override 36 | public void onInitialize() { 37 | Registry.register(Registries.ITEM, id("dynamic_book"), LavenderBookItem.DYNAMIC_BOOK); 38 | Registry.register(Registries.SOUND_EVENT, ITEM_BOOK_OPEN.id(), ITEM_BOOK_OPEN); 39 | 40 | PayloadTypeRegistry.playS2C().register(WorldUUIDPayload.ID, CodecUtils.toPacketCodec(WorldUUIDPayload.ENDEC)); 41 | 42 | ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { 43 | sender.sendPacket(new WorldUUIDPayload(server.getOverworld().getPersistentStateManager().getOrCreate(WorldUUIDState.TYPE, "lavender_world_id").id)); 44 | }); 45 | 46 | LavenderClientRecipeCache.initialize(); 47 | } 48 | 49 | public static Identifier id(String path) { 50 | return Identifier.of(MOD_ID, path); 51 | } 52 | 53 | public static class WorldUUIDState extends PersistentState { 54 | 55 | public static final PersistentState.Type TYPE = new Type<>(() -> { 56 | var state = new WorldUUIDState(UUID.randomUUID()); 57 | state.markDirty(); 58 | return state; 59 | }, WorldUUIDState::read, DataFixTypes.LEVEL); 60 | 61 | public final UUID id; 62 | 63 | private WorldUUIDState(UUID id) { 64 | this.id = id; 65 | } 66 | 67 | @Override 68 | public NbtCompound writeNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) { 69 | nbt.putUuid("UUID", id); 70 | return nbt; 71 | } 72 | 73 | public static WorldUUIDState read(NbtCompound nbt, RegistryWrapper.WrapperLookup lookup) { 74 | return new WorldUUIDState(nbt.contains("UUID", NbtElement.INT_ARRAY_TYPE) ? nbt.getUuid("UUID") : null); 75 | } 76 | } 77 | 78 | public record WorldUUIDPayload(UUID worldUuid) implements CustomPayload { 79 | public static final CustomPayload.Id ID = new CustomPayload.Id<>(Lavender.id("world_uuid")); 80 | public static final Endec ENDEC = StructEndecBuilder.of( 81 | BuiltInEndecs.UUID.fieldOf("world_uuid", WorldUUIDPayload::worldUuid), 82 | WorldUUIDPayload::new 83 | ); 84 | 85 | @Override 86 | public Id getId() { 87 | return ID; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/LavenderClientRecipeCache.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender; 2 | 3 | import io.wispforest.endec.StructEndec; 4 | import io.wispforest.endec.impl.StructEndecBuilder; 5 | import io.wispforest.lavender.client.LavenderBookScreen; 6 | import io.wispforest.owo.serialization.CodecUtils; 7 | import io.wispforest.owo.serialization.endec.MinecraftEndecs; 8 | import it.unimi.dsi.fastutil.objects.Reference2LongMap; 9 | import it.unimi.dsi.fastutil.objects.Reference2LongOpenHashMap; 10 | import net.fabricmc.api.EnvType; 11 | import net.fabricmc.api.Environment; 12 | import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; 13 | import net.minecraft.client.MinecraftClient; 14 | import net.minecraft.recipe.Recipe; 15 | import net.minecraft.recipe.RecipeEntry; 16 | import net.minecraft.registry.RegistryKey; 17 | import net.minecraft.registry.RegistryKeys; 18 | import net.minecraft.util.Identifier; 19 | 20 | import java.util.HashMap; 21 | import java.util.Map; 22 | import java.util.Optional; 23 | 24 | public class LavenderClientRecipeCache { 25 | 26 | private static final Map> RECIPE_CACHE = new HashMap<>(); 27 | private static final Reference2LongMap LAST_FETCHED_TIMESTAMP = new Reference2LongOpenHashMap<>(); 28 | 29 | public static Optional> getOrFetchRecipe(Identifier recipeId) { 30 | if (RECIPE_CACHE.containsKey(recipeId)) return Optional.of(RECIPE_CACHE.get(recipeId)); 31 | 32 | if (System.currentTimeMillis() - LAST_FETCHED_TIMESTAMP.getOrDefault(recipeId, 0) < 5_000) { 33 | return Optional.empty(); 34 | } 35 | 36 | LAST_FETCHED_TIMESTAMP.put(recipeId, System.currentTimeMillis()); 37 | Lavender.CHANNEL.clientHandle().send(new RequestRecipePacket(recipeId)); 38 | 39 | return Optional.empty(); 40 | } 41 | 42 | // --- 43 | 44 | public static void initialize() { 45 | Lavender.CHANNEL.registerServerbound(RequestRecipePacket.class, (packet, serverAccess) -> { 46 | var recipeEntry = serverAccess.runtime().getRecipeManager().get(RegistryKey.of(RegistryKeys.RECIPE, packet.recipeId)); 47 | if (recipeEntry.isEmpty()) return; 48 | 49 | var recipe = recipeEntry.get().value(); 50 | Lavender.CHANNEL.serverHandle(serverAccess.player()).send(new RecipePayloadPacket(packet.recipeId, recipe)); 51 | }); 52 | 53 | ServerLifecycleEvents.SYNC_DATA_PACK_CONTENTS.register((player, b) -> { 54 | Lavender.CHANNEL.serverHandle(player).send(new ClearRecipeCachePacket()); 55 | }); 56 | 57 | Lavender.CHANNEL.registerClientboundDeferred(RecipePayloadPacket.class, RecipePayloadPacket.ENDEC); 58 | Lavender.CHANNEL.registerClientboundDeferred(ClearRecipeCachePacket.class); 59 | } 60 | 61 | @Environment(EnvType.CLIENT) 62 | public static void initializeClient() { 63 | Lavender.CHANNEL.registerClientbound(RecipePayloadPacket.class, RecipePayloadPacket.ENDEC, (payload, clientAccess) -> handleRecipePayload(payload)); 64 | Lavender.CHANNEL.registerClientbound(ClearRecipeCachePacket.class, (packet, clientAccess) -> handleClearCache()); 65 | } 66 | 67 | @Environment(EnvType.CLIENT) 68 | private static void handleRecipePayload(RecipePayloadPacket payload) { 69 | RECIPE_CACHE.put(payload.recipeId, new RecipeEntry<>(RegistryKey.of(RegistryKeys.RECIPE, payload.recipeId), payload.recipe)); 70 | 71 | if (MinecraftClient.getInstance().currentScreen instanceof LavenderBookScreen bookScreen) { 72 | bookScreen.rebuildContent(null); 73 | } 74 | } 75 | 76 | @Environment(EnvType.CLIENT) 77 | private static void handleClearCache() { 78 | RECIPE_CACHE.clear(); 79 | LAST_FETCHED_TIMESTAMP.clear(); 80 | } 81 | 82 | public record ClearRecipeCachePacket() {} 83 | 84 | public record RequestRecipePacket(Identifier recipeId) {} 85 | 86 | public record RecipePayloadPacket(Identifier recipeId, Recipe recipe) { 87 | public static final StructEndec ENDEC = StructEndecBuilder.of( 88 | MinecraftEndecs.IDENTIFIER.fieldOf("recipe_id", RecipePayloadPacket::recipeId), 89 | CodecUtils.toEndec(Recipe.CODEC).fieldOf("recipe", RecipePayloadPacket::recipe), 90 | RecipePayloadPacket::new 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/LavenderCommands.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender; 2 | 3 | import com.mojang.brigadier.CommandDispatcher; 4 | import com.mojang.brigadier.arguments.BoolArgumentType; 5 | import com.mojang.brigadier.context.CommandContext; 6 | import com.mojang.brigadier.exceptions.CommandSyntaxException; 7 | import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; 8 | import com.mojang.brigadier.suggestion.SuggestionProvider; 9 | import io.wispforest.lavender.book.Book; 10 | import io.wispforest.lavender.book.BookLoader; 11 | import io.wispforest.lavender.book.LavenderBookItem; 12 | import io.wispforest.lavender.client.StructureOverlayRenderer; 13 | import io.wispforest.lavender.structure.LavenderStructures; 14 | import net.fabricmc.api.EnvType; 15 | import net.fabricmc.api.Environment; 16 | import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; 17 | import net.minecraft.client.gui.screen.ChatScreen; 18 | import net.minecraft.command.CommandRegistryAccess; 19 | import net.minecraft.command.CommandSource; 20 | import net.minecraft.command.argument.IdentifierArgumentType; 21 | import net.minecraft.component.Component; 22 | import net.minecraft.nbt.NbtOps; 23 | import net.minecraft.registry.Registries; 24 | import net.minecraft.text.Text; 25 | import net.minecraft.util.Identifier; 26 | 27 | import java.util.stream.Collectors; 28 | import java.util.stream.Stream; 29 | 30 | import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument; 31 | import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; 32 | 33 | public class LavenderCommands { 34 | 35 | private static final SimpleCommandExceptionType NO_SUCH_BOOK = new SimpleCommandExceptionType(Text.literal("No such book is loaded")); 36 | 37 | private static final SuggestionProvider LOADED_BOOKS = (context, builder) -> { 38 | return CommandSource.suggestIdentifiers(BookLoader.loadedBooks().stream().map(Book::id), builder); 39 | }; 40 | 41 | @Environment(EnvType.CLIENT) 42 | public static class Client { 43 | 44 | private static final SimpleCommandExceptionType NO_SUCH_STRUCTURE = new SimpleCommandExceptionType(Text.literal("No such structure is loaded")); 45 | private static final SuggestionProvider STRUCTURE_INFO = (context, builder) -> 46 | CommandSource.suggestMatching(LavenderStructures.loadedStructures().stream().map(Identifier::toString), builder); 47 | 48 | private static int executeGetLavenderBook(CommandContext context, boolean forceDynamicBook) throws CommandSyntaxException { 49 | var book = BookLoader.get(context.getArgument("book_id", Identifier.class)); 50 | if (book == null) { 51 | throw NO_SUCH_BOOK.create(); 52 | } 53 | 54 | var stack = forceDynamicBook 55 | ? LavenderBookItem.createDynamic(book) 56 | : LavenderBookItem.itemOf(book); 57 | 58 | var command = "/give @s " + Registries.ITEM.getId(stack.getItem()); 59 | 60 | var ops = context.getSource().getWorld().getRegistryManager().getOps(NbtOps.INSTANCE); 61 | var components = stack.getComponentChanges().entrySet().stream().flatMap(entry -> { 62 | var componentType = entry.getKey(); 63 | var typeId = Registries.DATA_COMPONENT_TYPE.getId(componentType); 64 | if (typeId == null) return Stream.empty(); 65 | 66 | var componentOptional = entry.getValue(); 67 | if (componentOptional.isPresent()) { 68 | Component component = Component.of(componentType, componentOptional.get()); 69 | return component.encode(ops).result().stream().map(value -> typeId + "=" + value); 70 | } else { 71 | return Stream.of("!" + typeId); 72 | } 73 | }).collect(Collectors.joining(String.valueOf(','))); 74 | 75 | if (!components.isEmpty()) { 76 | command += "[" + components + "]"; 77 | } 78 | 79 | if (stack.getCount() > 1) { 80 | command += " " + stack.getCount(); 81 | } 82 | 83 | var jAvAsE = command; 84 | context.getSource().getClient().send(() -> { 85 | context.getSource().getClient().setScreen(new ChatScreen(jAvAsE)); 86 | }); 87 | 88 | return 0; 89 | } 90 | 91 | public static void register(CommandDispatcher dispatcher, CommandRegistryAccess access) { 92 | dispatcher.register(literal("get-lavender-book").requires(source -> source.hasPermissionLevel(2)) 93 | .then(argument("book_id", IdentifierArgumentType.identifier()).suggests(LOADED_BOOKS) 94 | .executes(context -> executeGetLavenderBook(context, false)) 95 | .then(argument("force_dynamic_book", BoolArgumentType.bool()) 96 | .executes(context -> executeGetLavenderBook(context, BoolArgumentType.getBool(context, "force_dynamic_book")))))); 97 | 98 | 99 | dispatcher.register(literal("structure-overlay") 100 | .then(literal("clear-all").executes(context -> { 101 | StructureOverlayRenderer.clearOverlays(); 102 | return 0; 103 | })) 104 | 105 | .then(literal("add") 106 | .then(argument("structure", IdentifierArgumentType.identifier()).suggests(STRUCTURE_INFO).executes(context -> { 107 | var structureId = context.getArgument("structure", Identifier.class); 108 | if (LavenderStructures.get(structureId) == null) throw NO_SUCH_STRUCTURE.create(); 109 | 110 | StructureOverlayRenderer.addPendingOverlay(structureId); 111 | return 0; 112 | })))); 113 | } 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/book/BookLoader.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.book; 2 | 3 | import com.google.gson.*; 4 | import com.google.gson.reflect.TypeToken; 5 | import com.mojang.serialization.JsonOps; 6 | import io.wispforest.lavender.Lavender; 7 | import io.wispforest.lavender.client.BookBakedModel; 8 | import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin; 9 | import net.minecraft.registry.Registries; 10 | import net.minecraft.resource.ResourceFinder; 11 | import net.minecraft.resource.ResourceManager; 12 | import net.minecraft.text.Text; 13 | import net.minecraft.text.TextCodecs; 14 | import net.minecraft.util.Identifier; 15 | import net.minecraft.util.JsonHelper; 16 | import org.jetbrains.annotations.Nullable; 17 | 18 | import java.io.IOException; 19 | import java.util.Collection; 20 | import java.util.Collections; 21 | import java.util.HashMap; 22 | import java.util.Map; 23 | 24 | public class BookLoader { 25 | 26 | private static final Gson GSON = new GsonBuilder().setLenient().disableHtmlEscaping().create(); 27 | private static final TypeToken> MACROS_TOKEN = new TypeToken<>() {}; 28 | private static final ResourceFinder BOOK_FINDER = ResourceFinder.json("lavender/books"); 29 | 30 | private static final Map LOADED_BOOKS = new HashMap<>(); 31 | private static final Map VISIBLE_BOOKS = new HashMap<>(); 32 | 33 | public static void initialize() { 34 | ModelLoadingPlugin.register(context -> { 35 | for (var book : VISIBLE_BOOKS.values()) { 36 | if (book.dynamicBookModel() == null) return; 37 | context.addModels(book.dynamicBookModel()); 38 | } 39 | }); 40 | } 41 | 42 | public static @Nullable Book get(Identifier bookId) { 43 | return LOADED_BOOKS.get(bookId); 44 | } 45 | 46 | public static Collection loadedBooks() { 47 | return Collections.unmodifiableCollection(VISIBLE_BOOKS.values()); 48 | } 49 | 50 | static Collection allBooks() { 51 | return Collections.unmodifiableCollection(LOADED_BOOKS.values()); 52 | } 53 | 54 | public static void reload(ResourceManager manager) { 55 | LOADED_BOOKS.clear(); 56 | BOOK_FINDER.findResources(manager).forEach((identifier, resource) -> { 57 | JsonElement jsonElement; 58 | try (var reader = resource.getReader()) { 59 | jsonElement = JsonHelper.deserialize(GSON, reader, JsonElement.class); 60 | } catch (IOException e) { 61 | Lavender.LOGGER.warn("Could not load book '{}'", identifier, e); 62 | return; 63 | } 64 | 65 | if (!jsonElement.isJsonObject()) return; 66 | var bookObject = jsonElement.getAsJsonObject(); 67 | var resourceId = BOOK_FINDER.toResourceId(identifier); 68 | 69 | var textureId = tryGetId(bookObject, "texture"); 70 | var extendId = tryGetId(bookObject, "extend"); 71 | var dynamicBookModelId = tryGetId(bookObject, "dynamic_book_model"); 72 | 73 | Text dynamicBookName = null; 74 | if (bookObject.has("dynamic_book_name")) { 75 | dynamicBookName = TextCodecs.CODEC.parse(JsonOps.INSTANCE, bookObject.get("dynamic_book_name")).getOrThrow(JsonParseException::new); 76 | } 77 | 78 | var openSoundId = tryGetId(bookObject, "open_sound"); 79 | var openSoundEvent = openSoundId != null ? Registries.SOUND_EVENT.get(openSoundId) : null; 80 | var flippingSoundId = tryGetId(bookObject, "flipping_sound"); 81 | var flippingSoundEvent = flippingSoundId != null ? Registries.SOUND_EVENT.get(flippingSoundId) : null; 82 | 83 | var introEntryId = tryGetId(bookObject, "intro_entry"); 84 | 85 | var displayCompletion = JsonHelper.getBoolean(bookObject, "display_completion", false); 86 | var displayUnreadEntryNotifications = JsonHelper.getBoolean(bookObject, "display_unread_entry_notifications", true); 87 | var macros = GSON.fromJson(JsonHelper.getObject(bookObject, "macros", new JsonObject()), MACROS_TOKEN); 88 | 89 | Book.ToastSettings newEntriesToast = null; 90 | if (bookObject.has("new_entries_toast")) { 91 | var toastObject = bookObject.getAsJsonObject("new_entries_toast"); 92 | 93 | Identifier backgroundSprite = null; 94 | if (toastObject.has("background_sprite")) { 95 | backgroundSprite = Identifier.of(JsonHelper.getString(toastObject, "background_sprite")); 96 | } 97 | 98 | newEntriesToast = new Book.ToastSettings( 99 | BookContentLoader.itemStackFromString(JsonHelper.getString(toastObject, "icon_stack")), 100 | TextCodecs.CODEC.parse(JsonOps.INSTANCE, toastObject.get("book_name")).getOrThrow(JsonParseException::new), 101 | backgroundSprite 102 | ); 103 | } 104 | 105 | var book = new Book( 106 | resourceId, 107 | extendId, 108 | textureId, 109 | dynamicBookModelId, 110 | dynamicBookName, 111 | openSoundEvent, 112 | flippingSoundEvent, 113 | introEntryId, 114 | displayUnreadEntryNotifications, 115 | displayCompletion, 116 | newEntriesToast, 117 | macros 118 | ); 119 | LOADED_BOOKS.put(resourceId, book); 120 | if (extendId == null) VISIBLE_BOOKS.put(resourceId, book); 121 | }); 122 | 123 | LOADED_BOOKS.values().removeIf(book -> { 124 | if (book.tryResolveExtension()) return false; 125 | 126 | Lavender.LOGGER.warn("Book '" + book.id() + "' (an extension) failed to load because its target was not found"); 127 | return true; 128 | }); 129 | } 130 | 131 | private static @Nullable Identifier tryGetId(JsonObject json, String key) { 132 | var jsonString = JsonHelper.getString(json, key, null); 133 | if (jsonString == null) return null; 134 | 135 | return Identifier.tryParse(jsonString); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/book/Category.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.book; 2 | 3 | import io.wispforest.owo.ui.core.Component; 4 | import io.wispforest.owo.ui.core.Sizing; 5 | import net.minecraft.util.Identifier; 6 | import org.jetbrains.annotations.Nullable; 7 | 8 | import java.util.function.Function; 9 | 10 | public record Category( 11 | Identifier id, 12 | @Nullable Identifier parent, 13 | String title, 14 | Function iconFactory, 15 | boolean secret, 16 | int ordinal, 17 | String content 18 | ) implements Book.BookmarkableElement {} 19 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/book/ClientNewEntriesUnlockedCallback.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.book; 2 | 3 | import net.fabricmc.fabric.api.event.Event; 4 | import net.fabricmc.fabric.api.event.EventFactory; 5 | import net.minecraft.client.MinecraftClient; 6 | 7 | @FunctionalInterface 8 | public interface ClientNewEntriesUnlockedCallback { 9 | 10 | Event EVENT = EventFactory.createArrayBacked(ClientNewEntriesUnlockedCallback.class, callbacks -> (client, book, newEntryCount) -> { 11 | for (var callback : callbacks) { 12 | callback.newEntriesUnlocked(client, book, newEntryCount); 13 | } 14 | }); 15 | 16 | void newEntriesUnlocked(MinecraftClient client, Book book, int newEntryCount); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/book/Entry.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.book; 2 | 3 | import com.google.common.collect.ImmutableSet; 4 | import io.wispforest.lavender.mixin.access.ClientAdvancementManagerAccessor; 5 | import io.wispforest.owo.ui.core.Component; 6 | import io.wispforest.owo.ui.core.Sizing; 7 | import net.minecraft.client.network.ClientPlayerEntity; 8 | import net.minecraft.item.ItemStack; 9 | import net.minecraft.util.Identifier; 10 | 11 | import java.util.List; 12 | import java.util.function.Function; 13 | 14 | public record Entry( 15 | Identifier id, 16 | List categories, 17 | String title, 18 | Function iconFactory, 19 | boolean secret, 20 | int ordinal, 21 | ImmutableSet requiredAdvancements, 22 | ImmutableSet associatedItems, 23 | ImmutableSet additionalSearchTerms, 24 | String content 25 | ) implements Book.BookmarkableElement { 26 | 27 | public boolean canPlayerView(ClientPlayerEntity player) { 28 | var advancementHandler = player.networkHandler.getAdvancementHandler(); 29 | 30 | for (var advancementId : this.requiredAdvancements) { 31 | var advancement = advancementHandler.getManager().get(advancementId); 32 | if (advancement == null) return false; 33 | 34 | var progress = ((ClientAdvancementManagerAccessor) advancementHandler).lavender$getAdvancementProgresses().get(advancement.getAdvancementEntry()); 35 | if (progress == null || !progress.isDone()) return false; 36 | } 37 | 38 | return true; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/book/LavenderBookItem.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.book; 2 | 3 | import com.google.common.base.Preconditions; 4 | import io.wispforest.lavender.Lavender; 5 | import io.wispforest.lavender.client.LavenderBookScreen; 6 | import io.wispforest.owo.ops.TextOps; 7 | import net.fabricmc.api.EnvType; 8 | import net.fabricmc.api.Environment; 9 | import net.minecraft.client.MinecraftClient; 10 | import net.minecraft.component.ComponentType; 11 | import net.minecraft.entity.player.PlayerEntity; 12 | import net.minecraft.item.Item; 13 | import net.minecraft.item.ItemStack; 14 | import net.minecraft.item.tooltip.TooltipType; 15 | import net.minecraft.registry.Registries; 16 | import net.minecraft.registry.Registry; 17 | import net.minecraft.registry.RegistryKey; 18 | import net.minecraft.registry.RegistryKeys; 19 | import net.minecraft.text.Text; 20 | import net.minecraft.util.ActionResult; 21 | import net.minecraft.util.Formatting; 22 | import net.minecraft.util.Hand; 23 | import net.minecraft.util.Identifier; 24 | import net.minecraft.world.World; 25 | import org.jetbrains.annotations.NotNull; 26 | import org.jetbrains.annotations.Nullable; 27 | 28 | import java.util.HashMap; 29 | import java.util.List; 30 | import java.util.Map; 31 | 32 | public class LavenderBookItem extends Item { 33 | 34 | public static final ComponentType BOOK_ID = Registry.register( 35 | Registries.DATA_COMPONENT_TYPE, 36 | Lavender.id("book_id"), 37 | ComponentType.builder() 38 | .codec(Identifier.CODEC) 39 | .packetCodec(Identifier.PACKET_CODEC) 40 | .build() 41 | ); 42 | 43 | public static final LavenderBookItem DYNAMIC_BOOK = new LavenderBookItem(null, new Settings().maxCount(1).registryKey(RegistryKey.of(RegistryKeys.ITEM, Lavender.id("dynamic_book")))); 44 | 45 | private static final Map BOOK_ITEMS = new HashMap<>(); 46 | 47 | private final @Nullable Identifier bookId; 48 | 49 | private LavenderBookItem(@Nullable Identifier bookId, Settings settings) { 50 | super(settings); 51 | this.bookId = bookId; 52 | } 53 | 54 | protected LavenderBookItem(Settings settings, @NotNull Identifier bookId) { 55 | super(settings); 56 | this.bookId = Preconditions.checkNotNull(bookId, "Book-specific book items must have a non-null book ID"); 57 | } 58 | 59 | @SuppressWarnings("DataFlowIssue") 60 | protected @NotNull Identifier bookId() { 61 | return this.bookId; 62 | } 63 | 64 | /** 65 | * Shorthand of {@link #registerForBook(Identifier, Identifier, net.minecraft.item.Item.Settings)} which 66 | * uses {@code bookId} as the item id 67 | */ 68 | public static LavenderBookItem registerForBook(@NotNull Identifier bookId, Settings settings) { 69 | return registerForBook(bookId, bookId, settings); 70 | } 71 | 72 | /** 73 | * Create, register and return a book item under {@code itemId} as the canonical 74 | * item for the book referred to by the given {@code bookId} 75 | */ 76 | public static LavenderBookItem registerForBook(@NotNull Identifier bookId, @NotNull Identifier itemId, Settings settings) { 77 | return registerForBook(Registry.register(Registries.ITEM, itemId, new LavenderBookItem(bookId, settings.registryKey(RegistryKey.of(RegistryKeys.ITEM, itemId))))); 78 | } 79 | 80 | /** 81 | * Register and return the given book item as the canonical item 82 | * for the book referred to by the item's bookId field 83 | */ 84 | public static LavenderBookItem registerForBook(LavenderBookItem item) { 85 | BOOK_ITEMS.put(item.bookId(), item); 86 | return item; 87 | } 88 | 89 | /** 90 | * @return The id of the book referred to by the given item stack 91 | * (either through static associating of NBT in the case the dynamic book), 92 | * or {@code null} if neither a static association nor NBT exist 93 | */ 94 | public static @Nullable Identifier bookIdOf(ItemStack bookStack) { 95 | if (!(bookStack.getItem() instanceof LavenderBookItem book)) return null; 96 | return book.bookId != null ? book.bookId : bookStack.get(BOOK_ID); 97 | } 98 | 99 | /** 100 | * Convenience variant of {@link #bookIdOf(ItemStack)} which attempts 101 | * looking up the book referred to by the id said method returns 102 | */ 103 | public static @Nullable Book bookOf(ItemStack bookStack) { 104 | var bookId = bookIdOf(bookStack); 105 | if (bookId == null) return null; 106 | 107 | return BookLoader.get(bookId); 108 | } 109 | 110 | /** 111 | * @return An item stack representing the given book. If a canonical item 112 | * was registered, it is used - otherwise a dynamic book with NBT is created 113 | */ 114 | public static ItemStack itemOf(Book book) { 115 | var bookItem = BOOK_ITEMS.get(book.id()); 116 | if (bookItem != null) { 117 | return bookItem.getDefaultStack(); 118 | } else { 119 | return createDynamic(book); 120 | } 121 | } 122 | 123 | @Override 124 | public Text getName(ItemStack stack) { 125 | if (this.bookId != null) return super.getName(stack); 126 | 127 | var book = bookOf(stack); 128 | if (book == null || book.dynamicBookName() == null) return super.getName(stack); 129 | 130 | return book.dynamicBookName(); 131 | } 132 | 133 | /** 134 | * @return A dynamic book with the correct NBT to represent the given book 135 | */ 136 | public static ItemStack createDynamic(Book book) { 137 | var stack = DYNAMIC_BOOK.getDefaultStack(); 138 | stack.set(BOOK_ID, book.id()); 139 | return stack; 140 | } 141 | 142 | @Override 143 | public ActionResult use(World world, PlayerEntity user, Hand hand) { 144 | var playerStack = user.getStackInHand(hand); 145 | 146 | var bookId = bookIdOf(playerStack); 147 | if (bookId == null) return ActionResult.SUCCESS; 148 | if (!world.isClient) return ActionResult.SUCCESS; 149 | 150 | var book = BookLoader.get(bookId); 151 | if (book == null) { 152 | user.sendMessage(Text.translatable("text.lavender.unknown_book", bookId).formatted(Formatting.RED), false); 153 | return ActionResult.PASS; 154 | } 155 | 156 | openBookScreen(book); 157 | return ActionResult.SUCCESS; 158 | } 159 | 160 | @Environment(EnvType.CLIENT) 161 | private static void openBookScreen(Book book) { 162 | MinecraftClient.getInstance().setScreen(new LavenderBookScreen(book)); 163 | } 164 | 165 | @Override 166 | public void appendTooltip(ItemStack stack, TooltipContext context, List tooltip, TooltipType type) { 167 | var bookId = bookIdOf(stack); 168 | if (bookId == null) { 169 | tooltip.add(TextOps.withFormatting("⚠ §No associated book", Formatting.RED, Formatting.DARK_GRAY)); 170 | } else { 171 | var book = BookLoader.get(bookId); 172 | if (book != null) return; 173 | 174 | tooltip.add(TextOps.withFormatting("⚠ §Unknown book \"" + bookId + "\"", Formatting.RED, Formatting.DARK_GRAY)); 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/book/LavenderClientStorage.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.book; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | import com.google.gson.JsonObject; 6 | import com.google.gson.reflect.TypeToken; 7 | import io.wispforest.lavender.Lavender; 8 | import io.wispforest.lavender.client.LavenderClient; 9 | import net.fabricmc.loader.api.FabricLoader; 10 | import net.minecraft.util.Identifier; 11 | import org.jetbrains.annotations.Nullable; 12 | 13 | import java.io.IOException; 14 | import java.nio.file.Files; 15 | import java.nio.file.Path; 16 | import java.util.*; 17 | 18 | public class LavenderClientStorage { 19 | 20 | private static final TypeToken>>> BOOKMARKS_TYPE = new TypeToken<>() {}; 21 | private static Map>> bookmarks; 22 | 23 | private static final TypeToken>> OPENED_BOOKS_TYPE = new TypeToken<>() {}; 24 | private static Map> openedBooks; 25 | 26 | private static final TypeToken>>> VIEWED_ENTRIES_TYPE = new TypeToken<>() {}; 27 | private static Map>> viewedEntries; 28 | 29 | private static final Gson GSON = new GsonBuilder().registerTypeAdapter(Identifier.class, new Identifier.Serializer()).setPrettyPrinting().create(); 30 | 31 | static { 32 | try { 33 | var data = GSON.fromJson(Files.readString(storageFile()), JsonObject.class); 34 | 35 | bookmarks = GSON.fromJson(data.get("bookmarks"), BOOKMARKS_TYPE); 36 | openedBooks = GSON.fromJson(data.get("opened_books"), OPENED_BOOKS_TYPE); 37 | viewedEntries = GSON.fromJson(data.get("viewed_entries"), VIEWED_ENTRIES_TYPE); 38 | 39 | if (bookmarks == null) bookmarks = new HashMap<>(); 40 | if (openedBooks == null) openedBooks = new HashMap<>(); 41 | if (viewedEntries == null) viewedEntries = new HashMap<>(); 42 | } catch (Exception e) { 43 | bookmarks = new HashMap<>(); 44 | openedBooks = new HashMap<>(); 45 | viewedEntries = new HashMap<>(); 46 | save(); 47 | } 48 | } 49 | 50 | public static List getBookmarks(Book book) { 51 | var worldBookmarks = bookmarks.get(LavenderClient.currentWorldId()); 52 | if (worldBookmarks == null) return List.of(); 53 | 54 | if (!worldBookmarks.containsKey(book.id())) return List.of(); 55 | return worldBookmarks.get(book.id()); 56 | } 57 | 58 | public static void addBookmark(Book book, Entry entry) { 59 | getBookmarkList(book).add(new Bookmark(Bookmark.Type.ENTRY, entry.id())); 60 | save(); 61 | } 62 | 63 | public static void addBookmark(Book book, Category entry) { 64 | getBookmarkList(book).add(new Bookmark(Bookmark.Type.CATEGORY, entry.id())); 65 | save(); 66 | } 67 | 68 | public static void removeBookmark(Book book, Bookmark bookmark) { 69 | getBookmarkList(book).remove(bookmark); 70 | save(); 71 | } 72 | 73 | private static List getBookmarkList(Book book) { 74 | return bookmarks.computeIfAbsent(LavenderClient.currentWorldId(), $ -> new HashMap<>()).computeIfAbsent(book.id(), $ -> new ArrayList<>()); 75 | } 76 | 77 | public static boolean wasBookOpened(Identifier book) { 78 | return getOpenedBooksSet().contains(book); 79 | } 80 | 81 | public static void markBookOpened(Identifier book) { 82 | getOpenedBooksSet().add(book); 83 | save(); 84 | } 85 | 86 | public static boolean wasEntryViewed(Book book, Entry entry) { 87 | return viewedEntries.containsKey(LavenderClient.currentWorldId()) 88 | && viewedEntries.get(LavenderClient.currentWorldId()).containsKey(book.id()) 89 | && viewedEntries.get(LavenderClient.currentWorldId()).get(book.id()).contains(entry.id()); 90 | } 91 | 92 | public static void markEntryViewed(Book book, Entry entry) { 93 | viewedEntries.computeIfAbsent(LavenderClient.currentWorldId(), $ -> new HashMap<>()).computeIfAbsent(book.id(), $ -> new HashSet<>()).add(entry.id()); 94 | save(); 95 | } 96 | 97 | private static Set getOpenedBooksSet() { 98 | return openedBooks.computeIfAbsent(LavenderClient.currentWorldId(), $ -> new HashSet<>()); 99 | } 100 | 101 | private static void save() { 102 | try { 103 | var data = new JsonObject(); 104 | data.add("bookmarks", GSON.toJsonTree(bookmarks, BOOKMARKS_TYPE.getType())); 105 | data.add("opened_books", GSON.toJsonTree(openedBooks, OPENED_BOOKS_TYPE.getType())); 106 | data.add("viewed_entries", GSON.toJsonTree(viewedEntries, VIEWED_ENTRIES_TYPE.getType())); 107 | 108 | Files.writeString(storageFile(), GSON.toJson(data)); 109 | } catch (IOException e) { 110 | Lavender.LOGGER.warn("Failed to save Lavender client data", e); 111 | } 112 | } 113 | 114 | private static Path storageFile() { 115 | return FabricLoader.getInstance().getConfigDir().resolve("lavender_client_storage.json"); 116 | } 117 | 118 | public record Bookmark(Type type, Identifier id) { 119 | public enum Type { 120 | ENTRY, CATEGORY; 121 | } 122 | 123 | public @Nullable Book.BookmarkableElement tryResolve(Book book) { 124 | return switch (this.type) { 125 | case CATEGORY -> book.categoryById(this.id); 126 | case ENTRY -> book.entryById(this.id); 127 | }; 128 | } 129 | } 130 | } -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/book/StructureComponent.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.book; 2 | 3 | import io.wispforest.lavender.client.StructureOverlayRenderer; 4 | import io.wispforest.lavender.structure.LavenderStructures; 5 | import io.wispforest.lavender.structure.StructureTemplate; 6 | import io.wispforest.owo.ui.base.BaseComponent; 7 | import io.wispforest.owo.ui.core.CursorStyle; 8 | import io.wispforest.owo.ui.core.Easing; 9 | import io.wispforest.owo.ui.core.OwoUIDrawContext; 10 | import io.wispforest.owo.ui.parsing.UIModelParsingException; 11 | import io.wispforest.owo.ui.parsing.UIParsing; 12 | import net.minecraft.client.MinecraftClient; 13 | import net.minecraft.client.gui.screen.Screen; 14 | import net.minecraft.client.gui.tooltip.TooltipComponent; 15 | import net.minecraft.client.render.DiffuseLighting; 16 | import net.minecraft.client.render.LightmapTextureManager; 17 | import net.minecraft.client.render.OverlayTexture; 18 | import net.minecraft.text.Text; 19 | import net.minecraft.util.Identifier; 20 | import net.minecraft.util.Util; 21 | import net.minecraft.util.math.RotationAxis; 22 | import org.lwjgl.glfw.GLFW; 23 | import org.w3c.dom.Element; 24 | 25 | import java.util.List; 26 | 27 | public class StructureComponent extends BaseComponent { 28 | 29 | private final StructureTemplate structure; 30 | private final int displayAngle; 31 | 32 | private float rotation = -45; 33 | private long lastInteractionTime = 0L; 34 | 35 | private boolean placeable = true; 36 | private int visibleLayer = -1; 37 | 38 | public StructureComponent(StructureTemplate structure, int displayAngle) { 39 | this.structure = structure; 40 | this.displayAngle = displayAngle; 41 | this.cursorStyle(CursorStyle.HAND); 42 | } 43 | 44 | @Override 45 | public void update(float delta, int mouseX, int mouseY) { 46 | super.update(delta, mouseX, mouseY); 47 | 48 | var diff = Util.getMeasuringTimeMs() - this.lastInteractionTime; 49 | if (diff < 5000L) return; 50 | 51 | this.rotation += delta * Easing.SINE.apply(Math.min(1f, (diff - 5000) / 1500f)); 52 | } 53 | 54 | @Override 55 | public void draw(OwoUIDrawContext context, int mouseX, int mouseY, float partialTicks, float delta) { 56 | var client = MinecraftClient.getInstance(); 57 | var entityBuffers = client.getBufferBuilders().getEntityVertexConsumers(); 58 | 59 | float scale = Math.min(this.width, this.height); 60 | scale /= Math.max(structure.xSize, Math.max(structure.ySize, structure.zSize)); 61 | scale /= 1.625f; 62 | 63 | var matrices = context.getMatrices(); 64 | 65 | matrices.push(); 66 | matrices.translate(this.x + this.width / 2f, this.y + this.height / 2f, 100); 67 | matrices.scale(scale, -scale, scale); 68 | 69 | matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(this.displayAngle)); 70 | matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(this.rotation)); 71 | matrices.translate(this.structure.xSize / -2f, this.structure.ySize / -2f, this.structure.zSize / -2f); 72 | 73 | structure.forEachPredicate((blockPos, predicate) -> { 74 | if (this.visibleLayer != -1 && this.visibleLayer != blockPos.getY()) return; 75 | 76 | matrices.push(); 77 | matrices.translate(blockPos.getX(), blockPos.getY(), blockPos.getZ()); 78 | 79 | client.getBlockRenderManager().renderBlockAsEntity( 80 | predicate.preview(), matrices, entityBuffers, 81 | LightmapTextureManager.MAX_BLOCK_LIGHT_COORDINATE, 82 | OverlayTexture.DEFAULT_UV 83 | ); 84 | 85 | matrices.pop(); 86 | }); 87 | 88 | matrices.pop(); 89 | 90 | DiffuseLighting.disableGuiDepthLighting(); 91 | entityBuffers.draw(); 92 | DiffuseLighting.enableGuiDepthLighting(); 93 | 94 | if (this.placeable) { 95 | if (StructureOverlayRenderer.isShowingOverlay(this.structure.id)) { 96 | context.drawText(client.textRenderer, Text.translatable("text.lavender.structure_component.active_overlay_hint"), this.x + this.width - 5 - client.textRenderer.getWidth("⚓"), this.y + this.height - 9 - 5, 0, false); 97 | this.tooltip(Text.translatable("text.lavender.structure_component.hide_hint")); 98 | } else { 99 | this.tooltip(Text.translatable("text.lavender.structure_component.place_hint")); 100 | } 101 | } 102 | } 103 | 104 | @Override 105 | public boolean onMouseDown(double mouseX, double mouseY, int button) { 106 | var result = super.onMouseDown(mouseX, mouseY, button); 107 | if (!this.placeable || button != GLFW.GLFW_MOUSE_BUTTON_LEFT || !Screen.hasShiftDown()) return result; 108 | 109 | if (StructureOverlayRenderer.isShowingOverlay(this.structure.id)) { 110 | StructureOverlayRenderer.removeAllOverlays(this.structure.id); 111 | } else { 112 | StructureOverlayRenderer.addPendingOverlay(this.structure.id); 113 | StructureOverlayRenderer.restrictVisibleLayer(this.structure.id, this.visibleLayer); 114 | 115 | MinecraftClient.getInstance().setScreen(null); 116 | } 117 | 118 | return true; 119 | } 120 | 121 | @Override 122 | public boolean onMouseDrag(double mouseX, double mouseY, double deltaX, double deltaY, int button) { 123 | var result = super.onMouseDrag(mouseX, mouseY, deltaX, deltaY, button); 124 | if (button != GLFW.GLFW_MOUSE_BUTTON_LEFT) return result; 125 | 126 | this.rotation += (float) deltaX; 127 | this.lastInteractionTime = Util.getMeasuringTimeMs(); 128 | 129 | return true; 130 | } 131 | 132 | @Override 133 | public boolean canFocus(FocusSource source) { 134 | return source == FocusSource.MOUSE_CLICK; 135 | } 136 | 137 | public StructureComponent visibleLayer(int visibleLayer) { 138 | StructureOverlayRenderer.restrictVisibleLayer(this.structure.id, visibleLayer); 139 | 140 | this.visibleLayer = visibleLayer; 141 | return this; 142 | } 143 | 144 | public StructureComponent placeable(boolean placeable) { 145 | if (!placeable) { 146 | this.tooltip((List) null); 147 | } 148 | 149 | this.cursorStyle(placeable ? CursorStyle.HAND : CursorStyle.POINTER); 150 | 151 | this.placeable = placeable; 152 | return this; 153 | } 154 | 155 | public boolean placeable() { 156 | return this.placeable; 157 | } 158 | 159 | public static StructureComponent parse(Element element) { 160 | UIParsing.expectAttributes(element, "structure-id"); 161 | 162 | var structureId = Identifier.tryParse(element.getAttribute("structure-id")); 163 | if (structureId == null) { 164 | throw new UIModelParsingException("Invalid structure id '" + element.getAttribute("structure-id") + "'"); 165 | } 166 | 167 | var structure = LavenderStructures.get(structureId); 168 | if (structure == null) throw new UIModelParsingException("Unknown structure '" + structureId + "'"); 169 | 170 | int displayAngle = 35; 171 | if (element.hasAttribute("display-angle")) { 172 | displayAngle = UIParsing.parseSignedInt(element.getAttributeNode("display-angle")); 173 | } 174 | 175 | return new StructureComponent(structure, displayAngle); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/client/AlphanumComparator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 Dave Koelle www.davekoelle.com/ 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the “Software”), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | package io.wispforest.lavender.client; 23 | 24 | public class AlphanumComparator { 25 | 26 | public static int compare(String s1, String s2) { 27 | if ((s1 == null) || (s2 == null)) { 28 | return 0; 29 | } 30 | 31 | int thisMarker = 0; 32 | int thatMarker = 0; 33 | int s1Length = s1.length(); 34 | int s2Length = s2.length(); 35 | 36 | while (thisMarker < s1Length && thatMarker < s2Length) { 37 | String thisChunk = getChunk(s1, s1Length, thisMarker); 38 | thisMarker += thisChunk.length(); 39 | 40 | String thatChunk = getChunk(s2, s2Length, thatMarker); 41 | thatMarker += thatChunk.length(); 42 | 43 | // If both chunks contain numeric characters, sort them numerically 44 | int result = 0; 45 | if (isDigit(thisChunk.charAt(0)) && isDigit(thatChunk.charAt(0))) { 46 | // Simple chunk comparison by length. 47 | int thisChunkLength = thisChunk.length(); 48 | result = thisChunkLength - thatChunk.length(); 49 | // If equal, the first different number counts 50 | if (result == 0) { 51 | for (int i = 0; i < thisChunkLength; i++) { 52 | result = thisChunk.charAt(i) - thatChunk.charAt(i); 53 | if (result != 0) { 54 | return result; 55 | } 56 | } 57 | } 58 | } else { 59 | result = thisChunk.compareTo(thatChunk); 60 | } 61 | 62 | if (result != 0) { 63 | return result; 64 | } 65 | } 66 | 67 | return s1Length - s2Length; 68 | } 69 | 70 | /** 71 | * Length of string is passed in for improved efficiency (only need to calculate it once) 72 | **/ 73 | private static String getChunk(String s, int slength, int marker) { 74 | StringBuilder chunk = new StringBuilder(); 75 | char c = s.charAt(marker); 76 | chunk.append(c); 77 | marker++; 78 | if (isDigit(c)) { 79 | while (marker < slength) { 80 | c = s.charAt(marker); 81 | if (!isDigit(c)) { 82 | break; 83 | } 84 | chunk.append(c); 85 | marker++; 86 | } 87 | } else { 88 | while (marker < slength) { 89 | c = s.charAt(marker); 90 | if (isDigit(c)) { 91 | break; 92 | } 93 | chunk.append(c); 94 | marker++; 95 | } 96 | } 97 | return chunk.toString(); 98 | } 99 | 100 | private static boolean isDigit(char ch) { 101 | return ((ch >= 48) && (ch <= 57)); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/client/AssociatedEntryTooltipComponent.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.client; 2 | 3 | import io.wispforest.lavender.book.Entry; 4 | import io.wispforest.owo.ui.component.Components; 5 | import io.wispforest.owo.ui.container.Containers; 6 | import io.wispforest.owo.ui.container.FlowLayout; 7 | import io.wispforest.owo.ui.core.*; 8 | import io.wispforest.owo.ui.util.Delta; 9 | import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; 10 | import net.minecraft.client.font.TextRenderer; 11 | import net.minecraft.client.gui.DrawContext; 12 | import net.minecraft.client.gui.screen.Screen; 13 | import net.minecraft.client.gui.tooltip.TooltipComponent; 14 | import net.minecraft.item.ItemStack; 15 | import net.minecraft.text.Text; 16 | import net.minecraft.util.Formatting; 17 | import org.jetbrains.annotations.Nullable; 18 | 19 | import java.lang.ref.WeakReference; 20 | 21 | public class AssociatedEntryTooltipComponent implements TooltipComponent { 22 | 23 | public static float entryTriggerProgress = 0f; 24 | public static @Nullable WeakReference tooltipStack = null; 25 | 26 | private final FlowLayout layout; 27 | 28 | public AssociatedEntryTooltipComponent(ItemStack book, Entry entry, float progress) { 29 | this.layout = Containers.horizontalFlow(Sizing.content(), Sizing.content()).gap(2); 30 | 31 | this.layout.child(Containers.verticalFlow(Sizing.content(), Sizing.content()) 32 | .child(entry.iconFactory().apply(Sizing.fixed(16)).margins(Insets.of(2))) 33 | .child(Components.item(book).sizing(Sizing.fixed(8)).positioning(Positioning.absolute(11, 11)).zIndex(50))); 34 | 35 | this.layout.child(Containers.verticalFlow(Sizing.content(), Sizing.content()) 36 | .child(Components.label(Text.literal(entry.title()).formatted(Formatting.GRAY))) 37 | .child(Components.label(progress >= .05f 38 | ? Text.translatable("text.lavender.entry_tooltip.progress", "|".repeat((int) (30 * progress)), "|".repeat((int) Math.ceil(30 * (1 - progress)))) 39 | : Text.translatable("text.lavender.entry_tooltip")))); 40 | 41 | this.layout.verticalAlignment(VerticalAlignment.CENTER); 42 | 43 | this.layout.inflate(Size.of(1000, 1000)); 44 | this.layout.mount(null, 0, 0); 45 | } 46 | 47 | @Override 48 | public void drawItems(TextRenderer textRenderer, int x, int y, int width, int height, DrawContext context) { 49 | context = OwoUIDrawContext.of(context); 50 | context.getMatrices().push(); 51 | context.getMatrices().translate(0, 0, 1000); 52 | 53 | this.layout.moveTo(x, y); 54 | this.layout.draw((OwoUIDrawContext) context, 0, 0, 0, 0); 55 | 56 | context.getMatrices().pop(); 57 | } 58 | 59 | @Override 60 | public int getHeight(TextRenderer textRenderer) { 61 | return this.layout.height(); 62 | } 63 | 64 | @Override 65 | public int getWidth(TextRenderer textRenderer) { 66 | return this.layout.width(); 67 | } 68 | 69 | static { 70 | ClientTickEvents.END_CLIENT_TICK.register(client -> { 71 | if (Screen.hasAltDown()) return; 72 | entryTriggerProgress += Delta.compute(entryTriggerProgress, 0f, .125f); 73 | }); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/client/BasicVertexConsumerProvider.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.client; 2 | 3 | import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap; 4 | import net.minecraft.client.render.RenderLayer; 5 | import net.minecraft.client.render.TexturedRenderLayers; 6 | import net.minecraft.client.render.VertexConsumerProvider; 7 | import net.minecraft.client.util.BufferAllocator; 8 | import net.minecraft.util.Util; 9 | 10 | public class BasicVertexConsumerProvider extends VertexConsumerProvider.Immediate { 11 | public BasicVertexConsumerProvider(int initialBufferSize) { 12 | super(new BufferAllocator(initialBufferSize), Util.make(new Object2ObjectLinkedOpenHashMap<>(), buffers -> { 13 | buffers.put(TexturedRenderLayers.getEntitySolid(), new BufferAllocator(TexturedRenderLayers.getEntitySolid().getExpectedBufferSize())); 14 | buffers.put(TexturedRenderLayers.getEntityCutout(), new BufferAllocator(TexturedRenderLayers.getEntityCutout().getExpectedBufferSize())); 15 | buffers.put(TexturedRenderLayers.getBannerPatterns(), new BufferAllocator(TexturedRenderLayers.getBannerPatterns().getExpectedBufferSize())); 16 | buffers.put(TexturedRenderLayers.getItemEntityTranslucentCull(), new BufferAllocator(TexturedRenderLayers.getItemEntityTranslucentCull().getExpectedBufferSize())); 17 | buffers.put(RenderLayer.getArmorEntityGlint(), new BufferAllocator(RenderLayer.getArmorEntityGlint().getExpectedBufferSize())); 18 | buffers.put(RenderLayer.getGlint(), new BufferAllocator(RenderLayer.getGlint().getExpectedBufferSize())); 19 | buffers.put(RenderLayer.getGlintTranslucent(), new BufferAllocator(RenderLayer.getGlintTranslucent().getExpectedBufferSize())); 20 | buffers.put(RenderLayer.getEntityGlint(), new BufferAllocator(RenderLayer.getEntityGlint().getExpectedBufferSize())); 21 | buffers.put(RenderLayer.getWaterMask(), new BufferAllocator(RenderLayer.getWaterMask().getExpectedBufferSize())); 22 | })); 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/client/BlitAlphaProgram.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.client; 2 | 3 | import io.wispforest.lavender.Lavender; 4 | import io.wispforest.owo.shader.GlProgram; 5 | import net.minecraft.client.gl.ShaderProgram; 6 | import net.minecraft.client.gl.ShaderProgramKey; 7 | import net.minecraft.client.gl.Uniform; 8 | import net.minecraft.client.render.VertexFormats; 9 | 10 | public class BlitAlphaProgram extends GlProgram { 11 | 12 | private Uniform alpha; 13 | 14 | public BlitAlphaProgram() { 15 | super(Lavender.id("blit_alpha"), VertexFormats.BLIT_SCREEN); 16 | } 17 | 18 | @Override 19 | protected void setup() { 20 | super.setup(); 21 | this.alpha = this.findUniform("Alpha"); 22 | } 23 | 24 | public void setAlpha(float alpha) { 25 | this.alpha.set(alpha); 26 | } 27 | 28 | public ShaderProgramKey key() { 29 | return this.programKey; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/client/BlitCutoutProgram.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.client; 2 | 3 | import io.wispforest.lavender.Lavender; 4 | import io.wispforest.owo.shader.GlProgram; 5 | import net.minecraft.client.gl.ShaderProgramKey; 6 | import net.minecraft.client.render.VertexFormats; 7 | 8 | public class BlitCutoutProgram extends GlProgram { 9 | 10 | public BlitCutoutProgram() { 11 | super(Lavender.id("blit_cutout"), VertexFormats.BLIT_SCREEN); 12 | } 13 | 14 | public ShaderProgramKey key() { 15 | return this.programKey; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/client/BookBakedModel.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.client; 2 | 3 | import io.wispforest.lavender.book.LavenderBookItem; 4 | import net.minecraft.client.MinecraftClient; 5 | import net.minecraft.client.item.ItemModelManager; 6 | import net.minecraft.client.render.item.ItemRenderState; 7 | import net.minecraft.client.render.item.model.ItemModel; 8 | import net.minecraft.client.world.ClientWorld; 9 | import net.minecraft.entity.LivingEntity; 10 | import net.minecraft.item.ItemStack; 11 | import net.minecraft.item.ModelTransformationMode; 12 | import org.jetbrains.annotations.Nullable; 13 | 14 | public class BookBakedModel implements ItemModel { 15 | 16 | private final ItemModel defaultModel; 17 | 18 | public BookBakedModel(ItemModel defaultModel) { 19 | this.defaultModel = defaultModel; 20 | } 21 | 22 | @Override 23 | public void update(ItemRenderState state, ItemStack stack, ItemModelManager resolver, ModelTransformationMode transformationMode, @Nullable ClientWorld world, @Nullable LivingEntity user, int seed) { 24 | var book = LavenderBookItem.bookOf(stack); 25 | if (book != null && book.dynamicBookModel() != null) { 26 | MinecraftClient.getInstance().getBakedModelManager().getItemModel(book.dynamicBookModel()).update(state, stack, resolver, transformationMode, world, user, seed); 27 | } else { 28 | this.defaultModel.update(state, stack, resolver, transformationMode, world, user, seed); 29 | } 30 | } 31 | 32 | // private final ModelOverrideList overrides = new ModelOverrideList() { 33 | // @Override 34 | // public @Nullable BakedModel getModel(ItemStack stack, @Nullable ClientWorld world, @Nullable LivingEntity entity, int seed) { 35 | // var book = LavenderBookItem.bookOf(stack); 36 | // if (book == null || book.dynamicBookModel() == null) return null; 37 | // 38 | // var bookModel = MinecraftClient.getInstance().getBakedModelManager().getModel(new ModelIdentifier(book.dynamicBookModel(), "inventory")); 39 | // return bookModel != MinecraftClient.getInstance().getBakedModelManager().getMissingModel() 40 | // ? bookModel 41 | // : null; 42 | // } 43 | // }; 44 | // 45 | // private BookBakedModel(BakedModel parent) { 46 | // this.wrapped = parent; 47 | // } 48 | // 49 | // @Override 50 | // public ModelOverrideList getOverrides() { 51 | // return this.overrides; 52 | // } 53 | // 54 | // public static class Unbaked implements UnbakedModel { 55 | // 56 | // 57 | // @Override 58 | // public void resolve(Resolver resolver) { 59 | // resolver.resolve(BROWN_BOOK_ID); 60 | // } 61 | // 62 | // @Nullable 63 | // @Override 64 | // public BakedModel bake(Baker baker, Function textureGetter, ModelBakeSettings rotationContainer) { 65 | // return new BookBakedModel(baker.bake(BROWN_BOOK_ID, rotationContainer)); 66 | // } 67 | // } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/client/LavenderClient.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.client; 2 | 3 | import io.wispforest.lavender.Lavender; 4 | import io.wispforest.lavender.LavenderClientRecipeCache; 5 | import io.wispforest.lavender.LavenderCommands; 6 | import io.wispforest.lavender.book.*; 7 | import io.wispforest.lavender.md.ItemListComponent; 8 | import io.wispforest.lavender.structure.LavenderStructures; 9 | import io.wispforest.owo.ui.component.Components; 10 | import io.wispforest.owo.ui.container.Containers; 11 | import io.wispforest.owo.ui.container.FlowLayout; 12 | import io.wispforest.owo.ui.core.Insets; 13 | import io.wispforest.owo.ui.core.Positioning; 14 | import io.wispforest.owo.ui.core.Size; 15 | import io.wispforest.owo.ui.core.Sizing; 16 | import io.wispforest.owo.ui.hud.Hud; 17 | import io.wispforest.owo.ui.parsing.UIParsing; 18 | import it.unimi.dsi.fastutil.ints.Int2ObjectMap; 19 | import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; 20 | import net.fabricmc.api.ClientModInitializer; 21 | import net.fabricmc.api.EnvType; 22 | import net.fabricmc.api.Environment; 23 | import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; 24 | import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; 25 | import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin; 26 | import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; 27 | import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; 28 | import net.fabricmc.fabric.api.event.player.UseBlockCallback; 29 | import net.minecraft.client.MinecraftClient; 30 | import net.minecraft.client.gl.Framebuffer; 31 | import net.minecraft.client.network.ClientPlayerEntity; 32 | import net.minecraft.client.render.item.model.ItemModelTypes; 33 | import net.minecraft.item.Items; 34 | import net.minecraft.text.Text; 35 | import net.minecraft.util.ActionResult; 36 | import net.minecraft.util.Identifier; 37 | import net.minecraft.util.hit.BlockHitResult; 38 | import org.jetbrains.annotations.Nullable; 39 | 40 | import java.util.Objects; 41 | import java.util.UUID; 42 | 43 | @Environment(EnvType.CLIENT) 44 | public class LavenderClient implements ClientModInitializer { 45 | 46 | public static final BlitCutoutProgram BLIT_CUTOUT_PROGRAM = new BlitCutoutProgram(); 47 | public static final BlitAlphaProgram BLIT_ALPHA_PROGRAM = new BlitAlphaProgram(); 48 | 49 | private static final Int2ObjectMap TEXTURE_SIZES = new Int2ObjectOpenHashMap<>(); 50 | private static final Identifier ENTRY_HUD_ID = Lavender.id("entry_hud"); 51 | 52 | private static UUID currentWorldId = null; 53 | 54 | public static @Nullable Framebuffer mainTargetOverride = null; 55 | 56 | @Override 57 | public void onInitializeClient() { 58 | ClientCommandRegistrationCallback.EVENT.register(LavenderCommands.Client::register); 59 | 60 | ItemModelTypes.ID_MAPPER.put(Lavender.id("dynamic_book_model"), UnbakedBookModel.CODEC); 61 | 62 | // ModelLoadingPlugin.register(pluginContext -> { 63 | // pluginContext.modifyModelOnLoad().register((model, context) -> { 64 | // if (!Objects.equals(context.id(), Lavender.id("item/dynamic_book"))) return model; 65 | // return new BookBakedModel.Unbaked(); 66 | // }); 67 | // 68 | // // pluginContext.resolveModel().register(context -> { 69 | // // if (!context.id().equals(Lavender.id("item/dynamic_book"))) return null; 70 | // // return new BookBakedModel.Unbaked(); 71 | // // }); 72 | // }); 73 | 74 | StructureOverlayRenderer.initialize(); 75 | OffhandBookRenderer.initialize(); 76 | 77 | LavenderStructures.initialize(); 78 | BookLoader.initialize(); 79 | BookContentLoader.initialize(); 80 | 81 | ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> { 82 | BookLoader.reload(MinecraftClient.getInstance().getResourceManager()); 83 | BookContentLoader.reloadContents(MinecraftClient.getInstance().getResourceManager()); 84 | }); 85 | 86 | Hud.add(ENTRY_HUD_ID, () -> Containers.horizontalFlow(Sizing.content(), Sizing.content()).gap(5).positioning(Positioning.across(50, 52))); 87 | ClientTickEvents.END_CLIENT_TICK.register(client -> { 88 | if (client.world == null || !(Hud.getComponent(ENTRY_HUD_ID) instanceof FlowLayout hudComponent)) return; 89 | 90 | hudComponent.configure(container -> { 91 | container.clearChildren(); 92 | 93 | Book book = LavenderBookItem.bookOf(client.player.getMainHandStack()); 94 | if (book == null) book = LavenderBookItem.bookOf(client.player.getOffHandStack()); 95 | if (book == null) return; 96 | 97 | if (!(client.crosshairTarget instanceof BlockHitResult hitResult)) return; 98 | var item = client.world.getBlockState(hitResult.getBlockPos()).getBlock().asItem(); 99 | if (item == Items.AIR) return; 100 | 101 | var associatedEntry = book.entryByAssociatedItem(item.getDefaultStack()); 102 | if (associatedEntry == null || !associatedEntry.canPlayerView(client.player)) return; 103 | 104 | container.child(Containers.verticalFlow(Sizing.content(), Sizing.content()) 105 | .child(associatedEntry.iconFactory().apply(Sizing.fixed(16)).margins(Insets.of(0, 1, 0, 1))) 106 | .child(Components.item(LavenderBookItem.itemOf(book)).sizing(Sizing.fixed(8)).positioning(Positioning.absolute(9, 9)).zIndex(50))); 107 | container.child(Containers.verticalFlow(Sizing.content(), Sizing.content()) 108 | .child(Components.label(Text.literal(associatedEntry.title())).shadow(true)) 109 | .child(Components.label(Text.translatable(client.player.isSneaking() ? "text.lavender.entry_hud.click_to_view" : "text.lavender.entry_hud.sneak_to_view")))); 110 | }); 111 | }); 112 | 113 | UseBlockCallback.EVENT.register((player, world, hand, hitResult) -> { 114 | var stack = player.getStackInHand(hand); 115 | if (!player.isSneaking()) return ActionResult.PASS; 116 | 117 | var book = LavenderBookItem.bookOf(stack); 118 | if (book == null) return ActionResult.PASS; 119 | 120 | var item = world.getBlockState(hitResult.getBlockPos()).getBlock().asItem(); 121 | if (item == Items.AIR) return ActionResult.PASS; 122 | 123 | var associatedEntry = book.entryByAssociatedItem(item.getDefaultStack()); 124 | if (associatedEntry == null || !associatedEntry.canPlayerView((ClientPlayerEntity) player)) { 125 | return ActionResult.PASS; 126 | } 127 | 128 | LavenderBookScreen.pushEntry(book, associatedEntry); 129 | MinecraftClient.getInstance().setScreen(new LavenderBookScreen(book)); 130 | 131 | player.swingHand(hand); 132 | return ActionResult.FAIL; 133 | }); 134 | 135 | ClientNewEntriesUnlockedCallback.EVENT.register((client, book, newEntryCount) -> { 136 | if (book.newEntriesToast() != null) { 137 | client.getToastManager().add(new NewEntriesToast(book.newEntriesToast())); 138 | } 139 | }); 140 | 141 | LavenderClientRecipeCache.initializeClient(); 142 | 143 | ClientPlayNetworking.registerGlobalReceiver(Lavender.WorldUUIDPayload.ID, (payload, context) -> { 144 | currentWorldId = payload.worldUuid(); 145 | }); 146 | 147 | UIParsing.registerFactory(Lavender.id("ingredient"), element -> { 148 | Lavender.LOGGER.warn("Deprecated element used, migrate to instead"); 149 | return new ItemListComponent(); 150 | }); 151 | 152 | UIParsing.registerFactory(Lavender.id("item-list"), element -> new ItemListComponent()); 153 | } 154 | 155 | public static UUID currentWorldId() { 156 | return currentWorldId; 157 | } 158 | 159 | public static void registerTextureSize(int textureId, int width, int height) { 160 | TEXTURE_SIZES.put(textureId, Size.of(width, height)); 161 | } 162 | 163 | public static @Nullable Size getTextureSize(Identifier texture) { 164 | return TEXTURE_SIZES.get(MinecraftClient.getInstance().getTextureManager().getTexture(texture).getGlId()); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/client/NewEntriesToast.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.client; 2 | 3 | import io.wispforest.lavender.Lavender; 4 | import io.wispforest.lavender.book.Book; 5 | import io.wispforest.owo.ui.base.BaseOwoToast; 6 | import io.wispforest.owo.ui.component.Components; 7 | import io.wispforest.owo.ui.container.Containers; 8 | import io.wispforest.owo.ui.container.StackLayout; 9 | import io.wispforest.owo.ui.core.Insets; 10 | import io.wispforest.owo.ui.core.Sizing; 11 | import io.wispforest.owo.ui.core.VerticalAlignment; 12 | import net.minecraft.client.MinecraftClient; 13 | import net.minecraft.text.Text; 14 | import net.minecraft.util.Identifier; 15 | 16 | @SuppressWarnings("UnstableApiUsage") 17 | public class NewEntriesToast extends BaseOwoToast { 18 | 19 | public static final Identifier TEXTURE = Lavender.id("new_entries_toast"); 20 | 21 | public NewEntriesToast(Book.ToastSettings settings) { 22 | super( 23 | () -> Containers.stack(Sizing.content(), Sizing.content()).configure(component -> component 24 | .child(Components.sprite(MinecraftClient.getInstance().getGuiAtlasManager().getSprite(settings.backgroundSprite() != null ? settings.backgroundSprite() : TEXTURE))) 25 | .child(Containers.horizontalFlow(Sizing.content(), Sizing.content()) 26 | .child(Components.item(settings.iconStack()).margins(Insets.of(0, 0, 8, 6))) 27 | .child(Components.label(Text.translatable("text.lavender.toast.new_entries", settings.bookName()))) 28 | .verticalAlignment(VerticalAlignment.CENTER)) 29 | .verticalAlignment(VerticalAlignment.CENTER)), 30 | (baseOwoToast, time) -> time <= 5000 ? Visibility.SHOW : Visibility.HIDE 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/client/OffhandBookRenderer.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.client; 2 | 3 | import com.google.common.base.Suppliers; 4 | import com.mojang.blaze3d.platform.GlStateManager; 5 | import com.mojang.blaze3d.systems.RenderSystem; 6 | import io.wispforest.lavender.Lavender; 7 | import io.wispforest.lavender.book.Book; 8 | import io.wispforest.lavender.pond.LavenderFramebufferExtension; 9 | import io.wispforest.owo.ui.event.WindowResizeCallback; 10 | import net.minecraft.client.MinecraftClient; 11 | import net.minecraft.client.gl.Framebuffer; 12 | import net.minecraft.client.gl.SimpleFramebuffer; 13 | import net.minecraft.client.gui.DrawContext; 14 | import net.minecraft.client.render.RenderLayer; 15 | import net.minecraft.client.texture.AbstractTexture; 16 | import net.minecraft.client.util.math.MatrixStack; 17 | import net.minecraft.resource.ResourceManager; 18 | import net.minecraft.util.Arm; 19 | import net.minecraft.util.math.RotationAxis; 20 | import org.jetbrains.annotations.Nullable; 21 | 22 | import java.util.function.Supplier; 23 | 24 | public class OffhandBookRenderer { 25 | 26 | public static boolean rendering = false; 27 | 28 | private static final Supplier BACK_BUFFER = Suppliers.memoize(() -> { 29 | var window = MinecraftClient.getInstance().getWindow(); 30 | 31 | var framebuffer = new SimpleFramebuffer(window.getFramebufferWidth(), window.getFramebufferHeight(), true); 32 | ((LavenderFramebufferExtension) framebuffer).lavender$setBlitProgram(() -> { 33 | GlStateManager._colorMask(true, true, true, true); 34 | return LavenderClient.BLIT_CUTOUT_PROGRAM.key(); 35 | }); 36 | framebuffer.setClearColor(0f, 0f, 0f, 0f); 37 | return framebuffer; 38 | }); 39 | 40 | private static final Supplier DISPLAY_BUFFER = Suppliers.memoize(() -> { 41 | var window = MinecraftClient.getInstance().getWindow(); 42 | 43 | var framebuffer = new SimpleFramebuffer(window.getFramebufferWidth(), window.getFramebufferHeight(), true); 44 | framebuffer.setClearColor(0f, 0f, 0f, 0f); 45 | return framebuffer; 46 | }); 47 | 48 | private static LavenderBookScreen cachedScreen = null; 49 | private static boolean cacheExpired = true; 50 | 51 | public static void initialize() { 52 | WindowResizeCallback.EVENT.register((client, window) -> { 53 | DISPLAY_BUFFER.get().resize(window.getFramebufferWidth(), window.getFramebufferHeight()); 54 | BACK_BUFFER.get().resize(window.getFramebufferWidth(), window.getFramebufferHeight()); 55 | cachedScreen = null; 56 | }); 57 | } 58 | 59 | public static void beginFrame(@Nullable Book book) { 60 | cacheExpired = true; 61 | 62 | if (book == null) return; 63 | var client = MinecraftClient.getInstance(); 64 | 65 | rendering = true; 66 | var backBuffer = BACK_BUFFER.get(); 67 | 68 | try { 69 | // --- render book screen to separate framebuffer --- 70 | 71 | var screen = cachedScreen; 72 | if (screen == null || screen.book != book) { 73 | cachedScreen = screen = new LavenderBookScreen(book, true); 74 | screen.init(client, client.getWindow().getScaledWidth(), client.getWindow().getScaledHeight()); 75 | 76 | // we dispose the ui adapter here to 77 | // stop it from messing with and/or 78 | // leaking GLFW cursor objects 79 | screen.adapter().dispose(); 80 | } 81 | 82 | var modelView = RenderSystem.getModelViewStack(); 83 | modelView.pushMatrix(); 84 | modelView.identity(); 85 | modelView.translate(0, 0, -2000); 86 | 87 | backBuffer.clear(); 88 | backBuffer.beginWrite(false); 89 | LavenderClient.mainTargetOverride = backBuffer; 90 | 91 | screen.render(new DrawContext(client, client.getBufferBuilders().getEntityVertexConsumers()), -69, -69, 0); 92 | RenderSystem.disableDepthTest(); 93 | 94 | modelView.popMatrix(); 95 | 96 | var displayBuffer = DISPLAY_BUFFER.get(); 97 | displayBuffer.clear(); 98 | displayBuffer.beginWrite(false); 99 | LavenderClient.mainTargetOverride = displayBuffer; 100 | 101 | backBuffer.drawInternal(backBuffer.textureWidth, backBuffer.textureHeight); 102 | 103 | client.getFramebuffer().beginWrite(false); 104 | LavenderClient.mainTargetOverride = null; 105 | } finally { 106 | rendering = false; 107 | } 108 | } 109 | 110 | public static void render(MatrixStack matrices, int light) { 111 | cacheExpired = false; 112 | var client = MinecraftClient.getInstance(); 113 | 114 | // --- draw color attachment in place of map texture --- 115 | 116 | var framebuffer = DISPLAY_BUFFER.get(); 117 | 118 | var texture = new FramebufferTexture(framebuffer.getColorAttachment()); 119 | client.getTextureManager().registerTexture(Lavender.id("offhand_book_framebuffer"), texture); 120 | 121 | var rightHanded = client.player.getMainArm() == Arm.RIGHT; 122 | 123 | matrices.push(); 124 | matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(rightHanded ? 15 : -15)); 125 | matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(-10)); 126 | 127 | matrices.scale(1 * (framebuffer.textureWidth / (float) framebuffer.textureHeight), 1f, 1f); 128 | matrices.translate(rightHanded ? -.4f : -.6f, -.35f, -.165f); 129 | 130 | var buffer = client.getBufferBuilders().getEntityVertexConsumers().getBuffer(RenderLayer.getText(Lavender.id("offhand_book_framebuffer"))); 131 | var matrix = matrices.peek().getPositionMatrix(); 132 | 133 | buffer.vertex(matrix, 0, 1, 0).color(1f, 1f, 1f, 1f).texture(0, 1).light(light); 134 | buffer.vertex(matrix, 0, 0, 0).color(1f, 1f, 1f, 1f).texture(0, 0).light(light); 135 | buffer.vertex(matrix, 1, 0, 0).color(1f, 1f, 1f, 1f).texture(1, 0).light(light); 136 | buffer.vertex(matrix, 1, 1, 0).color(1f, 1f, 1f, 1f).texture(1, 1).light(light); 137 | 138 | client.getBufferBuilders().getEntityVertexConsumers().draw(); 139 | 140 | matrices.pop(); 141 | } 142 | 143 | public static void endFrame() { 144 | if (cacheExpired) cachedScreen = null; 145 | } 146 | 147 | private static class FramebufferTexture extends AbstractTexture { 148 | 149 | private FramebufferTexture(int textureId) { 150 | this.glId = textureId; 151 | } 152 | 153 | @Override 154 | public void clearGlId() {} 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/client/UnbakedBookModel.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.client; 2 | 3 | import com.mojang.serialization.MapCodec; 4 | import com.mojang.serialization.codecs.RecordCodecBuilder; 5 | import net.minecraft.client.render.item.model.ItemModel; 6 | import net.minecraft.client.render.item.model.ItemModelTypes; 7 | 8 | public class UnbakedBookModel implements ItemModel.Unbaked { 9 | 10 | public static final MapCodec CODEC = RecordCodecBuilder.mapCodec( 11 | instance -> instance.group( 12 | ItemModelTypes.CODEC.fieldOf("default").forGetter(unbakedBookModel -> unbakedBookModel.defaultModel) 13 | ).apply(instance, UnbakedBookModel::new) 14 | ); 15 | 16 | private final ItemModel.Unbaked defaultModel; 17 | 18 | public UnbakedBookModel(ItemModel.Unbaked defaultModel) { 19 | this.defaultModel = defaultModel; 20 | } 21 | 22 | @Override 23 | public MapCodec getCodec() { 24 | return CODEC; 25 | } 26 | 27 | @Override 28 | public void resolve(Resolver resolver) { 29 | this.defaultModel.resolve(resolver); 30 | } 31 | 32 | @Override 33 | public ItemModel bake(ItemModel.BakeContext context) { 34 | return new BookBakedModel(this.defaultModel.bake(context)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/client/UnreadNotificationComponent.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.client; 2 | 3 | import io.wispforest.owo.ui.base.BaseComponent; 4 | import io.wispforest.owo.ui.core.OwoUIDrawContext; 5 | import io.wispforest.owo.ui.core.Sizing; 6 | import io.wispforest.owo.ui.parsing.UIParsing; 7 | import net.minecraft.client.render.RenderLayer; 8 | import net.minecraft.text.Text; 9 | import net.minecraft.util.Identifier; 10 | import org.w3c.dom.Element; 11 | 12 | public class UnreadNotificationComponent extends BaseComponent { 13 | 14 | private final Identifier bookTexture; 15 | 16 | public UnreadNotificationComponent(Identifier bookTexture, boolean plural) { 17 | this.bookTexture = bookTexture; 18 | this.tooltip(Text.translatable(plural ? "text.lavender.entry.multiple_unread" : "text.lavender.entry.unread")); 19 | } 20 | 21 | @Override 22 | protected int determineHorizontalContentSize(Sizing sizing) { 23 | return 8; 24 | } 25 | 26 | @Override 27 | protected int determineVerticalContentSize(Sizing sizing) { 28 | return 8; 29 | } 30 | 31 | @Override 32 | public void draw(OwoUIDrawContext context, int mouseX, int mouseY, float partialTicks, float delta) { 33 | context.push().translate(0, 0, 200); 34 | context.drawTexture( 35 | RenderLayer::getGuiTextured, 36 | this.bookTexture, 37 | this.x, 38 | this.y, 39 | (long) (System.currentTimeMillis() / 1500d) % 2 == 0 ? 188 : 204, 40 | 180, 41 | this.width, 42 | this.height, 43 | 16, 44 | 16, 45 | 512, 46 | 256 47 | ); 48 | context.pop(); 49 | } 50 | 51 | public static UnreadNotificationComponent parse(Element element) { 52 | UIParsing.expectAttributes(element, "book-texture", "plural"); 53 | return new UnreadNotificationComponent( 54 | UIParsing.parseIdentifier(element.getAttributeNode("book-texture")), 55 | UIParsing.parseBool(element.getAttributeNode("plural")) 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/md/ItemListComponent.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.md; 2 | 3 | import com.google.common.collect.ImmutableList; 4 | import com.google.gson.Gson; 5 | import com.google.gson.GsonBuilder; 6 | import com.google.gson.JsonElement; 7 | import com.mojang.serialization.JsonOps; 8 | import io.wispforest.owo.ui.component.ItemComponent; 9 | import io.wispforest.owo.ui.core.Component; 10 | import io.wispforest.owo.ui.parsing.UIModel; 11 | import io.wispforest.owo.ui.parsing.UIParsing; 12 | import net.minecraft.client.MinecraftClient; 13 | import net.minecraft.client.gui.tooltip.TooltipComponent; 14 | import net.minecraft.item.Item; 15 | import net.minecraft.item.ItemStack; 16 | import net.minecraft.recipe.Ingredient; 17 | import net.minecraft.recipe.display.SlotDisplay; 18 | import net.minecraft.recipe.display.SlotDisplayContexts; 19 | import net.minecraft.registry.Registries; 20 | import net.minecraft.registry.RegistryKeys; 21 | import net.minecraft.registry.entry.RegistryEntry; 22 | import net.minecraft.registry.tag.TagKey; 23 | import org.jetbrains.annotations.Nullable; 24 | import org.w3c.dom.Element; 25 | 26 | import java.util.ArrayList; 27 | import java.util.List; 28 | import java.util.Map; 29 | 30 | public class ItemListComponent extends ItemComponent { 31 | 32 | private static final Gson GSON = new GsonBuilder().setLenient().create(); 33 | 34 | private @Nullable ImmutableList items; 35 | 36 | private float time = 0f; 37 | private List extraTooltipSection = List.of(); 38 | private int currentStackIndex; 39 | 40 | public ItemListComponent() { 41 | super(ItemStack.EMPTY); 42 | this.setTooltipFromStack(true); 43 | } 44 | 45 | @Override 46 | public void update(float delta, int mouseX, int mouseY) { 47 | super.update(delta, mouseX, mouseY); 48 | 49 | this.time += delta; 50 | if (this.time >= 20) { 51 | this.time -= 20; 52 | this.updateForItems(); 53 | } 54 | } 55 | 56 | @Override 57 | public Component tooltip(List tooltip) { 58 | if (tooltip == null) return super.tooltip((List) null); 59 | 60 | tooltip = new ArrayList<>(tooltip); 61 | tooltip.addAll(this.extraTooltipSection); 62 | 63 | this.tooltip = tooltip; 64 | return this; 65 | } 66 | 67 | private void updateForItems() { 68 | if (this.items != null && !this.items.isEmpty()) { 69 | this.currentStackIndex = (this.currentStackIndex + 1) % this.items.size(); 70 | this.stack(this.items.get(this.currentStackIndex)); 71 | } else { 72 | this.currentStackIndex = 0; 73 | this.stack(ItemStack.EMPTY); 74 | } 75 | } 76 | 77 | public ItemListComponent slotDisplay(SlotDisplay display) { 78 | this.items = ImmutableList.copyOf(display.getStacks(SlotDisplayContexts.createParameters(MinecraftClient.getInstance().world))); 79 | this.updateForItems(); 80 | 81 | return this; 82 | } 83 | 84 | public ItemListComponent ingredient(Ingredient ingredient) { 85 | return this.slotDisplay(ingredient.toDisplay()); 86 | } 87 | 88 | public ItemListComponent tag(TagKey tag) { 89 | this.items = Registries.ITEM.getOptional(tag) 90 | .map(entries -> entries.stream().map(RegistryEntry::value).map(Item::getDefaultStack).collect(ImmutableList.toImmutableList())) 91 | .orElse(ImmutableList.of()); 92 | this.updateForItems(); 93 | 94 | return this; 95 | } 96 | 97 | public void extraTooltipSection(List section) { 98 | this.extraTooltipSection = section; 99 | this.updateTooltipForStack(); 100 | } 101 | 102 | @Override 103 | public void parseProperties(UIModel model, Element element, Map children) { 104 | super.parseProperties(model, element, children); 105 | 106 | UIParsing.apply(children, "tag", tagElement -> TagKey.of(RegistryKeys.ITEM, UIParsing.parseIdentifier(tagElement)), this::tag); 107 | UIParsing.apply( 108 | children, 109 | "ingredient", 110 | ingredientElement -> Ingredient.CODEC.parse(JsonOps.INSTANCE, GSON.fromJson(ingredientElement.getTextContent().strip(), JsonElement.class)).getOrThrow(), 111 | this::ingredient 112 | ); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/md/compiler/BookCompiler.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.md.compiler; 2 | 3 | import com.google.common.primitives.Ints; 4 | import io.wispforest.lavender.Lavender; 5 | import io.wispforest.lavender.client.LavenderBookScreen; 6 | import io.wispforest.lavendermd.compiler.OwoUICompiler; 7 | import io.wispforest.lavendermd.feature.OwoUITemplateFeature; 8 | import io.wispforest.owo.ui.component.LabelComponent; 9 | import io.wispforest.owo.ui.component.TextureComponent; 10 | import io.wispforest.owo.ui.container.Containers; 11 | import io.wispforest.owo.ui.container.FlowLayout; 12 | import io.wispforest.owo.ui.container.StackLayout; 13 | import io.wispforest.owo.ui.core.*; 14 | import io.wispforest.owo.ui.parsing.UIModel; 15 | import io.wispforest.owo.ui.parsing.UIModelLoader; 16 | import net.minecraft.client.MinecraftClient; 17 | import net.minecraft.text.*; 18 | import net.minecraft.util.Identifier; 19 | import org.jetbrains.annotations.NotNull; 20 | import org.jetbrains.annotations.Nullable; 21 | 22 | import java.util.Map; 23 | import java.util.function.Supplier; 24 | 25 | public class BookCompiler extends OwoUICompiler { 26 | 27 | private static final Style UNICODE_FONT_STYLE = Style.EMPTY.withFont(MinecraftClient.UNICODE_FONT_ID); 28 | 29 | private final FlowLayout resultContainer = Containers.verticalFlow(Sizing.content(), Sizing.content()); 30 | private final ComponentSource bookComponentSource; 31 | 32 | private boolean addImageBackground = false; 33 | 34 | public BookCompiler(ComponentSource bookComponentSource) { 35 | this.push(Containers.verticalFlow(Sizing.content(), Sizing.content())); 36 | this.bookComponentSource = bookComponentSource; 37 | } 38 | 39 | @Override 40 | protected LabelComponent makeLabel(MutableText text) { 41 | return new BookLabelComponent(text.styled(style -> style.withParent(UNICODE_FONT_STYLE))).color(Color.BLACK).lineHeight(7); 42 | } 43 | 44 | @Override 45 | public void visitImage(Identifier image, String description, boolean fit) { 46 | this.addImageBackground = fit; 47 | super.visitImage(image, description, fit); 48 | } 49 | 50 | @Override 51 | public void visitHorizontalRule() { 52 | this.append(this.bookComponentSource.builtinTemplate(Component.class, "horizontal-rule")); 53 | } 54 | 55 | public void visitPageBreak() { 56 | this.resultContainer.child(components.peek()); 57 | this.pop(); 58 | this.push(Containers.verticalFlow(Sizing.content(), Sizing.content())); 59 | } 60 | 61 | @Override 62 | protected void append(Component component) { 63 | if (this.addImageBackground) { 64 | this.addImageBackground = false; 65 | if (component instanceof StackLayout stack) { 66 | stack.children().get(0).margins(Insets.of(3)); 67 | stack.child(0, this.bookComponentSource.builtinTemplate(TextureComponent.class, "fit-image-background")); 68 | } 69 | } 70 | 71 | super.append(component); 72 | } 73 | 74 | @Override 75 | public ParentComponent compile() { 76 | this.pop(); 77 | return super.compile(); 78 | } 79 | 80 | @Override 81 | public String name() { 82 | return "lavender_builtin_book"; 83 | } 84 | 85 | public static class BookLabelComponent extends LabelComponent { 86 | 87 | private @Nullable LavenderBookScreen owner; 88 | 89 | protected BookLabelComponent(Text text) { 90 | super(text); 91 | this.margins(Insets.horizontal(1)); 92 | this.textClickHandler(style -> { 93 | if (style == null || this.owner == null) return false; 94 | 95 | var clickEvent = style.getClickEvent(); 96 | if (clickEvent != null && clickEvent.getAction() == ClickEvent.Action.OPEN_URL && clickEvent.getValue().startsWith("^")) { 97 | var linkTarget = this.resolveLinkTarget(clickEvent.getValue()); 98 | if (linkTarget != null && linkTarget.supplier != null) { 99 | this.owner.navPush(linkTarget.supplier.get()); 100 | return true; 101 | } else { 102 | return false; 103 | } 104 | } else { 105 | return this.owner.handleTextClick(style); 106 | } 107 | }); 108 | } 109 | 110 | public void setOwner(@NotNull LavenderBookScreen screen) { 111 | this.owner = screen; 112 | } 113 | 114 | protected @Nullable LinkTarget resolveLinkTarget(String link) { 115 | if (this.owner == null) return null; 116 | 117 | var rawLinkText = link.substring(1); 118 | int targetPage; 119 | 120 | int pageSeparatorIndex = rawLinkText.indexOf('#'); 121 | if (pageSeparatorIndex > 0) { 122 | var parsed = Ints.tryParse(rawLinkText.substring(pageSeparatorIndex + 1)); 123 | if (parsed == null) return null; 124 | 125 | targetPage = Math.max(0, (parsed - 1) / 2 * 2); 126 | rawLinkText = rawLinkText.substring(0, pageSeparatorIndex); 127 | } else { 128 | targetPage = 0; // effectively final my ass 129 | } 130 | 131 | var entryId = Identifier.tryParse(rawLinkText); 132 | if (entryId == null) return null; 133 | 134 | var entry = this.owner.book.entryById(entryId); 135 | if (entry != null) { 136 | return new LinkTarget( 137 | Text.literal(entry.title()), 138 | entry.canPlayerView(MinecraftClient.getInstance().player) 139 | ? () -> new LavenderBookScreen.NavFrame(new LavenderBookScreen.EntryPageSupplier(this.owner, entry), targetPage) 140 | : null 141 | ); 142 | } 143 | 144 | var category = this.owner.book.categoryById(entryId); 145 | if (category != null) { 146 | return new LinkTarget( 147 | Text.literal(category.title()), 148 | this.owner.book.shouldDisplayCategory(category, MinecraftClient.getInstance().player) 149 | ? () -> new LavenderBookScreen.NavFrame(new LavenderBookScreen.CategoryPageSupplier(this.owner, category), targetPage) 150 | : null 151 | ); 152 | } 153 | 154 | return null; 155 | } 156 | 157 | @Override 158 | protected Style styleAt(int mouseX, int mouseY) { 159 | var style = super.styleAt(mouseX, mouseY); 160 | if (style == null) return null; 161 | 162 | var event = style.getHoverEvent(); 163 | if (this.owner != null && event != null && event.getAction() == HoverEvent.Action.SHOW_TEXT && event.getValue(HoverEvent.Action.SHOW_TEXT).getString().startsWith("^")) { 164 | var rawLink = event.getValue(HoverEvent.Action.SHOW_TEXT).getString(); 165 | var linkTarget = this.resolveLinkTarget(rawLink); 166 | 167 | style = style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, linkTarget != null 168 | ? linkTarget.supplier != null ? linkTarget.title : Text.translatable("text.lavender.locked_internal_link") 169 | : Text.translatable("text.lavender.invalid_internal_link", rawLink) 170 | )); 171 | } 172 | 173 | return style; 174 | } 175 | 176 | protected record LinkTarget(Text title, @Nullable Supplier supplier) {} 177 | } 178 | 179 | @FunctionalInterface 180 | public interface ComponentSource extends OwoUITemplateFeature.TemplateProvider { 181 | C template(UIModel model, Class expectedComponentClass, String name, Map params); 182 | 183 | @Override 184 | default C template(Identifier model, Class expectedClass, String templateName, Map templateParams) { 185 | return this.template(UIModelLoader.get(model), expectedClass, templateName, templateParams); 186 | } 187 | 188 | default C builtinTemplate(Class expectedComponentClass, String name, Map params) { 189 | return this.template(UIModelLoader.get(Lavender.id("book_components")), expectedComponentClass, name, params); 190 | } 191 | 192 | default C builtinTemplate(Class expectedComponentClass, String name) { 193 | return this.builtinTemplate(expectedComponentClass, name, Map.of()); 194 | } 195 | 196 | default C template(UIModel model, Class expectedComponentClass, String name) { 197 | return this.template(model, expectedComponentClass, name, Map.of()); 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/md/features/AlternativesExtension.java.disabled: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.md.features; 2 | 3 | import io.wispforest.lavendermd.Lexer; 4 | import io.wispforest.lavendermd.compiler.MarkdownCompiler; 5 | import io.wispforest.lavendermd.compiler.OwoUICompiler; 6 | 7 | public class AlternativesExtension implements MarkdownExtension { 8 | @Override 9 | public String name() { 10 | return "alternatives"; 11 | } 12 | 13 | @Override 14 | public boolean supportsCompiler(MarkdownCompiler compiler) { 15 | return compiler instanceof OwoUICompiler; 16 | } 17 | 18 | @Override 19 | public void registerTokens(TokenRegistrar registrar) { 20 | registrar.registerToken((lexer, reader, tokens) -> { 21 | if (!lexer.expectString(reader, "")) return false; 22 | 23 | tokens.add(new OpenAlternativesToken()); 24 | return true; 25 | }, '<'); 26 | 27 | registrar.registerToken((lexer, reader, tokens) -> { 28 | if (!lexer.expectString(reader, "")) return false; 29 | 30 | tokens.add(new NextAlternativeToken()); 31 | return true; 32 | }, '<'); 33 | 34 | registrar.registerToken((lexer, reader, tokens) -> { 35 | if (!lexer.expectString(reader, "")) return false; 36 | 37 | tokens.add(new CloseAlternativesToken()); 38 | return true; 39 | }, '<'); 40 | } 41 | 42 | @Override 43 | public void registerNodes(NodeRegistrar registrar) { 44 | // registrar.registerNode((parser, trigger, tokens) -> { 45 | // 46 | // }); 47 | } 48 | 49 | private static class OpenAlternativesToken extends Lexer.Token { 50 | public OpenAlternativesToken() { 51 | super(""); 52 | } 53 | } 54 | 55 | private static class NextAlternativeToken extends Lexer.Token { 56 | public NextAlternativeToken() { 57 | super(""); 58 | } 59 | } 60 | 61 | private static class CloseAlternativesToken extends Lexer.Token { 62 | public CloseAlternativesToken() { 63 | super(""); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/md/features/ItemTagFeature.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.md.features; 2 | 3 | import io.wispforest.lavender.md.ItemListComponent; 4 | import io.wispforest.lavendermd.Lexer; 5 | import io.wispforest.lavendermd.MarkdownFeature; 6 | import io.wispforest.lavendermd.Parser; 7 | import io.wispforest.lavendermd.compiler.MarkdownCompiler; 8 | import io.wispforest.lavendermd.compiler.OwoUICompiler; 9 | import net.minecraft.item.Item; 10 | import net.minecraft.registry.Registries; 11 | import net.minecraft.registry.RegistryKeys; 12 | import net.minecraft.registry.tag.TagKey; 13 | import net.minecraft.util.Identifier; 14 | 15 | public class ItemTagFeature implements MarkdownFeature { 16 | 17 | @Override 18 | public String name() { 19 | return "item_tags"; 20 | } 21 | 22 | @Override 23 | public boolean supportsCompiler(MarkdownCompiler compiler) { 24 | return compiler instanceof OwoUICompiler; 25 | } 26 | 27 | @Override 28 | public void registerTokens(TokenRegistrar registrar) { 29 | registrar.registerToken((nibbler, tokens) -> { 30 | if (!nibbler.tryConsume("'); 33 | if (tagString == null) return false; 34 | 35 | var tagId = Identifier.tryParse(tagString); 36 | if (tagId == null) return false; 37 | 38 | var tagKey = TagKey.of(RegistryKeys.ITEM, tagId); 39 | if (Registries.ITEM.getOptional(tagKey).isEmpty()) return false; 40 | 41 | tokens.add(new ItemTagToken(tagString, tagKey)); 42 | return true; 43 | }, '<'); 44 | } 45 | 46 | @Override 47 | public void registerNodes(NodeRegistrar registrar) { 48 | registrar.registerNode( 49 | (parser, tagToken, tokens) -> new ItemStackNode(tagToken.tag), 50 | (token, tokens) -> token instanceof ItemTagToken tag ? tag : null 51 | ); 52 | } 53 | 54 | private static class ItemTagToken extends Lexer.Token { 55 | 56 | public final TagKey tag; 57 | 58 | public ItemTagToken(String content, TagKey tag) { 59 | super(content); 60 | this.tag = tag; 61 | } 62 | } 63 | 64 | private static class ItemStackNode extends Parser.Node { 65 | 66 | private final TagKey tag; 67 | 68 | public ItemStackNode(TagKey tag) { 69 | this.tag = tag; 70 | } 71 | 72 | @Override 73 | protected void visitStart(MarkdownCompiler compiler) { 74 | ((OwoUICompiler) compiler).visitComponent(new ItemListComponent().tag(this.tag)); 75 | } 76 | 77 | @Override 78 | protected void visitEnd(MarkdownCompiler compiler) {} 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/md/features/OwoUIModelFeature.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.md.features; 2 | 3 | import io.wispforest.lavender.Lavender; 4 | import io.wispforest.lavendermd.Lexer; 5 | import io.wispforest.lavendermd.MarkdownFeature; 6 | import io.wispforest.lavendermd.Parser; 7 | import io.wispforest.lavendermd.compiler.MarkdownCompiler; 8 | import io.wispforest.lavendermd.compiler.OwoUICompiler; 9 | import io.wispforest.owo.ui.component.Components; 10 | import io.wispforest.owo.ui.container.Containers; 11 | import io.wispforest.owo.ui.core.Component; 12 | import io.wispforest.owo.ui.core.Insets; 13 | import io.wispforest.owo.ui.core.Sizing; 14 | import io.wispforest.owo.ui.core.Surface; 15 | import io.wispforest.owo.ui.parsing.UIModel; 16 | import net.minecraft.text.Text; 17 | import org.xml.sax.SAXException; 18 | 19 | import javax.xml.parsers.ParserConfigurationException; 20 | import java.io.ByteArrayInputStream; 21 | import java.io.IOException; 22 | import java.nio.charset.StandardCharsets; 23 | import java.util.Map; 24 | 25 | public class OwoUIModelFeature implements MarkdownFeature { 26 | 27 | @Override 28 | public String name() { 29 | return "owo_ui_models"; 30 | } 31 | 32 | @Override 33 | public boolean supportsCompiler(MarkdownCompiler compiler) { 34 | return compiler instanceof OwoUICompiler; 35 | } 36 | 37 | @Override 38 | public void registerTokens(TokenRegistrar registrar) { 39 | registrar.registerToken((nibbler, tokens) -> { 40 | if (!nibbler.tryConsume("```xml owo-ui")) return false; 41 | 42 | var content = nibbler.consumeUntil('`'); 43 | if (content == null || !nibbler.tryConsume("``")) return false; 44 | 45 | tokens.add(new UIModelToken(content, content)); 46 | return true; 47 | }, '`'); 48 | } 49 | 50 | @Override 51 | public void registerNodes(NodeRegistrar registrar) { 52 | registrar.registerNode( 53 | (parser, stackToken, tokens) -> new UIModelNode(stackToken.xmlContent), 54 | (token, tokens) -> token instanceof UIModelToken model ? model : null 55 | ); 56 | } 57 | 58 | private static class UIModelToken extends Lexer.Token { 59 | 60 | public final String xmlContent; 61 | 62 | public UIModelToken(String content, String xmlContent) { 63 | super(content); 64 | this.xmlContent = xmlContent; 65 | } 66 | } 67 | 68 | private static class UIModelNode extends Parser.Node { 69 | 70 | private static final String MODEL_TEMPLATE = """ 71 | 72 | 73 | 76 | 77 | 78 | """; 79 | 80 | private final String modelString; 81 | 82 | public UIModelNode(String xmlContent) { 83 | this.modelString = MODEL_TEMPLATE.replaceFirst("\\{\\{template-content}}", xmlContent); 84 | } 85 | 86 | @Override 87 | protected void visitStart(MarkdownCompiler compiler) { 88 | try { 89 | var model = UIModel.load(new ByteArrayInputStream(this.modelString.getBytes(StandardCharsets.UTF_8))); 90 | ((OwoUICompiler) compiler).visitComponent(model.expandTemplate(Component.class, "__model-feature-generated__", Map.of())); 91 | } catch (Exception e) { 92 | Lavender.LOGGER.warn("Failed to build owo-ui model markdown element", e); 93 | ((OwoUICompiler) compiler).visitComponent( 94 | Containers.verticalFlow(Sizing.fill(100), Sizing.content()) 95 | .child(Components.label(Text.literal(e.getMessage())).horizontalSizing(Sizing.fill(100))) 96 | .padding(Insets.of(10)) 97 | .surface(Surface.flat(0x77A00000).and(Surface.outline(0x77FF0000))) 98 | ); 99 | } 100 | 101 | } 102 | 103 | @Override 104 | protected void visitEnd(MarkdownCompiler compiler) {} 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/md/features/PageBreakFeature.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.md.features; 2 | 3 | import io.wispforest.lavender.md.compiler.BookCompiler; 4 | import io.wispforest.lavendermd.Lexer; 5 | import io.wispforest.lavendermd.MarkdownFeature; 6 | import io.wispforest.lavendermd.Parser; 7 | import io.wispforest.lavendermd.compiler.MarkdownCompiler; 8 | 9 | public class PageBreakFeature implements MarkdownFeature { 10 | 11 | @Override 12 | public String name() { 13 | return "book_page_breaks"; 14 | } 15 | 16 | @Override 17 | public boolean supportsCompiler(MarkdownCompiler compiler) { 18 | return compiler instanceof BookCompiler; 19 | } 20 | 21 | @Override 22 | public void registerTokens(TokenRegistrar registrar) { 23 | registrar.registerToken((nibbler, tokens) -> { 24 | if (!nibbler.expect(-1, '\n') || !nibbler.expect(-2, '\n')) return false; 25 | if (!nibbler.tryConsume(";;;;;\n\n")) return false; 26 | 27 | tokens.add(new PageBreakToken()); 28 | return true; 29 | }, ';'); 30 | } 31 | 32 | @Override 33 | public void registerNodes(NodeRegistrar registrar) { 34 | registrar.registerNode( 35 | (parser, trigger, tokens) -> new PageBreakNode(), 36 | (token, tokenListNibbler) -> token instanceof PageBreakToken pageBreak ? pageBreak : null 37 | ); 38 | } 39 | 40 | private static class PageBreakToken extends Lexer.Token { 41 | public PageBreakToken() { 42 | super(";;;;;\n\n"); 43 | } 44 | 45 | @Override 46 | public boolean isBoundary() { 47 | return true; 48 | } 49 | } 50 | 51 | private static class PageBreakNode extends Parser.Node { 52 | @Override 53 | protected void visitStart(MarkdownCompiler compiler) { 54 | ((BookCompiler) compiler).visitPageBreak(); 55 | } 56 | 57 | @Override 58 | protected void visitEnd(MarkdownCompiler compiler) {} 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/md/features/RecipeFeature.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.md.features; 2 | 3 | import io.wispforest.lavender.LavenderClientRecipeCache; 4 | import io.wispforest.lavender.md.ItemListComponent; 5 | import io.wispforest.lavender.md.compiler.BookCompiler; 6 | import io.wispforest.lavendermd.Lexer; 7 | import io.wispforest.lavendermd.MarkdownFeature; 8 | import io.wispforest.lavendermd.Parser; 9 | import io.wispforest.lavendermd.compiler.MarkdownCompiler; 10 | import io.wispforest.lavendermd.compiler.OwoUICompiler; 11 | import io.wispforest.owo.ui.component.Components; 12 | import io.wispforest.owo.ui.component.ItemComponent; 13 | import io.wispforest.owo.ui.container.Containers; 14 | import io.wispforest.owo.ui.core.*; 15 | import net.minecraft.client.MinecraftClient; 16 | import net.minecraft.item.ItemStack; 17 | import net.minecraft.item.Items; 18 | import net.minecraft.recipe.*; 19 | import net.minecraft.recipe.display.SlotDisplayContexts; 20 | import net.minecraft.registry.Registries; 21 | import net.minecraft.text.Text; 22 | import net.minecraft.util.Identifier; 23 | import net.minecraft.util.context.ContextParameterMap; 24 | import org.jetbrains.annotations.NotNull; 25 | import org.jetbrains.annotations.Nullable; 26 | 27 | import java.util.HashMap; 28 | import java.util.List; 29 | import java.util.Map; 30 | 31 | public class RecipeFeature implements MarkdownFeature { 32 | 33 | private final BookCompiler.ComponentSource bookComponentSource; 34 | private final Map, RecipePreviewBuilder> previewBuilders; 35 | 36 | public static final RecipePreviewBuilder CRAFTING_PREVIEW_BUILDER = new RecipePreviewBuilder<>() { 37 | @Override 38 | public @NotNull Component buildRecipePreview(BookCompiler.ComponentSource componentSource, ContextParameterMap slotContext, RecipeEntry recipeEntry) { 39 | var recipeComponent = componentSource.builtinTemplate(ParentComponent.class, "crafting-recipe"); 40 | var value = recipeEntry.value(); 41 | 42 | this.populateIngredientsGrid(recipeEntry, recipeComponent.childById(ParentComponent.class, "input-grid"), 3, 3); 43 | recipeComponent.childById(ItemComponent.class, "output").stack(value.getDisplays().getFirst().result().getFirst(slotContext)); 44 | 45 | return recipeComponent; 46 | } 47 | }; 48 | 49 | public static final RecipePreviewBuilder SMELTING_PREVIEW_BUILDER = (componentSource, slotContext, recipeEntry) -> { 50 | var recipe = recipeEntry.value(); 51 | var recipeComponent = componentSource.builtinTemplate(ParentComponent.class, "smelting-recipe"); 52 | 53 | recipeComponent.childById(ItemListComponent.class, "input").ingredient(recipe.ingredient()); 54 | recipeComponent.childById(ItemComponent.class, "output").stack(recipe.getDisplays().getFirst().result().getFirst(slotContext)); 55 | 56 | var workstation = ItemStack.EMPTY; 57 | if (recipe instanceof SmeltingRecipe) workstation = Items.FURNACE.getDefaultStack(); 58 | if (recipe instanceof BlastingRecipe) workstation = Items.BLAST_FURNACE.getDefaultStack(); 59 | if (recipe instanceof SmokingRecipe) workstation = Items.SMOKER.getDefaultStack(); 60 | if (recipe instanceof CampfireCookingRecipe) workstation = Items.CAMPFIRE.getDefaultStack(); 61 | recipeComponent.childById(ItemComponent.class, "workstation").stack(workstation); 62 | 63 | return recipeComponent; 64 | }; 65 | 66 | public static final RecipePreviewBuilder SMITHING_PREVIEW_BUILDER = (componentSource, slotContext, recipeEntry) -> { 67 | var recipe = recipeEntry.value(); 68 | var recipeComponent = componentSource.builtinTemplate(ParentComponent.class, "smithing-recipe"); 69 | 70 | recipe.template().ifPresent(ingredient -> recipeComponent.childById(ItemListComponent.class, "input-1").ingredient(ingredient)); 71 | recipe.base().ifPresent(ingredient -> recipeComponent.childById(ItemListComponent.class, "input-2").ingredient(ingredient)); 72 | recipe.addition().ifPresent(ingredient -> recipeComponent.childById(ItemListComponent.class, "input-3").ingredient(ingredient)); 73 | 74 | recipeComponent.childById(ItemComponent.class, "output").stack(recipe.getDisplays().getFirst().result().getFirst(slotContext)); 75 | 76 | return recipeComponent; 77 | }; 78 | 79 | public static final RecipePreviewBuilder STONECUTTING_PREVIEW_BUILDER = (componentSource, slotContext, recipeEntry) -> { 80 | var recipe = recipeEntry.value(); 81 | var recipeComponent = componentSource.builtinTemplate(ParentComponent.class, "stonecutting-recipe"); 82 | 83 | recipeComponent.childById(ItemListComponent.class, "input").ingredient(recipe.ingredient()); 84 | recipeComponent.childById(ItemComponent.class, "output").stack(recipe.getDisplays().getFirst().result().getFirst(slotContext)); 85 | 86 | return recipeComponent; 87 | }; 88 | 89 | public RecipeFeature(BookCompiler.ComponentSource bookComponentSource, @Nullable Map, RecipePreviewBuilder> previewBuilders) { 90 | this.bookComponentSource = bookComponentSource; 91 | 92 | this.previewBuilders = new HashMap<>(previewBuilders != null ? previewBuilders : Map.of()); 93 | this.previewBuilders.putIfAbsent(RecipeType.CRAFTING, CRAFTING_PREVIEW_BUILDER); 94 | this.previewBuilders.putIfAbsent(RecipeType.SMELTING, SMELTING_PREVIEW_BUILDER); 95 | this.previewBuilders.putIfAbsent(RecipeType.BLASTING, SMELTING_PREVIEW_BUILDER); 96 | this.previewBuilders.putIfAbsent(RecipeType.SMOKING, SMELTING_PREVIEW_BUILDER); 97 | this.previewBuilders.putIfAbsent(RecipeType.CAMPFIRE_COOKING, SMELTING_PREVIEW_BUILDER); 98 | this.previewBuilders.putIfAbsent(RecipeType.SMITHING, SMITHING_PREVIEW_BUILDER); 99 | this.previewBuilders.putIfAbsent(RecipeType.STONECUTTING, STONECUTTING_PREVIEW_BUILDER); 100 | } 101 | 102 | @Override 103 | public String name() { 104 | return "recipes"; 105 | } 106 | 107 | @Override 108 | public boolean supportsCompiler(MarkdownCompiler compiler) { 109 | return compiler instanceof OwoUICompiler; 110 | } 111 | 112 | @Override 113 | public void registerTokens(TokenRegistrar registrar) { 114 | registrar.registerToken((nibbler, tokens) -> { 115 | if (!nibbler.tryConsume("'); 118 | if (recipeIdString == null) return false; 119 | 120 | var recipeId = Identifier.tryParse(recipeIdString); 121 | if (recipeId == null) return false; 122 | 123 | var recipe = LavenderClientRecipeCache.getOrFetchRecipe(recipeId); 124 | if (recipe.isEmpty()) return false; 125 | 126 | //noinspection unchecked 127 | tokens.add(new RecipeToken(recipeIdString, (RecipeEntry>) recipe.get())); 128 | return true; 129 | }, '<'); 130 | } 131 | 132 | @Override 133 | public void registerNodes(NodeRegistrar registrar) { 134 | registrar.registerNode( 135 | (parser, recipeToken, tokens) -> new RecipeNode(recipeToken.recipe), 136 | (token, tokens) -> token instanceof RecipeToken recipe ? recipe : null 137 | ); 138 | } 139 | 140 | private static class RecipeToken extends Lexer.Token { 141 | 142 | public final RecipeEntry> recipe; 143 | 144 | public RecipeToken(String content, RecipeEntry> recipe) { 145 | super(content); 146 | this.recipe = recipe; 147 | } 148 | } 149 | 150 | private class RecipeNode extends Parser.Node { 151 | 152 | private final RecipeEntry> recipe; 153 | 154 | public RecipeNode(RecipeEntry> recipe) { 155 | this.recipe = recipe; 156 | } 157 | 158 | @Override 159 | @SuppressWarnings({"rawtypes", "unchecked"}) 160 | protected void visitStart(MarkdownCompiler compiler) { 161 | var previewBuilder = (RecipePreviewBuilder) RecipeFeature.this.previewBuilders.get(this.recipe.value().getType()); 162 | if (previewBuilder != null) { 163 | ((OwoUICompiler) compiler).visitComponent(previewBuilder.buildRecipePreview(RecipeFeature.this.bookComponentSource, SlotDisplayContexts.createParameters(MinecraftClient.getInstance().world), this.recipe)); 164 | } else { 165 | ((OwoUICompiler) compiler).visitComponent( 166 | Containers.verticalFlow(Sizing.fill(100), Sizing.content()) 167 | .child(Components.label(Text.literal("No preview builder registered for recipe type '" + Registries.RECIPE_TYPE.getId(this.recipe.value().getType()) + "'")).horizontalSizing(Sizing.fill(100))) 168 | .padding(Insets.of(10)) 169 | .surface(Surface.flat(0x77A00000).and(Surface.outline(0x77FF0000))) 170 | ); 171 | } 172 | } 173 | 174 | @Override 175 | protected void visitEnd(MarkdownCompiler compiler) {} 176 | } 177 | 178 | @FunctionalInterface 179 | public interface RecipePreviewBuilder> { 180 | @NotNull 181 | Component buildRecipePreview(BookCompiler.ComponentSource componentSource, ContextParameterMap slotContext, RecipeEntry recipeEntry); 182 | 183 | default void populateIngredients(RecipeEntry recipe, List ingredients, ParentComponent componentContainer) { 184 | for (int i = 0; i < ingredients.size(); i++) { 185 | if (!(componentContainer.children().get(i) instanceof ItemListComponent ingredient)) continue; 186 | ingredient.ingredient(ingredients.get(i)); 187 | } 188 | } 189 | 190 | default void populateIngredientsGrid(RecipeEntry recipe, ParentComponent componentContainer, int gridWidth, int gridHeight) { 191 | var ingredients = recipe.value().getIngredientPlacement().getIngredients(); 192 | RecipeGridAligner.alignRecipeToGrid(gridWidth, gridHeight, recipe.value(), recipe.value().getIngredientPlacement().getPlacementSlots(), (input, index, x, y) -> { 193 | if (!(componentContainer.children().get(index) instanceof ItemListComponent ingredient)) return; 194 | 195 | if (input >= 0) { 196 | ingredient.ingredient(ingredients.get(input)); 197 | } 198 | }); 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/md/features/StructureFeature.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.md.features; 2 | 3 | import com.google.common.primitives.Ints; 4 | import io.wispforest.lavender.book.StructureComponent; 5 | import io.wispforest.lavender.client.StructureOverlayRenderer; 6 | import io.wispforest.lavender.md.compiler.BookCompiler; 7 | import io.wispforest.lavender.structure.LavenderStructures; 8 | import io.wispforest.lavender.structure.StructureTemplate; 9 | import io.wispforest.lavendermd.Lexer; 10 | import io.wispforest.lavendermd.MarkdownFeature; 11 | import io.wispforest.lavendermd.Parser; 12 | import io.wispforest.lavendermd.compiler.MarkdownCompiler; 13 | import io.wispforest.lavendermd.compiler.OwoUICompiler; 14 | import io.wispforest.owo.ui.component.SlimSliderComponent; 15 | import io.wispforest.owo.ui.core.ParentComponent; 16 | import net.minecraft.text.Text; 17 | import net.minecraft.util.Identifier; 18 | 19 | import java.util.Map; 20 | 21 | public class StructureFeature implements MarkdownFeature { 22 | 23 | private final BookCompiler.ComponentSource bookComponentSource; 24 | 25 | public StructureFeature(BookCompiler.ComponentSource bookComponentSource) { 26 | this.bookComponentSource = bookComponentSource; 27 | } 28 | 29 | @Override 30 | public String name() { 31 | return "structures"; 32 | } 33 | 34 | @Override 35 | public boolean supportsCompiler(MarkdownCompiler compiler) { 36 | return compiler instanceof OwoUICompiler; 37 | } 38 | 39 | @Override 40 | public void registerTokens(TokenRegistrar registrar) { 41 | registrar.registerToken(structureLexer("structure", true), '<'); 42 | registrar.registerToken(structureLexer("structure-visualizer", false), '<'); 43 | } 44 | 45 | private Lexer.LexFunction structureLexer(String token, boolean placeable) { 46 | var marker = "<" + token + ";"; 47 | return (nibbler, tokens) -> { 48 | if (!nibbler.tryConsume(marker)) return false; 49 | 50 | var structureIdString = nibbler.consumeUntil('>'); 51 | if (structureIdString == null) return false; 52 | 53 | int angle = 35; 54 | if (structureIdString.matches("-?\\d+;.+")) { 55 | var parsedAngle = Ints.tryParse(structureIdString.substring(0, structureIdString.indexOf(';'))); 56 | if (parsedAngle == null) return false; 57 | 58 | angle = parsedAngle; 59 | structureIdString = structureIdString.substring(structureIdString.indexOf(';') + 1); 60 | } 61 | 62 | var structureId = Identifier.tryParse(structureIdString); 63 | if (structureId == null) return false; 64 | 65 | var structure = LavenderStructures.get(structureId); 66 | if (structure == null) return false; 67 | 68 | tokens.add(new StructureToken(structureIdString, structure, angle, placeable)); 69 | return true; 70 | }; 71 | } 72 | 73 | @Override 74 | public void registerNodes(NodeRegistrar registrar) { 75 | registrar.registerNode( 76 | (parser, structureToken, tokens) -> new StructureNode(structureToken.structure, structureToken.angle, structureToken.placeable), 77 | (token, tokens) -> token instanceof StructureToken structure ? structure : null 78 | ); 79 | } 80 | 81 | private static class StructureToken extends Lexer.Token { 82 | 83 | public final StructureTemplate structure; 84 | public final int angle; 85 | public final boolean placeable; 86 | 87 | public StructureToken(String content, StructureTemplate structure, int angle, boolean placeable) { 88 | super(content); 89 | this.structure = structure; 90 | this.angle = angle; 91 | this.placeable = placeable; 92 | } 93 | } 94 | 95 | private class StructureNode extends Parser.Node { 96 | 97 | private final StructureTemplate structure; 98 | private final int angle; 99 | private final boolean placeable; 100 | 101 | public StructureNode(StructureTemplate structure, int angle, boolean placeable) { 102 | this.structure = structure; 103 | this.angle = angle; 104 | this.placeable = placeable; 105 | } 106 | 107 | @Override 108 | protected void visitStart(MarkdownCompiler compiler) { 109 | var structureComponent = StructureFeature.this.bookComponentSource.builtinTemplate( 110 | ParentComponent.class, 111 | this.structure.ySize > 1 ? "structure-preview-with-layers" : "structure-preview", 112 | Map.of("structure", this.structure.id.toString(), "angle", String.valueOf(this.angle)) 113 | ); 114 | 115 | var structurePreview = structureComponent.childById(StructureComponent.class, "structure").placeable(this.placeable); 116 | var layerSlider = structureComponent.childById(SlimSliderComponent.class, "layer-slider"); 117 | 118 | if (layerSlider != null) { 119 | layerSlider.max(0).min(this.structure.ySize).tooltipSupplier(layer -> { 120 | return layer > 0 121 | ? Text.translatable("text.lavender.structure_component.layer_tooltip", layer.intValue()) 122 | : Text.translatable("text.lavender.structure_component.all_layers_tooltip"); 123 | }).onChanged().subscribe(layer -> { 124 | structurePreview.visibleLayer((int) layer - 1); 125 | }); 126 | 127 | layerSlider.value(StructureOverlayRenderer.getLayerRestriction(this.structure.id) + 1); 128 | } 129 | 130 | ((OwoUICompiler) compiler).visitComponent(structureComponent); 131 | } 132 | 133 | @Override 134 | protected void visitEnd(MarkdownCompiler compiler) {} 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/mixin/ClientAdvancementManagerMixin.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.mixin; 2 | 3 | import com.google.common.collect.Iterables; 4 | import com.llamalad7.mixinextras.sugar.Share; 5 | import com.llamalad7.mixinextras.sugar.ref.LocalRef; 6 | import io.wispforest.lavender.book.Book; 7 | import io.wispforest.lavender.book.BookLoader; 8 | import io.wispforest.lavender.book.ClientNewEntriesUnlockedCallback; 9 | import it.unimi.dsi.fastutil.objects.Reference2IntMap; 10 | import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; 11 | import net.minecraft.client.MinecraftClient; 12 | import net.minecraft.client.network.ClientAdvancementManager; 13 | import net.minecraft.network.packet.s2c.play.AdvancementUpdateS2CPacket; 14 | import org.spongepowered.asm.mixin.Final; 15 | import org.spongepowered.asm.mixin.Mixin; 16 | import org.spongepowered.asm.mixin.Shadow; 17 | import org.spongepowered.asm.mixin.Unique; 18 | import org.spongepowered.asm.mixin.injection.At; 19 | import org.spongepowered.asm.mixin.injection.Inject; 20 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 21 | 22 | @Mixin(ClientAdvancementManager.class) 23 | public class ClientAdvancementManagerMixin { 24 | 25 | @Shadow 26 | @Final 27 | private MinecraftClient client; 28 | 29 | @Unique 30 | private boolean receivedInitialPacket = false; 31 | 32 | @Inject(method = "onAdvancements", at = @At("HEAD")) 33 | private void captureAdvancementsPreUpdate(AdvancementUpdateS2CPacket packet, CallbackInfo ci, @Share("entriesPreUpdate") LocalRef> entriesPreUpdate) { 34 | if (!this.receivedInitialPacket) { 35 | return; 36 | } 37 | 38 | var entryCountByBook = new Reference2IntOpenHashMap(); 39 | 40 | for (var book : Iterables.filter(BookLoader.loadedBooks(), book -> book.newEntriesToast() != null)) { 41 | entryCountByBook.put(book, book.countVisibleEntries(this.client.player)); 42 | } 43 | 44 | entriesPreUpdate.set(entryCountByBook); 45 | } 46 | 47 | @Inject(method = "onAdvancements", at = @At("TAIL")) 48 | private void checkForNewAdvancements(AdvancementUpdateS2CPacket packet, CallbackInfo ci, @Share("entriesPreUpdate") LocalRef> entriesPreUpdate) { 49 | if (!this.receivedInitialPacket) { 50 | this.receivedInitialPacket = true; 51 | return; 52 | } 53 | 54 | var entryCountByBook = entriesPreUpdate.get(); 55 | 56 | for (var book : Iterables.filter(BookLoader.loadedBooks(), book -> book.newEntriesToast() != null)) { 57 | var newEntryCount = book.countVisibleEntries(this.client.player) - entryCountByBook.getInt(book); 58 | if (newEntryCount > 0) { 59 | ClientNewEntriesUnlockedCallback.EVENT.invoker().newEntriesUnlocked(this.client, book, newEntryCount); 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/mixin/CreativeInventoryScreenMixin.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.mixin; 2 | 3 | import io.wispforest.lavender.client.AssociatedEntryTooltipComponent; 4 | import net.minecraft.client.gui.screen.ingame.CreativeInventoryScreen; 5 | import net.minecraft.item.ItemStack; 6 | import net.minecraft.text.Text; 7 | import org.spongepowered.asm.mixin.Mixin; 8 | import org.spongepowered.asm.mixin.injection.At; 9 | import org.spongepowered.asm.mixin.injection.Inject; 10 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 11 | 12 | import java.lang.ref.WeakReference; 13 | import java.util.List; 14 | 15 | @Mixin(CreativeInventoryScreen.class) 16 | public class CreativeInventoryScreenMixin { 17 | 18 | @Inject(method = "getTooltipFromItem", at = @At("HEAD")) 19 | private void captureTooltipStack(ItemStack stack, CallbackInfoReturnable> cir) { 20 | AssociatedEntryTooltipComponent.tooltipStack = new WeakReference<>(stack); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/mixin/DrawContextMixin.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.mixin; 2 | 3 | import com.llamalad7.mixinextras.sugar.Local; 4 | import com.llamalad7.mixinextras.sugar.ref.LocalRef; 5 | import io.wispforest.lavender.book.LavenderBookItem; 6 | import io.wispforest.lavender.book.BookLoader; 7 | import io.wispforest.lavender.client.AssociatedEntryTooltipComponent; 8 | import io.wispforest.lavender.client.LavenderBookScreen; 9 | import io.wispforest.owo.ui.util.Delta; 10 | import net.minecraft.client.MinecraftClient; 11 | import net.minecraft.client.font.TextRenderer; 12 | import net.minecraft.client.gui.DrawContext; 13 | import net.minecraft.client.gui.screen.Screen; 14 | import net.minecraft.client.gui.tooltip.TooltipComponent; 15 | import net.minecraft.client.gui.tooltip.TooltipPositioner; 16 | import net.minecraft.util.Identifier; 17 | import org.jetbrains.annotations.Nullable; 18 | import org.spongepowered.asm.mixin.Mixin; 19 | import org.spongepowered.asm.mixin.injection.At; 20 | import org.spongepowered.asm.mixin.injection.Inject; 21 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 22 | 23 | import java.util.ArrayList; 24 | import java.util.List; 25 | 26 | import static io.wispforest.lavender.client.AssociatedEntryTooltipComponent.entryTriggerProgress; 27 | 28 | @Mixin(DrawContext.class) 29 | public class DrawContextMixin { 30 | 31 | @Inject(method = "drawTooltip(Lnet/minecraft/client/font/TextRenderer;Ljava/util/List;IILnet/minecraft/client/gui/tooltip/TooltipPositioner;Lnet/minecraft/util/Identifier;)V", at = @At("HEAD")) 32 | private void injectTooltipComponents(TextRenderer textRenderer, List components, int x, int y, TooltipPositioner positioner, @Nullable Identifier texture, CallbackInfo ci, @Local(argsOnly = true) LocalRef> componentsRef) { 33 | var client = MinecraftClient.getInstance(); 34 | 35 | if (AssociatedEntryTooltipComponent.tooltipStack != null && AssociatedEntryTooltipComponent.tooltipStack.get() != null) { 36 | var stack = AssociatedEntryTooltipComponent.tooltipStack.get(); 37 | AssociatedEntryTooltipComponent.tooltipStack = null; 38 | 39 | for (var book : BookLoader.loadedBooks()) { 40 | var associatedEntry = book.entryByAssociatedItem(stack); 41 | if (associatedEntry == null || !associatedEntry.canPlayerView(client.player)) continue; 42 | 43 | int bookIndex = -1; 44 | for (int i = 0; i < 9; i++) { 45 | if (LavenderBookItem.bookOf(client.player.getInventory().getStack(i)) == book) { 46 | bookIndex = i; 47 | break; 48 | } 49 | } 50 | 51 | if (LavenderBookItem.bookOf(client.player.getOffHandStack()) == book) { 52 | bookIndex = -69; 53 | } 54 | 55 | if (bookIndex == -1) return; 56 | 57 | components = new ArrayList<>(components); 58 | components.add(new AssociatedEntryTooltipComponent(LavenderBookItem.itemOf(book), associatedEntry, entryTriggerProgress)); 59 | componentsRef.set(components); 60 | 61 | entryTriggerProgress += Delta.compute(entryTriggerProgress, Screen.hasAltDown() ? 1.35f : 0f, client.getRenderTickCounter().getLastFrameDuration() * .125f); 62 | 63 | if (entryTriggerProgress >= .95) { 64 | LavenderBookScreen.pushEntry(book, associatedEntry); 65 | client.setScreen(new LavenderBookScreen(book)); 66 | 67 | if (bookIndex >= 0) { 68 | client.player.getInventory().selectedSlot = bookIndex; 69 | } 70 | 71 | entryTriggerProgress = 0f; 72 | } 73 | 74 | return; 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/mixin/FramebufferMixin.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.mixin; 2 | 3 | import com.mojang.blaze3d.platform.GlStateManager; 4 | import io.wispforest.lavender.pond.LavenderFramebufferExtension; 5 | import net.minecraft.client.gl.Framebuffer; 6 | import net.minecraft.client.gl.ShaderProgramKey; 7 | import org.spongepowered.asm.mixin.Mixin; 8 | import org.spongepowered.asm.mixin.Unique; 9 | import org.spongepowered.asm.mixin.injection.At; 10 | import org.spongepowered.asm.mixin.injection.Inject; 11 | import org.spongepowered.asm.mixin.injection.ModifyArg; 12 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 13 | 14 | import java.util.function.Supplier; 15 | 16 | @Mixin(Framebuffer.class) 17 | public class FramebufferMixin implements LavenderFramebufferExtension { 18 | 19 | @Unique 20 | private Supplier blitProgram = null; 21 | 22 | @Unique 23 | private boolean enableDepthTest = false; 24 | 25 | @Override 26 | public void lavender$setBlitProgram(Supplier blitProgram) { 27 | this.blitProgram = blitProgram; 28 | } 29 | 30 | @Override 31 | public void lavender$enableDepthTest() { 32 | this.enableDepthTest = true; 33 | } 34 | 35 | @Inject(method = "drawInternal", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/platform/GlStateManager;_disableDepthTest()V")) 36 | private void weDeep(int width, int height, CallbackInfo ci) { 37 | if (!this.enableDepthTest) return; 38 | GlStateManager._enableDepthTest(); 39 | } 40 | 41 | @ModifyArg(method = "drawInternal", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/RenderSystem;setShader(Lnet/minecraft/client/gl/ShaderProgramKey;)Lnet/minecraft/client/gl/ShaderProgram;")) 42 | private ShaderProgramKey applyBlitProgram(ShaderProgramKey shaderProgramKey) { 43 | if (this.blitProgram == null) return shaderProgramKey; 44 | return this.blitProgram.get(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/mixin/HeldItemRendererMixin.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.mixin; 2 | 3 | import com.llamalad7.mixinextras.injector.ModifyExpressionValue; 4 | import io.wispforest.lavender.book.LavenderBookItem; 5 | import io.wispforest.lavender.client.LavenderBookScreen; 6 | import io.wispforest.lavender.client.OffhandBookRenderer; 7 | import net.minecraft.client.MinecraftClient; 8 | import net.minecraft.client.network.AbstractClientPlayerEntity; 9 | import net.minecraft.client.render.VertexConsumerProvider; 10 | import net.minecraft.client.render.item.HeldItemRenderer; 11 | import net.minecraft.client.util.math.MatrixStack; 12 | import net.minecraft.item.ItemStack; 13 | import net.minecraft.util.Hand; 14 | import org.spongepowered.asm.mixin.Mixin; 15 | import org.spongepowered.asm.mixin.injection.At; 16 | import org.spongepowered.asm.mixin.injection.Inject; 17 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 18 | 19 | @Mixin(HeldItemRenderer.class) 20 | public abstract class HeldItemRendererMixin { 21 | 22 | @ModifyExpressionValue(method = "renderFirstPersonItem", at = @At(value = "INVOKE", target = "Lnet/minecraft/item/ItemStack;contains(Lnet/minecraft/component/ComponentType;)Z")) 23 | private boolean injectMap(boolean original, AbstractClientPlayerEntity player, float tickDelta, float pitch, Hand hand, float swingProgress, ItemStack stack) { 24 | if (!(stack.getItem() instanceof LavenderBookItem) || LavenderBookItem.bookOf(stack) == null || hand == Hand.MAIN_HAND || MinecraftClient.getInstance().currentScreen instanceof LavenderBookScreen) { 25 | return original; 26 | } 27 | 28 | return true; 29 | } 30 | 31 | @Inject(method = "renderFirstPersonMap", at = @At("HEAD"), cancellable = true) 32 | private void injectBook(MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, ItemStack stack, CallbackInfo ci) { 33 | if (!(stack.getItem() instanceof LavenderBookItem)) return; 34 | ci.cancel(); 35 | 36 | OffhandBookRenderer.render(matrices, light); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/mixin/LifecycledResourceManagerMixin.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.mixin; 2 | 3 | import io.wispforest.lavender.pond.LavenderLifecycledResourceManagerExtension; 4 | import net.minecraft.resource.LifecycledResourceManagerImpl; 5 | import net.minecraft.resource.ResourceType; 6 | import org.spongepowered.asm.mixin.Mixin; 7 | import org.spongepowered.asm.mixin.Unique; 8 | import org.spongepowered.asm.mixin.injection.At; 9 | import org.spongepowered.asm.mixin.injection.Inject; 10 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 11 | 12 | import java.util.List; 13 | 14 | @Mixin(LifecycledResourceManagerImpl.class) 15 | public class LifecycledResourceManagerMixin implements LavenderLifecycledResourceManagerExtension { 16 | 17 | @Unique 18 | private ResourceType resourceType; 19 | 20 | @Inject(method = "", at = @At("TAIL")) 21 | private void captureResourceType(ResourceType type, List packs, CallbackInfo ci) { 22 | this.resourceType = type; 23 | } 24 | 25 | @Override 26 | public ResourceType lavender$resourceType() { 27 | return this.resourceType; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/mixin/MinecraftClientMixin.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.mixin; 2 | 3 | import io.wispforest.lavender.book.Book; 4 | import io.wispforest.lavender.book.LavenderBookItem; 5 | import io.wispforest.lavender.client.LavenderBookScreen; 6 | import io.wispforest.lavender.client.OffhandBookRenderer; 7 | import net.minecraft.client.MinecraftClient; 8 | import net.minecraft.client.gui.screen.Screen; 9 | import net.minecraft.client.network.ClientPlayerEntity; 10 | import org.jetbrains.annotations.Nullable; 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 | @Mixin(MinecraftClient.class) 18 | public class MinecraftClientMixin { 19 | 20 | @Shadow 21 | @Nullable 22 | public ClientPlayerEntity player; 23 | 24 | @Shadow 25 | @Nullable 26 | public Screen currentScreen; 27 | 28 | @Inject(method = "render", at = @At("HEAD")) 29 | private void onFrameStart(boolean tick, CallbackInfo ci) { 30 | if (this.player == null) return; 31 | 32 | Book bookToRender = null; 33 | var offhandStack = this.player.getOffHandStack(); 34 | if (offhandStack.getItem() instanceof LavenderBookItem && LavenderBookItem.bookOf(offhandStack) != null && !(this.currentScreen instanceof LavenderBookScreen)) { 35 | bookToRender = LavenderBookItem.bookOf(offhandStack); 36 | } 37 | 38 | OffhandBookRenderer.beginFrame(bookToRender); 39 | } 40 | 41 | @Inject(method = "render", at = @At("TAIL")) 42 | private void onFrameEnd(boolean tick, CallbackInfo ci) { 43 | if (this.player == null) return; 44 | OffhandBookRenderer.endFrame(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/mixin/MouseMixin.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.mixin; 2 | 3 | import io.wispforest.lavender.client.StructureOverlayRenderer; 4 | import net.minecraft.client.Mouse; 5 | import org.spongepowered.asm.mixin.Mixin; 6 | import org.spongepowered.asm.mixin.injection.At; 7 | import org.spongepowered.asm.mixin.injection.Inject; 8 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 9 | 10 | @Mixin(Mouse.class) 11 | public class MouseMixin { 12 | 13 | @Inject(method = "onMouseScroll", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MinecraftClient;getOverlay()Lnet/minecraft/client/gui/screen/Overlay;"), cancellable = true) 14 | private void captureMouseScroll(long window, double horizontal, double vertical, CallbackInfo ci) { 15 | if (!StructureOverlayRenderer.hasPending()) return; 16 | 17 | StructureOverlayRenderer.rotatePending(vertical > 0); 18 | ci.cancel(); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/mixin/RenderPhaseMixin.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.mixin; 2 | 3 | import com.llamalad7.mixinextras.injector.ModifyExpressionValue; 4 | import io.wispforest.lavender.client.LavenderClient; 5 | import net.minecraft.client.gl.Framebuffer; 6 | import net.minecraft.client.render.RenderPhase; 7 | import org.spongepowered.asm.mixin.Mixin; 8 | import org.spongepowered.asm.mixin.injection.At; 9 | 10 | @Mixin(RenderPhase.class) 11 | public class RenderPhaseMixin { 12 | 13 | @ModifyExpressionValue(method = {"method_62272", "method_34555", "method_29377"}, at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MinecraftClient;getFramebuffer()Lnet/minecraft/client/gl/Framebuffer;")) 14 | private static Framebuffer injectProperRenderTarget(Framebuffer original) { 15 | if (LavenderClient.mainTargetOverride != null) { 16 | return LavenderClient.mainTargetOverride; 17 | } 18 | 19 | return original; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/mixin/ScreenMixin.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.mixin; 2 | 3 | import io.wispforest.lavender.client.AssociatedEntryTooltipComponent; 4 | import net.minecraft.client.MinecraftClient; 5 | import net.minecraft.client.gui.screen.Screen; 6 | import net.minecraft.item.ItemStack; 7 | import net.minecraft.text.Text; 8 | import org.jetbrains.annotations.Nullable; 9 | import org.spongepowered.asm.mixin.Mixin; 10 | import org.spongepowered.asm.mixin.Shadow; 11 | import org.spongepowered.asm.mixin.injection.At; 12 | import org.spongepowered.asm.mixin.injection.Inject; 13 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 14 | 15 | import java.lang.ref.WeakReference; 16 | import java.util.List; 17 | 18 | @Mixin(Screen.class) 19 | public class ScreenMixin { 20 | 21 | @Shadow 22 | @Nullable 23 | protected MinecraftClient client; 24 | 25 | @Inject(method = "getTooltipFromItem", at = @At("HEAD")) 26 | private static void captureTooltipStack(MinecraftClient client, ItemStack stack, CallbackInfoReturnable> cir) { 27 | AssociatedEntryTooltipComponent.tooltipStack = new WeakReference<>(stack); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/mixin/SimpleResourceReloadMixin.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.mixin; 2 | 3 | import io.wispforest.lavender.book.BookLoader; 4 | import io.wispforest.lavender.pond.LavenderLifecycledResourceManagerExtension; 5 | import net.minecraft.client.MinecraftClient; 6 | import net.minecraft.resource.ResourceManager; 7 | import net.minecraft.resource.ResourceType; 8 | import net.minecraft.resource.SimpleResourceReload; 9 | import org.spongepowered.asm.mixin.Mixin; 10 | import org.spongepowered.asm.mixin.injection.At; 11 | import org.spongepowered.asm.mixin.injection.Coerce; 12 | import org.spongepowered.asm.mixin.injection.Inject; 13 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 14 | 15 | import java.util.List; 16 | import java.util.concurrent.CompletableFuture; 17 | import java.util.concurrent.Executor; 18 | 19 | @Mixin(SimpleResourceReload.class) 20 | public class SimpleResourceReloadMixin { 21 | 22 | @Inject(method = "", at = @At(value = "INVOKE_ASSIGN", target = "Lcom/google/common/collect/Sets;newHashSet(Ljava/lang/Iterable;)Ljava/util/HashSet;")) 23 | private void loadLavenderBooks(Executor prepareExecutor, Executor applyExecutor, ResourceManager manager, List reloaders, @Coerce Object factory, CompletableFuture initialStage, CallbackInfo ci) { 24 | if (!(manager instanceof LavenderLifecycledResourceManagerExtension extension) || extension.lavender$resourceType() != ResourceType.CLIENT_RESOURCES) return; 25 | if (MinecraftClient.getInstance().world == null) return; 26 | 27 | BookLoader.reload(manager); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/mixin/TextureUtilMixin.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.mixin; 2 | 3 | import com.mojang.blaze3d.platform.TextureUtil; 4 | import io.wispforest.lavender.client.LavenderClient; 5 | import net.minecraft.client.texture.NativeImage; 6 | import org.spongepowered.asm.mixin.Mixin; 7 | import org.spongepowered.asm.mixin.injection.At; 8 | import org.spongepowered.asm.mixin.injection.Inject; 9 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 10 | 11 | @Mixin(TextureUtil.class) 12 | public class TextureUtilMixin { 13 | 14 | @Inject(method = "prepareImage(Lnet/minecraft/client/texture/NativeImage$InternalFormat;IIII)V", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/platform/TextureUtil;bind(I)V")) 15 | private static void captureTextureSize(NativeImage.InternalFormat internalFormat, int id, int maxLevel, int width, int height, CallbackInfo ci) { 16 | LavenderClient.registerTextureSize(id, width, height); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/mixin/access/ClientAdvancementManagerAccessor.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.mixin.access; 2 | 3 | import net.minecraft.advancement.AdvancementEntry; 4 | import net.minecraft.advancement.AdvancementProgress; 5 | import net.minecraft.client.network.ClientAdvancementManager; 6 | import org.spongepowered.asm.mixin.Mixin; 7 | import org.spongepowered.asm.mixin.gen.Accessor; 8 | 9 | import java.util.Map; 10 | 11 | @Mixin(ClientAdvancementManager.class) 12 | public interface ClientAdvancementManagerAccessor { 13 | @Accessor("advancementProgresses") 14 | Map lavender$getAdvancementProgresses(); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/mixin/access/RegistryOpsAccessor.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.mixin.access; 2 | 3 | import net.minecraft.registry.RegistryOps; 4 | import org.spongepowered.asm.mixin.Mixin; 5 | import org.spongepowered.asm.mixin.gen.Accessor; 6 | 7 | @Mixin(RegistryOps.class) 8 | public interface RegistryOpsAccessor { 9 | 10 | @Accessor("registryInfoGetter") 11 | RegistryOps.RegistryInfoGetter lavender$getInfoGetter(); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/pond/LavenderFramebufferExtension.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.pond; 2 | 3 | import net.minecraft.client.gl.ShaderProgramKey; 4 | 5 | import java.util.function.Supplier; 6 | 7 | public interface LavenderFramebufferExtension { 8 | void lavender$setBlitProgram(Supplier blitProgram); 9 | 10 | void lavender$enableDepthTest(); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/pond/LavenderLifecycledResourceManagerExtension.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.pond; 2 | 3 | import net.minecraft.resource.ResourceType; 4 | 5 | public interface LavenderLifecycledResourceManagerExtension { 6 | ResourceType lavender$resourceType(); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/structure/BlockStatePredicate.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.structure; 2 | 3 | import net.minecraft.block.BlockState; 4 | import net.minecraft.block.Blocks; 5 | 6 | /** 7 | * A predicate used for matching the elements of a structure 8 | * template against some concrete block state in the world 9 | *

10 | * Importantly, it also provides a mechanism for getting a representative 11 | * sample block state that can be used for the structure preview 12 | */ 13 | public interface BlockStatePredicate { 14 | 15 | /** 16 | * The built-in null predicate that always returns 17 | * a full state match 18 | */ 19 | BlockStatePredicate NULL_PREDICATE = new BlockStatePredicate() { 20 | @Override 21 | public BlockState preview() { 22 | return Blocks.AIR.getDefaultState(); 23 | } 24 | 25 | @Override 26 | public Result test(BlockState blockState) { 27 | return Result.STATE_MATCH; 28 | } 29 | 30 | @Override 31 | public boolean isOf(MatchCategory type) { 32 | return type == MatchCategory.ANY || type == MatchCategory.NULL; 33 | } 34 | }; 35 | 36 | /** 37 | * The built-in air predicate which returns a full state 38 | * match on any air block 39 | */ 40 | BlockStatePredicate AIR_PREDICATE = new BlockStatePredicate() { 41 | @Override 42 | public BlockState preview() { 43 | return Blocks.AIR.getDefaultState(); 44 | } 45 | 46 | @Override 47 | public Result test(BlockState blockState) { 48 | return blockState.isAir() ? Result.STATE_MATCH : Result.NO_MATCH; 49 | } 50 | 51 | @Override 52 | public boolean isOf(MatchCategory type) { 53 | return type == MatchCategory.ANY || type == MatchCategory.NON_NULL || type == MatchCategory.AIR; 54 | } 55 | }; 56 | 57 | Result test(BlockState state); 58 | 59 | /** 60 | * @return {@code true} if this predicate finds a {@linkplain Result#STATE_MATCH state match} 61 | * on the given state 62 | */ 63 | default boolean matches(BlockState state) { 64 | return this.test(state) == Result.STATE_MATCH; 65 | } 66 | 67 | /** 68 | * @return A representative sample state for this predicate. As this function 69 | * is called every frame the preview is rendered, returning a different sample 70 | * depending on system time (e.g. to cycle to a block tag) is valid behavior 71 | */ 72 | BlockState preview(); 73 | 74 | /** 75 | * @return Whether this predicate falls into the given matching category, generally 76 | * useful for communicating information about predicates to the user 77 | */ 78 | default boolean isOf(MatchCategory type) { 79 | return type != MatchCategory.AIR && type != MatchCategory.NULL; 80 | } 81 | 82 | enum Result { 83 | /** 84 | * The predicate fully rejected 85 | * the tested state 86 | */ 87 | NO_MATCH, 88 | /** 89 | * The predicate rejected the tested 90 | * state's properties but accepted the 91 | * base block 92 | */ 93 | BLOCK_MATCH, 94 | /** 95 | * The predicate accepted the entire 96 | * block state including properties 97 | */ 98 | STATE_MATCH 99 | } 100 | 101 | enum MatchCategory { 102 | /** 103 | * No requirements on the matched states, 104 | * every predicate falls into this category 105 | */ 106 | ANY, 107 | /** 108 | * All predicates which are more specific than 109 | * the null predicate, that is, they have at least 110 | * some requirements for states to match 111 | */ 112 | NON_NULL, 113 | /** 114 | * All predicates which are more specific than the 115 | * null predicate and which don't match air 116 | */ 117 | NON_AIR, 118 | /** 119 | * The air predicate 120 | */ 121 | AIR, 122 | /** 123 | * The null predicate 124 | */ 125 | NULL 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/io/wispforest/lavender/structure/LavenderStructures.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavender.structure; 2 | 3 | import com.google.gson.JsonObject; 4 | import com.google.gson.JsonParseException; 5 | import com.google.gson.JsonParser; 6 | import io.wispforest.lavender.Lavender; 7 | import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; 8 | import net.fabricmc.fabric.api.event.lifecycle.v1.CommonLifecycleEvents; 9 | import net.fabricmc.fabric.api.resource.ResourceManagerHelper; 10 | import net.fabricmc.fabric.api.resource.SimpleSynchronousResourceReloadListener; 11 | import net.minecraft.resource.ResourceFinder; 12 | import net.minecraft.resource.ResourceManager; 13 | import net.minecraft.resource.ResourceType; 14 | import net.minecraft.util.Identifier; 15 | import org.jetbrains.annotations.ApiStatus; 16 | import org.jetbrains.annotations.Nullable; 17 | 18 | import java.io.IOException; 19 | import java.util.Collections; 20 | import java.util.HashMap; 21 | import java.util.Map; 22 | import java.util.Set; 23 | 24 | public class LavenderStructures { 25 | 26 | private static final Map PENDING_STRUCTURES = new HashMap<>(); 27 | private static final Map LOADED_STRUCTURES = new HashMap<>(); 28 | 29 | private static boolean tagsAvailable = false; 30 | 31 | @ApiStatus.Internal 32 | public static void initialize() { 33 | ResourceManagerHelper.get(ResourceType.CLIENT_RESOURCES).registerReloadListener(new ReloadListener()); 34 | 35 | ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> tagsAvailable = false); 36 | CommonLifecycleEvents.TAGS_LOADED.register((registries, client) -> { 37 | tagsAvailable = true; 38 | tryParseStructures(); 39 | }); 40 | } 41 | 42 | /** 43 | * @return A view over the identifiers of all currently loaded structures 44 | */ 45 | public static Set loadedStructures() { 46 | return Collections.unmodifiableSet(LOADED_STRUCTURES.keySet()); 47 | } 48 | 49 | /** 50 | * @return The structure currently associated with the given id, 51 | * or {@code null} if no such structure is loaded 52 | */ 53 | public static @Nullable StructureTemplate get(Identifier structureId) { 54 | return LOADED_STRUCTURES.get(structureId); 55 | } 56 | 57 | private static void tryParseStructures() { 58 | LOADED_STRUCTURES.clear(); 59 | PENDING_STRUCTURES.forEach((identifier, pending) -> { 60 | try { 61 | LOADED_STRUCTURES.put(identifier, StructureTemplate.parse(identifier, pending)); 62 | } catch (JsonParseException e) { 63 | Lavender.LOGGER.warn("Failed to load structure info {}", identifier, e); 64 | } 65 | }); 66 | } 67 | 68 | private static class ReloadListener implements SimpleSynchronousResourceReloadListener { 69 | 70 | @Override 71 | public void reload(ResourceManager manager) { 72 | PENDING_STRUCTURES.clear(); 73 | 74 | var resourceFinder = ResourceFinder.json("lavender/structures"); 75 | for (var entry : resourceFinder.findResources(manager).entrySet()) { 76 | var resourceId = entry.getKey(); 77 | var structureId = resourceFinder.toResourceId(resourceId); 78 | 79 | try (var reader = entry.getValue().getReader()) { 80 | var json = JsonParser.parseReader(reader); 81 | 82 | if (!json.isJsonObject()) return; 83 | PENDING_STRUCTURES.put(structureId, json.getAsJsonObject()); 84 | } catch (IllegalArgumentException | IOException | JsonParseException error) { 85 | Lavender.LOGGER.error("Couldn't parse data file '{}' from '{}'", structureId, resourceId, error); 86 | } 87 | } 88 | 89 | if (tagsAvailable) tryParseStructures(); 90 | } 91 | 92 | @Override 93 | public Identifier getFabricId() { 94 | return Lavender.id("structure_info_loader"); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/resources/assets/lavender/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisp-forest/lavender/96ff335fe8194aa36e1b75c0eb039c7e69fc081c/src/main/resources/assets/lavender/icon.png -------------------------------------------------------------------------------- /src/main/resources/assets/lavender/items/dynamic_book.json: -------------------------------------------------------------------------------- 1 | { 2 | "model": { 3 | "type": "lavender:dynamic_book_model", 4 | "default": { 5 | "type": "minecraft:model", 6 | "model": "lavender:item/brown_book" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /src/main/resources/assets/lavender/lang/de_de.json: -------------------------------------------------------------------------------- 1 | { 2 | "item.lavender.dynamic_book": "Buch", 3 | 4 | "text.lavender.structure_hud.completion": "%s: %s / %s", 5 | "text.lavender.entry_hud.sneak_to_view": {"text": "Schleichen zum öffnen", "color": "gray"}, 6 | "text.lavender.entry_hud.click_to_view": {"text": "Klick zum öffnen", "color": "gray"}, 7 | 8 | "text.lavender.entry_tooltip": [ 9 | {"text": "Halte ", "color": "dark_gray"}, 10 | {"text": "Alt ", "color": "gold"}, 11 | {"text": "[", "color": "green"}, 12 | {"index": 0, "color": "gray"}, 13 | {"index": 1, "color": "dark_gray"}, 14 | {"text": "]", "color": "green"} 15 | ], 16 | 17 | "text.lavender.structure_component.hide_hint": "❌ Klick zum verstecken", 18 | "text.lavender.structure_component.place_hint": "⚓ Klick zum platzieren", 19 | "text.lavender.structure_component.active_overlay_hint": {"text": "⚓", "color": "gray"}, 20 | "text.lavender.structure_component.layer_tooltip": "Schicht: %d", 21 | "text.lavender.structure_component.all_layers_tooltip": "Alle Schichten", 22 | 23 | "text.lavender.keybind_tooltip": [ 24 | {"text": "Tastebelegung:\n "}, 25 | {"index": 0, "color": "gray"}, 26 | {"text": " > ", "color": "dark_gray"}, 27 | {"index": 1, "color": "gray"} 28 | ], 29 | 30 | "text.lavender.locked_internal_link": {"text": "Gesperrt", "color": "gray"}, 31 | "text.lavender.invalid_internal_link": [ 32 | {"text": "Ungültiger interner Verweis: ", "color": "red"}, 33 | {"index": 0, "color": "gray"} 34 | ], 35 | 36 | "text.lavender.entry.locked": {"text": "Gesperrt", "font": "minecraft:uniform", "color": "gray"}, 37 | 38 | "text.lavender.categories": "Kategorien", 39 | "text.lavender.index": "Index", 40 | "text.lavender.index_category": "Alle Einträge", 41 | "text.lavender.index_category.title": "Einträge", 42 | 43 | "text.lavender.book.reload": "Neu laden", 44 | 45 | "text.lavender.book.unlock_progress": [ 46 | {"text": "Entdeckt: ", "font": "minecraft:uniform", "color": "dark_gray"}, 47 | {"index": 0}, 48 | {"text": "/"}, 49 | {"index": 1} 50 | ], 51 | 52 | "text.lavender.book.bookmark.add": "Leszeichen hinzufügen", 53 | "text.lavender.book.bookmark.remove_hint": {"text": "Shift-Klick zum entfernen", "color": "dark_gray"} 54 | } -------------------------------------------------------------------------------- /src/main/resources/assets/lavender/lang/en_us.json: -------------------------------------------------------------------------------- 1 | { 2 | "item.lavender.dynamic_book": "Book", 3 | 4 | "text.lavender.unknown_book": [{"text": "Unknown book: ", "color": "red"}, {"index": 0, "color": "gray"}], 5 | 6 | "text.lavender.structure_hud.completion": "%s: %s / %s", 7 | "text.lavender.entry_hud.sneak_to_view": {"text": "Sneak to view", "color": "gray"}, 8 | "text.lavender.entry_hud.click_to_view": {"text": "Click to view", "color": "gray"}, 9 | 10 | "text.lavender.entry_tooltip": [ 11 | {"text": "Hold ", "color": "dark_gray"}, 12 | {"text": "Alt ", "color": "gold"} 13 | ], 14 | "text.lavender.entry_tooltip.progress": [ 15 | {"text": "[", "color": "green"}, 16 | {"index": 0, "color": "gray"}, 17 | {"index": 1, "color": "dark_gray"}, 18 | {"text": "]", "color": "green"} 19 | ], 20 | 21 | "text.lavender.structure_component.hide_hint": "❌ Shift-click to hide", 22 | "text.lavender.structure_component.place_hint": "⚓ Shift-click to place", 23 | "text.lavender.structure_component.active_overlay_hint": {"text": "⚓", "color": "gray"}, 24 | "text.lavender.structure_component.layer_tooltip": "Layer: %d", 25 | "text.lavender.structure_component.all_layers_tooltip": "All layers", 26 | 27 | "text.lavender.toast.new_entries": [ 28 | "", 29 | {"text": "New entries unlocked:\n", "color": "yellow"}, 30 | {"index": 0} 31 | ], 32 | 33 | "text.lavender.keybind_tooltip": [ 34 | {"text": "Key Bind:\n "}, 35 | {"index": 0, "color": "gray"}, 36 | {"text": " > ", "color": "dark_gray"}, 37 | {"index": 1, "color": "gray"} 38 | ], 39 | 40 | "text.lavender.locked_internal_link": {"text": "Locked", "color": "gray"}, 41 | "text.lavender.invalid_internal_link": [ 42 | {"text": "Invalid internal link: ", "color": "red"}, 43 | {"index": 0, "color": "gray"} 44 | ], 45 | 46 | "text.lavender.entry.locked": {"text": "Locked", "font": "minecraft:uniform", "color": "gray"}, 47 | "text.lavender.entry.multiple_unread": "Unread entries", 48 | "text.lavender.entry.unread": "Unread entry", 49 | 50 | "text.lavender.categories": "Categories", 51 | "text.lavender.index": "Index", 52 | "text.lavender.index_category": "All Entries", 53 | "text.lavender.index_category.title": "Entries", 54 | 55 | "text.lavender.book.click_to_open": {"text": "Click to open: ", "color": "dark_gray"}, 56 | 57 | "text.lavender.book.next": [ 58 | "Next Page\n", 59 | {"text": "↑ Scroll Up", "color": "gray"} 60 | ], 61 | "text.lavender.book.back": [ 62 | "Go Back\n", 63 | {"text": "Shift-click to return home", "color": "gray"} 64 | ], 65 | "text.lavender.book.previous": [ 66 | "Previous Page\n", 67 | {"text": "↓ Scroll Down", "color": "gray"} 68 | ], 69 | "text.lavender.book.reload": "Reload", 70 | 71 | "text.lavender.book.unlock_progress": [ 72 | {"text": "Unlocked: ", "font": "minecraft:uniform", "color": "dark_gray"}, 73 | {"index": 0}, 74 | {"text": "/"}, 75 | {"index": 1} 76 | ], 77 | 78 | "text.lavender.book.bookmark.add": "Add Bookmark", 79 | "text.lavender.book.bookmark.remove_hint": {"text": "Shift-click to remove", "color": "dark_gray"} 80 | } -------------------------------------------------------------------------------- /src/main/resources/assets/lavender/models/item/brown_book.json: -------------------------------------------------------------------------------- 1 | { 2 | "parent": "minecraft:item/generated", 3 | "textures": { 4 | "layer0": "lavender:item/brown_book" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/main/resources/assets/lavender/models/item/red_book.json: -------------------------------------------------------------------------------- 1 | { 2 | "parent": "minecraft:item/generated", 3 | "textures": { 4 | "layer0": "lavender:item/red_book" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/main/resources/assets/lavender/shaders/core/blit_alpha.fsh: -------------------------------------------------------------------------------- 1 | #version 150 2 | 3 | uniform sampler2D InSampler; 4 | uniform float Alpha; 5 | 6 | in vec2 texCoord; 7 | 8 | out vec4 fragColor; 9 | 10 | void main() { 11 | vec4 color = texture(InSampler, texCoord); 12 | color.a *= Alpha; 13 | 14 | fragColor = color; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/resources/assets/lavender/shaders/core/blit_alpha.json: -------------------------------------------------------------------------------- 1 | { 2 | "vertex": "core/blit_screen", 3 | "fragment": "lavender:core/blit_alpha", 4 | "samplers": [ 5 | { "name": "InSampler" } 6 | ], 7 | "uniforms": [ 8 | { "name": "Alpha", "type": "float", "count": 1, "values": [ 1.0 ] } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/main/resources/assets/lavender/shaders/core/blit_cutout.fsh: -------------------------------------------------------------------------------- 1 | #version 150 2 | 3 | uniform sampler2D InSampler; 4 | 5 | in vec2 texCoord; 6 | 7 | out vec4 fragColor; 8 | 9 | void main() { 10 | vec4 color = texture(InSampler, texCoord); 11 | color.a = min(1.0f, color.a * 1e8f); 12 | 13 | fragColor = color; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/resources/assets/lavender/shaders/core/blit_cutout.json: -------------------------------------------------------------------------------- 1 | { 2 | "vertex": "core/blit_screen", 3 | "fragment": "lavender:core/blit_cutout", 4 | "samplers": [ 5 | { "name": "InSampler" } 6 | ], 7 | "uniforms": [] 8 | } 9 | -------------------------------------------------------------------------------- /src/main/resources/assets/lavender/sounds.json: -------------------------------------------------------------------------------- 1 | { 2 | "item.book.open": { 3 | "sounds": [ 4 | "lavender:item/book_open_1", 5 | "lavender:item/book_open_2", 6 | "lavender:item/book_open_3" 7 | ] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/resources/assets/lavender/sounds/item/book_open_1.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisp-forest/lavender/96ff335fe8194aa36e1b75c0eb039c7e69fc081c/src/main/resources/assets/lavender/sounds/item/book_open_1.ogg -------------------------------------------------------------------------------- /src/main/resources/assets/lavender/sounds/item/book_open_2.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisp-forest/lavender/96ff335fe8194aa36e1b75c0eb039c7e69fc081c/src/main/resources/assets/lavender/sounds/item/book_open_2.ogg -------------------------------------------------------------------------------- /src/main/resources/assets/lavender/sounds/item/book_open_3.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisp-forest/lavender/96ff335fe8194aa36e1b75c0eb039c7e69fc081c/src/main/resources/assets/lavender/sounds/item/book_open_3.ogg -------------------------------------------------------------------------------- /src/main/resources/assets/lavender/textures/gui/brown_book.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisp-forest/lavender/96ff335fe8194aa36e1b75c0eb039c7e69fc081c/src/main/resources/assets/lavender/textures/gui/brown_book.png -------------------------------------------------------------------------------- /src/main/resources/assets/lavender/textures/gui/purple_book.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisp-forest/lavender/96ff335fe8194aa36e1b75c0eb039c7e69fc081c/src/main/resources/assets/lavender/textures/gui/purple_book.png -------------------------------------------------------------------------------- /src/main/resources/assets/lavender/textures/gui/red_book.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisp-forest/lavender/96ff335fe8194aa36e1b75c0eb039c7e69fc081c/src/main/resources/assets/lavender/textures/gui/red_book.png -------------------------------------------------------------------------------- /src/main/resources/assets/lavender/textures/gui/sprites/new_entries_toast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisp-forest/lavender/96ff335fe8194aa36e1b75c0eb039c7e69fc081c/src/main/resources/assets/lavender/textures/gui/sprites/new_entries_toast.png -------------------------------------------------------------------------------- /src/main/resources/assets/lavender/textures/gui/structure_overlay_bars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisp-forest/lavender/96ff335fe8194aa36e1b75c0eb039c7e69fc081c/src/main/resources/assets/lavender/textures/gui/structure_overlay_bars.png -------------------------------------------------------------------------------- /src/main/resources/assets/lavender/textures/item/brown_book.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisp-forest/lavender/96ff335fe8194aa36e1b75c0eb039c7e69fc081c/src/main/resources/assets/lavender/textures/item/brown_book.png -------------------------------------------------------------------------------- /src/main/resources/assets/lavender/textures/item/red_book.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisp-forest/lavender/96ff335fe8194aa36e1b75c0eb039c7e69fc081c/src/main/resources/assets/lavender/textures/item/red_book.png -------------------------------------------------------------------------------- /src/main/resources/fabric.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "id": "lavender", 4 | "version": "${version}", 5 | "name": "Lavender", 6 | "description": "Apparently Lavender and Patchouli are really similar-looking flowers. Oh yeah, also this is a Guidebook API", 7 | "authors": [ 8 | "glisco" 9 | ], 10 | "contact": { 11 | "repo": "https://github.com/wisp-forest/lavender", 12 | "homepage": "https://modrinth.com/project/lavender", 13 | "sources": "https://github.com/wisp-forest/lavender", 14 | "issues": "https://github.com/wisp-forest/lavender/issues" 15 | }, 16 | "license": "MIT", 17 | "icon": "assets/lavender/icon.png", 18 | "environment": "*", 19 | "entrypoints": { 20 | "client": [ 21 | "io.wispforest.lavender.client.LavenderClient" 22 | ], 23 | "main": [ 24 | "io.wispforest.lavender.Lavender" 25 | ] 26 | }, 27 | "accessWidener": "lavender.accesswidener", 28 | "mixins": [ 29 | "lavender.mixins.json" 30 | ], 31 | "depends": { 32 | "fabricloader": ">=0.15.0", 33 | "fabric": "*", 34 | "minecraft": ">=1.21.2", 35 | "owo-lib": "*", 36 | "lavender-md": ">=0.1.2", 37 | "lavender-md-owo-ui": "*" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/resources/lavender.accesswidener: -------------------------------------------------------------------------------- 1 | accessWidener v2 named 2 | #extendable method net/minecraft/client/render/model/json/ModelOverrideList ()V -------------------------------------------------------------------------------- /src/main/resources/lavender.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "minVersion": "0.8", 4 | "package": "io.wispforest.lavender.mixin", 5 | "compatibilityLevel": "JAVA_17", 6 | "mixins": [ 7 | "LifecycledResourceManagerMixin", 8 | "access.RegistryOpsAccessor" 9 | ], 10 | "client": [ 11 | "ClientAdvancementManagerMixin", 12 | "CreativeInventoryScreenMixin", 13 | "DrawContextMixin", 14 | "FramebufferMixin", 15 | "HeldItemRendererMixin", 16 | "MinecraftClientMixin", 17 | "MouseMixin", 18 | "RenderPhaseMixin", 19 | "ScreenMixin", 20 | "SimpleResourceReloadMixin", 21 | "TextureUtilMixin", 22 | "access.ClientAdvancementManagerAccessor" 23 | ], 24 | "injectors": { 25 | "defaultRequire": 1 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/testmod/java/io/wispforest/lavenderflower/LavenderFlower.java: -------------------------------------------------------------------------------- 1 | package io.wispforest.lavenderflower; 2 | 3 | import io.wispforest.lavender.book.LavenderBookItem; 4 | import net.fabricmc.api.ModInitializer; 5 | import net.minecraft.item.Item; 6 | import net.minecraft.util.Identifier; 7 | 8 | public class LavenderFlower implements ModInitializer { 9 | 10 | public static final String MOD_ID = "lavender-flower"; 11 | 12 | @Override 13 | public void onInitialize() { 14 | LavenderBookItem.registerForBook(id("the_book"), new Item.Settings()); 15 | } 16 | 17 | public static Identifier id(String path) { 18 | return Identifier.of(MOD_ID, path); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-bud/lavender/books/book_from_another_world.json: -------------------------------------------------------------------------------- 1 | { 2 | "extend": "lavender-flower:more_book" 3 | } -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-bud/lavender/entries/book_from_another_world/de_de/entry_from_another_world.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "title": "Eintrag aus einer anderen Welt", 4 | "icon": "minecraft:oak_log", 5 | "category": "lavender-flower:epic_category", 6 | "associated_items": [ 7 | "minecraft:diamond" 8 | ], 9 | "required_advancements": [ 10 | "minecraft:story/iron_tools" 11 | ] 12 | } 13 | ``` 14 | 15 | you might not believe it, but this entry actually comes from an extension loaded through a different namespace -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-bud/lavender/entries/book_from_another_world/entry_from_another_world.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "title": "Entry from Another World", 4 | "icon_sprite": "minecraft:social_interactions/mute_button", 5 | "categories": [ 6 | "lavender-flower:epic_category", 7 | "lavender-flower:a_category" 8 | ], 9 | "ordinal": -1, 10 | "associated_items": [ 11 | "minecraft:diamond", 12 | "minecraft:enchanted_book{StoredEnchantments:[{id:'minecraft:sharpness', lvl:2s}]}" 13 | ], 14 | "required_advancements": [ 15 | "minecraft:story/iron_tools" 16 | ] 17 | } 18 | ``` 19 | 20 | you might not believe it, but this entry actually comes from an extension loaded through a different namespace 21 | 22 | 23 | 24 | ;;;;; 25 | 26 | a list: 27 | - some text 28 | - more text 29 | - text! -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-bud/models/item/wispen_testament.json: -------------------------------------------------------------------------------- 1 | { 2 | "parent": "minecraft:item/generated", 3 | "textures": { 4 | "layer0": "lavender-bud:item/wispen_testament" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-bud/textures/item/wispen_testament.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisp-forest/lavender/96ff335fe8194aa36e1b75c0eb039c7e69fc081c/src/testmod/resources/assets/lavender-bud/textures/item/wispen_testament.png -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/items/marketing_book.json: -------------------------------------------------------------------------------- 1 | { 2 | "model": { 3 | "type": "minecraft:select", 4 | "cases": [ 5 | { 6 | "model": { 7 | "type": "minecraft:model", 8 | "model": "minecraft:item/trident" 9 | }, 10 | "when": [ 11 | "gui", 12 | "ground", 13 | "fixed" 14 | ] 15 | } 16 | ], 17 | "fallback": { 18 | "type": "minecraft:condition", 19 | "on_false": { 20 | "type": "minecraft:special", 21 | "base": "minecraft:item/trident_in_hand", 22 | "model": { 23 | "type": "minecraft:trident" 24 | } 25 | }, 26 | "on_true": { 27 | "type": "minecraft:special", 28 | "base": "minecraft:item/trident_throwing", 29 | "model": { 30 | "type": "minecraft:trident" 31 | } 32 | }, 33 | "property": "minecraft:using_item" 34 | }, 35 | "property": "minecraft:display_context" 36 | } 37 | } -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/items/the_book.json: -------------------------------------------------------------------------------- 1 | { 2 | "model": { 3 | "type": "minecraft:model", 4 | "model": "lavender:item/red_book" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lang/de_de.json: -------------------------------------------------------------------------------- 1 | { 2 | "structure.lavender-flower.iron_golem": "Eisengolem", 3 | "structure.lavender-flower.nether_portal": "Netherportal", 4 | "structure.lavender-flower.ritual_of_extraction": "Ritual der Extraktion", 5 | "structure.lavender-flower.well": "Der Brunnen", 6 | "structure.affinity.augmented_crafting_table": "Erweiterte Werkbank", 7 | 8 | "item.lavender-flower.the_book": "Das Buch" 9 | } -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lang/en_us.json: -------------------------------------------------------------------------------- 1 | { 2 | "structure.lavender-flower.iron_golem": "Iron Golem", 3 | "structure.lavender-flower.nether_portal": "Nether Portal", 4 | "structure.lavender-flower.ritual_of_extraction": "Ritual of Extraction", 5 | "structure.lavender-flower.well": "The Well", 6 | "structure.affinity.augmented_crafting_table": "Augmented Crafting Table", 7 | 8 | "item.lavender-flower.the_book": "The Book" 9 | } -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/books/marketing.json: -------------------------------------------------------------------------------- 1 | { 2 | "display_completion": true, 3 | "dynamic_book_model": "lavender-flower:marketing_book" 4 | } -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/books/more_book.json: -------------------------------------------------------------------------------- 1 | { 2 | "extend": "lavender-flower:the_book" 3 | } -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/books/the_book.json: -------------------------------------------------------------------------------- 1 | { 2 | "display_completion": true, 3 | "texture": "lavender:textures/gui/red_book.png", 4 | "intro_entry": "lavender-flower:profound_page", 5 | "open_sound": "minecraft:block.anvil.destroy", 6 | "macros": { 7 | "nested_macro": "ah yes: $1", 8 | "macro_moment": "that's pretty nested_macro($1) - $2" 9 | }, 10 | "new_entries_toast": { 11 | "icon_stack": "lavender-flower:the_book", 12 | "book_name": "The Book™", 13 | "background_sprite": "minecraft:toast/system" 14 | } 15 | } -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/categories/marketing/category_1.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "icon": "minecraft:splash_potion{Potion:'minecraft:strength'}", 4 | "title": "Category 1" 5 | } 6 | ``` -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/categories/marketing/category_2.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "icon": "minecraft:warped_nylium", 4 | "title": "Category 1" 5 | } 6 | ``` -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/categories/marketing/category_3.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "icon": "minecraft:diamond", 4 | "title": "Category 1" 5 | } 6 | ``` -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/categories/marketing/category_4.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "icon": "minecraft:netherite_hoe", 4 | "title": "Category 1" 5 | } 6 | ``` -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/categories/marketing/category_5.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "icon": "minecraft:glowstone_dust", 4 | "title": "Category 1", 5 | "parent": "category_2" 6 | } 7 | ``` -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/categories/marketing/category_6.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "icon": "minecraft:magenta_glazed_terracotta", 4 | "title": "Category 1" 5 | } 6 | ``` -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/categories/marketing/category_7.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "icon": "minecraft:magenta_glazed_terracotta", 4 | "title": "Category 1" 5 | } 6 | ``` -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/categories/more_book/de_de/epic_category.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "icon": "minecraft:nether_star", 4 | "title": "Epische Kategorie" 5 | } 6 | ``` 7 | 8 | Diese Kategorie ist **besonders cool**, weil sie durch eine Erweiterung hinzugefügt wurde -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/categories/more_book/epic_category.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "icon": "minecraft:splash_potion{Potion:'minecraft:strength'}", 4 | "title": "Epic Category" 5 | } 6 | ``` 7 | 8 | this category is *very* cool, because it was added through an extension -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/categories/the_book/a_category.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "icon": "minecraft:stone", 4 | "title": "A Category" 5 | } 6 | ``` 7 | 8 | that's a category -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/categories/the_book/another_category.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "icon": "minecraft:diamond", 4 | "title": "Another Category" 5 | } 6 | ``` 7 | 8 | so many categoriessss! -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/categories/the_book/b_category.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "icon": "minecraft:cobblestone", 4 | "parent": "a_category", 5 | "title": "B Category" 6 | } 7 | ``` 8 | 9 | that's a category -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/categories/the_book/de_de/a_category.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "icon": "minecraft:stone", 4 | "title": "Eine Kategorie" 5 | } 6 | ``` 7 | 8 | Dies, meine Damen und Herren, ist eine Kategorie -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/entries/marketing/entry_1.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "title": "a\na", 4 | "icon": "minecraft:melon_slice" 5 | } 6 | ``` -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/entries/marketing/entry_2.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "title": "a\na", 4 | "ordinal": 2, 5 | "icon": "minecraft:melon_slice" 6 | } 7 | ``` -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/entries/marketing/entry_3.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "title": "a\na", 4 | "icon": "minecraft:melon_slice" 5 | } 6 | ``` -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/entries/marketing/entry_33.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "title": "a", 4 | "icon": "minecraft:melon_slice", 5 | "required_advancements": [ 6 | "minecraft:adventure/adventuring_time" 7 | ] 8 | } 9 | ``` -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/entries/marketing/entry_4.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "title": "a\na", 4 | "icon": "minecraft:melon_slice" 5 | } 6 | ``` -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/entries/marketing/entry_44.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "title": "a", 4 | "icon": "minecraft:melon_slice", 5 | "required_advancements": [ 6 | "minecraft:adventure/adventuring_time" 7 | ] 8 | } 9 | ``` -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/entries/marketing/entry_5.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "title": "a", 4 | "icon": "minecraft:melon_slice", 5 | "category": "category_5" 6 | } 7 | ``` -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/entries/marketing/entry_55.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "title": "a", 4 | "icon": "minecraft:melon_slice", 5 | "required_advancements": [ 6 | "minecraft:adventure/adventuring_time" 7 | ] 8 | } 9 | ``` -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/entries/marketing/entry_6.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "title": "a", 4 | "icon": "minecraft:melon_slice" 5 | } 6 | ``` -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/entries/marketing/entry_66.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "title": "a", 4 | "icon": "minecraft:melon_slice", 5 | "required_advancements": [ 6 | "minecraft:adventure/adventuring_time" 7 | ] 8 | } 9 | ``` -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/entries/marketing/landing_page.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "title": "Lavender" 4 | } 5 | ``` 6 | 7 | Lavender is a modern, {dark_green}markdown-driven{} and feature-rich guidebook API for mod developers and modpack makers alike 8 | 9 | 10 | {gray}*Apparently Lavender and Patchouli are really similar-looking flowers*{} -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/entries/marketing/lone_entry.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "title": "A Lone Entry", 4 | "icon": "minecraft:stick" 5 | } 6 | ``` -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/entries/more_book/page.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "title": "Extension", 4 | "icon": "minecraft:obsidian", 5 | "category": "epic_category" 6 | } 7 | ``` 8 | 9 | a 10 | 11 | --- 12 | 13 | 14 | 15 | --- 16 | 17 | 18 | 19 | ;;;;; 20 | 21 | <|page-title@lavender:book_components|title=Pedestal Recipe|> 22 | content here 23 | 24 | ;;;;; 25 | 26 | <|item-spotlight@lavender:book_components|item=affinity:strengthened_artifact_blade|> 27 | <|item-spotlight@lavender:book_components|item=affinity:strengthened_artifact_blade{Enchantments: [{id: "minecraft:unbreaking"\, lvl: 3s}]}|> 28 | 29 | This is where we explain why Artifact Blades are very cool, like ur mom :) 30 | -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/entries/the_book/de_de/landing_page.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "title": "Das Buch\n Teil 69" 4 | } 5 | ``` 6 | 7 | Diese **Hauptseite** ist jetzt also quasi der Ort wo wir die gesamte Prosa unterbringen welche den Nutzer mit der 8 | Modifikation vertraut macht. Sie ist, selbverständlich, {green}vollkommen{} markdown-fähig 9 | 10 | 11 | , -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/entries/the_book/landing_page.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "title": "The Book\nVolume 69" 4 | } 5 | ``` 6 | 7 | This **landing page** is now the place where we would put all the nice prose that introduces the user to our mod. It is, 8 | unsurprisingly, still a {green}fully capable{} page with complete markdown support 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/entries/the_book/profound_page.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "title": "A Profound Page With a Long Name", 4 | "icon": "minecraft:melon_slice{Enchantments:[{id:'minecraft:unbreaking', lvl:1}]}", 5 | "category": "a_category", 6 | "associated_items": [ 7 | "minecraft:enchanted_book{StoredEnchantments:[{id:'minecraft:unbreaking', lvl:3s}]}", 8 | "#minecraft:candles" 9 | ], 10 | "required_advancements": [ 11 | "minecraft:story/lava_bucket" 12 | ] 13 | } 14 | ``` 15 | 16 | and let's also have some **markdown** here 17 | 18 | > a very profound quote 19 | 20 | and a [link](https://wispforest.io) 21 | 22 | --- 23 | 24 | macro_moment(aaaa,bruh) 25 | 26 | ;;;;; 27 | 28 | - list item 1 29 | - and another one 30 | - nesting moment, with some really long 31 | prose because that should wrap correctly now 32 | - less nesting 33 | 34 | ayo, were on page 2! 35 | 36 | 37 | 38 | ;;;;; 39 | 40 | page 3 41 | 42 | > we'll maybe put a quote here 43 | >> and for good measure, let's also nest one 44 | > and then back to normal 45 | 46 | still page three 47 | 48 | [**ritual basics**](^lavender-flower:ritual_basics) 49 | 50 | --- 51 | 52 | rule here 53 | 54 | and now just some prose. pretty epic 55 | 56 | ;;;;; 57 | 58 | page 4 59 | 60 | 61 | 62 | 63 | 64 | ;;;;; 65 | 66 | page 5 67 | 68 | 69 | 70 | 71 | ;;;;; 72 | 73 | While being the most basic substance involved in summoning, {light_purple}Conjuration Essence{} is still of utmost importance. It embodies 74 | the most basic properties all souls share and is therefore involved in the creation of many more capable materials. 75 | 76 | 77 | One can obtain some essence of their own from {gold}breaking ordinary spawners{} as well as {gold}plundering chests{}. 78 | 79 | ;;;;; 80 | 81 | For when a less concentrated material is required, simply {gold}sneak-right-clicking{} some essence on any stone-like surface 82 | smashes it into 4 pieces of {light_purple}Lesser Conjuration Essence{} 83 | 84 | *~a fine predicament* 85 | 86 | 87 | [a category link](^lavender-flower:a_category) 88 | 89 | ```xml owo-ui 90 | 91 | 92 | 93 | 94 | 95 | minecraft:diamond 96 | true 97 | 98 | 99 | 103 | 104 | 105 | 5 106 | center 107 | 108 | 5 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 100 118 | 119 | center 120 | 121 | ``` 122 | 123 | ;;;;; 124 | 125 | 126 | 127 | %{text.lavender.keybind_tooltip}% -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/entries/the_book/profounder_pagee.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "title": "A Profounder Page With a Long Name", 4 | "icon": "minecraft:melon_slice{Enchantments:[{id:'minecraft:unbreaking', lvl:1}]}", 5 | "category": "b_category", 6 | "associated_items": [ 7 | "minecraft:enchanted_book{StoredEnchantments:[{id:'minecraft:unbreaking', lvl:3s}]}", 8 | "#minecraft:candles" 9 | ], 10 | "additional_search_terms": [ 11 | "incredible", 12 | "im 14 and this is deep", 13 | "I NEED PSYCHIATRIC HELP" 14 | ] 15 | } 16 | ``` 17 | 18 | and let's also have some **markdown** here 19 | 20 | > a very profound quote 21 | 22 | and a [link](https://wispforest.io) 23 | 24 | --- 25 | 26 | macro_moment(aaaa,bruh) 27 | 28 | ;;;;; 29 | 30 | - list item 1 31 | - and another one 32 | - nesting moment, with some really long 33 | prose because that should wrap correctly now 34 | - less nesting 35 | 36 | ayo, were on page 2! 37 | 38 | 39 | 40 | ;;;;; 41 | 42 | page 3 43 | 44 | > we'll maybe put a quote here 45 | >> and for good measure, let's also nest one 46 | > and then back to normal 47 | 48 | still page three 49 | 50 | [**ritual basics**](^lavender-flower:ritual_basics) 51 | 52 | --- 53 | 54 | rule here 55 | 56 | and now just some prose. pretty epic 57 | 58 | ;;;;; 59 | 60 | page 4 61 | 62 | 63 | 64 | 65 | 66 | ;;;;; 67 | 68 | page 5 69 | 70 | 71 | 72 | ;;;;; 73 | 74 | While being the most basic substance involved in summoning, {light_purple}Conjuration Essence{} is still of utmost importance. It embodies 75 | the most basic properties all souls share and is therefore involved in the creation of many more capable materials. 76 | 77 | 78 | One can obtain some essence of their own from {gold}breaking ordinary spawners{} as well as {gold}plundering chests{}. 79 | 80 | ;;;;; 81 | 82 | For when a less concentrated material is required, simply {gold}sneak-right-clicking{} some essence on any stone-like surface 83 | smashes it into 4 pieces of {light_purple}Lesser Conjuration Essence{} 84 | 85 | *~a fine predicament* 86 | 87 | 88 | [a category link](^lavender-flower:a_category) 89 | 90 | ```xml owo-ui 91 | 92 | 93 | 94 | 95 | 96 | minecraft:diamond 97 | true 98 | 99 | 100 | 104 | 105 | 106 | 5 107 | center 108 | 109 | 5 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 100 119 | 120 | center 121 | 122 | ``` 123 | 124 | ;;;;; 125 | 126 | 127 | 128 | %{text.lavender.keybind_tooltip}% -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/structures/iron_golem.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": { 3 | "i": "minecraft:iron_block", 4 | "p": "minecraft:carved_pumpkin" 5 | }, 6 | "layers": [ 7 | [ 8 | " i " 9 | ], 10 | [ 11 | "iii" 12 | ], 13 | [ 14 | " p " 15 | ] 16 | ] 17 | } -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/structures/nether_portal.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": { 3 | "o": "minecraft:obsidian", 4 | "n": "minecraft:nether_portal", 5 | "e": "minecraft:emerald_block", 6 | "anchor": "minecraft:diamond_block" 7 | }, 8 | "layers": [ 9 | [ 10 | "ooooooo" 11 | ], 12 | [ 13 | "onnnnno" 14 | ], 15 | [ 16 | "onnnnno" 17 | ], 18 | [ 19 | "onnnnno" 20 | ], 21 | [ 22 | "onnnnno" 23 | ], 24 | [ 25 | "onnnnno" 26 | ], 27 | [ 28 | "eoo#ooo" 29 | ] 30 | ] 31 | } -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/lavender/structures/well.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": { 3 | "c": "minecraft:chiseled_stone_bricks", 4 | "s": "minecraft:stone_brick_stairs[facing=north]", 5 | "u": "minecraft:stone_brick_stairs[facing=south]", 6 | "e": "minecraft:stone_brick_stairs[facing=east]", 7 | "t": "minecraft:stone_brick_stairs[facing=west]", 8 | "l": "minecraft:smooth_stone_slab", 9 | "b": "minecraft:stone_bricks", 10 | "w": "minecraft:stone_brick_wall", 11 | "a": "minecraft:lantern" 12 | }, 13 | "layers": [ 14 | [ 15 | "bbbbb", 16 | "bcbcb", 17 | "bbbbb", 18 | "bcbcb", 19 | "bbbbb" 20 | ], 21 | [ 22 | "culuc", 23 | "e t", 24 | "l b l", 25 | "e t", 26 | "cslsc" 27 | ], 28 | [ 29 | "w w", 30 | " ", 31 | " c ", 32 | " ", 33 | "w w" 34 | ], 35 | [ 36 | "a a", 37 | " ", 38 | " b ", 39 | " ", 40 | "a a" 41 | ] 42 | ] 43 | } -------------------------------------------------------------------------------- /src/testmod/resources/assets/lavender-flower/owo_ui/ritual_basics.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 |