diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..f811f6a
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,5 @@
+# Disable autocrlf on generated files, they always generate with LF
+# Add any extra files or paths here to make git stop saying they
+# are changed when only line endings change.
+src/generated/**/.cache/cache text eol=lf
+src/generated/**/*.json text eol=lf
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..dbeb6ff
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,32 @@
+# eclipse
+bin
+*.launch
+.settings
+.metadata
+.classpath
+.project
+
+# idea
+out
+*.ipr
+*.iws
+*.iml
+.idea
+
+# gradle
+build
+.gradle
+
+# other
+eclipse
+run
+
+# Files from Forge MDK
+forge*changelog.txt
+/.architectury-transformer/
+/*/.architectury-transformer/
+/*/.idea/
+/*/.gradle/
+.cache
+/*/.cache/
+/.cache/
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..7b917bd
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,128 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, religion, or sexual identity
+and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the
+ overall community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or
+ advances of any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email
+ address, without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+support@thevrglab.com.
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series
+of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or
+permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within
+the community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.0, available at
+https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by [Mozilla's code of conduct
+enforcement ladder](https://github.com/mozilla/diversity).
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see the FAQ at
+https://www.contributor-covenant.org/faq. Translations are available at
+https://www.contributor-covenant.org/translations.
diff --git a/CREDITS.txt b/CREDITS.txt
new file mode 100644
index 0000000..a70c53d
--- /dev/null
+++ b/CREDITS.txt
@@ -0,0 +1,65 @@
+Minecraft Forge: Credits/Thank You
+
+Forge is a set of tools and modifications to the Minecraft base game code to assist
+mod developers in creating new and exciting content. It has been in development for
+several years now, but I would like to take this time thank a few people who have
+helped it along it's way.
+
+First, the people who originally created the Forge projects way back in Minecraft
+alpha. Eloraam of RedPower, and SpaceToad of Buildcraft, without their acceptiance
+of me taking over the project, who knows what Minecraft modding would be today.
+
+Secondly, someone who has worked with me, and developed some of the core features
+that allow modding to be as functional, and as simple as it is, cpw. For developing
+FML, which stabelized the client and server modding ecosystem. As well as the base
+loading system that allows us to modify Minecraft's code as elegently as possible.
+
+Mezz, who has stepped up as the issue and pull request manager. Helping to keep me
+sane as well as guiding the community into creating better additions to Forge.
+
+Searge, Bspks, Fesh0r, ProfMobious, and all the rest over on the MCP team {of which
+I am a part}. For creating some of the core tools needed to make Minecraft modding
+both possible, and as stable as can be.
+ On that note, here is some specific information of the MCP data we use:
+ * Minecraft Coder Pack (MCP) *
+ Forge Mod Loader and Minecraft Forge have permission to distribute and automatically
+ download components of MCP and distribute MCP data files. This permission is not
+ transitive and others wishing to redistribute the Minecraft Forge source independently
+ should seek permission of MCP or remove the MCP data files and request their users
+ to download MCP separately.
+
+And lastly, the countless community members who have spent time submitting bug reports,
+pull requests, and just helping out the community in general. Thank you.
+
+--LexManos
+
+=========================================================================
+
+This is Forge Mod Loader.
+
+You can find the source code at all times at https://github.com/MinecraftForge/MinecraftForge/tree/1.12.x/src/main/java/net/minecraftforge/fml
+
+This minecraft mod is a clean open source implementation of a mod loader for minecraft servers
+and minecraft clients.
+
+The code is authored by cpw.
+
+It began by partially implementing an API defined by the client side ModLoader, authored by Risugami.
+http://www.minecraftforum.net/topic/75440-
+This support has been dropped as of Minecraft release 1.7, as Risugami no longer maintains ModLoader.
+
+It also contains suggestions and hints and generous helpings of code from LexManos, author of MinecraftForge.
+http://www.minecraftforge.net/
+
+Additionally, it contains an implementation of topological sort based on that
+published at http://keithschwarz.com/interesting/code/?dir=topological-sort
+
+It also contains code from the Maven project for performing versioned dependency
+resolution. http://maven.apache.org/
+
+It also contains a partial repackaging of the javaxdelta library from http://sourceforge.net/projects/javaxdelta/
+with credit to it's authors.
+
+Forge Mod Loader downloads components from the Minecraft Coder Pack
+(http://mcp.ocean-labs.de/index.php/Main_Page) with kind permission from the MCP team.
+
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..fcb68ba
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,8 @@
+MIT License
+Copyright (c) 2023-Present Arad Bozorgmehr (Vrglab)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9366295
--- /dev/null
+++ b/README.md
@@ -0,0 +1,29 @@
+# Vrglab's AzureLib
+ ![Minecraft: 1.19.2](https://img.shields.io/static/v1?label=&message=1.19.2&color=2d2d2d&labelColor=4e4e4e&style=flat-square&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMjCGJ1kDAAACoElEQVQ4T22SeU8aURTF/ULGtNRWWVQY9lXABWldIDPIMgVbNgEVtaa0damiqGBdipXaJcY2ofEf4ycbTt97pVAabzK5b27u+Z377kwXgK77QthRy7OfXbeJM+ttqKSXN8sdwbT/A0L7elmsYqrPHZmROLPh5YkV4oEBwaKuHj+yyJptLDoAhbq3O1V1XCVObY3FL24mfn5oRPrcwSCRfQOyNWcjVjZdCbtcdwcgXrXUspdOKbDN/XE9tiBJMhXHT60gUIT2dMhcDLMc3NVKQklz0QIkf5qlyEcO6Qs7yPhMJB4amDMFimQSmqNlE8SKAZFzDfxHfVILIIZ10sJ3OwIbcqSuiOjchkzNCboHev9o2YhgiUP8mxnLN24I6/3ghYdtQG5iUMpFBuCP9iKwLsfiLyeCp2rMnZgwX3NArGoxW1Ridl+BzLEVKa8KSxOqNmDdz0kFnxaLHhWEgAyZigWhHXL+pEDy2ozsDxv8vAzTnh7w5kcghqCaFmCT10of4iPIT2mRdPUh4HoCcVwBH/8Ac2kzUkEV5r3EfVSOvbAJa5NDyI0r2oDtWb1EClh+OoC3Pg7v/Bw7p939yI4rsRW2Y3lKh01eh7WpIRyKZqzyjjYgPdIvlaMWRqYuG7wWryYHsRM0sFolZiPvQ3jheIwSmSBPdkByG/B6Wi3RYiVmRX7GiAPiUCRisii8D+jZNKvPBrHCW1GY0bAz6WkDCtOaSyKQFsi4K5NqNiZtehN2Y5uAShETqolhBqJXpfdPuPsuWwAaRdHSkxdc11mPqkGnyY4pyKbpl1GyJ0Pel7yqBoFcF3zqno5f+d8ohYy9Sx7lzQpxo1eirluCDgt++00p6uxttrG4F/A39sJGZWZMfrcp6O6+5kaVzXJHAOj6DeSs8qw5o8oxAAAAAElFTkSuQmCC)
+![Minecraft: 1.20,4](https://img.shields.io/static/v1?label=&message=1.20.4&color=2d2d2d&labelColor=4e4e4e&style=flat-square&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMjCGJ1kDAAACoElEQVQ4T22SeU8aURTF/ULGtNRWWVQY9lXABWldIDPIMgVbNgEVtaa0damiqGBdipXaJcY2ofEf4ycbTt97pVAabzK5b27u+Z377kwXgK77QthRy7OfXbeJM+ttqKSXN8sdwbT/A0L7elmsYqrPHZmROLPh5YkV4oEBwaKuHj+yyJptLDoAhbq3O1V1XCVObY3FL24mfn5oRPrcwSCRfQOyNWcjVjZdCbtcdwcgXrXUspdOKbDN/XE9tiBJMhXHT60gUIT2dMhcDLMc3NVKQklz0QIkf5qlyEcO6Qs7yPhMJB4amDMFimQSmqNlE8SKAZFzDfxHfVILIIZ10sJ3OwIbcqSuiOjchkzNCboHev9o2YhgiUP8mxnLN24I6/3ghYdtQG5iUMpFBuCP9iKwLsfiLyeCp2rMnZgwX3NArGoxW1Ridl+BzLEVKa8KSxOqNmDdz0kFnxaLHhWEgAyZigWhHXL+pEDy2ozsDxv8vAzTnh7w5kcghqCaFmCT10of4iPIT2mRdPUh4HoCcVwBH/8Ac2kzUkEV5r3EfVSOvbAJa5NDyI0r2oDtWb1EClh+OoC3Pg7v/Bw7p939yI4rsRW2Y3lKh01eh7WpIRyKZqzyjjYgPdIvlaMWRqYuG7wWryYHsRM0sFolZiPvQ3jheIwSmSBPdkByG/B6Wi3RYiVmRX7GiAPiUCRisii8D+jZNKvPBrHCW1GY0bAz6WkDCtOaSyKQFsi4K5NqNiZtehN2Y5uAShETqolhBqJXpfdPuPsuWwAaRdHSkxdc11mPqkGnyY4pyKbpl1GyJ0Pel7yqBoFcF3zqno5f+d8ohYy9Sx7lzQpxo1eirluCDgt++00p6uxttrG4F/A39sJGZWZMfrcp6O6+5kaVzXJHAOj6DeSs8qw5o8oxAAAAAElFTkSuQmCC)
+[](https://www.curseforge.com/minecraft/mc-mods/vrglabs-azurelib) [![License](https://img.shields.io/github/license/vrglab/Vrglabs-AzureLib)](LICENSE.txt) [![Releases](https://img.shields.io/github/v/release/vrglab/Vrglabs-AzureLib)](https://github.com/vrglab/Vrglabs-AzureLib/releases)
+
+
+
+This project is a Fork Of [AzureLib](https://www.curseforge.com/minecraft/mc-mods/azurelib) for using with [Vrglabs Lib](https://www.curseforge.com/minecraft/mc-mods/vrglabs-lib).
+It aims to add QOL functionality and Quilt support.
+
+## Added Functionalities:
+### QOL Improvements:
+* easier Custom Armor model Loading
+* easier Custom Item model Loading
+* easier Custom Block model Loading
+
+### Other Functionalities:
+* Added Support for the Quilt ModLoader
+* Added Support for Architectury API
+* Added Support for Vrglabs Lib
+
+
+# Supported Modloader's
+1.20.4: NeoForge, Fabric, Quilt
+
+# Using in your Own Mod
+
+*Please Refere To the [Documentation](https://docs.vrglabs-azurelib.thevrglab.com) for Clear instructions on how to use for mod Development*
+
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..b0f537d
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,10 @@
+# Security Policy
+
+## Supported Versions
+
+| Version | ModLoader |Supported |
+| ------- |------|------------------ |
+| 1.0.0-mc1.19.2 | Fabric, Forge, Quilt |:white_check_mark: |
+
+## Reporting a Vulnerability
+Please Report issues and Vulnerablities in the issues segment of the repository
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..621123f
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,78 @@
+plugins {
+ id 'dev.architectury.loom' version '1.6-SNAPSHOT' apply false
+ id 'architectury-plugin' version '3.4-SNAPSHOT'
+ id 'com.github.johnrengelman.shadow' version '8.1.1' apply false
+}
+
+architectury {
+ minecraft = project.minecraft_version
+}
+
+allprojects {
+ group = rootProject.maven_group
+ version = "$project.name-$rootProject.mod_version-mc$rootProject.minecraft_version"
+ repositories {
+ maven {url 'https://libs.azuredoom.com:4443/mods'}
+ maven { url "https://maven.terraformersmc.com/releases" }
+ maven { url "https://maven.thevrglab.com/" }
+ }
+}
+
+subprojects {
+ apply plugin: 'dev.architectury.loom'
+ apply plugin: 'architectury-plugin'
+ apply plugin: 'maven-publish'
+
+ base {
+ // Set up a suffixed format for the mod jar names, e.g. `example-fabric`.
+ archivesName = "$rootProject.archives_name"
+ }
+
+ repositories {
+ // Add repositories to retrieve artifacts from in here.
+ // You should only use this when depending on other mods because
+ // Loom adds the essential maven repositories to download Minecraft and libraries from automatically.
+ // See https://docs.gradle.org/current/userguide/declaring_repositories.html
+ // for more information about repositories.
+ }
+
+ dependencies {
+ minecraft "net.minecraft:minecraft:$rootProject.minecraft_version"
+ mappings loom.layered() {
+ "net.fabricmc:yarn:$rootProject.yarn_mappings:v2"
+ officialMojangMappings()
+ }
+ }
+
+ java {
+ // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task
+ // if it is present.
+ // If you remove this line, sources will not be generated.
+ withSourcesJar()
+
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ tasks.withType(JavaCompile).configureEach {
+ it.options.release = 17
+ }
+
+ // Configure Maven publishing.
+ publishing {
+ publications {
+ mavenJava(MavenPublication) {
+ artifactId = base.archivesName.get()
+ from components.java
+ }
+ }
+
+ // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing.
+ repositories {
+ // Add repositories to publish to here.
+ // Notice: This block does NOT have the same function as the block in the top level.
+ // The repositories here will be used for publishing your artifact, not for
+ // retrieving dependencies.
+ }
+ }
+}
diff --git a/changelog.txt b/changelog.txt
new file mode 100644
index 0000000..00dced5
--- /dev/null
+++ b/changelog.txt
@@ -0,0 +1,400 @@
+1.19.x Changelog
+41.1
+====
+ - 41.1.0 Mark 1.19 RB
+
+41.0
+====
+ - 41.0.113 Allow faces of an "elements" model to be made emissive (#8890)
+ - 41.0.112 Fix invalid channel names sent from the server causing the network thread to error. (#8902)
+ - 41.0.111 Fix PlayerEvent.BreakSpeed using magic block position to signify invalid position. Closes #8906
+ - 41.0.110 Fix cases where URIs would not work properly with JarInJar (#8900)
+ - 41.0.109 Add new hook to allow modification of lightmap via Dimension special effects (#8863)
+ - 41.0.108 Fix Forge's packet handling on play messages. (#8875)
+ - 41.0.107 Add API for tab list header/footer (#8803)
+ - 41.0.106 Allow modded blocks overriding canStickTo prevent sticking to vanilla blocks/other modded blocks (#8837)
+ - 41.0.105 Multiple tweaks and fixes to the recent changes in the client refactor PR: Part 3 (#8864)
+ Fix weighted baked models not respecting children render types
+ Allow fluid container model to use base texture as particle
+ Fix inverted behavior in composite model building. Fixes #8871
+ - 41.0.104 Fix crossbows not firing ArrowLooseEvent (#8887)
+ - 41.0.103 Add User-Agent header to requests made by the update checker (#8881)
+ Format: Java-http-client/ MinecraftForge//
+ - 41.0.102 Output the full path in a crash report so it is easier to find the outer mod when a crash in Jar-In-Jar occurs. (#8856)
+ - 41.0.101 Clean up the pick item ("middle mouse click") patches (#8870)
+ - 41.0.100 [1.19.x] Hotfix for test mods while the refactor is ongoing
+ - 41.0.99 add event to SugarCaneBlock (#8877)
+ - 41.0.98 Fix Global Loot Modifiers not using Dispatch Codec (#8859)
+ - 41.0.97 Allow block render types to be set in datagen (#8852)
+ - 41.0.96 Fix renderBreakingTexture not using the target's model data (#8849)
+ - 41.0.95 Multiple tweaks and fixes to the recent changes in the client refactor PR: Part 2 (#8854)
+ * Add getter for the component names in an unbaked geometry
+ * Fix render type hint not being copied in BlockGeometryBakingContext
+ * Ensure BlockRenderDispatches's renderSingleBlock uses the correct buffer
+ - 41.0.94 [1.19.x] Apply general renames, A SRG is provided for modders. (#8840)
+ See https://gist.github.com/SizableShrimp/882a671ff74256d150776da08c89ef72
+ - 41.0.93 Fix mob block breaking AI not working correctly when chunk 0,0 is unloaded. Closes #8853
+ - 41.0.92 Fix crash when breaking blocks with multipart models and remove caching. Closes #8850
+ - 41.0.91 Fixed `CompositeModel.Baked.Builder.build()` passing arguments in the wrong order (#8846)
+ - 41.0.90 Make cutout mipmaps explicitly opt-in for item/entity rendering (#8845)
+ * Make cutout mipmaps explicitly opt-in for item/entity rendering
+ * Default render type domain to "minecraft" in model datagens
+ - 41.0.89 Fixed multipart block models not using the new model driven render type system. (#8844)
+ - 41.0.88 Update to the latest JarJar to fix a collision issue where multiple jars could provide an exact match. (#8847)
+ - 41.0.87 Add FML config to disable DFU optimizations client-side. (#8842)
+ * Add client-side command line argument to disable DFU optimizations.
+ * Switch to using FMLConfig value instead.
+ - 41.0.86 [1.19] Fixed broken BufferBuilder.putBulkData(ByteBuffer) added by Forge (#8819)
+ * Fixes BufferBuilder.putBulkData(ByteBuffer)
+ * use nextElementByte
+ * Fixed merge conflict
+ - 41.0.85 [1.19.x] Fix shulker boxes allowing input of items, that return false for Item#canFitInsideContainerItems, through hoppers. (#8823)
+ * Make ShulkerBoxBlockEntity#canPlaceItemThroughFace delegate to Item#canFitInsideContainerItems.
+ * Switch to using Or and add comment.
+ * Switch Or to And.
+ - 41.0.84 [1.19.x] Added RenderLevelStageEvent to replace RenderLevelLastEvent (#8820)
+ * Ported RenderLevelStageEvent from 1.18.2
+ * Updated to fix merge conflicts
+ - 41.0.83 [1.19.x] Fix door datagenerator (#8821)
+ * Fix door datagenerator
+ Fix datagenerator for door blocks. Successor to #8687, addresses comments made there about statement complexity.
+ * Fix extra space around parameter
+ Fix extra space before comma around a parameter.
+ - 41.0.82 Create PieceBeardifierModifier to re-enable piecewise beardifier definitions (#8798)
+ - 41.0.81 Allow blocks to provide a dynamic MaterialColor for display on maps (#8812)
+ - 41.0.80 [1.19.x] BiomeTags Fixes/Improvements (#8711)
+ * dimension specific tag fix
+ * remove forge:is_beach cause vanilla has it already
+ * remove forge tags for new 1.19 vanilla tags (savanna, beach, overworld, end)
+ Co-authored-by: Flemmli97
+ - 41.0.79 1.19 - Remove GlobalLootModifierSerializer and move to Codecs (#8721)
+ * convert GLM serializer class to codec
+ * cleanup
+ * GLM list needs to be sorted
+ * datagen
+ * simplify serialization
+ * fix test mods (oops)
+ * properly use suppliers for codec as they are registry obj
+ - 41.0.78 Implement item hooks for potions and enchantments (#8718)
+ * Implement item hooks for potions and enchantments
+ * code style fixes
+ - 41.0.77 Re-apply missing patch to ServerLevel.EntityCallbacks#onTrackingEnd() (#8828)
+ - 41.0.76 Double Bar Rendering fixed (#8806) (#8807)
+ * Double Bar Rendering fixed (#8806)
+ * Added requested changes by sciwhiz12
+ - 41.0.75 Multiple tweaks and fixes to the recent changes in the client refactor PR (#8836)
+ * Add an easy way to get the NamedGuiOverlay from a vanilla overlay
+ * Fix static member ordering crash in UnitTextureAtlasSprite
+ * Allow boss bar rendering to be cancelled
+ * Make fluid container datagen use the new name
+ - 41.0.74 Add FogMode to ViewportEvent.RenderFog (#8825)
+ - 41.0.73 Provide additional context to the getFieldOfView event (#8830)
+ - 41.0.72 Pass renderType to IForgeBakedModel.useAmbientOcclusion (#8834)
+ - 41.0.71 Load custom ITransformationServices from the classpath in dev (#8818)
+ * Add a classpath transformer discoverer to load custom transformation services from the classpath
+ * Update ClasspathTransformerDiscoverer to 1.18
+ * Update license year
+ * Update license header
+ * Fix the other license headers
+ * Update ClasspathTransformerDiscoverer to 1.19
+ - 41.0.70 Handle modded packets on the network thread (#8703)
+ * Handle modded packets on the network thread
+ - On the server we simply need to remove the call to
+ ensureRunningOnSameThread.
+ - On the client side, we now handle the packet at the very start of the
+ call. We make sure we're running from a network thread to prevent
+ calling the handling code twice.
+ While this does mean we no longer call .release(), in practice this
+ doesn't cause any leaks as ClientboundCustomPayloadPacket releases
+ for us.
+ * Clarify behaviour a little in the documentation
+ * Javadoc formatting
+ * Add a helper method for handling packets on the main thread
+ Also rename the network thread one. Should make it clearer the expected
+ behaviour of the two, and make it clearer there's a potentially breaking
+ change.
+ * Add back consumer() methods
+ Also document EventNetworkChannel, to clarify the thread behaviour
+ there.
+ * Add since = "1.19" to deprecated annotations
+ - 41.0.69 Cache resource listing calls in resource packs (#8829)
+ * Make the resource lookups cached.
+ * Include configurability and handle patch cleanup.
+ * Document and comment the cache manager.
+ * Make thread selection configurable.
+ * Implement a configurable loading mechanic that falls back to default behaviour when the config is not bound yet.
+ * Use boolean supplier and fix wildcard import.
+ * Clean up the VPR since this is more elegant.
+ * Clean up the VPR since this is more elegant.
+ * Address review comments.
+ * Address more review comments.
+ * Fix formatting on `getSource`
+ * Address comments by ichtt
+ * Adapt to pups requests.
+ * Stupid idea.
+ * Attempt this again with a copy on write list.
+ * Fix a concurrency and loading issue.
+ * Fix #8813
+ Checks if the paths are valid resource paths.
+ * Move the new methods on vanilla Patch.
+ - 41.0.68 Update SJH and JIJ
+ - 41.0.67 Fix #8833 (#8835)
+ - 41.0.66 Fix backwards fabulous check in SimpleBakedModel (#8832)
+ Yet another blunder we missed during the review of #8786.
+ - 41.0.65 Make texture atlas in StandaloneGeometryBakingContext configurable (#8831)
+ - 41.0.64 [1.19.X] Client code cleanup, updates, and other refactors (#8786)
+ * Revert "Allow safely registering RenderType predicates at any time (#8685)"
+ This reverts commit be7275443fd939db9c58bcad47079c3767789ac1.
+ * Renderable API refactors
+ - Rename "render values" to "context"
+ - Rename SimpleRenderable to CompositeRenderable to better reflect its use
+ - Remove IMultipartRenderValues since it doesn't have any real use
+ - Add extensive customization options to BakedModelRenderable
+ * ClientRegistry and MinecraftForgeClient refactors
+ - Add sprite loader manager and registration event
+ - Add spectator shader manager and registration event
+ - Add client tooltip factory manager and registration event
+ - Add recipe book manager and registration event
+ - Add key mapping registration event
+ - Remove ClientRegistry, as everything has been moved out of it
+ - Remove registration methods from MinecraftForgeClient, as they have dedicated events now
+ * Dimension special effects refactors
+ - Fold handlers into an extension class and remove public mutable fields
+ - Add dimension special effects manager and registration event
+ * HUD overlay refactors
+ - Rename to IGuiOverlay match vanilla (instead of Ingame)
+ - Add overlay manager and registration event
+ - Move vanilla overlays to a standalone enum
+ * Model loader refactors
+ - Rename IModelLoader to IGeometryLoader
+ - Add loader manager and registration event
+ - Fold all model events into one
+ - Move registration of additionally loaded models to an event
+ - Remove ForgeModelBakery and related classes as they served no purpose anymore
+ * Render properties refactors
+ - Rename all render properties to client extensions and relocate accordingly
+ - Move lookups to the respective interfaces
+ * Model data refactors
+ - Convert model data to a final class backed by an immutable map and document mutability requirements. This addresses several thread-safety issues in the current implementation which could result in race conditions
+ - Transfer ownership of the data manager to the client level. This addresses several issues that arise when multiple levels are used at once
+ * GUI and widget refactors
+ - Move all widgets to the correct package
+ - Rename GuiUtils and children to match vanilla naming
+ * New vertex pipeline API
+ - Move to vanilla's VertexConsumer
+ - Roll back recent PR making VertexConsumer format-aware. This is the opposite of what vanilla does, and should not be relevant with the updated lighting pipeline
+ * Lighting pipeline refactors
+ - Move to dedicated lighting package
+ - Separate flat and smooth lighters
+ - Convert from a vertex pipeline transformer to a pure vertex source (input is baked quads)
+ * Model geometry API refactors
+ - Rename IModelGeometry to IUnbakedGeometry
+ - Rename IModelConfiguration to IGeometryBakingContext
+ - Rename other elements to match vanilla naming
+ - Remove current changes to ModelState, as they do not belong there. Transforms should be specified through vanilla's system. ModelState is intended to transfer state from the blockstate JSON
+ - Remove multipart geometries and geometry parts. After some discussion, these should not be exposed. Instead, geometries should be baked with only the necessary parts enabled
+ * Make render types a first-class citizen in baked models
+ - Add named render types (block + entity + fabulous entity)
+ - Add named render type manager + registration event
+ - Make BakedModel aware of render types and transfer control over which ones are used to it instead of ItemBlockRenderTypes (fallback)
+ - (additional) Add concatenated list view. A wrapper for multiple lists that iterates through them in order without the cost of merging them. Useful for merging lists of baked quads
+ * General event refactors
+ - Several renames to either match vanilla or improve clarity
+ - Relocate client chat event dispatching out of common code
+ * Forge model type refactors
+ - Rename SeparatePerspectiveModel to SeparateTransformsModel
+ - Rename ItemModelMesherForge to ForgeItemModelShaper
+ - Rename DynamicBucketModel to DynamicFluidContainerModel
+ - Prefix all OBJ-related classes with "Obj" and decouple parsing from construction
+ - Extract ElementsModel from model loader registry
+ - Add EmptyModel (baked, unbaked and loader)
+ - Refactor CompositeModel to take over ItemMultiLayerBakedModel
+ - Remove FluidModel as it's not used and isn't compatible with the new fluid rendering in modern versions
+ - Move model loader registration to a proper event handler
+ - Update names of several JSON fields (backwards-compatible)
+ - Update datagens to match
+ * Miscellaneous changes and overlapping patches
+ - Dispatch all new registration events
+ - Convert ExtendedServerListData to a record
+ - Add/remove hooks from ForgeHooksClient as necessary
+ * Update test mods
+ * Fix VertexConsumerWrapper returning parent instead of itself
+ * Additional event cleanup pass
+ As discussed on Discord:
+ - Remove "@hidden" and "@see " javadoc annotations from all client events and replace them with @ApiStatus.Internal annotation
+ - Make all events that shouldn't be fired directly into abstract classes with protected constructors
+ - Another styling pass, just in case (caught some missed classes)
+ * Add proper deprecation javadocs and de-dupe some vertex consumer code
+ * Replace sets of chunk render types with a faster BitSet-backed collection
+ This largely addresses potential performance concerns that using a plain HashSet might involve by making lookups and iteration as linear as they can likely be (aside from using a plain byte/int/long for bit storage). Further performance concerns related to the implementation may be addressed separately, as all the implementation details are hidden from the end user
+ * Requested changes
+ - Remove MinecraftForgeClient and move members to Minecraft, IForgeMinecraft and StencilManager
+ - Allow non-default elements to be passed into VertexConsumer and add support to derived classes
+ - Move array instantiation out of quad processing in lighting pipeline
+ - Fix flipped fluid container model
+ - Set default UV1 to the correct values in the remapping pipeline
+ - Minor documentation changes
+ * Add/update EXC entries and fix AT comment
+ * Add test mod as per Orion's request
+ * Additional requested changes
+ * Allow custom model types to request the particle texture to be loaded
+ * Even more requested changes
+ * Improve generics in ConcatenatedListView and add missing fallbacks
+ * Fix fluid render types being bound to the fluid and not its holder
+ * Remove non-contractual nullability in ChunkRenderTypeSet and add isEmpty
+ Additionally, introduce chunk render type checks in ItemBlockRenderTypes
+ Co-authored-by: Dennis C
+ - 41.0.63 Implement full support for IPv6 (#8742)
+ - 41.0.62 Fix certain user-configured options being overwritten incorrectly due to validators. (#8780)
+ - 41.0.61 Allow safely registering RenderType predicates at any time (#8685)
+ - 41.0.60 Fix crash after loading error due to fluid texture gathering and config lookup (#8802)
+ - 41.0.59 Remove the configuration option for handling empty tags in ingredients. (#8799)
+ Now empty tags are considered broken in all states.
+ - 41.0.58 Fix MC-105317 Structure blocks do not rotate entities correctly when loading (#8792)
+ - 41.0.57 Fire ChunkWatchEvents after sending packets (#8747)
+ - 41.0.56 Add item handler capability to chest boats (#8787)
+ - 41.0.55 Add getter for correct BiomeSpecialEffectsBuilder to BiomeInfo$Builder (#8781)
+ - 41.0.54 Fix BlockToolModificationEvent missing cancelable annotation (#8778)
+ - 41.0.53 Fix ticking chunk tickets from forge's chunk manager not causing chunks to fully tick (#8775)
+ - 41.0.52 Fix default audio device config loading string comparison issue (#8767)
+ - 41.0.51 Fix missed vanilla method overrides in ForgeRegistry (#8766)
+ - 41.0.50 Add MinecraftServer reference to ServerTickEvent (#8765)
+ - 41.0.49 Fix TagsProviders for datapack registries not recognizing existing files (#8761)
+ - 41.0.48 Add callback after a BlockState was changed and the neighbors were updated (#8686)
+ - 41.0.47 Add biome tag entries for 1.19 biomes (#8684)
+ - 41.0.46 Make fishing rods use tool actions for relevant logic (#8681)
+ - 41.0.45 Update BootstrapLauncher to 1.1.1 and remove the forced
+ merge of text2speech since new BSL does it.
+ - 41.0.44 Merge text2speech libs together so the natives are part of the jar
+ - 41.0.43 Make Forge ConfigValues implement Supplier. (#8776)
+ - 41.0.42 Fix merge derp in AbstractModProvider and logic derp in ModDiscoverer
+ - 41.0.41 Add "send to mods in order" method to ModList and use it (#8759)
+ * Add "send to mods in order" method to ModList and use it in RegistryEvents and DataGen..
+ * Also preserve order in runAll
+ * Do better comparator thanks @pupnewfster
+ * postEvent as well.
+ - 41.0.40 Update SJH to 2.0.2.. (#8774)
+ * Update SJH to 2.0.3..
+ - 41.0.39 Sanity check the version specified in the mod file (#8749)
+ * Sanity check the version specified in the mod file to
+ make sure it's compatible with JPMS standards for
+ version strings.
+ Closes #8748
+ Requires SPI 6
+ - 41.0.38 Fix SP-Devtime world loading crash due to missing server configs (#8757)
+ - 41.0.37 Remove ForgeWorldPreset and related code (#8756)
+ Vanilla has a working replacement.
+ - 41.0.36 Change ConfigValue#get() to throw if called before config loaded (#8236)
+ This prevents silent issues where a mod gets the value of the setting
+ before configs are loaded, which means the default value is always
+ returned.
+ As there may be situations where the getting the config setting before
+ configs are loaded is needed, and it is not preferable to hardcode the
+ default value, the original behavior is made available through #getRaw.
+ Implements and closes #7716
+ * Remove getRaw() method
+ This is effectively replaced with the expression `spec.isLoaded() ?
+ configValue.get() : configValue.getDefault()`.
+ * Remove forceSystemNanoTime config setting
+ As implemented, it never had any effect as any place where the config
+ value would be queried happens before the configs are loaded.
+ - 41.0.35 Fix EnumArgument to use enum names for suggestions (#8728)
+ Previously, the suggestions used the string representation of the enum
+ through Enum#toString, which can differ from the name of the enum as
+ required by Enum#valueOf, causing invalid suggestions (both in gui and
+ through the error message).
+ - 41.0.34 Jar-In-Jar (#8715)
+ - 41.0.33 [1.19] Fix data-gen output path of custom data-pack registries (#8724)
+ - 41.0.32 Fix player dive and surface animations in custom fluids (#8738)
+ - 41.0.31 [1.19.x] Affect ItemEntity Motion in Custom Fluids (#8737)
+ - 41.0.30 [1.19] Add support for items to add enchantments without setting them in NBT (#8719)
+ - 41.0.29 [1.19.x] Add stock biome modifier types for adding features and spawns (#8697)
+ - 41.0.28 [1.19.x] Fluid API Overhaul (#8695)
+ - 41.0.27 Replace StructureSpawnListGatherEvent with StructureModifiers (#8717)
+ - 41.0.26 Use stack sensitive translation key by default for FluidAttributes. (#8707)
+ - 41.0.25 Delete LootItemRandomChanceCondition which added looting bonus enchantment incorrectly. (#8733)
+ - 41.0.24 Update EventBus to 6.0, ModLauncher to 10.0.1 and BootstrapLauncher to 1.1 (#8725)
+ - 41.0.23 Replace support bot with support action (#8700)
+ - 41.0.22 Fix Reach Distance / Attack Range being clamped at 6.0 (#8699)
+ - 41.0.21 [1.19.x] Fix mods' worldgen data not being loaded when creating new singleplayer worlds (#8693)
+ - 41.0.20 [1.19.x] Fix experimental confirmation screen (#8727)
+ - 41.0.19 Move is_mountain to forge's tag instead of vanilla's (#8726)
+ - 41.0.18 [1.19.x] Add CommandBuildContext to Register Command Events (#8716)
+ - 41.0.17 Only rewrite datagen cache when needed (#8709)
+ - 41.0.16 Implement a simple feature system for Forge (#8670)
+ * Implement a simple feature system for Forge. Allows mods to demand certain features are available in the loading system. An example for java_version is provided, but not expected to be used widely. This is more targeted to properties of the display, such as GL version and glsl profile.
+ Requires https://github.com/MinecraftForge/ForgeSPI/pull/13 to be merged first in ForgeSPI, and the SPI to be updated appropriately in build.gradle files.
+ * rebase onto 1.19 and add in SPI update
+ - 41.0.15 displayTest option in mods.toml (#8656)
+ * displayTest option in mods.toml
+ * "MATCH_VERSION" (or none) is existing match version string behaviour
+ * "IGNORE_SERVER_VERSION" accepts anything and sends special SERVERONLY string
+ * "IGNORE_ALL_VERSION" accepts anything and sends an empty string
+ * "NONE" allows the mod to supply their own displaytest using the IExtensionPoint mechanism.
+ * Update display test with feedback and added the mods.toml discussion in mdk.
+ - 41.0.14 Update forgeSPI to v5 (#8696)
+ - 41.0.13 Make IVertexConsumers such as the lighting pipeline, be aware of which format they are dealing with. (#8692)
+ Also fix Lighting pipeline ignoring the overlay coords from the block renderer.
+ - 41.0.12 Fixed misaligned patch to invalidateCaps in Entity (#8705)
+ - 41.0.11 Fix readAdditionalLevelSaveData (#8704)
+ - 41.0.10 Fixes setPos to syncPacketPositionCodec (#8702)
+ - 41.0.9 Fix wrong param passed to PlayLevelSoundEvent.AtEntity (#8688)
+ - 41.0.8 Override initialize in SlotItemHandler, so it uses the itemhandler instead of container (#8679)
+ - 41.0.7 Update MDK for 1.19 changes (#8675)
+ - 41.0.6 Add helper to RecipeType, and fix eclipse compiler error in test class.
+ - 41.0.5 Update modlauncher to latest (#8691)
+ - 41.0.4 Fix getting entity data serializer id crashing due to improper port to new registry system (#8678)
+ - 41.0.3 Fire registry events in the order vanilla registers to registries (#8677)
+ Custom registries are still fired in alphabetical order, after all vanilla registries.
+ Move forge's data_serializers registry to forge namespace.
+ - 41.0.2 Add method with pre/post wrap to allow setting/clearing mod context. (#8682)
+ Fixes ActiveContainer in ModContext not being present in registry events. Closes #8680
+ - 41.0.1 Fix the Curlie oopsie
+ - 41.0.0 Forge 1.19
+ * Bump pack.mcmeta formats
+ * 1.19 biome modifiers
+ * Mark ClientPlayerNetworkEvent.LoggedOutEvent's getters as nullable
+ * Add docs and package-info to client extension interfaces package
+ * Move RenderBlockOverlayEvent hooks to ForgeHooksClient
+ * Add package-infos to client events package
+ * Rename SoundLoadEvent to SoundEngineLoadEvent
+ This reduces confusion from consumers which may think the
+ name SoundLoadEvent refers to an individual sound being loaded rather
+ than the sound engine.
+ * Document and change SoundLoadEvent to fire on mod bus
+ Previously, it fired on both the mod bus and the Forge bus, which is
+ confusing for consumers.
+ * Delete SoundSetupEvent
+ Looking at its original implementation shows that there isn't an
+ appropriate place in the new sound code to reinsert the event, and the
+ place of 'sound engine/manager initialization event' is taken already by SoundLoadEvent.
+ * Perform some cleanup on client events
+ - Removed nullable annotations from ClientPlayerNetworkEvent
+ - Renamed #getPartialTicks methods to #getPartialTick, to be consistent
+ with vanilla's naming of the partial tick
+ - Cleanup documentation to remove line breaks, use the
+ spelling 'cancelled' over
+ 'canceled', and improve docs on existing and
+ new methods.
+ * Remove EntityEvent.CanUpdate
+ Closes MinecraftForge/MinecraftForge#6394
+ * Switch to Jetbrains nullability annotations
+ * New PlayLevelSoundEvent; replaces old PlaySoundAtEntityEvent
+ * Remove ForgeWorldPresetScreens
+ * Remove IForgeRegistryEntry
+ * Remove use of List in FML's CompletableFutures
+ * Add docs to mod loading stages, stages, and phases
+ * Gradle 7.4.2
+ * Use SLF4J in FMLLoader and other subprojects
+ * Switch dynamic versions in subprojects to pinned ones
+ * Switch ForgeRoot and MDK to FG plugin markers
+ * Configure Forge javadoc task
+ The task now uses a custom stylesheet with MCForge elements, and
+ configured to combine the generation from the four FML subprojects
+ (fmlloader, fmlcore, javafmllanguage, mclanguage) and the Forge project
+ into the javadoc output.
+ * Update docs/md files, for 1.19 update and the move away from IRC to Discord.
+ * Make "Potentially dangerous alternative prefix" a debug warning, not info.
+ Co-authored-by: Curle
+ Co-authored-by: sciwhiz12
+
diff --git a/common/build.gradle b/common/build.gradle
new file mode 100644
index 0000000..345a65d
--- /dev/null
+++ b/common/build.gradle
@@ -0,0 +1,15 @@
+architectury {
+ common rootProject.enabled_platforms.split(',')
+}
+
+dependencies {
+ // We depend on Fabric Loader here to use the Fabric @Environment annotations,
+ // which get remapped to the correct annotations on each platform.
+ // Do NOT use other classes from Fabric Loader.
+ modImplementation "net.fabricmc:fabric-loader:$rootProject.fabric_loader_version"
+
+ modImplementation "org.Vrglab:vrglabslib:common-$rootProject.vrglabs_lib_version-mc$rootProject.minecraft_version"
+
+ // Architectury API. This is optional, and you can comment it out if you don't need it.
+ modImplementation "dev.architectury:architectury:$rootProject.architectury_api_version"
+}
diff --git a/common/src/main/java/mod/azure/azurelib/common/api/client/helper/ClientUtils.java b/common/src/main/java/mod/azure/azurelib/common/api/client/helper/ClientUtils.java
new file mode 100644
index 0000000..c8c4b39
--- /dev/null
+++ b/common/src/main/java/mod/azure/azurelib/common/api/client/helper/ClientUtils.java
@@ -0,0 +1,57 @@
+package mod.azure.azurelib.common.api.client.helper;
+
+import com.mojang.blaze3d.platform.InputConstants;
+import com.mojang.blaze3d.vertex.PoseStack;
+import com.mojang.math.Axis;
+import mod.azure.azurelib.common.internal.client.renderer.GeoRenderer;
+import net.minecraft.client.KeyMapping;
+import net.minecraft.client.Minecraft;
+import net.minecraft.util.Mth;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.world.level.Level;
+import org.lwjgl.glfw.GLFW;
+
+/**
+ * Helper class for segregating client-side code
+ */
+public final class ClientUtils {
+
+ /**
+ * Translates the provided {@link PoseStack} to face towards the given {@link Entity}'s rotation.
+ * Usually used for rotating projectiles towards their trajectory, in an {@link GeoRenderer#preRender} override.
+ */
+ public static void faceRotation(PoseStack poseStack, Entity animatable, float partialTick) {
+ poseStack.mulPose(Axis.YP.rotationDegrees(Mth.lerp(partialTick, animatable.yRotO, animatable.getYRot()) - 90));
+ poseStack.mulPose(Axis.ZP.rotationDegrees(Mth.lerp(partialTick, animatable.xRotO, animatable.getXRot())));
+ }
+
+ /**
+ * Get the player on the client
+ */
+ public static Player getClientPlayer() {
+ return Minecraft.getInstance().player;
+ }
+
+ /**
+ * Gets the current level on the client
+ */
+ public static Level getLevel() {
+ return Minecraft.getInstance().level;
+ }
+
+ /**
+ * Common reload KeyMapping for my various mods
+ */
+ public static KeyMapping RELOAD;
+
+ /**
+ * Common scope KeyMapping for my various mods
+ */
+ public static KeyMapping SCOPE;
+
+ /**
+ * Common scope KeyMapping for my various mods
+ */
+ public static KeyMapping FIRE_WEAPON;
+}
diff --git a/common/src/main/java/mod/azure/azurelib/common/api/client/model/DefaultedBlockGeoModel.java b/common/src/main/java/mod/azure/azurelib/common/api/client/model/DefaultedBlockGeoModel.java
new file mode 100644
index 0000000..af80096
--- /dev/null
+++ b/common/src/main/java/mod/azure/azurelib/common/api/client/model/DefaultedBlockGeoModel.java
@@ -0,0 +1,54 @@
+package mod.azure.azurelib.common.api.client.model;
+
+import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable;
+import net.minecraft.resources.ResourceLocation;
+
+/**
+ * {@link DefaultedGeoModel} specific to {@link net.minecraft.world.level.block.Block Blocks}.
+ * Using this class pre-sorts provided asset paths into the "block" subdirectory
+ */
+public class DefaultedBlockGeoModel extends DefaultedGeoModel {
+ /**
+ * Create a new instance of this model class.
+ * The asset path should be the truncated relative path from the base folder.
+ * E.G.
+ *
{@code
+ * new ResourceLocation("myMod", "workbench/sawmill")
+ * }
+ */
+ public DefaultedBlockGeoModel(ResourceLocation assetSubpath) {
+ super(assetSubpath);
+ }
+
+ @Override
+ protected String subtype() {
+ return "block";
+ }
+
+ /**
+ * Changes the constructor-defined model path for this model to an alternate.
+ * This is useful if your animatable shares a model path with another animatable that differs in path to the texture and animations for this model
+ */
+ @Override
+ public DefaultedBlockGeoModel withAltModel(ResourceLocation altPath) {
+ return (DefaultedBlockGeoModel)super.withAltModel(altPath);
+ }
+
+ /**
+ * Changes the constructor-defined animations path for this model to an alternate.
+ * This is useful if your animatable shares an animations path with another animatable that differs in path to the model and texture for this model
+ */
+ @Override
+ public DefaultedBlockGeoModel withAltAnimations(ResourceLocation altPath) {
+ return (DefaultedBlockGeoModel)super.withAltAnimations(altPath);
+ }
+
+ /**
+ * Changes the constructor-defined texture path for this model to an alternate.
+ * This is useful if your animatable shares a texture path with another animatable that differs in path to the model and animations for this model
+ */
+ @Override
+ public DefaultedBlockGeoModel withAltTexture(ResourceLocation altPath) {
+ return (DefaultedBlockGeoModel)super.withAltTexture(altPath);
+ }
+}
diff --git a/common/src/main/java/mod/azure/azurelib/common/api/client/model/DefaultedEntityGeoModel.java b/common/src/main/java/mod/azure/azurelib/common/api/client/model/DefaultedEntityGeoModel.java
new file mode 100644
index 0000000..23a690a
--- /dev/null
+++ b/common/src/main/java/mod/azure/azurelib/common/api/client/model/DefaultedEntityGeoModel.java
@@ -0,0 +1,83 @@
+package mod.azure.azurelib.common.api.client.model;
+
+import mod.azure.azurelib.common.internal.common.constant.DataTickets;
+import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable;
+import mod.azure.azurelib.common.internal.common.core.animatable.model.CoreGeoBone;
+import mod.azure.azurelib.common.internal.common.core.animation.AnimationState;
+import mod.azure.azurelib.common.internal.client.model.data.EntityModelData;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.util.Mth;
+
+/**
+ * {@link DefaultedGeoModel} specific to {@link net.minecraft.world.entity.Entity Entities}.
+ * Using this class pre-sorts provided asset paths into the "entity" subdirectory
+ * Additionally it can automatically handle head-turning if the entity has a "head" bone
+ */
+public class DefaultedEntityGeoModel extends DefaultedGeoModel {
+ private final boolean turnsHead;
+
+ /**
+ * Create a new instance of this model class.
+ * The asset path should be the truncated relative path from the base folder.
+ * E.G.
+ *
{@code
+ * new ResourceLocation("myMod", "animals/red_fish")
+ * }
+ */
+ public DefaultedEntityGeoModel(ResourceLocation assetSubpath) {
+ this(assetSubpath, false);
+ }
+
+ public DefaultedEntityGeoModel(ResourceLocation assetSubpath, boolean turnsHead) {
+ super(assetSubpath);
+
+ this.turnsHead = turnsHead;
+ }
+
+ @Override
+ protected String subtype() {
+ return "entity";
+ }
+
+ @Override
+ public void setCustomAnimations(T animatable, long instanceId, AnimationState animationState) {
+ if (!this.turnsHead)
+ return;
+
+ CoreGeoBone head = getAnimationProcessor().getBone("head");
+
+ if (head != null) {
+ EntityModelData entityData = animationState.getData(DataTickets.ENTITY_MODEL_DATA);
+
+ head.setRotX(entityData.headPitch() * Mth.DEG_TO_RAD);
+ head.setRotY(entityData.netHeadYaw() * Mth.DEG_TO_RAD);
+ }
+ }
+
+ /**
+ * Changes the constructor-defined model path for this model to an alternate.
+ * This is useful if your animatable shares a model path with another animatable that differs in path to the texture and animations for this model
+ */
+ @Override
+ public DefaultedEntityGeoModel withAltModel(ResourceLocation altPath) {
+ return (DefaultedEntityGeoModel)super.withAltModel(altPath);
+ }
+
+ /**
+ * Changes the constructor-defined animations path for this model to an alternate.
+ * This is useful if your animatable shares an animations path with another animatable that differs in path to the model and texture for this model
+ */
+ @Override
+ public DefaultedEntityGeoModel withAltAnimations(ResourceLocation altPath) {
+ return (DefaultedEntityGeoModel)super.withAltAnimations(altPath);
+ }
+
+ /**
+ * Changes the constructor-defined texture path for this model to an alternate.
+ * This is useful if your animatable shares a texture path with another animatable that differs in path to the model and animations for this model
+ */
+ @Override
+ public DefaultedEntityGeoModel withAltTexture(ResourceLocation altPath) {
+ return (DefaultedEntityGeoModel)super.withAltTexture(altPath);
+ }
+}
diff --git a/common/src/main/java/mod/azure/azurelib/common/api/client/model/DefaultedGeoModel.java b/common/src/main/java/mod/azure/azurelib/common/api/client/model/DefaultedGeoModel.java
new file mode 100644
index 0000000..6009615
--- /dev/null
+++ b/common/src/main/java/mod/azure/azurelib/common/api/client/model/DefaultedGeoModel.java
@@ -0,0 +1,119 @@
+package mod.azure.azurelib.common.api.client.model;
+
+import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable;
+import net.minecraft.resources.ResourceLocation;
+
+/**
+ * Defaulted model class for AzureLib models.
+ * This class allows for minimal boilerplate when implementing basic models, and saves on new classes.
+ * Additionally, it encourages consistency and sorting of asset paths.
+ */
+public abstract class DefaultedGeoModel extends GeoModel {
+ private ResourceLocation modelPath;
+ private ResourceLocation texturePath;
+ private ResourceLocation animationsPath;
+
+ /**
+ * Create a new instance of this model class.
+ * The asset path should be the truncated relative path from the base folder.
+ * E.G.
+ *
+ * @param assetSubpath
+ */
+ protected DefaultedGeoModel(ResourceLocation assetSubpath) {
+ this.modelPath = buildFormattedModelPath(assetSubpath);
+ this.texturePath = buildFormattedTexturePath(assetSubpath);
+ this.animationsPath = buildFormattedAnimationPath(assetSubpath);
+ }
+
+ /**
+ * Changes the constructor-defined model path for this model to an alternate.
+ * This is useful if your animatable shares a model path with another animatable that differs in path to the texture and animations for this model
+ */
+ public DefaultedGeoModel withAltModel(ResourceLocation altPath) {
+ this.modelPath = buildFormattedModelPath(altPath);
+
+ return this;
+ }
+
+ /**
+ * Changes the constructor-defined animations path for this model to an alternate.
+ * This is useful if your animatable shares an animations path with another animatable that differs in path to the model and texture for this model
+ */
+ public DefaultedGeoModel withAltAnimations(ResourceLocation altPath) {
+ this.animationsPath = buildFormattedAnimationPath(altPath);
+
+ return this;
+ }
+
+ /**
+ * Changes the constructor-defined texture path for this model to an alternate.
+ * This is useful if your animatable shares a texture path with another animatable that differs in path to the model and animations for this model
+ */
+ public DefaultedGeoModel withAltTexture(ResourceLocation altPath) {
+ this.texturePath = buildFormattedTexturePath(altPath);
+
+ return this;
+ }
+
+ /**
+ * Constructs a defaulted resource path for a geo.json file based on the input namespace and subpath, automatically using the {@link DefaultedGeoModel#subtype() subtype}
+ * @param basePath The base path of your resource. E.G.
{@code new ResourceLocation(MyMod.MOD_ID, "animal/goat")}
+ * @return The formatted model resource path based on recommended defaults. E.G.
{@code "mymod:geo/entity/animal/goat.geo.json"}
+ */
+ public ResourceLocation buildFormattedModelPath(ResourceLocation basePath) {
+ return new ResourceLocation(basePath.getNamespace(), "geo/" + subtype() + "/" + basePath.getPath() + ".geo.json");
+ }
+
+ /**
+ * Constructs a defaulted resource path for a animation.json file based on the input namespace and subpath, automatically using the {@link DefaultedGeoModel#subtype() subtype}
+ * @param basePath The base path of your resource. E.G.
{@code new ResourceLocation(MyMod.MOD_ID, "animal/goat")}
+ * @return The formatted animation resource path based on recommended defaults. E.G.
+ */
+ public ResourceLocation buildFormattedAnimationPath(ResourceLocation basePath) {
+ return new ResourceLocation(basePath.getNamespace(), "animations/" + subtype() + "/" + basePath.getPath() + ".animation.json");
+ }
+
+ /**
+ * Constructs a defaulted resource path for a geo.json file based on the input namespace and subpath, automatically using the {@link DefaultedGeoModel#subtype() subtype}
+ * @param basePath The base path of your resource. E.G.
{@code new ResourceLocation(MyMod.MOD_ID, "animal/goat")}
+ * @return The formatted texture resource path based on recommended defaults. E.G.
{@code "mymod:textures/entity/animal/goat.png"}
+ */
+ public ResourceLocation buildFormattedTexturePath(ResourceLocation basePath) {
+ return new ResourceLocation(basePath.getNamespace(), "textures/" + subtype() + "/" + basePath.getPath() + ".png");
+ }
+
+ /**
+ * Returns the subtype string for this type of model.
+ * This allows for sorting of asset files into neat subdirectories for clean management.
+ * Examples:
+ *
+ *
"entity"
+ *
"block"
+ *
"item"
+ *
+ */
+ protected abstract String subtype();
+
+ @Override
+ public ResourceLocation getModelResource(T animatable) {
+ return this.modelPath;
+ }
+
+ @Override
+ public ResourceLocation getTextureResource(T animatable) {
+ return this.texturePath;
+ }
+
+ public ResourceLocation getTexture(T animatable) {
+ return this.texturePath;
+ }
+
+ @Override
+ public ResourceLocation getAnimationResource(T animatable) {
+ return this.animationsPath;
+ }
+}
diff --git a/common/src/main/java/mod/azure/azurelib/common/api/client/model/DefaultedItemGeoModel.java b/common/src/main/java/mod/azure/azurelib/common/api/client/model/DefaultedItemGeoModel.java
new file mode 100644
index 0000000..f060250
--- /dev/null
+++ b/common/src/main/java/mod/azure/azurelib/common/api/client/model/DefaultedItemGeoModel.java
@@ -0,0 +1,54 @@
+package mod.azure.azurelib.common.api.client.model;
+
+import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable;
+import net.minecraft.resources.ResourceLocation;
+
+/**
+ * {@link DefaultedGeoModel} specific to {@link net.minecraft.world.item.Item Items}.
+ * Using this class pre-sorts provided asset paths into the "item" subdirectory
+ */
+public class DefaultedItemGeoModel extends DefaultedGeoModel {
+ /**
+ * Create a new instance of this model class.
+ * The asset path should be the truncated relative path from the base folder.
+ * E.G.
+ *
{@code
+ * new ResourceLocation("myMod", "armor/obsidian")
+ * }
+ */
+ public DefaultedItemGeoModel(ResourceLocation assetSubpath) {
+ super(assetSubpath);
+ }
+
+ @Override
+ protected String subtype() {
+ return "item";
+ }
+
+ /**
+ * Changes the constructor-defined model path for this model to an alternate.
+ * This is useful if your animatable shares a model path with another animatable that differs in path to the texture and animations for this model
+ */
+ @Override
+ public DefaultedItemGeoModel withAltModel(ResourceLocation altPath) {
+ return (DefaultedItemGeoModel)super.withAltModel(altPath);
+ }
+
+ /**
+ * Changes the constructor-defined animations path for this model to an alternate.
+ * This is useful if your animatable shares an animations path with another animatable that differs in path to the model and texture for this model
+ */
+ @Override
+ public DefaultedItemGeoModel withAltAnimations(ResourceLocation altPath) {
+ return (DefaultedItemGeoModel)super.withAltAnimations(altPath);
+ }
+
+ /**
+ * Changes the constructor-defined texture path for this model to an alternate.
+ * This is useful if your animatable shares a texture path with another animatable that differs in path to the model and animations for this model
+ */
+ @Override
+ public DefaultedItemGeoModel withAltTexture(ResourceLocation altPath) {
+ return (DefaultedItemGeoModel)super.withAltTexture(altPath);
+ }
+}
diff --git a/common/src/main/java/mod/azure/azurelib/common/api/client/model/GeoModel.java b/common/src/main/java/mod/azure/azurelib/common/api/client/model/GeoModel.java
new file mode 100644
index 0000000..c4f62e5
--- /dev/null
+++ b/common/src/main/java/mod/azure/azurelib/common/api/client/model/GeoModel.java
@@ -0,0 +1,204 @@
+package mod.azure.azurelib.common.api.client.model;
+
+import java.util.Optional;
+import java.util.function.BiConsumer;
+
+import mod.azure.azurelib.common.internal.common.AzureLibException;
+import mod.azure.azurelib.common.internal.common.cache.AzureLibCache;
+import mod.azure.azurelib.common.internal.common.cache.object.BakedGeoModel;
+import mod.azure.azurelib.common.internal.common.cache.object.GeoBone;
+import mod.azure.azurelib.common.internal.common.constant.DataTickets;
+import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable;
+import mod.azure.azurelib.common.internal.common.core.animatable.model.CoreGeoModel;
+import mod.azure.azurelib.common.internal.common.core.animation.AnimatableManager;
+import mod.azure.azurelib.common.internal.common.core.animation.Animation;
+import mod.azure.azurelib.common.internal.common.core.animation.AnimationProcessor;
+import mod.azure.azurelib.common.internal.common.core.animation.AnimationState;
+import mod.azure.azurelib.common.internal.common.core.molang.MolangParser;
+import mod.azure.azurelib.common.internal.common.core.molang.MolangQueries;
+import mod.azure.azurelib.common.internal.common.core.object.DataTicket;
+import mod.azure.azurelib.common.internal.common.loading.object.BakedAnimations;
+import mod.azure.azurelib.common.internal.client.renderer.GeoRenderer;
+import mod.azure.azurelib.common.internal.client.util.RenderUtils;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.RenderType;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.util.Mth;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.phys.Vec3;
+
+/**
+ * Base class for all code-based model objects.
+ * All models to registered to a {@link GeoRenderer} should be an instance of this or one of its subclasses.
+ */
+public abstract class GeoModel implements CoreGeoModel {
+ private final AnimationProcessor processor = new AnimationProcessor<>(this);
+
+ private BakedGeoModel currentModel = null;
+ private double animTime;
+ private double lastGameTickTime;
+ private long lastRenderedInstance = -1;
+
+ /**
+ * Returns the resource path for the {@link BakedGeoModel} (model json file) to render based on the provided animatable
+ */
+ public abstract ResourceLocation getModelResource(T animatable);
+
+ /**
+ * Returns the resource path for the texture file to render based on the provided animatable
+ */
+ public abstract ResourceLocation getTextureResource(T animatable);
+
+ /**
+ * Returns the resourcepath for the {@link BakedAnimations} (animation json file) to use for animations based on the provided animatable
+ */
+ public abstract ResourceLocation getAnimationResource(T animatable);
+
+ /**
+ * Override this and return true if AzureLib should crash when attempting to animate the model, but fails to find a bone.
+ * By default, AzureLib will just gracefully ignore a missing bone, which might cause oddities with incorrect models or mismatching variables.
+ */
+ public boolean crashIfBoneMissing() {
+ return false;
+ }
+
+ /**
+ * Gets the default render type for this animatable, to be selected by default by the renderer using it
+ */
+ public RenderType getRenderType(T animatable, ResourceLocation texture) {
+ return RenderType.entityCutoutNoCull(texture);
+ }
+
+ @Override
+ public final BakedGeoModel getBakedGeoModel(String location) {
+ return getBakedModel(new ResourceLocation(location));
+ }
+
+ /**
+ * Get the baked geo model object used for rendering from the given resource path
+ */
+ public BakedGeoModel getBakedModel(ResourceLocation location) {
+ BakedGeoModel model = AzureLibCache.getBakedModels().get(location);
+
+ if (model == null)
+ throw new AzureLibException(location, "Unable to find model");
+
+ if (model != this.currentModel) {
+ this.processor.setActiveModel(model);
+ this.currentModel = model;
+ }
+
+ return this.currentModel;
+ }
+
+ /**
+ * Gets a bone from this model by name
+ *
+ * @param name The name of the bone
+ * @return An {@link Optional} containing the {@link GeoBone} if one matches, otherwise an empty Optional
+ */
+ public Optional getBone(String name) {
+ return Optional.ofNullable((GeoBone) getAnimationProcessor().getBone(name));
+ }
+
+ /**
+ * Get the baked animation object used for rendering from the given resource path
+ */
+ @Override
+ public Animation getAnimation(T animatable, String name) {
+ ResourceLocation location = getAnimationResource(animatable);
+ BakedAnimations bakedAnimations = AzureLibCache.getBakedAnimations().get(location);
+
+ if (bakedAnimations == null)
+ throw new AzureLibException(location, "Unable to find animation.");
+
+ return bakedAnimations.getAnimation(name);
+ }
+
+ @Override
+ public AnimationProcessor getAnimationProcessor() {
+ return this.processor;
+ }
+
+ /**
+ * Add additional {@link DataTicket DataTickets} to the {@link AnimationState} to be handled by your animation handler at render time
+ *
+ * @param animatable The animatable instance currently being animated
+ * @param instanceId The unique instance id of the animatable being animated
+ * @param dataConsumer The DataTicket + data consumer to be added to the AnimationEvent
+ */
+ public void addAdditionalStateData(T animatable, long instanceId, BiConsumer, T> dataConsumer) {
+ }
+
+ @Override
+ public void handleAnimations(T animatable, long instanceId, AnimationState animationState) {
+ Minecraft mc = Minecraft.getInstance();
+ AnimatableManager animatableManager = animatable.getAnimatableInstanceCache().getManagerForId(instanceId);
+ Double currentTick = animationState.getData(DataTickets.TICK);
+
+ if (currentTick == null)
+ currentTick = animatable instanceof LivingEntity livingEntity ? (double) livingEntity.tickCount : RenderUtils.getCurrentTick();
+
+ if (animatableManager.getFirstTickTime() == -1)
+ animatableManager.startedAt(currentTick + mc.getFrameTime());
+
+ double currentFrameTime = currentTick - animatableManager.getFirstTickTime();
+ boolean isReRender = !animatableManager.isFirstTick() && currentFrameTime == animatableManager.getLastUpdateTime();
+
+ if (isReRender && instanceId == this.lastRenderedInstance)
+ return;
+
+ if (!isReRender && (!mc.isPaused() || animatable.shouldPlayAnimsWhileGamePaused())) {
+ if (animatable instanceof LivingEntity) {
+ animatableManager.updatedAt(currentFrameTime);
+ } else {
+ animatableManager.updatedAt(currentFrameTime);
+ }
+
+ double lastUpdateTime = animatableManager.getLastUpdateTime();
+ this.animTime += lastUpdateTime - this.lastGameTickTime;
+ this.lastGameTickTime = lastUpdateTime;
+ }
+
+ animationState.animationTick = this.animTime;
+ AnimationProcessor processor = getAnimationProcessor();
+
+ processor.preAnimationSetup(animationState.getAnimatable(), this.animTime);
+
+ if (!processor.getRegisteredBones().isEmpty())
+ processor.tickAnimation(animatable, this, animatableManager, this.animTime, animationState, crashIfBoneMissing());
+
+ setCustomAnimations(animatable, instanceId, animationState);
+ }
+
+ @Override
+ public void applyMolangQueries(T animatable, double animTime) {
+ MolangParser parser = MolangParser.INSTANCE;
+ Minecraft mc = Minecraft.getInstance();
+
+ parser.setMemoizedValue(MolangQueries.LIFE_TIME, () -> animTime / 20d);
+ parser.setMemoizedValue(MolangQueries.ACTOR_COUNT, mc.level::getEntityCount);
+ parser.setMemoizedValue(MolangQueries.TIME_OF_DAY, () -> mc.level.getDayTime() / 24000f);
+ parser.setMemoizedValue(MolangQueries.MOON_PHASE, mc.level::getMoonPhase);
+
+ if (animatable instanceof Entity entity) {
+ parser.setMemoizedValue(MolangQueries.DISTANCE_FROM_CAMERA, () -> mc.gameRenderer.getMainCamera().getPosition().distanceTo(entity.position()));
+ parser.setMemoizedValue(MolangQueries.IS_ON_GROUND, () -> RenderUtils.booleanToFloat(entity.onGround()));
+ parser.setMemoizedValue(MolangQueries.IS_IN_WATER, () -> RenderUtils.booleanToFloat(entity.isInWater()));
+ parser.setMemoizedValue(MolangQueries.IS_IN_WATER_OR_RAIN, () -> RenderUtils.booleanToFloat(entity.isInWaterRainOrBubble()));
+
+ if (entity instanceof LivingEntity livingEntity) {
+ parser.setMemoizedValue(MolangQueries.HEALTH, livingEntity::getHealth);
+ parser.setMemoizedValue(MolangQueries.MAX_HEALTH, livingEntity::getMaxHealth);
+ parser.setMemoizedValue(MolangQueries.IS_ON_FIRE, () -> RenderUtils.booleanToFloat(livingEntity.isOnFire()));
+ parser.setMemoizedValue(MolangQueries.GROUND_SPEED, () -> {
+ Vec3 velocity = livingEntity.getDeltaMovement();
+
+ return Mth.sqrt((float) ((velocity.x * velocity.x) + (velocity.z * velocity.z)));
+ });
+ parser.setMemoizedValue(MolangQueries.YAW_SPEED, () -> livingEntity.getViewYRot((float) animTime - livingEntity.getViewYRot((float) animTime - 0.1f)));
+ }
+ }
+ }
+}
diff --git a/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/DyeableGeoArmorRenderer.java b/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/DyeableGeoArmorRenderer.java
new file mode 100644
index 0000000..304eade
--- /dev/null
+++ b/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/DyeableGeoArmorRenderer.java
@@ -0,0 +1,89 @@
+package mod.azure.azurelib.common.api.client.renderer;
+
+import com.mojang.blaze3d.vertex.PoseStack;
+import com.mojang.blaze3d.vertex.VertexConsumer;
+import it.unimi.dsi.fastutil.objects.ObjectArraySet;
+import mod.azure.azurelib.common.api.client.model.GeoModel;
+import mod.azure.azurelib.common.api.common.animatable.GeoItem;
+import mod.azure.azurelib.common.internal.common.cache.object.BakedGeoModel;
+import mod.azure.azurelib.common.internal.common.cache.object.GeoBone;
+import mod.azure.azurelib.common.internal.common.core.object.Color;
+import net.minecraft.client.renderer.MultiBufferSource;
+import net.minecraft.world.item.Item;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * A dyeable armour renderer for AzureLib armor models.
+ */
+public abstract class DyeableGeoArmorRenderer extends GeoArmorRenderer {
+ protected final Set dyeableBones = new ObjectArraySet<>();
+
+ protected DyeableGeoArmorRenderer(GeoModel model) {
+ super(model);
+ }
+
+ @Override
+ public void preRender(PoseStack poseStack, T animatable, BakedGeoModel model, @Nullable MultiBufferSource bufferSource, @Nullable VertexConsumer buffer, boolean isReRender, float partialTick, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) {
+ super.preRender(poseStack, animatable, model, bufferSource, buffer, isReRender, partialTick, packedLight, packedOverlay, red, green, blue, alpha);
+
+ if (!isReRender)
+ checkBoneDyeCache(model);
+ }
+
+ @Override
+ public void renderCubesOfBone(PoseStack poseStack, GeoBone bone, VertexConsumer buffer, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) {
+ if (this.dyeableBones.contains(bone)) {
+ final var color = getColorForBone(bone);
+
+ red *= color.getRedFloat();
+ green *= color.getGreenFloat();
+ blue *= color.getBlueFloat();
+ alpha *= color.getAlphaFloat();
+ }
+
+ super.renderCubesOfBone(poseStack, bone, buffer, packedLight, packedOverlay, red, green, blue, alpha);
+ }
+
+ /**
+ * Whether the given GeoBone should be considered dyeable or not.
+ *
Note that values returned from here are cached for the last rendered {@link BakedGeoModel} and require a manual reset if you intend to change these results.
+ *
+ * @return whether the bone should be dyed or not
+ */
+ protected abstract boolean isBoneDyeable(GeoBone bone);
+
+ /**
+ * What color the given GeoBone should be dyed as.
+ *
Only bones that were marked as 'dyeable' in {@link DyeableGeoArmorRenderer#isBoneDyeable(GeoBone)} are provided here
+ */
+ @NotNull
+ protected abstract Color getColorForBone(GeoBone bone);
+
+ /**
+ * Check whether the dye cache should be considered dirty and recomputed.
+ *
The less this forces re-computation, the better for performance
+ */
+ protected void checkBoneDyeCache(BakedGeoModel model) {
+ if (model != this.lastModel) {
+ this.dyeableBones.clear();
+ this.lastModel = model;
+ collectDyeableBones(model.topLevelBones());
+ }
+ }
+
+ /**
+ * Recursively parse through the given bones collection, collecting and caching dyeable bones as applicable
+ */
+ protected void collectDyeableBones(Collection bones) {
+ for (var bone : bones) {
+ if (isBoneDyeable(bone))
+ this.dyeableBones.add(bone);
+
+ collectDyeableBones(bone.getChildBones());
+ }
+ }
+}
diff --git a/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/DynamicGeoEntityRenderer.java b/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/DynamicGeoEntityRenderer.java
new file mode 100644
index 0000000..c2da92b
--- /dev/null
+++ b/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/DynamicGeoEntityRenderer.java
@@ -0,0 +1,175 @@
+package mod.azure.azurelib.common.api.client.renderer;
+
+import java.util.Map;
+
+import mod.azure.azurelib.common.api.client.model.GeoModel;
+import org.jetbrains.annotations.Nullable;
+import org.joml.Matrix4f;
+import org.joml.Vector3f;
+import org.joml.Vector4f;
+
+import com.mojang.blaze3d.vertex.PoseStack;
+import com.mojang.blaze3d.vertex.VertexConsumer;
+
+import it.unimi.dsi.fastutil.ints.IntIntPair;
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import mod.azure.azurelib.common.internal.common.cache.object.BakedGeoModel;
+import mod.azure.azurelib.common.internal.common.cache.object.GeoBone;
+import mod.azure.azurelib.common.internal.common.cache.object.GeoCube;
+import mod.azure.azurelib.common.internal.common.cache.object.GeoQuad;
+import mod.azure.azurelib.common.internal.common.cache.object.GeoVertex;
+import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable;
+import mod.azure.azurelib.common.internal.client.util.RenderUtils;
+import net.minecraft.client.renderer.MultiBufferSource;
+import net.minecraft.client.renderer.RenderType;
+import net.minecraft.client.renderer.entity.EntityRendererProvider;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.entity.Entity;
+
+/**
+ * Extended special-entity renderer for more advanced or dynamic models.
+ * Because of the extra performance cost of this renderer, it is advised to avoid using it unnecessarily,
+ * and consider whether the benefits are worth the cost for your needs.
+ */
+public abstract class DynamicGeoEntityRenderer extends GeoEntityRenderer {
+ protected static final Map TEXTURE_DIMENSIONS_CACHE = new Object2ObjectOpenHashMap<>();
+
+ protected ResourceLocation textureOverride = null;
+
+ protected DynamicGeoEntityRenderer(EntityRendererProvider.Context renderManager, GeoModel model) {
+ super(renderManager, model);
+ }
+
+ /**
+ * For each bone rendered, this method is called.
+ * If a ResourceLocation is returned, the renderer will render the bone using that texture instead of the default.
+ * This can be useful for custom rendering on a per-bone basis.
+ * There is a somewhat significant performance cost involved in this however, so only use as needed.
+ * @return The specified ResourceLocation, or null if no override
+ */
+ @Nullable
+ protected ResourceLocation getTextureOverrideForBone(GeoBone bone, T animatable, float partialTick) {
+ return null;
+ }
+
+ /**
+ * For each bone rendered, this method is called.
+ * If a RenderType is returned, the renderer will render the bone using that RenderType instead of the default.
+ * This can be useful for custom rendering operations on a per-bone basis.
+ * There is a somewhat significant performance cost involved in this however, so only use as needed.
+ * @return The specified RenderType, or null if no override
+ */
+ @Nullable
+ protected RenderType getRenderTypeOverrideForBone(GeoBone bone, T animatable, ResourceLocation texturePath, MultiBufferSource bufferSource, float partialTick) {
+ return null;
+ }
+
+ /**
+ * Override this to handle a given {@link GeoBone GeoBone's} rendering in a particular way
+ * @return Whether the renderer should skip rendering the {@link GeoCube cubes} of the given GeoBone or not
+ */
+ protected boolean boneRenderOverride(PoseStack poseStack, GeoBone bone, MultiBufferSource bufferSource, VertexConsumer buffer,
+ float partialTick, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) {
+ return false;
+ }
+
+ /**
+ * Renders the provided {@link GeoBone} and its associated child bones
+ */
+ @Override
+ public void renderRecursively(PoseStack poseStack, T animatable, GeoBone bone, RenderType renderType, MultiBufferSource bufferSource, VertexConsumer buffer, boolean isReRender, float partialTick, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) {
+ poseStack.pushPose();
+ RenderUtils.translateMatrixToBone(poseStack, bone);
+ RenderUtils.translateToPivotPoint(poseStack, bone);
+ RenderUtils.rotateMatrixAroundBone(poseStack, bone);
+ RenderUtils.scaleMatrixForBone(poseStack, bone);
+
+ if (bone.isTrackingMatrices()) {
+ Matrix4f poseState = new Matrix4f(poseStack.last().pose());
+ Matrix4f localMatrix = RenderUtils.invertAndMultiplyMatrices(poseState, this.entityRenderTranslations);
+
+ bone.setModelSpaceMatrix(RenderUtils.invertAndMultiplyMatrices(poseState, this.modelRenderTranslations));
+ bone.setLocalSpaceMatrix(RenderUtils.translateMatrix(localMatrix, getRenderOffset(this.animatable, 1).toVector3f()));
+ bone.setWorldSpaceMatrix(RenderUtils.translateMatrix(new Matrix4f(localMatrix), this.animatable.position().toVector3f()));
+ }
+
+ RenderUtils.translateAwayFromPivotPoint(poseStack, bone);
+
+ this.textureOverride = getTextureOverrideForBone(bone, this.animatable, partialTick);
+ ResourceLocation texture = this.textureOverride == null ? getTextureLocation(this.animatable) : this.textureOverride;
+ RenderType renderTypeOverride = getRenderTypeOverrideForBone(bone, this.animatable, texture, bufferSource, partialTick);
+
+ if (texture != null && renderTypeOverride == null)
+ renderTypeOverride = getRenderType(this.animatable, texture, bufferSource, partialTick);
+
+ if (renderTypeOverride != null)
+ buffer = bufferSource.getBuffer(renderTypeOverride);
+
+ if (!boneRenderOverride(poseStack, bone, bufferSource, buffer, partialTick, packedLight, packedOverlay, red, green, blue, alpha))
+ super.renderCubesOfBone(poseStack, bone, buffer, packedLight, packedOverlay, red, green, blue, alpha);
+
+ if (renderTypeOverride != null)
+ buffer = bufferSource.getBuffer(getRenderType(this.animatable, getTextureLocation(this.animatable), bufferSource, partialTick));
+
+ if (!isReRender)
+ applyRenderLayersForBone(poseStack, animatable, bone, renderType, bufferSource, buffer, partialTick, packedLight, packedOverlay);
+
+ super.renderChildBones(poseStack, animatable, bone, renderType, bufferSource, buffer, isReRender, partialTick, packedLight, packedOverlay, red, green, blue, alpha);
+
+ poseStack.popPose();
+ }
+
+ /**
+ * Called after rendering the model to buffer. Post-render modifications should be performed here.
+ * {@link PoseStack} transformations will be unused and lost once this method ends
+ */
+ @Override
+ public void postRender(PoseStack poseStack, T animatable, BakedGeoModel model, MultiBufferSource bufferSource, VertexConsumer buffer, boolean isReRender, float partialTick, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) {
+ this.textureOverride = null;
+
+ super.postRender(poseStack, animatable, model, bufferSource, buffer, isReRender, partialTick, packedLight, packedOverlay, red, green, blue, alpha);
+ }
+
+ /**
+ * Applies the {@link GeoQuad Quad's} {@link GeoVertex vertices} to the given {@link VertexConsumer buffer} for rendering.
+ * Custom override to handle custom non-baked textures for ExtendedGeoEntityRenderer
+ */
+ @Override
+ public void createVerticesOfQuad(GeoQuad quad, Matrix4f poseState, Vector3f normal, VertexConsumer buffer,
+ int packedLight, int packedOverlay, float red, float green, float blue, float alpha) {
+ if (this.textureOverride == null) {
+ super.createVerticesOfQuad(quad, poseState, normal, buffer, packedLight, packedOverlay, red, green,
+ blue, alpha);
+
+ return;
+ }
+
+ IntIntPair boneTextureSize = computeTextureSize(this.textureOverride);
+ IntIntPair entityTextureSize = computeTextureSize(getTextureLocation(this.animatable));
+
+ if (boneTextureSize == null || entityTextureSize == null) {
+ super.createVerticesOfQuad(quad, poseState, normal, buffer, packedLight, packedOverlay, red, green,
+ blue, alpha);
+
+ return;
+ }
+
+ for (GeoVertex vertex : quad.vertices()) {
+ Vector4f vector4f = poseState.transform(new Vector4f(vertex.position().x(), vertex.position().y(), vertex.position().z(), 1.0f));
+ float texU = (vertex.texU() * entityTextureSize.firstInt()) / boneTextureSize.firstInt();
+ float texV = (vertex.texV() * entityTextureSize.secondInt()) / boneTextureSize.secondInt();
+
+ buffer.vertex(vector4f.x(), vector4f.y(), vector4f.z(), red, green, blue, alpha, texU, texV,
+ packedOverlay, packedLight, normal.x(), normal.y(), normal.z());
+ }
+ }
+
+ /**
+ * Retrieve or compute the height and width of a given texture from its {@link ResourceLocation}.
+ * This is used for dynamically mapping vertices on a given quad.
+ * This is inefficient however, and should only be used where required.
+ */
+ protected IntIntPair computeTextureSize(ResourceLocation texture) {
+ return TEXTURE_DIMENSIONS_CACHE.computeIfAbsent(texture, RenderUtils::getTextureDimensions);
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/GeoArmorRenderer.java b/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/GeoArmorRenderer.java
new file mode 100644
index 0000000..bcdd4ab
--- /dev/null
+++ b/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/GeoArmorRenderer.java
@@ -0,0 +1,589 @@
+package mod.azure.azurelib.common.api.client.renderer;
+
+import java.util.List;
+
+import mod.azure.azurelib.common.api.common.event.GeoRenderArmorEvent;
+import mod.azure.azurelib.common.api.client.model.GeoModel;
+import mod.azure.azurelib.common.api.common.animatable.GeoItem;
+import mod.azure.azurelib.common.internal.client.renderer.GeoRenderer;
+import mod.azure.azurelib.common.platform.Services;
+import net.minecraft.client.renderer.RenderBuffers;
+import org.Vrglab.AzureLib.Utility.Utils;
+import org.jetbrains.annotations.Nullable;
+import org.joml.Matrix4f;
+
+import com.mojang.blaze3d.vertex.PoseStack;
+import com.mojang.blaze3d.vertex.VertexConsumer;
+
+import mod.azure.azurelib.common.internal.common.cache.object.BakedGeoModel;
+import mod.azure.azurelib.common.internal.common.cache.object.GeoBone;
+import mod.azure.azurelib.common.internal.common.cache.texture.AnimatableTexture;
+import mod.azure.azurelib.common.internal.common.constant.DataTickets;
+import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable;
+import mod.azure.azurelib.common.internal.common.core.animation.AnimationState;
+import mod.azure.azurelib.common.api.client.renderer.layer.GeoRenderLayer;
+import mod.azure.azurelib.common.api.client.renderer.layer.GeoRenderLayersContainer;
+import mod.azure.azurelib.common.internal.client.util.RenderUtils;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.model.HumanoidModel;
+import net.minecraft.client.model.geom.ModelLayers;
+import net.minecraft.client.model.geom.ModelPart;
+import net.minecraft.client.renderer.MultiBufferSource;
+import net.minecraft.client.renderer.RenderType;
+import net.minecraft.client.renderer.entity.ItemRenderer;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.EquipmentSlot;
+import net.minecraft.world.item.Item;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.phys.Vec3;
+
+/**
+ * Base {@link GeoRenderer} for rendering in-world armor specifically.
+ * All custom armor added to be rendered in-world by AzureLib should use an instance of this class.
+ *
+ * @see GeoItem
+ * @param
+ */
+public class GeoArmorRenderer extends HumanoidModel implements GeoRenderer {
+ protected final GeoRenderLayersContainer renderLayers = new GeoRenderLayersContainer<>(this);
+ protected final GeoModel model;
+
+ protected T animatable;
+ protected HumanoidModel> baseModel;
+ protected float scaleWidth = 1;
+ protected float scaleHeight = 1;
+
+ protected Matrix4f entityRenderTranslations = new Matrix4f();
+ protected Matrix4f modelRenderTranslations = new Matrix4f();
+
+ public BakedGeoModel lastModel = null;
+ protected GeoBone head = null;
+ protected GeoBone body = null;
+ protected GeoBone rightArm = null;
+ protected GeoBone leftArm = null;
+ protected GeoBone rightLeg = null;
+ protected GeoBone leftLeg = null;
+ protected GeoBone rightBoot = null;
+ protected GeoBone leftBoot = null;
+
+ protected Entity currentEntity = null;
+ protected ItemStack currentStack = null;
+ protected EquipmentSlot currentSlot = null;
+
+ public GeoArmorRenderer(GeoModel model) {
+ super(Minecraft.getInstance().getEntityModels().bakeLayer(ModelLayers.PLAYER_INNER_ARMOR));
+
+ this.model = model;
+ this.young = false;
+ }
+
+ /**
+ * Gets the model instance for this renderer
+ */
+ @Override
+ public GeoModel getGeoModel() {
+ return this.model;
+ }
+
+ /**
+ * Gets the {@link GeoItem} instance currently being rendered
+ */
+ public T getAnimatable() {
+ return this.animatable;
+ }
+
+ /**
+ * Returns the entity currently being rendered with armour equipped
+ */
+ public Entity getCurrentEntity() {
+ return this.currentEntity;
+ }
+
+ /**
+ * Returns the ItemStack pertaining to the current piece of armor being rendered
+ */
+ public ItemStack getCurrentStack() {
+ return this.currentStack;
+ }
+
+ /**
+ * Returns the equipped slot of the armor piece being rendered
+ */
+ public EquipmentSlot getCurrentSlot() {
+ return this.currentSlot;
+ }
+
+ /**
+ * Gets the id that represents the current animatable's instance for animation purposes. This is mostly useful for things like items, which have a single registered instance for all objects
+ */
+ @Override
+ public long getInstanceId(T animatable) {
+ return GeoItem.getId(this.currentStack) + this.currentEntity.getId();
+ }
+
+ /**
+ * Gets the {@link RenderType} to render the given animatable with.
+ * Uses the {@link RenderType#armorCutoutNoCull} {@code RenderType} by default.
+ * Override this to change the way a model will render (such as translucent models, etc)
+ */
+ @Override
+ public RenderType getRenderType(T animatable, ResourceLocation texture, @org.jetbrains.annotations.Nullable MultiBufferSource bufferSource, float partialTick) {
+ return RenderType.armorCutoutNoCull(texture);
+ }
+
+ /**
+ * Returns the list of registered {@link GeoRenderLayer GeoRenderLayers} for this renderer
+ */
+ @Override
+ public List> getRenderLayers() {
+ return this.renderLayers.getRenderLayers();
+ }
+
+ /**
+ * Adds a {@link GeoRenderLayer} to this renderer, to be called after the main model is rendered each frame
+ */
+ public GeoArmorRenderer addRenderLayer(GeoRenderLayer renderLayer) {
+ this.renderLayers.addLayer(renderLayer);
+
+ return this;
+ }
+
+ /**
+ * Sets a scale override for this renderer, telling AzureLib to pre-scale the model
+ */
+ public GeoArmorRenderer withScale(float scale) {
+ return withScale(scale, scale);
+ }
+
+ /**
+ * Sets a scale override for this renderer, telling AzureLib to pre-scale the model
+ */
+ public GeoArmorRenderer withScale(float scaleWidth, float scaleHeight) {
+ this.scaleWidth = scaleWidth;
+ this.scaleHeight = scaleHeight;
+
+ return this;
+ }
+
+ /**
+ * Returns the 'head' GeoBone from this model.
+ * Override if your geo model has different bone names for these bones
+ *
+ * @return The bone for the head model piece, or null if not using it
+ */
+ @Nullable
+ public GeoBone getHeadBone() {
+ return this.model.getBone("armorHead").orElse(null);
+ }
+
+ /**
+ * Returns the 'body' GeoBone from this model.
+ * Override if your geo model has different bone names for these bones
+ *
+ * @return The bone for the body model piece, or null if not using it
+ */
+ @Nullable
+ public GeoBone getBodyBone() {
+ return this.model.getBone("armorBody").orElse(null);
+ }
+
+ /**
+ * Returns the 'right arm' GeoBone from this model.
+ * Override if your geo model has different bone names for these bones
+ *
+ * @return The bone for the right arm model piece, or null if not using it
+ */
+ @Nullable
+ public GeoBone getRightArmBone() {
+ return this.model.getBone("armorRightArm").orElse(null);
+ }
+
+ /**
+ * Returns the 'left arm' GeoBone from this model.
+ * Override if your geo model has different bone names for these bones
+ *
+ * @return The bone for the left arm model piece, or null if not using it
+ */
+ @Nullable
+ public GeoBone getLeftArmBone() {
+ return this.model.getBone("armorLeftArm").orElse(null);
+ }
+
+ /**
+ * Returns the 'right leg' GeoBone from this model.
+ * Override if your geo model has different bone names for these bones
+ *
+ * @return The bone for the right leg model piece, or null if not using it
+ */
+ @Nullable
+ public GeoBone getRightLegBone() {
+ return this.model.getBone("armorRightLeg").orElse(null);
+ }
+
+ /**
+ * Returns the 'left leg' GeoBone from this model.
+ * Override if your geo model has different bone names for these bones
+ *
+ * @return The bone for the left leg model piece, or null if not using it
+ */
+ @Nullable
+ public GeoBone getLeftLegBone() {
+ return this.model.getBone("armorLeftLeg").orElse(null);
+ }
+
+ /**
+ * Returns the 'right boot' GeoBone from this model.
+ * Override if your geo model has different bone names for these bones
+ *
+ * @return The bone for the right boot model piece, or null if not using it
+ */
+ @Nullable
+ public GeoBone getRightBootBone() {
+ return this.model.getBone("armorRightBoot").orElse(null);
+ }
+
+ /**
+ * Returns the 'left boot' GeoBone from this model.
+ * Override if your geo model has different bone names for these bones
+ *
+ * @return The bone for the left boot model piece, or null if not using it
+ */
+ @Nullable
+ public GeoBone getLeftBootBone() {
+ return this.model.getBone("armorLeftBoot").orElse(null);
+ }
+
+ /**
+ * Called before rendering the model to buffer. Allows for render modifications and preparatory work such as scaling and translating.
+ * {@link PoseStack} translations made here are kept until the end of the render process
+ */
+ @Override
+ public void preRender(PoseStack poseStack, T animatable, BakedGeoModel model, @Nullable MultiBufferSource bufferSource, @Nullable VertexConsumer buffer, boolean isReRender, float partialTick, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) {
+ this.entityRenderTranslations = new Matrix4f(poseStack.last().pose());
+
+ applyBaseModel(this.baseModel);
+ grabRelevantBones(getGeoModel().getBakedModel(getGeoModel().getModelResource(this.animatable)));
+ applyBaseTransformations(this.baseModel);
+ scaleModelForBaby(poseStack, animatable, partialTick, isReRender);
+ scaleModelForRender(this.scaleWidth, this.scaleHeight, poseStack, animatable, model, isReRender, partialTick, packedLight, packedOverlay);
+
+ if (!(this.currentEntity instanceof GeoAnimatable))
+ applyBoneVisibilityBySlot(this.currentSlot);
+ }
+
+ @Override
+ public void renderToBuffer(PoseStack poseStack, VertexConsumer buffer, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) {
+ Minecraft mc = Minecraft.getInstance();
+ RenderBuffers buff = Utils.getPrivateFinalStaticField(Minecraft.getInstance().levelRenderer, Minecraft.getInstance().levelRenderer.getClass(), "renderBuffers");
+ MultiBufferSource bufferSource = buff.bufferSource();
+
+
+
+ if (((boolean)Utils.callPrivateMethod(Minecraft.getInstance().levelRenderer, "shouldShowEntityOutlines", new Class[0])) && mc.shouldEntityAppearGlowing(this.currentEntity))
+ bufferSource = buff.outlineBufferSource();
+
+ float partialTick = mc.getFrameTime();
+ RenderType renderType = getRenderType(this.animatable, getTextureLocation(this.animatable), bufferSource, partialTick);
+ buffer = ItemRenderer.getArmorFoilBuffer(bufferSource, renderType, false, this.currentStack.hasFoil());
+
+ defaultRender(poseStack, this.animatable, bufferSource, null, buffer, 0, partialTick, packedLight);
+ }
+
+ /**
+ * The actual render method that subtype renderers should override to handle their specific rendering tasks.
+ * {@link GeoRenderer#preRender} has already been called by this stage, and {@link GeoRenderer#postRender} will be called directly after
+ */
+ @Override
+ public void actuallyRender(PoseStack poseStack, T animatable, BakedGeoModel model, RenderType renderType, MultiBufferSource bufferSource, VertexConsumer buffer, boolean isReRender, float partialTick, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) {
+ poseStack.pushPose();
+ poseStack.translate(0, 24 / 16f, 0);
+ poseStack.scale(-1, -1, 1);
+
+ if (!isReRender) {
+ AnimationState animationState = new AnimationState<>(animatable, 0, 0, partialTick, false);
+ long instanceId = getInstanceId(animatable);
+
+ animationState.setData(DataTickets.TICK, animatable.getTick(this.currentEntity));
+ animationState.setData(DataTickets.ITEMSTACK, this.currentStack);
+ animationState.setData(DataTickets.ENTITY, this.currentEntity);
+ animationState.setData(DataTickets.EQUIPMENT_SLOT, this.currentSlot);
+ this.model.addAdditionalStateData(animatable, instanceId, animationState::setData);
+ this.model.handleAnimations(animatable, instanceId, animationState);
+ }
+
+ this.modelRenderTranslations = new Matrix4f(poseStack.last().pose());
+
+ GeoRenderer.super.actuallyRender(poseStack, animatable, model, renderType, bufferSource, buffer, isReRender, partialTick, packedLight, packedOverlay, red, green, blue, alpha);
+ poseStack.popPose();
+ }
+
+ /**
+ * Renders the provided {@link GeoBone} and its associated child bones
+ */
+ @Override
+ public void renderRecursively(PoseStack poseStack, T animatable, GeoBone bone, RenderType renderType, MultiBufferSource bufferSource, VertexConsumer buffer, boolean isReRender, float partialTick, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) {
+ if (bone.isTrackingMatrices()) {
+ Matrix4f poseState = new Matrix4f(poseStack.last().pose());
+ Matrix4f localMatrix = RenderUtils.invertAndMultiplyMatrices(poseState, this.entityRenderTranslations);
+
+ bone.setModelSpaceMatrix(RenderUtils.invertAndMultiplyMatrices(poseState, this.modelRenderTranslations));
+ bone.setLocalSpaceMatrix(RenderUtils.translateMatrix(localMatrix, getRenderOffset(this.currentEntity, 1).toVector3f()));
+ bone.setWorldSpaceMatrix(RenderUtils.translateMatrix(new Matrix4f(localMatrix), this.currentEntity.position().toVector3f()));
+ }
+
+ GeoRenderer.super.renderRecursively(poseStack, animatable, bone, renderType, bufferSource, buffer, isReRender, partialTick, packedLight, packedOverlay, red, green, blue, alpha);
+ }
+
+ public Vec3 getRenderOffset(Entity entity, float f) {
+ return Vec3.ZERO;
+ }
+
+ /**
+ * Gets and caches the relevant armor model bones for this baked model if it hasn't been done already
+ */
+ protected void grabRelevantBones(BakedGeoModel bakedModel) {
+ if (this.lastModel == bakedModel)
+ return;
+
+ this.lastModel = bakedModel;
+ this.head = getHeadBone();
+ this.body = getBodyBone();
+ this.rightArm = getRightArmBone();
+ this.leftArm = getLeftArmBone();
+ this.rightLeg = getRightLegBone();
+ this.leftLeg = getLeftLegBone();
+ this.rightBoot = getRightBootBone();
+ this.leftBoot = getLeftBootBone();
+ }
+
+ /**
+ * Prepare the renderer for the current render cycle.
+ * Must be called prior to render as the default HumanoidModel doesn't give render context.
+ * Params have been left nullable so that the renderer can be called for model/texture purposes safely. If you do grab the renderer using null parameters, you should not use it for actual rendering.
+ *
+ * @param entity The entity being rendered with the armor on
+ * @param stack The ItemStack being rendered
+ * @param slot The slot being rendered
+ * @param baseModel The default (vanilla) model that would have been rendered if this model hadn't replaced it
+ */
+ public void prepForRender(@Nullable Entity entity, ItemStack stack, @Nullable EquipmentSlot slot, @Nullable HumanoidModel> baseModel) {
+ if (entity == null || slot == null || baseModel == null)
+ return;
+
+ this.baseModel = baseModel;
+ this.currentEntity = entity;
+ this.currentStack = stack;
+ this.animatable = (T) stack.getItem();
+ this.currentSlot = slot;
+ }
+
+ /**
+ * Applies settings and transformations pre-render based on the default model
+ */
+ protected void applyBaseModel(HumanoidModel> baseModel) {
+ this.young = baseModel.young;
+ this.crouching = baseModel.crouching;
+ this.riding = baseModel.riding;
+ this.rightArmPose = baseModel.rightArmPose;
+ this.leftArmPose = baseModel.leftArmPose;
+ }
+
+ /**
+ * Resets the bone visibility for the model based on the currently rendering slot, and then sets bones relevant to the current slot as visible for rendering.
+ *
+ * This is only called by default for non-geo entities (I.E. players or vanilla mobs)
+ */
+ protected void applyBoneVisibilityBySlot(EquipmentSlot currentSlot) {
+ setAllVisible(false);
+
+ switch (currentSlot) {
+ case HEAD -> setBoneVisible(this.head, true);
+ case CHEST -> {
+ setBoneVisible(this.body, true);
+ setBoneVisible(this.rightArm, true);
+ setBoneVisible(this.leftArm, true);
+ }
+ case LEGS -> {
+ setBoneVisible(this.rightLeg, true);
+ setBoneVisible(this.leftLeg, true);
+ }
+ case FEET -> {
+ setBoneVisible(this.rightBoot, true);
+ setBoneVisible(this.leftBoot, true);
+ }
+ case MAINHAND, OFFHAND -> { /* No-Op */ }
+ }
+ }
+
+ /**
+ * Resets the bone visibility for the model based on the current {@link ModelPart} and {@link EquipmentSlot}, and then sets the bones relevant to the current part as visible for rendering.
+ *
+ * If you are rendering a geo entity with armor, you should probably be calling this prior to rendering
+ */
+ public void applyBoneVisibilityByPart(EquipmentSlot currentSlot, ModelPart currentPart, HumanoidModel> model) {
+ setAllVisible(false);
+
+ currentPart.visible = true;
+ GeoBone bone = null;
+
+ if (currentPart == model.hat || currentPart == model.head) {
+ bone = this.head;
+ } else if (currentPart == model.body) {
+ bone = this.body;
+ } else if (currentPart == model.leftArm) {
+ bone = this.leftArm;
+ } else if (currentPart == model.rightArm) {
+ bone = this.rightArm;
+ } else if (currentPart == model.leftLeg) {
+ bone = currentSlot == EquipmentSlot.FEET ? this.leftBoot : this.leftLeg;
+ } else if (currentPart == model.rightLeg) {
+ bone = currentSlot == EquipmentSlot.FEET ? this.rightBoot : this.rightLeg;
+ }
+
+ if (bone != null)
+ bone.setHidden(false);
+ }
+
+ /**
+ * Transform the currently rendering {@link GeoModel} to match the positions and rotations of the base model
+ */
+ protected void applyBaseTransformations(HumanoidModel> baseModel) {
+ if (this.head != null) {
+ ModelPart headPart = baseModel.head;
+
+ RenderUtils.matchModelPartRot(headPart, this.head);
+ this.head.updatePosition(headPart.x, -headPart.y, headPart.z);
+ }
+
+ if (this.body != null) {
+ ModelPart bodyPart = baseModel.body;
+
+ RenderUtils.matchModelPartRot(bodyPart, this.body);
+ this.body.updatePosition(bodyPart.x, -bodyPart.y, bodyPart.z);
+ }
+
+ if (this.rightArm != null) {
+ ModelPart rightArmPart = baseModel.rightArm;
+
+ RenderUtils.matchModelPartRot(rightArmPart, this.rightArm);
+ this.rightArm.updatePosition(rightArmPart.x + 5, 2 - rightArmPart.y, rightArmPart.z);
+ }
+
+ if (this.leftArm != null) {
+ ModelPart leftArmPart = baseModel.leftArm;
+
+ RenderUtils.matchModelPartRot(leftArmPart, this.leftArm);
+ this.leftArm.updatePosition(leftArmPart.x - 5f, 2f - leftArmPart.y, leftArmPart.z);
+ }
+
+ if (this.rightLeg != null) {
+ ModelPart rightLegPart = baseModel.rightLeg;
+
+ RenderUtils.matchModelPartRot(rightLegPart, this.rightLeg);
+ this.rightLeg.updatePosition(rightLegPart.x + 2, 12 - rightLegPart.y, rightLegPart.z);
+
+ if (this.rightBoot != null) {
+ RenderUtils.matchModelPartRot(rightLegPart, this.rightBoot);
+ this.rightBoot.updatePosition(rightLegPart.x + 2, 12 - rightLegPart.y, rightLegPart.z);
+ }
+ }
+
+ if (this.leftLeg != null) {
+ ModelPart leftLegPart = baseModel.leftLeg;
+
+ RenderUtils.matchModelPartRot(leftLegPart, this.leftLeg);
+ this.leftLeg.updatePosition(leftLegPart.x - 2, 12 - leftLegPart.y, leftLegPart.z);
+
+ if (this.leftBoot != null) {
+ RenderUtils.matchModelPartRot(leftLegPart, this.leftBoot);
+ this.leftBoot.updatePosition(leftLegPart.x - 2, 12 - leftLegPart.y, leftLegPart.z);
+ }
+ }
+ }
+
+ @Override
+ public void setAllVisible(boolean pVisible) {
+ super.setAllVisible(pVisible);
+
+ setBoneVisible(this.head, pVisible);
+ setBoneVisible(this.body, pVisible);
+ setBoneVisible(this.rightArm, pVisible);
+ setBoneVisible(this.leftArm, pVisible);
+ setBoneVisible(this.rightLeg, pVisible);
+ setBoneVisible(this.leftLeg, pVisible);
+ setBoneVisible(this.rightBoot, pVisible);
+ setBoneVisible(this.leftBoot, pVisible);
+ }
+
+ /**
+ * Apply custom scaling to account for {@link net.minecraft.client.model.AgeableListModel AgeableListModel} baby models
+ */
+ public void scaleModelForBaby(PoseStack poseStack, T animatable, float partialTick, boolean isReRender) {
+ if (!this.young || isReRender)
+ return;
+
+ if (this.currentSlot == EquipmentSlot.HEAD) {
+ if (Utils.getPrivateFinalStaticField(this.baseModel, this.baseModel.getClass(), "scaleHead")) {
+ float headScale = 1.5f / (float)Utils.getPrivateFinalStaticField(this.baseModel, this.baseModel.getClass(), "babyHeadScale");
+
+ poseStack.scale(headScale, headScale, headScale);
+ }
+
+ poseStack.translate(0, (float)Utils.getPrivateFinalStaticField(this.baseModel, this.baseModel.getClass(), "babyYHeadOffset") / 16f, (float)Utils.getPrivateFinalStaticField(this.baseModel, this.baseModel.getClass(), "babyZHeadOffset") / 16f);
+ } else {
+ float bodyScale = 1 / (float)Utils.getPrivateFinalStaticField(this.baseModel, this.baseModel.getClass(), "babyBodyScale");
+
+ poseStack.scale(bodyScale, bodyScale, bodyScale);
+ poseStack.translate(0, (float)Utils.getPrivateFinalStaticField(this.baseModel, this.baseModel.getClass(), "bodyYOffset") / 16f, 0);
+ }
+ }
+
+ /**
+ * Sets a bone as visible or hidden, with nullability
+ */
+ protected void setBoneVisible(@Nullable GeoBone bone, boolean visible) {
+ if (bone == null)
+ return;
+
+ bone.setHidden(!visible);
+ }
+
+ /**
+ * Update the current frame of a {@link AnimatableTexture potentially animated} texture used by this GeoRenderer.
+ * This should only be called immediately prior to rendering, and only
+ *
+ * @see AnimatableTexture#setAndUpdate(ResourceLocation, int)
+ */
+ @Override
+ public void updateAnimatedTextureFrame(T animatable) {
+ if (this.currentEntity != null)
+ AnimatableTexture.setAndUpdate(getTextureLocation(animatable), this.currentEntity.getId() + this.currentEntity.tickCount);
+ }
+
+ /**
+ * Create and fire the relevant {@code CompileLayers} event hook for this renderer
+ */
+ @Override
+ public void fireCompileRenderLayersEvent() {
+ GeoRenderArmorEvent.CompileRenderLayers.EVENT.handle(new GeoRenderArmorEvent.CompileRenderLayers(this));
+ }
+
+ /**
+ * Create and fire the relevant {@code Pre-Render} event hook for this renderer.
+ *
+ * @return Whether the renderer should proceed based on the cancellation state of the event
+ */
+ @Override
+ public boolean firePreRenderEvent(PoseStack poseStack, BakedGeoModel model, MultiBufferSource bufferSource, float partialTick, int packedLight) {
+ return GeoRenderArmorEvent.Pre.EVENT.handle(new GeoRenderArmorEvent.Pre(this, poseStack, model, bufferSource, partialTick, packedLight));
+ }
+
+ /**
+ * Create and fire the relevant {@code Post-Render} event hook for this renderer
+ */
+ @Override
+ public void firePostRenderEvent(PoseStack poseStack, BakedGeoModel model, MultiBufferSource bufferSource, float partialTick, int packedLight) {
+ GeoRenderArmorEvent.Post.EVENT.handle(new GeoRenderArmorEvent.Post(this, poseStack, model, bufferSource, partialTick, packedLight));
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/GeoBlockRenderer.java b/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/GeoBlockRenderer.java
new file mode 100644
index 0000000..82e041a
--- /dev/null
+++ b/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/GeoBlockRenderer.java
@@ -0,0 +1,244 @@
+package mod.azure.azurelib.common.api.client.renderer;
+
+import java.util.List;
+
+import mod.azure.azurelib.common.api.common.event.GeoRenderBlockEvent;
+import mod.azure.azurelib.common.api.client.model.GeoModel;
+import mod.azure.azurelib.common.internal.client.renderer.GeoRenderer;
+import mod.azure.azurelib.common.platform.Services;
+import org.joml.Matrix4f;
+import org.joml.Vector3f;
+
+import com.mojang.blaze3d.systems.RenderSystem;
+import com.mojang.blaze3d.vertex.PoseStack;
+import com.mojang.blaze3d.vertex.VertexConsumer;
+import com.mojang.math.Axis;
+
+import mod.azure.azurelib.common.internal.common.cache.object.BakedGeoModel;
+import mod.azure.azurelib.common.internal.common.cache.object.GeoBone;
+import mod.azure.azurelib.common.internal.common.cache.texture.AnimatableTexture;
+import mod.azure.azurelib.common.internal.common.constant.DataTickets;
+import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable;
+import mod.azure.azurelib.common.internal.common.core.animation.AnimationState;
+import mod.azure.azurelib.common.api.client.renderer.layer.GeoRenderLayer;
+import mod.azure.azurelib.common.api.client.renderer.layer.GeoRenderLayersContainer;
+import mod.azure.azurelib.common.internal.client.util.RenderUtils;
+import net.minecraft.client.renderer.MultiBufferSource;
+import net.minecraft.client.renderer.RenderType;
+import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
+import net.minecraft.core.Direction;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.level.block.DirectionalBlock;
+import net.minecraft.world.level.block.HorizontalDirectionalBlock;
+import net.minecraft.world.level.block.entity.BlockEntity;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.phys.Vec3;
+
+/**
+ * Base {@link GeoRenderer} class for rendering {@link BlockEntity Blocks} specifically.
+ * All blocks added to be rendered by AzureLib should use an instance of this class.
+ */
+public class GeoBlockRenderer implements GeoRenderer, BlockEntityRenderer {
+ protected final GeoModel model;
+ protected final GeoRenderLayersContainer renderLayers = new GeoRenderLayersContainer<>(this);
+
+ protected T animatable;
+ protected float scaleWidth = 1;
+ protected float scaleHeight = 1;
+
+ protected Matrix4f blockRenderTranslations = new Matrix4f();
+ protected Matrix4f modelRenderTranslations = new Matrix4f();
+
+ public GeoBlockRenderer(GeoModel model) {
+ this.model = model;
+
+ }
+
+ /**
+ * Gets the model instance for this renderer
+ */
+ @Override
+ public GeoModel getGeoModel() {
+ return this.model;
+ }
+
+ /**
+ * Gets the {@link GeoAnimatable} instance currently being rendered
+ */
+ @Override
+ public T getAnimatable() {
+ return this.animatable;
+ }
+
+ /**
+ * Gets the id that represents the current animatable's instance for animation purposes. This is mostly useful for things like items, which have a single registered instance for all objects
+ */
+ @Override
+ public long getInstanceId(T animatable) {
+ return animatable.getBlockPos().hashCode();
+ }
+
+ /**
+ * Returns the list of registered {@link GeoRenderLayer GeoRenderLayers} for this renderer
+ */
+ @Override
+ public List> getRenderLayers() {
+ return this.renderLayers.getRenderLayers();
+ }
+
+ /**
+ * Adds a {@link GeoRenderLayer} to this renderer, to be called after the main model is rendered each frame
+ */
+ public GeoBlockRenderer addRenderLayer(GeoRenderLayer renderLayer) {
+ this.renderLayers.addLayer(renderLayer);
+
+ return this;
+ }
+
+ /**
+ * Sets a scale override for this renderer, telling AzureLib to pre-scale the model
+ */
+ public GeoBlockRenderer withScale(float scale) {
+ return withScale(scale, scale);
+ }
+
+ /**
+ * Sets a scale override for this renderer, telling AzureLib to pre-scale the model
+ */
+ public GeoBlockRenderer withScale(float scaleWidth, float scaleHeight) {
+ this.scaleWidth = scaleWidth;
+ this.scaleHeight = scaleHeight;
+
+ return this;
+ }
+
+ /**
+ * Called before rendering the model to buffer. Allows for render modifications and preparatory work such as scaling and translating.
+ * {@link PoseStack} translations made here are kept until the end of the render process
+ */
+ @Override
+ public void preRender(PoseStack poseStack, T animatable, BakedGeoModel model, MultiBufferSource bufferSource, VertexConsumer buffer, boolean isReRender, float partialTick, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) {
+ this.blockRenderTranslations = new Matrix4f(poseStack.last().pose());
+
+ scaleModelForRender(this.scaleWidth, this.scaleHeight, poseStack, animatable, model, isReRender, partialTick, packedLight, packedOverlay);
+ }
+
+ @Override
+ public void render(T animatable, float partialTick, PoseStack poseStack, MultiBufferSource bufferSource, int packedLight, int packedOverlay) {
+ this.animatable = animatable;
+
+ defaultRender(poseStack, this.animatable, bufferSource, null, null, 0, partialTick, packedLight);
+ }
+
+ /**
+ * The actual render method that subtype renderers should override to handle their specific rendering tasks.
+ * {@link GeoRenderer#preRender} has already been called by this stage, and {@link GeoRenderer#postRender} will be called directly after
+ */
+ @Override
+ public void actuallyRender(PoseStack poseStack, T animatable, BakedGeoModel model, RenderType renderType, MultiBufferSource bufferSource, VertexConsumer buffer, boolean isReRender, float partialTick, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) {
+
+ if (!isReRender) {
+ AnimationState animationState = new AnimationState(animatable, 0, 0, partialTick, false);
+ long instanceId = getInstanceId(animatable);
+
+ animationState.setData(DataTickets.TICK, animatable.getTick(animatable));
+ animationState.setData(DataTickets.BLOCK_ENTITY, animatable);
+ this.model.addAdditionalStateData(animatable, instanceId, animationState::setData);
+ poseStack.translate(0.5, 0, 0.5);
+ rotateBlock(getFacing(animatable), poseStack);
+ this.model.handleAnimations(animatable, instanceId, animationState);
+ }
+
+ this.modelRenderTranslations = new Matrix4f(poseStack.last().pose());
+
+ RenderSystem.setShaderTexture(0, getTextureLocation(animatable));
+ GeoRenderer.super.actuallyRender(poseStack, animatable, model, renderType, bufferSource, buffer, isReRender, partialTick, packedLight, packedOverlay, red, green, blue, alpha);
+ }
+
+ /**
+ * Renders the provided {@link GeoBone} and its associated child bones
+ */
+ @Override
+ public void renderRecursively(PoseStack poseStack, T animatable, GeoBone bone, RenderType renderType, MultiBufferSource bufferSource, VertexConsumer buffer, boolean isReRender, float partialTick, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) {
+ if (bone.isTrackingMatrices()) {
+ Matrix4f poseState = new Matrix4f(poseStack.last().pose());
+ Matrix4f localMatrix = RenderUtils.invertAndMultiplyMatrices(poseState, this.blockRenderTranslations);
+
+ bone.setModelSpaceMatrix(RenderUtils.invertAndMultiplyMatrices(poseState, this.modelRenderTranslations));
+ bone.setLocalSpaceMatrix(RenderUtils.translateMatrix(localMatrix, getRenderOffset(this.animatable, 1).toVector3f()));
+ bone.setWorldSpaceMatrix(RenderUtils.translateMatrix(new Matrix4f(localMatrix), new Vector3f(this.animatable.getBlockPos().getX(), this.animatable.getBlockPos().getY(), this.animatable.getBlockPos().getZ())));
+ }
+
+ GeoRenderer.super.renderRecursively(poseStack, animatable, bone, renderType, bufferSource, buffer, isReRender, partialTick, packedLight, packedOverlay, red, green, blue, alpha);
+ }
+
+ public Vec3 getRenderOffset(BlockEntity entity, float f) {
+ return Vec3.ZERO;
+ }
+
+ /**
+ * Rotate the {@link PoseStack} based on the determined {@link Direction} the block is facing
+ */
+ protected void rotateBlock(Direction facing, PoseStack poseStack) {
+ switch (facing) {
+ case SOUTH -> poseStack.mulPose(Axis.YP.rotationDegrees(180));
+ case WEST -> poseStack.mulPose(Axis.YP.rotationDegrees(90));
+ case NORTH -> poseStack.mulPose(Axis.YP.rotationDegrees(0));
+ case EAST -> poseStack.mulPose(Axis.YP.rotationDegrees(270));
+ case UP -> poseStack.mulPose(Axis.XP.rotationDegrees(90));
+ case DOWN -> poseStack.mulPose(Axis.XN.rotationDegrees(90));
+ }
+ }
+
+ /**
+ * Attempt to extract a direction from the block so that the model can be oriented correctly
+ */
+ protected Direction getFacing(T block) {
+ BlockState blockState = block.getBlockState();
+
+ if (blockState.hasProperty(HorizontalDirectionalBlock.FACING))
+ return blockState.getValue(HorizontalDirectionalBlock.FACING);
+
+ if (blockState.hasProperty(DirectionalBlock.FACING))
+ return blockState.getValue(DirectionalBlock.FACING);
+
+ return Direction.NORTH;
+ }
+
+ /**
+ * Update the current frame of a {@link AnimatableTexture potentially animated} texture used by this GeoRenderer.
+ * This should only be called immediately prior to rendering, and only
+ *
+ * @see AnimatableTexture#setAndUpdate(ResourceLocation, int)
+ */
+ @Override
+ public void updateAnimatedTextureFrame(T animatable) {
+ AnimatableTexture.setAndUpdate(getTextureLocation(animatable), animatable.getBlockPos().getX() + animatable.getBlockPos().getY() + animatable.getBlockPos().getZ() + (int) animatable.getTick(animatable));
+ }
+
+ /**
+ * Create and fire the relevant {@code CompileLayers} event hook for this renderer
+ */
+ @Override
+ public void fireCompileRenderLayersEvent() {
+ GeoRenderBlockEvent.CompileRenderLayers.EVENT.handle(new GeoRenderBlockEvent.CompileRenderLayers(this));
+ }
+
+ /**
+ * Create and fire the relevant {@code Pre-Render} event hook for this renderer.
+ *
+ * @return Whether the renderer should proceed based on the cancellation state of the event
+ */
+ @Override
+ public boolean firePreRenderEvent(PoseStack poseStack, BakedGeoModel model, MultiBufferSource bufferSource, float partialTick, int packedLight) {
+ return GeoRenderBlockEvent.Pre.EVENT.handle(new GeoRenderBlockEvent.Pre(this, poseStack, model, bufferSource, partialTick, packedLight));
+ }
+
+ /**
+ * Create and fire the relevant {@code Post-Render} event hook for this renderer
+ */
+ @Override
+ public void firePostRenderEvent(PoseStack poseStack, BakedGeoModel model, MultiBufferSource bufferSource, float partialTick, int packedLight) {
+ GeoRenderBlockEvent.Post.EVENT.handle(new GeoRenderBlockEvent.Post(this, poseStack, model, bufferSource, partialTick, packedLight));
+ }
+}
diff --git a/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/GeoEntityRenderer.java b/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/GeoEntityRenderer.java
new file mode 100644
index 0000000..e59a353
--- /dev/null
+++ b/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/GeoEntityRenderer.java
@@ -0,0 +1,526 @@
+package mod.azure.azurelib.common.api.client.renderer;
+
+import com.mojang.blaze3d.vertex.BufferBuilder;
+import com.mojang.blaze3d.vertex.PoseStack;
+import com.mojang.blaze3d.vertex.VertexConsumer;
+import com.mojang.math.Axis;
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import mod.azure.azurelib.common.api.client.model.GeoModel;
+import mod.azure.azurelib.common.api.client.renderer.layer.GeoRenderLayer;
+import mod.azure.azurelib.common.api.common.event.GeoRenderEntityEvent;
+import mod.azure.azurelib.common.internal.client.model.data.EntityModelData;
+import mod.azure.azurelib.common.internal.client.renderer.GeoRenderer;
+import mod.azure.azurelib.common.internal.client.util.RenderUtils;
+import mod.azure.azurelib.common.internal.common.cache.object.BakedGeoModel;
+import mod.azure.azurelib.common.internal.common.cache.object.GeoBone;
+import mod.azure.azurelib.common.internal.common.cache.texture.AnimatableTexture;
+import mod.azure.azurelib.common.internal.common.constant.DataTickets;
+import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable;
+import mod.azure.azurelib.common.internal.common.core.animation.AnimationState;
+import mod.azure.azurelib.common.platform.Services;
+import net.minecraft.ChatFormatting;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.LightTexture;
+import net.minecraft.client.renderer.MultiBufferSource;
+import net.minecraft.client.renderer.RenderType;
+import net.minecraft.client.renderer.entity.EntityRenderer;
+import net.minecraft.client.renderer.entity.EntityRendererProvider;
+import net.minecraft.client.renderer.entity.LivingEntityRenderer;
+import net.minecraft.client.renderer.texture.OverlayTexture;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.Direction;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.util.Mth;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.entity.Mob;
+import net.minecraft.world.entity.Pose;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.world.entity.player.PlayerModelPart;
+import net.minecraft.world.level.LightLayer;
+import net.minecraft.world.phys.Vec3;
+import org.Vrglab.AzureLib.Utility.Utils;
+import org.joml.Matrix4f;
+
+import java.util.List;
+
+/**
+ * Base {@link GeoRenderer} class for rendering {@link Entity Entities} specifically.
+ * All entities added to be rendered by AzureLib should use an instance of this class.
+ * This also includes {@link net.minecraft.world.entity.projectile.Projectile Projectiles}
+ */
+public class GeoEntityRenderer extends EntityRenderer implements GeoRenderer {
+ protected final List> renderLayers = new ObjectArrayList<>();
+ protected final GeoModel model;
+
+ protected T animatable;
+ protected float scaleWidth = 1;
+ protected float scaleHeight = 1;
+
+ protected Matrix4f entityRenderTranslations = new Matrix4f();
+ protected Matrix4f modelRenderTranslations = new Matrix4f();
+
+ public GeoEntityRenderer(EntityRendererProvider.Context renderManager, GeoModel model) {
+ super(renderManager);
+
+ this.model = model;
+ }
+
+ /**
+ * Gets the model instance for this renderer
+ */
+ @Override
+ public GeoModel getGeoModel() {
+ return this.model;
+ }
+
+ /**
+ * Gets the {@link GeoAnimatable} instance currently being rendered
+ */
+ @Override
+ public T getAnimatable() {
+ return this.animatable;
+ }
+
+ /**
+ * Gets the id that represents the current animatable's instance for animation purposes. This is mostly useful for things like items, which have a single registered instance for all objects
+ */
+ @Override
+ public long getInstanceId(T animatable) {
+ return animatable.getId();
+ }
+
+ /**
+ * Shadowing override of {@link EntityRenderer#getTextureLocation}.
+ * This redirects the call to {@link GeoRenderer#getTextureLocation}
+ */
+ @Override
+ public ResourceLocation getTextureLocation(T animatable) {
+ return GeoRenderer.super.getTextureLocation(animatable);
+ }
+
+ /**
+ * Returns the list of registered {@link GeoRenderLayer GeoRenderLayers} for this renderer
+ */
+ @Override
+ public List> getRenderLayers() {
+ return this.renderLayers;
+ }
+
+ /**
+ * Adds a {@link GeoRenderLayer} to this renderer, to be called after the main model is rendered each frame
+ */
+ public GeoEntityRenderer addRenderLayer(GeoRenderLayer renderLayer) {
+ this.renderLayers.add(renderLayer);
+
+ return this;
+ }
+
+ /**
+ * Sets a scale override for this renderer, telling AzureLib to pre-scale the model
+ */
+ public GeoEntityRenderer withScale(float scale) {
+ return withScale(scale, scale);
+ }
+
+ /**
+ * Sets a scale override for this renderer, telling AzureLib to pre-scale the model
+ */
+ public GeoEntityRenderer withScale(float scaleWidth, float scaleHeight) {
+ this.scaleWidth = scaleWidth;
+ this.scaleHeight = scaleHeight;
+
+ return this;
+ }
+
+ /**
+ * Called before rendering the model to buffer. Allows for render modifications and preparatory work such as scaling and translating.
+ * {@link PoseStack} translations made here are kept until the end of the render process
+ */
+ @Override
+ public void preRender(PoseStack poseStack, T animatable, BakedGeoModel model, MultiBufferSource bufferSource, VertexConsumer buffer, boolean isReRender, float partialTick, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) {
+ this.entityRenderTranslations = new Matrix4f(poseStack.last().pose());
+
+ scaleModelForRender(this.scaleWidth, this.scaleHeight, poseStack, animatable, model, isReRender, partialTick,
+ packedLight, packedOverlay);
+ }
+
+ @Override
+ public void render(T entity, float entityYaw, float partialTick, PoseStack poseStack, MultiBufferSource bufferSource, int packedLight) {
+ this.animatable = entity;
+
+ defaultRender(poseStack, entity, bufferSource, null, null, entityYaw, partialTick, packedLight);
+ }
+
+ /**
+ * The actual render method that subtype renderers should override to handle their specific rendering tasks.
+ * {@link GeoRenderer#preRender} has already been called by this stage, and {@link GeoRenderer#postRender} will be called directly after
+ */
+ @Override
+ public void actuallyRender(PoseStack poseStack, T animatable, BakedGeoModel model, RenderType renderType, MultiBufferSource bufferSource, VertexConsumer buffer, boolean isReRender, float partialTick, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) {
+ poseStack.pushPose();
+
+ LivingEntity livingEntity = animatable instanceof LivingEntity entity ? entity : null;
+
+ boolean shouldSit = animatable.isPassenger() && (animatable.getVehicle() != null);
+ float lerpBodyRot = livingEntity == null ? 0 : Mth.rotLerp(partialTick, livingEntity.yBodyRotO,
+ livingEntity.yBodyRot);
+ float lerpHeadRot = livingEntity == null ? 0 : Mth.rotLerp(partialTick, livingEntity.yHeadRotO,
+ livingEntity.yHeadRot);
+ float netHeadYaw = lerpHeadRot - lerpBodyRot;
+
+ if (shouldSit && animatable.getVehicle() instanceof LivingEntity livingentity) {
+ lerpBodyRot = Mth.rotLerp(partialTick, livingentity.yBodyRotO, livingentity.yBodyRot);
+ netHeadYaw = lerpHeadRot - lerpBodyRot;
+ float clampedHeadYaw = Mth.clamp(Mth.wrapDegrees(netHeadYaw), -85, 85);
+ lerpBodyRot = lerpHeadRot - clampedHeadYaw;
+
+ if (clampedHeadYaw * clampedHeadYaw > 2500f) lerpBodyRot += clampedHeadYaw * 0.2f;
+
+ netHeadYaw = lerpHeadRot - lerpBodyRot;
+ }
+
+ if (animatable.getPose() == Pose.SLEEPING && livingEntity != null) {
+ Direction bedDirection = livingEntity.getBedOrientation();
+
+ if (bedDirection != null) {
+ float eyePosOffset = livingEntity.getEyeHeight(Pose.STANDING) - 0.1F;
+
+ poseStack.translate(-bedDirection.getStepX() * eyePosOffset, 0,
+ -bedDirection.getStepZ() * eyePosOffset);
+ }
+ }
+
+ float nativeScale = livingEntity != null ? livingEntity.getScale() : 1;
+ float ageInTicks = animatable.tickCount + partialTick;
+ float limbSwingAmount = 0;
+ float limbSwing = 0;
+
+ poseStack.scale(nativeScale, nativeScale, nativeScale);
+ applyRotations(animatable, poseStack, ageInTicks, lerpBodyRot, partialTick, nativeScale);
+
+ if (!shouldSit && animatable.isAlive() && livingEntity != null) {
+ limbSwingAmount = Mth.lerp(partialTick, Utils.getPrivateFinalStaticField(livingEntity.walkAnimation, livingEntity.walkAnimation.getClass(), "speedOld"),
+ livingEntity.walkAnimation.speed());
+ limbSwing = livingEntity.walkAnimation.position(partialTick);
+
+ if (livingEntity.isBaby()) limbSwing *= 3f;
+
+ if (limbSwingAmount > 1f) limbSwingAmount = 1f;
+ }
+
+ if (!isReRender) {
+ float headPitch = Mth.lerp(partialTick, animatable.xRotO, animatable.getXRot());
+ float motionThreshold = getMotionAnimThreshold(animatable);
+ Vec3 velocity = animatable.getDeltaMovement();
+ float avgVelocity = (float) (Math.abs(velocity.x) + Math.abs(velocity.z) / 2f);
+ AnimationState animationState = new AnimationState(animatable, limbSwing, limbSwingAmount,
+ partialTick, avgVelocity >= motionThreshold && limbSwingAmount != 0);
+ long instanceId = getInstanceId(animatable);
+
+ animationState.setData(DataTickets.TICK, animatable.getTick(animatable));
+ animationState.setData(DataTickets.ENTITY, animatable);
+ animationState.setData(DataTickets.ENTITY_MODEL_DATA,
+ new EntityModelData(shouldSit, livingEntity != null && livingEntity.isBaby(), -netHeadYaw,
+ -headPitch));
+ this.model.addAdditionalStateData(animatable, instanceId, animationState::setData);
+ this.model.handleAnimations(animatable, instanceId, animationState);
+ }
+
+ poseStack.translate(0, 0.01f, 0);
+
+ this.modelRenderTranslations = new Matrix4f(poseStack.last().pose());
+
+ if (!animatable.isInvisibleTo(Minecraft.getInstance().player))
+ GeoRenderer.super.actuallyRender(poseStack, animatable, model, renderType, bufferSource, buffer, isReRender,
+ partialTick, packedLight, packedOverlay, red, green, blue, alpha);
+
+ poseStack.popPose();
+ }
+
+ /**
+ * Render the various {@link GeoRenderLayer RenderLayers} that have been registered to this renderer
+ */
+ @Override
+ public void applyRenderLayers(PoseStack poseStack, T animatable, BakedGeoModel model, RenderType renderType, MultiBufferSource bufferSource, VertexConsumer buffer, float partialTick, int packedLight, int packedOverlay) {
+ if (!animatable.isSpectator())
+ GeoRenderer.super.applyRenderLayers(poseStack, animatable, model, renderType, bufferSource, buffer,
+ partialTick, packedLight, packedOverlay);
+ }
+
+ /**
+ * Call after all other rendering work has taken place, including reverting the {@link PoseStack}'s state. This method is not called in {@link GeoRenderer#reRender re-render}
+ */
+ @Override
+ public void renderFinal(PoseStack poseStack, T animatable, BakedGeoModel model, MultiBufferSource bufferSource, VertexConsumer buffer, float partialTick, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) {
+ super.render(animatable, 0, partialTick, poseStack, bufferSource, packedLight);
+
+ if (animatable instanceof Mob mob) {
+ Entity leashHolder = mob.getLeashHolder();
+
+ if (leashHolder != null) renderLeash(mob, partialTick, poseStack, bufferSource, leashHolder);
+ }
+ }
+
+ /**
+ * Renders the provided {@link GeoBone} and its associated child bones
+ */
+ @Override
+ public void renderRecursively(PoseStack poseStack, T animatable, GeoBone bone, RenderType renderType, MultiBufferSource bufferSource, VertexConsumer buffer, boolean isReRender, float partialTick, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) {
+ poseStack.pushPose();
+ RenderUtils.translateMatrixToBone(poseStack, bone);
+ RenderUtils.translateToPivotPoint(poseStack, bone);
+ RenderUtils.rotateMatrixAroundBone(poseStack, bone);
+ RenderUtils.scaleMatrixForBone(poseStack, bone);
+
+ if (bone.isTrackingMatrices()) {
+ Matrix4f poseState = new Matrix4f(poseStack.last().pose());
+ Matrix4f localMatrix = RenderUtils.invertAndMultiplyMatrices(poseState, this.entityRenderTranslations);
+
+ bone.setModelSpaceMatrix(RenderUtils.invertAndMultiplyMatrices(poseState, this.modelRenderTranslations));
+ bone.setLocalSpaceMatrix(
+ RenderUtils.translateMatrix(localMatrix, getRenderOffset(this.animatable, 1).toVector3f()));
+ bone.setWorldSpaceMatrix(
+ RenderUtils.translateMatrix(new Matrix4f(localMatrix), this.animatable.position().toVector3f()));
+ }
+
+ RenderUtils.translateAwayFromPivotPoint(poseStack, bone);
+
+ renderCubesOfBone(poseStack, bone, buffer, packedLight, packedOverlay, red, green, blue, alpha);
+
+ if (!isReRender) {
+ applyRenderLayersForBone(poseStack, animatable, bone, renderType, bufferSource, buffer, partialTick,
+ packedLight, packedOverlay);
+ if (buffer instanceof BufferBuilder builder && !((boolean)Utils.getPrivateFinalStaticField(builder, builder.getClass(), "building")))
+ buffer = bufferSource.getBuffer(renderType);
+ }
+
+ renderChildBones(poseStack, animatable, bone, renderType, bufferSource, buffer, isReRender, partialTick,
+ packedLight, packedOverlay, red, green, blue, alpha);
+
+ poseStack.popPose();
+ }
+
+ /**
+ * Applies rotation transformations to the renderer prior to render time to account for various entity states, default scale of 1
+ */
+ protected void applyRotations(T animatable, PoseStack poseStack, float ageInTicks, float rotationYaw, float partialTick) {
+
+ applyRotations(animatable, poseStack, ageInTicks, rotationYaw, partialTick, 1);
+ }
+
+ /**
+ * Applies rotation transformations to the renderer prior to render time to account for various entity states, scalable
+ */
+ protected void applyRotations(T animatable, PoseStack poseStack, float ageInTicks, float rotationYaw,
+ float partialTick, float nativeScale) {
+ if (isShaking(animatable))
+ rotationYaw += (float)(Math.cos(animatable.tickCount * 3.25d) * Math.PI * 0.4d);
+
+ if (!animatable.hasPose(Pose.SLEEPING))
+ poseStack.mulPose(Axis.YP.rotationDegrees(180f - rotationYaw));
+
+ if (animatable instanceof LivingEntity livingEntity) {
+ if (livingEntity.deathTime > 0) {
+ float deathRotation = (livingEntity.deathTime + partialTick - 1f) / 20f * 1.6f;
+
+ poseStack.mulPose(Axis.ZP.rotationDegrees(Math.min(Mth.sqrt(deathRotation), 1) * getDeathMaxRotation(animatable)));
+ }
+ else if (livingEntity.isAutoSpinAttack()) {
+ poseStack.mulPose(Axis.XP.rotationDegrees(-90f - livingEntity.getXRot()));
+ poseStack.mulPose(Axis.YP.rotationDegrees((livingEntity.tickCount + partialTick) * -75f));
+ }
+ else if (animatable.hasPose(Pose.SLEEPING)) {
+ Direction bedOrientation = livingEntity.getBedOrientation();
+
+ poseStack.mulPose(Axis.YP.rotationDegrees(bedOrientation != null ? RenderUtils.getDirectionAngle(bedOrientation) : rotationYaw));
+ poseStack.mulPose(Axis.ZP.rotationDegrees(getDeathMaxRotation(animatable)));
+ poseStack.mulPose(Axis.YP.rotationDegrees(270f));
+ }
+ else if (LivingEntityRenderer.isEntityUpsideDown(livingEntity)) {
+ poseStack.translate(0, (animatable.getBbHeight() + 0.1f) / nativeScale, 0);
+ poseStack.mulPose(Axis.ZP.rotationDegrees(180f));
+ }
+ }
+ }
+
+ /**
+ * Gets the max rotation value for dying entities.
+ * You might want to modify this for different aesthetics, such as a {@link net.minecraft.world.entity.monster.Spider} flipping upside down on death.
+ * Functionally equivalent to {@link net.minecraft.client.renderer.entity.LivingEntityRenderer#getFlipDegrees}
+ */
+ protected float getDeathMaxRotation(T animatable) {
+ return 90f;
+ }
+
+ /**
+ * Whether the entity's nametag should be rendered or not.
+ * Pretty much exclusively used in {@link EntityRenderer#renderNameTag}
+ */
+ @Override
+ public boolean shouldShowName(T animatable) {
+ var nameRenderDistance = animatable.isDiscrete() ? 32d : 64d;
+
+ if (!(animatable instanceof LivingEntity))
+ return false;
+
+ if (this.entityRenderDispatcher.distanceToSqr(animatable) >= nameRenderDistance * nameRenderDistance)
+ return false;
+
+ if (animatable instanceof Mob && (!animatable.shouldShowName() && (!animatable.hasCustomName() || animatable != this.entityRenderDispatcher.crosshairPickEntity)))
+ return false;
+
+ final var minecraft = Minecraft.getInstance();
+ var visibleToClient = !animatable.isInvisibleTo(minecraft.player);
+ var entityTeam = animatable.getTeam();
+
+ if (entityTeam == null)
+ return Minecraft.renderNames() && animatable != minecraft.getCameraEntity() && visibleToClient && !animatable.isVehicle();
+
+ var playerTeam = minecraft.player.getTeam();
+
+ return switch (entityTeam.getNameTagVisibility()) {
+ case ALWAYS -> visibleToClient;
+ case NEVER -> false;
+ case HIDE_FOR_OTHER_TEAMS -> playerTeam == null ? visibleToClient : entityTeam.isAlliedTo(
+ playerTeam) && (entityTeam.canSeeFriendlyInvisibles() || visibleToClient);
+ case HIDE_FOR_OWN_TEAM ->
+ playerTeam == null ? visibleToClient : !entityTeam.isAlliedTo(playerTeam) && visibleToClient;
+ };
+ }
+
+ /**
+ * Gets a packed overlay coordinate pair for rendering.
+ * Mostly just used for the red tint when an entity is hurt, but can be used for other things like the {@link net.minecraft.world.entity.monster.Creeper} white tint when exploding.
+ */
+ @Override
+ public int getPackedOverlay(T animatable, float u) {
+ if (!(animatable instanceof LivingEntity entity)) return OverlayTexture.NO_OVERLAY;
+
+ return OverlayTexture.pack(OverlayTexture.u(u), OverlayTexture.v(entity.hurtTime > 0 || entity.deathTime > 0));
+ }
+
+ /**
+ * Gets a packed overlay coordinate pair for rendering.
+ * Mostly just used for the red tint when an entity is hurt,
+ * but can be used for other things like the {@link net.minecraft.world.entity.monster.Creeper}
+ * white tint when exploding.
+ */
+ @Override
+ public int getPackedOverlay(T animatable, float u, float partialTick) {
+ return getPackedOverlay(animatable, u);
+ }
+
+ /**
+ * Static rendering code for rendering a leash segment.
+ * It's a like-for-like from {@link net.minecraft.client.renderer.entity.MobRenderer#renderLeash} that had to be duplicated here for flexible usage
+ */
+ public void renderLeash(M mob, float partialTick, PoseStack poseStack, MultiBufferSource bufferSource, E leashHolder) {
+ double lerpBodyAngle = (Mth.lerp(partialTick, mob.yBodyRotO, mob.yBodyRot) * Mth.DEG_TO_RAD) + Mth.HALF_PI;
+ Vec3 leashOffset = Utils.callPrivateMethod(mob, "getLeashOffset", new Class[0]);
+ double xAngleOffset = Math.cos(lerpBodyAngle) * leashOffset.z + Math.sin(lerpBodyAngle) * leashOffset.x;
+ double zAngleOffset = Math.sin(lerpBodyAngle) * leashOffset.z - Math.cos(lerpBodyAngle) * leashOffset.x;
+ double lerpOriginX = Mth.lerp(partialTick, mob.xo, mob.getX()) + xAngleOffset;
+ double lerpOriginY = Mth.lerp(partialTick, mob.yo, mob.getY()) + leashOffset.y;
+ double lerpOriginZ = Mth.lerp(partialTick, mob.zo, mob.getZ()) + zAngleOffset;
+ Vec3 ropeGripPosition = leashHolder.getRopeHoldPosition(partialTick);
+ float xDif = (float) (ropeGripPosition.x - lerpOriginX);
+ float yDif = (float) (ropeGripPosition.y - lerpOriginY);
+ float zDif = (float) (ropeGripPosition.z - lerpOriginZ);
+ float offsetMod = (float) (Mth.fastInvSqrt(xDif * xDif + zDif * zDif) * 0.025f / 2f);
+ float xOffset = zDif * offsetMod;
+ float zOffset = xDif * offsetMod;
+ VertexConsumer vertexConsumer = bufferSource.getBuffer(RenderType.leash());
+ BlockPos entityEyePos = BlockPos.containing(mob.getEyePosition(partialTick));
+ BlockPos holderEyePos = BlockPos.containing(leashHolder.getEyePosition(partialTick));
+ int entityBlockLight = getBlockLightLevel((T) mob, entityEyePos);
+ int holderBlockLight = leashHolder.isOnFire() ? 15 : leashHolder.level().getBrightness(LightLayer.BLOCK,
+ holderEyePos);
+ int entitySkyLight = mob.level().getBrightness(LightLayer.SKY, entityEyePos);
+ int holderSkyLight = mob.level().getBrightness(LightLayer.SKY, holderEyePos);
+
+ poseStack.pushPose();
+ poseStack.translate(xAngleOffset, leashOffset.y, zAngleOffset);
+
+ Matrix4f posMatrix = new Matrix4f(poseStack.last().pose());
+
+ for (int segment = 0; segment <= 24; ++segment) {
+ GeoEntityRenderer.renderLeashPiece(vertexConsumer, posMatrix, xDif, yDif, zDif, entityBlockLight,
+ holderBlockLight, entitySkyLight, holderSkyLight, 0.025f, 0.025f, xOffset, zOffset, segment, false);
+ }
+
+ for (int segment = 24; segment >= 0; --segment) {
+ GeoEntityRenderer.renderLeashPiece(vertexConsumer, posMatrix, xDif, yDif, zDif, entityBlockLight,
+ holderBlockLight, entitySkyLight, holderSkyLight, 0.025f, 0.0f, xOffset, zOffset, segment, true);
+ }
+
+ poseStack.popPose();
+ }
+
+ /**
+ * Static rendering code for rendering a leash segment.
+ * It's a like-for-like from {@link net.minecraft.client.renderer.entity.MobRenderer#addVertexPair} that had to be duplicated here for flexible usage
+ */
+ private static void renderLeashPiece(VertexConsumer buffer, Matrix4f positionMatrix, float xDif, float yDif, float zDif, int entityBlockLight, int holderBlockLight, int entitySkyLight, int holderSkyLight, float width, float yOffset, float xOffset, float zOffset, int segment, boolean isLeashKnot) {
+ float piecePosPercent = segment / 24f;
+ int lerpBlockLight = (int) Mth.lerp(piecePosPercent, entityBlockLight, holderBlockLight);
+ int lerpSkyLight = (int) Mth.lerp(piecePosPercent, entitySkyLight, holderSkyLight);
+ int packedLight = LightTexture.pack(lerpBlockLight, lerpSkyLight);
+ float knotColourMod = segment % 2 == (isLeashKnot ? 1 : 0) ? 0.7f : 1f;
+ float red = 0.5f * knotColourMod;
+ float green = 0.4f * knotColourMod;
+ float blue = 0.3f * knotColourMod;
+ float x = xDif * piecePosPercent;
+ float y = yDif > 0.0f ? yDif * piecePosPercent * piecePosPercent : yDif - yDif * (1.0f - piecePosPercent) * (1.0f - piecePosPercent);
+ float z = zDif * piecePosPercent;
+
+ buffer.vertex(positionMatrix, x - xOffset, y + yOffset, z + zOffset).color(red, green, blue, 1).uv2(
+ packedLight).endVertex();
+ buffer.vertex(positionMatrix, x + xOffset, y + width - yOffset, z - zOffset).color(red, green, blue, 1).uv2(
+ packedLight).endVertex();
+ }
+
+ public boolean isShaking(T entity) {
+ return entity.isFullyFrozen();
+ }
+
+ /**
+ * Update the current frame of a {@link AnimatableTexture potentially animated} texture used by this GeoRenderer.
+ * This should only be called immediately prior to rendering, and only
+ *
+ * @see AnimatableTexture#setAndUpdate(ResourceLocation, int)
+ */
+ @Override
+ public void updateAnimatedTextureFrame(T animatable) {
+ AnimatableTexture.setAndUpdate(getTextureLocation(animatable),
+ animatable.getId() + (int) animatable.getTick(animatable));
+ }
+
+ /**
+ * Create and fire the relevant {@code CompileLayers} event hook for this renderer
+ */
+ @Override
+ public void fireCompileRenderLayersEvent() {
+ GeoRenderEntityEvent.CompileRenderLayers.EVENT.handle(new GeoRenderEntityEvent.CompileRenderLayers(this));
+ }
+
+ /**
+ * Create and fire the relevant {@code Pre-Render} event hook for this renderer.
+ *
+ * @return Whether the renderer should proceed based on the cancellation state of the event
+ */
+ @Override
+ public boolean firePreRenderEvent(PoseStack poseStack, BakedGeoModel model, MultiBufferSource bufferSource, float partialTick, int packedLight) {
+ return GeoRenderEntityEvent.Pre.EVENT.handle(
+ new GeoRenderEntityEvent.Pre(this, poseStack, model, bufferSource, partialTick, packedLight));
+ }
+
+ /**
+ * Create and fire the relevant {@code Post-Render} event hook for this renderer
+ */
+ @Override
+ public void firePostRenderEvent(PoseStack poseStack, BakedGeoModel model, MultiBufferSource bufferSource, float partialTick, int packedLight) {
+ GeoRenderEntityEvent.Post.EVENT.handle(
+ new GeoRenderEntityEvent.Post(this, poseStack, model, bufferSource, partialTick, packedLight));
+ }
+}
diff --git a/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/GeoItemRenderer.java b/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/GeoItemRenderer.java
new file mode 100644
index 0000000..21fd5ef
--- /dev/null
+++ b/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/GeoItemRenderer.java
@@ -0,0 +1,291 @@
+package mod.azure.azurelib.common.api.client.renderer;
+
+import java.util.List;
+
+import mod.azure.azurelib.common.api.common.event.GeoRenderItemEvent;
+import mod.azure.azurelib.common.api.client.model.GeoModel;
+import mod.azure.azurelib.common.api.common.animatable.GeoItem;
+import mod.azure.azurelib.common.internal.client.renderer.GeoRenderer;
+import mod.azure.azurelib.common.platform.Services;
+import net.minecraft.client.renderer.RenderBuffers;
+import org.Vrglab.AzureLib.Utility.Utils;
+import org.joml.Matrix4f;
+
+import com.mojang.blaze3d.platform.Lighting;
+import com.mojang.blaze3d.systems.RenderSystem;
+import com.mojang.blaze3d.vertex.PoseStack;
+import com.mojang.blaze3d.vertex.VertexConsumer;
+
+import mod.azure.azurelib.common.internal.common.cache.object.BakedGeoModel;
+import mod.azure.azurelib.common.internal.common.cache.object.GeoBone;
+import mod.azure.azurelib.common.internal.common.cache.texture.AnimatableTexture;
+import mod.azure.azurelib.common.internal.common.constant.DataTickets;
+import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable;
+import mod.azure.azurelib.common.internal.common.core.animation.AnimationState;
+import mod.azure.azurelib.common.api.client.renderer.layer.GeoRenderLayer;
+import mod.azure.azurelib.common.api.client.renderer.layer.GeoRenderLayersContainer;
+import mod.azure.azurelib.common.internal.client.util.RenderUtils;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.model.geom.EntityModelSet;
+import net.minecraft.client.renderer.BlockEntityWithoutLevelRenderer;
+import net.minecraft.client.renderer.MultiBufferSource;
+import net.minecraft.client.renderer.RenderType;
+import net.minecraft.client.renderer.blockentity.BlockEntityRenderDispatcher;
+import net.minecraft.client.renderer.entity.EntityRenderer;
+import net.minecraft.client.renderer.entity.ItemRenderer;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.item.Item;
+import net.minecraft.world.item.ItemDisplayContext;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.phys.Vec3;
+
+/**
+ * Base {@link GeoRenderer} class for rendering {@link Item Items} specifically.
+ * All items added to be rendered by AzureLib should use an instance of this class.
+ */
+public class GeoItemRenderer extends BlockEntityWithoutLevelRenderer implements GeoRenderer {
+ protected final GeoRenderLayersContainer renderLayers = new GeoRenderLayersContainer<>(this);
+ protected final GeoModel model;
+
+ protected ItemStack currentItemStack;
+ protected ItemDisplayContext renderPerspective;
+ protected T animatable;
+ protected float scaleWidth = 1;
+ protected float scaleHeight = 1;
+ protected boolean useEntityGuiLighting = false;
+
+ protected Matrix4f itemRenderTranslations = new Matrix4f();
+ protected Matrix4f modelRenderTranslations = new Matrix4f();
+
+ public GeoItemRenderer(GeoModel model) {
+ this(Minecraft.getInstance().getBlockEntityRenderDispatcher(), Minecraft.getInstance().getEntityModels(), model);
+ }
+
+ public GeoItemRenderer(BlockEntityRenderDispatcher dispatcher, EntityModelSet modelSet, GeoModel model) {
+ super(dispatcher, modelSet);
+
+ this.model = model;
+
+ }
+
+ /**
+ * Gets the model instance for this renderer
+ */
+ @Override
+ public GeoModel getGeoModel() {
+ return this.model;
+ }
+
+ /**
+ * Gets the {@link GeoAnimatable} instance currently being rendered
+ */
+ @Override
+ public T getAnimatable() {
+ return this.animatable;
+ }
+
+ /**
+ * Returns the current ItemStack being rendered
+ */
+ public ItemStack getCurrentItemStack() {
+ return this.currentItemStack;
+ }
+
+ /**
+ * Mark this renderer so that it uses an alternate lighting scheme when rendering the item in GUI
+ *
+ * This can help with improperly lit 3d models
+ */
+ public GeoItemRenderer useAlternateGuiLighting() {
+ this.useEntityGuiLighting = true;
+
+ return this;
+ }
+
+ /**
+ * Gets the id that represents the current animatable's instance for animation purposes. This is mostly useful for things like items, which have a single registered instance for all objects
+ */
+ @Override
+ public long getInstanceId(T animatable) {
+ return GeoItem.getId(this.currentItemStack);
+ }
+
+ /**
+ * Shadowing override of {@link EntityRenderer#getTextureLocation}.
+ * This redirects the call to {@link GeoRenderer#getTextureLocation}
+ */
+ @Override
+ public ResourceLocation getTextureLocation(T animatable) {
+ return GeoRenderer.super.getTextureLocation(animatable);
+ }
+
+ /**
+ * Returns the list of registered {@link GeoRenderLayer GeoRenderLayers} for this renderer
+ */
+ @Override
+ public List> getRenderLayers() {
+ return this.renderLayers.getRenderLayers();
+ }
+
+ /**
+ * Adds a {@link GeoRenderLayer} to this renderer, to be called after the main model is rendered each frame
+ */
+ public GeoItemRenderer addRenderLayer(GeoRenderLayer renderLayer) {
+ this.renderLayers.addLayer(renderLayer);
+
+ return this;
+ }
+
+ /**
+ * Sets a scale override for this renderer, telling AzureLib to pre-scale the model
+ */
+ public GeoItemRenderer withScale(float scale) {
+ return withScale(scale, scale);
+ }
+
+ /**
+ * Sets a scale override for this renderer, telling AzureLib to pre-scale the model
+ */
+ public GeoItemRenderer withScale(float scaleWidth, float scaleHeight) {
+ this.scaleWidth = scaleWidth;
+ this.scaleHeight = scaleHeight;
+
+ return this;
+ }
+
+ /**
+ * Called before rendering the model to buffer. Allows for render modifications and preparatory work such as scaling and translating.
+ * {@link PoseStack} translations made here are kept until the end of the render process
+ */
+ @Override
+ public void preRender(PoseStack poseStack, T animatable, BakedGeoModel model, MultiBufferSource bufferSource, VertexConsumer buffer, boolean isReRender, float partialTick, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) {
+ this.itemRenderTranslations = new Matrix4f(poseStack.last().pose());
+
+ scaleModelForRender(this.scaleWidth, this.scaleHeight, poseStack, animatable, model, isReRender, partialTick, packedLight, packedOverlay);
+
+ if (!isReRender)
+ poseStack.translate(0.5f, 0.51f, 0.5f);
+ }
+
+ @Override
+ public void renderByItem(ItemStack stack, ItemDisplayContext transformType, PoseStack poseStack, MultiBufferSource bufferSource, int packedLight, int packedOverlay) {
+ this.animatable = (T) stack.getItem();
+ this.currentItemStack = stack;
+ this.renderPerspective = transformType;
+
+ if (transformType == ItemDisplayContext.GUI) {
+ renderInGui(transformType, poseStack, bufferSource, packedLight, packedOverlay);
+ } else {
+ RenderType renderType = getRenderType(this.animatable, getTextureLocation(this.animatable), bufferSource, Minecraft.getInstance().getFrameTime());
+ VertexConsumer buffer = ItemRenderer.getFoilBufferDirect(bufferSource, renderType, false, this.currentItemStack != null && this.currentItemStack.hasFoil());
+
+ defaultRender(poseStack, this.animatable, bufferSource, renderType, buffer, 0, Minecraft.getInstance().getFrameTime(), packedLight);
+ }
+ }
+
+ /**
+ * Wrapper method to handle rendering the item in a GUI context (defined by {@link net.minecraft.world.item.ItemDisplayContext#GUI} normally).
+ * Just includes some additional required transformations and settings.
+ */
+ protected void renderInGui(ItemDisplayContext transformType, PoseStack poseStack, MultiBufferSource bufferSource, int packedLight, int packedOverlay) {
+ if (this.useEntityGuiLighting) {
+ Lighting.setupForEntityInInventory();
+ }
+ else {
+ Lighting.setupForFlatItems();
+ }
+
+
+
+ MultiBufferSource.BufferSource defaultBufferSource = bufferSource instanceof MultiBufferSource.BufferSource bufferSource2 ? bufferSource2 : ((RenderBuffers) Utils.getPrivateFinalStaticField(Minecraft.getInstance().levelRenderer, Minecraft.getInstance().levelRenderer.getClass(), "renderBuffers")).bufferSource();
+ RenderType renderType = getRenderType(this.animatable, getTextureLocation(this.animatable), defaultBufferSource, Minecraft.getInstance().getFrameTime());
+ VertexConsumer buffer = ItemRenderer.getFoilBufferDirect(bufferSource, renderType, true, this.currentItemStack != null && this.currentItemStack.hasFoil());
+ poseStack.pushPose();
+ defaultRender(poseStack, this.animatable, defaultBufferSource, renderType, buffer, 0, Minecraft.getInstance().getFrameTime(), packedLight);
+ defaultBufferSource.endBatch();
+ RenderSystem.enableDepthTest();
+ Lighting.setupFor3DItems();
+ poseStack.popPose();
+ }
+
+ /**
+ * The actual render method that subtype renderers should override to handle their specific rendering tasks.
+ * {@link GeoRenderer#preRender} has already been called by this stage, and {@link GeoRenderer#postRender} will be called directly after
+ */
+ @Override
+ public void actuallyRender(PoseStack poseStack, T animatable, BakedGeoModel model, RenderType renderType, MultiBufferSource bufferSource, VertexConsumer buffer, boolean isReRender, float partialTick, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) {
+
+ if (!isReRender) {
+ AnimationState animationState = new AnimationState<>(animatable, 0, 0, partialTick, false);
+ long instanceId = getInstanceId(animatable);
+
+ animationState.setData(DataTickets.TICK, animatable.getTick(this.currentItemStack));
+ animationState.setData(DataTickets.ITEM_RENDER_PERSPECTIVE, this.renderPerspective);
+ animationState.setData(DataTickets.ITEMSTACK, this.currentItemStack);
+ animatable.getAnimatableInstanceCache().getManagerForId(instanceId).setData(DataTickets.ITEM_RENDER_PERSPECTIVE, this.renderPerspective);
+ this.model.addAdditionalStateData(animatable, instanceId, animationState::setData);
+ this.model.handleAnimations(animatable, instanceId, animationState);
+ }
+
+ this.modelRenderTranslations = new Matrix4f(poseStack.last().pose());
+
+ GeoRenderer.super.actuallyRender(poseStack, animatable, model, renderType, bufferSource, buffer, isReRender, partialTick, packedLight, packedOverlay, red, green, blue, alpha);
+ }
+
+ /**
+ * Renders the provided {@link GeoBone} and its associated child bones
+ */
+ @Override
+ public void renderRecursively(PoseStack poseStack, T animatable, GeoBone bone, RenderType renderType, MultiBufferSource bufferSource, VertexConsumer buffer, boolean isReRender, float partialTick, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) {
+ if (bone.isTrackingMatrices()) {
+ Matrix4f poseState = new Matrix4f(poseStack.last().pose());
+ Matrix4f localMatrix = RenderUtils.invertAndMultiplyMatrices(poseState, this.itemRenderTranslations);
+
+ bone.setModelSpaceMatrix(RenderUtils.invertAndMultiplyMatrices(poseState, this.modelRenderTranslations));
+ bone.setLocalSpaceMatrix(RenderUtils.translateMatrix(localMatrix, getRenderOffset(this.animatable, 1).toVector3f()));
+ }
+
+ GeoRenderer.super.renderRecursively(poseStack, animatable, bone, renderType, bufferSource, buffer, isReRender, partialTick, packedLight, packedOverlay, red, green, blue, alpha);
+ }
+
+ public Vec3 getRenderOffset(Item entity, float f) {
+ return Vec3.ZERO;
+ }
+
+ /**
+ * Update the current frame of a {@link AnimatableTexture potentially animated} texture used by this GeoRenderer.
+ * This should only be called immediately prior to rendering, and only
+ *
+ * @see AnimatableTexture#setAndUpdate(ResourceLocation, int)
+ */
+ @Override
+ public void updateAnimatedTextureFrame(T animatable) {
+ AnimatableTexture.setAndUpdate(getTextureLocation(animatable), Item.getId(animatable) + (int) animatable.getTick(animatable));
+ }
+
+ /**
+ * Create and fire the relevant {@code CompileLayers} event hook for this renderer
+ */
+ @Override
+ public void fireCompileRenderLayersEvent() {
+ GeoRenderItemEvent.CompileRenderLayers.EVENT.handle(new GeoRenderItemEvent.CompileRenderLayers(this));
+ }
+
+ /**
+ * Create and fire the relevant {@code Pre-Render} event hook for this renderer.
+ *
+ * @return Whether the renderer should proceed based on the cancellation state of the event
+ */
+ @Override
+ public boolean firePreRenderEvent(PoseStack poseStack, BakedGeoModel model, MultiBufferSource bufferSource, float partialTick, int packedLight) {
+ return GeoRenderItemEvent.Pre.EVENT.handle(new GeoRenderItemEvent.Pre(this, poseStack, model, bufferSource, partialTick, packedLight));
+ }
+
+ /**
+ * Create and fire the relevant {@code Post-Render} event hook for this renderer
+ */
+ @Override
+ public void firePostRenderEvent(PoseStack poseStack, BakedGeoModel model, MultiBufferSource bufferSource, float partialTick, int packedLight) {
+ GeoRenderItemEvent.Post.EVENT.handle(new GeoRenderItemEvent.Post(this, poseStack, model, bufferSource, partialTick, packedLight));
+ }
+}
diff --git a/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/GeoObjectRenderer.java b/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/GeoObjectRenderer.java
new file mode 100644
index 0000000..a6e768b
--- /dev/null
+++ b/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/GeoObjectRenderer.java
@@ -0,0 +1,225 @@
+package mod.azure.azurelib.common.api.client.renderer;
+
+import java.util.List;
+
+import mod.azure.azurelib.common.api.common.event.GeoRenderObjectEvent;
+import mod.azure.azurelib.common.api.client.model.GeoModel;
+import mod.azure.azurelib.common.internal.client.renderer.GeoRenderer;
+import mod.azure.azurelib.common.platform.Services;
+import net.minecraft.client.renderer.RenderBuffers;
+import org.Vrglab.AzureLib.Utility.Utils;
+import org.jetbrains.annotations.Nullable;
+import org.joml.Matrix4f;
+
+import com.mojang.blaze3d.vertex.PoseStack;
+import com.mojang.blaze3d.vertex.VertexConsumer;
+
+import mod.azure.azurelib.common.internal.common.cache.object.BakedGeoModel;
+import mod.azure.azurelib.common.internal.common.cache.object.GeoBone;
+import mod.azure.azurelib.common.internal.common.cache.texture.AnimatableTexture;
+import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable;
+import mod.azure.azurelib.common.internal.common.core.animation.AnimationState;
+import mod.azure.azurelib.common.api.client.renderer.layer.GeoRenderLayer;
+import mod.azure.azurelib.common.api.client.renderer.layer.GeoRenderLayersContainer;
+import mod.azure.azurelib.common.internal.client.util.RenderUtils;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.MultiBufferSource;
+import net.minecraft.client.renderer.RenderType;
+import net.minecraft.client.renderer.entity.EntityRenderer;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.phys.Vec3;
+
+/**
+ * Base {@link GeoRenderer} class for rendering anything that isn't already handled by the other builtin GeoRenderer subclasses.
+ * Before using this class you should ensure your use-case isn't already covered by one of the other existing renderers.
+ *
+ * It is strongly recommended you override {@link GeoRenderer#getInstanceId} if using this renderer
+ */
+public class GeoObjectRenderer implements GeoRenderer {
+ protected final GeoRenderLayersContainer renderLayers = new GeoRenderLayersContainer<>(this);
+ protected final GeoModel model;
+
+ protected T animatable;
+ protected float scaleWidth = 1;
+ protected float scaleHeight = 1;
+
+ protected Matrix4f objectRenderTranslations = new Matrix4f();
+ protected Matrix4f modelRenderTranslations = new Matrix4f();
+
+ public GeoObjectRenderer(GeoModel model) {
+ this.model = model;
+
+ }
+
+ /**
+ * Gets the model instance for this renderer
+ */
+ @Override
+ public GeoModel getGeoModel() {
+ return this.model;
+ }
+
+ /**
+ * Gets the {@link GeoAnimatable} instance currently being rendered
+ */
+ @Override
+ public T getAnimatable() {
+ return this.animatable;
+ }
+
+ /**
+ * Shadowing override of {@link EntityRenderer#getTextureLocation}.
+ * This redirects the call to {@link GeoRenderer#getTextureLocation}
+ */
+ @Override
+ public ResourceLocation getTextureLocation(T animatable) {
+ return GeoRenderer.super.getTextureLocation(animatable);
+ }
+
+ /**
+ * Returns the list of registered {@link GeoRenderLayer GeoRenderLayers} for this renderer
+ */
+ @Override
+ public List> getRenderLayers() {
+ return this.renderLayers.getRenderLayers();
+ }
+
+ /**
+ * Adds a {@link GeoRenderLayer} to this renderer, to be called after the main model is rendered each frame
+ */
+ public GeoObjectRenderer addRenderLayer(GeoRenderLayer renderLayer) {
+ this.renderLayers.addLayer(renderLayer);
+
+ return this;
+ }
+
+ /**
+ * Sets a scale override for this renderer, telling AzureLib to pre-scale the model
+ */
+ public GeoObjectRenderer withScale(float scale) {
+ return withScale(scale, scale);
+ }
+
+ /**
+ * Sets a scale override for this renderer, telling AzureLib to pre-scale the model
+ */
+ public GeoObjectRenderer withScale(float scaleWidth, float scaleHeight) {
+ this.scaleWidth = scaleWidth;
+ this.scaleHeight = scaleHeight;
+
+ return this;
+ }
+
+ /**
+ * The entry render point for this renderer.
+ * Call this whenever you want to render your object
+ *
+ * @param poseStack The PoseStack to render under
+ * @param animatable The {@link T} instance to render
+ * @param bufferSource The BufferSource to render with, or null to use the default
+ * @param renderType The specific RenderType to use, or null to fall back to {@link GeoRenderer#getRenderType}
+ * @param buffer The VertexConsumer to use for rendering, or null to use the default for the RenderType
+ * @param packedLight The light level at the given render position for rendering
+ */
+ public void render(PoseStack poseStack, T animatable, @Nullable MultiBufferSource bufferSource, @Nullable RenderType renderType, @Nullable VertexConsumer buffer, int packedLight) {
+ this.animatable = animatable;
+ Minecraft mc = Minecraft.getInstance();
+
+ if (buffer == null)
+ bufferSource = ((RenderBuffers) Utils.getPrivateFinalStaticField(Minecraft.getInstance().levelRenderer, Minecraft.getInstance().levelRenderer.getClass(), "renderBuffers")).bufferSource();
+
+ defaultRender(poseStack, animatable, bufferSource, renderType, buffer, 0, mc.getFrameTime(), packedLight);
+ }
+
+ /**
+ * Called before rendering the model to buffer. Allows for render modifications and preparatory work such as scaling and translating.
+ * {@link PoseStack} translations made here are kept until the end of the render process
+ */
+ @Override
+ public void preRender(PoseStack poseStack, T animatable, BakedGeoModel model, MultiBufferSource bufferSource, VertexConsumer buffer, boolean isReRender, float partialTick, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) {
+ this.objectRenderTranslations = new Matrix4f(poseStack.last().pose());
+
+ scaleModelForRender(this.scaleWidth, this.scaleHeight, poseStack, animatable, model, isReRender, partialTick, packedLight, packedOverlay);
+
+ poseStack.translate(0.5f, 0.51f, 0.5f);
+ }
+
+ /**
+ * The actual render method that subtype renderers should override to handle their specific rendering tasks.
+ * {@link GeoRenderer#preRender} has already been called by this stage, and {@link GeoRenderer#postRender} will be called directly after
+ */
+ @Override
+ public void actuallyRender(PoseStack poseStack, T animatable, BakedGeoModel model, RenderType renderType, MultiBufferSource bufferSource, VertexConsumer buffer, boolean isReRender, float partialTick, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) {
+ poseStack.pushPose();
+
+ if (!isReRender) {
+ AnimationState animationState = new AnimationState<>(animatable, 0, 0, partialTick, false);
+ long instanceId = getInstanceId(animatable);
+
+ this.model.addAdditionalStateData(animatable, instanceId, animationState::setData);
+ this.model.handleAnimations(animatable, instanceId, animationState);
+ }
+
+ this.modelRenderTranslations = new Matrix4f(poseStack.last().pose());
+
+ GeoRenderer.super.actuallyRender(poseStack, animatable, model, renderType, bufferSource, buffer, isReRender, partialTick, packedLight, packedOverlay, red, green, blue, alpha);
+ poseStack.popPose();
+ }
+
+ /**
+ * Renders the provided {@link GeoBone} and its associated child bones
+ */
+ @Override
+ public void renderRecursively(PoseStack poseStack, T animatable, GeoBone bone, RenderType renderType, MultiBufferSource bufferSource, VertexConsumer buffer, boolean isReRender, float partialTick, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) {
+ if (bone.isTrackingMatrices()) {
+ Matrix4f poseState = new Matrix4f(poseStack.last().pose());
+ Matrix4f localMatrix = RenderUtils.invertAndMultiplyMatrices(poseState, this.objectRenderTranslations);
+
+ bone.setModelSpaceMatrix(RenderUtils.invertAndMultiplyMatrices(poseState, this.modelRenderTranslations));
+ bone.setLocalSpaceMatrix(RenderUtils.translateMatrix(localMatrix, getRenderOffset(this.animatable, 1).toVector3f()));
+ }
+
+ GeoRenderer.super.renderRecursively(poseStack, animatable, bone, renderType, bufferSource, buffer, isReRender, partialTick, packedLight, packedOverlay, red, green, blue, alpha);
+ }
+
+ public Vec3 getRenderOffset(T entity, float f) {
+ return Vec3.ZERO;
+ }
+
+ /**
+ * Update the current frame of a {@link AnimatableTexture potentially animated} texture used by this GeoRenderer.
+ * This should only be called immediately prior to rendering, and only
+ *
+ * @see AnimatableTexture#setAndUpdate(ResourceLocation, int)
+ */
+ @Override
+ public void updateAnimatedTextureFrame(T animatable) {
+ AnimatableTexture.setAndUpdate(getTextureLocation(animatable), (int) animatable.getTick(animatable));
+ }
+
+ /**
+ * Create and fire the relevant {@code CompileLayers} event hook for this renderer
+ */
+ @Override
+ public void fireCompileRenderLayersEvent() {
+ GeoRenderObjectEvent.CompileRenderLayers.EVENT.handle(new GeoRenderObjectEvent.CompileRenderLayers(this));
+ }
+
+ /**
+ * Create and fire the relevant {@code Pre-Render} event hook for this renderer.
+ *
+ * @return Whether the renderer should proceed based on the cancellation state of the event
+ */
+ @Override
+ public boolean firePreRenderEvent(PoseStack poseStack, BakedGeoModel model, MultiBufferSource bufferSource, float partialTick, int packedLight) {
+ return GeoRenderObjectEvent.Pre.EVENT.handle(new GeoRenderObjectEvent.Pre(this, poseStack, model, bufferSource, partialTick, packedLight));
+ }
+
+ /**
+ * Create and fire the relevant {@code Post-Render} event hook for this renderer
+ */
+ @Override
+ public void firePostRenderEvent(PoseStack poseStack, BakedGeoModel model, MultiBufferSource bufferSource, float partialTick, int packedLight) {
+ GeoRenderObjectEvent.Post.EVENT.handle(new GeoRenderObjectEvent.Post(this, poseStack, model, bufferSource, partialTick, packedLight));
+ }
+}
diff --git a/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/GeoReplacedEntityRenderer.java b/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/GeoReplacedEntityRenderer.java
new file mode 100644
index 0000000..739d432
--- /dev/null
+++ b/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/GeoReplacedEntityRenderer.java
@@ -0,0 +1,527 @@
+package mod.azure.azurelib.common.api.client.renderer;
+
+import com.mojang.blaze3d.systems.RenderSystem;
+import com.mojang.blaze3d.vertex.BufferBuilder;
+import com.mojang.blaze3d.vertex.PoseStack;
+import com.mojang.blaze3d.vertex.VertexConsumer;
+import com.mojang.math.Axis;
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import mod.azure.azurelib.common.api.client.model.GeoModel;
+import mod.azure.azurelib.common.internal.client.renderer.GeoRenderer;
+import mod.azure.azurelib.common.internal.common.cache.object.BakedGeoModel;
+import mod.azure.azurelib.common.internal.common.cache.object.GeoBone;
+import mod.azure.azurelib.common.internal.common.cache.texture.AnimatableTexture;
+import mod.azure.azurelib.common.internal.common.constant.DataTickets;
+import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable;
+import mod.azure.azurelib.common.internal.common.core.animation.AnimationState;
+import mod.azure.azurelib.common.api.common.event.GeoRenderReplacedEntityEvent;
+import mod.azure.azurelib.common.internal.client.model.data.EntityModelData;
+import mod.azure.azurelib.common.platform.Services;
+import mod.azure.azurelib.common.api.client.renderer.layer.GeoRenderLayer;
+import mod.azure.azurelib.common.internal.client.util.RenderUtils;
+import net.minecraft.ChatFormatting;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.LightTexture;
+import net.minecraft.client.renderer.MultiBufferSource;
+import net.minecraft.client.renderer.RenderType;
+import net.minecraft.client.renderer.entity.EntityRenderer;
+import net.minecraft.client.renderer.entity.EntityRendererProvider;
+import net.minecraft.client.renderer.entity.LivingEntityRenderer;
+import net.minecraft.client.renderer.texture.OverlayTexture;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.Direction;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.util.Mth;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.entity.Mob;
+import net.minecraft.world.entity.Pose;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.world.entity.player.PlayerModelPart;
+import net.minecraft.world.level.LightLayer;
+import net.minecraft.world.phys.Vec3;
+import org.Vrglab.AzureLib.Utility.Utils;
+import org.joml.Matrix4f;
+
+import java.util.List;
+
+/**
+ * An alternate to {@link GeoEntityRenderer}, used specifically for replacing existing non-AzureLib entities with AzureLib rendering dynamically, without the need for an additional entity class
+ */
+public class GeoReplacedEntityRenderer extends EntityRenderer implements GeoRenderer {
+ protected final GeoModel model;
+ protected final List> renderLayers = new ObjectArrayList<>();
+ protected final T animatable;
+
+ protected E currentEntity;
+ protected float scaleWidth = 1;
+ protected float scaleHeight = 1;
+
+ protected Matrix4f entityRenderTranslations = new Matrix4f();
+ protected Matrix4f modelRenderTranslations = new Matrix4f();
+
+ public GeoReplacedEntityRenderer(EntityRendererProvider.Context renderManager, GeoModel model, T animatable) {
+ super(renderManager);
+
+ this.model = model;
+ this.animatable = animatable;
+ }
+
+ /**
+ * Gets the model instance for this renderer
+ */
+ @Override
+ public GeoModel getGeoModel() {
+ return this.model;
+ }
+
+ /**
+ * Gets the {@link GeoAnimatable} instance currently being rendered
+ *
+ * @see GeoReplacedEntityRenderer#getCurrentEntity()
+ */
+ @Override
+ public T getAnimatable() {
+ return this.animatable;
+ }
+
+ /**
+ * Returns the current entity having its rendering replaced by this renderer
+ *
+ * @see GeoReplacedEntityRenderer#getAnimatable()
+ */
+ public E getCurrentEntity() {
+ return this.currentEntity;
+ }
+
+ /**
+ * Gets the id that represents the current animatable's instance for animation purposes. This is mostly useful for things like items, which have a single registered instance for all objects
+ */
+ @Override
+ public long getInstanceId(T animatable) {
+ return this.currentEntity.getId();
+ }
+
+ /**
+ * Shadowing override of {@link EntityRenderer#getTextureLocation}.
+ * This redirects the call to {@link GeoRenderer#getTextureLocation}
+ */
+ @Override
+ public ResourceLocation getTextureLocation(E entity) {
+ return GeoRenderer.super.getTextureLocation(this.animatable);
+ }
+
+ /**
+ * Returns the list of registered {@link GeoRenderLayer GeoRenderLayers} for this renderer
+ */
+ @Override
+ public List> getRenderLayers() {
+ return this.renderLayers;
+ }
+
+ /**
+ * Adds a {@link GeoRenderLayer} to this renderer, to be called after the main model is rendered each frame
+ */
+ public GeoReplacedEntityRenderer addRenderLayer(GeoRenderLayer renderLayer) {
+ this.renderLayers.add(renderLayer);
+
+ return this;
+ }
+
+ /**
+ * Sets a scale override for this renderer, telling AzureLib to pre-scale the model
+ */
+ public GeoReplacedEntityRenderer withScale(float scale) {
+ return withScale(scale, scale);
+ }
+
+ /**
+ * Sets a scale override for this renderer, telling AzureLib to pre-scale the model
+ */
+ public GeoReplacedEntityRenderer withScale(float scaleWidth, float scaleHeight) {
+ this.scaleWidth = scaleWidth;
+ this.scaleHeight = scaleHeight;
+
+ return this;
+ }
+
+ /**
+ * Called before rendering the model to buffer. Allows for render modifications and preparatory work such as scaling and translating.
+ * {@link PoseStack} translations made here are kept until the end of the render process
+ */
+ @Override
+ public void preRender(PoseStack poseStack, T animatable, BakedGeoModel model, MultiBufferSource bufferSource, VertexConsumer buffer, boolean isReRender, float partialTick, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) {
+ this.entityRenderTranslations = new Matrix4f(poseStack.last().pose());
+
+ scaleModelForRender(this.scaleWidth, this.scaleHeight, poseStack, animatable, model, isReRender, partialTick, packedLight, packedOverlay);
+ }
+
+ @Override
+ public void render(E entity, float entityYaw, float partialTick, PoseStack poseStack, MultiBufferSource bufferSource, int packedLight) {
+ this.currentEntity = entity;
+
+ defaultRender(poseStack, this.animatable, bufferSource, null, null, entityYaw, partialTick, packedLight);
+ }
+
+ /**
+ * The actual render method that subtype renderers should override to handle their specific rendering tasks.
+ * {@link GeoRenderer#preRender} has already been called by this stage, and {@link GeoRenderer#postRender} will be called directly after
+ */
+ @Override
+ public void actuallyRender(PoseStack poseStack, T animatable, BakedGeoModel model, RenderType renderType, MultiBufferSource bufferSource, VertexConsumer buffer, boolean isReRender, float partialTick, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) {
+ poseStack.pushPose();
+
+ LivingEntity livingEntity = this.currentEntity instanceof LivingEntity entity ? entity : null;
+
+ boolean shouldSit = this.currentEntity.isPassenger() && (this.currentEntity.getVehicle() != null);
+ float lerpBodyRot = livingEntity == null ? 0 : Mth.rotLerp(partialTick, livingEntity.yBodyRotO, livingEntity.yBodyRot);
+ float lerpHeadRot = livingEntity == null ? 0 : Mth.rotLerp(partialTick, livingEntity.yHeadRotO, livingEntity.yHeadRot);
+ float netHeadYaw = lerpHeadRot - lerpBodyRot;
+
+ if (shouldSit && this.currentEntity.getVehicle()instanceof LivingEntity livingentity) {
+ lerpBodyRot = Mth.rotLerp(partialTick, livingentity.yBodyRotO, livingentity.yBodyRot);
+ netHeadYaw = lerpHeadRot - lerpBodyRot;
+ float clampedHeadYaw = Mth.clamp(Mth.wrapDegrees(netHeadYaw), -85, 85);
+ lerpBodyRot = lerpHeadRot - clampedHeadYaw;
+
+ if (clampedHeadYaw * clampedHeadYaw > 2500f)
+ lerpBodyRot += clampedHeadYaw * 0.2f;
+
+ netHeadYaw = lerpHeadRot - lerpBodyRot;
+ }
+
+ if (this.currentEntity.getPose() == Pose.SLEEPING && livingEntity != null) {
+ Direction bedDirection = livingEntity.getBedOrientation();
+
+ if (bedDirection != null) {
+ float eyePosOffset = livingEntity.getEyeHeight(Pose.STANDING) - 0.1F;
+
+ poseStack.translate(-bedDirection.getStepX() * eyePosOffset, 0, -bedDirection.getStepZ() * eyePosOffset);
+ }
+ }
+
+ float nativeScale = livingEntity != null ? livingEntity.getScale() : 1;
+ float ageInTicks = this.currentEntity.tickCount + partialTick;
+ float limbSwingAmount = 0;
+ float limbSwing = 0;
+
+ poseStack.scale(nativeScale, nativeScale, nativeScale);
+ applyRotations(animatable, poseStack, ageInTicks, lerpBodyRot, partialTick, nativeScale);
+
+ if (!shouldSit && this.currentEntity.isAlive() && livingEntity != null) {
+ limbSwingAmount = Mth.lerp(partialTick, Utils.getPrivateFinalStaticField(livingEntity.walkAnimation, livingEntity.walkAnimation.getClass(), "speedOld"), livingEntity.walkAnimation.speed());
+ limbSwing = livingEntity.walkAnimation.position() - livingEntity.walkAnimation.speed() * (1 - partialTick);
+
+ if (livingEntity.isBaby())
+ limbSwing *= 3f;
+
+ if (limbSwingAmount > 1f)
+ limbSwingAmount = 1f;
+ }
+
+ float headPitch = Mth.lerp(partialTick, this.currentEntity.xRotO, this.currentEntity.getXRot());
+ float motionThreshold = getMotionAnimThreshold(animatable);
+ boolean isMoving;
+
+ if (livingEntity != null) {
+ Vec3 velocity = livingEntity.getDeltaMovement();
+ float avgVelocity = (float) (Math.abs(velocity.x) + Math.abs(velocity.z)) / 2f;
+
+ isMoving = avgVelocity >= motionThreshold && limbSwingAmount != 0;
+ } else {
+ isMoving = (limbSwingAmount <= -motionThreshold || limbSwingAmount >= motionThreshold);
+ }
+
+ if (!isReRender) {
+ AnimationState animationState = new AnimationState(animatable, limbSwing, limbSwingAmount, partialTick, isMoving);
+ long instanceId = getInstanceId(animatable);
+
+ animationState.setData(DataTickets.TICK, animatable.getTick(this.currentEntity));
+ animationState.setData(DataTickets.ENTITY, this.currentEntity);
+ animationState.setData(DataTickets.ENTITY_MODEL_DATA, new EntityModelData(shouldSit, livingEntity != null && livingEntity.isBaby(), -netHeadYaw, -headPitch));
+ this.model.addAdditionalStateData(animatable, instanceId, animationState::setData);
+ this.model.handleAnimations(animatable, instanceId, animationState);
+ }
+
+ poseStack.translate(0, 0.01f, 0);
+ RenderSystem.setShaderTexture(0, getTextureLocation(animatable));
+
+ this.modelRenderTranslations = new Matrix4f(poseStack.last().pose());
+
+ if (!this.currentEntity.isInvisibleTo(Minecraft.getInstance().player))
+ GeoRenderer.super.actuallyRender(poseStack, animatable, model, renderType, bufferSource, buffer, isReRender, partialTick, packedLight, packedOverlay, red, green, blue, alpha);
+
+ poseStack.popPose();
+ }
+
+ /**
+ * Render the various {@link GeoRenderLayer RenderLayers} that have been registered to this renderer
+ */
+ @Override
+ public void applyRenderLayers(PoseStack poseStack, T animatable, BakedGeoModel model, RenderType renderType, MultiBufferSource bufferSource, VertexConsumer buffer, float partialTick, int packedLight, int packedOverlay) {
+ if (!this.currentEntity.isSpectator())
+ GeoRenderer.super.applyRenderLayers(poseStack, animatable, model, renderType, bufferSource, buffer, partialTick, packedLight, packedOverlay);
+ }
+
+ /**
+ * Call after all other rendering work has taken place, including reverting the {@link PoseStack}'s state. This method is not called in {@link GeoRenderer#reRender re-render}
+ */
+ @Override
+ public void renderFinal(PoseStack poseStack, T animatable, BakedGeoModel model, MultiBufferSource bufferSource, VertexConsumer buffer, float partialTick, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) {
+ super.render(this.currentEntity, 0, partialTick, poseStack, bufferSource, packedLight);
+
+ if (this.currentEntity instanceof Mob mob) {
+ Entity leashHolder = mob.getLeashHolder();
+
+ if (leashHolder != null)
+ renderLeash(mob, partialTick, poseStack, bufferSource, leashHolder);
+ }
+ }
+
+ /**
+ * Renders the provided {@link GeoBone} and its associated child bones
+ */
+ @Override
+ public void renderRecursively(PoseStack poseStack, T animatable, GeoBone bone, RenderType renderType, MultiBufferSource bufferSource, VertexConsumer buffer, boolean isReRender, float partialTick, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) {
+ poseStack.pushPose();
+ RenderUtils.translateMatrixToBone(poseStack, bone);
+ RenderUtils.translateToPivotPoint(poseStack, bone);
+ RenderUtils.rotateMatrixAroundBone(poseStack, bone);
+ RenderUtils.scaleMatrixForBone(poseStack, bone);
+
+ if (bone.isTrackingMatrices()) {
+ Matrix4f poseState = new Matrix4f(poseStack.last().pose());
+ Matrix4f localMatrix = RenderUtils.invertAndMultiplyMatrices(poseState, this.entityRenderTranslations);
+
+ bone.setModelSpaceMatrix(RenderUtils.invertAndMultiplyMatrices(poseState, this.modelRenderTranslations));
+ bone.setLocalSpaceMatrix(RenderUtils.translateMatrix(localMatrix, getRenderOffset(this.currentEntity, 1).toVector3f()));
+ bone.setWorldSpaceMatrix(RenderUtils.translateMatrix(new Matrix4f(localMatrix), this.currentEntity.position().toVector3f()));
+ }
+
+ RenderUtils.translateAwayFromPivotPoint(poseStack, bone);
+
+ renderCubesOfBone(poseStack, bone, buffer, packedLight, packedOverlay, red, green, blue, alpha);
+
+ if (!isReRender) {
+ applyRenderLayersForBone(poseStack, animatable, bone, renderType, bufferSource, buffer, partialTick, packedLight, packedOverlay);
+ if (buffer instanceof BufferBuilder builder && !((boolean)Utils.getPrivateFinalStaticField(builder, builder.getClass(), "building")))
+ buffer = bufferSource.getBuffer(renderType);
+ }
+
+ renderChildBones(poseStack, animatable, bone, renderType, bufferSource, buffer, isReRender, partialTick, packedLight, packedOverlay, red, green, blue, alpha);
+
+ poseStack.popPose();
+ }
+
+ /**
+ * Applies rotation transformations to the renderer prior to render time to account for various entity states, default scale of 1
+ */
+ protected void applyRotations(T animatable, PoseStack poseStack, float ageInTicks, float rotationYaw, float partialTick) {
+ applyRotations(animatable, poseStack, ageInTicks, rotationYaw, partialTick, 1);
+ }
+
+ /**
+ * Applies rotation transformations to the renderer prior to render time to account for various entity states, scalable
+ */
+ protected void applyRotations(T animatable, PoseStack poseStack, float ageInTicks, float rotationYaw,
+ float partialTick, float nativeScale) {
+ if (isShaking(animatable))
+ rotationYaw += (float)(Math.cos(this.currentEntity.tickCount * 3.25d) * Math.PI * 0.4d);
+
+ if (!this.currentEntity.hasPose(Pose.SLEEPING))
+ poseStack.mulPose(Axis.YP.rotationDegrees(180f - rotationYaw));
+
+ if (this.currentEntity instanceof LivingEntity livingEntity) {
+ if (livingEntity.deathTime > 0) {
+ float deathRotation = (livingEntity.deathTime + partialTick - 1f) / 20f * 1.6f;
+
+ poseStack.mulPose(Axis.ZP.rotationDegrees(Math.min(Mth.sqrt(deathRotation), 1) * getDeathMaxRotation(animatable)));
+ }
+ else if (livingEntity.isAutoSpinAttack()) {
+ poseStack.mulPose(Axis.XP.rotationDegrees(-90f - livingEntity.getXRot()));
+ poseStack.mulPose(Axis.YP.rotationDegrees((livingEntity.tickCount + partialTick) * -75f));
+ }
+ else if (livingEntity.hasPose(Pose.SLEEPING)) {
+ Direction bedOrientation = livingEntity.getBedOrientation();
+
+ poseStack.mulPose(Axis.YP.rotationDegrees(bedOrientation != null ? RenderUtils.getDirectionAngle(bedOrientation) : rotationYaw));
+ poseStack.mulPose(Axis.ZP.rotationDegrees(getDeathMaxRotation(animatable)));
+ poseStack.mulPose(Axis.YP.rotationDegrees(270f));
+ }
+ else if (LivingEntityRenderer.isEntityUpsideDown(livingEntity)) {
+ poseStack.translate(0, (livingEntity.getBbHeight() + 0.1f) / nativeScale, 0);
+ poseStack.mulPose(Axis.ZP.rotationDegrees(180f));
+ }
+ }
+ }
+
+ /**
+ * Gets the max rotation value for dying entities.
+ * You might want to modify this for different aesthetics, such as a {@link net.minecraft.world.entity.monster.Spider} flipping upside down on death.
+ * Functionally equivalent to {@link net.minecraft.client.renderer.entity.LivingEntityRenderer#getFlipDegrees}
+ */
+ protected float getDeathMaxRotation(T animatable) {
+ return 90f;
+ }
+
+ /**
+ * Whether the entity's nametag should be rendered or not.
+ * Pretty much exclusively used in {@link EntityRenderer#renderNameTag}
+ */
+ @Override
+ public boolean shouldShowName(E entity) {
+ if (!(entity instanceof LivingEntity))
+ return super.shouldShowName(entity);
+
+ var nameRenderCutoff = entity.isDiscrete() ? 32d : 64d;
+
+ if (this.entityRenderDispatcher.distanceToSqr(entity) >= nameRenderCutoff * nameRenderCutoff)
+ return false;
+
+ if (entity instanceof Mob && (!entity.shouldShowName() && (!entity.hasCustomName() || entity != this.entityRenderDispatcher.crosshairPickEntity)))
+ return false;
+
+ final var minecraft = Minecraft.getInstance();
+ var visibleToClient = !entity.isInvisibleTo(minecraft.player);
+ var entityTeam = entity.getTeam();
+
+ if (entityTeam == null)
+ return Minecraft.renderNames() && entity != minecraft.getCameraEntity() && visibleToClient && !entity.isVehicle();
+
+ var playerTeam = minecraft.player.getTeam();
+
+ return switch (entityTeam.getNameTagVisibility()) {
+ case ALWAYS -> visibleToClient;
+ case NEVER -> false;
+ case HIDE_FOR_OTHER_TEAMS -> playerTeam == null ? visibleToClient : entityTeam.isAlliedTo(playerTeam) && (entityTeam.canSeeFriendlyInvisibles() || visibleToClient);
+ case HIDE_FOR_OWN_TEAM -> playerTeam == null ? visibleToClient : !entityTeam.isAlliedTo(playerTeam) && visibleToClient;
+ };
+ }
+
+ /**
+ * Gets a packed overlay coordinate pair for rendering.
+ * Mostly just used for the red tint when an entity is hurt, but can be used for other things like the {@link net.minecraft.world.entity.monster.Creeper} white tint when exploding.
+ */
+ @Override
+ public int getPackedOverlay(T animatable, float u) {
+ if (!(this.currentEntity instanceof LivingEntity entity))
+ return OverlayTexture.NO_OVERLAY;
+
+ return OverlayTexture.pack(OverlayTexture.u(u), OverlayTexture.v(entity.hurtTime > 0 || entity.deathTime > 0));
+ }
+
+ /**
+ * Gets a packed overlay coordinate pair for rendering.
+ * Mostly just used for the red tint when an entity is hurt,
+ * but can be used for other things like the {@link net.minecraft.world.entity.monster.Creeper}
+ * white tint when exploding.
+ */
+ @Override
+ public int getPackedOverlay(T animatable, float u, float partialTick) {
+ return getPackedOverlay(animatable, u);
+ }
+
+ /**
+ * Static rendering code for rendering a leash segment.
+ * It's a like-for-like from {@link net.minecraft.client.renderer.entity.MobRenderer#renderLeash} that had to be duplicated here for flexible usage
+ */
+ public void renderLeash(M mob, float partialTick, PoseStack poseStack, MultiBufferSource bufferSource, H leashHolder) {
+ double lerpBodyAngle = (Mth.lerp(partialTick, mob.yBodyRotO, mob.yBodyRot) * Mth.DEG_TO_RAD) + Mth.HALF_PI;
+ Vec3 leashOffset = Utils.callPrivateMethod(mob, "getLeashOffset", new Class[0]);
+ double xAngleOffset = Math.cos(lerpBodyAngle) * leashOffset.z + Math.sin(lerpBodyAngle) * leashOffset.x;
+ double zAngleOffset = Math.sin(lerpBodyAngle) * leashOffset.z - Math.cos(lerpBodyAngle) * leashOffset.x;
+ double lerpOriginX = Mth.lerp(partialTick, mob.xo, mob.getX()) + xAngleOffset;
+ double lerpOriginY = Mth.lerp(partialTick, mob.yo, mob.getY()) + leashOffset.y;
+ double lerpOriginZ = Mth.lerp(partialTick, mob.zo, mob.getZ()) + zAngleOffset;
+ Vec3 ropeGripPosition = leashHolder.getRopeHoldPosition(partialTick);
+ float xDif = (float) (ropeGripPosition.x - lerpOriginX);
+ float yDif = (float) (ropeGripPosition.y - lerpOriginY);
+ float zDif = (float) (ropeGripPosition.z - lerpOriginZ);
+ float offsetMod = (float) (Mth.fastInvSqrt(xDif * xDif + zDif * zDif) * 0.025f / 2f);
+ float xOffset = zDif * offsetMod;
+ float zOffset = xDif * offsetMod;
+ VertexConsumer vertexConsumer = bufferSource.getBuffer(RenderType.leash());
+ BlockPos entityEyePos = BlockPos.containing(mob.getEyePosition(partialTick));
+ BlockPos holderEyePos = BlockPos.containing(leashHolder.getEyePosition(partialTick));
+ int entityBlockLight = getBlockLightLevel((E) mob, entityEyePos);
+ int holderBlockLight = leashHolder.isOnFire() ? 15 : leashHolder.level().getBrightness(LightLayer.BLOCK, holderEyePos);
+ int entitySkyLight = mob.level().getBrightness(LightLayer.SKY, entityEyePos);
+ int holderSkyLight = mob.level().getBrightness(LightLayer.SKY, holderEyePos);
+
+ poseStack.pushPose();
+ poseStack.translate(xAngleOffset, leashOffset.y, zAngleOffset);
+
+ Matrix4f posMatrix = new Matrix4f(poseStack.last().pose());
+
+ for (int segment = 0; segment <= 24; ++segment) {
+ renderLeashPiece(vertexConsumer, posMatrix, xDif, yDif, zDif, entityBlockLight, holderBlockLight, entitySkyLight, holderSkyLight, 0.025f, 0.025f, xOffset, zOffset, segment, false);
+ }
+
+ for (int segment = 24; segment >= 0; --segment) {
+ renderLeashPiece(vertexConsumer, posMatrix, xDif, yDif, zDif, entityBlockLight, holderBlockLight, entitySkyLight, holderSkyLight, 0.025f, 0.0f, xOffset, zOffset, segment, true);
+ }
+
+ poseStack.popPose();
+ }
+
+ /**
+ * Static rendering code for rendering a leash segment.
+ * It's a like-for-like from {@link net.minecraft.client.renderer.entity.MobRenderer#addVertexPair} that had to be duplicated here for flexible usage
+ */
+ private static void renderLeashPiece(VertexConsumer buffer, Matrix4f positionMatrix, float xDif, float yDif, float zDif, int entityBlockLight, int holderBlockLight, int entitySkyLight, int holderSkyLight, float width, float yOffset, float xOffset, float zOffset, int segment, boolean isLeashKnot) {
+ float piecePosPercent = segment / 24f;
+ int lerpBlockLight = (int) Mth.lerp(piecePosPercent, entityBlockLight, holderBlockLight);
+ int lerpSkyLight = (int) Mth.lerp(piecePosPercent, entitySkyLight, holderSkyLight);
+ int packedLight = LightTexture.pack(lerpBlockLight, lerpSkyLight);
+ float knotColourMod = segment % 2 == (isLeashKnot ? 1 : 0) ? 0.7f : 1f;
+ float red = 0.5f * knotColourMod;
+ float green = 0.4f * knotColourMod;
+ float blue = 0.3f * knotColourMod;
+ float x = xDif * piecePosPercent;
+ float y = yDif > 0.0f ? yDif * piecePosPercent * piecePosPercent : yDif - yDif * (1.0f - piecePosPercent) * (1.0f - piecePosPercent);
+ float z = zDif * piecePosPercent;
+
+ buffer.vertex(positionMatrix, x - xOffset, y + yOffset, z + zOffset).color(red, green, blue, 1).uv2(packedLight).endVertex();
+ buffer.vertex(positionMatrix, x + xOffset, y + width - yOffset, z - zOffset).color(red, green, blue, 1).uv2(packedLight).endVertex();
+ }
+
+ public boolean isShaking(T entity) {
+ return this.currentEntity.isFullyFrozen();
+ }
+
+ /**
+ * Update the current frame of a {@link AnimatableTexture potentially animated} texture used by this GeoRenderer.
+ * This should only be called immediately prior to rendering, and only
+ * @see AnimatableTexture#setAndUpdate(ResourceLocation, int)
+ */
+ @Override
+ public void updateAnimatedTextureFrame(T animatable) {
+ AnimatableTexture.setAndUpdate(getTextureLocation(animatable), this.currentEntity.getId() + (int)animatable.getTick(this.currentEntity));
+ }
+
+ /**
+ * Create and fire the relevant {@code CompileLayers} event hook for this renderer
+ */
+ @Override
+ public void fireCompileRenderLayersEvent() {
+ GeoRenderReplacedEntityEvent.CompileRenderLayers.EVENT.handle(new GeoRenderReplacedEntityEvent.CompileRenderLayers(this));
+ }
+
+ /**
+ * Create and fire the relevant {@code Pre-Render} event hook for this renderer.
+ *
+ * @return Whether the renderer should proceed based on the cancellation state of the event
+ */
+ @Override
+ public boolean firePreRenderEvent(PoseStack poseStack, BakedGeoModel model, MultiBufferSource bufferSource, float partialTick, int packedLight) {
+ return GeoRenderReplacedEntityEvent.Pre.EVENT.handle(new GeoRenderReplacedEntityEvent.Pre(this, poseStack, model, bufferSource, partialTick, packedLight));
+ }
+
+ /**
+ * Create and fire the relevant {@code Post-Render} event hook for this renderer
+ */
+ @Override
+ public void firePostRenderEvent(PoseStack poseStack, BakedGeoModel model, MultiBufferSource bufferSource, float partialTick, int packedLight) {
+ GeoRenderReplacedEntityEvent.Post.EVENT.handle(new GeoRenderReplacedEntityEvent.Post(this, poseStack, model, bufferSource, partialTick, packedLight));
+ }
+}
diff --git a/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/layer/AutoGlowingGeoLayer.java b/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/layer/AutoGlowingGeoLayer.java
new file mode 100644
index 0000000..f12ec4a
--- /dev/null
+++ b/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/layer/AutoGlowingGeoLayer.java
@@ -0,0 +1,41 @@
+package mod.azure.azurelib.common.api.client.renderer.layer;
+
+import com.mojang.blaze3d.vertex.PoseStack;
+import com.mojang.blaze3d.vertex.VertexConsumer;
+
+import mod.azure.azurelib.common.internal.common.cache.object.BakedGeoModel;
+import mod.azure.azurelib.common.internal.common.cache.texture.AutoGlowingTexture;
+import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable;
+import mod.azure.azurelib.common.internal.client.renderer.GeoRenderer;
+import net.minecraft.client.renderer.MultiBufferSource;
+import net.minecraft.client.renderer.RenderType;
+import net.minecraft.client.renderer.texture.OverlayTexture;
+import net.minecraft.resources.ResourceLocation;
+
+/**
+ * {@link GeoRenderLayer} for rendering the auto-generated glowlayer functionality implemented by AzureLib using the _glowing appendixed texture files.
+ */
+public class AutoGlowingGeoLayer extends GeoRenderLayer {
+ public AutoGlowingGeoLayer(GeoRenderer renderer) {
+ super(renderer);
+ }
+
+ /**
+ * Get the render type to use for this glowlayer renderer.
+ * Uses {@link RenderType#eyes(ResourceLocation)} by default, which may not be ideal in all circumstances.
+ */
+ protected RenderType getRenderType(T animatable) {
+ return AutoGlowingTexture.getRenderType(getTextureResource(animatable));
+ }
+
+ /**
+ * This is the method that is actually called by the render for your render layer to function.
+ * This is called after the animatable has been rendered, but before supplementary rendering like nametags.
+ */
+ @Override
+ public void render(PoseStack poseStack, T animatable, BakedGeoModel bakedModel, RenderType renderType, MultiBufferSource bufferSource, VertexConsumer buffer, float partialTick, int packedLight, int packedOverlay) {
+ RenderType emissiveRenderType = getRenderType(animatable);
+
+ getRenderer().reRender(bakedModel, poseStack, bufferSource, animatable, emissiveRenderType, bufferSource.getBuffer(emissiveRenderType), partialTick, 15728640, OverlayTexture.NO_OVERLAY, 1, 1, 1, 1);
+ }
+}
diff --git a/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/layer/BlockAndItemGeoLayer.java b/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/layer/BlockAndItemGeoLayer.java
new file mode 100644
index 0000000..a7dfbf1
--- /dev/null
+++ b/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/layer/BlockAndItemGeoLayer.java
@@ -0,0 +1,115 @@
+package mod.azure.azurelib.common.api.client.renderer.layer;
+
+import java.util.function.BiFunction;
+
+import org.jetbrains.annotations.Nullable;
+
+import com.mojang.blaze3d.vertex.PoseStack;
+import com.mojang.blaze3d.vertex.VertexConsumer;
+
+import mod.azure.azurelib.common.internal.common.cache.object.GeoBone;
+import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable;
+import mod.azure.azurelib.common.internal.client.renderer.GeoRenderer;
+import mod.azure.azurelib.common.internal.client.util.RenderUtils;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.MultiBufferSource;
+import net.minecraft.client.renderer.RenderType;
+import net.minecraft.client.renderer.texture.OverlayTexture;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.item.ItemDisplayContext;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.level.block.state.BlockState;
+
+/**
+ * {@link GeoRenderLayer} for rendering {@link net.minecraft.world.level.block.state.BlockState BlockStates} or {@link net.minecraft.world.item.ItemStack ItemStacks} on a given {@link GeoAnimatable}
+ */
+public class BlockAndItemGeoLayer extends GeoRenderLayer {
+ protected final BiFunction stackForBone;
+ protected final BiFunction blockForBone;
+
+ public BlockAndItemGeoLayer(GeoRenderer renderer) {
+ this(renderer, (bone, animatable) -> null, (bone, animatable) -> null);
+ }
+
+ public BlockAndItemGeoLayer(GeoRenderer renderer, BiFunction stackForBone, BiFunction blockForBone) {
+ super(renderer);
+
+ this.stackForBone = stackForBone;
+ this.blockForBone = blockForBone;
+ }
+
+ /**
+ * Return an ItemStack relevant to this bone for rendering, or null if no ItemStack to render
+ */
+ @Nullable
+ protected ItemStack getStackForBone(GeoBone bone, T animatable) {
+ return this.stackForBone.apply(bone, animatable);
+ }
+
+ /**
+ * Return a BlockState relevant to this bone for rendering, or null if no BlockState to render
+ */
+ @Nullable
+ protected BlockState getBlockForBone(GeoBone bone, T animatable) {
+ return this.blockForBone.apply(bone, animatable);
+ }
+
+ /**
+ * Return a specific TransFormType for this {@link ItemStack} render for this bone.
+ */
+ protected ItemDisplayContext getTransformTypeForStack(GeoBone bone, ItemStack stack, T animatable) {
+ return ItemDisplayContext.NONE;
+ }
+
+ /**
+ * This method is called by the {@link GeoRenderer} for each bone being rendered.
+ * This is a more expensive call, particularly if being used to render something on a different buffer.
+ * It does however have the benefit of having the matrix translations and other transformations already applied from render-time.
+ * It's recommended to avoid using this unless necessary.
+ *
+ * The {@link GeoBone} in question has already been rendered by this stage.
+ *
+ * If you do use it, and you render something that changes the {@link VertexConsumer buffer}, you need to reset it back to the previous buffer using {@link MultiBufferSource#getBuffer} before ending the method
+ */
+ @Override
+ public void renderForBone(PoseStack poseStack, T animatable, GeoBone bone, RenderType renderType, MultiBufferSource bufferSource, VertexConsumer buffer, float partialTick, int packedLight, int packedOverlay) {
+ ItemStack stack = getStackForBone(bone, animatable);
+ BlockState blockState = getBlockForBone(bone, animatable);
+
+ if (stack == null && blockState == null)
+ return;
+
+ poseStack.pushPose();
+ RenderUtils.translateAndRotateMatrixForBone(poseStack, bone);
+
+ if (stack != null)
+ renderStackForBone(poseStack, bone, stack, animatable, bufferSource, partialTick, packedLight, packedOverlay);
+
+ if (blockState != null)
+ renderBlockForBone(poseStack, bone, blockState, animatable, bufferSource, partialTick, packedLight, packedOverlay);
+
+ poseStack.popPose();
+ }
+
+ /**
+ * Render the given {@link ItemStack} for the provided {@link GeoBone}.
+ */
+ protected void renderStackForBone(PoseStack poseStack, GeoBone bone, ItemStack stack, T animatable, MultiBufferSource bufferSource, float partialTick, int packedLight, int packedOverlay) {
+ if (animatable instanceof LivingEntity livingEntity) {
+ Minecraft.getInstance().getItemRenderer().renderStatic(livingEntity, stack, getTransformTypeForStack(bone, stack, animatable), false, poseStack, bufferSource, livingEntity.level(), packedLight, packedOverlay, livingEntity.getId());
+ } else {
+ Minecraft.getInstance().getItemRenderer().renderStatic(stack, getTransformTypeForStack(bone, stack, animatable), packedLight, packedOverlay, poseStack, bufferSource, Minecraft.getInstance().level, (int) this.renderer.getInstanceId(animatable));
+ }
+ }
+
+ /**
+ * Render the given {@link BlockState} for the provided {@link GeoBone}.
+ */
+ protected void renderBlockForBone(PoseStack poseStack, GeoBone bone, BlockState state, T animatable, MultiBufferSource bufferSource, float partialTick, int packedLight, int packedOverlay) {
+ poseStack.pushPose();
+ poseStack.translate(-0.25f, -0.25f, -0.25f);
+ poseStack.scale(0.5f, 0.5f, 0.5f);
+ Minecraft.getInstance().getBlockRenderer().renderSingleBlock(state, poseStack, bufferSource, packedLight, OverlayTexture.NO_OVERLAY);
+ poseStack.popPose();
+ }
+}
diff --git a/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/layer/BoneFilterGeoLayer.java b/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/layer/BoneFilterGeoLayer.java
new file mode 100644
index 0000000..a1d9ef9
--- /dev/null
+++ b/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/layer/BoneFilterGeoLayer.java
@@ -0,0 +1,55 @@
+package mod.azure.azurelib.common.api.client.renderer.layer;
+
+import com.mojang.blaze3d.vertex.PoseStack;
+import com.mojang.blaze3d.vertex.VertexConsumer;
+
+import mod.azure.azurelib.common.internal.common.cache.object.BakedGeoModel;
+import mod.azure.azurelib.common.internal.common.cache.object.GeoBone;
+import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable;
+import mod.azure.azurelib.common.internal.client.renderer.GeoRenderer;
+import net.minecraft.client.renderer.MultiBufferSource;
+import net.minecraft.client.renderer.RenderType;
+import org.apache.logging.log4j.util.TriConsumer;
+
+/**
+ * {@link GeoRenderLayer} for auto-applying some form of modification to bones of a model prior to rendering.
+ * This can be useful for enabling or disabling bone rendering based on arbitrary conditions.
+ *
+ * NOTE: Despite this layer existing, it is much more efficient to use {@link FastBoneFilterGeoLayer} instead
+ */
+public class BoneFilterGeoLayer extends GeoRenderLayer {
+ protected final TriConsumer checkAndApply;
+
+ public BoneFilterGeoLayer(GeoRenderer renderer) {
+ this(renderer, (bone, animatable, partialTick) -> {});
+ }
+
+ public BoneFilterGeoLayer(GeoRenderer renderer, TriConsumer checkAndApply) {
+ super(renderer);
+
+ this.checkAndApply = checkAndApply;
+ }
+
+ /**
+ * This method is called for each bone in the model.
+ * Check whether the bone should be affected and apply the modification as needed.
+ */
+ protected void checkAndApply(GeoBone bone, T animatable, float partialTick) {
+ this.checkAndApply.accept(bone, animatable, partialTick);
+ }
+
+ @Override
+ public void preRender(PoseStack poseStack, T animatable, BakedGeoModel bakedModel, RenderType renderType, MultiBufferSource bufferSource, VertexConsumer buffer, float partialTick, int packedLight, int packedOverlay) {
+ for (GeoBone bone : bakedModel.topLevelBones()) {
+ checkChildBones(bone, animatable, partialTick);
+ }
+ }
+
+ private void checkChildBones(GeoBone parentBone, T animatable, float partialTick) {
+ checkAndApply(parentBone, animatable, partialTick);
+
+ for (GeoBone bone : parentBone.getChildBones()) {
+ checkChildBones(bone, animatable, partialTick);
+ }
+ }
+}
diff --git a/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/layer/FastBoneFilterGeoLayer.java b/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/layer/FastBoneFilterGeoLayer.java
new file mode 100644
index 0000000..32185a2
--- /dev/null
+++ b/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/layer/FastBoneFilterGeoLayer.java
@@ -0,0 +1,54 @@
+package mod.azure.azurelib.common.api.client.renderer.layer;
+
+import com.mojang.blaze3d.vertex.PoseStack;
+import com.mojang.blaze3d.vertex.VertexConsumer;
+
+import mod.azure.azurelib.common.internal.common.cache.object.BakedGeoModel;
+import mod.azure.azurelib.common.internal.common.cache.object.GeoBone;
+import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable;
+import mod.azure.azurelib.common.internal.client.renderer.GeoRenderer;
+import net.minecraft.client.renderer.MultiBufferSource;
+import net.minecraft.client.renderer.RenderType;
+import org.apache.logging.log4j.util.TriConsumer;
+
+import java.util.List;
+import java.util.function.Supplier;
+
+/**
+ * A more efficient version of {@link BoneFilterGeoLayer}.
+ * This version requires you provide the list of bones to filter up-front,
+ * so that the bone hierarchy doesn't need to be traversed.
+ */
+public class FastBoneFilterGeoLayer extends BoneFilterGeoLayer {
+ protected final Supplier> boneSupplier;
+
+ public FastBoneFilterGeoLayer(GeoRenderer renderer) {
+ this(renderer, List::of);
+ }
+
+ public FastBoneFilterGeoLayer(GeoRenderer renderer, Supplier> boneSupplier) {
+ this(renderer, boneSupplier, (bone, animatable, partialTick) -> {});
+ }
+
+ public FastBoneFilterGeoLayer(GeoRenderer renderer, Supplier> boneSupplier, TriConsumer checkAndApply) {
+ super(renderer, checkAndApply);
+
+ this.boneSupplier = boneSupplier;
+ }
+
+ /**
+ * Return a list of bone names to grab to then be filtered.
+ * This is even more efficient if you use a cached list.
+ */
+ protected List getAffectedBones() {
+ return boneSupplier.get();
+ }
+
+ @Override
+ public void preRender(PoseStack poseStack, T animatable, BakedGeoModel bakedModel, RenderType renderType, MultiBufferSource bufferSource,
+ VertexConsumer buffer, float partialTick, int packedLight, int packedOverlay) {
+ for (String boneName : getAffectedBones()) {
+ this.renderer.getGeoModel().getBone(boneName).ifPresent(bone -> checkAndApply(bone, animatable, partialTick));
+ }
+ }
+}
diff --git a/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/layer/GeoRenderLayer.java b/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/layer/GeoRenderLayer.java
new file mode 100644
index 0000000..aa01602
--- /dev/null
+++ b/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/layer/GeoRenderLayer.java
@@ -0,0 +1,85 @@
+package mod.azure.azurelib.common.api.client.renderer.layer;
+
+import com.mojang.blaze3d.vertex.PoseStack;
+import com.mojang.blaze3d.vertex.VertexConsumer;
+
+import mod.azure.azurelib.common.api.client.model.GeoModel;
+import mod.azure.azurelib.common.internal.common.cache.object.BakedGeoModel;
+import mod.azure.azurelib.common.internal.common.cache.object.GeoBone;
+import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable;
+import mod.azure.azurelib.common.internal.client.renderer.GeoRenderer;
+import net.minecraft.client.renderer.MultiBufferSource;
+import net.minecraft.client.renderer.RenderType;
+import net.minecraft.resources.ResourceLocation;
+
+/**
+ * Render layer base class for rendering additional layers of effects or textures over an existing model at runtime.
+ * Contains the base boilerplate and helper code for various render layer features
+ */
+public abstract class GeoRenderLayer {
+ protected final GeoRenderer renderer;
+
+ protected GeoRenderLayer(GeoRenderer entityRendererIn) {
+ this.renderer = entityRendererIn;
+ }
+
+ /**
+ * Get the {@link GeoModel} currently being rendered
+ */
+ public GeoModel getGeoModel() {
+ return this.renderer.getGeoModel();
+ }
+
+ /**
+ * Gets the {@link BakedGeoModel} instance that is currently being used.
+ * This can be directly used for re-rendering
+ */
+ public BakedGeoModel getDefaultBakedModel(T animatable) {
+ return getGeoModel().getBakedModel(getGeoModel().getModelResource(animatable));
+ }
+
+ /**
+ * Get the renderer responsible for the current render operation
+ */
+ public GeoRenderer getRenderer(){
+ return this.renderer;
+ }
+
+ /**
+ * Get the texture resource path for the given {@link GeoAnimatable}.
+ * By default, falls back to {@link GeoModel#getTextureResource(GeoAnimatable)}
+ */
+ protected ResourceLocation getTextureResource(T animatable) {
+ return this.renderer.getTextureLocation(animatable);
+ }
+
+ /**
+ * This method is called by the {@link GeoRenderer} before rendering, immediately after {@link GeoRenderer#preRender} has been called.
+ * This allows for RenderLayers to perform pre-render manipulations such as hiding or showing bones
+ */
+ public void preRender(PoseStack poseStack, T animatable, BakedGeoModel bakedModel, RenderType renderType,
+ MultiBufferSource bufferSource, VertexConsumer buffer, float partialTick,
+ int packedLight, int packedOverlay) {}
+
+ /**
+ * This is the method that is actually called by the render for your render layer to function.
+ * This is called after the animatable has been rendered, but before supplementary rendering like nametags.
+ */
+ public void render(PoseStack poseStack, T animatable, BakedGeoModel bakedModel, RenderType renderType,
+ MultiBufferSource bufferSource, VertexConsumer buffer, float partialTick,
+ int packedLight, int packedOverlay) {}
+
+ /**
+ * This method is called by the {@link GeoRenderer} for each bone being rendered.
+ * This is a more expensive call, particularly if being used to render something on a different buffer.
+ * It does however have the benefit of having the matrix translations and other transformations already applied from render-time.
+ * It's recommended to avoid using this unless necessary.
+ *
+ * The {@link GeoBone} in question has already been rendered by this stage.
+ *
+ * If you do use it, and you render something that changes the {@link VertexConsumer buffer}, you need to reset it back to the previous buffer
+ * using {@link MultiBufferSource#getBuffer} before ending the method
+ */
+ public void renderForBone(PoseStack poseStack, T animatable, GeoBone bone, RenderType renderType,
+ MultiBufferSource bufferSource, VertexConsumer buffer, float partialTick, int packedLight, int packedOverlay) {}
+}
\ No newline at end of file
diff --git a/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/layer/GeoRenderLayersContainer.java b/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/layer/GeoRenderLayersContainer.java
new file mode 100644
index 0000000..f36f9d9
--- /dev/null
+++ b/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/layer/GeoRenderLayersContainer.java
@@ -0,0 +1,47 @@
+package mod.azure.azurelib.common.api.client.renderer.layer;
+
+import java.util.List;
+
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable;
+import mod.azure.azurelib.common.internal.client.renderer.GeoRenderer;
+
+/**
+ * Base interface for a container for {@link GeoRenderLayer GeoRenderLayers}
+ * Each renderer should contain an instance of this, for holding its layers and handling events.
+ */
+public class GeoRenderLayersContainer {
+ private final GeoRenderer renderer;
+ private final List> layers = new ObjectArrayList<>();
+ private boolean compiledLayers = false;
+
+ public GeoRenderLayersContainer(GeoRenderer renderer) {
+ this.renderer = renderer;
+ }
+
+ /**
+ * Get the {@link GeoRenderLayer} list for usage
+ */
+ public List> getRenderLayers() {
+ if (!this.compiledLayers)
+ fireCompileRenderLayersEvent();
+
+ return this.layers;
+ }
+
+ /**
+ * Add a new render layer to the container
+ */
+ public void addLayer(GeoRenderLayer layer) {
+ this.layers.add(layer);
+ }
+
+ /**
+ * Create and fire the relevant {@code CompileRenderLayers} event hook for the owning renderer
+ */
+ public void fireCompileRenderLayersEvent() {
+ this.compiledLayers = true;
+
+ this.renderer.fireCompileRenderLayersEvent();
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/layer/ItemArmorGeoLayer.java b/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/layer/ItemArmorGeoLayer.java
new file mode 100644
index 0000000..6b5668d
--- /dev/null
+++ b/common/src/main/java/mod/azure/azurelib/common/api/client/renderer/layer/ItemArmorGeoLayer.java
@@ -0,0 +1,286 @@
+package mod.azure.azurelib.common.api.client.renderer.layer;
+
+import java.util.List;
+import java.util.Map;
+
+import org.Vrglab.AzureLib.Utility.Utils;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import com.mojang.authlib.GameProfile;
+import com.mojang.blaze3d.vertex.PoseStack;
+import com.mojang.blaze3d.vertex.VertexConsumer;
+
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import mod.azure.azurelib.common.api.common.animatable.GeoItem;
+import mod.azure.azurelib.common.internal.client.RenderProvider;
+import mod.azure.azurelib.common.internal.common.cache.object.BakedGeoModel;
+import mod.azure.azurelib.common.internal.common.cache.object.GeoBone;
+import mod.azure.azurelib.common.internal.common.cache.object.GeoCube;
+import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable;
+import mod.azure.azurelib.common.api.client.renderer.GeoArmorRenderer;
+import mod.azure.azurelib.common.internal.client.renderer.GeoRenderer;
+import mod.azure.azurelib.common.internal.client.util.RenderUtils;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.model.HumanoidModel;
+import net.minecraft.client.model.SkullModelBase;
+import net.minecraft.client.model.geom.ModelLayers;
+import net.minecraft.client.model.geom.ModelPart;
+import net.minecraft.client.model.geom.ModelPart.Cube;
+import net.minecraft.client.renderer.MultiBufferSource;
+import net.minecraft.client.renderer.RenderType;
+import net.minecraft.client.renderer.blockentity.SkullBlockRenderer;
+import net.minecraft.client.renderer.entity.ItemRenderer;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.EquipmentSlot;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.item.ArmorItem;
+import net.minecraft.world.item.BlockItem;
+import net.minecraft.world.item.DyeableArmorItem;
+import net.minecraft.world.item.Item;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.level.block.AbstractSkullBlock;
+import net.minecraft.world.level.block.SkullBlock;
+import net.minecraft.world.level.block.entity.SkullBlockEntity;
+
+/**
+ * Builtin class for handling dynamic armor rendering on AzureLib entities.
+ * Supports both {@link GeoItem AzureLib} and {@link net.minecraft.world.item.ArmorItem Vanilla} armor models.
+ * Unlike a traditional armor renderer, this renderer renders per-bone, giving much more flexible armor rendering.
+ */
+public class ItemArmorGeoLayer extends GeoRenderLayer {
+ protected static final Map ARMOR_PATH_CACHE = new Object2ObjectOpenHashMap<>();
+ protected static final HumanoidModel INNER_ARMOR_MODEL = new HumanoidModel<>(Minecraft.getInstance().getEntityModels().bakeLayer(ModelLayers.PLAYER_INNER_ARMOR));
+ protected static final HumanoidModel OUTER_ARMOR_MODEL = new HumanoidModel<>(Minecraft.getInstance().getEntityModels().bakeLayer(ModelLayers.PLAYER_OUTER_ARMOR));
+
+ @Nullable protected ItemStack mainHandStack;
+ @Nullable protected ItemStack offhandStack;
+ @Nullable protected ItemStack helmetStack;
+ @Nullable protected ItemStack chestplateStack;
+ @Nullable protected ItemStack leggingsStack;
+ @Nullable protected ItemStack bootsStack;
+
+ public ItemArmorGeoLayer(GeoRenderer geoRenderer) {
+ super(geoRenderer);
+ }
+
+ /**
+ * Return an EquipmentSlot for a given {@link ItemStack} and animatable instance.
+ * This is what determines the base model to use for rendering a particular stack
+ */
+ @NotNull
+ protected EquipmentSlot getEquipmentSlotForBone(GeoBone bone, ItemStack stack, T animatable) {
+ for(EquipmentSlot slot : EquipmentSlot.values()) {
+ if(slot.getType() == EquipmentSlot.Type.ARMOR) {
+ if(stack == animatable.getItemBySlot(slot))
+ return slot;
+ }
+ }
+
+ return EquipmentSlot.CHEST;
+ }
+
+ /**
+ * Return a ModelPart for a given {@link GeoBone}.
+ * This is then transformed into position for the final render
+ */
+ @NotNull
+ protected ModelPart getModelPartForBone(GeoBone bone, EquipmentSlot slot, ItemStack stack, T animatable, HumanoidModel> baseModel) {
+ return baseModel.body;
+ }
+
+ /**
+ * Get the {@link ItemStack} relevant to the bone being rendered.
+ * Return null if this bone should be ignored
+ */
+ @Nullable
+ protected ItemStack getArmorItemForBone(GeoBone bone, T animatable) {
+ return null;
+ }
+
+ /**
+ * This method is called by the {@link GeoRenderer} before rendering, immediately after {@link GeoRenderer#preRender} has been called.
+ * This allows for RenderLayers to perform pre-render manipulations such as hiding or showing bones
+ */
+ @Override
+ public void preRender(PoseStack poseStack, T animatable, BakedGeoModel bakedModel, RenderType renderType, MultiBufferSource bufferSource,
+ VertexConsumer buffer, float partialTick, int packedLight, int packedOverlay) {
+ this.mainHandStack = animatable.getItemBySlot(EquipmentSlot.MAINHAND);
+ this.offhandStack = animatable.getItemBySlot(EquipmentSlot.OFFHAND);
+ this.helmetStack = animatable.getItemBySlot(EquipmentSlot.HEAD);
+ this.chestplateStack = animatable.getItemBySlot(EquipmentSlot.CHEST);
+ this.leggingsStack = animatable.getItemBySlot(EquipmentSlot.LEGS);
+ this.bootsStack = animatable.getItemBySlot(EquipmentSlot.FEET);
+ }
+
+ /**
+ * This method is called by the {@link GeoRenderer} for each bone being rendered.
+ * This is a more expensive call, particularly if being used to render something on a different buffer.
+ * It does however have the benefit of having the matrix translations and other transformations already applied from render-time.
+ * It's recommended to avoid using this unless necessary.
+ *
+ * The {@link GeoBone} in question has already been rendered by this stage.
+ *
+ * If you do use it, and you render something that changes the {@link VertexConsumer buffer}, you need to reset it back to the previous buffer
+ * using {@link MultiBufferSource#getBuffer} before ending the method
+ */
+ @Override
+ public void renderForBone(PoseStack poseStack, T animatable, GeoBone bone, RenderType renderType, MultiBufferSource bufferSource,
+ VertexConsumer buffer, float partialTick, int packedLight, int packedOverlay) {
+ ItemStack armorStack = getArmorItemForBone(bone, animatable);
+
+ if (armorStack == null)
+ return;
+
+ if (armorStack.getItem() instanceof BlockItem blockItem && blockItem.getBlock() instanceof AbstractSkullBlock skullBlock) {
+ renderSkullAsArmor(poseStack, bone, armorStack, skullBlock, bufferSource, packedLight);
+ }
+ else {
+ EquipmentSlot slot = getEquipmentSlotForBone(bone, armorStack, animatable);
+ HumanoidModel> model = getModelForItem(bone, slot, armorStack, animatable);
+ ModelPart modelPart = getModelPartForBone(bone, slot, armorStack, animatable, model);
+
+ if (!((List)Utils.getPrivateFinalStaticField(modelPart, modelPart.getClass(), "cubes")).isEmpty()) {
+ poseStack.pushPose();
+ poseStack.scale(-1, -1, 1);
+
+ if (model instanceof GeoArmorRenderer> geoArmorRenderer) {
+ prepModelPartForRender(poseStack, bone, modelPart);
+ geoArmorRenderer.prepForRender(animatable, armorStack, slot, model);
+ geoArmorRenderer.applyBoneVisibilityByPart(slot, modelPart, model);
+ geoArmorRenderer.renderToBuffer(poseStack, null, packedLight, packedOverlay, 1, 1, 1, 1);
+ }
+ else if (armorStack.getItem() instanceof ArmorItem) {
+ prepModelPartForRender(poseStack, bone, modelPart);
+ renderVanillaArmorPiece(poseStack, animatable, bone, slot, armorStack, modelPart, bufferSource, partialTick, packedLight, packedOverlay);
+ }
+
+ poseStack.popPose();
+ }
+ }
+ }
+
+ /**
+ * Renders an individual armor piece base on the given {@link GeoBone} and {@link ItemStack}
+ */
+ protected void renderVanillaArmorPiece(PoseStack poseStack, T animatable, GeoBone bone, EquipmentSlot slot, ItemStack armorStack,
+ ModelPart modelPart, MultiBufferSource bufferSource, float partialTick, int packedLight, int packedOverlay) {
+ ResourceLocation texture = getVanillaArmorResource(animatable, armorStack, slot, "");
+ VertexConsumer buffer = getArmorBuffer(bufferSource, null, texture, armorStack.hasFoil());
+
+ if (armorStack.getItem() instanceof DyeableArmorItem dyable) {
+ int color = dyable.getColor(armorStack);
+
+ modelPart.render(poseStack, buffer, packedLight, packedOverlay, (color >> 16 & 255) / 255f, (color >> 8 & 255) / 255f, (color & 255) / 255f, 1);
+
+ texture = getVanillaArmorResource(animatable, armorStack, slot, "overlay");
+ buffer = getArmorBuffer(bufferSource, null, texture, false);
+ }
+
+ modelPart.render(poseStack, buffer, packedLight, packedOverlay, 1, 1, 1, 1);
+ }
+
+ /**
+ * Returns the standard VertexConsumer for armor rendering from the given buffer source.
+ * @param bufferSource The BufferSource to draw the buffer from
+ * @param renderType The RenderType to use for rendering, or null to use the default
+ * @param texturePath The texture path for the render. May be null if renderType is not null
+ * @param enchanted Whether the render should have an enchanted glint or not
+ * @return The buffer to draw to
+ */
+ protected VertexConsumer getArmorBuffer(MultiBufferSource bufferSource, @Nullable RenderType renderType, @Nullable ResourceLocation texturePath, boolean enchanted) {
+ if (renderType == null)
+ renderType = RenderType.armorCutoutNoCull(texturePath);
+
+ return ItemRenderer.getArmorFoilBuffer(bufferSource, renderType, false, enchanted);
+ }
+
+ /**
+ * Returns a cached instance of a base HumanoidModel that is used for rendering/modelling the provided {@link ItemStack}
+ */
+ @NotNull
+ protected HumanoidModel> getModelForItem(GeoBone bone, EquipmentSlot slot, ItemStack stack, T animatable) {
+ HumanoidModel defaultModel = slot == EquipmentSlot.LEGS ? INNER_ARMOR_MODEL : OUTER_ARMOR_MODEL;
+
+ return RenderProvider.of(stack).getHumanoidArmorModel(animatable, stack, slot, defaultModel);
+ }
+
+ /**
+ * Gets a cached resource path for the vanilla armor layer texture for this armor piece.
+ * Equivalent to {@link net.minecraft.client.renderer.entity.layers.HumanoidArmorLayer#getArmorLocation HumanoidArmorLayer.getArmorLocation}
+ */
+ public ResourceLocation getVanillaArmorResource(Entity entity, ItemStack stack, EquipmentSlot slot, String type) {
+ String domain = "minecraft";
+ String path = ((ArmorItem) stack.getItem()).getMaterial().getName();
+ String[] materialNameSplit = path.split(":", 2);
+
+ if (materialNameSplit.length > 1) {
+ domain = materialNameSplit[0];
+ path = materialNameSplit[1];
+ }
+
+ if (!type.isBlank())
+ type = "_" + type;
+
+ String texture = String.format("%s:textures/models/armor/%s_layer_%d%s.png", domain, path, (slot == EquipmentSlot.LEGS ? 2 : 1), type);
+ ResourceLocation ResourceLocation = ARMOR_PATH_CACHE.get(texture);
+
+ if (ResourceLocation == null) {
+ ResourceLocation = new ResourceLocation(texture);
+ ARMOR_PATH_CACHE.put(texture, ResourceLocation);
+ }
+
+ return ARMOR_PATH_CACHE.computeIfAbsent(texture, ResourceLocation::new);
+ }
+
+ /**
+ * Render a given {@link AbstractSkullBlock} as a worn armor piece in relation to a given {@link GeoBone}
+ */
+ protected void renderSkullAsArmor(PoseStack poseStack, GeoBone bone, ItemStack stack, AbstractSkullBlock skullBlock, MultiBufferSource bufferSource, int packedLight) {
+ CompoundTag stackTag = stack.getTag();
+ GameProfile gameProfile = stackTag != null ? SkullBlockEntity.getOrResolveGameProfile(stackTag) : null;
+
+ SkullBlock.Type type = skullBlock.getType();
+ SkullModelBase model = SkullBlockRenderer.createSkullRenderers(Minecraft.getInstance().getEntityModels()).get(type);
+ RenderType renderType = SkullBlockRenderer.getRenderType(type, gameProfile);
+
+ poseStack.pushPose();
+ RenderUtils.translateAndRotateMatrixForBone(poseStack, bone);
+ poseStack.scale(1.1875f, 1.1875f, 1.1875f);
+ poseStack.translate(-0.5f, 0, -0.5f);
+ SkullBlockRenderer.renderSkull(null, 0, 0, poseStack, bufferSource, packedLight, model, renderType);
+ poseStack.popPose();
+ }
+
+ /**
+ * Prepares the given {@link ModelPart} for render by setting its translation, position, and rotation values based on the provided {@link GeoBone}
+ * @param poseStack The PoseStack being used for rendering
+ * @param bone The GeoBone to base the translations on
+ * @param sourcePart The ModelPart to translate
+ */
+ protected void prepModelPartForRender(PoseStack poseStack, GeoBone bone, ModelPart sourcePart) {
+ final GeoCube firstCube = bone.getCubes().get(0);
+ final Cube armorCube = ((List)Utils.getPrivateFinalStaticField(sourcePart, sourcePart.getClass(), "cubes")).get(0);
+ final double armorBoneSizeX = firstCube.size().x();
+ final double armorBoneSizeY = firstCube.size().y();
+ final double armorBoneSizeZ = firstCube.size().z();
+ final double actualArmorSizeX = Math.abs(armorCube.maxX - armorCube.minX);
+ final double actualArmorSizeY = Math.abs(armorCube.maxY - armorCube.minY);
+ final double actualArmorSizeZ = Math.abs(armorCube.maxZ - armorCube.minZ);
+ float scaleX = (float)(armorBoneSizeX / actualArmorSizeX);
+ float scaleY = (float)(armorBoneSizeY / actualArmorSizeY);
+ float scaleZ = (float)(armorBoneSizeZ / actualArmorSizeZ);
+
+ sourcePart.setPos(-(bone.getPivotX() - ((bone.getPivotX() * scaleX) - bone.getPivotX()) / scaleX),
+ -(bone.getPivotY() - ((bone.getPivotY() * scaleY) - bone.getPivotY()) / scaleY),
+ (bone.getPivotZ() - ((bone.getPivotZ() * scaleZ) - bone.getPivotZ()) / scaleZ));
+
+ sourcePart.xRot = -bone.getRotX();
+ sourcePart.yRot = -bone.getRotY();
+ sourcePart.zRot = bone.getRotZ();
+
+ poseStack.scale(scaleX, scaleY, scaleZ);
+ }
+}
diff --git a/common/src/main/java/mod/azure/azurelib/common/api/common/ai/pathing/AzureNavigation.java b/common/src/main/java/mod/azure/azurelib/common/api/common/ai/pathing/AzureNavigation.java
new file mode 100644
index 0000000..b1f3d62
--- /dev/null
+++ b/common/src/main/java/mod/azure/azurelib/common/api/common/ai/pathing/AzureNavigation.java
@@ -0,0 +1,242 @@
+package mod.azure.azurelib.common.api.common.ai.pathing;
+
+import java.util.Objects;
+
+import mod.azure.azurelib.common.internal.common.ai.pathing.AzurePathFinder;
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.core.BlockPos;
+import net.minecraft.tags.BlockTags;
+import net.minecraft.util.Mth;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.Mob;
+import net.minecraft.world.entity.ai.navigation.GroundPathNavigation;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.pathfinder.BlockPathTypes;
+import net.minecraft.world.level.pathfinder.Node;
+import net.minecraft.world.level.pathfinder.Path;
+import net.minecraft.world.level.pathfinder.PathComputationType;
+import net.minecraft.world.level.pathfinder.PathFinder;
+import net.minecraft.world.level.pathfinder.WalkNodeEvaluator;
+import net.minecraft.world.phys.Vec3;
+
+/* Credit to Bob Mowzie and pau101 for most of the code,
+ * code source for the base class can be found here:
+ * https://github.com/BobMowzie/MowziesMobs/blob/master/src/main/java/com/bobmowzie/mowziesmobs/server/ai/MMPathNavigateGround.java
+ * */
+public class AzureNavigation extends GroundPathNavigation {
+ @Nullable
+ private BlockPos pathToPosition;
+
+ public AzureNavigation(Mob entity, Level world) {
+ super(entity, world);
+ }
+
+ @Override
+ protected PathFinder createPathFinder(int maxVisitedNodes) {
+ this.nodeEvaluator = new WalkNodeEvaluator();
+ this.nodeEvaluator.setCanPassDoors(true);
+ return new AzurePathFinder(this.nodeEvaluator, maxVisitedNodes);
+ }
+
+ @Override
+ protected void trimPath() {
+ super.trimPath();
+ for (int i = 0; i < this.path.getNodeCount(); ++i) {
+ Node node = this.path.getNode(i);
+ Node node2 = i + 1 < this.path.getNodeCount() ? this.path.getNode(i + 1) : null;
+ BlockState blockState = this.level.getBlockState(new BlockPos(node.x, node.y, node.z));
+ if (!blockState.is(BlockTags.STAIRS))
+ continue;
+ this.path.replaceNode(i, node.cloneAndMove(node.x, node.y + 1, node.z));
+ if (node2 == null || node.y < node2.y)
+ continue;
+ this.path.replaceNode(i + 1, node.cloneAndMove(node2.x, node.y + 1, node2.z));
+ }
+ }
+
+ @Override
+ protected void followThePath() {
+ Path path = Objects.requireNonNull(this.path);
+ Vec3 entityPos = this.getTempMobPos();
+ int pathLength = path.getNodeCount();
+ for (int i = path.getNextNodeIndex(); i < path.getNodeCount(); i++) {
+ if (path.getNode(i).y != Math.floor(entityPos.y)) {
+ pathLength = i;
+ break;
+ }
+ }
+ final Vec3 base = entityPos.add(-this.mob.getBbWidth() * 0.5F, 0.0F, -this.mob.getBbWidth() * 0.5F);
+ final Vec3 max = base.add(this.mob.getBbWidth(), this.mob.getBbHeight(), this.mob.getBbWidth());
+ if (this.tryShortcut(path, new Vec3(this.mob.getX(), this.mob.getY(), this.mob.getZ()), pathLength, base, max)) {
+ if (this.isAt(path, 0.5F) || this.atElevationChange(path) && this.isAt(path, this.mob.getBbWidth() * 0.5F)) {
+ this.mob.getLookControl().setLookAt(path.getNextEntityPos(this.mob));
+ path.setNextNodeIndex(path.getNextNodeIndex() + 1);
+ }
+ }
+ this.doStuckDetection(entityPos);
+ }
+
+ @Override
+ public Path createPath(BlockPos blockPos, int i) {
+ this.pathToPosition = blockPos;
+ return super.createPath(blockPos, i);
+ }
+
+ @Override
+ public Path createPath(Entity entity, int i) {
+ this.pathToPosition = entity.blockPosition();
+ return super.createPath(entity, i);
+ }
+
+ @Override
+ public boolean moveTo(Entity entity, double d) {
+ Path path = this.createPath(entity, 0);
+ if (path != null) {
+ return this.moveTo(path, d);
+ }
+ this.pathToPosition = entity.blockPosition();
+ this.speedModifier = d;
+ return true;
+ }
+
+ @Override
+ public void tick() {
+ super.tick();
+ if (this.isDone()) {
+ if (this.pathToPosition != null) {
+ if (this.pathToPosition.closerToCenterThan(this.mob.position(), this.mob.getBbWidth()) || this.mob.getY() > (double)this.pathToPosition.getY() && BlockPos.containing(this.pathToPosition.getX(), this.mob.getY(), this.pathToPosition.getZ()).closerToCenterThan(this.mob.position(), this.mob.getBbWidth())) {
+ this.pathToPosition = null;
+ } else {
+ this.mob.getMoveControl().setWantedPosition(this.pathToPosition.getX(), this.pathToPosition.getY(), this.pathToPosition.getZ(), this.speedModifier);
+ }
+ }
+ return;
+ }
+ if (this.getTargetPos() != null)
+ this.mob.getLookControl().setLookAt(this.getTargetPos().getX(), this.getTargetPos().getY(), this.getTargetPos().getZ());
+ }
+
+ private boolean isAt(Path path, float threshold) {
+ final Vec3 pathPos = path.getNextEntityPos(this.mob);
+ return Mth.abs((float) (this.mob.getX() - pathPos.x)) < threshold && Mth.abs((float) (this.mob.getZ() - pathPos.z)) < threshold && Math.abs(this.mob.getY() - pathPos.y) < 1.0D;
+ }
+
+ private boolean atElevationChange(Path path) {
+ final int curr = path.getNextNodeIndex();
+ final int end = Math.min(path.getNodeCount(), curr + Mth.ceil(this.mob.getBbWidth() * 0.5F) + 1);
+ final int currY = path.getNode(curr).y;
+ for (int i = curr + 1; i < end; i++) {
+ if (path.getNode(i).y != currY) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean tryShortcut(Path path, Vec3 entityPos, int pathLength, Vec3 base, Vec3 max) {
+ for (int i = pathLength; --i > path.getNextNodeIndex();) {
+ final Vec3 vec = path.getEntityPosAtNode(this.mob, i).subtract(entityPos);
+ if (this.sweep(vec, base, max)) {
+ path.setNextNodeIndex(i);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ static final float EPSILON = 1.0E-8F;
+
+ // Based off of
+ // https://github.com/andyhall/voxel-aabb-sweep/blob/d3ef85b19c10e4c9d2395c186f9661b052c50dc7/index.js
+ private boolean sweep(Vec3 vec, Vec3 base, Vec3 max) {
+ float t = 0.0F;
+ float max_t = (float) vec.length();
+ if (max_t < EPSILON)
+ return true;
+ final float[] tr = new float[3];
+ final int[] ldi = new int[3];
+ final int[] tri = new int[3];
+ final int[] step = new int[3];
+ final float[] tDelta = new float[3];
+ final float[] tNext = new float[3];
+ final float[] normed = new float[3];
+ for (int i = 0; i < 3; i++) {
+ float value = element(vec, i);
+ boolean dir = value >= 0.0F;
+ step[i] = dir ? 1 : -1;
+ float lead = element(dir ? max : base, i);
+ tr[i] = element(dir ? base : max, i);
+ ldi[i] = leadEdgeToInt(lead, step[i]);
+ tri[i] = trailEdgeToInt(tr[i], step[i]);
+ normed[i] = value / max_t;
+ tDelta[i] = Mth.abs(max_t / value);
+ float dist = dir ? (ldi[i] + 1 - lead) : (lead - ldi[i]);
+ tNext[i] = tDelta[i] < Float.POSITIVE_INFINITY ? tDelta[i] * dist : Float.POSITIVE_INFINITY;
+ }
+ final BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
+ do {
+ // stepForward
+ int axis = (tNext[0] < tNext[1]) ? ((tNext[0] < tNext[2]) ? 0 : 2) : ((tNext[1] < tNext[2]) ? 1 : 2);
+ float dt = tNext[axis] - t;
+ t = tNext[axis];
+ ldi[axis] += step[axis];
+ tNext[axis] += tDelta[axis];
+ for (int i = 0; i < 3; i++) {
+ tr[i] += dt * normed[i];
+ tri[i] = trailEdgeToInt(tr[i], step[i]);
+ }
+ // checkCollision
+ int stepx = step[0];
+ int x0 = (axis == 0) ? ldi[0] : tri[0];
+ int x1 = ldi[0] + stepx;
+ int stepy = step[1];
+ int y0 = (axis == 1) ? ldi[1] : tri[1];
+ int y1 = ldi[1] + stepy;
+ int stepz = step[2];
+ int z0 = (axis == 2) ? ldi[2] : tri[2];
+ int z1 = ldi[2] + stepz;
+ for (int x = x0; x != x1; x += stepx) {
+ for (int z = z0; z != z1; z += stepz) {
+ for (int y = y0; y != y1; y += stepy) {
+ BlockState block = this.level.getBlockState(pos.set(x, y, z));
+ if (!block.isPathfindable(this.level, pos, PathComputationType.LAND))
+ return false;
+ }
+ BlockPathTypes below = this.nodeEvaluator.getBlockPathType(this.level, x, y0 - 1, z, this.mob);
+ if (below == BlockPathTypes.WATER || below == BlockPathTypes.LAVA || below == BlockPathTypes.OPEN)
+ return false;
+ BlockPathTypes in = this.nodeEvaluator.getBlockPathType(this.level, x, y0, z, this.mob);
+ float priority = this.mob.getPathfindingMalus(in);
+ if (priority < 0.0F || priority >= 8.0F)
+ return false;
+ if (in == BlockPathTypes.DAMAGE_FIRE || in == BlockPathTypes.DANGER_FIRE || in == BlockPathTypes.DAMAGE_OTHER)
+ return false;
+ }
+ }
+ } while (t <= max_t);
+ return true;
+ }
+
+ static int leadEdgeToInt(float coord, int step) {
+ return Mth.floor(coord - step * EPSILON);
+ }
+
+ static int trailEdgeToInt(float coord, int step) {
+ return Mth.floor(coord + step * EPSILON);
+ }
+
+ static float element(Vec3 v, int i) {
+ switch (i) {
+ case 0:
+ return (float) v.x;
+ case 1:
+ return (float) v.y;
+ case 2:
+ return (float) v.z;
+ default:
+ return 0.0F;
+ }
+ }
+}
diff --git a/common/src/main/java/mod/azure/azurelib/common/api/common/animatable/GeoBlockEntity.java b/common/src/main/java/mod/azure/azurelib/common/api/common/animatable/GeoBlockEntity.java
new file mode 100644
index 0000000..3b1e97a
--- /dev/null
+++ b/common/src/main/java/mod/azure/azurelib/common/api/common/animatable/GeoBlockEntity.java
@@ -0,0 +1,99 @@
+package mod.azure.azurelib.common.api.common.animatable;
+
+import mod.azure.azurelib.common.internal.common.AzureLib;
+import mod.azure.azurelib.common.internal.common.core.animation.AnimationController;
+import mod.azure.azurelib.common.platform.Services;
+import org.jetbrains.annotations.Nullable;
+
+import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable;
+import mod.azure.azurelib.common.internal.common.core.animation.AnimatableManager;
+import mod.azure.azurelib.common.internal.common.network.SerializableDataTicket;
+import mod.azure.azurelib.common.internal.common.network.packet.BlockEntityAnimDataSyncPacket;
+import mod.azure.azurelib.common.internal.common.network.packet.BlockEntityAnimTriggerPacket;
+import mod.azure.azurelib.common.internal.client.util.RenderUtils;
+import net.minecraft.core.BlockPos;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.block.entity.BlockEntity;
+
+/**
+ * The {@link GeoAnimatable} interface specific to {@link BlockEntity BlockEntities}
+ */
+public interface GeoBlockEntity extends GeoAnimatable {
+ /**
+ * Get server-synced animation data via its relevant {@link SerializableDataTicket}.
+ * Should only be used on the client-side.
+ * DO NOT OVERRIDE
+ * @param dataTicket The data ticket for the data to retrieve
+ * @return The synced data, or null if no data of that type has been synced
+ */
+ @Nullable
+ default D getAnimData(SerializableDataTicket dataTicket) {
+ return getAnimatableInstanceCache().getManagerForId(0).getData(dataTicket);
+ }
+
+ /**
+ * Saves an arbitrary piece of data to this animatable's {@link AnimatableManager}.
+ * DO NOT OVERRIDE
+ * @param dataTicket The DataTicket to sync the data for
+ * @param data The data to sync
+ */
+ default void setAnimData(SerializableDataTicket dataTicket, D data) {
+ BlockEntity blockEntity = (BlockEntity)this;
+ Level level = blockEntity.getLevel();
+
+ if (level == null) {
+ AzureLib.LOGGER.error("Attempting to set animation data for BlockEntity too early! Must wait until after the BlockEntity has been set in the world. ({})", blockEntity.getClass());
+
+ return;
+ }
+
+ if (level.isClientSide()) {
+ getAnimatableInstanceCache().getManagerForId(0).setData(dataTicket, data);
+ }
+ else {
+ BlockPos pos = blockEntity.getBlockPos();
+
+ BlockEntityAnimDataSyncPacket blockEntityAnimDataSyncPacket = new BlockEntityAnimDataSyncPacket<>(pos, dataTicket, data);
+ Services.NETWORK.sendToEntitiesTrackingChunk(blockEntityAnimDataSyncPacket, (ServerLevel) level, pos);
+ }
+ }
+
+ /**
+ * Trigger an animation for this BlockEntity, based on the controller name and animation name.
+ * DO NOT OVERRIDE
+ * @param controllerName The name of the controller name the animation belongs to, or null to do an inefficient lazy search
+ * @param animName The name of animation to trigger. This needs to have been registered with the controller via {@link AnimationController#triggerableAnim AnimationController.triggerableAnim}
+ */
+ default void triggerAnim(@Nullable String controllerName, String animName) {
+ BlockEntity blockEntity = (BlockEntity)this;
+ Level level = blockEntity.getLevel();
+
+ if (level == null) {
+ AzureLib.LOGGER.error("Attempting to trigger an animation for a BlockEntity too early! Must wait until after the BlockEntity has been set in the world. ({})", blockEntity.getClass());
+
+ return;
+ }
+
+ if (level.isClientSide()) {
+ getAnimatableInstanceCache().getManagerForId(0).tryTriggerAnimation(controllerName, animName);
+ }
+ else {
+ BlockPos pos = blockEntity.getBlockPos();
+
+ BlockEntityAnimTriggerPacket blockEntityAnimTriggerPacket = new BlockEntityAnimTriggerPacket(pos, controllerName, animName);
+ Services.NETWORK.sendToEntitiesTrackingChunk(blockEntityAnimTriggerPacket, (ServerLevel) level, pos);
+ }
+ }
+
+ /**
+ * Returns the current age/tick of the animatable instance.
+ * By default this is just the animatable's age in ticks, but this method allows for non-ticking custom animatables to provide their own values
+ * @param blockEntity The BlockEntity representing this animatable
+ * @return The current tick/age of the animatable, for animation purposes
+ */
+ @Override
+ default double getTick(Object blockEntity) {
+ return RenderUtils.getCurrentTick();
+ }
+}
diff --git a/common/src/main/java/mod/azure/azurelib/common/api/common/animatable/GeoEntity.java b/common/src/main/java/mod/azure/azurelib/common/api/common/animatable/GeoEntity.java
new file mode 100644
index 0000000..eac16ce
--- /dev/null
+++ b/common/src/main/java/mod/azure/azurelib/common/api/common/animatable/GeoEntity.java
@@ -0,0 +1,81 @@
+package mod.azure.azurelib.common.api.common.animatable;
+
+import mod.azure.azurelib.common.api.client.renderer.GeoReplacedEntityRenderer;
+import mod.azure.azurelib.common.internal.common.core.animation.AnimationController;
+import mod.azure.azurelib.common.platform.Services;
+import org.jetbrains.annotations.Nullable;
+
+import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable;
+import mod.azure.azurelib.common.internal.common.core.animation.AnimatableManager;
+import mod.azure.azurelib.common.internal.common.network.SerializableDataTicket;
+import mod.azure.azurelib.common.internal.common.network.packet.EntityAnimDataSyncPacket;
+import mod.azure.azurelib.common.internal.common.network.packet.EntityAnimTriggerPacket;
+import mod.azure.azurelib.common.internal.client.util.RenderUtils;
+import net.minecraft.world.entity.Entity;
+
+/**
+ * The {@link GeoAnimatable} interface specific to {@link net.minecraft.world.entity.Entity Entities}. This also applies to Projectiles and other Entity subclasses.
+ * NOTE: This cannot be used for entities using the {@link GeoReplacedEntityRenderer} as you aren't extending {@code Entity}. Use {@link GeoReplacedEntity} instead.
+ */
+public interface GeoEntity extends GeoAnimatable {
+ /**
+ * Get server-synced animation data via its relevant {@link SerializableDataTicket}.
+ * Should only be used on the client-side.
+ * DO NOT OVERRIDE
+ *
+ * @param dataTicket The data ticket for the data to retrieve
+ * @return The synced data, or null if no data of that type has been synced
+ */
+ @Nullable
+ default D getAnimData(SerializableDataTicket dataTicket) {
+ return getAnimatableInstanceCache().getManagerForId(((Entity) this).getId()).getData(dataTicket);
+ }
+
+ /**
+ * Saves an arbitrary syncable piece of data to this animatable's {@link AnimatableManager}.
+ * DO NOT OVERRIDE
+ *
+ * @param dataTicket The DataTicket to sync the data for
+ * @param data The data to sync
+ */
+ default void setAnimData(SerializableDataTicket dataTicket, D data) {
+ Entity entity = (Entity) this;
+
+ if (entity.level().isClientSide()) {
+ getAnimatableInstanceCache().getManagerForId(entity.getId()).setData(dataTicket, data);
+ } else {
+ EntityAnimDataSyncPacket entityAnimDataSyncPacket = new EntityAnimDataSyncPacket<>(entity.getId(), dataTicket, data);
+ Services.NETWORK.sendToTrackingEntityAndSelf(entityAnimDataSyncPacket, entity);
+ }
+ }
+
+ /**
+ * Trigger an animation for this Entity, based on the controller name and animation name.
+ * DO NOT OVERRIDE
+ *
+ * @param controllerName The name of the controller name the animation belongs to, or null to do an inefficient lazy search
+ * @param animName The name of animation to trigger. This needs to have been registered with the controller via {@link AnimationController#triggerableAnim AnimationController.triggerableAnim}
+ */
+ default void triggerAnim(@Nullable String controllerName, String animName) {
+ Entity entity = (Entity) this;
+
+ if (entity.level().isClientSide()) {
+ getAnimatableInstanceCache().getManagerForId(entity.getId()).tryTriggerAnimation(controllerName, animName);
+ } else {
+ EntityAnimTriggerPacket entityAnimTriggerPacket = new EntityAnimTriggerPacket(entity.getId(), controllerName, animName);
+ Services.NETWORK.sendToTrackingEntityAndSelf(entityAnimTriggerPacket, entity);
+ }
+ }
+
+ /**
+ * Returns the current age/tick of the animatable instance.
+ * By default this is just the animatable's age in ticks, but this method allows for non-ticking custom animatables to provide their own values
+ *
+ * @param entity The Entity representing this animatable
+ * @return The current tick/age of the animatable, for animation purposes
+ */
+ @Override
+ default double getTick(Object entity) {
+ return RenderUtils.getCurrentTick();
+ }
+}
diff --git a/common/src/main/java/mod/azure/azurelib/common/api/common/animatable/GeoItem.java b/common/src/main/java/mod/azure/azurelib/common/api/common/animatable/GeoItem.java
new file mode 100644
index 0000000..8f4a53f
--- /dev/null
+++ b/common/src/main/java/mod/azure/azurelib/common/api/common/animatable/GeoItem.java
@@ -0,0 +1,157 @@
+package mod.azure.azurelib.common.api.common.animatable;
+
+import java.util.EnumMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
+
+import mod.azure.azurelib.common.internal.common.animatable.SingletonGeoAnimatable;
+import mod.azure.azurelib.common.platform.Services;
+import org.jetbrains.annotations.Nullable;
+
+import com.google.common.base.Suppliers;
+
+import mod.azure.azurelib.common.internal.common.cache.AnimatableIdCache;
+import mod.azure.azurelib.common.internal.common.constant.DataTickets;
+import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable;
+import mod.azure.azurelib.common.internal.common.core.animatable.instance.AnimatableInstanceCache;
+import mod.azure.azurelib.common.internal.common.core.animatable.instance.SingletonAnimatableInstanceCache;
+import mod.azure.azurelib.common.internal.common.core.animation.AnimatableManager;
+import mod.azure.azurelib.common.internal.common.core.animation.ContextAwareAnimatableManager;
+import mod.azure.azurelib.common.internal.client.util.RenderUtils;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.Tag;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.item.ItemDisplayContext;
+import net.minecraft.world.item.ItemStack;
+
+/**
+ * The {@link GeoAnimatable GeoAnimatable} interface specific to {@link net.minecraft.world.item.Item Items}. This also applies to armor, as they are just items too.
+ */
+public interface GeoItem extends SingletonGeoAnimatable {
+ String ID_NBT_KEY = "AzureLibID";
+
+ /**
+ * Safety wrapper to distance the client-side code from common code.
+ * This should be cached in your {@link net.minecraft.world.item.Item Item} class
+ */
+ static Supplier