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) +[Curse Forge](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. + *
+	 *     {@code
+	 *		new ResourceLocation("myMod", "animals/red_fish")
+	 *		}
+ * @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.
{@code "mymod:animations/entity/animal/goat.animation.json"}
+ */ + 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 makeRenderer(GeoItem item) { + if (Services.PLATFORM.isServerEnvironment()) + return () -> null; + + return Suppliers.memoize(() -> { + AtomicReference renderProvider = new AtomicReference<>(); + item.createRenderer(renderProvider::set); + return renderProvider.get(); + }); + } + + /** + * Register this as a synched {@code GeoAnimatable} instance with AzureLib's networking functions + *

+ * This should be called inside the constructor of your object. + */ + static void registerSyncedAnimatable(GeoAnimatable animatable) { + SingletonGeoAnimatable.registerSyncedAnimatable(animatable); + } + + /** + * Gets the unique identifying number from this ItemStack's {@link net.minecraft.nbt.Tag NBT}, or {@link Long#MAX_VALUE} if one hasn't been assigned + */ + static long getId(ItemStack stack) { + CompoundTag tag = stack.getTag(); + + if (tag == null) + return Long.MAX_VALUE; + + return tag.getLong(ID_NBT_KEY); + } + + /** + * Gets the unique identifying number from this ItemStack's {@link net.minecraft.nbt.Tag NBT}.
+ * If no ID has been reserved for this stack yet, it will reserve a new id and assign it + */ + static long getOrAssignId(ItemStack stack, ServerLevel level) { + CompoundTag tag = stack.getOrCreateTag(); + long id = tag.getLong(ID_NBT_KEY); + + if (tag.contains(ID_NBT_KEY, Tag.TAG_ANY_NUMERIC)) + return id; + + id = AnimatableIdCache.getFreeId(level); + + tag.putLong(ID_NBT_KEY, id); + + return id; + } + + /** + * 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 itemStack The ItemStack representing this animatable + * @return The current tick/age of the animatable, for animation purposes + */ + @Override + default double getTick(Object itemStack) { + return RenderUtils.getCurrentTick(); + } + + /** + * Whether this item animatable is perspective aware, handling animations differently depending on the {@link net.minecraft.world.item.ItemDisplayContext render perspective} + */ + default boolean isPerspectiveAware() { + return false; + } + + /** + * Replaces the default AnimatableInstanceCache for GeoItems if {@link GeoItem#isPerspectiveAware()} is true, for perspective-dependent handling + */ + @Nullable + @Override + default AnimatableInstanceCache animatableCacheOverride() { + if (isPerspectiveAware()) + return new ContextBasedAnimatableInstanceCache(this); + + return SingletonGeoAnimatable.super.animatableCacheOverride(); + } + + /** + * AnimatableInstanceCache specific to GeoItems, for doing render perspective based animations + */ + class ContextBasedAnimatableInstanceCache extends SingletonAnimatableInstanceCache { + public ContextBasedAnimatableInstanceCache(GeoAnimatable animatable) { + super(animatable); + } + + /** + * Gets an {@link AnimatableManager} instance from this cache, cached under the id provided, or a new one if one doesn't already exist.
+ * This subclass assumes that all animatable instances will be sharing this cache instance, and so differentiates data by ids. + */ + @Override + public AnimatableManager getManagerForId(long uniqueId) { + if (!this.managers.containsKey(uniqueId)) + this.managers.put(uniqueId, new ContextAwareAnimatableManager(this.animatable) { + @Override + protected Map> buildContextOptions(GeoAnimatable animatable) { + Map> map = new EnumMap<>(ItemDisplayContext.class); + + for (ItemDisplayContext context : ItemDisplayContext.values()) { + map.put(context, new AnimatableManager<>(animatable)); + } + + return map; + } + + @Override + public ItemDisplayContext getCurrentContext() { + ItemDisplayContext context = getData(DataTickets.ITEM_RENDER_PERSPECTIVE); + + return context == null ? ItemDisplayContext.NONE : context; + } + }); + + return this.managers.get(uniqueId); + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/api/common/animatable/GeoReplacedEntity.java b/common/src/main/java/mod/azure/azurelib/common/api/common/animatable/GeoReplacedEntity.java new file mode 100644 index 0000000..cc4e576 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/api/common/animatable/GeoReplacedEntity.java @@ -0,0 +1,99 @@ +package mod.azure.azurelib.common.api.common.animatable; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +import mod.azure.azurelib.common.internal.common.animatable.SingletonGeoAnimatable; +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 net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; + +/** + * The {@link GeoAnimatable} interface specific to {@link Entity Entities}. This interface is specifically for entities replacing the rendering of other, existing entities. + */ +public interface GeoReplacedEntity extends SingletonGeoAnimatable { + /** + * Returns the {@link EntityType} this entity is intending to replace.
+ * This is used for rendering an animation purposes. + */ + EntityType getReplacingEntityType(); + + /** + * Get server-synced animation data via its relevant {@link SerializableDataTicket}.
+ * Should only be used on the client-side.
+ * DO NOT OVERRIDE + * + * @param entity The entity instance relevant to the data being set + * @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(Entity entity, SerializableDataTicket dataTicket) { + return getAnimatableInstanceCache().getManagerForId(entity.getId()).getData(dataTicket); + } + + /** + * Saves an arbitrary syncable piece of data to this animatable's {@link AnimatableManager}.
+ * DO NOT OVERRIDE + * + * @param relatedEntity An entity related to the state of the data for syncing + * @param dataTicket The DataTicket to sync the data for + * @param data The data to sync + */ + default void setAnimData(Entity relatedEntity, SerializableDataTicket dataTicket, D data) { + if (relatedEntity.level().isClientSide()) { + getAnimatableInstanceCache().getManagerForId(relatedEntity.getId()).setData(dataTicket, data); + } else { + EntityAnimDataSyncPacket entityAnimDataSyncPacket = new EntityAnimDataSyncPacket<>(relatedEntity.getId(), dataTicket, data); + Services.NETWORK.sendToTrackingEntityAndSelf(entityAnimDataSyncPacket, relatedEntity); + } + } + + /** + * Trigger an animation for this Entity, based on the controller name and animation name.
+ * DO NOT OVERRIDE + * + * @param relatedEntity An entity related to the state of the data for syncing + * @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(Entity relatedEntity, @Nullable String controllerName, String animName) { + if (relatedEntity.level().isClientSide()) { + getAnimatableInstanceCache().getManagerForId(relatedEntity.getId()).tryTriggerAnimation(controllerName, animName); + } else { + EntityAnimTriggerPacket entityAnimTriggerPacket = new EntityAnimTriggerPacket(relatedEntity.getId(), controllerName, animName); + Services.NETWORK.sendToTrackingEntityAndSelf(entityAnimTriggerPacket, relatedEntity); + } + } + + /** + * 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 ((Entity) entity).tickCount; + } + + // These methods aren't used for GeoReplacedEntity + @Override + default void createRenderer(Consumer consumer) { + } + + // These methods aren't used for GeoReplacedEntity + @Override + default Supplier getRenderProvider() { + return null; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/api/common/config/Config.java b/common/src/main/java/mod/azure/azurelib/common/api/common/config/Config.java new file mode 100644 index 0000000..17d315f --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/api/common/config/Config.java @@ -0,0 +1,55 @@ +package mod.azure.azurelib.common.api.common.config; + +import mod.azure.azurelib.common.internal.common.config.Configurable; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Config marker annotation. Every registered config class must have this annotation. + * Inside this class you should define all configurable fields (cannot be {@code STATIC})!. + * All configurable fields must be annotated with {@link Configurable} annotation, otherwise it will + * be ignored. + * + * @author Toma + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Config { + + /** + * This value should be globally unique. It is suggested to use + * your mod ID as prefix or standalone based on how many configs you're + * creating. + * + * @return Unique config identifier + */ + String id(); + + /** + * Allows you to customize your config filename. Your custom filename must be valid + * according to your operating system, otherwise {@link java.io.IOException} will + * be raised during config processing. + * Using {@code empty} string as filename will use your {@link Config#id()} value as default. + * + * @return Your custom filename. + */ + String filename() default ""; + + /** + * Allows you to group multiple configs under one identifier. + * Useful when you have 2 or more config files which should be accessible via GUI. + * + * @return Custom config group identifier. By default, value defined by {@link Config#id()} will be used. + */ + String group() default ""; + + /** + * Annotating your config class with this will block config auto-sync when config file is updated + */ + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @interface NoAutoSync {} +} diff --git a/common/src/main/java/mod/azure/azurelib/common/api/common/config/TestingConfig.java b/common/src/main/java/mod/azure/azurelib/common/api/common/config/TestingConfig.java new file mode 100644 index 0000000..aaeb0f8 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/api/common/config/TestingConfig.java @@ -0,0 +1,91 @@ +package mod.azure.azurelib.common.api.common.config; + +import java.util.Arrays; +import java.util.regex.Pattern; + +import mod.azure.azurelib.common.internal.common.AzureLib; +import mod.azure.azurelib.common.internal.client.config.IValidationHandler; +import mod.azure.azurelib.common.internal.common.config.Configurable; +import mod.azure.azurelib.common.internal.common.config.validate.ValidationResult; +import net.minecraft.network.chat.Component; + +@Config(id = AzureLib.MOD_ID) +public final class TestingConfig { + + @Configurable + public boolean bool = true; + + @Configurable + @Configurable.Synchronized + public int number = 15; + + @Configurable + public long longNumber = 16644564564561651L; + + @Configurable + public float floatNumber = 151.3123F; + + @Configurable + public double doubleNumber = 316.15646556D; + + @Configurable + @Configurable.StringPattern(value = "[a-z\\s]+", flags = Pattern.CASE_INSENSITIVE) + public String string = "random text"; + + @Configurable + @Configurable.StringPattern(value = "#[0-9a-fA-F]{1,6}") + @Configurable.Gui.ColorValue + public String color = "#33AADD"; + + @Configurable + @Configurable.StringPattern(value = "#[0-9a-fA-F]{1,8}") + @Configurable.Gui.ColorValue(isARGB = true) + public String color2 = "#66771166"; + + @Configurable + @Configurable.FixedSize + public boolean[] boolArray = {false, false, true, false}; + + @Configurable + @Configurable.Range(min = 50, max = 160) + public int[] intArray = {153, 123, 54}; + + @Configurable + public long[] longArray = {13, 56, 133}; + + @Configurable + @Configurable.DecimalRange(min = 500.0F) + public float[] floatArray = {135.32F, 1561.23F}; + + @Configurable + @Configurable.ValueUpdateCallback(method = "onUpdate") + public String[] stringArray = {"minecraft:test"}; + + @Configurable + public TestEnum testEnum = TestEnum.C; + + @Configurable + public NestedTest nestedTest = new NestedTest(); + + public enum TestEnum { + A, B, C, D + } + + public void onUpdate(String[] value, IValidationHandler handler) { + AzureLib.LOGGER.debug(() -> Arrays.toString(value)); + handler.setValidationResult(ValidationResult.warn(Component.translatable("config.azurelib.option.genericwarning"))); + } + + public static class NestedTest { + + @Configurable + @Configurable.ValueUpdateCallback(method = "onUpdate") + public int testInt = 13; + + public void onUpdate(int value, IValidationHandler handler) { + if (value == 0) { + handler.setValidationResult(ValidationResult.warn(Component.literal("value is 0"))); + } + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/api/common/enchantments/IncendiaryEnchantment.java b/common/src/main/java/mod/azure/azurelib/common/api/common/enchantments/IncendiaryEnchantment.java new file mode 100644 index 0000000..925656e --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/api/common/enchantments/IncendiaryEnchantment.java @@ -0,0 +1,48 @@ +package mod.azure.azurelib.common.api.common.enchantments; + +import mod.azure.azurelib.common.api.common.tags.AzureTags; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.enchantment.Enchantment; +import net.minecraft.world.item.enchantment.EnchantmentCategory; + +public class IncendiaryEnchantment extends Enchantment { + public IncendiaryEnchantment(Rarity rarity, EquipmentSlot... slots) { + super(rarity, EnchantmentCategory.BREAKABLE, slots); + } + + @Override + public int getMaxCost(int level) { + return 1; + } + + @Override + public int getMinCost(int level) { + return 1; + } + + @Override + public int getMaxLevel() { + return 1; + } + + @Override + public boolean isTreasureOnly() { + return true; + } + + @Override + public boolean isTradeable() { + return true; + } + + @Override + public boolean isDiscoverable() { + return true; + } + + @Override + public boolean canEnchant(ItemStack stack) { + return stack.is(AzureTags.GUNS); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/api/common/entities/AzureVibrationUser.java b/common/src/main/java/mod/azure/azurelib/common/api/common/entities/AzureVibrationUser.java new file mode 100644 index 0000000..eafcdff --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/api/common/entities/AzureVibrationUser.java @@ -0,0 +1,123 @@ +package mod.azure.azurelib.common.api.common.entities; + +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.BlockTags; +import net.minecraft.tags.GameEventTags; +import net.minecraft.tags.TagKey; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ambient.Bat; +import net.minecraft.world.entity.monster.warden.Warden; +import net.minecraft.world.level.gameevent.EntityPositionSource; +import net.minecraft.world.level.gameevent.GameEvent; +import net.minecraft.world.level.gameevent.GameEvent.Context; +import net.minecraft.world.level.gameevent.PositionSource; +import net.minecraft.world.level.gameevent.vibrations.VibrationSystem; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Nullable; + +/** + * Custom Vibration class that removes the {@link Warden} particle that usually spawns + */ +public class AzureVibrationUser implements VibrationSystem.User { + protected final Mob mob; + protected final float moveSpeed; + protected final int range; + protected final PositionSource positionSource; + + public AzureVibrationUser(Mob entity, float speed, int range) { + this.positionSource = new EntityPositionSource(entity, entity.getEyeHeight()); + this.mob = entity; + this.moveSpeed = speed; + this.range = range; + } + + @Override + public int getListenerRadius() { + return range; + } + + @Override + public PositionSource getPositionSource() { + return this.positionSource; + } + + @Override + public TagKey getListenableEvents() { + return GameEventTags.WARDEN_CAN_LISTEN; + } + + @Override + public boolean canTriggerAvoidVibration() { + return true; + } + + @Override + public boolean isValidVibration(GameEvent gameEvent, Context context) { + if (!gameEvent.is(this.getListenableEvents())) + return false; + + var entity = context.sourceEntity(); + if (entity != null) { + if (entity.isSpectator()) + return false; + if (entity.isSteppingCarefully() && gameEvent.is(GameEventTags.IGNORE_VIBRATIONS_SNEAKING)) + return false; + if (entity.dampensVibrations()) + return false; + } + if (context.affectedState() != null) + return !context.affectedState().is(BlockTags.DAMPENS_VIBRATIONS); + return true; + } + + @Override + public boolean canReceiveVibration(ServerLevel serverLevel, BlockPos blockPos, GameEvent gameEvent, GameEvent.Context context) { + if (mob.isNoAi() || mob.isDeadOrDying() || !mob.level().getWorldBorder().isWithinBounds(blockPos) || mob.isRemoved()) + return false; + var entity = context.sourceEntity(); + return !(entity instanceof LivingEntity) || canTargetEntity((LivingEntity) entity); + } + + @Override + public void onReceiveVibration(ServerLevel serverLevel, BlockPos blockPos, GameEvent gameEvent, @Nullable Entity entity, @Nullable Entity entity2, float f) { + if (this.mob.isDeadOrDying()) + return; + if (this.mob.isVehicle()) + return; + } + + @Contract(value = "null->false") + public boolean canTargetEntity(@Nullable Entity entity) { + if (!(entity instanceof LivingEntity)) + return false; + var livingEntity = (LivingEntity) entity; + if (this.mob.level() != entity.level()) + return false; + if (!EntitySelector.NO_CREATIVE_OR_SPECTATOR.test(entity)) + return false; + if (this.mob.isVehicle()) + return false; + if (this.mob.isAlliedTo(entity)) + return false; + if (livingEntity.getMobType() == MobType.UNDEAD) + return false; + if (livingEntity.getType() == EntityType.ARMOR_STAND) + return false; + if (livingEntity.getType() == EntityType.WARDEN) + return false; + if (livingEntity instanceof Bat) + return false; + if (entity instanceof Marker) + return false; + if (entity instanceof AreaEffectCloud) + return false; + if (livingEntity.isInvulnerable()) + return false; + if (livingEntity.isDeadOrDying()) + return false; + if (!this.mob.level().getWorldBorder().isWithinBounds(livingEntity.getBoundingBox())) + return false; + return true; + } +} \ No newline at end of file diff --git a/common/src/main/java/mod/azure/azurelib/common/api/common/event/GeoRenderArmorEvent.java b/common/src/main/java/mod/azure/azurelib/common/api/common/event/GeoRenderArmorEvent.java new file mode 100644 index 0000000..d97af68 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/api/common/event/GeoRenderArmorEvent.java @@ -0,0 +1,194 @@ +package mod.azure.azurelib.common.api.common.event; + +import com.mojang.blaze3d.vertex.PoseStack; +import mod.azure.azurelib.common.api.client.renderer.GeoArmorRenderer; +import mod.azure.azurelib.common.api.client.renderer.GeoEntityRenderer; +import mod.azure.azurelib.common.api.client.renderer.layer.GeoRenderLayer; +import mod.azure.azurelib.common.internal.common.cache.object.BakedGeoModel; +import mod.azure.azurelib.common.internal.common.event.GeoRenderEvent; +import mod.azure.azurelib.common.platform.Services; +import mod.azure.azurelib.common.platform.services.GeoRenderPhaseEventFactory; +import mod.azure.azurelib.common.internal.client.renderer.GeoRenderer; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Nullable; + +/** + * Renderer events for armor pieces being rendered by {@link GeoArmorRenderer} + */ +public abstract class GeoRenderArmorEvent implements GeoRenderEvent { + private final GeoArmorRenderer renderer; + + public GeoRenderArmorEvent(GeoArmorRenderer renderer) { + this.renderer = renderer; + } + + /** + * Returns the renderer for this event + */ + @Override + public GeoArmorRenderer getRenderer() { + return this.renderer; + } + + /** + * Shortcut method for retrieving the entity being rendered + */ + @Nullable + public net.minecraft.world.entity.Entity getEntity() { + return getRenderer().getCurrentEntity(); + } + + /** + * Shortcut method for retrieving the ItemStack relevant to the armor piece being rendered + */ + @Nullable + public ItemStack getItemStack() { + return getRenderer().getCurrentStack(); + } + + /** + * Shortcut method for retrieving the equipped slot of the armor piece being rendered + */ + @Nullable + public EquipmentSlot getEquipmentSlot() { + return getRenderer().getCurrentSlot(); + } + + /** + * Pre-render event for armor pieces being rendered by {@link GeoArmorRenderer}.
+ * This event is called before rendering, but after {@link GeoRenderer#preRender}
+ *
+ * This event is cancellable.
+ * If the event is cancelled by returning false in the {@link Listener}, the armor piece will not be rendered and the corresponding {@link Post} event will not be fired. + */ + public static class Pre extends GeoRenderArmorEvent { + public static final GeoRenderPhaseEventFactory.GeoRenderPhaseEvent EVENT = Services.GEO_RENDER_PHASE_EVENT_FACTORY.create(); + + private final PoseStack poseStack; + private final BakedGeoModel model; + private final MultiBufferSource bufferSource; + private final float partialTick; + private final int packedLight; + + public Pre(GeoArmorRenderer renderer, PoseStack poseStack, BakedGeoModel model, MultiBufferSource bufferSource, float partialTick, int packedLight) { + super(renderer); + + this.poseStack = poseStack; + this.model = model; + this.bufferSource = bufferSource; + this.partialTick = partialTick; + this.packedLight = packedLight; + } + + public PoseStack getPoseStack() { + return this.poseStack; + } + + public BakedGeoModel getModel() { + return this.model; + } + + public MultiBufferSource getBufferSource() { + return this.bufferSource; + } + + public float getPartialTick() { + return this.partialTick; + } + + public int getPackedLight() { + return this.packedLight; + } + + /** + * Event listener interface for the Armor.Pre GeoRenderEvent.
+ * Return false to cancel the render pass + */ + @FunctionalInterface + public interface Listener { + boolean handle(Pre event); + } + } + + /** + * Post-render event for armor pieces being rendered by {@link GeoEntityRenderer}.
+ * This event is called after {@link GeoRenderer#postRender} + */ + public static class Post extends GeoRenderArmorEvent { + public static final GeoRenderPhaseEventFactory.GeoRenderPhaseEvent EVENT = Services.GEO_RENDER_PHASE_EVENT_FACTORY.create(); + + private final PoseStack poseStack; + private final BakedGeoModel model; + private final MultiBufferSource bufferSource; + private final float partialTick; + private final int packedLight; + + public Post(GeoArmorRenderer renderer, PoseStack poseStack, BakedGeoModel model, MultiBufferSource bufferSource, float partialTick, int packedLight) { + super(renderer); + + this.poseStack = poseStack; + this.model = model; + this.bufferSource = bufferSource; + this.partialTick = partialTick; + this.packedLight = packedLight; + } + + public PoseStack getPoseStack() { + return this.poseStack; + } + + public BakedGeoModel getModel() { + return this.model; + } + + public MultiBufferSource getBufferSource() { + return this.bufferSource; + } + + public float getPartialTick() { + return this.partialTick; + } + + public int getPackedLight() { + return this.packedLight; + } + + /** + * Event listener interface for the Armor.Post GeoRenderEvent + */ + @FunctionalInterface + public interface Listener { + void handle(Post event); + } + } + + /** + * One-time event for a {@link GeoArmorRenderer} called on first initialisation.
+ * Use this event to add render layers to the renderer as needed + */ + public static class CompileRenderLayers extends GeoRenderArmorEvent { + public static final GeoRenderPhaseEventFactory.GeoRenderPhaseEvent EVENT = Services.GEO_RENDER_PHASE_EVENT_FACTORY.create(); + + public CompileRenderLayers(GeoArmorRenderer renderer) { + super(renderer); + } + + /** + * Adds a {@link GeoRenderLayer} to the renderer.
+ * Type-safety is not checked here, so ensure that your layer is compatible with this animatable and renderer + */ + public void addLayer(GeoRenderLayer renderLayer) { + getRenderer().addRenderLayer(renderLayer); + } + + /** + * Event listener interface for the Armor.CompileRenderLayers GeoRenderEvent + */ + @FunctionalInterface + public interface Listener { + void handle(CompileRenderLayers event); + } + } + } \ No newline at end of file diff --git a/common/src/main/java/mod/azure/azurelib/common/api/common/event/GeoRenderBlockEvent.java b/common/src/main/java/mod/azure/azurelib/common/api/common/event/GeoRenderBlockEvent.java new file mode 100644 index 0000000..fa864fa --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/api/common/event/GeoRenderBlockEvent.java @@ -0,0 +1,174 @@ +package mod.azure.azurelib.common.api.common.event; + +import com.mojang.blaze3d.vertex.PoseStack; +import mod.azure.azurelib.common.api.client.renderer.GeoBlockRenderer; +import mod.azure.azurelib.common.api.client.renderer.layer.GeoRenderLayer; +import mod.azure.azurelib.common.internal.common.cache.object.BakedGeoModel; +import mod.azure.azurelib.common.internal.common.event.GeoRenderEvent; +import mod.azure.azurelib.common.platform.Services; +import mod.azure.azurelib.common.platform.services.GeoRenderPhaseEventFactory; +import mod.azure.azurelib.common.internal.client.renderer.GeoRenderer; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.world.level.block.entity.BlockEntity; + +/** + * Renderer events for {@link BlockEntity BlockEntities} being rendered by {@link GeoBlockRenderer} + */ +public abstract class GeoRenderBlockEvent implements GeoRenderEvent { + private final GeoBlockRenderer renderer; + + public GeoRenderBlockEvent(GeoBlockRenderer renderer) { + this.renderer = renderer; + } + + /** + * Returns the renderer for this event + */ + @Override + public GeoBlockRenderer getRenderer() { + return this.renderer; + } + + /** + * Shortcut method for retrieving the block entity being rendered + */ + public BlockEntity getBlockEntity() { + return getRenderer().getAnimatable(); + } + + /** + * Pre-render event for block entities being rendered by {@link GeoBlockRenderer}.
+ * This event is called before rendering, but after {@link GeoRenderer#preRender}
+ *
+ * This event is cancellable.
+ * If the event is cancelled by returning false in the {@link Listener}, the block entity will not be rendered and the corresponding {@link Post} event will not be fired. + */ + public static class Pre extends GeoRenderBlockEvent { + public static final GeoRenderPhaseEventFactory.GeoRenderPhaseEvent EVENT = Services.GEO_RENDER_PHASE_EVENT_FACTORY.create(); + + private final PoseStack poseStack; + private final BakedGeoModel model; + private final MultiBufferSource bufferSource; + private final float partialTick; + private final int packedLight; + + public Pre(GeoBlockRenderer renderer, PoseStack poseStack, BakedGeoModel model, MultiBufferSource bufferSource, float partialTick, int packedLight) { + super(renderer); + + this.poseStack = poseStack; + this.model = model; + this.bufferSource = bufferSource; + this.partialTick = partialTick; + this.packedLight = packedLight; + } + + public PoseStack getPoseStack() { + return this.poseStack; + } + + public BakedGeoModel getModel() { + return this.model; + } + + public MultiBufferSource getBufferSource() { + return this.bufferSource; + } + + public float getPartialTick() { + return this.partialTick; + } + + public int getPackedLight() { + return this.packedLight; + } + + /** + * Event listener interface for the Block.Pre GeoRenderEvent.
+ * Return false to cancel the render pass + */ + @FunctionalInterface + public interface Listener { + boolean handle(Pre event); + } + } + + /** + * Post-render event for block entities being rendered by {@link GeoBlockRenderer}.
+ * This event is called after {@link GeoRenderer#postRender} + */ + public static class Post extends GeoRenderBlockEvent { + public static final GeoRenderPhaseEventFactory.GeoRenderPhaseEvent EVENT = Services.GEO_RENDER_PHASE_EVENT_FACTORY.create(); + + private final PoseStack poseStack; + private final BakedGeoModel model; + private final MultiBufferSource bufferSource; + private final float partialTick; + private final int packedLight; + + public Post(GeoBlockRenderer renderer, PoseStack poseStack, BakedGeoModel model, MultiBufferSource bufferSource, float partialTick, int packedLight) { + super(renderer); + + this.poseStack = poseStack; + this.model = model; + this.bufferSource = bufferSource; + this.partialTick = partialTick; + this.packedLight = packedLight; + } + + public PoseStack getPoseStack() { + return this.poseStack; + } + + public BakedGeoModel getModel() { + return this.model; + } + + public MultiBufferSource getBufferSource() { + return this.bufferSource; + } + + public float getPartialTick() { + return this.partialTick; + } + + public int getPackedLight() { + return this.packedLight; + } + + /** + * Event listener interface for the Block.Post GeoRenderEvent + */ + @FunctionalInterface + public interface Listener { + void handle(Post event); + } + } + + /** + * One-time event for a {@link GeoBlockRenderer} called on first initialisation.
+ * Use this event to add render layers to the renderer as needed + */ + public static class CompileRenderLayers extends GeoRenderBlockEvent { + public static final GeoRenderPhaseEventFactory.GeoRenderPhaseEvent EVENT = Services.GEO_RENDER_PHASE_EVENT_FACTORY.create(); + + public CompileRenderLayers(GeoBlockRenderer renderer) { + super(renderer); + } + + /** + * Adds a {@link GeoRenderLayer} to the renderer.
+ * Type-safety is not checked here, so ensure that your layer is compatible with this animatable and renderer + */ + public void addLayer(GeoRenderLayer renderLayer) { + getRenderer().addRenderLayer(renderLayer); + } + + /** + * Event listener interface for the Armor.CompileRenderLayers GeoRenderEvent + */ + @FunctionalInterface + public interface Listener { + void handle(CompileRenderLayers event); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/mod/azure/azurelib/common/api/common/event/GeoRenderEntityEvent.java b/common/src/main/java/mod/azure/azurelib/common/api/common/event/GeoRenderEntityEvent.java new file mode 100644 index 0000000..fd9eca0 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/api/common/event/GeoRenderEntityEvent.java @@ -0,0 +1,175 @@ +package mod.azure.azurelib.common.api.common.event; + +import com.mojang.blaze3d.vertex.PoseStack; +import mod.azure.azurelib.common.api.client.renderer.DynamicGeoEntityRenderer; +import mod.azure.azurelib.common.api.client.renderer.GeoEntityRenderer; +import mod.azure.azurelib.common.api.client.renderer.layer.GeoRenderLayer; +import mod.azure.azurelib.common.internal.common.cache.object.BakedGeoModel; +import mod.azure.azurelib.common.internal.common.event.GeoRenderEvent; +import mod.azure.azurelib.common.platform.Services; +import mod.azure.azurelib.common.platform.services.GeoRenderPhaseEventFactory; +import mod.azure.azurelib.common.internal.client.renderer.GeoRenderer; +import net.minecraft.client.renderer.MultiBufferSource; + +/** + * Renderer events for {@link net.minecraft.world.entity.Entity Entities} being rendered by {@link GeoEntityRenderer}, as well as + * {@link DynamicGeoEntityRenderer DynamicGeoEntityRenderer} + */ +public abstract class GeoRenderEntityEvent implements GeoRenderEvent { + private final GeoEntityRenderer renderer; + + public GeoRenderEntityEvent(GeoEntityRenderer renderer) { + this.renderer = renderer; + } + + /** + * Returns the renderer for this event + */ + @Override + public GeoEntityRenderer getRenderer() { + return this.renderer; + } + + /** + * Shortcut method for retrieving the entity being rendered + */ + public net.minecraft.world.entity.Entity getEntity() { + return this.renderer.getAnimatable(); + } + + /** + * Pre-render event for entities being rendered by {@link GeoEntityRenderer}.
+ * This event is called before rendering, but after {@link GeoRenderer#preRender}
+ *
+ * This event is cancellable.
+ * If the event is cancelled by returning false in the {@link Listener}, the entity will not be rendered and the corresponding {@link Post} event will not be fired. + */ + public static class Pre extends GeoRenderEntityEvent { + public static final GeoRenderPhaseEventFactory.GeoRenderPhaseEvent EVENT = Services.GEO_RENDER_PHASE_EVENT_FACTORY.create(); + + private final PoseStack poseStack; + private final BakedGeoModel model; + private final MultiBufferSource bufferSource; + private final float partialTick; + private final int packedLight; + + public Pre(GeoEntityRenderer renderer, PoseStack poseStack, BakedGeoModel model, MultiBufferSource bufferSource, float partialTick, int packedLight) { + super(renderer); + + this.poseStack = poseStack; + this.model = model; + this.bufferSource = bufferSource; + this.partialTick = partialTick; + this.packedLight = packedLight; + } + + public PoseStack getPoseStack() { + return this.poseStack; + } + + public BakedGeoModel getModel() { + return this.model; + } + + public MultiBufferSource getBufferSource() { + return this.bufferSource; + } + + public float getPartialTick() { + return this.partialTick; + } + + public int getPackedLight() { + return this.packedLight; + } + + /** + * Event listener interface for the Armor.Pre GeoRenderEvent.
+ * Return false to cancel the render pass + */ + @FunctionalInterface + public interface Listener { + boolean handle(Pre event); + } + } + + /** + * Post-render event for entities being rendered by {@link GeoEntityRenderer}.
+ * This event is called after {@link GeoRenderer#postRender} + */ + public static class Post extends GeoRenderEntityEvent { + public static final GeoRenderPhaseEventFactory.GeoRenderPhaseEvent EVENT = Services.GEO_RENDER_PHASE_EVENT_FACTORY.create(); + + private final PoseStack poseStack; + private final BakedGeoModel model; + private final MultiBufferSource bufferSource; + private final float partialTick; + private final int packedLight; + + public Post(GeoEntityRenderer renderer, PoseStack poseStack, BakedGeoModel model, MultiBufferSource bufferSource, float partialTick, int packedLight) { + super(renderer); + + this.poseStack = poseStack; + this.model = model; + this.bufferSource = bufferSource; + this.partialTick = partialTick; + this.packedLight = packedLight; + } + + public PoseStack getPoseStack() { + return this.poseStack; + } + + public BakedGeoModel getModel() { + return this.model; + } + + public MultiBufferSource getBufferSource() { + return this.bufferSource; + } + + public float getPartialTick() { + return this.partialTick; + } + + public int getPackedLight() { + return this.packedLight; + } + + /** + * Event listener interface for the Entity.Post GeoRenderEvent + */ + @FunctionalInterface + public interface Listener { + void handle(Post event); + } + } + + /** + * One-time event for a {@link GeoEntityRenderer} called on first initialisation.
+ * Use this event to add render layers to the renderer as needed + */ + public static class CompileRenderLayers extends GeoRenderEntityEvent { + public static final GeoRenderPhaseEventFactory.GeoRenderPhaseEvent EVENT = Services.GEO_RENDER_PHASE_EVENT_FACTORY.create(); + + public CompileRenderLayers(GeoEntityRenderer renderer) { + super(renderer); + } + + /** + * Adds a {@link GeoRenderLayer} to the renderer.
+ * Type-safety is not checked here, so ensure that your layer is compatible with this animatable and renderer + */ + public void addLayer(GeoRenderLayer renderLayer) { + getRenderer().addRenderLayer(renderLayer); + } + + /** + * Event listener interface for the Entity.CompileRenderLayers GeoRenderEvent + */ + @FunctionalInterface + public interface Listener { + void handle(CompileRenderLayers event); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/mod/azure/azurelib/common/api/common/event/GeoRenderItemEvent.java b/common/src/main/java/mod/azure/azurelib/common/api/common/event/GeoRenderItemEvent.java new file mode 100644 index 0000000..9875ad1 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/api/common/event/GeoRenderItemEvent.java @@ -0,0 +1,174 @@ +package mod.azure.azurelib.common.api.common.event; + +import com.mojang.blaze3d.vertex.PoseStack; +import mod.azure.azurelib.common.api.client.renderer.GeoItemRenderer; +import mod.azure.azurelib.common.api.client.renderer.layer.GeoRenderLayer; +import mod.azure.azurelib.common.internal.common.cache.object.BakedGeoModel; +import mod.azure.azurelib.common.internal.common.event.GeoRenderEvent; +import mod.azure.azurelib.common.platform.Services; +import mod.azure.azurelib.common.platform.services.GeoRenderPhaseEventFactory; +import mod.azure.azurelib.common.internal.client.renderer.GeoRenderer; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.world.item.ItemStack; + +/** + * Renderer events for {@link ItemStack Items} being rendered by {@link GeoItemRenderer} + */ +public abstract class GeoRenderItemEvent implements GeoRenderEvent { + private final GeoItemRenderer renderer; + + public GeoRenderItemEvent(GeoItemRenderer renderer) { + this.renderer = renderer; + } + + /** + * Returns the renderer for this event + */ + @Override + public GeoItemRenderer getRenderer() { + return this.renderer; + } + + /** + * Shortcut method for retrieving the ItemStack being rendered + */ + public ItemStack getItemStack() { + return getRenderer().getCurrentItemStack(); + } + + /** + * Pre-render event for armor being rendered by {@link GeoItemRenderer}.
+ * This event is called before rendering, but after {@link GeoRenderer#preRender}
+ *
+ * This event is cancellable.
+ * If the event is cancelled by returning false in the {@link Listener}, the ItemStack will not be rendered and the corresponding {@link Post} event will not be fired. + */ + public static class Pre extends GeoRenderItemEvent { + public static final GeoRenderPhaseEventFactory.GeoRenderPhaseEvent EVENT = Services.GEO_RENDER_PHASE_EVENT_FACTORY.create(); + + private final PoseStack poseStack; + private final BakedGeoModel model; + private final MultiBufferSource bufferSource; + private final float partialTick; + private final int packedLight; + + public Pre(GeoItemRenderer renderer, PoseStack poseStack, BakedGeoModel model, MultiBufferSource bufferSource, float partialTick, int packedLight) { + super(renderer); + + this.poseStack = poseStack; + this.model = model; + this.bufferSource = bufferSource; + this.partialTick = partialTick; + this.packedLight = packedLight; + } + + public PoseStack getPoseStack() { + return this.poseStack; + } + + public BakedGeoModel getModel() { + return this.model; + } + + public MultiBufferSource getBufferSource() { + return this.bufferSource; + } + + public float getPartialTick() { + return this.partialTick; + } + + public int getPackedLight() { + return this.packedLight; + } + + /** + * Event listener interface for the Item.Pre GeoRenderEvent.
+ * Return false to cancel the render pass + */ + @FunctionalInterface + public interface Listener { + boolean handle(Pre event); + } + } + + /** + * Post-render event for ItemStacks being rendered by {@link GeoItemRenderer}.
+ * This event is called after {@link GeoRenderer#postRender} + */ + public static class Post extends GeoRenderItemEvent { + public static final GeoRenderPhaseEventFactory.GeoRenderPhaseEvent EVENT = Services.GEO_RENDER_PHASE_EVENT_FACTORY.create(); + + private final PoseStack poseStack; + private final BakedGeoModel model; + private final MultiBufferSource bufferSource; + private final float partialTick; + private final int packedLight; + + public Post(GeoItemRenderer renderer, PoseStack poseStack, BakedGeoModel model, MultiBufferSource bufferSource, float partialTick, int packedLight) { + super(renderer); + + this.poseStack = poseStack; + this.model = model; + this.bufferSource = bufferSource; + this.partialTick = partialTick; + this.packedLight = packedLight; + } + + public PoseStack getPoseStack() { + return this.poseStack; + } + + public BakedGeoModel getModel() { + return this.model; + } + + public MultiBufferSource getBufferSource() { + return this.bufferSource; + } + + public float getPartialTick() { + return this.partialTick; + } + + public int getPackedLight() { + return this.packedLight; + } + + /** + * Event listener interface for the Item.Post GeoRenderEvent + */ + @FunctionalInterface + public interface Listener { + void handle(Post event); + } + } + + /** + * One-time event for a {@link GeoItemRenderer} called on first initialisation.
+ * Use this event to add render layers to the renderer as needed + */ + public static class CompileRenderLayers extends GeoRenderItemEvent { + public static final GeoRenderPhaseEventFactory.GeoRenderPhaseEvent EVENT = Services.GEO_RENDER_PHASE_EVENT_FACTORY.create(); + + public CompileRenderLayers(GeoItemRenderer renderer) { + super(renderer); + } + + /** + * Adds a {@link GeoRenderLayer} to the renderer.
+ * Type-safety is not checked here, so ensure that your layer is compatible with this animatable and renderer + */ + public void addLayer(GeoRenderLayer renderLayer) { + getRenderer().addRenderLayer(renderLayer); + } + + /** + * Event listener interface for the Item.CompileRenderLayers GeoRenderEvent + */ + @FunctionalInterface + public interface Listener { + void handle(CompileRenderLayers event); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/mod/azure/azurelib/common/api/common/event/GeoRenderObjectEvent.java b/common/src/main/java/mod/azure/azurelib/common/api/common/event/GeoRenderObjectEvent.java new file mode 100644 index 0000000..3c800b7 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/api/common/event/GeoRenderObjectEvent.java @@ -0,0 +1,167 @@ +package mod.azure.azurelib.common.api.common.event; + +import com.mojang.blaze3d.vertex.PoseStack; +import mod.azure.azurelib.common.api.client.renderer.GeoObjectRenderer; +import mod.azure.azurelib.common.api.client.renderer.layer.GeoRenderLayer; +import mod.azure.azurelib.common.internal.common.cache.object.BakedGeoModel; +import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable; +import mod.azure.azurelib.common.internal.common.event.GeoRenderEvent; +import mod.azure.azurelib.common.platform.Services; +import mod.azure.azurelib.common.platform.services.GeoRenderPhaseEventFactory; +import mod.azure.azurelib.common.internal.client.renderer.GeoRenderer; +import net.minecraft.client.renderer.MultiBufferSource; + +/** + * Renderer events for miscellaneous {@link GeoAnimatable animatables} being rendered by {@link GeoObjectRenderer} + */ +public abstract class GeoRenderObjectEvent implements GeoRenderEvent { + private final GeoObjectRenderer renderer; + + protected GeoRenderObjectEvent(GeoObjectRenderer renderer) { + this.renderer = renderer; + } + + /** + * Returns the renderer for this event + */ + @Override + public GeoObjectRenderer getRenderer() { + return this.renderer; + } + + /** + * Pre-render event for miscellaneous animatables being rendered by {@link GeoObjectRenderer}.
+ * This event is called before rendering, but after {@link GeoRenderer#preRender}
+ *
+ * This event is cancellable.
+ * If the event is cancelled by returning false in the {@link Listener}, the object will not be rendered and the corresponding {@link Post} event will not be fired. + */ + public static class Pre extends GeoRenderObjectEvent { + public static final GeoRenderPhaseEventFactory.GeoRenderPhaseEvent EVENT = Services.GEO_RENDER_PHASE_EVENT_FACTORY.create(); + + private final PoseStack poseStack; + private final BakedGeoModel model; + private final MultiBufferSource bufferSource; + private final float partialTick; + private final int packedLight; + + public Pre(GeoObjectRenderer renderer, PoseStack poseStack, BakedGeoModel model, MultiBufferSource bufferSource, float partialTick, int packedLight) { + super(renderer); + + this.poseStack = poseStack; + this.model = model; + this.bufferSource = bufferSource; + this.partialTick = partialTick; + this.packedLight = packedLight; + } + + public PoseStack getPoseStack() { + return this.poseStack; + } + + public BakedGeoModel getModel() { + return this.model; + } + + public MultiBufferSource getBufferSource() { + return this.bufferSource; + } + + public float getPartialTick() { + return this.partialTick; + } + + public int getPackedLight() { + return this.packedLight; + } + + /** + * Event listener interface for the Object.Pre GeoRenderEvent.
+ * Return false to cancel the render pass + */ + @FunctionalInterface + public interface Listener { + boolean handle(Pre event); + } + } + + /** + * Post-render event for miscellaneous animatables being rendered by {@link GeoObjectRenderer}.
+ * This event is called after {@link GeoRenderer#postRender} + */ + public static class Post extends GeoRenderObjectEvent { + public static final GeoRenderPhaseEventFactory.GeoRenderPhaseEvent EVENT = Services.GEO_RENDER_PHASE_EVENT_FACTORY.create(); + + private final PoseStack poseStack; + private final BakedGeoModel model; + private final MultiBufferSource bufferSource; + private final float partialTick; + private final int packedLight; + + public Post(GeoObjectRenderer renderer, PoseStack poseStack, BakedGeoModel model, MultiBufferSource bufferSource, float partialTick, int packedLight) { + super(renderer); + + this.poseStack = poseStack; + this.model = model; + this.bufferSource = bufferSource; + this.partialTick = partialTick; + this.packedLight = packedLight; + } + + public PoseStack getPoseStack() { + return this.poseStack; + } + + public BakedGeoModel getModel() { + return this.model; + } + + public MultiBufferSource getBufferSource() { + return this.bufferSource; + } + + public float getPartialTick() { + return this.partialTick; + } + + public int getPackedLight() { + return this.packedLight; + } + + /** + * Event listener interface for the Object.Post GeoRenderEvent + */ + @FunctionalInterface + public interface Listener { + void handle(Post event); + } + } + + /** + * One-time event for a {@link GeoObjectRenderer} called on first initialisation.
+ * Use this event to add render layers to the renderer as needed + */ + public static class CompileRenderLayers extends GeoRenderObjectEvent { + public static final GeoRenderPhaseEventFactory.GeoRenderPhaseEvent EVENT = Services.GEO_RENDER_PHASE_EVENT_FACTORY.create(); + + public CompileRenderLayers(GeoObjectRenderer renderer) { + super(renderer); + } + + /** + * Adds a {@link GeoRenderLayer} to the renderer.
+ * Type-safety is not checked here, so ensure that your layer is compatible with this animatable and renderer + */ + public void addLayer(GeoRenderLayer renderLayer) { + getRenderer().addRenderLayer(renderLayer); + } + + /** + * Event listener interface for the Object.CompileRenderLayers GeoRenderEvent + */ + @FunctionalInterface + public interface Listener { + void handle(CompileRenderLayers event); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/mod/azure/azurelib/common/api/common/event/GeoRenderReplacedEntityEvent.java b/common/src/main/java/mod/azure/azurelib/common/api/common/event/GeoRenderReplacedEntityEvent.java new file mode 100644 index 0000000..df22564 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/api/common/event/GeoRenderReplacedEntityEvent.java @@ -0,0 +1,174 @@ +package mod.azure.azurelib.common.api.common.event; + +import com.mojang.blaze3d.vertex.PoseStack; +import mod.azure.azurelib.common.api.common.animatable.GeoReplacedEntity; +import mod.azure.azurelib.common.api.client.renderer.GeoReplacedEntityRenderer; +import mod.azure.azurelib.common.api.client.renderer.layer.GeoRenderLayer; +import mod.azure.azurelib.common.internal.common.cache.object.BakedGeoModel; +import mod.azure.azurelib.common.internal.common.event.GeoRenderEvent; +import mod.azure.azurelib.common.platform.Services; +import mod.azure.azurelib.common.platform.services.GeoRenderPhaseEventFactory; +import mod.azure.azurelib.common.internal.client.renderer.GeoRenderer; +import net.minecraft.client.renderer.MultiBufferSource; + +/** + * Renderer events for miscellaneous {@link GeoReplacedEntity replaced entities} being rendered by {@link GeoReplacedEntityRenderer} + */ +public abstract class GeoRenderReplacedEntityEvent implements GeoRenderEvent { + private final GeoReplacedEntityRenderer renderer; + + protected GeoRenderReplacedEntityEvent(GeoReplacedEntityRenderer renderer) { + this.renderer = renderer; + } + + /** + * Returns the renderer for this event + */ + @Override + public GeoReplacedEntityRenderer getRenderer() { + return this.renderer; + } + + /** + * Shortcut method to get the Entity currently being rendered + */ + public net.minecraft.world.entity.Entity getReplacedEntity() { + return getRenderer().getCurrentEntity(); + } + + /** + * Pre-render event for replaced entities being rendered by {@link GeoReplacedEntityRenderer}.
+ * This event is called before rendering, but after {@link GeoRenderer#preRender}
+ *
+ * This event is cancellable.
+ * If the event is cancelled by returning false in the {@link Listener}, the entity will not be rendered and the corresponding {@link Post} event will not be fired. + */ + public static class Pre extends GeoRenderReplacedEntityEvent { + public static final GeoRenderPhaseEventFactory.GeoRenderPhaseEvent EVENT = Services.GEO_RENDER_PHASE_EVENT_FACTORY.create(); + + private final PoseStack poseStack; + private final BakedGeoModel model; + private final MultiBufferSource bufferSource; + private final float partialTick; + private final int packedLight; + + public Pre(GeoReplacedEntityRenderer renderer, PoseStack poseStack, BakedGeoModel model, MultiBufferSource bufferSource, float partialTick, int packedLight) { + super(renderer); + + this.poseStack = poseStack; + this.model = model; + this.bufferSource = bufferSource; + this.partialTick = partialTick; + this.packedLight = packedLight; + } + + public PoseStack getPoseStack() { + return this.poseStack; + } + + public BakedGeoModel getModel() { + return this.model; + } + + public MultiBufferSource getBufferSource() { + return this.bufferSource; + } + + public float getPartialTick() { + return this.partialTick; + } + + public int getPackedLight() { + return this.packedLight; + } + + /** + * Event listener interface for the ReplacedEntity.Pre GeoRenderEvent.
+ * Return false to cancel the render pass + */ + @FunctionalInterface + public interface Listener { + boolean handle(Pre event); + } + } + + /** + * Post-render event for replaced entities being rendered by {@link GeoReplacedEntityRenderer}.
+ * This event is called after {@link GeoRenderer#postRender} + */ + public static class Post extends GeoRenderReplacedEntityEvent { + public static final GeoRenderPhaseEventFactory.GeoRenderPhaseEvent EVENT = Services.GEO_RENDER_PHASE_EVENT_FACTORY.create(); + + private final PoseStack poseStack; + private final BakedGeoModel model; + private final MultiBufferSource bufferSource; + private final float partialTick; + private final int packedLight; + + public Post(GeoReplacedEntityRenderer renderer, PoseStack poseStack, BakedGeoModel model, MultiBufferSource bufferSource, float partialTick, int packedLight) { + super(renderer); + + this.poseStack = poseStack; + this.model = model; + this.bufferSource = bufferSource; + this.partialTick = partialTick; + this.packedLight = packedLight; + } + + public PoseStack getPoseStack() { + return this.poseStack; + } + + public BakedGeoModel getModel() { + return this.model; + } + + public MultiBufferSource getBufferSource() { + return this.bufferSource; + } + + public float getPartialTick() { + return this.partialTick; + } + + public int getPackedLight() { + return this.packedLight; + } + + /** + * Event listener interface for the ReplacedEntity.Post GeoRenderEvent + */ + @FunctionalInterface + public interface Listener { + void handle(Post event); + } + } + + /** + * One-time event for a {@link GeoReplacedEntityRenderer} called on first initialisation.
+ * Use this event to add render layers to the renderer as needed + */ + public static class CompileRenderLayers extends GeoRenderReplacedEntityEvent { + public static final GeoRenderPhaseEventFactory.GeoRenderPhaseEvent EVENT = Services.GEO_RENDER_PHASE_EVENT_FACTORY.create(); + + public CompileRenderLayers(GeoReplacedEntityRenderer renderer) { + super(renderer); + } + + /** + * Adds a {@link GeoRenderLayer} to the renderer.
+ * Type-safety is not checked here, so ensure that your layer is compatible with this animatable and renderer + */ + public void addLayer(GeoRenderLayer renderLayer) { + getRenderer().addRenderLayer(renderLayer); + } + + /** + * Event listener interface for the ReplacedEntity.CompileRenderLayers GeoRenderEvent + */ + @FunctionalInterface + public interface Listener { + void handle(CompileRenderLayers event); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/mod/azure/azurelib/common/api/common/helper/AzureGunTypeEnum.java b/common/src/main/java/mod/azure/azurelib/common/api/common/helper/AzureGunTypeEnum.java new file mode 100644 index 0000000..4d67bee --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/api/common/helper/AzureGunTypeEnum.java @@ -0,0 +1,4 @@ +package mod.azure.azurelib.common.api.common.helper; + +public enum AzureGunTypeEnum { +} diff --git a/common/src/main/java/mod/azure/azurelib/common/api/common/helper/CommonUtils.java b/common/src/main/java/mod/azure/azurelib/common/api/common/helper/CommonUtils.java new file mode 100644 index 0000000..ea52fa5 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/api/common/helper/CommonUtils.java @@ -0,0 +1,111 @@ +package mod.azure.azurelib.common.api.common.helper; + +import mod.azure.azurelib.common.internal.common.blocks.TickingLightEntity; +import mod.azure.azurelib.common.internal.common.util.AzureLibUtil; +import mod.azure.azurelib.common.platform.Services; +import mod.azure.azurelib.common.platform.services.IPlatformHelper; +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.world.effect.MobEffect; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.entity.AreaEffectCloud; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.entity.projectile.Projectile; +import net.minecraft.world.entity.projectile.ProjectileUtil; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.EntityHitResult; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; + +public record CommonUtils() { + + /** + * Summons an Area of Effect Cloud with the set particle, y offset, radius, duration, and effect options. + * + * @param entity The Entity summoning the AoE + * @param particle Sets the Particle + * @param yOffset Set the yOffset if wanted + * @param duration Sets the duration of the AoE + * @param radius Sets the radius of the AoE + * @param hasEffect Should this have an effect? + * @param effect If it should effect, what effect? + * @param effectTime How long the effect should be applied for? + */ + public static void summonAoE(LivingEntity entity, ParticleOptions particle, int yOffset, int duration, float radius, boolean hasEffect, @Nullable MobEffect effect, int effectTime) { + var areaEffectCloudEntity = new AreaEffectCloud(entity.level(), entity.getX(), entity.getY() + yOffset, + entity.getZ()); + areaEffectCloudEntity.setRadius(radius); + areaEffectCloudEntity.setDuration(duration); + areaEffectCloudEntity.setParticle(particle); + areaEffectCloudEntity.setRadiusPerTick( + -areaEffectCloudEntity.getRadius() / areaEffectCloudEntity.getDuration()); + if (hasEffect && effect != null && !entity.hasEffect(effect)) + areaEffectCloudEntity.addEffect(new MobEffectInstance(effect, effectTime, 0)); + entity.level().addFreshEntity(areaEffectCloudEntity); + } + + /** + * Call wherever you are firing weapon to place the half tick light-block, making sure do so only on the server. + * + * @param entity Usually the player or mob that is using the weapon + * @param isInWaterBlock Checks if it's in a water block to refresh faster. + */ + public static void spawnLightSource(Entity entity, boolean isInWaterBlock) { + BlockPos lightBlockPos = null; + if (lightBlockPos == null) { + lightBlockPos = AzureLibUtil.findFreeSpace(entity.level(), entity.blockPosition(), 2); + if (lightBlockPos == null) return; + entity.level().setBlockAndUpdate(lightBlockPos, + Services.PLATFORM.getTickingLightBlock().defaultBlockState()); + } else if (AzureLibUtil.checkDistance(lightBlockPos, entity.blockPosition(), + 2) && entity.level().getBlockEntity(lightBlockPos) instanceof TickingLightEntity tickingLightEntity) { + tickingLightEntity.refresh(isInWaterBlock ? 20 : 0); + } + } + + /** + * Hitscan between the player and the target. Useful for doing damage + * TODO: Fix why it doesn't work if going about shoulder level on zombie sized mobs + * + * @param livingEntity The Shooter Entity. + * @param range The block distance it can fire. + * @param ticks The amount of ticks to take, usually will be 1.0f + * @return returns a EntityHitResult + */ + public static EntityHitResult hitscanTrace(LivingEntity livingEntity, double range, float ticks) { + var look = livingEntity.getViewVector(ticks); + var start = livingEntity.getEyePosition(ticks); + var end = new Vec3(livingEntity.getX() + look.x * range, livingEntity.getEyeY() + look.y * range, + livingEntity.getZ() + look.z * range); + var traceDistance = livingEntity.level().clip( + new ClipContext(start, end, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, + livingEntity)).getLocation().distanceToSqr(end); + for (var possible : livingEntity.level().getEntities(livingEntity, + livingEntity.getBoundingBox().expandTowards(look.scale(traceDistance)).expandTowards(3.0D, 3.0D, 3.0D), + (entity -> !entity.isSpectator() && entity.isPickable() && entity instanceof LivingEntity))) { + var clip = possible.getBoundingBox().inflate(0.3D).clip(start, end); + if (clip.isPresent() && start.distanceToSqr(clip.get()) < traceDistance) + return ProjectileUtil.getEntityHitResult(livingEntity.level(), livingEntity, start, end, + livingEntity.getBoundingBox().expandTowards(look.scale(traceDistance)).inflate(3.0D, 3.0D, + 3.0D), + target -> !target.isSpectator() && livingEntity.isAttackable() && livingEntity.hasLineOfSight( + target)); + } + return null; + } + + /** + * Handles setting fire to targets when using the {@link IPlatformHelper#getIncendairyenchament()} + * + * @param projectile The Projectile being used + */ + public static void setOnFire(Projectile projectile) { + if (projectile.isOnFire()) + projectile.level().getEntitiesOfClass(LivingEntity.class, projectile.getBoundingBox().inflate(2)).forEach( + e -> { + if (e.isAlive() && !(e instanceof Player)) e.setRemainingFireTicks(90); + }); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/api/common/interfaces/AzureTicker.java b/common/src/main/java/mod/azure/azurelib/common/api/common/interfaces/AzureTicker.java new file mode 100644 index 0000000..ac0e1fb --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/api/common/interfaces/AzureTicker.java @@ -0,0 +1,75 @@ +package mod.azure.azurelib.common.api.common.interfaces; + +import mod.azure.azurelib.common.api.common.entities.AzureVibrationUser; +import mod.azure.azurelib.common.platform.Services; +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.VibrationParticleOption; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.gameevent.vibrations.VibrationInfo; +import net.minecraft.world.level.gameevent.vibrations.VibrationSystem.Data; +import net.minecraft.world.level.gameevent.vibrations.VibrationSystem.Listener; +import net.minecraft.world.level.gameevent.vibrations.VibrationSystem.User; + +/** + * Custom class for use with {@link AzureVibrationUser} + */ +public interface AzureTicker { + public static void tick(Level level, Data data, User user) { + if (!(level instanceof ServerLevel)) { + return; + } + ServerLevel serverLevel = (ServerLevel) level; + if (data.getCurrentVibration() == null) { + AzureTicker.trySelectAndScheduleVibration(serverLevel, data, user); + } + if (data.getCurrentVibration() == null) { + return; + } + boolean bl = data.getTravelTimeInTicks() > 0; + data.decrementTravelTime(); + if (data.getTravelTimeInTicks() <= 0) { + bl = AzureTicker.receiveVibration(serverLevel, data, user, data.getCurrentVibration()); + } + if (bl) { + user.onDataChanged(); + } + } + + private static void trySelectAndScheduleVibration(ServerLevel serverLevel, Data data, User user) { + data.getSelectionStrategy().chosenCandidate(serverLevel.getGameTime()).ifPresent(vibrationInfo -> { + data.setCurrentVibration(vibrationInfo); + var vec3 = vibrationInfo.pos(); + data.setTravelTimeInTicks(user.calculateTravelTimeInTicks(vibrationInfo.distance())); + if (Services.PLATFORM.isDevelopmentEnvironment()) + serverLevel.sendParticles(new VibrationParticleOption(user.getPositionSource(), data.getTravelTimeInTicks()), vec3.x, vec3.y, vec3.z, 1, 0.0, 0.0, 0.0, 0.0); + user.onDataChanged(); + data.getSelectionStrategy().startOver(); + }); + } + + private static boolean receiveVibration(ServerLevel serverLevel, Data data, User user, VibrationInfo vibrationInfo) { + var blockPos = BlockPos.containing(vibrationInfo.pos()); + var blockPos2 = user.getPositionSource().getPosition(serverLevel).map(BlockPos::containing).orElse(blockPos); + if (user.requiresAdjacentChunksToBeTicking() && !AzureTicker.areAdjacentChunksTicking(serverLevel, blockPos2)) { + return false; + } + user.onReceiveVibration(serverLevel, blockPos, vibrationInfo.gameEvent(), vibrationInfo.getEntity(serverLevel).orElse(null), vibrationInfo.getProjectileOwner(serverLevel).orElse(null), Listener.distanceBetweenInBlocks(blockPos, blockPos2)); + data.setCurrentVibration(null); + return true; + } + + private static boolean areAdjacentChunksTicking(Level level, BlockPos blockPos) { + var chunkPos = new ChunkPos(blockPos); + for (var i = chunkPos.x - 1; i < chunkPos.x + 1; ++i) { + for (var j = chunkPos.z - 1; j < chunkPos.z + 1; ++j) { + var chunkAccess = level.getChunkSource().getChunkNow(i, j); + if (chunkAccess != null && level.shouldTickBlocksAt(chunkAccess.getPos().toLong())) + continue; + return false; + } + } + return true; + } +} \ No newline at end of file diff --git a/common/src/main/java/mod/azure/azurelib/common/api/common/interfaces/Growable.java b/common/src/main/java/mod/azure/azurelib/common/api/common/interfaces/Growable.java new file mode 100644 index 0000000..b1a2203 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/api/common/interfaces/Growable.java @@ -0,0 +1,46 @@ +package mod.azure.azurelib.common.api.common.interfaces; + +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; + +import static java.lang.Math.min; + +/** + * Interface of having entities grow into entity based a growth value. + * @author Boston Vanseghi + */ +public interface Growable { + float getGrowth(); + + void setGrowth(float growth); + + float getMaxGrowth(); + + default void grow(LivingEntity entity, float amount) { + setGrowth(min(getGrowth() + amount, getMaxGrowth())); + if (getGrowth() >= getMaxGrowth()) + growUp(entity); + } + + LivingEntity growInto(); + + default void growUp(LivingEntity entity) { + var world = entity.level(); + if (!world.isClientSide()) { + var newEntity = growInto(); + if (newEntity == null) + return; + newEntity.moveTo(entity.blockPosition(), entity.getYRot(), entity.getXRot()); + world.addFreshEntity(newEntity); + entity.remove(Entity.RemovalReason.DISCARDED); + } + } + + default float getGrowthNeededUntilGrowUp() { + return getMaxGrowth() - getGrowth(); + } + + default float getGrowthMultiplier() { + return 1.0f; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/api/common/items/AzureBaseGunItem.java b/common/src/main/java/mod/azure/azurelib/common/api/common/items/AzureBaseGunItem.java new file mode 100644 index 0000000..a26e52b --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/api/common/items/AzureBaseGunItem.java @@ -0,0 +1,163 @@ +package mod.azure.azurelib.common.api.common.items; + +import mod.azure.azurelib.common.api.common.animatable.GeoItem; +import mod.azure.azurelib.common.api.common.helper.AzureGunTypeEnum; +import mod.azure.azurelib.common.api.common.helper.CommonUtils; +import mod.azure.azurelib.common.internal.common.animatable.SingletonGeoAnimatable; +import mod.azure.azurelib.common.internal.common.core.animatable.instance.AnimatableInstanceCache; +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.AnimationController; +import mod.azure.azurelib.common.internal.common.core.animation.RawAnimation; +import mod.azure.azurelib.common.internal.common.core.object.PlayState; +import mod.azure.azurelib.common.internal.common.util.AzureLibUtil; +import net.minecraft.ChatFormatting; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResultHolder; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.function.Supplier; + +public abstract class AzureBaseGunItem extends Item implements GeoItem { + protected final AzureGunTypeEnum azureGunTypeEnum; + private static final String firing = "firing"; + private static final String controller = "controller"; + private final Supplier renderProvider = GeoItem.makeRenderer(this); + private final AnimatableInstanceCache cache = AzureLibUtil.createInstanceCache(this); + + public AzureBaseGunItem(AzureGunTypeEnum azureGunTypeEnum, int maxClipSize) { + super(new Properties().stacksTo(1).durability(maxClipSize + 1)); + this.azureGunTypeEnum = azureGunTypeEnum; + SingletonGeoAnimatable.registerSyncedAnimatable(this); + } + + public AzureGunTypeEnum getAzureGunTypeEnum() { + return this.azureGunTypeEnum; + } + + public Item getAmmoType() { + return null; + } + + public SoundEvent getReloadSound() { + return null; + } + + public SoundEvent getFiringSound() { + return null; + } + + public int getReloadAmount() { + return 1; + } + + public int getCoolDown() { + return 1; + } + + public int getReloadCoolDown() { + return 1; + } + + private void singleFire(@NotNull ItemStack itemStack, @NotNull Level level, @NotNull Player player) { + player.getCooldowns().addCooldown(this, this.getCoolDown()); + CommonUtils.spawnLightSource(player, player.level().isWaterAt(player.blockPosition())); + itemStack.hurtAndBreak(1, player, p -> p.broadcastBreakEvent(player.getUsedItemHand())); + } + + public static void shoot(Player player) { + if (player.getMainHandItem().getDamageValue() < (player.getMainHandItem().getMaxDamage() - 1) && player.getMainHandItem().getItem() instanceof AzureBaseGunItem gunBase) { + if (!player.getCooldowns().isOnCooldown(player.getMainHandItem().getItem())) + gunBase.singleFire(player.getMainHandItem(), player.level(), player); + } else { + player.level().playSound(null, player.getX(), player.getY(), player.getZ(), SoundEvents.LEVER_CLICK, + SoundSource.PLAYERS, 0.25F, 1.3F); + } + } + + + /** + * Handles the item reloading. + * + * @param user Player who's reloading + * @param hand Currently only sets the {@link InteractionHand#MAIN_HAND} + */ + public static void reload(Player user, InteractionHand hand) { + if (user.getMainHandItem().getItem() instanceof AzureBaseGunItem gunBase) { + while (!user.isCreative() && user.getMainHandItem().getDamageValue() != 0 && user.getInventory().countItem( + gunBase.getAmmoType()) > 0) { + AzureLibUtil.removeAmmo(gunBase.getAmmoType(), user); + user.getCooldowns().addCooldown(gunBase, gunBase.getReloadCoolDown()); + user.getMainHandItem().hurtAndBreak(-gunBase.getReloadAmount(), user, + s -> user.broadcastBreakEvent(hand)); + user.getMainHandItem().setPopTime(3); + if (gunBase.getReloadSound() != null) + user.level().playSound(null, user.getX(), user.getY(), user.getZ(), gunBase.getReloadSound(), + SoundSource.PLAYERS, 1.00F, 1.0F); + if (!user.level().isClientSide) { + gunBase.triggerAnim(user, + GeoItem.getOrAssignId(user.getItemInHand(hand), (ServerLevel) user.level()), + AzureBaseGunItem.controller, "reload"); + } + } + } + } + + @Override + public @NotNull InteractionResultHolder use(@NotNull Level world, Player user, @NotNull InteractionHand hand) { + final var itemStack = user.getItemInHand(hand); + user.startUsingItem(hand); + return InteractionResultHolder.consume(itemStack); + } + + @Override + public int getUseDuration(@NotNull ItemStack stack) { + return 72000; + } + + @Override + public boolean mineBlock(@NotNull ItemStack itemStack, @NotNull Level level, @NotNull BlockState blockState, @NotNull BlockPos blockPos, @NotNull LivingEntity livingEntity) { + return false; + } + + @Override + public void appendHoverText(ItemStack itemStack, Level level, List tooltip, @NotNull TooltipFlag tooltipFlag) { + tooltip.add(Component.translatable( + "Ammo: " + (itemStack.getMaxDamage() - itemStack.getDamageValue() - 1) + " / " + (itemStack.getMaxDamage() - 1)).withStyle( + ChatFormatting.ITALIC)); + super.appendHoverText(itemStack, level, tooltip, tooltipFlag); + } + + @Override + public Supplier getRenderProvider() { + return renderProvider; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, AzureBaseGunItem.controller, + event -> PlayState.CONTINUE).triggerableAnim(AzureBaseGunItem.firing, + RawAnimation.begin().then(AzureBaseGunItem.firing, Animation.LoopType.PLAY_ONCE)).triggerableAnim( + "reload", RawAnimation.begin().then("reload", Animation.LoopType.PLAY_ONCE))); + + } + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/api/common/items/AzureSpawnEgg.java b/common/src/main/java/mod/azure/azurelib/common/api/common/items/AzureSpawnEgg.java new file mode 100644 index 0000000..2ea492e --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/api/common/items/AzureSpawnEgg.java @@ -0,0 +1,21 @@ +package mod.azure.azurelib.common.api.common.items; + +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.SpawnEggItem; + +public class AzureSpawnEgg extends SpawnEggItem { + + /** + * TODO: Make egg work correctly for both loaders using this common version, currently only works correctly with Fabric. + * + * @param type Your registered Entity + * @param primaryColor Primary Egg Color + * @param secondaryColor Secondary Egg Color + */ + public AzureSpawnEgg(EntityType type, int primaryColor, int secondaryColor) { + super(type, primaryColor, secondaryColor, new Item.Properties().stacksTo(64)); + } + +} \ No newline at end of file diff --git a/common/src/main/java/mod/azure/azurelib/common/api/common/tags/AzureTags.java b/common/src/main/java/mod/azure/azurelib/common/api/common/tags/AzureTags.java new file mode 100644 index 0000000..fb6286b --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/api/common/tags/AzureTags.java @@ -0,0 +1,10 @@ +package mod.azure.azurelib.common.api.common.tags; + +import mod.azure.azurelib.common.internal.common.AzureLib; +import net.minecraft.core.registries.Registries; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.Item; + +public class AzureTags { + public static final TagKey GUNS = TagKey.create(Registries.ITEM, AzureLib.modResource("guns")); +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/client/AzureLibClient.java b/common/src/main/java/mod/azure/azurelib/common/internal/client/AzureLibClient.java new file mode 100644 index 0000000..71fabe2 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/client/AzureLibClient.java @@ -0,0 +1,67 @@ +package mod.azure.azurelib.common.internal.client; + +import mod.azure.azurelib.common.internal.client.config.screen.ConfigGroupScreen; +import mod.azure.azurelib.common.internal.client.config.screen.ConfigScreen; +import mod.azure.azurelib.common.internal.common.config.ConfigHolder; +import mod.azure.azurelib.common.api.common.config.Config; +import mod.azure.azurelib.common.internal.common.config.value.ConfigValue; +import net.minecraft.client.gui.screens.Screen; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Map; + +public final class AzureLibClient { + + /** + * You can obtain default config screen based on provided config class. + * + * @param configClass Your config class + * @param previous Previously open screen + * @return Either new config screen or {@code null} when no config exists for the provided class + */ + @Nullable + public static Screen getConfigScreen(Class configClass, Screen previous) { + Config cfg = configClass.getAnnotation(Config.class); + if (cfg == null) { + return null; + } + String id = cfg.id(); + return getConfigScreen(id, previous); + } + + /** + * You can obtain default config screen based on provided config ID. + * + * @param configId ID of your config + * @param previous Previously open screen + * @return Either new config screen or {@code null} when no config exists with the provided ID + */ + @Nullable + public static Screen getConfigScreen(String configId, Screen previous) { + return ConfigHolder.getConfig(configId).map(holder -> getConfigScreenForHolder(holder, previous)).orElse(null); + } + + /** + * Obtain group of multiple configs based on group ID. This is useful when you have multiple config files for your mod. + * + * @param group Group ID, usually mod ID + * @param previous Previously open screen + * @return Either new config group screen or null when no config exists under the provided group + */ + public static Screen getConfigScreenByGroup(String group, Screen previous) { + List> list = ConfigHolder.getConfigsByGroup(group); + if (list.isEmpty()) + return null; + return getConfigScreenByGroup(list, group, previous); + } + + public static Screen getConfigScreenForHolder(ConfigHolder holder, Screen previous) { + Map> valueMap = holder.getValueMap(); + return new ConfigScreen(holder.getConfigId(), holder.getConfigId(), valueMap, previous); + } + + public static Screen getConfigScreenByGroup(List> group, String groupId, Screen previous) { + return new ConfigGroupScreen(previous, groupId, group); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/client/RenderProvider.java b/common/src/main/java/mod/azure/azurelib/common/internal/client/RenderProvider.java new file mode 100644 index 0000000..8fbaa84 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/client/RenderProvider.java @@ -0,0 +1,52 @@ +package mod.azure.azurelib.common.internal.client; + +import mod.azure.azurelib.common.api.common.animatable.GeoItem; +import mod.azure.azurelib.common.internal.mixins.ItemRendererAccessor; +import net.minecraft.client.Minecraft; +import net.minecraft.client.model.HumanoidModel; +import net.minecraft.client.model.Model; +import net.minecraft.client.renderer.BlockEntityWithoutLevelRenderer; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; + +/** + * Internal interface for safely providing a custom renderer instances at runtime.
+ * This can be safely instantiated as a new anonymous class inside your {@link Item} class + */ +public interface RenderProvider { + RenderProvider DEFAULT = new RenderProvider() {}; + + static RenderProvider of(ItemStack itemStack) { + return of(itemStack.getItem()); + } + + static RenderProvider of(Item item) { + if(item instanceof GeoItem geoItem){ + return (RenderProvider)geoItem.getRenderProvider().get(); + } + + return DEFAULT; + } + + default BlockEntityWithoutLevelRenderer getCustomRenderer(){ + return ((ItemRendererAccessor)Minecraft.getInstance().getItemRenderer()).getBlockEntityRenderer(); + } + + + default Model getGenericArmorModel(LivingEntity livingEntity, ItemStack itemStack, EquipmentSlot equipmentSlot, HumanoidModel original) { + HumanoidModel replacement = getHumanoidArmorModel(livingEntity, itemStack, equipmentSlot, original); + + if (replacement != original) { + original.copyPropertiesTo(replacement); + return replacement; + } + + return original; + } + + default HumanoidModel getHumanoidArmorModel(LivingEntity livingEntity, ItemStack itemStack, EquipmentSlot equipmentSlot, HumanoidModel original) { + return original; + } +} \ No newline at end of file diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/client/config/ClientErrors.java b/common/src/main/java/mod/azure/azurelib/common/internal/client/config/ClientErrors.java new file mode 100644 index 0000000..7f6ada3 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/client/config/ClientErrors.java @@ -0,0 +1,42 @@ +package mod.azure.azurelib.common.internal.client.config; + +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; + +import java.util.regex.Pattern; + +import mod.azure.azurelib.common.internal.common.config.value.DecimalValue; +import mod.azure.azurelib.common.internal.common.config.value.IntegerValue; + +public final class ClientErrors { + + public static final MutableComponent CHAR_VALUE_EMPTY = Component.translatable("text.azurelib.error.character_value_empty"); + + private static final String KEY_NAN = "text.azurelib.error.nan"; + private static final String KEY_NUM_BOUNDS = "text.azurelib.error.num_bounds"; + private static final String KEY_MISMATCHED_PATTERN = "text.azurelib.error.pattern_mismatch"; + + public static MutableComponent notANumber(String value) { + return Component.translatable(KEY_NAN, value); + } + + public static MutableComponent outOfBounds(int i, IntegerValue.Range range) { + return Component.translatable(KEY_NUM_BOUNDS, i, range.min(), range.max()); + } + + public static MutableComponent outOfBounds(long i, IntegerValue.Range range) { + return Component.translatable(KEY_NUM_BOUNDS, i, range.min(), range.max()); + } + + public static MutableComponent outOfBounds(float i, DecimalValue.Range range) { + return Component.translatable(KEY_NUM_BOUNDS, i, range.min(), range.max()); + } + + public static MutableComponent outOfBounds(double i, DecimalValue.Range range) { + return Component.translatable(KEY_NUM_BOUNDS, i, range.min(), range.max()); + } + + public static MutableComponent invalidText(String text, Pattern pattern) { + return Component.translatable(KEY_MISMATCHED_PATTERN, text, pattern); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/client/config/DisplayAdapter.java b/common/src/main/java/mod/azure/azurelib/common/internal/client/config/DisplayAdapter.java new file mode 100644 index 0000000..87dfeec --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/client/config/DisplayAdapter.java @@ -0,0 +1,451 @@ +package mod.azure.azurelib.common.internal.client.config; + +import mod.azure.azurelib.common.internal.client.config.screen.ArrayConfigScreen; +import mod.azure.azurelib.common.internal.client.config.screen.ConfigScreen; +import mod.azure.azurelib.common.internal.client.config.widget.BooleanWidget; +import mod.azure.azurelib.common.internal.client.config.widget.ColorWidget; +import mod.azure.azurelib.common.internal.client.config.widget.ConfigEntryWidget; +import mod.azure.azurelib.common.internal.client.config.widget.EnumWidget; +import mod.azure.azurelib.common.internal.common.config.ConfigUtils; +import mod.azure.azurelib.common.internal.common.config.Configurable; +import mod.azure.azurelib.common.internal.common.config.validate.ValidationResult; +import mod.azure.azurelib.common.internal.common.config.value.*; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; + +import java.lang.reflect.Field; +import java.text.DecimalFormat; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.regex.Pattern; + +@FunctionalInterface +public interface DisplayAdapter { + + void placeWidgets(ConfigValue value, Field field, WidgetAdder container); + + static DisplayAdapter booleanValue() { + return (value, field, container) -> container.addConfigWidget((x, y, width, height, configId) -> new BooleanWidget(getValueX(x, width), y, getValueWidth(width), 20, (BooleanValue) value)); + } + + static DisplayAdapter characterValue() { + return (value, field, container) -> container.addConfigWidget((x, y, width, height, configId) -> { + EditBox widget = new EditBox(Minecraft.getInstance().font, getValueX(x, width), y, getValueWidth(width), 20, CommonComponents.EMPTY); + CharValue charValue = (CharValue) value; + char character = charValue.get(); + widget.setValue(String.valueOf(character)); + widget.setFilter(str -> str.length() <= 1); + widget.setResponder(str -> { + if (!str.isEmpty()) { + container.setOkStatus(); + char toSet = str.charAt(0); + charValue.setWithValidationHandler(toSet, container); + } else { + container.setValidationResult(ValidationResult.error(ClientErrors.CHAR_VALUE_EMPTY)); + } + }); + ConfigUtils.adjustCharacterLimit(field, widget); + return widget; + }); + } + + static DisplayAdapter integerValue() { + return (value, field, container) -> container.addConfigWidget((x, y, width, height, configId) -> { + EditBox tfw = new EditBox(Minecraft.getInstance().font, getValueX(x, width), y, getValueWidth(width), 20, CommonComponents.EMPTY); + IntValue intValue = (IntValue) value; + int num = intValue.get(); + tfw.setValue(String.valueOf(num)); + tfw.setFilter(str -> ConfigUtils.containsOnlyValidCharacters(str, ConfigUtils.INTEGER_CHARS)); + tfw.setResponder(str -> { + if (!ConfigUtils.INTEGER_PATTERN.matcher(str).matches()) { + container.setValidationResult(ValidationResult.error(ClientErrors.notANumber(str))); + return; + } + int n; + try { + n = Integer.parseInt(str); + } catch (NumberFormatException e) { + container.setValidationResult(ValidationResult.error(ClientErrors.notANumber(str))); + return; + } + IntegerValue.Range range = intValue.getRange(); + if (!range.isWithin(n)) { + container.setValidationResult(ValidationResult.error(ClientErrors.outOfBounds(n, range))); + return; + } + container.setOkStatus(); + intValue.setWithValidationHandler(n, container); + }); + ConfigUtils.adjustCharacterLimit(field, tfw); + return tfw; + }); + } + + static DisplayAdapter longValue() { + return (value, field, container) -> container.addConfigWidget((x, y, width, height, configId) -> { + EditBox tfw = new EditBox(Minecraft.getInstance().font, getValueX(x, width), y, getValueWidth(width), 20, CommonComponents.EMPTY); + LongValue longValue = (LongValue) value; + long num = longValue.get(); + tfw.setValue(String.valueOf(num)); + tfw.setFilter(str -> ConfigUtils.containsOnlyValidCharacters(str, ConfigUtils.INTEGER_CHARS)); + tfw.setResponder(str -> { + if (!ConfigUtils.INTEGER_PATTERN.matcher(str).matches()) { + container.setValidationResult(ValidationResult.error(ClientErrors.notANumber(str))); + return; + } + long n; + try { + n = Long.parseLong(str); + } catch (NumberFormatException e) { + container.setValidationResult(ValidationResult.error(ClientErrors.notANumber(str))); + return; + } + IntegerValue.Range range = longValue.getRange(); + if (!range.isWithin(n)) { + container.setValidationResult(ValidationResult.error(ClientErrors.outOfBounds(n, range))); + return; + } + container.setOkStatus(); + longValue.setWithValidationHandler(n, container); + }); + ConfigUtils.adjustCharacterLimit(field, tfw); + return tfw; + }); + } + + static DisplayAdapter floatValue() { + return (value, field, container) -> container.addConfigWidget((x, y, width, height, configId) -> { + EditBox tfw = new EditBox(Minecraft.getInstance().font, getValueX(x, width), y, getValueWidth(width), 20, CommonComponents.EMPTY); + FloatValue floatValue = (FloatValue) value; + DecimalFormat format = ConfigUtils.getDecimalFormat(field); + float number = floatValue.get(); + tfw.setValue(format != null ? format.format(number) : String.valueOf(number)); + tfw.setFilter(str -> ConfigUtils.containsOnlyValidCharacters(str, ConfigUtils.DECIMAL_CHARS)); + tfw.setResponder(str -> { + if (!ConfigUtils.DECIMAL_PATTERN.matcher(str).matches()) { + container.setValidationResult(ValidationResult.error(ClientErrors.notANumber(str))); + return; + } + float n; + try { + n = Float.parseFloat(str); + } catch (NumberFormatException e) { + container.setValidationResult(ValidationResult.error(ClientErrors.notANumber(str))); + return; + } + DecimalValue.Range range = floatValue.getRange(); + if (!range.isWithin(n)) { + container.setValidationResult(ValidationResult.error(ClientErrors.outOfBounds(n, range))); + return; + } + container.setOkStatus(); + floatValue.setWithValidationHandler(n, container); + }); + ConfigUtils.adjustCharacterLimit(field, tfw); + return tfw; + }); + } + + static DisplayAdapter doubleValue() { + return (value, field, container) -> container.addConfigWidget((x, y, width, height, configId) -> { + EditBox tfw = new EditBox(Minecraft.getInstance().font, getValueX(x, width), y, getValueWidth(width), 20, CommonComponents.EMPTY); + DoubleValue doubleValue = (DoubleValue) value; + DecimalFormat format = ConfigUtils.getDecimalFormat(field); + double number = doubleValue.get(); + tfw.setValue(format != null ? format.format(number) : String.valueOf(number)); + tfw.setFilter(str -> ConfigUtils.containsOnlyValidCharacters(str, ConfigUtils.DECIMAL_CHARS)); + tfw.setResponder(str -> { + if (!ConfigUtils.DECIMAL_PATTERN.matcher(str).matches()) { + container.setValidationResult(ValidationResult.error(ClientErrors.notANumber(str))); + return; + } + double n; + try { + n = Double.parseDouble(str); + } catch (NumberFormatException e) { + container.setValidationResult(ValidationResult.error(ClientErrors.notANumber(str))); + return; + } + DecimalValue.Range range = doubleValue.getRange(); + if (!range.isWithin(n)) { + container.setValidationResult(ValidationResult.error(ClientErrors.outOfBounds(n, range))); + return; + } + container.setOkStatus(); + doubleValue.setWithValidationHandler(n, container); + }); + ConfigUtils.adjustCharacterLimit(field, tfw); + return tfw; + }); + } + + static DisplayAdapter stringValue() { + return (value, field, container) -> { + Configurable.Gui.ColorValue colorValue = field.getAnnotation(Configurable.Gui.ColorValue.class); + StringValue strValue = (StringValue) value; + EditBox widget = container.addConfigWidget((x, y, width, height, configId) -> { + EditBox tfw = new EditBox(Minecraft.getInstance().font, getValueX(x, width), y, getValueWidth(width), 20, CommonComponents.EMPTY); + String val = strValue.get(); + tfw.setValue(val); + tfw.setResponder(str -> { + Pattern pattern = strValue.getPattern(); + if (pattern != null) { + if (!pattern.matcher(str).matches()) { + String errDescriptor = strValue.getErrorDescriptor(); + MutableComponent error = errDescriptor != null ? Component.translatable(errDescriptor, str, pattern) : ClientErrors.invalidText(str, pattern); + container.setValidationResult(ValidationResult.error(error)); + return; + } + } + container.setOkStatus(); + strValue.setWithValidationHandler(str, container); + }); + ConfigUtils.adjustCharacterLimit(field, tfw); + return tfw; + }); + if (colorValue != null) { + container.addConfigWidget((x, y, width, height, configId) -> { + int left = getValueX(x, width) - 25; + ColorWidget.GetSet provider = ColorWidget.GetSet.of(widget::getValue, widget::setValue); + Screen currentScreen = Minecraft.getInstance().screen; + return new ColorWidget(left, y, 20, 20, colorValue, provider, currentScreen); + }); + } + }; + } + + static DisplayAdapter booleanArrayValue() { + return (value, field, container) -> container.addConfigWidget((x, y, width, height, configId) -> { + BooleanArrayValue arrayValue = (BooleanArrayValue) value; + BiConsumer setCallback = (val, i) -> { + boolean[] arr = arrayValue.get(); + arr[i] = val; + arrayValue.set(arr); + }; + Button.OnPress pressable = btn -> { + Minecraft client = Minecraft.getInstance(); + Screen usedScreen = client.screen; + ArrayConfigScreen screen = new ArrayConfigScreen<>(value.getId(), configId, arrayValue, usedScreen); + screen.fetchSize(() -> arrayValue.get().length); + screen.valueFactory((id, i) -> { + boolean[] arr = arrayValue.get(); + return new BooleanValue(ValueData.of(id, arr[i], ArrayConfigScreen.callbackCtx(field, Boolean.TYPE, setCallback, i))); + }); + screen.addElement(() -> { + boolean[] arr = arrayValue.get(); + boolean[] expanded = new boolean[arr.length + 1]; + System.arraycopy(arr, 0, expanded, 0, arr.length); + expanded[arr.length] = false; + arrayValue.set(expanded); + }); + screen.removeElement((i, trimmer) -> { + boolean[] arr = arrayValue.get(); + arrayValue.set(trimmer.trim(i, arr, new boolean[arr.length - 1])); + }); + client.setScreen(screen); + }; + return Button.builder(ConfigEntryWidget.EDIT, pressable).pos(getValueX(x, width), y).size(getValueWidth(width), 20).build(); + }); + } + + static DisplayAdapter integerArrayValue() { + return (value, field, container) -> container.addConfigWidget((x, y, width, height, configId) -> { + IntArrayValue arrayValue = (IntArrayValue) value; + BiConsumer setCallback = (val, i) -> { + int[] arr = arrayValue.get(); + arr[i] = val; + arrayValue.set(arr); + }; + Button.OnPress pressable = btn -> { + Minecraft client = Minecraft.getInstance(); + Screen usedScreen = client.screen; + ArrayConfigScreen screen = new ArrayConfigScreen<>(value.getId(), configId, arrayValue, usedScreen); + screen.fetchSize(() -> arrayValue.get().length); + screen.valueFactory((id, i) -> { + int[] arr = arrayValue.get(); + return new IntValue(ValueData.of(id, arr[i], ArrayConfigScreen.callbackCtx(field, Integer.TYPE, setCallback, i))); + }); + screen.addElement(() -> { + int[] arr = arrayValue.get(); + int[] expanded = new int[arr.length + 1]; + System.arraycopy(arr, 0, expanded, 0, arr.length); + expanded[arr.length] = Math.max((int) arrayValue.getRange().min(), 0); + arrayValue.set(expanded); + }); + screen.removeElement((i, trimmer) -> { + int[] arr = arrayValue.get(); + arrayValue.set(trimmer.trim(i, arr, new int[arr.length - 1])); + }); + client.setScreen(screen); + }; + return Button.builder(ConfigEntryWidget.EDIT, pressable).pos(getValueX(x, width), y).size(getValueWidth(width), 20).build(); + }); + } + + static DisplayAdapter longArrayValue() { + return (value, field, container) -> container.addConfigWidget((x, y, width, height, configId) -> { + LongArrayValue arrayValue = (LongArrayValue) value; + BiConsumer setCallback = (val, i) -> { + long[] arr = arrayValue.get(); + arr[i] = val; + arrayValue.set(arr); + }; + Button.OnPress pressable = btn -> { + Minecraft client = Minecraft.getInstance(); + Screen usedScreen = client.screen; + ArrayConfigScreen screen = new ArrayConfigScreen<>(value.getId(), configId, arrayValue, usedScreen); + screen.fetchSize(() -> arrayValue.get().length); + screen.valueFactory((id, i) -> { + long[] arr = arrayValue.get(); + return new LongValue(ValueData.of(id, arr[i], ArrayConfigScreen.callbackCtx(field, Long.TYPE, setCallback, i))); + }); + screen.addElement(() -> { + long[] arr = arrayValue.get(); + long[] expanded = new long[arr.length + 1]; + System.arraycopy(arr, 0, expanded, 0, arr.length); + expanded[arr.length] = Math.max(arrayValue.getRange().min(), 0); + arrayValue.set(expanded); + }); + screen.removeElement((i, trimmer) -> { + long[] arr = arrayValue.get(); + arrayValue.set(trimmer.trim(i, arr, new long[arr.length - 1])); + }); + client.setScreen(screen); + }; + return Button.builder(ConfigEntryWidget.EDIT, pressable).pos(getValueX(x, width), y).size(getValueWidth(width), 20).build(); + }); + } + + static DisplayAdapter floatArrayValue() { + return (value, field, container) -> container.addConfigWidget((x, y, width, height, configId) -> { + FloatArrayValue arrayValue = (FloatArrayValue) value; + BiConsumer setCallback = (val, i) -> { + float[] arr = arrayValue.get(); + arr[i] = val; + arrayValue.set(arr); + }; + Button.OnPress pressable = btn -> { + Minecraft client = Minecraft.getInstance(); + Screen usedScreen = client.screen; + ArrayConfigScreen screen = new ArrayConfigScreen<>(value.getId(), configId, arrayValue, usedScreen); + screen.fetchSize(() -> arrayValue.get().length); + screen.valueFactory((id, i) -> { + float[] arr = arrayValue.get(); + return new FloatValue(ValueData.of(id, arr[i], ArrayConfigScreen.callbackCtx(field, Float.TYPE, setCallback, i))); + }); + screen.addElement(() -> { + float[] arr = arrayValue.get(); + float[] expanded = new float[arr.length + 1]; + System.arraycopy(arr, 0, expanded, 0, arr.length); + expanded[arr.length] = Math.max((float) arrayValue.getRange().min(), 0); + arrayValue.set(expanded); + }); + screen.removeElement((i, trimmer) -> { + float[] arr = arrayValue.get(); + arrayValue.set(trimmer.trim(i, arr, new float[arr.length - 1])); + }); + client.setScreen(screen); + }; + return Button.builder(ConfigEntryWidget.EDIT, pressable).pos(getValueX(x, width), y).size(getValueWidth(width), 20).build(); + }); + } + + static DisplayAdapter doubleArrayValue() { + return (value, field, container) -> container.addConfigWidget((x, y, width, height, configId) -> { + DoubleArrayValue arrayValue = (DoubleArrayValue) value; + BiConsumer setCallback = (val, i) -> { + double[] arr = arrayValue.get(); + arr[i] = val; + arrayValue.set(arr); + }; + Button.OnPress pressable = btn -> { + Minecraft client = Minecraft.getInstance(); + Screen usedScreen = client.screen; + ArrayConfigScreen screen = new ArrayConfigScreen<>(value.getId(), configId, arrayValue, usedScreen); + screen.fetchSize(() -> arrayValue.get().length); + screen.valueFactory((id, i) -> { + double[] arr = arrayValue.get(); + return new DoubleValue(ValueData.of(id, arr[i], ArrayConfigScreen.callbackCtx(field, Double.TYPE, setCallback, i))); + }); + screen.addElement(() -> { + double[] arr = arrayValue.get(); + double[] expanded = new double[arr.length + 1]; + System.arraycopy(arr, 0, expanded, 0, arr.length); + expanded[arr.length] = Math.max(arrayValue.getRange().min(), 0); + arrayValue.set(expanded); + }); + screen.removeElement((i, trimmer) -> { + double[] arr = arrayValue.get(); + arrayValue.set(trimmer.trim(i, arr, new double[arr.length - 1])); + }); + client.setScreen(screen); + }; + return Button.builder(ConfigEntryWidget.EDIT, pressable).pos(getValueX(x, width), y).size(getValueWidth(width), 20).build(); + }); + } + + static DisplayAdapter stringArrayValue() { + return (value, field, container) -> container.addConfigWidget((x, y, width, height, configId) -> { + StringArrayValue arrayValue = (StringArrayValue) value; + BiConsumer setCallback = (val, i) -> { + String[] arr = arrayValue.get(); + arr[i] = val; + arrayValue.set(arr); + }; + Button.OnPress pressable = btn -> { + Minecraft client = Minecraft.getInstance(); + Screen usedScreen = client.screen; + ArrayConfigScreen screen = new ArrayConfigScreen<>(value.getId(), configId, arrayValue, usedScreen); + screen.fetchSize(() -> arrayValue.get().length); + screen.valueFactory((id, i) -> { + String[] arr = arrayValue.get(); + return new StringValue(ValueData.of(id, arr[i], ArrayConfigScreen.callbackCtx(field, String.class, setCallback, i))); + }); + screen.addElement(() -> { + String[] arr = arrayValue.get(); + String[] expanded = new String[arr.length + 1]; + System.arraycopy(arr, 0, expanded, 0, arr.length); + expanded[arr.length] = arrayValue.getDefaultElementValue(); + arrayValue.set(expanded); + }); + screen.removeElement((i, trimmer) -> { + String[] arr = arrayValue.get(); + arrayValue.set(trimmer.trim(i, arr, new String[arr.length - 1])); + }); + client.setScreen(screen); + }; + return Button.builder(ConfigEntryWidget.EDIT, pressable).pos(getValueX(x, width), y).size(getValueWidth(width), 20).build(); + }); + } + + static DisplayAdapter enumValue() { + return (value, field, container) -> container.addConfigWidget((x, y, width, height, configId) -> new EnumWidget<>(getValueX(x, width), y, getValueWidth(width), 20, (EnumValue) value)); + } + + static DisplayAdapter objectValue() { + return (value, field, container) -> container.addConfigWidget((x, y, width, height, configId) -> { + ObjectValue objectValue = (ObjectValue) value; + Map> valueMap = objectValue.get(); + Button.OnPress pressable = btn -> { + Minecraft client = Minecraft.getInstance(); + Screen currentScreen = client.screen; + Screen nestedConfigScreen = new ConfigScreen(container.getComponentName(), configId, valueMap, currentScreen); + client.setScreen(nestedConfigScreen); + }; + return Button.builder(ConfigEntryWidget.EDIT, pressable).pos(getValueX(x, width), y).size(getValueWidth(width), 20).build(); + }); + } + + static int getValueX(int x, int width) { + return x + width - getValueWidth(width); + } + + static int getValueWidth(int width) { + return width / 3; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/client/config/DisplayAdapterManager.java b/common/src/main/java/mod/azure/azurelib/common/internal/client/config/DisplayAdapterManager.java new file mode 100644 index 0000000..2e1413d --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/client/config/DisplayAdapterManager.java @@ -0,0 +1,49 @@ +package mod.azure.azurelib.common.internal.client.config; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; + +import mod.azure.azurelib.common.internal.common.config.adapter.TypeMatcher; + +public final class DisplayAdapterManager { + + private static final Map ADAPTER_MAP = new HashMap<>(); + + public static DisplayAdapter forType(Class type) { + return ADAPTER_MAP.entrySet().stream() + .filter(entry -> entry.getKey().test(type)) + .sorted(Comparator.comparingInt(value -> value.getKey().priority())) + .map(Map.Entry::getValue) + .findFirst() + .orElse(null); + } + + public static void registerDisplayAdapter(TypeMatcher matcher, DisplayAdapter adapter) { + if (ADAPTER_MAP.put(matcher, adapter) != null) { + throw new IllegalArgumentException("Duplicate type matcher with id: " + matcher.getIdentifier()); + } + } + + static { + registerDisplayAdapter(TypeMatcher.matchBoolean(), DisplayAdapter.booleanValue()); + registerDisplayAdapter(TypeMatcher.matchCharacter(), DisplayAdapter.characterValue()); + registerDisplayAdapter(TypeMatcher.matchInteger(), DisplayAdapter.integerValue()); + registerDisplayAdapter(TypeMatcher.matchLong(), DisplayAdapter.longValue()); + registerDisplayAdapter(TypeMatcher.matchFloat(), DisplayAdapter.floatValue()); + registerDisplayAdapter(TypeMatcher.matchDouble(), DisplayAdapter.doubleValue()); + registerDisplayAdapter(TypeMatcher.matchString(), DisplayAdapter.stringValue()); + registerDisplayAdapter(TypeMatcher.matchBooleanArray(), DisplayAdapter.booleanArrayValue()); + registerDisplayAdapter(TypeMatcher.matchIntegerArray(), DisplayAdapter.integerArrayValue()); + registerDisplayAdapter(TypeMatcher.matchLongArray(), DisplayAdapter.longArrayValue()); + registerDisplayAdapter(TypeMatcher.matchFloatArray(), DisplayAdapter.floatArrayValue()); + registerDisplayAdapter(TypeMatcher.matchDoubleArray(), DisplayAdapter.doubleArrayValue()); + registerDisplayAdapter(TypeMatcher.matchStringArray(), DisplayAdapter.stringArrayValue()); + registerDisplayAdapter(TypeMatcher.matchEnum(), DisplayAdapter.enumValue()); + registerDisplayAdapter(TypeMatcher.matchObject(), DisplayAdapter.objectValue()); + } + + private DisplayAdapterManager() { + throw new UnsupportedOperationException(); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/client/config/IValidationHandler.java b/common/src/main/java/mod/azure/azurelib/common/internal/client/config/IValidationHandler.java new file mode 100644 index 0000000..6f7080b --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/client/config/IValidationHandler.java @@ -0,0 +1,12 @@ +package mod.azure.azurelib.common.internal.client.config; + +import mod.azure.azurelib.common.internal.common.config.validate.ValidationResult; + +public interface IValidationHandler { + + void setValidationResult(ValidationResult result); + + default void setOkStatus() { + this.setValidationResult(ValidationResult.ok()); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/client/config/WidgetAdder.java b/common/src/main/java/mod/azure/azurelib/common/internal/client/config/WidgetAdder.java new file mode 100644 index 0000000..fa511bb --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/client/config/WidgetAdder.java @@ -0,0 +1,17 @@ +package mod.azure.azurelib.common.internal.client.config; + +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.network.chat.Component; + +public interface WidgetAdder extends IValidationHandler { + + W addConfigWidget(ToWidgetFunction function); + + Component getComponentName(); + + @FunctionalInterface + interface ToWidgetFunction { + + W asWidget(int x, int y, int width, int height, String configId); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/client/config/screen/AbstractConfigScreen.java b/common/src/main/java/mod/azure/azurelib/common/internal/client/config/screen/AbstractConfigScreen.java new file mode 100644 index 0000000..fb9daa7 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/client/config/screen/AbstractConfigScreen.java @@ -0,0 +1,239 @@ +package mod.azure.azurelib.common.internal.client.config.screen; + +import java.util.Collection; +import java.util.List; + +import mod.azure.azurelib.common.internal.client.config.IValidationHandler; +import mod.azure.azurelib.common.internal.client.config.widget.ConfigEntryWidget; +import mod.azure.azurelib.common.internal.common.config.ConfigHolder; +import mod.azure.azurelib.common.internal.common.AzureLib; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.MarkerManager; +import org.joml.Matrix4f; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.BufferBuilder; +import com.mojang.blaze3d.vertex.BufferUploader; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.Tesselator; +import com.mojang.blaze3d.vertex.VertexFormat; + +import mod.azure.azurelib.common.internal.common.config.io.ConfigIO; +import mod.azure.azurelib.common.internal.common.config.validate.NotificationSeverity; +import mod.azure.azurelib.common.internal.common.config.value.ConfigValue; +import mod.azure.azurelib.common.internal.common.config.value.ObjectValue; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.FormattedCharSequence; +import net.minecraft.util.Mth; + +public abstract class AbstractConfigScreen extends Screen { + + public static final int HEADER_HEIGHT = 35; + public static final int FOOTER_HEIGHT = 30; + public static final Marker MARKER = MarkerManager.getMarker("Screen"); + protected final Screen last; + protected final String configId; + + protected int index; + protected int pageSize; + + protected AbstractConfigScreen(Component title, Screen previous, String configId) { + super(title); + this.last = previous; + this.configId = configId; + } + + @Override + public void onClose() { + super.onClose(); + this.saveConfig(true); + } + + public static void renderScrollbar(GuiGraphics graphics, int x, int y, int width, int height, int index, int valueCount, int paging) { + if (valueCount <= paging) + return; + double step = height / (double) valueCount; + int min = Mth.floor(index * step); + int max = Mth.ceil((index + paging) * step); + int y1 = y + min; + int y2 = y + max; + graphics.fill(x, y, x + width, y + height, 0xFF << 24); + + graphics.fill(x, y1, x + width, y2, 0xFF888888); + graphics.fill(x, y1, x + width - 1, y2 - 1, 0xFFEEEEEE); + graphics.fill(x + 1, y1 + 1, x + width - 1, y2 - 1, 0xFFCCCCCC); + } + + protected void addFooter() { + int centerY = this.height - FOOTER_HEIGHT + (FOOTER_HEIGHT - 20) / 2; + addRenderableWidget(Button.builder(ConfigEntryWidget.BACK, this::buttonBackClicked).pos(20, centerY).size(50, 20).build()); + addRenderableWidget(Button.builder(ConfigEntryWidget.REVERT_DEFAULTS, this::buttonRevertToDefaultClicked).pos(75, centerY).size(120, 20).build()); + addRenderableWidget(Button.builder(ConfigEntryWidget.REVERT_CHANGES, this::buttonRevertChangesClicked).pos(200, centerY).size(120, 20).build()); + } + + protected void correctScrollingIndex(int count) { + if (index + pageSize > count) { + index = Math.max(count - pageSize, 0); + } + } + + protected Screen getFirstNonConfigScreen() { + Screen screen = last; + while (screen instanceof ConfigScreen configScreen) { + screen = configScreen.last; + } + return screen; + } + + private void buttonBackClicked(Button button) { + this.minecraft.setScreen(this.last); + this.saveConfig(); + } + + private void buttonRevertToDefaultClicked(Button button) { + DialogScreen dialog = new DialogScreen(ConfigEntryWidget.REVERT_DEFAULTS, new Component[] { ConfigEntryWidget.REVERT_DEFAULTS_DIALOG_TEXT }, this); + dialog.onConfirmed(screen -> { + AzureLib.LOGGER.info(MARKER, "Reverting config {} to default values", this.configId); + ConfigHolder.getConfig(this.configId).ifPresent(holder -> { + revertToDefault(holder.values()); + ConfigIO.saveClientValues(holder); + }); + this.backToConfigList(); + }); + minecraft.setScreen(dialog); + } + + private void buttonRevertChangesClicked(Button button) { + DialogScreen dialog = new DialogScreen(ConfigEntryWidget.REVERT_CHANGES, new Component[] { ConfigEntryWidget.REVERT_CHANGES_DIALOG_TEXT }, this); + dialog.onConfirmed(screen -> { + ConfigHolder.getConfig(this.configId).ifPresent(ConfigIO::reloadClientValues); + this.backToConfigList(); + }); + minecraft.setScreen(dialog); + } + + private void revertToDefault(Collection> configValues) { + configValues.forEach(val -> { + if (val instanceof ObjectValue objVal) { + this.revertToDefault(objVal.get().values()); + } else { + val.useDefaultValue(); + } + }); + } + + private void backToConfigList() { + this.minecraft.setScreen(this.getFirstNonConfigScreen()); + this.saveConfig(); + } + + private void saveConfig() { + saveConfig(false); + } + + private void saveConfig(boolean force) { + if (force || !(last instanceof AbstractConfigScreen)) { + ConfigHolder.getConfig(this.configId).ifPresent(ConfigIO::saveClientValues); + } + } + + public void renderNotification(NotificationSeverity severity, GuiGraphics graphics, List texts, int mouseX, int mouseY) { + if (!texts.isEmpty()) { + int maxTextWidth = 0; + int iconOffset = 13; + for (FormattedCharSequence textComponent : texts) { + int textWidth = this.font.width(textComponent); + if (!severity.isOkStatus()) { + textWidth += iconOffset; + } + if (textWidth > maxTextWidth) { + maxTextWidth = textWidth; + } + } + + int startX = mouseX + 12; + int startY = mouseY - 12; + int heightOffset = 8; + if (texts.size() > 1) { + heightOffset += 2 + (texts.size() - 1) * 10; + } + + if (startX + maxTextWidth > this.width) { + startX -= 28 + maxTextWidth; + } + + if (startY + heightOffset + 6 > this.height) { + startY = this.height - heightOffset - 6; + } + + int background = severity.background; + int fadeMin = severity.fadeMin; + int fadeMax = severity.fadeMax; + int zIndex = 400; + Tesselator tessellator = Tesselator.getInstance(); + RenderSystem.setShader(GameRenderer::getPositionColorShader); + BufferBuilder bufferbuilder = tessellator.getBuilder(); + bufferbuilder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR); + + Matrix4f matrix4f = graphics.pose().last().pose(); + graphics.fillGradient(startX - 3, startY - 4, startX + maxTextWidth + 3, startY - 3, zIndex, background, background); + graphics.fillGradient(startX - 3, startY + heightOffset + 3, startX + maxTextWidth + 3, startY + heightOffset + 4, zIndex, background, background); + graphics.fillGradient(startX - 3, startY - 3, startX + maxTextWidth + 3, startY + heightOffset + 3, zIndex, background, background); + graphics.fillGradient(startX - 4, startY - 3, startX - 3, startY + heightOffset + 3, zIndex, background, background); + graphics.fillGradient(startX + maxTextWidth + 3, startY - 3, startX + maxTextWidth + 4, startY + heightOffset + 3, zIndex, background, background); + graphics.fillGradient(startX - 3, startY - 3 + 1, startX - 3 + 1, startY + heightOffset + 3 - 1, zIndex, fadeMin, fadeMax); + graphics.fillGradient(startX + maxTextWidth + 2, startY - 3 + 1, startX + maxTextWidth + 3, startY + heightOffset + 3 - 1, zIndex, fadeMin, fadeMax); + graphics.fillGradient(startX - 3, startY - 3, startX + maxTextWidth + 3, startY - 3 + 1, zIndex, fadeMin, fadeMin); + graphics.fillGradient(startX - 3, startY + heightOffset + 2, startX + maxTextWidth + 3, startY + heightOffset + 3, zIndex, fadeMax, fadeMax); + RenderSystem.enableDepthTest(); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + BufferUploader.drawWithShader(bufferbuilder.end()); + + if (!severity.isOkStatus()) { + ResourceLocation icon = severity.getIcon(); + RenderSystem.setShader(GameRenderer::getPositionTexShader); + RenderSystem.setShaderTexture(0, icon); + bufferbuilder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX); + float min = -0.5f; + float max = 8.5f; + bufferbuilder.vertex(matrix4f, startX + min, startY + min, zIndex).uv(0.0F, 0.0F).endVertex(); + bufferbuilder.vertex(matrix4f, startX + min, startY + max, zIndex).uv(0.0F, 1.0F).endVertex(); + bufferbuilder.vertex(matrix4f, startX + max, startY + max, zIndex).uv(1.0F, 1.0F).endVertex(); + bufferbuilder.vertex(matrix4f, startX + max, startY + min, zIndex).uv(1.0F, 0.0F).endVertex(); + BufferUploader.drawWithShader(bufferbuilder.end()); + } + + RenderSystem.disableBlend(); + MultiBufferSource.BufferSource bufferSource = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder()); + + int textOffset = severity.isOkStatus() ? 0 : iconOffset; + for (int i = 0; i < texts.size(); i++) { + FormattedCharSequence textComponent = texts.get(i); + if (textComponent != null) { + this.font.drawInBatch(textComponent, (float) startX + textOffset, startY, -1, true, matrix4f, bufferSource, Font.DisplayMode.NORMAL, 0, 0xf000f0); + } + + if (i == 0) { + startY += 2; + } + + startY += 10; + } + + bufferSource.endBatch(); + } + } + + protected void initializeGuiValue(ConfigValue value, IValidationHandler handler) { + T t = value.get(); + value.setWithValidationHandler(t, handler); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/client/config/screen/ArrayConfigScreen.java b/common/src/main/java/mod/azure/azurelib/common/internal/client/config/screen/ArrayConfigScreen.java new file mode 100644 index 0000000..b18e02a --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/client/config/screen/ArrayConfigScreen.java @@ -0,0 +1,201 @@ +package mod.azure.azurelib.common.internal.client.config.screen; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Supplier; + +import mod.azure.azurelib.common.internal.common.AzureLib; +import mod.azure.azurelib.common.internal.client.config.DisplayAdapter; +import mod.azure.azurelib.common.internal.client.config.DisplayAdapterManager; +import mod.azure.azurelib.common.internal.client.config.widget.ConfigEntryWidget; +import mod.azure.azurelib.common.internal.common.config.adapter.TypeAdapter; +import mod.azure.azurelib.common.internal.common.config.adapter.TypeAdapters; +import mod.azure.azurelib.common.internal.common.config.validate.NotificationSeverity; +import mod.azure.azurelib.common.internal.common.config.value.ArrayValue; +import mod.azure.azurelib.common.internal.common.config.value.ConfigValue; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.util.FormattedCharSequence; + +public class ArrayConfigScreen & ArrayValue> extends AbstractConfigScreen { + + public static final Component ADD_ELEMENT = Component.translatable("text.azurelib.value.add_element"); + + public final C array; + private final boolean fixedSize; + + private Supplier sizeSupplier = () -> 0; + private DummyConfigValueFactory valueFactory; + private ElementAddHandler addHandler; + private ElementRemoveHandler removeHandler; + + public ArrayConfigScreen(String ownerIdentifier, String configId, C array, Screen previous) { + super(Component.translatable(String.format("config.%s.option.%s", configId, ownerIdentifier)), previous, configId); + this.array = array; + this.fixedSize = array.isFixedSize(); + } + + public void fetchSize(Supplier integerSupplier) { + this.sizeSupplier = integerSupplier; + } + + public void valueFactory(DummyConfigValueFactory factory) { + this.valueFactory = factory; + } + + public void addElement(ElementAddHandler handler) { + this.addHandler = handler; + } + + public void removeElement(ElementRemoveHandler handler) { + this.removeHandler = handler; + } + + @Override + protected void init() { + final int viewportMin = HEADER_HEIGHT; + final int viewportHeight = this.height - viewportMin - FOOTER_HEIGHT; + this.pageSize = (viewportHeight - 20) / 25; + this.correctScrollingIndex(this.sizeSupplier.get()); + int errorOffset = (viewportHeight - 20) - (this.pageSize * 25 - 5); + int offset = 0; + + Class compType = array.get().getClass().getComponentType(); + DisplayAdapter adapter = DisplayAdapterManager.forType(compType); + TypeAdapter.AdapterContext context = array.getSerializationContext(); + Field owner = context.getOwner(); + for (int i = this.index; i < this.index + this.pageSize; i++) { + int j = i - this.index; + if (i >= this.sizeSupplier.get()) + break; + int correct = errorOffset / (this.pageSize - j); + errorOffset -= correct; + offset += correct; + ConfigValue dummy = valueFactory.create(array.getId(), i); + dummy.processFieldData(owner); + ConfigEntryWidget widget = addRenderableWidget(new ConfigEntryWidget(30, viewportMin + 10 + j * 25 + offset, this.width - 60, 20, dummy, this.configId)); + widget.setDescriptionRenderer((graphics, widget1, severity, text) -> renderEntryDescription(graphics, widget1, severity, text)); + if (adapter == null) { + AzureLib.LOGGER.error(MARKER, "Missing display adapter for {} type, will not be displayed in GUI", compType.getSimpleName()); + continue; + } + try { + adapter.placeWidgets(dummy, owner, widget); + initializeGuiValue(dummy, widget); + } catch (ClassCastException e) { + AzureLib.LOGGER.error(MARKER, "Unable to create config field for {} type due to error {}", compType.getSimpleName(), e); + } + if (!fixedSize) { + final int elementIndex = i; + addRenderableWidget(Button.builder(Component.literal("x"), btn -> { + this.removeHandler.removeElementAt(elementIndex, (index, src, dest) -> { + System.arraycopy(src, 0, dest, 0, index); + System.arraycopy(src, index + 1, dest, index, this.sizeSupplier.get() - 1 - index); + return dest; + }); + this.init(minecraft, width, height); + }).pos(this.width - 28, widget.getY()).size(20, 20).build()); + } + } + addFooter(); + } + + private void renderEntryDescription(GuiGraphics graphics, AbstractWidget widget, NotificationSeverity severity, List text) { + if (!severity.isOkStatus()) { + this.renderNotification(severity, graphics, text, widget.getX() + 5, widget.getY() + widget.getHeight() + 10); + } + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + renderBackground(graphics, mouseY, mouseY, partialTicks); + // HEADER + int titleWidth = this.font.width(this.title); + graphics.drawString(this.font, this.title, (int) ((this.width - titleWidth) / 2.0F), (int) ((HEADER_HEIGHT - this.font.lineHeight) / 2.0F), 0xFFFFFF, true); + graphics.fill(0, HEADER_HEIGHT, width, height - FOOTER_HEIGHT, 0x99 << 24); + renderScrollbar(graphics, width - 5, HEADER_HEIGHT, 5, height - FOOTER_HEIGHT - HEADER_HEIGHT, index, sizeSupplier.get(), pageSize); + super.render(graphics, mouseX, mouseY, partialTicks); + } + + @Override + protected void addFooter() { + super.addFooter(); + if (!this.fixedSize) { + int centerY = this.height - FOOTER_HEIGHT + (FOOTER_HEIGHT - 20) / 2; + addRenderableWidget(Button.builder(ADD_ELEMENT, btn -> { + this.addHandler.insertElement(); + this.init(minecraft, width, height); + }).pos(width - 100, centerY).size(80, 20).build()); + } + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double amount, double g) { + int scale = (int) -amount; + int next = this.index + scale; + if (next >= 0 && next + this.pageSize <= this.sizeSupplier.get()) { + this.index = next; + this.init(minecraft, width, height); + return true; + } + return false; + } + + public static TypeAdapter.AdapterContext callbackCtx(Field parent, Class componentType, BiConsumer callback, int index) { + return new DummyCallbackAdapter<>(componentType, parent, callback, index); + } + + @FunctionalInterface + public interface ElementAddHandler { + void insertElement(); + } + + @FunctionalInterface + public interface DummyConfigValueFactory { + ConfigValue create(String id, int elementIndex); + } + + @FunctionalInterface + public interface ElementRemoveHandler { + void removeElementAt(int index, ArrayTrimmer trimmer); + + @FunctionalInterface + interface ArrayTrimmer { + V trim(int index, V src, V dest); + } + } + + private static class DummyCallbackAdapter implements TypeAdapter.AdapterContext { + + private final TypeAdapter typeAdapter; + private final Field parentField; + private final BiConsumer setCallback; + private final int index; + + private DummyCallbackAdapter(Class type, Field parentField, BiConsumer setCallback, int index) { + this.typeAdapter = TypeAdapters.forType(type); + this.parentField = parentField; + this.setCallback = setCallback; + this.index = index; + } + + @Override + public TypeAdapter getAdapter() { + return typeAdapter; + } + + @Override + public Field getOwner() { + return parentField; + } + + @Override + public void setFieldValue(Object value) { + this.setCallback.accept((V) value, this.index); + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/client/config/screen/ConfigGroupScreen.java b/common/src/main/java/mod/azure/azurelib/common/internal/client/config/screen/ConfigGroupScreen.java new file mode 100644 index 0000000..a792c41 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/client/config/screen/ConfigGroupScreen.java @@ -0,0 +1,120 @@ +package mod.azure.azurelib.common.internal.client.config.screen; + +import static mod.azure.azurelib.common.internal.client.config.screen.AbstractConfigScreen.FOOTER_HEIGHT; +import static mod.azure.azurelib.common.internal.client.config.screen.AbstractConfigScreen.HEADER_HEIGHT; + +import java.util.List; + +import mod.azure.azurelib.common.internal.client.config.DisplayAdapter; +import mod.azure.azurelib.common.internal.client.config.widget.ConfigEntryWidget; +import mod.azure.azurelib.common.internal.common.config.ConfigHolder; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; + +public class ConfigGroupScreen extends Screen { + + protected final Screen last; + protected final String groupId; + protected final List> configHolders; + protected int index; + protected int pageSize; + + public ConfigGroupScreen(Screen last, String groupId, List> configHolders) { + super(Component.translatable("text.azurelib.screen.select_config")); + this.last = last; + this.groupId = groupId; + this.configHolders = configHolders; + } + + @Override + protected void init() { + final int viewportMin = HEADER_HEIGHT; + final int viewportHeight = this.height - viewportMin - FOOTER_HEIGHT; + this.pageSize = (viewportHeight - 20) / 25; + this.correctScrollingIndex(this.configHolders.size()); + int errorOffset = (viewportHeight - 20) - (this.pageSize * 25 - 5); + int offset = 0; + int posX = 30; + int componentWidth = this.width - 2 * posX; + for (int i = this.index; i < this.index + this.pageSize; i++) { + int j = i - this.index; + if (i >= configHolders.size()) + break; + int correct = errorOffset / (this.pageSize - j); + errorOffset -= correct; + offset += correct; + ConfigHolder value = configHolders.get(i); + int y = viewportMin + 10 + j * 25 + offset; + String configId = value.getConfigId(); + this.addRenderableWidget(new LeftAlignedLabel(posX, y, componentWidth, 20, Component.translatable("config.screen." + configId), this.font)); + this.addRenderableWidget(Button.builder(ConfigEntryWidget.EDIT, btn -> { + ConfigScreen screen = new ConfigScreen(configId, configId, value.getValueMap(), this); + minecraft.setScreen(screen); + }).pos(DisplayAdapter.getValueX(posX, componentWidth), y).size(DisplayAdapter.getValueWidth(componentWidth), 20).build()); + } + initFooter(); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + renderBackground(graphics, mouseY, mouseY, partialTicks); + // HEADER + int titleWidth = this.font.width(this.title); + graphics.drawString(this.font, this.title, (int) ((this.width - titleWidth) / 2.0F), (int) ((HEADER_HEIGHT - this.font.lineHeight) / 2.0F), 0xFFFFFF, true); + graphics.fill(0, HEADER_HEIGHT, width, height - FOOTER_HEIGHT, 0x99 << 24); + AbstractConfigScreen.renderScrollbar(graphics, width - 5, HEADER_HEIGHT, 5, height - FOOTER_HEIGHT - HEADER_HEIGHT, index, configHolders.size(), pageSize); + super.render(graphics, mouseX, mouseY, partialTicks); + } + + protected void initFooter() { + int centerY = this.height - FOOTER_HEIGHT + (FOOTER_HEIGHT - 20) / 2; + addRenderableWidget(Button.builder(ConfigEntryWidget.BACK, btn -> minecraft.setScreen(last)).pos(20, centerY).size(50, 20).build()); + } + + protected void correctScrollingIndex(int count) { + if (index + pageSize > count) { + index = Math.max(count - pageSize, 0); + } + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double amount, double g) { + int scale = (int) -amount; + int next = this.index + scale; + if (next >= 0 && next + this.pageSize <= this.configHolders.size()) { + this.index = next; + this.init(minecraft, width, height); + return true; + } + return false; + } + + protected static final class LeftAlignedLabel extends AbstractWidget { + + private final Font font; + + public LeftAlignedLabel(int x, int y, int width, int height, Component label, Font font) { + super(x, y, width, height, label); + this.font = font; + } + + @Override + public void updateWidgetNarration(NarrationElementOutput narrationElementOutput) { + } + + @Override + public void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + graphics.drawString(font, this.getMessage(), this.getX(), this.getY() + (this.height - this.font.lineHeight) / 2, 0xAAAAAA); + } + + @Override + protected boolean isValidClickButton(int p_230987_1_) { + return false; + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/client/config/screen/ConfigScreen.java b/common/src/main/java/mod/azure/azurelib/common/internal/client/config/screen/ConfigScreen.java new file mode 100644 index 0000000..30a120d --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/client/config/screen/ConfigScreen.java @@ -0,0 +1,102 @@ +package mod.azure.azurelib.common.internal.client.config.screen; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import mod.azure.azurelib.common.internal.client.config.widget.ConfigEntryWidget; +import mod.azure.azurelib.common.internal.common.AzureLib; +import mod.azure.azurelib.common.internal.client.config.DisplayAdapter; +import mod.azure.azurelib.common.internal.client.config.DisplayAdapterManager; +import mod.azure.azurelib.common.internal.common.config.adapter.TypeAdapter; +import mod.azure.azurelib.common.internal.common.config.validate.NotificationSeverity; +import mod.azure.azurelib.common.internal.common.config.value.ConfigValue; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.util.FormattedCharSequence; + +public class ConfigScreen extends AbstractConfigScreen { + + private final Map> valueMap; + + public ConfigScreen(String ownerIdentifier, String configId, Map> valueMap, Screen previous) { + this(Component.translatable("config.screen." + ownerIdentifier), configId, valueMap, previous); + } + + public ConfigScreen(Component screenTitle, String configId, Map> valueMap, Screen previous) { + super(screenTitle, previous, configId); + this.valueMap = valueMap; + } + + @Override + protected void init() { + final int viewportMin = HEADER_HEIGHT; + final int viewportHeight = this.height - viewportMin - FOOTER_HEIGHT; + this.pageSize = (viewportHeight - 20) / 25; + this.correctScrollingIndex(this.valueMap.size()); + List> values = new ArrayList<>(this.valueMap.values()); + int errorOffset = (viewportHeight - 20) - (this.pageSize * 25 - 5); + int offset = 0; + for (int i = this.index; i < this.index + this.pageSize; i++) { + int j = i - this.index; + if (i >= values.size()) + break; + int correct = errorOffset / (this.pageSize - j); + errorOffset -= correct; + offset += correct; + ConfigValue value = values.get(i); + ConfigEntryWidget widget = addRenderableWidget(new ConfigEntryWidget(30, viewportMin + 10 + j * 25 + offset, this.width - 60, 20, value, this.configId)); + widget.setDescriptionRenderer((graphics, widget1, severity, text) -> renderEntryDescription(graphics, widget1, severity, text)); + TypeAdapter.AdapterContext context = value.getSerializationContext(); + Field field = context.getOwner(); + DisplayAdapter adapter = DisplayAdapterManager.forType(field.getType()); + if (adapter == null) { + AzureLib.LOGGER.error(MARKER, "Missing display adapter for {} type, will not be displayed in GUI", field.getType().getSimpleName()); + continue; + } + try { + adapter.placeWidgets(value, field, widget); + initializeGuiValue(value, widget); + } catch (ClassCastException e) { + AzureLib.LOGGER.error(MARKER, "Unable to create config field for {} type due to error {}", field.getType().getSimpleName(), e); + } + } + this.addFooter(); + } + + private void renderEntryDescription(GuiGraphics graphics, AbstractWidget widget, NotificationSeverity severity, List text) { + int x = widget.getX() + 5; + int y = widget.getY() + widget.getHeight() + 10; + if (!severity.isOkStatus()) { + this.renderNotification(severity, graphics, text, x, y); + } else { + this.renderNotification(NotificationSeverity.INFO, graphics, text, x, y); + } + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + renderBackground(graphics, mouseY, mouseY, partialTicks); + // HEADER + int titleWidth = this.font.width(this.title); + graphics.drawString(this.font, this.title, (int) ((this.width - titleWidth) / 2.0F), (int) ((HEADER_HEIGHT - this.font.lineHeight) / 2.0F), 0xFFFFFF, true); + graphics.fill(0, HEADER_HEIGHT, width, height - FOOTER_HEIGHT, 0x99 << 24); + renderScrollbar(graphics, width - 5, HEADER_HEIGHT, 5, height - FOOTER_HEIGHT - HEADER_HEIGHT, index, valueMap.size(), pageSize); + super.render(graphics, mouseX, mouseY, partialTicks); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double amount, double g) { + int scale = (int) -amount; + int next = this.index + scale; + if (next >= 0 && next + this.pageSize <= this.valueMap.size()) { + this.index = next; + this.init(minecraft, width, height); + return true; + } + return false; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/client/config/screen/DialogScreen.java b/common/src/main/java/mod/azure/azurelib/common/internal/client/config/screen/DialogScreen.java new file mode 100644 index 0000000..9cd058e --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/client/config/screen/DialogScreen.java @@ -0,0 +1,133 @@ +package mod.azure.azurelib.common.internal.client.config.screen; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +import org.lwjgl.glfw.GLFW; + +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.util.FormattedCharSequence; + +public class DialogScreen extends Screen { + + public static final Component TEXT_CONFIRM = Component.translatable("text.azurelib.screen.dialog.confirm"); + public static final Component TEXT_CANCEL = Component.translatable("text.azurelib.screen.dialog.cancel"); + + private final Screen background; + private DialogRespondEvent onCancel; + private DialogRespondEvent onConfirm; + protected final Component[] text; + protected int dialogWidth; + protected int dialogHeight; + protected int dialogLeft; + protected int dialogTop; + private List splitText = new ArrayList<>(); + + public DialogScreen(Component title, Component[] text, Screen background) { + super(title); + this.text = text; + this.background = background; + this.onCancel = this::displayPreviousScreen; + this.onConfirm = this::displayPreviousScreen; + } + + public void onCancelled(DialogRespondEvent cancelEvent) { + this.onCancel = Objects.requireNonNull(cancelEvent); + } + + public void onConfirmed(DialogRespondEvent confirmEvent) { + this.onConfirm = Objects.requireNonNull(confirmEvent); + } + + public void setDimensions(int dialogWidth, int dialogHeight) { + this.dialogWidth = dialogWidth; + this.dialogHeight = dialogHeight; + + this.dialogLeft = (this.width - this.dialogWidth) / 2; + this.dialogTop = (this.height - this.dialogHeight) / 2; + this.splitText = Arrays.stream(this.text).map(line -> this.font.split(line, this.dialogWidth - 10)).flatMap(Collection::stream).toList(); + } + + @Override + protected void init() { + this.background.init(minecraft, width, height); + this.setDimensions(140, 100); + this.addDefaultDialogButtons(); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + int backgroundColor = 0xFF << 24; + this.background.render(graphics, mouseX, mouseY, partialTicks); + graphics.pose().pushPose(); + graphics.pose().translate(0, 0, 400); + graphics.fillGradient(this.dialogLeft - 1, this.dialogTop - 1, this.dialogLeft + this.dialogWidth + 1, this.dialogTop + this.dialogHeight + 1, 0xFFFFFFFF, 0xFFFFFFFF); + graphics.fillGradient(this.dialogLeft, this.dialogTop, this.dialogLeft + this.dialogWidth, this.dialogTop + this.dialogHeight, backgroundColor, backgroundColor); + this.renderForeground(graphics, mouseX, mouseY, partialTicks); + super.render(graphics, mouseX, mouseY, partialTicks); + graphics.pose().popPose(); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (this.allowKeyboardInteractions()) { + if (keyCode == GLFW.GLFW_KEY_ESCAPE) { + this.cancel(); + return true; + } else if (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) { + this.confirm(); + return true; + } + return false; + } + return super.keyPressed(keyCode, scanCode, modifiers); + } + + protected void renderForeground(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + int headerWidth = this.font.width(this.title); + graphics.drawString(this.font, this.title, (int) (this.dialogLeft + (this.dialogWidth - headerWidth) / 2.0F), this.dialogTop + 5, 0xFFFFFF, true); + int line = 0; + for (FormattedCharSequence textLine : this.splitText) { + graphics.drawString(this.font, textLine, this.dialogLeft + 5, this.dialogTop + 20 + line * 10, 0xFFFFFF, true); + ++line; + } + } + + protected void addDefaultDialogButtons() { + int useableWidth = this.dialogWidth - 15; + int componentWidth = useableWidth / 2; + int cancelX = this.dialogLeft + 5; + int confirmX = this.dialogLeft + this.dialogWidth - 5 - componentWidth; + int componentY = this.dialogTop + this.dialogHeight - 25; + + this.addRenderableWidget(Button.builder(TEXT_CANCEL, btn -> cancel()).pos(cancelX, componentY).size(componentWidth, 20).build()); + this.addRenderableWidget(Button.builder(TEXT_CONFIRM, btn -> confirm()).pos(confirmX, componentY).size(componentWidth, 20).build()); + } + + protected void confirm() { + this.onConfirm.respond(this); + } + + protected void cancel() { + this.onCancel.respond(this); + } + + public void displayPreviousScreen(DialogScreen screen) { + this.minecraft.setScreen(this.background); + } + + protected boolean allowKeyboardInteractions() { + return true; + } + + @FunctionalInterface + public interface DialogRespondEvent { + void respond(DialogScreen screen); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/client/config/screen/OptifineWarningScreen.java b/common/src/main/java/mod/azure/azurelib/common/internal/client/config/screen/OptifineWarningScreen.java new file mode 100644 index 0000000..440dcce --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/client/config/screen/OptifineWarningScreen.java @@ -0,0 +1,62 @@ +package mod.azure.azurelib.common.internal.client.config.screen; + +import mod.azure.azurelib.common.internal.mixins.AccessorWarningScreen; +import mod.azure.azurelib.common.platform.Services; +import net.minecraft.ChatFormatting; +import net.minecraft.Util; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.MultiLineLabel; +import net.minecraft.client.gui.screens.multiplayer.WarningScreen; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; + +public class OptifineWarningScreen extends WarningScreen { + public OptifineWarningScreen() { + super(HEADER, MESSAGE, CHECK_MESSAGE, NARRATED_TEXT); + } + + @Override + protected void initButtons(int yOffset) { + addRenderableWidget( + Button.builder(OPEN_MODS_FOLDER, buttonWidget -> Util.getPlatform().openFile(Services.PLATFORM.modsDir().toFile())) + .bounds(width / 2 - 155, 100 + yOffset, 150, 20) + .build() + ); + + addRenderableWidget( + Button.builder(OPTIFINE_ALTERNATIVES, buttonWidget -> Util.getPlatform().openUri( + "https://prismlauncher.org/wiki/getting-started/install-of-alternatives/" + )) + .bounds(width / 2 - 155 + 160, 100 + yOffset, 150, 20) + .build() + ); + + addRenderableWidget( + Button.builder(QUIT_GAME, buttonWidget -> this.minecraft.stop()) + .bounds(width / 2 - 75, 130 + yOffset, 150, 20) + .build() + ); + } + + @Override + protected void init() { + ((AccessorWarningScreen) this).setMessageText(MultiLineLabel.create(font, MESSAGE, width - 50)); + int yOffset = (((AccessorWarningScreen) this).getMessageText().getLineCount() + 1) * font.lineHeight * 2 - 20; + initButtons(yOffset); + } + + + @Override + public boolean shouldCloseOnEsc() { + return false; + } + + private static final MutableComponent HEADER = Component.translatable("header.azurelib.optifine").withStyle(ChatFormatting.DARK_RED, ChatFormatting.BOLD); + private static final Component MESSAGE = Component.translatable("message.azurelib.optifine"); + private static final Component CHECK_MESSAGE = Component.translatable("multiplayerWarning.check"); + private static final Component NARRATED_TEXT = HEADER.copy().append("\n").append(MESSAGE); + + private static final Component OPEN_MODS_FOLDER = Component.translatable("label.azurelib.open_mods_folder"); + private static final Component OPTIFINE_ALTERNATIVES = Component.translatable("label.azurelib.optifine_alternatives"); + private static final Component QUIT_GAME = Component.translatable("menu.quit"); +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/client/config/widget/BooleanWidget.java b/common/src/main/java/mod/azure/azurelib/common/internal/client/config/widget/BooleanWidget.java new file mode 100644 index 0000000..94eb3e9 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/client/config/widget/BooleanWidget.java @@ -0,0 +1,73 @@ +package mod.azure.azurelib.common.internal.client.config.widget; + +import com.mojang.blaze3d.systems.RenderSystem; +import mod.azure.azurelib.common.internal.common.config.value.BooleanValue; +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.WidgetSprites; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; + +public class BooleanWidget extends AbstractWidget { + + private static final WidgetSprites SPRITES = new WidgetSprites(new ResourceLocation("widget/button"), new ResourceLocation("widget/button_disabled"), new ResourceLocation("widget/button_highlighted")); + public static final Component TRUE = Component.translatable("text.azurelib.value.true").withStyle(ChatFormatting.GREEN); + public static final Component FALSE = Component.translatable("text.azurelib.value.false").withStyle(ChatFormatting.RED); + private final BooleanValue value; + + public BooleanWidget(int x, int y, int w, int h, BooleanValue value) { + super(x, y, w, h, CommonComponents.EMPTY); + this.value = value; + this.readState(); + } + + @Override + public void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + Minecraft minecraft = Minecraft.getInstance(); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, this.alpha); + RenderSystem.enableBlend(); + RenderSystem.enableDepthTest(); + graphics.blitSprite(SPRITES.get(this.active, this.isHoveredOrFocused()), this.getX(), this.getY(), this.getWidth(), this.getHeight()); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + this.renderString(graphics, minecraft.font, Mth.ceil(this.alpha * 255.0F) << 24); + } + + private int getTextureY() { + int i = 1; + if (!this.active) { + i = 0; + } else if (this.isHoveredOrFocused()) { + i = 2; + } + return 46 + i * 20; + } + + private void renderString(GuiGraphics graphics, Font font, int color) { + this.renderScrollingString(graphics, font, 2, color); + } + + @Override + public void onClick(double x, double y) { + this.setState(!this.value.get()); + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput narrationElementOutput) { + } + + private void readState() { + boolean value = this.value.get(); + this.setMessage(value ? TRUE : FALSE); + } + + private void setState(boolean state) { + this.value.set(state); + this.readState(); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/client/config/widget/ColorWidget.java b/common/src/main/java/mod/azure/azurelib/common/internal/client/config/widget/ColorWidget.java new file mode 100644 index 0000000..3250ade --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/client/config/widget/ColorWidget.java @@ -0,0 +1,235 @@ +package mod.azure.azurelib.common.internal.client.config.widget; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.IntSupplier; +import java.util.function.Supplier; + +import mod.azure.azurelib.common.internal.client.config.screen.DialogScreen; +import mod.azure.azurelib.common.internal.common.config.Configurable; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractSliderButton; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; + +public final class ColorWidget extends AbstractWidget { + + public static final Component SELECT_COLOR = Component.translatable("text.azurelib.screen.color_dialog"); + private final boolean argb; + private final String colorPrefix; + private final IntSupplier colorSupplier; + private final GetSet colorWidget; + private final Screen lastScreen; + + public ColorWidget(int x, int y, int width, int height, Configurable.Gui.ColorValue colorOptions, GetSet colorWidget, Screen lastScreen) { + super(x, y, width, height, CommonComponents.EMPTY); + this.argb = colorOptions.isARGB(); + this.colorPrefix = colorOptions.getGuiColorPrefix(); + this.colorWidget = colorWidget; + this.colorSupplier = () -> { + String rawColor = colorWidget.get(); + try { + long longClr = Long.decode(rawColor); + return (int) longClr; + } catch (NumberFormatException e) { + return 0; + } + }; + this.lastScreen = lastScreen; + } + + @Override + public void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float partialRenderTicks) { + int borderColor = this.isFocused() ? 0xffffffff : 0xffa0a0a0; + int providedColor = this.colorSupplier.getAsInt(); + int color = this.argb ? providedColor : (0xFF << 24) | providedColor; + graphics.fill(this.getX() - 1, this.getY() - 1, this.getX() + this.width + 1, this.getY() + this.height + 1, borderColor); + graphics.fillGradient(this.getX(), this.getY(), this.getX() + this.width, this.getY() + this.height, 0xFFFFFFFF, 0xFF888888); + graphics.fill(this.getX(), this.getY(), this.getX() + this.width, this.getY() + this.height, color); + } + + @Override + protected boolean isValidClickButton(int button) { + return button == 0; + } + + @Override + public void onClick(double mouseX, double mouseY) { + ColorSelectorDialog dialog = new ColorSelectorDialog(SELECT_COLOR, this.lastScreen, this.argb, this.colorSupplier); + dialog.onConfirmed(screen -> { + int color = dialog.getResultColor(); + String colorText = this.colorPrefix + Integer.toHexString(color).toUpperCase(); + this.colorWidget.set(colorText); + dialog.displayPreviousScreen(dialog); + }); + Minecraft.getInstance().setScreen(dialog); + } + + @Override + public void updateWidgetNarration(NarrationElementOutput elementOutput) { + } + + public interface GetSet { + + T get(); + + void set(T t); + + static GetSet of(Supplier get, Consumer set) { + return new GetSet() { + @Override + public T get() { + return get.get(); + } + + @Override + public void set(T t) { + set.accept(t); + } + }; + } + } + + private static final class ColorSelectorDialog extends DialogScreen { + + private final boolean argb; + private final IntSupplier colorProvider; + private final List sliders = new ArrayList<>(); + + public ColorSelectorDialog(Component title, Screen background, boolean allowTransparency, IntSupplier colorProvider) { + super(title, new Component[0], background); + this.argb = allowTransparency; + this.colorProvider = colorProvider; + } + + @Override + protected void init() { + this.sliders.clear(); + int width = 190; + int height = 120; + int rightMargin = 85; + if (this.argb) { + height = 150; + rightMargin = 110; + width = 230; + } + super.init(); + this.setDimensions(width, height); + int color = this.colorProvider.getAsInt(); + this.sliders.add(this.addRenderableWidget(new ColorSlider(dialogLeft + 5, dialogTop + 20, dialogWidth - rightMargin, 20, color, ColorComponent.RED))); + this.sliders.add(this.addRenderableWidget(new ColorSlider(dialogLeft + 5, dialogTop + 45, dialogWidth - rightMargin, 20, color, ColorComponent.GREEN))); + this.sliders.add(this.addRenderableWidget(new ColorSlider(dialogLeft + 5, dialogTop + 70, dialogWidth - rightMargin, 20, color, ColorComponent.BLUE))); + if (this.argb) { + this.sliders.add(this.addRenderableWidget(new ColorSlider(dialogLeft + 5, dialogTop + 95, dialogWidth - rightMargin, 20, color, ColorComponent.ALPHA))); + } + this.addRenderableWidget(new ColorDisplay(dialogLeft + 5 + dialogWidth - rightMargin + 5, dialogTop + 20, rightMargin - 15, rightMargin - 15, argb, this::getResultColor)); + super.addDefaultDialogButtons(); + } + + @Override + protected void addDefaultDialogButtons() { + } + + public int getResultColor() { + int color = 0; + for (ColorSlider slider : this.sliders) { + color |= slider.getColor(); + } + return color; + } + + private static final class ColorDisplay extends AbstractWidget { + + private final boolean argb; + private final IntSupplier colorProvider; + + public ColorDisplay(int x, int y, int width, int height, boolean argb, IntSupplier colorProvider) { + super(x, y, width, height, CommonComponents.EMPTY); + this.argb = argb; + this.colorProvider = colorProvider; + } + + @Override + public void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + int color = this.colorProvider.getAsInt(); + if (!this.argb) { + color |= 0xFF << 24; + } + int borderColor = 0xffa0a0a0; + graphics.fill(this.getX(), this.getY(), this.getX() + this.width, this.getY() + this.height, borderColor); + graphics.fillGradient(this.getX() + 1, this.getY() + 1, this.getX() + this.width - 1, this.getY() + this.height - 1, 0xFFFFFFFF, 0xFF888888); + graphics.fill(this.getX() + 1, this.getY() + 1, this.getX() + this.width - 1, this.getY() + this.height - 1, color); + } + + @Override + protected boolean isValidClickButton(int button) { + return false; + } + + @Override + public void updateWidgetNarration(NarrationElementOutput p_169152_) { + } + } + + private static final class ColorSlider extends AbstractSliderButton { + + private final ColorComponent colorComponent; + + public ColorSlider(int x, int y, int width, int height, int color, ColorComponent colorComponent) { + super(x, y, width, height, CommonComponents.EMPTY, (colorComponent.getByteColor(color) / 255.0D)); + this.colorComponent = colorComponent; + this.updateMessage(); + } + + @Override + protected void updateMessage() { + Component colorLabel = this.colorComponent.updateTitle(this.value); + this.setMessage(colorLabel); + } + + @Override + protected void applyValue() { + } + + int getColor() { + return this.colorComponent.getOffsetColor((int) (0xFF * this.value)); + } + } + + private enum ColorComponent { + + ALPHA(24), RED(16), GREEN(8), BLUE(0); + + private final int bitOffset; + private final Function title; + + ColorComponent(int bitOffset) { + this.bitOffset = bitOffset; + this.title = val -> { + String name = this.name().toLowerCase(); + String translate = "text.azurelib.screen.color." + name; + int colorValue = (int) (val * 255); + return Component.translatable(translate, colorValue); + }; + } + + public int getOffsetColor(int value) { + return value << bitOffset; + } + + public int getByteColor(int value) { + return (value >> bitOffset) & 0xFF; + } + + public Component updateTitle(double sliderValue) { + return this.title.apply(sliderValue); + } + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/client/config/widget/ConfigEntryWidget.java b/common/src/main/java/mod/azure/azurelib/common/internal/client/config/widget/ConfigEntryWidget.java new file mode 100644 index 0000000..028d55a --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/client/config/widget/ConfigEntryWidget.java @@ -0,0 +1,95 @@ +package mod.azure.azurelib.common.internal.client.config.widget; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import mod.azure.azurelib.common.internal.client.config.WidgetAdder; +import mod.azure.azurelib.common.internal.common.config.validate.NotificationSeverity; +import mod.azure.azurelib.common.internal.common.config.validate.ValidationResult; +import mod.azure.azurelib.common.internal.common.config.value.ConfigValue; +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.util.FormattedCharSequence; + +public class ConfigEntryWidget extends ContainerWidget implements WidgetAdder { + + public static final Component EDIT = Component.translatable("text.azurelib.value.edit"); + public static final Component BACK = Component.translatable("text.azurelib.value.back"); + public static final Component REVERT_DEFAULTS = Component.translatable("text.azurelib.value.revert.default"); + public static final Component REVERT_DEFAULTS_DIALOG_TEXT = Component.translatable("text.azurelib.value.revert.default.dialog"); + public static final Component REVERT_CHANGES = Component.translatable("text.azurelib.value.revert.changes"); + public static final Component REVERT_CHANGES_DIALOG_TEXT = Component.translatable("text.azurelib.value.revert.changes.dialog"); + + private final String configId; + private final List description; + + private ValidationResult result = ValidationResult.ok(); + private IDescriptionRenderer renderer; + private boolean lastHoverState; + private long hoverTimeStart; + + public ConfigEntryWidget(int x, int y, int w, int h, ConfigValue value, String configId) { + super(x, y, w, h, Component.translatable("config." + configId + ".option." + value.getId())); + this.configId = configId; + this.description = Arrays.stream(value.getDescription()).map(text -> Component.literal(text).withStyle(ChatFormatting.GRAY)).collect(Collectors.toList()); + } + + public void setDescriptionRenderer(IDescriptionRenderer renderer) { + this.renderer = renderer; + } + + @Override + public Component getComponentName() { + return this.getMessage(); + } + + @Override + public void updateWidgetNarration(NarrationElementOutput narrationElementOutput) { + } + + @Override + public void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + Font font = Minecraft.getInstance().font; + if (!lastHoverState && isHovered) { + hoverTimeStart = System.currentTimeMillis(); + } + boolean isError = !this.result.isOk(); + graphics.drawString(font, this.getMessage(), this.getX(), (int) (this.getY() + (this.height - font.lineHeight) / 2.0F), 0xAAAAAA, true); + super.renderWidget(graphics, mouseX, mouseY, partialTicks); + if ((isError || isHovered) && renderer != null) { + long totalHoverTime = System.currentTimeMillis() - hoverTimeStart; + if (isError || totalHoverTime >= 750L) { + NotificationSeverity severity = this.result.severity(); + MutableComponent textComponent = this.result.text().withStyle(severity.getExtraFormatting()); + List desc = isError ? Collections.singletonList(textComponent) : this.description; + List split = desc.stream().flatMap(text -> font.split(text, this.width / 2).stream()).collect(Collectors.toList()); + renderer.drawDescription(graphics, this, severity, split); + } + } + this.lastHoverState = isHovered; + } + + @Override + public void setValidationResult(ValidationResult result) { + this.result = result; + } + + @Override + public W addConfigWidget(ToWidgetFunction function) { + W widget = function.asWidget(this.getX(), this.getY(), this.width, this.height, this.configId); + return this.addRenderableWidget(widget); + } + + @FunctionalInterface + public interface IDescriptionRenderer { + void drawDescription(GuiGraphics graphics, AbstractWidget widget, NotificationSeverity severity, List text); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/client/config/widget/ContainerWidget.java b/common/src/main/java/mod/azure/azurelib/common/internal/client/config/widget/ContainerWidget.java new file mode 100644 index 0000000..6704ced --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/client/config/widget/ContainerWidget.java @@ -0,0 +1,119 @@ +package mod.azure.azurelib.common.internal.client.config.widget; + +import java.util.ArrayList; +import java.util.List; + +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.events.ContainerEventHandler; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.network.chat.Component; + +public abstract class ContainerWidget extends AbstractWidget implements ContainerEventHandler { + + private final List listeners = new ArrayList<>(); + private final List widgets = new ArrayList<>(); + private GuiEventListener focused; + private boolean dragging; + + public ContainerWidget(int x, int y, int w, int h, Component component) { + super(x, y, w, h, component); + } + + public L addGuiEventListener(L listener) { + this.listeners.add(listener); + return listener; + } + + public void removeGuiEventListener(GuiEventListener listener) { + listeners.remove(listener); + } + + public W addRenderableWidget(W widget) { + widgets.add(widget); + return addGuiEventListener(widget); + } + + public void removeWidget(AbstractWidget widget) { + widgets.remove(widget); + removeGuiEventListener(widget); + } + + public void clear() { + listeners.clear(); + widgets.clear(); + focused = null; + } + + @Override + public void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + widgets.forEach(widget -> widget.render(graphics, mouseX, mouseY, partialTicks)); + } + + @Override + public boolean mouseClicked(double p_231044_1_, double p_231044_3_, int p_231044_5_) { + var result = ContainerEventHandler.super.mouseClicked(p_231044_1_, p_231044_3_, p_231044_5_); + if (!result && this.focused != null) + this.setFocused(null); + return result; + } + + @Override + public boolean mouseReleased(double p_231048_1_, double p_231048_3_, int p_231048_5_) { + return ContainerEventHandler.super.mouseReleased(p_231048_1_, p_231048_3_, p_231048_5_); + } + + @Override + public boolean mouseDragged(double p_231045_1_, double p_231045_3_, int p_231045_5_, double p_231045_6_, double p_231045_8_) { + return ContainerEventHandler.super.mouseDragged(p_231045_1_, p_231045_3_, p_231045_5_, p_231045_6_, p_231045_8_); + } + + @Override + public boolean mouseScrolled(double p_231043_1_, double p_231043_3_, double p_231043_5_, double g) { + return ContainerEventHandler.super.mouseScrolled(p_231043_1_, p_231043_3_, p_231043_5_, g); + } + + @Override + public void mouseMoved(double p_212927_1_, double p_212927_3_) { + ContainerEventHandler.super.mouseMoved(p_212927_1_, p_212927_3_); + } + + @Override + public boolean keyPressed(int p_231046_1_, int p_231046_2_, int p_231046_3_) { + return ContainerEventHandler.super.keyPressed(p_231046_1_, p_231046_2_, p_231046_3_); + } + + @Override + public boolean keyReleased(int p_223281_1_, int p_223281_2_, int p_223281_3_) { + return ContainerEventHandler.super.keyReleased(p_223281_1_, p_223281_2_, p_223281_3_); + } + + @Override + public List children() { + return listeners; + } + + @Override + public boolean isDragging() { + return dragging; + } + + @Override + public void setDragging(boolean dragging) { + this.dragging = dragging; + } + + @Override + public GuiEventListener getFocused() { + return focused; + } + + @Override + public void setFocused(GuiEventListener focused) { + if (this.focused != null) + this.focused.setFocused(false); + if (focused != null) + focused.setFocused(true); + this.focused = focused; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/client/config/widget/EnumWidget.java b/common/src/main/java/mod/azure/azurelib/common/internal/client/config/widget/EnumWidget.java new file mode 100644 index 0000000..ade01c0 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/client/config/widget/EnumWidget.java @@ -0,0 +1,74 @@ +package mod.azure.azurelib.common.internal.client.config.widget; + +import com.mojang.blaze3d.systems.RenderSystem; +import mod.azure.azurelib.common.internal.common.config.value.EnumValue; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.WidgetSprites; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; + +public class EnumWidget> extends AbstractWidget { + private static final WidgetSprites SPRITES = new WidgetSprites(new ResourceLocation("widget/button"), new ResourceLocation("widget/button_disabled"), new ResourceLocation("widget/button_highlighted")); + private final EnumValue value; + + public EnumWidget(int x, int y, int w, int h, EnumValue value) { + super(x, y, w, h, CommonComponents.EMPTY); + this.value = value; + this.updateText(); + } + + @Override + public void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + Minecraft minecraft = Minecraft.getInstance(); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, this.alpha); + RenderSystem.enableBlend(); + RenderSystem.enableDepthTest(); + graphics.blitSprite(SPRITES.get(this.active, this.isHoveredOrFocused()), this.getX(), this.getY(), this.getWidth(), this.getHeight()); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + this.renderString(graphics, minecraft.font, Mth.ceil(this.alpha * 255.0F) << 24); + } + + private int getTextureY() { + int i = 1; + if (!this.active) { + i = 0; + } else if (this.isHoveredOrFocused()) { + i = 2; + } + return 46 + i * 20; + } + + private void renderString(GuiGraphics graphics, Font font, int color) { + this.renderScrollingString(graphics, font, 2, color); + } + + @Override + public void onClick(double p_230982_1_, double p_230982_3_) { + this.nextValue(); + this.updateText(); + } + + @Override + public void updateWidgetNarration(NarrationElementOutput narrationElementOutput) { + } + + private void nextValue() { + E e = this.value.get(); + E[] values = e.getDeclaringClass().getEnumConstants(); + int i = e.ordinal(); + int j = (i + 1) % values.length; + E next = values[j]; + this.value.set(next); + } + + private void updateText() { + E e = this.value.get(); + this.setMessage(Component.literal(e.name())); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/client/model/data/EntityModelData.java b/common/src/main/java/mod/azure/azurelib/common/internal/client/model/data/EntityModelData.java new file mode 100644 index 0000000..fe19115 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/client/model/data/EntityModelData.java @@ -0,0 +1,6 @@ +package mod.azure.azurelib.common.internal.client.model.data; + +/** + * Container class for various pieces of data relating to a model's current state. + */ +public record EntityModelData(boolean isSitting, boolean isChild, float netHeadYaw, float headPitch) {} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/client/renderer/GeoRenderer.java b/common/src/main/java/mod/azure/azurelib/common/internal/client/renderer/GeoRenderer.java new file mode 100644 index 0000000..25d63ad --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/client/renderer/GeoRenderer.java @@ -0,0 +1,339 @@ +package mod.azure.azurelib.common.internal.client.renderer; + +import java.util.List; + +import com.mojang.blaze3d.vertex.BufferBuilder; +import mod.azure.azurelib.common.api.common.animatable.GeoBlockEntity; +import mod.azure.azurelib.common.api.common.animatable.GeoItem; +import mod.azure.azurelib.common.internal.client.util.RenderUtils; +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 org.Vrglab.AzureLib.Utility.Utils; +import org.jetbrains.annotations.Nullable; +import org.joml.Matrix3f; +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 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.object.Color; +import mod.azure.azurelib.common.api.client.model.GeoModel; +import mod.azure.azurelib.common.api.client.renderer.layer.GeoRenderLayer; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.ResourceLocation; + +/** + * Base interface for all AzureLib renderers.
+ */ +public interface GeoRenderer { + /** + * Gets the model instance for this renderer + */ + GeoModel getGeoModel(); + + /** + * Gets the {@link GeoAnimatable} instance currently being rendered + */ + T getAnimatable(); + + /** + * Gets the texture resource location to render for the given animatable + */ + default ResourceLocation getTextureLocation(T animatable) { + return getGeoModel().getTextureResource(animatable); + } + + /** + * Returns the list of registered {@link GeoRenderLayer GeoRenderLayers} for this renderer + */ + default List> getRenderLayers() { + return List.of(); + } + + /** + * Gets the {@link RenderType} to render the given animatable with.
+ * Uses the {@link RenderType#entityCutoutNoCull} {@code RenderType} by default.
+ * Override this to change the way a model will render (such as translucent models, etc) + */ + default RenderType getRenderType(T animatable, ResourceLocation texture, @Nullable MultiBufferSource bufferSource, float partialTick) { + return getGeoModel().getRenderType(animatable, texture); + } + + /** + * Gets a tint-applying color to render the given animatable with.
+ * Returns {@link Color#WHITE} by default + */ + default Color getRenderColor(T animatable, float partialTick, int packedLight) { + return Color.WHITE; + } + + /** + * 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. + */ + @Deprecated(forRemoval = true) + default int getPackedOverlay(T animatable, float u) { + return OverlayTexture.NO_OVERLAY; + } + + /** + * 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. + */ + default int getPackedOverlay(T animatable, float u, float partialTick) { + return getPackedOverlay(animatable, u); + } + + /** + * 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 + */ + default long getInstanceId(T animatable) { + return animatable.hashCode(); + } + + /** + * Determines the threshold value before the animatable should be considered moving for animation purposes.
+ * The default value and usage for this varies depending on the renderer.
+ *
    + *
  • For entities, it represents the averaged lateral velocity of the object.
  • + *
  • For {@link GeoBlockEntity Tile Entities} and {@link GeoItem Items}, it's currently unused
  • + *
+ * The lower the value, the more sensitive the {@link AnimationState#isMoving()} check will be.
+ * Particularly low values may have adverse effects however + */ + default float getMotionAnimThreshold(T animatable) { + return 0.015f; + } + + /** + * Initial access point for rendering. It all begins here.
+ * All AzureLib renderers should immediately defer their respective default {@code render} calls to this, for consistent handling + */ + default void defaultRender(PoseStack poseStack, T animatable, MultiBufferSource bufferSource, @Nullable RenderType renderType, @Nullable VertexConsumer buffer, float yaw, float partialTick, int packedLight) { + poseStack.pushPose(); + + Color renderColor = getRenderColor(animatable, partialTick, packedLight); + float red = renderColor.getRedFloat(); + float green = renderColor.getGreenFloat(); + float blue = renderColor.getBlueFloat(); + float alpha = renderColor.getAlphaFloat(); + int packedOverlay = getPackedOverlay(animatable, 0, partialTick); + BakedGeoModel model = getGeoModel().getBakedModel(getGeoModel().getModelResource(animatable)); + + if (renderType == null) + renderType = getRenderType(animatable, getTextureLocation(animatable), bufferSource, partialTick); + + if (buffer == null) + buffer = bufferSource.getBuffer(renderType); + + preRender(poseStack, animatable, model, bufferSource, buffer, false, partialTick, packedLight, packedOverlay, red, green, blue, alpha); + + if (firePreRenderEvent(poseStack, model, bufferSource, partialTick, packedLight)) { + preApplyRenderLayers(poseStack, animatable, model, renderType, bufferSource, buffer, packedLight, packedLight, packedOverlay); + actuallyRender(poseStack, animatable, model, renderType, bufferSource, buffer, false, partialTick, packedLight, packedOverlay, red, green, blue, alpha); + applyRenderLayers(poseStack, animatable, model, renderType, bufferSource, buffer, partialTick, packedLight, packedOverlay); + postRender(poseStack, animatable, model, bufferSource, buffer, false, partialTick, packedLight, packedOverlay, red, green, blue, alpha); + firePostRenderEvent(poseStack, model, bufferSource, partialTick, packedLight); + } + + poseStack.popPose(); + + renderFinal(poseStack, animatable, model, bufferSource, buffer, partialTick, packedLight, packedOverlay, red, green, blue, alpha); + } + + /** + * Re-renders the provided {@link BakedGeoModel} using the existing {@link GeoRenderer}.
+ * Usually you'd use this for rendering alternate {@link RenderType} layers or for sub-model rendering whilst inside a {@link GeoRenderLayer} or similar + */ + default void reRender(BakedGeoModel model, PoseStack poseStack, MultiBufferSource bufferSource, T animatable, RenderType renderType, VertexConsumer buffer, float partialTick, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) { + poseStack.pushPose(); + preRender(poseStack, animatable, model, bufferSource, buffer, true, partialTick, packedLight, packedOverlay, red, green, blue, alpha); + actuallyRender(poseStack, animatable, model, renderType, bufferSource, buffer, true, partialTick, packedLight, packedOverlay, red, green, blue, alpha); + postRender(poseStack, animatable, model, bufferSource, buffer, true, partialTick, packedLight, packedOverlay, red, green, blue, alpha); + poseStack.popPose(); + } + + /** + * The actual render method that sub-type 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 + */ + default 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) { + updateAnimatedTextureFrame(animatable); + for (GeoBone group : model.topLevelBones()) { + renderRecursively(poseStack, animatable, group, renderType, bufferSource, buffer, isReRender, partialTick, packedLight, packedOverlay, red, green, blue, alpha); + } + } + + /** + * Calls back to the various {@link GeoRenderLayer RenderLayers} that have been registered to this renderer for their {@link GeoRenderLayer#preRender pre-render} actions. + */ + default void preApplyRenderLayers(PoseStack poseStack, T animatable, BakedGeoModel model, RenderType renderType, MultiBufferSource bufferSource, VertexConsumer buffer, float partialTick, int packedLight, int packedOverlay) { + for (GeoRenderLayer renderLayer : getRenderLayers()) { + renderLayer.preRender(poseStack, animatable, model, renderType, bufferSource, buffer, partialTick, packedLight, packedOverlay); + } + } + + /** + * Calls back to the various {@link GeoRenderLayer RenderLayers} that have been registered to this renderer for their {@link GeoRenderLayer#renderForBone per-bone} render actions. + */ + default void applyRenderLayersForBone(PoseStack poseStack, T animatable, GeoBone bone, RenderType renderType, MultiBufferSource bufferSource, VertexConsumer buffer, float partialTick, int packedLight, int packedOverlay) { + for (GeoRenderLayer renderLayer : getRenderLayers()) { + renderLayer.renderForBone(poseStack, animatable, bone, renderType, bufferSource, buffer, partialTick, packedLight, packedOverlay); + } + } + + /** + * Render the various {@link GeoRenderLayer RenderLayers} that have been registered to this renderer + */ + default void applyRenderLayers(PoseStack poseStack, T animatable, BakedGeoModel model, RenderType renderType, MultiBufferSource bufferSource, VertexConsumer buffer, float partialTick, int packedLight, int packedOverlay) { + for (GeoRenderLayer renderLayer : getRenderLayers()) { + renderLayer.render(poseStack, animatable, model, renderType, bufferSource, buffer, partialTick, packedLight, packedOverlay); + } + } + + /** + * 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 + */ + default 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) { + } + + /** + * 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 + */ + default 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) { + } + + /** + * 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} + */ + default 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) { + } + + /** + * Renders the provided {@link GeoBone} and its associated child bones + */ + default 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.prepMatrixForBone(poseStack, bone); + renderCubesOfBone(poseStack, bone, buffer, packedLight, packedOverlay, red, green, blue, alpha); + + if (!isReRender) { + applyRenderLayersForBone(poseStack, getAnimatable(), 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(); + } + + /** + * Renders the {@link GeoCube GeoCubes} associated with a given {@link GeoBone} + */ + default void renderCubesOfBone(PoseStack poseStack, GeoBone bone, VertexConsumer buffer, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) { + if (bone.isHidden()) + return; + + for (GeoCube cube : bone.getCubes()) { + poseStack.pushPose(); + renderCube(poseStack, cube, buffer, packedLight, packedOverlay, red, green, blue, alpha); + poseStack.popPose(); + } + } + + /** + * Render the child bones of a given {@link GeoBone}.
+ * Note that this does not render the bone itself. That should be done through {@link GeoRenderer#renderCubesOfBone} separately + */ + default void renderChildBones(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.isHidingChildren()) + return; + + for (GeoBone childBone : bone.getChildBones()) { + renderRecursively(poseStack, animatable, childBone, renderType, bufferSource, buffer, isReRender, partialTick, packedLight, packedOverlay, red, green, blue, alpha); + } + } + + /** + * Renders an individual {@link GeoCube}.
+ * This tends to be called recursively from something like {@link GeoRenderer#renderCubesOfBone} + */ + default void renderCube(PoseStack poseStack, GeoCube cube, VertexConsumer buffer, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) { + RenderUtils.translateToPivotPoint(poseStack, cube); + RenderUtils.rotateMatrixAroundCube(poseStack, cube); + RenderUtils.translateAwayFromPivotPoint(poseStack, cube); + + Matrix3f normalisedPoseState = poseStack.last().normal(); + Matrix4f poseState = new Matrix4f(poseStack.last().pose()); + + for (GeoQuad quad : cube.quads()) { + if (quad == null) + continue; + + Vector3f normal = normalisedPoseState.transform(new Vector3f(quad.normal())); + + RenderUtils.fixInvertedFlatCube(cube, normal); + createVerticesOfQuad(quad, poseState, normal, buffer, packedLight, packedOverlay, red, green, blue, alpha); + } + } + + /** + * Applies the {@link GeoQuad Quad's} {@link GeoVertex vertices} to the given {@link VertexConsumer buffer} for rendering + */ + default void createVerticesOfQuad(GeoQuad quad, Matrix4f poseState, Vector3f normal, VertexConsumer buffer, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) { + for (GeoVertex vertex : quad.vertices()) { + Vector3f position = vertex.position(); + Vector4f vector4f = poseState.transform(new Vector4f(position.x(), position.y(), position.z(), 1.0f)); + + buffer.vertex(vector4f.x(), vector4f.y(), vector4f.z(), red, green, blue, alpha, vertex.texU(), vertex.texV(), packedOverlay, packedLight, normal.x(), normal.y(), normal.z()); + } + } + + /** + * Create and fire the relevant {@code CompileLayers} event hook for this renderer + */ + void fireCompileRenderLayersEvent(); + + /** + * 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 + */ + boolean firePreRenderEvent(PoseStack poseStack, BakedGeoModel model, MultiBufferSource bufferSource, float partialTick, int packedLight); + + /** + * Create and fire the relevant {@code Post-Render} event hook for this renderer + */ + void firePostRenderEvent(PoseStack poseStack, BakedGeoModel model, MultiBufferSource bufferSource, float partialTick, int packedLight); + + /** + * Scales the {@link PoseStack} in preparation for rendering the model, excluding when re-rendering the model as part of a {@link GeoRenderLayer} or external render call.
+ * Override and call super with modified scale values as needed to further modify the scale of the model (E.G. child entities) + */ + default void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, T animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + if (!isReRender && (widthScale != 1 || heightScale != 1)) + poseStack.scale(widthScale, heightScale, widthScale); + } + + /** + * 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) + */ + void updateAnimatedTextureFrame(T animatable); +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/client/util/RenderUtils.java b/common/src/main/java/mod/azure/azurelib/common/internal/client/util/RenderUtils.java new file mode 100644 index 0000000..572b58b --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/client/util/RenderUtils.java @@ -0,0 +1,317 @@ +package mod.azure.azurelib.common.internal.client.util; + +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.internal.common.core.animatable.model.CoreGeoBone; +import mod.azure.azurelib.common.internal.common.AzureLib; +import mod.azure.azurelib.common.internal.common.cache.object.GeoQuad; +import org.Vrglab.AzureLib.Utility.Utils; +import org.jetbrains.annotations.Nullable; +import org.joml.Matrix4f; +import org.joml.Quaternionf; +import org.joml.Vector3f; + +import com.mojang.blaze3d.Blaze3D; +import com.mojang.blaze3d.platform.NativeImage; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; + +import it.unimi.dsi.fastutil.ints.IntIntImmutablePair; +import it.unimi.dsi.fastutil.ints.IntIntPair; +import mod.azure.azurelib.common.internal.client.RenderProvider; +import mod.azure.azurelib.common.api.client.model.GeoModel; +import mod.azure.azurelib.common.api.client.renderer.GeoArmorRenderer; +import mod.azure.azurelib.common.internal.client.renderer.GeoRenderer; +import mod.azure.azurelib.common.api.client.renderer.GeoReplacedEntityRenderer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.model.geom.ModelPart; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.texture.AbstractTexture; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.core.Direction; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.phys.Vec3; + +import java.util.Map; + +/** + * Helper class for various methods and functions useful while rendering + */ +public final class RenderUtils { + public static void translateMatrixToBone(PoseStack poseStack, CoreGeoBone bone) { + poseStack.translate(-bone.getPosX() / 16f, bone.getPosY() / 16f, bone.getPosZ() / 16f); + } + + public static void rotateMatrixAroundBone(PoseStack poseStack, CoreGeoBone bone) { + if (bone.getRotZ() != 0) + poseStack.mulPose(Axis.ZP.rotation(bone.getRotZ())); + + if (bone.getRotY() != 0) + poseStack.mulPose(Axis.YP.rotation(bone.getRotY())); + + if (bone.getRotX() != 0) + poseStack.mulPose(Axis.XP.rotation(bone.getRotX())); + } + + public static void rotateMatrixAroundCube(PoseStack poseStack, GeoCube cube) { + Vec3 rotation = cube.rotation(); + + poseStack.mulPose(new Quaternionf().rotationXYZ(0, 0, (float) rotation.z())); + poseStack.mulPose(new Quaternionf().rotationXYZ(0, (float) rotation.y(), 0)); + poseStack.mulPose(new Quaternionf().rotationXYZ((float) rotation.x(), 0, 0)); + } + + public static void scaleMatrixForBone(PoseStack poseStack, CoreGeoBone bone) { + poseStack.scale(bone.getScaleX(), bone.getScaleY(), bone.getScaleZ()); + } + + public static void translateToPivotPoint(PoseStack poseStack, GeoCube cube) { + Vec3 pivot = cube.pivot(); + poseStack.translate(pivot.x() / 16f, pivot.y() / 16f, pivot.z() / 16f); + } + + public static void translateToPivotPoint(PoseStack poseStack, CoreGeoBone bone) { + poseStack.translate(bone.getPivotX() / 16f, bone.getPivotY() / 16f, bone.getPivotZ() / 16f); + } + + public static void translateAwayFromPivotPoint(PoseStack poseStack, GeoCube cube) { + Vec3 pivot = cube.pivot(); + + poseStack.translate(-pivot.x() / 16f, -pivot.y() / 16f, -pivot.z() / 16f); + } + + public static void translateAwayFromPivotPoint(PoseStack poseStack, CoreGeoBone bone) { + poseStack.translate(-bone.getPivotX() / 16f, -bone.getPivotY() / 16f, -bone.getPivotZ() / 16f); + } + + public static void translateAndRotateMatrixForBone(PoseStack poseStack, CoreGeoBone bone) { + translateToPivotPoint(poseStack, bone); + rotateMatrixAroundBone(poseStack, bone); + } + + public static void prepMatrixForBone(PoseStack poseStack, CoreGeoBone bone) { + translateMatrixToBone(poseStack, bone); + translateToPivotPoint(poseStack, bone); + rotateMatrixAroundBone(poseStack, bone); + scaleMatrixForBone(poseStack, bone); + translateAwayFromPivotPoint(poseStack, bone); + } + + public static Matrix4f invertAndMultiplyMatrices(Matrix4f baseMatrix, Matrix4f inputMatrix) { + inputMatrix = new Matrix4f(inputMatrix); + + inputMatrix.invert(); + inputMatrix.mul(baseMatrix); + + return inputMatrix; + } + + /** + * Add a positional vector to a matrix. This is specifically implemented to act as a translation of an x/y/z coordinate triplet to a render matrix + */ + public static Matrix4f translateMatrix(Matrix4f matrix, Vector3f vector) { + return matrix.add(new Matrix4f().m30(vector.x).m31(vector.y).m32(vector.z)); + } + + /** + * Gets the actual dimensions of a texture resource from a given path.
+ * Not performance-efficient, and should not be relied upon + * + * @param texture The path of the texture resource to check + * @return The dimensions (width x height) of the texture, or null if unable to find or read the file + */ + @Nullable + public static IntIntPair getTextureDimensions(ResourceLocation texture) { + if (texture == null) + return null; + + AbstractTexture originalTexture = null; + Minecraft mc = Minecraft.getInstance(); + + try { + originalTexture = mc.submit(() -> mc.getTextureManager().getTexture(texture)).get(); + } catch (Exception e) { + AzureLib.LOGGER.warn("Failed to load image for id {}", texture); + e.printStackTrace(); + } + + if (originalTexture == null) + return null; + + NativeImage image = null; + + try { + image = originalTexture instanceof DynamicTexture dynamicTexture ? dynamicTexture.getPixels() : NativeImage.read(mc.getResourceManager().getResource(texture).get().open()); + } catch (Exception e) { + AzureLib.LOGGER.error("Failed to read image for id {}", texture); + e.printStackTrace(); + } + + return image == null ? null : IntIntImmutablePair.of(image.getWidth(), image.getHeight()); + } + + public static double getCurrentSystemTick() { + return System.nanoTime() / 1E6 / 50d; + } + + /** + * Returns the current time (in ticks) that the {@link org.lwjgl.glfw.GLFW GLFW} instance has been running. This is effectively a permanent timer that counts up since the game was launched. + */ + public static double getCurrentTick() { + return Blaze3D.getTime() * 20d; + } + + /** + * Returns a float equivalent of a boolean.
+ * Output table: + *
    + *
  • true -> 1
  • + *
  • false -> 0
  • + *
+ */ + public static float booleanToFloat(boolean input) { + return input ? 1f : 0f; + } + + /** + * Converts a given double array to its {@link Vec3} equivalent + */ + public static Vec3 arrayToVec(double[] array) { + return new Vec3(array[0], array[1], array[2]); + } + + /** + * Rotates a {@link CoreGeoBone} to match a provided {@link ModelPart}'s rotations.
+ * Usually used for items or armor rendering to match the rotations of other non-geo model parts. + */ + public static void matchModelPartRot(ModelPart from, CoreGeoBone to) { + to.updateRotation(-from.xRot, -from.yRot, from.zRot); + } + + /** + * If a {@link GeoCube} is a 2d plane the {@link GeoQuad Quad's} normal is inverted in an intersecting plane,it can cause issues with shaders and other lighting tasks.
+ * This performs a pseudo-ABS function to help resolve some of those issues. + */ + public static void fixInvertedFlatCube(GeoCube cube, Vector3f normal) { + if (normal.x() < 0 && (cube.size().y() == 0 || cube.size().z() == 0)) + normal.mul(-1, 1, 1); + + if (normal.y() < 0 && (cube.size().x() == 0 || cube.size().z() == 0)) + normal.mul(1, -1, 1); + + if (normal.z() < 0 && (cube.size().x() == 0 || cube.size().y() == 0)) + normal.mul(1, 1, -1); + } + + /** + * Converts a {@link Direction} to a rotational float for rotation purposes + */ + public static float getDirectionAngle(Direction direction) { + return switch (direction) { + case SOUTH -> 90f; + case NORTH -> 270f; + case EAST -> 180f; + default -> 0f; + }; + } + + /** + * Gets a {@link GeoModel} instance from a given {@link EntityType}.
+ * This only works if you're calling this method for an EntityType known to be using a {@link GeoRenderer AzureLib Renderer}.
+ * Generally speaking you probably shouldn't be calling this method at all. + * + * @param entityType The {@code EntityType} to retrieve the GeoModel for + * @return The GeoModel, or null if one isn't found + */ + @Nullable + public static GeoModel getGeoModelForEntityType(EntityType entityType) { + EntityRenderer renderer = ((Map, EntityRenderer>)Utils.getPrivateFinalStaticField(Minecraft.getInstance().getEntityRenderDispatcher(), Minecraft.getInstance().getEntityRenderDispatcher().getClass(), "renderers")).get(entityType); + + return renderer instanceof GeoRenderer geoRenderer ? geoRenderer.getGeoModel() : null; + } + + /** + * Gets a GeoAnimatable instance that has been registered as the replacement renderer for a given {@link EntityType} + * + * @param entityType The {@code EntityType} to retrieve the replaced {@link GeoAnimatable} for + * @return The {@code GeoAnimatable} instance, or null if one isn't found + */ + @Nullable + public static GeoAnimatable getReplacedAnimatable(EntityType entityType) { + EntityRenderer renderer = ((Map, EntityRenderer>)Utils.getPrivateFinalStaticField(Minecraft.getInstance().getEntityRenderDispatcher(), Minecraft.getInstance().getEntityRenderDispatcher().getClass(), "renderers")).get(entityType); + + return renderer instanceof GeoReplacedEntityRenderer replacedEntityRenderer ? replacedEntityRenderer.getAnimatable() : null; + } + + /** + * Gets a {@link GeoModel} instance from a given {@link Entity}.
+ * This only works if you're calling this method for an Entity known to be using a {@link GeoRenderer AzureLib Renderer}.
+ * Generally speaking you probably shouldn't be calling this method at all. + * + * @param entity The {@code Entity} to retrieve the GeoModel for + * @return The GeoModel, or null if one isn't found + */ + @Nullable + public static GeoModel getGeoModelForEntity(Entity entity) { + EntityRenderer renderer = Minecraft.getInstance().getEntityRenderDispatcher().getRenderer(entity); + + return renderer instanceof GeoRenderer geoRenderer ? geoRenderer.getGeoModel() : null; + } + + /** + * Gets a {@link GeoModel} instance from a given {@link Item}.
+ * This only works if you're calling this method for an Item known to be using a {@link GeoRenderer AzureLib Renderer}.
+ * Generally speaking you probably shouldn't be calling this method at all. + * + * @param item The {@code Item} to retrieve the GeoModel for + * @return The GeoModel, or null if one isn't found + */ + @Nullable + public static GeoModel getGeoModelForItem(Item item) { + if (RenderProvider.of(item).getCustomRenderer()instanceof GeoRenderer geoRenderer) + return geoRenderer.getGeoModel(); + + return null; + } + + /** + * Gets a {@link GeoModel} instance from a given {@link BlockEntity}.
+ * This only works if you're calling this method for a BlockEntity known to be using a {@link GeoRenderer AzureLib Renderer}.
+ * Generally speaking you probably shouldn't be calling this method at all. + * + * @param blockEntity The {@code BlockEntity} to retrieve the GeoModel for + * @return The GeoModel, or null if one isn't found + */ + @Nullable + public static GeoModel getGeoModelForBlock(BlockEntity blockEntity) { + BlockEntityRenderer renderer = Minecraft.getInstance().getBlockEntityRenderDispatcher().getRenderer(blockEntity); + + return renderer instanceof GeoRenderer geoRenderer ? geoRenderer.getGeoModel() : null; + } + + /** + * Gets a {@link GeoModel} instance from a given {@link Item}.
+ * This only works if you're calling this method for an Item known to be using a {@link GeoArmorRenderer GeoArmorRenderer}.
+ * Generally speaking you probably shouldn't be calling this method at all. + * + * @param stack The ItemStack to retrieve the GeoModel for + * @return The GeoModel, or null if one isn't found + */ + @Nullable + public static GeoModel getGeoModelForArmor(ItemStack stack) { + if (RenderProvider.of(stack).getHumanoidArmorModel(null, stack, null, null)instanceof GeoArmorRenderer armorRenderer) + return armorRenderer.getGeoModel(); + + return null; + } + + private RenderUtils() { + throw new UnsupportedOperationException(); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/AzureLib.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/AzureLib.java new file mode 100644 index 0000000..6a75fd8 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/AzureLib.java @@ -0,0 +1,33 @@ +package mod.azure.azurelib.common.internal.common; + +import mod.azure.azurelib.common.internal.common.util.AzureLibUtil; +import mod.azure.azurelib.common.platform.Services; +import net.minecraft.resources.ResourceLocation; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.MarkerManager; + +/** + * Base class for AzureLib!
+ * Hello World!
+ * There's not much to really see here, but feel free to stay a while and have a snack or something. + * @see AzureLibUtil + */ +public class AzureLib { + public static final Logger LOGGER = LogManager.getLogger("vrglabs_azurelib"); + public static final Marker MAIN_MARKER = MarkerManager.getMarker("main"); + public static final String MOD_ID = "vrglabs_azurelib"; + public static boolean hasInitialized; + + public static void initialize() { + if (!hasInitialized) { + Services.INITIALIZER.initialize(); + } + hasInitialized = true; + } + + public static final ResourceLocation modResource(String name) { + return new ResourceLocation(MOD_ID, name); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/AzureLibException.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/AzureLibException.java new file mode 100644 index 0000000..a2aeacf --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/AzureLibException.java @@ -0,0 +1,34 @@ +package mod.azure.azurelib.common.internal.common; + +import net.minecraft.resources.ResourceLocation; + +import java.io.Serial; + +/** + * Generic {@link Exception} wrapper for AzureLib.
+ * Mostly just serves as a marker for internal error handling. + */ +public class AzureLibException extends RuntimeException { + @Serial + private static final long serialVersionUID = 1L; + + public AzureLibException(ResourceLocation fileLocation, String message) { + super(fileLocation + ": " + message); + } + + public AzureLibException(String message, Throwable cause) { + super(message, cause); + } + + public AzureLibException(String message) { + super(message); + } + + public AzureLibException(Throwable cause) { + super(cause); + } + + public AzureLibException(ResourceLocation fileLocation, String message, Throwable cause) { + super(fileLocation + ": " + message, cause); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/AzureLibMod.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/AzureLibMod.java new file mode 100644 index 0000000..a6f2f81 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/AzureLibMod.java @@ -0,0 +1,43 @@ +package mod.azure.azurelib.common.internal.common; + +import mod.azure.azurelib.common.api.common.config.Config; +import mod.azure.azurelib.common.internal.common.config.AzureLibConfig; +import mod.azure.azurelib.common.internal.common.config.ConfigHolder; +import mod.azure.azurelib.common.internal.common.config.format.ConfigFormats; +import mod.azure.azurelib.common.internal.common.config.format.IConfigFormatHandler; +import mod.azure.azurelib.common.internal.common.config.io.ConfigIO; + +public final class AzureLibMod { + + public static AzureLibConfig config; + + /** + * Registers your config class. Config will be immediately loaded upon calling. + * + * @param cfgClass Your config class + * @param formatFactory File format to be used by this config class. You can use values from {@link ConfigFormats} for example. + * @param Config type + * @return Config holder containing your config instance. You obtain it by calling {@link ConfigHolder#getConfigInstance()} method. + */ + public static ConfigHolder registerConfig(Class cfgClass, IConfigFormatHandler formatFactory) { + Config cfg = cfgClass.getAnnotation(Config.class); + if (cfg == null) { + throw new IllegalArgumentException("Config class must be annotated with '@Config' annotation"); + } + String id = cfg.id(); + String filename = cfg.filename(); + if (filename.isEmpty()) { + filename = id; + } + String group = cfg.group(); + if (group.isEmpty()) { + group = id; + } + ConfigHolder holder = new ConfigHolder<>(cfgClass, id, filename, group, formatFactory); + ConfigHolder.registerConfig(holder); + if (cfgClass.getAnnotation(Config.NoAutoSync.class) == null) { + ConfigIO.FILE_WATCH_MANAGER.addTrackedConfig(holder); + } + return holder; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/ai/pathing/AzurePathFinder.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/ai/pathing/AzurePathFinder.java new file mode 100644 index 0000000..3113b57 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/ai/pathing/AzurePathFinder.java @@ -0,0 +1,60 @@ +package mod.azure.azurelib.common.internal.common.ai.pathing; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.jetbrains.annotations.Nullable; + +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.level.PathNavigationRegion; +import net.minecraft.world.level.pathfinder.Node; +import net.minecraft.world.level.pathfinder.NodeEvaluator; +import net.minecraft.world.level.pathfinder.Path; +import net.minecraft.world.level.pathfinder.PathFinder; +import net.minecraft.world.phys.Vec3; + +public class AzurePathFinder extends PathFinder { + public AzurePathFinder(NodeEvaluator processor, int maxVisitedNodes) { + super(processor, maxVisitedNodes); + } + + @Nullable + @Override + public Path findPath(PathNavigationRegion regionIn, Mob mob, Set targetPositions, float maxRange, + int accuracy, float searchDepthMultiplier) { + Path path = super.findPath(regionIn, mob, targetPositions, maxRange, accuracy, searchDepthMultiplier); + return path == null ? null : new PatchedPath(path); + } + + @Override + protected float distance(Node first, Node second) { + return first.distanceToXZ(second); + } + + static class PatchedPath extends Path { + public PatchedPath(Path original) { + super(copyPathPoints(original), original.getTarget(), original.canReach()); + } + + @Override + public Vec3 getEntityPosAtNode(Entity entity, int index) { + Node point = this.getNode(index); + double d0 = point.x + Mth.floor(entity.getBbWidth() + 1.0F) * 0.5D; + double d1 = point.y; + double d2 = point.z + Mth.floor(entity.getBbWidth() + 1.0F) * 0.5D; + return new Vec3(d0, d1, d2); + } + + private static List copyPathPoints(Path original) { + List points = new ArrayList(); + for (int i = 0; i < original.getNodeCount(); i++) { + points.add(original.getNode(i)); + } + return points; + } + } +} \ No newline at end of file diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/animatable/SingletonGeoAnimatable.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/animatable/SingletonGeoAnimatable.java new file mode 100644 index 0000000..3d071ad --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/animatable/SingletonGeoAnimatable.java @@ -0,0 +1,146 @@ +package mod.azure.azurelib.common.internal.common.animatable; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +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.AnimationController; +import mod.azure.azurelib.common.internal.common.network.SerializableDataTicket; +import mod.azure.azurelib.common.internal.common.network.packet.AnimDataSyncPacket; +import mod.azure.azurelib.common.internal.common.network.packet.AnimTriggerPacket; +import mod.azure.azurelib.common.platform.Services; +import mod.azure.azurelib.common.platform.services.AzureLibNetwork; +import org.jetbrains.annotations.Nullable; + +import net.minecraft.world.entity.Entity; + +/** + * The {@link GeoAnimatable} interface specific to singleton objects. This primarily applies to armor and items + */ +public interface SingletonGeoAnimatable extends GeoAnimatable { + /** + * Register this as a synched {@code GeoAnimatable} instance with AzureLib's networking functions.
+ * This should be called inside the constructor of your object. + */ + static void registerSyncedAnimatable(GeoAnimatable animatable) { + Services.NETWORK.registerSyncedAnimatable(animatable); + } + + /** + * Get server-synced animation data via its relevant {@link SerializableDataTicket}.
+ * Should only be used on the client-side.
+ * DO NOT OVERRIDE + * + * @param instanceId The animatable's instance id + * @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(long instanceId, SerializableDataTicket dataTicket) { + return getAnimatableInstanceCache().getManagerForId(instanceId).getData(dataTicket); + } + + /** + * Saves an arbitrary piece of syncable data to this animatable's {@link AnimatableManager}.
+ * DO NOT OVERRIDE + * + * @param relatedEntity An entity related to the state of the data for syncing (E.G. The player holding the item) + * @param instanceId The unique id that identifies the specific animatable instance + * @param dataTicket The DataTicket to sync the data for + * @param data The data to sync + */ + default void setAnimData(Entity relatedEntity, long instanceId, SerializableDataTicket dataTicket, D data) { + if (relatedEntity.level().isClientSide()) { + getAnimatableInstanceCache().getManagerForId(instanceId).setData(dataTicket, data); + } else { + syncAnimData(instanceId, dataTicket, data, relatedEntity); + } + } + + /** + * Syncs an arbitrary piece of data to all players targeted by the packetTarget.
+ * This method should only be called on the server side.
+ * DO NOT OVERRIDE + * + * @param instanceId The unique id that identifies the specific animatable instance + * @param dataTicket The DataTicket to sync the data for + * @param data The data to sync + */ + default void syncAnimData(long instanceId, SerializableDataTicket dataTicket, D data, Entity entityToTrack) { + Services.NETWORK.sendToTrackingEntityAndSelf(new AnimDataSyncPacket<>(getClass().toString(), instanceId, dataTicket, data), entityToTrack); + } + + /** + * Trigger a client-side animation for this GeoAnimatable for the given controller name and animation name.
+ * This can be fired from either the client or the server, but optimally you would call it from the server.
+ * DO NOT OVERRIDE + * + * @param relatedEntity An entity related to the animatable to trigger the animation for (E.G. The player holding the item) + * @param instanceId The unique id that identifies the specific animatable instance + * @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(Entity relatedEntity, long instanceId, @Nullable String controllerName, String animName) { + if (relatedEntity.level().isClientSide()) { + getAnimatableInstanceCache().getManagerForId(instanceId).tryTriggerAnimation(controllerName, animName); + } else { + Services.NETWORK.sendToTrackingEntityAndSelf(new AnimTriggerPacket(getClass().toString(), instanceId, controllerName, animName), relatedEntity); + } + } + + /** + * Remotely triggers a client-side animation for this GeoAnimatable for all players targeted by the packetTarget.
+ * This method should only be called on the server side.
+ * DO NOT OVERRIDE + * + * @param instanceId The unique id that identifies the specific animatable instance + * @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} + * @param packetCallback The packet callback. Used to call a custom network code + */ + default void triggerAnim(long instanceId, @Nullable String controllerName, String animName, AzureLibNetwork.IPacketCallback packetCallback) { + AzureLibNetwork.sendWithCallback(new AnimTriggerPacket(getClass().toString(), instanceId, controllerName, animName), packetCallback); + } + + /** + * Override the default handling for instantiating an AnimatableInstanceCache for this animatable.
+ * Don't override this unless you know what you're doing. + */ + @Override + default @Nullable AnimatableInstanceCache animatableCacheOverride() { + return new SingletonAnimatableInstanceCache(this); + } + + /** + * Create your RenderProvider reference here.
+ * MUST provide an anonymous class
+ * Example Code: + * + *
+	 * {@code
+	 * @Override
+	 * public void createRenderer(Consumer consumer) {
+	 * 	consumer.accept(new RenderProvider() {
+	 * 		private final GeoArmorRenderer renderer = new MyArmorRenderer();
+	 *
+	 *        @Override
+	 *        GeoArmorRenderer getRenderer(GeoArmor armor) {
+	 * 			return this.renderer;
+	 *        }
+	 *    }
+	 * }
+	 * }
+	 * 
+ * + * @param consumer + */ + void createRenderer(Consumer consumer); + + /** + * Getter for the cached RenderProvider in your class + */ + Supplier getRenderProvider(); +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/blocks/TickingLightBlock.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/blocks/TickingLightBlock.java new file mode 100644 index 0000000..856a6a2 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/blocks/TickingLightBlock.java @@ -0,0 +1,75 @@ +package mod.azure.azurelib.common.internal.common.blocks; + +import com.mojang.serialization.MapCodec; +import mod.azure.azurelib.common.platform.Services; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.*; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition.Builder; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.IntegerProperty; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; + +import java.util.function.ToIntFunction; + +public class TickingLightBlock extends BaseEntityBlock { + public static final MapCodec CODEC = simpleCodec(TickingLightBlock::new); + + public static final IntegerProperty LIGHT_LEVEL = BlockStateProperties.AGE_15; + + public TickingLightBlock(BlockBehaviour.Properties properties) { + super(properties); + } + + public static ToIntFunction litBlockEmission(int p_50760_) { + return p_50763_ -> BlockStateProperties.MAX_LEVEL_15; + } + + public static IntegerProperty getLightLevel() { + return LIGHT_LEVEL; + } + + @Override + protected void createBlockStateDefinition(Builder builder) { + builder.add(LIGHT_LEVEL); + } + + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new TickingLightEntity(pos, state); + } + + @Override + public VoxelShape getShape(BlockState p_60555_, BlockGetter p_60556_, BlockPos p_60557_, CollisionContext p_60558_) { + return Shapes.empty(); + } + + @Override + public boolean propagatesSkylightDown(BlockState state, BlockGetter world, BlockPos pos) { + return true; + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + public RenderShape getRenderShape(BlockState state) { + return RenderShape.INVISIBLE; + } + + @Override + public BlockEntityTicker getTicker(Level world, BlockState state, BlockEntityType type) { + return createTickerHelper(type, Services.PLATFORM.getTickingLightEntity(), TickingLightEntity::tick); + } + +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/blocks/TickingLightEntity.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/blocks/TickingLightEntity.java new file mode 100644 index 0000000..db1bb04 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/blocks/TickingLightEntity.java @@ -0,0 +1,34 @@ +package mod.azure.azurelib.common.internal.common.blocks; + +import mod.azure.azurelib.common.platform.Services; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +public class TickingLightEntity extends BlockEntity { + private int lifespan = 0; + + public TickingLightEntity(BlockPos blockPos, BlockState blockState) { + super(Services.PLATFORM.getTickingLightEntity(), blockPos, blockState); + } + + public void refresh(int lifeExtension) { + lifespan = -lifeExtension; + } + + private void tick() { + if (lifespan++ >= 5) { + if (level.getBlockState(getBlockPos()).getBlock() instanceof TickingLightBlock) + level.setBlockAndUpdate(getBlockPos(), Blocks.AIR.defaultBlockState()); + else + setRemoved(); + } + } + + public static void tick(Level world, BlockPos blockPos, BlockState blockState, + TickingLightEntity blockEntity) { + blockEntity.tick(); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/AnimatableIdCache.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/AnimatableIdCache.java new file mode 100644 index 0000000..5aafb75 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/AnimatableIdCache.java @@ -0,0 +1,54 @@ +package mod.azure.azurelib.common.internal.common.cache; + +import mod.azure.azurelib.common.internal.common.core.animatable.instance.SingletonAnimatableInstanceCache; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.datafix.DataFixTypes; +import net.minecraft.world.level.saveddata.SavedData; + +/** + * Storage class that keeps track of the last animatable id used, and provides new ones on request.
+ * Generally only used for {@link net.minecraft.world.item.Item Items}, but any + * {@link SingletonAnimatableInstanceCache singleton} will likely use this. + */ +public final class AnimatableIdCache extends SavedData { + private static final String DATA_KEY = "AzureLib_id_cache"; + private long lastId; + + private AnimatableIdCache() { + this(new CompoundTag()); + } + + private AnimatableIdCache(CompoundTag tag) { + this.lastId = tag.getLong("last_id"); + } + + public static SavedData.Factory factory() { + return new SavedData.Factory(AnimatableIdCache::new, AnimatableIdCache::new, DataFixTypes.SAVED_DATA_MAP_DATA); + } + + /** + * Get the next free id from the id cache + * + * @param level An arbitrary ServerLevel. It doesn't matter which one + * @return The next free ID, which is immediately reserved for use after calling this method + */ + public static long getFreeId(ServerLevel level) { + return getCache(level).getNextId(); + } + + private long getNextId() { + setDirty(); + return ++this.lastId; + } + + @Override + public CompoundTag save(CompoundTag tag) { + tag.putLong("last_id", this.lastId); + return tag; + } + + private static AnimatableIdCache getCache(ServerLevel level) { + return level.getServer().overworld().getDataStorage().computeIfAbsent(AnimatableIdCache.factory(), DATA_KEY); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/AzureLibCache.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/AzureLibCache.java new file mode 100644 index 0000000..86c4916 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/AzureLibCache.java @@ -0,0 +1,124 @@ +package mod.azure.azurelib.common.internal.common.cache; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import mod.azure.azurelib.common.internal.common.core.animatable.model.CoreGeoModel; +import mod.azure.azurelib.common.internal.common.core.animation.Animation; +import mod.azure.azurelib.common.internal.common.AzureLib; +import mod.azure.azurelib.common.internal.common.AzureLibException; +import mod.azure.azurelib.common.internal.common.cache.object.BakedGeoModel; +import mod.azure.azurelib.common.internal.common.loading.FileLoader; +import mod.azure.azurelib.common.internal.common.loading.json.FormatVersion; +import mod.azure.azurelib.common.internal.common.loading.json.raw.Model; +import mod.azure.azurelib.common.internal.common.loading.object.BakedAnimations; +import mod.azure.azurelib.common.internal.common.loading.object.BakedModelFactory; +import mod.azure.azurelib.common.internal.common.loading.object.GeometryTree; +import net.minecraft.client.Minecraft; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.PreparableReloadListener.PreparationBarrier; +import net.minecraft.server.packs.resources.ReloadableResourceManager; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.util.profiling.ProfilerFiller; + +import java.util.Collections; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * Cache class for holding loaded + * {@link Animation Animations} and + * {@link CoreGeoModel Models} + */ +public final class AzureLibCache { + private static final Set EXCLUDED_NAMESPACES = ObjectOpenHashSet.of("moreplayermodels", "customnpcs", "gunsrpg"); + + private static Map ANIMATIONS = Collections.emptyMap(); + private static Map MODELS = Collections.emptyMap(); + + public static Map getBakedAnimations() { + if (!AzureLib.hasInitialized) + throw new AzureLibException("AzureLib was never initialized! Please read the documentation!"); + + return ANIMATIONS; + } + + public static Map getBakedModels() { + if (!AzureLib.hasInitialized) + throw new AzureLibException("AzureLib was never initialized! Please read the documentation!"); + + return MODELS; + } + + public static void registerReloadListener() { + Minecraft mc = Minecraft.getInstance(); + + if (mc == null) { + return; + } + + if (!(mc.getResourceManager() instanceof ReloadableResourceManager resourceManager)) + throw new AzureLibException("AzureLib was initialized too early!"); + + resourceManager.registerReloadListener(AzureLibCache::reload); + } + + public static CompletableFuture reload(PreparationBarrier stage, ResourceManager resourceManager, + ProfilerFiller preparationsProfiler, ProfilerFiller reloadProfiler, Executor backgroundExecutor, + Executor gameExecutor) { + Map animations = new Object2ObjectOpenHashMap<>(); + Map models = new Object2ObjectOpenHashMap<>(); + + return CompletableFuture + .allOf(loadAnimations(backgroundExecutor, resourceManager, animations::put), + loadModels(backgroundExecutor, resourceManager, models::put)) + .thenCompose(stage::wait).thenAcceptAsync(empty -> { + AzureLibCache.ANIMATIONS = animations; + AzureLibCache.MODELS = models; + }, gameExecutor); + } + + private static CompletableFuture loadAnimations(Executor backgroundExecutor, ResourceManager resourceManager, + BiConsumer elementConsumer) { + return loadResources(backgroundExecutor, resourceManager, "animations", + resource -> FileLoader.loadAnimationsFile(resource, resourceManager), elementConsumer); + } + + private static CompletableFuture loadModels(Executor backgroundExecutor, ResourceManager resourceManager, + BiConsumer elementConsumer) { + return loadResources(backgroundExecutor, resourceManager, "geo", resource -> { + Model model = FileLoader.loadModelFile(resource, resourceManager); + + if (model.formatVersion() != FormatVersion.V_1_12_0) + throw new AzureLibException(resource, "Unsupported geometry json version. Supported versions: 1.12.0"); + + return BakedModelFactory.getForNamespace(resource.getNamespace()) + .constructGeoModel(GeometryTree.fromModel(model)); + }, elementConsumer); + } + + private static CompletableFuture loadResources(Executor executor, ResourceManager resourceManager, + String type, Function loader, BiConsumer map) { + return CompletableFuture.supplyAsync( + () -> resourceManager.listResources(type, fileName -> fileName.toString().endsWith(".json")), executor) + .thenApplyAsync(resources -> { + Map> tasks = new Object2ObjectOpenHashMap<>(); + + for (ResourceLocation resource : resources.keySet()) { + tasks.put(resource, CompletableFuture.supplyAsync(() -> loader.apply(resource), executor)); + } + + return tasks; + }, executor).thenAcceptAsync(tasks -> { + for (Entry> entry : tasks.entrySet()) { + if (!EXCLUDED_NAMESPACES.contains(entry.getKey().getNamespace().toLowerCase(Locale.ROOT))) + map.accept(entry.getKey(), entry.getValue().join()); + } + }, executor); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/object/BakedGeoModel.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/object/BakedGeoModel.java new file mode 100644 index 0000000..450f72b --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/object/BakedGeoModel.java @@ -0,0 +1,39 @@ +package mod.azure.azurelib.common.internal.common.cache.object; + +import java.util.List; +import java.util.Optional; + +import mod.azure.azurelib.common.internal.common.core.animatable.model.CoreBakedGeoModel; +import mod.azure.azurelib.common.internal.common.core.animatable.model.CoreGeoBone; +import mod.azure.azurelib.common.internal.common.loading.json.raw.ModelProperties; + +/** + * Baked model object for AzureLib models. + */ +public record BakedGeoModel(List topLevelBones, ModelProperties properties) implements CoreBakedGeoModel { + /** + * Gets the list of top-level bones for this model. + * Identical to calling {@link BakedGeoModel#topLevelBones()} + */ + @Override + public List getBones() { + return this.topLevelBones; + } + + /** + * Gets a bone from this model by name.
+ * Generally not a very efficient method, should be avoided where possible. + * @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) { + for (GeoBone bone : this.topLevelBones) { + CoreGeoBone childBone = searchForChildBone(bone, name); + + if (childBone != null) + return Optional.of((GeoBone)childBone); + } + + return Optional.empty(); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/object/GeoBone.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/object/GeoBone.java new file mode 100644 index 0000000..802f9b2 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/object/GeoBone.java @@ -0,0 +1,446 @@ +package mod.azure.azurelib.common.internal.common.cache.object; + +import java.util.List; +import java.util.Objects; + +import mod.azure.azurelib.common.internal.common.core.animatable.model.CoreGeoBone; +import org.jetbrains.annotations.Nullable; +import org.joml.Matrix3f; +import org.joml.Matrix4f; +import org.joml.Vector3d; +import org.joml.Vector4f; + +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import mod.azure.azurelib.common.internal.common.core.state.BoneSnapshot; + +/** + * Mutable bone object representing a set of cubes, as well as child bones.
+ * This is the object that is directly modified by animations to handle movement + */ +public class GeoBone implements CoreGeoBone { + private final GeoBone parent; + private final String name; + + private final List children = new ObjectArrayList<>(); + private final List cubes = new ObjectArrayList<>(); + + private final Boolean mirror; + private final Double inflate; + private final Boolean dontRender; + private final Boolean reset; + + private BoneSnapshot initialSnapshot; + + private boolean hidden; + private boolean childrenHidden = false; + + private float scaleX = 1; + private float scaleY = 1; + private float scaleZ = 1; + + private float positionX; + private float positionY; + private float positionZ; + + private float pivotX; + private float pivotY; + private float pivotZ; + + private float rotX; + private float rotY; + private float rotZ; + + private boolean positionChanged = false; + private boolean rotationChanged = false; + private boolean scaleChanged = false; + private final Matrix4f modelSpaceMatrix = new Matrix4f(); + private final Matrix4f localSpaceMatrix = new Matrix4f(); + private final Matrix4f worldSpaceMatrix = new Matrix4f(); + private Matrix3f worldSpaceNormal = new Matrix3f(); + + private boolean trackingMatrices; + + public GeoBone(@Nullable GeoBone parent, String name, Boolean mirror, @Nullable Double inflate, @Nullable Boolean dontRender, @Nullable Boolean reset) { + this.parent = parent; + this.name = name; + this.mirror = mirror; + this.inflate = inflate; + this.dontRender = dontRender; + this.reset = reset; + this.trackingMatrices = false; + this.hidden = this.dontRender == Boolean.TRUE; + + this.worldSpaceNormal.identity(); + this.worldSpaceMatrix.identity(); + this.localSpaceMatrix.identity(); + this.modelSpaceMatrix.identity(); + } + + @Override + public String getName() { + return this.name; + } + + @Override + public GeoBone getParent() { + return this.parent; + } + + @Override + public float getRotX() { + return this.rotX; + } + + @Override + public float getRotY() { + return this.rotY; + } + + @Override + public float getRotZ() { + return this.rotZ; + } + + @Override + public float getPosX() { + return this.positionX; + } + + @Override + public float getPosY() { + return this.positionY; + } + + @Override + public float getPosZ() { + return this.positionZ; + } + + @Override + public float getScaleX() { + return this.scaleX; + } + + @Override + public float getScaleY() { + return this.scaleY; + } + + @Override + public float getScaleZ() { + return this.scaleZ; + } + + @Override + public void setRotX(float value) { + this.rotX = value; + + markRotationAsChanged(); + } + + @Override + public void setRotY(float value) { + this.rotY = value; + + markRotationAsChanged(); + } + + @Override + public void setRotZ(float value) { + this.rotZ = value; + + markRotationAsChanged(); + } + + @Override + public void setPosX(float value) { + this.positionX = value; + + markPositionAsChanged(); + } + + @Override + public void setPosY(float value) { + this.positionY = value; + + markPositionAsChanged(); + } + + @Override + public void setPosZ(float value) { + this.positionZ = value; + + markPositionAsChanged(); + } + + @Override + public void setScaleX(float value) { + this.scaleX = value; + + markScaleAsChanged(); + } + + @Override + public void setScaleY(float value) { + this.scaleY = value; + + markScaleAsChanged(); + } + + @Override + public void setScaleZ(float value) { + this.scaleZ = value; + + markScaleAsChanged(); + } + + @Override + public boolean isHidden() { + return this.hidden; + } + + @Override + public void setHidden(boolean hidden) { + this.hidden = hidden; + + setChildrenHidden(hidden); + } + + @Override + public void setChildrenHidden(boolean hideChildren) { + this.childrenHidden = hideChildren; + } + + @Override + public void setPivotX(float value) { + this.pivotX = value; + } + + @Override + public void setPivotY(float value) { + this.pivotY = value; + } + + @Override + public void setPivotZ(float value) { + this.pivotZ = value; + } + + @Override + public float getPivotX() { + return this.pivotX; + } + + @Override + public float getPivotY() { + return this.pivotY; + } + + @Override + public float getPivotZ() { + return this.pivotZ; + } + + @Override + public boolean isHidingChildren() { + return this.childrenHidden; + } + + @Override + public void markScaleAsChanged() { + this.scaleChanged = true; + } + + @Override + public void markRotationAsChanged() { + this.rotationChanged = true; + } + + @Override + public void markPositionAsChanged() { + this.positionChanged = true; + } + + @Override + public boolean hasScaleChanged() { + return this.scaleChanged; + } + + @Override + public boolean hasRotationChanged() { + return this.rotationChanged; + } + + @Override + public boolean hasPositionChanged() { + return this.positionChanged; + } + + @Override + public void resetStateChanges() { + this.scaleChanged = false; + this.rotationChanged = false; + this.positionChanged = false; + } + + @Override + public BoneSnapshot getInitialSnapshot() { + return this.initialSnapshot; + } + + @Override + public List getChildBones() { + return this.children; + } + + @Override + public void saveInitialSnapshot() { + if (this.initialSnapshot == null) + this.initialSnapshot = saveSnapshot(); + } + + public Boolean getMirror() { + return this.mirror; + } + + public Double getInflate() { + return this.inflate; + } + + public Boolean shouldNeverRender() { + return this.dontRender; + } + + public Boolean getReset() { + return this.reset; + } + + public List getCubes() { + return this.cubes; + } + + public boolean isTrackingMatrices() { + return trackingMatrices; + } + + public void setTrackingMatrices(boolean trackingMatrices) { + this.trackingMatrices = trackingMatrices; + } + + public Matrix4f getModelSpaceMatrix() { + setTrackingMatrices(true); + + return this.modelSpaceMatrix; + } + + public void setModelSpaceMatrix(Matrix4f matrix) { + this.modelSpaceMatrix.set(matrix); + } + + public Matrix4f getLocalSpaceMatrix() { + setTrackingMatrices(true); + + return this.localSpaceMatrix; + } + + public void setLocalSpaceMatrix(Matrix4f matrix) { + this.localSpaceMatrix.set(matrix); + } + + public Matrix4f getWorldSpaceMatrix() { + setTrackingMatrices(true); + + return this.worldSpaceMatrix; + } + + public void setWorldSpaceMatrix(Matrix4f matrix) { + this.worldSpaceMatrix.set(matrix); + } + + public void setWorldSpaceNormal(Matrix3f matrix) { + this.worldSpaceNormal = matrix; + } + + public Matrix3f getWorldSpaceNormal() { + return worldSpaceNormal; + } + + /** + * Get the position of the bone relative to its owner + */ + public Vector3d getLocalPosition() { + Vector4f vec = getLocalSpaceMatrix().transform(new Vector4f(0, 0, 0, 1)); + + return new Vector3d(vec.x(), vec.y(), vec.z()); + } + + /** + * Get the position of the bone relative to the model it belongs to + */ + public Vector3d getModelPosition() { + Vector4f vec = getModelSpaceMatrix().transform(new Vector4f(0, 0, 0, 1)); + + return new Vector3d(-vec.x() * 16f, vec.y() * 16f, vec.z() * 16f); + } + + /** + * Get the position of the bone relative to the world + */ + public Vector3d getWorldPosition() { + Vector4f vec = getWorldSpaceMatrix().transform(new Vector4f(0, 0, 0, 1)); + + return new Vector3d(vec.x(), vec.y(), vec.z()); + } + + public void setModelPosition(Vector3d pos) { + // Doesn't work on bones with parent transforms + GeoBone parent = getParent(); + Matrix4f matrix = (parent == null ? new Matrix4f().identity() : new Matrix4f(parent.getModelSpaceMatrix())).invert(); + Vector4f vec = matrix.transform(new Vector4f(-(float)pos.x / 16f, (float)pos.y / 16f, (float)pos.z / 16f, 1)); + + updatePosition(-vec.x() * 16f, vec.y() * 16f, vec.z() * 16f); + } + + public Matrix4f getModelRotationMatrix() { + Matrix4f matrix = new Matrix4f(getModelSpaceMatrix()); + matrix.m03(0); + matrix.m13(0); + matrix.m23(0); + + return matrix; + } + + public Vector3d getPositionVector() { + return new Vector3d(getPosX(), getPosY(), getPosZ()); + } + + public Vector3d getRotationVector() { + return new Vector3d(getRotX(), getRotY(), getRotZ()); + } + + public Vector3d getScaleVector() { + return new Vector3d(getScaleX(), getScaleY(), getScaleZ()); + } + + public void addRotationOffsetFromBone(GeoBone source) { + setRotX(getRotX() + source.getRotX() - source.getInitialSnapshot().getRotX()); + setRotY(getRotY() + source.getRotY() - source.getInitialSnapshot().getRotY()); + setRotZ(getRotZ() + source.getRotZ() - source.getInitialSnapshot().getRotZ()); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + + if (obj == null || getClass() != obj.getClass()) + return false; + + return hashCode() == obj.hashCode(); + } + + @Override + public int hashCode() { + return Objects.hash(getName(), (getParent() != null ? getParent().getName() : 0), getCubes().size(), getChildBones().size()); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/object/GeoCube.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/object/GeoCube.java new file mode 100644 index 0000000..f51b87c --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/object/GeoCube.java @@ -0,0 +1,8 @@ +package mod.azure.azurelib.common.internal.common.cache.object; + +import net.minecraft.world.phys.Vec3; + +/** + * Baked cuboid for a {@link GeoBone} + */ +public record GeoCube(GeoQuad[] quads, Vec3 pivot, Vec3 rotation, Vec3 size, double inflate, boolean mirror) {} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/object/GeoQuad.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/object/GeoQuad.java new file mode 100644 index 0000000..e94c389 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/object/GeoQuad.java @@ -0,0 +1,38 @@ +package mod.azure.azurelib.common.internal.common.cache.object; + +import org.joml.Vector3f; + +import net.minecraft.core.Direction; + +/** + * Quad data holder + */ +public record GeoQuad(GeoVertex[] vertices, Vector3f normal, Direction direction) { + public static GeoQuad build(GeoVertex[] vertices, double[] uvCoords, double[] uvSize, float texWidth, float texHeight, boolean mirror, Direction direction) { + return build(vertices, (float)uvCoords[0], (float)uvCoords[1], (float)uvSize[0], (float)uvSize[1], texWidth, texHeight, mirror, direction); + } + + public static GeoQuad build(GeoVertex[] vertices, float u, float v, float uSize, float vSize, float texWidth, + float texHeight, boolean mirror, Direction direction) { + float uWidth = (u + uSize) / texWidth; + float vHeight = (v + vSize) / texHeight; + u /= texWidth; + v /= texHeight; + Vector3f normal = direction.step(); + + if (!mirror) { + float tempWidth = uWidth; + uWidth = u; + u = tempWidth; + + normal.mul(-1, 1, 1); + } + + vertices[0] = vertices[0].withUVs(u, v); + vertices[1] = vertices[1].withUVs(uWidth, v); + vertices[2] = vertices[2].withUVs(uWidth, vHeight); + vertices[3] = vertices[3].withUVs(u, vHeight); + + return new GeoQuad(vertices, normal, direction); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/object/GeoVertex.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/object/GeoVertex.java new file mode 100644 index 0000000..512e86d --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/object/GeoVertex.java @@ -0,0 +1,19 @@ +package mod.azure.azurelib.common.internal.common.cache.object; + +import org.joml.Vector3f; + +/** + * Vertex data holder + * @param position The position of the vertex + * @param texU The texture U coordinate + * @param texV The texture V coordinate + */ +public record GeoVertex(Vector3f position, float texU, float texV) { + public GeoVertex(double x, double y, double z) { + this(new Vector3f((float)x, (float)y, (float)z), 0, 0); + } + + public GeoVertex withUVs(float texU, float texV) { + return new GeoVertex(this.position, texU, texV); + } +} \ No newline at end of file diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/texture/AnimatableTexture.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/texture/AnimatableTexture.java new file mode 100644 index 0000000..3a92fa3 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/texture/AnimatableTexture.java @@ -0,0 +1,288 @@ +package mod.azure.azurelib.common.internal.common.cache.texture; + +import com.mojang.blaze3d.pipeline.RenderCall; +import com.mojang.blaze3d.platform.NativeImage; +import com.mojang.blaze3d.platform.TextureUtil; +import com.mojang.blaze3d.systems.RenderSystem; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import mod.azure.azurelib.common.internal.common.AzureLib; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.AbstractTexture; +import net.minecraft.client.renderer.texture.SimpleTexture; +import net.minecraft.client.resources.metadata.animation.AnimationMetadataSection; +import net.minecraft.client.resources.metadata.animation.FrameSize; +import net.minecraft.client.resources.metadata.texture.TextureMetadataSection; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.ResourceMetadata; +import net.minecraft.util.Mth; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +/** + * Wrapper for {@link SimpleTexture SimpleTexture} implementation allowing for casual use of animated non-atlas textures + */ +public class AnimatableTexture extends SimpleTexture { + private AnimationContents animationContents = null; + + public AnimatableTexture(final ResourceLocation location) { + super(location); + } + + @Override + public void load(ResourceManager manager) throws IOException { + Optional resource = manager.getResource(this.location); + + NativeImage nativeImage; + TextureMetadataSection simpleTextureMeta = new TextureMetadataSection(false, false); + + if (resource.isPresent()) { + try (InputStream inputstream = resource.get().open()) { + nativeImage = NativeImage.read(inputstream); + } + + try { + ResourceMetadata meta = resource.get().metadata(); + + simpleTextureMeta = meta.getSection(TextureMetadataSection.SERIALIZER).orElse(simpleTextureMeta); + this.animationContents = meta.getSection(AnimationMetadataSection.SERIALIZER).map( + animMeta -> new AnimationContents(nativeImage, animMeta)).orElse(null); + + if (this.animationContents != null) { + if (!this.animationContents.isValid()) { + nativeImage.close(); + + return; + } + + onRenderThread(() -> { + TextureUtil.prepareImage(getId(), 0, this.animationContents.frameSize.width(), + this.animationContents.frameSize.height()); + nativeImage.upload(0, 0, 0, 0, 0, this.animationContents.frameSize.width(), + this.animationContents.frameSize.height(), false, false); + }); + + return; + } + } catch (RuntimeException exception) { + AzureLib.LOGGER.warn("Failed reading metadata of: {}", this.location, exception); + } + + boolean blur = simpleTextureMeta.isBlur(); + boolean clamp = simpleTextureMeta.isClamp(); + + onRenderThread(() -> GeoAbstractTexture.uploadSimple(getId(), nativeImage, blur, clamp)); + } + } + + public static void setAndUpdate(ResourceLocation texturePath, int frameTick) { + AbstractTexture texture = Minecraft.getInstance().getTextureManager().getTexture(texturePath); + + if (texture instanceof AnimatableTexture animatableTexture) + animatableTexture.setAnimationFrame(frameTick); + + RenderSystem.setShaderTexture(0, texture.getId()); + } + + public void setAnimationFrame(int tick) { + if (this.animationContents != null) + this.animationContents.animatedTexture.setCurrentFrame(tick); + } + + private static void onRenderThread(RenderCall renderCall) { + if (!RenderSystem.isOnRenderThread()) { + RenderSystem.recordRenderCall(renderCall); + } else { + renderCall.execute(); + } + } + + private class AnimationContents { + private final FrameSize frameSize; + private final Texture animatedTexture; + + private AnimationContents(NativeImage image, AnimationMetadataSection animMeta) { + this.frameSize = animMeta.calculateFrameSize(image.getWidth(), image.getHeight()); + this.animatedTexture = generateAnimatedTexture(image, animMeta); + } + + private boolean isValid() { + return this.animatedTexture != null; + } + + private Texture generateAnimatedTexture(NativeImage image, AnimationMetadataSection animMeta) { + if (!Mth.isMultipleOf(image.getWidth(), this.frameSize.width()) || !Mth.isMultipleOf(image.getHeight(), + this.frameSize.height())) { + AzureLib.LOGGER.error("Image {} size {},{} is not multiple of frame size {},{}", + AnimatableTexture.this.location, image.getWidth(), image.getHeight(), this.frameSize.width(), + this.frameSize.height()); + + return null; + } + + int columns = image.getWidth() / this.frameSize.width(); + int rows = image.getHeight() / this.frameSize.height(); + int frameCount = columns * rows; + List frames = new ObjectArrayList<>(); + + animMeta.forEachFrame((frame, frameTime) -> frames.add(new Frame(frame, frameTime))); + + if (frames.isEmpty()) { + for (int frame = 0; frame < frameCount; ++frame) { + frames.add(new Frame(frame, animMeta.getDefaultFrameTime())); + } + } else { + int index = 0; + IntSet unusedFrames = new IntOpenHashSet(); + + for (Frame frame : frames) { + if (frame.time <= 0) { + AzureLib.LOGGER.warn("Invalid frame duration on sprite {} frame {}: {}", + AnimatableTexture.this.location, index, frame.time); + unusedFrames.add(frame.index); + } else if (frame.index < 0 || frame.index >= frameCount) { + AzureLib.LOGGER.warn("Invalid frame index on sprite {} frame {}: {}", + AnimatableTexture.this.location, index, frame.index); + unusedFrames.add(frame.index); + } + + index++; + } + + if (!unusedFrames.isEmpty()) + AzureLib.LOGGER.warn("Unused frames in sprite {}: {}", AnimatableTexture.this.location, + Arrays.toString(unusedFrames.toArray())); + } + + return frames.size() <= 1 ? null : new Texture(image, frames.toArray(new Frame[0]), columns, + animMeta.isInterpolatedFrames()); + } + + private record Frame(int index, int time) { + } + + private class Texture implements AutoCloseable { + private final NativeImage baseImage; + private final Frame[] frames; + private final int framePanelSize; + private final boolean interpolating; + private final NativeImage interpolatedFrame; + private final int totalFrameTime; + + private int currentFrame; + private int currentSubframe; + + private Texture(NativeImage baseImage, Frame[] frames, int framePanelSize, boolean interpolating) { + this.baseImage = baseImage; + this.frames = frames; + this.framePanelSize = framePanelSize; + this.interpolating = interpolating; + this.interpolatedFrame = interpolating ? new NativeImage(AnimationContents.this.frameSize.width(), + AnimationContents.this.frameSize.height(), false) : null; + int time = 0; + + for (Frame frame : this.frames) { + time += frame.time; + } + + this.totalFrameTime = time; + } + + private int getFrameX(int frameIndex) { + return frameIndex % this.framePanelSize; + } + + private int getFrameY(int frameIndex) { + return frameIndex / this.framePanelSize; + } + + public void setCurrentFrame(int ticks) { + ticks %= this.totalFrameTime; + + if (ticks == this.currentSubframe) + return; + + int lastSubframe = this.currentSubframe; + int lastFrame = this.currentFrame; + int time = 0; + + for (Frame frame : this.frames) { + time += frame.time; + + if (ticks < time) { + this.currentFrame = frame.index; + this.currentSubframe = ticks % frame.time; + + break; + } + } + + if (this.currentFrame != lastFrame && this.currentSubframe == 0) { + onRenderThread(() -> { + TextureUtil.prepareImage(AnimatableTexture.this.getId(), 0, + AnimationContents.this.frameSize.width(), AnimationContents.this.frameSize.height()); + this.baseImage.upload(0, 0, 0, + getFrameX(this.currentFrame) * AnimationContents.this.frameSize.width(), + getFrameY(this.currentFrame) * AnimationContents.this.frameSize.height(), + AnimationContents.this.frameSize.width(), AnimationContents.this.frameSize.height(), + false, false); + }); + } else if (this.currentSubframe != lastSubframe && this.interpolating) { + onRenderThread(this::generateInterpolatedFrame); + } + } + + private void generateInterpolatedFrame() { + Frame frame = this.frames[this.currentFrame]; + double frameProgress = 1 - (double) this.currentSubframe / (double) frame.time; + int nextFrameIndex = this.frames[(this.currentFrame + 1) % this.frames.length].index; + + if (frame.index != nextFrameIndex) { + for (int y = 0; y < this.interpolatedFrame.getHeight(); ++y) { + for (int x = 0; x < this.interpolatedFrame.getWidth(); ++x) { + int prevFramePixel = getPixel(frame.index, x, y); + int nextFramePixel = getPixel(nextFrameIndex, x, y); + int blendedRed = interpolate(frameProgress, prevFramePixel >> 16 & 255, + nextFramePixel >> 16 & 255); + int blendedGreen = interpolate(frameProgress, prevFramePixel >> 8 & 255, + nextFramePixel >> 8 & 255); + int blendedBlue = interpolate(frameProgress, prevFramePixel & 255, nextFramePixel & 255); + + this.interpolatedFrame.setPixelRGBA(x, y, + prevFramePixel & -16777216 | blendedRed << 16 | blendedGreen << 8 | blendedBlue); + } + } + + TextureUtil.prepareImage(AnimatableTexture.this.getId(), 0, + AnimationContents.this.frameSize.width(), AnimationContents.this.frameSize.height()); + this.interpolatedFrame.upload(0, 0, 0, 0, 0, AnimationContents.this.frameSize.width(), + AnimationContents.this.frameSize.height(), false, false); + } + } + + private int getPixel(int frameIndex, int x, int y) { + return this.baseImage.getPixelRGBA(x + getFrameX(frameIndex) * AnimationContents.this.frameSize.width(), + y + getFrameY(frameIndex) * AnimationContents.this.frameSize.height()); + } + + private int interpolate(double frameProgress, double prevColour, double nextColour) { + return (int) (frameProgress * prevColour + (1 - frameProgress) * nextColour); + } + + @Override + public void close() { + this.baseImage.close(); + + if (this.interpolatedFrame != null) + this.interpolatedFrame.close(); + } + } + } +} \ No newline at end of file diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/texture/AutoGlowingTexture.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/texture/AutoGlowingTexture.java new file mode 100644 index 0000000..8638874 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/texture/AutoGlowingTexture.java @@ -0,0 +1,145 @@ +package mod.azure.azurelib.common.internal.common.cache.texture; + +import com.mojang.blaze3d.pipeline.RenderCall; +import com.mojang.blaze3d.platform.GlStateManager; +import com.mojang.blaze3d.platform.NativeImage; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.VertexFormat; +import mod.azure.azurelib.common.internal.common.AzureLib; +import mod.azure.azurelib.common.platform.Services; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.renderer.RenderStateShard; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.texture.AbstractTexture; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.client.resources.metadata.texture.TextureMetadataSection; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.function.Function; + +/** + * Texture object type responsible for AzureLib's emissive render textures + */ +public class AutoGlowingTexture extends GeoAbstractTexture { + private static final RenderStateShard.ShaderStateShard SHADER_STATE = new RenderStateShard.ShaderStateShard(GameRenderer::getRendertypeEntityTranslucentEmissiveShader); + private static final RenderStateShard.TransparencyStateShard TRANSPARENCY_STATE = new RenderStateShard.TransparencyStateShard("translucent_transparency", () -> { + RenderSystem.enableBlend(); + RenderSystem.blendFuncSeparate(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA, GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA); + }, () -> { + RenderSystem.disableBlend(); + RenderSystem.defaultBlendFunc(); + }); + private static final RenderStateShard.WriteMaskStateShard WRITE_MASK = new RenderStateShard.WriteMaskStateShard(true, true); + private static final Function RENDER_TYPE_FUNCTION = Util.memoize(texture -> { + RenderStateShard.TextureStateShard textureState = new RenderStateShard.TextureStateShard(texture, false, false); + + return RenderType.create("geo_glowing_layer", DefaultVertexFormat.NEW_ENTITY, VertexFormat.Mode.QUADS, 256, false, true, RenderType.CompositeState.builder().setShaderState(SHADER_STATE).setTextureState(textureState).setTransparencyState(TRANSPARENCY_STATE).setWriteMaskState(WRITE_MASK).createCompositeState(false)); + }); + + private static final String APPENDIX = "_glowmask"; + + protected final ResourceLocation textureBase; + protected final ResourceLocation glowLayer; + + public AutoGlowingTexture(ResourceLocation originalLocation, ResourceLocation location) { + this.textureBase = originalLocation; + this.glowLayer = location; + } + + /** + * Get the emissive resource equivalent of the input resource path.
+ * Additionally prepares the texture manager for the missing texture if the resource is not present + * + * @return The glowlayer resourcepath for the provided input path + */ + protected static ResourceLocation getEmissiveResource(ResourceLocation baseResource) { + ResourceLocation path = appendToPath(baseResource, APPENDIX); + + generateTexture(path, textureManager -> textureManager.register(path, new AutoGlowingTexture(baseResource, path))); + + return path; + } + + /** + * Generates the glow layer {@link NativeImage} and appropriately modifies the base texture for use in glow render layers + */ + @Nullable + @Override + protected RenderCall loadTexture(ResourceManager resourceManager, Minecraft mc) throws IOException { + AbstractTexture originalTexture; + + try { + originalTexture = mc.submit(() -> mc.getTextureManager().getTexture(this.textureBase)).get(); + } catch (InterruptedException | ExecutionException e) { + throw new IOException("Failed to load original texture: " + this.textureBase, e); + } + + Resource textureBaseResource = resourceManager.getResource(this.textureBase).get(); + NativeImage baseImage = originalTexture instanceof DynamicTexture dynamicTexture ? dynamicTexture.getPixels() : NativeImage.read(textureBaseResource.open()); + NativeImage glowImage = null; + Optional textureBaseMeta = textureBaseResource.metadata().getSection(TextureMetadataSection.SERIALIZER); + boolean blur = textureBaseMeta.isPresent() && textureBaseMeta.get().isBlur(); + boolean clamp = textureBaseMeta.isPresent() && textureBaseMeta.get().isClamp(); + + try { + Optional glowLayerResource = resourceManager.getResource(this.glowLayer); + GeoGlowingTextureMeta glowLayerMeta = null; + + if (glowLayerResource.isPresent()) { + glowImage = NativeImage.read(glowLayerResource.get().open()); + glowLayerMeta = GeoGlowingTextureMeta.fromExistingImage(glowImage); + } else { + Optional meta = textureBaseResource.metadata().getSection(GeoGlowingTextureMeta.DESERIALIZER); + + if (meta.isPresent()) { + glowLayerMeta = meta.get(); + glowImage = new NativeImage(baseImage.getWidth(), baseImage.getHeight(), true); + } + } + + if (glowLayerMeta != null) { + glowLayerMeta.createImageMask(baseImage, glowImage); + + if (Services.PLATFORM.isDevelopmentEnvironment()) { + printDebugImageToDisk(this.textureBase, baseImage); + printDebugImageToDisk(this.glowLayer, glowImage); + } + } + } catch (IOException e) { + AzureLib.LOGGER.warn("Resource failed to open for glowlayer meta: {}", this.glowLayer, e); + } + + NativeImage mask = glowImage; + + if (mask == null) + return null; + + return () -> { + uploadSimple(getId(), mask, blur, clamp); + + if (originalTexture instanceof DynamicTexture dynamicTexture) { + dynamicTexture.upload(); + } else { + uploadSimple(originalTexture.getId(), baseImage, blur, clamp); + } + }; + } + + /** + * Return a cached instance of the RenderType for the given texture for GeoGlowingLayer rendering. + * + * @param texture The texture of the resource to apply a glow layer to + */ + public static RenderType getRenderType(ResourceLocation texture) { + return RENDER_TYPE_FUNCTION.apply(getEmissiveResource(texture)); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/texture/GeoAbstractTexture.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/texture/GeoAbstractTexture.java new file mode 100644 index 0000000..3df508a --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/texture/GeoAbstractTexture.java @@ -0,0 +1,104 @@ +package mod.azure.azurelib.common.internal.common.cache.texture; + +import java.io.File; +import java.io.IOException; +import java.util.function.Consumer; + +import mod.azure.azurelib.common.platform.Services; +import org.jetbrains.annotations.Nullable; + +import com.mojang.blaze3d.pipeline.RenderCall; +import com.mojang.blaze3d.platform.NativeImage; +import com.mojang.blaze3d.platform.TextureUtil; +import com.mojang.blaze3d.systems.RenderSystem; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.AbstractTexture; +import net.minecraft.client.renderer.texture.MissingTextureAtlasSprite; +import net.minecraft.client.renderer.texture.TextureManager; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.ResourceManager; + +/** + * Abstract texture wrapper for AzureLib textures.
+ * Mostly just handles boilerplate + */ +public abstract class GeoAbstractTexture extends AbstractTexture { + /** + * Generates the texture instance for the given path with the given appendix if it hasn't already been generated + */ + protected static void generateTexture(ResourceLocation texturePath, Consumer textureManagerConsumer) { + if (!RenderSystem.isOnRenderThreadOrInit()) + throw new IllegalThreadStateException("Texture loading called outside of the render thread! This should DEFINITELY not be happening."); + + TextureManager textureManager = Minecraft.getInstance().getTextureManager(); + + if (!(textureManager.getTexture(texturePath, MissingTextureAtlasSprite.getTexture()) instanceof GeoAbstractTexture)) + textureManagerConsumer.accept(textureManager); + } + + @Override + public final void load(ResourceManager resourceManager) throws IOException { + RenderCall renderCall = loadTexture(resourceManager, Minecraft.getInstance()); + + if (renderCall == null) + return; + + if (!RenderSystem.isOnRenderThreadOrInit()) { + RenderSystem.recordRenderCall(renderCall); + } + else { + renderCall.execute(); + } + } + + /** + * Debugging function to write out the generated glowmap image to disk + */ + protected void printDebugImageToDisk(ResourceLocation id, NativeImage newImage) { + try { + File file = new File(Services.PLATFORM.getGameDir().toFile(), "GeoTexture Debug Printouts"); + + if (!file.exists()) { + file.mkdirs(); + } + else if (!file.isDirectory()) { + file.delete(); + file.mkdirs(); + } + + file = new File(file, id.getPath().replace('/', '.')); + + if (!file.exists()) + file.createNewFile(); + + newImage.writeToFile(file); + } + catch (IOException ex) { + ex.printStackTrace(); + } + } + + /** + * Called at {@link AbstractTexture#load} time to load this texture for the first time into the render cache. + * Generate and apply the necessary functions here, then return the RenderCall to submit to the render pipeline. + * @return The RenderCall to submit to the render pipeline, or null if no further action required + */ + @Nullable + protected abstract RenderCall loadTexture(ResourceManager resourceManager, Minecraft mc) throws IOException; + + /** + * No-frills helper method for uploading {@link NativeImage images} into memory for use + */ + public static void uploadSimple(int texture, NativeImage image, boolean blur, boolean clamp) { + TextureUtil.prepareImage(texture, 0, image.getWidth(), image.getHeight()); + image.upload(0, 0, 0, 0, 0, image.getWidth(), image.getHeight(), blur, clamp, false, true); + } + + public static ResourceLocation appendToPath(ResourceLocation location, String suffix) { + String path = location.getPath(); + int i = path.lastIndexOf('.'); + + return new ResourceLocation(location.getNamespace(), path.substring(0, i) + suffix + path.substring(i)); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/texture/GeoGlowingTextureMeta.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/texture/GeoGlowingTextureMeta.java new file mode 100644 index 0000000..1c9c9a4 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/cache/texture/GeoGlowingTextureMeta.java @@ -0,0 +1,121 @@ +package mod.azure.azurelib.common.internal.common.cache.texture; + +import java.util.List; + +import mod.azure.azurelib.common.api.client.renderer.layer.AutoGlowingGeoLayer; +import org.jetbrains.annotations.Nullable; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.mojang.blaze3d.platform.NativeImage; + +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.minecraft.server.packs.metadata.MetadataSectionSerializer; +import net.minecraft.util.FastColor; +import net.minecraft.util.GsonHelper; + +/** + * Metadata class that stores the data for AzureLib's {@link AutoGlowingGeoLayer emissive texture feature} for a given texture + */ +public class GeoGlowingTextureMeta { + public static final MetadataSectionSerializer DESERIALIZER = new MetadataSectionSerializer<>() { + @Override + public String getMetadataSectionName() { + return "glowsections"; + } + + @Override + public GeoGlowingTextureMeta fromJson(JsonObject json) { + List pixels = fromSections(GsonHelper.getAsJsonArray(json, "sections", null)); + + if (pixels.isEmpty()) + throw new JsonParseException("Empty glowlayer sections file. Must have at least one glow section!"); + + return new GeoGlowingTextureMeta(pixels); + } + + /** + * Generate a {@link Pixel} collection from the "sections" array of the mcmeta file + */ + private List fromSections(@Nullable JsonArray sectionsArray) { + if (sectionsArray == null) + return List.of(); + + List pixels = new ObjectArrayList<>(); + + for (JsonElement element : sectionsArray) { + if (!(element instanceof JsonObject obj)) + throw new JsonParseException("Invalid glowsections json format, expected a JsonObject, found: " + element.getClass()); + + int x1 = GsonHelper.getAsInt(obj, "x1", GsonHelper.getAsInt(obj, "x", 0)); + int y1 = GsonHelper.getAsInt(obj, "y1", GsonHelper.getAsInt(obj, "y", 0)); + int x2 = GsonHelper.getAsInt(obj, "x2", GsonHelper.getAsInt(obj, "w", 0) + x1); + int y2 = GsonHelper.getAsInt(obj, "y2", GsonHelper.getAsInt(obj, "h", 0) + y1); + int alpha = GsonHelper.getAsInt(obj, "alpha", GsonHelper.getAsInt(obj, "a", 0)); + + if (x1 + y1 + x2 + y2 == 0) + throw new IllegalArgumentException("Invalid glowsections section object, section must be at least one pixel in size"); + + for (int x = x1; x <= x2; x++) { + for (int y = y1; y <= y2; y++) { + pixels.add(new Pixel(x, y, alpha)); + } + } + } + + return pixels; + } + }; + + private final List pixels; + + public GeoGlowingTextureMeta(List pixels) { + this.pixels = pixels; + } + + /** + * Generate the GlowLayer pixels list from an existing image resource, instead of using the .png.mcmeta file + */ + public static GeoGlowingTextureMeta fromExistingImage(NativeImage glowLayer) { + List pixels = new ObjectArrayList<>(); + + for (int x = 0; x < glowLayer.getWidth(); x++) { + for (int y = 0; y < glowLayer.getHeight(); y++) { + int color = glowLayer.getPixelRGBA(x, y); + + if (color != 0) + pixels.add(new Pixel(x, y, FastColor.ABGR32.alpha(color))); + } + } + + if (pixels.isEmpty()) + throw new IllegalStateException("Invalid glow layer texture provided, must have at least one pixel!"); + + return new GeoGlowingTextureMeta(pixels); + } + + /** + * Create a new mask image based on the pre-determined pixel data + */ + public void createImageMask(NativeImage originalImage, NativeImage newImage) { + for (Pixel pixel : this.pixels) { + int color = originalImage.getPixelRGBA(pixel.x, pixel.y); + + if (pixel.alpha > 0) + color = FastColor.ABGR32.color(pixel.alpha, FastColor.ABGR32.blue(color), FastColor.ABGR32.green(color), FastColor.ABGR32.red(color)); + + newImage.setPixelRGBA(pixel.x, pixel.y, color); + originalImage.setPixelRGBA(pixel.x, pixel.y, 0); + } + } + + /** + * A pixel marker for a glowlayer mask + * @param x The X coordinate of the pixel + * @param y The Y coordinate of the pixel + * @param alpha The alpha value of the mask + */ + private record Pixel(int x, int y, int alpha) {} +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/AzureLibConfig.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/AzureLibConfig.java new file mode 100644 index 0000000..d57018c --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/AzureLibConfig.java @@ -0,0 +1,14 @@ +package mod.azure.azurelib.common.internal.common.config; + +import mod.azure.azurelib.common.api.common.config.Config; +import mod.azure.azurelib.common.internal.common.AzureLib; + +@Config(id = AzureLib.MOD_ID) +public class AzureLibConfig { + @Configurable + @Configurable.Synchronized + public boolean disableOptifineWarning = false; + @Configurable + @Configurable.Synchronized + public boolean useVanillaUseKey = true; +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/ConfigHolder.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/ConfigHolder.java new file mode 100644 index 0000000..9bb575b --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/ConfigHolder.java @@ -0,0 +1,328 @@ +package mod.azure.azurelib.common.internal.common.config; + +import mod.azure.azurelib.common.internal.common.AzureLib; +import mod.azure.azurelib.common.internal.common.AzureLibException; +import mod.azure.azurelib.common.internal.client.config.IValidationHandler; +import mod.azure.azurelib.common.internal.common.AzureLibMod; +import mod.azure.azurelib.common.internal.common.config.adapter.TypeAdapter; +import mod.azure.azurelib.common.internal.common.config.adapter.TypeAdapters; +import mod.azure.azurelib.common.internal.common.config.format.IConfigFormatHandler; +import mod.azure.azurelib.common.internal.common.config.io.ConfigIO; +import mod.azure.azurelib.common.internal.common.config.value.ConfigValue; +import mod.azure.azurelib.common.internal.common.config.value.ObjectValue; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Manages config values and stores some default parameters of your config class. + * This class also acts as config registry. + * + * @param Your config type + * @author Toma + */ +public final class ConfigHolder { + + // Map of all registered configs + private static final Map> REGISTERED_CONFIGS = new HashMap<>(); + // Unique config ID + private final String configId; + // Config filename without extension + private final String filename; + // Config group, same as config ID unless changed + private final String group; + // Registered config instance + private final CFG configInstance; + // Type of config + private final Class configClass; + // File format used by this config + private final IConfigFormatHandler format; + // Mapping of all config values + private final Map> valueMap = new LinkedHashMap<>(); + // Map of fields which will be synced to client upon login + private final Map> networkSerializedFields = new HashMap<>(); + // Set of file refresh listeners + private final Set> fileRefreshListeners = new HashSet<>(); + // Lock for async operations + private final Object lock = new Object(); + + public ConfigHolder(Class cfgClass, String configId, String filename, String group, IConfigFormatHandler format) { + this.configClass = cfgClass; + this.configId = configId; + this.filename = filename; + this.group = group; + try { + this.configInstance = cfgClass.getDeclaredConstructor().newInstance(); + } catch (NoSuchMethodException | InstantiationException | InvocationTargetException | + IllegalAccessException e) { + AzureLib.LOGGER.fatal(AzureLib.MAIN_MARKER, "Failed to instantiate config class for {} config", configId); + throw new AzureLibException("Config create failed", e); + } + try { + serializeType(configClass, configInstance, true); + } catch (IllegalAccessException e) { + throw new AzureLibException("Config serialize failed", e); + } + this.format = format; + this.loadNetworkFields(valueMap, networkSerializedFields); + } + + /** + * Registers config to internal registry. You should never call + * this method. Instead, use {@link AzureLibMod#registerConfig(Class, IConfigFormatHandler)} for config registration + * + * @param holder Config holder to be registered + */ + public static void registerConfig(ConfigHolder holder) { + REGISTERED_CONFIGS.put(holder.configId, holder); + ConfigIO.processConfig(holder); + } + + /** + * Allows you to get your config holder based on ID + * + * @param id Config ID + * @param Config type + * @return Optional with config holder when such object exists + */ + public static Optional> getConfig(String id) { + ConfigHolder value = (ConfigHolder) REGISTERED_CONFIGS.get(id); + return value == null ? Optional.empty() : Optional.of(value); + } + + /** + * Groups all configs from registry into Group-List + * + * @return Mapped values + */ + public static Map>> getConfigGroupingByGroup() { + return REGISTERED_CONFIGS.values().stream().collect(Collectors.groupingBy(ConfigHolder::getGroup)); + } + + /** + * Returns list of config holders for the specified group + * + * @param group Group ID + * @return List with config holders. May be empty. + */ + public static List> getConfigsByGroup(String group) { + return REGISTERED_CONFIGS.values().stream() + .filter(configHolder -> configHolder.group.equals(group)) + .collect(Collectors.toList()); + } + + /** + * Obtain all configs which have some network serialized values + * + * @return Set of config holders which need to be synchronized to client + */ + public static Set getSynchronizedConfigs() { + return REGISTERED_CONFIGS.entrySet() + .stream() + .filter(e -> e.getValue().networkSerializedFields.size() > 0) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + } + + /** + * Register new file refresh listener for this config holder + * + * @param listener The file listener + */ + public void addFileRefreshListener(IFileRefreshListener listener) { + this.fileRefreshListeners.add(Objects.requireNonNull(listener)); + } + + /** + * @return ID of this config + */ + public String getConfigId() { + return configId; + } + + /** + * @return Filename without extension for this config + */ + public String getFilename() { + return filename; + } + + /** + * @return Group ID of this config + */ + public String getGroup() { + return group; + } + + /** + * @return Your registered config + */ + public CFG getConfigInstance() { + return configInstance; + } + + /** + * @return Type of config + */ + public Class getConfigClass() { + return configClass; + } + + /** + * @return File format factory for this config + */ + public IConfigFormatHandler getFormat() { + return format; + } + + /** + * @return Collection of mapped config values + */ + public Collection> values() { + return this.valueMap.values(); + } + + /** + * @return Map ID-ConfigValue for this config + */ + public Map> getValueMap() { + return valueMap; + } + + /** + * @return Map ID-ConfigValue for network serialization + */ + public Map> getNetworkSerializedFields() { + return networkSerializedFields; + } + + /** + * Dispatches file refresh event to all registered listeners + */ + public void dispatchFileRefreshEvent() { + this.fileRefreshListeners.forEach(listener -> listener.onFileRefresh(this)); + } + + /** + * @return Lock for async operations. Used for IO operations currently + */ + public Object getLock() { + return lock; + } + + private Map> serializeType(Class type, Object instance, boolean saveValue) throws IllegalAccessException { + Map> map = new LinkedHashMap<>(); + Field[] fields = type.getFields(); + for (Field field : fields) { + Configurable value = field.getAnnotation(Configurable.class); + if (value == null) + continue; + int modifiers = field.getModifiers(); + if (Modifier.isStatic(modifiers) || Modifier.isFinal(modifiers)) { + AzureLib.LOGGER.warn(ConfigIO.MARKER, "Skipping config field {}, only instance non-final types are supported", field); + continue; + } + TypeAdapter adapter = TypeAdapters.forType(field.getType()); + if (adapter == null) { + AzureLib.LOGGER.warn(ConfigIO.MARKER, "Missing adapter for type {}, skipping serialization", field.getType()); + continue; + } + String[] comments = new String[0]; + Configurable.Comment comment = field.getAnnotation(Configurable.Comment.class); + if (comment != null) { + comments = comment.value(); + } + field.setAccessible(true); + ConfigValue cfgValue = adapter.serialize(field.getName(), comments, field.get(instance), (type1, instance1) -> serializeType(type1, instance1, false), new TypeAdapter.AdapterContext() { + @Override + public TypeAdapter getAdapter() { + return adapter; + } + + @Override + public Field getOwner() { + return field; + } + + @Override + public void setFieldValue(Object value) { + field.setAccessible(true); + try { + adapter.setFieldValue(field, instance, value); + } catch (IllegalAccessException e) { + AzureLib.LOGGER.error(ConfigIO.MARKER, "Failed to update config value for field {} from {} to a new value {} due to error {}", field.getName(), type, value, e); + } + } + }); + Configurable.ValueUpdateCallback callback = field.getAnnotation(Configurable.ValueUpdateCallback.class); + if (callback != null) { + this.processCallback(callback, type, instance, cfgValue); + } + cfgValue.processFieldData(field); + map.put(field.getName(), cfgValue); + if (saveValue) { + this.assignValue(cfgValue); + } + } + return map; + } + + private void processCallback(Configurable.ValueUpdateCallback callback, Class type, Object instance, ConfigValue value) { + String methodName = callback.method(); + try { + Class valueType = value.getValueType(); + if (callback.allowPrimitivesMapping()) { + valueType = ConfigUtils.remapPrimitiveType(valueType); + } + Method method = type.getDeclaredMethod(methodName, valueType, IValidationHandler.class); + ConfigValue.SetValueCallback setValueCallback = (val, handler) -> { + try { + method.setAccessible(true); + method.invoke(instance, val, handler); + } catch (IllegalAccessException | InvocationTargetException e) { + AzureLib.LOGGER.error(ConfigIO.MARKER, "Error occurred while invoking {} method: {}", method, e); + } + }; + value.setValueValidator(setValueCallback); + AzureLib.LOGGER.debug(ConfigIO.MARKER, "Attached new value listener method '{}' for config value {}", methodName, value.getId()); + } catch (NoSuchMethodException e) { + AzureLib.LOGGER.error(ConfigIO.MARKER, "Unable to map method {} for config value {} due to {}", methodName, value.getId(), e); + } catch (Exception e) { + AzureLib.LOGGER.fatal(ConfigIO.MARKER, "Fatal error occurred while trying to map value listener for {} method", methodName); + throw new AzureLibException("Value listener map failed", e); + } + } + + private void assignValue(ConfigValue value) { + this.valueMap.put(value.getId(), value); + } + + private void loadNetworkFields(Map> src, Map> dest) { + src.values().forEach(value -> { + if (value instanceof ObjectValue objValue) { + Map> data = objValue.get(); + loadNetworkFields(data, dest); + } else { + if (!value.shouldSynchronize()) + return; + String path = value.getFieldPath(); + dest.put(path, value); + } + }); + } + + /** + * Listener which is triggered when config file changes on disk + * + * @param Config type + * @author Toma + */ + @FunctionalInterface + public interface IFileRefreshListener { + void onFileRefresh(ConfigHolder holder); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/ConfigUtils.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/ConfigUtils.java new file mode 100644 index 0000000..3f3eb16 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/ConfigUtils.java @@ -0,0 +1,141 @@ +package mod.azure.azurelib.common.internal.common.config; + +import java.lang.reflect.Field; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Pattern; + +import mod.azure.azurelib.common.internal.common.AzureLib; +import org.jetbrains.annotations.Nullable; + +import mod.azure.azurelib.common.internal.common.config.exception.ConfigValueMissingException; +import mod.azure.azurelib.common.internal.common.config.io.ConfigIO; +import net.minecraft.client.gui.components.EditBox; + +public final class ConfigUtils { + + public static final char[] INTEGER_CHARS = { '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }; + public static final char[] DECIMAL_CHARS = { '-', '.', 'E', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }; + public static final Pattern INTEGER_PATTERN = Pattern.compile("-?[0-9]+"); + public static final Pattern DECIMAL_PATTERN = Pattern.compile("-?[0-9]+(\\.[0-9]+)?(E[0-9]+)?"); + public static final Map, Class> PRIMITIVE_MAPPINGS = new HashMap<>(); + + public static void logCorrectedMessage(String field, @Nullable Object prevValue, Object corrected) { + AzureLib.LOGGER.warn(ConfigIO.MARKER, "Correcting config value '{}' from '{}' to '{}'", field, Objects.toString(prevValue), corrected); + } + + public static void logArraySizeCorrectedMessage(String field, Object prevValue, Object corrected) { + AzureLib.LOGGER.warn(ConfigIO.MARKER, "Correcting config array value '{}' due to invalid size from '{}' to '{}'", field, prevValue, corrected); + } + + public static boolean[] unboxArray(Boolean[] values) { + boolean[] primitive = new boolean[values.length]; + int i = 0; + for (boolean v : values) { + primitive[i++] = v; + } + return primitive; + } + + public static int[] unboxArray(Integer[] values) { + int[] primitive = new int[values.length]; + int i = 0; + for (int v : values) { + primitive[i++] = v; + } + return primitive; + } + + public static long[] unboxArray(Long[] values) { + long[] primitive = new long[values.length]; + int i = 0; + for (long v : values) { + primitive[i++] = v; + } + return primitive; + } + + public static float[] unboxArray(Float[] values) { + float[] primitive = new float[values.length]; + int i = 0; + for (float v : values) { + primitive[i++] = v; + } + return primitive; + } + + public static double[] unboxArray(Double[] values) { + double[] primitive = new double[values.length]; + int i = 0; + for (double v : values) { + primitive[i++] = v; + } + return primitive; + } + + public static > E getEnumConstant(String value, Class declaringClass) throws ConfigValueMissingException { + E[] constants = declaringClass.getEnumConstants(); + for (E e : constants) { + if (e.name().equals(value)) { + return e; + } + } + throw new ConfigValueMissingException("Missing enum value: " + value); + } + + public static boolean containsOnlyValidCharacters(String in, char[] allowedChars) { + char[] arr = in.toCharArray(); + for (char c : arr) { + boolean valid = false; + for (char validate : allowedChars) { + if (validate == c) { + valid = true; + break; + } + } + if (!valid) { + return false; + } + } + return true; + } + + public static DecimalFormat getDecimalFormat(Field field) { + Configurable.Gui.NumberFormat format = field.getAnnotation(Configurable.Gui.NumberFormat.class); + if (format != null) { + DecimalFormatSymbols symbols = new DecimalFormatSymbols(); + symbols.setDecimalSeparator('.'); + return new DecimalFormat(format.value(), symbols); + } + return null; + } + + public static Class remapPrimitiveType(Class type) { + return PRIMITIVE_MAPPINGS.getOrDefault(type, type); + } + + public static void adjustCharacterLimit(Field field, EditBox widget) { + Configurable.Gui.CharacterLimit limit = field.getAnnotation(Configurable.Gui.CharacterLimit.class); + if (limit != null) { + widget.setMaxLength(Math.max(limit.value(), 1)); + } + } + + static { + PRIMITIVE_MAPPINGS.put(Boolean.class, Boolean.TYPE); + PRIMITIVE_MAPPINGS.put(Character.class, Character.TYPE); + PRIMITIVE_MAPPINGS.put(Byte.class, Byte.TYPE); + PRIMITIVE_MAPPINGS.put(Short.class, Short.TYPE); + PRIMITIVE_MAPPINGS.put(Integer.class, Integer.TYPE); + PRIMITIVE_MAPPINGS.put(Long.class, Long.TYPE); + PRIMITIVE_MAPPINGS.put(Float.class, Float.TYPE); + PRIMITIVE_MAPPINGS.put(Double.class, Double.TYPE); + } + + private ConfigUtils() { + throw new UnsupportedOperationException(); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/Configurable.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/Configurable.java new file mode 100644 index 0000000..99f2b07 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/Configurable.java @@ -0,0 +1,207 @@ +package mod.azure.azurelib.common.internal.common.config; + +import mod.azure.azurelib.common.internal.client.config.IValidationHandler; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marker annotation for field to config serialization. + * Only public instance fields are allowed. + * + * @author Toma + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Configurable { + + /** + * Allows you to add description to configurable value. + * This description will be visible on hover in GUI or as + * comment if config file (if supported by file format) + */ + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + @interface Comment { + + /** + * @return Array of comments for this configurable value + */ + String[] value(); + } + + /** + * Field values annotated by this will be automatically + * synchronized to client when joining server. + * Does not rewrite client config file, all values + * are recovered when leaving server + */ + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + @interface Synchronized { + } + + /** + * Allows you to specify number range for int or long values. + * This annotation is also applicable to int/long arrays + */ + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + @interface Range { + + /** + * @return Minimum allowed value for this config field + * @throws IllegalArgumentException when minimum value is larger than maximum + */ + long min() default Long.MIN_VALUE; + + /** + * @return Maximum allowed value for this config field + * @throws IllegalArgumentException when maximum value is smaller than minimum + */ + long max() default Long.MAX_VALUE; + } + + /** + * Allows you to specify decimal number range for float or double values. + * This annotation is also applicable to float/double arrays + */ + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + @interface DecimalRange { + + /** + * @return Minimum allowed value for this config field + * @throws IllegalArgumentException when minimum value is larger than maximum + */ + double min() default -Double.MAX_VALUE; + + /** + * @return Maximum allowed value for this config field + * @throws IllegalArgumentException when maximum value is smaller than minimum + */ + double max() default Double.MAX_VALUE; + } + + /** + * Allows you to require strings to be in specific format. + * Useful when you for example want to use this for resource locations etc. + * This annotation is also applicable to string arrays + */ + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + @interface StringPattern { + + /** + * @return Regular expression used for value checking + * @throws IllegalArgumentException When value is not valid regex syntax + */ + String value(); + + /** + * This value is used only for string arrays in case entered value does not + * match the regular expression. + * @return Default value to be used when user enters invalid value + */ + String defaultValue() default ""; + + /** + * @return Flags used for {@link java.util.regex.Pattern} object. + * You can use for example value like {@code flags = Pattern.CASE_INSENTITIVE | Pattern.LITERAL} + * for flag specification + */ + int flags() default 0; + + /** + * Gui error message when user enters invalid value + * @return Error message to be displayed on GUI + */ + String errorDescriptor() default ""; + } + + /** + * Allows you to lock array size based on default provided value. + * Applicable to all arrays. + */ + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + @interface FixedSize { + } + + /** + * Allows you to map custom listener method to listen for value change. + * Could be useful for example when validating item ID or something like that. + */ + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + @interface ValueUpdateCallback { + + /** + * You must have defined custom method in the same class as where this configurable value is. + * The method also requires specific signature with {@code void} return type, value type and {@link IValidationHandler} parameter. + * For example value listener method for int config field would look like this + * {@code public void onValueChange(int value, IValidationHandler validationHandler) {}} + * + * @return Name of your method + */ + String method(); + + /** + * Handles remapping of boxed java types to their primitive values + * @return Whether remapping is allowed, unless specific implementation is provided, this should always + * be set to true + */ + boolean allowPrimitivesMapping() default true; + } + + /** + * Group of GUI cosmetic properties + */ + final class Gui { + + /** + * Allows you to specify number formatting for float and double values + * in GUI. + */ + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + public @interface NumberFormat { + + /** + * @return Number format according to {@link java.text.DecimalFormat}. + * @throws IllegalArgumentException When invalid format is provided + */ + String value(); + } + + /** + * Adds color display next to your string value + */ + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + public @interface ColorValue { + + /** + * @return If your value supports alpha values, otherwise will always be rendered as solid color + */ + boolean isARGB() default false; + + String getGuiColorPrefix() default "#"; + } + + /** + * Allows you to change character limit for text fields + */ + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + public @interface CharacterLimit { + + /** + * @return Character limit to be used by text field + */ + int value() default 32; + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/adapter/TypeAdapter.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/adapter/TypeAdapter.java new file mode 100644 index 0000000..f80fc01 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/adapter/TypeAdapter.java @@ -0,0 +1,34 @@ +package mod.azure.azurelib.common.internal.common.config.adapter; + +import mod.azure.azurelib.common.internal.common.config.value.ConfigValue; +import net.minecraft.network.FriendlyByteBuf; + +import java.lang.reflect.Field; +import java.util.Map; + +public abstract class TypeAdapter { + + public abstract ConfigValue serialize(String name, String[] comments, Object value, TypeSerializer serializer, AdapterContext context) throws IllegalAccessException; + + public abstract void encodeToBuffer(ConfigValue value, FriendlyByteBuf buffer); + + public abstract Object decodeFromBuffer(ConfigValue value, FriendlyByteBuf buffer); + + public void setFieldValue(Field field, Object instance, Object value) throws IllegalAccessException { + field.set(instance, value); + } + + @FunctionalInterface + public interface TypeSerializer { + Map> serialize(Class type, Object instance) throws IllegalAccessException; + } + + public interface AdapterContext { + + TypeAdapter getAdapter(); + + Field getOwner(); + + void setFieldValue(Object value); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/adapter/TypeAdapters.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/adapter/TypeAdapters.java new file mode 100644 index 0000000..0093dce --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/adapter/TypeAdapters.java @@ -0,0 +1,57 @@ +package mod.azure.azurelib.common.internal.common.config.adapter; + +import mod.azure.azurelib.common.internal.common.config.value.*; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; + +public final class TypeAdapters { + + private static final Map ADAPTER_MAP = new HashMap<>(); + + public static TypeAdapter forType(Class type) { + return ADAPTER_MAP.entrySet().stream() + .filter(entry -> entry.getKey().test(type)) + .sorted(Comparator.comparingInt(value -> value.getKey().priority())) + .map(Map.Entry::getValue) + .findFirst() + .orElse(null); + } + + public static void registerTypeAdapter(TypeMatcher matcher, TypeAdapter adapter) { + if (ADAPTER_MAP.put(matcher, adapter) != null) { + throw new IllegalArgumentException("Duplicate type matcher with id: " + matcher.getIdentifier()); + } + } + + static { + // primitives + registerTypeAdapter(TypeMatcher.matchBoolean(), new BooleanValue.Adapter()); + registerTypeAdapter(TypeMatcher.matchCharacter(), new CharValue.Adapter()); + registerTypeAdapter(TypeMatcher.matchInteger(), new IntValue.Adapter()); + registerTypeAdapter(TypeMatcher.matchLong(), new LongValue.Adapter()); + registerTypeAdapter(TypeMatcher.matchFloat(), new FloatValue.Adapter()); + registerTypeAdapter(TypeMatcher.matchDouble(), new DoubleValue.Adapter()); + registerTypeAdapter(TypeMatcher.matchString(), new StringValue.Adapter()); + + // primitive arrays + registerTypeAdapter(TypeMatcher.matchBooleanArray(), new BooleanArrayValue.Adapter()); + registerTypeAdapter(TypeMatcher.matchIntegerArray(), new IntArrayValue.Adapter()); + registerTypeAdapter(TypeMatcher.matchLongArray(), new LongArrayValue.Adapter()); + registerTypeAdapter(TypeMatcher.matchFloatArray(), new FloatArrayValue.Adapter()); + registerTypeAdapter(TypeMatcher.matchDoubleArray(), new DoubleArrayValue.Adapter()); + registerTypeAdapter(TypeMatcher.matchStringArray(), new StringArrayValue.Adapter()); + + // enums + registerTypeAdapter(TypeMatcher.matchEnum(), new EnumValue.Adapter<>()); + registerTypeAdapter(TypeMatcher.matchEnumArray(), new EnumArrayValue.Adapter<>()); + + // objects + registerTypeAdapter(TypeMatcher.matchObject(), new ObjectValue.Adapter()); + } + + private TypeAdapters() { + throw new UnsupportedOperationException(); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/adapter/TypeMatcher.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/adapter/TypeMatcher.java new file mode 100644 index 0000000..46aeaf7 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/adapter/TypeMatcher.java @@ -0,0 +1,137 @@ +package mod.azure.azurelib.common.internal.common.config.adapter; + +import java.util.Objects; +import java.util.function.Predicate; + +import mod.azure.azurelib.common.internal.common.AzureLib; +import net.minecraft.resources.ResourceLocation; + +public interface TypeMatcher extends Predicate> { + + ResourceLocation getIdentifier(); + + int priority(); + + static TypeMatcher matchBoolean() { + return NamedMatcherImpl.vanilla("boolean", Boolean.TYPE); + } + + static TypeMatcher matchCharacter() { + return NamedMatcherImpl.vanilla("character", Character.TYPE); + } + + static TypeMatcher matchInteger() { + return NamedMatcherImpl.vanilla("integer", Integer.TYPE); + } + + static TypeMatcher matchLong() { + return NamedMatcherImpl.vanilla("long", Long.TYPE); + } + + static TypeMatcher matchFloat() { + return NamedMatcherImpl.vanilla("float", Float.TYPE); + } + + static TypeMatcher matchDouble() { + return NamedMatcherImpl.vanilla("double", Double.TYPE); + } + + static TypeMatcher matchString() { + return NamedMatcherImpl.vanilla("string", String.class); + } + + static TypeMatcher matchBooleanArray() { + return NamedMatcherImpl.vanilla("array/boolean", boolean[].class); + } + + static TypeMatcher matchIntegerArray() { + return NamedMatcherImpl.vanilla("array/integer", int[].class); + } + + static TypeMatcher matchLongArray() { + return NamedMatcherImpl.vanilla("array/long", long[].class); + } + + static TypeMatcher matchFloatArray() { + return NamedMatcherImpl.vanilla("array/float", float[].class); + } + + static TypeMatcher matchDoubleArray() { + return NamedMatcherImpl.vanilla("array/double", double[].class); + } + + static TypeMatcher matchStringArray() { + return NamedMatcherImpl.vanilla("array/string", String[].class); + } + + static TypeMatcher matchEnum() { + return NamedMatcherImpl.vanilla("enum", Class::isEnum); + } + + static TypeMatcher matchEnumArray() { + return NamedMatcherImpl.vanilla("array/enum", type -> type.isArray() && type.getComponentType().isEnum()); + } + + static TypeMatcher matchObject() { + return NamedMatcherImpl.vanilla("object", type -> !type.isArray()) + .withPriority(Integer.MAX_VALUE); + } + + class NamedMatcherImpl implements TypeMatcher { + + private final ResourceLocation identifier; + private final Predicate> matcher; + private int priority; + + public NamedMatcherImpl(ResourceLocation identifier, Predicate> matcher) { + this.identifier = Objects.requireNonNull(identifier); + this.matcher = Objects.requireNonNull(matcher); + } + + public static NamedMatcherImpl vanilla(String path, Predicate> matcher) { + return new NamedMatcherImpl(AzureLib.modResource(path), matcher); + } + + public static NamedMatcherImpl vanilla(String path, Class requiredType) { + return new NamedMatcherImpl(AzureLib.modResource(path), type -> type.equals(requiredType)); + } + + public NamedMatcherImpl withPriority(int priority) { + this.priority = priority; + return this; + } + + @Override + public boolean test(Class aClass) { + return matcher.test(aClass); + } + + @Override + public ResourceLocation getIdentifier() { + return identifier; + } + + @Override + public int priority() { + return priority; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NamedMatcherImpl that = (NamedMatcherImpl) o; + return identifier.equals(that.identifier); + } + + @Override + public int hashCode() { + return Objects.hash(identifier); + } + + @Override + public String toString() { + return "NamedMatcherImpl{identifier=" + identifier + "}"; + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/exception/ConfigReadException.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/exception/ConfigReadException.java new file mode 100644 index 0000000..59b04bb --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/exception/ConfigReadException.java @@ -0,0 +1,25 @@ +package mod.azure.azurelib.common.internal.common.config.exception; + +public class ConfigReadException extends Exception { + + /** + * + */ + private static final long serialVersionUID = -3140380119490334328L; + + public ConfigReadException() { + super(); + } + + public ConfigReadException(String message) { + super(message); + } + + public ConfigReadException(String message, Throwable cause) { + super(message, cause); + } + + public ConfigReadException(Throwable cause) { + super(cause); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/exception/ConfigValueMissingException.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/exception/ConfigValueMissingException.java new file mode 100644 index 0000000..2bcf7c1 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/exception/ConfigValueMissingException.java @@ -0,0 +1,24 @@ +package mod.azure.azurelib.common.internal.common.config.exception; + +public class ConfigValueMissingException extends Exception { + + /** + * + */ + private static final long serialVersionUID = -6063813873167943417L; + + public ConfigValueMissingException() { + } + + public ConfigValueMissingException(String message) { + super(message); + } + + public ConfigValueMissingException(String message, Throwable cause) { + super(message, cause); + } + + public ConfigValueMissingException(Throwable cause) { + super(cause); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/format/ConfigFormats.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/format/ConfigFormats.java new file mode 100644 index 0000000..24baa94 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/format/ConfigFormats.java @@ -0,0 +1,87 @@ +package mod.azure.azurelib.common.internal.common.config.format; + +import java.util.function.Supplier; + +/** + * Collection and factory methods for config formats natively supported by + * this library. Note that there are provided methods which allow you to + * customize the config format, for example you can customize the GSON object + * in for JSON configs or spacing/separators for Properties configs. + * + * @author Toma + */ +public final class ConfigFormats { + + // file extensions + private static final String EXT_JSON = "json"; + private static final String EXT_YAML = "yaml"; + private static final String EXT_PROPERTIES = "properties"; + + /** + * Creates new JSON config format handler with customized format settings. + * + * @param settings Settings to be used by this format + * @return new instance of config format handler for JSON configs + */ + public static IConfigFormatHandler json(GsonFormat.Settings settings) { + return new SimpleFormatImpl(EXT_JSON, () -> new GsonFormat(settings)); + } + + /** + * Creates new JSON config format handler with default format settings. + * + * @return new instance of config format handler for JSON configs + */ + public static IConfigFormatHandler json() { + return json(new GsonFormat.Settings()); + } + + /** + * Creates new YAML config format handler with default format settings + * + * @return new instance of config format handler for YAML configs + */ + public static IConfigFormatHandler yaml() { + return new SimpleFormatImpl(EXT_YAML, YamlFormat::new); + } + + /** + * Creates new Properties based config format handler with customized format settings + * + * @param settings Settings to be used by this format + * @return new instance of config format handler for Properties configs + */ + public static IConfigFormatHandler properties(PropertiesFormat.Settings settings) { + return new SimpleFormatImpl(EXT_PROPERTIES, () -> new PropertiesFormat(settings)); + } + + /** + * Creates new Properties based config format handler with default format settings + + * @return new instance of config format handler for Properties configs + */ + public static IConfigFormatHandler properties() { + return properties(new PropertiesFormat.Settings()); + } + + private static final class SimpleFormatImpl implements IConfigFormatHandler { + + private final String extension; + private final Supplier factory; + + public SimpleFormatImpl(String extension, Supplier factory) { + this.extension = extension; + this.factory = factory; + } + + @Override + public IConfigFormat createFormat() { + return factory.get(); + } + + @Override + public String fileExt() { + return extension; + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/format/GsonFormat.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/format/GsonFormat.java new file mode 100644 index 0000000..54f8e57 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/format/GsonFormat.java @@ -0,0 +1,339 @@ +package mod.azure.azurelib.common.internal.common.config.format; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; + +import mod.azure.azurelib.common.internal.common.config.ConfigUtils; +import mod.azure.azurelib.common.internal.common.config.value.ConfigValue; +import mod.azure.azurelib.common.internal.common.config.value.IDescriptionProvider; +import mod.azure.azurelib.common.internal.common.AzureLib; +import mod.azure.azurelib.common.internal.common.config.exception.ConfigReadException; +import mod.azure.azurelib.common.internal.common.config.exception.ConfigValueMissingException; +import mod.azure.azurelib.common.internal.common.config.io.ConfigIO; + +public final class GsonFormat implements IConfigFormat { + + private final Gson gson; + private final JsonObject root; + + public GsonFormat(Settings settings) { + this.gson = settings.builder.create(); + this.root = new JsonObject(); + } + + private GsonFormat(JsonObject root) { + this.root = root; + this.gson = null; // no need to propagate + } + + @Override + public void writeBoolean(String field, boolean value) { + this.root.addProperty(field, value); + } + + @Override + public boolean readBoolean(String field) throws ConfigValueMissingException { + return this.tryRead(field, JsonElement::getAsBoolean); + } + + @Override + public void writeChar(String field, char value) { + this.root.addProperty(field, value); + } + + @Override + public char readChar(String field) throws ConfigValueMissingException { + return this.tryRead(field, JsonElement::getAsCharacter); + } + + @Override + public void writeInt(String field, int value) { + this.root.addProperty(field, value); + } + + @Override + public int readInt(String field) throws ConfigValueMissingException { + return this.tryRead(field, JsonElement::getAsInt); + } + + @Override + public void writeLong(String field, long value) { + this.root.addProperty(field, value); + } + + @Override + public long readLong(String field) throws ConfigValueMissingException { + return this.tryRead(field, JsonElement::getAsLong); + } + + @Override + public void writeFloat(String field, float value) { + this.root.addProperty(field, value); + } + + @Override + public float readFloat(String field) throws ConfigValueMissingException { + return this.tryRead(field, JsonElement::getAsFloat); + } + + @Override + public void writeDouble(String field, double value) { + this.root.addProperty(field, value); + } + + @Override + public double readDouble(String field) throws ConfigValueMissingException { + return this.tryRead(field, JsonElement::getAsDouble); + } + + @Override + public void writeString(String field, String value) { + this.root.addProperty(field, value); + } + + @Override + public String readString(String field) throws ConfigValueMissingException { + return this.tryRead(field, JsonElement::getAsString); + } + + @Override + public void writeBoolArray(String field, boolean[] values) { + JsonArray array = new JsonArray(); + for (boolean b : values) { + array.add(b); + } + this.root.add(field, array); + } + + // I love Java primitive types (: + @Override + public boolean[] readBoolArray(String field) throws ConfigValueMissingException { + return ConfigUtils.unboxArray(this.readArray(field, Boolean[]::new, JsonElement::getAsBoolean)); + } + + @Override + public void writeIntArray(String field, int[] values) { + JsonArray array = new JsonArray(); + for (int i : values) { + array.add(i); + } + this.root.add(field, array); + } + + @Override + public int[] readIntArray(String field) throws ConfigValueMissingException { + return ConfigUtils.unboxArray(this.readArray(field, Integer[]::new, JsonElement::getAsInt)); + } + + @Override + public void writeLongArray(String field, long[] values) { + JsonArray array = new JsonArray(); + for (long i : values) { + array.add(i); + } + this.root.add(field, array); + } + + @Override + public long[] readLongArray(String field) throws ConfigValueMissingException { + return ConfigUtils.unboxArray(this.readArray(field, Long[]::new, JsonElement::getAsLong)); + } + + @Override + public void writeFloatArray(String field, float[] values) { + JsonArray array = new JsonArray(); + for (float i : values) { + array.add(i); + } + this.root.add(field, array); + } + + @Override + public float[] readFloatArray(String field) throws ConfigValueMissingException { + return ConfigUtils.unboxArray(this.readArray(field, Float[]::new, JsonElement::getAsFloat)); + } + + @Override + public void writeDoubleArray(String field, double[] values) { + JsonArray array = new JsonArray(); + for (double i : values) { + array.add(i); + } + this.root.add(field, array); + } + + @Override + public double[] readDoubleArray(String field) throws ConfigValueMissingException { + return ConfigUtils.unboxArray(this.readArray(field, Double[]::new, JsonElement::getAsDouble)); + } + + @Override + public void writeStringArray(String field, String[] values) { + this.writeArray(field, values, JsonArray::add); + } + + @Override + public String[] readStringArray(String field) throws ConfigValueMissingException { + return this.readArray(field, String[]::new, JsonElement::getAsString); + } + + @Override + public > void writeEnum(String field, E value) { + this.root.addProperty(field, value.name()); + } + + @Override + public > E readEnum(String field, Class enumClass) throws ConfigValueMissingException { + String value = readString(field); + return ConfigUtils.getEnumConstant(value, enumClass); + } + + @Override + public > void writeEnumArray(String field, E[] value) { + String[] strings = Arrays.stream(value).map(Enum::name).toArray(String[]::new); + writeStringArray(field, strings); + } + + @SuppressWarnings("unchecked") + @Override + public > E[] readEnumArray(String field, Class enumClass) throws ConfigValueMissingException { + String[] strings = readStringArray(field); + E[] arr = (E[]) Array.newInstance(enumClass, strings.length); + for (int i = 0; i < strings.length; i++) { + arr[i] = ConfigUtils.getEnumConstant(strings[i], enumClass); + } + return arr; + } + + @Override + public void writeMap(String field, Map> value) { + GsonFormat config = new GsonFormat(new Settings()); + value.values().forEach(val -> val.serializeValue(config)); + this.root.add(field, config.root); + } + + @Override + public void readMap(String field, Collection> values) throws ConfigValueMissingException { + JsonElement element = this.root.get(field); + if (element == null || !element.isJsonObject()) + throw new ConfigValueMissingException("Missing config value: " + field); + JsonObject object = element.getAsJsonObject(); + GsonFormat config = new GsonFormat(object); + for (ConfigValue value : values) { + value.deserializeValue(config); + } + } + + @Override + public void writeFile(File file) throws IOException { + try (FileWriter writer = new FileWriter(file)) { + writer.write(gson.toJson(this.root)); + } + } + + @Override + public void readFile(File file) throws IOException, ConfigReadException { + try (FileReader reader = new FileReader(file)) { + JsonParser parser = new JsonParser(); + try { + JsonElement element = parser.parse(reader); + if (!element.isJsonObject()) { + throw new ConfigReadException("Gson config must contain JsonObject as root element!"); + } + JsonObject object = element.getAsJsonObject(); + for (Map.Entry entry : object.entrySet()) { + this.root.add(entry.getKey(), entry.getValue()); + } + } catch (JsonParseException e) { + throw new ConfigReadException("Config read failed", e); + } + } + } + + @Override + public void addComments(IDescriptionProvider provider) { + // comments are not supported for JSON4 files + } + + private void writeArray(String field, T[] array, BiConsumer elementConsumer) { + JsonArray ar = new JsonArray(); + for (T t : array) { + elementConsumer.accept(ar, t); + } + this.root.add(field, ar); + } + + private T tryRead(String field, Function function) throws ConfigValueMissingException { + JsonElement element = this.root.get(field); + if (element == null) { + throw new ConfigValueMissingException("Missing value: " + field); + } + try { + return function.apply(element); + } catch (Exception e) { + AzureLib.LOGGER.error(ConfigIO.MARKER, "Error loading value for field {} - {}", field, e); + throw new ConfigValueMissingException("Invalid value"); + } + } + + private T[] readArray(String field, Function arrayFactory, Function function) throws ConfigValueMissingException { + JsonElement element = this.root.get(field); + if (element == null || !element.isJsonArray()) { + throw new ConfigValueMissingException("Missing value: " + field); + } + JsonArray array = element.getAsJsonArray(); + T[] arr = arrayFactory.apply(array.size()); + try { + int j = 0; + for (JsonElement el : array) { + arr[j++] = function.apply(el); + } + return arr; + } catch (Exception e) { + AzureLib.LOGGER.error(ConfigIO.MARKER, "Error loading value for field {} - {}", field, e); + throw new ConfigValueMissingException("Invalid value"); + } + } + + /** + * Settings holder for JSON configs + * + * @author Toma + */ + public static final class Settings { + + private final GsonBuilder builder = new GsonBuilder(); + + /** + * Default settings constructor + */ + public Settings() { + this.builder.setPrettyPrinting().disableHtmlEscaping(); + } + + /** + * Constructs new settings and allows you to customize {@link GsonBuilder} object + * @param consumer Consumer of {@link GsonBuilder} for this settings object + */ + public Settings(Consumer consumer) { + consumer.accept(builder); + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/format/IConfigFormat.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/format/IConfigFormat.java new file mode 100644 index 0000000..b058a7e --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/format/IConfigFormat.java @@ -0,0 +1,89 @@ +package mod.azure.azurelib.common.internal.common.config.format; + +import mod.azure.azurelib.common.internal.common.config.value.ConfigValue; +import mod.azure.azurelib.common.internal.common.config.value.IDescriptionProvider; +import mod.azure.azurelib.common.internal.common.config.exception.ConfigReadException; +import mod.azure.azurelib.common.internal.common.config.exception.ConfigValueMissingException; + +import java.io.File; +import java.io.IOException; +import java.util.Collection; +import java.util.Map; + +/** + * Handles exporting of data to custom file format + * + * @author Toma + */ +public interface IConfigFormat { + + void writeBoolean(String field, boolean value); + + boolean readBoolean(String field) throws ConfigValueMissingException; + + void writeChar(String field, char value); + + char readChar(String field) throws ConfigValueMissingException; + + void writeInt(String field, int value); + + int readInt(String field) throws ConfigValueMissingException; + + void writeLong(String field, long value); + + long readLong(String field) throws ConfigValueMissingException; + + void writeFloat(String field, float value); + + float readFloat(String field) throws ConfigValueMissingException; + + void writeDouble(String field, double value); + + double readDouble(String field) throws ConfigValueMissingException; + + void writeString(String field, String value); + + String readString(String field) throws ConfigValueMissingException; + + void writeBoolArray(String field, boolean[] values); + + boolean[] readBoolArray(String field) throws ConfigValueMissingException; + + void writeIntArray(String field, int[] values); + + int[] readIntArray(String field) throws ConfigValueMissingException; + + void writeLongArray(String field, long[] values); + + long[] readLongArray(String field) throws ConfigValueMissingException; + + void writeFloatArray(String field, float[] values); + + float[] readFloatArray(String field) throws ConfigValueMissingException; + + void writeDoubleArray(String field, double[] values); + + double[] readDoubleArray(String field) throws ConfigValueMissingException; + + void writeStringArray(String field, String[] values); + + String[] readStringArray(String field) throws ConfigValueMissingException; + + > void writeEnum(String field, E value); + + > E readEnum(String field, Class enumClass) throws ConfigValueMissingException; + + > void writeEnumArray(String field, E[] value); + + > E[] readEnumArray(String field, Class enumClass) throws ConfigValueMissingException; + + void writeMap(String field, Map> value); + + void readMap(String field, Collection> values) throws ConfigValueMissingException; + + void readFile(File file) throws IOException, ConfigReadException; + + void writeFile(File file) throws IOException; + + void addComments(IDescriptionProvider provider); +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/format/IConfigFormatHandler.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/format/IConfigFormatHandler.java new file mode 100644 index 0000000..c5e31c8 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/format/IConfigFormatHandler.java @@ -0,0 +1,8 @@ +package mod.azure.azurelib.common.internal.common.config.format; + +public interface IConfigFormatHandler { + + IConfigFormat createFormat(); + + String fileExt(); +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/format/PropertiesFormat.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/format/PropertiesFormat.java new file mode 100644 index 0000000..75f5cc1 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/format/PropertiesFormat.java @@ -0,0 +1,413 @@ +package mod.azure.azurelib.common.internal.common.config.format; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import mod.azure.azurelib.common.internal.common.config.ConfigUtils; +import mod.azure.azurelib.common.internal.common.config.value.ConfigValue; +import mod.azure.azurelib.common.internal.common.config.value.IDescriptionProvider; +import org.jetbrains.annotations.Nullable; + +import mod.azure.azurelib.common.internal.common.config.exception.ConfigReadException; +import mod.azure.azurelib.common.internal.common.config.exception.ConfigValueMissingException; + +public final class PropertiesFormat implements IConfigFormat { + + private final Settings settings; + private final StringBuilder buffer; + @Nullable + private final String prefix; + private final Map parsed; + + public PropertiesFormat(Settings settings) { + this(null, new StringBuilder(), settings); + } + + private PropertiesFormat(String prefix, StringBuilder bufferRef, Settings settings) { + this(prefix, bufferRef, new HashMap<>(), settings); + } + + private PropertiesFormat(String prefix, StringBuilder bufferRef, Map parsed, Settings settings) { + this.prefix = prefix; + this.buffer = bufferRef; + this.parsed = parsed; + this.settings = settings; + } + + @Override + public void writeBoolean(String field, boolean value) { + this.writePair(field, String.valueOf(value)); + } + + @Override + public boolean readBoolean(String field) throws ConfigValueMissingException { + return this.parse(field, Boolean::parseBoolean); + } + + @Override + public void writeChar(String field, char value) { + this.writePair(field, String.valueOf(value)); + } + + @Override + public char readChar(String field) throws ConfigValueMissingException { + return this.parse(field, s -> s.charAt(0)); + } + + @Override + public void writeInt(String field, int value) { + this.writePair(field, String.valueOf(value)); + } + + @Override + public int readInt(String field) throws ConfigValueMissingException { + return this.parse(field, Integer::parseInt); + } + + @Override + public void writeLong(String field, long value) { + this.writePair(field, String.valueOf(value)); + } + + @Override + public long readLong(String field) throws ConfigValueMissingException { + return this.parse(field, Long::parseLong); + } + + @Override + public void writeFloat(String field, float value) { + this.writePair(field, String.valueOf(value)); + } + + @Override + public float readFloat(String field) throws ConfigValueMissingException { + return this.parse(field, Float::parseFloat); + } + + @Override + public void writeDouble(String field, double value) { + this.writePair(field, String.valueOf(value)); + } + + @Override + public double readDouble(String field) throws ConfigValueMissingException { + return this.parse(field, Double::parseDouble); + } + + @Override + public void writeString(String field, String value) { + this.writePair(field, value); + } + + @Override + public String readString(String field) throws ConfigValueMissingException { + return this.parse(field, Function.identity()); + } + + @Override + public void writeBoolArray(String field, boolean[] values) { + String[] strings = new String[values.length]; + int i = 0; + for (boolean value : values) { + strings[i++] = String.valueOf(value); + } + this.writePair(field, String.join(settings.arraySeparator, strings)); + } + + @Override + public boolean[] readBoolArray(String field) throws ConfigValueMissingException { + String[] strings = this.getStringArray(field); + boolean[] values = new boolean[strings.length]; + int i = 0; + for (String string : strings) { + try { + values[i++] = Boolean.parseBoolean(string); + } catch (Exception e) { + throw new ConfigValueMissingException("Invalid value: " + string); + } + } + return values; + } + + @Override + public void writeIntArray(String field, int[] values) { + String[] strings = new String[values.length]; + int i = 0; + for (int value : values) { + strings[i++] = String.valueOf(value); + } + this.writePair(field, String.join(settings.arraySeparator, strings)); + } + + @Override + public int[] readIntArray(String field) throws ConfigValueMissingException { + String[] strings = this.getStringArray(field); + int[] values = new int[strings.length]; + int i = 0; + for (String string : strings) { + try { + values[i++] = Integer.parseInt(string); + } catch (Exception e) { + throw new ConfigValueMissingException("Invalid value: " + string); + } + } + return values; + } + + @Override + public void writeLongArray(String field, long[] values) { + String[] strings = new String[values.length]; + int i = 0; + for (long value : values) { + strings[i++] = String.valueOf(value); + } + this.writePair(field, String.join(settings.arraySeparator, strings)); + } + + @Override + public long[] readLongArray(String field) throws ConfigValueMissingException { + String[] strings = this.getStringArray(field); + long[] values = new long[strings.length]; + int i = 0; + for (String string : strings) { + try { + values[i++] = Long.parseLong(string); + } catch (Exception e) { + throw new ConfigValueMissingException("Invalid value: " + string); + } + } + return values; + } + + @Override + public void writeFloatArray(String field, float[] values) { + String[] strings = new String[values.length]; + int i = 0; + for (float value : values) { + strings[i++] = String.valueOf(value); + } + this.writePair(field, String.join(settings.arraySeparator, strings)); + } + + @Override + public float[] readFloatArray(String field) throws ConfigValueMissingException { + String[] strings = this.getStringArray(field); + float[] values = new float[strings.length]; + int i = 0; + for (String string : strings) { + try { + values[i++] = Float.parseFloat(string); + } catch (Exception e) { + throw new ConfigValueMissingException("Invalid value: " + string); + } + } + return values; + } + + @Override + public void writeDoubleArray(String field, double[] values) { + String[] strings = new String[values.length]; + int i = 0; + for (double value : values) { + strings[i++] = String.valueOf(value); + } + this.writePair(field, String.join(settings.arraySeparator, strings)); + } + + @Override + public double[] readDoubleArray(String field) throws ConfigValueMissingException { + String[] strings = this.getStringArray(field); + double[] values = new double[strings.length]; + int i = 0; + for (String string : strings) { + try { + values[i++] = Double.parseDouble(string); + } catch (Exception e) { + throw new ConfigValueMissingException("Invalid value: " + string); + } + } + return values; + } + + @Override + public void writeStringArray(String field, String[] values) { + this.writePair(field, String.join(settings.arraySeparator, values)); + } + + @Override + public String[] readStringArray(String field) throws ConfigValueMissingException { + return this.getStringArray(field); + } + + @Override + public > void writeEnum(String field, E value) { + this.writePair(field, value.name()); + } + + @Override + public > E readEnum(String field, Class enumClass) throws ConfigValueMissingException { + return ConfigUtils.getEnumConstant(this.getValue(field), enumClass); + } + + @Override + public > void writeEnumArray(String field, E[] value) { + String[] strings = Arrays.stream(value).map(Enum::name).toArray(String[]::new); + writeStringArray(field, strings); + } + + @SuppressWarnings("unchecked") + @Override + public > E[] readEnumArray(String field, Class enumClass) throws ConfigValueMissingException { + String[] strings = readStringArray(field); + E[] arr = (E[]) Array.newInstance(enumClass, strings.length); + for (int i = 0; i < strings.length; i++) { + arr[i] = ConfigUtils.getEnumConstant(strings[i], enumClass); + } + return arr; + } + + @Override + public void writeMap(String field, Map> value) { + String prefix = this.prefix != null ? this.prefix + "." + field : field; + PropertiesFormat format = new PropertiesFormat(prefix, this.buffer, this.settings); + value.values().forEach(val -> val.serializeValue(format)); + } + + @Override + public void readMap(String field, Collection> values) throws ConfigValueMissingException { + Set validElements = this.parsed.keySet() + .stream() + .filter(key -> { + String[] strings = key.split("\\.", 2); + if (strings.length < 2) { + return false; + } + String prefix = strings[0]; + return prefix.equals(field); + }) + .collect(Collectors.toSet()); + Map parsed = new HashMap<>(); + for (String key : validElements) { + String s = key.split("\\.", 2)[1]; + parsed.put(s, this.getValue(key)); + } + PropertiesFormat format = new PropertiesFormat(this.prefix, this.buffer, parsed, this.settings); + for (ConfigValue value : values) { + value.deserializeValue(format); + } + } + + @Override + public void readFile(File file) throws IOException, ConfigReadException { + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String line; + while ((line = reader.readLine()) != null) { + String filtered = line.replaceAll("#.+$", ""); + boolean isPair = filtered.contains("="); + String[] components = filtered.split("="); + if (components.length != 2) { + if (isPair) { + parsed.put(components[0], ""); + } + continue; + } + parsed.put(components[0], components[1]); + } + } + } + + @Override + public void writeFile(File file) throws IOException { + try (FileWriter writer = new FileWriter(file)) { + writer.write(this.buffer.toString()); + } + } + + @Override + public void addComments(IDescriptionProvider provider) { + String[] comments = provider.getDescription(); + if (comments.length == 0) { + return; + } + for (String string : comments) { + this.buffer.append("# ").append(string).append("\n"); + } + } + + private String getValue(String field) throws ConfigValueMissingException { + String res = this.parsed.get(field); + if (res == null) { + throw new ConfigValueMissingException("Missing value " + field); + } + return res; + } + + private T parse(String s, Function parser) throws ConfigValueMissingException { + String val = this.getValue(s); + try { + return parser.apply(val); + } catch (Exception e) { + throw new ConfigValueMissingException("Value parse failed", e); + } + } + + private void writePair(String field, String value) { + if (this.prefix != null) { + this.buffer.append(this.prefix).append("."); + } + this.buffer.append(field).append("=").append(value); + for (int i = 0; i < settings.newlines; i++) { + this.buffer.append("\n"); + } + } + + private String[] getStringArray(String field) throws ConfigValueMissingException { + String value = this.getValue(field); + return value.split(this.settings.arraySeparator); + } + + /** + * Settings holder for JSON configs + * + * @author Toma + */ + public static final class Settings { + + private String arraySeparator = ";"; + private int newlines = 1; + + /** + * Allows you to configure custom separator used for arrays in case the default + * one is causing issues + * + * @param arraySeparator Nonnull separator to be used + * @return This instance + */ + public Settings arraySeparator(String arraySeparator) { + this.arraySeparator = Objects.requireNonNull(arraySeparator); + return this; + } + + /** + * Specifies amount of newlines after each value (Not comments) + * @param count Count of newlines + * @return This instance + */ + public Settings newlines(int count) { + this.newlines = Math.max(1, count); + return this; + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/format/YamlFormat.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/format/YamlFormat.java new file mode 100644 index 0000000..3c9f3c5 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/format/YamlFormat.java @@ -0,0 +1,451 @@ +package mod.azure.azurelib.common.internal.common.config.format; + +import mod.azure.azurelib.common.internal.common.config.ConfigUtils; +import mod.azure.azurelib.common.internal.common.config.value.ConfigValue; +import mod.azure.azurelib.common.internal.common.config.value.IDescriptionProvider; +import mod.azure.azurelib.common.internal.common.config.exception.ConfigReadException; +import mod.azure.azurelib.common.internal.common.config.exception.ConfigValueMissingException; + +import java.io.*; +import java.lang.reflect.Array; +import java.util.*; +import java.util.function.Function; +import java.util.regex.Pattern; + +public class YamlFormat implements IConfigFormat { + + // writing + private final StringBuilder buffer; + private final int currentNesting; + + // reading + private final Map processedData; + private int readerIndex; + + + public YamlFormat() { + this(new HashMap<>()); + } + + public YamlFormat(StringBuilder buffer, int nesting) { + this.buffer = buffer; + this.currentNesting = nesting; + this.processedData = new HashMap<>(); + } + + public YamlFormat(Map processed) { + this.buffer = new StringBuilder(); + this.currentNesting = 0; + this.processedData = processed; + } + + @Override + public void writeBoolean(String field, boolean value) { + writeValuePair(field, String.valueOf(value)); + } + + @Override + public boolean readBoolean(String field) throws ConfigValueMissingException { + return getValue(field, Boolean::parseBoolean); + } + + @Override + public void writeChar(String field, char value) { + writeValuePair(field, String.valueOf(value)); + } + + @Override + public char readChar(String field) throws ConfigValueMissingException { + return getValue(field, str -> str.charAt(0)); + } + + @Override + public void writeInt(String field, int value) { + writeValuePair(field, String.valueOf(value)); + } + + @Override + public int readInt(String field) throws ConfigValueMissingException { + return getValue(field, Integer::parseInt); + } + + @Override + public void writeLong(String field, long value) { + writeValuePair(field, String.valueOf(value)); + } + + @Override + public long readLong(String field) throws ConfigValueMissingException { + return getValue(field, Long::parseLong); + } + + @Override + public void writeFloat(String field, float value) { + writeValuePair(field, String.valueOf(value)); + } + + @Override + public float readFloat(String field) throws ConfigValueMissingException { + return getValue(field, Float::parseFloat); + } + + @Override + public void writeDouble(String field, double value) { + writeValuePair(field, String.valueOf(value)); + } + + @Override + public double readDouble(String field) throws ConfigValueMissingException { + return getValue(field, Double::parseDouble); + } + + @Override + public void writeString(String field, String value) { + writeValuePair(field, value); + } + + @Override + public String readString(String field) throws ConfigValueMissingException { + return getValue(field, Function.identity()); + } + + @Override + public void writeBoolArray(String field, boolean[] values) { + writeKey(field); + for (boolean value : values) { + writeArrayEntry(String.valueOf(value)); + } + newLine(); + } + + @Override + public boolean[] readBoolArray(String field) throws ConfigValueMissingException { + String[] arr = this.getValueArray(field); + boolean[] res = new boolean[arr.length]; + for (int i = 0; i < arr.length; i++) { + res[i] = Boolean.parseBoolean(arr[i]); + } + return res; + } + + @Override + public void writeIntArray(String field, int[] values) { + writeKey(field); + for (int value : values) { + writeArrayEntry(String.valueOf(value)); + } + newLine(); + } + + @Override + public int[] readIntArray(String field) throws ConfigValueMissingException { + String[] arr = this.getValueArray(field); + int[] res = new int[arr.length]; + for (int i = 0; i < arr.length; i++) { + try { + res[i] = Integer.parseInt(arr[i]); + } catch (NumberFormatException e) { + throw new ConfigValueMissingException("Invalid value: " + field); + } + } + return res; + } + + @Override + public void writeLongArray(String field, long[] values) { + writeKey(field); + for (long value : values) { + writeArrayEntry(String.valueOf(value)); + } + newLine(); + } + + @Override + public long[] readLongArray(String field) throws ConfigValueMissingException { + String[] arr = this.getValueArray(field); + long[] res = new long[arr.length]; + for (int i = 0; i < arr.length; i++) { + try { + res[i] = Long.parseLong(arr[i]); + } catch (NumberFormatException e) { + throw new ConfigValueMissingException("Invalid value: " + field); + } + } + return res; + } + + @Override + public void writeFloatArray(String field, float[] values) { + writeKey(field); + for (float value : values) { + writeArrayEntry(String.valueOf(value)); + } + newLine(); + } + + @Override + public float[] readFloatArray(String field) throws ConfigValueMissingException { + String[] arr = this.getValueArray(field); + float[] res = new float[arr.length]; + for (int i = 0; i < arr.length; i++) { + try { + res[i] = Float.parseFloat(arr[i]); + } catch (NumberFormatException e) { + throw new ConfigValueMissingException("Invalid value: " + field); + } + } + return res; + } + + @Override + public void writeDoubleArray(String field, double[] values) { + writeKey(field); + for (double value : values) { + writeArrayEntry(String.valueOf(value)); + } + newLine(); + } + + @Override + public double[] readDoubleArray(String field) throws ConfigValueMissingException { + String[] arr = this.getValueArray(field); + double[] res = new double[arr.length]; + for (int i = 0; i < arr.length; i++) { + try { + res[i] = Double.parseDouble(arr[i]); + } catch (NumberFormatException e) { + throw new ConfigValueMissingException("Invalid value: " + field); + } + } + return res; + } + + @Override + public void writeStringArray(String field, String[] values) { + writeKey(field); + for (String value : values) { + writeArrayEntry(value); + } + newLine(); + } + + @Override + public String[] readStringArray(String field) throws ConfigValueMissingException { + return this.getValueArray(field); + } + + @Override + public > void writeEnum(String field, E value) { + writeValuePair(field, value.name()); + } + + @Override + public > E readEnum(String field, Class enumClass) throws ConfigValueMissingException { + String name = this.readString(field); + return ConfigUtils.getEnumConstant(name, enumClass); + } + + @Override + public > void writeEnumArray(String field, E[] value) { + String[] strings = Arrays.stream(value).map(Enum::name).toArray(String[]::new); + writeStringArray(field, strings); + } + + @SuppressWarnings("unchecked") + @Override + public > E[] readEnumArray(String field, Class enumClass) throws ConfigValueMissingException { + String[] strings = readStringArray(field); + E[] arr = (E[]) Array.newInstance(enumClass, strings.length); + for (int i = 0; i < strings.length; i++) { + arr[i] = ConfigUtils.getEnumConstant(strings[i], enumClass); + } + return arr; + } + + @Override + public void writeMap(String field, Map> value) { + writeKey(field); + YamlFormat format = new YamlFormat(this.buffer, this.currentNesting + 1); + value.values().forEach(val -> val.serializeValue(format)); + } + + @Override + public void readMap(String field, Collection> values) throws ConfigValueMissingException { + Map map = this.getValueMap(field); + YamlFormat format = new YamlFormat(map); + values.forEach(val -> val.deserializeValue(format)); + } + + @Override + public void readFile(File file) throws IOException, ConfigReadException { + List lines = new ArrayList<>(); + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String line; + while ((line = reader.readLine()) != null) { + String editedText = line.replaceAll("^[\\s|\\t]*#.+$", ""); + if (!editedText.isEmpty()) { + lines.add(editedText); + } + } + } + try { + while (readerIndex < lines.size()) { + this.process(lines); + } + } catch (Exception e) { + throw new ConfigReadException("Config process failed", e); + } + } + + private void process(List list) throws ConfigReadException { + String value = list.get(readerIndex); + String[] components = value.split(":\\s*", 2); + Pattern pattern = Pattern.compile("^.+:\\s?\n?$"); + if (components.length == 1 || pattern.matcher(value).matches()) { + // Arrays / objects + if (readerIndex == list.size() - 1) { + this.processedData.put(components[0].trim(), new String[0]); + ++readerIndex; + return; + } + String next = list.get(readerIndex + 1); + if (next.trim().startsWith("-")) { + this.processArray(list, components[0].trim()); + } else { + this.processMap(list, components[0].trim()); + } + return; + } else if (components.length == 2) { + // Primitives + this.processedData.put(components[0].trim(), components[1].trim()); + ++readerIndex; + return; + } + throw new ConfigReadException("Invalid config format"); + } + + private void processMap(List list, String key) throws ConfigReadException { + String prefix = "^ {2}"; + List newValues = new ArrayList<>(); + while (readerIndex < list.size()) { + int next = readerIndex + 1; + if (next >= list.size()) + break; + String value = list.get(next); + if (value.startsWith(" ")) { + newValues.add(value.replaceFirst(prefix, "")); + ++readerIndex; + } else { + break; + } + } + YamlFormat format = new YamlFormat(new HashMap<>()); + while (format.readerIndex < newValues.size()) { + format.process(newValues); + } + this.processedData.put(key, format.processedData); + ++this.readerIndex; + } + + private void processArray(List list, String key) { + ++readerIndex; + List entries = new ArrayList<>(); + while (readerIndex < list.size()) { + String entry = list.get(readerIndex).trim(); + if (!entry.startsWith("-")) + break; + entries.add(entry.replaceAll("^-\\s", "")); + ++readerIndex; + } + this.processedData.put(key, entries.toArray(new String[0])); + } + + @Override + public void writeFile(File file) throws IOException { + try (FileWriter writer = new FileWriter(file)) { + writer.write(this.buffer.toString()); + } + } + + @Override + public void addComments(IDescriptionProvider provider) { + for (String comment : provider.getDescription()) { + spaces(); + buffer.append("# ").append(comment).append("\n"); + } + } + + private void spaces() { + this.spaces(this.currentNesting); + } + + private void spaces(int nestIndex) { + if (nestIndex > 0) { + for (int i = 0; i < nestIndex * 2; i++) { + buffer.append(" "); + } + } + } + + private void writeKey(String key) { + spaces(); + buffer.append(key).append(":\n"); + } + + private void newLine() { + buffer.append("\n"); + } + + private void writeArrayEntry(String value) { + spaces(this.currentNesting + 1); + buffer.append("- ").append(value).append("\n"); + } + + private void writeValuePair(String key, String value) { + spaces(); + buffer.append(key).append(": ").append(value).append("\n\n"); + } + + private V getValue(String key, Function parser) throws ConfigValueMissingException { + Object value = this.processedData.get(key); + if (value == null) { + throw new ConfigValueMissingException("Missing value: " + key); + } + try { + return parser.apply(value.toString()); + } catch (ClassCastException | NumberFormatException e) { + throw new ConfigValueMissingException("Value parse failed: " + key + ", value: " + value); + } + } + + private String[] getValueArray(String key) throws ConfigValueMissingException { + Object value = this.processedData.get(key); + if (value == null) { + throw new ConfigValueMissingException("Missing value: " + key); + } + try { + if (value instanceof Map) { + return new String[0]; + } + return (String[]) value; + } catch (ClassCastException e) { + throw new ConfigValueMissingException("Value parse failed: " + key + ", value: " + value); + } + } + + @SuppressWarnings("unchecked") + private Map getValueMap(String key) throws ConfigValueMissingException { + Object value = this.processedData.get(key); + if (value == null) { + throw new ConfigValueMissingException("Missing value: " + key); + } + try { + if (value instanceof String[]) { + return new HashMap<>(); + } + return (Map) value; + } catch (ClassCastException e) { + throw new ConfigValueMissingException("Value parse failed: " + key + ", value: " + value); + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/io/ConfigIO.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/io/ConfigIO.java new file mode 100644 index 0000000..af27355 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/io/ConfigIO.java @@ -0,0 +1,110 @@ +package mod.azure.azurelib.common.internal.common.config.io; + +import mod.azure.azurelib.common.internal.common.config.ConfigHolder; +import mod.azure.azurelib.common.internal.common.config.format.IConfigFormat; +import mod.azure.azurelib.common.internal.common.config.format.IConfigFormatHandler; +import mod.azure.azurelib.common.internal.common.AzureLib; +import mod.azure.azurelib.common.internal.common.AzureLibException; +import mod.azure.azurelib.common.internal.common.config.exception.ConfigReadException; +import net.minecraft.CrashReport; +import net.minecraft.ReportedException; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.MarkerManager; + +import java.io.File; +import java.io.IOException; + +public final class ConfigIO { + + public static final Marker MARKER = MarkerManager.getMarker("IO"); + public static final FileWatchManager FILE_WATCH_MANAGER = new FileWatchManager(); + + private ConfigIO() {} + public static void processConfig(ConfigHolder holder) { + AzureLib.LOGGER.debug(MARKER, "Starting processing of config {}", holder.getConfigId()); + processSafely(holder, () -> { + File file = getConfigFile(holder); + if (file.exists()) { + try { + readConfig(holder); + } catch (IOException e) { + AzureLib.LOGGER.error(MARKER, "Config read failed for config ID {}, will create default config file", holder.getConfigId()); + } + } + try { + writeConfig(holder); + } catch (IOException e) { + AzureLib.LOGGER.fatal(MARKER, "Couldn't write config {}, aborting mod startup", holder.getConfigId()); + throw new AzureLibException("Config write failed", e); + } + }); + AzureLib.LOGGER.debug(MARKER, "Processing of config {} has finished", holder.getConfigId()); + } + + public static void reloadClientValues(ConfigHolder configHolder) { + processSafely(configHolder, () -> { + try { + readConfig(configHolder); + } catch (IOException e) { + AzureLib.LOGGER.error(MARKER, "Failed to read config file {}", configHolder.getConfigId()); + } + }); + } + + public static void saveClientValues(ConfigHolder configHolder) { + processSafely(configHolder, () -> { + try { + writeConfig(configHolder); + } catch (IOException e) { + AzureLib.LOGGER.error(MARKER, "Failed to write config file {}", configHolder.getConfigId()); + } + }); + } + + private static void processSafely(ConfigHolder holder, Runnable action) { + try { + synchronized (holder.getLock()) { + action.run(); + } + } catch (Exception e) { + AzureLib.LOGGER.fatal(MARKER, "Error loading config {} due to critical error '{}'. Report this issue to this config's owner!", holder.getConfigId(), e.getMessage()); + throw new ReportedException(CrashReport.forThrowable(e, "Config " + holder.getConfigId() + " failed. Report issue to config owner")); + } + } + + private static void readConfig(ConfigHolder holder) throws IOException { + AzureLib.LOGGER.debug(MARKER, "Reading config {}", holder.getConfigId()); + IConfigFormat format = holder.getFormat().createFormat(); + File file = getConfigFile(holder); + if (!file.exists()) + return; + try { + format.readFile(file); + holder.values().forEach(value -> value.deserializeValue(format)); + } catch (ConfigReadException e) { + AzureLib.LOGGER.error(MARKER, "Config read failed, using default values", e); + } + } + + public static void writeConfig(ConfigHolder holder) throws IOException { + AzureLib.LOGGER.debug(MARKER, "Writing config {}", holder.getConfigId()); + File file = getConfigFile(holder); + File dir = file.getParentFile(); + if (dir.mkdirs()) { + AzureLib.LOGGER.debug(MARKER, "Created file directories at {}", dir.getAbsolutePath()); + } + if (!file.exists() && !file.createNewFile()) { + throw new AzureLibException("Config file create failed"); + } + IConfigFormatHandler handler = holder.getFormat(); + IConfigFormat format = handler.createFormat(); + holder.values().forEach(value -> value.serializeValue(format)); + format.writeFile(file); + } + + public static File getConfigFile(ConfigHolder holder) { + IConfigFormatHandler handler = holder.getFormat(); + String filename = holder.getFilename(); + return new File("./config/" + filename + "." + handler.fileExt()); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/io/FileWatchManager.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/io/FileWatchManager.java new file mode 100644 index 0000000..3b5cfc7 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/io/FileWatchManager.java @@ -0,0 +1,107 @@ +package mod.azure.azurelib.common.internal.common.config.io; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardWatchEventKinds; +import java.nio.file.WatchEvent; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import mod.azure.azurelib.common.internal.common.config.ConfigHolder; +import mod.azure.azurelib.common.internal.common.AzureLib; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.MarkerManager; +import org.jetbrains.annotations.Nullable; + +public final class FileWatchManager { + + public static final Marker MARKER = MarkerManager.getMarker("FileWatching"); + private final Map> configPaths = new HashMap<>(); + private final List watchKeys = new ArrayList<>(); + @Nullable + private final WatchService service; + private final ScheduledExecutorService executorService; + private final Set processCache = new HashSet<>(); + + public FileWatchManager() { + WatchService watchService = null; + try { + watchService = FileSystems.getDefault().newWatchService(); + } catch (IOException e) { + AzureLib.LOGGER.error(MARKER, "Failed to initialize file watch service due to error, configs won't be automatically refreshed", e); + } finally { + this.service = watchService; + this.executorService = Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r); + t.setName("Auto-Sync thread"); + return t; + }); + } + } + + public void stopService() { + this.executorService.shutdown(); + } + + public void startService() { + AzureLib.LOGGER.debug(MARKER, "Starting file watching service"); + if (this.service == null) { + AzureLib.LOGGER.error(MARKER, "Unable to start file watch service"); + return; + } + Path configDir = Paths.get("./config"); + try { + Files.walkFileTree(configDir, new SimpleFileVisitor() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + WatchKey key = dir.register(FileWatchManager.this.service, StandardWatchEventKinds.ENTRY_MODIFY); + FileWatchManager.this.watchKeys.add(key); + return FileVisitResult.CONTINUE; + } + }); + this.executorService.scheduleAtFixedRate(() -> { + this.processCache.clear(); + this.watchKeys.forEach(key -> { + List> eventList = key.pollEvents(); + eventList.forEach(event -> { + Path path = (Path) event.context(); + String strPath = path.toString().replaceAll("\\..+$", ""); + if (this.processCache.contains(strPath)) + return; // Ignore duplicate reads from subdirectories + ConfigHolder holder = this.configPaths.get(strPath); + if (holder != null) { + ConfigIO.reloadClientValues(holder); + holder.dispatchFileRefreshEvent(); + this.processCache.add(strPath); + } + }); + }); + }, 0L, 1000L, TimeUnit.MILLISECONDS); + } catch (IOException e) { + AzureLib.LOGGER.error(MARKER, "Unable to create watch key for config directory, disabling auto-sync function", e); + } + } + + public void addTrackedConfig(ConfigHolder holder) { + Path path = Paths.get(holder.getFilename()); + File file = path.toFile(); + this.configPaths.put(file.getName(), holder); + AzureLib.LOGGER.info(MARKER, "Registered {} config for auto-sync function", holder.getConfigId()); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/validate/NotificationSeverity.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/validate/NotificationSeverity.java new file mode 100644 index 0000000..b2b881b --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/validate/NotificationSeverity.java @@ -0,0 +1,38 @@ +package mod.azure.azurelib.common.internal.common.config.validate; + +import mod.azure.azurelib.common.internal.common.AzureLib; +import net.minecraft.ChatFormatting; +import net.minecraft.resources.ResourceLocation; + +public enum NotificationSeverity { + + INFO("", ChatFormatting.RESET, 0xF0030319, 0x502493E5, 0x502469E5), + WARNING("warning", ChatFormatting.GOLD, 0xF0563900, 0x50FFB200, 0x509E6900), + ERROR("error", ChatFormatting.RED, 0xF0270006, 0x50FF0000, 0x50880000); + + private final ResourceLocation icon; + private final ChatFormatting extraFormatting; + public final int background; + public final int fadeMin; + public final int fadeMax; + + NotificationSeverity(String iconName, ChatFormatting formatting, int background, int fadeMin, int fadeMax) { + this.icon = AzureLib.modResource("textures/icons/" + iconName + ".png"); + this.extraFormatting = formatting; + this.background = background; + this.fadeMin = fadeMin; + this.fadeMax = fadeMax; + } + + public ResourceLocation getIcon() { + return icon; + } + + public ChatFormatting getExtraFormatting() { + return extraFormatting; + } + + public boolean isOkStatus() { + return this == INFO; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/validate/ValidationResult.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/validate/ValidationResult.java new file mode 100644 index 0000000..a63d680 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/validate/ValidationResult.java @@ -0,0 +1,25 @@ +package mod.azure.azurelib.common.internal.common.config.validate; + +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.MutableComponent; + +public record ValidationResult(NotificationSeverity severity, MutableComponent text) { + + private static final ValidationResult OK = new ValidationResult(NotificationSeverity.INFO, (MutableComponent) CommonComponents.EMPTY); + + public static ValidationResult ok() { + return OK; + } + + public static ValidationResult warn(MutableComponent text) { + return new ValidationResult(NotificationSeverity.WARNING, text); + } + + public static ValidationResult error(MutableComponent text) { + return new ValidationResult(NotificationSeverity.ERROR, text); + } + + public boolean isOk() { + return this.severity.isOkStatus(); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/ArrayValue.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/ArrayValue.java new file mode 100644 index 0000000..4a8e8c3 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/ArrayValue.java @@ -0,0 +1,10 @@ +package mod.azure.azurelib.common.internal.common.config.value; + +public interface ArrayValue { + + boolean isFixedSize(); + + default String elementToString(Object element) { + return element.toString(); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/BooleanArrayValue.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/BooleanArrayValue.java new file mode 100644 index 0000000..fcf1a65 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/BooleanArrayValue.java @@ -0,0 +1,93 @@ +package mod.azure.azurelib.common.internal.common.config.value; + +import mod.azure.azurelib.common.internal.common.config.ConfigUtils; +import mod.azure.azurelib.common.internal.common.config.Configurable; +import mod.azure.azurelib.common.internal.common.config.format.IConfigFormat; +import mod.azure.azurelib.common.internal.common.config.adapter.TypeAdapter; +import mod.azure.azurelib.common.internal.common.config.exception.ConfigValueMissingException; +import net.minecraft.network.FriendlyByteBuf; + +import java.lang.reflect.Field; +import java.util.Arrays; + +public class BooleanArrayValue extends ConfigValue implements ArrayValue { + + private boolean fixedSize; + + public BooleanArrayValue(ValueData valueData) { + super(valueData); + } + + @Override + public boolean isFixedSize() { + return fixedSize; + } + + @Override + protected void readFieldData(Field field) { + this.fixedSize = field.getAnnotation(Configurable.FixedSize.class) != null; + } + + @Override + protected boolean[] getCorrectedValue(boolean[] in) { + if (this.fixedSize) { + boolean[] defaultArray = this.valueData.getDefaultValue(); + if (in.length != defaultArray.length) { + ConfigUtils.logArraySizeCorrectedMessage(this.getId(), Arrays.toString(in), Arrays.toString(defaultArray)); + return defaultArray; + } + } + return in; + } + + @Override + protected void serialize(IConfigFormat format) { + format.writeBoolArray(this.getId(), this.get()); + } + + @Override + protected void deserialize(IConfigFormat format) throws ConfigValueMissingException { + this.set(format.readBoolArray(this.getId())); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("["); + boolean[] booleans = this.get(); + for (int i = 0; i < booleans.length; i++) { + builder.append(this.elementToString(booleans[i])); + if (i < booleans.length - 1) { + builder.append(","); + } + } + builder.append("]"); + return builder.toString(); + } + + public static final class Adapter extends TypeAdapter { + + @Override + public ConfigValue serialize(String name, String[] comments, Object value, TypeSerializer serializer, AdapterContext context) throws IllegalAccessException { + return new BooleanArrayValue(ValueData.of(name, (boolean[]) value, context, comments)); + } + + @Override + public void encodeToBuffer(ConfigValue value, FriendlyByteBuf buffer) { + boolean[] arr = (boolean[]) value.get(); + buffer.writeInt(arr.length); + for (boolean b : arr) { + buffer.writeBoolean(b); + } + } + + @Override + public Object decodeFromBuffer(ConfigValue value, FriendlyByteBuf buffer) { + boolean[] arr = new boolean[buffer.readInt()]; + for (int i = 0; i < arr.length; i++) { + arr[i] = buffer.readBoolean(); + } + return arr; + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/BooleanValue.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/BooleanValue.java new file mode 100644 index 0000000..8f11186 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/BooleanValue.java @@ -0,0 +1,50 @@ +package mod.azure.azurelib.common.internal.common.config.value; + +import mod.azure.azurelib.common.internal.common.config.adapter.TypeAdapter; +import mod.azure.azurelib.common.internal.common.config.exception.ConfigValueMissingException; +import mod.azure.azurelib.common.internal.common.config.format.IConfigFormat; +import net.minecraft.network.FriendlyByteBuf; + +import java.lang.reflect.Field; + +public final class BooleanValue extends ConfigValue { + + public BooleanValue(ValueData valueData) { + super(valueData); + } + + @Override + public void serialize(IConfigFormat format) { + boolean value = this.get(); + format.writeBoolean(this.getId(), value); + } + + @Override + public void deserialize(IConfigFormat format) throws ConfigValueMissingException { + String field = this.getId(); + this.set(format.readBoolean(field)); + } + + public static class Adapter extends TypeAdapter { + + @Override + public ConfigValue serialize(String name, String[] comments, Object value, TypeSerializer serializer, AdapterContext context) { + return new BooleanValue(ValueData.of(name, (boolean) value, context, comments)); + } + + @Override + public void encodeToBuffer(ConfigValue value, FriendlyByteBuf buffer) { + buffer.writeBoolean((Boolean) value.get()); + } + + @Override + public Object decodeFromBuffer(ConfigValue value, FriendlyByteBuf buffer) { + return buffer.readBoolean(); + } + + @Override + public void setFieldValue(Field field, Object instance, Object value) throws IllegalAccessException { + field.setBoolean(instance, (boolean) value); + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/CharValue.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/CharValue.java new file mode 100644 index 0000000..87f78e8 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/CharValue.java @@ -0,0 +1,48 @@ +package mod.azure.azurelib.common.internal.common.config.value; + +import mod.azure.azurelib.common.internal.common.config.format.IConfigFormat; +import mod.azure.azurelib.common.internal.common.config.adapter.TypeAdapter; +import mod.azure.azurelib.common.internal.common.config.exception.ConfigValueMissingException; +import net.minecraft.network.FriendlyByteBuf; + +import java.lang.reflect.Field; + +public final class CharValue extends ConfigValue { + + public CharValue(ValueData valueData) { + super(valueData); + } + + @Override + protected void serialize(IConfigFormat format) { + format.writeChar(this.getId(), this.get()); + } + + @Override + protected void deserialize(IConfigFormat format) throws ConfigValueMissingException { + this.set(format.readChar(this.getId())); + } + + public static final class Adapter extends TypeAdapter { + + @Override + public ConfigValue serialize(String name, String[] comments, Object value, TypeSerializer serializer, AdapterContext context) throws IllegalAccessException { + return new CharValue(ValueData.of(name, (char) value, context, comments)); + } + + @Override + public void encodeToBuffer(ConfigValue value, FriendlyByteBuf buffer) { + buffer.writeChar((Integer) value.get()); + } + + @Override + public Object decodeFromBuffer(ConfigValue value, FriendlyByteBuf buffer) { + return buffer.readChar(); + } + + @Override + public void setFieldValue(Field field, Object instance, Object value) throws IllegalAccessException { + field.setChar(instance, (char) value); + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/ConfigValue.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/ConfigValue.java new file mode 100644 index 0000000..09455ff --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/ConfigValue.java @@ -0,0 +1,145 @@ +package mod.azure.azurelib.common.internal.common.config.value; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +import mod.azure.azurelib.common.internal.client.config.IValidationHandler; +import mod.azure.azurelib.common.internal.common.config.ConfigUtils; +import mod.azure.azurelib.common.internal.common.config.Configurable; +import mod.azure.azurelib.common.internal.common.config.adapter.TypeAdapter; +import mod.azure.azurelib.common.internal.common.config.exception.ConfigValueMissingException; +import org.jetbrains.annotations.Nullable; + +import mod.azure.azurelib.common.internal.common.config.format.IConfigFormat; + +public abstract class ConfigValue implements Supplier{ + + protected final ValueData valueData; + private T value; + private boolean synchronizeToClient; + @Nullable + private SetValueCallback setValueCallback; + + protected ConfigValue(ValueData valueData) { + this.valueData = valueData; + this.useDefaultValue(); + } + + @Override + public final T get() { + return value; + } + + public final boolean shouldSynchronize() { + return synchronizeToClient; + } + + public final void set(T value) { + T corrected = this.getCorrectedValue(value); + if (corrected == null) { + this.useDefaultValue(); + corrected = this.get(); + } + this.value = corrected; + this.valueData.setValueToMemory(corrected); + } + + public final void setWithValidationHandler(T value, IValidationHandler handler) { + this.invokeValueValidator(value, handler); + this.set(value); + } + + public final String getId() { + return this.valueData.getId(); + } + + public final void setParent(@Nullable ConfigValue parent) { + this.valueData.setParent(parent); + } + + public final void processFieldData(Field field) { + this.synchronizeToClient = field.getAnnotation(Configurable.Synchronized.class) != null; + this.readFieldData(field); + } + + protected void readFieldData(Field field) { + + } + + protected T getCorrectedValue(T in) { + return in; + } + + public final void useDefaultValue() { + this.set(this.valueData.getDefaultValue()); + } + + public void setValueValidator(SetValueCallback callback) { + this.setValueCallback = callback; + } + + public final void invokeValueValidator(T value, IValidationHandler handler) { + if (this.setValueCallback != null) { + this.setValueCallback.processValue(value, handler); + } + } + + protected abstract void serialize(IConfigFormat format); + + public final void serializeValue(IConfigFormat format) { + format.addComments(valueData); + this.serialize(format); + } + + public final String[] getDescription() { + return this.valueData.getDescription(); + } + + protected abstract void deserialize(IConfigFormat format) throws ConfigValueMissingException; + + public final void deserializeValue(IConfigFormat format) { + try { + this.deserialize(format); + } catch (ConfigValueMissingException e) { + this.useDefaultValue(); + ConfigUtils.logCorrectedMessage(this.getId(), null, this.get()); + } + } + + public final TypeAdapter.AdapterContext getSerializationContext() { + return this.valueData.getContext(); + } + + public final TypeAdapter getAdapter() { + return this.getSerializationContext().getAdapter(); + } + + public final Class getValueType() { + return this.valueData.getValueType(); + } + + public final String getFieldPath() { + List paths = new ArrayList<>(); + paths.add(this.getId()); + ConfigValue parent = this; + while ((parent = parent.valueData.getParent()) != null) { + paths.add(parent.getId()); + } + Collections.reverse(paths); + return paths.stream().reduce("$", (a, b) -> a + "." + b); + } + + @Override + public String toString() { + return this.value.toString(); + } + + @FunctionalInterface + public interface SetValueCallback { + + void processValue(V value, IValidationHandler handler); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/DecimalValue.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/DecimalValue.java new file mode 100644 index 0000000..59d27b1 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/DecimalValue.java @@ -0,0 +1,78 @@ +package mod.azure.azurelib.common.internal.common.config.value; + +import mod.azure.azurelib.common.internal.common.config.Configurable; + +import java.lang.reflect.Field; +import java.util.Objects; + +public abstract class DecimalValue extends ConfigValue { + + protected Range range; + + protected DecimalValue(ValueData data, Range range) { + super(data); + this.range = Objects.requireNonNull(range); + } + + @Override + protected void readFieldData(Field field) { + super.readFieldData(field); + Configurable.DecimalRange decimalRange = field.getAnnotation(Configurable.DecimalRange.class); + if (decimalRange != null) { + this.range = Range.newBoundedRange(decimalRange.min(), decimalRange.max()); + } + } + + @Override + public abstract N getCorrectedValue(N in); + + public Range getRange() { + return range; + } + + public static final class Range { + + private final double min; + private final double max; + + private Range(double min, double max) { + this.min = min; + this.max = max; + } + + public static Range newBoundedRange(double min, double max) { + if (min > max) { + throw new IllegalArgumentException(String.format("Invalid number range: Min value (%f) cannot be bigger than max value (%f)", min, max)); + } + return new Range(min, max); + } + + public static Range unboundedDouble() { + return newBoundedRange(-Double.MAX_VALUE, Double.MAX_VALUE); + } + + public static Range unboundedFloat() { + return newBoundedRange(-Float.MAX_VALUE, Float.MAX_VALUE); + } + + public boolean isWithin(double number) { + return number >= min && number <= max; + } + + public double min() { + return this.min; + } + + public double max() { + return this.max; + } + + public double clamp(double in) { + return Math.min(max, Math.max(min, in)); + } + + public float clamp(float in) { + return (float) Math.min(max, Math.max(min, in)); + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/DoubleArrayValue.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/DoubleArrayValue.java new file mode 100644 index 0000000..38cf518 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/DoubleArrayValue.java @@ -0,0 +1,110 @@ +package mod.azure.azurelib.common.internal.common.config.value; + +import mod.azure.azurelib.common.internal.common.config.ConfigUtils; +import mod.azure.azurelib.common.internal.common.config.Configurable; +import mod.azure.azurelib.common.internal.common.config.adapter.TypeAdapter; +import mod.azure.azurelib.common.internal.common.config.exception.ConfigValueMissingException; +import mod.azure.azurelib.common.internal.common.config.format.IConfigFormat; +import net.minecraft.network.FriendlyByteBuf; + +import java.lang.reflect.Field; +import java.util.Arrays; + +public class DoubleArrayValue extends ConfigValue implements ArrayValue { + + private boolean fixedSize; + private DecimalValue.Range range; + + public DoubleArrayValue(ValueData valueData) { + super(valueData); + } + + @Override + public boolean isFixedSize() { + return fixedSize; + } + + @Override + protected void readFieldData(Field field) { + this.fixedSize = field.getAnnotation(Configurable.FixedSize.class) != null; + Configurable.DecimalRange decimalRange = field.getAnnotation(Configurable.DecimalRange.class); + this.range = decimalRange != null ? DecimalValue.Range.newBoundedRange(decimalRange.min(), decimalRange.max()) : DecimalValue.Range.unboundedDouble(); + } + + @Override + protected double[] getCorrectedValue(double[] in) { + if (this.fixedSize) { + double[] defaultArray = this.valueData.getDefaultValue(); + if (in.length != defaultArray.length) { + ConfigUtils.logArraySizeCorrectedMessage(this.getId(), Arrays.toString(in), Arrays.toString(defaultArray)); + in = defaultArray; + } + } + if (this.range == null) + return in; + for (int i = 0; i < in.length; i++) { + double value = in[i]; + if (!this.range.isWithin(value)) { + double corrected = this.range.clamp(value); + ConfigUtils.logCorrectedMessage(this.getId() + "[" + i + "]", value, corrected); + in[i] = corrected; + } + } + return in; + } + + @Override + protected void serialize(IConfigFormat format) { + format.writeDoubleArray(this.getId(), this.get()); + } + + @Override + protected void deserialize(IConfigFormat format) throws ConfigValueMissingException { + this.set(format.readDoubleArray(this.getId())); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("["); + double[] doubles = this.get(); + for (int i = 0; i < doubles.length; i++) { + builder.append(this.elementToString(doubles[i])); + if (i < doubles.length - 1) { + builder.append(","); + } + } + builder.append("]"); + return builder.toString(); + } + + public DecimalValue.Range getRange() { + return range; + } + + public static final class Adapter extends TypeAdapter { + + @Override + public void encodeToBuffer(ConfigValue value, FriendlyByteBuf buffer) { + double[] arr = (double[]) value.get(); + buffer.writeInt(arr.length); + for (double v : arr) { + buffer.writeDouble(v); + } + } + + @Override + public Object decodeFromBuffer(ConfigValue value, FriendlyByteBuf buffer) { + double[] arr = new double[buffer.readInt()]; + for (int i = 0; i < arr.length; i++) { + arr[i] = buffer.readDouble(); + } + return arr; + } + + @Override + public ConfigValue serialize(String name, String[] comments, Object value, TypeSerializer serializer, AdapterContext context) throws IllegalAccessException { + return new DoubleArrayValue(ValueData.of(name, (double[]) value, context, comments)); + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/DoubleValue.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/DoubleValue.java new file mode 100644 index 0000000..8d6fa41 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/DoubleValue.java @@ -0,0 +1,61 @@ +package mod.azure.azurelib.common.internal.common.config.value; + +import mod.azure.azurelib.common.internal.common.config.ConfigUtils; +import mod.azure.azurelib.common.internal.common.config.adapter.TypeAdapter; +import mod.azure.azurelib.common.internal.common.config.exception.ConfigValueMissingException; +import mod.azure.azurelib.common.internal.common.config.format.IConfigFormat; +import net.minecraft.network.FriendlyByteBuf; + +import java.lang.reflect.Field; + +public class DoubleValue extends DecimalValue { + + public DoubleValue(ValueData valueData) { + super(valueData, Range.unboundedDouble()); + } + + @Override + public Double getCorrectedValue(Double in) { + if (this.range == null) + return in; + if (!this.range.isWithin(in)) { + double corrected = this.range.clamp(in); + ConfigUtils.logCorrectedMessage(this.getId(), in, corrected); + return corrected; + } + return in; + } + + @Override + protected void serialize(IConfigFormat format) { + format.writeDouble(this.getId(), this.get()); + } + + @Override + protected void deserialize(IConfigFormat format) throws ConfigValueMissingException { + this.set(format.readDouble(this.getId())); + } + + public static final class Adapter extends TypeAdapter { + + @Override + public ConfigValue serialize(String name, String[] comments, Object value, TypeSerializer serializer, AdapterContext context) throws IllegalAccessException { + return new DoubleValue(ValueData.of(name, (double) value, context, comments)); + } + + @Override + public void encodeToBuffer(ConfigValue value, FriendlyByteBuf buffer) { + buffer.writeDouble((Double) value.get()); + } + + @Override + public Object decodeFromBuffer(ConfigValue value, FriendlyByteBuf buffer) { + return buffer.readDouble(); + } + + @Override + public void setFieldValue(Field field, Object instance, Object value) throws IllegalAccessException { + field.setDouble(instance, (Double) value); + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/EnumArrayValue.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/EnumArrayValue.java new file mode 100644 index 0000000..cec8d0d --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/EnumArrayValue.java @@ -0,0 +1,72 @@ +package mod.azure.azurelib.common.internal.common.config.value; + +import java.lang.reflect.Array; +import java.lang.reflect.Field; + +import mod.azure.azurelib.common.internal.common.config.Configurable; +import mod.azure.azurelib.common.internal.common.config.format.IConfigFormat; +import mod.azure.azurelib.common.internal.common.config.adapter.TypeAdapter; +import mod.azure.azurelib.common.internal.common.config.exception.ConfigValueMissingException; +import net.minecraft.network.FriendlyByteBuf; + +public class EnumArrayValue> extends ConfigValue implements ArrayValue { + + private boolean fixedSize; + + public EnumArrayValue(ValueData value) { + super(value); + } + + @Override + public boolean isFixedSize() { + return fixedSize; + } + + @Override + protected void serialize(IConfigFormat format) { + format.writeEnumArray(getId(), get()); + } + + @SuppressWarnings("unchecked") + @Override + protected void deserialize(IConfigFormat format) throws ConfigValueMissingException { + Class type = (Class) getValueType().getComponentType(); + set(format.readEnumArray(getId(), type)); + } + + @Override + protected void readFieldData(Field field) { + this.fixedSize = field.getAnnotation(Configurable.FixedSize.class) != null; + } + + public static final class Adapter> extends TypeAdapter { + + @SuppressWarnings("unchecked") + @Override + public ConfigValue serialize(String name, String[] comments, Object value, TypeSerializer serializer, AdapterContext context) throws IllegalAccessException { + return new EnumArrayValue<>(ValueData.of(name, (E[]) value, context, comments)); + } + + @SuppressWarnings("unchecked") + @Override + public void encodeToBuffer(ConfigValue value, FriendlyByteBuf buffer) { + E[] values = (E[]) value.get(); + buffer.writeInt(values.length); + for (E e : values) { + buffer.writeEnum(e); + } + } + + @SuppressWarnings("unchecked") + @Override + public Object decodeFromBuffer(ConfigValue value, FriendlyByteBuf buffer) { + int count = buffer.readInt(); + Class type = (Class) value.getValueType().getComponentType(); + E[] enumArray = (E[]) Array.newInstance(type, count); + for (int i = 0; i < count; i++) { + enumArray[i] = buffer.readEnum(type); + } + return enumArray; + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/EnumValue.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/EnumValue.java new file mode 100644 index 0000000..dc29f03 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/EnumValue.java @@ -0,0 +1,45 @@ +package mod.azure.azurelib.common.internal.common.config.value; + +import mod.azure.azurelib.common.internal.common.config.adapter.TypeAdapter; +import mod.azure.azurelib.common.internal.common.config.exception.ConfigValueMissingException; +import mod.azure.azurelib.common.internal.common.config.format.IConfigFormat; +import net.minecraft.network.FriendlyByteBuf; + +public class EnumValue> extends ConfigValue { + + public EnumValue(ValueData valueData) { + super(valueData); + } + + @Override + protected void serialize(IConfigFormat format) { + format.writeEnum(this.getId(), this.get()); + } + + @Override + protected void deserialize(IConfigFormat format) throws ConfigValueMissingException { + this.set(format.readEnum(this.getId(), getValueType())); + } + + public static final class Adapter> extends TypeAdapter { + + @SuppressWarnings("unchecked") + @Override + public ConfigValue serialize(String name, String[] comments, Object value, TypeSerializer serializer, AdapterContext context) throws IllegalAccessException { + return new EnumValue<>(ValueData.of(name, (E) value, context, comments)); + } + + @SuppressWarnings("unchecked") + @Override + public void encodeToBuffer(ConfigValue value, FriendlyByteBuf buffer) { + buffer.writeEnum((E) value.get()); + } + + @SuppressWarnings("unchecked") + @Override + public Object decodeFromBuffer(ConfigValue value, FriendlyByteBuf buffer) { + Class type = (Class) value.getValueType(); + return buffer.readEnum(type); + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/FloatArrayValue.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/FloatArrayValue.java new file mode 100644 index 0000000..bf2afa5 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/FloatArrayValue.java @@ -0,0 +1,110 @@ +package mod.azure.azurelib.common.internal.common.config.value; + +import mod.azure.azurelib.common.internal.common.config.ConfigUtils; +import mod.azure.azurelib.common.internal.common.config.Configurable; +import mod.azure.azurelib.common.internal.common.config.format.IConfigFormat; +import mod.azure.azurelib.common.internal.common.config.adapter.TypeAdapter; +import mod.azure.azurelib.common.internal.common.config.exception.ConfigValueMissingException; +import net.minecraft.network.FriendlyByteBuf; + +import java.lang.reflect.Field; +import java.util.Arrays; + +public class FloatArrayValue extends ConfigValue implements ArrayValue { + + private boolean fixedSize; + private DecimalValue.Range range; + + public FloatArrayValue(ValueData valueData) { + super(valueData); + } + + @Override + public boolean isFixedSize() { + return fixedSize; + } + + @Override + protected void readFieldData(Field field) { + this.fixedSize = field.getAnnotation(Configurable.FixedSize.class) != null; + Configurable.DecimalRange decimalRange = field.getAnnotation(Configurable.DecimalRange.class); + this.range = decimalRange != null ? DecimalValue.Range.newBoundedRange(decimalRange.min(), decimalRange.max()) : DecimalValue.Range.unboundedFloat(); + } + + @Override + protected float[] getCorrectedValue(float[] in) { + if (this.fixedSize) { + float[] defaultArray = this.valueData.getDefaultValue(); + if (in.length != defaultArray.length) { + ConfigUtils.logArraySizeCorrectedMessage(this.getId(), Arrays.toString(in), Arrays.toString(defaultArray)); + in = defaultArray; + } + } + if (this.range == null) + return in; + for (int i = 0; i < in.length; i++) { + float value = in[i]; + if (!this.range.isWithin(value)) { + float corrected = this.range.clamp(value); + ConfigUtils.logCorrectedMessage(this.getId() + "[" + i + "]", value, corrected); + in[i] = corrected; + } + } + return in; + } + + @Override + protected void serialize(IConfigFormat format) { + format.writeFloatArray(this.getId(), this.get()); + } + + @Override + protected void deserialize(IConfigFormat format) throws ConfigValueMissingException { + this.set(format.readFloatArray(this.getId())); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("["); + float[] floats = this.get(); + for (int i = 0; i < floats.length; i++) { + builder.append(this.elementToString(floats[i])); + if (i < floats.length - 1) { + builder.append(","); + } + } + builder.append("]"); + return builder.toString(); + } + + public DecimalValue.Range getRange() { + return range; + } + + public static final class Adapter extends TypeAdapter { + + @Override + public void encodeToBuffer(ConfigValue value, FriendlyByteBuf buffer) { + float[] arr = (float[]) value.get(); + buffer.writeInt(arr.length); + for (float v : arr) { + buffer.writeFloat(v); + } + } + + @Override + public Object decodeFromBuffer(ConfigValue value, FriendlyByteBuf buffer) { + float[] arr = new float[buffer.readInt()]; + for (int i = 0; i < arr.length; i++) { + arr[i] = buffer.readFloat(); + } + return arr; + } + + @Override + public ConfigValue serialize(String name, String[] comments, Object value, TypeSerializer serializer, AdapterContext context) throws IllegalAccessException { + return new FloatArrayValue(ValueData.of(name, (float[]) value, context, comments)); + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/FloatValue.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/FloatValue.java new file mode 100644 index 0000000..337e907 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/FloatValue.java @@ -0,0 +1,61 @@ +package mod.azure.azurelib.common.internal.common.config.value; + +import mod.azure.azurelib.common.internal.common.config.ConfigUtils; +import mod.azure.azurelib.common.internal.common.config.adapter.TypeAdapter; +import mod.azure.azurelib.common.internal.common.config.exception.ConfigValueMissingException; +import mod.azure.azurelib.common.internal.common.config.format.IConfigFormat; +import net.minecraft.network.FriendlyByteBuf; + +import java.lang.reflect.Field; + +public class FloatValue extends DecimalValue { + + public FloatValue(ValueData valueData) { + super(valueData, Range.unboundedFloat()); + } + + @Override + public Float getCorrectedValue(Float in) { + if (this.range == null) + return in; + if (!this.range.isWithin(in)) { + float corrected = this.range.clamp(in); + ConfigUtils.logCorrectedMessage(this.getId(), in, corrected); + return corrected; + } + return in; + } + + @Override + protected void serialize(IConfigFormat format) { + format.writeFloat(this.getId(), this.get()); + } + + @Override + protected void deserialize(IConfigFormat format) throws ConfigValueMissingException { + this.set(format.readFloat(this.getId())); + } + + public static final class Adapter extends TypeAdapter { + + @Override + public ConfigValue serialize(String name, String[] comments, Object value, TypeSerializer serializer, AdapterContext context) throws IllegalAccessException { + return new FloatValue(ValueData.of(name, (float) value, context, comments)); + } + + @Override + public void encodeToBuffer(ConfigValue value, FriendlyByteBuf buffer) { + buffer.writeFloat((Float) value.get()); + } + + @Override + public Object decodeFromBuffer(ConfigValue value, FriendlyByteBuf buffer) { + return buffer.readFloat(); + } + + @Override + public void setFieldValue(Field field, Object instance, Object value) throws IllegalAccessException { + field.setFloat(instance, (Float) value); + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/IDescriptionProvider.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/IDescriptionProvider.java new file mode 100644 index 0000000..e60e679 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/IDescriptionProvider.java @@ -0,0 +1,6 @@ +package mod.azure.azurelib.common.internal.common.config.value; + +public interface IDescriptionProvider { + + String[] getDescription(); +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/IntArrayValue.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/IntArrayValue.java new file mode 100644 index 0000000..cb741f5 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/IntArrayValue.java @@ -0,0 +1,110 @@ +package mod.azure.azurelib.common.internal.common.config.value; + +import mod.azure.azurelib.common.internal.common.config.ConfigUtils; +import mod.azure.azurelib.common.internal.common.config.Configurable; +import mod.azure.azurelib.common.internal.common.config.format.IConfigFormat; +import mod.azure.azurelib.common.internal.common.config.adapter.TypeAdapter; +import mod.azure.azurelib.common.internal.common.config.exception.ConfigValueMissingException; +import net.minecraft.network.FriendlyByteBuf; + +import java.lang.reflect.Field; +import java.util.Arrays; + +public class IntArrayValue extends ConfigValue implements ArrayValue { + + private boolean fixedSize; + private IntegerValue.Range range; + + public IntArrayValue(ValueData valueData) { + super(valueData); + } + + @Override + public boolean isFixedSize() { + return fixedSize; + } + + @Override + protected void readFieldData(Field field) { + this.fixedSize = field.getAnnotation(Configurable.FixedSize.class) != null; + Configurable.Range intRange = field.getAnnotation(Configurable.Range.class); + this.range = intRange != null ? IntegerValue.Range.newBoundedRange(intRange.min(), intRange.max()) : IntegerValue.Range.unboundedInt(); + } + + @Override + protected int[] getCorrectedValue(int[] in) { + if (this.fixedSize) { + int[] defaultArray = this.valueData.getDefaultValue(); + if (in.length != defaultArray.length) { + ConfigUtils.logArraySizeCorrectedMessage(this.getId(), Arrays.toString(in), Arrays.toString(defaultArray)); + in = defaultArray; + } + } + if (this.range == null) + return in; + for (int i = 0; i < in.length; i++) { + int value = in[i]; + if (!this.range.isWithin(value)) { + int corrected = this.range.clamp(value); + ConfigUtils.logCorrectedMessage(this.getId() + "[" + i + "]", value, corrected); + in[i] = corrected; + } + } + return in; + } + + @Override + public void serialize(IConfigFormat format) { + format.writeIntArray(this.getId(), this.get()); + } + + @Override + protected void deserialize(IConfigFormat format) throws ConfigValueMissingException { + this.set(format.readIntArray(this.getId())); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("["); + int[] ints = this.get(); + for (int i = 0; i < ints.length; i++) { + builder.append(this.elementToString(ints[i])); + if (i < ints.length - 1) { + builder.append(","); + } + } + builder.append("]"); + return builder.toString(); + } + + public IntegerValue.Range getRange() { + return range; + } + + public static final class Adapter extends TypeAdapter { + + @Override + public void encodeToBuffer(ConfigValue value, FriendlyByteBuf buffer) { + int[] arr = (int[]) value.get(); + buffer.writeInt(arr.length); + for (int v : arr) { + buffer.writeInt(v); + } + } + + @Override + public Object decodeFromBuffer(ConfigValue value, FriendlyByteBuf buffer) { + int[] arr = new int[buffer.readInt()]; + for (int i = 0; i < arr.length; i++) { + arr[i] = buffer.readInt(); + } + return arr; + } + + @Override + public ConfigValue serialize(String name, String[] comments, Object value, TypeSerializer serializer, AdapterContext context) throws IllegalAccessException { + return new IntArrayValue(ValueData.of(name, (int[]) value, context, comments)); + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/IntValue.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/IntValue.java new file mode 100644 index 0000000..365953e --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/IntValue.java @@ -0,0 +1,61 @@ +package mod.azure.azurelib.common.internal.common.config.value; + +import mod.azure.azurelib.common.internal.common.config.ConfigUtils; +import mod.azure.azurelib.common.internal.common.config.adapter.TypeAdapter; +import mod.azure.azurelib.common.internal.common.config.exception.ConfigValueMissingException; +import mod.azure.azurelib.common.internal.common.config.format.IConfigFormat; +import net.minecraft.network.FriendlyByteBuf; + +import java.lang.reflect.Field; + +public final class IntValue extends IntegerValue { + + public IntValue(ValueData valueData) { + super(valueData, Range.unboundedInt()); + } + + @Override + public Integer getCorrectedValue(Integer in) { + if (this.range == null) + return in; + if (!this.range.isWithin(in)) { + int corrected = this.range.clamp(in); + ConfigUtils.logCorrectedMessage(this.getId(), in, corrected); + return corrected; + } + return in; + } + + @Override + public void serialize(IConfigFormat format) { + format.writeInt(this.getId(), this.get()); + } + + @Override + public void deserialize(IConfigFormat format) throws ConfigValueMissingException { + this.set(format.readInt(this.getId())); + } + + public static final class Adapter extends TypeAdapter { + + @Override + public ConfigValue serialize(String name, String[] comments, Object value, TypeSerializer serializer, AdapterContext context) { + return new IntValue(ValueData.of(name, (int) value, context, comments)); + } + + @Override + public void encodeToBuffer(ConfigValue value, FriendlyByteBuf buffer) { + buffer.writeInt((Integer) value.get()); + } + + @Override + public Object decodeFromBuffer(ConfigValue value, FriendlyByteBuf buffer) { + return buffer.readInt(); + } + + @Override + public void setFieldValue(Field field, Object instance, Object value) throws IllegalAccessException { + field.setInt(instance, (Integer) value); + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/IntegerValue.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/IntegerValue.java new file mode 100644 index 0000000..ccbe1ad --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/IntegerValue.java @@ -0,0 +1,77 @@ +package mod.azure.azurelib.common.internal.common.config.value; + +import mod.azure.azurelib.common.internal.common.config.Configurable; + +import java.lang.reflect.Field; +import java.util.Objects; + +public abstract class IntegerValue extends ConfigValue { + + protected Range range; + + public IntegerValue(ValueData valueData, Range range) { + super(valueData); + this.range = Objects.requireNonNull(range); + } + + @Override + protected void readFieldData(Field field) { + super.readFieldData(field); + Configurable.Range intRange = field.getAnnotation(Configurable.Range.class); + if (intRange != null) { + this.range = Range.newBoundedRange(intRange.min(), intRange.max()); + } + } + + @Override + public abstract N getCorrectedValue(N in); + + public Range getRange() { + return range; + } + + public static final class Range { + + private final long min, max; + + private Range(long min, long max) { + this.min = min; + this.max = max; + } + + public static Range newBoundedRange(long min, long max) { + if (min > max) { + throw new IllegalArgumentException(String.format("Invalid number range: Min value (%d) cannot be bigger than max value (%d)", min, max)); + } + return new Range(min, max); + } + + public static Range unboundedLong() { + return newBoundedRange(Long.MIN_VALUE, Long.MAX_VALUE); + } + + public static Range unboundedInt() { + return newBoundedRange(Integer.MIN_VALUE, Integer.MAX_VALUE); + } + + public boolean isWithin(long number) { + return number >= min && number <= max; + } + + public long min() { + return this.min; + } + + public long max() { + return this.max; + } + + public long clamp(long in) { + return Math.min(max, Math.max(min, in)); + } + + public int clamp(int in) { + return (int) Math.min(max, Math.max(min, in)); + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/LongArrayValue.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/LongArrayValue.java new file mode 100644 index 0000000..da0e5eb --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/LongArrayValue.java @@ -0,0 +1,110 @@ +package mod.azure.azurelib.common.internal.common.config.value; + +import mod.azure.azurelib.common.internal.common.config.ConfigUtils; +import mod.azure.azurelib.common.internal.common.config.Configurable; +import mod.azure.azurelib.common.internal.common.config.format.IConfigFormat; +import mod.azure.azurelib.common.internal.common.config.adapter.TypeAdapter; +import mod.azure.azurelib.common.internal.common.config.exception.ConfigValueMissingException; +import net.minecraft.network.FriendlyByteBuf; + +import java.lang.reflect.Field; +import java.util.Arrays; + +public class LongArrayValue extends ConfigValue implements ArrayValue { + + private boolean fixedSize; + private IntegerValue.Range range; + + public LongArrayValue(ValueData valueData) { + super(valueData); + } + + @Override + public boolean isFixedSize() { + return fixedSize; + } + + @Override + protected void readFieldData(Field field) { + this.fixedSize = field.getAnnotation(Configurable.FixedSize.class) != null; + Configurable.Range intRange = field.getAnnotation(Configurable.Range.class); + this.range = intRange != null ? IntegerValue.Range.newBoundedRange(intRange.min(), intRange.max()) : IntegerValue.Range.unboundedLong(); + } + + @Override + protected long[] getCorrectedValue(long[] in) { + if (this.fixedSize) { + long[] defaultArray = this.valueData.getDefaultValue(); + if (in.length != defaultArray.length) { + ConfigUtils.logArraySizeCorrectedMessage(this.getId(), Arrays.toString(in), Arrays.toString(defaultArray)); + in = defaultArray; + } + } + if (this.range == null) + return in; + for (int i = 0; i < in.length; i++) { + long value = in[i]; + if (!this.range.isWithin(value)) { + long corrected = this.range.clamp(value); + ConfigUtils.logCorrectedMessage(this.getId() + "[" + i + "]", value, corrected); + in[i] = corrected; + } + } + return in; + } + + @Override + protected void serialize(IConfigFormat format) { + format.writeLongArray(this.getId(), this.get()); + } + + @Override + protected void deserialize(IConfigFormat format) throws ConfigValueMissingException { + this.set(format.readLongArray(this.getId())); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("["); + long[] longs = this.get(); + for (int i = 0; i < longs.length; i++) { + builder.append(this.elementToString(longs[i])); + if (i < longs.length - 1) { + builder.append(","); + } + } + builder.append("]"); + return builder.toString(); + } + + public IntegerValue.Range getRange() { + return range; + } + + public static final class Adapter extends TypeAdapter { + + @Override + public void encodeToBuffer(ConfigValue value, FriendlyByteBuf buffer) { + long[] arr = (long[]) value.get(); + buffer.writeInt(arr.length); + for (long v : arr) { + buffer.writeLong(v); + } + } + + @Override + public Object decodeFromBuffer(ConfigValue value, FriendlyByteBuf buffer) { + long[] arr = new long[buffer.readInt()]; + for (int i = 0; i < arr.length; i++) { + arr[i] = buffer.readLong(); + } + return arr; + } + + @Override + public ConfigValue serialize(String name, String[] comments, Object value, TypeSerializer serializer, AdapterContext context) throws IllegalAccessException { + return new LongArrayValue(ValueData.of(name, (long[]) value, context, comments)); + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/LongValue.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/LongValue.java new file mode 100644 index 0000000..5c209ed --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/LongValue.java @@ -0,0 +1,61 @@ +package mod.azure.azurelib.common.internal.common.config.value; + +import mod.azure.azurelib.common.internal.common.config.ConfigUtils; +import mod.azure.azurelib.common.internal.common.config.adapter.TypeAdapter; +import mod.azure.azurelib.common.internal.common.config.exception.ConfigValueMissingException; +import mod.azure.azurelib.common.internal.common.config.format.IConfigFormat; +import net.minecraft.network.FriendlyByteBuf; + +import java.lang.reflect.Field; + +public class LongValue extends IntegerValue { + + public LongValue(ValueData valueData) { + super(valueData, Range.unboundedLong()); + } + + @Override + public Long getCorrectedValue(Long in) { + if (this.range == null) + return in; + if (!this.range.isWithin(in)) { + long corrected = this.range.clamp(in); + ConfigUtils.logCorrectedMessage(this.getId(), in, corrected); + return corrected; + } + return in; + } + + @Override + protected void serialize(IConfigFormat format) { + format.writeLong(this.getId(), this.get()); + } + + @Override + protected void deserialize(IConfigFormat format) throws ConfigValueMissingException { + this.set(format.readLong(this.getId())); + } + + public static final class Adapter extends TypeAdapter { + + @Override + public ConfigValue serialize(String name, String[] comments, Object value, TypeSerializer serializer, AdapterContext context) throws IllegalAccessException { + return new LongValue(ValueData.of(name, (long) value, context, comments)); + } + + @Override + public void encodeToBuffer(ConfigValue value, FriendlyByteBuf buffer) { + buffer.writeLong((Long) value.get()); + } + + @Override + public Object decodeFromBuffer(ConfigValue value, FriendlyByteBuf buffer) { + return buffer.readLong(); + } + + @Override + public void setFieldValue(Field field, Object instance, Object value) throws IllegalAccessException { + field.setLong(instance, (Long) value); + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/ObjectValue.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/ObjectValue.java new file mode 100644 index 0000000..7cf51b5 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/ObjectValue.java @@ -0,0 +1,56 @@ +package mod.azure.azurelib.common.internal.common.config.value; + +import mod.azure.azurelib.common.internal.common.config.format.IConfigFormat; +import mod.azure.azurelib.common.internal.common.config.adapter.TypeAdapter; +import mod.azure.azurelib.common.internal.common.config.exception.ConfigValueMissingException; +import net.minecraft.network.FriendlyByteBuf; + +import java.lang.reflect.Field; +import java.util.Map; + +public class ObjectValue extends ConfigValue>> { + + public ObjectValue(ValueData>> valueData) { + super(valueData); + this.get().values().forEach(value -> value.setParent(this)); + } + + @Override + public void serialize(IConfigFormat format) { + format.writeMap(this.getId(), this.get()); + } + + @Override + protected void deserialize(IConfigFormat format) throws ConfigValueMissingException { + format.readMap(this.getId(), this.get().values()); + } + + @Override + public void setValueValidator(SetValueCallback>> callback) { + throw new UnsupportedOperationException("Cannot attach value validator to Object types!"); + } + + public static final class Adapter extends TypeAdapter { + + @Override + public ConfigValue serialize(String name, String[] comments, Object value, TypeSerializer serializer, AdapterContext context) throws IllegalAccessException { + Class type = value.getClass(); + Map> map = serializer.serialize(type, value); + return new ObjectValue(ValueData.of(name, map, context, comments)); + } + + @Override + public void encodeToBuffer(ConfigValue value, FriendlyByteBuf buffer) { + } + + @Override + public Object decodeFromBuffer(ConfigValue value, FriendlyByteBuf buffer) { + return null; + } + + @Override + public void setFieldValue(Field field, Object instance, Object value) throws IllegalAccessException { + // Do not set anything, keep existing instance + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/StringArrayValue.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/StringArrayValue.java new file mode 100644 index 0000000..65e38a8 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/StringArrayValue.java @@ -0,0 +1,122 @@ +package mod.azure.azurelib.common.internal.common.config.value; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.regex.Pattern; + +import mod.azure.azurelib.common.internal.common.config.ConfigUtils; +import mod.azure.azurelib.common.internal.common.config.Configurable; +import mod.azure.azurelib.common.internal.common.config.format.IConfigFormat; +import mod.azure.azurelib.common.internal.common.config.io.ConfigIO; +import mod.azure.azurelib.common.internal.common.AzureLib; +import mod.azure.azurelib.common.internal.common.config.adapter.TypeAdapter; +import mod.azure.azurelib.common.internal.common.config.exception.ConfigValueMissingException; +import net.minecraft.network.FriendlyByteBuf; + +public class StringArrayValue extends ConfigValue implements ArrayValue { + + private boolean fixedSize; + private Pattern pattern; + private String defaultElementValue = ""; + + public StringArrayValue(ValueData valueData) { + super(valueData); + } + + @Override + public boolean isFixedSize() { + return fixedSize; + } + + @Override + protected void readFieldData(Field field) { + this.fixedSize = field.getAnnotation(Configurable.FixedSize.class) != null; + Configurable.StringPattern stringPattern = field.getAnnotation(Configurable.StringPattern.class); + if (stringPattern != null) { + String value = stringPattern.value(); + this.defaultElementValue = stringPattern.defaultValue(); + try { + this.pattern = Pattern.compile(value, stringPattern.flags()); + } catch (IllegalArgumentException e) { + AzureLib.LOGGER.error(ConfigIO.MARKER, "Invalid @StringPattern value for {} field - {}", this.getId(), e); + } + if (this.pattern != null && !this.pattern.matcher(this.defaultElementValue).matches()) { + throw new IllegalArgumentException(String.format("Invalid config default value '%s' for field '%s' - does not match required pattern \\%s\\", this.defaultElementValue, this.getId(), this.pattern.toString())); + } + } + } + + @Override + protected String[] getCorrectedValue(String[] in) { + String[] defaultArray = this.valueData.getDefaultValue(); + if (this.fixedSize && (in.length != defaultArray.length)) { + ConfigUtils.logArraySizeCorrectedMessage(this.getId(), Arrays.toString(in), Arrays.toString(defaultArray)); + return defaultArray; + } + if (this.pattern != null) { + for (int i = 0; i < in.length; i++) { + String string = in[i]; + if (!this.pattern.matcher(string).matches()) { + ConfigUtils.logCorrectedMessage(this.getId() + "[" + i + "]", string, this.defaultElementValue); + in[i] = this.defaultElementValue; + } + } + } + return in; + } + + public String getDefaultElementValue() { + return defaultElementValue; + } + + @Override + protected void serialize(IConfigFormat format) { + format.writeStringArray(this.getId(), this.get()); + } + + @Override + protected void deserialize(IConfigFormat format) throws ConfigValueMissingException { + this.set(format.readStringArray(this.getId())); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("["); + String[] strings = this.get(); + for (int i = 0; i < strings.length; i++) { + builder.append(this.elementToString(strings[i])); + if (i < strings.length - 1) { + builder.append(","); + } + } + builder.append("]"); + return builder.toString(); + } + + public static final class Adapter extends TypeAdapter { + + @Override + public void encodeToBuffer(ConfigValue value, FriendlyByteBuf buffer) { + String[] arr = (String[]) value.get(); + buffer.writeInt(arr.length); + for (String v : arr) { + buffer.writeUtf(v); + } + } + + @Override + public Object decodeFromBuffer(ConfigValue value, FriendlyByteBuf buffer) { + String[] arr = new String[buffer.readInt()]; + for (int i = 0; i < arr.length; i++) { + arr[i] = buffer.readUtf(); + } + return arr; + } + + @Override + public ConfigValue serialize(String name, String[] comments, Object value, TypeSerializer serializer, AdapterContext context) throws IllegalAccessException { + return new StringArrayValue(ValueData.of(name, (String[]) value, context, comments)); + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/StringValue.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/StringValue.java new file mode 100644 index 0000000..4847fbc --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/StringValue.java @@ -0,0 +1,86 @@ +package mod.azure.azurelib.common.internal.common.config.value; + +import java.lang.reflect.Field; +import java.util.regex.Pattern; + +import mod.azure.azurelib.common.internal.common.config.ConfigUtils; +import mod.azure.azurelib.common.internal.common.config.Configurable; +import mod.azure.azurelib.common.internal.common.config.format.IConfigFormat; +import mod.azure.azurelib.common.internal.common.config.io.ConfigIO; +import mod.azure.azurelib.common.internal.common.AzureLib; +import mod.azure.azurelib.common.internal.common.config.adapter.TypeAdapter; +import mod.azure.azurelib.common.internal.common.config.exception.ConfigValueMissingException; +import net.minecraft.network.FriendlyByteBuf; + +public class StringValue extends ConfigValue { + + private Pattern pattern; + private String descriptor; + + public StringValue(ValueData valueData) { + super(valueData); + } + + @Override + protected void readFieldData(Field field) { + Configurable.StringPattern stringPattern = field.getAnnotation(Configurable.StringPattern.class); + if (stringPattern != null) { + String value = stringPattern.value(); + this.descriptor = stringPattern.errorDescriptor().isEmpty() ? null : stringPattern.errorDescriptor(); + try { + this.pattern = Pattern.compile(value, stringPattern.flags()); + } catch (IllegalArgumentException e) { + AzureLib.LOGGER.error(ConfigIO.MARKER, "Invalid @StringPattern value for {} field - {}", this.getId(), e); + } + } + } + + @Override + protected String getCorrectedValue(String in) { + if (this.pattern != null && (!this.pattern.matcher(in).matches())) { + String defaultValue = this.valueData.getDefaultValue(); + if (!this.pattern.matcher(defaultValue).matches()) { + throw new IllegalArgumentException(String.format("Invalid config default value '%s' for field '%s' - does not match required pattern \\%s\\", defaultValue, this.getId(), this.pattern.toString())); + } + ConfigUtils.logCorrectedMessage(this.getId(), in, defaultValue); + return defaultValue; + } + return in; + } + + @Override + protected void serialize(IConfigFormat format) { + format.writeString(this.getId(), this.get()); + } + + @Override + protected void deserialize(IConfigFormat format) throws ConfigValueMissingException { + this.set(format.readString(this.getId())); + } + + public Pattern getPattern() { + return pattern; + } + + public String getErrorDescriptor() { + return descriptor; + } + + public static final class Adapter extends TypeAdapter { + + @Override + public void encodeToBuffer(ConfigValue value, FriendlyByteBuf buffer) { + buffer.writeUtf((String) value.get()); + } + + @Override + public Object decodeFromBuffer(ConfigValue value, FriendlyByteBuf buffer) { + return buffer.readUtf(); + } + + @Override + public ConfigValue serialize(String name, String[] comments, Object value, TypeSerializer serializer, AdapterContext context) throws IllegalAccessException { + return new StringValue(ValueData.of(name, (String) value, context, comments)); + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/ValueData.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/ValueData.java new file mode 100644 index 0000000..e534245 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/config/value/ValueData.java @@ -0,0 +1,63 @@ +package mod.azure.azurelib.common.internal.common.config.value; + +import java.util.Objects; + +import mod.azure.azurelib.common.internal.common.config.adapter.TypeAdapter; +import org.jetbrains.annotations.Nullable; + +public final class ValueData implements IDescriptionProvider { + + private final String id; + private final String[] tooltip; + private final T defaultValue; + private final TypeAdapter.AdapterContext context; + private final Class valueType; + @Nullable + private ConfigValue parent; + + private ValueData(String id, String[] tooltip, T defaultValue, TypeAdapter.AdapterContext context) { + this.id = id; + this.tooltip = tooltip; + this.defaultValue = defaultValue; + this.context = context; + this.valueType = (Class) defaultValue.getClass(); + } + + public static ValueData of(String id, V value, TypeAdapter.AdapterContext setter, String... comments) { + return new ValueData<>(id, comments, Objects.requireNonNull(value), setter); + } + + public String getId() { + return id; + } + + @Override + public String[] getDescription() { + return tooltip; + } + + public T getDefaultValue() { + return defaultValue; + } + + public void setValueToMemory(Object value) { + this.context.setFieldValue(value); + } + + public void setParent(@Nullable ConfigValue parent) { + this.parent = parent; + } + + @Nullable + public ConfigValue getParent() { + return parent; + } + + public TypeAdapter.AdapterContext getContext() { + return context; + } + + public Class getValueType() { + return valueType; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/constant/DataTickets.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/constant/DataTickets.java new file mode 100644 index 0000000..8668a11 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/constant/DataTickets.java @@ -0,0 +1,72 @@ +package mod.azure.azurelib.common.internal.common.constant; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import mod.azure.azurelib.common.internal.client.model.data.EntityModelData; +import mod.azure.azurelib.common.internal.common.core.object.DataTicket; +import mod.azure.azurelib.common.internal.common.util.AzureLibUtil; +import mod.azure.azurelib.common.internal.common.AzureLib; +import org.jetbrains.annotations.Nullable; + +import mod.azure.azurelib.common.internal.common.network.SerializableDataTicket; +import net.minecraft.core.Direction; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.item.ItemDisplayContext; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.BlockEntity; + +/** + * Stores the default (builtin) {@link DataTicket DataTickets} used in AzureLib.
+ * Additionally handles registration of {@link SerializableDataTicket SerializableDataTickets} + */ +public final class DataTickets { + private static final Map> SERIALIZABLE_TICKETS = new ConcurrentHashMap<>(); + + // Builtin tickets + // These tickets are used by AzureLib by default, usually added in by the GeoRenderer for use in animations + public static final DataTicket BLOCK_ENTITY = new DataTicket<>("block_entity", BlockEntity.class); + public static final DataTicket ITEMSTACK = new DataTicket<>("itemstack", ItemStack.class); + public static final DataTicket ENTITY = new DataTicket<>("entity", Entity.class); + public static final DataTicket EQUIPMENT_SLOT = new DataTicket<>("equipment_slot", EquipmentSlot.class); + public static final DataTicket ENTITY_MODEL_DATA = new DataTicket<>("entity_model_data", EntityModelData.class); + public static final DataTicket TICK = new DataTicket<>("tick", Double.class); + public static final DataTicket ITEM_RENDER_PERSPECTIVE = new DataTicket<>("item_render_perspective", ItemDisplayContext.class); + + // Builtin serializable tickets + // These are not used anywhere by default, but are provided as examples + // and for ease of use + public static final SerializableDataTicket ANIM_STATE = AzureLibUtil.addDataTicket(SerializableDataTicket.ofInt(AzureLib.modResource("anim_state"))); + public static final SerializableDataTicket ANIM = AzureLibUtil.addDataTicket(SerializableDataTicket.ofString(AzureLib.modResource("anim"))); + public static final SerializableDataTicket USE_TICKS = AzureLibUtil.addDataTicket(SerializableDataTicket.ofInt(AzureLib.modResource("use_ticks"))); + public static final SerializableDataTicket ACTIVE = AzureLibUtil.addDataTicket(SerializableDataTicket.ofBoolean(AzureLib.modResource("active"))); + public static final SerializableDataTicket OPEN = AzureLibUtil.addDataTicket(SerializableDataTicket.ofBoolean(AzureLib.modResource("open"))); + public static final SerializableDataTicket CLOSED = AzureLibUtil.addDataTicket(SerializableDataTicket.ofBoolean(AzureLib.modResource("closed"))); + public static final SerializableDataTicket DIRECTION = AzureLibUtil.addDataTicket(SerializableDataTicket.ofEnum(AzureLib.modResource("direction"), Direction.class)); + + @Nullable + public static SerializableDataTicket byName(String id) { + return SERIALIZABLE_TICKETS.getOrDefault(id, null); + } + + /** + * Register a {@link SerializableDataTicket} with AzureLib for handling custom data transmission.
+ * It is recommended you don't call this directly, and instead call it via {@link AzureLibUtil#addDataTicket} + * @param ticket The SerializableDataTicket instance to register + * @return The registered instance + */ + public static SerializableDataTicket registerSerializable(SerializableDataTicket ticket) { + SerializableDataTicket existingTicket = SERIALIZABLE_TICKETS.putIfAbsent(ticket.id(), ticket); + + if (existingTicket != null) + AzureLib.LOGGER.error("Duplicate SerializableDataTicket registered! This will cause issues. Existing: {}, New: {}", existingTicket.id(), ticket.id()); + + return ticket; + } + + private DataTickets() { + throw new UnsupportedOperationException(); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/constant/DefaultAnimations.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/constant/DefaultAnimations.java new file mode 100644 index 0000000..b09e26c --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/constant/DefaultAnimations.java @@ -0,0 +1,208 @@ +package mod.azure.azurelib.common.internal.common.constant; + +import mod.azure.azurelib.common.api.common.animatable.GeoBlockEntity; +import mod.azure.azurelib.common.api.common.animatable.GeoEntity; +import mod.azure.azurelib.common.api.common.animatable.GeoItem; +import mod.azure.azurelib.common.api.common.animatable.GeoReplacedEntity; +import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable; +import mod.azure.azurelib.common.internal.common.core.animation.AnimationController; +import mod.azure.azurelib.common.internal.common.core.animation.AnimationState; +import mod.azure.azurelib.common.internal.common.core.animation.RawAnimation; +import mod.azure.azurelib.common.internal.common.core.object.PlayState; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; + +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * Optionally usable class that holds constants for recommended animation paths.
+ * Using these won't affect much, but it may help keep some consistency in animation namings.
+ * Additionally, it encourages use of cached {@link RawAnimation RawAnimations}, to reduce overheads. + */ +public final class DefaultAnimations { + public static final RawAnimation ITEM_ON_USE = RawAnimation.begin().thenPlay("item.use"); + + public static final RawAnimation IDLE = RawAnimation.begin().thenLoop("misc.idle"); + public static final RawAnimation LIVING = RawAnimation.begin().thenLoop("misc.living"); + public static final RawAnimation SPAWN = RawAnimation.begin().thenPlay("misc.spawn"); + public static final RawAnimation INTERACT = RawAnimation.begin().thenPlay("misc.interact"); + public static final RawAnimation DEPLOY = RawAnimation.begin().thenPlay("misc.deploy"); + public static final RawAnimation REST = RawAnimation.begin().thenPlay("misc.rest"); + public static final RawAnimation SIT = RawAnimation.begin().thenPlayAndHold("misc.sit"); + + public static final RawAnimation WALK = RawAnimation.begin().thenLoop("move.walk"); + public static final RawAnimation SWIM = RawAnimation.begin().thenLoop("move.swim"); + public static final RawAnimation RUN = RawAnimation.begin().thenLoop("move.run"); + public static final RawAnimation DRIVE = RawAnimation.begin().thenLoop("move.drive"); + public static final RawAnimation FLY = RawAnimation.begin().thenLoop("move.fly"); + public static final RawAnimation CRAWL = RawAnimation.begin().thenLoop("move.crawl"); + public static final RawAnimation JUMP = RawAnimation.begin().thenPlay("move.jump"); + public static final RawAnimation SNEAK = RawAnimation.begin().thenLoop("move.sneak"); + + public static final RawAnimation ATTACK_CAST = RawAnimation.begin().thenPlay("attack.cast"); + public static final RawAnimation ATTACK_SWING = RawAnimation.begin().thenPlay("attack.swing"); + public static final RawAnimation ATTACK_THROW = RawAnimation.begin().thenPlay("attack.throw"); + public static final RawAnimation ATTACK_BITE = RawAnimation.begin().thenPlay("attack.bite"); + public static final RawAnimation ATTACK_SLAM = RawAnimation.begin().thenPlay("attack.slam"); + public static final RawAnimation ATTACK_STOMP = RawAnimation.begin().thenPlay("attack.stomp"); + public static final RawAnimation ATTACK_STRIKE = RawAnimation.begin().thenPlay("attack.strike"); + public static final RawAnimation ATTACK_FLYING_ATTACK = RawAnimation.begin().thenPlay("attack.flying_attack"); + public static final RawAnimation ATTACK_SHOOT = RawAnimation.begin().thenPlay("attack.shoot"); + public static final RawAnimation ATTACK_BLOCK = RawAnimation.begin().thenPlay("attack.block"); + public static final RawAnimation ATTACK_CHARGE = RawAnimation.begin().thenPlay("attack.charge"); + public static final RawAnimation ATTACK_CHARGE_END = RawAnimation.begin().thenPlay("attack.charge_end"); + public static final RawAnimation ATTACK_POWERUP = RawAnimation.begin().thenPlay("attack.powerup"); + + /** + * A basic predicate-based {@link AnimationController} implementation.
+ * Provide an {@code option A} {@link RawAnimation animation} and an {@code option B} animation, and use the predicate to determine which to play.
+ * Outcome table: + *
  true  -> Animation Option A
+	 * false -> Animation Option B
+	 * null  -> Stop Controller
+ */ + public static AnimationController basicPredicateController(T animatable, RawAnimation optionA, RawAnimation optionB, BiFunction, Boolean> predicate) { + return new AnimationController<>(animatable, "Generic", 10, state -> { + Boolean result = predicate.apply(animatable, state); + + if (result == null) + return PlayState.STOP; + + return state.setAndContinue(result ? optionA : optionB); + }); + } + + /** + * Generic {@link DefaultAnimations#LIVING living} controller.
+ * Continuously plays the living animation + */ + public static AnimationController genericLivingController(T animatable) { + return new AnimationController<>(animatable, "Living", 10, state -> state.setAndContinue(LIVING)); + } + + /** + * Generic {@link DefaultAnimations#IDLE idle} controller.
+ * Continuously plays the idle animation + */ + public static AnimationController genericIdleController(T animatable) { + return new AnimationController(animatable, "Idle", 10, state -> state.setAndContinue(IDLE)); + } + + /** + * Generic {@link DefaultAnimations#SPAWN spawn} controller.
+ * Plays the spawn animation as long as the current {@link GeoAnimatable#getTick tick} of the animatable is less than or equal to the value provided in {@code ticks}.
+ * For the {@code objectSupplier}, provide the relevant object for the animatable being animated. + * Recommended: + *
    + *
  • {@link GeoEntity GeoEntity}: state -> animatable
  • + *
  • {@link GeoBlockEntity GeoBlockEntity}: state -> animatable
  • + *
  • {@link GeoReplacedEntity GeoReplacedEntity}: state -> state.getData(DataTickets.ENTITY)
  • + *
  • {@link GeoItem GeoItem}: state -> state.getData(DataTickets.ITEMSTACK)
  • + *
  • {@code GeoArmor}: state -> state.getData(DataTickets.ENTITY)
  • + *
+ * @param animatable The animatable the animation is for + * @param objectSupplier The supplier of the associated object for the {@link GeoAnimatable#getTick} call + * @param ticks The number of ticks the animation should run for. After this value is surpassed, the animation will no longer play + */ + public static AnimationController getSpawnController(T animatable, Function, Object> objectSupplier, int ticks) { + return new AnimationController(animatable, "Spawn", 0, state -> { + if (animatable.getTick(objectSupplier.apply(state)) <= ticks) + return state.setAndContinue(DefaultAnimations.SPAWN); + + return PlayState.STOP; + }); + } + + /** + * Generic {@link DefaultAnimations#WALK walk} controller.
+ * Will play the walk animation if the animatable is considered moving, or stop if not. + */ + public static AnimationController genericWalkController(T animatable) { + return new AnimationController(animatable, "Walk", 0, state -> { + if (state.isMoving()) + return state.setAndContinue(WALK); + + return PlayState.STOP; + }); + } + + /** + * Generic attack controller.
+ * Plays an attack animation if the animatable is {@link net.minecraft.world.entity.LivingEntity#swinging}.
+ * Resets the animation each time it stops, ready for the next swing + * @param animatable The entity that should swing + * @param attackAnimation The attack animation to play (E.G. swipe, strike, stomp, swing, etc) + * @return A new {@link AnimationController} instance to use + */ + public static AnimationController genericAttackAnimation(T animatable, RawAnimation attackAnimation) { + return new AnimationController<>(animatable, "Attack", 5, state -> { + if (animatable.swinging) + return state.setAndContinue(attackAnimation); + + state.getController().forceAnimationReset(); + + return PlayState.STOP; + }); + } + + /** + * Generic {@link DefaultAnimations#WALK walk} + {@link DefaultAnimations#IDLE idle} controller.
+ * Will play the walk animation if the animatable is considered moving, or idle if not + */ + public static AnimationController genericWalkIdleController(T animatable) { + return new AnimationController<>(animatable, "Walk/Idle", 0, state -> state.setAndContinue(state.isMoving() ? WALK : IDLE)); + } + + /** + * Generic {@link DefaultAnimations#SWIM swim} controller.
+ * Will play the swim animation if the animatable is considered moving, or stop if not. + */ + public static AnimationController genericSwimController(T entity) { + return new AnimationController<>(entity, "Swim", 0, state -> { + if (state.isMoving()) + return state.setAndContinue(SWIM); + + return PlayState.STOP; + }); + } + + /** + * Generic {@link DefaultAnimations#SWIM swim} + {@link DefaultAnimations#IDLE idle} controller.
+ * Will play the swim animation if the animatable is considered moving, or idle if not + */ + public static AnimationController genericSwimIdleController(T animatable) { + return new AnimationController<>(animatable, "Swim/Idle", 0, state -> state.setAndContinue(state.isMoving() ? SWIM : IDLE)); + } + + /** + * Generic {@link DefaultAnimations#FLY walk} controller.
+ * Will play the fly animation if the animatable is considered moving, or stop if not. + */ + public static AnimationController genericFlyController(T animatable) { + return new AnimationController<>(animatable, "Fly", 0, state -> state.setAndContinue(FLY)); + } + + /** + * Generic {@link DefaultAnimations#FLY walk} + {@link DefaultAnimations#IDLE idle} controller.
+ * Will play the walk animation if the animatable is considered moving, or idle if not + */ + public static AnimationController genericFlyIdleController(T animatable) { + return new AnimationController<>(animatable, "Fly/Idle", 0, state -> state.setAndContinue(state.isMoving() ? FLY : IDLE)); + } + + /** + * Generic {@link DefaultAnimations#WALK walk} + {@link DefaultAnimations#RUN run} + {@link DefaultAnimations#IDLE idle} controller.
+ * If the entity is considered moving, will either walk or run depending on the {@link Entity#isSprinting()} method, otherwise it will idle + */ + public static AnimationController genericWalkRunIdleController(T entity) { + return new AnimationController<>(entity, "Walk/Run/Idle", 0, state -> { + if (state.isMoving()) { + return state.setAndContinue(entity.isSprinting() ? RUN : WALK); + } + else { + return state.setAndContinue(IDLE); + } + }); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animatable/GeoAnimatable.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animatable/GeoAnimatable.java new file mode 100644 index 0000000..47a90a2 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animatable/GeoAnimatable.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2020. + * Author: Bernie G. (Gecko) + */ +package mod.azure.azurelib.common.internal.common.core.animatable; + +import mod.azure.azurelib.common.internal.common.core.animation.AnimatableManager; +import mod.azure.azurelib.common.internal.common.core.animation.AnimationController; +import mod.azure.azurelib.common.internal.common.core.animation.AnimationProcessor; +import org.jetbrains.annotations.Nullable; + +import mod.azure.azurelib.common.internal.common.core.animatable.instance.AnimatableInstanceCache; +import mod.azure.azurelib.common.internal.common.core.animatable.model.CoreGeoBone; + +/** + * This is the root interface for all animatable objects in AzureLib. + * Generally speaking you should use one of the sub-interfaces relevant to your specific object so that your model can be automatically handled.
+ * See:
+ *
    + *
  • {@code GeoBlock}
  • + *
  • {@code GeoEntity}
  • + *
  • {@code GeoItem}
  • + *
+ */ +public interface GeoAnimatable { + /** + * Register your {@link AnimationController AnimationControllers} and their respective animations and conditions. + * Override this method in your animatable object and add your controllers via {@link AnimatableManager.ControllerRegistrar#add ControllerRegistrar.add}. + * You may add as many controllers as wanted. + *

+ * Each controller can only play one animation at a time, and so animations that you intend to play concurrently should be handled in independent controllers. + * Note having multiple animations playing via multiple controllers can override parts of one animation with another if both animations use the same bones or child bones. + * + * @param controllers The object to register your controller instances to + */ + void registerControllers(AnimatableManager.ControllerRegistrar controllers); + + /** + * Each instance of a {@code GeoAnimatable} must return an instance of an {@link AnimatableInstanceCache}, which handles instance-specific animation info. + * Generally speaking, you should create your cache using {@code AzureLibUtil#createCache} and store it in your animatable instance, returning that cached instance when called. + * @return A cached instance of an {@code AnimatableInstanceCache} + */ + AnimatableInstanceCache getAnimatableInstanceCache(); + + /** + * Defines the speed in which the {@link AnimationProcessor} should return + * {@link CoreGeoBone GeoBones} that currently have no animations + * to their default position. + */ + default double getBoneResetTime() { + return 1; + } + + /** + * Defines whether the animations for this animatable should continue playing in the background when the game is paused.
+ * By default, animation progress will be stalled while the game is paused. + */ + default boolean shouldPlayAnimsWhileGamePaused() { + return false; + } + + /** + * 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 object An object related to this animatable relevant to tick calculation. Different subclasses will use this differently + * @return The current tick/age of the animatable, for animation purposes + */ + double getTick(Object object); + + /** + * Override the default handling for instantiating an AnimatableInstanceCache for this animatable.
+ * Don't override this unless you know what you're doing. + */ + @Nullable + default AnimatableInstanceCache animatableCacheOverride() { + return null; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animatable/instance/AnimatableInstanceCache.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animatable/instance/AnimatableInstanceCache.java new file mode 100644 index 0000000..c158984 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animatable/instance/AnimatableInstanceCache.java @@ -0,0 +1,45 @@ +package mod.azure.azurelib.common.internal.common.core.animatable.instance; + +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.core.object.DataTicket; + +/** + * The base cache class responsible for returning the {@link AnimatableManager} for a given instanceof of a {@link GeoAnimatable}. + * This class is abstracted and not intended for direct use. See either {@link SingletonAnimatableInstanceCache} or {@link InstancedAnimatableInstanceCache} + */ +public abstract class AnimatableInstanceCache { + protected final GeoAnimatable animatable; + + protected AnimatableInstanceCache(GeoAnimatable animatable) { + this.animatable = animatable; + } + + /** + * This creates or gets the cached animatable manager for any unique ID.
+ * For itemstacks, this is typically a reserved ID provided by AzureLib. {@code Entities} and {@code BlockEntities} + * pass their position or int ID. They typically only have one {@link AnimatableManager} per cache anyway + * @param uniqueId A unique ID. For every ID the same animation manager + * will be returned. + */ + public abstract AnimatableManager getManagerForId(long uniqueId); + + /** + * Helper method to set a data point in the {@link AnimatableManager#setData manager} for this animatable. + * @param uniqueId The unique identifier for this animatable instance + * @param dataTicket The DataTicket for the data + * @param data The data to store + */ + public void addDataPoint(long uniqueId, DataTicket dataTicket, D data) { + getManagerForId(uniqueId).setData(dataTicket, data); + } + + /** + * Helper method to get a data point from the {@link AnimatableManager#getData data collection} for this animatable. + * @param uniqueId The unique identifier for this animatable instance + * @param dataTicket The DataTicket for the data + */ + public D getDataPoint(long uniqueId, DataTicket dataTicket) { + return getManagerForId(uniqueId).getData(dataTicket); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animatable/instance/InstancedAnimatableInstanceCache.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animatable/instance/InstancedAnimatableInstanceCache.java new file mode 100644 index 0000000..97876da --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animatable/instance/InstancedAnimatableInstanceCache.java @@ -0,0 +1,28 @@ +package mod.azure.azurelib.common.internal.common.core.animatable.instance; + +import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable; +import mod.azure.azurelib.common.internal.common.core.animation.AnimatableManager; + +/** + * AnimatableInstanceCache implementation for instantiated objects such as Entities or BlockEntities. Returns a single {@link AnimatableManager} instance per cache. + */ +public class InstancedAnimatableInstanceCache extends AnimatableInstanceCache { + protected AnimatableManager manager; + + public InstancedAnimatableInstanceCache(GeoAnimatable animatable) { + super(animatable); + } + + /** + * Gets the {@link AnimatableManager} instance from this cache. + * Because this cache subclass expects a 1:1 relationship of cache to animatable, + * only one {@code AnimatableManager} instance is used + */ + @Override + public AnimatableManager getManagerForId(long uniqueId) { + if (this.manager == null) + this.manager = new AnimatableManager<>(this.animatable); + + return this.manager; + } +} \ No newline at end of file diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animatable/instance/SingletonAnimatableInstanceCache.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animatable/instance/SingletonAnimatableInstanceCache.java new file mode 100644 index 0000000..fe8b924 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animatable/instance/SingletonAnimatableInstanceCache.java @@ -0,0 +1,26 @@ +package mod.azure.azurelib.common.internal.common.core.animatable.instance; + +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable; +import mod.azure.azurelib.common.internal.common.core.animation.AnimatableManager; + +/** + * AnimatableInstanceCache implementation for singleton/flyweight objects such as Items. Utilises a keyed map to differentiate different instances of the object. + */ +public class SingletonAnimatableInstanceCache extends AnimatableInstanceCache { + protected final Long2ObjectMap> managers = new Long2ObjectOpenHashMap<>(); + + public SingletonAnimatableInstanceCache(GeoAnimatable animatable) { + super(animatable); + } + + /** + * Gets an {@link AnimatableManager} instance from this cache, cached under the id provided, or a new one if one doesn't already exist.
+ * This subclass assumes that all animatable instances will be sharing this cache instance, and so differentiates data by ids. + */ + @Override + public AnimatableManager getManagerForId(long uniqueId) { + return this.managers.computeIfAbsent(uniqueId, key -> new AnimatableManager<>(this.animatable)); + } +} \ No newline at end of file diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animatable/model/CoreBakedGeoModel.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animatable/model/CoreBakedGeoModel.java new file mode 100644 index 0000000..c0203a4 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animatable/model/CoreBakedGeoModel.java @@ -0,0 +1,42 @@ +package mod.azure.azurelib.common.internal.common.core.animatable.model; + +import java.util.List; +import java.util.Optional; + +/** + * Baked model object for AzureLib models.
+ * Mostly an internal placeholder to allow for splitting up core (non-Minecraft) libraries + */ +public interface CoreBakedGeoModel { + List getBones(); + /** + * Gets a bone from this model by name.
+ * Generally not a very efficient method, should be avoided where possible. + * @param name The name of the bone + * @return An {@link Optional} containing the {@link CoreGeoBone} if one matches, otherwise an empty Optional + */ + Optional getBone(String name); + + /** + * Search a given {@link CoreGeoBone}'s child bones and see if any of them match the given name, then return it.
+ * @param parent The parent bone to search the children of + * @param name The name of the child bone to find + * @return The {@code GeoBone} found in the parent's children list, or null if not found + */ + default CoreGeoBone searchForChildBone(CoreGeoBone parent, String name) { + if (parent.getName().equals(name)) + return parent; + + for (CoreGeoBone bone : parent.getChildBones()) { + if (bone.getName().equals(name)) + return bone; + + CoreGeoBone subChildBone = searchForChildBone(bone, name); + + if (subChildBone != null) + return subChildBone; + } + + return null; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animatable/model/CoreGeoBone.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animatable/model/CoreGeoBone.java new file mode 100644 index 0000000..4739f46 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animatable/model/CoreGeoBone.java @@ -0,0 +1,119 @@ +package mod.azure.azurelib.common.internal.common.core.animatable.model; + +import java.util.List; + +import mod.azure.azurelib.common.internal.common.core.state.BoneSnapshot; + +/** + * Base class for AzureLib {@link CoreGeoModel model} bones.
+ * Mostly a placeholder to allow for splitting up core (non-Minecraft) libraries + */ +public interface CoreGeoBone { + String getName(); + + CoreGeoBone getParent(); + + float getRotX(); + + float getRotY(); + + float getRotZ(); + + float getPosX(); + + float getPosY(); + + float getPosZ(); + + float getScaleX(); + + float getScaleY(); + + float getScaleZ(); + + void setRotX(float value); + + void setRotY(float value); + + void setRotZ(float value); + + default void updateRotation(float xRot, float yRot, float zRot) { + setRotX(xRot); + setRotY(yRot); + setRotZ(zRot); + } + + void setPosX(float value); + + void setPosY(float value); + + void setPosZ(float value); + + default void updatePosition(float posX, float posY, float posZ) { + setPosX(posX); + setPosY(posY); + setPosZ(posZ); + } + + void setScaleX(float value); + + void setScaleY(float value); + + void setScaleZ(float value); + + default void updateScale(float scaleX, float scaleY, float scaleZ) { + setScaleX(scaleX); + setScaleY(scaleY); + setScaleZ(scaleZ); + } + + void setPivotX(float value); + + void setPivotY(float value); + + void setPivotZ(float value); + + default void updatePivot(float pivotX, float pivotY, float pivotZ) { + setPivotX(pivotX); + setPivotY(pivotY); + setPivotZ(pivotZ); + } + + float getPivotX(); + + float getPivotY(); + + float getPivotZ(); + + boolean isHidden(); + + boolean isHidingChildren(); + + void setHidden(boolean hidden); + + void setChildrenHidden(boolean hideChildren); + + void saveInitialSnapshot(); + + void markScaleAsChanged(); + + void markRotationAsChanged(); + + void markPositionAsChanged(); + + boolean hasScaleChanged(); + + boolean hasRotationChanged(); + + boolean hasPositionChanged(); + + void resetStateChanges(); + + BoneSnapshot getInitialSnapshot(); + + List getChildBones(); + + default BoneSnapshot saveSnapshot() { + return new BoneSnapshot(this); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animatable/model/CoreGeoModel.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animatable/model/CoreGeoModel.java new file mode 100644 index 0000000..f1905d0 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animatable/model/CoreGeoModel.java @@ -0,0 +1,68 @@ +package mod.azure.azurelib.common.internal.common.core.animatable.model; + +import java.util.Optional; + +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.core.animation.Animation; +import mod.azure.azurelib.common.internal.common.core.animation.AnimationProcessor; +import mod.azure.azurelib.common.internal.common.core.animation.AnimationState; + +/** + * Base class for AzureLib models.
+ * Mostly an internal placeholder to allow for splitting up core (non-Minecraft) libraries + */ +public interface CoreGeoModel { + /** + * Get the baked model data for this model based on the provided string location + * @param location The resource path of the baked model (usually the animatable's id string) + * @return The BakedGeoModel + */ + CoreBakedGeoModel getBakedGeoModel(String location); + + /** + * Gets a bone from this model by name.
+ * Generally not a very efficient method, should be avoided where possible. + * @param name The name of the bone + * @return An {@link Optional} containing the {@link CoreGeoBone} if one matches, otherwise an empty Optional + */ + default Optional getBone(String name) { + return Optional.ofNullable(getAnimationProcessor().getBone(name)); + } + + /** + * Gets the {@link AnimationProcessor} for this model. + */ + AnimationProcessor getAnimationProcessor(); + + /** + * Gets the loaded {@link Animation} for the given animation {@code name}, if it exists + * @param animatable The {@code GeoAnimatable} instance being referred to + * @param name The name of the animation to retrieve + * @return The {@code Animation} instance for the provided {@code name}, or null if none match + */ + Animation getAnimation(E animatable, String name); + + /** + * This method is called once per render frame for each {@link GeoAnimatable} being rendered.
+ * It is an internal method for automated animation parsing. Use {@link CoreGeoModel#setCustomAnimations(GeoAnimatable, long, AnimationState)} for custom animation work + */ + void handleAnimations(E animatable, long instanceId, AnimationState animationState); + + /** + * This method is called once per render frame for each {@link GeoAnimatable} being rendered.
+ * Override to set custom animations (such as head rotation, etc). + * @param animatable The {@code GeoAnimatable} instance currently being rendered + * @param instanceId The instance id of the {@code GeoAnimatable} + * @param animationState An {@link AnimationState} instance created to hold animation data for the {@code animatable} for this method call + */ + default void setCustomAnimations(E animatable, long instanceId, AnimationState animationState) {} + + /** + * This method is called once per render frame for each {@link GeoAnimatable} being rendered.
+ * Is generally overridden by default to apply the builtin queries, but can be extended further for custom query handling. + * @param animatable The {@code GeoAnimatable} instance currently being rendered + * @param animTime The internal tick counter kept by the {@link AnimatableManager manager} for this animatable + */ + default void applyMolangQueries(E animatable, double animTime) {} +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animation/AnimatableManager.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animation/AnimatableManager.java new file mode 100644 index 0000000..6485057 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animation/AnimatableManager.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2020. + * Author: Bernie G. (Gecko) + */ + +package mod.azure.azurelib.common.internal.common.core.animation; + +import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable; +import mod.azure.azurelib.common.internal.common.core.object.DataTicket; +import mod.azure.azurelib.common.internal.common.core.state.BoneSnapshot; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * The animation data collection for a given animatable instance.
+ * Generally speaking, a single working-instance of an {@link GeoAnimatable Animatable} + * will have a single instance of {@code AnimatableManager} associated with it.
+ * + */ +public class AnimatableManager { + private final Map boneSnapshotCollection = new Object2ObjectOpenHashMap<>(); + private final Map> animationControllers; + private Map, Object> extraData; + + private double lastUpdateTime; + private boolean isFirstTick = true; + private double firstTickTime = -1; + + /** + * Instantiates a new AnimatableManager for the given animatable, calling {@link GeoAnimatable#registerControllers} to define its controllers + */ + public AnimatableManager(GeoAnimatable animatable) { + ControllerRegistrar registrar = new ControllerRegistrar(); + + animatable.registerControllers(registrar); + + this.animationControllers = registrar.build(); + } + + /** + * Add an {@link AnimationController} to this animatable's manager.
+ * Generally speaking you probably should have added it during {@link GeoAnimatable#registerControllers} + */ + public void addController(AnimationController controller) { + this.animationControllers.put(controller.getName(), controller); + } + + /** + * Removes an {@link AnimationController} from this manager by the given name, if present. + */ + public void removeController(String name) { + this.animationControllers.remove(name); + } + + public Map> getAnimationControllers() { + return animationControllers; + } + + public Map getBoneSnapshotCollection() { + return boneSnapshotCollection; + } + + public void clearSnapshotCache() { + this.boneSnapshotCollection.clear(); + } + + public double getLastUpdateTime() { + return this.lastUpdateTime; + } + + public void updatedAt(double updateTime) { + this.lastUpdateTime = updateTime; + } + + public double getFirstTickTime() { + return this.firstTickTime; + } + + public void startedAt(double time) { + this.firstTickTime = time; + } + + public boolean isFirstTick() { + return this.isFirstTick; + } + + protected void finishFirstTick() { + this.isFirstTick = false; + } + + /** + * Set a custom data point to be used later + * @param dataTicket The DataTicket for the data point + * @param data The piece of data to store + */ + public void setData(DataTicket dataTicket, D data) { + if (this.extraData == null) + this.extraData = new Object2ObjectOpenHashMap<>(); + + this.extraData.put(dataTicket, data); + } + + /** + * Retrieve a custom data point that was stored earlier, or null if it hasn't been stored + */ + public D getData(DataTicket dataTicket) { + return this.extraData != null ? dataTicket.getData(this.extraData) : null; + } + + /** + * Attempt to trigger an animation from a given controller name and registered triggerable animation name.
+ * This pseudo-overloaded method checks each controller in turn until one of them accepts the trigger.
+ * This can be sped up by specifying which controller you intend to receive the trigger in {@link AnimatableManager#tryTriggerAnimation(String, String)} + * @param animName The name of animation to trigger. This needs to have been registered with the controller via {@link AnimationController#triggerableAnim AnimationController.triggerableAnim} + */ + public void tryTriggerAnimation(String animName) { + for (AnimationController controller : getAnimationControllers().values()) { + if (controller.tryTriggerAnimation(animName)) + return; + } + } + + /** + * Attempt to trigger an animation from a given controller name and registered triggerable animation name + * @param controllerName The name of the controller name the animation belongs to + * @param animName The name of animation to trigger. This needs to have been registered with the controller via {@link AnimationController#triggerableAnim AnimationController.triggerableAnim} + */ + public void tryTriggerAnimation(String controllerName, String animName) { + AnimationController controller = getAnimationControllers().get(controllerName); + + if (controller != null) + controller.tryTriggerAnimation(animName); + } + + /** + * Helper class for the AnimatableManager to cleanly register controllers in one shot at instantiation for efficiency + */ + public static final class ControllerRegistrar { + private final List> controllers = new ObjectArrayList<>(4); + + /** + * Add an {@link AnimationController} to this registrar + */ + public ControllerRegistrar add(AnimationController... controllers) { + this.controllers.addAll(Arrays.asList(controllers)); + + return this; + } + + /** + * Remove an {@link AnimationController} from this registrar by name.
+ * This is mostly only useful if you're sub-classing an existing animatable object and want to modify the super list + */ + public ControllerRegistrar remove(String name) { + this.controllers.removeIf(controller -> controller.getName().equals(name)); + + return this; + } + + private Object2ObjectArrayMap> build() { + Object2ObjectArrayMap> map = new Object2ObjectArrayMap<>(this.controllers.size()); + + this.controllers.forEach(controller -> map.put(controller.getName(), controller)); + + return (Object2ObjectArrayMap)map; + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animation/Animation.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animation/Animation.java new file mode 100644 index 0000000..c7f89d1 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animation/Animation.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2020. + * Author: Bernie G. (Gecko) + */ + +package mod.azure.azurelib.common.internal.common.core.animation; + +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; + +import mod.azure.azurelib.common.internal.common.core.keyframe.event.data.CustomInstructionKeyframeData; +import mod.azure.azurelib.common.internal.common.core.keyframe.event.data.ParticleKeyframeData; +import mod.azure.azurelib.common.internal.common.core.keyframe.event.data.SoundKeyframeData; +import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable; +import mod.azure.azurelib.common.internal.common.core.keyframe.BoneAnimation; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A compiled animation instance for use by the {@link AnimationController}
+ * Modifications or extensions of a compiled Animation are not supported, and therefore an instance of Animation is considered final and immutable. + */ +public record Animation(String name, double length, LoopType loopType, BoneAnimation[] boneAnimations, Keyframes keyFrames) { + public record Keyframes(SoundKeyframeData[] sounds, ParticleKeyframeData[] particles, CustomInstructionKeyframeData[] customInstructions) {} + + static Animation generateWaitAnimation(double length) { + return new Animation(RawAnimation.Stage.WAIT, length, LoopType.PLAY_ONCE, new BoneAnimation[0], + new Keyframes(new SoundKeyframeData[0], new ParticleKeyframeData[0], new CustomInstructionKeyframeData[0])); + } + + /** + * Loop type functional interface to define post-play handling for a given animation.
+ * Custom loop types are supported by extending this class and providing the extended class instance as the loop type for the animation + */ + @FunctionalInterface + public interface LoopType { + final Map LOOP_TYPES = new ConcurrentHashMap<>(4); + + LoopType DEFAULT = (animatable, controller, currentAnimation) -> currentAnimation.loopType().shouldPlayAgain(animatable, controller, currentAnimation); + LoopType PLAY_ONCE = register("play_once", register("false", (animatable, controller, currentAnimation) -> false)); + LoopType HOLD_ON_LAST_FRAME = register("hold_on_last_frame", (animatable, controller, currentAnimation) -> { + controller.animationState = AnimationController.State.PAUSED; + + return true; + }); + LoopType LOOP = register("loop", register("true", (animatable, controller, currentAnimation) -> true)); + + /** + * Override in a custom instance to dynamically decide whether an animation should repeat or stop + * @param animatable The animating object relevant to this method call + * @param controller The {@link AnimationController} playing the current animation + * @param currentAnimation The current animation that just played + * @return Whether the animation should play again, or stop + */ + boolean shouldPlayAgain(GeoAnimatable animatable, AnimationController controller, Animation currentAnimation); + + /** + * Retrieve a LoopType instance based on a {@link JsonElement}. + * Returns either {@link LoopType#PLAY_ONCE} or {@link LoopType#LOOP} based on a boolean or string element type, + * or any other registered loop type with a matching type string. + * @param json The loop {@link JsonElement} to attempt to parse + * @return A usable LoopType instance + */ + static LoopType fromJson(JsonElement json) { + if (json == null || !json.isJsonPrimitive()) + return PLAY_ONCE; + + JsonPrimitive primitive = json.getAsJsonPrimitive(); + + if (primitive.isBoolean()) + return primitive.getAsBoolean() ? LOOP : PLAY_ONCE; + + if (primitive.isString()) + return fromString(primitive.getAsString()); + + return PLAY_ONCE; + } + + static LoopType fromString(String name) { + return LOOP_TYPES.getOrDefault(name, PLAY_ONCE); + } + + /** + * Register a LoopType with AzureLib for handling loop functionality of animations..
+ * MUST be called during mod construct
+ * It is recommended you don't call this directly, and instead call it via {@code AzureLibUtil#addCustomLoopType} + * @param name The name of the loop type + * @param loopType The loop type to register + * @return The registered {@code LoopType} + */ + static LoopType register(String name, LoopType loopType) { + LOOP_TYPES.put(name, loopType); + + return loopType; + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animation/AnimationController.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animation/AnimationController.java new file mode 100644 index 0000000..f92ebc3 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animation/AnimationController.java @@ -0,0 +1,797 @@ +/* + * Copyright (c) 2020. + * Author: Bernie G. (Gecko) + */ + +package mod.azure.azurelib.common.internal.common.core.animation; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import mod.azure.azurelib.common.internal.common.AzureLib; +import mod.azure.azurelib.common.internal.common.AzureLibException; +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.animatable.model.CoreGeoModel; +import mod.azure.azurelib.common.internal.common.core.keyframe.*; +import mod.azure.azurelib.common.internal.common.core.keyframe.event.CustomInstructionKeyframeEvent; +import mod.azure.azurelib.common.internal.common.core.keyframe.event.ParticleKeyframeEvent; +import mod.azure.azurelib.common.internal.common.core.keyframe.event.SoundKeyframeEvent; +import mod.azure.azurelib.common.internal.common.core.keyframe.event.data.CustomInstructionKeyframeData; +import mod.azure.azurelib.common.internal.common.core.keyframe.event.data.KeyFrameData; +import mod.azure.azurelib.common.internal.common.core.keyframe.event.data.ParticleKeyframeData; +import mod.azure.azurelib.common.internal.common.core.keyframe.event.data.SoundKeyframeData; +import mod.azure.azurelib.common.internal.common.core.math.Constant; +import mod.azure.azurelib.common.internal.common.core.math.IValue; +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.Axis; +import mod.azure.azurelib.common.internal.common.core.object.PlayState; +import mod.azure.azurelib.common.internal.common.core.state.BoneSnapshot; + +import java.util.*; +import java.util.function.Function; +import java.util.function.ToDoubleFunction; + +/** + * The actual controller that handles the playing and usage of animations, including their various keyframes and instruction markers. + * Each controller can only play a single animation at a time - for example you may have one controller to animate walking, + * one to control attacks, one to control size, etc. + */ +public class AnimationController { + protected final T animatable; + protected final String name; + protected final AnimationStateHandler stateHandler; + protected final Map boneAnimationQueues = new Object2ObjectOpenHashMap<>(); + protected final Map boneSnapshots = new Object2ObjectOpenHashMap<>(); + protected Queue animationQueue = new LinkedList<>(); + + protected boolean isJustStarting = false; + protected boolean needsAnimationReload = false; + protected boolean shouldResetTick = false; + private boolean justStopped = true; + protected boolean justStartedTransition = false; + + protected SoundKeyframeHandler soundKeyframeHandler = null; + protected ParticleKeyframeHandler particleKeyframeHandler = null; + protected CustomKeyframeHandler customKeyframeHandler = null; + + protected final Map triggerableAnimations = new Object2ObjectOpenHashMap<>(0); + protected RawAnimation triggeredAnimation = null; + protected boolean handlingTriggeredAnimations = false; + + protected double transitionLength; + protected RawAnimation currentRawAnimation; + protected AnimationProcessor.QueuedAnimation currentAnimation; + protected State animationState = State.STOPPED; + protected double tickOffset; + protected ToDoubleFunction animationSpeedModifier = obj -> 1d; + protected Function overrideEasingTypeFunction = obj -> null; + private final Set executedKeyFrames = new ObjectOpenHashSet<>(); + protected CoreGeoModel lastModel; + + /** + * Instantiates a new {@code AnimationController}.
+ * This constructor assumes a 0-tick transition length between animations, and a generic name. + * + * @param animatable The object that will be animated by this controller + * @param animationHandler The {@link AnimationStateHandler} animation state handler responsible for deciding which animations to play + */ + public AnimationController(T animatable, AnimationStateHandler animationHandler) { + this(animatable, "base_controller", 0, animationHandler); + } + + /** + * Instantiates a new {@code AnimationController}.
+ * This constructor assumes a 0-tick transition length between animations. + * + * @param animatable The object that will be animated by this controller + * @param name The name of the controller - should represent what animations it handles + * @param animationHandler The {@link AnimationStateHandler} animation state handler responsible for deciding which animations to play + */ + public AnimationController(T animatable, String name, AnimationStateHandler animationHandler) { + this(animatable, name, 0, animationHandler); + } + + /** + * Instantiates a new {@code AnimationController}.
+ * This constructor assumes a generic name. + * + * @param animatable The object that will be animated by this controller + * @param transitionTickTime The amount of time (in ticks) that the controller should take to transition between animations. + * Lerping is automatically applied where possible + * @param animationHandler The {@link AnimationStateHandler} animation state handler responsible for deciding which animations to play + */ + public AnimationController(T animatable, int transitionTickTime, AnimationStateHandler animationHandler) { + this(animatable, "base_controller", transitionTickTime, animationHandler); + } + + /** + * Instantiates a new {@code AnimationController}.
+ * + * @param animatable The object that will be animated by this controller + * @param name The name of the controller - should represent what animations it handles + * @param transitionTickTime The amount of time (in ticks) that the controller should take to transition between animations. + * Lerping is automatically applied where possible + * @param animationHandler The {@link AnimationStateHandler} animation state handler responsible for deciding which animations to play + */ + public AnimationController(T animatable, String name, int transitionTickTime, AnimationStateHandler animationHandler) { + this.animatable = animatable; + this.name = name; + this.transitionLength = transitionTickTime; + this.stateHandler = animationHandler; + } + + /** + * Applies the given {@link SoundKeyframeHandler} to this controller, for handling {@link SoundKeyframeEvent sound keyframe instructions}. + * + * @return this + */ + public AnimationController setSoundKeyframeHandler(SoundKeyframeHandler soundHandler) { + this.soundKeyframeHandler = soundHandler; + + return this; + } + + /** + * Applies the given {@link ParticleKeyframeHandler} to this controller, for handling {@link ParticleKeyframeEvent particle keyframe instructions}. + * + * @return this + */ + public AnimationController setParticleKeyframeHandler(ParticleKeyframeHandler particleHandler) { + this.particleKeyframeHandler = particleHandler; + + return this; + } + + /** + * Applies the given {@link CustomKeyframeHandler} to this controller, for handling {@link CustomInstructionKeyframeEvent sound keyframe instructions}. + * + * @return this + */ + public AnimationController setCustomInstructionKeyframeHandler(CustomKeyframeHandler customInstructionHandler) { + this.customKeyframeHandler = customInstructionHandler; + + return this; + } + + /** + * Applies the given modifier function to this controller, for handling the speed that the controller should play its animations at.
+ * An output value of 1 is considered neutral, with 2 playing an animation twice as fast, 0.5 playing half as fast, etc. + * + * @param speedModFunction The function to apply to this controller to handle animation speed + * @return this + */ + public AnimationController setAnimationSpeedHandler(ToDoubleFunction speedModFunction) { + this.animationSpeedModifier = speedModFunction; + + return this; + } + + /** + * Applies the given modifier value to this controller, for handlign the speed that the controller hsould play its animations at.
+ * A value of 1 is considered neutral, with 2 playing an animation twice as fast, 0.5 playing half as fast, etc. + * + * @param speed The speed modifier to apply to this controller to handle animation speed. + * @return this + */ + public AnimationController setAnimationSpeed(double speed) { + return setAnimationSpeedHandler(obj -> speed); + } + + /** + * Sets the controller's {@link EasingType} override for animations.
+ * By default, the controller will use whatever {@code EasingType} was defined in the animation json + * + * @param easingTypeFunction The new {@code EasingType} to use + * @return this + */ + public AnimationController setOverrideEasingType(EasingType easingTypeFunction) { + return setOverrideEasingTypeFunction(obj -> easingTypeFunction); + } + + /** + * Sets the controller's {@link EasingType} override function for animations.
+ * By default, the controller will use whatever {@code EasingType} was defined in the animation json + * + * @param easingType The new {@code EasingType} to use + * @return this + */ + public AnimationController setOverrideEasingTypeFunction(Function easingType) { + this.overrideEasingTypeFunction = easingType; + + return this; + } + + /** + * Registers a triggerable {@link RawAnimation} with the controller.
+ * These can then be triggered by the various {@code triggerAnim} methods in {@code GeoAnimatable's} subclasses + * + * @param name The name of the triggerable animation + * @param animation The RawAnimation for this triggerable animation + * @return this + */ + public AnimationController triggerableAnim(String name, RawAnimation animation) { + this.triggerableAnimations.put(name, animation); + + return this; + } + + /** + * Tells the AnimationController that you want to receive the {@link AnimationController.AnimationStateHandler} + * while a triggered animation is playing.
+ *
+ * This has no effect if no triggered animation has been registered, or one isn't currently playing.
+ * If a triggered animation is playing, it can be checked in your AnimationStateHandler via {@link AnimationController#isPlayingTriggeredAnimation()} + */ + public AnimationController receiveTriggeredAnimations() { + this.handlingTriggeredAnimations = true; + + return this; + } + + /** + * Gets the controller's name. + * + * @return The name + */ + public String getName() { + return this.name; + } + + /** + * Gets the currently loaded {@link Animation}. Can be null
+ * An animation returned here does not guarantee it is currently playing, just that it is the currently loaded animation for this controller + */ + + public AnimationProcessor.QueuedAnimation getCurrentAnimation() { + return this.currentAnimation; + } + + /** + * Returns the current state of this controller. + */ + public State getAnimationState() { + return this.animationState; + } + + /** + * Gets the currently loaded animation's {@link BoneAnimationQueue BoneAnimationQueues}. + */ + public Map getBoneAnimationQueues() { + return this.boneAnimationQueues; + } + + /** + * Gets the current animation speed modifier.
+ * This modifier defines the relative speed in which animations will be played based on the current state of the game. + * + * @return The computed current animation speed modifier + */ + public double getAnimationSpeed() { + return this.animationSpeedModifier.applyAsDouble(this.animatable); + } + + /** + * Marks the controller as needing to reset its animation and state the next time {@link AnimationController#setAnimation(RawAnimation)} is called.
+ *
+ * Use this if you have a {@link RawAnimation} with multiple stages and you want it to start again from the first stage, or if you want to reset the currently playing animation to the start + */ + public void forceAnimationReset() { + this.needsAnimationReload = true; + } + + /** + * Tells the controller to stop all animations until told otherwise.
+ * Calling this will prevent the controller from continuing to play the currently loaded animation until + * either {@link AnimationController#forceAnimationReset()} is called, or + * {@link AnimationController#setAnimation(RawAnimation)} is called with a different animation + */ + public void stop() { + this.animationState = State.STOPPED; + } + + /** + * Overrides the animation transition time for the controller + */ + public void setTransitionLength(int ticks) { + this.transitionLength = ticks; + } + + /** + * Checks whether the last animation that was playing on this controller has finished or not.
+ * This will return true if the controller has had an animation set previously, and it has finished playing + * and isn't going to loop or proceed to another animation.
+ * + * @return Whether the previous animation finished or not + */ + public boolean hasAnimationFinished() { + return this.currentRawAnimation != null && this.animationState == State.STOPPED; + } + + /** + * Returns the currently cached {@link RawAnimation}.
+ * This animation may or may not still be playing, but it is the last one to be set in {@link AnimationController#setAnimation} + */ + public RawAnimation getCurrentRawAnimation() { + return this.currentRawAnimation; + } + + /** + * Returns whether the controller is currently playing a triggered animation registered in + * {@link AnimationController#triggerableAnim}
+ * Used for custom handling if {@link AnimationController#receiveTriggeredAnimations()} was marked + */ + public boolean isPlayingTriggeredAnimation() { + return this.triggeredAnimation != null && !hasAnimationFinished(); + } + + /** + * Sets the currently loaded animation to the one provided.
+ * This method may be safely called every render frame, as passing the same builder that is already loaded will do nothing.
+ * Pass null to this method to tell the controller to stop.
+ * If {@link AnimationController#forceAnimationReset()} has been called prior to this, the controller will reload the animation regardless of whether it matches the currently loaded one or not + */ + public void setAnimation(RawAnimation rawAnimation) { + if (rawAnimation == null || rawAnimation.getAnimationStages().isEmpty()) { + stop(); + + return; + } + + if (this.needsAnimationReload || !rawAnimation.equals(this.currentRawAnimation)) { + if (this.lastModel != null) { + Queue animations = this.lastModel.getAnimationProcessor().buildAnimationQueue(this.animatable, rawAnimation); + + if (animations != null) { + this.animationQueue = animations; + this.currentRawAnimation = rawAnimation; + this.shouldResetTick = true; + this.animationState = State.TRANSITIONING; + this.justStartedTransition = true; + this.needsAnimationReload = false; + + return; + } + } + + stop(); + } + } + + /** + * Attempt to trigger an animation from the list of {@link AnimationController#triggerableAnimations triggerable animations} this controller contains. + * + * @param animName The name of the animation to trigger + * @return Whether the controller triggered an animation or not + */ + public boolean tryTriggerAnimation(String animName) { + RawAnimation anim = this.triggerableAnimations.get(animName); + + if (anim == null) + return false; + + this.triggeredAnimation = anim; + + if (this.animationState == State.STOPPED) { + this.animationState = State.TRANSITIONING; + this.shouldResetTick = true; + this.justStartedTransition = true; + } + + return true; + } + + /** + * Handle a given AnimationState, alongside the current triggered animation if applicable + */ + protected PlayState handleAnimationState(AnimationState state) { + if (this.triggeredAnimation != null) { + if (this.currentRawAnimation != this.triggeredAnimation) + this.currentAnimation = null; + + setAnimation(this.triggeredAnimation); + + if (!hasAnimationFinished() && (!this.handlingTriggeredAnimations || this.stateHandler.handle(state) == PlayState.CONTINUE)) + return PlayState.CONTINUE; + + this.triggeredAnimation = null; + this.needsAnimationReload = true; + } + + return this.stateHandler.handle(state); + } + + /** + * This method is called every frame in order to populate the animation point + * queues, and process animation state logic. + * + * @param model The model currently being processed + * @param state The animation test state + * @param bones The registered {@link CoreGeoBone bones} for this model + * @param snapshots The {@link BoneSnapshot} map + * @param seekTime The current tick + partial tick + * @param crashWhenCantFindBone Whether to hard-fail when a bone can't be found, or to continue with the remaining bones + */ + public void process(CoreGeoModel model, AnimationState state, Map bones, Map snapshots, final double seekTime, boolean crashWhenCantFindBone) { + double adjustedTick = adjustTick(seekTime); + this.lastModel = model; + + if (animationState == State.TRANSITIONING && adjustedTick >= this.transitionLength) { + this.shouldResetTick = true; + this.animationState = State.RUNNING; + adjustedTick = adjustTick(seekTime); + } + + PlayState playState = handleAnimationState(state); + + if (playState == PlayState.STOP || (this.currentAnimation == null && this.animationQueue.isEmpty())) { + this.animationState = State.STOPPED; + this.justStopped = true; + + return; + } + + createInitialQueues(bones.values()); + + if (this.justStartedTransition && (this.shouldResetTick || this.justStopped)) { + this.justStopped = false; + adjustedTick = adjustTick(seekTime); + + if (this.currentAnimation == null) + this.animationState = State.TRANSITIONING; + } else if (this.currentAnimation == null) { + this.shouldResetTick = true; + this.animationState = State.TRANSITIONING; + this.justStartedTransition = true; + this.needsAnimationReload = false; + adjustedTick = adjustTick(seekTime); + } else if (this.animationState != State.TRANSITIONING) { + this.animationState = State.RUNNING; + } + + if (getAnimationState() == State.RUNNING) { + processCurrentAnimation(adjustedTick, seekTime, crashWhenCantFindBone); + } else if (this.animationState == State.TRANSITIONING) { + if (adjustedTick == 0 || this.isJustStarting) { + this.justStartedTransition = false; + this.currentAnimation = this.animationQueue.poll(); + + resetEventKeyFrames(); + + if (this.currentAnimation == null) + return; + + saveSnapshotsForAnimation(this.currentAnimation, snapshots); + } + + if (this.currentAnimation != null) { + MolangParser.INSTANCE.setValue(MolangQueries.ANIM_TIME, () -> 0); + + for (BoneAnimation boneAnimation : this.currentAnimation.animation().boneAnimations()) { + BoneAnimationQueue boneAnimationQueue = this.boneAnimationQueues.get(boneAnimation.boneName()); + BoneSnapshot boneSnapshot = this.boneSnapshots.get(boneAnimation.boneName()); + CoreGeoBone bone = bones.get(boneAnimation.boneName()); + + if (bone == null) { + if (crashWhenCantFindBone) + throw new AzureLibException("Could not find bone: " + boneAnimation.boneName()); + + continue; + } + + KeyframeStack> rotationKeyFrames = boneAnimation.rotationKeyFrames(); + KeyframeStack> positionKeyFrames = boneAnimation.positionKeyFrames(); + KeyframeStack> scaleKeyFrames = boneAnimation.scaleKeyFrames(); + + if (!rotationKeyFrames.xKeyframes().isEmpty()) { + boneAnimationQueue.addNextRotation(null, adjustedTick, this.transitionLength, boneSnapshot, bone.getInitialSnapshot(), + getAnimationPointAtTick(rotationKeyFrames.xKeyframes(), 0, true, Axis.X), + getAnimationPointAtTick(rotationKeyFrames.yKeyframes(), 0, true, Axis.Y), + getAnimationPointAtTick(rotationKeyFrames.zKeyframes(), 0, true, Axis.Z)); + } + + if (!positionKeyFrames.xKeyframes().isEmpty()) { + boneAnimationQueue.addNextPosition(null, adjustedTick, this.transitionLength, boneSnapshot, + getAnimationPointAtTick(positionKeyFrames.xKeyframes(), 0, false, Axis.X), + getAnimationPointAtTick(positionKeyFrames.yKeyframes(), 0, false, Axis.Y), + getAnimationPointAtTick(positionKeyFrames.zKeyframes(), 0, false, Axis.Z)); + } + + if (!scaleKeyFrames.xKeyframes().isEmpty()) { + boneAnimationQueue.addNextScale(null, adjustedTick, this.transitionLength, boneSnapshot, + getAnimationPointAtTick(scaleKeyFrames.xKeyframes(), 0, false, Axis.X), + getAnimationPointAtTick(scaleKeyFrames.yKeyframes(), 0, false, Axis.Y), + getAnimationPointAtTick(scaleKeyFrames.zKeyframes(), 0, false, Axis.Z)); + } + } + } + } + } + + /** + * Handle the current animation's state modifications and translations + * + * @param adjustedTick The controller-adjusted tick for animation purposes + * @param seekTime The lerped tick (current tick + partial tick) + * @param crashWhenCantFindBone Whether the controller should throw an exception when unable to find the required bone, or continue with the remaining bones + */ + private void processCurrentAnimation(double adjustedTick, double seekTime, boolean crashWhenCantFindBone) { + if (adjustedTick >= this.currentAnimation.animation().length()) { + if (this.currentAnimation.loopType().shouldPlayAgain(this.animatable, this, this.currentAnimation.animation())) { + if (this.animationState != State.PAUSED) { + this.shouldResetTick = true; + + adjustedTick = adjustTick(seekTime); + resetEventKeyFrames(); + } + } else { + AnimationProcessor.QueuedAnimation nextAnimation = this.animationQueue.peek(); + + resetEventKeyFrames(); + + if (nextAnimation == null) { + this.animationState = State.STOPPED; + + return; + } else { + this.animationState = State.TRANSITIONING; + this.shouldResetTick = true; + this.currentAnimation = nextAnimation; + } + } + } + + final double finalAdjustedTick = adjustedTick; + + MolangParser.INSTANCE.setMemoizedValue(MolangQueries.ANIM_TIME, () -> finalAdjustedTick / 20d); + + for (BoneAnimation boneAnimation : this.currentAnimation.animation().boneAnimations()) { + BoneAnimationQueue boneAnimationQueue = this.boneAnimationQueues.get(boneAnimation.boneName()); + + if (boneAnimationQueue == null) { + if (crashWhenCantFindBone) + throw new AzureLibException("Could not find bone: " + boneAnimation.boneName()); + + continue; + } + + KeyframeStack> rotationKeyFrames = boneAnimation.rotationKeyFrames(); + KeyframeStack> positionKeyFrames = boneAnimation.positionKeyFrames(); + KeyframeStack> scaleKeyFrames = boneAnimation.scaleKeyFrames(); + + if (!rotationKeyFrames.xKeyframes().isEmpty()) { + boneAnimationQueue.addRotations( + getAnimationPointAtTick(rotationKeyFrames.xKeyframes(), adjustedTick, true, Axis.X), + getAnimationPointAtTick(rotationKeyFrames.yKeyframes(), adjustedTick, true, Axis.Y), + getAnimationPointAtTick(rotationKeyFrames.zKeyframes(), adjustedTick, true, Axis.Z)); + } + + if (!positionKeyFrames.xKeyframes().isEmpty()) { + boneAnimationQueue.addPositions( + getAnimationPointAtTick(positionKeyFrames.xKeyframes(), adjustedTick, false, Axis.X), + getAnimationPointAtTick(positionKeyFrames.yKeyframes(), adjustedTick, false, Axis.Y), + getAnimationPointAtTick(positionKeyFrames.zKeyframes(), adjustedTick, false, Axis.Z)); + } + + if (!scaleKeyFrames.xKeyframes().isEmpty()) { + boneAnimationQueue.addScales( + getAnimationPointAtTick(scaleKeyFrames.xKeyframes(), adjustedTick, false, Axis.X), + getAnimationPointAtTick(scaleKeyFrames.yKeyframes(), adjustedTick, false, Axis.Y), + getAnimationPointAtTick(scaleKeyFrames.zKeyframes(), adjustedTick, false, Axis.Z)); + } + } + + adjustedTick += this.transitionLength; + + for (SoundKeyframeData keyframeData : this.currentAnimation.animation().keyFrames().sounds()) { + if (adjustedTick >= keyframeData.getStartTick() && this.executedKeyFrames.add(keyframeData)) { + if (this.soundKeyframeHandler == null) { + AzureLib.LOGGER.warn("Sound Keyframe found for {} -> {}, but no keyframe handler registered", this.animatable.getClass().getSimpleName(), getName()); + break; + } + + this.soundKeyframeHandler.handle(new SoundKeyframeEvent<>(this.animatable, adjustedTick, this, keyframeData)); + } + } + + for (ParticleKeyframeData keyframeData : this.currentAnimation.animation().keyFrames().particles()) { + if (adjustedTick >= keyframeData.getStartTick() && this.executedKeyFrames.add(keyframeData)) { + if (this.particleKeyframeHandler == null) { + AzureLib.LOGGER.warn("Particle Keyframe found for {} -> {}, but no keyframe handler registered", this.animatable.getClass().getSimpleName(), getName()); + break; + } + + this.particleKeyframeHandler.handle(new ParticleKeyframeEvent<>(this.animatable, adjustedTick, this, keyframeData)); + } + } + + for (CustomInstructionKeyframeData keyframeData : this.currentAnimation.animation().keyFrames().customInstructions()) { + if (adjustedTick >= keyframeData.getStartTick() && this.executedKeyFrames.add(keyframeData)) { + if (this.customKeyframeHandler == null) { + AzureLib.LOGGER.warn("Custom Instruction Keyframe found for {} -> {}, but no keyframe handler registered", this.animatable.getClass().getSimpleName(), getName()); + break; + } + + this.customKeyframeHandler.handle(new CustomInstructionKeyframeEvent<>(this.animatable, adjustedTick, this, keyframeData)); + } + } + + if (this.transitionLength == 0 && this.shouldResetTick && this.animationState == State.TRANSITIONING) { + this.currentAnimation = this.animationQueue.poll(); + } + } + + /** + * Prepare the {@link BoneAnimationQueue} map for the current render frame + * + * @param modelRendererList The bone list from the {@link AnimationProcessor} + */ + private void createInitialQueues(Collection modelRendererList) { + this.boneAnimationQueues.clear(); + + for (CoreGeoBone modelRenderer : modelRendererList) { + this.boneAnimationQueues.put(modelRenderer.getName(), new BoneAnimationQueue(modelRenderer)); + } + } + + /** + * Cache the relevant {@link BoneSnapshot BoneSnapshots} for the current {@link AnimationProcessor.QueuedAnimation} + * for animation lerping + * + * @param animation The {@code QueuedAnimation} to filter {@code BoneSnapshots} for + * @param snapshots The master snapshot collection to pull filter from + */ + private void saveSnapshotsForAnimation(AnimationProcessor.QueuedAnimation animation, Map snapshots) { + if (animation.animation().boneAnimations() == null) { + return; + } + + for (BoneSnapshot snapshot : snapshots.values()) { + for (BoneAnimation boneAnimation : animation.animation().boneAnimations()) { + if (boneAnimation.boneName().equals(snapshot.getBone().getName())) { + this.boneSnapshots.put(boneAnimation.boneName(), BoneSnapshot.copy(snapshot)); + break; + } + } + } + } + + /** + * Adjust a tick value depending on the controller's current state and speed modifier.
+ * Is used when starting a new animation, transitioning, and a few other key areas + * + * @param tick The currently used tick value + * @return 0 if {@link AnimationController#shouldResetTick} is set to false, or a {@link AnimationController#animationSpeedModifier} modified value otherwise + */ + protected double adjustTick(double tick) { + if (!this.shouldResetTick) + return this.animationSpeedModifier.applyAsDouble(this.animatable) * Math.max(tick - this.tickOffset, 0); + + if (getAnimationState() != State.STOPPED) + this.tickOffset = tick; + + this.shouldResetTick = false; + + return 0; + } + + /** + * Convert a {@link KeyframeLocation} to an {@link AnimationPoint} + */ + private AnimationPoint getAnimationPointAtTick(List> frames, double tick, boolean isRotation, + Axis axis) { + KeyframeLocation> location = getCurrentKeyFrameLocation(frames, tick); + Keyframe currentFrame = location.keyframe(); + double startValue = currentFrame.startValue().get(); + double endValue = currentFrame.endValue().get(); + + if (isRotation) { + if (!(currentFrame.startValue() instanceof Constant)) { + startValue = Math.toRadians(startValue); + + if (axis == Axis.X || axis == Axis.Y) + startValue *= -1; + } + + if (!(currentFrame.endValue() instanceof Constant)) { + endValue = Math.toRadians(endValue); + + if (axis == Axis.X || axis == Axis.Y) + endValue *= -1; + } + } + + return new AnimationPoint(currentFrame, location.startTick(), currentFrame.length(), startValue, endValue); + } + + /** + * Returns the {@link Keyframe} relevant to the current tick time + * + * @param frames The list of {@code KeyFrames} to filter through + * @param ageInTicks The current tick time + * @return A new {@code KeyFrameLocation} containing the current {@code KeyFrame} and the tick time used to find it + */ + private KeyframeLocation> getCurrentKeyFrameLocation(List> frames, + double ageInTicks) { + double totalFrameTime = 0; + + for (Keyframe frame : frames) { + totalFrameTime += frame.length(); + + if (totalFrameTime > ageInTicks) + return new KeyframeLocation<>(frame, (ageInTicks - (totalFrameTime - frame.length()))); + } + + return new KeyframeLocation<>(frames.get(frames.size() - 1), ageInTicks); + } + + /** + * Clear the {@link KeyFrameData} cache in preparation for the next animation + */ + private void resetEventKeyFrames() { + this.executedKeyFrames.clear(); + } + + /** + * Every render frame, the {@code AnimationController} will call this handler for each animatable that is being rendered. + * This handler defines which animation should be currently playing, and returning a {@link PlayState} to tell the controller what to do next.
+ * Example Usage:
+ *
{@code
+     * AnimationFrameHandler myIdleWalkHandler = state -> {
+     * 	if (state.isMoving()) {
+     * 		state.getController().setAnimation(myWalkAnimation);
+     *    }
+     * 	else {
+     * 		state.getController().setAnimation(myIdleAnimation);
+     *    }
+     *
+     * 	return PlayState.CONTINUE;
+     * };}
+ */ + @FunctionalInterface + public interface AnimationStateHandler { + /** + * The handling method, called each frame. + * Return {@link PlayState#CONTINUE} to tell the controller to continue animating, + * or return {@link PlayState#STOP} to tell it to stop playing all animations and wait for the next {@code PlayState.CONTINUE} return. + */ + PlayState handle(AnimationState state); + } + + /** + * A handler for when a predefined sound keyframe is hit. + * When the keyframe is encountered, the {@link SoundKeyframeHandler#handle(SoundKeyframeEvent)} method will be called. + * Play the sound(s) of your choice at this time. + */ + @FunctionalInterface + public interface SoundKeyframeHandler { + void handle(SoundKeyframeEvent event); + } + + /** + * A handler for when a predefined particle keyframe is hit. + * When the keyframe is encountered, the {@link ParticleKeyframeHandler#handle(ParticleKeyframeEvent)} method will be called. + * Spawn the particles/effects of your choice at this time. + */ + @FunctionalInterface + public interface ParticleKeyframeHandler { + void handle(ParticleKeyframeEvent event); + } + + /** + * A handler for pre-defined custom instruction keyframes. + * When the keyframe is encountered, the {@link CustomKeyframeHandler#handle(CustomInstructionKeyframeEvent)} method will be called. + * You can then take whatever action you want at this point. + */ + @FunctionalInterface + public interface CustomKeyframeHandler { + void handle(CustomInstructionKeyframeEvent event); + } + + public enum State { + RUNNING, + TRANSITIONING, + PAUSED, + STOPPED; + } +} \ No newline at end of file diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animation/AnimationProcessor.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animation/AnimationProcessor.java new file mode 100644 index 0000000..52ea522 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animation/AnimationProcessor.java @@ -0,0 +1,268 @@ +package mod.azure.azurelib.common.internal.common.core.animation; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.Map; +import java.util.Queue; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable; +import mod.azure.azurelib.common.internal.common.core.animatable.model.CoreBakedGeoModel; +import mod.azure.azurelib.common.internal.common.core.animatable.model.CoreGeoBone; +import mod.azure.azurelib.common.internal.common.core.animatable.model.CoreGeoModel; +import mod.azure.azurelib.common.internal.common.core.keyframe.AnimationPoint; +import mod.azure.azurelib.common.internal.common.core.keyframe.BoneAnimationQueue; +import mod.azure.azurelib.common.internal.common.core.state.BoneSnapshot; +import mod.azure.azurelib.common.internal.common.core.utils.Interpolations; + +public class AnimationProcessor { + private final Map bones = new Object2ObjectOpenHashMap<>(); + private final CoreGeoModel model; + + public boolean reloadAnimations = false; + + public AnimationProcessor(CoreGeoModel model) { + this.model = model; + } + + /** + * Build an animation queue for the given {@link RawAnimation} + * @param animatable The animatable object being rendered + * @param rawAnimation The raw animation to be compiled + * @return A queue of animations and loop types to play + */ + public Queue buildAnimationQueue(T animatable, RawAnimation rawAnimation) { + LinkedList animations = new LinkedList<>(); + boolean error = false; + + for (RawAnimation.Stage stage : rawAnimation.getAnimationStages()) { + Animation animation; + + if (stage.animationName() == RawAnimation.Stage.WAIT) { + animation = Animation.generateWaitAnimation(stage.additionalTicks()); + } + else { + animation = this.model.getAnimation(animatable, stage.animationName()); + } + + if (animation == null) { + System.out.println("Unable to find animation: " + stage.animationName() + " for " + animatable.getClass().getSimpleName()); + + error = true; + } + else { + animations.add(new QueuedAnimation(animation, stage.loopType())); + } + } + + return error ? null : animations; + } + + /** + * Tick and apply transformations to the model based on the current state of the {@link AnimationController} + * + * @param animatable The animatable object relevant to the animation being played + * @param model The model currently being processed + * @param animatableManager The AnimatableManager instance being used for this animation processor + * @param animTime The internal tick counter kept by the {@link AnimatableManager} for this animatable + * @param event An {@link AnimationState} instance applied to this render frame + * @param crashWhenCantFindBone Whether to crash if unable to find a required bone, or to continue with the remaining bones + */ + public void tickAnimation(T animatable, CoreGeoModel model, AnimatableManager animatableManager, double animTime, AnimationState event, boolean crashWhenCantFindBone) { + Map boneSnapshots = updateBoneSnapshots(animatableManager.getBoneSnapshotCollection()); + + for (AnimationController controller : animatableManager.getAnimationControllers().values()) { + if (this.reloadAnimations) { + controller.forceAnimationReset(); + controller.getBoneAnimationQueues().clear(); + } + + controller.isJustStarting = animatableManager.isFirstTick(); + + event.withController(controller); + controller.process(model, event, this.bones, boneSnapshots, animTime, crashWhenCantFindBone); + + for (BoneAnimationQueue boneAnimation : controller.getBoneAnimationQueues().values()) { + CoreGeoBone bone = boneAnimation.bone(); + BoneSnapshot snapshot = boneSnapshots.get(bone.getName()); + BoneSnapshot initialSnapshot = bone.getInitialSnapshot(); + + AnimationPoint rotXPoint = boneAnimation.rotationXQueue().poll(); + AnimationPoint rotYPoint = boneAnimation.rotationYQueue().poll(); + AnimationPoint rotZPoint = boneAnimation.rotationZQueue().poll(); + AnimationPoint posXPoint = boneAnimation.positionXQueue().poll(); + AnimationPoint posYPoint = boneAnimation.positionYQueue().poll(); + AnimationPoint posZPoint = boneAnimation.positionZQueue().poll(); + AnimationPoint scaleXPoint = boneAnimation.scaleXQueue().poll(); + AnimationPoint scaleYPoint = boneAnimation.scaleYQueue().poll(); + AnimationPoint scaleZPoint = boneAnimation.scaleZQueue().poll(); + EasingType easingType = controller.overrideEasingTypeFunction.apply(animatable); + + if (rotXPoint != null && rotYPoint != null && rotZPoint != null) { + bone.setRotX((float)EasingType.lerpWithOverride(rotXPoint, easingType) + initialSnapshot.getRotX()); + bone.setRotY((float)EasingType.lerpWithOverride(rotYPoint, easingType) + initialSnapshot.getRotY()); + bone.setRotZ((float)EasingType.lerpWithOverride(rotZPoint, easingType) + initialSnapshot.getRotZ()); + snapshot.updateRotation(bone.getRotX(), bone.getRotY(), bone.getRotZ()); + snapshot.startRotAnim(); + bone.markRotationAsChanged(); + } + + if (posXPoint != null && posYPoint != null && posZPoint != null) { + bone.setPosX((float)EasingType.lerpWithOverride(posXPoint, easingType)); + bone.setPosY((float)EasingType.lerpWithOverride(posYPoint, easingType)); + bone.setPosZ((float)EasingType.lerpWithOverride(posZPoint, easingType)); + snapshot.updateOffset(bone.getPosX(), bone.getPosY(), bone.getPosZ()); + snapshot.startPosAnim(); + bone.markPositionAsChanged(); + } + + if (scaleXPoint != null && scaleYPoint != null && scaleZPoint != null) { + bone.setScaleX((float)EasingType.lerpWithOverride(scaleXPoint, easingType)); + bone.setScaleY((float)EasingType.lerpWithOverride(scaleYPoint, easingType)); + bone.setScaleZ((float)EasingType.lerpWithOverride(scaleZPoint, easingType)); + snapshot.updateScale(bone.getScaleX(), bone.getScaleY(), bone.getScaleZ()); + snapshot.startScaleAnim(); + bone.markScaleAsChanged(); + } + } + } + + this.reloadAnimations = false; + double resetTickLength = animatable.getBoneResetTime(); + + for (CoreGeoBone bone : getRegisteredBones()) { + if (!bone.hasRotationChanged()) { + BoneSnapshot initialSnapshot = bone.getInitialSnapshot(); + BoneSnapshot saveSnapshot = boneSnapshots.get(bone.getName()); + + if (saveSnapshot.isRotAnimInProgress()) + saveSnapshot.stopRotAnim(animTime); + + double percentageReset = Math.min((animTime - saveSnapshot.getLastResetRotationTick()) / resetTickLength, 1); + + bone.setRotX((float)Interpolations.lerp(saveSnapshot.getRotX(), initialSnapshot.getRotX(), percentageReset)); + bone.setRotY((float)Interpolations.lerp(saveSnapshot.getRotY(), initialSnapshot.getRotY(), percentageReset)); + bone.setRotZ((float)Interpolations.lerp(saveSnapshot.getRotZ(), initialSnapshot.getRotZ(), percentageReset)); + + if (percentageReset >= 1) + saveSnapshot.updateRotation(bone.getRotX(), bone.getRotY(), bone.getRotZ()); + } + + if (!bone.hasPositionChanged()) { + BoneSnapshot initialSnapshot = bone.getInitialSnapshot(); + BoneSnapshot saveSnapshot = boneSnapshots.get(bone.getName()); + + if (saveSnapshot.isPosAnimInProgress()) + saveSnapshot.stopPosAnim(animTime); + + double percentageReset = Math.min((animTime - saveSnapshot.getLastResetPositionTick()) / resetTickLength, 1); + + bone.setPosX((float)Interpolations.lerp(saveSnapshot.getOffsetX(), initialSnapshot.getOffsetX(), percentageReset)); + bone.setPosY((float)Interpolations.lerp(saveSnapshot.getOffsetY(), initialSnapshot.getOffsetY(), percentageReset)); + bone.setPosZ((float)Interpolations.lerp(saveSnapshot.getOffsetZ(), initialSnapshot.getOffsetZ(), percentageReset)); + + if (percentageReset >= 1) + saveSnapshot.updateOffset(bone.getPosX(), bone.getPosY(), bone.getPosZ()); + } + + if (!bone.hasScaleChanged()) { + BoneSnapshot initialSnapshot = bone.getInitialSnapshot(); + BoneSnapshot saveSnapshot = boneSnapshots.get(bone.getName()); + + if (saveSnapshot.isScaleAnimInProgress()) + saveSnapshot.stopScaleAnim(animTime); + + double percentageReset = Math.min((animTime - saveSnapshot.getLastResetScaleTick()) / resetTickLength, 1); + + bone.setScaleX((float)Interpolations.lerp(saveSnapshot.getScaleX(), initialSnapshot.getScaleX(), percentageReset)); + bone.setScaleY((float)Interpolations.lerp(saveSnapshot.getScaleY(), initialSnapshot.getScaleY(), percentageReset)); + bone.setScaleZ((float)Interpolations.lerp(saveSnapshot.getScaleZ(), initialSnapshot.getScaleZ(), percentageReset)); + + if (percentageReset >= 1) + saveSnapshot.updateScale(bone.getScaleX(), bone.getScaleY(), bone.getScaleZ()); + } + } + + resetBoneTransformationMarkers(); + animatableManager.finishFirstTick(); + } + + /** + * Reset the transformation markers applied to each {@link CoreGeoBone} ready for the next render frame + */ + private void resetBoneTransformationMarkers() { + getRegisteredBones().forEach(CoreGeoBone::resetStateChanges); + } + + /** + * Create new bone {@link BoneSnapshot} based on the bone's initial snapshot for the currently registered {@link CoreGeoBone GeoBones}, + * filtered by the bones already present in the master snapshots map + * @param snapshots The master bone snapshots map from the related {@link AnimatableManager} + * @return The input snapshots map, for easy assignment + */ + private Map updateBoneSnapshots(Map snapshots) { + for (CoreGeoBone bone : getRegisteredBones()) { + if (!snapshots.containsKey(bone.getName())) + snapshots.put(bone.getName(), BoneSnapshot.copy(bone.getInitialSnapshot())); + } + + return snapshots; + } + + /** + * Gets a bone by name. + * + * @param boneName The bone name + * @return the bone + */ + public CoreGeoBone getBone(String boneName) { + return this.bones.get(boneName); + } + + /** + * Adds the given bone to the bones list for this processor.
+ * This is normally handled automatically by AzureLib.
+ * Failure to properly register a bone will break things. + */ + public void registerGeoBone(CoreGeoBone bone) { + bone.saveInitialSnapshot(); + this.bones.put(bone.getName(), bone); + + for (CoreGeoBone child : bone.getChildBones()) { + registerGeoBone(child); + } + } + + /** + * Clear the {@link CoreGeoBone GeoBones} currently registered to the processor, + * then prepares the processor for a new model.
+ * Should be called whenever switching models to render/animate + */ + public void setActiveModel(CoreBakedGeoModel model) { + this.bones.clear(); + + for (CoreGeoBone bone : model.getBones()) { + registerGeoBone(bone); + } + } + + /** + * Get an iterable collection of the {@link CoreGeoBone GeoBones} currently registered to the processor + */ + public Collection getRegisteredBones() { + return this.bones.values(); + } + + /** + * Apply transformations and settings prior to acting on any animation-related functionality + */ + public void preAnimationSetup(T animatable, double animTime) { + this.model.applyMolangQueries(animatable, animTime); + } + + /** + * {@link Animation} and {@link Animation.LoopType} override pair, + * used to define a playable animation stage for a {@link GeoAnimatable} + */ + public record QueuedAnimation(Animation animation, Animation.LoopType loopType) {} +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animation/AnimationState.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animation/AnimationState.java new file mode 100644 index 0000000..021acff --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animation/AnimationState.java @@ -0,0 +1,172 @@ +package mod.azure.azurelib.common.internal.common.core.animation; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable; +import mod.azure.azurelib.common.internal.common.core.object.DataTicket; +import mod.azure.azurelib.common.internal.common.core.object.PlayState; + +import java.util.Map; +import java.util.Objects; + +/** + * Animation state handler for end-users.
+ * This is where users would set their selected animation to play, + * stop the controller, or any number of other animation-related actions. + */ +public class AnimationState { + private final T animatable; + private final float limbSwing; + private final float limbSwingAmount; + private final float partialTick; + private final boolean isMoving; + private final Map, Object> extraData = new Object2ObjectOpenHashMap<>(); + + protected AnimationController controller; + public double animationTick; + + public AnimationState(T animatable, float limbSwing, float limbSwingAmount, float partialTick, boolean isMoving) { + this.animatable = animatable; + this.limbSwing = limbSwing; + this.limbSwingAmount = limbSwingAmount; + this.partialTick = partialTick; + this.isMoving = isMoving; + } + + /** + * Gets the amount of ticks that have passed in either the current transition or + * animation, depending on the controller's AnimationState. + */ + public double getAnimationTick() { + return this.animationTick; + } + + /** + * Gets the current {@link GeoAnimatable} being rendered + */ + public T getAnimatable() { + return this.animatable; + } + + public float getLimbSwing() { + return this.limbSwing; + } + + public float getLimbSwingAmount() { + return this.limbSwingAmount; + } + + /** + * Gets the fractional value of the current game tick that has passed in rendering + */ + public float getPartialTick() { + return this.partialTick; + } + + /** + * Gets whether the current {@link GeoAnimatable} is considered to be moving for animation purposes.
+ * Note that this is a best-case approximation of movement, and your needs may vary. + */ + public boolean isMoving() { + return this.isMoving; + } + + /** + * Gets the current {@link AnimationController} responsible for the current animation + */ + public AnimationController getController() { + return this.controller; + } + + /** + * Sets the {@code AnimationEvent}'s current {@link AnimationController} + */ + public AnimationState withController(AnimationController controller) { + this.controller = controller; + + return this; + } + + /** + * Gets the optional additional data map for the event.
+ * @see DataTicket + */ + public Map, ?> getExtraData() { + return this.extraData; + } + + /** + * Get a data value saved to this animation event by the ticket for that data.
+ * @see DataTicket + * @param dataTicket The {@link DataTicket} for the data to retrieve + * @return The cached data for the given {@code DataTicket}, or null if not saved + */ + public D getData(DataTicket dataTicket) { + return dataTicket.getData(this.extraData); + } + + /** + * Save a data value for the given {@link DataTicket} in the additional data map + * @param dataTicket The {@code DataTicket} for the data value + * @param data The data value + */ + public void setData(DataTicket dataTicket, D data) { + this.extraData.put(dataTicket, data); + } + + /** + * Sets the animation for the controller to start/continue playing.
+ * Basically just a shortcut for
getController().setAnimation()
+ * @param animation The animation to play + */ + public void setAnimation(RawAnimation animation) { + getController().setAnimation(animation); + } + + /** + * Helper method to set an animation to start/continue playing, and return {@link PlayState#CONTINUE} + */ + public PlayState setAndContinue(RawAnimation animation) { + getController().setAnimation(animation); + + return PlayState.CONTINUE; + } + + /** + * Checks whether the current {@link AnimationController}'s last animation was the one provided. + * This allows for multi-stage animation shifting where the next animation to play may depend on the previous one + * @param animation The animation to check + * @return Whether the controller's last animation is the one provided + */ + public boolean isCurrentAnimation(RawAnimation animation) { + return Objects.equals(getController().currentRawAnimation, animation); + } + + /** + * Similar to {@link AnimationState#isCurrentAnimation}, but additionally checks the current stage of the animation by name.
+ * This can be used to check if a multi-stage animation has reached a given stage (if it is running at all)
+ * Note that this will still return true even if the animation has finished, matching with the last animation stage in the {@link RawAnimation} last provided + * + * @param name The name of the animation stage to check (I.E. "move.walk") + * @return Whether the controller's current stage is the one provided + */ + public boolean isCurrentAnimationStage(String name) { + return getController().getCurrentAnimation() != null && getController().getCurrentAnimation().animation().name().equals(name); + } + + /** + * Helper method for {@link AnimationController#forceAnimationReset()}
+ * This should be used in controllers when stopping a non-looping animation, so that it is reset to the start for the next time it starts + */ + public void resetCurrentAnimation() { + getController().forceAnimationReset(); + } + + /** + * Helper method for {@link AnimationController#setAnimationSpeed} + * + * @param speed The speed modifier for the controller (2 = twice as fast, 0.5 = half as fast, etc) + */ + public void setControllerSpeed(float speed) { + getController().setAnimationSpeed(speed); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animation/ContextAwareAnimatableManager.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animation/ContextAwareAnimatableManager.java new file mode 100644 index 0000000..1a1ff5b --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animation/ContextAwareAnimatableManager.java @@ -0,0 +1,146 @@ +package mod.azure.azurelib.common.internal.common.core.animation; + +import java.util.Map; + +import org.jetbrains.annotations.Nullable; + +import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable; +import mod.azure.azurelib.common.internal.common.core.object.DataTicket; +import mod.azure.azurelib.common.internal.common.core.state.BoneSnapshot; + +/** + * Context-aware wrapper for {@link AnimatableManager}.
+ * This can be used for things like perspective-dependent animation handling and other similar functionality.
+ * This relies entirely on data present in {@link AnimatableManager#extraData} saved to this manager to determine context + */ +public abstract class ContextAwareAnimatableManager extends AnimatableManager { + private final Map> managers; + + /** + * Instantiates a new AnimatableManager for the given animatable, calling {@link GeoAnimatable#registerControllers} to define its controllers + * + * @param animatable + */ + public ContextAwareAnimatableManager(GeoAnimatable animatable) { + super(animatable); + + this.managers = buildContextOptions(animatable); + } + + /** + * Build the context-manager map for this manager.
+ * The resulting map MUST contain all possible contexts. + * + * @param animatable + */ + protected abstract Map> buildContextOptions(GeoAnimatable animatable); + + /** + * Get the current context for the manager, to determine which sub-manager to retrieve + */ + public abstract C getCurrentContext(); + + /** + * Get the AnimatableManager for the given context + */ + public AnimatableManager getManagerForContext(C context) { + return this.managers.get(context); + } + + /** + * Add an {@link AnimationController} to this animatable's manager.
+ * Generally speaking you probably should have added it during {@link GeoAnimatable#registerControllers} + */ + @Override + public void addController(AnimationController controller) { + getManagerForContext(getCurrentContext()).addController(controller); + } + + /** + * Removes an {@link AnimationController} from this manager by the given name, if present. + */ + @Override + public void removeController(String name) { + getManagerForContext(getCurrentContext()).removeController(name); + } + + @Override + public Map> getAnimationControllers() { + return getManagerForContext(getCurrentContext()).getAnimationControllers(); + } + + @Override + public Map getBoneSnapshotCollection() { + return getManagerForContext(getCurrentContext()).getBoneSnapshotCollection(); + } + + @Override + public void clearSnapshotCache() { + getManagerForContext(getCurrentContext()).clearSnapshotCache(); + } + + @Override + public double getLastUpdateTime() { + return getManagerForContext(getCurrentContext()).getLastUpdateTime(); + } + + @Override + public void updatedAt(double updateTime) { + getManagerForContext(getCurrentContext()).updatedAt(updateTime); + } + + @Override + public double getFirstTickTime() { + return getManagerForContext(getCurrentContext()).getFirstTickTime(); + } + + @Override + public void startedAt(double time) { + getManagerForContext(getCurrentContext()).startedAt(time); + } + + @Override + public boolean isFirstTick() { + return getManagerForContext(getCurrentContext()).isFirstTick(); + } + + @Override + protected void finishFirstTick() { + getManagerForContext(getCurrentContext()).finishFirstTick(); + } + + /** + * Attempt to trigger an animation from a given controller name and registered triggerable animation name.
+ * This pseudo-overloaded method checks each controller in turn until one of them accepts the trigger.
+ * This can be sped up by specifying which controller you intend to receive the trigger in {@link AnimatableManager#tryTriggerAnimation(String, String)} + * @param animName The name of animation to trigger. This needs to have been registered with the controller via {@link AnimationController#triggerableAnim AnimationController.triggerableAnim} + */ + @Override + public void tryTriggerAnimation(String animName) { + for (AnimatableManager manager : this.managers.values()) { + manager.tryTriggerAnimation(animName); + } + } + + /** + * Attempt to trigger an animation from a given controller name and registered triggerable animation name + * @param controllerName The name of the controller name the animation belongs to + * @param animName The name of animation to trigger. This needs to have been registered with the controller via {@link AnimationController#triggerableAnim AnimationController.triggerableAnim} + */ + @Override + public void tryTriggerAnimation(String controllerName, String animName) { + for (AnimatableManager manager : this.managers.values()) { + manager.tryTriggerAnimation(controllerName, animName); + } + } + + /** + * Retrieve a custom data point that was stored earlier, or null if it hasn't been stored.
+ * Sub-managers do not have their data set, and instead it is all kept in this parent manager + */ + @Nullable + @Override + public D getData(DataTicket dataTicket) { + return super.getData(dataTicket); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animation/EasingType.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animation/EasingType.java new file mode 100644 index 0000000..5943ef3 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animation/EasingType.java @@ -0,0 +1,354 @@ +package mod.azure.azurelib.common.internal.common.core.animation; + +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; + +import it.unimi.dsi.fastutil.doubles.Double2DoubleFunction; +import mod.azure.azurelib.common.internal.common.core.keyframe.AnimationPoint; +import mod.azure.azurelib.common.internal.common.core.keyframe.Keyframe; +import mod.azure.azurelib.common.internal.common.core.utils.Interpolations; + +/** + * Functional interface defining an easing function.
+ * {@code value} is the easing value provided from the keyframe's {@link Keyframe#easingArgs()} + *

+ * For more information on easings, see:
+ *
Easings.net
+ * Cubic-Bezier.com
+ */ +@FunctionalInterface +public interface EasingType { + Map EASING_TYPES = new ConcurrentHashMap<>(64); + + EasingType LINEAR = register("linear", register("none", value -> easeIn(EasingType::linear))); + EasingType STEP = register("step", value -> easeIn(step(value))); + EasingType EASE_IN_SINE = register("easeinsine", value -> easeIn(EasingType::sine)); + EasingType EASE_OUT_SINE = register("easeoutsine", value -> easeOut(EasingType::sine)); + EasingType EASE_IN_OUT_SINE = register("easeinoutsine", value -> easeInOut(EasingType::sine)); + EasingType EASE_IN_QUAD = register("easeinquad", value -> easeIn(EasingType::quadratic)); + EasingType EASE_OUT_QUAD = register("easeoutquad", value -> easeOut(EasingType::quadratic)); + EasingType EASE_IN_OUT_QUAD = register("easeinoutquad", value -> easeInOut(EasingType::quadratic)); + EasingType EASE_IN_CUBIC = register("easeincubic", value -> easeIn(EasingType::cubic)); + EasingType EASE_OUT_CUBIC = register("easeoutcubic", value -> easeOut(EasingType::cubic)); + EasingType EASE_IN_OUT_CUBIC = register("easeinoutcubic", value -> easeInOut(EasingType::cubic)); + EasingType EASE_IN_QUART = register("easeinquart", value -> easeIn(pow(4))); + EasingType EASE_OUT_QUART = register("easeoutquart", value -> easeOut(pow(4))); + EasingType EASE_IN_OUT_QUART = register("easeinoutquart", value -> easeInOut(pow(4))); + EasingType EASE_IN_QUINT = register("easeinquint", value -> easeIn(pow(4))); + EasingType EASE_OUT_QUINT = register("easeoutquint", value -> easeOut(pow(5))); + EasingType EASE_IN_OUT_QUINT = register("easeinoutquint", value -> easeInOut(pow(5))); + EasingType EASE_IN_EXPO = register("easeinexpo", value -> easeIn(EasingType::exp)); + EasingType EASE_OUT_EXPO = register("easeoutexpo", value -> easeOut(EasingType::exp)); + EasingType EASE_IN_OUT_EXPO = register("easeinoutexpo", value -> easeInOut(EasingType::exp)); + EasingType EASE_IN_CIRC = register("easeincirc", value -> easeIn(EasingType::circle)); + EasingType EASE_OUT_CIRC = register("easeoutcirc", value -> easeOut(EasingType::circle)); + EasingType EASE_IN_OUT_CIRC = register("easeinoutcirc", value -> easeInOut(EasingType::circle)); + EasingType EASE_IN_BACK = register("easeinback", value -> easeIn(back(value))); + EasingType EASE_OUT_BACK = register("easeoutback", value -> easeOut(back(value))); + EasingType EASE_IN_OUT_BACK = register("easeinoutback", value -> easeInOut(back(value))); + EasingType EASE_IN_ELASTIC = register("easeinelastic", value -> easeIn(elastic(value))); + EasingType EASE_OUT_ELASTIC = register("easeoutelastic", value -> easeOut(elastic(value))); + EasingType EASE_IN_OUT_ELASTIC = register("easeinoutelastic", value -> easeInOut(elastic(value))); + EasingType EASE_IN_BOUNCE = register("easeinbounce", value -> easeIn(bounce(value))); + EasingType EASE_OUT_BOUNCE = register("easeoutbounce", value -> easeOut(bounce(value))); + EasingType EASE_IN_OUT_BOUNCE = register("easeinoutbounce", value -> easeInOut(bounce(value))); + EasingType CATMULLROM = register("catmullrom", value -> easeInOut(EasingType::catmullRom)); + + Double2DoubleFunction buildTransformer(Double value); + + static double lerpWithOverride(AnimationPoint animationPoint, EasingType override) { + EasingType easingType = override; + + if (override == null) + easingType = animationPoint.keyFrame() == null ? LINEAR : animationPoint.keyFrame().easingType(); + + return easingType.apply(animationPoint); + } + + default double apply(AnimationPoint animationPoint) { + Double easingVariable = null; + + if (animationPoint.keyFrame() != null && animationPoint.keyFrame().easingArgs().size() > 0) + easingVariable = animationPoint.keyFrame().easingArgs().get(0).get(); + + return apply(animationPoint, easingVariable, animationPoint.currentTick() / animationPoint.transitionLength()); + } + + default double apply(AnimationPoint animationPoint, Double easingValue, double lerpValue) { + if (animationPoint.currentTick() >= animationPoint.transitionLength()) + return (float)animationPoint.animationEndValue(); + + return Interpolations.lerp(animationPoint.animationStartValue(), animationPoint.animationEndValue(), buildTransformer(easingValue).apply(lerpValue)); + } + + /** + * Register an {@code EasingType} with AzureLib for handling animation transitions and value curves.
+ * MUST be called during mod construct
+ * It is recommended you don't call this directly, and instead call it via {@code AzureLibUtil#addCustomEasingType} + * @param name The name of the easing type + * @param easingType The {@code EasingType} to associate with the given name + * @return The {@code EasingType} you registered + */ + static EasingType register(String name, EasingType easingType) { + EASING_TYPES.putIfAbsent(name, easingType); + + return easingType; + } + + /** + * Retrieve an {@code EasingType} instance based on a {@link JsonElement}. Returns one of the default {@code EasingTypes} if the name matches, or any other registered {@code EasingType} with a matching name. + * @param json The {@code easing} {@link JsonElement} to attempt to parse. + * @return A usable {@code EasingType} instance + */ + static EasingType fromJson(JsonElement json) { + if (!(json instanceof JsonPrimitive primitive) || !primitive.isString()) + return LINEAR; + + return fromString(primitive.getAsString().toLowerCase(Locale.ROOT)); + } + + /** + * Get an existing {@code EasingType} from a given string, matching the string to its name. + * @param name The name of the easing function + * @return The relevant {@code EasingType}, or {@link EasingType#LINEAR} if none match + */ + static EasingType fromString(String name) { + return EASING_TYPES.getOrDefault(name, EasingType.LINEAR); + } + + // ---> Easing Transition Type Functions <--- // + + /** + * Returns an easing function running linearly. Functionally equivalent to no easing + */ + static Double2DoubleFunction linear(Double2DoubleFunction function) { + return function; + } + + /** + * Performs a Catmull-Rom interpolation, used to get smooth interpolated motion between keyframes.
+ * CatmullRom#position + */ + static double catmullRom(double n) { + return (0.5f * (2.0f * (n + 1) + ((n + 2) - n) * 1 + + (2.0f * n - 5.0f * (n + 1) + 4.0f * (n + 2) - (n + 3)) * 1 + + (3.0f * (n + 1) - n - 3.0f * (n + 2) + (n + 3)) * 1)); + } + + /** + * Returns an easing function running forward in time + */ + static Double2DoubleFunction easeIn(Double2DoubleFunction function) { + return function; + } + + /** + * Returns an easing function running backwards in time + */ + static Double2DoubleFunction easeOut(Double2DoubleFunction function) { + return time -> 1 - function.apply(1 - time); + } + + /** + * Returns an easing function that runs equally both forwards and backwards in time based on the halfway point, generating a symmetrical curve.
+ */ + static Double2DoubleFunction easeInOut(Double2DoubleFunction function) { + return time -> { + if (time < 0.5d) + return function.apply(time * 2d) / 2d; + + return 1 - function.apply((1 - time) * 2d) / 2d; + }; + } + + // ---> Stepping Functions <--- // + + /** + * Returns a stepping function that returns 1 for any input value greater than 0, or otherwise returning 0 + */ + static Double2DoubleFunction stepPositive(Double2DoubleFunction function) { + return n -> n > 0 ? 1 : 0; + } + + /** + * Returns a stepping function that returns 1 for any input value greater than or equal to 0, or otherwise returning 0 + */ + static Double2DoubleFunction stepNonNegative(Double2DoubleFunction function) { + return n -> n >= 0 ? 1 : 0; + } + + // ---> Mathematical Functions <--- // + + /** + * A linear function, equivalent to a null-operation.
+ * {@code f(n) = n} + */ + static double linear(double n) { + return n; + } + + /** + * A quadratic function, equivalent to the square (n^2) of elapsed time.
+ * {@code f(n) = n^2}
+ * Easings.net#easeInQuad + */ + static double quadratic(double n) { + return n * n; + } + + /** + * A cubic function, equivalent to cube (n^3) of elapsed time.
+ * {@code f(n) = n^3}
+ * Easings.net#easeInCubic + */ + static double cubic(double n) { + return n * n * n; + } + + /** + * A sinusoidal function, equivalent to a sine curve output.
+ * {@code f(n) = 1 - cos(n * π / 2)}
+ * Easings.net#easeInSine + */ + static double sine(double n) { + return 1 - Math.cos(n * Math.PI / 2f); + } + + /** + * A circular function, equivalent to a normally symmetrical curve.
+ * {@code f(n) = 1 - sqrt(1 - n^2)}
+ * Easings.net#easeInCirc + */ + static double circle(double n) { + return 1 - Math.sqrt(1 - n * n); + } + + /** + * An exponential function, equivalent to an exponential curve.
+ * {@code f(n) = 2^(10 * (n - 1))}
+ * Easings.net#easeInExpo + */ + static double exp(double n) { + return Math.pow(2, 10 * (n - 1)); + } + + // ---> Easing Curve Functions <--- // + + /** + * An elastic function, equivalent to an oscillating curve.
+ * n defines the elasticity of the output.
+ * {@code f(t) = 1 - (cos(t * π) / 2))^3 * cos(t * n * π)}
+ * Easings.net#easeInElastic + */ + static Double2DoubleFunction elastic(Double n) { + double n2 = n == null ? 1 : n; + + return t -> 1 - Math.pow(Math.cos(t * Math.PI / 2f), 3) * Math.cos(t * n2 * Math.PI); + } + + /** + * A bouncing function, equivalent to a bouncing ball curve.
+ * n defines the bounciness of the output.
+ * Thanks to Waterded#6455 for making the bounce adjustable, and GiantLuigi4#6616 for additional cleanup.
+ * Easings.net#easeInBounce + */ + static Double2DoubleFunction bounce(Double n) { + final double n2 = n == null ? 0.5d : n; + + Double2DoubleFunction one = x -> 121f / 16f * x * x; + Double2DoubleFunction two = x -> 121f / 4f * n2 * Math.pow(x - 6f / 11f, 2) + 1 - n2; + Double2DoubleFunction three = x -> 121 * n2 * n2 * Math.pow(x - 9f / 11f, 2) + 1 - n2 * n2; + Double2DoubleFunction four = x -> 484 * n2 * n2 * n2 * Math.pow(x - 10.5f / 11f, 2) + 1 - n2 * n2 * n2; + + return t -> Math.min(Math.min(one.apply(t), two.apply(t)), Math.min(three.apply(t), four.apply(t))); + } + + /** + * A negative elastic function, equivalent to inverting briefly before increasing.
+ * f(t) = t^2 * ((n * 1.70158 + 1) * t - n * 1.70158)
+ * Easings.net#easeInBack + */ + static Double2DoubleFunction back(Double n) { + final double n2 = n == null ? 1.70158d : n * 1.70158d; + + return t -> t * t * ((n2 + 1) * t - n2); + } + + /** + * An exponential function, equivalent to an exponential curve to the {@code n} root.
+ * f(t) = t^n + * @param n The exponent + */ + static Double2DoubleFunction pow(double n) { + return t -> Math.pow(t, n); + } + + // The MIT license notice below applies to the function step + /** + * The MIT License (MIT) + *

+ * Copyright (c) 2015 Boris Chumichev + *

+ * 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. + *

+ * Returns a stepped value based on the nearest step to the input value.
+ * The size (grade) of the steps depends on the provided value of {@code n} + **/ + static Double2DoubleFunction step(Double n) { + double n2 = n == null ? 2 : n; + + if (n2 < 2) + throw new IllegalArgumentException("Steps must be >= 2, got: " + n2); + + final int steps = (int)n2; + + return t -> { + double result = 0; + + if (t < 0) + return result; + + double stepLength = (1 / (double)steps); + + if (t > (result = (steps - 1) * stepLength)) + return result; + + int testIndex; + int leftBorderIndex = 0; + int rightBorderIndex = steps - 1; + + while (rightBorderIndex - leftBorderIndex != 1) { + testIndex = leftBorderIndex + (rightBorderIndex - leftBorderIndex) / 2; + + if (t >= testIndex * stepLength) { + leftBorderIndex = testIndex; + } + else { + rightBorderIndex = testIndex; + } + } + + return leftBorderIndex * stepLength; + }; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animation/RawAnimation.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animation/RawAnimation.java new file mode 100644 index 0000000..90bed59 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/animation/RawAnimation.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2020. + * Author: Bernie G. (Gecko) + */ + +package mod.azure.azurelib.common.internal.common.core.animation; + +import it.unimi.dsi.fastutil.objects.ObjectArrayList; + +import java.util.List; +import java.util.Objects; + +/** + * A builder class for a raw/unbaked animation. These are constructed to pass to the + * {@link AnimationController} to build into full-fledged animations for usage. + *

+ * Animations added to this builder are added in order of insertion - the animations will play in the order that you define them.
+ * RawAnimation instances should be cached statically where possible to reduce overheads and improve efficiency. + *

+ * Example usage:
+ *
{@code RawAnimation.begin().thenPlay("action.open_box").thenLoop("state.stay_open")}
+ */ +public final class RawAnimation { + private final List animationList = new ObjectArrayList<>(); + + // Private constructor to force usage of factory for logical operations + private RawAnimation() {} + + /** + * Start a new RawAnimation instance. This is the start point for creating an animation chain. + * @return A new RawAnimation instance + */ + public static RawAnimation begin() { + return new RawAnimation(); + } + + /** + * Append an animation to the animation chain, playing the named animation and stopping + * or progressing to the next chained animation depending on the loop type set in the animation json + * @param animationName The name of the animation to play once + */ + public RawAnimation thenPlay(String animationName) { + return then(animationName, Animation.LoopType.DEFAULT); + } + + /** + * Append an animation to the animation chain, playing the named animation and repeating it continuously until the animation is stopped by external sources. + * @param animationName The name of the animation to play on a loop + */ + public RawAnimation thenLoop(String animationName) { + return then(animationName, Animation.LoopType.LOOP); + } + + /** + * Appends a 'wait' animation to the animation chain.
+ * This causes the animatable to do nothing for a set period of time before performing the next animation. + * @param ticks The number of ticks to 'wait' for + */ + public RawAnimation thenWait(int ticks) { + this.animationList.add(new Stage(Stage.WAIT, Animation.LoopType.PLAY_ONCE, ticks)); + + return this; + } + + /** + * Appends an animation to the animation chain, then has the animatable hold the pose at the end of the + * animation until it is stopped by external sources. + * @param animation The name of the animation to play and hold + */ + public RawAnimation thenPlayAndHold(String animation) { + return then(animation, Animation.LoopType.HOLD_ON_LAST_FRAME); + } + + /** + * Append an animation to the animation chain, playing the named animation playCount times, + * then stopping or progressing to the next chained animation depending on the loop type set in the animation json + * @param animationName The name of the animation to play X times + * @param playCount The number of times to repeat the animation before proceeding + */ + public RawAnimation thenPlayXTimes(String animationName, int playCount) { + for (int i = 0; i < playCount; i++) { + then(animationName, i == playCount - 1 ? Animation.LoopType.DEFAULT : Animation.LoopType.PLAY_ONCE); + } + + return this; + } + + /** + * Append an animation to the animation chain, playing the named animation and proceeding based on the loopType parameter provided. + * @param animationName The name of the animation to play. MUST match the name of the animation in the .animation.json file. + * @param loopType The loop type handler for the animation, overriding the default value set in the animation json + */ + public RawAnimation then(String animationName, Animation.LoopType loopType) { + this.animationList.add(new Stage(animationName, loopType)); + + return this; + } + + public List getAnimationStages() { + return this.animationList; + } + + /** + * Create a new RawAnimation instance based on an existing RawAnimation instance. + * The new instance will be a shallow copy of the other instance, and can then be appended to or otherwise modified + * @param other The existing RawAnimation instance to copy + * @return A new instance of RawAnimation + */ + public static RawAnimation copyOf(RawAnimation other) { + RawAnimation newInstance = RawAnimation.begin(); + + newInstance.animationList.addAll(other.animationList); + + return newInstance; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + + if (obj == null || getClass() != obj.getClass()) + return false; + + return hashCode() == obj.hashCode(); + } + + @Override + public int hashCode() { + return Objects.hash(this.animationList); + } + + /** + * An animation stage for a {@link RawAnimation} builder.
+ * This is an entry object representing a single animation stage of the final compiled animation. + */ + public record Stage(String animationName, Animation.LoopType loopType, int additionalTicks) { + static final String WAIT = "internal.wait"; + + public Stage(String animationName, Animation.LoopType loopType) { + this(animationName, loopType, 0); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + + if (obj == null || getClass() != obj.getClass()) + return false; + + return hashCode() == obj.hashCode(); + } + + @Override + public int hashCode() { + return Objects.hash(this.animationName, this.loopType); + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/AnimationPoint.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/AnimationPoint.java new file mode 100644 index 0000000..61096a2 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/AnimationPoint.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2020. + * Author: Bernie G. (Gecko) + */ + +package mod.azure.azurelib.common.internal.common.core.keyframe; + +/** + * Animation state record that holds the state of an animation at a given point + * @param currentTick The lerped tick time (current tick + partial tick) of the point + * @param transitionLength The length of time (in ticks) that the point should take to transition + * @param animationStartValue The start value to provide to the animation handling system + * @param animationEndValue The end value to provide to the animation handling system + * @param keyFrame The {@code Nullable} Keyframe + */ +public record AnimationPoint(Keyframe keyFrame, double currentTick, double transitionLength, double animationStartValue, double animationEndValue) { + @Override + public String toString() { + return "Tick: " + this.currentTick + + " | Transition Length: " + this.transitionLength + + " | Start Value: " + this.animationStartValue + + " | End Value: " + this.animationEndValue; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/AnimationPointQueue.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/AnimationPointQueue.java new file mode 100644 index 0000000..226fa28 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/AnimationPointQueue.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2020. + * Author: Bernie G. (Gecko) + */ + +package mod.azure.azurelib.common.internal.common.core.keyframe; + +import mod.azure.azurelib.common.internal.common.core.animation.AnimationController; + +import java.io.Serial; +import java.util.LinkedList; + +/** + * An {@link AnimationPoint} queue holds a queue of {@code AnimationPoints} which are used in + * the {@link AnimationController} to lerp between values + */ +public final class AnimationPointQueue extends LinkedList { + @Serial + private static final long serialVersionUID = 5472797438476621193L; +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/BoneAnimation.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/BoneAnimation.java new file mode 100644 index 0000000..4a1d045 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/BoneAnimation.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2020. + * Author: Bernie G. (Gecko) + */ + +package mod.azure.azurelib.common.internal.common.core.keyframe; + +import mod.azure.azurelib.common.internal.common.core.math.IValue; + +/** + * A record of a deserialized animation for a given bone.
+ * Responsible for holding the various {@link Keyframe Keyframes} for the bone's animation transformations + * + * @param boneName The name of the bone as listed in the {@code animation.json} + * @param rotationKeyFrames The deserialized rotation {@code Keyframe} stack + * @param positionKeyFrames The deserialized position {@code Keyframe} stack + * @param scaleKeyFrames The deserialized scale {@code Keyframe} stack + */ +public record BoneAnimation(String boneName, + KeyframeStack> rotationKeyFrames, + KeyframeStack> positionKeyFrames, + KeyframeStack> scaleKeyFrames) { +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/BoneAnimationQueue.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/BoneAnimationQueue.java new file mode 100644 index 0000000..262389f --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/BoneAnimationQueue.java @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2020. + * Author: Bernie G. (Gecko) + */ + +package mod.azure.azurelib.common.internal.common.core.keyframe; + +import mod.azure.azurelib.common.internal.common.core.animation.AnimationController; +import mod.azure.azurelib.common.internal.common.core.animatable.model.CoreGeoBone; +import mod.azure.azurelib.common.internal.common.core.state.BoneSnapshot; + +/** + * A bone pseudo-stack for bone animation positions, scales, and rotations. + * Animation points are calculated then pushed onto their respective queues to be used for transformations in rendering + */ +public record BoneAnimationQueue(CoreGeoBone bone, AnimationPointQueue rotationXQueue, AnimationPointQueue rotationYQueue, + AnimationPointQueue rotationZQueue, AnimationPointQueue positionXQueue, AnimationPointQueue positionYQueue, + AnimationPointQueue positionZQueue, AnimationPointQueue scaleXQueue, AnimationPointQueue scaleYQueue, + AnimationPointQueue scaleZQueue) { + public BoneAnimationQueue(CoreGeoBone bone) { + this(bone, new AnimationPointQueue(), new AnimationPointQueue(), new AnimationPointQueue(), + new AnimationPointQueue(), new AnimationPointQueue(), new AnimationPointQueue(), + new AnimationPointQueue(), new AnimationPointQueue(), new AnimationPointQueue()); + } + + /** + * Add a new {@link AnimationPoint} to the {@link BoneAnimationQueue#positionXQueue} + * @param keyFrame The {@code Nullable} Keyframe relevant to the animation point + * @param lerpedTick The lerped time (current tick + partial tick) that the point starts at + * @param transitionLength The length of the transition (based on the {@link AnimationController}) + * @param startValue The value of the point at the start of its transition + * @param endValue The value of the point at the end of its transition + */ + public void addPosXPoint(Keyframe keyFrame, double lerpedTick, double transitionLength, double startValue, double endValue) { + this.positionXQueue.add(new AnimationPoint(keyFrame, lerpedTick, transitionLength, startValue, endValue)); + } + + /** + * Add a new {@link AnimationPoint} to the {@link BoneAnimationQueue#positionYQueue} + * @param keyFrame The {@code Nullable} Keyframe relevant to the animation point + * @param lerpedTick The lerped time (current tick + partial tick) that the point starts at + * @param transitionLength The length of the transition (based on the {@link AnimationController}) + * @param startValue The value of the point at the start of its transition + * @param endValue The value of the point at the end of its transition + */ + public void addPosYPoint(Keyframe keyFrame, double lerpedTick, double transitionLength, double startValue, double endValue) { + this.positionYQueue.add(new AnimationPoint(keyFrame, lerpedTick, transitionLength, startValue, endValue)); + } + + /** + * Add a new {@link AnimationPoint} to the {@link BoneAnimationQueue#positionZQueue} + * @param keyFrame The {@code Nullable} Keyframe relevant to the animation point + * @param lerpedTick The lerped time (current tick + partial tick) that the point starts at + * @param transitionLength The length of the transition (based on the {@link AnimationController}) + * @param startValue The value of the point at the start of its transition + * @param endValue The value of the point at the end of its transition + */ + public void addPosZPoint(Keyframe keyFrame, double lerpedTick, double transitionLength, double startValue, double endValue) { + this.positionZQueue.add(new AnimationPoint(keyFrame, lerpedTick, transitionLength, startValue, endValue)); + } + + /** + * Add a new X, Y, and Z position {@link AnimationPoint} to their respective queues + * @param keyFrame The {@code Nullable} Keyframe relevant to the animation point + * @param lerpedTick The lerped time (current tick + partial tick) that the point starts at + * @param transitionLength The length of the transition (base on the {@link AnimationController} + * @param startSnapshot The {@link BoneSnapshot} that serves as the starting positions relevant to the keyframe provided + * @param nextXPoint The X {@code AnimationPoint} that is next in the queue, to serve as the end value of the new point + * @param nextYPoint The Y {@code AnimationPoint} that is next in the queue, to serve as the end value of the new point + * @param nextZPoint The Z {@code AnimationPoint} that is next in the queue, to serve as the end value of the new point + */ + public void addNextPosition(Keyframe keyFrame, double lerpedTick, double transitionLength, BoneSnapshot startSnapshot, AnimationPoint nextXPoint, AnimationPoint nextYPoint, AnimationPoint nextZPoint) { + addPosXPoint(keyFrame, lerpedTick, transitionLength, startSnapshot.getOffsetX(), nextXPoint.animationStartValue()); + addPosYPoint(keyFrame, lerpedTick, transitionLength, startSnapshot.getOffsetY(), nextYPoint.animationStartValue()); + addPosZPoint(keyFrame, lerpedTick, transitionLength, startSnapshot.getOffsetZ(), nextZPoint.animationStartValue()); + } + + /** + * Add a new {@link AnimationPoint} to the {@link BoneAnimationQueue#scaleXQueue} + * @param keyFrame The {@code Nullable} Keyframe relevant to the animation point + * @param lerpedTick The lerped time (current tick + partial tick) that the point starts at + * @param transitionLength The length of the transition (based on the {@link AnimationController}) + * @param startValue The value of the point at the start of its transition + * @param endValue The value of the point at the end of its transition + */ + public void addScaleXPoint(Keyframe keyFrame, double lerpedTick, double transitionLength, double startValue, double endValue) { + this.scaleXQueue.add(new AnimationPoint(keyFrame, lerpedTick, transitionLength, startValue, endValue)); + } + + /** + * Add a new {@link AnimationPoint} to the {@link BoneAnimationQueue#scaleYQueue} + * @param keyFrame The {@code Nullable} Keyframe relevant to the animation point + * @param lerpedTick The lerped time (current tick + partial tick) that the point starts at + * @param transitionLength The length of the transition (based on the {@link AnimationController}) + * @param startValue The value of the point at the start of its transition + * @param endValue The value of the point at the end of its transition + */ + public void addScaleYPoint(Keyframe keyFrame, double lerpedTick, double transitionLength, double startValue, double endValue) { + this.scaleYQueue.add(new AnimationPoint(keyFrame, lerpedTick, transitionLength, startValue, endValue)); + } + + /** + * Add a new {@link AnimationPoint} to the {@link BoneAnimationQueue#scaleZQueue} + * @param keyFrame The {@code Nullable} Keyframe relevant to the animation point + * @param lerpedTick The lerped time (current tick + partial tick) that the point starts at + * @param transitionLength The length of the transition (based on the {@link AnimationController}) + * @param startValue The value of the point at the start of its transition + * @param endValue The value of the point at the end of its transition + */ + public void addScaleZPoint(Keyframe keyFrame, double lerpedTick, double transitionLength, double startValue, double endValue) { + this.scaleZQueue.add(new AnimationPoint(keyFrame, lerpedTick, transitionLength, startValue, endValue)); + } + + /** + * Add a new X, Y, and Z scale {@link AnimationPoint} to their respective queues + * @param keyFrame The {@code Nullable} Keyframe relevant to the animation point + * @param lerpedTick The lerped time (current tick + partial tick) that the point starts at + * @param transitionLength The length of the transition (base on the {@link AnimationController} + * @param startSnapshot The {@link BoneSnapshot} that serves as the starting scales relevant to the keyframe provided + * @param nextXPoint The X {@code AnimationPoint} that is next in the queue, to serve as the end value of the new point + * @param nextYPoint The Y {@code AnimationPoint} that is next in the queue, to serve as the end value of the new point + * @param nextZPoint The Z {@code AnimationPoint} that is next in the queue, to serve as the end value of the new point + */ + public void addNextScale(Keyframe keyFrame, double lerpedTick, double transitionLength, BoneSnapshot startSnapshot, AnimationPoint nextXPoint, AnimationPoint nextYPoint, AnimationPoint nextZPoint) { + addScaleXPoint(keyFrame, lerpedTick, transitionLength, startSnapshot.getScaleX(), nextXPoint.animationStartValue()); + addScaleYPoint(keyFrame, lerpedTick, transitionLength, startSnapshot.getScaleY(), nextYPoint.animationStartValue()); + addScaleZPoint(keyFrame, lerpedTick, transitionLength, startSnapshot.getScaleZ(), nextZPoint.animationStartValue()); + } + + /** + * Add a new {@link AnimationPoint} to the {@link BoneAnimationQueue#rotationXQueue} + * @param keyFrame The {@code Nullable} Keyframe relevant to the animation point + * @param lerpedTick The lerped time (current tick + partial tick) that the point starts at + * @param transitionLength The length of the transition (based on the {@link AnimationController}) + * @param startValue The value of the point at the start of its transition + * @param endValue The value of the point at the end of its transition + */ + public void addRotationXPoint(Keyframe keyFrame, double lerpedTick, double transitionLength, double startValue, double endValue) { + this.rotationXQueue.add(new AnimationPoint(keyFrame, lerpedTick, transitionLength, startValue, endValue)); + } + + /** + * Add a new {@link AnimationPoint} to the {@link BoneAnimationQueue#rotationYQueue} + * @param keyFrame The {@code Nullable} Keyframe relevant to the animation point + * @param lerpedTick The lerped time (current tick + partial tick) that the point starts at + * @param transitionLength The length of the transition (based on the {@link AnimationController}) + * @param startValue The value of the point at the start of its transition + * @param endValue The value of the point at the end of its transition + */ + public void addRotationYPoint(Keyframe keyFrame, double lerpedTick, double transitionLength, double startValue, double endValue) { + this.rotationYQueue.add(new AnimationPoint(keyFrame, lerpedTick, transitionLength, startValue, endValue)); + } + + /** + * Add a new {@link AnimationPoint} to the {@link BoneAnimationQueue#rotationZQueue} + * @param keyFrame The {@code Nullable} Keyframe relevant to the animation point + * @param lerpedTick The lerped time (current tick + partial tick) that the point starts at + * @param transitionLength The length of the transition (based on the {@link AnimationController}) + * @param startValue The value of the point at the start of its transition + * @param endValue The value of the point at the end of its transition + */ + public void addRotationZPoint(Keyframe keyFrame, double lerpedTick, double transitionLength, double startValue, double endValue) { + this.rotationZQueue.add(new AnimationPoint(keyFrame, lerpedTick, transitionLength, startValue, endValue)); + } + + /** + * Add a new X, Y, and Z scale {@link AnimationPoint} to their respective queues + * @param keyFrame The {@code Nullable} Keyframe relevant to the animation point + * @param lerpedTick The lerped time (current tick + partial tick) that the point starts at + * @param transitionLength The length of the transition (base on the {@link AnimationController} + * @param startSnapshot The {@link BoneSnapshot} that serves as the starting rotations relevant to the keyframe provided + * @param initialSnapshot The {@link BoneSnapshot} that serves as the unmodified rotations of the bone + * @param nextXPoint The X {@code AnimationPoint} that is next in the queue, to serve as the end value of the new point + * @param nextYPoint The Y {@code AnimationPoint} that is next in the queue, to serve as the end value of the new point + * @param nextZPoint The Z {@code AnimationPoint} that is next in the queue, to serve as the end value of the new point + */ + public void addNextRotation(Keyframe keyFrame, double lerpedTick, double transitionLength, BoneSnapshot startSnapshot, BoneSnapshot initialSnapshot, AnimationPoint nextXPoint, AnimationPoint nextYPoint, AnimationPoint nextZPoint) { + addRotationXPoint(keyFrame, lerpedTick, transitionLength, startSnapshot.getRotX() - initialSnapshot.getRotX(), nextXPoint.animationStartValue()); + addRotationYPoint(keyFrame, lerpedTick, transitionLength, startSnapshot.getRotY() - initialSnapshot.getRotY(), nextYPoint.animationStartValue()); + addRotationZPoint(keyFrame, lerpedTick, transitionLength, startSnapshot.getRotZ() - initialSnapshot.getRotZ(), nextZPoint.animationStartValue()); + } + + /** + * Add an X, Y, and Z position {@link AnimationPoint} to their respective queues + * @param xPoint The x position {@code AnimationPoint} to add + * @param yPoint The y position {@code AnimationPoint} to add + * @param zPoint The z position {@code AnimationPoint} to add + */ + public void addPositions(AnimationPoint xPoint, AnimationPoint yPoint, AnimationPoint zPoint) { + this.positionXQueue.add(xPoint); + this.positionYQueue.add(yPoint); + this.positionZQueue.add(zPoint); + } + + /** + * Add an X, Y, and Z scale {@link AnimationPoint} to their respective queues + * @param xPoint The x scale {@code AnimationPoint} to add + * @param yPoint The y scale {@code AnimationPoint} to add + * @param zPoint The z scale {@code AnimationPoint} to add + */ + public void addScales(AnimationPoint xPoint, AnimationPoint yPoint, AnimationPoint zPoint) { + this.scaleXQueue.add(xPoint); + this.scaleYQueue.add(yPoint); + this.scaleZQueue.add(zPoint); + } + + /** + * Add an X, Y, and Z rotation {@link AnimationPoint} to their respective queues + * @param xPoint The x rotation {@code AnimationPoint} to add + * @param yPoint The y rotation {@code AnimationPoint} to add + * @param zPoint The z rotation {@code AnimationPoint} to add + */ + public void addRotations(AnimationPoint xPoint, AnimationPoint yPoint, AnimationPoint zPoint) { + this.rotationXQueue.add(xPoint); + this.rotationYQueue.add(yPoint); + this.rotationZQueue.add(zPoint); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/Keyframe.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/Keyframe.java new file mode 100644 index 0000000..ce3c1a0 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/Keyframe.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2020. + * Author: Bernie G. (Gecko) + */ + +package mod.azure.azurelib.common.internal.common.core.keyframe; + +import java.util.List; +import java.util.Objects; + +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import mod.azure.azurelib.common.internal.common.core.animation.EasingType; +import mod.azure.azurelib.common.internal.common.core.math.IValue; + +/** + * Animation keyframe data + * @param length The length (in ticks) the keyframe lasts for + * @param startValue The value to start the keyframe's transformation with + * @param endValue The value to end the keyframe's transformation with + * @param easingType The {@code EasingType} to use for transformations + * @param easingArgs The arguments to provide to the easing calculation + */ +public record Keyframe(double length, T startValue, T endValue, EasingType easingType, List easingArgs) { + public Keyframe(double length, T startValue, T endValue) { + this(length, startValue, endValue, EasingType.LINEAR); + } + + public Keyframe(double length, T startValue, T endValue, EasingType easingType) { + this(length, startValue, endValue, easingType, new ObjectArrayList<>(0)); + } + + @Override + public int hashCode() { + return Objects.hash(this.length, this.startValue, this.endValue, this.easingType, this.easingArgs); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + + if (obj == null || getClass() != obj.getClass()) + return false; + + return hashCode() == obj.hashCode(); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/KeyframeLocation.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/KeyframeLocation.java new file mode 100644 index 0000000..12ad0f5 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/KeyframeLocation.java @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2020. + * Author: Bernie G. (Gecko) + */ + +package mod.azure.azurelib.common.internal.common.core.keyframe; + +/** + * A named pair object that stores a {@link Keyframe} and a double representing a temporally placed {@code Keyframe} + * @param keyframe The {@code Keyframe} at the tick time + * @param startTick The animation tick time at the start of this {@code Keyframe} + */ +public record KeyframeLocation>(T keyframe, double startTick) { } +//TODO: public record KeyframeLocation(Keyframe keyframe, double startTick) { } \ No newline at end of file diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/KeyframeStack.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/KeyframeStack.java new file mode 100644 index 0000000..3e32d9e --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/KeyframeStack.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2020. + * Author: Bernie G. (Gecko) + */ + +package mod.azure.azurelib.common.internal.common.core.keyframe; + +import it.unimi.dsi.fastutil.objects.ObjectArrayList; + +import java.util.List; + +/** + * Stores a triplet of {@link Keyframe Keyframes} in an ordered stack + */ +public record KeyframeStack>(List xKeyframes, List yKeyframes, List zKeyframes) { + public KeyframeStack() { + this(new ObjectArrayList<>(), new ObjectArrayList<>(), new ObjectArrayList<>()); + } + + public static > KeyframeStack from(KeyframeStack otherStack) { + return new KeyframeStack<>(otherStack.xKeyframes, otherStack.yKeyframes, otherStack.zKeyframes); + } + + public double getLastKeyframeTime() { + double xTime = 0; + double yTime = 0; + double zTime = 0; + + for (T frame : xKeyframes()) { + xTime += frame.length(); + } + + for (T frame : yKeyframes()) { + yTime += frame.length(); + } + + for (T frame : zKeyframes()) { + zTime += frame.length(); + } + + return Math.max(xTime, Math.max(yTime, zTime)); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/event/CustomInstructionKeyframeEvent.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/event/CustomInstructionKeyframeEvent.java new file mode 100644 index 0000000..fd9f10d --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/event/CustomInstructionKeyframeEvent.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2020. + * Author: Bernie G. (Gecko) + */ + +package mod.azure.azurelib.common.internal.common.core.keyframe.event; + +import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable; +import mod.azure.azurelib.common.internal.common.core.animation.AnimationController; +import mod.azure.azurelib.common.internal.common.core.keyframe.event.data.CustomInstructionKeyframeData; + +/** + * The {@link KeyFrameEvent} specific to the {@link AnimationController#customKeyframeHandler}.
+ * Called when a custom instruction keyframe is encountered + */ +public class CustomInstructionKeyframeEvent extends KeyFrameEvent { + public CustomInstructionKeyframeEvent(T entity, double animationTick, AnimationController controller, + CustomInstructionKeyframeData customInstructionKeyframeData) { + super(entity, animationTick, controller, customInstructionKeyframeData); + } + /** + * Get the {@link CustomInstructionKeyframeData} relevant to this event call + */ + @Override + public CustomInstructionKeyframeData getKeyframeData() { + return super.getKeyframeData(); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/event/KeyFrameEvent.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/event/KeyFrameEvent.java new file mode 100644 index 0000000..49ea6c9 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/event/KeyFrameEvent.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2020. + * Author: Bernie G. (Gecko) + */ + +package mod.azure.azurelib.common.internal.common.core.keyframe.event; + +import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable; +import mod.azure.azurelib.common.internal.common.core.animation.AnimationController; +import mod.azure.azurelib.common.internal.common.core.keyframe.Keyframe; +import mod.azure.azurelib.common.internal.common.core.keyframe.event.data.KeyFrameData; + +/** + * The base class for {@link Keyframe} events.
+ * These will be passed to one of the controllers in {@link AnimationController} when encountered during animation. + * @see CustomInstructionKeyframeEvent + * @see ParticleKeyframeEvent + * @see SoundKeyframeEvent + */ +public abstract class KeyFrameEvent { + private final T animatable; + private final double animationTick; + private final AnimationController controller; + private final E eventKeyFrame; + + protected KeyFrameEvent(T animatable, double animationTick, AnimationController controller, E eventKeyFrame) { + this.animatable = animatable; + this.animationTick = animationTick; + this.controller = controller; + this.eventKeyFrame = eventKeyFrame; + } + + /** + * Gets the amount of ticks that have passed in either the current transition or + * animation, depending on the controller's AnimationState. + */ + public double getAnimationTick() { + return animationTick; + } + + /** + * Gets the {@link GeoAnimatable} object being rendered + */ + public T getAnimatable() { + return animatable; + } + + /** + * Gets the {@link AnimationController} responsible for the currently playing animation + */ + public AnimationController getController() { + return controller; + } + + /** + * Returns the {@link KeyFrameData} relevant to the encountered {@link Keyframe} + */ + public E getKeyframeData() { + return this.eventKeyFrame; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/event/ParticleKeyframeEvent.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/event/ParticleKeyframeEvent.java new file mode 100644 index 0000000..93778dc --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/event/ParticleKeyframeEvent.java @@ -0,0 +1,23 @@ +package mod.azure.azurelib.common.internal.common.core.keyframe.event; + +import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable; +import mod.azure.azurelib.common.internal.common.core.animation.AnimationController; +import mod.azure.azurelib.common.internal.common.core.keyframe.event.data.ParticleKeyframeData; + +/** + * The {@link KeyFrameEvent} specific to the {@link AnimationController#particleKeyframeHandler}.
+ * Called when a particle instruction keyframe is encountered + */ +public class ParticleKeyframeEvent extends KeyFrameEvent { + public ParticleKeyframeEvent(T animatable, double animationTick, AnimationController controller, ParticleKeyframeData particleKeyFrameData) { + super(animatable, animationTick, controller, particleKeyFrameData); + } + + /** + * Get the {@link ParticleKeyframeData} relevant to this event call + */ + @Override + public ParticleKeyframeData getKeyframeData() { + return super.getKeyframeData(); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/event/SoundKeyframeEvent.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/event/SoundKeyframeEvent.java new file mode 100644 index 0000000..c3130f2 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/event/SoundKeyframeEvent.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020. + * Author: Bernie G. (Gecko) + */ + +package mod.azure.azurelib.common.internal.common.core.keyframe.event; + +import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable; +import mod.azure.azurelib.common.internal.common.core.animation.AnimationController; +import mod.azure.azurelib.common.internal.common.core.keyframe.event.data.SoundKeyframeData; + +/** + * The {@link KeyFrameEvent} specific to the {@link AnimationController#soundKeyframeHandler}.
+ * Called when a sound instruction keyframe is encountered + */ +public class SoundKeyframeEvent extends KeyFrameEvent { + /** + * This stores all the fields that are needed in the AnimationTestEvent + * + * @param entity the entity + * @param animationTick The amount of ticks that have passed in either the + * current transition or animation, depending on the + * controller's AnimationState. + * @param controller the controller + */ + public SoundKeyframeEvent(T entity, double animationTick, AnimationController controller, SoundKeyframeData keyFrameData) { + super(entity, animationTick, controller, keyFrameData); + } + + /** + * Get the {@link SoundKeyframeData} relevant to this event call + */ + @Override + public SoundKeyframeData getKeyframeData() { + return super.getKeyframeData(); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/event/data/CustomInstructionKeyframeData.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/event/data/CustomInstructionKeyframeData.java new file mode 100644 index 0000000..bdc4e24 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/event/data/CustomInstructionKeyframeData.java @@ -0,0 +1,30 @@ +package mod.azure.azurelib.common.internal.common.core.keyframe.event.data; + +import java.util.Objects; + +import mod.azure.azurelib.common.internal.common.core.keyframe.Keyframe; + +/** + * Custom instruction {@link Keyframe} instruction holder + */ +public class CustomInstructionKeyframeData extends KeyFrameData { + private final String instructions; + + public CustomInstructionKeyframeData(double startTick, String instructions) { + super(startTick); + + this.instructions = instructions; + } + + /** + * Gets the instructions string given by the {@link Keyframe} instruction from the {@code animation.json} + */ + public String getInstructions() { + return this.instructions; + } + + @Override + public int hashCode() { + return Objects.hash(getStartTick(), instructions); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/event/data/KeyFrameData.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/event/data/KeyFrameData.java new file mode 100644 index 0000000..9292845 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/event/data/KeyFrameData.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020. + * Author: Bernie G. (Gecko) + */ + +package mod.azure.azurelib.common.internal.common.core.keyframe.event.data; + +import java.util.Objects; + +import mod.azure.azurelib.common.internal.common.core.keyframe.Keyframe; + +/** + * Base class for custom {@link Keyframe} events.
+ * @see ParticleKeyframeData + * @see SoundKeyframeData + */ +public abstract class KeyFrameData { + private final double startTick; + + protected KeyFrameData(double startTick) { + this.startTick = startTick; + } + + /** + * Gets the start tick of the keyframe instruction + */ + public double getStartTick() { + return this.startTick; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + + if (obj == null || getClass() != obj.getClass()) + return false; + + return this.hashCode() == obj.hashCode(); + } + + @Override + public int hashCode() { + return Objects.hashCode(this.startTick); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/event/data/ParticleKeyframeData.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/event/data/ParticleKeyframeData.java new file mode 100644 index 0000000..8ef50b9 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/event/data/ParticleKeyframeData.java @@ -0,0 +1,48 @@ +package mod.azure.azurelib.common.internal.common.core.keyframe.event.data; + +import java.util.Objects; + +import mod.azure.azurelib.common.internal.common.core.keyframe.Keyframe; + +/** + * Particle {@link Keyframe} instruction holder + */ +public class ParticleKeyframeData extends KeyFrameData { + private final String effect; + private final String locator; + private final String script; + + public ParticleKeyframeData(double startTick, String effect, String locator, String script) { + super(startTick); + + this.script = script; + this.locator = locator; + this.effect = effect; + } + + /** + * Gets the effect id given by the {@link Keyframe} instruction from the {@code animation.json} + */ + public String getEffect() { + return this.effect; + } + + /** + * Gets the locator string given by the {@link Keyframe} instruction from the {@code animation.json} + */ + public String getLocator() { + return this.locator; + } + + /** + * Gets the script string given by the {@link Keyframe} instruction from the {@code animation.json} + */ + public String script() { + return this.script; + } + + @Override + public int hashCode() { + return Objects.hash(getStartTick(), effect, locator, script); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/event/data/SoundKeyframeData.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/event/data/SoundKeyframeData.java new file mode 100644 index 0000000..9ad2c7c --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/keyframe/event/data/SoundKeyframeData.java @@ -0,0 +1,30 @@ +package mod.azure.azurelib.common.internal.common.core.keyframe.event.data; + +import java.util.Objects; + +import mod.azure.azurelib.common.internal.common.core.keyframe.Keyframe; + +/** + * Sound {@link Keyframe} instruction holder + */ +public class SoundKeyframeData extends KeyFrameData { + private final String sound; + + public SoundKeyframeData(Double startTick, String sound) { + super(startTick); + + this.sound = sound; + } + + /** + * Gets the sound id given by the {@link Keyframe} instruction from the {@code animation.json} + */ + public String getSound() { + return this.sound; + } + + @Override + public int hashCode() { + return Objects.hash(getStartTick(), this.sound); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/Constant.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/Constant.java new file mode 100644 index 0000000..729a8a9 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/Constant.java @@ -0,0 +1,28 @@ +package mod.azure.azurelib.common.internal.common.core.math; + +/** + * Constant class + * + * This class simply returns supplied in the constructor value + */ +public class Constant implements IValue { + private double value; + + public Constant(double value) { + this.value = value; + } + + @Override + public double get() { + return this.value; + } + + public void set(double value) { + this.value = value; + } + + @Override + public String toString() { + return String.valueOf(this.value); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/Group.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/Group.java new file mode 100644 index 0000000..b9ce756 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/Group.java @@ -0,0 +1,25 @@ +package mod.azure.azurelib.common.internal.common.core.math; + +/** + * Group class + * + * Simply wraps given {@link IValue} into parenthesis in the {@link #toString()} + * method. + */ +public class Group implements IValue { + private IValue value; + + public Group(IValue value) { + this.value = value; + } + + @Override + public double get() { + return this.value.get(); + } + + @Override + public String toString() { + return "(" + this.value.toString() + ")"; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/IValue.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/IValue.java new file mode 100644 index 0000000..b5851be --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/IValue.java @@ -0,0 +1,16 @@ +package mod.azure.azurelib.common.internal.common.core.math; + +/** + * Math value interface + * + * This interface provides only one method which is used by all mathematical + * related classes. The point of this interface is to provide generalized + * abstract method for computing/fetching some value from different mathematical + * classes. + */ +public interface IValue { + /** + * Get computed or stored value + */ + public double get(); +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/MathBuilder.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/MathBuilder.java new file mode 100644 index 0000000..aed941e --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/MathBuilder.java @@ -0,0 +1,533 @@ +package mod.azure.azurelib.common.internal.common.core.math; + +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import mod.azure.azurelib.common.internal.common.core.math.functions.Function; +import mod.azure.azurelib.common.internal.common.core.math.functions.limit.Clamp; +import mod.azure.azurelib.common.internal.common.core.math.functions.limit.Max; +import mod.azure.azurelib.common.internal.common.core.math.functions.limit.Min; +import mod.azure.azurelib.common.internal.common.core.math.functions.classic.ACos; +import mod.azure.azurelib.common.internal.common.core.math.functions.classic.ASin; +import mod.azure.azurelib.common.internal.common.core.math.functions.classic.ATan; +import mod.azure.azurelib.common.internal.common.core.math.functions.classic.ATan2; +import mod.azure.azurelib.common.internal.common.core.math.functions.classic.Abs; +import mod.azure.azurelib.common.internal.common.core.math.functions.classic.Cos; +import mod.azure.azurelib.common.internal.common.core.math.functions.classic.Exp; +import mod.azure.azurelib.common.internal.common.core.math.functions.classic.Ln; +import mod.azure.azurelib.common.internal.common.core.math.functions.classic.Mod; +import mod.azure.azurelib.common.internal.common.core.math.functions.classic.Pow; +import mod.azure.azurelib.common.internal.common.core.math.functions.classic.Sin; +import mod.azure.azurelib.common.internal.common.core.math.functions.classic.Sqrt; +import mod.azure.azurelib.common.internal.common.core.math.functions.rounding.Ceil; +import mod.azure.azurelib.common.internal.common.core.math.functions.rounding.Floor; +import mod.azure.azurelib.common.internal.common.core.math.functions.rounding.Round; +import mod.azure.azurelib.common.internal.common.core.math.functions.rounding.Trunc; +import mod.azure.azurelib.common.internal.common.core.math.functions.utility.DieRoll; +import mod.azure.azurelib.common.internal.common.core.math.functions.utility.DieRollInteger; +import mod.azure.azurelib.common.internal.common.core.math.functions.utility.HermiteBlend; +import mod.azure.azurelib.common.internal.common.core.math.functions.utility.Lerp; +import mod.azure.azurelib.common.internal.common.core.math.functions.utility.LerpRotate; +import mod.azure.azurelib.common.internal.common.core.math.functions.utility.Random; +import mod.azure.azurelib.common.internal.common.core.math.functions.utility.RandomInteger; + +/** + * Math builder + * + * This class is responsible for parsing math expressions provided by user in a + * string to an {@link IValue} which can be used to compute some value + * dynamically using different math operators, variables and functions. + * + * It works by first breaking down given string into a list of tokens and then + * putting them together in a binary tree-like {@link IValue}. + * + * TODO: maybe implement constant pool (to reuse same values)? TODO: maybe + * pre-compute constant expressions? + */ +public class MathBuilder { + /** + * Named variables that can be used in math expression by this builder + */ + public Map variables = new HashMap(); + + /** + * Map of functions which can be used in the math expressions + */ + public Map> functions = new HashMap>(); + + public MathBuilder() { + /* Some default values */ + this.register(new Variable("PI", Math.PI)); + this.register(new Variable("E", Math.E)); + + /* Rounding functions */ + this.functions.put("floor", Floor.class); + this.functions.put("round", Round.class); + this.functions.put("ceil", Ceil.class); + this.functions.put("trunc", Trunc.class); + + /* Selection and limit functions */ + this.functions.put("clamp", Clamp.class); + this.functions.put("max", Max.class); + this.functions.put("min", Min.class); + + /* Classical functions */ + this.functions.put("abs", Abs.class); + this.functions.put("acos", ACos.class); + this.functions.put("asin", ASin.class); + this.functions.put("atan", ATan.class); + this.functions.put("atan2", ATan2.class); + this.functions.put("cos", Cos.class); + this.functions.put("sin", Sin.class); + this.functions.put("exp", Exp.class); + this.functions.put("ln", Ln.class); + this.functions.put("sqrt", Sqrt.class); + this.functions.put("mod", Mod.class); + this.functions.put("pow", Pow.class); + + /* Utility functions */ + this.functions.put("lerp", Lerp.class); + this.functions.put("lerprotate", LerpRotate.class); + this.functions.put("hermite_blend", HermiteBlend.class); + this.functions.put("die_roll", DieRoll.class); + this.functions.put("die_roll_integer", DieRollInteger.class); + this.functions.put("random", Random.class); + this.functions.put("random_integer", RandomInteger.class); + } + + /** + * Register a variable + */ + public void register(Variable variable) { + this.variables.put(variable.getName(), variable); + } + + /** + * Parse given math expression into a {@link IValue} which can be used to + * execute math. + */ + public IValue parse(String expression) throws Exception { + return this.parseSymbols(this.breakdownChars(this.breakdown(expression))); + } + + /** + * Breakdown an expression + */ + public String[] breakdown(String expression) throws Exception { + /* If given string have illegal characters, then it can't be parsed */ + if (!expression.matches("^[\\w\\d\\s_+-/*%^&|<>=!?:.,()]+$")) { + throw new Exception("Given expression '" + expression + "' contains illegal characters!"); + } + + /* Remove all spaces, and leading and trailing parenthesis */ + expression = expression.replaceAll("\\s+", ""); + + String[] chars = expression.split("(?!^)"); + + int left = 0; + int right = 0; + + for (String s : chars) { + if (s.equals("(")) { + left++; + } else if (s.equals(")")) { + right++; + } + } + + /* Amount of left and right brackets should be the same */ + if (left != right) { + throw new Exception("Given expression '" + expression + + "' has more uneven amount of parenthesis, there are " + left + " open and " + right + " closed!"); + } + + return chars; + } + + /** + * Breakdown characters into a list of math expression symbols. + */ + public List breakdownChars(String[] chars) { + List symbols = new ArrayList<>(); + String buffer = ""; + int len = chars.length; + + for (int i = 0; i < len; i++) { + String s = chars[i]; + boolean longOperator = i > 0 && this.isOperator(chars[i - 1] + s); + + if (this.isOperator(s) || longOperator || s.equals(",")) { + /* + * Taking care of a special case of using minus sign to invert the positive + * value + */ + if (s.equals("-")) { + int size = symbols.size(); + + boolean isFirst = size == 0 && buffer.isEmpty(); + boolean isOperatorBehind = size > 0 + && (this.isOperator(symbols.get(size - 1)) || symbols.get(size - 1).equals(",")) + && buffer.isEmpty(); + + if (isFirst || isOperatorBehind) { + buffer += s; + + continue; + } + } + + if (longOperator) { + s = chars[i - 1] + s; + buffer = buffer.substring(0, buffer.length() - 1); + } + + /* Push buffer and operator */ + if (!buffer.isEmpty()) { + symbols.add(buffer); + buffer = ""; + } + + symbols.add(s); + } else if (s.equals("(")) { + /* Push a list of symbols */ + if (!buffer.isEmpty()) { + symbols.add(buffer); + buffer = ""; + } + + int counter = 1; + + for (int j = i + 1; j < len; j++) { + String c = chars[j]; + + if (c.equals("(")) { + counter++; + } else if (c.equals(")")) { + counter--; + } + + if (counter == 0) { + symbols.add(this.breakdownChars(buffer.split("(?!^)"))); + + i = j; + buffer = ""; + + break; + } else { + buffer += c; + } + } + } else { + /* Accumulate the buffer */ + buffer += s; + } + } + + if (!buffer.isEmpty()) { + symbols.add(buffer); + } + + return symbols; + } + + /** + * Parse symbols + * + * This function is the most important part of this class. It's responsible for + * turning list of symbols into {@link IValue}. This is done by constructing a + * binary tree-like {@link IValue} based on {@link Operator} class. + * + * However, beside parsing operations, it's also can return one or two item + * sized symbol lists. + */ + public IValue parseSymbols(List symbols) throws Exception { + IValue ternary = this.tryTernary(symbols); + + if (ternary != null) { + return ternary; + } + + int size = symbols.size(); + + /* Constant, variable or group (parenthesis) */ + if (size == 1) { + return this.valueFromObject(symbols.get(0)); + } + + /* Function */ + if (size == 2) { + Object first = symbols.get(0); + Object second = symbols.get(1); + + if ((this.isVariable(first) || first.equals("-")) && second instanceof List) { + return this.createFunction((String) first, (List) second); + } + } + + /* Any other math expression */ + int lastOp = this.seekLastOperator(symbols); + int op = lastOp; + + while (op != -1) { + int leftOp = this.seekLastOperator(symbols, op - 1); + + if (leftOp != -1) { + Operation left = this.operationForOperator((String) symbols.get(leftOp)); + Operation right = this.operationForOperator((String) symbols.get(op)); + + if (right.value > left.value) { + IValue leftValue = this.parseSymbols(symbols.subList(0, leftOp)); + IValue rightValue = this.parseSymbols(symbols.subList(leftOp + 1, size)); + + return new Operator(left, leftValue, rightValue); + } else if (left.value > right.value) { + Operation initial = this.operationForOperator((String) symbols.get(lastOp)); + + if (initial.value < left.value) { + IValue leftValue = this.parseSymbols(symbols.subList(0, lastOp)); + IValue rightValue = this.parseSymbols(symbols.subList(lastOp + 1, size)); + + return new Operator(initial, leftValue, rightValue); + } + + IValue leftValue = this.parseSymbols(symbols.subList(0, op)); + IValue rightValue = this.parseSymbols(symbols.subList(op + 1, size)); + + return new Operator(right, leftValue, rightValue); + } + } + + op = leftOp; + } + + Operation operation = this.operationForOperator((String) symbols.get(lastOp)); + + return new Operator(operation, this.parseSymbols(symbols.subList(0, lastOp)), + this.parseSymbols(symbols.subList(lastOp + 1, size))); + } + + protected int seekLastOperator(List symbols) { + return this.seekLastOperator(symbols, symbols.size() - 1); + } + + /** + * Find the index of the first operator + */ + protected int seekLastOperator(List symbols, int offset) { + for (int i = offset; i >= 0; i--) { + Object o = symbols.get(i); + + if (this.isOperator(o)) { + return i; + } + } + + return -1; + } + + protected int seekFirstOperator(List symbols) { + return this.seekFirstOperator(symbols, 0); + } + + /** + * Find the index of the first operator + */ + protected int seekFirstOperator(List symbols, int offset) { + for (int i = offset, size = symbols.size(); i < size; i++) { + Object o = symbols.get(i); + + if (this.isOperator(o)) { + return i; + } + } + + return -1; + } + + /** + * Try parsing a ternary expression + * + * From what we know, with ternary expressions, we should have only one ? and :, + * and some elements from beginning till ?, in between ? and :, and also some + * remaining elements after :. + */ + protected IValue tryTernary(List symbols) throws Exception { + int question = -1; + int questions = 0; + int colon = -1; + int colons = 0; + int size = symbols.size(); + + for (int i = 0; i < size; i++) { + Object object = symbols.get(i); + + if (object instanceof String) { + if (object.equals("?")) { + if (question == -1) { + question = i; + } + + questions++; + } else if (object.equals(":")) { + if (colons + 1 == questions && colon == -1) { + colon = i; + } + + colons++; + } + } + } + + if (questions == colons && question > 0 && question + 1 < colon && colon < size - 1) { + return new Ternary(this.parseSymbols(symbols.subList(0, question)), + this.parseSymbols(symbols.subList(question + 1, colon)), + this.parseSymbols(symbols.subList(colon + 1, size))); + } + + return null; + } + + /** + * Create a function value + * + * This method in comparison to {@link #valueFromObject(Object)} needs the name + * of the function and list of args (which can't be stored in one object). + * + * This method will constructs {@link IValue}s from list of args mixed with + * operators, groups, values and commas. And then plug it in to a class + * constructor with given name. + */ + protected IValue createFunction(String first, List args) throws Exception { + /* Handle special cases with negation */ + if (first.equals("!")) { + return new Negate(this.parseSymbols(args)); + } + + if (first.startsWith("!") && first.length() > 1) { + return new Negate(this.createFunction(first.substring(1), args)); + } + + /* Handle inversion of the value */ + if (first.equals("-")) { + return new Negative(new Group(this.parseSymbols(args))); + } + + if (first.startsWith("-") && first.length() > 1) { + return new Negative(this.createFunction(first.substring(1), args)); + } + + if (!this.functions.containsKey(first)) { + throw new Exception("Function '" + first + "' couldn't be found!"); + } + + List values = new ArrayList<>(); + List buffer = new ArrayList<>(); + + for (Object o : args) { + if (o.equals(",")) { + values.add(this.parseSymbols(buffer)); + buffer.clear(); + } else { + buffer.add(o); + } + } + + if (!buffer.isEmpty()) { + values.add(this.parseSymbols(buffer)); + } + + Class function = this.functions.get(first); + Constructor ctor = function.getConstructor(IValue[].class, String.class); + return ctor.newInstance(values.toArray(new IValue[values.size()]), first); + } + + /** + * Get value from an object. + * + * This method is responsible for creating different sort of values based on the + * input object. It can create constants, variables and groups. + */ + public IValue valueFromObject(Object object) throws Exception { + if (object instanceof List) { + return new Group(this.parseSymbols((List) object)); + } + + if (object instanceof String symbol) { + /* Variable and constant negation */ + if (symbol.startsWith("!")) { + return new Negate(this.valueFromObject(symbol.substring(1))); + } + + if (this.isDecimal(symbol)) { + return new Constant(Double.parseDouble(symbol)); + } else if (this.isVariable(symbol)) { + /* Need to account for a negative value variable */ + if (symbol.startsWith("-")) { + symbol = symbol.substring(1); + Variable value = this.getVariable(symbol); + + if (value != null) { + return new Negative(value); + } + } else { + IValue value = this.getVariable(symbol); + + /* Avoid NPE */ + if (value != null) { + return value; + } + } + } + } + + throw new Exception("Given object couldn't be converted to value! " + object); + } + + /** + * Get variable + */ + protected Variable getVariable(String name) { + return this.variables.get(name); + } + + /** + * Get operation for given operator strings + */ + protected Operation operationForOperator(String op) throws Exception { + for (Operation operation : Operation.values()) { + if (operation.sign.equals(op)) { + return operation; + } + } + + throw new Exception("There is no such operator '" + op + "'!"); + } + + /** + * Whether given object is a variable + */ + protected boolean isVariable(Object o) { + return o instanceof String string && !this.isDecimal((String) o) && !this.isOperator(string); + } + + protected boolean isOperator(Object o) { + return o instanceof String string && this.isOperator(string); + } + + /** + * Whether string is an operator + */ + protected boolean isOperator(String s) { + return Operation.OPERATORS.contains(s) || s.equals("?") || s.equals(":"); + } + + /** + * Whether string is numeric (including whether it's a floating number) + */ + protected boolean isDecimal(String s) { + return s.matches("^-?\\d+(\\.\\d+)?$"); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/Negate.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/Negate.java new file mode 100644 index 0000000..95697b4 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/Negate.java @@ -0,0 +1,24 @@ +package mod.azure.azurelib.common.internal.common.core.math; + +/** + * Negate operator class + * + * This class is responsible for negating given value + */ +public class Negate implements IValue { + public IValue value; + + public Negate(IValue value) { + this.value = value; + } + + @Override + public double get() { + return this.value.get() == 0 ? 1 : 0; + } + + @Override + public String toString() { + return "!" + this.value.toString(); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/Negative.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/Negative.java new file mode 100644 index 0000000..17e5f97 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/Negative.java @@ -0,0 +1,24 @@ +package mod.azure.azurelib.common.internal.common.core.math; + +/** + * Negative operator class + * + * This class is responsible for inverting given value + */ +public class Negative implements IValue { + public IValue value; + + public Negative(IValue value) { + this.value = value; + } + + @Override + public double get() { + return -this.value.get(); + } + + @Override + public String toString() { + return "-" + this.value.toString(); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/Operation.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/Operation.java new file mode 100644 index 0000000..13c4c6b --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/Operation.java @@ -0,0 +1,134 @@ +package mod.azure.azurelib.common.internal.common.core.math; + +import java.util.HashSet; +import java.util.Set; + +/** + * Operation enumeration + * + * This enumeration provides different hardcoded enumerations of default math + * operators such addition, substraction, multiplication, division, modulo and + * power. + * + * TODO: maybe convert to classes (for the sake of API)? + */ +public enum Operation { + ADD("+", 1) { + @Override + public double calculate(double a, double b) { + return a + b; + } + }, + SUB("-", 1) { + @Override + public double calculate(double a, double b) { + return a - b; + } + }, + MUL("*", 2) { + @Override + public double calculate(double a, double b) { + return a * b; + } + }, + DIV("/", 2) { + @Override + public double calculate(double a, double b) { + /* To avoid any exceptions */ + return a / (b == 0 ? 1 : b); + } + }, + MOD("%", 2) { + @Override + public double calculate(double a, double b) { + return a % b; + } + }, + POW("^", 3) { + @Override + public double calculate(double a, double b) { + return Math.pow(a, b); + } + }, + AND("&&", 5) { + @Override + public double calculate(double a, double b) { + return a != 0 && b != 0 ? 1 : 0; + } + }, + OR("||", 5) { + @Override + public double calculate(double a, double b) { + return a != 0 || b != 0 ? 1 : 0; + } + }, + LESS("<", 5) { + @Override + public double calculate(double a, double b) { + return a < b ? 1 : 0; + } + }, + LESS_THAN("<=", 5) { + @Override + public double calculate(double a, double b) { + return a <= b ? 1 : 0; + } + }, + GREATER_THAN(">=", 5) { + @Override + public double calculate(double a, double b) { + return a >= b ? 1 : 0; + } + }, + GREATER(">", 5) { + @Override + public double calculate(double a, double b) { + return a > b ? 1 : 0; + } + }, + EQUALS("==", 5) { + @Override + public double calculate(double a, double b) { + return equals(a, b) ? 1 : 0; + } + }, + NOT_EQUALS("!=", 5) { + @Override + public double calculate(double a, double b) { + return !equals(a, b) ? 1 : 0; + } + }; + + public final static Set OPERATORS = new HashSet(); + + public static boolean equals(double a, double b) { + return Math.abs(a - b) < 0.00001; + } + + static { + for (Operation op : values()) { + OPERATORS.add(op.sign); + } + } + + /** + * String-ified name of this operation + */ + public final String sign; + + /** + * Value of this operation in relation to other operations (i.e precedence + * importance) + */ + public final int value; + + private Operation(String sign, int value) { + this.sign = sign; + this.value = value; + } + + /** + * Calculate the value based on given two doubles + */ + public abstract double calculate(double a, double b); +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/Operator.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/Operator.java new file mode 100644 index 0000000..1374b38 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/Operator.java @@ -0,0 +1,29 @@ +package mod.azure.azurelib.common.internal.common.core.math; + +/** + * Operator class + * + * This class is responsible for performing a calculation of two values based on + * given operation. + */ +public class Operator implements IValue { + public Operation operation; + public IValue a; + public IValue b; + + public Operator(Operation op, IValue a, IValue b) { + this.operation = op; + this.a = a; + this.b = b; + } + + @Override + public double get() { + return this.operation.calculate(a.get(), b.get()); + } + + @Override + public String toString() { + return a.toString() + " " + this.operation.sign + " " + b.toString(); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/Ternary.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/Ternary.java new file mode 100644 index 0000000..8a5bc24 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/Ternary.java @@ -0,0 +1,29 @@ +package mod.azure.azurelib.common.internal.common.core.math; + +/** + * Ternary operator class + * + * This value implementation allows to return different values depending on + * given condition value + */ +public class Ternary implements IValue { + public final IValue condition; + public final IValue ifTrue; + public final IValue ifFalse; + + public Ternary(IValue condition, IValue ifTrue, IValue ifFalse) { + this.condition = condition; + this.ifTrue = ifTrue; + this.ifFalse = ifFalse; + } + + @Override + public double get() { + return this.condition.get() != 0 ? this.ifTrue.get() : this.ifFalse.get(); + } + + @Override + public String toString() { + return this.condition.toString() + " ? " + this.ifTrue.toString() + " : " + this.ifFalse.toString(); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/Variable.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/Variable.java new file mode 100644 index 0000000..a1b98c3 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/Variable.java @@ -0,0 +1,42 @@ +package mod.azure.azurelib.common.internal.common.core.math; + +/** + * Variable class + * + * This class is responsible for providing a mutable {@link IValue} which can be + * modifier during runtime and still getting referenced in the expressions + * parsed by {@link MathBuilder}. + * + * But in practice, it's simply returns stored value and provides a method to + * modify it. + */ +public class Variable implements IValue { + private String name; + private double value; + + public Variable(String name, double value) { + this.name = name; + this.value = value; + } + + /** + * Set the value of this variable + */ + public void set(double value) { + this.value = value; + } + + @Override + public double get() { + return this.value; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return this.name; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/Function.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/Function.java new file mode 100644 index 0000000..cfa3608 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/Function.java @@ -0,0 +1,66 @@ +package mod.azure.azurelib.common.internal.common.core.math.functions; + +import mod.azure.azurelib.common.internal.common.core.math.IValue; + +/** + * Abstract function class + * + * This class provides function capability (i.e. giving it arguments and upon + * {@link #get()} method you receive output). + */ +public abstract class Function implements IValue { + protected IValue[] args; + protected String name; + + protected Function(IValue[] values, String name) throws Exception { + if (values.length < this.getRequiredArguments()) { + String message = String.format("Function '%s' requires at least %s arguments. %s are given!", + this.getName(), this.getRequiredArguments(), values.length); + + throw new Exception(message); + } + + this.args = values; + this.name = name; + } + + /** + * Get the value of nth argument + */ + public double getArg(int index) { + if (index < 0 || index >= this.args.length) { + return 0; + } + + return this.args[index].get(); + } + + @Override + public String toString() { + StringBuilder argsBuilder = new StringBuilder(); + + for (int i = 0; i < this.args.length; i++) { + argsBuilder.append(this.args[i].toString()); + + if (i < this.args.length - 1) { + argsBuilder.append(", "); + } + } + + return this.getName() + "(" + argsBuilder + ")"; + } + + /** + * Get name of this function + */ + public String getName() { + return this.name; + } + + /** + * Get minimum count of arguments this function needs + */ + public int getRequiredArguments() { + return 0; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/ACos.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/ACos.java new file mode 100644 index 0000000..0e69f5e --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/ACos.java @@ -0,0 +1,23 @@ +package mod.azure.azurelib.common.internal.common.core.math.functions.classic; + +import mod.azure.azurelib.common.internal.common.core.math.functions.Function; +import mod.azure.azurelib.common.internal.common.core.math.IValue; + +/** + * Absolute value function + */ +public class ACos extends Function { + public ACos(IValue[] values, String name) throws Exception { + super(values, name); + } + + @Override + public int getRequiredArguments() { + return 1; + } + + @Override + public double get() { + return Math.acos(this.getArg(0)); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/ASin.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/ASin.java new file mode 100644 index 0000000..bbb448d --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/ASin.java @@ -0,0 +1,23 @@ +package mod.azure.azurelib.common.internal.common.core.math.functions.classic; + +import mod.azure.azurelib.common.internal.common.core.math.functions.Function; +import mod.azure.azurelib.common.internal.common.core.math.IValue; + +/** + * Absolute value function + */ +public class ASin extends Function { + public ASin(IValue[] values, String name) throws Exception { + super(values, name); + } + + @Override + public int getRequiredArguments() { + return 1; + } + + @Override + public double get() { + return Math.asin(this.getArg(0)); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/ATan.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/ATan.java new file mode 100644 index 0000000..3ae38a1 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/ATan.java @@ -0,0 +1,23 @@ +package mod.azure.azurelib.common.internal.common.core.math.functions.classic; + +import mod.azure.azurelib.common.internal.common.core.math.functions.Function; +import mod.azure.azurelib.common.internal.common.core.math.IValue; + +/** + * Absolute value function + */ +public class ATan extends Function { + public ATan(IValue[] values, String name) throws Exception { + super(values, name); + } + + @Override + public int getRequiredArguments() { + return 1; + } + + @Override + public double get() { + return Math.atan(this.getArg(0)); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/ATan2.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/ATan2.java new file mode 100644 index 0000000..55ee30d --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/ATan2.java @@ -0,0 +1,23 @@ +package mod.azure.azurelib.common.internal.common.core.math.functions.classic; + +import mod.azure.azurelib.common.internal.common.core.math.functions.Function; +import mod.azure.azurelib.common.internal.common.core.math.IValue; + +/** + * Absolute value function + */ +public class ATan2 extends Function { + public ATan2(IValue[] values, String name) throws Exception { + super(values, name); + } + + @Override + public int getRequiredArguments() { + return 2; + } + + @Override + public double get() { + return Math.atan2(this.getArg(0), this.getArg(1)); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/Abs.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/Abs.java new file mode 100644 index 0000000..676d5b6 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/Abs.java @@ -0,0 +1,23 @@ +package mod.azure.azurelib.common.internal.common.core.math.functions.classic; + +import mod.azure.azurelib.common.internal.common.core.math.functions.Function; +import mod.azure.azurelib.common.internal.common.core.math.IValue; + +/** + * Absolute value function + */ +public class Abs extends Function { + public Abs(IValue[] values, String name) throws Exception { + super(values, name); + } + + @Override + public int getRequiredArguments() { + return 1; + } + + @Override + public double get() { + return Math.abs(this.getArg(0)); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/Cos.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/Cos.java new file mode 100644 index 0000000..33d00f3 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/Cos.java @@ -0,0 +1,20 @@ +package mod.azure.azurelib.common.internal.common.core.math.functions.classic; + +import mod.azure.azurelib.common.internal.common.core.math.functions.Function; +import mod.azure.azurelib.common.internal.common.core.math.IValue; + +public class Cos extends Function { + public Cos(IValue[] values, String name) throws Exception { + super(values, name); + } + + @Override + public int getRequiredArguments() { + return 1; + } + + @Override + public double get() { + return Math.cos(this.getArg(0)); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/Exp.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/Exp.java new file mode 100644 index 0000000..94f2634 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/Exp.java @@ -0,0 +1,20 @@ +package mod.azure.azurelib.common.internal.common.core.math.functions.classic; + +import mod.azure.azurelib.common.internal.common.core.math.functions.Function; +import mod.azure.azurelib.common.internal.common.core.math.IValue; + +public class Exp extends Function { + public Exp(IValue[] values, String name) throws Exception { + super(values, name); + } + + @Override + public int getRequiredArguments() { + return 1; + } + + @Override + public double get() { + return Math.exp(this.getArg(0)); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/Ln.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/Ln.java new file mode 100644 index 0000000..3880dd6 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/Ln.java @@ -0,0 +1,20 @@ +package mod.azure.azurelib.common.internal.common.core.math.functions.classic; + +import mod.azure.azurelib.common.internal.common.core.math.functions.Function; +import mod.azure.azurelib.common.internal.common.core.math.IValue; + +public class Ln extends Function { + public Ln(IValue[] values, String name) throws Exception { + super(values, name); + } + + @Override + public int getRequiredArguments() { + return 1; + } + + @Override + public double get() { + return Math.log(this.getArg(0)); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/Mod.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/Mod.java new file mode 100644 index 0000000..462ad38 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/Mod.java @@ -0,0 +1,20 @@ +package mod.azure.azurelib.common.internal.common.core.math.functions.classic; + +import mod.azure.azurelib.common.internal.common.core.math.functions.Function; +import mod.azure.azurelib.common.internal.common.core.math.IValue; + +public class Mod extends Function { + public Mod(IValue[] values, String name) throws Exception { + super(values, name); + } + + @Override + public int getRequiredArguments() { + return 2; + } + + @Override + public double get() { + return this.getArg(0) % this.getArg(1); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/Pi.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/Pi.java new file mode 100644 index 0000000..97338a9 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/Pi.java @@ -0,0 +1,15 @@ +package mod.azure.azurelib.common.internal.common.core.math.functions.classic; + +import mod.azure.azurelib.common.internal.common.core.math.functions.Function; +import mod.azure.azurelib.common.internal.common.core.math.IValue; + +public class Pi extends Function { + public Pi(IValue[] values, String name) throws Exception { + super(values, name); + } + + @Override + public double get() { + return Math.PI; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/Pow.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/Pow.java new file mode 100644 index 0000000..ca6eecc --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/Pow.java @@ -0,0 +1,20 @@ +package mod.azure.azurelib.common.internal.common.core.math.functions.classic; + +import mod.azure.azurelib.common.internal.common.core.math.functions.Function; +import mod.azure.azurelib.common.internal.common.core.math.IValue; + +public class Pow extends Function { + public Pow(IValue[] values, String name) throws Exception { + super(values, name); + } + + @Override + public int getRequiredArguments() { + return 2; + } + + @Override + public double get() { + return Math.pow(this.getArg(0), this.getArg(1)); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/Sin.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/Sin.java new file mode 100644 index 0000000..236589c --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/Sin.java @@ -0,0 +1,20 @@ +package mod.azure.azurelib.common.internal.common.core.math.functions.classic; + +import mod.azure.azurelib.common.internal.common.core.math.IValue; +import mod.azure.azurelib.common.internal.common.core.math.functions.Function; + +public class Sin extends Function { + public Sin(IValue[] values, String name) throws Exception { + super(values, name); + } + + @Override + public int getRequiredArguments() { + return 1; + } + + @Override + public double get() { + return Math.sin(this.getArg(0)); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/Sqrt.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/Sqrt.java new file mode 100644 index 0000000..189a4db --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/classic/Sqrt.java @@ -0,0 +1,20 @@ +package mod.azure.azurelib.common.internal.common.core.math.functions.classic; + +import mod.azure.azurelib.common.internal.common.core.math.IValue; +import mod.azure.azurelib.common.internal.common.core.math.functions.Function; + +public class Sqrt extends Function { + public Sqrt(IValue[] values, String name) throws Exception { + super(values, name); + } + + @Override + public int getRequiredArguments() { + return 1; + } + + @Override + public double get() { + return Math.sqrt(this.getArg(0)); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/limit/Clamp.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/limit/Clamp.java new file mode 100644 index 0000000..4bcb00b --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/limit/Clamp.java @@ -0,0 +1,21 @@ +package mod.azure.azurelib.common.internal.common.core.math.functions.limit; + +import mod.azure.azurelib.common.internal.common.core.math.functions.Function; +import mod.azure.azurelib.common.internal.common.core.math.IValue; +import mod.azure.azurelib.common.internal.common.core.utils.MathUtils; + +public class Clamp extends Function { + public Clamp(IValue[] values, String name) throws Exception { + super(values, name); + } + + @Override + public int getRequiredArguments() { + return 3; + } + + @Override + public double get() { + return MathUtils.clamp(this.getArg(0), this.getArg(1), this.getArg(2)); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/limit/Max.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/limit/Max.java new file mode 100644 index 0000000..770d250 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/limit/Max.java @@ -0,0 +1,20 @@ +package mod.azure.azurelib.common.internal.common.core.math.functions.limit; + +import mod.azure.azurelib.common.internal.common.core.math.functions.Function; +import mod.azure.azurelib.common.internal.common.core.math.IValue; + +public class Max extends Function { + public Max(IValue[] values, String name) throws Exception { + super(values, name); + } + + @Override + public int getRequiredArguments() { + return 2; + } + + @Override + public double get() { + return Math.max(this.getArg(0), this.getArg(1)); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/limit/Min.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/limit/Min.java new file mode 100644 index 0000000..f1cd79b --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/limit/Min.java @@ -0,0 +1,20 @@ +package mod.azure.azurelib.common.internal.common.core.math.functions.limit; + +import mod.azure.azurelib.common.internal.common.core.math.functions.Function; +import mod.azure.azurelib.common.internal.common.core.math.IValue; + +public class Min extends Function { + public Min(IValue[] values, String name) throws Exception { + super(values, name); + } + + @Override + public int getRequiredArguments() { + return 2; + } + + @Override + public double get() { + return Math.min(this.getArg(0), this.getArg(1)); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/rounding/Ceil.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/rounding/Ceil.java new file mode 100644 index 0000000..c9bc7c7 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/rounding/Ceil.java @@ -0,0 +1,20 @@ +package mod.azure.azurelib.common.internal.common.core.math.functions.rounding; + +import mod.azure.azurelib.common.internal.common.core.math.IValue; +import mod.azure.azurelib.common.internal.common.core.math.functions.Function; + +public class Ceil extends Function { + public Ceil(IValue[] values, String name) throws Exception { + super(values, name); + } + + @Override + public int getRequiredArguments() { + return 1; + } + + @Override + public double get() { + return Math.ceil(this.getArg(0)); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/rounding/Floor.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/rounding/Floor.java new file mode 100644 index 0000000..d513e14 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/rounding/Floor.java @@ -0,0 +1,20 @@ +package mod.azure.azurelib.common.internal.common.core.math.functions.rounding; + +import mod.azure.azurelib.common.internal.common.core.math.IValue; +import mod.azure.azurelib.common.internal.common.core.math.functions.Function; + +public class Floor extends Function { + public Floor(IValue[] values, String name) throws Exception { + super(values, name); + } + + @Override + public int getRequiredArguments() { + return 1; + } + + @Override + public double get() { + return Math.floor(this.getArg(0)); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/rounding/Round.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/rounding/Round.java new file mode 100644 index 0000000..ef0f164 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/rounding/Round.java @@ -0,0 +1,20 @@ +package mod.azure.azurelib.common.internal.common.core.math.functions.rounding; + +import mod.azure.azurelib.common.internal.common.core.math.IValue; +import mod.azure.azurelib.common.internal.common.core.math.functions.Function; + +public class Round extends Function { + public Round(IValue[] values, String name) throws Exception { + super(values, name); + } + + @Override + public int getRequiredArguments() { + return 1; + } + + @Override + public double get() { + return Math.round(this.getArg(0)); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/rounding/Trunc.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/rounding/Trunc.java new file mode 100644 index 0000000..e342cd7 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/rounding/Trunc.java @@ -0,0 +1,22 @@ +package mod.azure.azurelib.common.internal.common.core.math.functions.rounding; + +import mod.azure.azurelib.common.internal.common.core.math.IValue; +import mod.azure.azurelib.common.internal.common.core.math.functions.Function; + +public class Trunc extends Function { + public Trunc(IValue[] values, String name) throws Exception { + super(values, name); + } + + @Override + public int getRequiredArguments() { + return 1; + } + + @Override + public double get() { + double value = this.getArg(0); + + return value < 0 ? Math.ceil(value) : Math.floor(value); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/utility/DieRoll.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/utility/DieRoll.java new file mode 100644 index 0000000..ba396e2 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/utility/DieRoll.java @@ -0,0 +1,28 @@ +package mod.azure.azurelib.common.internal.common.core.math.functions.utility; + +import mod.azure.azurelib.common.internal.common.core.math.IValue; +import mod.azure.azurelib.common.internal.common.core.math.functions.Function; + +public class DieRoll extends Function { + public java.util.Random random; + + public DieRoll(IValue[] values, String name) throws Exception { + super(values, name); + + this.random = new java.util.Random(); + } + + @Override + public int getRequiredArguments() { + return 3; + } + + @Override + public double get() { + double i = 0; + double total = 0; + while (i < this.getArg(0)) + total += Math.random() * (this.getArg(2) - this.getArg(2)); + return total; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/utility/DieRollInteger.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/utility/DieRollInteger.java new file mode 100644 index 0000000..d41468a --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/utility/DieRollInteger.java @@ -0,0 +1,28 @@ +package mod.azure.azurelib.common.internal.common.core.math.functions.utility; + +import mod.azure.azurelib.common.internal.common.core.math.IValue; +import mod.azure.azurelib.common.internal.common.core.math.functions.Function; + +public class DieRollInteger extends Function { + public java.util.Random random; + + public DieRollInteger(IValue[] values, String name) throws Exception { + super(values, name); + + this.random = new java.util.Random(); + } + + @Override + public int getRequiredArguments() { + return 3; + } + + @Override + public double get() { + double i = 0; + double total = 0; + while (i < this.getArg(0)) + total += Math.round(this.getArg(1) + Math.random() * (this.getArg(2) - this.getArg(1))); + return total; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/utility/HermiteBlend.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/utility/HermiteBlend.java new file mode 100644 index 0000000..6209951 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/utility/HermiteBlend.java @@ -0,0 +1,25 @@ +package mod.azure.azurelib.common.internal.common.core.math.functions.utility; + +import mod.azure.azurelib.common.internal.common.core.math.IValue; +import mod.azure.azurelib.common.internal.common.core.math.functions.Function; + +public class HermiteBlend extends Function { + public java.util.Random random; + + public HermiteBlend(IValue[] values, String name) throws Exception { + super(values, name); + + this.random = new java.util.Random(); + } + + @Override + public int getRequiredArguments() { + return 1; + } + + @Override + public double get() { + double min = Math.ceil(this.getArg(0)); + return Math.floor(3 * Math.pow(min, 2) - 2 * Math.pow(min, 3)); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/utility/Lerp.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/utility/Lerp.java new file mode 100644 index 0000000..d0e9f3f --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/utility/Lerp.java @@ -0,0 +1,21 @@ +package mod.azure.azurelib.common.internal.common.core.math.functions.utility; + +import mod.azure.azurelib.common.internal.common.core.math.IValue; +import mod.azure.azurelib.common.internal.common.core.math.functions.Function; +import mod.azure.azurelib.common.internal.common.core.utils.Interpolations; + +public class Lerp extends Function { + public Lerp(IValue[] values, String name) throws Exception { + super(values, name); + } + + @Override + public int getRequiredArguments() { + return 3; + } + + @Override + public double get() { + return Interpolations.lerp(this.getArg(0), this.getArg(1), this.getArg(2)); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/utility/LerpRotate.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/utility/LerpRotate.java new file mode 100644 index 0000000..e81d57f --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/utility/LerpRotate.java @@ -0,0 +1,21 @@ +package mod.azure.azurelib.common.internal.common.core.math.functions.utility; + +import mod.azure.azurelib.common.internal.common.core.math.IValue; +import mod.azure.azurelib.common.internal.common.core.math.functions.Function; +import mod.azure.azurelib.common.internal.common.core.utils.Interpolations; + +public class LerpRotate extends Function { + public LerpRotate(IValue[] values, String name) throws Exception { + super(values, name); + } + + @Override + public int getRequiredArguments() { + return 3; + } + + @Override + public double get() { + return Interpolations.lerpYaw(this.getArg(0), this.getArg(1), this.getArg(2)); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/utility/Random.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/utility/Random.java new file mode 100644 index 0000000..726ae47 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/utility/Random.java @@ -0,0 +1,40 @@ +package mod.azure.azurelib.common.internal.common.core.math.functions.utility; + +import mod.azure.azurelib.common.internal.common.core.math.IValue; +import mod.azure.azurelib.common.internal.common.core.math.functions.Function; + +public class Random extends Function { + public java.util.Random random; + + public Random(IValue[] values, String name) throws Exception { + super(values, name); + + this.random = new java.util.Random(); + } + + @Override + public double get() { + double random = 0; + + if (this.args.length >= 3) { + this.random.setSeed((long) this.getArg(2)); + random = this.random.nextDouble(); + } else { + random = Math.random(); + } + + if (this.args.length >= 2) { + double a = this.getArg(0); + double b = this.getArg(1); + + double min = Math.min(a, b); + double max = Math.max(a, b); + + random = random * (max - min) + min; + } else if (this.args.length >= 1) { + random = random * this.getArg(0); + } + + return random; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/utility/RandomInteger.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/utility/RandomInteger.java new file mode 100644 index 0000000..252ea02 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/math/functions/utility/RandomInteger.java @@ -0,0 +1,26 @@ +package mod.azure.azurelib.common.internal.common.core.math.functions.utility; + +import mod.azure.azurelib.common.internal.common.core.math.IValue; +import mod.azure.azurelib.common.internal.common.core.math.functions.Function; + +public class RandomInteger extends Function { + public java.util.Random random; + + public RandomInteger(IValue[] values, String name) throws Exception { + super(values, name); + + this.random = new java.util.Random(); + } + + @Override + public int getRequiredArguments() { + return 2; + } + + @Override + public double get() { + double min = Math.ceil(this.getArg(0)); + double max = Math.floor(this.getArg(1)); + return Math.floor(Math.random() * (max - min) + min); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/molang/LazyVariable.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/molang/LazyVariable.java new file mode 100644 index 0000000..0ed0adf --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/molang/LazyVariable.java @@ -0,0 +1,53 @@ +package mod.azure.azurelib.common.internal.common.core.molang; + +import java.util.function.DoubleSupplier; + +import mod.azure.azurelib.common.internal.common.core.math.Variable; + +/** + * Lazy override of Variable, to allow for deferred value calculation.
+ * Optimises rendering as values are not touched until needed (if at all) + */ +public class LazyVariable extends Variable { + private DoubleSupplier valueSupplier; + + public LazyVariable(String name, double value) { + this(name, () -> value); + } + + public LazyVariable(String name, DoubleSupplier valueSupplier) { + super(name, 0); + + this.valueSupplier = valueSupplier; + } + + /** + * Set the new value for the variable, acting as a constant + */ + @Override + public void set(double value) { + this.valueSupplier = () -> value; + } + + /** + * Set the new value supplier for the variable + */ + public void set(DoubleSupplier valueSupplier) { + this.valueSupplier = valueSupplier; + } + + /** + * Get the current value of the variable + */ + @Override + public double get() { + return this.valueSupplier.getAsDouble(); + } + + /** + * Instantiates a copy of this variable from this variable's current value and name + */ + public static LazyVariable from(Variable variable) { + return new LazyVariable(variable.getName(), variable.get()); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/molang/MolangException.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/molang/MolangException.java new file mode 100644 index 0000000..b2b4869 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/molang/MolangException.java @@ -0,0 +1,12 @@ +package mod.azure.azurelib.common.internal.common.core.molang; + +import java.io.Serial; + +public class MolangException extends Exception { + @Serial + private static final long serialVersionUID = 1470247726869768015L; + + public MolangException(String message) { + super(message); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/molang/MolangParser.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/molang/MolangParser.java new file mode 100644 index 0000000..fbd9e20 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/molang/MolangParser.java @@ -0,0 +1,275 @@ +package mod.azure.azurelib.common.internal.common.core.molang; + +import java.util.List; +import java.util.Map; +import java.util.function.DoubleSupplier; + +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import mod.azure.azurelib.common.internal.common.core.math.Constant; +import mod.azure.azurelib.common.internal.common.core.math.IValue; +import mod.azure.azurelib.common.internal.common.core.math.MathBuilder; +import mod.azure.azurelib.common.internal.common.core.math.Variable; +import mod.azure.azurelib.common.internal.common.core.molang.expressions.MolangCompoundValue; +import mod.azure.azurelib.common.internal.common.core.molang.expressions.MolangValue; +import mod.azure.azurelib.common.internal.common.core.molang.expressions.MolangVariableHolder; +import mod.azure.azurelib.common.internal.common.core.molang.functions.CosDegrees; +import mod.azure.azurelib.common.internal.common.core.molang.functions.SinDegrees; + +/** + * Utility class for parsing and utilising MoLang functions and expressions + * @see Bedrock Dev - Molang + */ +public class MolangParser extends MathBuilder { + // Replace base variables map + public static final Map VARIABLES = new Object2ObjectOpenHashMap<>(); + public static final MolangVariableHolder ZERO = new MolangVariableHolder(null, new Constant(0)); + public static final MolangVariableHolder ONE = new MolangVariableHolder(null, new Constant(1)); + public static final String RETURN = "return "; + + public static final MolangParser INSTANCE = new MolangParser(); + + private MolangParser() { + super(); + + // Remap functions to be intact with Molang specification + doCoreRemaps(); + registerAdditionalVariables(); + } + + private void doCoreRemaps() { + // Replace radian based sin and cos with degree-based functions + this.functions.put("cos", CosDegrees.class); + this.functions.put("sin", SinDegrees.class); + + remap("abs", "math.abs"); + remap("acos", "math.acos"); + remap("asin", "math.asin"); + remap("atan", "math.atan"); + remap("atan2", "math.atan2"); + remap("ceil", "math.ceil"); + remap("clamp", "math.clamp"); + remap("cos", "math.cos"); + remap("die_roll", "math.die_roll"); + remap("die_roll_integer", "math.die_roll_integer"); + remap("exp", "math.exp"); + remap("floor", "math.floor"); + remap("hermite_blend", "math.hermite_blend"); + remap("lerp", "math.lerp"); + remap("lerprotate", "math.lerprotate"); + remap("ln", "math.ln"); + remap("max", "math.max"); + remap("min", "math.min"); + remap("mod", "math.mod"); + remap("pi", "math.pi"); + remap("pow", "math.pow"); + remap("random", "math.random"); + remap("random_integer", "math.random_integer"); + remap("round", "math.round"); + remap("sin", "math.sin"); + remap("sqrt", "math.sqrt"); + remap("trunc", "math.trunc"); + } + + private void registerAdditionalVariables() { + register(new LazyVariable(MolangQueries.ANIM_TIME, 0)); + register(new LazyVariable(MolangQueries.LIFE_TIME, 0)); + register(new LazyVariable(MolangQueries.ACTOR_COUNT, 0)); + register(new LazyVariable(MolangQueries.HEALTH, 0)); + register(new LazyVariable(MolangQueries.MAX_HEALTH, 0)); + register(new LazyVariable(MolangQueries.DISTANCE_FROM_CAMERA, 0)); + register(new LazyVariable(MolangQueries.YAW_SPEED, 0)); + register(new LazyVariable(MolangQueries.IS_IN_WATER_OR_RAIN, 0)); + register(new LazyVariable(MolangQueries.IS_IN_WATER, 0)); + register(new LazyVariable(MolangQueries.IS_ON_GROUND, 0)); + register(new LazyVariable(MolangQueries.TIME_OF_DAY, 0)); + register(new LazyVariable(MolangQueries.IS_ON_FIRE, 0)); + register(new LazyVariable(MolangQueries.GROUND_SPEED, 0)); + } + + /** + * Register a new {@link Variable} with the {@code MolangParser}.
+ * Ideally should be called from the mod constructor. + */ + @Override + public void register(Variable variable) { + if (!(variable instanceof LazyVariable)) + variable = LazyVariable.from(variable); + + VARIABLES.put(variable.getName(), (LazyVariable)variable); + } + + /** + * Remap a function to a new name, maintaining the actual functionality and removing the old registration entry + */ + public void remap(String old, String newName) { + this.functions.put(newName, this.functions.remove(old)); + } + + /** + * Set the value supplier for a variable.
+ * Consider using {@link MolangParser#setMemoizedValue} instead of you don't need per-call dynamic results + * @param name The name of the variable to set the value for + * @param value The value supplier to set + */ + public void setValue(String name, DoubleSupplier value) { + LazyVariable variable = getVariable(name); + + if (variable != null) + variable.set(value); + } + + /** + * Sets a memoized value supplier for a variable.
+ * This prevents re-calculation on successive calls, improving efficiency.
+ * This should be used wherever per-call accuracy is not needed. + */ + public void setMemoizedValue(String name, DoubleSupplier value) { + getVariable(name).set(new DoubleSupplier() { + private final DoubleSupplier supplier = value; + private double computedValue = Double.MIN_VALUE; + + @Override + public double getAsDouble() { + if (this.computedValue == Double.MIN_VALUE) + this.computedValue = this.supplier.getAsDouble(); + + return this.computedValue; + } + }); + } + + /** + * Get the registered {@link LazyVariable} for the given name + * @param name The name of the variable to get + * @return The registered {@code LazyVariable} instance, or a newly registered instance if one wasn't registered previously + */ + @Override + public LazyVariable getVariable(String name) { + return VARIABLES.computeIfAbsent(name, key -> new LazyVariable(key, 0)); + } + + public LazyVariable getVariable(String name, MolangCompoundValue currentStatement) { + LazyVariable variable; + + if (currentStatement != null) { + variable = currentStatement.locals.get(name); + + if (variable != null) + return variable; + } + + return getVariable(name); + } + + public static MolangValue parseJson(JsonElement element) throws MolangException { + if (!element.isJsonPrimitive()) + return ZERO; + + JsonPrimitive primitive = element.getAsJsonPrimitive(); + + if (primitive.isNumber()) + return new MolangValue(new Constant(primitive.getAsDouble())); + + if (primitive.isString()) { + String string = primitive.getAsString(); + + try { + return new MolangValue(new Constant(Double.parseDouble(string))); + } + catch (NumberFormatException ex) { + return parseExpression(string); + } + } + + return ZERO; + } + + /** + * Parse a molang expression + */ + public static MolangValue parseExpression(String expression) throws MolangException { + MolangCompoundValue result = null; + + for (String split : expression.toLowerCase().trim().split(";")) { + String trimmed = split.trim(); + + if (!trimmed.isEmpty()) { + if (result == null) { + result = new MolangCompoundValue(parseOneLine(trimmed, result)); + + continue; + } + + result.values.add(parseOneLine(trimmed, result)); + } + } + + if (result == null) + throw new MolangException("Molang expression cannot be blank!"); + + return result; + } + + /** + * Parse a single Molang statement + */ + protected static MolangValue parseOneLine(String expression, MolangCompoundValue currentStatement) throws MolangException { + if (expression.startsWith(RETURN)) { + try { + return new MolangValue(INSTANCE.parse(expression.substring(RETURN.length())), true); + } + catch (Exception e) { + throw new MolangException("Couldn't parse return '" + expression + "' expression!"); + } + } + + try { + List symbols = INSTANCE.breakdownChars(INSTANCE.breakdown(expression)); + + if (symbols.size() >= 3 && symbols.get(0) instanceof String name && INSTANCE.isVariable(symbols.get(0)) && symbols.get(1).equals("=")) { + symbols = symbols.subList(2, symbols.size()); + LazyVariable variable; + + if (!VARIABLES.containsKey(name) && !currentStatement.locals.containsKey(name)) { + currentStatement.locals.put(name, (variable = new LazyVariable(name, 0))); + } + else { + variable = INSTANCE.getVariable(name, currentStatement); + } + + return new MolangVariableHolder(variable, INSTANCE.parseSymbolsMolang(symbols)); + } + + return new MolangValue(INSTANCE.parseSymbolsMolang(symbols)); + } + catch (Exception e) { + throw new MolangException("Couldn't parse '" + expression + "' expression!"); + } + } + + /** + * Wrapper around {@link #parseSymbols(List)} to throw {@link MolangException} + */ + private IValue parseSymbolsMolang(List symbols) throws MolangException { + try { + return this.parseSymbols(symbols); + } + catch (Exception e) { + e.printStackTrace(); + + throw new MolangException("Couldn't parse an expression!"); + } + } + + /** + * Extend this method to allow {@link #breakdownChars(String[])} to capture "=" + * as an operator, so it was easier to parse assignment statements + */ + @Override + protected boolean isOperator(String s) { + return super.isOperator(s) || s.equals("="); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/molang/MolangQueries.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/molang/MolangQueries.java new file mode 100644 index 0000000..0ed14a7 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/molang/MolangQueries.java @@ -0,0 +1,26 @@ +package mod.azure.azurelib.common.internal.common.core.molang; + +/** + * Holder class for the various builtin query string constants for the {@link MolangParser}.
+ * These do not constitute a definitive list of queries; merely the default ones + */ +public final class MolangQueries { + public static final String ANIM_TIME = "query.anim_time"; + public static final String LIFE_TIME = "query.life_time"; + public static final String ACTOR_COUNT = "query.actor_count"; + public static final String TIME_OF_DAY = "query.time_of_day"; + public static final String MOON_PHASE = "query.moon_phase"; + public static final String DISTANCE_FROM_CAMERA = "query.distance_from_camera"; + public static final String IS_ON_GROUND = "query.is_on_ground"; + public static final String IS_IN_WATER = "query.is_in_water"; + public static final String IS_IN_WATER_OR_RAIN = "query.is_in_water_or_rain"; + public static final String HEALTH = "query.health"; + public static final String MAX_HEALTH = "query.max_health"; + public static final String IS_ON_FIRE = "query.is_on_fire"; + public static final String GROUND_SPEED = "query.ground_speed"; + public static final String YAW_SPEED = "query.yaw_speed"; + + private MolangQueries() { + throw new UnsupportedOperationException(); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/molang/expressions/MolangCompoundValue.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/molang/expressions/MolangCompoundValue.java new file mode 100644 index 0000000..577ae77 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/molang/expressions/MolangCompoundValue.java @@ -0,0 +1,48 @@ +package mod.azure.azurelib.common.internal.common.core.molang.expressions; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import mod.azure.azurelib.common.internal.common.core.molang.LazyVariable; + +import java.util.List; +import java.util.Map; +import java.util.StringJoiner; + +/** + * An extension of the {@link MolangValue} class, allowing for compound expressions. + */ +public class MolangCompoundValue extends MolangValue { + public final List values = new ObjectArrayList<>(); + public final Map locals = new Object2ObjectOpenHashMap<>(); + + public MolangCompoundValue(MolangValue baseValue) { + super(baseValue); + + this.values.add(baseValue); + } + + @Override + public double get() { + double value = 0; + + for (MolangValue molangValue : this.values) { + value = molangValue.get(); + } + + return value; + } + + @Override + public String toString() { + StringJoiner builder = new StringJoiner("; "); + + for (MolangValue molangValue : this.values) { + builder.add(molangValue.toString()); + + if (molangValue.isReturnValue()) + break; + } + + return builder.toString(); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/molang/expressions/MolangValue.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/molang/expressions/MolangValue.java new file mode 100644 index 0000000..000bf0c --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/molang/expressions/MolangValue.java @@ -0,0 +1,45 @@ +package mod.azure.azurelib.common.internal.common.core.molang.expressions; + +import mod.azure.azurelib.common.internal.common.core.math.Constant; +import mod.azure.azurelib.common.internal.common.core.math.IValue; +import mod.azure.azurelib.common.internal.common.core.molang.MolangParser; + +/** + * Molang extension for the {@link IValue} system. + * Used to handle values and expressions specific to Molang deserialization + */ +public class MolangValue implements IValue { + private final IValue value; + private final boolean returns; + + public MolangValue(IValue value) { + this(value, false); + } + + public MolangValue(IValue value, boolean isReturn) { + this.value = value; + this.returns = isReturn; + } + + @Override + public double get() { + return this.value.get(); + } + + public IValue getValueHolder() { + return this.value; + } + + public boolean isReturnValue() { + return this.returns; + } + + public boolean isConstant() { + return getClass() == MolangValue.class && value instanceof Constant; + } + + @Override + public String toString() { + return (this.returns ? MolangParser.RETURN : "") + this.value.toString(); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/molang/expressions/MolangVariableHolder.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/molang/expressions/MolangVariableHolder.java new file mode 100644 index 0000000..8f28da8 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/molang/expressions/MolangVariableHolder.java @@ -0,0 +1,31 @@ +package mod.azure.azurelib.common.internal.common.core.molang.expressions; + +import mod.azure.azurelib.common.internal.common.core.math.IValue; +import mod.azure.azurelib.common.internal.common.core.math.Variable; + +/** + * Extension of {@link MolangValue} that additionally sets the value of a provided {@link Variable} when being called. + */ +public class MolangVariableHolder extends MolangValue { + public Variable variable; + + public MolangVariableHolder(Variable variable, IValue value) { + super(value); + + this.variable = variable; + } + + @Override + public double get() { + double value = super.get(); + + this.variable.set(value); + + return value; + } + + @Override + public String toString() { + return this.variable.getName() + " = " + super.toString(); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/molang/functions/CosDegrees.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/molang/functions/CosDegrees.java new file mode 100644 index 0000000..5b5a73a --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/molang/functions/CosDegrees.java @@ -0,0 +1,24 @@ +package mod.azure.azurelib.common.internal.common.core.molang.functions; + +import mod.azure.azurelib.common.internal.common.core.math.IValue; +import mod.azure.azurelib.common.internal.common.core.math.functions.Function; +import mod.azure.azurelib.common.internal.common.core.math.functions.classic.Cos; + +/** + * Replacement function for {@link Cos}, operating in degrees rather than radians. + */ +public class CosDegrees extends Function { + public CosDegrees(IValue[] values, String name) throws Exception { + super(values, name); + } + + @Override + public int getRequiredArguments() { + return 1; + } + + @Override + public double get() { + return Math.cos(this.getArg(0) / 180 * Math.PI); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/molang/functions/SinDegrees.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/molang/functions/SinDegrees.java new file mode 100644 index 0000000..680ce9c --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/molang/functions/SinDegrees.java @@ -0,0 +1,24 @@ +package mod.azure.azurelib.common.internal.common.core.molang.functions; + +import mod.azure.azurelib.common.internal.common.core.math.IValue; +import mod.azure.azurelib.common.internal.common.core.math.functions.Function; +import mod.azure.azurelib.common.internal.common.core.math.functions.classic.Sin; + +/** + * Replacement function for {@link Sin}, operating in degrees rather than radians + */ +public class SinDegrees extends Function { + public SinDegrees(IValue[] values, String name) throws Exception { + super(values, name); + } + + @Override + public int getRequiredArguments() { + return 1; + } + + @Override + public double get() { + return Math.sin(getArg(0) / 180 * Math.PI); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/object/Axis.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/object/Axis.java new file mode 100644 index 0000000..44b8ffa --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/object/Axis.java @@ -0,0 +1,7 @@ +package mod.azure.azurelib.common.internal.common.core.object; + +public enum Axis { + X, + Y, + Z +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/object/Color.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/object/Color.java new file mode 100644 index 0000000..b25845c --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/object/Color.java @@ -0,0 +1,214 @@ +/* + Direct copy of https://github.com/shedaniel/cloth-basic-math/blob/master/src/main/java/me/shedaniel/math/Color.java under the unlicense. + */ +package mod.azure.azurelib.common.internal.common.core.object; + +/** + * Color holder object for storing a packed int argb value. + */ +public record Color(int argbInt) { + public static final Color WHITE = new Color(0xFFFFFFFF); + public static final Color LIGHT_GRAY = new Color(0xFFC0C0C0); + public static final Color GRAY = new Color(0xFF808080); + public static final Color DARK_GRAY = new Color(0xFF404040); + public static final Color BLACK = new Color(0xFF000000); + public static final Color RED = new Color(0xFFFF0000); + public static final Color PINK = new Color(0xFFFFAFAF); + public static final Color ORANGE = new Color(0xFFFFC800); + public static final Color YELLOW = new Color(0xFFFFFF00); + public static final Color GREEN = new Color(0xFF00FF00); + public static final Color MAGENTA = new Color(0xFFFF00FF); + public static final Color CYAN = new Color(0xFF00FFFF); + public static final Color BLUE = new Color(0xFF0000FF); + + /** + * Creates a new {@code Color} instance from RGB values, ensuring 100% opacity + */ + public static Color ofOpaque(int color) { + return new Color(0xFF000000 | color); + } + + /** + * Creates a new {@code Color} instance from RGB values with 100% opacity + */ + public static Color ofRGB(float red, float green, float blue) { + return ofRGBA(red, green, blue, 1f); + } + + /** + * Creates a new {@code Color} instance from RGB values with 100% opacity + */ + public static Color ofRGB(int r, int g, int b) { + return ofRGBA(r, g, b, 255); + } + + /** + * Creates a new {@code Color} instance from RGBA values + */ + public static Color ofRGBA(float r, float g, float b, float a) { + return ofRGBA((int)(r * 255f + 0.5), (int)(g * 255f + 0.5f), (int)(b * 255f + 0.5f), (int)(a * 255f + 0.5f)); + } + + /** + * Creates a new {@code Color} instance from RGBA values + */ + public static Color ofRGBA(int r, int g, int b, int a) { + return new Color(((a & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF)); + } + + /** + * Creates a new {@code Color} instance from HSB values with 100% opacity + */ + public static Color ofHSB(float hue, float saturation, float brightness) { + return ofOpaque(HSBtoARGB(hue, saturation, brightness)); + } + + /** + * Converts a HSB value triplet to a packed ARGB int + */ + public static int HSBtoARGB(float hue, float saturation, float brightness) { + int r = 0; + int g = 0; + int b = 0; + + if (saturation == 0) { + r = g = b = (int) (brightness * 255f + 0.5f); + } + else { + float h = (hue - (float)Math.floor(hue)) * 6f; + float f = h - (float)Math.floor(h); + float p = brightness * (1 - saturation); + float q = brightness * (1 - saturation * f); + float t = brightness * (1 - (saturation * (1 - f))); + + switch ((int)h) { + case 0 -> { + r = (int) (brightness * 255f + 0.5f); + g = (int) (t * 255f + 0.5f); + b = (int) (p * 255f + 0.5f); + } + case 1 -> { + r = (int) (q * 255f + 0.5f); + g = (int) (brightness * 255f + 0.5f); + b = (int) (p * 255f + 0.5f); + } + case 2 -> { + r = (int) (p * 255f + 0.5f); + g = (int) (brightness * 255f + 0.5f); + b = (int) (t * 255f + 0.5f); + } + case 3 -> { + r = (int) (p * 255f + 0.5f); + g = (int) (q * 255f + 0.5f); + b = (int) (brightness * 255f + 0.5f); + } + case 4 -> { + r = (int) (t * 255f + 0.5f); + g = (int) (p * 255f + 0.5f); + b = (int) (brightness * 255f + 0.5f); + } + case 5 -> { + r = (int) (brightness * 255f + 0.5f); + g = (int) (p * 255f + 0.5f); + b = (int) (q * 255f + 0.5f); + } + } + } + + return 0xFF000000 | (r << 16) | (g << 8) | b; + } + + public int getColor() { + return this.argbInt; + } + + public int getAlpha() { + return this.argbInt >> 24 & 0xFF; + } + + public float getAlphaFloat() { + return getAlpha() / 255f; + } + + public int getRed() { + return this.argbInt >> 16 & 0xFF; + } + + public float getRedFloat() { + return getRed() / 255f; + } + + public int getGreen() { + return this.argbInt >> 8 & 0xFF; + } + + public float getGreenFloat() { + return getGreen() / 255f; + } + + public int getBlue() { + return this.argbInt & 0xFF; + } + + public float getBlueFloat() { + return getBlue() / 255f; + } + + /** + * Returns a brighter variant of the same color.
+ * @param factor The factor for shading + */ + public Color brighter(double factor) { + int r = getRed(); + int g = getGreen(); + int b = getBlue(); + int i = (int)(1 / (1 - (1 / factor))); + + if (r == 0 && g == 0 && b == 0) + return ofRGBA(i, i, i, getAlpha()); + + if (r > 0 && r < i) + r = i; + + if (g > 0 && g < i) + g = i; + + if (b > 0 && b < i) + b = i; + + return ofRGBA(Math.min((int) (r / (1 / factor)), 255), Math.min((int) (g / (1 / factor)), 255), + Math.min((int) (b / (1 / factor)), 255), getAlpha()); + } + + /** + * Returns a darker variant of the same color.
+ * @param factor The factor for shading. The value provided is an inversely relative multiplier.
+ * E.G. input=2 -> 2x as dark.
+ * E.G. input=0.5 -> 0.5x as dark (brighter) + */ + public Color darker(float factor) { + return ofRGBA(Math.max((int)(getRed() * (1 / factor)), 0), Math.max((int)(getGreen() * (1 / factor)), 0), + Math.max((int)(getBlue() * (1 / factor)), 0), getAlpha()); + } + + @Override + public boolean equals(Object other) { + if (this == other) + return true; + + if (getClass() != other.getClass()) + return false; + + return hashCode() == other.hashCode(); + } + + @Override + public int hashCode() { + return argbInt; + } + + @Override + public String toString() { + return String.valueOf(argbInt); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/object/DataTicket.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/object/DataTicket.java new file mode 100644 index 0000000..061d16f --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/object/DataTicket.java @@ -0,0 +1,39 @@ +package mod.azure.azurelib.common.internal.common.core.object; + +import java.util.Map; +import java.util.Objects; + +/** + * Ticket object to define a typed data object + */ +public class DataTicket { + private final String id; + private final Class objectType; + + public DataTicket(String id, Class objectType) { + this.id = id; + this.objectType = objectType; + } + + public String id() { + return this.id; + } + + public Class objectType() { + return this.objectType; + } + + @Override + public int hashCode() { + return Objects.hash(this.id, this.objectType); + } + + /** + * Reverse getter function for consistent operation of ticket data retrieval + * @param dataMap The data map to retrieve the data from + * @return The data from the map, or null if the data hasn't been stored + */ + public D getData(Map, ?> dataMap) { + return (D)dataMap.get(this); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/object/PlayState.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/object/PlayState.java new file mode 100644 index 0000000..4d4f5dc --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/object/PlayState.java @@ -0,0 +1,11 @@ +package mod.azure.azurelib.common.internal.common.core.object; + +import mod.azure.azurelib.common.internal.common.core.animation.AnimationController; + +/** + * State enum to define whether an {@link AnimationController} should continue or stop + */ +public enum PlayState { + CONTINUE, + STOP +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/state/BoneSnapshot.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/state/BoneSnapshot.java new file mode 100644 index 0000000..7847517 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/state/BoneSnapshot.java @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2020. + * Author: Bernie G. (Gecko) + */ + +package mod.azure.azurelib.common.internal.common.core.state; + +import mod.azure.azurelib.common.internal.common.core.animatable.model.CoreGeoBone; +import mod.azure.azurelib.common.internal.common.core.animation.AnimationProcessor; + +/** + * A state monitoring class for a given {@link CoreGeoBone}.
+ * Transformations applied to the bone is monitored by the {@link AnimationProcessor} + * in the course of animations, and stored here for monitoring. + */ +public class BoneSnapshot { + private final CoreGeoBone bone; + + private float scaleX; + private float scaleY; + private float scaleZ; + + private float offsetPosX; + private float offsetPosY; + private float offsetPosZ; + + private float rotX; + private float rotY; + private float rotZ; + + private double lastResetRotationTick = 0; + private double lastResetPositionTick = 0; + private double lastResetScaleTick = 0; + + private boolean rotAnimInProgress = true; + private boolean posAnimInProgress = true; + private boolean scaleAnimInProgress = true; + + public BoneSnapshot(CoreGeoBone bone) { + this.rotX = bone.getRotX(); + this.rotY = bone.getRotY(); + this.rotZ = bone.getRotZ(); + + this.offsetPosX = bone.getPosX(); + this.offsetPosY = bone.getPosY(); + this.offsetPosZ = bone.getPosZ(); + + this.scaleX = bone.getScaleX(); + this.scaleY = bone.getScaleY(); + this.scaleZ = bone.getScaleZ(); + + this.bone = bone; + } + + public static BoneSnapshot copy(BoneSnapshot snapshot) { + BoneSnapshot newSnapshot = new BoneSnapshot(snapshot.bone); + + newSnapshot.scaleX = snapshot.scaleX; + newSnapshot.scaleY = snapshot.scaleY; + newSnapshot.scaleZ = snapshot.scaleZ; + + newSnapshot.offsetPosX = snapshot.offsetPosX; + newSnapshot.offsetPosY = snapshot.offsetPosY; + newSnapshot.offsetPosZ = snapshot.offsetPosZ; + + newSnapshot.rotX = snapshot.rotX; + newSnapshot.rotY = snapshot.rotY; + newSnapshot.rotZ = snapshot.rotZ; + + return newSnapshot; + } + + public CoreGeoBone getBone() { + return this.bone; + } + + public float getScaleX() { + return this.scaleX; + } + + public float getScaleY() { + return this.scaleY; + } + + public float getScaleZ() { + return this.scaleZ; + } + + public float getOffsetX() { + return this.offsetPosX; + } + + public float getOffsetY() { + return this.offsetPosY; + } + + public float getOffsetZ() { + return this.offsetPosZ; + } + + public float getRotX() { + return this.rotX; + } + + public float getRotY() { + return this.rotY; + } + + public float getRotZ() { + return this.rotZ; + } + + public double getLastResetRotationTick() { + return this.lastResetRotationTick; + } + + public double getLastResetPositionTick() { + return this.lastResetPositionTick; + } + + public double getLastResetScaleTick() { + return this.lastResetScaleTick; + } + + public boolean isRotAnimInProgress() { + return this.rotAnimInProgress; + } + + public boolean isPosAnimInProgress() { + return this.posAnimInProgress; + } + + public boolean isScaleAnimInProgress() { + return this.scaleAnimInProgress; + } + + /** + * Update the scale state of this snapshot + */ + public void updateScale(float scaleX, float scaleY, float scaleZ) { + this.scaleX = scaleX; + this.scaleY = scaleY; + this.scaleZ = scaleZ; + } + + /** + * Update the offset state of this snapshot + */ + public void updateOffset(float offsetX, float offsetY, float offsetZ) { + this.offsetPosX = offsetX; + this.offsetPosY = offsetY; + this.offsetPosZ = offsetZ; + } + + /** + * Update the rotation state of this snapshot + */ + public void updateRotation(float rotX, float rotY, float rotZ) { + this.rotX = rotX; + this.rotY = rotY; + this.rotZ = rotZ; + } + + public void startPosAnim() { + this.posAnimInProgress = true; + } + + public void stopPosAnim(double tick) { + this.posAnimInProgress = false; + this.lastResetPositionTick = tick; + } + + public void startRotAnim() { + this.rotAnimInProgress = true; + } + + public void stopRotAnim(double tick) { + this.rotAnimInProgress = false; + this.lastResetRotationTick = tick; + } + + public void startScaleAnim() { + this.scaleAnimInProgress = true; + } + + public void stopScaleAnim(double tick) { + this.scaleAnimInProgress = false; + this.lastResetScaleTick = tick; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + + if (obj == null || getClass() != obj.getClass()) + return false; + + return hashCode() == obj.hashCode(); + } + + @Override + public int hashCode() { + return this.bone.getName().hashCode(); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/utils/Interpolation.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/utils/Interpolation.java new file mode 100644 index 0000000..9b6169f --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/utils/Interpolation.java @@ -0,0 +1,103 @@ +package mod.azure.azurelib.common.internal.common.core.utils; + +public enum Interpolation { + LINEAR("linear") { + @Override + public float interpolate(float a, float b, float x) { + return Interpolations.lerp(a, b, x); + } + }, + QUAD_IN("quad_in") { + @Override + public float interpolate(float a, float b, float x) { + return a + (b - a) * x * x; + } + }, + QUAD_OUT("quad_out") { + @Override + public float interpolate(float a, float b, float x) { + return a - (b - a) * x * (x - 2); + } + }, + QUAD_INOUT("quad_inout") { + @Override + public float interpolate(float a, float b, float x) { + x *= 2; + + if (x < 1F) + return a + (b - a) / 2 * x * x; + + x -= 1; + + return a - (b - a) / 2 * (x * (x - 2) - 1); + } + }, + CUBIC_IN("cubic_in") { + @Override + public float interpolate(float a, float b, float x) { + return a + (b - a) * x * x * x; + } + }, + CUBIC_OUT("cubic_out") { + @Override + public float interpolate(float a, float b, float x) { + x -= 1; + return a + (b - a) * (x * x * x + 1); + } + }, + CUBIC_INOUT("cubic_inout") { + @Override + public float interpolate(float a, float b, float x) { + x *= 2; + + if (x < 1F) + return a + (b - a) / 2 * x * x * x; + + x -= 2; + + return a + (b - a) / 2 * (x * x * x + 2); + } + }, + EXP_IN("exp_in") { + @Override + public float interpolate(float a, float b, float x) { + return a + (b - a) * (float) Math.pow(2, 10 * (x - 1)); + } + }, + EXP_OUT("exp_out") { + @Override + public float interpolate(float a, float b, float x) { + return a + (b - a) * (float) (-Math.pow(2, -10 * x) + 1); + } + }, + EXP_INOUT("exp_inout") { + @Override + public float interpolate(float a, float b, float x) { + if (x == 0) + return a; + if (x == 1) + return b; + + x *= 2; + + if (x < 1F) + return a + (b - a) / 2 * (float) Math.pow(2, 10 * (x - 1)); + + x -= 1; + + return a + (b - a) / 2 * (float) (-Math.pow(2, -10 * x) + 2); + } + }; + + public final String key; + + private Interpolation(String key) { + this.key = key; + } + + public abstract float interpolate(float a, float b, float x); + + public String getName() { + return "mclib.interpolations." + this.key; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/utils/Interpolations.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/utils/Interpolations.java new file mode 100644 index 0000000..468b6f3 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/utils/Interpolations.java @@ -0,0 +1,353 @@ +package mod.azure.azurelib.common.internal.common.core.utils; + +/** + * Interpolation methods + * + * This class is responsible for doing different kind of interpolations. Cubic + * interpolation code was from website below, but BauerCam also uses this code. + * + * @author mchorse + * http://paulbourke.net/miscellaneous/interpolation/ + * https://github.com/daipenger/BauerCam + */ +public class Interpolations { + /** + * Linear interpolation + */ + public static float lerp(float a, float b, float position) { + return a + (b - a) * position; + } + + /** + * Special interpolation method for interpolating yaw. The problem with yaw, is + * that it may go in the "wrong" direction when having, for example, -170 (as a) + * and 170 (as b) degress or other way around (170 and -170). + * + * This interpolation method fixes this problem. + */ + public static float lerpYaw(float a, float b, float position) { + a = MathHelper.wrapDegrees(a); + b = MathHelper.wrapDegrees(b); + + return lerp(a, normalizeYaw(a, b), position); + } + + /** + * Cubic interpolation using Hermite between y1 and y2. Taken from paul's + * website. + * + * @param y0 - points[x-1] + * @param y1 - points[x] + * @param y2 - points[x+1] + * @param y3 - points[x+2] + * @param x - step between 0 and 1 + */ + public static double cubicHermite(double y0, double y1, double y2, double y3, double x) { + double a = -0.5 * y0 + 1.5 * y1 - 1.5 * y2 + 0.5 * y3; + double b = y0 - 2.5 * y1 + 2 * y2 - 0.5 * y3; + double c = -0.5 * y0 + 0.5 * y2; + + return ((a * x + b) * x + c) * x + y1; + } + + /** + * Yaw normalization for cubic interpolation + */ + public static double cubicHermiteYaw(float y0, float y1, float y2, float y3, float position) { + y0 = MathHelper.wrapDegrees(y0); + y1 = MathHelper.wrapDegrees(y1); + y2 = MathHelper.wrapDegrees(y2); + y3 = MathHelper.wrapDegrees(y3); + + y1 = normalizeYaw(y0, y1); + y2 = normalizeYaw(y1, y2); + y3 = normalizeYaw(y2, y3); + + return cubicHermite(y0, y1, y2, y3, position); + } + + /** + * Cubic interpolation between y1 and y2. Taken from paul's website. + * + * @param y0 - points[x-1] + * @param y1 - points[x] + * @param y2 - points[x+1] + * @param y3 - points[x+2] + * @param x - step between 0 and 1 + */ + public static float cubic(float y0, float y1, float y2, float y3, float x) { + float a = y3 - y2 - y0 + y1; + float b = y0 - y1 - a; + float c = y2 - y0; + + return ((a * x + b) * x + c) * x + y1; + } + + /** + * Yaw normalization for cubic interpolation + */ + public static float cubicYaw(float y0, float y1, float y2, float y3, float position) { + y0 = MathHelper.wrapDegrees(y0); + y1 = MathHelper.wrapDegrees(y1); + y2 = MathHelper.wrapDegrees(y2); + y3 = MathHelper.wrapDegrees(y3); + + y1 = normalizeYaw(y0, y1); + y2 = normalizeYaw(y1, y2); + y3 = normalizeYaw(y2, y3); + + return cubic(y0, y1, y2, y3, position); + } + + /** + * Calculate X value for given T using some brute force algorithm... This method + * should be precise enough + * + * @param x1 - control point of initial value + * @param x2 - control point of final value + * @param t - time (should be 0..1) + * @param epsilon - delta that would satisfy the approximation + */ + public static float bezierX(float x1, float x2, float t, final float epsilon) { + float x = t; + float init = bezier(0, x1, x2, 1, t); + float factor = Math.copySign(0.1F, t - init); + + while (Math.abs(t - init) > epsilon) { + float oldFactor = factor; + + x += factor; + init = bezier(0, x1, x2, 1, x); + + if (Math.copySign(factor, t - init) != oldFactor) { + factor *= -0.25F; + } + } + + return x; + } + + /** + * Calculate X value for given T using default epsilon value. See other overload + * method for more information. + */ + public static float bezierX(float x1, float x2, float t) { + return bezierX(x1, x2, t, 0.0005F); + } + + /** + * Calculate cubic bezier from given variables + * + * @param x1 - initial value + * @param x2 - control point of initial value + * @param x3 - control point of final value + * @param x4 - final value + * @param t - time (should be 0..1) + */ + public static float bezier(float x1, float x2, float x3, float x4, float t) { + float t1 = lerp(x1, x2, t); + float t2 = lerp(x2, x3, t); + float t3 = lerp(x3, x4, t); + float t4 = lerp(t1, t2, t); + float t5 = lerp(t2, t3, t); + + return lerp(t4, t5, t); + } + + /** + * Normalize yaw rotation (argument {@code b}) based on the previous yaw + * rotation. + */ + public static float normalizeYaw(float a, float b) { + float diff = a - b; + + if (diff > 180 || diff < -180) { + diff = Math.copySign(360 - Math.abs(diff), diff); + + return a + diff; + } + + return b; + } + + /** + * Envelope function allows to create simple attack, sustain and release + * function. + * + * This version only goes from 0 to duration with fade in/out being the same + */ + public static float envelope(float x, float duration, float fades) { + return envelope(x, 0, fades, duration - fades, duration); + } + + /** + * Envelope function allows to create simple attack, sustain and release + * function. + * + * This advanced version allows you to specify a more customized range + */ + public static float envelope(float x, float lowIn, float lowOut, float highIn, float highOut) { + if (x < lowIn || x > highOut) + return 0; + if (x < lowOut) + return (x - lowIn) / (lowOut - lowIn); + if (x > highIn) + return 1 - (x - highIn) / (highOut - highIn); + + return 1; + } + + /* --- Double versions of the functions --- */ + + /** + * Linear interpolation + */ + public static double lerp(double a, double b, double position) { + return a + (b - a) * position; + } + + /** + * Special interpolation method for interpolating yaw. The problem with yaw, is + * that it may go in the "wrong" direction when having, for example, -170 (as a) + * and 170 (as b) degress or other way around (170 and -170). + * + * This interpolation method fixes this problem. + */ + public static double lerpYaw(double a, double b, double position) { + a = MathHelper.wrapDegrees(a); + b = MathHelper.wrapDegrees(b); + + return lerp(a, normalizeYaw(a, b), position); + } + + /** + * Cubic interpolation between y1 and y2. Taken from paul's website. + * + * @param y0 - points[x-1] + * @param y1 - points[x] + * @param y2 - points[x+1] + * @param y3 - points[x+2] + * @param x - step between 0 and 1 + */ + public static double cubic(double y0, double y1, double y2, double y3, double x) { + double a = y3 - y2 - y0 + y1; + double b = y0 - y1 - a; + double c = y2 - y0; + + return ((a * x + b) * x + c) * x + y1; + } + + /** + * Yaw normalization for cubic interpolation + */ + public static double cubicYaw(double y0, double y1, double y2, double y3, double position) { + y0 = MathHelper.wrapDegrees(y0); + y1 = MathHelper.wrapDegrees(y1); + y2 = MathHelper.wrapDegrees(y2); + y3 = MathHelper.wrapDegrees(y3); + + y1 = normalizeYaw(y0, y1); + y2 = normalizeYaw(y1, y2); + y3 = normalizeYaw(y2, y3); + + return cubic(y0, y1, y2, y3, position); + } + + /** + * Calculate X value for given T using some brute force algorithm... This method + * should be precise enough + * + * @param x1 - control point of initial value + * @param x2 - control point of final value + * @param t - time (should be 0..1) + * @param epsilon - delta that would satisfy the approximation + */ + public static double bezierX(double x1, double x2, double t, final double epsilon) { + double x = t; + double init = bezier(0, x1, x2, 1, t); + double factor = Math.copySign(0.1F, t - init); + + while (Math.abs(t - init) > epsilon) { + double oldFactor = factor; + + x += factor; + init = bezier(0, x1, x2, 1, x); + + if (Math.copySign(factor, t - init) != oldFactor) { + factor *= -0.25F; + } + } + + return x; + } + + /** + * Calculate X value for given T using default epsilon value. See other overload + * method for more information. + */ + public static double bezierX(double x1, double x2, float t) { + return bezierX(x1, x2, t, 0.0005F); + } + + /** + * Calculate cubic bezier from given variables + * + * @param x1 - initial value + * @param x2 - control point of initial value + * @param x3 - control point of final value + * @param x4 - final value + * @param t - time (should be 0..1) + */ + public static double bezier(double x1, double x2, double x3, double x4, double t) { + double t1 = lerp(x1, x2, t); + double t2 = lerp(x2, x3, t); + double t3 = lerp(x3, x4, t); + double t4 = lerp(t1, t2, t); + double t5 = lerp(t2, t3, t); + + return lerp(t4, t5, t); + } + + /** + * Normalize yaw rotation (argument {@code b}) based on the previous yaw + * rotation. + */ + public static double normalizeYaw(double a, double b) { + double diff = a - b; + + if (diff > 180 || diff < -180) { + diff = Math.copySign(360 - Math.abs(diff), diff); + + return a + diff; + } + + return b; + } + + /** + * Envelope function allows to create simple attack, sustain and release + * function. + * + * This version only goes from 0 to duration with fade in/out being the same + */ + public static double envelope(double x, double duration, double fades) { + return envelope(x, 0, fades, duration - fades, duration); + } + + /** + * Envelope function allows to create simple attack, sustain and release + * function. + * + * This advanced version allows you to specify a more customized range + */ + public static double envelope(double x, double lowIn, double lowOut, double highIn, double highOut) { + if (x < lowIn || x > highOut) + return 0; + if (x < lowOut) + return (x - lowIn) / (lowOut - lowIn); + if (x > highIn) + return 1 - (x - highIn) / (highOut - highIn); + + return 1; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/utils/JsonUtils.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/utils/JsonUtils.java new file mode 100644 index 0000000..d660b13 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/utils/JsonUtils.java @@ -0,0 +1,22 @@ +package mod.azure.azurelib.common.internal.common.core.utils; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.stream.JsonWriter; + +import java.io.StringWriter; + +public class JsonUtils { + public static String jsonToPretty(JsonElement element) { + StringWriter writer = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(writer); + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + + jsonWriter.setIndent(" "); + gson.toJson(element, jsonWriter); + + /* Prettify arrays */ + return writer.toString(); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/utils/MathHelper.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/utils/MathHelper.java new file mode 100644 index 0000000..81789f1 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/utils/MathHelper.java @@ -0,0 +1,60 @@ +package mod.azure.azurelib.common.internal.common.core.utils; + +public final class MathHelper { + /** + * the angle is reduced to an angle between -180 and +180 by mod, and a 360 + * check + */ + public static float wrapDegrees(float value) { + value = value % 360.0F; + + if (value >= 180.0F) { + value -= 360.0F; + } + + if (value < -180.0F) { + value += 360.0F; + } + + return value; + } + + /** + * the angle is reduced to an angle between -180 and +180 by mod, and a 360 + * check + */ + public static double wrapDegrees(double value) { + value = value % 360.0D; + + if (value >= 180.0D) { + value -= 360.0D; + } + + if (value < -180.0D) { + value += 360.0D; + } + + return value; + } + + /** + * Adjust the angle so that his value is in range [-180;180[ + */ + public static int wrapDegrees(int angle) { + angle = angle % 360; + + if (angle >= 180) { + angle -= 360; + } + + if (angle < -180) { + angle += 360; + } + + return angle; + } + + private MathHelper() { + throw new UnsupportedOperationException(); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/utils/MathUtils.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/utils/MathUtils.java new file mode 100644 index 0000000..339e4d0 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/utils/MathUtils.java @@ -0,0 +1,43 @@ +package mod.azure.azurelib.common.internal.common.core.utils; + +public final class MathUtils { + public static int clamp(int x, int min, int max) { + return Math.max(Math.min(x, max), min); + } + + public static float clamp(float x, float min, float max) { + return Math.max(Math.min(x, max), min); + } + + public static double clamp(double x, double min, double max) { + return Math.max(Math.min(x, max), min); + } + + public static int cycler(int x, int min, int max) { + if (x > max) { + return min; + } + + return x < min ? max : x; + } + + public static float cycler(float x, float min, float max) { + if (x > max) { + return min; + } + + return x < min ? max : x; + } + + public static double cycler(double x, double min, double max) { + if (x > max) { + return min; + } + + return x < min ? max : x; + } + + private MathUtils() { + throw new UnsupportedOperationException(); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/core/utils/Timer.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/utils/Timer.java new file mode 100644 index 0000000..42c7230 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/core/utils/Timer.java @@ -0,0 +1,54 @@ +package mod.azure.azurelib.common.internal.common.core.utils; + +public class Timer { + private boolean enabled; + private long time; + private final long duration; + + public Timer(long duration) { + this.duration = duration; + } + + public long getRemaining() { + return this.time - System.currentTimeMillis(); + } + + public void mark() { + this.mark(this.duration); + } + + public void mark(long duration) { + this.enabled = true; + this.time = System.currentTimeMillis() + duration; + } + + public void reset() { + this.enabled = false; + } + + public boolean checkReset() { + boolean isEnabled = this.check(); + + if (isEnabled) { + this.reset(); + } + + return isEnabled; + } + + public boolean check() { + return this.enabled && this.isTime(); + } + + public boolean isTime() { + return System.currentTimeMillis() >= this.time; + } + + public boolean checkRepeat() { + if (!this.enabled) { + this.mark(); + } + + return this.checkReset(); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/event/GeoRenderEvent.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/event/GeoRenderEvent.java new file mode 100644 index 0000000..fbe3c3c --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/event/GeoRenderEvent.java @@ -0,0 +1,22 @@ +package mod.azure.azurelib.common.internal.common.event; + +import mod.azure.azurelib.common.api.client.renderer.*; +import mod.azure.azurelib.common.api.common.animatable.GeoItem; +import mod.azure.azurelib.common.internal.client.renderer.GeoRenderer; + +/** + * AzureLib events base-class for the various event stages of rendering.
+ */ +public interface GeoRenderEvent { + /** + * Returns the renderer for this event + * @see DynamicGeoEntityRenderer DynamicGeoEntityRenderer + * @see GeoArmorRenderer GeoArmorRenderer + * @see GeoBlockRenderer GeoBlockRenderer + * @see GeoEntityRenderer GeoEntityRenderer + * @see GeoItem GeoItem + * @see GeoObjectRenderer GeoObjectRenderer + * @see GeoReplacedEntityRenderer GeoReplacedEntityRenderer + */ + GeoRenderer getRenderer(); +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/event/GeoRenderPhaseEvent.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/event/GeoRenderPhaseEvent.java new file mode 100644 index 0000000..c517092 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/event/GeoRenderPhaseEvent.java @@ -0,0 +1,6 @@ +package mod.azure.azurelib.common.internal.common.event; + +public interface GeoRenderPhaseEvent { + + void handle(); +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/FileLoader.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/FileLoader.java new file mode 100644 index 0000000..c72b472 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/FileLoader.java @@ -0,0 +1,72 @@ +package mod.azure.azurelib.common.internal.common.loading; + +import com.google.gson.JsonObject; +import mod.azure.azurelib.common.internal.common.cache.object.BakedGeoModel; +import mod.azure.azurelib.common.internal.common.core.animation.Animation; +import mod.azure.azurelib.common.internal.common.util.JsonUtil; +import mod.azure.azurelib.common.internal.common.AzureLib; +import mod.azure.azurelib.common.internal.common.AzureLibException; +import mod.azure.azurelib.common.internal.common.loading.json.raw.Model; +import mod.azure.azurelib.common.internal.common.loading.object.BakedAnimations; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.util.GsonHelper; +import org.apache.commons.io.IOUtils; + +import java.io.InputStream; +import java.nio.charset.Charset; + +/** + * Extracts raw information from given files, and other similar functions + */ +public final class FileLoader { + /** + * Load up and deserialize an animation json file to its respective {@link Animation} components + * + * @param location The resource path of the animations file + * @param manager The Minecraft {@code ResourceManager} responsible for maintaining in-memory resource access + */ + public static BakedAnimations loadAnimationsFile(ResourceLocation location, ResourceManager manager) { + return JsonUtil.GEO_GSON.fromJson(loadFile(location, manager), BakedAnimations.class); + } + + /** + * Load up and deserialize a geo model json file to its respective {@link BakedGeoModel} format + * + * @param location The resource path of the model file + * @param manager The Minecraft {@code ResourceManager} responsible for maintaining in-memory resource access + */ + public static Model loadModelFile(ResourceLocation location, ResourceManager manager) { + return JsonUtil.GEO_GSON.fromJson(loadFile(location, manager), Model.class); + } + + /** + * Load a given json file into memory + * + * @param location The resource path of the json file + * @param manager The Minecraft {@code ResourceManager} responsible for maintaining in-memory resource access + */ + public static JsonObject loadFile(ResourceLocation location, ResourceManager manager) { + return GsonHelper.fromJson(JsonUtil.GEO_GSON, getFileContents(location, manager), JsonObject.class); + } + + /** + * Read a text-based file into memory in the form of a single string + * + * @param location The resource path of the file + * @param manager The Minecraft {@code ResourceManager} responsible for maintaining in-memory resource access + */ + public static String getFileContents(ResourceLocation location, ResourceManager manager) { + try (InputStream inputStream = manager.getResourceOrThrow(location).open()) { + return IOUtils.toString(inputStream, Charset.defaultCharset()); + } catch (Exception e) { + AzureLib.LOGGER.error("Couldn't load {}", location, e); + + throw new AzureLibException(location.toString()); + } + } + + private FileLoader() { + throw new UnsupportedOperationException(); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/FormatVersion.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/FormatVersion.java new file mode 100644 index 0000000..417008c --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/FormatVersion.java @@ -0,0 +1,11 @@ +package mod.azure.azurelib.common.internal.common.loading.json; + +import com.google.gson.annotations.SerializedName; + +/** + * Geo format version enum, mostly just used in deserialization at startup + */ +public enum FormatVersion { + @SerializedName("1.12.0") V_1_12_0, + @SerializedName("1.14.0") V_1_14_0 +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/Bone.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/Bone.java new file mode 100644 index 0000000..c610cef --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/Bone.java @@ -0,0 +1,46 @@ +package mod.azure.azurelib.common.internal.common.loading.json.raw; + +import java.util.Map; + +import mod.azure.azurelib.common.internal.common.util.JsonUtil; +import org.jetbrains.annotations.Nullable; + +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +import net.minecraft.util.GsonHelper; + +/** + * Container class for cube information, only used in deserialization at startup + */ +public record Bone(double[] bindPoseRotation, Cube[] cubes, @Nullable Boolean debug, + @Nullable Double inflate, @Nullable Map locators, + @Nullable Boolean mirror, @Nullable String name, @Nullable Boolean neverRender, + @Nullable String parent, double[] pivot, @Nullable PolyMesh polyMesh, + @Nullable Long renderGroupId, @Nullable Boolean reset, double[] rotation, + @Nullable TextureMesh[] textureMeshes) { + public static JsonDeserializer deserializer() throws JsonParseException { + return (json, type, context) -> { + JsonObject obj = json.getAsJsonObject(); + double[] bindPoseRotation = JsonUtil.jsonArrayToDoubleArray(GsonHelper.getAsJsonArray(obj, "bind_pose_rotation", null)); + Cube[] cubes = JsonUtil.jsonArrayToObjectArray(GsonHelper.getAsJsonArray(obj, "cubes", new JsonArray(0)), context, Cube.class); + Boolean debug = JsonUtil.getOptionalBoolean(obj, "debug"); + Double inflate = JsonUtil.getOptionalDouble(obj, "inflate"); + Map locators = obj.has("locators") ? JsonUtil.jsonObjToMap(GsonHelper.getAsJsonObject(obj, "locators"), context, LocatorValue.class) : null; + Boolean mirror = JsonUtil.getOptionalBoolean(obj, "mirror"); + String name = GsonHelper.getAsString(obj, "name", null); + Boolean neverRender = JsonUtil.getOptionalBoolean(obj, "neverRender"); + String parent = GsonHelper.getAsString(obj, "parent", null); + double[] pivot = JsonUtil.jsonArrayToDoubleArray(GsonHelper.getAsJsonArray(obj, "pivot", new JsonArray(0))); + PolyMesh polyMesh = GsonHelper.getAsObject(obj, "poly_mesh", null, context, PolyMesh.class); + Long renderGroupId = JsonUtil.getOptionalLong(obj, "render_group_id"); + Boolean reset = JsonUtil.getOptionalBoolean(obj, "reset"); + double[] rotation = JsonUtil.jsonArrayToDoubleArray(GsonHelper.getAsJsonArray(obj, "rotation", null)); + TextureMesh[] textureMeshes = JsonUtil.jsonArrayToObjectArray(GsonHelper.getAsJsonArray(obj, "texture_meshes", new JsonArray(0)), context, TextureMesh.class); + + return new Bone(bindPoseRotation, cubes, debug, inflate, locators, mirror, name, neverRender, parent, pivot, polyMesh, renderGroupId, reset, rotation, textureMeshes); + }; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/Cube.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/Cube.java new file mode 100644 index 0000000..22b3912 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/Cube.java @@ -0,0 +1,30 @@ +package mod.azure.azurelib.common.internal.common.loading.json.raw; + +import mod.azure.azurelib.common.internal.common.util.JsonUtil; +import org.jetbrains.annotations.Nullable; + +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +import net.minecraft.util.GsonHelper; + +/** + * Container class for cube information, only used in deserialization at startup + */ +public record Cube(@Nullable Double inflate, @Nullable Boolean mirror, double[] origin, double[] pivot, double[] rotation, double[] size, UVUnion uv) { + public static JsonDeserializer deserializer() throws JsonParseException { + return (json, type, context) -> { + JsonObject obj = json.getAsJsonObject(); + Double inflate = JsonUtil.getOptionalDouble(obj, "inflate"); + Boolean mirror = JsonUtil.getOptionalBoolean(obj, "mirror"); + double[] origin = JsonUtil.jsonArrayToDoubleArray(GsonHelper.getAsJsonArray(obj, "origin", null)); + double[] pivot = JsonUtil.jsonArrayToDoubleArray(GsonHelper.getAsJsonArray(obj, "pivot", null)); + double[] rotation = JsonUtil.jsonArrayToDoubleArray(GsonHelper.getAsJsonArray(obj, "rotation", null)); + double[] size = JsonUtil.jsonArrayToDoubleArray(GsonHelper.getAsJsonArray(obj, "size", null)); + UVUnion uvUnion = GsonHelper.getAsObject(obj, "uv", null, context, UVUnion.class); + + return new Cube(inflate, mirror, origin, pivot, rotation, size, uvUnion); + }; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/FaceUV.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/FaceUV.java new file mode 100644 index 0000000..3a5ce05 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/FaceUV.java @@ -0,0 +1,26 @@ +package mod.azure.azurelib.common.internal.common.loading.json.raw; + +import mod.azure.azurelib.common.internal.common.util.JsonUtil; +import org.jetbrains.annotations.Nullable; + +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +import net.minecraft.util.GsonHelper; + +/** + * Container class for face UV information, only used in deserialization at startup + */ +public record FaceUV(@Nullable String materialInstance, double[] uv, double[] uvSize) { + public static JsonDeserializer deserializer() throws JsonParseException { + return (json, type, context) -> { + JsonObject obj = json.getAsJsonObject(); + String materialInstance = GsonHelper.getAsString(obj, "material_instance", null); + double[] uv = JsonUtil.jsonArrayToDoubleArray(GsonHelper.getAsJsonArray(obj, "uv", null)); + double[] uvSize = JsonUtil.jsonArrayToDoubleArray(GsonHelper.getAsJsonArray(obj, "uv_size", null)); + + return new FaceUV(materialInstance, uv, uvSize); + }; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/LocatorClass.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/LocatorClass.java new file mode 100644 index 0000000..d662520 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/LocatorClass.java @@ -0,0 +1,26 @@ +package mod.azure.azurelib.common.internal.common.loading.json.raw; + +import mod.azure.azurelib.common.internal.common.util.JsonUtil; +import org.jetbrains.annotations.Nullable; + +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +import net.minecraft.util.GsonHelper; + +/** + * Container class for locator class information, only used in deserialization at startup + */ +public record LocatorClass(@Nullable Boolean ignoreInheritedScale, double[] offset, double[] rotation) { + public static JsonDeserializer deserializer() throws JsonParseException { + return (json, type, context) -> { + JsonObject obj = json.getAsJsonObject(); + Boolean ignoreInheritedScale = JsonUtil.getOptionalBoolean(obj, "ignore_inherited_scale"); + double[] offset = JsonUtil.jsonArrayToDoubleArray(GsonHelper.getAsJsonArray(obj, "offset", null)); + double[] rotation = JsonUtil.jsonArrayToDoubleArray(GsonHelper.getAsJsonArray(obj, "rotation", null)); + + return new LocatorClass(ignoreInheritedScale, offset, rotation); + }; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/LocatorValue.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/LocatorValue.java new file mode 100644 index 0000000..eb4e426 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/LocatorValue.java @@ -0,0 +1,26 @@ +package mod.azure.azurelib.common.internal.common.loading.json.raw; + +import mod.azure.azurelib.common.internal.common.util.JsonUtil; +import org.jetbrains.annotations.Nullable; + +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonParseException; + +/** + * Container class for locator value information, only used in deserialization at startup + */ +public record LocatorValue(@Nullable LocatorClass locatorClass, double[] values) { + public static JsonDeserializer deserializer() throws JsonParseException { + return (json, type, context) -> { + if (json.isJsonArray()) { + return new LocatorValue(null, JsonUtil.jsonArrayToDoubleArray(json.getAsJsonArray())); + } + else if (json.isJsonObject()) { + return new LocatorValue(context.deserialize(json.getAsJsonObject(), LocatorClass.class), new double[0]); + } + else { + throw new JsonParseException("Invalid format for LocatorValue in json"); + } + }; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/MinecraftGeometry.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/MinecraftGeometry.java new file mode 100644 index 0000000..1d623b3 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/MinecraftGeometry.java @@ -0,0 +1,27 @@ +package mod.azure.azurelib.common.internal.common.loading.json.raw; + +import mod.azure.azurelib.common.internal.common.util.JsonUtil; +import org.jetbrains.annotations.Nullable; + +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +import net.minecraft.util.GsonHelper; + +/** + * Container class for generic geometry information, only used in deserialization at startup + */ +public record MinecraftGeometry(Bone[] bones, @Nullable String cape, @Nullable ModelProperties modelProperties) { + public static JsonDeserializer deserializer() throws JsonParseException { + return (json, type, context) -> { + JsonObject obj = json.getAsJsonObject(); + Bone[] bones = JsonUtil.jsonArrayToObjectArray(GsonHelper.getAsJsonArray(obj, "bones", new JsonArray(0)), context, Bone.class); + String cape = GsonHelper.getAsString(obj, "cape", null); + ModelProperties modelProperties = GsonHelper.getAsObject(obj, "description", null, context, ModelProperties.class); + + return new MinecraftGeometry(bones, cape, modelProperties); + }; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/Model.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/Model.java new file mode 100644 index 0000000..53909df --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/Model.java @@ -0,0 +1,27 @@ +package mod.azure.azurelib.common.internal.common.loading.json.raw; + +import mod.azure.azurelib.common.internal.common.loading.json.FormatVersion; +import mod.azure.azurelib.common.internal.common.util.JsonUtil; +import org.jetbrains.annotations.Nullable; + +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +import net.minecraft.util.GsonHelper; + +/** + * Container class for model information, only used in deserialization at startup + */ +public record Model(@Nullable FormatVersion formatVersion, MinecraftGeometry[] minecraftGeometry) { + public static JsonDeserializer deserializer() throws JsonParseException { + return (json, type, context) -> { + JsonObject obj = json.getAsJsonObject(); + FormatVersion formatVersion = context.deserialize(obj.get("format_version"), FormatVersion.class); + MinecraftGeometry[] minecraftGeometry = JsonUtil.jsonArrayToObjectArray(GsonHelper.getAsJsonArray(obj, "minecraft:geometry", new JsonArray(0)), context, MinecraftGeometry.class); + + return new Model(formatVersion, minecraftGeometry); + }; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/ModelProperties.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/ModelProperties.java new file mode 100644 index 0000000..b2e4d83 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/ModelProperties.java @@ -0,0 +1,51 @@ +package mod.azure.azurelib.common.internal.common.loading.json.raw; + +import mod.azure.azurelib.common.internal.common.util.JsonUtil; +import org.jetbrains.annotations.Nullable; + +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +import net.minecraft.util.GsonHelper; + +/** + * Container class for model property information, only used in deserialization at startup + */ +public record ModelProperties(@Nullable Boolean animationArmsDown, @Nullable Boolean animationArmsOutFront, + @Nullable Boolean animationDontShowArmor, @Nullable Boolean animationInvertedCrouch, + @Nullable Boolean animationNoHeadBob, @Nullable Boolean animationSingleArmAnimation, + @Nullable Boolean animationSingleLegAnimation, @Nullable Boolean animationStationaryLegs, + @Nullable Boolean animationStatueOfLibertyArms, @Nullable Boolean animationUpsideDown, + @Nullable String identifier, @Nullable Boolean preserveModelPose, + double textureHeight, double textureWidth, + @Nullable Double visibleBoundsHeight, double[] visibleBoundsOffset, + @Nullable Double visibleBoundsWidth) { + public static JsonDeserializer deserializer() throws JsonParseException { + return (json, type, context) -> { + JsonObject obj = json.getAsJsonObject(); + Boolean animationArmsDown = JsonUtil.getOptionalBoolean(obj, "animationArmsDown"); + Boolean animationArmsOutFront = JsonUtil.getOptionalBoolean(obj, "animationArmsOutFront"); + Boolean animationDontShowArmor = JsonUtil.getOptionalBoolean(obj, "animationDontShowArmor"); + Boolean animationInvertedCrouch = JsonUtil.getOptionalBoolean(obj, "animationInvertedCrouch"); + Boolean animationNoHeadBob = JsonUtil.getOptionalBoolean(obj, "animationNoHeadBob"); + Boolean animationSingleArmAnimation = JsonUtil.getOptionalBoolean(obj, "animationSingleArmAnimation"); + Boolean animationSingleLegAnimation = JsonUtil.getOptionalBoolean(obj, "animationSingleLegAnimation"); + Boolean animationStationaryLegs = JsonUtil.getOptionalBoolean(obj, "animationStationaryLegs"); + Boolean animationStatueOfLibertyArms = JsonUtil.getOptionalBoolean(obj, "animationStatueOfLibertyArms"); + Boolean animationUpsideDown = JsonUtil.getOptionalBoolean(obj, "animationUpsideDown"); + String identifier = GsonHelper.getAsString(obj, "identifier", null); + Boolean preserveModelPose = JsonUtil.getOptionalBoolean(obj, "preserve_model_pose"); + double textureHeight = GsonHelper.getAsDouble(obj, "texture_height"); + double textureWidth = GsonHelper.getAsDouble(obj, "texture_width"); + Double visibleBoundsHeight = JsonUtil.getOptionalDouble(obj, "visible_bounds_height"); + double[] visibleBoundsOffset = JsonUtil.jsonArrayToDoubleArray(GsonHelper.getAsJsonArray(obj, "visible_bounds_offset", null)); + Double visibleBoundsWidth = JsonUtil.getOptionalDouble(obj, "visible_bounds_width"); + + return new ModelProperties(animationArmsDown, animationArmsOutFront, animationDontShowArmor, animationInvertedCrouch, + animationNoHeadBob, animationSingleArmAnimation, animationSingleLegAnimation, animationStationaryLegs, + animationStatueOfLibertyArms, animationUpsideDown, identifier, preserveModelPose, textureHeight, + textureWidth, visibleBoundsHeight, visibleBoundsOffset, visibleBoundsWidth); + }; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/PolyMesh.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/PolyMesh.java new file mode 100644 index 0000000..16bc6dc --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/PolyMesh.java @@ -0,0 +1,28 @@ +package mod.azure.azurelib.common.internal.common.loading.json.raw; + +import mod.azure.azurelib.common.internal.common.util.JsonUtil; +import org.jetbrains.annotations.Nullable; + +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +import net.minecraft.util.GsonHelper; + +/** + * Container class for poly mesh information, only used in deserialization at startup + */ +public record PolyMesh(@Nullable Boolean normalizedUVs, double[] normals, @Nullable PolysUnion polysUnion, double[] positions, double[] uvs) { + public static JsonDeserializer deserializer() throws JsonParseException { + return (json, type, context) -> { + JsonObject obj = json.getAsJsonObject(); + Boolean normalizedUVs = JsonUtil.getOptionalBoolean(obj, "normalized_uvs"); + double[] normals = JsonUtil.jsonArrayToDoubleArray(GsonHelper.getAsJsonArray(obj, "normals", null)); + PolysUnion polysUnion = GsonHelper.getAsObject(obj, "polys", null, context, PolysUnion.class); + double[] positions = JsonUtil.jsonArrayToDoubleArray(GsonHelper.getAsJsonArray(obj, "positions", null)); + double[] uvs = JsonUtil.jsonArrayToDoubleArray(GsonHelper.getAsJsonArray(obj, "uvs", null)); + + return new PolyMesh(normalizedUVs, normals, polysUnion, positions, uvs); + }; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/PolysUnion.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/PolysUnion.java new file mode 100644 index 0000000..9c7f2c2 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/PolysUnion.java @@ -0,0 +1,55 @@ +package mod.azure.azurelib.common.internal.common.loading.json.raw; + +import mod.azure.azurelib.common.internal.common.util.JsonUtil; +import org.jetbrains.annotations.Nullable; + +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonParseException; +import com.google.gson.annotations.SerializedName; + +/** + * Container class for poly union information, only used in deserialization at startup + */ +public record PolysUnion(double[][][] union, @Nullable Type type) { + public static JsonDeserializer deserializer() throws JsonParseException { + return (json, type, context) -> { + if (json.isJsonPrimitive() && json.getAsJsonPrimitive().isString()) { + return new PolysUnion(new double[0][0][0], context.deserialize(json.getAsJsonPrimitive(), Type.class)); + } + else if (json.isJsonArray()) { + JsonArray array = json.getAsJsonArray(); + double[][][] matrix = makeSizedMatrix(array); + + for (int x = 0; x < array.size(); x++) { + JsonArray xArray = array.get(x).getAsJsonArray(); + + for (int y = 0; y < xArray.size(); y++) { + JsonArray yArray = xArray.get(y).getAsJsonArray(); + + matrix[x][y] = JsonUtil.jsonArrayToDoubleArray(yArray); + } + } + + return new PolysUnion(matrix, null); + } + else { + throw new JsonParseException("Invalid format for PolysUnion, must be either string or array"); + } + }; + } + + private static double[][][] makeSizedMatrix(JsonArray array) { + JsonArray subArray = array.size() > 0 ? array.get(0).getAsJsonArray() : null; + JsonArray subSubArray = subArray != null && subArray.size() > 0 ? subArray.get(0).getAsJsonArray() : null; + int ySize = subArray != null ? subArray.size() : 0; + int zSize = subSubArray != null ? subSubArray.size() : 0; + + return new double[array.size()][ySize][zSize]; + } + + public enum Type { + @SerializedName(value = "quad_list") QUAD, + @SerializedName(value = "tri_list") TRI; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/TextureMesh.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/TextureMesh.java new file mode 100644 index 0000000..29093c4 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/TextureMesh.java @@ -0,0 +1,28 @@ +package mod.azure.azurelib.common.internal.common.loading.json.raw; + +import mod.azure.azurelib.common.internal.common.util.JsonUtil; +import org.jetbrains.annotations.Nullable; + +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +import net.minecraft.util.GsonHelper; + +/** + * Container class for texture mesh information, only used in deserialization at startup + */ +public record TextureMesh(double[] localPivot, double[] position, double[] rotation, double[] scale, @Nullable String texture) { + public static JsonDeserializer deserializer() throws JsonParseException { + return (json, type, context) -> { + JsonObject obj = json.getAsJsonObject(); + double[] pivot = JsonUtil.jsonArrayToDoubleArray(GsonHelper.getAsJsonArray(obj, "local_pivot", null)); + double[] position = JsonUtil.jsonArrayToDoubleArray(GsonHelper.getAsJsonArray(obj, "position", null)); + double[] rotation = JsonUtil.jsonArrayToDoubleArray(GsonHelper.getAsJsonArray(obj, "rotation", null)); + double[] scale = JsonUtil.jsonArrayToDoubleArray(GsonHelper.getAsJsonArray(obj, "scale", null)); + String texture = GsonHelper.getAsString(obj, "texture", null); + + return new TextureMesh(pivot, position, rotation, scale, texture); + }; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/UVFaces.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/UVFaces.java new file mode 100644 index 0000000..b7e80a0 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/UVFaces.java @@ -0,0 +1,39 @@ +package mod.azure.azurelib.common.internal.common.loading.json.raw; + +import org.jetbrains.annotations.Nullable; + +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonObject; + +import net.minecraft.core.Direction; +import net.minecraft.util.GsonHelper; + +/** + * Container class for UV face information, only used in deserialization at startup + */ +public record UVFaces(@Nullable FaceUV north, @Nullable FaceUV south, @Nullable FaceUV east, @Nullable FaceUV west, @Nullable FaceUV up, @Nullable FaceUV down) { + public static JsonDeserializer deserializer() { + return (json, type, context) -> { + JsonObject obj = json.getAsJsonObject(); + FaceUV north = GsonHelper.getAsObject(obj, "north", null, context, FaceUV.class); + FaceUV south = GsonHelper.getAsObject(obj, "south", null, context, FaceUV.class); + FaceUV east = GsonHelper.getAsObject(obj, "east", null, context, FaceUV.class); + FaceUV west = GsonHelper.getAsObject(obj, "west", null, context, FaceUV.class); + FaceUV up = GsonHelper.getAsObject(obj, "up", null, context, FaceUV.class); + FaceUV down = GsonHelper.getAsObject(obj, "down", null, context, FaceUV.class); + + return new UVFaces(north, south, east, west, up, down); + }; + } + + public FaceUV fromDirection(Direction direction) { + return switch(direction) { + case NORTH -> north; + case SOUTH -> south; + case EAST -> east; + case WEST -> west; + case UP -> up; + case DOWN -> down; + }; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/UVUnion.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/UVUnion.java new file mode 100644 index 0000000..3c6b821 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/raw/UVUnion.java @@ -0,0 +1,26 @@ +package mod.azure.azurelib.common.internal.common.loading.json.raw; + +import mod.azure.azurelib.common.internal.common.util.JsonUtil; +import org.jetbrains.annotations.Nullable; + +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonParseException; + +/** + * Container class for UV information, only used in deserialization at startup + */ +public record UVUnion(double[] boxUVCoords, @Nullable UVFaces faceUV, boolean isBoxUV) { + public static JsonDeserializer deserializer() throws JsonParseException { + return (json, type, context) -> { + if (json.isJsonObject()) { + return new UVUnion(new double[0], context.deserialize(json.getAsJsonObject(), UVFaces.class), false); + } + else if (json.isJsonArray()) { + return new UVUnion(JsonUtil.jsonArrayToDoubleArray(json.getAsJsonArray()), null, true); + } + else { + throw new JsonParseException("Invalid format provided for UVUnion, must be either double array or UVFaces collection"); + } + }; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/typeadapter/BakedAnimationsAdapter.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/typeadapter/BakedAnimationsAdapter.java new file mode 100644 index 0000000..5e59cef --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/typeadapter/BakedAnimationsAdapter.java @@ -0,0 +1,227 @@ +package mod.azure.azurelib.common.internal.common.loading.json.typeadapter; + +import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; + +import mod.azure.azurelib.common.internal.common.core.animation.Animation; +import mod.azure.azurelib.common.internal.common.core.animation.EasingType; +import mod.azure.azurelib.common.internal.common.core.keyframe.BoneAnimation; +import mod.azure.azurelib.common.internal.common.core.keyframe.Keyframe; +import mod.azure.azurelib.common.internal.common.core.keyframe.KeyframeStack; +import mod.azure.azurelib.common.internal.common.util.JsonUtil; +import mod.azure.azurelib.common.internal.common.AzureLib; +import org.apache.commons.lang3.math.NumberUtils; + +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.mojang.datafixers.util.Pair; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import mod.azure.azurelib.common.internal.common.core.math.Constant; +import mod.azure.azurelib.common.internal.common.core.math.IValue; +import mod.azure.azurelib.common.internal.common.core.molang.MolangException; +import mod.azure.azurelib.common.internal.common.core.molang.MolangParser; +import mod.azure.azurelib.common.internal.common.core.molang.expressions.MolangValue; +import mod.azure.azurelib.common.internal.common.loading.object.BakedAnimations; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.GsonHelper; + +/** + * {@link com.google.gson.Gson} {@link JsonDeserializer} for {@link BakedAnimations}.
+ * Acts as the deserialization interface for {@code BakedAnimations} + */ +public class BakedAnimationsAdapter implements JsonDeserializer { + @Override + public BakedAnimations deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException { + JsonObject jsonObj = json.getAsJsonObject(); + + JsonObject animationJsonList = jsonObj.getAsJsonObject("animations"); + JsonArray includeListJSONObj = jsonObj.getAsJsonArray("includes"); + Map includes = null; + if(includeListJSONObj != null) { + includes = new Object2ObjectOpenHashMap<>(includeListJSONObj.size()); + for(JsonElement entry : includeListJSONObj.asList()) { + JsonObject obj = entry.getAsJsonObject(); + ResourceLocation fileId = new ResourceLocation(obj.get("file_id").getAsString()); + for(JsonElement animName : obj.getAsJsonArray("animations")) { + String ani = animName.getAsString(); + if(includes.containsKey(ani)) { + AzureLib.LOGGER.warn("Animation {} is already included! File already including: {} File trying to include from again: {}", ani, includes.get(ani).toString(), fileId.toString()); + } else { + includes.put(ani, fileId); + } + } + } + } + + Map animations = new Object2ObjectOpenHashMap<>(animationJsonList.size()); + + for (Map.Entry entry : animationJsonList.entrySet()) { + try { + animations.put(entry.getKey(), bakeAnimation(entry.getKey(), entry.getValue().getAsJsonObject(), context)); + } + catch (MolangException ex) { + AzureLib.LOGGER.error("Unable to parse animation: " + entry.getKey()); + ex.printStackTrace(); + } + } + + return new BakedAnimations(animations, includes); + } + + private Animation bakeAnimation(String name, JsonObject animationObj, JsonDeserializationContext context) throws MolangException { + double length = animationObj.has("animation_length") ? GsonHelper.getAsDouble(animationObj, "animation_length") * 20d : -1; + Animation.LoopType loopType = Animation.LoopType.fromJson(animationObj.get("loop")); + BoneAnimation[] boneAnimations = bakeBoneAnimations(GsonHelper.getAsJsonObject(animationObj, "bones", new JsonObject())); + Animation.Keyframes keyframes = context.deserialize(animationObj, Animation.Keyframes.class); + + if (length == -1) + length = calculateAnimationLength(boneAnimations); + + return new Animation(name, length, loopType, boneAnimations, keyframes); + } + + private BoneAnimation[] bakeBoneAnimations(JsonObject bonesObj) throws MolangException { + BoneAnimation[] animations = new BoneAnimation[bonesObj.size()]; + int index = 0; + + for (Map.Entry entry : bonesObj.entrySet()) { + JsonObject entryObj = entry.getValue().getAsJsonObject(); + KeyframeStack> scaleFrames = buildKeyframeStack( + getTripletObj(entryObj.get("scale")), false); + KeyframeStack> positionFrames = buildKeyframeStack( + getTripletObj(entryObj.get("position")), false); + KeyframeStack> rotationFrames = buildKeyframeStack( + getTripletObj(entryObj.get("rotation")), true); + + animations[index] = new BoneAnimation(entry.getKey(), rotationFrames, positionFrames, scaleFrames); + index++; + } + + return animations; + } + + private static List> getTripletObj(JsonElement element) { + if (element == null) + return List.of(); + + if (element instanceof JsonPrimitive primitive) { + JsonArray array = new JsonArray(3); + + array.add(primitive); + array.add(primitive); + array.add(primitive); + + element = array; + } + + if (element instanceof JsonArray array) + return ObjectArrayList.of(Pair.of("0", array)); + + if (element instanceof JsonObject obj) { + List> list = new ObjectArrayList<>(); + + for (Map.Entry entry : obj.entrySet()) { + if (entry.getValue() instanceof JsonObject entryObj && !entryObj.has("vector")) { + list.add(getTripletObjBedrock(entry.getKey(), entryObj)); + + continue; + } + + list.add(Pair.of(entry.getKey(), entry.getValue())); + } + + return list; + } + + throw new JsonParseException("Invalid object type provided to getTripletObj, got: " + element); + } + + private static Pair getTripletObjBedrock(String timestamp, JsonObject keyframe) { + JsonArray keyframeValues = null; + + if (keyframe.has("pre")) { + JsonElement pre = keyframe.get("pre"); + keyframeValues = pre.isJsonArray() ? pre.getAsJsonArray() : GsonHelper.getAsJsonArray(pre.getAsJsonObject(), "vector"); + } + else if (keyframe.has("post")) { + JsonElement post = keyframe.get("post"); + keyframeValues = post.isJsonArray() ? post.getAsJsonArray() : GsonHelper.getAsJsonArray(post.getAsJsonObject(), "vector"); + } + + if (keyframeValues != null) + return Pair.of(NumberUtils.isCreatable(timestamp) ? timestamp : "0", keyframeValues); + + throw new JsonParseException("Invalid keyframe data - expected array, found " + keyframe); + } + + private KeyframeStack> buildKeyframeStack(List> entries, boolean isForRotation) throws MolangException { + if (entries.isEmpty()) + return new KeyframeStack<>(); + + List> xFrames = new ObjectArrayList<>(); + List> yFrames = new ObjectArrayList<>(); + List> zFrames = new ObjectArrayList<>(); + + IValue xPrev = null; + IValue yPrev = null; + IValue zPrev = null; + Pair prevEntry = null; + + for (Pair entry : entries) { + String key = entry.getFirst(); + JsonElement element = entry.getSecond(); + + if (key.equals("easing") || key.equals("easingArgs") || key.equals("lerp_mode")) + continue; + + double prevTime = prevEntry != null ? Double.parseDouble(prevEntry.getFirst()) : 0; + double curTime = NumberUtils.isCreatable(key) ? Double.parseDouble(entry.getFirst()) : 0; + double timeDelta = curTime - prevTime; + + JsonArray keyFrameVector = element instanceof JsonArray array ? array : GsonHelper.getAsJsonArray(element.getAsJsonObject(), "vector"); + MolangValue rawXValue = MolangParser.parseJson(keyFrameVector.get(0)); + MolangValue rawYValue = MolangParser.parseJson(keyFrameVector.get(1)); + MolangValue rawZValue = MolangParser.parseJson(keyFrameVector.get(2)); + IValue xValue = isForRotation && rawXValue.isConstant() ? new Constant(Math.toRadians(-rawXValue.get())) : rawXValue; + IValue yValue = isForRotation && rawYValue.isConstant() ? new Constant(Math.toRadians(-rawYValue.get())) : rawYValue; + IValue zValue = isForRotation && rawZValue.isConstant() ? new Constant(Math.toRadians(rawZValue.get())) : rawZValue; + + JsonObject entryObj = element instanceof JsonObject obj ? obj : null; + EasingType easingType = entryObj != null && entryObj.has("easing") ? EasingType.fromJson(entryObj.get("easing")) : EasingType.LINEAR; + List easingArgs = entryObj != null && entryObj.has("easingArgs") ? + JsonUtil.jsonArrayToList(GsonHelper.getAsJsonArray(entryObj, "easingArgs"), ele -> new Constant(ele.getAsDouble())) : + new ObjectArrayList<>(); + + xFrames.add(new Keyframe<>(timeDelta * 20, prevEntry == null ? xValue : xPrev, xValue, easingType, easingArgs)); + yFrames.add(new Keyframe<>(timeDelta * 20, prevEntry == null ? yValue : yPrev, yValue, easingType, easingArgs)); + zFrames.add(new Keyframe<>(timeDelta * 20, prevEntry == null ? zValue : zPrev, zValue, easingType, easingArgs)); + + xPrev = xValue; + yPrev = yValue; + zPrev = zValue; + prevEntry = entry; + } + + return new KeyframeStack<>(xFrames, yFrames, zFrames); + } + + private static double calculateAnimationLength(BoneAnimation[] boneAnimations) { + double length = 0; + + for (BoneAnimation animation : boneAnimations) { + length = Math.max(length, animation.rotationKeyFrames().getLastKeyframeTime()); + length = Math.max(length, animation.positionKeyFrames().getLastKeyframeTime()); + length = Math.max(length, animation.scaleKeyFrames().getLastKeyframeTime()); + } + + return length == 0 ? Double.MAX_VALUE : length; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/typeadapter/KeyFramesAdapter.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/typeadapter/KeyFramesAdapter.java new file mode 100644 index 0000000..3df9690 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/json/typeadapter/KeyFramesAdapter.java @@ -0,0 +1,82 @@ +package mod.azure.azurelib.common.internal.common.loading.json.typeadapter; + +import com.google.gson.*; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import mod.azure.azurelib.common.internal.common.core.animation.Animation; +import mod.azure.azurelib.common.internal.common.core.keyframe.event.data.CustomInstructionKeyframeData; +import mod.azure.azurelib.common.internal.common.core.keyframe.event.data.ParticleKeyframeData; +import mod.azure.azurelib.common.internal.common.core.keyframe.event.data.SoundKeyframeData; +import mod.azure.azurelib.common.internal.common.util.JsonUtil; +import net.minecraft.util.GsonHelper; + +import java.lang.reflect.Type; +import java.util.Map; + +/** + * {@link Gson} {@link JsonDeserializer} for {@link Animation.Keyframes}.
+ * Acts as the deserialization interface for {@code Keyframes} + */ +public class KeyFramesAdapter implements JsonDeserializer { + @Override + public Animation.Keyframes deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException { + JsonObject obj = json.getAsJsonObject(); + SoundKeyframeData[] sounds = buildSoundFrameData(obj); + ParticleKeyframeData[] particles = buildParticleFrameData(obj); + CustomInstructionKeyframeData[] customInstructions = buildCustomFrameData(obj); + + return new Animation.Keyframes(sounds, particles, customInstructions); + } + + private static SoundKeyframeData[] buildSoundFrameData(JsonObject rootObj) { + JsonObject soundsObj = GsonHelper.getAsJsonObject(rootObj, "sound_effects", new JsonObject()); + SoundKeyframeData[] sounds = new SoundKeyframeData[soundsObj.size()]; + int index = 0; + + for (Map.Entry entry : soundsObj.entrySet()) { + sounds[index] = new SoundKeyframeData(Double.parseDouble(entry.getKey()) * 20d, GsonHelper.getAsString(entry.getValue().getAsJsonObject(), "effect")); + index++; + } + + return sounds; + } + + private static ParticleKeyframeData[] buildParticleFrameData(JsonObject rootObj) { + JsonObject particlesObj = GsonHelper.getAsJsonObject(rootObj, "particle_effects", new JsonObject()); + ParticleKeyframeData[] particles = new ParticleKeyframeData[particlesObj.size()]; + int index = 0; + + for (Map.Entry entry : particlesObj.entrySet()) { + JsonObject obj = entry.getValue().getAsJsonObject(); + String effect = GsonHelper.getAsString(obj, "effect", ""); + String locator = GsonHelper.getAsString(obj, "locator", ""); + String script = GsonHelper.getAsString(obj, "pre_effect_script", ""); + + particles[index] = new ParticleKeyframeData(Double.parseDouble(entry.getKey()) * 20d, effect, locator, script); + index++; + } + + return particles; + } + + private static CustomInstructionKeyframeData[] buildCustomFrameData(JsonObject rootObj) { + JsonObject customInstructionsObj = GsonHelper.getAsJsonObject(rootObj, "timeline", new JsonObject()); + CustomInstructionKeyframeData[] customInstructions = new CustomInstructionKeyframeData[customInstructionsObj.size()]; + int index = 0; + + for (Map.Entry entry : customInstructionsObj.entrySet()) { + String instructions = ""; + + if (entry.getValue() instanceof JsonArray array) { + instructions = JsonUtil.GEO_GSON.fromJson(array, ObjectArrayList.class).toString(); + } + else if (entry.getValue() instanceof JsonPrimitive primitive) { + instructions = primitive.getAsString(); + } + + customInstructions[index] = new CustomInstructionKeyframeData(Double.parseDouble(entry.getKey()) * 20d, instructions); + index++; + } + + return customInstructions; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/object/BakedAnimations.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/object/BakedAnimations.java new file mode 100644 index 0000000..a58067e --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/object/BakedAnimations.java @@ -0,0 +1,41 @@ +package mod.azure.azurelib.common.internal.common.loading.object; + +import java.util.Map; + +import mod.azure.azurelib.common.internal.common.cache.AzureLibCache; +import mod.azure.azurelib.common.internal.common.core.animation.Animation; +import org.jetbrains.annotations.Nullable; + +import net.minecraft.resources.ResourceLocation; + +/** + * Container object that holds a deserialized map of {@link Animation Animations}.
+ * Kept as a unique object so that it can be registered as a {@link com.google.gson.JsonDeserializer deserializer} for {@link com.google.gson.Gson Gson} + */ +public record BakedAnimations(Map animations, Map includes) { + /** + * Gets an {@link Animation} by its name, if present + */ + @Nullable + public Animation getAnimation(String name){ + Animation result = animations.get(name); + if(result == null && includes != null) { + ResourceLocation otherFileID = includes.getOrDefault(name, null); + if(otherFileID != null) { + BakedAnimations otherBakedAnims = AzureLibCache.getBakedAnimations().get(otherFileID); + if (otherBakedAnims.equals(this)) { + //TODO: Throw exception + } else { + result = otherBakedAnims.getAnimationWithoutIncludes(name); + } + } + } + return result; + } + + @Nullable + private Animation getAnimationWithoutIncludes(String name) { + return animations.get(name); + } + +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/object/BakedModelFactory.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/object/BakedModelFactory.java new file mode 100644 index 0000000..46de00b --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/object/BakedModelFactory.java @@ -0,0 +1,258 @@ +package mod.azure.azurelib.common.internal.common.loading.object; + +import java.util.List; +import java.util.Map; + +import mod.azure.azurelib.common.internal.client.util.RenderUtils; +import mod.azure.azurelib.common.internal.common.util.AzureLibUtil; +import org.jetbrains.annotations.Nullable; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +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.loading.json.raw.Bone; +import mod.azure.azurelib.common.internal.common.loading.json.raw.Cube; +import mod.azure.azurelib.common.internal.common.loading.json.raw.FaceUV; +import mod.azure.azurelib.common.internal.common.loading.json.raw.ModelProperties; +import mod.azure.azurelib.common.internal.common.loading.json.raw.UVUnion; +import net.minecraft.core.Direction; +import net.minecraft.world.phys.Vec3; + +/** + * Base interface for a factory of {@link BakedGeoModel} objects. + * Handled by default by AzureLib, but custom implementations may be added by other mods for special needs + */ +public interface BakedModelFactory { + final Map FACTORIES = new Object2ObjectOpenHashMap<>(1); + final BakedModelFactory DEFAULT_FACTORY = new Builtin(); + + /** + * Construct the output model from the given {@link GeometryTree}.
+ */ + BakedGeoModel constructGeoModel(GeometryTree geometryTree); + + /** + * Construct a {@link GeoBone} from the relevant raw input data + * @param boneStructure The {@code BoneStructure} comprising the structure of the bone and its children + * @param properties The loaded properties for the model + * @param parent The parent bone for this bone, or null if a top-level bone + */ + GeoBone constructBone(BoneStructure boneStructure, ModelProperties properties, @Nullable GeoBone parent); + + /** + * Construct a {@link GeoCube} from the relevant raw input data + * @param cube The raw {@code Cube} comprising the structure and properties of the cube + * @param properties The loaded properties for the model + * @param bone The bone this cube belongs to + */ + GeoCube constructCube(Cube cube, ModelProperties properties, GeoBone bone); + + /** + * Builtin method to construct the quad list from the various vertices and related data, to make it easier.
+ * Vertices have already been mirrored here if {@code mirror} is true + */ + default GeoQuad[] buildQuads(UVUnion uvUnion, VertexSet vertices, Cube cube, float textureWidth, float textureHeight, boolean mirror) { + GeoQuad[] quads = new GeoQuad[6]; + + quads[0] = buildQuad(vertices, cube, uvUnion, textureWidth, textureHeight, mirror, Direction.WEST); + quads[1] = buildQuad(vertices, cube, uvUnion, textureWidth, textureHeight, mirror, Direction.EAST); + quads[2] = buildQuad(vertices, cube, uvUnion, textureWidth, textureHeight, mirror, Direction.NORTH); + quads[3] = buildQuad(vertices, cube, uvUnion, textureWidth, textureHeight, mirror, Direction.SOUTH); + quads[4] = buildQuad(vertices, cube, uvUnion, textureWidth, textureHeight, mirror, Direction.UP); + quads[5] = buildQuad(vertices, cube, uvUnion, textureWidth, textureHeight, mirror, Direction.DOWN); + + return quads; + } + + /** + * Build an individual quad + */ + default GeoQuad buildQuad(VertexSet vertices, Cube cube, UVUnion uvUnion, float textureWidth, float textureHeight, boolean mirror, Direction direction) { + if (!uvUnion.isBoxUV()) { + FaceUV faceUV = uvUnion.faceUV().fromDirection(direction); + + if (faceUV == null) + return null; + + return GeoQuad.build(vertices.verticesForQuad(direction, false, mirror || cube.mirror() == Boolean.TRUE), faceUV.uv(), faceUV.uvSize(), + textureWidth, textureHeight, mirror, direction); + } + + double[] uv = cube.uv().boxUVCoords(); + double[] uvSize = cube.size(); + Vec3 uvSizeVec = new Vec3(Math.floor(uvSize[0]), Math.floor(uvSize[1]), Math.floor(uvSize[2])); + double[][] uvData = switch(direction) { + case WEST -> new double[][] { + new double[] {uv[0] + uvSizeVec.z + uvSizeVec.x, uv[1] + uvSizeVec.z}, + new double[] {uvSizeVec.z, uvSizeVec.y} + }; + case EAST -> new double[][] { + new double[] { uv[0], uv[1] + uvSizeVec.z }, + new double[] { uvSizeVec.z, uvSizeVec.y } + }; + case NORTH -> new double[][] { + new double[] {uv[0] + uvSizeVec.z, uv[1] + uvSizeVec.z}, + new double[] {uvSizeVec.x, uvSizeVec.y} + }; + case SOUTH -> new double[][] { + new double[] {uv[0] + uvSizeVec.z + uvSizeVec.x + uvSizeVec.z, uv[1] + uvSizeVec.z}, + new double[] {uvSizeVec.x, uvSizeVec.y } + }; + case UP -> new double[][] { + new double[] {uv[0] + uvSizeVec.z, uv[1]}, + new double[] {uvSizeVec.x, uvSizeVec.z} + }; + case DOWN -> new double[][] { + new double[] {uv[0] + uvSizeVec.z + uvSizeVec.x, uv[1] + uvSizeVec.z}, + new double[] {uvSizeVec.x, -uvSizeVec.z} + }; + }; + + return GeoQuad.build(vertices.verticesForQuad(direction, true, mirror || cube.mirror() == Boolean.TRUE), uvData[0], uvData[1], textureWidth, textureHeight, mirror, direction); + } + + static BakedModelFactory getForNamespace(String namespace) { + return FACTORIES.getOrDefault(namespace, DEFAULT_FACTORY); + } + + /** + * Register a custom {@link BakedModelFactory} to handle loading models in a custom way.
+ * MUST be called during mod construct
+ * It is recommended you don't call this directly, and instead call it via {@link AzureLibUtil#addCustomBakedModelFactory} + * @param namespace The namespace (modid) to register the factory for + * @param factory The factory responsible for model loading under the given namespace + */ + static void register(String namespace, BakedModelFactory factory) { + FACTORIES.put(namespace, factory); + } + + final class Builtin implements BakedModelFactory { + @Override + public BakedGeoModel constructGeoModel(GeometryTree geometryTree) { + List bones = new ObjectArrayList<>(); + + for (BoneStructure boneStructure : geometryTree.topLevelBones().values()) { + bones.add(constructBone(boneStructure, geometryTree.properties(), null)); + } + + return new BakedGeoModel(bones, geometryTree.properties()); + } + + @Override + public GeoBone constructBone(BoneStructure boneStructure, ModelProperties properties, GeoBone parent) { + Bone bone = boneStructure.self(); + GeoBone newBone = new GeoBone(parent, bone.name(), bone.mirror(), bone.inflate(), bone.neverRender(), bone.reset()); + Vec3 rotation = RenderUtils.arrayToVec(bone.rotation()); + Vec3 pivot = RenderUtils.arrayToVec(bone.pivot()); + + newBone.updateRotation((float)Math.toRadians(-rotation.x), (float)Math.toRadians(-rotation.y), (float)Math.toRadians(rotation.z)); + newBone.updatePivot((float)-pivot.x, (float)pivot.y, (float)pivot.z); + + for (Cube cube : bone.cubes()) { + newBone.getCubes().add(constructCube(cube, properties, newBone)); + } + + for (BoneStructure child : boneStructure.children().values()) { + newBone.getChildBones().add(constructBone(child, properties, newBone)); + } + + return newBone; + } + + @Override + public GeoCube constructCube(Cube cube, ModelProperties properties, GeoBone bone) { + boolean mirror = cube.mirror() == Boolean.TRUE; + double inflate = cube.inflate() != null ? cube.inflate() / 16f : (bone.getInflate() == null ? 0 : bone.getInflate() / 16f); + Vec3 size = RenderUtils.arrayToVec(cube.size()); + Vec3 origin = RenderUtils.arrayToVec(cube.origin()); + Vec3 rotation = RenderUtils.arrayToVec(cube.rotation()); + Vec3 pivot = RenderUtils.arrayToVec(cube.pivot()); + origin = new Vec3(-(origin.x + size.x) / 16d, origin.y / 16d, origin.z / 16d); + Vec3 vertexSize = size.multiply(1 / 16d, 1 / 16d, 1 / 16d); + + pivot = pivot.multiply(-1, 1, 1); + rotation = new Vec3(Math.toRadians(-rotation.x), Math.toRadians(-rotation.y), Math.toRadians(rotation.z)); + GeoQuad[] quads = buildQuads(cube.uv(), new VertexSet(origin, vertexSize, inflate), cube, (float)properties.textureWidth(), (float)properties.textureHeight(), mirror); + + return new GeoCube(quads, pivot, rotation, size, inflate, mirror); + } + } + + /** + * Holder class to make it easier to store and refer to vertices for a given cube + */ + record VertexSet(GeoVertex bottomLeftBack, GeoVertex bottomRightBack, GeoVertex topLeftBack, GeoVertex topRightBack, + GeoVertex topLeftFront, GeoVertex topRightFront, GeoVertex bottomLeftFront, GeoVertex bottomRightFront) { + public VertexSet(Vec3 origin, Vec3 vertexSize, double inflation) { + this( + new GeoVertex(origin.x - inflation, origin.y - inflation, origin.z - inflation), + new GeoVertex(origin.x - inflation, origin.y - inflation, origin.z + vertexSize.z + inflation), + new GeoVertex(origin.x - inflation, origin.y + vertexSize.y + inflation, origin.z - inflation), + new GeoVertex(origin.x - inflation, origin.y + vertexSize.y + inflation, origin.z + vertexSize.z + inflation), + new GeoVertex(origin.x + vertexSize.x + inflation, origin.y + vertexSize.y + inflation, origin.z - inflation), + new GeoVertex(origin.x + vertexSize.x + inflation, origin.y + vertexSize.y + inflation, origin.z + vertexSize.z + inflation), + new GeoVertex(origin.x + vertexSize.x + inflation, origin.y - inflation, origin.z - inflation), + new GeoVertex(origin.x + vertexSize.x + inflation, origin.y - inflation, origin.z + vertexSize.z + inflation)); + } + + /** + * Returns the normal vertex array for a west-facing quad + */ + public GeoVertex[] quadWest() { + return new GeoVertex[] {this.topRightBack, this.topLeftBack, this.bottomLeftBack, this.bottomRightBack}; + } + + /** + * Returns the normal vertex array for an east-facing quad + */ + public GeoVertex[] quadEast() { + return new GeoVertex[] {this.topLeftFront, this.topRightFront, this.bottomRightFront, this.bottomLeftFront}; + } + + /** + * Returns the normal vertex array for a north-facing quad + */ + public GeoVertex[] quadNorth() { + return new GeoVertex[] {this.topLeftBack, this.topLeftFront, this.bottomLeftFront, this.bottomLeftBack}; + } + + /** + * Returns the normal vertex array for a south-facing quad + */ + public GeoVertex[] quadSouth() { + return new GeoVertex[] {this.topRightFront, this.topRightBack, this.bottomRightBack, this.bottomRightFront}; + } + + /** + * Returns the normal vertex array for a top-facing quad + */ + public GeoVertex[] quadUp() { + return new GeoVertex[] {this.topRightBack, this.topRightFront, this.topLeftFront, this.topLeftBack}; + } + + /** + * Returns the normal vertex array for a bottom-facing quad + */ + public GeoVertex[] quadDown() { + return new GeoVertex[] {this.bottomLeftBack, this.bottomLeftFront, this.bottomRightFront, this.bottomRightBack}; + } + + /** + * Return the vertex array relevant to the quad being built, taking into account mirroring and quad type + */ + public GeoVertex[] verticesForQuad(Direction direction, boolean boxUv, boolean mirror) { + return switch (direction) { + case WEST -> mirror ? quadEast() : quadWest(); + case EAST -> mirror ? quadWest() : quadEast(); + case NORTH -> quadNorth(); + case SOUTH -> quadSouth(); + case UP -> mirror && !boxUv ? quadDown() : quadUp(); + case DOWN -> mirror && !boxUv ? quadUp() : quadDown(); + }; + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/object/BoneStructure.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/object/BoneStructure.java new file mode 100644 index 0000000..f0c03ea --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/object/BoneStructure.java @@ -0,0 +1,15 @@ +package mod.azure.azurelib.common.internal.common.loading.object; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import mod.azure.azurelib.common.internal.common.loading.json.raw.Bone; + +import java.util.Map; + +/** + * Container class for holding a {@link Bone} structure. Used at startup in deserialization + */ +public record BoneStructure(Bone self, Map children) { + public BoneStructure(Bone self) { + this(self, new Object2ObjectOpenHashMap<>()); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/object/GeometryTree.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/object/GeometryTree.java new file mode 100644 index 0000000..5c77cf9 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/loading/object/GeometryTree.java @@ -0,0 +1,66 @@ +package mod.azure.azurelib.common.internal.common.loading.object; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import mod.azure.azurelib.common.internal.common.loading.json.raw.Bone; +import mod.azure.azurelib.common.internal.common.loading.json.raw.MinecraftGeometry; +import mod.azure.azurelib.common.internal.common.loading.json.raw.Model; +import mod.azure.azurelib.common.internal.common.loading.json.raw.ModelProperties; + +import java.util.List; +import java.util.Map; + +/** + * Container class for a {@link Bone} structure, used at startup during deserialization + */ +public record GeometryTree(Map topLevelBones, ModelProperties properties) { + public static GeometryTree fromModel(Model model) { + Map topLevelBones = new Object2ObjectOpenHashMap<>(); + MinecraftGeometry geometry = model.minecraftGeometry()[0]; + List bones = new ObjectArrayList<>(geometry.bones()); + int index = bones.size() - 1; + + while (true) { + Bone bone = bones.get(index); + + if (bone.parent() == null) { + topLevelBones.put(bone.name(), new BoneStructure(bone)); + bones.remove(index); + } + else { + BoneStructure structure = findBoneStructureInTree(topLevelBones, bone.parent()); + + if (structure != null) { + structure.children().put(bone.name(), new BoneStructure(bone)); + bones.remove(index); + } + } + + if (index == 0) { + index = bones.size() - 1; + + if (index == -1) + break; + } + else { + index--; + } + } + + return new GeometryTree(topLevelBones, geometry.modelProperties()); + } + + private static BoneStructure findBoneStructureInTree(Map bones, String boneName) { + for (BoneStructure entry : bones.values()) { + if (boneName.equals(entry.self().name())) + return entry; + + BoneStructure subStructure = findBoneStructureInTree(entry.children(), boneName); + + if (subStructure != null) + return subStructure; + } + + return null; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/network/AbstractPacket.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/network/AbstractPacket.java new file mode 100644 index 0000000..72887c8 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/network/AbstractPacket.java @@ -0,0 +1,14 @@ +package mod.azure.azurelib.common.internal.common.network; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; + +public abstract class AbstractPacket implements CustomPacketPayload { + + public abstract void write(FriendlyByteBuf buf); + + public abstract void handle(); + + public abstract ResourceLocation id(); +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/network/SerializableDataTicket.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/network/SerializableDataTicket.java new file mode 100644 index 0000000..8430d9f --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/network/SerializableDataTicket.java @@ -0,0 +1,139 @@ +package mod.azure.azurelib.common.internal.common.network; + +import mod.azure.azurelib.common.internal.common.core.object.DataTicket; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; + +/** + * Network-compatible {@link DataTicket} implementation. + * Used for sending data from server -> client in an easy manner + */ +public abstract class SerializableDataTicket extends DataTicket { + protected SerializableDataTicket(String id, Class objectType) { + super(id, objectType); + } + + /** + * Encode the object to a packet buffer for transmission + * @param data The object to be serialized + * @param buffer The buffer to serialize the object to + */ + public abstract void encode(D data, FriendlyByteBuf buffer); + + /** + * Decode the object from a packet buffer after transmission + * @param buffer The buffer to deserialize the object from + * @return A new instance of your data object + */ + public abstract D decode(FriendlyByteBuf buffer); + + // Pre-defined typings for use + + /** + * Generate a new {@code SerializableDataTicket} for the given id + * @param id The unique id of your ticket. Include your modid + */ + public static SerializableDataTicket ofDouble(ResourceLocation id) { + return new SerializableDataTicket<>(id.toString(), Double.class) { + @Override + public void encode(Double data, FriendlyByteBuf buffer) { + buffer.writeDouble(data); + } + + @Override + public Double decode(FriendlyByteBuf buffer) { + return buffer.readDouble(); + } + }; + } + + /** + * Generate a new {@code SerializableDataTicket} for the given id + * @param id The unique id of your ticket. Include your modid + */ + public static SerializableDataTicket ofFloat(ResourceLocation id) { + return new SerializableDataTicket<>(id.toString(), Float.class) { + @Override + public void encode(Float data, FriendlyByteBuf buffer) { + buffer.writeFloat(data); + } + + @Override + public Float decode(FriendlyByteBuf buffer) { + return buffer.readFloat(); + } + }; + } + + /** + * Generate a new {@code SerializableDataTicket} for the given id + * @param id The unique id of your ticket. Include your modid + */ + public static SerializableDataTicket ofBoolean(ResourceLocation id) { + return new SerializableDataTicket<>(id.toString(), Boolean.class) { + @Override + public void encode(Boolean data, FriendlyByteBuf buffer) { + buffer.writeBoolean(data); + } + + @Override + public Boolean decode(FriendlyByteBuf buffer) { + return buffer.readBoolean(); + } + }; + } + + /** + * Generate a new {@code SerializableDataTicket} for the given id + * @param id The unique id of your ticket. Include your modid + */ + public static SerializableDataTicket ofInt(ResourceLocation id) { + return new SerializableDataTicket<>(id.toString(), Integer.class) { + @Override + public void encode(Integer data, FriendlyByteBuf buffer) { + buffer.writeVarInt(data); + } + + @Override + public Integer decode(FriendlyByteBuf buffer) { + return buffer.readVarInt(); + } + }; + } + + /** + * Generate a new {@code SerializableDataTicket} for the given id + * @param id The unique id of your ticket. Include your modid + */ + public static SerializableDataTicket ofString(ResourceLocation id) { + return new SerializableDataTicket<>(id.toString(), String.class) { + @Override + public void encode(String data, FriendlyByteBuf buffer) { + buffer.writeUtf(data); + } + + @Override + public String decode(FriendlyByteBuf buffer) { + return buffer.readUtf(); + } + }; + } + + /** + * Generate a new {@code SerializableDataTicket} for the given id + * @param id The unique id of your ticket. Include your modid + */ + public static > SerializableDataTicket ofEnum(ResourceLocation id, Class enumClass) { + return new SerializableDataTicket<>(id.toString(), enumClass) { + @Override + public void encode(E data, FriendlyByteBuf buffer) { + buffer.writeUtf(data.toString()); + } + + @Override + public E decode(FriendlyByteBuf buffer) { + return Enum.valueOf(enumClass, buffer.readUtf()); + } + }; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/network/api/IPacket.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/network/api/IPacket.java new file mode 100644 index 0000000..2adc2bd --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/network/api/IPacket.java @@ -0,0 +1,14 @@ +package mod.azure.azurelib.common.internal.common.network.api; + +import net.minecraft.resources.ResourceLocation; + +public interface IPacket { + + ResourceLocation getPacketId(); + + T getPacketData(); + + IPacketEncoder getEncoder(); + + IPacketDecoder getDecoder(); +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/network/api/IPacketDecoder.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/network/api/IPacketDecoder.java new file mode 100644 index 0000000..2766995 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/network/api/IPacketDecoder.java @@ -0,0 +1,9 @@ +package mod.azure.azurelib.common.internal.common.network.api; + +import net.minecraft.network.FriendlyByteBuf; + +@FunctionalInterface +public interface IPacketDecoder { + + T decode(FriendlyByteBuf buffer); +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/network/api/IPacketEncoder.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/network/api/IPacketEncoder.java new file mode 100644 index 0000000..dc9e569 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/network/api/IPacketEncoder.java @@ -0,0 +1,9 @@ +package mod.azure.azurelib.common.internal.common.network.api; + +import net.minecraft.network.FriendlyByteBuf; + +@FunctionalInterface +public interface IPacketEncoder { + + void encode(T data, FriendlyByteBuf buffer); +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/network/packet/AnimDataSyncPacket.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/network/packet/AnimDataSyncPacket.java new file mode 100644 index 0000000..70d3ff3 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/network/packet/AnimDataSyncPacket.java @@ -0,0 +1,62 @@ +package mod.azure.azurelib.common.internal.common.network.packet; + +import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable; +import mod.azure.azurelib.common.internal.common.animatable.SingletonGeoAnimatable; +import mod.azure.azurelib.common.internal.common.constant.DataTickets; +import mod.azure.azurelib.common.internal.common.network.AbstractPacket; +import mod.azure.azurelib.common.internal.common.network.SerializableDataTicket; +import mod.azure.azurelib.common.platform.services.AzureLibNetwork; +import mod.azure.azurelib.common.api.client.helper.ClientUtils; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; + +import static mod.azure.azurelib.common.platform.services.AzureLibNetwork.ANIM_DATA_SYNC_PACKET_ID; + +/** + * Packet for syncing user-definable animation data for + * {@link SingletonGeoAnimatable} instances + */ +public class AnimDataSyncPacket extends AbstractPacket { + private final String syncableId; + private final long instanceId; + private final SerializableDataTicket dataTicket; + private final D data; + + public AnimDataSyncPacket(String syncableId, long instanceId, SerializableDataTicket dataTicket, D data) { + this.syncableId = syncableId; + this.instanceId = instanceId; + this.dataTicket = dataTicket; + this.data = data; + } + + @Override + public void write(FriendlyByteBuf buf) { + buf.writeUtf(this.syncableId); + buf.writeVarLong(this.instanceId); + buf.writeUtf(this.dataTicket.id()); + this.dataTicket.encode(this.data, buf); + } + + @Override + public ResourceLocation id() { + return ANIM_DATA_SYNC_PACKET_ID; + } + + public static AnimDataSyncPacket receive(FriendlyByteBuf buf) { + String syncableId = buf.readUtf(); + long instanceID = buf.readVarLong(); + SerializableDataTicket dataTicket = (SerializableDataTicket) DataTickets.byName(buf.readUtf()); + D data = dataTicket.decode(buf); + + return new AnimDataSyncPacket<>(syncableId, instanceID, dataTicket, data); + } + + @Override + public void handle() { + GeoAnimatable animatable = AzureLibNetwork.getSyncedAnimatable(syncableId); + + if (animatable instanceof SingletonGeoAnimatable singleton) { + singleton.setAnimData(ClientUtils.getClientPlayer(), instanceId, dataTicket, data); + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/network/packet/AnimTriggerPacket.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/network/packet/AnimTriggerPacket.java new file mode 100644 index 0000000..16f4c13 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/network/packet/AnimTriggerPacket.java @@ -0,0 +1,55 @@ +package mod.azure.azurelib.common.internal.common.network.packet; + +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.AbstractPacket; +import mod.azure.azurelib.common.platform.services.AzureLibNetwork; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.Nullable; + +/** + * Packet for syncing user-definable animations that can be triggered from the + * server + */ +public class AnimTriggerPacket extends AbstractPacket { + private final String syncableId; + private final long instanceId; + private final String controllerName; + private final String animName; + + public AnimTriggerPacket(String syncableId, long instanceId, @Nullable String controllerName, String animName) { + this.syncableId = syncableId; + this.instanceId = instanceId; + this.controllerName = controllerName == null ? "" : controllerName; + this.animName = animName; + } + + @Override + public void write(FriendlyByteBuf buf) { + buf.writeUtf(this.syncableId); + buf.writeVarLong(this.instanceId); + buf.writeUtf(this.controllerName); + buf.writeUtf(this.animName); + } + + @Override + public ResourceLocation id() { + return AzureLibNetwork.ANIM_TRIGGER_SYNC_PACKET_ID; + } + + public static AnimTriggerPacket receive(FriendlyByteBuf buf) { + return new AnimTriggerPacket(buf.readUtf(), buf.readVarLong(), buf.readUtf(), buf.readUtf()); + } + + @Override + public void handle() { + GeoAnimatable animatable = AzureLibNetwork.getSyncedAnimatable(syncableId); + + if (animatable != null) { + AnimatableManager manager = animatable.getAnimatableInstanceCache().getManagerForId(instanceId); + + manager.tryTriggerAnimation(controllerName, animName); + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/network/packet/BlockEntityAnimDataSyncPacket.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/network/packet/BlockEntityAnimDataSyncPacket.java new file mode 100644 index 0000000..9250249 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/network/packet/BlockEntityAnimDataSyncPacket.java @@ -0,0 +1,56 @@ +package mod.azure.azurelib.common.internal.common.network.packet; + +import mod.azure.azurelib.common.api.common.animatable.GeoBlockEntity; +import mod.azure.azurelib.common.internal.common.constant.DataTickets; +import mod.azure.azurelib.common.internal.common.network.AbstractPacket; +import mod.azure.azurelib.common.internal.common.network.SerializableDataTicket; +import mod.azure.azurelib.common.platform.services.AzureLibNetwork; +import mod.azure.azurelib.common.api.client.helper.ClientUtils; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.entity.BlockEntity; + +/** + * Packet for syncing user-definable animation data for {@link BlockEntity + * BlockEntities} + */ +public class BlockEntityAnimDataSyncPacket extends AbstractPacket { + private final BlockPos blockPos; + private final SerializableDataTicket dataTicket; + private final D data; + + public BlockEntityAnimDataSyncPacket(BlockPos pos, SerializableDataTicket dataTicket, D data) { + this.blockPos = pos; + this.dataTicket = dataTicket; + this.data = data; + } + + @Override + public void write(FriendlyByteBuf buf) { + buf.writeBlockPos(this.blockPos); + buf.writeUtf(this.dataTicket.id()); + this.dataTicket.encode(this.data, buf); + } + + @Override + public ResourceLocation id() { + return AzureLibNetwork.BLOCK_ENTITY_ANIM_DATA_SYNC_PACKET_ID; + } + + public static BlockEntityAnimDataSyncPacket receive(FriendlyByteBuf buf) { + BlockPos pos = buf.readBlockPos(); + SerializableDataTicket dataTicket = (SerializableDataTicket) DataTickets.byName(buf.readUtf()); + + return new BlockEntityAnimDataSyncPacket<>(pos, dataTicket, dataTicket.decode(buf)); + } + + @Override + public void handle() { + BlockEntity blockEntity = ClientUtils.getLevel().getBlockEntity(blockPos); + + if (blockEntity instanceof GeoBlockEntity geoBlockEntity) { + geoBlockEntity.setAnimData(dataTicket, data); + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/network/packet/BlockEntityAnimTriggerPacket.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/network/packet/BlockEntityAnimTriggerPacket.java new file mode 100644 index 0000000..b7885c1 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/network/packet/BlockEntityAnimTriggerPacket.java @@ -0,0 +1,54 @@ +package mod.azure.azurelib.common.internal.common.network.packet; + +import mod.azure.azurelib.common.platform.services.AzureLibNetwork; +import org.jetbrains.annotations.Nullable; + +import mod.azure.azurelib.common.api.common.animatable.GeoBlockEntity; +import mod.azure.azurelib.common.internal.common.network.AbstractPacket; +import mod.azure.azurelib.common.api.client.helper.ClientUtils; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.entity.BlockEntity; + +/** + * Packet for syncing user-definable animations that can be triggered from the + * server for {@link net.minecraft.world.level.block.entity.BlockEntity + * BlockEntities} + */ +public class BlockEntityAnimTriggerPacket extends AbstractPacket { + private final BlockPos blockPos; + private final String controllerName; + private final String animName; + + public BlockEntityAnimTriggerPacket(BlockPos blockPos, @Nullable String controllerName, String animName) { + this.blockPos = blockPos; + this.controllerName = controllerName == null ? "" : controllerName; + this.animName = animName; + } + + @Override + public void write(FriendlyByteBuf buf) { + buf.writeBlockPos(this.blockPos); + buf.writeUtf(this.controllerName); + buf.writeUtf(this.animName); + } + + @Override + public ResourceLocation id() { + return AzureLibNetwork.BLOCK_ENTITY_ANIM_TRIGGER_SYNC_PACKET_ID; + } + + public static BlockEntityAnimTriggerPacket receive(FriendlyByteBuf buf) { + return new BlockEntityAnimTriggerPacket(buf.readBlockPos(), buf.readUtf(), buf.readUtf()); + } + + @Override + public void handle() { + BlockEntity blockEntity = ClientUtils.getLevel().getBlockEntity(blockPos); + + if (blockEntity instanceof GeoBlockEntity getBlockEntity) { + getBlockEntity.triggerAnim(controllerName.isEmpty() ? null : controllerName, animName); + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/network/packet/EntityAnimDataSyncPacket.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/network/packet/EntityAnimDataSyncPacket.java new file mode 100644 index 0000000..7d3bea3 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/network/packet/EntityAnimDataSyncPacket.java @@ -0,0 +1,55 @@ +package mod.azure.azurelib.common.internal.common.network.packet; + +import mod.azure.azurelib.common.api.common.animatable.GeoEntity; +import mod.azure.azurelib.common.internal.common.constant.DataTickets; +import mod.azure.azurelib.common.internal.common.network.AbstractPacket; +import mod.azure.azurelib.common.internal.common.network.SerializableDataTicket; +import mod.azure.azurelib.common.platform.services.AzureLibNetwork; +import mod.azure.azurelib.common.api.client.helper.ClientUtils; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Entity; + +/** + * Packet for syncing user-definable animation data for + * {@link net.minecraft.world.entity.Entity Entities} + */ +public class EntityAnimDataSyncPacket extends AbstractPacket { + private final int entityId; + private final SerializableDataTicket dataTicket; + private final D data; + + public EntityAnimDataSyncPacket(int entityId, SerializableDataTicket dataTicket, D data) { + this.entityId = entityId; + this.dataTicket = dataTicket; + this.data = data; + } + + @Override + public void write(FriendlyByteBuf buf) { + buf.writeVarInt(this.entityId); + buf.writeUtf(this.dataTicket.id()); + this.dataTicket.encode(this.data, buf); + } + + @Override + public ResourceLocation id() { + return AzureLibNetwork.ENTITY_ANIM_DATA_SYNC_PACKET_ID; + } + + public static EntityAnimDataSyncPacket receive(FriendlyByteBuf buf) { + int entityId = buf.readVarInt(); + SerializableDataTicket dataTicket = (SerializableDataTicket) DataTickets.byName(buf.readUtf()); + + return new EntityAnimDataSyncPacket<>(entityId, dataTicket, dataTicket.decode(buf)); + } + + @Override + public void handle() { + Entity entity = ClientUtils.getLevel().getEntity(entityId); + + if (entity instanceof GeoEntity geoEntity) { + geoEntity.setAnimData(dataTicket, data); + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/network/packet/EntityAnimTriggerPacket.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/network/packet/EntityAnimTriggerPacket.java new file mode 100644 index 0000000..4b9ab80 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/network/packet/EntityAnimTriggerPacket.java @@ -0,0 +1,72 @@ +package mod.azure.azurelib.common.internal.common.network.packet; + +import mod.azure.azurelib.common.api.common.animatable.GeoEntity; +import mod.azure.azurelib.common.api.common.animatable.GeoReplacedEntity; +import mod.azure.azurelib.common.internal.client.util.RenderUtils; +import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable; +import mod.azure.azurelib.common.internal.common.network.AbstractPacket; +import mod.azure.azurelib.common.platform.services.AzureLibNetwork; +import mod.azure.azurelib.common.api.client.helper.ClientUtils; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Entity; +import org.jetbrains.annotations.Nullable; + +/** + * Packet for syncing user-definable animations that can be triggered from the + * server for {@link net.minecraft.world.entity.Entity Entities} + */ +public class EntityAnimTriggerPacket extends AbstractPacket { + private final int entityId; + private final boolean isReplacedEntity; + + private final String controllerName; + private final String animName; + + public EntityAnimTriggerPacket(int entityId, @Nullable String controllerName, String animName) { + this(entityId, false, controllerName, animName); + } + + public EntityAnimTriggerPacket(int entityId, boolean isReplacedEntity, @Nullable String controllerName, + String animName) { + this.entityId = entityId; + this.isReplacedEntity = isReplacedEntity; + this.controllerName = controllerName == null ? "" : controllerName; + this.animName = animName; + } + + @Override + public void write(FriendlyByteBuf buf) { + buf.writeVarInt(this.entityId); + buf.writeBoolean(this.isReplacedEntity); + + buf.writeUtf(this.controllerName); + buf.writeUtf(this.animName); + } + + @Override + public ResourceLocation id() { + return AzureLibNetwork.ENTITY_ANIM_TRIGGER_SYNC_PACKET_ID; + } + + public static EntityAnimTriggerPacket receive(FriendlyByteBuf buf) { + return new EntityAnimTriggerPacket(buf.readVarInt(), buf.readBoolean(), buf.readUtf(), buf.readUtf()); + } + + public void handle() { + Entity entity = ClientUtils.getLevel().getEntity(entityId); + if (entity == null) + return; + + if (!isReplacedEntity) { + if (entity instanceof GeoEntity geoEntity) { + geoEntity.triggerAnim(controllerName.isEmpty() ? null : controllerName, animName); + } + return; + } + + GeoAnimatable animatable = RenderUtils.getReplacedAnimatable(entity.getType()); + if (animatable instanceof GeoReplacedEntity replacedEntity) + replacedEntity.triggerAnim(entity, controllerName.isEmpty() ? null : controllerName, animName); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/network/packet/EntityPacket.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/network/packet/EntityPacket.java new file mode 100644 index 0000000..9b4e44c --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/network/packet/EntityPacket.java @@ -0,0 +1,15 @@ +package mod.azure.azurelib.common.internal.common.network.packet; + +import mod.azure.azurelib.common.platform.Services; +import net.minecraft.network.protocol.Packet; +import net.minecraft.world.entity.Entity; + +public class EntityPacket { + public static Packet createPacket(Entity entity) { + return Services.NETWORK.createPacket(entity); + } + + private EntityPacket() { + throw new UnsupportedOperationException(); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/network/packet/EntityPacketOnClient.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/network/packet/EntityPacketOnClient.java new file mode 100644 index 0000000..5bc2a1b --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/network/packet/EntityPacketOnClient.java @@ -0,0 +1,40 @@ +package mod.azure.azurelib.common.internal.common.network.packet; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; + +import java.util.UUID; + +public class EntityPacketOnClient { + public static void onPacket(Minecraft context, FriendlyByteBuf byteBuf) { + EntityType type = BuiltInRegistries.ENTITY_TYPE.byId(byteBuf.readVarInt()); + UUID entityUUID = byteBuf.readUUID(); + int entityID = byteBuf.readVarInt(); + double x = byteBuf.readDouble(); + double y = byteBuf.readDouble(); + double z = byteBuf.readDouble(); + float pitch = (byteBuf.readByte() * 360) / 256.0F; + float yaw = (byteBuf.readByte() * 360) / 256.0F; + context.execute(() -> { + ClientLevel world = Minecraft.getInstance().level; + Entity entity = type.create(world); + if (entity != null) { + entity.absMoveTo(x, y, z); + entity.syncPacketPositionCodec(x, y, z); + entity.setXRot(pitch); + entity.setYRot(yaw); + entity.setId(entityID); + entity.setUUID(entityUUID); + world.addEntity(entity); + } + }); + } + + private EntityPacketOnClient() { + throw new UnsupportedOperationException(); + } +} \ No newline at end of file diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/util/AzureLibUtil.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/util/AzureLibUtil.java new file mode 100644 index 0000000..87be77f --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/util/AzureLibUtil.java @@ -0,0 +1,148 @@ +package mod.azure.azurelib.common.internal.common.util; + +import mod.azure.azurelib.common.api.common.items.AzureBaseGunItem; +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.InstancedAnimatableInstanceCache; +import mod.azure.azurelib.common.internal.common.core.animatable.instance.SingletonAnimatableInstanceCache; +import mod.azure.azurelib.common.internal.common.core.animation.Animation; +import mod.azure.azurelib.common.internal.common.core.animation.EasingType; +import mod.azure.azurelib.common.internal.common.core.object.DataTicket; +import mod.azure.azurelib.common.internal.common.loading.object.BakedModelFactory; +import mod.azure.azurelib.common.internal.common.network.SerializableDataTicket; +import mod.azure.azurelib.common.platform.Services; +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +/** + * Helper class for various AzureLib-specific functions. + */ +public final record AzureLibUtil() { + /** + * Creates a new AnimatableInstanceCache for the given animatable object + * + * @param animatable The animatable object + */ + public static AnimatableInstanceCache createInstanceCache(GeoAnimatable animatable) { + AnimatableInstanceCache cache = animatable.animatableCacheOverride(); + + return cache != null ? cache : createInstanceCache(animatable, + !(animatable instanceof Entity) && !(animatable instanceof BlockEntity)); + } + + /** + * Creates a new AnimatableInstanceCache for the given animatable object.
+ * Recommended to use {@link AzureLibUtil#createInstanceCache(GeoAnimatable)} unless you know what you're doing. + * + * @param animatable The animatable object + * @param singletonObject Whether the object is a singleton/flyweight object, and uses ints to differentiate animatable instances + */ + public static AnimatableInstanceCache createInstanceCache(GeoAnimatable animatable, boolean singletonObject) { + AnimatableInstanceCache cache = animatable.animatableCacheOverride(); + + if (cache != null) return cache; + + return singletonObject ? new SingletonAnimatableInstanceCache( + animatable) : new InstancedAnimatableInstanceCache(animatable); + } + + /** + * Register a custom {@link Animation.LoopType} with AzureLib, allowing for dynamic handling of post-animation looping.
+ * MUST be called during mod construct
+ * + * @param name The name of the {@code LoopType} handler + * @param loopType The {@code LoopType} implementation to use for the given name + */ + public static synchronized Animation.LoopType addCustomLoopType(String name, Animation.LoopType loopType) { + return Animation.LoopType.register(name, loopType); + } + + /** + * Register a custom {@link EasingType} with AzureLib, allowing for dynamic handling of animation transitions and curves.
+ * MUST be called during mod construct
+ * + * @param name The name of the {@code EasingType} handler + * @param easingType The {@code EasingType} implementation to use for the given name + */ + public static synchronized EasingType addCustomEasingType(String name, EasingType easingType) { + return EasingType.register(name, easingType); + } + + /** + * Register a custom {@link BakedModelFactory} with AzureLib, allowing for dynamic handling of geo model loading.
+ * MUST be called during mod construct
+ * + * @param namespace The namespace (modid) to register the factory for + * @param factory The factory responsible for model loading under the given namespace + */ + public static synchronized void addCustomBakedModelFactory(String namespace, BakedModelFactory factory) { + BakedModelFactory.register(namespace, factory); + } + + /** + * Register a custom {@link SerializableDataTicket} with AzureLib for handling custom data transmission.
+ * NOTE: You do not need to register non-serializable {@link DataTicket DataTickets}. + * + * @param dataTicket The SerializableDataTicket to register + * @return The dataTicket you passed in + */ + public static synchronized SerializableDataTicket addDataTicket(SerializableDataTicket dataTicket) { + return DataTickets.registerSerializable(dataTicket); + } + + public static boolean checkDistance(BlockPos blockPosA, BlockPos blockPosB, int distance) { + return Math.abs(blockPosA.getX() - blockPosB.getX()) <= distance && Math.abs( + blockPosA.getY() - blockPosB.getY()) <= distance && Math.abs( + blockPosA.getZ() - blockPosB.getZ()) <= distance; + } + + public static BlockPos findFreeSpace(Level world, BlockPos blockPos, int maxDistance) { + if (blockPos == null) return null; + + int[] offsets = new int[maxDistance * 2 + 1]; + offsets[0] = 0; + for (int i = 2; i <= maxDistance * 2; i += 2) { + offsets[i - 1] = i / 2; + offsets[i] = -i / 2; + } + for (int x : offsets) + for (int y : offsets) + for (int z : offsets) { + BlockPos offsetPos = blockPos.offset(x, y, z); + BlockState state = world.getBlockState(offsetPos); + if (state.isAir() || state.getBlock().equals(Services.PLATFORM.getTickingLightBlock())) + return offsetPos; + } + return null; + } + + /** + * Removes matching item from offhand first then checks inventory for item + * + * @param ammo Item you want to be used as ammo + * @param playerEntity Player whose inventory is being checked. + */ + public static void removeAmmo(Item ammo, Player playerEntity) { + if ((playerEntity.getItemInHand( + playerEntity.getUsedItemHand()).getItem() instanceof AzureBaseGunItem) && !playerEntity.isCreative()) { // Creative mode reloading breaks things + for (var item : playerEntity.getInventory().offhand) { + if (item.getItem() == ammo) { + item.shrink(1); + break; + } + for (var item1 : playerEntity.getInventory().items) { + if (item1.getItem() == ammo) { + item1.shrink(1); + break; + } + } + } + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/util/IncompatibleModsCheck.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/util/IncompatibleModsCheck.java new file mode 100644 index 0000000..9bb29ea --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/util/IncompatibleModsCheck.java @@ -0,0 +1,30 @@ +package mod.azure.azurelib.common.internal.common.util; + +import mod.azure.azurelib.common.internal.client.config.screen.OptifineWarningScreen; +import mod.azure.azurelib.common.internal.common.AzureLib; +import mod.azure.azurelib.common.internal.common.AzureLibMod; +import net.minecraft.client.Minecraft; + +public class IncompatibleModsCheck { + public static boolean optifinePresent = false; + + public static void run() { + try { + Class.forName("net.optifine.Config"); + optifinePresent = true; + } catch (ClassNotFoundException e) { + optifinePresent = false; + } + } + + public static void warnings(Minecraft mc) { + if (IncompatibleModsCheck.optifinePresent) { + if (AzureLibMod.config.disableOptifineWarning) { + AzureLib.LOGGER.fatal("Optifine Has been detected, Disabled Warning Status: false"); + mc.setScreen(new OptifineWarningScreen()); + } else { + AzureLib.LOGGER.fatal("Optifine Has been detected, Disabled Warning Status: true"); + } + } + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/common/util/JsonUtil.java b/common/src/main/java/mod/azure/azurelib/common/internal/common/util/JsonUtil.java new file mode 100644 index 0000000..50e9c67 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/common/util/JsonUtil.java @@ -0,0 +1,168 @@ +package mod.azure.azurelib.common.internal.common.util; + +import java.lang.reflect.Array; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import mod.azure.azurelib.common.internal.common.core.animation.Animation; +import org.jetbrains.annotations.Nullable; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import mod.azure.azurelib.common.internal.common.loading.json.raw.Bone; +import mod.azure.azurelib.common.internal.common.loading.json.raw.Cube; +import mod.azure.azurelib.common.internal.common.loading.json.raw.FaceUV; +import mod.azure.azurelib.common.internal.common.loading.json.raw.LocatorClass; +import mod.azure.azurelib.common.internal.common.loading.json.raw.LocatorValue; +import mod.azure.azurelib.common.internal.common.loading.json.raw.MinecraftGeometry; +import mod.azure.azurelib.common.internal.common.loading.json.raw.Model; +import mod.azure.azurelib.common.internal.common.loading.json.raw.ModelProperties; +import mod.azure.azurelib.common.internal.common.loading.json.raw.PolyMesh; +import mod.azure.azurelib.common.internal.common.loading.json.raw.PolysUnion; +import mod.azure.azurelib.common.internal.common.loading.json.raw.TextureMesh; +import mod.azure.azurelib.common.internal.common.loading.json.raw.UVFaces; +import mod.azure.azurelib.common.internal.common.loading.json.raw.UVUnion; +import mod.azure.azurelib.common.internal.common.loading.json.typeadapter.BakedAnimationsAdapter; +import mod.azure.azurelib.common.internal.common.loading.json.typeadapter.KeyFramesAdapter; +import mod.azure.azurelib.common.internal.common.loading.object.BakedAnimations; +import net.minecraft.util.GsonHelper; + +/** + * Json helper class for various json functions + */ +public final class JsonUtil { + public static final Gson GEO_GSON = new GsonBuilder().setLenient() + .registerTypeAdapter(Bone.class, Bone.deserializer()) + .registerTypeAdapter(Cube.class, Cube.deserializer()) + .registerTypeAdapter(FaceUV.class, FaceUV.deserializer()) + .registerTypeAdapter(LocatorClass.class, LocatorClass.deserializer()) + .registerTypeAdapter(LocatorValue.class, LocatorValue.deserializer()) + .registerTypeAdapter(MinecraftGeometry.class, MinecraftGeometry.deserializer()) + .registerTypeAdapter(Model.class, Model.deserializer()) + .registerTypeAdapter(ModelProperties.class, ModelProperties.deserializer()) + .registerTypeAdapter(PolyMesh.class, PolyMesh.deserializer()) + .registerTypeAdapter(PolysUnion.class, PolysUnion.deserializer()) + .registerTypeAdapter(TextureMesh.class, TextureMesh.deserializer()) + .registerTypeAdapter(UVFaces.class, UVFaces.deserializer()) + .registerTypeAdapter(UVUnion.class, UVUnion.deserializer()) + .registerTypeAdapter(Animation.Keyframes.class, new KeyFramesAdapter()) + .registerTypeAdapter(BakedAnimations.class, new BakedAnimationsAdapter()) + .create(); + + /** + * Convert a {@link JsonArray} of doubles to a {@code double[]}.
+ * No type checking is done, so if the array contains anything other than doubles, this will throw an exception.
+ * Ensures a minimum size of 3, as this is the expected usage of this method + */ + public static double[] jsonArrayToDoubleArray(@Nullable JsonArray array) throws JsonParseException { + if (array == null) + return new double[3]; + + double[] output = new double[array.size()]; + + for (int i = 0; i < array.size(); i++) { + output[i] = array.get(i).getAsDouble(); + } + + return output; + } + + /** + * Converts a {@link JsonArray} of a given object type to an array of that object, deserialized from their respective {@link JsonElement JsonElements} + * @param array The array containing the objects to be converted + * @param context The {@link com.google.gson.Gson} context for deserialization + * @param objectClass The object type that the array contains + */ + public static T[] jsonArrayToObjectArray(JsonArray array, JsonDeserializationContext context, Class objectClass) { + T[] objArray = (T[])Array.newInstance(objectClass, array.size()); + + for (int i = 0; i < array.size(); i++) { + objArray[i] = context.deserialize(array.get(i), objectClass); + } + + return objArray; + } + + /** + * Converts a {@link JsonArray} to a {@link List} of elements of a pre-determined type. + * @param array The {@code JsonArray} to convert + * @param elementTransformer Transformation function that converts a {@link JsonElement} to the intended output object + */ + public static List jsonArrayToList(@Nullable JsonArray array, Function elementTransformer) { + if (array == null) + return new ObjectArrayList<>(); + + List list = new ObjectArrayList<>(array.size()); + + for (JsonElement element : array) { + list.add(elementTransformer.apply(element)); + } + + return list; + } + + /** + * Converts a {@link JsonObject} to a {@link Map} of String keys to their respective objects + * @param obj The base {@code JsonObject} to convert + * @param context The {@link Gson} deserialization context + * @param objectType The object class that the map should contain + */ + public static Map jsonObjToMap(JsonObject obj, JsonDeserializationContext context, Class objectType) { + Map map = new Object2ObjectOpenHashMap<>(obj.size()); + + for (Map.Entry entry : obj.entrySet()) { + map.put(entry.getKey(), context.deserialize(entry.getValue(), objectType)); + } + + return map; + } + + /** + * Retrieves an optionally present Long from the provided {@link JsonObject}, or null if the element isn't present + */ + @Nullable + public static Long getOptionalLong(JsonObject obj, String elementName) { + return obj.has(elementName) ? GsonHelper.getAsLong(obj, elementName) : null; + } + + /** + * Retrieves an optionally present Boolean from the provided {@link JsonObject}, or null if the element isn't present + */ + @Nullable + public static Boolean getOptionalBoolean(JsonObject obj, String elementName) { + return obj.has(elementName) ? GsonHelper.getAsBoolean(obj, elementName) : null; + } + + /** + * Retrieves an optionally present Float from the provided {@link JsonObject}, or null if the element isn't present + */ + @Nullable + public static Float getOptionalFloat(JsonObject obj, String elementName) { + return obj.has(elementName) ? GsonHelper.getAsFloat(obj, elementName) : null; + } + + /** + * Retrieves an optionally present Double from the provided {@link JsonObject}, or null if the element isn't present + */ + @Nullable + public static Double getOptionalDouble(JsonObject obj, String elementName) { + return obj.has(elementName) ? GsonHelper.getAsDouble(obj, elementName) : null; + } + + /** + * Retrieves an optionally present Integer from the provided {@link JsonObject}, or null if the element isn't present + */ + @Nullable + public static Integer getOptionalInteger(JsonObject obj, String elementName) { + return obj.has(elementName) ? GsonHelper.getAsInt(obj, elementName) : null; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/mixins/AccessorWarningScreen.java b/common/src/main/java/mod/azure/azurelib/common/internal/mixins/AccessorWarningScreen.java new file mode 100644 index 0000000..8bbad44 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/mixins/AccessorWarningScreen.java @@ -0,0 +1,15 @@ +package mod.azure.azurelib.common.internal.mixins; + +import net.minecraft.client.gui.components.MultiLineLabel; +import net.minecraft.client.gui.screens.multiplayer.WarningScreen; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(WarningScreen.class) +public interface AccessorWarningScreen { + @Accessor("message") + MultiLineLabel getMessageText(); + + @Accessor("message") + void setMessageText(MultiLineLabel messageText); +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/mixins/FabricMixinHumanoidArmorLayer.java b/common/src/main/java/mod/azure/azurelib/common/internal/mixins/FabricMixinHumanoidArmorLayer.java new file mode 100644 index 0000000..b9dbc65 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/mixins/FabricMixinHumanoidArmorLayer.java @@ -0,0 +1,41 @@ +package mod.azure.azurelib.common.internal.mixins; + +import com.mojang.blaze3d.vertex.PoseStack; +import mod.azure.azurelib.common.api.common.animatable.GeoItem; +import mod.azure.azurelib.common.internal.client.RenderProvider; +import net.minecraft.client.model.HumanoidModel; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.entity.layers.HumanoidArmorLayer; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.item.ItemStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyArg; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * Render hook for injecting AzureLib's armor rendering functionalities + */ +@Mixin(value = HumanoidArmorLayer.class, priority = 700) +public abstract class FabricMixinHumanoidArmorLayer> { + @Unique + private LivingEntity gl_storedEntity; + @Unique + private EquipmentSlot gl_storedSlot; + @Unique + private ItemStack gl_storedItemStack; + + @Inject(method = "renderArmorPiece", at = @At(value = "HEAD")) + public void armorModelHook(PoseStack poseStack, MultiBufferSource multiBufferSource, T livingEntity, EquipmentSlot equipmentSlot, int i, A humanoidModel, CallbackInfo ci){ + this.gl_storedEntity = livingEntity; + this.gl_storedSlot = equipmentSlot; + this.gl_storedItemStack = livingEntity.getItemBySlot(equipmentSlot); + } + + @ModifyArg(method = "renderArmorPiece", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/entity/layers/HumanoidArmorLayer;renderModel(Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;ILnet/minecraft/world/item/ArmorItem;Lnet/minecraft/client/model/HumanoidModel;ZFFFLjava/lang/String;)V"), index = 4) + public A injectArmor(A humanoidModel){ + return this.gl_storedItemStack.getItem() instanceof GeoItem ? (A) RenderProvider.of(this.gl_storedItemStack).getGenericArmorModel(this.gl_storedEntity, this.gl_storedItemStack, this.gl_storedSlot, (HumanoidModel) humanoidModel) : humanoidModel; } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/mixins/ItemRendererAccessor.java b/common/src/main/java/mod/azure/azurelib/common/internal/mixins/ItemRendererAccessor.java new file mode 100644 index 0000000..8cfe895 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/mixins/ItemRendererAccessor.java @@ -0,0 +1,12 @@ +package mod.azure.azurelib.common.internal.mixins; + +import net.minecraft.client.renderer.BlockEntityWithoutLevelRenderer; +import net.minecraft.client.renderer.entity.ItemRenderer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(ItemRenderer.class) +public interface ItemRendererAccessor { + @Accessor("blockEntityRenderer") + BlockEntityWithoutLevelRenderer getBlockEntityRenderer(); +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/mixins/MinecraftMixin.java b/common/src/main/java/mod/azure/azurelib/common/internal/mixins/MinecraftMixin.java new file mode 100644 index 0000000..41ab2e1 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/mixins/MinecraftMixin.java @@ -0,0 +1,31 @@ +package mod.azure.azurelib.common.internal.mixins; + +import com.mojang.blaze3d.platform.WindowEventHandler; +import mod.azure.azurelib.common.internal.common.config.ConfigHolder; +import mod.azure.azurelib.common.internal.common.config.io.ConfigIO; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.util.thread.ReentrantBlockableEventLoop; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Optional; + +@Mixin(Minecraft.class) +public abstract class MinecraftMixin extends ReentrantBlockableEventLoop implements WindowEventHandler { + + public MinecraftMixin(String p_i50401_1_) { + super(p_i50401_1_); + } + + @Inject(method = "disconnect(Lnet/minecraft/client/gui/screens/Screen;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/GameRenderer;resetData()V")) + private void configuration_reloadClientConfigs(Screen screen, CallbackInfo ci) { + ConfigHolder.getSynchronizedConfigs().stream() + .map(ConfigHolder::getConfig) + .filter(Optional::isPresent) + .map(Optional::get) + .forEach(ConfigIO::reloadClientValues); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/mixins/MixinItemRenderer.java b/common/src/main/java/mod/azure/azurelib/common/internal/mixins/MixinItemRenderer.java new file mode 100644 index 0000000..1529b12 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/mixins/MixinItemRenderer.java @@ -0,0 +1,28 @@ +package mod.azure.azurelib.common.internal.mixins; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import com.mojang.blaze3d.vertex.PoseStack; + +import mod.azure.azurelib.common.api.common.animatable.GeoItem; +import mod.azure.azurelib.common.internal.client.RenderProvider; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.entity.ItemRenderer; +import net.minecraft.client.resources.model.BakedModel; +import net.minecraft.world.item.ItemDisplayContext; +import net.minecraft.world.item.ItemStack; + +/** + * Render hook to inject AzureLib's ISTER rendering callback + */ +@Mixin(ItemRenderer.class) +public class MixinItemRenderer { + @Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/BlockEntityWithoutLevelRenderer;renderByItem(Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/item/ItemDisplayContext;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;II)V"), cancellable = true) + public void itemModelHook(ItemStack itemStack, ItemDisplayContext transformType, boolean bl, PoseStack poseStack, MultiBufferSource multiBufferSource, int i, int j, BakedModel bakedModel, CallbackInfo ci) { + if (itemStack.getItem() instanceof GeoItem) + RenderProvider.of(itemStack).getCustomRenderer().renderByItem(itemStack, transformType, poseStack, multiBufferSource, i, j); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/mixins/NeoMixinHumanoidArmorLayer.java b/common/src/main/java/mod/azure/azurelib/common/internal/mixins/NeoMixinHumanoidArmorLayer.java new file mode 100644 index 0000000..898f045 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/mixins/NeoMixinHumanoidArmorLayer.java @@ -0,0 +1,44 @@ +package mod.azure.azurelib.common.internal.mixins; + + +import com.mojang.blaze3d.vertex.PoseStack; +import mod.azure.azurelib.common.api.common.animatable.GeoItem; +import mod.azure.azurelib.common.internal.client.RenderProvider; +import net.minecraft.client.model.HumanoidModel; +import net.minecraft.client.model.Model; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.entity.layers.HumanoidArmorLayer; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.item.ItemStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyArg; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * Render hook for injecting AzureLib's armor rendering functionalities + */ +@Mixin(value = HumanoidArmorLayer.class, priority = 700) +public class NeoMixinHumanoidArmorLayer> { + + @Unique + private LivingEntity gl_storedEntity; + @Unique + private EquipmentSlot gl_storedSlot; + @Unique + private ItemStack gl_storedItemStack; + + @Inject(method = "renderArmorPiece", at = @At(value = "HEAD")) + public void armorModelHook(PoseStack poseStack, MultiBufferSource source, T livingEntity, EquipmentSlot equipmentSlot, int i, A model, CallbackInfo ci) { + this.gl_storedEntity = livingEntity; + this.gl_storedSlot = equipmentSlot; + this.gl_storedItemStack = livingEntity.getItemBySlot(equipmentSlot); + } + + @ModifyArg(method = "renderArmorPiece", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/entity/layers/HumanoidArmorLayer;renderModel(Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;ILnet/minecraft/world/item/ArmorItem;Lnet/minecraft/client/model/Model;ZFFFLnet/minecraft/resources/ResourceLocation;)V", remap = false), index = 4) + public Model injectArmor(Model humanoidModel) { + return this.gl_storedItemStack.getItem() instanceof GeoItem ? (A) RenderProvider.of(this.gl_storedItemStack).getGenericArmorModel(this.gl_storedEntity, this.gl_storedItemStack, this.gl_storedSlot, (HumanoidModel) humanoidModel) : humanoidModel; } +} \ No newline at end of file diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/mixins/PlayerListMixin.java b/common/src/main/java/mod/azure/azurelib/common/internal/mixins/PlayerListMixin.java new file mode 100644 index 0000000..4741a16 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/mixins/PlayerListMixin.java @@ -0,0 +1,24 @@ +package mod.azure.azurelib.common.internal.mixins; + +import mod.azure.azurelib.common.internal.common.config.ConfigHolder; +import mod.azure.azurelib.common.platform.Services; +import net.minecraft.network.Connection; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.CommonListenerCookie; +import net.minecraft.server.players.PlayerList; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Set; + +@Mixin(PlayerList.class) +public abstract class PlayerListMixin { + + @Inject(method = "placeNewPlayer", at = @At("TAIL")) + private void configuration_sendServerConfigs(Connection connection, ServerPlayer player, CommonListenerCookie commonListenerCookie, CallbackInfo ci) { + Set set = ConfigHolder.getSynchronizedConfigs(); + set.forEach(id -> Services.NETWORK.sendClientPacket(player, id)); + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/internal/mixins/TextureManagerMixin.java b/common/src/main/java/mod/azure/azurelib/common/internal/mixins/TextureManagerMixin.java new file mode 100644 index 0000000..eadc13b --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/internal/mixins/TextureManagerMixin.java @@ -0,0 +1,33 @@ +package mod.azure.azurelib.common.internal.mixins; + +import java.util.Map; + +import mod.azure.azurelib.common.internal.common.cache.texture.AnimatableTexture; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import net.minecraft.client.renderer.texture.AbstractTexture; +import net.minecraft.client.renderer.texture.TextureManager; +import net.minecraft.resources.ResourceLocation; + +@Mixin(TextureManager.class) +public abstract class TextureManagerMixin { + @Shadow @Final private Map byPath; + + @Shadow public abstract void register(ResourceLocation resourceLocation, AbstractTexture abstractTexture); + + @Inject(method = "getTexture(Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/client/renderer/texture/AbstractTexture;", at = @At("HEAD")) + private void wrapAnimatableTexture(ResourceLocation path, CallbackInfoReturnable callback) { + AbstractTexture existing = this.byPath.get(path); + + if (existing == null) { + existing = new AnimatableTexture(path); + + register(path, existing); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/mod/azure/azurelib/common/platform/Services.java b/common/src/main/java/mod/azure/azurelib/common/platform/Services.java new file mode 100644 index 0000000..1504830 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/platform/Services.java @@ -0,0 +1,26 @@ +package mod.azure.azurelib.common.platform; + +import mod.azure.azurelib.common.internal.common.AzureLib; +import mod.azure.azurelib.common.platform.services.AzureLibNetwork; +import mod.azure.azurelib.common.platform.services.IPlatformHelper; +import mod.azure.azurelib.common.platform.services.AzureLibInitializer; +import mod.azure.azurelib.common.platform.services.GeoRenderPhaseEventFactory; + +import java.util.ServiceLoader; + +public class Services { + + public static final GeoRenderPhaseEventFactory GEO_RENDER_PHASE_EVENT_FACTORY = load(GeoRenderPhaseEventFactory.class); + public static final AzureLibInitializer INITIALIZER = load(AzureLibInitializer.class); + public static final AzureLibNetwork NETWORK = load(AzureLibNetwork.class); + public static final IPlatformHelper PLATFORM = load(IPlatformHelper.class); + + public static T load(Class clazz) { + + final T loadedService = ServiceLoader.load(clazz) + .findFirst() + .orElseThrow(() -> new NullPointerException("Failed to load service for " + clazz.getName())); + AzureLib.LOGGER.debug("Loaded {} for service {}", loadedService, clazz); + return loadedService; + } +} \ No newline at end of file diff --git a/common/src/main/java/mod/azure/azurelib/common/platform/services/AccessWidener.java b/common/src/main/java/mod/azure/azurelib/common/platform/services/AccessWidener.java new file mode 100644 index 0000000..a11c1de --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/platform/services/AccessWidener.java @@ -0,0 +1,4 @@ +package mod.azure.azurelib.common.platform.services; + +public interface AccessWidener { +} diff --git a/common/src/main/java/mod/azure/azurelib/common/platform/services/AzureLibInitializer.java b/common/src/main/java/mod/azure/azurelib/common/platform/services/AzureLibInitializer.java new file mode 100644 index 0000000..a5807df --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/platform/services/AzureLibInitializer.java @@ -0,0 +1,9 @@ +package mod.azure.azurelib.common.platform.services; + +/** + * @author Boston Vanseghi + */ +public interface AzureLibInitializer { + + void initialize(); +} diff --git a/common/src/main/java/mod/azure/azurelib/common/platform/services/AzureLibNetwork.java b/common/src/main/java/mod/azure/azurelib/common/platform/services/AzureLibNetwork.java new file mode 100644 index 0000000..a686d3c --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/platform/services/AzureLibNetwork.java @@ -0,0 +1,87 @@ +package mod.azure.azurelib.common.platform.services; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import mod.azure.azurelib.common.internal.common.AzureLib; +import mod.azure.azurelib.common.internal.common.animatable.SingletonGeoAnimatable; +import mod.azure.azurelib.common.internal.common.core.animatable.GeoAnimatable; +import mod.azure.azurelib.common.internal.common.network.AbstractPacket; +import net.minecraft.core.BlockPos; +import net.minecraft.network.protocol.Packet; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; + +class LockHolder { // Package private class + public static Object LOCK = new Object(); +} + +public interface AzureLibNetwork { + ResourceLocation ANIM_DATA_SYNC_PACKET_ID = AzureLib.modResource("anim_data_sync"); + ResourceLocation ANIM_TRIGGER_SYNC_PACKET_ID = AzureLib.modResource("anim_trigger_sync"); + + ResourceLocation ENTITY_ANIM_DATA_SYNC_PACKET_ID = AzureLib.modResource("entity_anim_data_sync"); + ResourceLocation ENTITY_ANIM_TRIGGER_SYNC_PACKET_ID = AzureLib.modResource("entity_anim_trigger_sync"); + + ResourceLocation BLOCK_ENTITY_ANIM_DATA_SYNC_PACKET_ID = AzureLib.modResource("block_entity_anim_data_sync"); + ResourceLocation BLOCK_ENTITY_ANIM_TRIGGER_SYNC_PACKET_ID = AzureLib.modResource("block_entity_anim_trigger_sync"); + ResourceLocation CONFIG_PACKET_ID = AzureLib.modResource("config_packet"); + + ResourceLocation CUSTOM_ENTITY_ID = AzureLib.modResource("spawn_entity"); + + ResourceLocation RELOAD = AzureLib.modResource("reload"); + + Map SYNCED_ANIMATABLES = new Object2ObjectOpenHashMap<>(); + + /** + * Registers a synced {@link GeoAnimatable} object for networking support.
+ * It is recommended that you don't call this directly, instead implementing and calling {@link SingletonGeoAnimatable#registerSyncedAnimatable} + */ + default void registerSyncedAnimatable(GeoAnimatable animatable) { + synchronized (this) { + GeoAnimatable existing = SYNCED_ANIMATABLES.put(animatable.getClass().toString(), animatable); + + if (existing == null) + AzureLib.LOGGER.debug("Registered SyncedAnimatable for " + animatable.getClass()); + } + } + + Packet createPacket(Entity entity); + + /** + * Used to register packets that the server sends + **/ + void registerClientReceiverPackets(); + + void sendToTrackingEntityAndSelf(AbstractPacket packet, Entity entityToTrack); + + void sendToEntitiesTrackingChunk(AbstractPacket packet, ServerLevel level, BlockPos blockPos); + + void sendClientPacket(ServerPlayer player, String id); + + static void sendWithCallback(AbstractPacket packet, IPacketCallback callback) { + callback.onReadyToSend(packet); + } + + interface IPacketCallback { + void onReadyToSend(AbstractPacket packetToSend); + } + + /** + * Gets a registered synced {@link GeoAnimatable} object by name + * + * @param className the className + */ + @Nullable + static GeoAnimatable getSyncedAnimatable(String className) { + GeoAnimatable animatable = SYNCED_ANIMATABLES.get(className); + + if (animatable == null) + AzureLib.LOGGER.error("Attempting to retrieve unregistered synced animatable! (" + className + ")"); + + return animatable; + } +} diff --git a/common/src/main/java/mod/azure/azurelib/common/platform/services/GeoRenderPhaseEventFactory.java b/common/src/main/java/mod/azure/azurelib/common/platform/services/GeoRenderPhaseEventFactory.java new file mode 100644 index 0000000..b620bd8 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/platform/services/GeoRenderPhaseEventFactory.java @@ -0,0 +1,12 @@ +package mod.azure.azurelib.common.platform.services; + +import mod.azure.azurelib.common.internal.common.event.GeoRenderEvent; + +public interface GeoRenderPhaseEventFactory { + + interface GeoRenderPhaseEvent { + boolean handle(GeoRenderEvent geoRenderEvent); + } + + GeoRenderPhaseEvent create(); +} diff --git a/common/src/main/java/mod/azure/azurelib/common/platform/services/IPlatformHelper.java b/common/src/main/java/mod/azure/azurelib/common/platform/services/IPlatformHelper.java new file mode 100644 index 0000000..87b71d5 --- /dev/null +++ b/common/src/main/java/mod/azure/azurelib/common/platform/services/IPlatformHelper.java @@ -0,0 +1,58 @@ +package mod.azure.azurelib.common.platform.services; + +import mod.azure.azurelib.common.internal.common.blocks.TickingLightBlock; +import mod.azure.azurelib.common.internal.common.blocks.TickingLightEntity; +import net.minecraft.world.item.enchantment.Enchantment; +import net.minecraft.world.level.block.entity.BlockEntityType; + +import java.nio.file.Path; + +public interface IPlatformHelper { + + /** + * Gets the name of the current platform + * + * @return The name of the current platform. + */ + String getPlatformName(); + + /** + * Checks if a mod with the given id is loaded. + * + * @param modId The mod to check if it is loaded. + * @return True if the mod is loaded, false otherwise. + */ + boolean isModLoaded(String modId); + + /** + * Check if the game is currently in a development environment. + * + * @return True if in a development environment, false otherwise. + */ + boolean isDevelopmentEnvironment(); + + /** + * Gets the name of the environment type as a string. + * + * @return The name of the environment type. + */ + default String getEnvironmentName() { + return isDevelopmentEnvironment() ? "development" : "production"; + } + + Path getGameDir(); + + boolean isServerEnvironment(); + + default BlockEntityType getTickingLightEntity() { + return null; + } + + default TickingLightBlock getTickingLightBlock() { + return null; + } + + Enchantment getIncendairyenchament(); + + Path modsDir(); +} \ No newline at end of file diff --git a/common/src/main/java/org/Vrglab/AzureLib/Armor/AzureArmor.java b/common/src/main/java/org/Vrglab/AzureLib/Armor/AzureArmor.java new file mode 100644 index 0000000..980e9ed --- /dev/null +++ b/common/src/main/java/org/Vrglab/AzureLib/Armor/AzureArmor.java @@ -0,0 +1,80 @@ +package org.Vrglab.AzureLib.Armor; + +import mod.azure.azurelib.common.api.client.model.GeoModel; +import mod.azure.azurelib.common.api.client.renderer.GeoArmorRenderer; +import mod.azure.azurelib.common.api.common.animatable.GeoItem; +import mod.azure.azurelib.common.internal.client.RenderProvider; +import mod.azure.azurelib.common.internal.common.core.animatable.instance.AnimatableInstanceCache; +import mod.azure.azurelib.common.internal.common.core.animation.AnimatableManager; +import mod.azure.azurelib.common.internal.common.core.animation.AnimationController; +import mod.azure.azurelib.common.internal.common.util.AzureLibUtil; +import net.minecraft.client.model.HumanoidModel; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.item.ArmorItem; +import net.minecraft.world.item.ArmorMaterial; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import org.Vrglab.Modloader.Types.ICallBack; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Properties; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public abstract class AzureArmor extends ArmorItem implements GeoItem { + protected final AnimatableInstanceCache cache = AzureLibUtil.createInstanceCache(this); + protected final Supplier renderProvider = GeoItem.makeRenderer(this); + + public AzureArmor(ArmorMaterial armorMaterial, Type type, Properties properties) { + super(armorMaterial, type, properties); + } + + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + List controllerList = (List)getControllers().accept(controllers); + for (AnimationController controller: controllerList) { + controllers.add(controller); + } + } + + @Override + public void createRenderer(Consumer consumer) { + consumer.accept(new RenderProvider() { + private Renderer renderer; + + @Override + public @NotNull HumanoidModel getHumanoidArmorModel(LivingEntity livingEntity, ItemStack itemStack, EquipmentSlot equipmentSlot, HumanoidModel original) { + if (renderer == null) + renderer = new Renderer<>(getModel().get()); + renderer.prepForRender(livingEntity, itemStack, equipmentSlot, original); + return this.renderer; + } + }); + } + + @Override + public Supplier getRenderProvider() { + return renderProvider; + } + + /* ABSTRACT FUNCTIONS */ + public abstract ICallBack getControllers(); + public abstract Supplier> getModel(); + + + /* SUB CLASSES */ + + public class Renderer extends GeoArmorRenderer { + public Renderer(GeoModel model) { + super(model); + } + } +} diff --git a/common/src/main/java/org/Vrglab/AzureLib/Block/AzureEntityBlock.java b/common/src/main/java/org/Vrglab/AzureLib/Block/AzureEntityBlock.java new file mode 100644 index 0000000..830f0a9 --- /dev/null +++ b/common/src/main/java/org/Vrglab/AzureLib/Block/AzureEntityBlock.java @@ -0,0 +1,42 @@ +package org.Vrglab.AzureLib.Block; + +import mod.azure.azurelib.common.api.common.animatable.GeoBlockEntity; +import mod.azure.azurelib.common.internal.common.core.animatable.instance.AnimatableInstanceCache; +import mod.azure.azurelib.common.internal.common.core.animation.AnimatableManager; +import mod.azure.azurelib.common.internal.common.core.animation.AnimationController; +import mod.azure.azurelib.common.internal.common.util.AzureLibUtil; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.state.BlockState; +import org.Vrglab.Modloader.Types.ICallBack; + +import java.util.List; + +public abstract class AzureEntityBlock extends BaseEntityBlock implements GeoBlockEntity { + protected final AnimatableInstanceCache cache = AzureLibUtil.createInstanceCache(this); + + protected AzureEntityBlock(Properties properties) { + super(properties); + } + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + List controllerList = (List)getControllers().accept(controllers); + for (AnimationController controller: controllerList) { + controllers.add(controller); + } + } + + @Override + public RenderShape getRenderShape(BlockState blockState) { + return RenderShape.ENTITYBLOCK_ANIMATED; + } + + /* ABSTRACT FUNCTIONS */ + public abstract ICallBack getControllers(); +} diff --git a/common/src/main/java/org/Vrglab/AzureLib/Item/AzureItem.java b/common/src/main/java/org/Vrglab/AzureLib/Item/AzureItem.java new file mode 100644 index 0000000..afbd03b --- /dev/null +++ b/common/src/main/java/org/Vrglab/AzureLib/Item/AzureItem.java @@ -0,0 +1,71 @@ +package org.Vrglab.AzureLib.Item; + +import mod.azure.azurelib.common.api.client.model.GeoModel; +import mod.azure.azurelib.common.api.client.renderer.GeoItemRenderer; +import mod.azure.azurelib.common.api.common.animatable.GeoItem; +import mod.azure.azurelib.common.internal.client.RenderProvider; +import mod.azure.azurelib.common.internal.common.core.animatable.instance.AnimatableInstanceCache; +import mod.azure.azurelib.common.internal.common.core.animation.AnimatableManager; +import mod.azure.azurelib.common.internal.common.core.animation.AnimationController; +import mod.azure.azurelib.common.internal.common.util.AzureLibUtil; +import net.minecraft.client.renderer.BlockEntityWithoutLevelRenderer; +import net.minecraft.world.item.Item; +import org.Vrglab.Modloader.Types.ICallBack; + +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public abstract class AzureItem extends Item implements GeoItem { + protected final AnimatableInstanceCache cache = AzureLibUtil.createInstanceCache(this); + private final Supplier renderProvider = GeoItem.makeRenderer(this); + public AzureItem(Properties properties) { + super(properties); + } + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + List controllerList = (List)getControllers().accept(controllers); + for (AnimationController controller: controllerList) { + controllers.add(controller); + } + } + + @Override + public void createRenderer(Consumer consumer) { + consumer.accept(new RenderProvider() { + private AzureItem.Renderer renderer; + + @Override + public BlockEntityWithoutLevelRenderer getCustomRenderer() { + if (renderer == null) + renderer = new AzureItem.Renderer<>(getModel().get()); + return this.renderer; + } + }); + } + + @Override + public Supplier getRenderProvider() { + return renderProvider; + } + + + /* ABSTRACT FUNCTIONS */ + public abstract ICallBack getControllers(); + public abstract Supplier> getModel(); + + + /* SUB CLASSES */ + + public class Renderer extends GeoItemRenderer { + public Renderer(GeoModel model) { + super(model); + } + } +} diff --git a/common/src/main/java/org/Vrglab/AzureLib/Utility/Utils.java b/common/src/main/java/org/Vrglab/AzureLib/Utility/Utils.java new file mode 100644 index 0000000..4e008d1 --- /dev/null +++ b/common/src/main/java/org/Vrglab/AzureLib/Utility/Utils.java @@ -0,0 +1,82 @@ +package org.Vrglab.AzureLib.Utility; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +public class Utils { + + /** + * Gets the value of a private final static field from a class. + * + * @param clazz the class containing the field + * @param fieldName the name of the field + * @return the value of the field + */ + public static T getPrivateFinalStaticField(Class clazz, String fieldName){ + try { + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + return (T)field.get(null); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + /** + * Gets the value of a private final field from an instance of a class. + * + * @param instance the instance containing the field + * @param fieldName the name of the field + * @return the value of the field + */ + public static T getPrivateFinalStaticField(Object instance, Class clazz, String fieldName){ + try { + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + return (T)field.get(instance); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + /** + * Calls a private static method from a class. + * + * @param clazz the class containing the method + * @param methodName the name of the method + * @param paramTypes the parameter types of the method + * @param args the arguments to pass to the method + * @return the result of the method call + */ + public static T callPrivateStaticMethod(Class clazz, String methodName, Class[] paramTypes, Object... args) { + try { + Method method = clazz.getDeclaredMethod(methodName, paramTypes); + method.setAccessible(true); + return (T)method.invoke(null, args); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + /** + * Calls a private non-static method from an instance of a class. + * + * @param instance the instance containing the method + * @param methodName the name of the method + * @param paramTypes the parameter types of the method + * @param args the arguments to pass to the method + * @return the result of the method call + */ + public static T callPrivateMethod(Object instance, String methodName, Class[] paramTypes, Object... args) { + try { + Class clazz = instance.getClass(); + Method method = clazz.getDeclaredMethod(methodName, paramTypes); + method.setAccessible(true); + return (T)method.invoke(instance, args); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + +} diff --git a/common/src/main/java/org/Vrglab/AzureLib/VlAzureLibMod.java b/common/src/main/java/org/Vrglab/AzureLib/VlAzureLibMod.java new file mode 100644 index 0000000..e444599 --- /dev/null +++ b/common/src/main/java/org/Vrglab/AzureLib/VlAzureLibMod.java @@ -0,0 +1,11 @@ +package org.Vrglab.AzureLib; + +import mod.azure.azurelib.common.internal.common.AzureLib; + +public final class VlAzureLibMod { + public static final String MOD_ID = "vrglabs_azurelib"; + + public static void init() { + AzureLib.initialize(); + } +} diff --git a/common/src/main/resources/assets/vrglabs_azurelib/block/lightblock.json b/common/src/main/resources/assets/vrglabs_azurelib/block/lightblock.json new file mode 100644 index 0000000..2c63c08 --- /dev/null +++ b/common/src/main/resources/assets/vrglabs_azurelib/block/lightblock.json @@ -0,0 +1,2 @@ +{ +} diff --git a/common/src/main/resources/assets/vrglabs_azurelib/blockstates/lightblock.json b/common/src/main/resources/assets/vrglabs_azurelib/blockstates/lightblock.json new file mode 100644 index 0000000..2dc2a32 --- /dev/null +++ b/common/src/main/resources/assets/vrglabs_azurelib/blockstates/lightblock.json @@ -0,0 +1,52 @@ +{ + "variants": { + "age=0": { + "model": "azurelib:block/lightblock" + }, + "age=1": { + "model": "azurelib:block/lightblock" + }, + "age=2": { + "model": "azurelib:block/lightblock" + }, + "age=3": { + "model": "azurelib:block/lightblock" + }, + "age=4": { + "model": "azurelib:block/lightblock" + }, + "age=5": { + "model": "azurelib:block/lightblock" + }, + "age=6": { + "model": "azurelib:block/lightblock" + }, + "age=7": { + "model": "azurelib:block/lightblock" + }, + "age=8": { + "model": "azurelib:block/lightblock" + }, + "age=9": { + "model": "azurelib:block/lightblock" + }, + "age=10": { + "model": "azurelib:block/lightblock" + }, + "age=11": { + "model": "azurelib:block/lightblock" + }, + "age=12": { + "model": "azurelib:block/lightblock" + }, + "age=13": { + "model": "azurelib:block/lightblock" + }, + "age=14": { + "model": "azurelib:block/lightblock" + }, + "age=15": { + "model": "azurelib:block/lightblock" + } + } +} diff --git a/common/src/main/resources/assets/vrglabs_azurelib/lang/en_us.json b/common/src/main/resources/assets/vrglabs_azurelib/lang/en_us.json new file mode 100644 index 0000000..773cc55 --- /dev/null +++ b/common/src/main/resources/assets/vrglabs_azurelib/lang/en_us.json @@ -0,0 +1,56 @@ +{ + "category.azurelib.binds": "AzureLib Binds", + "key.azurelib.reload": "Reload Gun/Weapon", + "key.azurelib.scope": "Scope Weapon", + "key.azurelib.fire": "Alternate Weapon FIre", + "text.azurelib.value.true": "True", + "text.azurelib.value.false": "False", + "text.azurelib.value.edit": "Edit", + "text.azurelib.value.back": "Back", + "text.azurelib.value.revert.default": "Use default config", + "text.azurelib.value.revert.default.dialog": "Are you sure you want to reset config back to default values?", + "text.azurelib.value.revert.changes": "Rollback changes", + "text.azurelib.value.revert.changes.dialog": "Are you sure you want to discard your currently made changes?", + "text.azurelib.value.add_element": "Add element", + "text.azurelib.screen.select_config": "Select config", + "text.azurelib.screen.dialog.confirm": "Confirm", + "text.azurelib.screen.dialog.cancel": "Cancel", + "text.azurelib.screen.color_dialog": "Select color", + "text.azurelib.screen.color.red": "Red: %s", + "text.azurelib.screen.color.green": "Green: %s", + "text.azurelib.screen.color.blue": "Blue: %s", + "text.azurelib.screen.color.alpha": "Alpha: %s", + "text.azurelib.error.character_value_empty": "Character field cannot be empty!", + "text.azurelib.error.nan": "Value '%s' is not a valid number!", + "text.azurelib.error.num_bounds": "Value '%s' is not from interval <%s;%s>!", + "text.azurelib.error.pattern_mismatch": "Value '%s' does not match pattern \\%s\\!", + "config.screen.azurelib": "AzureLib Config", + "config.azurelib.option.disableOptifineWarning": "Disable Optifine Warning Screen", + "config.azurelib.option.useVanillaUseKey": "Toggle if AzureDooms Guns use Vanilla Use Key or Custom", + "config.azurelib.option.bool": "Test Boolean", + "config.azurelib.option.number": "Test Number", + "config.azurelib.option.longNumber": "Test Long Number", + "config.azurelib.option.floatNumber": "Test Float Number", + "config.azurelib.option.doubleNumber": "Test Double Number", + "config.azurelib.option.string": "Test String", + "config.azurelib.option.color": "Test Color", + "config.azurelib.option.color2": "Test Color ARGB", + "config.azurelib.option.boolArray": "Test Boolen Array", + "config.azurelib.option.intArray": "Test Int Array", + "config.azurelib.option.longArray": "Test Long Array", + "config.azurelib.option.floatArray": "Test Float Array", + "config.azurelib.option.stringArray": "Test String Array", + "config.azurelib.option.testEnum": "Test Enum", + "config.azurelib.option.nestedTest": "Test Nested", + "config.azurelib.option.testInt": "Test Int", + "config.azurelib.option.genericwarning": "Generic Warning", + "config.azurelib.option.disableOptifineWarning": "Disable Optfine Warning", + "header.azurelib.optifine": "Warning: Optifine present on game client", + "message.azurelib.optifine": "Warning: Optifine is unsupported by most mods, ranging from minor visual artifacts to game crashes. It's recommended to remove the mod to avoid these issues and more. This warning can be disabled in AzureLibs configs however you accept responsibility for any bugs or issues you encounter. If you needed Optifine for one reason or another, check PrismLauncher's Optifine Alternatives list:", + "label.azurelib.open_mods_folder": "Open Mods Folder", + "label.azurelib.open_mods_folder": "Open Mods Folder", + "label.azurelib.optifine_alternatives": "Optifine Alternatives", + "label.azurelib.proceed_anyway": "Proceed Anyway", + "enchantment.azurelib.incendiaryenchantment": "Incendiary Attachment", + "enchantment.azurelib.incendiaryenchantment.desc": "Allows bullets fired to light mobs on fire" +} \ No newline at end of file diff --git a/common/src/main/resources/assets/vrglabs_azurelib/lang/fr_fr.json b/common/src/main/resources/assets/vrglabs_azurelib/lang/fr_fr.json new file mode 100644 index 0000000..d316295 --- /dev/null +++ b/common/src/main/resources/assets/vrglabs_azurelib/lang/fr_fr.json @@ -0,0 +1,26 @@ +{ + "category.azurelib.binds": "Liaisons AzureLib", + "key.azurelib.reload": "Recharger le pistolet/l'arme", + "text.azurelib.value.true": "Oui", + "text.azurelib.value.false": "Non", + "text.azurelib.value.edit": "Modifier", + "text.azurelib.value.back": "Retour", + "text.azurelib.value.revert.default": "Utiliser la configuration par défaut", + "text.azurelib.value.revert.default.dialog": "Êtes-vous sûr de vouloir réinitialiser la configuration avec les valeurs par défaut ?", + "text.azurelib.value.revert.changes": "Annuler les modifications", + "text.azurelib.value.revert.changes.dialog": "Êtes-vous sûr de vouloir abandonner les modifications que vous avez apportées ?", + "text.azurelib.value.add_element": "Ajouter un élément", + "text.azurelib.screen.select_config": "Sélectionner une configuration", + "text.azurelib.screen.dialog.confirm": "Confirmer", + "text.azurelib.screen.dialog.cancel": "Annuler", + "text.azurelib.screen.color_dialog": "Sélectionner une couleur", + "text.azurelib.screen.color.red": "Rouge : %s", + "text.azurelib.screen.color.green": "Vert : %s", + "text.azurelib.screen.color.blue": "Bleu : %s", + "text.azurelib.screen.color.alpha": "Alpha : %s", + + "text.azurelib.error.character_value_empty": "Le champ de caractère ne peut pas être vide !", + "text.azurelib.error.nan": "La valeur '%s' n'est pas un nombre valide !", + "text.azurelib.error.num_bounds": "La valeur '%s' n'est pas comprise dans l'intervalle <%s;%s> !", + "text.azurelib.error.pattern_mismatch": "La valeur '%s' ne correspond pas au motif \\%s\\ !" +} diff --git a/common/src/main/resources/assets/vrglabs_azurelib/models/block/lightblock.json b/common/src/main/resources/assets/vrglabs_azurelib/models/block/lightblock.json new file mode 100644 index 0000000..2c63c08 --- /dev/null +++ b/common/src/main/resources/assets/vrglabs_azurelib/models/block/lightblock.json @@ -0,0 +1,2 @@ +{ +} diff --git a/common/src/main/resources/assets/vrglabs_azurelib/textures/blank.png b/common/src/main/resources/assets/vrglabs_azurelib/textures/blank.png new file mode 100644 index 0000000..bab0fab Binary files /dev/null and b/common/src/main/resources/assets/vrglabs_azurelib/textures/blank.png differ diff --git a/common/src/main/resources/assets/vrglabs_azurelib/textures/icons/error.png b/common/src/main/resources/assets/vrglabs_azurelib/textures/icons/error.png new file mode 100644 index 0000000..d88434f Binary files /dev/null and b/common/src/main/resources/assets/vrglabs_azurelib/textures/icons/error.png differ diff --git a/common/src/main/resources/assets/vrglabs_azurelib/textures/icons/warning.png b/common/src/main/resources/assets/vrglabs_azurelib/textures/icons/warning.png new file mode 100644 index 0000000..aadf5af Binary files /dev/null and b/common/src/main/resources/assets/vrglabs_azurelib/textures/icons/warning.png differ diff --git a/common/src/main/resources/data/consecration/tags/enchantment/holy.json b/common/src/main/resources/data/consecration/tags/enchantment/holy.json new file mode 100644 index 0000000..3e981fe --- /dev/null +++ b/common/src/main/resources/data/consecration/tags/enchantment/holy.json @@ -0,0 +1,6 @@ +{ + "replace": false, + "values": [ + "vrglabs_azurelib:incendiaryenchantment" + ] +} \ No newline at end of file diff --git a/common/src/main/resources/logo.png b/common/src/main/resources/logo.png new file mode 100644 index 0000000..797b266 Binary files /dev/null and b/common/src/main/resources/logo.png differ diff --git a/common/src/main/resources/pack.mcmeta b/common/src/main/resources/pack.mcmeta new file mode 100644 index 0000000..620dfd8 --- /dev/null +++ b/common/src/main/resources/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "description": "AzureLib resources", + "pack_format": 22 + } +} diff --git a/fabric-like/build.gradle b/fabric-like/build.gradle new file mode 100644 index 0000000..fe23d91 --- /dev/null +++ b/fabric-like/build.gradle @@ -0,0 +1,14 @@ +architectury { + common rootProject.enabled_platforms.split(',') +} + +dependencies { + modImplementation "net.fabricmc:fabric-loader:$rootProject.fabric_loader_version" + modImplementation "net.fabricmc.fabric-api:fabric-api:$rootProject.fabric_api_version" + modImplementation "org.Vrglab:vrglabslib:fabric-like-$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-fabric:$rootProject.architectury_api_version" + + compileOnly(project(path: ':common', configuration: 'namedElements')) { transitive false } +} diff --git a/fabric-like/src/main/java/org/Vrglab/AzureLib/fabriclike/VlAzureLibModFabricLike.java b/fabric-like/src/main/java/org/Vrglab/AzureLib/fabriclike/VlAzureLibModFabricLike.java new file mode 100644 index 0000000..1250568 --- /dev/null +++ b/fabric-like/src/main/java/org/Vrglab/AzureLib/fabriclike/VlAzureLibModFabricLike.java @@ -0,0 +1,9 @@ +package org.Vrglab.AzureLib.fabriclike; + +import org.Vrglab.AzureLib.VlAzureLibMod; + +public final class VlAzureLibModFabricLike { + public static void init() { + VlAzureLibMod.init(); + } +} diff --git a/fabric/build.gradle b/fabric/build.gradle new file mode 100644 index 0000000..8228192 --- /dev/null +++ b/fabric/build.gradle @@ -0,0 +1,58 @@ +plugins { + id 'com.github.johnrengelman.shadow' +} + +architectury { + platformSetupLoomIde() + fabric() +} + +configurations { + common { + canBeResolved = true + canBeConsumed = false + } + compileClasspath.extendsFrom common + runtimeClasspath.extendsFrom common + developmentFabric.extendsFrom common + + // Files in this configuration will be bundled into your mod using the Shadow plugin. + // Don't use the `shadow` configuration from the plugin itself as it's meant for excluding files. + shadowBundle { + canBeResolved = true + canBeConsumed = false + } +} + +dependencies { + modImplementation "net.fabricmc:fabric-loader:$rootProject.fabric_loader_version" + + // Fabric API. This is technically optional, but you probably want it anyway. + modImplementation "net.fabricmc.fabric-api:fabric-api:$rootProject.fabric_api_version" + + // Architectury API. This is optional, and you can comment it out if you don't need it. + modImplementation "dev.architectury:architectury-fabric:$rootProject.architectury_api_version" + modImplementation "org.Vrglab:vrglabslib:fabric-$rootProject.vrglabs_lib_version-mc$rootProject.minecraft_version" + + modApi "com.terraformersmc:modmenu:9.0.0" + + common(project(path: ':common', configuration: 'namedElements')) { transitive false } + shadowBundle project(path: ':common', configuration: 'transformProductionFabric') + common(project(path: ':fabric-like', configuration: 'namedElements')) { transitive false } + shadowBundle project(path: ':fabric-like', configuration: 'transformProductionFabric') +} + +processResources { + filesMatching('fabric.mod.json') { + expand rootProject.properties + } +} + +shadowJar { + configurations = [project.configurations.shadowBundle] + archiveClassifier = 'dev-shadow' +} + +remapJar { + input.set shadowJar.archiveFile +} diff --git a/fabric/src/main/java/mod/azure/azurelib/fabric/event/FabricGeoRenderPhaseEvent.java b/fabric/src/main/java/mod/azure/azurelib/fabric/event/FabricGeoRenderPhaseEvent.java new file mode 100644 index 0000000..7669848 --- /dev/null +++ b/fabric/src/main/java/mod/azure/azurelib/fabric/event/FabricGeoRenderPhaseEvent.java @@ -0,0 +1,31 @@ +package mod.azure.azurelib.fabric.event; + +import mod.azure.azurelib.common.internal.common.event.GeoRenderEvent; +import mod.azure.azurelib.common.platform.services.GeoRenderPhaseEventFactory; +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; + +/** + * @author Boston Vanseghi + */ +public class FabricGeoRenderPhaseEvent implements GeoRenderPhaseEventFactory.GeoRenderPhaseEvent { + + @FunctionalInterface + interface Listener { + boolean handle(GeoRenderEvent event); + } + + private final Event event = EventFactory.createArrayBacked(Listener.class, event -> true, listeners -> event -> { + for (Listener listener : listeners) { + if (!listener.handle(event)) + return false; + } + + return true; + }); + + @Override + public boolean handle(GeoRenderEvent geoRenderEvent) { + return this.event.invoker().handle(geoRenderEvent); + } +} diff --git a/fabric/src/main/java/mod/azure/azurelib/fabric/integration/ModMenuIntegration.java b/fabric/src/main/java/mod/azure/azurelib/fabric/integration/ModMenuIntegration.java new file mode 100644 index 0000000..c81e20d --- /dev/null +++ b/fabric/src/main/java/mod/azure/azurelib/fabric/integration/ModMenuIntegration.java @@ -0,0 +1,37 @@ +package mod.azure.azurelib.fabric.integration; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.terraformersmc.modmenu.api.ConfigScreenFactory; +import com.terraformersmc.modmenu.api.ModMenuApi; + +import mod.azure.azurelib.common.internal.client.AzureLibClient; +import mod.azure.azurelib.common.internal.common.config.ConfigHolder; +import mod.azure.azurelib.common.platform.Services; + +public class ModMenuIntegration implements ModMenuApi { + + @Override + public Map> getProvidedConfigScreenFactories() { + Map> map = new HashMap<>(); + Map>> byGroup = ConfigHolder.getConfigGroupingByGroup(); + if (!Services.PLATFORM.isServerEnvironment()) + for (Map.Entry>> entry : byGroup.entrySet()) { + String group = entry.getKey(); + List> configHolders = entry.getValue(); + ConfigScreenFactory factory = parent -> { + int i = configHolders.size(); + if (i > 1) { + return AzureLibClient.getConfigScreenByGroup(configHolders, group, parent); + } else if (i == 1) { + return AzureLibClient.getConfigScreenForHolder(configHolders.get(0), parent); + } + return null; + }; + map.put(group, factory); + } + return map; + } +} \ No newline at end of file diff --git a/fabric/src/main/java/mod/azure/azurelib/fabric/network/Networking.java b/fabric/src/main/java/mod/azure/azurelib/fabric/network/Networking.java new file mode 100644 index 0000000..b5136d5 --- /dev/null +++ b/fabric/src/main/java/mod/azure/azurelib/fabric/network/Networking.java @@ -0,0 +1,64 @@ +package mod.azure.azurelib.fabric.network; + +import java.lang.reflect.InvocationTargetException; +import java.util.function.BiConsumer; + +import mod.azure.azurelib.common.internal.common.AzureLib; +import mod.azure.azurelib.common.internal.common.AzureLibException; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.MarkerManager; + +import io.netty.buffer.Unpooled; +import mod.azure.azurelib.fabric.network.api.IClientPacket; +import mod.azure.azurelib.common.internal.common.network.api.IPacket; +import mod.azure.azurelib.common.internal.common.network.api.IPacketDecoder; +import mod.azure.azurelib.common.internal.common.network.api.IPacketEncoder; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; + +public final class Networking { + + public static final Marker MARKER = MarkerManager.getMarker("Network"); + + public static void sendClientPacket(ServerPlayer target, IClientPacket packet) { + dispatch(packet, (packetId, buffer) -> ServerPlayNetworking.send(target, packetId, buffer)); + } + + private static void dispatch(IPacket packet, BiConsumer dispatcher) { + ResourceLocation packetId = packet.getPacketId(); + FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.buffer()); + IPacketEncoder encoder = packet.getEncoder(); + T data = packet.getPacketData(); + encoder.encode(data, buffer); + dispatcher.accept(packetId, buffer); + } + + public static final class PacketRegistry { + + public static void registerClient() { + registerServer2ClientReceiver(S2C_SendConfigData.class); + } + + @Environment(EnvType.CLIENT) + private static void registerServer2ClientReceiver(Class> clientPacketClass) { + try { + IClientPacket packet = clientPacketClass.getDeclaredConstructor().newInstance(); + ResourceLocation packetId = packet.getPacketId(); + ClientPlayNetworking.registerGlobalReceiver(packetId, (client, handler, buffer, responseDispatcher) -> { + IPacketDecoder decoder = packet.getDecoder(); + T packetData = decoder.decode(buffer); + client.execute(() -> packet.handleClientsidePacket(client, handler, packetData, responseDispatcher)); + }); + } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | + IllegalAccessException exc) { + AzureLib.LOGGER.fatal(MARKER, "Couldn't instantiate new client packet from class {}, make sure it declares public default constructor", clientPacketClass.getSimpleName()); + throw new AzureLibException(exc); + } + } + } +} diff --git a/fabric/src/main/java/mod/azure/azurelib/fabric/network/S2C_SendConfigData.java b/fabric/src/main/java/mod/azure/azurelib/fabric/network/S2C_SendConfigData.java new file mode 100644 index 0000000..aa9d0af --- /dev/null +++ b/fabric/src/main/java/mod/azure/azurelib/fabric/network/S2C_SendConfigData.java @@ -0,0 +1,100 @@ +package mod.azure.azurelib.fabric.network; + +import java.util.Map; + +import mod.azure.azurelib.common.internal.common.AzureLib; +import mod.azure.azurelib.common.internal.common.AzureLibException; +import mod.azure.azurelib.common.internal.common.config.ConfigHolder; +import mod.azure.azurelib.common.internal.common.config.adapter.TypeAdapter; +import mod.azure.azurelib.common.internal.common.config.value.ConfigValue; +import mod.azure.azurelib.fabric.network.api.IClientPacket; +import mod.azure.azurelib.common.internal.common.network.api.IPacketDecoder; +import mod.azure.azurelib.common.internal.common.network.api.IPacketEncoder; +import net.fabricmc.fabric.api.networking.v1.PacketSender; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; + +public class S2C_SendConfigData implements IClientPacket { + + public static final ResourceLocation IDENTIFIER = AzureLib.modResource("s2c_send_config_data"); + + private final ConfigData config; + + S2C_SendConfigData() { + this.config = null; + } + + public S2C_SendConfigData(String config) { + this.config = new ConfigData(config); + } + + @Override + public ResourceLocation getPacketId() { + return IDENTIFIER; + } + + @Override + public ConfigData getPacketData() { + return config; + } + + @Override + public IPacketEncoder getEncoder() { + return (configData, buffer) -> { + buffer.writeUtf(configData.configId); + ConfigHolder.getConfig(configData.configId).ifPresent(data -> { + Map> serialized = data.getNetworkSerializedFields(); + buffer.writeInt(serialized.size()); + for (Map.Entry> entry : serialized.entrySet()) { + String id = entry.getKey(); + ConfigValue value = entry.getValue(); + TypeAdapter adapter = value.getAdapter(); + buffer.writeUtf(id); + adapter.encodeToBuffer(value, buffer); + } + }); + }; + } + + @Override + public IPacketDecoder getDecoder() { + return buffer -> { + String config = buffer.readUtf(); + int i = buffer.readInt(); + ConfigHolder.getConfig(config).ifPresent(data -> { + Map> serialized = data.getNetworkSerializedFields(); + for (int j = 0; j < i; j++) { + String fieldId = buffer.readUtf(); + ConfigValue value = serialized.get(fieldId); + if (value == null) { + AzureLib.LOGGER.fatal(Networking.MARKER, "Received unknown config value " + fieldId); + throw new AzureLibException("Unknown config field: " + fieldId); + } + setValue(value, buffer); + } + }); + return new ConfigData(config); + }; + } + + @Override + public void handleClientsidePacket(Minecraft client, ClientPacketListener listener, ConfigData packetData, PacketSender dispatcher) { + } + + private void setValue(ConfigValue value, FriendlyByteBuf buffer) { + TypeAdapter adapter = value.getAdapter(); + V v = (V) adapter.decodeFromBuffer(value, buffer); + value.set(v); + } + + static final class ConfigData { + + private final String configId; + + public ConfigData(String configId) { + this.configId = configId; + } + } +} diff --git a/fabric/src/main/java/mod/azure/azurelib/fabric/network/api/IClientPacket.java b/fabric/src/main/java/mod/azure/azurelib/fabric/network/api/IClientPacket.java new file mode 100644 index 0000000..4af4879 --- /dev/null +++ b/fabric/src/main/java/mod/azure/azurelib/fabric/network/api/IClientPacket.java @@ -0,0 +1,14 @@ +package mod.azure.azurelib.fabric.network.api; + +import mod.azure.azurelib.common.internal.common.network.api.IPacket; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.fabricmc.fabric.api.networking.v1.PacketSender; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientPacketListener; + +public interface IClientPacket extends IPacket { + + @Environment(EnvType.CLIENT) + void handleClientsidePacket(Minecraft client, ClientPacketListener listener, T packetData, PacketSender dispatcher); +} diff --git a/fabric/src/main/java/mod/azure/azurelib/fabric/platform/FabricAzureLibInitializer.java b/fabric/src/main/java/mod/azure/azurelib/fabric/platform/FabricAzureLibInitializer.java new file mode 100644 index 0000000..45cad71 --- /dev/null +++ b/fabric/src/main/java/mod/azure/azurelib/fabric/platform/FabricAzureLibInitializer.java @@ -0,0 +1,36 @@ +package mod.azure.azurelib.fabric.platform; + +import mod.azure.azurelib.common.internal.common.AzureLib; +import mod.azure.azurelib.common.internal.common.cache.AzureLibCache; +import mod.azure.azurelib.common.platform.services.AzureLibInitializer; +import net.fabricmc.fabric.api.resource.IdentifiableResourceReloadListener; +import net.fabricmc.fabric.api.resource.ResourceManagerHelper; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.PackType; +import net.minecraft.server.packs.resources.PreparableReloadListener; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.util.profiling.ProfilerFiller; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +public class FabricAzureLibInitializer implements AzureLibInitializer { + @Override + public void initialize() { + ResourceManagerHelper.get(PackType.CLIENT_RESOURCES) + .registerReloadListener(new IdentifiableResourceReloadListener() { + @Override + public ResourceLocation getFabricId() { + return AzureLib.modResource("models"); + } + + @Override + public CompletableFuture reload(PreparableReloadListener.PreparationBarrier synchronizer, ResourceManager manager, + ProfilerFiller prepareProfiler, ProfilerFiller applyProfiler, Executor prepareExecutor, + Executor applyExecutor) { + return AzureLibCache.reload(synchronizer, manager, prepareProfiler, + applyProfiler, prepareExecutor, applyExecutor); + } + }); + } +} \ No newline at end of file diff --git a/fabric/src/main/java/mod/azure/azurelib/fabric/platform/FabricAzureLibNetwork.java b/fabric/src/main/java/mod/azure/azurelib/fabric/platform/FabricAzureLibNetwork.java new file mode 100644 index 0000000..24aab26 --- /dev/null +++ b/fabric/src/main/java/mod/azure/azurelib/fabric/platform/FabricAzureLibNetwork.java @@ -0,0 +1,101 @@ +package mod.azure.azurelib.fabric.platform; + +import mod.azure.azurelib.common.internal.common.network.AbstractPacket; +import mod.azure.azurelib.common.internal.common.network.packet.*; +import mod.azure.azurelib.common.platform.services.AzureLibNetwork; +import mod.azure.azurelib.fabric.network.Networking; +import mod.azure.azurelib.fabric.network.S2C_SendConfigData; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; +import net.fabricmc.fabric.api.networking.v1.PlayerLookup; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.client.Minecraft; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.Packet; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; + +public class FabricAzureLibNetwork implements AzureLibNetwork { + + private void handlePacket(Minecraft client, AbstractPacket packet) { + client.execute(packet::handle); + } + + @Override + public void registerClientReceiverPackets() { + ClientPlayNetworking.registerGlobalReceiver(ANIM_DATA_SYNC_PACKET_ID, + (client, handler, buf, responseSender) -> this.handlePacket(client, AnimDataSyncPacket.receive(buf))); + ClientPlayNetworking.registerGlobalReceiver(ANIM_TRIGGER_SYNC_PACKET_ID, + (client, handler, buf, responseSender) -> this.handlePacket(client, AnimTriggerPacket.receive(buf))); + + ClientPlayNetworking.registerGlobalReceiver(ENTITY_ANIM_DATA_SYNC_PACKET_ID, + (client, handler, buf, responseSender) -> this.handlePacket(client, + EntityAnimDataSyncPacket.receive(buf))); + ClientPlayNetworking.registerGlobalReceiver(ENTITY_ANIM_TRIGGER_SYNC_PACKET_ID, + (client, handler, buf, responseSender) -> this.handlePacket(client, + EntityAnimTriggerPacket.receive(buf))); + + ClientPlayNetworking.registerGlobalReceiver(BLOCK_ENTITY_ANIM_DATA_SYNC_PACKET_ID, + (client, handler, buf, responseSender) -> this.handlePacket(client, + BlockEntityAnimDataSyncPacket.receive(buf))); + ClientPlayNetworking.registerGlobalReceiver(BLOCK_ENTITY_ANIM_TRIGGER_SYNC_PACKET_ID, + (client, handler, buf, responseSender) -> this.handlePacket(client, + BlockEntityAnimTriggerPacket.receive(buf))); + + ClientPlayNetworking.registerGlobalReceiver(CUSTOM_ENTITY_ID, + (client, handler, buf, responseSender) -> EntityPacketOnClient.onPacket(client, buf)); + } + + @Override + public Packet createPacket(Entity entity) { + FriendlyByteBuf buf = createFriendlyByteBuf(); + buf.writeVarInt(BuiltInRegistries.ENTITY_TYPE.getId(entity.getType())); + buf.writeUUID(entity.getUUID()); + buf.writeVarInt(entity.getId()); + buf.writeDouble(entity.getX()); + buf.writeDouble(entity.getY()); + buf.writeDouble(entity.getZ()); + buf.writeByte(Mth.floor(entity.getXRot() * 256.0F / 360.0F)); + buf.writeByte(Mth.floor(entity.getYRot() * 256.0F / 360.0F)); + buf.writeFloat(entity.getXRot()); + buf.writeFloat(entity.getYRot()); + return ServerPlayNetworking.createS2CPacket(AzureLibNetwork.CUSTOM_ENTITY_ID, buf); + } + + public FriendlyByteBuf createFriendlyByteBuf() { + return PacketByteBufs.create(); + } + + @Override + public void sendToTrackingEntityAndSelf(AbstractPacket packet, Entity entityToTrack) { + for (ServerPlayer trackingPlayer : PlayerLookup.tracking(entityToTrack)) { + FriendlyByteBuf buf = createFriendlyByteBuf(); + packet.write(buf); + ServerPlayNetworking.send(trackingPlayer, packet.id(), buf); + } + + if (entityToTrack instanceof ServerPlayer serverPlayer) { + FriendlyByteBuf buf = createFriendlyByteBuf(); + packet.write(buf); + ServerPlayNetworking.send(serverPlayer, packet.id(), buf); + } + } + + @Override + public void sendToEntitiesTrackingChunk(AbstractPacket packet, ServerLevel level, BlockPos blockPos) { + for (ServerPlayer trackingPlayer : PlayerLookup.tracking(level, blockPos)) { + FriendlyByteBuf buf = createFriendlyByteBuf(); + packet.write(buf); + ServerPlayNetworking.send(trackingPlayer, packet.id(), buf); + } + } + + @Override + public void sendClientPacket(ServerPlayer player, String id) { + Networking.sendClientPacket(player, new S2C_SendConfigData(id)); + } +} diff --git a/fabric/src/main/java/mod/azure/azurelib/fabric/platform/FabricGeoRenderPhaseEventFactory.java b/fabric/src/main/java/mod/azure/azurelib/fabric/platform/FabricGeoRenderPhaseEventFactory.java new file mode 100644 index 0000000..cfff2e9 --- /dev/null +++ b/fabric/src/main/java/mod/azure/azurelib/fabric/platform/FabricGeoRenderPhaseEventFactory.java @@ -0,0 +1,14 @@ +package mod.azure.azurelib.fabric.platform; + +import mod.azure.azurelib.fabric.event.FabricGeoRenderPhaseEvent; +import mod.azure.azurelib.common.platform.services.GeoRenderPhaseEventFactory; + +/** + * @author Boston Vanseghi + */ +public class FabricGeoRenderPhaseEventFactory implements GeoRenderPhaseEventFactory { + @Override + public GeoRenderPhaseEvent create() { + return new FabricGeoRenderPhaseEvent(); + } +} diff --git a/fabric/src/main/java/mod/azure/azurelib/fabric/platform/FabricPlatformHelper.java b/fabric/src/main/java/mod/azure/azurelib/fabric/platform/FabricPlatformHelper.java new file mode 100644 index 0000000..e42830a --- /dev/null +++ b/fabric/src/main/java/mod/azure/azurelib/fabric/platform/FabricPlatformHelper.java @@ -0,0 +1,62 @@ +package mod.azure.azurelib.fabric.platform; + +import mod.azure.azurelib.common.internal.common.blocks.TickingLightBlock; +import mod.azure.azurelib.common.internal.common.blocks.TickingLightEntity; +import mod.azure.azurelib.common.platform.services.IPlatformHelper; +import net.fabricmc.api.EnvType; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.world.item.enchantment.Enchantment; +import net.minecraft.world.level.block.entity.BlockEntityType; +import org.Vrglab.AzureLib.fabric.VlAzureLibModFabric; + +import java.nio.file.Path; + +public class FabricPlatformHelper implements IPlatformHelper { + + @Override + public String getPlatformName() { + return "Fabric"; + } + + @Override + public boolean isModLoaded(String modId) { + + return FabricLoader.getInstance().isModLoaded(modId); + } + + @Override + public boolean isDevelopmentEnvironment() { + + return FabricLoader.getInstance().isDevelopmentEnvironment(); + } + + @Override + public Path getGameDir() { + return FabricLoader.getInstance().getGameDir(); + } + + @Override + public boolean isServerEnvironment() { + return FabricLoader.getInstance().getEnvironmentType() == EnvType.SERVER; + } + + @Override + public TickingLightBlock getTickingLightBlock() { + return VlAzureLibModFabric.TICKING_LIGHT_BLOCK; + } + + @Override + public BlockEntityType getTickingLightEntity() { + return VlAzureLibModFabric.TICKING_LIGHT_ENTITY; + } + + @Override + public Enchantment getIncendairyenchament() { + return VlAzureLibModFabric.INCENDIARYENCHANTMENT; + } + + @Override + public Path modsDir() { + return FabricLoader.getInstance().getGameDir().resolve("mods"); + } +} \ No newline at end of file diff --git a/fabric/src/main/java/org/Vrglab/AzureLib/fabric/VlAzureLibModFabric.java b/fabric/src/main/java/org/Vrglab/AzureLib/fabric/VlAzureLibModFabric.java new file mode 100644 index 0000000..b73a81b --- /dev/null +++ b/fabric/src/main/java/org/Vrglab/AzureLib/fabric/VlAzureLibModFabric.java @@ -0,0 +1,49 @@ +package org.Vrglab.AzureLib.fabric; + +import mod.azure.azurelib.common.api.common.enchantments.IncendiaryEnchantment; +import mod.azure.azurelib.common.internal.common.AzureLib; +import mod.azure.azurelib.common.internal.common.AzureLibMod; +import mod.azure.azurelib.common.internal.common.blocks.TickingLightBlock; +import mod.azure.azurelib.common.internal.common.blocks.TickingLightEntity; +import mod.azure.azurelib.common.internal.common.config.AzureLibConfig; +import mod.azure.azurelib.common.internal.common.config.format.ConfigFormats; +import mod.azure.azurelib.common.internal.common.config.io.ConfigIO; +import net.fabricmc.api.ModInitializer; + +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.object.builder.v1.block.entity.FabricBlockEntityTypeBuilder; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.item.enchantment.Enchantment; +import net.minecraft.world.level.block.SoundType; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.material.PushReaction; +import org.Vrglab.AzureLib.VlAzureLibMod; +import org.Vrglab.AzureLib.fabriclike.VlAzureLibModFabricLike; +import org.Vrglab.fabriclike.VLModFabricLike; + +public final class VlAzureLibModFabric implements ModInitializer { + + public static BlockEntityType TICKING_LIGHT_ENTITY; + public static final TickingLightBlock TICKING_LIGHT_BLOCK = new TickingLightBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).lightLevel(TickingLightBlock.litBlockEmission(15)).pushReaction(PushReaction.DESTROY).noOcclusion()); + public static final Enchantment INCENDIARYENCHANTMENT = new IncendiaryEnchantment(Enchantment.Rarity.RARE, EquipmentSlot.MAINHAND); + + @Override + public void onInitialize() { + ConfigIO.FILE_WATCH_MANAGER.startService(); + VlAzureLibMod.init(); + Registry.register(BuiltInRegistries.BLOCK, AzureLib.modResource("lightblock"), VlAzureLibModFabric.TICKING_LIGHT_BLOCK); + VlAzureLibModFabric.TICKING_LIGHT_ENTITY = Registry.register(BuiltInRegistries.BLOCK_ENTITY_TYPE, AzureLib.MOD_ID + ":lightblock", FabricBlockEntityTypeBuilder.create(TickingLightEntity::new, VlAzureLibModFabric.TICKING_LIGHT_BLOCK).build(null)); + AzureLibMod.config = AzureLibMod.registerConfig(AzureLibConfig.class, ConfigFormats.json()).getConfigInstance(); + ServerLifecycleEvents.SERVER_STOPPING.register((server) -> { + ConfigIO.FILE_WATCH_MANAGER.stopService(); + }); + Registry.register(BuiltInRegistries.ENCHANTMENT, AzureLib.modResource("incendiaryenchantment"), INCENDIARYENCHANTMENT); + + VLModFabricLike.init(VlAzureLibMod.MOD_ID, ()->{ + VlAzureLibModFabricLike.init(); + }); + } +} diff --git a/fabric/src/main/java/org/Vrglab/AzureLib/fabric/client/VlAzureLibModFabricClient.java b/fabric/src/main/java/org/Vrglab/AzureLib/fabric/client/VlAzureLibModFabricClient.java new file mode 100644 index 0000000..e16c58b --- /dev/null +++ b/fabric/src/main/java/org/Vrglab/AzureLib/fabric/client/VlAzureLibModFabricClient.java @@ -0,0 +1,31 @@ +package org.Vrglab.AzureLib.fabric.client; + +import com.mojang.blaze3d.platform.InputConstants; +import mod.azure.azurelib.common.api.client.helper.ClientUtils; +import mod.azure.azurelib.common.internal.common.util.IncompatibleModsCheck; +import mod.azure.azurelib.common.platform.Services; +import mod.azure.azurelib.fabric.network.Networking; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; +import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; +import net.minecraft.client.KeyMapping; +import org.lwjgl.glfw.GLFW; + +public final class VlAzureLibModFabricClient implements ClientModInitializer { + @Override + public void onInitializeClient() { + ClientLifecycleEvents.CLIENT_STARTED.register(IncompatibleModsCheck::warnings); + ClientUtils.RELOAD = new KeyMapping("key.azurelib.reload", InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_R, + "category.azurelib.binds"); + KeyBindingHelper.registerKeyBinding(ClientUtils.RELOAD); + ClientUtils.SCOPE = new KeyMapping("key.azurelib.scope", InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_LEFT_ALT, + "category.azurelib.binds"); + KeyBindingHelper.registerKeyBinding(ClientUtils.SCOPE); + ClientUtils.FIRE_WEAPON = new KeyMapping("key.azurelib.fire", InputConstants.Type.KEYSYM, + GLFW.GLFW_KEY_UNKNOWN, + "category.azurelib.binds"); + KeyBindingHelper.registerKeyBinding(ClientUtils.FIRE_WEAPON); + Networking.PacketRegistry.registerClient(); + Services.NETWORK.registerClientReceiverPackets(); + } +} diff --git a/fabric/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.AzureLibInitializer b/fabric/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.AzureLibInitializer new file mode 100644 index 0000000..9db5ef9 --- /dev/null +++ b/fabric/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.AzureLibInitializer @@ -0,0 +1 @@ +mod.azure.azurelib.fabric.platform.FabricAzureLibInitializer \ No newline at end of file diff --git a/fabric/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.AzureLibNetwork b/fabric/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.AzureLibNetwork new file mode 100644 index 0000000..e68d42a --- /dev/null +++ b/fabric/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.AzureLibNetwork @@ -0,0 +1 @@ +mod.azure.azurelib.fabric.platform.FabricAzureLibNetwork \ No newline at end of file diff --git a/fabric/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.GeoRenderPhaseEventFactory b/fabric/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.GeoRenderPhaseEventFactory new file mode 100644 index 0000000..5573d31 --- /dev/null +++ b/fabric/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.GeoRenderPhaseEventFactory @@ -0,0 +1 @@ +mod.azure.azurelib.fabric.platform.FabricGeoRenderPhaseEventFactory \ No newline at end of file diff --git a/fabric/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.IPlatformHelper b/fabric/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.IPlatformHelper new file mode 100644 index 0000000..c45f504 --- /dev/null +++ b/fabric/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.IPlatformHelper @@ -0,0 +1 @@ +mod.azure.azurelib.fabric.platform.FabricPlatformHelper \ No newline at end of file diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..22f37c5 --- /dev/null +++ b/fabric/src/main/resources/fabric.mod.json @@ -0,0 +1,42 @@ +{ + "schemaVersion": 1, + "id": "vrglabs_azurelib", + "version": "${mod_version}", + "name": "Vrglabs AzureLib", + "description": "Utility classes for AzureLib for using with VrglabsLib", + "authors": [ + "Vrglab" + ], + "contact": { + "homepage": "https://fabricmc.net/", + "sources": "https://github.com/FabricMC/fabric-example-mod" + }, + "license": "MIT", + "icon": "logo.png", + "environment": "*", + "entrypoints": { + "main": [ + "org.Vrglab.AzureLib.fabric.VlAzureLibModFabric" + ], + "client": [ + "org.Vrglab.AzureLib.fabric.client.VlAzureLibModFabricClient" + ], + "modmenu": [ + "mod.azure.azurelib.fabric.integration.ModMenuIntegration" + ] + }, + "mixins": [ + "vrglabs_azurelib.mixins.json" + ], + "depends": { + "fabricloader": ">=0.15.11", + "minecraft": "~1.20.4", + "java": ">=17", + "architectury": ">=11.1.17", + "fabric-api": "*", + "vrglabslib": ">=1.0.0" + }, + "suggests": { + "another-mod": "*" + } +} diff --git a/fabric/src/main/resources/vrglabs_azurelib.mixins.json b/fabric/src/main/resources/vrglabs_azurelib.mixins.json new file mode 100644 index 0000000..4f33aa9 --- /dev/null +++ b/fabric/src/main/resources/vrglabs_azurelib.mixins.json @@ -0,0 +1,19 @@ +{ + "required": true, + "package": "mod.azure.azurelib.common.internal.mixins", + "compatibilityLevel": "JAVA_19", + "client": [ + "FabricMixinHumanoidArmorLayer", + "ItemRendererAccessor", + "MinecraftMixin", + "MixinItemRenderer", + "TextureManagerMixin", + "AccessorWarningScreen" + ], + "mixins": [ + "PlayerListMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..f3d44aa --- /dev/null +++ b/gradle.properties @@ -0,0 +1,22 @@ +# Done to increase the memory available to Gradle. +org.gradle.jvmargs=-Xmx2G +org.gradle.parallel=true + +# Mod properties +mod_version = 1.0.0 +maven_group = org.Vrglab.AzureLib +archives_name = azureLib +enabled_platforms = fabric,neoforge,quilt + +# Minecraft properties +minecraft_version = 1.20.4 +yarn_mappings = 1.20.4+build.1 + +# Dependencies +architectury_api_version = 11.1.17 +fabric_loader_version = 0.15.11 +fabric_api_version = 0.97.1+1.20.4 +neoforge_version = 20.4.234 +quilt_loader_version = 0.26.1-beta.1 +quilted_fabric_api_version = 9.0.0-alpha.8+0.97.0-1.20.4 +vrglabs_lib_version=1.0.0 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..d64cd49 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..b82aa23 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..1aa94a4 --- /dev/null +++ b/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..797b266 Binary files /dev/null and b/logo.png differ diff --git a/neoforge/build.gradle b/neoforge/build.gradle new file mode 100644 index 0000000..2c892d4 --- /dev/null +++ b/neoforge/build.gradle @@ -0,0 +1,58 @@ +plugins { + id 'com.github.johnrengelman.shadow' +} + +architectury { + platformSetupLoomIde() + neoForge() +} + +configurations { + common { + canBeResolved = true + canBeConsumed = false + } + compileClasspath.extendsFrom common + runtimeClasspath.extendsFrom common + developmentNeoForge.extendsFrom common + + // Files in this configuration will be bundled into your mod using the Shadow plugin. + // Don't use the `shadow` configuration from the plugin itself as it's meant for excluding files. + shadowBundle { + canBeResolved = true + canBeConsumed = false + } +} + +repositories { + maven { + name = 'NeoForged' + url = 'https://maven.neoforged.net/releases' + } +} + +dependencies { + neoForge "net.neoforged:neoforge:$rootProject.neoforge_version" + + // Architectury API. This is optional, and you can comment it out if you don't need it. + modImplementation "dev.architectury:architectury-neoforge:$rootProject.architectury_api_version" + modImplementation "org.Vrglab:vrglabslib:neoforge-$rootProject.vrglabs_lib_version-mc$rootProject.minecraft_version" + + common(project(path: ':common', configuration: 'namedElements')) { transitive false } + shadowBundle project(path: ':common', configuration: 'transformProductionNeoForge') +} + +processResources { + filesMatching('META-INF/mods.toml') { + expand rootProject.properties + } +} + +shadowJar { + configurations = [project.configurations.shadowBundle] + archiveClassifier = 'dev-shadow' +} + +remapJar { + input.set shadowJar.archiveFile +} diff --git a/neoforge/gradle.properties b/neoforge/gradle.properties new file mode 100644 index 0000000..2e6ed76 --- /dev/null +++ b/neoforge/gradle.properties @@ -0,0 +1 @@ +loom.platform = neoforge diff --git a/neoforge/src/main/java/mod/azure/azurelib/neoforge/ClientModListener.java b/neoforge/src/main/java/mod/azure/azurelib/neoforge/ClientModListener.java new file mode 100644 index 0000000..c0345e9 --- /dev/null +++ b/neoforge/src/main/java/mod/azure/azurelib/neoforge/ClientModListener.java @@ -0,0 +1,60 @@ +package mod.azure.azurelib.neoforge; + +import com.mojang.blaze3d.platform.InputConstants; +import mod.azure.azurelib.common.api.client.helper.ClientUtils; +import mod.azure.azurelib.common.internal.client.AzureLibClient; +import mod.azure.azurelib.common.internal.common.AzureLib; +import mod.azure.azurelib.common.internal.common.config.ConfigHolder; +import mod.azure.azurelib.common.platform.Services; +import net.minecraft.client.KeyMapping; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.ModList; +import net.neoforged.fml.common.Mod; +import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent; +import net.neoforged.neoforge.client.ConfigScreenHandler; +import net.neoforged.neoforge.client.event.RegisterKeyMappingsEvent; +import org.lwjgl.glfw.GLFW; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +@Mod.EventBusSubscriber(modid = AzureLib.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD, value = Dist.CLIENT) +public class ClientModListener { + + @SubscribeEvent + public static void registerKeys(final RegisterKeyMappingsEvent event) { + ClientUtils.RELOAD = new KeyMapping("key.azurelib.reload", InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_R, + "category.azurelib.binds"); + event.register(ClientUtils.RELOAD); + ClientUtils.SCOPE = new KeyMapping("key.azurelib.scope", InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_LEFT_ALT, + "category.azurelib.binds"); + event.register(ClientUtils.SCOPE); + ClientUtils.FIRE_WEAPON = new KeyMapping("key.azurelib.fire", InputConstants.Type.KEYSYM, + GLFW.GLFW_KEY_UNKNOWN, "category.azurelib.binds"); + event.register(ClientUtils.FIRE_WEAPON); + } + + @SubscribeEvent + public static void clientInit(final FMLClientSetupEvent event) { + Map>> groups = ConfigHolder.getConfigGroupingByGroup(); + ModList modList = ModList.get(); + for (Map.Entry>> entry : groups.entrySet()) { + String modId = entry.getKey(); + Optional optional = modList.getModContainerById(modId); + optional.ifPresent(modContainer -> { + List> list = entry.getValue(); + modContainer.registerExtensionPoint(ConfigScreenHandler.ConfigScreenFactory.class, + () -> new ConfigScreenHandler.ConfigScreenFactory((minecraft, screen) -> { + if (list.size() == 1) { + return AzureLibClient.getConfigScreen(list.get(0).getConfigId(), screen); + } + return AzureLibClient.getConfigScreenByGroup(list, modId, screen); + })); + }); + } + Services.NETWORK.registerClientReceiverPackets(); + } +} diff --git a/neoforge/src/main/java/mod/azure/azurelib/neoforge/ClientNonModListener.java b/neoforge/src/main/java/mod/azure/azurelib/neoforge/ClientNonModListener.java new file mode 100644 index 0000000..253dc39 --- /dev/null +++ b/neoforge/src/main/java/mod/azure/azurelib/neoforge/ClientNonModListener.java @@ -0,0 +1,19 @@ +package mod.azure.azurelib.neoforge; + +import mod.azure.azurelib.common.internal.common.util.IncompatibleModsCheck; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.TitleScreen; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.Mod; +import net.neoforged.neoforge.client.event.ScreenEvent; + +@Mod.EventBusSubscriber(Dist.CLIENT) +public class ClientNonModListener { + @SubscribeEvent + public static void onClientStart(ScreenEvent.Init.Post event) { + if (event.getScreen() instanceof TitleScreen) { + IncompatibleModsCheck.warnings(Minecraft.getInstance()); + } + } +} diff --git a/neoforge/src/main/java/mod/azure/azurelib/neoforge/event/NeoForgeGeoRenderPhaseEvent.java b/neoforge/src/main/java/mod/azure/azurelib/neoforge/event/NeoForgeGeoRenderPhaseEvent.java new file mode 100644 index 0000000..9383126 --- /dev/null +++ b/neoforge/src/main/java/mod/azure/azurelib/neoforge/event/NeoForgeGeoRenderPhaseEvent.java @@ -0,0 +1,30 @@ +package mod.azure.azurelib.neoforge.event; + +import mod.azure.azurelib.common.internal.common.event.GeoRenderEvent; +import mod.azure.azurelib.common.platform.services.GeoRenderPhaseEventFactory; +import net.neoforged.bus.api.Event; +import net.neoforged.bus.api.ICancellableEvent; +import net.neoforged.neoforge.common.NeoForge; + +/** + * @author Boston Vanseghi + */ +public class NeoForgeGeoRenderPhaseEvent implements GeoRenderPhaseEventFactory.GeoRenderPhaseEvent { + + public static class NeoForgeGeoRenderEvent extends Event implements ICancellableEvent { + public final GeoRenderEvent geoRenderEvent; + + public NeoForgeGeoRenderEvent(GeoRenderEvent geoRenderEvent) { + this.geoRenderEvent = geoRenderEvent; + } + + public GeoRenderEvent getGeoRenderEvent() { + return this.geoRenderEvent; + } + } + + @Override + public boolean handle(GeoRenderEvent geoRenderEvent) { + return !NeoForge.EVENT_BUS.post(new NeoForgeGeoRenderEvent(geoRenderEvent)).isCanceled(); + } +} diff --git a/neoforge/src/main/java/mod/azure/azurelib/neoforge/items/NeoForgeAzureSpawnEgg.java b/neoforge/src/main/java/mod/azure/azurelib/neoforge/items/NeoForgeAzureSpawnEgg.java new file mode 100644 index 0000000..ad470fd --- /dev/null +++ b/neoforge/src/main/java/mod/azure/azurelib/neoforge/items/NeoForgeAzureSpawnEgg.java @@ -0,0 +1,20 @@ +package mod.azure.azurelib.neoforge.items; + +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.item.Item; +import net.neoforged.neoforge.common.DeferredSpawnEggItem; + +import java.util.function.Supplier; + +public class NeoForgeAzureSpawnEgg extends DeferredSpawnEggItem { + + public NeoForgeAzureSpawnEgg(Supplier> type, int primaryColor, int secondaryColor) { + super(type, primaryColor, secondaryColor, new Item.Properties().stacksTo(64)); + } + + public NeoForgeAzureSpawnEgg(EntityType type, int primaryColor, int secondaryColor) { + super(() -> type, primaryColor, secondaryColor, new Item.Properties().stacksTo(64)); + } + +} \ No newline at end of file diff --git a/neoforge/src/main/java/mod/azure/azurelib/neoforge/mixins/ClientHooksMixin.java b/neoforge/src/main/java/mod/azure/azurelib/neoforge/mixins/ClientHooksMixin.java new file mode 100644 index 0000000..9cb3b49 --- /dev/null +++ b/neoforge/src/main/java/mod/azure/azurelib/neoforge/mixins/ClientHooksMixin.java @@ -0,0 +1,26 @@ +package mod.azure.azurelib.neoforge.mixins; + +import mod.azure.azurelib.common.api.common.animatable.GeoItem; +import mod.azure.azurelib.common.internal.client.RenderProvider; +import mod.azure.azurelib.common.internal.common.AzureLib; +import mod.azure.azurelib.common.internal.common.AzureLibMod; +import net.minecraft.client.model.HumanoidModel; +import net.minecraft.client.model.Model; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.item.ItemStack; +import net.neoforged.neoforge.client.ClientHooks; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(ClientHooks.class) +public class ClientHooksMixin { + + @Inject(method = "getArmorModel", at = @At("RETURN"), remap = false, cancellable = true) + private static void injectAzureArmors(LivingEntity entityLiving, ItemStack itemStack, EquipmentSlot slot, HumanoidModel _default, CallbackInfoReturnable cir) { + if (itemStack.getItem() instanceof GeoItem) + cir.setReturnValue((Model) RenderProvider.of(itemStack).getGenericArmorModel(entityLiving, itemStack, slot, (HumanoidModel) _default)); + } +} diff --git a/neoforge/src/main/java/mod/azure/azurelib/neoforge/network/IPacket.java b/neoforge/src/main/java/mod/azure/azurelib/neoforge/network/IPacket.java new file mode 100644 index 0000000..0d7c68f --- /dev/null +++ b/neoforge/src/main/java/mod/azure/azurelib/neoforge/network/IPacket.java @@ -0,0 +1,8 @@ +package mod.azure.azurelib.neoforge.network; + +import net.minecraft.network.FriendlyByteBuf; + +public interface IPacket

> { + + P decode(FriendlyByteBuf buffer); +} diff --git a/neoforge/src/main/java/mod/azure/azurelib/neoforge/network/S2C_NeoSendConfigData.java b/neoforge/src/main/java/mod/azure/azurelib/neoforge/network/S2C_NeoSendConfigData.java new file mode 100644 index 0000000..b4f314f --- /dev/null +++ b/neoforge/src/main/java/mod/azure/azurelib/neoforge/network/S2C_NeoSendConfigData.java @@ -0,0 +1,85 @@ +package mod.azure.azurelib.neoforge.network; + +import mod.azure.azurelib.common.internal.common.AzureLib; +import mod.azure.azurelib.common.internal.common.AzureLibException; +import mod.azure.azurelib.common.internal.common.config.ConfigHolder; +import mod.azure.azurelib.common.internal.common.config.adapter.TypeAdapter; +import mod.azure.azurelib.common.internal.common.config.value.ConfigValue; +import mod.azure.azurelib.common.internal.common.network.AbstractPacket; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.MarkerManager; + +import java.util.Map; + +public class S2C_NeoSendConfigData extends AbstractPacket implements IPacket { + + private final String config; + + public static final Marker MARKER = MarkerManager.getMarker("Network"); + + S2C_NeoSendConfigData() { + this.config = null; + } + + public S2C_NeoSendConfigData(String config) { + this.config = config; + } + + @Override + public void write(FriendlyByteBuf buffer) { + buffer.writeUtf(this.config); + ConfigHolder.getConfig(this.config).ifPresent(data -> { + Map> serialized = data.getNetworkSerializedFields(); + buffer.writeInt(serialized.size()); + for (Map.Entry> entry : serialized.entrySet()) { + String id = entry.getKey(); + ConfigValue value = entry.getValue(); + TypeAdapter adapter = value.getAdapter(); + buffer.writeUtf(id); + adapter.encodeToBuffer(value, buffer); + } + }); + } + + public static S2C_NeoSendConfigData receive(FriendlyByteBuf buf) { + return new S2C_NeoSendConfigData(); + } + + @Override + public void handle() { + + } + + @Override + public ResourceLocation id() { + return null; + } + + @Override + public S2C_NeoSendConfigData decode(FriendlyByteBuf buffer) { + String config = buffer.readUtf(); + int i = buffer.readInt(); + ConfigHolder.getConfig(config).ifPresent(data -> { + Map> serialized = data.getNetworkSerializedFields(); + for (int j = 0; j < i; j++) { + String fieldId = buffer.readUtf(); + ConfigValue value = serialized.get(fieldId); + if (value == null) { + AzureLib.LOGGER.fatal(MARKER, "Received unknown config value " + fieldId); + throw new AzureLibException("Unknown config field: " + fieldId); + } + setValue(value, buffer); + } + }); + return new S2C_NeoSendConfigData(config); + } + + @SuppressWarnings("unchecked") + private void setValue(ConfigValue value, FriendlyByteBuf buffer) { + TypeAdapter adapter = value.getAdapter(); + V v = (V) adapter.decodeFromBuffer(value, buffer); + value.set(v); + } +} diff --git a/neoforge/src/main/java/mod/azure/azurelib/neoforge/platform/NeoForgeAzureLibInitializer.java b/neoforge/src/main/java/mod/azure/azurelib/neoforge/platform/NeoForgeAzureLibInitializer.java new file mode 100644 index 0000000..df0794a --- /dev/null +++ b/neoforge/src/main/java/mod/azure/azurelib/neoforge/platform/NeoForgeAzureLibInitializer.java @@ -0,0 +1,15 @@ +package mod.azure.azurelib.neoforge.platform; + +import mod.azure.azurelib.common.internal.common.cache.AzureLibCache; +import mod.azure.azurelib.common.platform.services.AzureLibInitializer; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.fml.loading.FMLEnvironment; + +public class NeoForgeAzureLibInitializer implements AzureLibInitializer { + @Override + public void initialize() { + if (FMLEnvironment.dist == Dist.CLIENT) { + AzureLibCache.registerReloadListener(); + } + } +} \ No newline at end of file diff --git a/neoforge/src/main/java/mod/azure/azurelib/neoforge/platform/NeoForgeAzureLibNetwork.java b/neoforge/src/main/java/mod/azure/azurelib/neoforge/platform/NeoForgeAzureLibNetwork.java new file mode 100644 index 0000000..d3d6706 --- /dev/null +++ b/neoforge/src/main/java/mod/azure/azurelib/neoforge/platform/NeoForgeAzureLibNetwork.java @@ -0,0 +1,51 @@ +package mod.azure.azurelib.neoforge.platform; + +import mod.azure.azurelib.common.internal.common.network.AbstractPacket; +import mod.azure.azurelib.common.platform.services.AzureLibNetwork; +import mod.azure.azurelib.neoforge.network.S2C_NeoSendConfigData; +import net.minecraft.core.BlockPos; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.neoforged.neoforge.network.PacketDistributor; +import net.neoforged.neoforge.network.handling.PlayPayloadContext; + +public class NeoForgeAzureLibNetwork implements AzureLibNetwork { + + @Override + public Packet createPacket(Entity entity) { + return entity.getAddEntityPacket(); + } + + public static void handlePacket(AbstractPacket packet, PlayPayloadContext context) { + context.workHandler().execute(packet::handle); + } + + @Override + public void registerClientReceiverPackets() { + } + + @Override + public void sendToTrackingEntityAndSelf(AbstractPacket packet, Entity entityToTrack) { + send(packet, PacketDistributor.TRACKING_ENTITY_AND_SELF.with(entityToTrack)); + } + + @Override + public void sendToEntitiesTrackingChunk(AbstractPacket packet, ServerLevel level, BlockPos blockPos) { + send(packet, PacketDistributor.TRACKING_CHUNK.with(level.getChunkAt(blockPos))); + } + + /** + * Send a packet using AzureLib's packet channel + */ + public static void send(M packet, PacketDistributor.PacketTarget distributor) { + distributor.send((CustomPacketPayload) packet); + } + + @Override + public void sendClientPacket(ServerPlayer player, String id) { + send(new S2C_NeoSendConfigData(id), PacketDistributor.PLAYER.with(player)); + } +} diff --git a/neoforge/src/main/java/mod/azure/azurelib/neoforge/platform/NeoForgeGeoRenderPhaseEventFactory.java b/neoforge/src/main/java/mod/azure/azurelib/neoforge/platform/NeoForgeGeoRenderPhaseEventFactory.java new file mode 100644 index 0000000..9f700fc --- /dev/null +++ b/neoforge/src/main/java/mod/azure/azurelib/neoforge/platform/NeoForgeGeoRenderPhaseEventFactory.java @@ -0,0 +1,14 @@ +package mod.azure.azurelib.neoforge.platform; + +import mod.azure.azurelib.common.platform.services.GeoRenderPhaseEventFactory; +import mod.azure.azurelib.neoforge.event.NeoForgeGeoRenderPhaseEvent; + +/** + * @author Boston Vanseghi + */ +public class NeoForgeGeoRenderPhaseEventFactory implements GeoRenderPhaseEventFactory { + @Override + public GeoRenderPhaseEvent create() { + return new NeoForgeGeoRenderPhaseEvent(); + } +} diff --git a/neoforge/src/main/java/mod/azure/azurelib/neoforge/platform/NeoForgePlatformHelper.java b/neoforge/src/main/java/mod/azure/azurelib/neoforge/platform/NeoForgePlatformHelper.java new file mode 100644 index 0000000..6765496 --- /dev/null +++ b/neoforge/src/main/java/mod/azure/azurelib/neoforge/platform/NeoForgePlatformHelper.java @@ -0,0 +1,66 @@ +package mod.azure.azurelib.neoforge.platform; + +import mod.azure.azurelib.common.internal.common.blocks.TickingLightBlock; +import mod.azure.azurelib.common.internal.common.blocks.TickingLightEntity; +import mod.azure.azurelib.common.platform.services.IPlatformHelper; +import net.minecraft.world.item.enchantment.Enchantment; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.neoforged.fml.ModList; +import net.neoforged.fml.loading.FMLEnvironment; +import net.neoforged.fml.loading.FMLLoader; +import net.neoforged.fml.loading.FMLPaths; +import org.Vrglab.AzureLib.neoforge.VlAzureLibModNeoForge; + +import java.nio.file.Path; + +public class NeoForgePlatformHelper implements IPlatformHelper { + + @Override + public String getPlatformName() { + + return "Forge"; + } + + @Override + public boolean isModLoaded(String modId) { + + return ModList.get().isLoaded(modId); + } + + @Override + public boolean isDevelopmentEnvironment() { + + return !FMLLoader.isProduction(); + } + + @Override + public Path getGameDir() { + return FMLLoader.getGamePath(); + } + + @Override + public boolean isServerEnvironment() { + return FMLEnvironment.dist.isDedicatedServer(); + } + + + @Override + public TickingLightBlock getTickingLightBlock() { + return (TickingLightBlock) VlAzureLibModNeoForge.AzureBlocks.TICKING_LIGHT_BLOCK.get(); + } + + @Override + public BlockEntityType getTickingLightEntity() { + return VlAzureLibModNeoForge.AzureEntities.TICKING_LIGHT_ENTITY.get(); + } + + @Override + public Enchantment getIncendairyenchament() { + return VlAzureLibModNeoForge.AzureEnchantments.INCENDIARYENCHANTMENT.get(); + } + + @Override + public Path modsDir() { + return FMLPaths.MODSDIR.get(); + } +} \ No newline at end of file diff --git a/neoforge/src/main/java/org/Vrglab/AzureLib/neoforge/VlAzureLibModNeoForge.java b/neoforge/src/main/java/org/Vrglab/AzureLib/neoforge/VlAzureLibModNeoForge.java new file mode 100644 index 0000000..ee535ff --- /dev/null +++ b/neoforge/src/main/java/org/Vrglab/AzureLib/neoforge/VlAzureLibModNeoForge.java @@ -0,0 +1,100 @@ +package org.Vrglab.AzureLib.neoforge; + +import mod.azure.azurelib.common.api.common.enchantments.IncendiaryEnchantment; +import mod.azure.azurelib.common.internal.common.AzureLib; +import mod.azure.azurelib.common.internal.common.AzureLibMod; +import mod.azure.azurelib.common.internal.common.blocks.TickingLightBlock; +import mod.azure.azurelib.common.internal.common.blocks.TickingLightEntity; +import mod.azure.azurelib.common.internal.common.config.AzureLibConfig; +import mod.azure.azurelib.common.internal.common.config.format.ConfigFormats; +import mod.azure.azurelib.common.internal.common.config.io.ConfigIO; +import mod.azure.azurelib.common.internal.common.network.packet.*; +import mod.azure.azurelib.common.platform.services.AzureLibNetwork; +import mod.azure.azurelib.neoforge.network.S2C_NeoSendConfigData; +import mod.azure.azurelib.neoforge.platform.NeoForgeAzureLibNetwork; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.item.enchantment.Enchantment; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.SoundType; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.material.PushReaction; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.common.Mod; + +import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent; +import net.neoforged.neoforge.network.event.RegisterPayloadHandlerEvent; +import net.neoforged.neoforge.network.registration.IPayloadRegistrar; +import net.neoforged.neoforge.registries.DeferredRegister; +import org.Vrglab.AzureLib.VlAzureLibMod; +import org.Vrglab.neoforge.Utils.NeoForgeRegistryCreator; + +import java.util.function.Supplier; + +@Mod(VlAzureLibMod.MOD_ID) +public final class VlAzureLibModNeoForge { + + public static VlAzureLibModNeoForge instance; + + public VlAzureLibModNeoForge(IEventBus modEventBus) { + instance = this; + modEventBus.addListener(this::registerPackets); + AzureLibMod.config = AzureLibMod.registerConfig(AzureLibConfig.class, ConfigFormats.json()).getConfigInstance(); + modEventBus.addListener(this::init); + AzureEnchantments.ENCHANTMENTS.register(modEventBus); + AzureBlocks.BLOCKS.register(modEventBus); + AzureEntities.TILE_TYPES.register(modEventBus); + NeoForgeRegistryCreator.Create(modEventBus, AzureLib.MOD_ID); + VlAzureLibMod.init(); + } + + private void init(FMLCommonSetupEvent event) { + ConfigIO.FILE_WATCH_MANAGER.startService(); + } + + private void registerPackets(final RegisterPayloadHandlerEvent ev) { + final IPayloadRegistrar registrar = ev.registrar(AzureLib.MOD_ID); + + registrar.play(AzureLibNetwork.ANIM_DATA_SYNC_PACKET_ID, AnimDataSyncPacket::receive, + NeoForgeAzureLibNetwork::handlePacket); + registrar.play(AzureLibNetwork.ANIM_TRIGGER_SYNC_PACKET_ID, AnimTriggerPacket::receive, + NeoForgeAzureLibNetwork::handlePacket); + registrar.play(AzureLibNetwork.ENTITY_ANIM_DATA_SYNC_PACKET_ID, EntityAnimDataSyncPacket::receive, + NeoForgeAzureLibNetwork::handlePacket); + registrar.play(AzureLibNetwork.ENTITY_ANIM_TRIGGER_SYNC_PACKET_ID, EntityAnimTriggerPacket::receive, + NeoForgeAzureLibNetwork::handlePacket); + registrar.play(AzureLibNetwork.BLOCK_ENTITY_ANIM_DATA_SYNC_PACKET_ID, BlockEntityAnimDataSyncPacket::receive, + NeoForgeAzureLibNetwork::handlePacket); + registrar.play(AzureLibNetwork.BLOCK_ENTITY_ANIM_TRIGGER_SYNC_PACKET_ID, BlockEntityAnimTriggerPacket::receive, + NeoForgeAzureLibNetwork::handlePacket); + registrar.play(AzureLibNetwork.CONFIG_PACKET_ID, S2C_NeoSendConfigData::receive, + NeoForgeAzureLibNetwork::handlePacket); + } + + public class AzureEnchantments { + public static final DeferredRegister ENCHANTMENTS = DeferredRegister.create(Registries.ENCHANTMENT, + AzureLib.MOD_ID); + public static final Supplier INCENDIARYENCHANTMENT = ENCHANTMENTS.register("incendiaryenchantment", + () -> new IncendiaryEnchantment(Enchantment.Rarity.RARE, EquipmentSlot.MAINHAND)); + } + + public class AzureBlocks { + public static final DeferredRegister BLOCKS = DeferredRegister.create(Registries.BLOCK, AzureLib.MOD_ID); + + public static final Supplier TICKING_LIGHT_BLOCK = BLOCKS.register("lightblock", + () -> new TickingLightBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).lightLevel( + TickingLightBlock.litBlockEmission(15)).pushReaction(PushReaction.DESTROY).noOcclusion())); + } + + public class AzureEntities { + + public static final DeferredRegister> TILE_TYPES = DeferredRegister.create( + Registries.BLOCK_ENTITY_TYPE, AzureLib.MOD_ID); + + public static final Supplier> TICKING_LIGHT_ENTITY = TILE_TYPES.register( + "lightblock", + () -> BlockEntityType.Builder.of(TickingLightEntity::new, AzureBlocks.TICKING_LIGHT_BLOCK.get()).build( + null)); + } +} diff --git a/neoforge/src/main/resources/META-INF/accesstransformer.cfg b/neoforge/src/main/resources/META-INF/accesstransformer.cfg new file mode 100644 index 0000000..0718e85 --- /dev/null +++ b/neoforge/src/main/resources/META-INF/accesstransformer.cfg @@ -0,0 +1,13 @@ +public net.minecraft.client.model.geom.ModelPart$Cube +public net.minecraft.client.model.geom.ModelPart cubes # cubes +public net.minecraft.world.entity.Entity getLeashOffset()Lnet/minecraft/world/phys/Vec3; # getLeashOffset +public net.minecraft.world.entity.WalkAnimationState speedOld # speedOld +public net.minecraft.client.model.AgeableListModel scaleHead # scaleHead +public net.minecraft.client.model.AgeableListModel babyYHeadOffset # babyYHeadOffset +public net.minecraft.client.model.AgeableListModel babyZHeadOffset # babyZHeadOffset +public net.minecraft.client.model.AgeableListModel babyHeadScale # babyHeadScale +public net.minecraft.client.model.AgeableListModel babyBodyScale # babyBodyScale +public net.minecraft.client.model.AgeableListModel bodyYOffset # bodyYOffset +public-f net.minecraft.client.renderer.LevelRenderer renderBuffers # renderBuffers +public-f net.minecraft.client.renderer.entity.layers.HumanoidArmorLayer renderModel(Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;ILnet/minecraft/world/item/ArmorItem;Lnet/minecraft/client/model/HumanoidModel;ZFFFLjava/lang/String;)V # renderModel +public com.mojang.blaze3d.vertex.BufferBuilder building # building \ No newline at end of file diff --git a/neoforge/src/main/resources/META-INF/mods.toml b/neoforge/src/main/resources/META-INF/mods.toml new file mode 100644 index 0000000..143d277 --- /dev/null +++ b/neoforge/src/main/resources/META-INF/mods.toml @@ -0,0 +1,41 @@ +modLoader = "javafml" +loaderVersion = "[2,)" +#issueTrackerURL = "" +license = "MIT" + +[[mods]] +modId = "vrglabs_azurelib" +version = "${mod_version}" +displayName = "Vrglabs AzureLib" +authors = "Vrglab" +description = ''' +Utility classes for AzureLib for using with VrglabsLib +''' +logoFile = "logo.png" + +[[dependencies.vrglabs_azurelib]] +modId = "neoforge" +type = "required" +versionRange = "[20.4,)" +ordering = "NONE" +side = "BOTH" + +[[dependencies.vrglabs_azurelib]] +modId = "minecraft" +type = "required" +versionRange = "[1.20.4,)" +ordering = "NONE" +side = "BOTH" + +[[dependencies.vrglabs_azurelib]] +modId = "architectury" +type = "required" +versionRange = "[11.1.17,)" +ordering = "AFTER" +side = "BOTH" + +[[mixins]] +config = "vrglabs_azurelib.neo.mixins.json" + +[[mixins]] +config = "vrglabs_azurelib.neo2.mixins.json" diff --git a/neoforge/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.AzureLibInitializer b/neoforge/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.AzureLibInitializer new file mode 100644 index 0000000..be7b15e --- /dev/null +++ b/neoforge/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.AzureLibInitializer @@ -0,0 +1 @@ +mod.azure.azurelib.neoforge.platform.NeoForgeAzureLibInitializer \ No newline at end of file diff --git a/neoforge/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.AzureLibNetwork b/neoforge/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.AzureLibNetwork new file mode 100644 index 0000000..c1f9f9a --- /dev/null +++ b/neoforge/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.AzureLibNetwork @@ -0,0 +1 @@ +mod.azure.azurelib.neoforge.platform.NeoForgeAzureLibNetwork \ No newline at end of file diff --git a/neoforge/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.CommonRegistry b/neoforge/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.CommonRegistry new file mode 100644 index 0000000..4fde23f --- /dev/null +++ b/neoforge/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.CommonRegistry @@ -0,0 +1 @@ +mod.azure.azurelib.neoforge.platform.NeoForgeCommonRegistry \ No newline at end of file diff --git a/neoforge/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.GeoRenderPhaseEventFactory b/neoforge/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.GeoRenderPhaseEventFactory new file mode 100644 index 0000000..1836f7b --- /dev/null +++ b/neoforge/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.GeoRenderPhaseEventFactory @@ -0,0 +1 @@ +mod.azure.azurelib.neoforge.platform.NeoForgeGeoRenderPhaseEventFactory \ No newline at end of file diff --git a/neoforge/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.IPlatformHelper b/neoforge/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.IPlatformHelper new file mode 100644 index 0000000..a382f76 --- /dev/null +++ b/neoforge/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.IPlatformHelper @@ -0,0 +1 @@ +mod.azure.azurelib.neoforge.platform.NeoForgePlatformHelper \ No newline at end of file diff --git a/neoforge/src/main/resources/vrglabs_azurelib.neo.mixins.json b/neoforge/src/main/resources/vrglabs_azurelib.neo.mixins.json new file mode 100644 index 0000000..4de0a73 --- /dev/null +++ b/neoforge/src/main/resources/vrglabs_azurelib.neo.mixins.json @@ -0,0 +1,19 @@ +{ + "required": true, + "package": "mod.azure.azurelib.common.internal.mixins", + "compatibilityLevel": "JAVA_19", + "injectors": { + "defaultRequire": 1 + }, + "client": [ + "NeoMixinHumanoidArmorLayer", + "ItemRendererAccessor", + "MinecraftMixin", + "MixinItemRenderer", + "TextureManagerMixin", + "AccessorWarningScreen" + ], + "mixins": [ + "PlayerListMixin" + ] +} diff --git a/neoforge/src/main/resources/vrglabs_azurelib.neo2.mixins.json b/neoforge/src/main/resources/vrglabs_azurelib.neo2.mixins.json new file mode 100644 index 0000000..dd2289f --- /dev/null +++ b/neoforge/src/main/resources/vrglabs_azurelib.neo2.mixins.json @@ -0,0 +1,11 @@ +{ + "required": true, + "package": "mod.azure.azurelib.neoforge.mixins", + "compatibilityLevel": "JAVA_19", + "injectors": { + "defaultRequire": 1 + }, + "client": [ + "ClientHooksMixin" + ] +} \ No newline at end of file diff --git a/quilt/build.gradle b/quilt/build.gradle new file mode 100644 index 0000000..6214e5a --- /dev/null +++ b/quilt/build.gradle @@ -0,0 +1,90 @@ +plugins { + id 'com.github.johnrengelman.shadow' +} + +repositories { + maven { url 'https://maven.quiltmc.org/repository/release/' } +} + +architectury { + platformSetupLoomIde() + loader('quilt') +} + +loom { + runs { + // This adds a new gradle task that runs the datagen API: "gradlew runDatagen" + datagen { + inherit server + name "Data Generation" + vmArg "-Dquilt-api.datagen" + vmArg "-Dquilt-api.datagen.output-dir=${file("src/main/generated")}" + vmArg "-Dquilt-api.datagen.modid=vrglabslib" + + runDir "build/datagen" + } + } +} + +sourceSets { + main { + resources { + srcDirs += [ + 'src/main/generated' + ] + } + } +} + +configurations { + common { + canBeResolved = true + canBeConsumed = false + } + compileClasspath.extendsFrom common + runtimeClasspath.extendsFrom common + developmentQuilt.extendsFrom common + + // Files in this configuration will be bundled into your mod using the Shadow plugin. + // Don't use the `shadow` configuration from the plugin itself as it's meant for excluding files. + shadowBundle { + canBeResolved = true + canBeConsumed = false + } +} + +dependencies { + modImplementation "org.quiltmc:quilt-loader:$rootProject.quilt_loader_version" + + // Quilt Standard Libraries and QSL. + modImplementation "org.quiltmc.quilted-fabric-api:quilted-fabric-api:$rootProject.quilted_fabric_api_version" + + modImplementation "org.Vrglab:vrglabslib:quilt-$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-fabric:$rootProject.architectury_api_version") { + // We must not pull Fabric Loader and Fabric API from Architectury Fabric. + exclude group: 'net.fabricmc' + exclude group: 'net.fabricmc.fabric-api' + } + + common(project(path: ':common', configuration: 'namedElements')) { transitive false } + shadowBundle project(path: ':common', configuration: 'transformProductionQuilt') + common(project(path: ':fabric-like', configuration: 'namedElements')) { transitive false } + shadowBundle project(path: ':fabric-like', configuration: 'transformProductionQuilt') +} + +processResources { + filesMatching('quilt.mod.json') { + expand rootProject.properties + } +} + +shadowJar { + configurations = [project.configurations.shadowBundle] + archiveClassifier = 'dev-shadow' +} + +remapJar { + input.set shadowJar.archiveFile +} diff --git a/quilt/gradle.properties b/quilt/gradle.properties new file mode 100644 index 0000000..56fe802 --- /dev/null +++ b/quilt/gradle.properties @@ -0,0 +1 @@ +loom.platform = quilt diff --git a/quilt/src/main/java/mod/azure/azurelib/fabric/ClientListener.java b/quilt/src/main/java/mod/azure/azurelib/fabric/ClientListener.java new file mode 100644 index 0000000..b8c8ca3 --- /dev/null +++ b/quilt/src/main/java/mod/azure/azurelib/fabric/ClientListener.java @@ -0,0 +1,32 @@ +package mod.azure.azurelib.fabric; + +import com.mojang.blaze3d.platform.InputConstants; +import mod.azure.azurelib.common.api.client.helper.ClientUtils; +import mod.azure.azurelib.common.internal.common.util.IncompatibleModsCheck; +import mod.azure.azurelib.common.platform.Services; +import mod.azure.azurelib.fabric.network.Networking; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; +import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; +import net.minecraft.client.KeyMapping; +import org.lwjgl.glfw.GLFW; + +public final class ClientListener implements ClientModInitializer { + + @Override + public void onInitializeClient() { + ClientLifecycleEvents.CLIENT_STARTED.register(IncompatibleModsCheck::warnings); + ClientUtils.RELOAD = new KeyMapping("key.azurelib.reload", InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_R, + "category.azurelib.binds"); + KeyBindingHelper.registerKeyBinding(ClientUtils.RELOAD); + ClientUtils.SCOPE = new KeyMapping("key.azurelib.scope", InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_LEFT_ALT, + "category.azurelib.binds"); + KeyBindingHelper.registerKeyBinding(ClientUtils.SCOPE); + ClientUtils.FIRE_WEAPON = new KeyMapping("key.azurelib.fire", InputConstants.Type.KEYSYM, + GLFW.GLFW_KEY_UNKNOWN, + "category.azurelib.binds"); + KeyBindingHelper.registerKeyBinding(ClientUtils.FIRE_WEAPON); + Networking.PacketRegistry.registerClient(); + Services.NETWORK.registerClientReceiverPackets(); + } +} diff --git a/quilt/src/main/java/mod/azure/azurelib/fabric/event/QuiltGeoRenderPhaseEvent.java b/quilt/src/main/java/mod/azure/azurelib/fabric/event/QuiltGeoRenderPhaseEvent.java new file mode 100644 index 0000000..16aabcd --- /dev/null +++ b/quilt/src/main/java/mod/azure/azurelib/fabric/event/QuiltGeoRenderPhaseEvent.java @@ -0,0 +1,31 @@ +package mod.azure.azurelib.fabric.event; + +import mod.azure.azurelib.common.internal.common.event.GeoRenderEvent; +import mod.azure.azurelib.common.platform.services.GeoRenderPhaseEventFactory; +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; + +/** + * @author Boston Vanseghi + */ +public class QuiltGeoRenderPhaseEvent implements GeoRenderPhaseEventFactory.GeoRenderPhaseEvent { + + @FunctionalInterface + interface Listener { + boolean handle(GeoRenderEvent event); + } + + private final Event event = EventFactory.createArrayBacked(Listener.class, event -> true, listeners -> event -> { + for (Listener listener : listeners) { + if (!listener.handle(event)) + return false; + } + + return true; + }); + + @Override + public boolean handle(GeoRenderEvent geoRenderEvent) { + return this.event.invoker().handle(geoRenderEvent); + } +} diff --git a/quilt/src/main/java/mod/azure/azurelib/fabric/network/Networking.java b/quilt/src/main/java/mod/azure/azurelib/fabric/network/Networking.java new file mode 100644 index 0000000..b5136d5 --- /dev/null +++ b/quilt/src/main/java/mod/azure/azurelib/fabric/network/Networking.java @@ -0,0 +1,64 @@ +package mod.azure.azurelib.fabric.network; + +import java.lang.reflect.InvocationTargetException; +import java.util.function.BiConsumer; + +import mod.azure.azurelib.common.internal.common.AzureLib; +import mod.azure.azurelib.common.internal.common.AzureLibException; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.MarkerManager; + +import io.netty.buffer.Unpooled; +import mod.azure.azurelib.fabric.network.api.IClientPacket; +import mod.azure.azurelib.common.internal.common.network.api.IPacket; +import mod.azure.azurelib.common.internal.common.network.api.IPacketDecoder; +import mod.azure.azurelib.common.internal.common.network.api.IPacketEncoder; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; + +public final class Networking { + + public static final Marker MARKER = MarkerManager.getMarker("Network"); + + public static void sendClientPacket(ServerPlayer target, IClientPacket packet) { + dispatch(packet, (packetId, buffer) -> ServerPlayNetworking.send(target, packetId, buffer)); + } + + private static void dispatch(IPacket packet, BiConsumer dispatcher) { + ResourceLocation packetId = packet.getPacketId(); + FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.buffer()); + IPacketEncoder encoder = packet.getEncoder(); + T data = packet.getPacketData(); + encoder.encode(data, buffer); + dispatcher.accept(packetId, buffer); + } + + public static final class PacketRegistry { + + public static void registerClient() { + registerServer2ClientReceiver(S2C_SendConfigData.class); + } + + @Environment(EnvType.CLIENT) + private static void registerServer2ClientReceiver(Class> clientPacketClass) { + try { + IClientPacket packet = clientPacketClass.getDeclaredConstructor().newInstance(); + ResourceLocation packetId = packet.getPacketId(); + ClientPlayNetworking.registerGlobalReceiver(packetId, (client, handler, buffer, responseDispatcher) -> { + IPacketDecoder decoder = packet.getDecoder(); + T packetData = decoder.decode(buffer); + client.execute(() -> packet.handleClientsidePacket(client, handler, packetData, responseDispatcher)); + }); + } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | + IllegalAccessException exc) { + AzureLib.LOGGER.fatal(MARKER, "Couldn't instantiate new client packet from class {}, make sure it declares public default constructor", clientPacketClass.getSimpleName()); + throw new AzureLibException(exc); + } + } + } +} diff --git a/quilt/src/main/java/mod/azure/azurelib/fabric/network/S2C_SendConfigData.java b/quilt/src/main/java/mod/azure/azurelib/fabric/network/S2C_SendConfigData.java new file mode 100644 index 0000000..aa9d0af --- /dev/null +++ b/quilt/src/main/java/mod/azure/azurelib/fabric/network/S2C_SendConfigData.java @@ -0,0 +1,100 @@ +package mod.azure.azurelib.fabric.network; + +import java.util.Map; + +import mod.azure.azurelib.common.internal.common.AzureLib; +import mod.azure.azurelib.common.internal.common.AzureLibException; +import mod.azure.azurelib.common.internal.common.config.ConfigHolder; +import mod.azure.azurelib.common.internal.common.config.adapter.TypeAdapter; +import mod.azure.azurelib.common.internal.common.config.value.ConfigValue; +import mod.azure.azurelib.fabric.network.api.IClientPacket; +import mod.azure.azurelib.common.internal.common.network.api.IPacketDecoder; +import mod.azure.azurelib.common.internal.common.network.api.IPacketEncoder; +import net.fabricmc.fabric.api.networking.v1.PacketSender; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; + +public class S2C_SendConfigData implements IClientPacket { + + public static final ResourceLocation IDENTIFIER = AzureLib.modResource("s2c_send_config_data"); + + private final ConfigData config; + + S2C_SendConfigData() { + this.config = null; + } + + public S2C_SendConfigData(String config) { + this.config = new ConfigData(config); + } + + @Override + public ResourceLocation getPacketId() { + return IDENTIFIER; + } + + @Override + public ConfigData getPacketData() { + return config; + } + + @Override + public IPacketEncoder getEncoder() { + return (configData, buffer) -> { + buffer.writeUtf(configData.configId); + ConfigHolder.getConfig(configData.configId).ifPresent(data -> { + Map> serialized = data.getNetworkSerializedFields(); + buffer.writeInt(serialized.size()); + for (Map.Entry> entry : serialized.entrySet()) { + String id = entry.getKey(); + ConfigValue value = entry.getValue(); + TypeAdapter adapter = value.getAdapter(); + buffer.writeUtf(id); + adapter.encodeToBuffer(value, buffer); + } + }); + }; + } + + @Override + public IPacketDecoder getDecoder() { + return buffer -> { + String config = buffer.readUtf(); + int i = buffer.readInt(); + ConfigHolder.getConfig(config).ifPresent(data -> { + Map> serialized = data.getNetworkSerializedFields(); + for (int j = 0; j < i; j++) { + String fieldId = buffer.readUtf(); + ConfigValue value = serialized.get(fieldId); + if (value == null) { + AzureLib.LOGGER.fatal(Networking.MARKER, "Received unknown config value " + fieldId); + throw new AzureLibException("Unknown config field: " + fieldId); + } + setValue(value, buffer); + } + }); + return new ConfigData(config); + }; + } + + @Override + public void handleClientsidePacket(Minecraft client, ClientPacketListener listener, ConfigData packetData, PacketSender dispatcher) { + } + + private void setValue(ConfigValue value, FriendlyByteBuf buffer) { + TypeAdapter adapter = value.getAdapter(); + V v = (V) adapter.decodeFromBuffer(value, buffer); + value.set(v); + } + + static final class ConfigData { + + private final String configId; + + public ConfigData(String configId) { + this.configId = configId; + } + } +} diff --git a/quilt/src/main/java/mod/azure/azurelib/fabric/network/api/IClientPacket.java b/quilt/src/main/java/mod/azure/azurelib/fabric/network/api/IClientPacket.java new file mode 100644 index 0000000..4af4879 --- /dev/null +++ b/quilt/src/main/java/mod/azure/azurelib/fabric/network/api/IClientPacket.java @@ -0,0 +1,14 @@ +package mod.azure.azurelib.fabric.network.api; + +import mod.azure.azurelib.common.internal.common.network.api.IPacket; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.fabricmc.fabric.api.networking.v1.PacketSender; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientPacketListener; + +public interface IClientPacket extends IPacket { + + @Environment(EnvType.CLIENT) + void handleClientsidePacket(Minecraft client, ClientPacketListener listener, T packetData, PacketSender dispatcher); +} diff --git a/quilt/src/main/java/mod/azure/azurelib/fabric/platform/QuiltAzureLibInitializer.java b/quilt/src/main/java/mod/azure/azurelib/fabric/platform/QuiltAzureLibInitializer.java new file mode 100644 index 0000000..c679c14 --- /dev/null +++ b/quilt/src/main/java/mod/azure/azurelib/fabric/platform/QuiltAzureLibInitializer.java @@ -0,0 +1,36 @@ +package mod.azure.azurelib.fabric.platform; + +import mod.azure.azurelib.common.internal.common.AzureLib; +import mod.azure.azurelib.common.internal.common.cache.AzureLibCache; +import mod.azure.azurelib.common.platform.services.AzureLibInitializer; +import net.fabricmc.fabric.api.resource.IdentifiableResourceReloadListener; +import net.fabricmc.fabric.api.resource.ResourceManagerHelper; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.PackType; +import net.minecraft.server.packs.resources.PreparableReloadListener; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.util.profiling.ProfilerFiller; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +public class QuiltAzureLibInitializer implements AzureLibInitializer { + @Override + public void initialize() { + ResourceManagerHelper.get(PackType.CLIENT_RESOURCES) + .registerReloadListener(new IdentifiableResourceReloadListener() { + @Override + public ResourceLocation getFabricId() { + return AzureLib.modResource("models"); + } + + @Override + public CompletableFuture reload(PreparableReloadListener.PreparationBarrier synchronizer, ResourceManager manager, + ProfilerFiller prepareProfiler, ProfilerFiller applyProfiler, Executor prepareExecutor, + Executor applyExecutor) { + return AzureLibCache.reload(synchronizer, manager, prepareProfiler, + applyProfiler, prepareExecutor, applyExecutor); + } + }); + } +} \ No newline at end of file diff --git a/quilt/src/main/java/mod/azure/azurelib/fabric/platform/QuiltAzureLibNetwork.java b/quilt/src/main/java/mod/azure/azurelib/fabric/platform/QuiltAzureLibNetwork.java new file mode 100644 index 0000000..5e18db0 --- /dev/null +++ b/quilt/src/main/java/mod/azure/azurelib/fabric/platform/QuiltAzureLibNetwork.java @@ -0,0 +1,101 @@ +package mod.azure.azurelib.fabric.platform; + +import mod.azure.azurelib.common.internal.common.network.AbstractPacket; +import mod.azure.azurelib.common.internal.common.network.packet.*; +import mod.azure.azurelib.common.platform.services.AzureLibNetwork; +import mod.azure.azurelib.fabric.network.Networking; +import mod.azure.azurelib.fabric.network.S2C_SendConfigData; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; +import net.fabricmc.fabric.api.networking.v1.PlayerLookup; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.client.Minecraft; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.Packet; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; + +public class QuiltAzureLibNetwork implements AzureLibNetwork { + + private void handlePacket(Minecraft client, AbstractPacket packet) { + client.execute(packet::handle); + } + + @Override + public void registerClientReceiverPackets() { + ClientPlayNetworking.registerGlobalReceiver(ANIM_DATA_SYNC_PACKET_ID, + (client, handler, buf, responseSender) -> this.handlePacket(client, AnimDataSyncPacket.receive(buf))); + ClientPlayNetworking.registerGlobalReceiver(ANIM_TRIGGER_SYNC_PACKET_ID, + (client, handler, buf, responseSender) -> this.handlePacket(client, AnimTriggerPacket.receive(buf))); + + ClientPlayNetworking.registerGlobalReceiver(ENTITY_ANIM_DATA_SYNC_PACKET_ID, + (client, handler, buf, responseSender) -> this.handlePacket(client, + EntityAnimDataSyncPacket.receive(buf))); + ClientPlayNetworking.registerGlobalReceiver(ENTITY_ANIM_TRIGGER_SYNC_PACKET_ID, + (client, handler, buf, responseSender) -> this.handlePacket(client, + EntityAnimTriggerPacket.receive(buf))); + + ClientPlayNetworking.registerGlobalReceiver(BLOCK_ENTITY_ANIM_DATA_SYNC_PACKET_ID, + (client, handler, buf, responseSender) -> this.handlePacket(client, + BlockEntityAnimDataSyncPacket.receive(buf))); + ClientPlayNetworking.registerGlobalReceiver(BLOCK_ENTITY_ANIM_TRIGGER_SYNC_PACKET_ID, + (client, handler, buf, responseSender) -> this.handlePacket(client, + BlockEntityAnimTriggerPacket.receive(buf))); + + ClientPlayNetworking.registerGlobalReceiver(CUSTOM_ENTITY_ID, + (client, handler, buf, responseSender) -> EntityPacketOnClient.onPacket(client, buf)); + } + + @Override + public Packet createPacket(Entity entity) { + FriendlyByteBuf buf = createFriendlyByteBuf(); + buf.writeVarInt(BuiltInRegistries.ENTITY_TYPE.getId(entity.getType())); + buf.writeUUID(entity.getUUID()); + buf.writeVarInt(entity.getId()); + buf.writeDouble(entity.getX()); + buf.writeDouble(entity.getY()); + buf.writeDouble(entity.getZ()); + buf.writeByte(Mth.floor(entity.getXRot() * 256.0F / 360.0F)); + buf.writeByte(Mth.floor(entity.getYRot() * 256.0F / 360.0F)); + buf.writeFloat(entity.getXRot()); + buf.writeFloat(entity.getYRot()); + return ServerPlayNetworking.createS2CPacket(AzureLibNetwork.CUSTOM_ENTITY_ID, buf); + } + + public FriendlyByteBuf createFriendlyByteBuf() { + return PacketByteBufs.create(); + } + + @Override + public void sendToTrackingEntityAndSelf(AbstractPacket packet, Entity entityToTrack) { + for (ServerPlayer trackingPlayer : PlayerLookup.tracking(entityToTrack)) { + FriendlyByteBuf buf = createFriendlyByteBuf(); + packet.write(buf); + ServerPlayNetworking.send(trackingPlayer, packet.id(), buf); + } + + if (entityToTrack instanceof ServerPlayer serverPlayer) { + FriendlyByteBuf buf = createFriendlyByteBuf(); + packet.write(buf); + ServerPlayNetworking.send(serverPlayer, packet.id(), buf); + } + } + + @Override + public void sendToEntitiesTrackingChunk(AbstractPacket packet, ServerLevel level, BlockPos blockPos) { + for (ServerPlayer trackingPlayer : PlayerLookup.tracking(level, blockPos)) { + FriendlyByteBuf buf = createFriendlyByteBuf(); + packet.write(buf); + ServerPlayNetworking.send(trackingPlayer, packet.id(), buf); + } + } + + @Override + public void sendClientPacket(ServerPlayer player, String id) { + Networking.sendClientPacket(player, new S2C_SendConfigData(id)); + } +} diff --git a/quilt/src/main/java/mod/azure/azurelib/fabric/platform/QuiltGeoRenderPhaseEventFactory.java b/quilt/src/main/java/mod/azure/azurelib/fabric/platform/QuiltGeoRenderPhaseEventFactory.java new file mode 100644 index 0000000..e32ca5d --- /dev/null +++ b/quilt/src/main/java/mod/azure/azurelib/fabric/platform/QuiltGeoRenderPhaseEventFactory.java @@ -0,0 +1,14 @@ +package mod.azure.azurelib.fabric.platform; + +import mod.azure.azurelib.fabric.event.QuiltGeoRenderPhaseEvent; +import mod.azure.azurelib.common.platform.services.GeoRenderPhaseEventFactory; + +/** + * @author Boston Vanseghi + */ +public class QuiltGeoRenderPhaseEventFactory implements GeoRenderPhaseEventFactory { + @Override + public GeoRenderPhaseEvent create() { + return new QuiltGeoRenderPhaseEvent(); + } +} diff --git a/quilt/src/main/java/mod/azure/azurelib/fabric/platform/QuiltPlatformHelper.java b/quilt/src/main/java/mod/azure/azurelib/fabric/platform/QuiltPlatformHelper.java new file mode 100644 index 0000000..491a283 --- /dev/null +++ b/quilt/src/main/java/mod/azure/azurelib/fabric/platform/QuiltPlatformHelper.java @@ -0,0 +1,62 @@ +package mod.azure.azurelib.fabric.platform; + +import mod.azure.azurelib.common.internal.common.blocks.TickingLightBlock; +import mod.azure.azurelib.common.internal.common.blocks.TickingLightEntity; +import mod.azure.azurelib.common.platform.services.IPlatformHelper; +import net.fabricmc.api.EnvType; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.world.item.enchantment.Enchantment; +import net.minecraft.world.level.block.entity.BlockEntityType; +import org.Vrglab.AzureLib.quilt.VlAzureLibModQuilt; + +import java.nio.file.Path; + +public class QuiltPlatformHelper implements IPlatformHelper { + + @Override + public String getPlatformName() { + return "Fabric"; + } + + @Override + public boolean isModLoaded(String modId) { + + return FabricLoader.getInstance().isModLoaded(modId); + } + + @Override + public boolean isDevelopmentEnvironment() { + + return FabricLoader.getInstance().isDevelopmentEnvironment(); + } + + @Override + public Path getGameDir() { + return FabricLoader.getInstance().getGameDir(); + } + + @Override + public boolean isServerEnvironment() { + return FabricLoader.getInstance().getEnvironmentType() == EnvType.SERVER; + } + + @Override + public TickingLightBlock getTickingLightBlock() { + return VlAzureLibModQuilt.TICKING_LIGHT_BLOCK; + } + + @Override + public BlockEntityType getTickingLightEntity() { + return VlAzureLibModQuilt.TICKING_LIGHT_ENTITY; + } + + @Override + public Enchantment getIncendairyenchament() { + return VlAzureLibModQuilt.INCENDIARYENCHANTMENT; + } + + @Override + public Path modsDir() { + return FabricLoader.getInstance().getGameDir().resolve("mods"); + } +} \ No newline at end of file diff --git a/quilt/src/main/java/org/Vrglab/AzureLib/quilt/VlAzureLibModQuilt.java b/quilt/src/main/java/org/Vrglab/AzureLib/quilt/VlAzureLibModQuilt.java new file mode 100644 index 0000000..a2ec731 --- /dev/null +++ b/quilt/src/main/java/org/Vrglab/AzureLib/quilt/VlAzureLibModQuilt.java @@ -0,0 +1,49 @@ +package org.Vrglab.AzureLib.quilt; + +import mod.azure.azurelib.common.api.common.enchantments.IncendiaryEnchantment; +import mod.azure.azurelib.common.internal.common.AzureLib; +import mod.azure.azurelib.common.internal.common.AzureLibMod; +import mod.azure.azurelib.common.internal.common.blocks.TickingLightBlock; +import mod.azure.azurelib.common.internal.common.blocks.TickingLightEntity; +import mod.azure.azurelib.common.internal.common.config.AzureLibConfig; +import mod.azure.azurelib.common.internal.common.config.format.ConfigFormats; +import mod.azure.azurelib.common.internal.common.config.io.ConfigIO; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.object.builder.v1.block.entity.FabricBlockEntityTypeBuilder; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.item.enchantment.Enchantment; +import net.minecraft.world.level.block.SoundType; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.material.PushReaction; +import org.Vrglab.AzureLib.VlAzureLibMod; +import org.Vrglab.fabriclike.VLModFabricLike; +import org.quiltmc.loader.api.ModContainer; +import org.quiltmc.qsl.base.api.entrypoint.ModInitializer; + +import org.Vrglab.AzureLib.fabriclike.VlAzureLibModFabricLike; + +public final class VlAzureLibModQuilt implements ModInitializer { + public static BlockEntityType TICKING_LIGHT_ENTITY; + public static final TickingLightBlock TICKING_LIGHT_BLOCK = new TickingLightBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).lightLevel(TickingLightBlock.litBlockEmission(15)).pushReaction(PushReaction.DESTROY).noOcclusion()); + public static final Enchantment INCENDIARYENCHANTMENT = new IncendiaryEnchantment(Enchantment.Rarity.RARE, EquipmentSlot.MAINHAND); + + @Override + public void onInitialize(ModContainer mod) { + ConfigIO.FILE_WATCH_MANAGER.startService(); + VlAzureLibMod.init(); + Registry.register(BuiltInRegistries.BLOCK, AzureLib.modResource("lightblock"), VlAzureLibModQuilt.TICKING_LIGHT_BLOCK); + VlAzureLibModQuilt.TICKING_LIGHT_ENTITY = Registry.register(BuiltInRegistries.BLOCK_ENTITY_TYPE, AzureLib.MOD_ID + ":lightblock", FabricBlockEntityTypeBuilder.create(TickingLightEntity::new, VlAzureLibModQuilt.TICKING_LIGHT_BLOCK).build(null)); + AzureLibMod.config = AzureLibMod.registerConfig(AzureLibConfig.class, ConfigFormats.json()).getConfigInstance(); + ServerLifecycleEvents.SERVER_STOPPING.register((server) -> { + ConfigIO.FILE_WATCH_MANAGER.stopService(); + }); + Registry.register(BuiltInRegistries.ENCHANTMENT, AzureLib.modResource("incendiaryenchantment"), INCENDIARYENCHANTMENT); + + VLModFabricLike.init(VlAzureLibMod.MOD_ID, ()->{ + VlAzureLibModFabricLike.init(); + }); + } +} diff --git a/quilt/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.AzureLibInitializer b/quilt/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.AzureLibInitializer new file mode 100644 index 0000000..51b476f --- /dev/null +++ b/quilt/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.AzureLibInitializer @@ -0,0 +1 @@ +mod.azure.azurelib.fabric.platform.QuiltAzureLibInitializer \ No newline at end of file diff --git a/quilt/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.AzureLibNetwork b/quilt/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.AzureLibNetwork new file mode 100644 index 0000000..6f3c7ae --- /dev/null +++ b/quilt/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.AzureLibNetwork @@ -0,0 +1 @@ +mod.azure.azurelib.fabric.platform.QuiltAzureLibNetwork \ No newline at end of file diff --git a/quilt/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.GeoRenderPhaseEventFactory b/quilt/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.GeoRenderPhaseEventFactory new file mode 100644 index 0000000..4a539e9 --- /dev/null +++ b/quilt/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.GeoRenderPhaseEventFactory @@ -0,0 +1 @@ +mod.azure.azurelib.fabric.platform.QuiltGeoRenderPhaseEventFactory \ No newline at end of file diff --git a/quilt/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.IPlatformHelper b/quilt/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.IPlatformHelper new file mode 100644 index 0000000..e3425ca --- /dev/null +++ b/quilt/src/main/resources/META-INF/services/mod.azure.azurelib.common.platform.services.IPlatformHelper @@ -0,0 +1 @@ +mod.azure.azurelib.fabric.platform.QuiltPlatformHelper \ No newline at end of file diff --git a/quilt/src/main/resources/quilt.mod.json b/quilt/src/main/resources/quilt.mod.json new file mode 100644 index 0000000..96f7128 --- /dev/null +++ b/quilt/src/main/resources/quilt.mod.json @@ -0,0 +1,47 @@ +{ + "schema_version": 1, + "quilt_loader": { + "group": "${group}", + "id": "vrglabs_azurelib", + "version": "${mod_version}", + "metadata": { + "name": "Vrglabs AzureLib", + "description": "Utility classes for AzureLib for using with VrglabsLib", + "contributors": { + "Vrglab": "Author" + }, + "icon": "logo.png" + }, + "intermediate_mappings": "net.fabricmc:intermediary", + "entrypoints": { + "init": [ + "org.Vrglab.AzureLib.quilt.VlAzureLibModQuilt" + ], + "client": [ + "mod.azure.azurelib.fabric.ClientListener" + ] + }, + "depends": [ + { + "id": "quilt_loader", + "version": "*" + }, + { + "id": "quilt_base", + "version": "*" + }, + { + "id": "minecraft", + "version": ">=1.20.4" + }, + { + "id": "architectury", + "version": ">=11.1.17" + } + ] + }, + "mixin": [ + "vrglabs_azurelib.mixins.json" + ], + "accessWidener": "vrglabs_azurelib.aw" +} diff --git a/quilt/src/main/resources/vrglabs_azurelib.mixins.json b/quilt/src/main/resources/vrglabs_azurelib.mixins.json new file mode 100644 index 0000000..4f33aa9 --- /dev/null +++ b/quilt/src/main/resources/vrglabs_azurelib.mixins.json @@ -0,0 +1,19 @@ +{ + "required": true, + "package": "mod.azure.azurelib.common.internal.mixins", + "compatibilityLevel": "JAVA_19", + "client": [ + "FabricMixinHumanoidArmorLayer", + "ItemRendererAccessor", + "MinecraftMixin", + "MixinItemRenderer", + "TextureManagerMixin", + "AccessorWarningScreen" + ], + "mixins": [ + "PlayerListMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..076a348 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + maven { url "https://maven.fabricmc.net/" } + maven { url "https://maven.architectury.dev/" } + maven { url "https://files.minecraftforge.net/maven/" } + gradlePluginPortal() + } +} + +rootProject.name = 'vrglabs_azurelib' + +include 'common' +include 'fabric' +include 'fabric-like' +include 'neoforge' +include 'quilt'