From f44043bb0bd733bf699d3c5c25a7bef0b1d7a132 Mon Sep 17 00:00:00 2001 From: octarine-noise Date: Mon, 28 Dec 2015 11:49:46 +0100 Subject: [PATCH] first Kotlin version --- .gitignore | 7 + build.gradle | 68 ++++ settings.gradle | 2 + .../mods/betterfoliage/BetterFoliageMod.kt | 60 ++++ .../mods/betterfoliage/client/Client.kt | 75 +++++ .../kotlin/mods/betterfoliage/client/Hooks.kt | 58 ++++ .../client/config/BlockMatcher.kt | 88 +++++ .../betterfoliage/client/config/Config.kt | 183 +++++++++++ .../client/gui/BiomeListConfigEntry.kt | 19 ++ .../client/gui/ConfigGuiFactory.kt | 27 ++ .../client/integration/CLCIntegration.kt | 19 ++ .../integration/ShadersModIntegration.kt | 53 +++ .../client/integration/TFCIntegration.kt | 37 +++ .../client/render/AbstractRenderColumn.kt | 301 ++++++++++++++++++ .../client/render/EntityFallingLeavesFX.kt | 131 ++++++++ .../client/render/EntityRisingSoulFX.kt | 75 +++++ .../client/render/ModelColumn.kt | 140 ++++++++ .../client/render/RenderAlgae.kt | 49 +++ .../client/render/RenderCactus.kt | 83 +++++ .../client/render/RenderConnectedGrass.kt | 23 ++ .../client/render/RenderConnectedGrassLog.kt | 31 ++ .../client/render/RenderCoral.kt | 65 ++++ .../client/render/RenderGrass.kt | 106 ++++++ .../client/render/RenderLeaves.kt | 72 +++++ .../client/render/RenderLilypad.kt | 64 ++++ .../betterfoliage/client/render/RenderLog.kt | 30 ++ .../client/render/RenderMycelium.kt | 48 +++ .../client/render/RenderNetherrack.kt | 51 +++ .../client/render/RenderReeds.kt | 65 ++++ .../mods/betterfoliage/client/render/Utils.kt | 54 ++++ .../client/texture/GrassGenerator.kt | 35 ++ .../client/texture/GrassRegistry.kt | 69 ++++ .../client/texture/LeafGenerator.kt | 77 +++++ .../client/texture/LeafRegistry.kt | 94 ++++++ .../client/texture/TextureMatcher.kt | 35 ++ .../betterfoliage/client/texture/Utils.kt | 11 + .../betterfoliage/loader/BetterFoliageCore.kt | 106 ++++++ .../kotlin/mods/betterfoliage/loader/Refs.kt | 71 +++++ src/main/kotlin/mods/octarinecore/Utils.kt | 70 ++++ .../mods/octarinecore/client/KeyHandler.kt | 22 ++ .../client/gui/IdListConfigEntry.kt | 58 ++++ .../client/gui/NonVerboseArrayEntry.kt | 25 ++ .../mods/octarinecore/client/gui/Utils.kt | 13 + .../render/AbstractBlockRenderingHandler.kt | 160 ++++++++++ .../client/render/AbstractEntityFX.kt | 98 ++++++ .../octarinecore/client/render/Geometry.kt | 214 +++++++++++++ .../mods/octarinecore/client/render/Model.kt | 138 ++++++++ .../client/render/ModelRenderer.kt | 138 ++++++++ .../client/render/OffsetBlockAccess.kt | 68 ++++ .../octarinecore/client/render/PixelFormat.kt | 56 ++++ .../client/render/RenderBlocks.kt | 147 +++++++++ .../octarinecore/client/render/Shaders.kt | 148 +++++++++ .../octarinecore/client/render/Shading.kt | 131 ++++++++ .../resource/CenteringTextureGenerator.kt | 33 ++ .../client/resource/ResourceGeneration.kt | 126 ++++++++ .../client/resource/ResourceHandler.kt | 126 ++++++++ .../client/resource/TextureGenerator.kt | 88 +++++ .../octarinecore/client/resource/Utils.kt | 90 ++++++ .../octarinecore/config/DelegatingConfig.kt | 232 ++++++++++++++ .../mods/octarinecore/metaprog/Reflection.kt | 166 ++++++++++ .../octarinecore/metaprog/Transformation.kt | 199 ++++++++++++ .../assets/betterfoliage/CactusDefault.cfg | 5 + .../assets/betterfoliage/CropDefault.cfg | 35 ++ .../assets/betterfoliage/DirtDefault.cfg | 14 + .../assets/betterfoliage/GrassDefault.cfg | 19 ++ .../assets/betterfoliage/LeavesDefault.cfg | 8 + .../assets/betterfoliage/LilypadDefault.cfg | 5 + .../assets/betterfoliage/LogDefault.cfg | 23 ++ .../assets/betterfoliage/SandDefault.cfg | 5 + .../assets/betterfoliage/lang/en_US.lang | 212 ++++++++++++ .../blocks/falling_leaf_default_0.png | Bin 0 -> 281 bytes .../blocks/falling_leaf_default_1.png | Bin 0 -> 275 bytes .../blocks/falling_leaf_default_2.png | Bin 0 -> 273 bytes .../blocks/falling_leaf_default_3.png | Bin 0 -> 254 bytes .../textures/blocks/falling_leaf_jungle_0.png | Bin 0 -> 286 bytes .../textures/blocks/falling_leaf_jungle_1.png | Bin 0 -> 265 bytes .../textures/blocks/falling_leaf_jungle_2.png | Bin 0 -> 274 bytes .../textures/blocks/falling_leaf_jungle_3.png | Bin 0 -> 278 bytes .../textures/blocks/falling_leaf_spruce_0.png | Bin 0 -> 255 bytes .../textures/blocks/falling_leaf_spruce_1.png | Bin 0 -> 244 bytes .../textures/blocks/falling_leaf_spruce_2.png | Bin 0 -> 231 bytes .../textures/blocks/falling_leaf_spruce_3.png | Bin 0 -> 236 bytes .../textures/blocks/leafmask_32_default.png | Bin 0 -> 219 bytes .../textures/blocks/leafmask_32_fine.png | Bin 0 -> 305 bytes .../textures/blocks/leafmask_32_full.png | Bin 0 -> 176 bytes .../textures/blocks/leafmask_32_jumbled.png | Bin 0 -> 345 bytes .../textures/blocks/missing_leaf.png | Bin 0 -> 102 bytes .../textures/blocks/better_algae_0.png | Bin 0 -> 2986 bytes .../textures/blocks/better_algae_1.png | Bin 0 -> 3034 bytes .../textures/blocks/better_algae_2.png | Bin 0 -> 3045 bytes .../textures/blocks/better_algae_3.png | Bin 0 -> 3177 bytes .../textures/blocks/better_cactus.png | Bin 0 -> 4265 bytes .../textures/blocks/better_cactus_arm_0.png | Bin 0 -> 2963 bytes .../textures/blocks/better_cactus_arm_1.png | Bin 0 -> 3011 bytes .../textures/blocks/better_coral_0.png | Bin 0 -> 3033 bytes .../textures/blocks/better_coral_1.png | Bin 0 -> 3037 bytes .../textures/blocks/better_coral_2.png | Bin 0 -> 2973 bytes .../textures/blocks/better_coral_3.png | Bin 0 -> 3020 bytes .../textures/blocks/better_coral_4.png | Bin 0 -> 3054 bytes .../textures/blocks/better_coral_5.png | Bin 0 -> 2982 bytes .../textures/blocks/better_crust_0.png | Bin 0 -> 3420 bytes .../textures/blocks/better_crust_1.png | Bin 0 -> 3760 bytes .../textures/blocks/better_crust_2.png | Bin 0 -> 4131 bytes .../textures/blocks/better_crust_3.png | Bin 0 -> 4092 bytes .../textures/blocks/better_grass_long_0.png | Bin 0 -> 3137 bytes .../textures/blocks/better_grass_long_1.png | Bin 0 -> 3125 bytes .../textures/blocks/better_grass_long_2.png | Bin 0 -> 3023 bytes .../textures/blocks/better_grass_long_3.png | Bin 0 -> 3083 bytes .../textures/blocks/better_grass_long_4.png | Bin 0 -> 3069 bytes .../textures/blocks/better_grass_short_0.png | Bin 0 -> 2959 bytes .../textures/blocks/better_grass_short_1.png | Bin 0 -> 2979 bytes .../textures/blocks/better_grass_short_2.png | Bin 0 -> 2947 bytes .../textures/blocks/better_grass_side_0.png | Bin 0 -> 3294 bytes .../textures/blocks/better_grass_side_1.png | Bin 0 -> 3306 bytes .../textures/blocks/better_grass_snowed_0.png | Bin 0 -> 3137 bytes .../textures/blocks/better_grass_snowed_1.png | Bin 0 -> 3095 bytes .../blocks/better_leaves_snowed_0.png | Bin 0 -> 3382 bytes .../blocks/better_leaves_snowed_1.png | Bin 0 -> 3466 bytes .../blocks/better_leaves_snowed_2.png | Bin 0 -> 3422 bytes .../blocks/better_lilypad_flower_0.png | Bin 0 -> 2917 bytes .../blocks/better_lilypad_flower_1.png | Bin 0 -> 3004 bytes .../blocks/better_lilypad_roots_0.png | Bin 0 -> 2959 bytes .../blocks/better_lilypad_roots_1.png | Bin 0 -> 2912 bytes .../blocks/better_lilypad_roots_2.png | Bin 0 -> 2930 bytes .../textures/blocks/better_mycel_0.png | Bin 0 -> 3018 bytes .../textures/blocks/better_mycel_1.png | Bin 0 -> 3020 bytes .../textures/blocks/better_mycel_2.png | Bin 0 -> 3003 bytes .../textures/blocks/better_mycel_3.png | Bin 0 -> 2977 bytes .../textures/blocks/better_mycel_side_0.png | Bin 0 -> 3221 bytes .../textures/blocks/better_mycel_side_1.png | Bin 0 -> 3116 bytes .../textures/blocks/better_netherrack_0.png | Bin 0 -> 3547 bytes .../textures/blocks/better_netherrack_1.png | Bin 0 -> 3199 bytes .../textures/blocks/better_netherrack_2.png | Bin 0 -> 3456 bytes .../textures/blocks/better_reed_0.png | Bin 0 -> 3728 bytes .../textures/blocks/better_reed_1.png | Bin 0 -> 3564 bytes .../textures/blocks/better_reed_2.png | Bin 0 -> 3711 bytes .../textures/blocks/better_reed_3.png | Bin 0 -> 3788 bytes .../textures/blocks/rising_soul_0.png | Bin 0 -> 3058 bytes .../textures/blocks/rising_soul_1.png | Bin 0 -> 3047 bytes .../textures/blocks/soul_track.png | Bin 0 -> 3161 bytes .../textures/blocks/soul_track.png.mcmeta | 1 + src/main/resources/mcmod.info | 8 + src/main/resources/pack.mcmeta | 6 + 143 files changed, 5469 insertions(+) create mode 100644 .gitignore create mode 100644 build.gradle create mode 100644 settings.gradle create mode 100644 src/main/kotlin/mods/betterfoliage/BetterFoliageMod.kt create mode 100644 src/main/kotlin/mods/betterfoliage/client/Client.kt create mode 100644 src/main/kotlin/mods/betterfoliage/client/Hooks.kt create mode 100644 src/main/kotlin/mods/betterfoliage/client/config/BlockMatcher.kt create mode 100644 src/main/kotlin/mods/betterfoliage/client/config/Config.kt create mode 100644 src/main/kotlin/mods/betterfoliage/client/gui/BiomeListConfigEntry.kt create mode 100644 src/main/kotlin/mods/betterfoliage/client/gui/ConfigGuiFactory.kt create mode 100644 src/main/kotlin/mods/betterfoliage/client/integration/CLCIntegration.kt create mode 100644 src/main/kotlin/mods/betterfoliage/client/integration/ShadersModIntegration.kt create mode 100644 src/main/kotlin/mods/betterfoliage/client/integration/TFCIntegration.kt create mode 100644 src/main/kotlin/mods/betterfoliage/client/render/AbstractRenderColumn.kt create mode 100644 src/main/kotlin/mods/betterfoliage/client/render/EntityFallingLeavesFX.kt create mode 100644 src/main/kotlin/mods/betterfoliage/client/render/EntityRisingSoulFX.kt create mode 100644 src/main/kotlin/mods/betterfoliage/client/render/ModelColumn.kt create mode 100644 src/main/kotlin/mods/betterfoliage/client/render/RenderAlgae.kt create mode 100644 src/main/kotlin/mods/betterfoliage/client/render/RenderCactus.kt create mode 100644 src/main/kotlin/mods/betterfoliage/client/render/RenderConnectedGrass.kt create mode 100644 src/main/kotlin/mods/betterfoliage/client/render/RenderConnectedGrassLog.kt create mode 100644 src/main/kotlin/mods/betterfoliage/client/render/RenderCoral.kt create mode 100644 src/main/kotlin/mods/betterfoliage/client/render/RenderGrass.kt create mode 100644 src/main/kotlin/mods/betterfoliage/client/render/RenderLeaves.kt create mode 100644 src/main/kotlin/mods/betterfoliage/client/render/RenderLilypad.kt create mode 100644 src/main/kotlin/mods/betterfoliage/client/render/RenderLog.kt create mode 100644 src/main/kotlin/mods/betterfoliage/client/render/RenderMycelium.kt create mode 100644 src/main/kotlin/mods/betterfoliage/client/render/RenderNetherrack.kt create mode 100644 src/main/kotlin/mods/betterfoliage/client/render/RenderReeds.kt create mode 100644 src/main/kotlin/mods/betterfoliage/client/render/Utils.kt create mode 100644 src/main/kotlin/mods/betterfoliage/client/texture/GrassGenerator.kt create mode 100644 src/main/kotlin/mods/betterfoliage/client/texture/GrassRegistry.kt create mode 100644 src/main/kotlin/mods/betterfoliage/client/texture/LeafGenerator.kt create mode 100644 src/main/kotlin/mods/betterfoliage/client/texture/LeafRegistry.kt create mode 100644 src/main/kotlin/mods/betterfoliage/client/texture/TextureMatcher.kt create mode 100644 src/main/kotlin/mods/betterfoliage/client/texture/Utils.kt create mode 100644 src/main/kotlin/mods/betterfoliage/loader/BetterFoliageCore.kt create mode 100644 src/main/kotlin/mods/betterfoliage/loader/Refs.kt create mode 100644 src/main/kotlin/mods/octarinecore/Utils.kt create mode 100644 src/main/kotlin/mods/octarinecore/client/KeyHandler.kt create mode 100644 src/main/kotlin/mods/octarinecore/client/gui/IdListConfigEntry.kt create mode 100644 src/main/kotlin/mods/octarinecore/client/gui/NonVerboseArrayEntry.kt create mode 100644 src/main/kotlin/mods/octarinecore/client/gui/Utils.kt create mode 100644 src/main/kotlin/mods/octarinecore/client/render/AbstractBlockRenderingHandler.kt create mode 100644 src/main/kotlin/mods/octarinecore/client/render/AbstractEntityFX.kt create mode 100644 src/main/kotlin/mods/octarinecore/client/render/Geometry.kt create mode 100644 src/main/kotlin/mods/octarinecore/client/render/Model.kt create mode 100644 src/main/kotlin/mods/octarinecore/client/render/ModelRenderer.kt create mode 100644 src/main/kotlin/mods/octarinecore/client/render/OffsetBlockAccess.kt create mode 100644 src/main/kotlin/mods/octarinecore/client/render/PixelFormat.kt create mode 100644 src/main/kotlin/mods/octarinecore/client/render/RenderBlocks.kt create mode 100644 src/main/kotlin/mods/octarinecore/client/render/Shaders.kt create mode 100644 src/main/kotlin/mods/octarinecore/client/render/Shading.kt create mode 100644 src/main/kotlin/mods/octarinecore/client/resource/CenteringTextureGenerator.kt create mode 100644 src/main/kotlin/mods/octarinecore/client/resource/ResourceGeneration.kt create mode 100644 src/main/kotlin/mods/octarinecore/client/resource/ResourceHandler.kt create mode 100644 src/main/kotlin/mods/octarinecore/client/resource/TextureGenerator.kt create mode 100644 src/main/kotlin/mods/octarinecore/client/resource/Utils.kt create mode 100644 src/main/kotlin/mods/octarinecore/config/DelegatingConfig.kt create mode 100644 src/main/kotlin/mods/octarinecore/metaprog/Reflection.kt create mode 100644 src/main/kotlin/mods/octarinecore/metaprog/Transformation.kt create mode 100644 src/main/resources/assets/betterfoliage/CactusDefault.cfg create mode 100644 src/main/resources/assets/betterfoliage/CropDefault.cfg create mode 100644 src/main/resources/assets/betterfoliage/DirtDefault.cfg create mode 100644 src/main/resources/assets/betterfoliage/GrassDefault.cfg create mode 100644 src/main/resources/assets/betterfoliage/LeavesDefault.cfg create mode 100644 src/main/resources/assets/betterfoliage/LilypadDefault.cfg create mode 100644 src/main/resources/assets/betterfoliage/LogDefault.cfg create mode 100644 src/main/resources/assets/betterfoliage/SandDefault.cfg create mode 100644 src/main/resources/assets/betterfoliage/lang/en_US.lang create mode 100644 src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_default_0.png create mode 100644 src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_default_1.png create mode 100644 src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_default_2.png create mode 100644 src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_default_3.png create mode 100644 src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_jungle_0.png create mode 100644 src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_jungle_1.png create mode 100644 src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_jungle_2.png create mode 100644 src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_jungle_3.png create mode 100644 src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_spruce_0.png create mode 100644 src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_spruce_1.png create mode 100644 src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_spruce_2.png create mode 100644 src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_spruce_3.png create mode 100644 src/main/resources/assets/betterfoliage/textures/blocks/leafmask_32_default.png create mode 100644 src/main/resources/assets/betterfoliage/textures/blocks/leafmask_32_fine.png create mode 100644 src/main/resources/assets/betterfoliage/textures/blocks/leafmask_32_full.png create mode 100644 src/main/resources/assets/betterfoliage/textures/blocks/leafmask_32_jumbled.png create mode 100644 src/main/resources/assets/betterfoliage/textures/blocks/missing_leaf.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_algae_0.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_algae_1.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_algae_2.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_algae_3.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_cactus.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_cactus_arm_0.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_cactus_arm_1.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_coral_0.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_coral_1.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_coral_2.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_coral_3.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_coral_4.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_coral_5.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_crust_0.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_crust_1.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_crust_2.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_crust_3.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_long_0.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_long_1.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_long_2.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_long_3.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_long_4.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_short_0.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_short_1.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_short_2.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_side_0.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_side_1.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_snowed_0.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_snowed_1.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_leaves_snowed_0.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_leaves_snowed_1.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_leaves_snowed_2.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_lilypad_flower_0.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_lilypad_flower_1.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_lilypad_roots_0.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_lilypad_roots_1.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_lilypad_roots_2.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_mycel_0.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_mycel_1.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_mycel_2.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_mycel_3.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_mycel_side_0.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_mycel_side_1.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_netherrack_0.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_netherrack_1.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_netherrack_2.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_reed_0.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_reed_1.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_reed_2.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/better_reed_3.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/rising_soul_0.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/rising_soul_1.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/soul_track.png create mode 100644 src/main/resources/assets/bettergrassandleaves/textures/blocks/soul_track.png.mcmeta create mode 100644 src/main/resources/mcmod.info create mode 100644 src/main/resources/pack.mcmeta diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fc6f264 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.idea/ +*.iml +*.ipr +*.iws +run/ +.gradle/ +build/ diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..35a385c --- /dev/null +++ b/build.gradle @@ -0,0 +1,68 @@ +apply plugin: "forge" +apply plugin: "kotlin" + +group = 'com.github.octarine-noise' +version = "2.0" +archivesBaseName = rootProject.name + '-MC1.7.10' + +buildscript { + ext.kotlin_version = '1.0.0-beta-4583' + repositories { + mavenCentral() + maven { + name = "forge" + url = "http://files.minecraftforge.net/maven" + } + maven { + name = "sonatype" + url = "https://oss.sonatype.org/content/repositories/snapshots/" + } + } + dependencies { + classpath "net.minecraftforge.gradle:ForgeGradle:1.2-SNAPSHOT" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} +configurations { + shade + compile.extendsFrom shade +} +dependencies { + shade "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" +} +minecraft { + version = '1.7.10-10.13.4.1448-1.7.10' + srgExtra "PK: kotlin mods/betterfoliage/kotlin" +} + +processResources { + inputs.property "version", project.version + inputs.property "mcversion", project.minecraft.version + + from(sourceSets.main.resources.srcDirs) { + include 'mcmod.info' + expand 'version':project.version, 'mcversion':project.minecraft.version + } + + from(sourceSets.main.resources.srcDirs) { + exclude 'mcmod.info' + } + + into "${buildDir}/classes/main" +} + +jar { + manifest { + attributes "FMLCorePlugin": "mods.betterfoliage.loader.BetterFoliageLoader" + attributes "FMLCorePluginContainsFMLMod": "mods.betterfoliage.BetterFoliageMod" + } + configurations.shade.each { dep -> + from(project.zipTree(dep)){ + exclude 'META-INF', 'META-INF/**' + } + } +} + +repositories { + mavenCentral() +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..75d2e73 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'BetterFoliage' + diff --git a/src/main/kotlin/mods/betterfoliage/BetterFoliageMod.kt b/src/main/kotlin/mods/betterfoliage/BetterFoliageMod.kt new file mode 100644 index 0000000..bf40168 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/BetterFoliageMod.kt @@ -0,0 +1,60 @@ +package mods.betterfoliage + +import cpw.mods.fml.common.FMLCommonHandler +import cpw.mods.fml.common.Mod +import cpw.mods.fml.common.event.FMLPostInitializationEvent +import cpw.mods.fml.common.event.FMLPreInitializationEvent +import cpw.mods.fml.common.network.NetworkCheckHandler +import cpw.mods.fml.relauncher.Side +import mods.betterfoliage.client.Client +import mods.betterfoliage.client.config.Config +import mods.betterfoliage.client.integration.ShadersModIntegration +import mods.betterfoliage.client.integration.TFCIntegration +import mods.betterfoliage.loader.Refs +import mods.octarinecore.metaprog.ClassRef +import net.minecraftforge.common.config.Configuration +import org.apache.logging.log4j.Level.* +import org.apache.logging.log4j.Logger + +@Mod( + modid = BetterFoliageMod.MOD_ID, + name = BetterFoliageMod.MOD_NAME, + acceptedMinecraftVersions = BetterFoliageMod.MC_VERSIONS, + guiFactory = BetterFoliageMod.GUI_FACTORY +) +object BetterFoliageMod { + + const val MOD_ID = "BetterFoliage" + const val MOD_NAME = "Better Foliage" + const val DOMAIN = "betterfoliage" + const val LEGACY_DOMAIN = "bettergrassandleaves" + const val MC_VERSIONS = "[1.7.10]" + const val GUI_FACTORY = "mods.betterfoliage.client.gui.ConfigGuiFactory" + + var log: Logger? = null + var config: Configuration? = null + + @JvmStatic + @Mod.InstanceFactory + // the fun never stops with the fun factory! :) + fun factory() = this + + @Mod.EventHandler + fun preInit(event: FMLPreInitializationEvent) { + log = event.modLog + config = Configuration(event.suggestedConfigurationFile, null, true) + + } + + @Mod.EventHandler + fun postInit(event: FMLPostInitializationEvent) { + if (FMLCommonHandler.instance().effectiveSide == Side.CLIENT) { + Config.attach(config!!) + Client.log(INFO, "BetterFoliage initialized") + } + } + + /** Mod is cosmetic only, always allow connection. */ + @NetworkCheckHandler + fun checkVersion(mods: Map, side: Side) = true +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/Client.kt b/src/main/kotlin/mods/betterfoliage/client/Client.kt new file mode 100644 index 0000000..ad79fa4 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/Client.kt @@ -0,0 +1,75 @@ +package mods.betterfoliage.client + +import cpw.mods.fml.client.FMLClientHandler +import cpw.mods.fml.relauncher.Side +import cpw.mods.fml.relauncher.SideOnly +import mods.betterfoliage.BetterFoliageMod +import mods.betterfoliage.client.gui.ConfigGuiFactory +import mods.betterfoliage.client.integration.CLCIntegration +import mods.betterfoliage.client.integration.ShadersModIntegration +import mods.betterfoliage.client.integration.TFCIntegration +import mods.betterfoliage.client.render.* +import mods.betterfoliage.client.texture.* +import mods.octarinecore.client.KeyHandler +import mods.octarinecore.client.resource.CenteringTextureGenerator +import mods.octarinecore.client.resource.GeneratorPack +import net.minecraft.client.Minecraft +import org.apache.logging.log4j.Level + +/** + * Object responsible for initializing (and holding a reference to) all the infrastructure of the mod + * except for the call hooks. + * + * This and all other singletons are annotated [SideOnly] to avoid someone accidentally partially + * initializing the mod on a server environment. + */ +@SideOnly(Side.CLIENT) +object Client { + + val configKey = KeyHandler(BetterFoliageMod.MOD_NAME, 66, "key.betterfoliage.gui") { + FMLClientHandler.instance().showGuiScreen( + ConfigGuiFactory.ConfigGuiBetterFoliage(Minecraft.getMinecraft().currentScreen) + ) + } + + val genGrass = GrassGenerator("bf_gen_grass") + val genLeaves = LeafGenerator("bf_gen_leaves") + val genReeds = CenteringTextureGenerator("bf_gen_reeds", 1, 2) + + val generatorPack = GeneratorPack( + "Better Foliage generated", + genGrass, + genLeaves, + genReeds + ) + + val logRenderer = RenderLog() + + val renderers = listOf( + RenderGrass(), + RenderMycelium(), + RenderLeaves(), + RenderCactus(), + RenderLilypad(), + RenderReeds(), + RenderAlgae(), + RenderCoral(), + logRenderer, + RenderNetherrack(), + RenderConnectedGrass(), + RenderConnectedGrassLog() + ) + + val singletons = listOf( + LeafRegistry, + GrassRegistry, + LeafWindTracker, + RisingSoulTextures, + TFCIntegration, + ShadersModIntegration, + CLCIntegration + ) + + fun log(level: Level, msg: String) = BetterFoliageMod.log!!.log(level, msg) +} + diff --git a/src/main/kotlin/mods/betterfoliage/client/Hooks.kt b/src/main/kotlin/mods/betterfoliage/client/Hooks.kt new file mode 100644 index 0000000..a5ee1e7 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/Hooks.kt @@ -0,0 +1,58 @@ +@file:JvmName("Hooks") +@file:SideOnly(Side.CLIENT) +package mods.betterfoliage.client + +import cpw.mods.fml.relauncher.Side +import cpw.mods.fml.relauncher.SideOnly +import mods.betterfoliage.client.config.Config +import mods.betterfoliage.client.render.EntityFallingLeavesFX +import mods.betterfoliage.client.render.EntityRisingSoulFX +import mods.octarinecore.client.render.blockContext +import net.minecraft.block.Block +import net.minecraft.client.Minecraft +import net.minecraft.init.Blocks +import net.minecraft.world.IBlockAccess +import net.minecraft.world.World + +fun getRenderTypeOverride(blockAccess: IBlockAccess, x: Int, y: Int, z: Int, block: Block, original: Int): Int { + if (!Config.enabled) return original; + + // universal sign for DON'T RENDER ME! + if (original == -1) return original; + + return blockContext.let { ctx -> + ctx.set(blockAccess, x, y, z) + Client.renderers.find { it.isEligible(ctx) }?.renderId ?: original + } +} + +fun shouldRenderBlockSideOverride(original: Boolean, blockAccess: IBlockAccess, x: Int, y: Int, z: Int, side: Int): Boolean { + return original || (Config.enabled && Config.roundLogs.enabled && Config.blocks.logs.matchesID(blockAccess.getBlock(x, y, z))); +} + +fun getAmbientOcclusionLightValueOverride(original: Float, block: Block): Float { + if (Config.enabled && Config.roundLogs.enabled && Config.blocks.logs.matchesID(block)) return Config.roundLogs.dimming; + return original; +} + +fun getUseNeighborBrightnessOverride(original: Boolean, block: Block): Boolean { + return original || (Config.enabled && Config.roundLogs.enabled && Config.blocks.logs.matchesID(block)); +} + +fun onRandomDisplayTick(block: Block, world: World, x: Int, y: Int, z: Int) { + if (Config.enabled && + Config.risingSoul.enabled && + block == Blocks.soul_sand && + world.isAirBlock(x, y + 1, z) && + Math.random() < Config.risingSoul.chance) { + EntityRisingSoulFX(world, x, y, z).addIfValid() + } + + if (Config.enabled && + Config.fallingLeaves.enabled && + Config.blocks.leaves.matchesID(block) && + world.isAirBlock(x, y - 1, z) && + Math.random() < Config.fallingLeaves.chance) { + EntityFallingLeavesFX(world, x, y, z).addIfValid() + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/config/BlockMatcher.kt b/src/main/kotlin/mods/betterfoliage/client/config/BlockMatcher.kt new file mode 100644 index 0000000..73bd8bf --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/config/BlockMatcher.kt @@ -0,0 +1,88 @@ +package mods.betterfoliage.client.config + +import cpw.mods.fml.common.eventhandler.SubscribeEvent +import mods.octarinecore.client.gui.NonVerboseArrayEntry +import mods.octarinecore.client.resource.get +import mods.octarinecore.client.resource.getLines +import mods.octarinecore.client.resource.resourceManager +import mods.octarinecore.config.ConfigPropertyBase +import mods.octarinecore.metaprog.getJavaClass +import net.minecraft.block.Block +import net.minecraft.client.multiplayer.WorldClient +import net.minecraftforge.common.MinecraftForge +import net.minecraftforge.common.config.Configuration +import net.minecraftforge.common.config.Property +import net.minecraftforge.event.world.WorldEvent + +/** + * Match blocks based on their class names. Caches block IDs for faster lookup. + * + * @param[domain] resource domain for defaults file + * @param[path] resource path for defaults file + */ +class BlockMatcher(val domain: String, val path: String) : ConfigPropertyBase() { + + val blackList = linkedListOf>() + val whiteList = linkedListOf>() + val blockIDs = hashSetOf() + var blacklistProperty: Property? = null + var whitelistProperty: Property? = null + + fun matchesClass(block: Block): Boolean { + val blockClass = block.javaClass + blackList.forEach { if (it.isAssignableFrom(blockClass)) return false } + whiteList.forEach { if (it.isAssignableFrom(blockClass)) return true } + return false + } + fun matchesID(block: Block) = blockIDs.contains(Block.blockRegistry.getIDForObject(block)) + fun matchesID(blockId: Int) = blockIDs.contains(blockId) + + override fun attach(target: Configuration, langPrefix: String, categoryName: String, propertyName: String) { + lang = null + val defaults = readDefaults(domain, path) + blacklistProperty = target.get(categoryName, "${propertyName}Blacklist", defaults.first) + whitelistProperty = target.get(categoryName, "${propertyName}Whitelist", defaults.second) + listOf(blacklistProperty!!, whitelistProperty!!).forEach { + it.setConfigEntryClass(NonVerboseArrayEntry::class.java) + it.setLanguageKey("$langPrefix.$categoryName.${it.name}") + } + read() + } + + override fun read() { + listOf(Pair(blackList, blacklistProperty!!), Pair(whiteList, whitelistProperty!!)).forEach { + it.first.clear() + it.first.addAll(it.second.stringList.map { getJavaClass(it) }.filterNotNull()) + } + updateIDs() + } + + fun updateIDs() { + blockIDs.clear() + Block.blockRegistry.forEach { + if (matchesClass(it as Block)) blockIDs.add(Block.blockRegistry.getIDForObject(it)) + } + } + + override val hasChanged: Boolean + get() = blacklistProperty?.hasChanged() ?: false || whitelistProperty?.hasChanged() ?: false + + override val guiProperties: List get() = listOf(whitelistProperty!!, blacklistProperty!!) + + fun readDefaults(domain: String, path: String): Pair, Array> { + val blackList = arrayListOf() + val whiteList = arrayListOf() + val defaults = resourceManager[domain, path]?.getLines() + defaults?.map{ it.trim() }?.filter { !it.startsWith("//") && it.isNotEmpty() }?.forEach { + if (it.startsWith("-")) { blackList.add(it.substring(1)) } + else { whiteList.add(it) } + } + return (blackList.toTypedArray() to whiteList.toTypedArray()) + } + + @SubscribeEvent + fun onWorldLoad(event: WorldEvent.Load) { if (event.world is WorldClient) updateIDs() } + + init { MinecraftForge.EVENT_BUS.register(this) } + +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/config/Config.kt b/src/main/kotlin/mods/betterfoliage/client/config/Config.kt new file mode 100644 index 0000000..79b15fe --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/config/Config.kt @@ -0,0 +1,183 @@ +package mods.betterfoliage.client.config + +import cpw.mods.fml.client.event.ConfigChangedEvent +import cpw.mods.fml.relauncher.Side +import cpw.mods.fml.relauncher.SideOnly +import mods.betterfoliage.BetterFoliageMod +import mods.betterfoliage.client.gui.BiomeListConfigEntry +import mods.octarinecore.config.* +import mods.octarinecore.metaprog.reflectField +import net.minecraft.client.Minecraft +import net.minecraft.world.biome.BiomeGenBase + +// BetterFoliage-specific property delegates +private fun featureEnable() = boolean(true).lang("enabled") +private fun distanceLimit() = int(min=1, max=1000, default=1000).lang("distance") +fun biomeList(defaults: (BiomeGenBase) -> Boolean) = intList { + BiomeGenBase.getBiomeGenArray().filter { it != null && defaults(it) }.map { it.biomeID }.toTypedArray() +}.apply { guiClass = BiomeListConfigEntry::class.java } + +// Biome filter methods +private fun BiomeGenBase.filterTemp(min: Float?, max: Float?) = (min == null || min <= temperature) && (max == null || max >= temperature) +private fun BiomeGenBase.filterRain(min: Float?, max: Float?) = (min == null || min <= rainfall) && (max == null || max >= rainfall) +private fun BiomeGenBase.filterClass(vararg name: String) = name.any { it in this.javaClass.name.toLowerCase() } + +// Config singleton +@SideOnly(Side.CLIENT) +object Config : DelegatingConfig(BetterFoliageMod.MOD_ID, BetterFoliageMod.DOMAIN) { + + var enabled by boolean(true) + + object blocks { + val dirt = BlockMatcher(BetterFoliageMod.DOMAIN, "DirtDefault.cfg") + val grass = BlockMatcher(BetterFoliageMod.DOMAIN, "GrassDefault.cfg") + val leaves = BlockMatcher(BetterFoliageMod.DOMAIN, "LeavesDefault.cfg") + val crops = BlockMatcher(BetterFoliageMod.DOMAIN, "CropDefault.cfg") + val logs = BlockMatcher(BetterFoliageMod.DOMAIN, "LogDefault.cfg") + val sand = BlockMatcher(BetterFoliageMod.DOMAIN, "SandDefault.cfg") + val lilypad = BlockMatcher(BetterFoliageMod.DOMAIN, "LilypadDefault.cfg") + val cactus = BlockMatcher(BetterFoliageMod.DOMAIN, "CactusDefault.cfg") + } + + object leaves { + val enabled by featureEnable() + val distance by distanceLimit() + val hOffset by double(max=0.4, default=0.2).lang("hOffset") + val vOffset by double(max=0.4, default=0.1).lang("vOffset") + val size by double(min=0.75, max=2.5, default=1.4).lang("size") + val dense by boolean(false) + } + + object shortGrass { + val grassEnabled by boolean(true) + val myceliumEnabled by boolean(true) + val snowEnabled by boolean(true) + val distance by distanceLimit() + val hOffset by double(max=0.4, default=0.2).lang("hOffset") + val heightMin by double(min=0.1, max=2.5, default=0.6).lang("heightMin") + val heightMax by double(min=0.1, max=2.5, default=0.8).lang("heightMax") + val size by double(min=0.5, max=1.5, default=1.0).lang("size") + val useGenerated by boolean(false) + val shaderWind by boolean(true).lang("shaderWind") + val saturationThreshold by double(default=0.1) + } + +// object hangingGrass { +// var enabled by featureEnable() +// var distance by distanceLimit() +// var size by double(min=0.25, max=1.5, default=0.75).lang("size") +// var separation by double(max=0.5, default=0.25) +// } + + object connectedGrass { + val enabled by boolean(true) + val snowEnabled by boolean(false) + } + + object roundLogs { + val enabled by featureEnable() + val distance by distanceLimit() + val radiusSmall by double(max=0.5, default=0.25) + val radiusLarge by double(max=0.5, default=0.44) + val dimming by float(default = 0.7) + val connectSolids by boolean(false) + val lenientConnect by boolean(true) + val connectPerpendicular by boolean(true) + val connectGrass by boolean(true) + val zProtection by double(min = 0.9, default = 0.99) + } + + object cactus { + val enabled by featureEnable() + val distance by distanceLimit() + val size by double(min=0.5, max=1.5, default=0.8).lang("size") + val sizeVariation by double(max=0.5, default=0.1) + val hOffset by double(max=0.5, default=0.1).lang("hOffset") + } + + object lilypad { + val enabled by featureEnable() + val distance by distanceLimit() + val hOffset by double(max=0.25, default=0.1).lang("hOffset") + val flowerChance by int(max=64, default=16, min=0) + } + + object reed { + val enabled by featureEnable() + val distance by distanceLimit() + val hOffset by double(max=0.4, default=0.2).lang("hOffset") + val heightMin by double(min=1.5, max=3.5, default=1.7).lang("heightMin") + val heightMax by double(min=1.5, max=3.5, default=2.2).lang("heightMax") + val population by int(max=64, default=32).lang("population") + val biomes by biomeList { it.filterTemp(0.4f, null) && it.filterRain(0.4f, null) } + val shaderWind by boolean(true).lang("shaderWind") + } + + object algae { + val enabled by featureEnable() + val distance by distanceLimit() + val hOffset by double(max=0.25, default=0.1).lang("hOffset") + val size by double(min=0.5, max=1.5, default=1.0).lang("size") + val heightMin by double(min=0.1, max=1.5, default=0.5).lang("heightMin") + val heightMax by double(min=0.1, max=1.5, default=1.0).lang("heightMax") + val population by int(max=64, default=48).lang("population") + val biomes by biomeList { it.filterClass("river", "ocean") } + val shaderWind by boolean(true).lang("shaderWind") + } + + object coral { + val enabled by featureEnable() + val distance by distanceLimit() + val shallowWater by boolean(false) + val hOffset by double(max=0.4, default=0.2).lang("hOffset") + val vOffset by double(max=0.4, default=0.1).lang("vOffset") + val size by double(min=0.5, max=1.5, default=0.7).lang("size") + val crustSize by double(min=0.5, max=1.5, default=1.4) + val chance by int(max=64, default=32) + val population by int(max=64, default=48).lang("population") + val biomes by biomeList { it.filterClass("river", "ocean", "beach") } + } + + object netherrack { + val enabled by featureEnable() + val distance by distanceLimit() + val hOffset by double(max=0.4, default=0.2).lang("hOffset") + val heightMin by double(min=0.1, max=1.5, default=0.6).lang("heightMin") + val heightMax by double(min=0.1, max=1.5, default=0.8).lang("heightMax") + val size by double(min=0.5, max=1.5, default=1.0).lang("size") + } + + object fallingLeaves { + val enabled by featureEnable() + val speed by double(min=0.01, max=0.15, default=0.05) + val windStrength by double(min=0.1, max=2.0, default=0.5) + val stormStrength by double(min=0.1, max=2.0, default=0.8) + val size by double(min=0.25, max=1.5, default=0.75).lang("size") + val chance by double(min=0.001, max=1.0, default=0.05) + val perturb by double(min=0.01, max=1.0, default=0.25) + val lifetime by double(min=1.0, max=15.0, default=5.0) + val opacityHack by boolean(true) + } + + object risingSoul { + val enabled by featureEnable() + val chance by double(min=0.001, max=1.0, default=0.02) + val perturb by double(min=0.01, max=0.25, default=0.05) + val headSize by double(min=0.25, max=1.5, default=1.0) + val trailSize by double(min=0.25, max=1.5, default=0.75) + val opacity by float(min=0.05, max=1.0, default=0.5) + val sizeDecay by double(min=0.5, max=1.0, default=0.97) + val opacityDecay by float(min=0.5, max=1.0, default=0.97) + val lifetime by double(min=1.0, max=15.0, default=4.0) + val trailLength by int(min=2, max=128, default=48) + val trailDensity by int(min=1, max=16, default=3) + } + + override fun onChange(event: ConfigChangedEvent.OnConfigChangedEvent) { + super.onChange(event) + if (hasChanged(blocks, shortGrass["saturationThreshold"])) + Minecraft.getMinecraft().refreshResources() + else + Minecraft.getMinecraft().renderGlobal.loadRenderers() + } +} diff --git a/src/main/kotlin/mods/betterfoliage/client/gui/BiomeListConfigEntry.kt b/src/main/kotlin/mods/betterfoliage/client/gui/BiomeListConfigEntry.kt new file mode 100644 index 0000000..ef0a93e --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/gui/BiomeListConfigEntry.kt @@ -0,0 +1,19 @@ +package mods.betterfoliage.client.gui + +import cpw.mods.fml.client.config.GuiConfig +import cpw.mods.fml.client.config.GuiConfigEntries +import cpw.mods.fml.client.config.IConfigElement +import mods.octarinecore.client.gui.IdListConfigEntry +import net.minecraft.world.biome.BiomeGenBase + +/** Toggleable list of all defined biomes. */ +class BiomeListConfigEntry( + owningScreen: GuiConfig, + owningEntryList: GuiConfigEntries, + configElement: IConfigElement<*>) +: IdListConfigEntry(owningScreen, owningEntryList, configElement) { + + override val baseSet: List get() = BiomeGenBase.getBiomeGenArray().filterNotNull() + override val BiomeGenBase.itemId: Int get() = this.biomeID + override val BiomeGenBase.itemName: String get() = this.biomeName +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/gui/ConfigGuiFactory.kt b/src/main/kotlin/mods/betterfoliage/client/gui/ConfigGuiFactory.kt new file mode 100644 index 0000000..655b75f --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/gui/ConfigGuiFactory.kt @@ -0,0 +1,27 @@ +package mods.betterfoliage.client.gui + +import cpw.mods.fml.client.IModGuiFactory +import cpw.mods.fml.client.IModGuiFactory.RuntimeOptionCategoryElement +import cpw.mods.fml.client.config.GuiConfig +import mods.betterfoliage.BetterFoliageMod +import mods.betterfoliage.client.config.Config +import net.minecraft.client.Minecraft +import net.minecraft.client.gui.GuiScreen + +class ConfigGuiFactory : IModGuiFactory { + + override fun mainConfigGuiClass() = ConfigGuiBetterFoliage::class.java + override fun runtimeGuiCategories() = hashSetOf() + override fun getHandlerFor(element: RuntimeOptionCategoryElement?) = null + override fun initialize(minecraftInstance: Minecraft?) { } + + class ConfigGuiBetterFoliage(parentScreen: GuiScreen?) : GuiConfig( + parentScreen, + Config.rootGuiElements, + BetterFoliageMod.MOD_ID, + null, + false, + false, + BetterFoliageMod.MOD_NAME + ) +} diff --git a/src/main/kotlin/mods/betterfoliage/client/integration/CLCIntegration.kt b/src/main/kotlin/mods/betterfoliage/client/integration/CLCIntegration.kt new file mode 100644 index 0000000..90febdb --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/integration/CLCIntegration.kt @@ -0,0 +1,19 @@ +package mods.betterfoliage.client.integration + +import mods.betterfoliage.client.Client +import mods.betterfoliage.loader.Refs +import mods.octarinecore.client.render.brightnessComponents +import org.apache.logging.log4j.Level.* + +/** + * Integration for Colored Lights Core. + */ +object CLCIntegration { + + init { + if (Refs.CLCLoadingPlugin.element != null) { + Client.log(INFO, "Colored Lights Core integration enabled") + brightnessComponents = listOf(4, 8, 12, 16, 20) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/integration/ShadersModIntegration.kt b/src/main/kotlin/mods/betterfoliage/client/integration/ShadersModIntegration.kt new file mode 100644 index 0000000..86c3395 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/integration/ShadersModIntegration.kt @@ -0,0 +1,53 @@ +package mods.betterfoliage.client.integration + +import cpw.mods.fml.relauncher.Side +import cpw.mods.fml.relauncher.SideOnly +import mods.betterfoliage.client.Client +import mods.betterfoliage.client.config.Config +import mods.betterfoliage.loader.Refs +import mods.octarinecore.metaprog.allAvailable +import net.minecraft.block.Block +import net.minecraft.init.Blocks +import org.apache.logging.log4j.Level.* + +/** + * Integration for ShadersMod. + */ +@SideOnly(Side.CLIENT) +object ShadersModIntegration { + + @JvmStatic var isPresent = false + @JvmStatic val tallGrassEntityData = Block.blockRegistry.getIDForObject(Blocks.tallgrass) and 65535 or (Blocks.tallgrass.renderType shl 16) + @JvmStatic val leavesEntityData = Block.blockRegistry.getIDForObject(Blocks.leaves) and 65535 or (Blocks.leaves.renderType shl 16) + + /** + * Called from transformed ShadersMod code. + * @see mods.betterfoliage.loader.BetterFoliageTransformer + */ + @JvmStatic fun getBlockIdOverride(original: Int, block: Block): Int { + if (Config.blocks.leaves.matchesID(original and 65535)) return leavesEntityData + if (Config.blocks.crops.matchesID(original and 65535)) return tallGrassEntityData + return original + } + + init { + if (allAvailable(Refs.pushEntity_I, Refs.popEntity)) { + Client.log(INFO, "ShadersMod integration enabled") + isPresent = true + } + } + + /** Quads rendered inside this block will behave as tallgrass blocks in shader programs. */ + inline fun grass(enabled: Boolean = true, func: ()->Unit) { + if (isPresent && enabled) Refs.pushEntity_I.invokeStatic(tallGrassEntityData) + func() + if (isPresent && enabled) Refs.popEntity.invokeStatic() + } + + /** Quads rendered inside this block will behave as leaf blocks in shader programs. */ + inline fun leaves(enabled: Boolean = true, func: ()->Unit) { + if (isPresent && enabled) Refs.pushEntity_I.invokeStatic(leavesEntityData) + func() + if (isPresent && enabled) Refs.popEntity.invokeStatic() + } +} diff --git a/src/main/kotlin/mods/betterfoliage/client/integration/TFCIntegration.kt b/src/main/kotlin/mods/betterfoliage/client/integration/TFCIntegration.kt new file mode 100644 index 0000000..eb6f189 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/integration/TFCIntegration.kt @@ -0,0 +1,37 @@ +package mods.betterfoliage.client.integration + +import cpw.mods.fml.common.Loader +import cpw.mods.fml.relauncher.Side +import cpw.mods.fml.relauncher.SideOnly +import mods.betterfoliage.client.Client +import mods.octarinecore.client.render.Axis +import net.minecraft.block.Block +import org.apache.logging.log4j.Level + +/** + * Integration for TerraFirmaCraft + */ +@SideOnly(Side.CLIENT) +object TFCIntegration { + @JvmStatic val vanillaLogAxis = Client.logRenderer.axisFunc + + init { + if (Loader.isModLoaded("terrafirmacraft")) { + Client.log(Level.INFO, "TerraFirmaCraft found - setting up compatibility") + + // patch axis detection for log blocks to support TFC logs + Client.logRenderer.axisFunc = { block: Block, meta: Int -> + block.javaClass.name.let { + if (it.startsWith("com.bioxx.tfc")) { + if (it.contains("Horiz")) + if (meta shr 3 == 0) Axis.Z else Axis.X + else + Axis.Y + } else { + vanillaLogAxis(block, meta) + } + } + } + } + } +} diff --git a/src/main/kotlin/mods/betterfoliage/client/render/AbstractRenderColumn.kt b/src/main/kotlin/mods/betterfoliage/client/render/AbstractRenderColumn.kt new file mode 100644 index 0000000..fba339d --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/AbstractRenderColumn.kt @@ -0,0 +1,301 @@ +package mods.betterfoliage.client.render + +import mods.betterfoliage.client.render.AbstractRenderColumn.BlockType.* +import mods.betterfoliage.client.render.AbstractRenderColumn.QuadrantType.* +import mods.octarinecore.client.render.* +import net.minecraft.block.Block +import net.minecraft.client.renderer.RenderBlocks +import net.minecraftforge.common.util.ForgeDirection.* + +/** Index of SOUTH-EAST quadrant. */ +const val SE = 0 +/** Index of NORTH-EAST quadrant. */ +const val NE = 1 +/** Index of NORTH-WEST quadrant. */ +const val NW = 2 +/** Index of SOUTH-WEST quadrant. */ +const val SW = 3 + +@Suppress("NOTHING_TO_INLINE") +abstract class AbstractRenderColumn(modId: String) : AbstractBlockRenderingHandler(modId) { + + enum class BlockType { SOLID, NONSOLID, PARALLEL, PERPENDICULAR } + enum class QuadrantType { SMALL_RADIUS, LARGE_RADIUS, SQUARE, INVISIBLE } + + /** The rotations necessary to bring the models in position for the 4 quadrants */ + val quadrantRotations = Array(4) { Rotation.rot90[UP.ordinal] * it } + + // ============================ + // Configuration + // ============================ + abstract val radiusSmall: Double + abstract val radiusLarge: Double + abstract val surroundPredicate: (Block) -> Boolean + abstract val connectPerpendicular: Boolean + abstract val connectSolids: Boolean + abstract val lenientConnect: Boolean + + // ============================ + // Models + // ============================ + val sideSquare = model { columnSideSquare(-0.5, 0.5) } + val sideRoundSmall = model { columnSide(radiusSmall, -0.5, 0.5) } + val sideRoundLarge = model { columnSide(radiusLarge, -0.5, 0.5) } + + val extendTopSquare = model { columnSideSquare(0.5, 0.5 + radiusLarge, topExtension(radiusLarge)) } + val extendTopRoundSmall = model { columnSide(radiusSmall, 0.5, 0.5 + radiusLarge, topExtension(radiusLarge)) } + val extendTopRoundLarge = model { columnSide(radiusLarge, 0.5, 0.5 + radiusLarge, topExtension(radiusLarge)) } + inline fun extendTop(type: QuadrantType) = when(type) { + SMALL_RADIUS -> extendTopRoundSmall.model + LARGE_RADIUS -> extendTopRoundLarge.model + SQUARE -> extendTopSquare.model + INVISIBLE -> extendTopSquare.model + else -> null + } + + val extendBottomSquare = model { columnSideSquare(-0.5 - radiusLarge, -0.5, bottomExtension(radiusLarge)) } + val extendBottomRoundSmall = model { columnSide(radiusSmall, -0.5 - radiusLarge, -0.5, bottomExtension(radiusLarge)) } + val extendBottomRoundLarge = model { columnSide(radiusLarge, -0.5 - radiusLarge, -0.5, bottomExtension(radiusLarge)) } + inline fun extendBottom(type: QuadrantType) = when (type) { + SMALL_RADIUS -> extendBottomRoundSmall.model + LARGE_RADIUS -> extendBottomRoundLarge.model + SQUARE -> extendBottomSquare.model + INVISIBLE -> extendBottomSquare.model + else -> null + } + + val topSquare = model { columnLidSquare() } + val topRoundSmall = model { columnLid(radiusSmall) } + val topRoundLarge = model { columnLid(radiusLarge) } + inline fun flatTop(type: QuadrantType) = when(type) { + SMALL_RADIUS -> topRoundSmall.model + LARGE_RADIUS -> topRoundLarge.model + SQUARE -> topSquare.model + INVISIBLE -> topSquare.model + else -> null + } + + val bottomSquare = model { columnLidSquare() { it.rotate(rot(EAST) * 2 + rot(UP)) } } + val bottomRoundSmall = model { columnLid(radiusSmall) { it.rotate(rot(EAST) * 2 + rot(UP)) } } + val bottomRoundLarge = model { columnLid(radiusLarge) { it.rotate(rot(EAST) * 2 + rot(UP)) } } + inline fun flatBottom(type: QuadrantType) = when(type) { + SMALL_RADIUS -> bottomRoundSmall.model + LARGE_RADIUS -> bottomRoundLarge.model + SQUARE -> bottomSquare.model + INVISIBLE -> bottomSquare.model + else -> null + } + + val transitionTop = model { mix(sideRoundLarge.model, sideRoundSmall.model) { it > 1 } } + val transitionBottom = model { mix(sideRoundSmall.model, sideRoundLarge.model) { it > 1 } } + + val sideTexture = { ctx: ShadingContext, qi: Int, q: Quad -> if ((qi and 1) == 0) ctx.icon(SOUTH) else ctx.icon(EAST) } + val upTexture = { ctx: ShadingContext, qi: Int, q: Quad -> ctx.icon(UP) } + val downTexture = { ctx: ShadingContext, qi: Int, q: Quad -> ctx.icon(DOWN) } + + inline fun continous(q1: QuadrantType, q2: QuadrantType) = + q1 == q2 || ((q1 == SQUARE || q1 == INVISIBLE) && (q2 == SQUARE || q2 == INVISIBLE)) + + abstract val axisFunc: (Block, Int)->Axis + abstract val blockPredicate: (Block, Int)->Boolean + + @Suppress("NON_EXHAUSTIVE_WHEN") + override fun render(ctx: BlockContext, parent: RenderBlocks): Boolean { + if (ctx.isSurroundedBy(surroundPredicate) ) return false + + // get AO data + if (renderWorldBlockBase(parent, face = neverRender)) return true + + // check log neighborhood + val logAxis = ctx.blockAxis + val baseRotation = rotationFromUp[(logAxis to Dir.P).face.ordinal] + + val upType = ctx.blockType(baseRotation, logAxis, Int3(0, 1, 0)) + val downType = ctx.blockType(baseRotation, logAxis, Int3(0, -1, 0)) + + val quadrants = Array(4) { SMALL_RADIUS }.checkNeighbors(ctx, baseRotation, logAxis, 0) + val quadrantsTop = Array(4) { SMALL_RADIUS } + if (upType == PARALLEL) quadrantsTop.checkNeighbors(ctx, baseRotation, logAxis, 1) + val quadrantsBottom = Array(4) { SMALL_RADIUS } + if (downType == PARALLEL) quadrantsBottom.checkNeighbors(ctx, baseRotation, logAxis, -1) + + quadrantRotations.forEachIndexed { idx, quadrantRotation -> + // set rotation for the current quadrant + val rotation = baseRotation + quadrantRotation + + // disallow sharp discontinuities in the chamfer radius + if (quadrants[idx] == LARGE_RADIUS && + upType == PARALLEL && quadrantsTop[idx] == SMALL_RADIUS && + downType == PARALLEL && quadrantsBottom[idx] == SMALL_RADIUS) { + quadrants[idx] = SMALL_RADIUS + } + + // render side of current quadrant + val sideModel = when (quadrants[idx]) { + SMALL_RADIUS -> sideRoundSmall.model + LARGE_RADIUS -> if (upType == PARALLEL && quadrantsTop[idx] == SMALL_RADIUS) transitionTop.model + else if (downType == PARALLEL && quadrantsBottom[idx] == SMALL_RADIUS) transitionBottom.model + else sideRoundLarge.model + SQUARE -> sideSquare.model + else -> null + } + + if (sideModel != null) modelRenderer.render( + sideModel, + rotation, + blockContext.blockCenter, + icon = sideTexture, + rotateUV = { 0 }, + postProcess = noPost + ) + + // render top and bottom end of current quadrant + var upModel: Model? = null + var downModel: Model? = null + var upIcon = upTexture + var downIcon = downTexture + + when (upType) { + NONSOLID -> upModel = flatTop(quadrants[idx]) + PERPENDICULAR -> { + if (!connectPerpendicular) { + upModel = flatTop(quadrants[idx]) + } else { + upIcon = sideTexture + upModel = extendTop(quadrants[idx]) + } + } + PARALLEL -> { + if (!continous(quadrants[idx], quadrantsTop[idx])) { + if (quadrants[idx] == SQUARE || quadrants[idx] == INVISIBLE) { + upModel = topSquare.model + } + } + } + } + when (downType) { + NONSOLID -> downModel = flatBottom(quadrants[idx]) + PERPENDICULAR -> { + if (!connectPerpendicular) { + downModel = flatBottom(quadrants[idx]) + } else { + downIcon = sideTexture + downModel = extendBottom(quadrants[idx]) + } + } + PARALLEL -> { + if (!continous(quadrants[idx], quadrantsBottom[idx]) && + (quadrants[idx] == SQUARE || quadrants[idx] == INVISIBLE)) { + downModel = bottomSquare.model + } + } + } + + if (upModel != null) modelRenderer.render( + upModel, + rotation, + blockContext.blockCenter, + icon = upIcon, + rotateUV = { 0 }, + postProcess = noPost + ) + if (downModel != null) modelRenderer.render( + downModel, + rotation, + blockContext.blockCenter, + icon = downIcon, + rotateUV = { 0 }, + postProcess = noPost + ) + } + + return true + } + + /** Sets the type of the given quadrant only if the new value is "stronger" (larger ordinal). */ + inline fun Array.upgrade(idx: Int, value: QuadrantType) { + if (this[idx].ordinal < value.ordinal) this[idx] = value + } + + /** Fill the array of [QuadrantType]s based on the blocks to the sides of this one. */ + fun Array.checkNeighbors(ctx: BlockContext, rotation: Rotation, logAxis: Axis, yOff: Int): Array { + val blkS = ctx.blockType(rotation, logAxis, Int3(0, yOff, 1)) + val blkE = ctx.blockType(rotation, logAxis, Int3(1, yOff, 0)) + val blkN = ctx.blockType(rotation, logAxis, Int3(0, yOff, -1)) + val blkW = ctx.blockType(rotation, logAxis, Int3(-1, yOff, 0)) + + // a solid block on one side will make the 2 neighboring quadrants SQUARE + // if there are solid blocks to both sides of a quadrant, it is INVISIBLE + if (connectSolids) { + if (blkS == SOLID) { + upgrade(SW, SQUARE); upgrade(SE, SQUARE) + } + if (blkE == SOLID) { + upgrade(SE, SQUARE); upgrade(NE, SQUARE) + } + if (blkN == SOLID) { + upgrade(NE, SQUARE); upgrade(NW, SQUARE) + } + if (blkW == SOLID) { + upgrade(NW, SQUARE); upgrade(SW, SQUARE) + } + if (blkS == SOLID && blkE == SOLID) upgrade(SE, INVISIBLE) + if (blkN == SOLID && blkE == SOLID) upgrade(NE, INVISIBLE) + if (blkN == SOLID && blkW == SOLID) upgrade(NW, INVISIBLE) + if (blkS == SOLID && blkW == SOLID) upgrade(SW, INVISIBLE) + } + val blkSE = ctx.blockType(rotation, logAxis, Int3(1, yOff, 1)) + val blkNE = ctx.blockType(rotation, logAxis, Int3(1, yOff, -1)) + val blkNW = ctx.blockType(rotation, logAxis, Int3(-1, yOff, -1)) + val blkSW = ctx.blockType(rotation, logAxis, Int3(-1, yOff, 1)) + + if (lenientConnect) { + // if the block forms the tip of an L-shape, connect to its neighbor with SQUARE quadrants + if (blkE == PARALLEL && (blkSE == PARALLEL || blkNE == PARALLEL)) { + upgrade(SE, SQUARE); upgrade(NE, SQUARE) + } + if (blkN == PARALLEL && (blkNE == PARALLEL || blkNW == PARALLEL)) { + upgrade(NE, SQUARE); upgrade(NW, SQUARE) + } + if (blkW == PARALLEL && (blkNW == PARALLEL || blkSW == PARALLEL)) { + upgrade(NW, SQUARE); upgrade(SW, SQUARE) + } + if (blkS == PARALLEL && (blkSE == PARALLEL || blkSW == PARALLEL)) { + upgrade(SW, SQUARE); upgrade(SE, SQUARE) + } + } + + // if the block forms the middle of an L-shape, or is part of a 2x2 configuration, + // connect to its neighbors with SQUARE quadrants, INVISIBLE on the inner corner, and LARGE_RADIUS on the outer corner + if (blkN == PARALLEL && blkW == PARALLEL && (lenientConnect || blkNW == PARALLEL)) { + upgrade(SE, LARGE_RADIUS); upgrade(NE, SQUARE); upgrade(SW, SQUARE); upgrade(NW, INVISIBLE) + } + if (blkS == PARALLEL && blkW == PARALLEL && (lenientConnect || blkSW == PARALLEL)) { + upgrade(NE, LARGE_RADIUS); upgrade(SE, SQUARE); upgrade(NW, SQUARE); upgrade(SW, INVISIBLE) + } + if (blkS == PARALLEL && blkE == PARALLEL && (lenientConnect || blkSE == PARALLEL)) { + upgrade(NW, LARGE_RADIUS); upgrade(NE, SQUARE); upgrade(SW, SQUARE); upgrade(SE, INVISIBLE) + } + if (blkN == PARALLEL && blkE == PARALLEL && (lenientConnect || blkNE == PARALLEL)) { + upgrade(SW, LARGE_RADIUS); upgrade(SE, SQUARE); upgrade(NW, SQUARE); upgrade(NE, INVISIBLE) + } + return this + } + + /** Get the axis of the block */ + val BlockContext.blockAxis: Axis get() = axisFunc(block(Int3.zero), meta(Int3.zero)) + + /** + * Get the type of the block at the given offset in a rotated reference frame. + */ + fun BlockContext.blockType(rotation: Rotation, axis: Axis, offset: Int3): BlockType { + val offsetRot = offset.rotate(rotation) + val logBlock = block(offsetRot) + val logMeta = meta(offsetRot) + return if (!blockPredicate(logBlock, logMeta)) { + if (logBlock.isOpaqueCube) SOLID else NONSOLID + } else { + if (axisFunc(logBlock, logMeta) == axis) PARALLEL else PERPENDICULAR + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/render/EntityFallingLeavesFX.kt b/src/main/kotlin/mods/betterfoliage/client/render/EntityFallingLeavesFX.kt new file mode 100644 index 0000000..da37ac3 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/EntityFallingLeavesFX.kt @@ -0,0 +1,131 @@ +package mods.betterfoliage.client.render + +import cpw.mods.fml.common.FMLCommonHandler +import cpw.mods.fml.common.eventhandler.SubscribeEvent +import cpw.mods.fml.common.gameevent.TickEvent +import cpw.mods.fml.relauncher.Side +import cpw.mods.fml.relauncher.SideOnly +import mods.betterfoliage.client.config.Config +import mods.betterfoliage.client.texture.LeafRegistry +import mods.octarinecore.PI2 +import mods.octarinecore.client.render.AbstractEntityFX +import mods.octarinecore.client.render.Double3 +import mods.octarinecore.client.render.HSB +import mods.octarinecore.minmax +import mods.octarinecore.random +import net.minecraft.client.Minecraft +import net.minecraft.client.renderer.Tessellator +import net.minecraft.util.MathHelper +import net.minecraft.world.World +import net.minecraftforge.common.MinecraftForge +import net.minecraftforge.common.util.ForgeDirection.DOWN +import net.minecraftforge.event.world.WorldEvent +import org.lwjgl.opengl.GL11 +import java.lang.Math.* +import java.util.* + +class EntityFallingLeavesFX(world: World, x: Int, y: Int, z: Int) : +AbstractEntityFX(world, x.toDouble() + 0.5, y.toDouble(), z.toDouble() + 0.5) { + + companion object { + @JvmStatic val biomeBrightnessMultiplier = 0.5f + } + + var particleRot = rand.nextInt(64) + var rotPositive = true + val isMirrored = (rand.nextInt() and 1) == 1 + var wasOnGround = false + + init { + particleMaxAge = MathHelper.floor_double(random(0.6, 1.0) * Config.fallingLeaves.lifetime * 20.0) + motionY = -Config.fallingLeaves.speed + particleScale = Config.fallingLeaves.size.toFloat() * 0.1f + + val block = world.getBlock(x, y, z) + LeafRegistry.leaves[block.getIcon(world, x, y, z, DOWN.ordinal)]?.let { + particleIcon = it.particleTextures[rand.nextInt(1024)] + calculateParticleColor(it.averageColor, block.colorMultiplier(world, x, y, z)) + } + } + + override val isValid: Boolean get() = (particleIcon != null) + + override fun update() { + if (rand.nextFloat() > 0.95f) rotPositive = !rotPositive + if (particleAge > particleMaxAge - 20) particleAlpha = 0.05f * (particleMaxAge - particleAge) + + if (onGround || wasOnGround) { + velocity.setTo(0.0, 0.0, 0.0) + if (!wasOnGround) { + particleAge = Math.max(particleAge, particleMaxAge - 20) + wasOnGround = true + } + } else { + velocity.setTo(cos[particleRot], 0.0, sin[particleRot]).mul(Config.fallingLeaves.perturb) + .add(LeafWindTracker.current).add(0.0, -1.0, 0.0).mul(Config.fallingLeaves.speed) + particleRot = (particleRot + (if (rotPositive) 1 else -1)) and 63 + } + } + + override fun render(tessellator: Tessellator, partialTickTime: Float) { + if (Config.fallingLeaves.opacityHack) GL11.glDepthMask(true) + renderParticleQuad(tessellator, partialTickTime, rotation = particleRot, isMirrored = isMirrored) + } + + fun calculateParticleColor(textureAvgColor: Int, blockColor: Int) { + val texture = HSB.fromColor(textureAvgColor) + val block = HSB.fromColor(blockColor) + + val weightTex = texture.saturation / (texture.saturation + block.saturation) + val weightBlock = 1.0f - weightTex + + // avoid circular average for hue for performance reasons + // one of the color components should dominate anyway + val particle = HSB( + weightTex * texture.hue + weightBlock * block.hue, + weightTex * texture.saturation + weightBlock * block.saturation, + weightTex * texture.brightness + weightBlock * block.brightness * biomeBrightnessMultiplier + ) + setColor(particle.asColor) + } +} + +@SideOnly(Side.CLIENT) +object LeafWindTracker { + var random = Random() + val target = Double3.zero + val current = Double3.zero + var nextChange: Long = 0 + + init { + MinecraftForge.EVENT_BUS.register(this) + FMLCommonHandler.instance().bus().register(this) + } + + fun changeWind(world: World) { + nextChange = world.worldInfo.worldTime + 120 + random.nextInt(80) + val direction = PI2 * random.nextDouble() + val speed = abs(random.nextGaussian()) * Config.fallingLeaves.windStrength + + (if (!world.isRaining) 0.0 else abs(random.nextGaussian()) * Config.fallingLeaves.stormStrength) + target.setTo(cos(direction) * speed, 0.0, sin(direction) * speed) + } + + @SubscribeEvent + fun handleWorldTick(event: TickEvent.ClientTickEvent) { + if (event.phase == TickEvent.Phase.START) Minecraft.getMinecraft().theWorld?.let { world -> + // change target wind speed + if (world.worldInfo.worldTime >= nextChange) changeWind(world) + + // change current wind speed + val changeRate = if (world.isRaining) 0.015 else 0.005 + current.add( + (target.x - current.x).minmax(-changeRate, changeRate), + 0.0, + (target.z - current.z).minmax(-changeRate, changeRate) + ) + } + } + + @SubscribeEvent + fun handleWorldLoad(event: WorldEvent.Load) { if (event.world.isRemote) changeWind(event.world) } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/render/EntityRisingSoulFX.kt b/src/main/kotlin/mods/betterfoliage/client/render/EntityRisingSoulFX.kt new file mode 100644 index 0000000..624c924 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/EntityRisingSoulFX.kt @@ -0,0 +1,75 @@ +package mods.betterfoliage.client.render + +import cpw.mods.fml.relauncher.Side +import cpw.mods.fml.relauncher.SideOnly +import mods.betterfoliage.BetterFoliageMod +import mods.betterfoliage.client.Client +import mods.betterfoliage.client.config.Config +import mods.octarinecore.client.render.AbstractEntityFX +import mods.octarinecore.client.render.Double3 +import mods.octarinecore.client.resource.ResourceHandler +import mods.octarinecore.forEachPairIndexed +import net.minecraft.client.renderer.Tessellator +import net.minecraft.util.MathHelper +import net.minecraft.world.World +import org.apache.logging.log4j.Level.* +import java.util.* + +class EntityRisingSoulFX(world: World, x: Int, y: Int, z: Int) : +AbstractEntityFX(world, x.toDouble() + 0.5, y.toDouble() + 1.0, z.toDouble() + 0.5) { + + val particleTrail: Deque = linkedListOf() + val initialPhase = rand.nextInt(64) + + init { + motionY = 0.1 + particleGravity = 0.0f + particleIcon = RisingSoulTextures.headIcons[rand.nextInt(256)] + particleMaxAge = MathHelper.floor_double((0.6 + 0.4 * rand.nextDouble()) * Config.risingSoul.lifetime * 20.0) + } + + override val isValid: Boolean get() = true + + override fun update() { + val phase = (initialPhase + particleAge) % 64 + velocity.setTo(cos[phase] * Config.risingSoul.perturb, 0.1, sin[phase] * Config.risingSoul.perturb) + + particleTrail.addFirst(currentPos.copy()) + while (particleTrail.size > Config.risingSoul.trailLength) particleTrail.removeLast() + + if (!Config.enabled) setDead() + } + + override fun render(tessellator: Tessellator, partialTickTime: Float) { + var alpha = Config.risingSoul.opacity + if (particleAge > particleMaxAge - 40) alpha *= (particleMaxAge - particleAge) / 40.0f + + renderParticleQuad(tessellator, partialTickTime, + size = Config.risingSoul.headSize * 0.25, + alpha = alpha + ) + + var scale = Config.risingSoul.trailSize * 0.25 + particleTrail.forEachPairIndexed { idx, current, previous -> + scale *= Config.risingSoul.sizeDecay + alpha *= Config.risingSoul.opacityDecay + if (idx % Config.risingSoul.trailDensity == 0) renderParticleQuad(tessellator, partialTickTime, + currentPos = current, + prevPos = previous, + size = scale, + alpha = alpha, + icon = RisingSoulTextures.trackIcon.icon!! + ) + } + } +} + +@SideOnly(Side.CLIENT) +object RisingSoulTextures : ResourceHandler(BetterFoliageMod.MOD_ID) { + val headIcons = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "rising_soul_%d") + val trackIcon = iconStatic(BetterFoliageMod.LEGACY_DOMAIN, "soul_track") + + override fun afterStitch() { + Client.log(INFO, "Registered ${headIcons.num} soul particle textures") + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/render/ModelColumn.kt b/src/main/kotlin/mods/betterfoliage/client/render/ModelColumn.kt new file mode 100644 index 0000000..3a32d0f --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/ModelColumn.kt @@ -0,0 +1,140 @@ +@file:JvmName("ModelColumn") +package mods.betterfoliage.client.render + +import mods.betterfoliage.client.config.Config +import mods.octarinecore.client.render.* +import mods.octarinecore.exchange +import net.minecraftforge.common.util.ForgeDirection.* + +/** Weight of the same-side AO values on the outer edges of the 45deg chamfered column faces. */ +const val chamferAffinity = 0.9f + +/** Amount to shrink column extension bits to stop Z-fighting. */ +val zProtectionScale: Double3 get() = Double3(Config.roundLogs.zProtection, 1.0, Config.roundLogs.zProtection) + +fun Model.columnSide(radius: Double, yBottom: Double, yTop: Double, transform: (Quad) -> Quad = { it }) { + val halfRadius = radius * 0.5 + listOf( + verticalRectangle(x1 = 0.0, z1 = 0.5, x2 = 0.5 - radius, z2 = 0.5, yBottom = yBottom, yTop = yTop) + .clampUV(minU = 0.0, maxU = 0.5 - radius) + .setAoShader(faceOrientedInterpolate(overrideFace = SOUTH)), + + verticalRectangle(x1 = 0.5 - radius, z1 = 0.5, x2 = 0.5 - halfRadius, z2 = 0.5 - halfRadius, yBottom = yBottom, yTop = yTop) + .clampUV(minU = 0.5 - radius) + .setAoShader( + faceOrientedAuto(overrideFace = SOUTH, corner = cornerInterpolate(Axis.Y, chamferAffinity, Config.roundLogs.dimming)) + ) + .setAoShader( + faceOrientedAuto(overrideFace = SOUTH, corner = cornerInterpolate(Axis.Y, 0.5f, Config.roundLogs.dimming)), + predicate = { v, vi -> vi == 1 || vi == 2} + ) + ).forEach { transform(it.setFlatShader(FaceFlat(SOUTH))).add() } + + listOf( + verticalRectangle(x1 = 0.5 - halfRadius, z1 = 0.5 - halfRadius, x2 = 0.5, z2 = 0.5 - radius, yBottom = yBottom, yTop = yTop) + .clampUV(maxU = radius - 0.5) + .setAoShader( + faceOrientedAuto(overrideFace = EAST, corner = cornerInterpolate(Axis.Y, chamferAffinity, Config.roundLogs.dimming))) + .setAoShader( + faceOrientedAuto(overrideFace = EAST, corner = cornerInterpolate(Axis.Y, 0.5f, Config.roundLogs.dimming)), + predicate = { v, vi -> vi == 0 || vi == 3} + ), + + verticalRectangle(x1 = 0.5, z1 = 0.5 - radius, x2 = 0.5, z2 = 0.0, yBottom = yBottom, yTop = yTop) + .clampUV(minU = radius - 0.5, maxU = 0.0) + .setAoShader(faceOrientedInterpolate(overrideFace = EAST)) + ).forEach { transform(it.setFlatShader(FaceFlat(EAST))).add() } + + quads.exchange(1, 2) +} + +/** + * Create a model of the side of a square column quadrant. + * + * @param[transform] transformation to apply to the model + */ +fun Model.columnSideSquare(yBottom: Double, yTop: Double, transform: (Quad) -> Quad = { it }) { + listOf( + verticalRectangle(x1 = 0.0, z1 = 0.5, x2 = 0.5, z2 = 0.5, yBottom = yBottom, yTop = yTop) + .clampUV(minU = 0.0) + .setAoShader(faceOrientedInterpolate(overrideFace = SOUTH)) + .setAoShader(faceOrientedAuto(corner = cornerAo(Axis.Y)), predicate = { v, vi -> vi == 1 || vi == 2}), + + verticalRectangle(x1 = 0.5, z1 = 0.5, x2 = 0.5, z2 = 0.0, yBottom = yBottom, yTop = yTop) + .clampUV(maxU = 0.0) + .setAoShader(faceOrientedInterpolate(overrideFace = EAST)) + .setAoShader(faceOrientedAuto(corner = cornerAo(Axis.Y)), predicate = { v, vi -> vi == 1 || vi == 2}) + ).forEach { + transform(it.setFlatShader(faceOrientedAuto(corner = cornerFlat))).add() + } +} + +/** + * Create a model of the top lid of a chamfered column quadrant. + * + * @param[radius] the chamfer radius + * @param[transform] transformation to apply to the model + */ +fun Model.columnLid(radius: Double, transform: (Quad)->Quad = { it }) { + val v1 = Vertex(Double3(0.0, 0.5, 0.0), UV(0.0, 0.0)) + val v2 = Vertex(Double3(0.0, 0.5, 0.5), UV(0.0, 0.5)) + val v3 = Vertex(Double3(0.5 - radius, 0.5, 0.5), UV(0.5 - radius, 0.5)) + val v4 = Vertex(Double3(0.5 - radius * 0.5, 0.5, 0.5 - radius * 0.5), UV(0.5, 0.5)) + val v5 = Vertex(Double3(0.5, 0.5, 0.5 - radius), UV(0.5, 0.5 - radius)) + val v6 = Vertex(Double3(0.5, 0.5, 0.0), UV(0.5, 0.0)) + + val q1 = Quad(v1, v2, v3, v4).setAoShader(faceOrientedAuto(overrideFace = UP, corner = cornerAo(Axis.Y))) + .transformVI { vertex, idx -> vertex.copy(aoShader = when(idx) { + 0 -> FaceCenter(UP) + 1 -> EdgeInterpolateFallback(UP, SOUTH, 0.0) + else -> vertex.aoShader + })} + val q2 = Quad(v1, v4, v5, v6).setAoShader(faceOrientedAuto(overrideFace = UP, corner = cornerAo(Axis.Y))) + .transformVI { vertex, idx -> vertex.copy(aoShader = when(idx) { + 0 -> FaceCenter(UP) + 3 -> EdgeInterpolateFallback(UP, EAST, 0.0) + else -> vertex.aoShader + })} + listOf(q1, q2).forEach { transform(it.setFlatShader(FaceFlat(UP))).add() } +} + +/** + * Create a model of the top lid of a square column quadrant. + * + * @param[transform] transformation to apply to the model + */ +fun Model.columnLidSquare(transform: (Quad)-> Quad = { it }) { + transform( + horizontalRectangle(x1 = 0.0, x2 = 0.5, z1 = 0.0, z2 = 0.5, y = 0.5) + .transformVI { vertex, idx -> vertex.copy(uv = UV(vertex.xyz.x, vertex.xyz.z), aoShader = when(idx) { + 0 -> FaceCenter(UP) + 1 -> EdgeInterpolateFallback(UP, SOUTH, 0.0) + 2 -> CornerSingleFallback(UP, SOUTH, EAST, UP) + else -> EdgeInterpolateFallback(UP, EAST, 0.0) + }) } + .setFlatShader(FaceFlat(UP)) + ).add() +} + +/** + * Transform a chamfered side quadrant model of a column that extends from the top of the block. + * (clamp UV coordinates, apply some scaling to avoid Z-fighting). + * + * @param[size] amount that the model extends from the top + */ +fun topExtension(size: Double) = { q: Quad -> + q.clampUV(minV = 0.5 - size).transformVI { vertex, idx -> + if (idx < 2) vertex else vertex.copy(xyz = vertex.xyz * zProtectionScale) + } +} +/** + * Transform a chamfered side quadrant model of a column that extends from the bottom of the block. + * (clamp UV coordinates, apply some scaling to avoid Z-fighting). + * + * @param[size] amount that the model extends from the bottom + */ +fun bottomExtension(size: Double) = { q: Quad -> + q.clampUV(maxV = -0.5 + size).transformVI { vertex, idx -> + if (idx > 1) vertex else vertex.copy(xyz = vertex.xyz * zProtectionScale) + } +} diff --git a/src/main/kotlin/mods/betterfoliage/client/render/RenderAlgae.kt b/src/main/kotlin/mods/betterfoliage/client/render/RenderAlgae.kt new file mode 100644 index 0000000..03f3392 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/RenderAlgae.kt @@ -0,0 +1,49 @@ +package mods.betterfoliage.client.render + +import mods.betterfoliage.BetterFoliageMod +import mods.betterfoliage.client.Client +import mods.betterfoliage.client.config.Config +import mods.betterfoliage.client.integration.ShadersModIntegration +import mods.octarinecore.client.render.* +import net.minecraft.block.material.Material +import net.minecraft.client.renderer.RenderBlocks +import net.minecraft.init.Blocks +import org.apache.logging.log4j.Level.INFO + +class RenderAlgae : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) { + + val noise = simplexNoise() + + val algaeIcons = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "better_algae_%d") + val algaeModels = modelSet(64, RenderGrass.grassTopQuads) + + override fun afterStitch() { + Client.log(INFO, "Registered ${algaeIcons.num} algae textures") + } + + override fun isEligible(ctx: BlockContext) = + Config.enabled && Config.algae.enabled && + ctx.cameraDistance < Config.algae.distance && + ctx.block(up2).material == Material.water && + ctx.block(up1).material == Material.water && + Config.blocks.dirt.matchesID(ctx.block) && + ctx.biomeId in Config.algae.biomes && + noise[ctx.x, ctx.z] < Config.algae.population + + override fun render(ctx: BlockContext, parent: RenderBlocks): Boolean { + if (renderWorldBlockBase(parent, face = alwaysRender)) return true + + val rand = ctx.semiRandomArray(3) + + ShadersModIntegration.grass(Config.algae.shaderWind) { + modelRenderer.render( + algaeModels[rand[2]], + Rotation.identity, + icon = { ctx, qi, q -> algaeIcons[rand[qi and 1]]!! }, + rotateUV = { 0 }, + postProcess = noPost + ) + } + return true + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/render/RenderCactus.kt b/src/main/kotlin/mods/betterfoliage/client/render/RenderCactus.kt new file mode 100644 index 0000000..9fbddeb --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/RenderCactus.kt @@ -0,0 +1,83 @@ +package mods.betterfoliage.client.render + +import mods.betterfoliage.BetterFoliageMod +import mods.betterfoliage.client.Client +import mods.betterfoliage.client.config.Config +import mods.octarinecore.client.render.* +import net.minecraft.client.renderer.RenderBlocks +import net.minecraftforge.common.util.ForgeDirection.* +import org.apache.logging.log4j.Level + +class RenderCactus : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) { + + val cactusStemRadius = 0.4375 + val cactusArmRotation = listOf(NORTH, SOUTH, EAST, WEST).map { Rotation.rot90[it.ordinal] } + + val iconCross = iconStatic(BetterFoliageMod.LEGACY_DOMAIN, "better_cactus") + val iconArm = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "better_cactus_arm_%d") + + val modelStem = model { + horizontalRectangle(x1 = -cactusStemRadius, x2 = cactusStemRadius, z1 = -cactusStemRadius, z2 = cactusStemRadius, y = 0.5) + .scaleUV(cactusStemRadius * 2.0) + .let { listOf(it.flipped.move(1.0 to DOWN), it) } + .forEach { it.setAoShader(faceOrientedAuto(corner = cornerAo(Axis.Y), edge = null)).add() } + + verticalRectangle(x1 = -0.5, z1 = cactusStemRadius, x2 = 0.5, z2 = cactusStemRadius, yBottom = -0.5, yTop = 0.5) + .setAoShader(faceOrientedAuto(corner = cornerAo(Axis.Y), edge = null)) + .toCross(UP).addAll() + } + val modelCross = modelSet(64) { modelIdx -> + verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = -0.5 * 1.41, yTop = 0.5 * 1.41) + .setAoShader(edgeOrientedAuto(corner = cornerAoMaxGreen)) + .scale(1.4) + .transformV { v -> + val perturb = xzDisk(modelIdx) * Config.cactus.sizeVariation + Vertex(v.xyz + (if (v.uv.u < 0.0) perturb else -perturb), v.uv, v.aoShader) + } + .toCross(UP).addAll() + } + val modelArm = modelSet(64) { modelIdx -> + verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = 0.0, yTop = 1.0) + .scale(Config.cactus.size).move(0.5 to UP) + + .setAoShader(faceOrientedAuto(overrideFace = UP, corner = cornerAo(Axis.Y), edge = null)) + .toCross(UP) { it.move(xzDisk(modelIdx) * Config.cactus.hOffset) }.addAll() + } + + override fun afterStitch() { + Client.log(Level.INFO, "Registered ${iconArm.num} cactus arm textures") + } + + override fun isEligible(ctx: BlockContext): Boolean = + Config.enabled && Config.cactus.enabled && + ctx.cameraDistance < Config.cactus.distance && + Config.blocks.cactus.matchesID(ctx.block) + + override fun render(ctx: BlockContext, parent: RenderBlocks): Boolean { + // get AO data + if (renderWorldBlockBase(parent, face = neverRender)) return true + + modelRenderer.render( + modelStem.model, + Rotation.identity, + icon = { ctx, qi, q -> ctx.icon(forgeDirs[qi])}, + rotateUV = { 0 }, + postProcess = noPost + ) + modelRenderer.render( + modelCross[ctx.random(0)], + Rotation.identity, + icon = { ctx, qi, q -> iconCross.icon!!}, + rotateUV = { 0 }, + postProcess = noPost + ) + modelRenderer.render( + modelArm[ctx.random(1)], + cactusArmRotation[ctx.random(2) % 4], + icon = { ctx2, qi, q -> iconArm[ctx.random(3)]!!}, + rotateUV = { 0 }, + postProcess = noPost + ) + return true + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/render/RenderConnectedGrass.kt b/src/main/kotlin/mods/betterfoliage/client/render/RenderConnectedGrass.kt new file mode 100644 index 0000000..f80eed6 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/RenderConnectedGrass.kt @@ -0,0 +1,23 @@ +package mods.betterfoliage.client.render + +import mods.betterfoliage.BetterFoliageMod +import mods.betterfoliage.client.config.Config +import mods.octarinecore.client.render.* +import net.minecraft.block.material.Material +import net.minecraft.client.renderer.RenderBlocks + +class RenderConnectedGrass : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) { + override fun isEligible(ctx: BlockContext) = + Config.enabled && Config.connectedGrass.enabled && + Config.blocks.dirt.matchesID(ctx.block) && + Config.blocks.grass.matchesID(ctx.block(up1)) && + (Config.connectedGrass.snowEnabled || !ctx.block(up2).isSnow) + + override fun render(ctx: BlockContext, parent: RenderBlocks): Boolean { + return ctx.withOffset(Int3.zero, up1) { + ctx.withOffset(up1, up2) { + renderWorldBlockBase(parent, face = alwaysRender) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/render/RenderConnectedGrassLog.kt b/src/main/kotlin/mods/betterfoliage/client/render/RenderConnectedGrassLog.kt new file mode 100644 index 0000000..208ae65 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/RenderConnectedGrassLog.kt @@ -0,0 +1,31 @@ +package mods.betterfoliage.client.render + +import mods.betterfoliage.BetterFoliageMod +import mods.betterfoliage.client.config.Config +import mods.octarinecore.client.render.* +import net.minecraft.client.renderer.RenderBlocks +import net.minecraftforge.common.util.ForgeDirection.* + +class RenderConnectedGrassLog : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) { + + val grassCheckDirs = listOf(EAST, WEST, NORTH, SOUTH) + + override fun isEligible(ctx: BlockContext) = + Config.enabled && Config.roundLogs.enabled && Config.roundLogs.connectGrass && + Config.blocks.dirt.matchesID(ctx.block) && + Config.blocks.logs.matchesID(ctx.block(up1)) + + override fun render(ctx: BlockContext, parent: RenderBlocks): Boolean { + val grassDir = grassCheckDirs.find { + Config.blocks.grass.matchesID(ctx.block(it.offset)) + } + + return if (grassDir != null) { + ctx.withOffset(Int3.zero, grassDir.offset) { + renderWorldBlockBase(parent, face = alwaysRender) + } + } else { + renderWorldBlockBase(parent, face = alwaysRender) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/render/RenderCoral.kt b/src/main/kotlin/mods/betterfoliage/client/render/RenderCoral.kt new file mode 100644 index 0000000..68383b1 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/RenderCoral.kt @@ -0,0 +1,65 @@ +package mods.betterfoliage.client.render + +import mods.betterfoliage.BetterFoliageMod +import mods.betterfoliage.client.Client +import mods.betterfoliage.client.config.Config +import mods.octarinecore.client.render.* +import mods.octarinecore.random +import net.minecraft.block.material.Material +import net.minecraft.client.renderer.RenderBlocks +import net.minecraftforge.common.util.ForgeDirection.UP +import org.apache.logging.log4j.Level.INFO + +class RenderCoral : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) { + + val noise = simplexNoise() + + val coralIcons = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "better_coral_%d") + val crustIcons = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "better_crust_%d") + val coralModels = modelSet(64) { modelIdx -> + verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = 0.0, yTop = 1.0) + .scale(Config.coral.size).move(0.5 to UP) + .toCross(UP) { it.move(xzDisk(modelIdx) * Config.coral.hOffset) }.addAll() + + val separation = random(0.01, Config.coral.vOffset) + horizontalRectangle(x1 = -0.5, x2 = 0.5, z1 = -0.5, z2 = 0.5, y = 0.0) + .scale(Config.coral.crustSize).move(0.5 + separation to UP).add() + + transformQ { + it.setAoShader(faceOrientedAuto(overrideFace = UP, corner = cornerAo(Axis.Y))) + .setFlatShader(faceOrientedAuto(overrideFace = UP, corner = cornerFlat)) + } + } + + override fun afterStitch() { + Client.log(INFO, "Registered ${coralIcons.num} algae textures") + } + + override fun isEligible(ctx: BlockContext) = + Config.enabled && Config.coral.enabled && + ctx.cameraDistance < Config.coral.distance && + ctx.block(up2).material == Material.water && + ctx.block(up1).material == Material.water && + Config.blocks.sand.matchesID(ctx.block) && + ctx.biomeId in Config.coral.biomes && + noise[ctx.x, ctx.z] < Config.coral.population + + override fun render(ctx: BlockContext, parent: RenderBlocks): Boolean { + if (renderWorldBlockBase(parent, face = alwaysRender)) return true + + forgeDirs.forEachIndexed { idx, face -> + if (!ctx.block(forgeDirOffsets[idx]).isOpaqueCube && blockContext.random(idx) < Config.coral.chance) { + var variation = blockContext.random(6) + modelRenderer.render( + coralModels[variation++], + rotationFromUp[idx], + icon = { ctx, qi, q -> if (qi == 4) crustIcons[variation]!! else coralIcons[variation + (qi and 1)]!!}, + rotateUV = { 0 }, + postProcess = noPost + ) + } + } + + return true + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/render/RenderGrass.kt b/src/main/kotlin/mods/betterfoliage/client/render/RenderGrass.kt new file mode 100644 index 0000000..f4282c0 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/RenderGrass.kt @@ -0,0 +1,106 @@ +package mods.betterfoliage.client.render + +import mods.betterfoliage.BetterFoliageMod +import mods.betterfoliage.client.Client +import mods.betterfoliage.client.config.Config +import mods.betterfoliage.client.integration.ShadersModIntegration +import mods.betterfoliage.client.texture.GrassRegistry +import mods.octarinecore.client.render.* +import mods.octarinecore.random +import net.minecraft.block.material.Material +import net.minecraft.client.renderer.RenderBlocks +import net.minecraftforge.common.util.ForgeDirection.UP +import org.apache.logging.log4j.Level.INFO + +class RenderGrass : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) { + + companion object { + @JvmStatic val grassTopQuads: Model.(Int)->Unit = { modelIdx -> + verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = 0.5, + yTop = 0.5 + random(Config.shortGrass.heightMin, Config.shortGrass.heightMax) + ) + .setAoShader(faceOrientedAuto(overrideFace = UP, corner = cornerAo(Axis.Y))) + .setFlatShader(faceOrientedAuto(overrideFace = UP, corner = cornerFlat)) + .toCross(UP) { it.move(xzDisk(modelIdx) * Config.shortGrass.hOffset) }.addAll() + } + } + + val normalIcons = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "better_grass_long_%d") + val snowedIcons = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "better_grass_snowed_%d") + val normalGenIcon = iconStatic(Client.genGrass.generatedResource("minecraft:tallgrass", "snowed" to false)) + val snowedGenIcon = iconStatic(Client.genGrass.generatedResource("minecraft:tallgrass", "snowed" to true)) + + val grassModels = modelSet(64, grassTopQuads) + + override fun afterStitch() { + Client.log(INFO, "Registered ${normalIcons.num} grass textures") + Client.log(INFO, "Registered ${snowedIcons.num} snowed grass textures") + } + + override fun isEligible(ctx: BlockContext) = + Config.enabled && + ctx.cameraDistance < Config.shortGrass.distance && + (Config.shortGrass.grassEnabled || Config.connectedGrass.enabled) && + Config.blocks.grass.matchesID(ctx.block) + + override fun render(ctx: BlockContext, parent: RenderBlocks): Boolean { + val isConnected = ctx.block(down1).let { Config.blocks.dirt.matchesID(it) || Config.blocks.grass.matchesID(it) } + val isSnowed = ctx.block(up1).isSnow + val connectedGrass = isConnected && Config.connectedGrass.enabled && (!isSnowed || Config.connectedGrass.snowEnabled) + + val grassInfo = GrassRegistry.grass[ctx.icon(UP)] + if (grassInfo == null) { + renderWorldBlockBase(parent, face = alwaysRender) + return true + } + val cubeTexture = if (isSnowed) ctx.icon(up1, UP) else null ?: grassInfo.grassTopTexture + val blockColor = ctx.blockColor(Int3.zero) + + if (connectedGrass) { + // get AO data + if (renderWorldBlockBase(parent, face = neverRender)) return true + + // render full grass block + modelRenderer.render( + fullCube, + Rotation.identity, + ctx.blockCenter, + icon = { ctx, qi, q -> cubeTexture }, + rotateUV = { 2 }, + postProcess = { ctx, qi, q, vi, v -> + if (isSnowed) { if(!ctx.aoEnabled) setGrey(1.4f) } + else if (qi != UP.ordinal && ctx.aoEnabled) multiplyColor(blockColor) + } + ) + } else { + // render normally + if (renderWorldBlockBase(parent, face = alwaysRender)) return true + } + + if (!Config.shortGrass.grassEnabled) return true + if (isSnowed && !Config.shortGrass.snowEnabled) return true + if (ctx.block(up1).isOpaqueCube) return true + + // render grass quads + val iconset = if (isSnowed) snowedIcons else normalIcons + val iconGen = if (isSnowed) snowedGenIcon else normalGenIcon + val rand = ctx.semiRandomArray(2) + + ShadersModIntegration.grass(Config.shortGrass.shaderWind) { + modelRenderer.render( + grassModels[rand[0]], + Rotation.identity, + ctx.blockCenter + (if (isSnowed) snowOffset else Double3.zero), + icon = if (Config.shortGrass.useGenerated) + { ctx: ShadingContext, qi: Int, q: Quad -> iconGen.icon!! } + else + { ctx: ShadingContext, qi: Int, q: Quad -> iconset[rand[qi and 1]]!! }, + rotateUV = { 0 }, + postProcess = if (isSnowed) whitewash else if (grassInfo.overrideColor == null) noPost else + { ctx, qi, q, vi, v -> multiplyColor(grassInfo.overrideColor) } + ) + } + + return true + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/render/RenderLeaves.kt b/src/main/kotlin/mods/betterfoliage/client/render/RenderLeaves.kt new file mode 100644 index 0000000..9cfad37 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/RenderLeaves.kt @@ -0,0 +1,72 @@ +package mods.betterfoliage.client.render + +import mods.betterfoliage.BetterFoliageMod +import mods.betterfoliage.client.config.Config +import mods.betterfoliage.client.integration.ShadersModIntegration +import mods.betterfoliage.client.texture.LeafRegistry +import mods.octarinecore.PI2 +import mods.octarinecore.client.render.* +import mods.octarinecore.random +import net.minecraft.block.material.Material +import net.minecraft.client.renderer.RenderBlocks +import net.minecraftforge.common.util.ForgeDirection.DOWN +import net.minecraftforge.common.util.ForgeDirection.UP +import java.lang.Math.cos +import java.lang.Math.sin + +class RenderLeaves : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) { + + val leavesModel = model { + verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = -0.5 * 1.41, yTop = 0.5 * 1.41) + .setAoShader(edgeOrientedAuto(corner = cornerAoMaxGreen)) + .setFlatShader(FlatOffset(Int3.zero)) + .scale(Config.leaves.size) + .toCross(UP).addAll() + } + val snowedIcon = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "better_leaves_snowed_%d") + + val perturbs = vectorSet(64) { idx -> + val angle = PI2 * idx / 64.0 + Double3(cos(angle), 0.0, sin(angle)) * Config.leaves.hOffset + + UP.vec * random(-1.0, 1.0) * Config.leaves.vOffset + } + + override fun isEligible(ctx: BlockContext) = + Config.enabled && + Config.leaves.enabled && + ctx.cameraDistance < Config.leaves.distance && + Config.blocks.leaves.matchesID(ctx.block) + + override fun render(ctx: BlockContext, parent: RenderBlocks): Boolean { + val isSnowed = ctx.block(up1).material.let { + it == Material.snow || it == Material.craftedSnow + } + + if (renderWorldBlockBase(parent, face = alwaysRender)) return true + + val leafInfo = LeafRegistry.leaves[ctx.icon(DOWN)] + if (leafInfo != null) ShadersModIntegration.leaves { + val rand = ctx.semiRandomArray(2) + (if (Config.leaves.dense) denseLeavesRot else normalLeavesRot).forEach { rotation -> + modelRenderer.render( + leavesModel.model, + rotation, + ctx.blockCenter + perturbs[rand[0]], + icon = { ctx, qi, q -> leafInfo.roundLeafTexture }, + rotateUV = { q -> rand[1] }, + postProcess = noPost + ) + } + if (isSnowed) modelRenderer.render( + leavesModel.model, + Rotation.identity, + ctx.blockCenter + perturbs[rand[0]], + icon = { ctx, qi, q -> snowedIcon[rand[1]]!! }, + rotateUV = { 0 }, + postProcess = whitewash + ) + } + + return true + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/render/RenderLilypad.kt b/src/main/kotlin/mods/betterfoliage/client/render/RenderLilypad.kt new file mode 100644 index 0000000..19cc6ac --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/RenderLilypad.kt @@ -0,0 +1,64 @@ +package mods.betterfoliage.client.render + +import mods.betterfoliage.BetterFoliageMod +import mods.betterfoliage.client.Client +import mods.betterfoliage.client.config.Config +import mods.octarinecore.client.render.* +import net.minecraft.client.renderer.RenderBlocks +import net.minecraftforge.common.util.ForgeDirection.* +import org.apache.logging.log4j.Level + +class RenderLilypad : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) { + + val rootModel = model { + verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = -1.5, yTop = -0.5) + .setFlatShader(FlatOffsetNoColor(Int3.zero)) + .toCross(UP).addAll() + } + val flowerModel = model { + verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = 0.0, yTop = 1.0) + .scale(0.5).move(0.5 to DOWN) + .setFlatShader(FlatOffsetNoColor(Int3.zero)) + .toCross(UP).addAll() + } + val rootIcon = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "better_lilypad_roots_%d") + val flowerIcon = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "better_lilypad_flower_%d") + val perturbs = vectorSet(64) { modelIdx -> xzDisk(modelIdx) * Config.lilypad.hOffset } + + override fun afterStitch() { + Client.log(Level.INFO, "Registered ${rootIcon.num} lilypad root textures") + Client.log(Level.INFO, "Registered ${flowerIcon.num} lilypad flower textures") + } + + override fun isEligible(ctx: BlockContext): Boolean = + Config.enabled && Config.lilypad.enabled && + ctx.cameraDistance < Config.lilypad.distance && + Config.blocks.lilypad.matchesID(ctx.block) + + override fun render(ctx: BlockContext, parent: RenderBlocks): Boolean { + if (renderWorldBlockBase(parent, face = alwaysRender)) return true + + val rand = ctx.semiRandomArray(5) + modelRenderer.render( + rootModel.model, + Rotation.identity, + ctx.blockCenter.add(perturbs[rand[2]]), + forceFlat = true, + icon = { ctx, qi, q -> rootIcon[rand[qi and 1]]!! }, + rotateUV = { 0 }, + postProcess = noPost + ) + + if (rand[3] < Config.lilypad.flowerChance) modelRenderer.render( + flowerModel.model, + Rotation.identity, + ctx.blockCenter.add(perturbs[rand[4]]), + forceFlat = true, + icon = { ctx, qi, q -> flowerIcon[rand[0]]!! }, + rotateUV = { 0 }, + postProcess = noPost + ) + + return true + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/render/RenderLog.kt b/src/main/kotlin/mods/betterfoliage/client/render/RenderLog.kt new file mode 100644 index 0000000..0a74550 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/RenderLog.kt @@ -0,0 +1,30 @@ +package mods.betterfoliage.client.render + +import mods.betterfoliage.BetterFoliageMod +import mods.betterfoliage.client.config.Config +import mods.octarinecore.client.render.Axis +import mods.octarinecore.client.render.BlockContext +import net.minecraft.block.Block + +class RenderLog : AbstractRenderColumn(BetterFoliageMod.MOD_ID) { + + override fun isEligible(ctx: BlockContext) = + Config.enabled && Config.roundLogs.enabled && + ctx.cameraDistance < Config.roundLogs.distance && + Config.blocks.logs.matchesID(ctx.block) + + override var axisFunc = { block: Block, meta: Int -> when ((meta shr 2) and 3) { + 1 -> Axis.X + 2 -> Axis.Z + else -> Axis.Y + } } + + override val blockPredicate = { block: Block, meta: Int -> Config.blocks.logs.matchesID(block) } + override val surroundPredicate = { block: Block -> block.isOpaqueCube && !Config.blocks.logs.matchesID(block) } + + override val connectPerpendicular: Boolean get() = Config.roundLogs.connectPerpendicular + override val connectSolids: Boolean get() = Config.roundLogs.connectSolids + override val lenientConnect: Boolean get() = Config.roundLogs.lenientConnect + override val radiusLarge: Double get() = Config.roundLogs.radiusLarge + override val radiusSmall: Double get() = Config.roundLogs.radiusSmall +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/render/RenderMycelium.kt b/src/main/kotlin/mods/betterfoliage/client/render/RenderMycelium.kt new file mode 100644 index 0000000..84cc053 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/RenderMycelium.kt @@ -0,0 +1,48 @@ +package mods.betterfoliage.client.render + +import mods.betterfoliage.BetterFoliageMod +import mods.betterfoliage.client.Client +import mods.betterfoliage.client.config.Config +import mods.octarinecore.client.render.* +import net.minecraft.block.material.Material +import net.minecraft.client.renderer.RenderBlocks +import net.minecraft.init.Blocks +import org.apache.logging.log4j.Level.INFO + +class RenderMycelium : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) { + + val myceliumIcon = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "better_mycel_%d") + val myceliumModel = modelSet(64, RenderGrass.grassTopQuads) + + override fun afterStitch() { + Client.log(INFO, "Registered ${myceliumIcon.num} mycelium textures") + } + + override fun isEligible(ctx: BlockContext): Boolean { + if (!Config.enabled || !Config.shortGrass.myceliumEnabled) return false + return ctx.block == Blocks.mycelium && + ctx.cameraDistance < Config.shortGrass.distance + } + + override fun render(ctx: BlockContext, parent: RenderBlocks): Boolean { + val isSnowed = ctx.block(up1).material.let { + it == Material.snow || it == Material.craftedSnow + } + + if (renderWorldBlockBase(parent, face = alwaysRender)) return true + if (isSnowed && !Config.shortGrass.snowEnabled) return true + if (ctx.block(up1).isOpaqueCube) return true + + val rand = ctx.semiRandomArray(2) + modelRenderer.render( + myceliumModel[rand[0]], + Rotation.identity, + ctx.blockCenter + (if (isSnowed) snowOffset else Double3.zero), + icon = { ctx, qi, q -> myceliumIcon[rand[qi and 1]]!! }, + rotateUV = { 0 }, + postProcess = if (isSnowed) whitewash else noPost + ) + + return true + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/render/RenderNetherrack.kt b/src/main/kotlin/mods/betterfoliage/client/render/RenderNetherrack.kt new file mode 100644 index 0000000..67eaa68 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/RenderNetherrack.kt @@ -0,0 +1,51 @@ +package mods.betterfoliage.client.render + +import mods.betterfoliage.BetterFoliageMod +import mods.betterfoliage.client.Client +import mods.betterfoliage.client.config.Config +import mods.octarinecore.client.render.* +import mods.octarinecore.random +import net.minecraft.client.renderer.RenderBlocks +import net.minecraft.init.Blocks +import net.minecraftforge.common.util.ForgeDirection.DOWN +import net.minecraftforge.common.util.ForgeDirection.UP +import org.apache.logging.log4j.Level.INFO + +class RenderNetherrack : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) { + + val netherrackIcon = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "better_netherrack_%d") + val netherrackModel = modelSet(64) { modelIdx -> + verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yTop = -0.5, + yBottom = -0.5 - random(Config.netherrack.heightMin, Config.netherrack.heightMax)) + .setAoShader(faceOrientedAuto(overrideFace = DOWN, corner = cornerAo(Axis.Y))) + .setFlatShader(faceOrientedAuto(overrideFace = DOWN, corner = cornerFlat)) + .toCross(UP) { it.move(xzDisk(modelIdx) * Config.shortGrass.hOffset) }.addAll() + + } + + override fun afterStitch() { + Client.log(INFO, "Registered ${netherrackIcon.num} netherrack textures") + } + + override fun isEligible(ctx: BlockContext): Boolean { + if (!Config.enabled || !Config.netherrack.enabled) return false + return ctx.block == Blocks.netherrack && + ctx.cameraDistance < Config.netherrack.distance + } + + override fun render(ctx: BlockContext, parent: RenderBlocks): Boolean { + if (renderWorldBlockBase(parent, face = alwaysRender)) return true + if (ctx.block(down1).isOpaqueCube) return true + + val rand = ctx.semiRandomArray(2) + modelRenderer.render( + netherrackModel[rand[0]], + Rotation.identity, + icon = { ctx, qi, q -> netherrackIcon[rand[qi and 1]]!! }, + rotateUV = { 0 }, + postProcess = noPost + ) + + return true + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/render/RenderReeds.kt b/src/main/kotlin/mods/betterfoliage/client/render/RenderReeds.kt new file mode 100644 index 0000000..13cf9a8 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/RenderReeds.kt @@ -0,0 +1,65 @@ +package mods.betterfoliage.client.render + +import mods.betterfoliage.BetterFoliageMod +import mods.betterfoliage.client.Client +import mods.betterfoliage.client.config.Config +import mods.betterfoliage.client.integration.ShadersModIntegration +import mods.octarinecore.client.render.* +import mods.octarinecore.random +import net.minecraft.block.material.Material +import net.minecraft.client.renderer.RenderBlocks +import net.minecraftforge.common.util.ForgeDirection.UP +import org.apache.logging.log4j.Level + +class RenderReeds : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) { + + val noise = simplexNoise() + val reedIcons = iconSet(Client.genReeds.generatedResource("${BetterFoliageMod.LEGACY_DOMAIN}:better_reed_%d")) + val reedModels = modelSet(64) { modelIdx -> + val height = random(Config.reed.heightMin, Config.reed.heightMax) + val waterline = 0.875f + val vCutLine = 0.5 - waterline / height + listOf( + // below waterline + verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = 0.5, yTop = 0.5 + waterline) + .setFlatShader(FlatOffsetNoColor(up1)).clampUV(minV = vCutLine), + + // above waterline + verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = 0.5 + waterline, yTop = 0.5 + height) + .setFlatShader(FlatOffsetNoColor(up2)).clampUV(maxV = vCutLine) + ).forEach { + it.clampUV(minU = -0.25, maxU = 0.25) + .toCross(UP) { it.move(xzDisk(modelIdx) * Config.reed.hOffset) }.addAll() + } + } + + override fun afterStitch() { + Client.log(Level.INFO, "Registered ${reedIcons.num} reed textures") + } + + override fun isEligible(ctx: BlockContext) = + Config.enabled && Config.reed.enabled && + ctx.cameraDistance < Config.reed.distance && + ctx.block(up2).material == Material.air && + ctx.block(up1).material == Material.water && + Config.blocks.dirt.matchesID(ctx.block) && + ctx.biomeId in Config.reed.biomes && + noise[ctx.x, ctx.z] < Config.reed.population + + override fun render(ctx: BlockContext, parent: RenderBlocks): Boolean { + if (renderWorldBlockBase(parent, face = alwaysRender)) return true + + val iconVar = ctx.random(1) + ShadersModIntegration.grass(Config.reed.shaderWind) { + modelRenderer.render( + reedModels[ctx.random(0)], + Rotation.identity, + forceFlat = true, + icon = { ctx, qi, q -> reedIcons[iconVar]!! }, + rotateUV = { 0 }, + postProcess = noPost + ) + } + return true + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/render/Utils.kt b/src/main/kotlin/mods/betterfoliage/client/render/Utils.kt new file mode 100644 index 0000000..0982195 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/Utils.kt @@ -0,0 +1,54 @@ +@file:JvmName("Utils") +package mods.betterfoliage.client.render + +import mods.octarinecore.PI2 +import mods.octarinecore.client.render.* +import net.minecraft.block.Block +import net.minecraft.block.material.Material +import net.minecraft.tileentity.TileEntity +import net.minecraft.world.IBlockAccess +import net.minecraft.world.biome.BiomeGenBase +import net.minecraftforge.common.util.ForgeDirection +import net.minecraftforge.common.util.ForgeDirection.* + +val up1 = Int3(1 to UP) +val up2 = Int3(2 to UP) +val down1 = Int3(1 to DOWN) +val snowOffset = UP * 0.0625 + +val normalLeavesRot = arrayOf(Rotation.identity) +val denseLeavesRot = arrayOf(Rotation.identity, Rotation.rot90[EAST.ordinal], Rotation.rot90[SOUTH.ordinal]) + +val whitewash: RenderVertex.(ShadingContext, Int, Quad, Int, Vertex)->Unit = { ctx, qi, q, vi, v -> setGrey(1.4f) } +val greywash: RenderVertex.(ShadingContext, Int, Quad, Int, Vertex)->Unit = { ctx, qi, q, vi, v -> setGrey(1.0f) } + +val Block.isSnow: Boolean get() = material.let { it == Material.snow || it == Material.craftedSnow } + +fun Quad.toCross(rotAxis: ForgeDirection, trans: (Quad)->Quad) = + (0..3).map { rotIdx -> + trans(rotate(Rotation.rot90[rotAxis.ordinal] * rotIdx).mirrorUV(rotIdx > 1, false)) + } +fun Quad.toCross(rotAxis: ForgeDirection) = toCross(rotAxis) { it } + +fun xzDisk(modelIdx: Int) = (PI2 * modelIdx / 64.0).let { Double3(Math.cos(it), 0.0, Math.sin(it)) } + +val rotationFromUp = arrayOf( + Rotation.rot90[EAST.ordinal] * 2, + Rotation.identity, + Rotation.rot90[WEST.ordinal], + Rotation.rot90[EAST.ordinal], + Rotation.rot90[SOUTH.ordinal], + Rotation.rot90[NORTH.ordinal] +) + +fun Model.mix(first: Model, second: Model, predicate: (Int)->Boolean) { + first.quads.forEachIndexed { qi, quad -> + val otherQuad = second.quads[qi] + Quad( + if (predicate(0)) otherQuad.v1.copy() else quad.v1.copy(), + if (predicate(1)) otherQuad.v2.copy() else quad.v2.copy(), + if (predicate(2)) otherQuad.v3.copy() else quad.v3.copy(), + if (predicate(3)) otherQuad.v4.copy() else quad.v4.copy() + ).add() + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/texture/GrassGenerator.kt b/src/main/kotlin/mods/betterfoliage/client/texture/GrassGenerator.kt new file mode 100644 index 0000000..b0d6c6d --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/texture/GrassGenerator.kt @@ -0,0 +1,35 @@ +package mods.betterfoliage.client.texture + +import mods.octarinecore.client.resource.* +import net.minecraft.util.ResourceLocation +import java.awt.image.BufferedImage + +/** + * Generate Short Grass textures from [Blocks.tallgrass] block textures. + * The bottom 3/8 of the base texture is chopped off. + * + * @param[domain] Resource domain of generator + */ +class GrassGenerator(domain: String) : TextureGenerator(domain) { + + override fun generate(params: ParameterList): BufferedImage? { + val target = targetResource(params)!! + val isSnowed = params["snowed"]?.toBoolean() ?: false + + val baseTexture = resourceManager[target.second]?.loadImage() ?: return null + + // draw bottom half of texture + val result = BufferedImage(baseTexture.width, baseTexture.height, BufferedImage.TYPE_4BYTE_ABGR) + val graphics = result.createGraphics() + graphics.drawImage(baseTexture, 0, 3 * baseTexture.height / 8, null) + + // blend with white if snowed + if (isSnowed && target.first == ResourceType.COLOR) { + for (x in 0..result.width - 1) for (y in 0..result.height - 1) { + result[x, y] = blendRGB(result[x, y], 16777215, 2, 3) + } + } + + return result + } +} diff --git a/src/main/kotlin/mods/betterfoliage/client/texture/GrassRegistry.kt b/src/main/kotlin/mods/betterfoliage/client/texture/GrassRegistry.kt new file mode 100644 index 0000000..3d72fc8 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/texture/GrassRegistry.kt @@ -0,0 +1,69 @@ +package mods.betterfoliage.client.texture + +import cpw.mods.fml.common.eventhandler.SubscribeEvent +import cpw.mods.fml.relauncher.Side +import cpw.mods.fml.relauncher.SideOnly +import mods.betterfoliage.client.Client +import mods.betterfoliage.client.config.Config +import mods.octarinecore.client.render.HSB +import mods.octarinecore.client.resource.averageColor +import net.minecraft.block.Block +import net.minecraft.client.renderer.texture.TextureAtlasSprite +import net.minecraft.client.renderer.texture.TextureMap +import net.minecraft.util.IIcon +import net.minecraftforge.client.event.TextureStitchEvent +import net.minecraftforge.common.MinecraftForge +import org.apache.logging.log4j.Level.DEBUG +import org.apache.logging.log4j.Level.INFO + +const val defaultGrassColor = 0 + +/** Rendering-related information for a grass block. */ +class GrassInfo( + /** Top texture of the grass block. */ + val grassTopTexture: TextureAtlasSprite, + + /** + * Color to use for Short Grass rendering instead of the biome color. + * + * Value is null if the texture is mostly grey (the saturation of its average color is under a configurable limit), + * the average color of the texture (significantly brightened) otherwise. + */ + val overrideColor: Int? +) + +/** Collects and manages rendering-related information for grass blocks. */ +@SideOnly(Side.CLIENT) +object GrassRegistry { + + val grass: MutableMap = hashMapOf() + + init { + MinecraftForge.EVENT_BUS.register(this) + } + + @SubscribeEvent + fun handleTextureReload(event: TextureStitchEvent.Pre) { + if (event.map.textureType != 0) return + grass.clear() + Client.log(INFO, "Inspecting grass textures") + + Block.blockRegistry.forEach { block -> + if (Config.blocks.grass.matchesClass(block as Block)) { + block.registerBlockIcons { location -> + val original = event.map.getTextureExtry(location) + Client.log(DEBUG, "Found grass texture: $location") + registerGrass(event.map, original) + return@registerBlockIcons original + } + } + } + } + + fun registerGrass(atlas: TextureMap, icon: TextureAtlasSprite) { + val hsb = HSB.fromColor(icon.averageColor ?: defaultGrassColor) + val overrideColor = if (hsb.saturation > Config.shortGrass.saturationThreshold) hsb.copy(brightness = 0.8f).asColor else null + grass.put(icon, GrassInfo(icon, overrideColor)) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/texture/LeafGenerator.kt b/src/main/kotlin/mods/betterfoliage/client/texture/LeafGenerator.kt new file mode 100644 index 0000000..1ef79dd --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/texture/LeafGenerator.kt @@ -0,0 +1,77 @@ +package mods.betterfoliage.client.texture + +import mods.betterfoliage.BetterFoliageMod +import mods.octarinecore.client.resource.* +import mods.octarinecore.stripStart +import net.minecraft.util.ResourceLocation +import java.awt.image.BufferedImage + +/** + * Generate round leaf textures from leaf block textures. + * The base texture is tiled 2x2, then parts of it are made transparent by applying a mask to the alpha channel. + * + * Generator parameter _type_: Leaf type (configurable by user). Different leaf types may have their own alpha mask. + * + * @param[domain] Resource domain of generator + */ +class LeafGenerator(domain: String) : TextureGenerator(domain) { + + override fun generate(params: ParameterList): BufferedImage? { + val target = targetResource(params)!! + val leafType = params["type"] ?: "default" + + val handDrawnLoc = target.second.stripStart("textures/").stripStart("blocks/").let { + ResourceLocation(BetterFoliageMod.DOMAIN, "textures/blocks/${it.resourceDomain}/${it.resourcePath}") + } + resourceManager[handDrawnLoc]?.loadImage()?.let { return it } + + val baseTexture = resourceManager[target.second]?.loadImage() ?: return null + val size = baseTexture.width + val frames = baseTexture.height / size + + val maskTexture = (getLeafMask(leafType, size * 2) ?: getLeafMask("default", size * 2))?.loadImage() + fun scale(i: Int) = i * maskTexture!!.width / (size * 2) + + val leafTexture = BufferedImage(size * 2, size * 2 * frames, BufferedImage.TYPE_4BYTE_ABGR) + val graphics = leafTexture.createGraphics() + + // iterate all frames + for (frame in 0 .. frames - 1) { + val baseFrame = baseTexture.getSubimage(0, size * frame, size, size) + val leafFrame = BufferedImage(size * 2, size * 2, BufferedImage.TYPE_4BYTE_ABGR) + + // tile leaf texture 2x2 + leafFrame.createGraphics().apply { + drawImage(baseFrame, 0, 0, null) + drawImage(baseFrame, 0, size, null) + drawImage(baseFrame, size, 0, null) + drawImage(baseFrame, size, size, null) + } + + // overlay alpha mask + if (target.first == ResourceType.COLOR && maskTexture != null) { + for (x in 0 .. size * 2 - 1) for (y in 0 .. size * 2 - 1) { + val basePixel = leafFrame[x, y].toLong() and 0xFFFFFFFFL + val maskPixel = maskTexture[scale(x), scale(y)].toLong() and 0xFF000000L or 0xFFFFFFL + leafFrame[x, y] = (basePixel and maskPixel).toInt() + } + } + + // add to animated png + graphics.drawImage(leafFrame, 0, size * frame * 2, null) + } + + return leafTexture + } + + /** + * Get the alpha mask to use + * + * @param[type] Alpha mask type. + * @param[maxSize] Preferred mask size. + */ + fun getLeafMask(type: String, maxSize: Int) = getMultisizeTexture(maxSize) { size -> + ResourceLocation(BetterFoliageMod.DOMAIN, "textures/blocks/leafmask_${size}_${type}.png") + } + +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/texture/LeafRegistry.kt b/src/main/kotlin/mods/betterfoliage/client/texture/LeafRegistry.kt new file mode 100644 index 0000000..3da9dab --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/texture/LeafRegistry.kt @@ -0,0 +1,94 @@ +package mods.betterfoliage.client.texture + +import cpw.mods.fml.common.FMLCommonHandler +import cpw.mods.fml.common.eventhandler.SubscribeEvent +import cpw.mods.fml.relauncher.Side +import cpw.mods.fml.relauncher.SideOnly +import mods.betterfoliage.client.Client +import mods.betterfoliage.client.config.Config +import mods.octarinecore.client.resource.IconSet +import mods.octarinecore.client.resource.averageColor +import net.minecraft.block.Block +import net.minecraft.client.renderer.texture.TextureAtlasSprite +import net.minecraft.client.renderer.texture.TextureMap +import net.minecraft.util.IIcon +import net.minecraft.util.ResourceLocation +import net.minecraftforge.client.event.TextureStitchEvent +import net.minecraftforge.common.MinecraftForge +import org.apache.logging.log4j.Level.* + +const val defaultLeafColor = 0 + +/** Rendering-related information for a leaf block. */ +class LeafInfo( + /** The generated round leaf texture. */ + val roundLeafTexture: TextureAtlasSprite, + + /** Type of the leaf block (configurable by user). */ + val leafType: String, + + /** Average color of the round leaf texture. */ + val averageColor: Int = roundLeafTexture.averageColor ?: defaultLeafColor +) { + /** [IconSet] of the textures to use for leaf particles emitted from this block. */ + val particleTextures: IconSet get() = LeafRegistry.particles[leafType]!! +} + +/** Collects and manages rendering-related information for leaf blocks. */ +@SideOnly(Side.CLIENT) +object LeafRegistry { + + val leaves: MutableMap = hashMapOf() + val particles: MutableMap = hashMapOf() + val typeMappings = TextureMatcher() + + init { + MinecraftForge.EVENT_BUS.register(this) + } + + @SubscribeEvent + fun handleTextureReload(event: TextureStitchEvent.Pre) { + if (event.map.textureType != 0) return + leaves.clear() + particles.clear() + typeMappings.loadMappings(ResourceLocation("betterfoliage", "leafTypeMappings.cfg")) + Client.log(INFO, "Generating leaf textures") + + IconSet("betterfoliage", "falling_leaf_default_%d").let { + it.onStitch(event.map) + particles.put("default", it) + } + + Block.blockRegistry.forEach { block -> + if (Config.blocks.leaves.matchesClass(block as Block)) { + block.registerBlockIcons { location -> + val original = event.map.getTextureExtry(location) + Client.log(DEBUG, "Found leaf texture: $location") + registerLeaf(event.map, original) + return@registerBlockIcons original + } + } + } + } + + fun registerLeaf(atlas: TextureMap, icon: TextureAtlasSprite) { + var leafType = typeMappings.getType(icon) ?: "default" + val generated = atlas.registerIcon( + Client.genLeaves.generatedResource(icon.iconName, "type" to leafType).toString() + ) + + if (leafType !in particles.keys) { + val particleSet = IconSet("betterfoliage", "falling_leaf_${leafType}_%d") + particleSet.onStitch(atlas) + if (particleSet.num == 0) { + Client.log(WARN, "Leaf particle textures not found for leaf type: $leafType") + leafType == "default" + } else { + particles.put(leafType, particleSet) + } + } + + val leafInfo = LeafInfo(generated as TextureAtlasSprite, leafType) + leaves.put(icon, leafInfo) + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/texture/TextureMatcher.kt b/src/main/kotlin/mods/betterfoliage/client/texture/TextureMatcher.kt new file mode 100644 index 0000000..1791f99 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/texture/TextureMatcher.kt @@ -0,0 +1,35 @@ +package mods.betterfoliage.client.texture + +import mods.octarinecore.client.resource.resourceManager +import mods.octarinecore.client.resource.get +import mods.octarinecore.client.resource.getLines +import net.minecraft.client.renderer.texture.TextureAtlasSprite +import net.minecraft.util.ResourceLocation + +class TextureMatcher() { + + data class Mapping(val domain: String?, val path: String, val type: String) { + fun matches(icon: TextureAtlasSprite): Boolean { + val iconLocation = ResourceLocation(icon.iconName) + return (domain == null || domain == iconLocation.resourceDomain) && iconLocation.resourcePath.contains(path) + } + } + + val mappings: MutableList = linkedListOf() + + fun getType(icon: TextureAtlasSprite): String? = mappings.filter { it.matches(icon) }.map { it.type }.firstOrNull() + + fun loadMappings(mappingLocation: ResourceLocation) { + mappings.clear() + resourceManager[mappingLocation]?.getLines()?.let { lines -> + lines.filter { !it.startsWith("//") }.filter { !it.isEmpty() }.forEach { line -> + val line2 = line.trim().split('=') + if (line2.size == 2) { + val mapping = line2[0].trim().split(':') + if (mapping.size == 1) mappings.add(Mapping(null, mapping[0].trim(), line2[1].trim())) + else if (mapping.size == 2) mappings.add(Mapping(mapping[1].trim(), mapping[0].trim(), line2[1].trim())) + } + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/texture/Utils.kt b/src/main/kotlin/mods/betterfoliage/client/texture/Utils.kt new file mode 100644 index 0000000..e714990 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/texture/Utils.kt @@ -0,0 +1,11 @@ +@file:JvmName("Utils") +package mods.betterfoliage.client.texture + +fun blendRGB(rgb1: Int, rgb2: Int, weight1: Int, weight2: Int): Int { + val r = (((rgb1 shr 16) and 255) * weight1 + ((rgb2 shr 16) and 255) * weight2) / (weight1 + weight2) + val g = (((rgb1 shr 8) and 255) * weight1 + ((rgb2 shr 8) and 255) * weight2) / (weight1 + weight2) + val b = ((rgb1 and 255) * weight1 + (rgb2 and 255) * weight2) / (weight1 + weight2) + val a = (rgb1 shr 24) and 255 + val result = ((a shl 24) or (r shl 16) or (g shl 8) or b).toInt() + return result +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/loader/BetterFoliageCore.kt b/src/main/kotlin/mods/betterfoliage/loader/BetterFoliageCore.kt new file mode 100644 index 0000000..0e897d9 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/loader/BetterFoliageCore.kt @@ -0,0 +1,106 @@ +package mods.betterfoliage.loader + +import cpw.mods.fml.relauncher.FMLLaunchHandler +import cpw.mods.fml.relauncher.IFMLLoadingPlugin +import mods.octarinecore.metaprog.* +import org.objectweb.asm.Opcodes.* + +@IFMLLoadingPlugin.TransformerExclusions( + "mods.betterfoliage.loader", + "mods.octarinecore.metaprog", + "kotlin", + "mods.betterfoliage.kotlin" +) +class BetterFoliageLoader : ASMPlugin(BetterFoliageTransformer::class.java) + +class BetterFoliageTransformer : Transformer() { + + init { + if (FMLLaunchHandler.side().isClient) setupClient() + } + + fun setupClient() { + // where: RenderBlocks.renderBlockByRenderType() + // what: invoke BF code to overrule the return value of Block.getRenderType() + // why: allows us to use custom block renderers for any block, without touching block code + transformMethod(Refs.renderBlockByRenderType) { + find(varinsn(ISTORE, 5))?.insertAfter { + log.info("Applying block render type override") + varinsn(ALOAD, 0) + getField(Refs.blockAccess) + varinsn(ILOAD, 2) + varinsn(ILOAD, 3) + varinsn(ILOAD, 4) + varinsn(ALOAD, 1) + varinsn(ILOAD, 5) + invokeStatic(Refs.getRenderTypeOverride) + varinsn(ISTORE, 5) + } ?: log.warn("Failed to apply block render type override!") + } + + // where: WorldClient.doVoidFogParticles(), right before the end of the loop + // what: invoke BF code for every random display tick + // why: allows us to catch random display ticks, without touching block code + transformMethod(Refs.doVoidFogParticles) { + find(IINC)?.insertBefore { + log.info("Applying random display tick call hook") + varinsn(ALOAD, 10) + varinsn(ALOAD, 0) + varinsn(ILOAD, 7) + varinsn(ILOAD, 8) + varinsn(ILOAD, 9) + invokeStatic(Refs.onRandomDisplayTick) + } ?: log.warn("Failed to apply random display tick call hook!") + } + + // where: shadersmodcore.client.Shaders.pushEntity() + // what: invoke BF code to overrule block data + // why: allows us to change the block ID seen by shader programs + transformMethod(Refs.pushEntity) { + find(IASTORE)?.insertBefore { + log.info("Applying Shaders.pushEntity() block id override") + varinsn(ALOAD, 1) + invokeStatic(Refs.getBlockIdOverride) + } ?: log.warn("Failed to apply Shaders.pushEntity() block id override!") + } + + // where: Block.getAmbientOcclusionLightValue() + // what: invoke BF code to overrule AO transparency value + // why: allows us to have light behave properly on non-solid log blocks without + // messing with isOpaqueBlock(), which could have gameplay effects + transformMethod(Refs.getAmbientOcclusionLightValue) { + find(FRETURN)?.insertBefore { + log.info("Applying Block.getAmbientOcclusionLightValue() override") + varinsn(ALOAD, 0) + invokeStatic(Refs.getAmbientOcclusionLightValueOverride) + } ?: log.warn("Failed to apply Block.getAmbientOcclusionLightValue() override!") + } + + // where: Block.getUseNeighborBrightness() + // what: invoke BF code to overrule _useNeighborBrightness_ + // why: allows us to have light behave properly on non-solid log blocks + transformMethod(Refs.getUseNeighborBrightness) { + find(IRETURN)?.insertBefore { + log.info("Applying Block.getUseNeighborBrightness() override") + varinsn(ALOAD, 0) + invokeStatic(Refs.getUseNeighborBrightnessOverride) + } ?: log.warn("Failed to apply Block.getUseNeighborBrightness() override!") + } + + // where: Block.shouldSideBeRendered() + // what: invoke BF code to overrule condition + // why: allows us to make log blocks non-solid without + // messing with isOpaqueBlock(), which could have gameplay effects + transformMethod(Refs.shouldSideBeRendered) { + find(IRETURN)?.insertBefore { + log.info("Applying Block.shouldSideBeRendered() override") + varinsn(ALOAD, 1) + varinsn(ILOAD, 2) + varinsn(ILOAD, 3) + varinsn(ILOAD, 4) + varinsn(ILOAD, 5) + invokeStatic(Refs.shouldRenderBlockSideOverride) + } ?: log.warn("Failed to apply Block.shouldSideBeRendered() override!") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/loader/Refs.kt b/src/main/kotlin/mods/betterfoliage/loader/Refs.kt new file mode 100644 index 0000000..6b91abd --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/loader/Refs.kt @@ -0,0 +1,71 @@ +package mods.betterfoliage.loader + +import cpw.mods.fml.relauncher.FMLInjectionData +import mods.octarinecore.metaprog.ClassRef +import mods.octarinecore.metaprog.FieldRef +import mods.octarinecore.metaprog.MethodRef + +/** Singleton object holding references to foreign code elements. */ +object Refs { + val mcVersion = FMLInjectionData.data()[4].toString() + + // Java + val Map = ClassRef("java.util.Map") + val List = ClassRef("java.util.List") + + // Minecraft + val IBlockAccess = ClassRef("net.minecraft.world.IBlockAccess", "ahl") + + val Block = ClassRef("net.minecraft.block.Block", "aji") + val getAmbientOcclusionLightValue = MethodRef(Block, "getAmbientOcclusionLightValue", "func_149685_I", "I", ClassRef.float) + val getUseNeighborBrightness = MethodRef(Block, "getUseNeighborBrightness", "func_149710_n", "n", ClassRef.boolean) + val shouldSideBeRendered = MethodRef(Block, "shouldSideBeRendered", "func_149646_a", "a", ClassRef.boolean, IBlockAccess, ClassRef.int, ClassRef.int, ClassRef.int, ClassRef.int) + + val RenderBlocks = ClassRef("net.minecraft.client.renderer.RenderBlocks", "blm") + val blockAccess = FieldRef(RenderBlocks, "blockAccess", null, "a", IBlockAccess) + val renderBlockByRenderType = MethodRef(RenderBlocks, "renderBlockByRenderType", null, "b", ClassRef.boolean, Block, ClassRef.int, ClassRef.int, ClassRef.int) + + val WorldClient = ClassRef("net.minecraft.client.multiplayer.WorldClient", "bjf") + val doVoidFogParticles = MethodRef(WorldClient, "doVoidFogParticles", null, "C", ClassRef.void, ClassRef.int, ClassRef.int, ClassRef.int) + + val World = ClassRef("net.minecraft.world.World", "ahb") + + val TextureMap = ClassRef("net.minecraft.client.renderer.texture.TextureMap", "bpr") + val mapRegisteredSprites = FieldRef(TextureMap, "mapRegisteredSprites", "field_110574_e", "bpr", Map) + + val IMetadataSerializer = ClassRef("net.minecraft.client.resources.data.IMetadataSerializer", "brw") + val SimpleReloadableResourceManager = ClassRef("net.minecraft.client.resources.SimpleReloadableResourceManager", "brg") + val metadataSerializer = FieldRef(SimpleReloadableResourceManager, "rmMetadataSerializer", "field_110547_c", "f", IMetadataSerializer) + + val IIcon = ClassRef("net.minecraft.util.IIcon", "rf") + val TextureAtlasSprite = ClassRef("net.minecraft.client.renderer.texture.TextureAtlasSprite", "bqd") + + // Better Foliage + val BetterFoliageHooks = ClassRef("mods.betterfoliage.client.Hooks") + val getAmbientOcclusionLightValueOverride = MethodRef(BetterFoliageHooks, "getAmbientOcclusionLightValueOverride", ClassRef.float, ClassRef.float, Block) + val getUseNeighborBrightnessOverride = MethodRef(BetterFoliageHooks, "getUseNeighborBrightnessOverride", ClassRef.boolean, ClassRef.boolean, Block) + val shouldRenderBlockSideOverride = MethodRef(BetterFoliageHooks, "shouldRenderBlockSideOverride", ClassRef.boolean, ClassRef.boolean, IBlockAccess, ClassRef.int, ClassRef.int, ClassRef.int, ClassRef.int) + val getRenderTypeOverride = MethodRef(BetterFoliageHooks, "getRenderTypeOverride", ClassRef.int, IBlockAccess, ClassRef.int, ClassRef.int, ClassRef.int, Block, ClassRef.int) + val onRandomDisplayTick = MethodRef(BetterFoliageHooks, "onRandomDisplayTick", ClassRef.void, Block, World, ClassRef.int, ClassRef.int, ClassRef.int) + + // Shaders mod + val Shaders = ClassRef("shadersmodcore.client.Shaders") + val pushEntity = MethodRef(Shaders, "pushEntity", ClassRef.void, RenderBlocks, Block, ClassRef.int, ClassRef.int, ClassRef.int) + val pushEntity_I = MethodRef(Shaders, "pushEntity", ClassRef.void, ClassRef.int) + val popEntity = MethodRef(Shaders, "popEntity", ClassRef.void) + + val ShadersModIntegration = ClassRef("mods.betterfoliage.client.integration.ShadersModIntegration") + val getBlockIdOverride = MethodRef(ShadersModIntegration, "getBlockIdOverride", ClassRef.int, ClassRef.int, Block) + + // Optifine + val ConnectedTextures = ClassRef("ConnectedTextures") + val getConnectedTexture = MethodRef(ConnectedTextures, "getConnectedTexture", IIcon, IBlockAccess, Block, ClassRef.int, ClassRef.int, ClassRef.int, ClassRef.int, IIcon) + val CTBlockProperties = FieldRef(ConnectedTextures, "blockProperties", null) + val CTTileProperties = FieldRef(ConnectedTextures, "tileProperties", null) + + val ConnectedProperties = ClassRef("ConnectedProperties") + val CPTileIcons = FieldRef(ConnectedProperties, "tileIcons", null) + + // Colored Lights Core + val CLCLoadingPlugin = ClassRef("coloredlightscore.src.asm.ColoredLightsCoreLoadingPlugin") +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/Utils.kt b/src/main/kotlin/mods/octarinecore/Utils.kt new file mode 100644 index 0000000..c4fda09 --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/Utils.kt @@ -0,0 +1,70 @@ +@file:JvmName("Utils") +@file:Suppress("NOTHING_TO_INLINE") +package mods.octarinecore + +import net.minecraft.util.ResourceLocation +import kotlin.reflect.KProperty +import java.lang.Math.* + +const val PI2 = 2.0 * PI + +inline fun String.stripStart(str: String) = if (startsWith(str)) substring(str.length) else this +inline fun ResourceLocation.stripStart(str: String) = ResourceLocation(resourceDomain, resourcePath.stripStart(str)) + +/** Mutating version of _map_. Replace each element of the list with the result of the given transformation. */ +inline fun MutableList.replace(transform: (T) -> T) = forEachIndexed { idx, value -> this[idx] = transform(value) } + +/** Exchange the two elements of the list with the given indices */ +inline fun MutableList.exchange(idx1: Int, idx2: Int) { + val e = this[idx1] + this[idx1] = this[idx2] + this[idx2] = e +} + +/** Cross product of this [Iterable] with the parameter. */ +fun Iterable.cross(other: Iterable) = flatMap { a -> other.map { b -> a to b } } + +/** + * Property-level delegate backed by a [ThreadLocal]. + * + * @param[init] Lambda to get initial value + */ +class ThreadLocalDelegate(init: () -> T) { + var tlVal = ThreadLocal.withInitial(init) + operator fun getValue(thisRef: Any?, property: KProperty<*>): T = tlVal.get() + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { tlVal.set(value) } +} + +/** + * Starting with the second element of this [Iterable] until the last, call the supplied lambda with + * the parameters (index, element, previous element). + */ +inline fun Iterable.forEachPairIndexed(func: (Int, T, T)->Unit) { + var previous: T? = null + forEachIndexed { idx, current -> + if (previous != null) func(idx, current, previous!!) + previous = current + } +} + +/** Call the supplied lambda and return its result, or the given default value if an exception is thrown. */ +fun tryDefault(default: T, work: ()->T) = try { work() } catch (e: Throwable) { default } + +/** Return a random [Double] value between the given two limits (inclusive min, exclusive max). */ +fun random(min: Double, max: Double) = Math.random().let { min + (max - min) * it } + +/** + * Return this [Double] value if it lies between the two limits. If outside, return the + * minimum/maximum value correspondingly. + */ +fun Double.minmax(minVal: Double, maxVal: Double) = min(max(this, minVal), maxVal) + +/** + * Return this [Int] value if it lies between the two limits. If outside, return the + * minimum/maximum value correspondingly. + */ +fun Int.minmax(minVal: Int, maxVal: Int) = min(max(this, minVal), maxVal) + +fun nextPowerOf2(x: Int): Int { + return 1 shl (if (x == 0) 0 else 32 - Integer.numberOfLeadingZeros(x - 1)) +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/KeyHandler.kt b/src/main/kotlin/mods/octarinecore/client/KeyHandler.kt new file mode 100644 index 0000000..0c6748d --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/KeyHandler.kt @@ -0,0 +1,22 @@ +package mods.octarinecore.client + +import cpw.mods.fml.client.registry.ClientRegistry +import cpw.mods.fml.common.FMLCommonHandler +import cpw.mods.fml.common.eventhandler.SubscribeEvent +import cpw.mods.fml.common.gameevent.InputEvent +import net.minecraft.client.settings.KeyBinding + +class KeyHandler(val modId: String, val defaultKey: Int, val lang: String, val action: (InputEvent.KeyInputEvent)->Unit) { + + val keyBinding = KeyBinding(lang, defaultKey, modId) + + init { + ClientRegistry.registerKeyBinding(keyBinding) + FMLCommonHandler.instance().bus().register(this) + } + + @SubscribeEvent + fun handleKeyPress(event: InputEvent.KeyInputEvent) { + if (keyBinding.isPressed) action(event) + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/gui/IdListConfigEntry.kt b/src/main/kotlin/mods/octarinecore/client/gui/IdListConfigEntry.kt new file mode 100644 index 0000000..9f722ee --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/gui/IdListConfigEntry.kt @@ -0,0 +1,58 @@ +package mods.octarinecore.client.gui + +import cpw.mods.fml.client.config.* +import net.minecraft.client.gui.GuiScreen +import net.minecraft.client.resources.I18n +import net.minecraft.util.EnumChatFormatting.GOLD +import net.minecraft.util.EnumChatFormatting.YELLOW + +/** + * Base class for a config GUI element. + * The GUI representation is a list of toggleable objects. + * The config representation is an integer list of the selected objects' IDs. + */ +abstract class IdListConfigEntry( + owningScreen: GuiConfig, + owningEntryList: GuiConfigEntries, + configElement: IConfigElement<*> +) : GuiConfigEntries.CategoryEntry(owningScreen, owningEntryList, configElement) { + + /** Create the child GUI elements. */ + fun createChildren() = baseSet.map { + ItemWrapperElement(it, it.itemId in configElement.list, it.itemId in configElement.defaults) + } + + init { stripTooltipDefaultText(toolTip as MutableList) } + + override fun buildChildScreen(): GuiScreen { + return GuiConfig(this.owningScreen, createChildren(), this.owningScreen.modID, + owningScreen.allRequireWorldRestart || this.configElement.requiresWorldRestart(), + owningScreen.allRequireMcRestart || this.configElement.requiresMcRestart(), this.owningScreen.title, + ((if (this.owningScreen.titleLine2 == null) "" else this.owningScreen.titleLine2) + " > " + this.name)) + } + + override fun saveConfigElement(): Boolean { + val requiresRestart = (childScreen as GuiConfig).entryList.saveConfigElements() + val children = (childScreen as GuiConfig).configElements as List + val ids = children.filter { it.booleanValue == true }.map { it.item.itemId } + configElement.set(ids.sorted().toTypedArray()) + return requiresRestart + } + + abstract val baseSet: List + abstract val T.itemId: Int + abstract val T.itemName: String + + /** Child config GUI element of a single toggleable object. */ + inner class ItemWrapperElement(val item: T, value: Boolean, val default: Boolean) : + DummyConfigElement(item.itemName, default, ConfigGuiType.BOOLEAN, item.itemName) { + init { set(value) } + + override fun getComment() = I18n.format("${configElement.languageKey}.tooltip.element", "${GOLD}${item.itemName}${YELLOW}") + override fun set(value: Boolean) { this.value = value } + fun setDefault(value: Boolean) { this.defaultValue = value } + val booleanValue: Boolean get() = value as Boolean + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/gui/NonVerboseArrayEntry.kt b/src/main/kotlin/mods/octarinecore/client/gui/NonVerboseArrayEntry.kt new file mode 100644 index 0000000..89f97f3 --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/gui/NonVerboseArrayEntry.kt @@ -0,0 +1,25 @@ +package mods.octarinecore.client.gui + +import cpw.mods.fml.client.config.GuiConfig +import cpw.mods.fml.client.config.GuiConfigEntries +import cpw.mods.fml.client.config.IConfigElement +import net.minecraft.client.resources.I18n +import net.minecraft.util.EnumChatFormatting.* + +class NonVerboseArrayEntry( + owningScreen: GuiConfig, + owningEntryList: GuiConfigEntries, + configElement: IConfigElement<*> +) : GuiConfigEntries.ArrayEntry(owningScreen, owningEntryList, configElement) { + + init { + stripTooltipDefaultText(toolTip as MutableList) + val shortDefaults = I18n.format("${configElement.languageKey}.arrayEntry", configElement.defaults.size) + toolTip.addAll(mc.fontRenderer.listFormattedStringToWidth("$AQUA${I18n.format("fml.configgui.tooltip.default", shortDefaults)}", 300)) + } + + override fun updateValueButtonText() { + btnValue.displayString = I18n.format("${configElement.languageKey}.arrayEntry", currentValues.size) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/gui/Utils.kt b/src/main/kotlin/mods/octarinecore/client/gui/Utils.kt new file mode 100644 index 0000000..e5c4ea5 --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/gui/Utils.kt @@ -0,0 +1,13 @@ +@file:JvmName("Utils") +package mods.octarinecore.client.gui + +import net.minecraft.util.EnumChatFormatting + +fun stripTooltipDefaultText(tooltip: MutableList) { + var defaultRows = false + val iter = tooltip.iterator() + while (iter.hasNext()) { + if (iter.next().startsWith(EnumChatFormatting.AQUA.toString())) defaultRows = true + if (defaultRows) iter.remove() + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/render/AbstractBlockRenderingHandler.kt b/src/main/kotlin/mods/octarinecore/client/render/AbstractBlockRenderingHandler.kt new file mode 100644 index 0000000..d92703d --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/render/AbstractBlockRenderingHandler.kt @@ -0,0 +1,160 @@ +@file:JvmName("RendererHolder") +package mods.octarinecore.client.render + +import cpw.mods.fml.client.registry.ISimpleBlockRenderingHandler +import cpw.mods.fml.client.registry.RenderingRegistry +import mods.octarinecore.ThreadLocalDelegate +import mods.octarinecore.client.resource.ResourceHandler +import net.minecraft.block.Block +import net.minecraft.client.Minecraft +import net.minecraft.client.renderer.RenderBlocks +import net.minecraft.util.IIcon +import net.minecraft.util.MathHelper +import net.minecraft.world.IBlockAccess +import net.minecraftforge.common.util.ForgeDirection + +/** + * [ThreadLocal] instance of [ExtendedRenderBlocks] used instead of the vanilla [RenderBlocks] to get the + * AO values and textures used in rendering without duplicating vanilla code. + */ +val renderBlocks by ThreadLocalDelegate { ExtendedRenderBlocks() } + +/** + * [ThreadLocal] instance of [BlockContext] representing the block being rendered. + */ +val blockContext by ThreadLocalDelegate { BlockContext() } + +/** + * [ThreadLocal] instance of [ModelRenderer]. + */ +val modelRenderer by ThreadLocalDelegate { ModelRenderer() } + +abstract class AbstractBlockRenderingHandler(modId: String) : ResourceHandler(modId), ISimpleBlockRenderingHandler { + + // ============================ + // Self-registration + // ============================ + val id = RenderingRegistry.getNextAvailableRenderId() + init { + RenderingRegistry.registerBlockHandler(this); + } + + // ============================ + // Custom rendering + // ============================ + abstract fun isEligible(ctx: BlockContext): Boolean + abstract fun render(ctx: BlockContext, parent: RenderBlocks): Boolean + + // ============================ + // Interface implementation + // ============================ + override fun renderWorldBlock(world: IBlockAccess?, x: Int, y: Int, z: Int, block: Block?, modelId: Int, parentRenderer: RenderBlocks?): Boolean { + renderBlocks.blockAccess = world + return render(blockContext, parentRenderer!!) + } + override fun renderInventoryBlock(block: Block?, metadata: Int, modelId: Int, renderer: RenderBlocks?) {} + override fun shouldRender3DInInventory(modelId: Int) = true + override fun getRenderId(): Int = id + + // ============================ + // Vanilla rendering wrapper + // ============================ + /** + * Render the block in the current [BlockContext], and capture shading and texture data. + * + * @param[parentRenderer] parent renderer passed in by rendering pipeline, used only for block breaking overlay + * @param[targetPass] which render pass to save shading and texture data from + * @param[block] lambda to use to render the block if it does not have a custom renderer + * @param[face] lambda to determine which faces of the block to render + */ + fun renderWorldBlockBase( + parentRenderer: RenderBlocks = renderBlocks, + targetPass: Int = 1, + block: () -> Unit = { blockContext.let { ctx -> renderBlocks.renderStandardBlock(ctx.block, ctx.x, ctx.y, ctx.z) } }, + face: (ShadingCapture, ForgeDirection, Int, IIcon?) -> Boolean + ): Boolean { + val ctx = blockContext + val renderBlocks = renderBlocks + + // use original renderer for block breaking overlay + if (parentRenderer.hasOverrideBlockTexture()) { + parentRenderer.setRenderBoundsFromBlock(ctx.block); + parentRenderer.renderStandardBlock(ctx.block, ctx.x, ctx.y, ctx.z); + return true; + } + + // render block + renderBlocks.capture.reset(targetPass) + renderBlocks.capture.renderCallback = face + renderBlocks.setRenderBoundsFromBlock(ctx.block); + val handler = renderingHandlers[ctx.block.renderType]; + if (handler != null && ctx.block.renderType != 0) { + handler.renderWorldBlock(ctx.world, ctx.x, ctx.y, ctx.z, ctx.block, ctx.block.renderType, renderBlocks); + } else { + block() + } + return false; + } + +} + +/** + * Represents the block being rendered. Has properties and methods to query the neighborhood of the block in + * block-relative coordinates. + */ +class BlockContext() { + var world: IBlockAccess? = null + var x: Int = 0 + var y: Int = 0 + var z: Int = 0 + + fun set(world: IBlockAccess, x: Int, y: Int, z: Int) { this.world = world; this.x = x; this.y = y; this.z = z; } + + /** Get the [Block] at the given offset. */ + val block: Block get() = world!!.getBlock(x, y, z) + fun block(offset: Int3) = world!!.getBlock(x + offset.x, y + offset.y, z + offset.z) + + /** Get the metadata at the given offset. */ + val meta: Int get() = world!!.getBlockMetadata(x, y, z) + fun meta(offset: Int3) = world!!.getBlockMetadata(x + offset.x, y + offset.y, z + offset.z) + + /** Get the block color multiplier at the given offset. */ + val blockColor: Int get() = block.colorMultiplier(world, x, y, z) + fun blockColor(offset: Int3) = block(offset).colorMultiplier(world, x + offset.x, y + offset.y, z + offset.z) + + /** Get the block brightness at the given offset. */ + val blockBrightness: Int get() = block.getMixedBrightnessForBlock(world, x, y, z) + fun blockBrightness(offset: Int3) = block(offset).getMixedBrightnessForBlock(world, x + offset.x, y + offset.y, z + offset.z) + + /** Get the biome ID at the block position. */ + val biomeId: Int get() = world!!.getBiomeGenForCoords(x, z).biomeID + + /** Get the texture on a given face of the block being rendered. */ + fun icon(face: ForgeDirection) = block(Int3.zero).getIcon(face.ordinal, meta(Int3.zero)) + /** Get the texture on a given face of the block at the given offset. */ + fun icon(offset: Int3, face: ForgeDirection) = block(offset).getIcon(face.ordinal, meta(offset)) + + /** Get the centerpoint of the block being rendered. */ + val blockCenter: Double3 get() = Double3(x + 0.5, y + 0.5, z + 0.5) + + /** Is the block surrounded by other blocks that satisfy the predicate on all sides? */ + fun isSurroundedBy(predicate: (Block)->Boolean) = forgeDirOffsets.all { predicate(block(it)) } + + /** Get a semi-random value based on the block coordinate and the given seed. */ + fun random(seed: Int): Int { + var value = (x * x + y * y + z * z + x * y + y * z + z * x + (seed * seed)) and 63 + value = (3 * x * value + 5 * y * value + 7 * z * value + (11 * seed)) and 63 + return value + } + + /** Get an array of semi-random values based on the block coordinate. */ + fun semiRandomArray(num: Int): Array = Array(num) { random(it) } + + /** Get the distance of the block from the camera (player). */ + val cameraDistance: Int get() { + val camera = Minecraft.getMinecraft().renderViewEntity ?: return 0 + return Math.abs(x - MathHelper.floor_double(camera.posX)) + + Math.abs(y - MathHelper.floor_double(camera.posY)) + + Math.abs(z - MathHelper.floor_double(camera.posZ)) + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/render/AbstractEntityFX.kt b/src/main/kotlin/mods/octarinecore/client/render/AbstractEntityFX.kt new file mode 100644 index 0000000..348159f --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/render/AbstractEntityFX.kt @@ -0,0 +1,98 @@ +package mods.octarinecore.client.render + +import mods.octarinecore.PI2 +import net.minecraft.client.Minecraft +import net.minecraft.client.particle.EntityFX +import net.minecraft.client.renderer.Tessellator +import net.minecraft.util.IIcon +import net.minecraft.world.World + +abstract class AbstractEntityFX(world: World, x: Double, y: Double, z: Double) : EntityFX(world, x, y, z) { + + companion object { + @JvmStatic val sin = Array(64) { idx -> Math.sin(PI2 / 64.0 * idx) } + @JvmStatic val cos = Array(64) { idx -> Math.cos(PI2 / 64.0 * idx) } + } + + val billboardRot = Pair(Double3.zero, Double3.zero) + val currentPos = Double3.zero + val prevPos = Double3.zero + val velocity = Double3.zero + + override fun onUpdate() { + super.onUpdate() + currentPos.setTo(posX, posY, posZ) + prevPos.setTo(prevPosX, prevPosY, prevPosZ) + velocity.setTo(motionX, motionY, motionZ) + update() + posX = currentPos.x; posY = currentPos.y; posZ = currentPos.z; + motionX = velocity.x; motionY = velocity.y; motionZ = velocity.z; + } + + /** Render the particle. */ + abstract fun render(tessellator: Tessellator, partialTickTime: Float) + + /** Update particle on world tick. */ + abstract fun update() + + /** True if the particle is renderable. */ + abstract val isValid: Boolean + + /** Add the particle to the effect renderer if it is valid. */ + fun addIfValid() { if (isValid) Minecraft.getMinecraft().effectRenderer.addEffect(this) } + + override fun renderParticle(tessellator: Tessellator, partialTickTime: Float, rotX: Float, rotZ: Float, rotYZ: Float, rotXY: Float, rotXZ: Float) { + billboardRot.first.setTo(rotX + rotXY, rotZ, rotYZ + rotXZ) + billboardRot.second.setTo(rotX - rotXY, -rotZ, rotYZ - rotXZ) + render(tessellator, partialTickTime) + } + + /** + * Render a particle quad. + * + * @param[tessellator] the [Tessellator] instance to use + * @param[partialTickTime] partial tick time + * @param[currentPos] render position + * @param[prevPos] previous tick position for interpolation + * @param[size] particle size + * @param[rotation] viewpoint-dependent particle rotation (64 steps) + * @param[icon] particle texture + * @param[isMirrored] mirror particle texture along V-axis + * @param[alpha] aplha blending + */ + fun renderParticleQuad(tessellator: Tessellator, + partialTickTime: Float, + currentPos: Double3 = this.currentPos, + prevPos: Double3 = this.prevPos, + size: Double = particleScale.toDouble(), + rotation: Int = 0, + icon: IIcon = particleIcon, + isMirrored: Boolean = false, + alpha: Float = this.particleAlpha) { + + val minU = (if (isMirrored) icon.minU else icon.maxU).toDouble() + val maxU = (if (isMirrored) icon.maxU else icon.minU).toDouble() + val minV = icon.minV.toDouble() + val maxV = icon.maxV.toDouble() + + val center = currentPos.copy().sub(prevPos).mul(partialTickTime.toDouble()).add(prevPos).sub(interpPosX, interpPosY, interpPosZ) + val v1 = if (rotation == 0) billboardRot.first * size else + Double3.weight(billboardRot.first, cos[rotation and 63] * size, billboardRot.second, sin[rotation and 63] * size) + val v2 = if (rotation == 0) billboardRot.second * size else + Double3.weight(billboardRot.first, -sin[rotation and 63] * size, billboardRot.second, cos[rotation and 63] * size) + + tessellator.setColorRGBA_F(this.particleRed, this.particleGreen, this.particleBlue, alpha) + tessellator.addVertexWithUV(center.x - v1.x, center.y - v1.y, center.z - v1.z, maxU, maxV) + tessellator.addVertexWithUV(center.x - v2.x, center.y - v2.y, center.z - v2.z, maxU, minV) + tessellator.addVertexWithUV(center.x + v1.x, center.y + v1.y, center.z + v1.z, minU, minV) + tessellator.addVertexWithUV(center.x + v2.x, center.y + v2.y, center.z + v2.z, minU, maxV) + } + + override fun getFXLayer() = 1 + + fun setColor(color: Int) { + particleBlue = (color and 255) / 256.0f + particleGreen = ((color shr 8) and 255) / 256.0f + particleRed = ((color shr 16) and 255) / 256.0f + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/render/Geometry.kt b/src/main/kotlin/mods/octarinecore/client/render/Geometry.kt new file mode 100644 index 0000000..ccddb10 --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/render/Geometry.kt @@ -0,0 +1,214 @@ +package mods.octarinecore.client.render + +import mods.octarinecore.client.render.Axis.* +import mods.octarinecore.client.render.Dir.N +import mods.octarinecore.client.render.Dir.P +import mods.octarinecore.cross +import net.minecraftforge.common.util.ForgeDirection +import net.minecraftforge.common.util.ForgeDirection.* + +// ================================ +// Axes and directions +// ================================ +enum class Axis { X, Y, Z } +enum class Dir { P, N } +val axes = listOf(X, Y, Z) +val axisDirs = listOf(P, N) +val forgeDirs = ForgeDirection.VALID_DIRECTIONS +val forgeDirOffsets = forgeDirs.map { Int3(it) } +val ForgeDirection.axis: Axis get() = when(this) {EAST, WEST -> X; UP, DOWN -> Y; else -> Z } +val ForgeDirection.dir: Dir get() = when(this) {UP, SOUTH, EAST -> P; else -> N } +val Pair.face: ForgeDirection get() = when(this) { + X to P -> EAST; X to N -> WEST; Y to P -> UP; Y to N -> DOWN; Z to P -> SOUTH; Z to N -> NORTH; else -> UNKNOWN +} +val ForgeDirection.perpendiculars: List get() = + axes.filter { it != this.axis }.cross(axisDirs).map { it.face } +val ForgeDirection.offset: Int3 get() = forgeDirOffsets[ordinal] + +// ================================ +// Vectors +// ================================ +operator fun ForgeDirection.times(scale: Double) = + Double3(offsetX.toDouble() * scale, offsetY.toDouble() * scale, offsetZ.toDouble() * scale) +val ForgeDirection.vec: Double3 get() = Double3(offsetX.toDouble(), offsetY.toDouble(), offsetZ.toDouble()) + +/** 3D vector of [Double]s. Offers both mutable operations, and immutable operations in operator notation. */ +data class Double3(var x: Double, var y: Double, var z: Double) { + constructor(x: Float, y: Float, z: Float) : this(x.toDouble(), y.toDouble(), z.toDouble()) + constructor(dir: ForgeDirection) : this(dir.offsetX.toDouble(), dir.offsetY.toDouble(), dir.offsetZ.toDouble()) + companion object { + val zero: Double3 get() = Double3(0.0, 0.0, 0.0) + fun weight(v1: Double3, weight1: Double, v2: Double3, weight2: Double) = + Double3(v1.x * weight1 + v2.x * weight2, v1.y * weight1 + v2.y * weight2, v1.z * weight1 + v2.z * weight2) + } + + // immutable operations + operator fun plus(other: Double3) = Double3(x + other.x, y + other.y, z + other.z) + operator fun unaryMinus() = Double3(-x, -y, -z) + operator fun minus(other: Double3) = Double3(x - other.x, y - other.y, z - other.z) + operator fun times(scale: Double) = Double3(x * scale, y * scale, z * scale) + operator fun times(other: Double3) = Double3(x * other.x, y * other.y, z * other.z) + + /** Rotate this vector, and return coordinates in the unrotated frame */ + fun rotate(rot: Rotation) = Double3( + rot.rotatedComponent(EAST, x, y, z), + rot.rotatedComponent(UP, x, y, z), + rot.rotatedComponent(SOUTH, x, y, z) + ) + + // mutable operations + fun setTo(other: Double3): Double3 { x = other.x; y = other.y; z = other.z; return this } + fun setTo(x: Double, y: Double, z: Double): Double3 { this.x = x; this.y = y; this.z = z; return this } + fun setTo(x: Float, y: Float, z: Float) = setTo(x.toDouble(), y.toDouble(), z.toDouble()) + fun add(other: Double3): Double3 { x += other.x; y += other.y; z += other.z; return this } + fun add(x: Double, y: Double, z: Double): Double3 { this.x += x; this.y += y; this.z += z; return this } + fun sub(other: Double3): Double3 { x -= other.x; y -= other.y; z -= other.z; return this } + fun sub(x: Double, y: Double, z: Double): Double3 { this.x -= x; this.y -= y; this.z -= z; return this } + fun invert(): Double3 { x = -x; y = -y; z = -z; return this } + fun mul(scale: Double): Double3 { x *= scale; y *= scale; z *= scale; return this } + fun mul(other: Double3): Double3 { x *= other.x; y *= other.y; z *= other.z; return this } + fun rotateMut(rot: Rotation): Double3 { + val rotX = rot.rotatedComponent(EAST, x, y, z) + val rotY = rot.rotatedComponent(UP, x, y, z) + val rotZ = rot.rotatedComponent(SOUTH, x, y, z) + return setTo(rotX, rotY, rotZ) + } + + // misc operations + infix fun dot(other: Double3) = x * other.x + y * other.y + z * other.z + infix fun cross(o: Double3) = Double3(y * o.z - z * o.y, z * o.x - x * o.z, x * o.y - y * o.x) + val length: Double get() = Math.sqrt(x * x + y * y + z * z) + val normalize: Double3 get() = (1.0 / length).let { Double3(x * it, y * it, z * it) } + val nearestCardinal: ForgeDirection get() = nearestAngle(this, forgeDirs.asIterable()) { it.vec }.first +} + +/** 3D vector of [Int]s. Offers both mutable operations, and immutable operations in operator notation. */ +data class Int3(var x: Int, var y: Int, var z: Int) { + constructor(dir: ForgeDirection) : this(dir.offsetX, dir.offsetY, dir.offsetZ) + constructor(offset: Pair) : this( + offset.first * offset.second.offsetX, + offset.first * offset.second.offsetY, + offset.first * offset.second.offsetZ + ) + companion object { + val zero = Int3(0, 0, 0) + } + + // immutable operations + operator fun plus(other: Int3) = Int3(x + other.x, y + other.y, z + other.z) + operator fun plus(other: Pair) = Int3( + x + other.first * other.second.offsetX, + y + other.first * other.second.offsetY, + z + other.first * other.second.offsetZ + ) + operator fun unaryMinus() = Int3(-x, -y, -z) + operator fun minus(other: Int3) = Int3(x - other.x, y - other.y, z - other.z) + operator fun times(scale: Int) = Int3(x * scale, y * scale, z * scale) + operator fun times(other: Int3) = Int3(x * other.x, y * other.y, z * other.z) + + /** Rotate this vector, and return coordinates in the unrotated frame */ + fun rotate(rot: Rotation) = Int3( + rot.rotatedComponent(EAST, x, y, z), + rot.rotatedComponent(UP, x, y, z), + rot.rotatedComponent(SOUTH, x, y, z) + ) + + // mutable operations + fun setTo(other: Int3): Int3 { x = other.x; y = other.y; z = other.z; return this } + fun setTo(x: Int, y: Int, z: Int): Int3 { this.x = x; this.y = y; this.z = z; return this } + fun add(other: Int3): Int3 { x += other.x; y += other.y; z += other.z; return this } + fun sub(other: Int3): Int3 { x -= other.x; y -= other.y; z -= other.z; return this } + fun invert(): Int3 { x = -x; y = -y; z = -z; return this } + fun mul(scale: Int): Int3 { x *= scale; y *= scale; z *= scale; return this } + fun mul(other: Int3): Int3 { x *= other.x; y *= other.y; z *= other.z; return this } + fun rotateMut(rot: Rotation): Int3 { + val rotX = rot.rotatedComponent(EAST, x, y, z) + val rotY = rot.rotatedComponent(UP, x, y, z) + val rotZ = rot.rotatedComponent(SOUTH, x, y, z) + return setTo(rotX, rotY, rotZ) + } +} + +// ================================ +// Rotation +// ================================ +val ForgeDirection.rotations: Array get() = + Array(6) { idx -> ForgeDirection.values()[ForgeDirection.ROTATION_MATRIX[ordinal][idx]] } +fun ForgeDirection.rotate(rot: Rotation) = rot.forward[ordinal] +fun rot(axis: ForgeDirection) = Rotation.rot90[axis.ordinal] + +/** + * Class representing an arbitrary rotation (or combination of rotations) around cardinal axes by 90 degrees. + * In effect, a permutation of [ForgeDirection]s. + */ +@Suppress("NOTHING_TO_INLINE") +class Rotation(val forward: Array, val reverse: Array) { + operator fun plus(other: Rotation) = Rotation( + Array(6) { idx -> forward[other.forward[idx].ordinal] }, + Array(6) { idx -> other.reverse[reverse[idx].ordinal] } + ) + operator fun unaryMinus() = Rotation(reverse, forward) + operator fun times(num: Int) = when(num % 4) { 1 -> this; 2 -> this + this; 3 -> -this; else -> identity } + + inline fun rotatedComponent(dir: ForgeDirection, x: Int, y: Int, z: Int) = + when(reverse[dir.ordinal]) { EAST -> x; WEST -> -x; UP -> y; DOWN -> -y; SOUTH -> z; NORTH -> -z; else -> 0 } + inline fun rotatedComponent(dir: ForgeDirection, x: Double, y: Double, z: Double) = + when(reverse[dir.ordinal]) { EAST -> x; WEST -> -x; UP -> y; DOWN -> -y; SOUTH -> z; NORTH -> -z; else -> 0.0 } + + companion object { + // Forge rotation matrix is left-hand + val rot90 = Array(6) { idx -> Rotation(forgeDirs[idx].opposite.rotations, forgeDirs[idx].rotations) } + val identity = Rotation(forgeDirs, forgeDirs) + } + +} + +// ================================ +// Miscellaneous +// ================================ +/** List of all 12 box edges, represented as a [Pair] of [ForgeDirection]s */ +val boxEdges = forgeDirs.flatMap { face1 -> forgeDirs.filter { it.axis > face1.axis }.map { face1 to it } } + +/** + * Get the closest object to the specified point from a list of objects. + * + * @param[vertex] the reference point + * @param[objs] list of geomertric objects + * @param[objPos] lambda to calculate the position of an object + * @return [Pair] of (object, distance) + */ +fun nearestPosition(vertex: Double3, objs: Iterable, objPos: (T)->Double3): Pair = + objs.map { it to (objPos(it) - vertex).length }.minBy { it.second }!! + +/** + * Get the object closest in orientation to the specified vector from a list of objects. + * + * @param[vector] the reference vector (direction) + * @param[objs] list of geomertric objects + * @param[objAngle] lambda to calculate the orientation of an object + * @return [Pair] of (object, normalized dot product) + */ +fun nearestAngle(vector: Double3, objs: Iterable, objAngle: (T)->Double3): Pair = + objs.map { it to objAngle(it).dot(vector) }.maxBy { it.second }!! + +data class FaceCorners(val topLeft: Pair, + val topRight: Pair, + val bottomLeft: Pair, + val bottomRight: Pair) { + constructor(top: ForgeDirection, left: ForgeDirection) : + this(top to left, top to left.opposite, top.opposite to left, top.opposite to left.opposite) + + val asArray = arrayOf(topLeft, topRight, bottomLeft, bottomRight) + val asList = listOf(topLeft, topRight, bottomLeft, bottomRight) +} + +val faceCorners = forgeDirs.map { when(it) { + DOWN -> FaceCorners(SOUTH, WEST) + UP -> FaceCorners(SOUTH, EAST) + NORTH -> FaceCorners(WEST, UP) + SOUTH -> FaceCorners(UP, WEST) + WEST -> FaceCorners(SOUTH, UP) + EAST ->FaceCorners(SOUTH, DOWN) + else -> FaceCorners(UNKNOWN, UNKNOWN) +}} + diff --git a/src/main/kotlin/mods/octarinecore/client/render/Model.kt b/src/main/kotlin/mods/octarinecore/client/render/Model.kt new file mode 100644 index 0000000..7a07377 --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/render/Model.kt @@ -0,0 +1,138 @@ +package mods.octarinecore.client.render + +import mods.octarinecore.minmax +import mods.octarinecore.replace +import net.minecraftforge.common.util.ForgeDirection +import java.lang.Math.* + +/** + * Vertex UV coordinates + * + * Zero-centered: coordinates fall between (-0.5, 0.5) (inclusive) + */ +data class UV(val u: Double, val v: Double) { + companion object { + val topLeft = UV(-0.5, -0.5) + val topRight = UV(0.5, -0.5) + val bottomLeft = UV(-0.5, 0.5) + val bottomRight = UV(0.5, 0.5) + } + + val rotate: UV get() = UV(v, -u) + + fun rotate(n: Int) = when(n % 4) { + 0 -> copy() + 1 -> UV(v, -u) + 2 -> UV(-u, -v) + else -> UV(-v, u) + } + + fun clamp(minU: Double = -0.5, maxU: Double = 0.5, minV: Double = -0.5, maxV: Double = 0.5) = + UV(u.minmax(minU, maxU), v.minmax(minV, maxV)) + + fun mirror(mirrorU: Boolean, mirrorV: Boolean) = UV(if (mirrorU) -u else u, if (mirrorV) -v else v) +} + +/** + * Model vertex + * + * @param[xyz] x, y, z coordinates + * @param[uv] u, v coordinates + * @param[aoShader] [Shader] instance to use with AO rendering + * @param[flatShader] [Shader] instance to use with non-AO rendering + */ +data class Vertex(val xyz: Double3 = Double3(0.0, 0.0, 0.0), + val uv: UV = UV(0.0, 0.0), + val aoShader: Shader = NoShader, + val flatShader: Shader = NoShader) + +/** + * Model quad + */ +data class Quad(val v1: Vertex, val v2: Vertex, val v3: Vertex, val v4: Vertex) { + val verts = arrayOf(v1, v2, v3, v4) + inline fun transformV(trans: (Vertex)->Vertex): Quad = transformVI { vertex, idx -> trans(vertex) } + inline fun transformVI(trans: (Vertex, Int)->Vertex): Quad = + Quad(trans(v1, 0), trans(v2, 1), trans(v3, 2), trans(v4, 3)) + val normal: Double3 get() = (v2.xyz - v1.xyz).cross(v4.xyz - v1.xyz).normalize + + fun move(trans: Double3) = transformV { it.copy(xyz = it.xyz + trans) } + fun move(trans: Pair) = move(Double3(trans.second) * trans.first) + fun scale (scale: Double) = transformV { it.copy(xyz = it.xyz * scale) } + fun scale (scale: Double3) = transformV { it.copy(xyz = Double3(it.xyz.x * scale.x, it.xyz.y * scale.y, it.xyz.z * scale.z)) } + fun scaleUV (scale: Double) = transformV { it.copy(uv = UV(it.uv.u * scale, it.uv.v * scale)) } + fun rotate(rot: Rotation) = transformV { + it.copy(xyz = it.xyz.rotate(rot), aoShader = it.aoShader.rotate(rot), flatShader = it.flatShader.rotate(rot)) + } + fun rotateUV(n: Int) = transformV { it.copy(uv = it.uv.rotate(n)) } + fun clampUV(minU: Double = -0.5, maxU: Double = 0.5, minV: Double = -0.5, maxV: Double = 0.5) = + transformV { it.copy(uv = it.uv.clamp(minU, maxU, minV, maxV)) } + fun mirrorUV(mirrorU: Boolean, mirrorV: Boolean) = transformV { it.copy(uv = it.uv.mirror(mirrorU, mirrorV)) } + fun setAoShader(resolver: (Quad, Vertex)->Shader, predicate: (Vertex, Int)->Boolean = { v, vi -> true }) = + transformVI { vertex, idx -> + if (!predicate(vertex, idx)) vertex else vertex.copy(aoShader = resolver(this@Quad, vertex)) + } + fun setFlatShader(resolver: (Quad, Vertex)->Shader, predicate: (Vertex, Int)->Boolean = { v, vi -> true }) = + transformVI { vertex, idx -> + if (!predicate(vertex, idx)) vertex else vertex.copy(flatShader = resolver(this@Quad, vertex)) + } + fun setFlatShader(shader: Shader) = transformVI { vertex, idx -> vertex.copy(flatShader = shader) } + val flipped: Quad get() = Quad(v4, v3, v2, v1) + +} + +/** + * Model. The basic unit of rendering blocks with OctarineCore. + * + * The model should be positioned so that (0,0,0) is the block center. + * The block extends to (-0.5, 0.5) in all directions (inclusive). + */ +class Model() { + constructor(other: List) : this() { quads.addAll(other) } + val quads = linkedListOf() + + fun Quad.add() = quads.add(this) + fun Iterable.addAll() = forEach { quads.add(it) } + + fun transformQ(trans: (Quad)->Quad) = quads.replace(trans) + fun transformV(trans: (Vertex)->Vertex) = quads.replace{ it.transformV(trans) } + + fun verticalRectangle(x1: Double, z1: Double, x2: Double, z2: Double, yBottom: Double, yTop: Double) = Quad( + Vertex(Double3(x1, yBottom, z1), UV.bottomLeft), + Vertex(Double3(x2, yBottom, z2), UV.bottomRight), + Vertex(Double3(x2, yTop, z2), UV.topRight), + Vertex(Double3(x1, yTop, z1), UV.topLeft) + ) + + fun horizontalRectangle(x1: Double, z1: Double, x2: Double, z2: Double, y: Double): Quad { + val xMin = min(x1, x2); val xMax = max(x1, x2) + val zMin = min(z1, z2); val zMax = max(z1, z2) + return Quad( + Vertex(Double3(xMin, y, zMin), UV.topLeft), + Vertex(Double3(xMin, y, zMax), UV.bottomLeft), + Vertex(Double3(xMax, y, zMax), UV.bottomRight), + Vertex(Double3(xMax, y, zMin), UV.topRight) + ) + } + + fun faceQuad(face: ForgeDirection): Quad { + val base = face.vec * 0.5 + val top = faceCorners[face.ordinal].topLeft.first.vec * 0.5 + val left = faceCorners[face.ordinal].topLeft.second.vec * 0.5 + return Quad( + Vertex(base + top + left, UV.topLeft), + Vertex(base - top + left, UV.bottomLeft), + Vertex(base - top - left, UV.bottomRight), + Vertex(base + top - left, UV.topRight) + ) + } +} + +val fullCube = Model().apply { + forgeDirs.forEach { + faceQuad(it) + .setAoShader(faceOrientedAuto(corner = cornerAo(it.axis), edge = null)) + .setFlatShader(faceOrientedAuto(corner = cornerFlat, edge = null)) + .add() + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/render/ModelRenderer.kt b/src/main/kotlin/mods/octarinecore/client/render/ModelRenderer.kt new file mode 100644 index 0000000..509030f --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/render/ModelRenderer.kt @@ -0,0 +1,138 @@ +package mods.octarinecore.client.render + +import net.minecraft.client.Minecraft +import net.minecraft.client.renderer.Tessellator +import net.minecraft.util.IIcon +import net.minecraftforge.common.util.ForgeDirection +import net.minecraftforge.common.util.ForgeDirection.* + +class ModelRenderer() : ShadingContext() { + + /** Holds final vertex data before it goes to the [Tessellator]. */ + val temp = RenderVertex() + + /** + * Render a [Model]. + * The [blockContext] and [renderBlocks] need to be set up correctly, including first rendering the + * corresponding block to capture shading values! + * + * @param[model] model to render + * @param[rot] rotation to apply to the model + * @param[trans] translation to apply to the model + * @param[forceFlat] force flat shading even if AO is enabled + * @param[icon] lambda to resolve the texture to use for each quad + * @param[rotateUV] lambda to get amount of UV rotation for each quad + * @param[postProcess] lambda to perform arbitrary modifications on the [RenderVertex] just before it goes to the [Tessellator] + */ + inline fun render( + model: Model, + rot: Rotation, + trans: Double3 = blockContext.blockCenter, + forceFlat: Boolean = false, + icon: (ShadingContext, Int, Quad) -> IIcon, + rotateUV: (Quad) -> Int, + postProcess: RenderVertex.(ShadingContext, Int, Quad, Int, Vertex) -> Unit + ) { + rotation = rot + aoEnabled = Minecraft.isAmbientOcclusionEnabled() + + model.quads.forEachIndexed { quadIdx, quad -> + val drawIcon = icon(this, quadIdx, quad) + val uvRot = rotateUV(quad) + quad.verts.forEachIndexed { vertIdx, vert -> + temp.init(vert) + temp.rotate(rotation).translate(trans).rotateUV(uvRot).setIcon(drawIcon) + val shader = if (aoEnabled && !forceFlat) vert.aoShader else vert.flatShader + shader.shade(this, temp) + temp.postProcess(this, quadIdx, quad, vertIdx, vert) + Tessellator.instance.apply { + setBrightness(temp.brightness) + setColorOpaque_F(temp.red, temp.green, temp.blue) + addVertexWithUV(temp.x, temp.y, temp.z, temp.u, temp.v) + } + } + } + } +} + +/** + * Queried by [Shader] objects to get rendering-relevant data of the current block in a rotated frame of reference. + */ +open class ShadingContext { + var rotation = Rotation.identity + var aoEnabled = Minecraft.isAmbientOcclusionEnabled() + + fun aoShading(face: ForgeDirection, corner1: ForgeDirection, corner2: ForgeDirection) = + renderBlocks.capture.aoShading(face.rotate(rotation), corner1.rotate(rotation), corner2.rotate(rotation)) + + fun blockColor(offset: Int3) = blockContext.blockColor(offset.rotate(rotation)) + fun blockBrightness(offset: Int3) = blockContext.blockBrightness(offset.rotate(rotation)) + fun icon(face: ForgeDirection) = blockContext.icon(face.rotate(rotation)) +} + +/** + * + */ +@Suppress("NOTHING_TO_INLINE") +class RenderVertex() { + var x: Double = 0.0 + var y: Double = 0.0 + var z: Double = 0.0 + var u: Double = 0.0 + var v: Double = 0.0 + var brightness: Int = 0 + var red: Float = 0.0f + var green: Float = 0.0f + var blue: Float = 0.0f + + fun init(vertex: Vertex, rot: Rotation, trans: Double3): RenderVertex { + val result = vertex.xyz.rotate(rot) + trans + x = result.x; y = result.y; z = result.z + return this + } + fun init(vertex: Vertex): RenderVertex { + x = vertex.xyz.x; y = vertex.xyz.y; z = vertex.xyz.z; + u = vertex.uv.u; v = vertex.uv.v + return this + } + fun translate(trans: Double3): RenderVertex { x += trans.x; y += trans.y; z += trans.z; return this } + fun rotate(rot: Rotation): RenderVertex { + if (rot === Rotation.identity) return this + val rotX = rot.rotatedComponent(EAST, x, y, z) + val rotY = rot.rotatedComponent(UP, x, y, z) + val rotZ = rot.rotatedComponent(SOUTH, x, y, z) + x = rotX; y = rotY; z = rotZ + return this + } + inline fun rotateUV(n: Int): RenderVertex { + when (n % 4) { + 1 -> { val t = v; v = -u; u = t; return this } + 2 -> { u = -u; v = -v; return this } + 3 -> { val t = -v; v = u; u = t; return this } + else -> { return this } + } + } + inline fun setIcon(icon: IIcon): RenderVertex { + u = (icon.maxU - icon.minU) * (u + 0.5) + icon.minU + v = (icon.maxV - icon.minV) * (v + 0.5) + icon.minV + return this + } + + inline fun setGrey(level: Float) { + val grey = Math.min((red + green + blue) * 0.333f * level, 1.0f) + red = grey; green = grey; blue = grey + } + inline fun multiplyColor(color: Int) { + red *= (color shr 16 and 255) / 256.0f + green *= (color shr 8 and 255) / 256.0f + blue *= (color and 255) / 256.0f + } + inline fun setColor(color: Int) { + red = (color shr 16 and 255) / 256.0f + green = (color shr 8 and 255) / 256.0f + blue = (color and 255) / 256.0f + } +} + +/** Perform no post-processing */ +val noPost: RenderVertex.(ShadingContext, Int, Quad, Int, Vertex) -> Unit = { ctx, qi, q, vi, v -> } \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/render/OffsetBlockAccess.kt b/src/main/kotlin/mods/octarinecore/client/render/OffsetBlockAccess.kt new file mode 100644 index 0000000..c0581aa --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/render/OffsetBlockAccess.kt @@ -0,0 +1,68 @@ +package mods.octarinecore.client.render + +import mods.octarinecore.minmax +import net.minecraft.world.IBlockAccess +import net.minecraftforge.common.util.ForgeDirection + +/** + * Delegating [IBlockAccess] that fakes a _modified_ location to return values from a _target_ location. + * All other locations are handled normally. + * + * @param[original] the [IBlockAccess] that is delegated to + * @param[xModded] x coordinate of the _modified_ location + * @param[yModded] y coordinate of the _modified_ location + * @param[zModded] z coordinate of the _modified_ location + * @param[xTarget] x coordinate of the _target_ location + * @param[yTarget] y coordinate of the _target_ location + * @param[zTarget] z coordinate of the _target_ location + */ +class OffsetBlockAccess(val original: IBlockAccess, + @JvmField val xModded: Int, @JvmField val yModded: Int, @JvmField val zModded: Int, + @JvmField val xTarget: Int, @JvmField val yTarget: Int, @JvmField val zTarget: Int) : IBlockAccess { + + inline fun withOffset(x: Int, y: Int, z: Int, func: (Int,Int,Int)->T): T { + if (x == xModded && y == yModded && z == zModded) { + return func(xTarget, yTarget, zTarget) + } else { + return func(x, y, z) + } + } + + override fun getBlock(x: Int, y: Int, z: Int) = withOffset(x, y, z) + { xAct, yAct, zAct -> original.getBlock(xAct, yAct, zAct) } + override fun getBlockMetadata(x: Int, y: Int, z: Int) = withOffset(x, y, z) + { xAct, yAct, zAct -> original.getBlockMetadata(xAct, yAct, zAct) } + override fun getTileEntity(x: Int, y: Int, z: Int) = withOffset(x, y, z) + { xAct, yAct, zAct -> original.getTileEntity(xAct, yAct, zAct) } + override fun isSideSolid(x: Int, y: Int, z: Int, side: ForgeDirection?, _default: Boolean) = withOffset(x, y, z) + { xAct, yAct, zAct -> original.isSideSolid(xAct, yAct, zAct, side, _default) } + override fun isAirBlock(x: Int, y: Int, z: Int) = withOffset(x, y, z) + { xAct, yAct, zAct -> original.isAirBlock(xAct, yAct, zAct) } + override fun getLightBrightnessForSkyBlocks(x: Int, y: Int, z: Int, side: Int) = withOffset(x, y, z) + { xAct, yAct, zAct -> original.getLightBrightnessForSkyBlocks(xAct, yAct, zAct, side) } + override fun isBlockProvidingPowerTo(x: Int, y: Int, z: Int, side: Int) = withOffset(x, y, z) + { xAct, yAct, zAct -> original.isBlockProvidingPowerTo(xAct, yAct, zAct, side) } + override fun getBiomeGenForCoords(x: Int, z: Int) = withOffset(x, 0, z) + { xAct, yAct, zAct -> original.getBiomeGenForCoords(xAct, zAct) } + + override fun getHeight() = original.height + override fun extendedLevelsInChunkCache() = original.extendedLevelsInChunkCache() +} + +/** + * Temporarily replaces the [IBlockAccess] used by this [BlockContext] and the corresponding [ExtendedRenderBlocks] + * to use an [OffsetBlockAccess] while executing this lambda. + * + * @param[modded] the _modified_ location + * @param[target] the _target_ location + * @param[func] the lambda to execute + */ +inline fun BlockContext.withOffset(modded: Int3, target: Int3, func: () -> T): T { + val original = world!! + world = OffsetBlockAccess(original, x + modded.x, y + modded.y, z + modded.z, x + target.x, y + target.y, z + target.z) + renderBlocks.blockAccess = world + val result = func() + world = original + renderBlocks.blockAccess = original + return result +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/render/PixelFormat.kt b/src/main/kotlin/mods/octarinecore/client/render/PixelFormat.kt new file mode 100644 index 0000000..bc4e58a --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/render/PixelFormat.kt @@ -0,0 +1,56 @@ +@file:JvmName("PixelFormat") +package mods.octarinecore.client.render + +import java.awt.Color + +/** List of bit-shift offsets in packed brightness values where meaningful (4-bit) data is contained. */ +var brightnessComponents = listOf(20, 4) + +/** Multiply the components of this packed brightness value with the given [Float]. */ +infix fun Int.brMul(f: Float): Int { + val weight = (f * 256.0f).toInt() + var result = 0 + brightnessComponents.forEach { shift -> + val raw = (this shr shift) and 15 + val weighted = (raw) * weight / 256 + result = result or (weighted shl shift) + } + return result +} + +/** Multiply the components of this packed color value with the given [Float]. */ +infix fun Int.colorMul(f: Float): Int { + val weight = (f * 256.0f).toInt() + val red = (this shr 16 and 255) * weight / 256 + val green = (this shr 8 and 255) * weight / 256 + val blue = (this and 255) * weight / 256 + return (red shl 16) or (green shl 8) or blue +} + +/** Sum the components of all packed brightness values given. */ +fun brSum(multiplier: Float?, vararg brightness: Int): Int { + val sum = Array(brightnessComponents.size) { 0 } + brightnessComponents.forEachIndexed { idx, shift -> brightness.forEach { br -> + val comp = (br shr shift) and 15 + sum[idx] += comp + } } + var result = 0 + brightnessComponents.forEachIndexed { idx, shift -> + val comp = if (multiplier == null) + ((sum[idx]) shl shift) + else + ((sum[idx].toFloat() * multiplier).toInt() shl shift) + result = result or comp + } + return result +} + +data class HSB(var hue: Float, var saturation: Float, var brightness: Float) { + companion object { + fun fromColor(color: Int): HSB { + val hsbVals = Color.RGBtoHSB((color shr 16) and 255, (color shr 8) and 255, color and 255, null) + return HSB(hsbVals[0], hsbVals[1], hsbVals[2]) + } + } + val asColor: Int get() = Color.HSBtoRGB(hue, saturation, brightness) +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/render/RenderBlocks.kt b/src/main/kotlin/mods/octarinecore/client/render/RenderBlocks.kt new file mode 100644 index 0000000..22cfb43 --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/render/RenderBlocks.kt @@ -0,0 +1,147 @@ +package mods.octarinecore.client.render + +import cpw.mods.fml.client.registry.ISimpleBlockRenderingHandler +import cpw.mods.fml.client.registry.RenderingRegistry +import mods.octarinecore.metaprog.reflectField +import mods.octarinecore.metaprog.reflectStaticField +import net.minecraft.block.Block +import net.minecraft.client.renderer.RenderBlocks +import net.minecraft.util.IIcon +import net.minecraftforge.common.util.ForgeDirection +import net.minecraftforge.common.util.ForgeDirection.* + +/** Reference to the handler list in Forge [RenderingRegistry]. */ +val renderingHandlers: Map = RenderingRegistry::class.java + .reflectStaticField("INSTANCE")!! + .reflectField>("blockRenderers")!! + +/** + * Used instead of the vanilla [RenderBlocks] to get to the AO values and textures used in rendering + * without duplicating vanilla code. + */ +class ExtendedRenderBlocks : RenderBlocks() { + + /** Captures the AO values and textures used in a specific rendering pass when rendering a block. */ + val capture = ShadingCapture() + + override fun renderFaceXPos(block: Block?, x: Double, y: Double, z: Double, icon: IIcon?) = renderFace(EAST, block, x, y, z, icon) + override fun renderFaceXNeg(block: Block?, x: Double, y: Double, z: Double, icon: IIcon?) = renderFace(WEST, block, x, y, z, icon) + override fun renderFaceYPos(block: Block?, x: Double, y: Double, z: Double, icon: IIcon?) = renderFace(UP, block, x, y, z, icon) + override fun renderFaceYNeg(block: Block?, x: Double, y: Double, z: Double, icon: IIcon?) = renderFace(DOWN, block, x, y, z, icon) + override fun renderFaceZPos(block: Block?, x: Double, y: Double, z: Double, icon: IIcon?) = renderFace(SOUTH, block, x, y, z, icon) + override fun renderFaceZNeg(block: Block?, x: Double, y: Double, z: Double, icon: IIcon?) = renderFace(NORTH, block, x, y, z, icon) + + /** + * Render a block face, saving relevant data if appropriate. + */ + @Suppress("NON_EXHAUSTIVE_WHEN") + fun renderFace(face: ForgeDirection, block: Block?, x: Double, y: Double, z: Double, icon: IIcon?) { + if (capture.isCorrectPass(face)) { + saveAllShading(face); capture.icons[face.ordinal] = icon + } + if (capture.renderCallback(capture, face, capture.passes[face.ordinal], icon)) when (face) { + EAST -> super.renderFaceXPos(block, x, y, z, icon) + WEST -> super.renderFaceXNeg(block, x, y, z, icon) + UP -> super.renderFaceYPos(block, x, y, z, icon) + DOWN -> super.renderFaceYNeg(block, x, y, z, icon) + SOUTH -> super.renderFaceZPos(block, x, y, z, icon) + NORTH -> super.renderFaceZNeg(block, x, y, z, icon) + } + } + + fun saveTopLeft(face: ForgeDirection, corner: Pair) = + capture.aoShading(face, corner.first, corner.second) + .set(brightnessTopLeft, colorRedTopLeft, colorGreenTopLeft, colorBlueTopLeft) + + fun saveTopRight(face: ForgeDirection, corner: Pair) = + capture.aoShading(face, corner.first, corner.second) + .set(brightnessTopRight, colorRedTopRight, colorGreenTopRight, colorBlueTopRight) + + fun saveBottomLeft(face: ForgeDirection, corner: Pair) = + capture.aoShading(face, corner.first, corner.second) + .set(brightnessBottomLeft, colorRedBottomLeft, colorGreenBottomLeft, colorBlueBottomLeft) + + fun saveBottomRight(face: ForgeDirection, corner: Pair) = + capture.aoShading(face, corner.first, corner.second) + .set(brightnessBottomRight, colorRedBottomRight, colorGreenBottomRight, colorBlueBottomRight) + + fun saveAllShading(face: ForgeDirection) { + saveTopLeft(face, faceCorners[face.ordinal].topLeft) + saveTopRight(face, faceCorners[face.ordinal].topRight) + saveBottomLeft(face, faceCorners[face.ordinal].bottomLeft) + saveBottomRight(face, faceCorners[face.ordinal].bottomRight) + } +} + +/** + * Captures the AO values and textures used in a specific rendering pass when rendering a block. + */ +class ShadingCapture { + /** Sparse array of stored AO data. */ + val aoShadings = arrayOfNulls(6 * 6 * 6) + + /** List of stored AO data (only valid instances). */ + var shadingsList = listOf() + + /** List of stored texture data. */ + val icons = arrayOfNulls(6) + + /** Number of passes to go on a given face. */ + val passes = Array(6) { 0 } + + /** lambda to determine which faces to render. */ + var renderCallback = alwaysRender + + init { + (0..5).forEach { i1 -> + (0..5).forEach { i2 -> + (i2..5).forEach { i3 -> + aoShadings[cornerId(i1, i2, i3)] = AoData() + } + } + } + shadingsList = aoShadings.filterNotNull() + } + + /** + * Get the AO data of a specific corner. + * + * The two corner directions are interchangeable. All 3 parameters must lie on different axes. + * + * @param[face] block face + * @param[corner1] first direction of corner on face + * @param[corner2] second direction of corner on face + */ + fun aoShading(face: ForgeDirection, corner1: ForgeDirection, corner2: ForgeDirection) = + aoShadings[cornerId(face, corner1, corner2)]!! + + /** Returns true if the AO and texture data should be saved. Mutates state. */ + fun isCorrectPass(face: ForgeDirection) = (passes[face.ordinal]-- > 0) + + /** + * Reset all data and pass counters. + * + * @param[targetPass] which render pass to save + */ + fun reset(targetPass: Int) { + shadingsList.forEach { it.reset() } + (0..5).forEach { idx -> icons[idx] = null; passes[idx] = targetPass } + } + + /** One-dimensional index of a specific corner. */ + protected fun cornerId(face: Int, corner1: Int, corner2: Int) = when (corner2 > corner1) { + true -> 36 * face + 6 * corner1 + corner2 + false -> 36 * face + 6 * corner2 + corner1 + } + + /** One-dimensional index of a specific corner. */ + protected fun cornerId(face: ForgeDirection, corner1: ForgeDirection, corner2: ForgeDirection) = + cornerId(face.ordinal, corner1.ordinal, corner2.ordinal) +} + +/** Lambda to render all faces of a block */ +val alwaysRender: (ShadingCapture, ForgeDirection, Int, IIcon?) -> Boolean = { ctx, face, pass, icon -> true } + +/** Lambda to render no faces of a block */ +val neverRender: (ShadingCapture, ForgeDirection, Int, IIcon?) -> Boolean = { ctx, face, pass, icon -> false } + diff --git a/src/main/kotlin/mods/octarinecore/client/render/Shaders.kt b/src/main/kotlin/mods/octarinecore/client/render/Shaders.kt new file mode 100644 index 0000000..d08eea9 --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/render/Shaders.kt @@ -0,0 +1,148 @@ +package mods.octarinecore.client.render + +import net.minecraftforge.common.util.ForgeDirection + +const val defaultCornerDimming = 0.5f +const val defaultEdgeDimming = 0.8f + +// ================================ +// Resolvers for automatic shading +// ================================ +fun cornerAo(fallbackAxis: Axis): (ForgeDirection, ForgeDirection, ForgeDirection)->Shader = { face, dir1, dir2 -> + val fallbackDir = listOf(face, dir1, dir2).find { it.axis == fallbackAxis }!! + CornerSingleFallback(face, dir1, dir2, fallbackDir) +} +val cornerFlat = { face: ForgeDirection, dir1: ForgeDirection, dir2: ForgeDirection -> FaceFlat(face) } +fun cornerAoTri(func: (AoData, AoData)-> AoData) = { face: ForgeDirection, dir1: ForgeDirection, dir2: ForgeDirection -> + CornerTri(face, dir1, dir2, func) +} +val cornerAoMaxGreen = cornerAoTri { s1, s2 -> if (s1.green > s2.green) s1 else s2 } + +fun cornerInterpolate(edgeAxis: Axis, weight: Float, dimming: Float): (ForgeDirection, ForgeDirection, ForgeDirection)->Shader = { dir1, dir2, dir3 -> + val edgeDir = listOf(dir1, dir2, dir3).find { it.axis == edgeAxis }!! + val faceDirs = listOf(dir1, dir2, dir3).filter { it.axis != edgeAxis } + CornerInterpolateDimming(faceDirs[0], faceDirs[1], edgeDir, weight, dimming) +} + +// ================================ +// Shaders +// ================================ +object NoShader : Shader { + override fun shade(context: ShadingContext, vertex: RenderVertex) = vertex.shade(AoData.black) + override fun rotate(rot: Rotation) = this +} + +class CornerSingleFallback(val face: ForgeDirection, val dir1: ForgeDirection, val dir2: ForgeDirection, val fallbackDir: ForgeDirection, val fallbackDimming: Float = defaultCornerDimming) : Shader { + val offset = Int3(fallbackDir) + override fun shade(context: ShadingContext, vertex: RenderVertex) { + val shading = context.aoShading(face, dir1, dir2) + if (shading.valid) + vertex.shade(shading) + else + vertex.shade(context.blockBrightness(offset) brMul fallbackDimming, context.blockColor(offset) colorMul fallbackDimming) + } + override fun rotate(rot: Rotation) = CornerSingleFallback(face.rotate(rot), dir1.rotate(rot), dir2.rotate(rot), fallbackDir.rotate(rot), fallbackDimming) +} + +inline fun accumulate(v1: AoData?, v2: AoData?, func: ((AoData, AoData)-> AoData)): AoData? { + val v1ok = v1 != null && v1.valid + val v2ok = v2 != null && v2.valid + if (v1ok && v2ok) return func(v1!!, v2!!) + if (v1ok) return v1 + if (v2ok) return v2 + return null +} + +class CornerTri(val face: ForgeDirection, val dir1: ForgeDirection, val dir2: ForgeDirection, + val func: ((AoData, AoData)-> AoData)) : Shader { + override fun shade(context: ShadingContext, vertex: RenderVertex) { + var acc = accumulate( + context.aoShading(face, dir1, dir2), + context.aoShading(dir1, face, dir2), + func) + acc = accumulate( + acc, + context.aoShading(dir2, face, dir1), + func) + vertex.shade(acc ?: AoData.black) + } + override fun rotate(rot: Rotation) = CornerTri(face.rotate(rot), dir1.rotate(rot), dir2.rotate(rot), func) +} + +class EdgeInterpolateFallback(val face: ForgeDirection, val edgeDir: ForgeDirection, val pos: Double, val fallbackDimming: Float = defaultEdgeDimming): Shader { + val offset = Int3(edgeDir) + val edgeAxis = axes.find { it != face.axis && it != edgeDir.axis }!! + val weightN = (0.5 - pos).toFloat() + val weightP = (0.5 + pos).toFloat() + + override fun shade(context: ShadingContext, vertex: RenderVertex) { + val shadingP = context.aoShading(face, edgeDir, (edgeAxis to Dir.P).face) + val shadingN = context.aoShading(face, edgeDir, (edgeAxis to Dir.N).face) + if (!shadingP.valid && !shadingN.valid) + return vertex.shade(context.blockBrightness(offset) brMul fallbackDimming, context.blockColor(offset) colorMul fallbackDimming) + if (!shadingP.valid) return vertex.shade(shadingN) + if (!shadingN.valid) return vertex.shade(shadingP) + vertex.shade(shadingP, shadingN, weightP, weightN) + } + override fun rotate(rot: Rotation) = EdgeInterpolateFallback(face.rotate(rot), edgeDir.rotate(rot), pos) +} + +class CornerInterpolateDimming(val face1: ForgeDirection, val face2: ForgeDirection, val edgeDir: ForgeDirection, + val weight: Float, val dimming: Float, val fallbackDimming: Float = defaultCornerDimming) : Shader { + val offset = Int3(edgeDir) + override fun shade(context: ShadingContext, vertex: RenderVertex) { + var shading1 = context.aoShading(face1, edgeDir, face2) + var shading2 = context.aoShading(face2, edgeDir, face1) + var weight1 = weight + var weight2 = 1.0f - weight + if (!shading1.valid && !shading2.valid) + return vertex.shade(context.blockBrightness(offset) brMul fallbackDimming, context.blockColor(offset) colorMul fallbackDimming) + if (!shading1.valid) { shading1 = shading2; weight1 *= dimming } + if (!shading2.valid) { shading2 = shading1; weight2 *= dimming } + vertex.shade(shading1, shading2, weight1, weight2) + } + + override fun rotate(rot: Rotation) = + CornerInterpolateDimming(face1.rotate(rot), face2.rotate(rot), edgeDir.rotate(rot), weight, dimming, fallbackDimming) +} + +class FaceCenter(val face: ForgeDirection): Shader { + override fun shade(context: ShadingContext, vertex: RenderVertex) { + vertex.red = 0.0f; vertex.green = 0.0f; vertex.blue = 0.0f; + val b = IntArray(4) + faceCorners[face.ordinal].asList.forEachIndexed { idx, corner -> + val shading = context.aoShading(face, corner.first, corner.second) + vertex.red += shading.red + vertex.green += shading.green + vertex.blue += shading.blue + b[idx] = shading.brightness + } + vertex.apply { red *= 0.25f; green *= 0.25f; blue *= 0.25f } + vertex.brightness = brSum(0.25f, *b) + } + override fun rotate(rot: Rotation) = FaceCenter(face.rotate(rot)) +} + +class FaceFlat(val face: ForgeDirection): Shader { + override fun shade(context: ShadingContext, vertex: RenderVertex) { + val color = context.blockColor(Int3.zero) + vertex.shade(context.blockBrightness(face.offset), color) + } + override fun rotate(rot: Rotation): Shader = FaceFlat(face.rotate(rot)) +} + +class FlatOffset(val offset: Int3): Shader { + override fun shade(context: ShadingContext, vertex: RenderVertex) { + vertex.brightness = context.blockBrightness(offset) + vertex.setColor(context.blockColor(offset)) + } + override fun rotate(rot: Rotation): Shader = this +} + +class FlatOffsetNoColor(val offset: Int3): Shader { + override fun shade(context: ShadingContext, vertex: RenderVertex) { + vertex.brightness = context.blockBrightness(offset) + vertex.red = 1.0f; vertex.green = 1.0f; vertex.blue = 1.0f + } + override fun rotate(rot: Rotation): Shader = this +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/render/Shading.kt b/src/main/kotlin/mods/octarinecore/client/render/Shading.kt new file mode 100644 index 0000000..568bdf1 --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/render/Shading.kt @@ -0,0 +1,131 @@ +package mods.octarinecore.client.render + +import net.minecraftforge.common.util.ForgeDirection +import java.lang.Math.* + +/** Holds shading values for block corners as calculated by vanilla Minecraft rendering. */ +class AoData() { + var valid = false + var brightness = 0 + var red: Float = 0.0f + var green: Float = 0.0f + var blue: Float = 0.0f + + fun reset() { valid = false } + + fun set(brightness: Int, red: Float, green: Float, blue: Float) { + if (valid) return + this.valid = true + this.brightness = brightness + this.red = red + this.green = green + this.blue = blue + } + + companion object { + val black = AoData(); + } +} + +/** + * Instances of this interface are associated with [Model] vertices, and used to apply brightness and color + * values to a [RenderVertex]. + */ +interface Shader { + /** + * Set shading values of a [RenderVertex] + * + * @param[context] context that can be queried for shading data in a [Model]-relative frame of reference + * @param[vertex] the [RenderVertex] to manipulate + */ + fun shade(context: ShadingContext, vertex: RenderVertex) + + /** + * Return a new rotated version of this [Shader]. Used during [Model] setup when rotating the model itself. + */ + fun rotate(rot: Rotation): Shader + + /** Set all shading values on the [RenderVertex] to match the given [AoData]. */ + fun RenderVertex.shade(shading: AoData) { + brightness = shading.brightness; red = shading.red; green = shading.green; blue = shading.blue + } + + /** Set the shading values on the [RenderVertex] to a weighted average of the two [AoData] instances. */ + fun RenderVertex.shade(shading1: AoData, shading2: AoData, weight1: Float = 0.5f, weight2: Float = 0.5f) { + red = min(shading1.red * weight1 + shading2.red * weight2, 1.0f) + green = min(shading1.green * weight1 + shading2.green * weight2, 1.0f) + blue = min(shading1.blue * weight1 + shading2.blue * weight2, 1.0f) + brightness = brSum(null, shading1.brightness brMul weight1, shading2.brightness brMul weight2) + } + + /** + * Set the shading values on the [RenderVertex] directly. + * + * @param[brightness] packed brightness value + * @param[color] packed color value + */ + fun RenderVertex.shade(brightness: Int, color: Int) { + this.brightness = brightness; setColor(color) + } +} + +/** + * Returns a shader resolver for quads that point towards one of the 6 block faces. + * The resolver works the following way: + * - determines which face the _quad_ normal points towards (if not overridden) + * - determines the distance of the _vertex_ to the corners and edge midpoints on that block face + * - if _corner_ is given, and the _vertex_ is closest to a block corner, returns the [Shader] created by _corner_ + * - if _edge_ is given, and the _vertex_ is closest to an edge midpoint, returns the [Shader] created by _edge_ + * + * @param[overrideFace] assume the given face instead of going by the _quad_ normal + * @param[corner] shader instantiation lambda for corner vertices + * @param[edge] shader instantiation lambda for edge midpoint vertices + */ +fun faceOrientedAuto(overrideFace: ForgeDirection? = null, + corner: ((ForgeDirection, ForgeDirection, ForgeDirection)->Shader)? = null, + edge: ((ForgeDirection, ForgeDirection)->Shader)? = null) = + fun(quad: Quad, vertex: Vertex): Shader { + val quadFace = overrideFace ?: quad.normal.nearestCardinal + val nearestCorner = nearestPosition(vertex.xyz, faceCorners[quadFace.ordinal].asList) { + (quadFace.vec + it.first.vec + it.second.vec) * 0.5 + } + val nearestEdge = nearestPosition(vertex.xyz, quadFace.perpendiculars) { + (quadFace.vec + it.vec) * 0.5 + } + if (edge != null && (nearestEdge.second < nearestCorner.second || corner == null)) + return edge(quadFace, nearestEdge.first) + else return corner!!(quadFace, nearestCorner.first.first, nearestCorner.first.second) + } + +/** + * Returns a shader resolver for quads that point towards one of the 12 block edges. + * The resolver works the following way: + * - determines which edge the _quad_ normal points towards (if not overridden) + * - determines which face midpoint the _vertex_ is closest to, of the 2 block faces that share this edge + * - determines which block corner _of this face_ the _vertex_ is closest to + * - returns the [Shader] created by _corner_ + * + * @param[overrideEdge] assume the given edge instead of going by the _quad_ normal + * @param[corner] shader instantiation lambda + */ +fun edgeOrientedAuto(overrideEdge: Pair? = null, + corner: (ForgeDirection, ForgeDirection, ForgeDirection)->Shader) = + fun(quad: Quad, vertex: Vertex): Shader { + val edgeDir = overrideEdge ?: nearestAngle(quad.normal, boxEdges) { it.first.vec + it.second.vec }.first + val nearestFace = nearestPosition(vertex.xyz, edgeDir.toList()) { it.vec }.first + val nearestCorner = nearestPosition(vertex.xyz, faceCorners[nearestFace.ordinal].asList) { + (nearestFace.vec + it.first.vec + it.second.vec) * 0.5 + }.first + return corner(nearestFace, nearestCorner.first, nearestCorner.second) + } + +fun faceOrientedInterpolate(overrideFace: ForgeDirection? = null) = + fun(quad: Quad, vertex: Vertex): Shader { + val resolver = faceOrientedAuto(overrideFace, edge = { face, edgeDir -> + val axis = axes.find { it != face.axis && it != edgeDir.axis }!! + val vec = Double3((axis to Dir.P).face) + val pos = vertex.xyz.dot(vec) + EdgeInterpolateFallback(face, edgeDir, pos) + }) + return resolver(quad, vertex) + } \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/resource/CenteringTextureGenerator.kt b/src/main/kotlin/mods/octarinecore/client/resource/CenteringTextureGenerator.kt new file mode 100644 index 0000000..623e0b9 --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/resource/CenteringTextureGenerator.kt @@ -0,0 +1,33 @@ +package mods.octarinecore.client.resource + +import java.awt.image.BufferedImage +import java.lang.Math.* + +class CenteringTextureGenerator(domain: String, val aspectWidth: Int, val aspectHeight: Int) : TextureGenerator(domain) { + + override fun generate(params: ParameterList): BufferedImage? { + val target = targetResource(params)!! + val baseTexture = resourceManager[target.second]?.loadImage() ?: return null + + val frameWidth = baseTexture.width + val frameHeight = baseTexture.width * aspectHeight / aspectWidth + val frames = baseTexture.height / frameHeight + val size = max(frameWidth, frameHeight) + + val resultTexture = BufferedImage(size, size * frames, BufferedImage.TYPE_4BYTE_ABGR) + val graphics = resultTexture.createGraphics() + + // iterate all frames + for (frame in 0 .. frames - 1) { + val baseFrame = baseTexture.getSubimage(0, size * frame, frameWidth, frameHeight) + val resultFrame = BufferedImage(size, size, BufferedImage.TYPE_4BYTE_ABGR) + + resultFrame.createGraphics().apply { + drawImage(baseFrame, (size - frameWidth) / 2, (size - frameHeight) / 2, null) + } + graphics.drawImage(resultFrame, 0, size * frame, null) + } + + return resultTexture + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/resource/ResourceGeneration.kt b/src/main/kotlin/mods/octarinecore/client/resource/ResourceGeneration.kt new file mode 100644 index 0000000..8941669 --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/resource/ResourceGeneration.kt @@ -0,0 +1,126 @@ +package mods.octarinecore.client.resource + +import cpw.mods.fml.client.FMLClientHandler +import mods.betterfoliage.loader.Refs +import mods.octarinecore.metaprog.reflectField +import net.minecraft.client.resources.IResourcePack +import net.minecraft.client.resources.data.IMetadataSerializer +import net.minecraft.client.resources.data.PackMetadataSection +import net.minecraft.util.ChatComponentText +import net.minecraft.util.ResourceLocation +import java.io.InputStream +import java.util.* + +/** + * [IResourcePack] containing generated resources. Adds itself to the default resource pack list + * of Minecraft, so it is invisible and always active. + * + * @param[name] Name of the resource pack + * @param[generators] List of resource generators + */ +class GeneratorPack(val name: String, vararg val generators: GeneratorBase) : IResourcePack { + + init { + // add to the default resource packs + FMLClientHandler.instance().reflectField>("resourcePackList")!!.add(this) + } + + override fun getPackName() = name + override fun getPackImage() = null + override fun getResourceDomains() = HashSet(generators.map { it.domain }) + override fun getPackMetadata(serializer: IMetadataSerializer?, type: String?) = + if (type == "pack") PackMetadataSection(ChatComponentText("Generated resources"), 1) else null + + override fun resourceExists(location: ResourceLocation?): Boolean = + if (location == null) false + else generators.find { + it.domain == location.resourceDomain && it.resourceExists(location) + } != null + + override fun getInputStream(location: ResourceLocation?): InputStream? = + if (location == null) null + else generators.filter { + it.domain == location.resourceDomain && it.resourceExists(location) + }.map { it.getInputStream(location) } + .filterNotNull().first() + + operator fun get(location: ResourceLocation?) = getInputStream(location) +} + +/** + * Abstract base class for resource generators + * + * @param[domain] Resource domain of generator + */ +abstract class GeneratorBase(val domain: String) { + /** @see [IResourcePack.resourceExists] */ + abstract fun resourceExists(location: ResourceLocation?): Boolean + + /** @see [IResourcePack.getInputStream] */ + abstract fun getInputStream(location: ResourceLocation?): InputStream? +} + +/** + * Collection of named [String]-valued key-value pairs, with an extra unnamed (keyless) value. + * Meant to be encoded as a pipe-delimited list, and used as a [ResourceLocation] path + * to parametrized generated resources. + * + * @param[params] key-value pairs + * @param[value] keyless extra value + */ +class ParameterList(val params: Map, val value: String?) { + override fun toString() = + params.entries + .sortedBy { it.key } + .fold("") { result, entry -> result + "|${entry.key}=${entry.value}"} + + (value?.let { "|$it" } ?: "") + + /** Return the value of the given parameter. */ + operator fun get(key: String) = params[key] + + /** Check if the given parameter exists in this list. */ + operator fun contains(key: String) = key in params + + /** Return a new [ParameterList] with the given key-value pair appended to it. */ + operator fun plus(pair: Pair) = ParameterList(params + pair, this.value) + + companion object { + /** + * Recreate the parameter list from the encoded string, i.e. the opposite of [toString]. + * + * Everything before the first pipe character is dropped, so the decoding works even if + * something is prepended to the list (like _textures/blocks/_) + */ + fun fromString(input: String): ParameterList { + val params = hashMapOf() + var value: String? = null + val slices = input.dropWhile { it != '|'}.split('|') + slices.forEach { + if (it.contains('=')) { + val keyValue = it.split('=') + if (keyValue.size == 2) params.put(keyValue[0], keyValue[1]) + } else value = it + } + return ParameterList(params, value) + } + + } +} + +/** + * [GeneratorBase] returning parametrized generated resources. + * + * @param[domain] Resource domain of generator. + */ +abstract class ParameterBasedGenerator(domain: String) : GeneratorBase(domain) { + /** @see [IResourcePack.resourceExists] */ + abstract fun resourceExists(params: ParameterList): Boolean + + /** @see [IResourcePack.getInputStream] */ + abstract fun getInputStream(params: ParameterList): InputStream? + + override fun resourceExists(location: ResourceLocation?) = + resourceExists(ParameterList.fromString(location?.resourcePath ?: "")) + override fun getInputStream(location: ResourceLocation?) = + getInputStream(ParameterList.fromString(location?.resourcePath ?: "")) +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/resource/ResourceHandler.kt b/src/main/kotlin/mods/octarinecore/client/resource/ResourceHandler.kt new file mode 100644 index 0000000..199ca5d --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/resource/ResourceHandler.kt @@ -0,0 +1,126 @@ +package mods.octarinecore.client.resource + +import cpw.mods.fml.client.event.ConfigChangedEvent +import cpw.mods.fml.common.FMLCommonHandler +import cpw.mods.fml.common.eventhandler.SubscribeEvent +import mods.octarinecore.client.render.Double3 +import mods.octarinecore.client.render.Int3 +import mods.octarinecore.client.render.Model +import net.minecraft.client.renderer.texture.IIconRegister +import net.minecraft.util.IIcon +import net.minecraft.util.MathHelper +import net.minecraft.util.ResourceLocation +import net.minecraft.world.World +import net.minecraft.world.gen.NoiseGeneratorSimplex +import net.minecraftforge.client.event.TextureStitchEvent +import net.minecraftforge.common.MinecraftForge +import net.minecraftforge.event.world.WorldEvent +import java.util.* + +// ============================ +// Resource types +// ============================ +interface IStitchListener { fun onStitch(atlas: IIconRegister) } +interface IConfigChangeListener { fun onConfigChange() } +interface IWorldLoadListener { fun onWorldLoad(world: World) } + +/** + * Base class for declarative resource handling. + * + * Resources are automatically reloaded/recalculated when the appropriate events are fired. + * + * @param[modId] mod ID associated with this handler (used to filter config change events) + */ +open class ResourceHandler(val modId: String) { + + val resources = linkedListOf() + open fun afterStitch() {} + + // ============================ + // Self-registration + // ============================ + init { + MinecraftForge.EVENT_BUS.register(this) + FMLCommonHandler.instance().bus().register(this) + } + + // ============================ + // Resource declarations + // ============================ + fun iconStatic(domain: String, path: String) = IconHolder(domain, path).apply { resources.add(this) } + fun iconStatic(location: ResourceLocation) = iconStatic(location.resourceDomain, location.resourcePath) + fun iconSet(domain: String, pathPattern: String) = IconSet(domain, pathPattern).apply { resources.add(this) } + fun iconSet(location: ResourceLocation) = iconSet(location.resourceDomain, location.resourcePath) + fun model(init: Model.()->Unit) = ModelHolder(init).apply { resources.add(this) } + fun modelSet(num: Int, init: Model.(Int)->Unit) = ModelSet(num, init).apply { resources.add(this) } + fun vectorSet(num: Int, init: (Int)-> Double3) = VectorSet(num, init).apply { resources.add(this) } + fun simplexNoise() = SimplexNoise().apply { resources.add(this) } + + // ============================ + // Event registration + // ============================ + @SubscribeEvent + fun onStitch(event: TextureStitchEvent.Pre) { + if (event.map.textureType == 0) { + resources.forEach { (it as? IStitchListener)?.onStitch(event.map) } + afterStitch() + } + } + + @SubscribeEvent + fun handleConfigChange(event: ConfigChangedEvent.OnConfigChangedEvent) { + if (event.modID == modId) resources.forEach { (it as? IConfigChangeListener)?.onConfigChange() } + } + + @SubscribeEvent + fun handleWorldLoad(event: WorldEvent.Load) = + resources.forEach { (it as? IWorldLoadListener)?.onWorldLoad(event.world) } +} + +// ============================ +// Resource container classes +// ============================ +class IconHolder(val domain: String, val name: String) : IStitchListener { + var icon: IIcon? = null + override fun onStitch(atlas: IIconRegister) { icon = atlas.registerIcon("$domain:$name") } +} + +class ModelHolder(val init: Model.()->Unit): IConfigChangeListener { + var model: Model = Model().apply(init) + override fun onConfigChange() { model = Model().apply(init) } +} + +class IconSet(val domain: String, val namePattern: String) : IStitchListener { + val icons = arrayOfNulls(16) + var num = 0 + + override fun onStitch(atlas: IIconRegister) { + num = 0; + (0..15).forEach { idx -> + val locReal = ResourceLocation(domain, "textures/blocks/${namePattern.format(idx)}.png") + if (resourceManager[locReal] != null) icons[num++] = atlas.registerIcon("$domain:${namePattern.format(idx)}") + } + } + + operator fun get(idx: Int) = if (num == 0) null else icons[idx % num] +} + +class ModelSet(val num: Int, val init: Model.(Int)->Unit): IConfigChangeListener { + val models = Array(num) { Model().apply{ init(it) } } + override fun onConfigChange() { (0..num-1).forEach { models[it] = Model().apply{ init(it) } } } + operator fun get(idx: Int) = models[idx % num] +} + +class VectorSet(val num: Int, val init: (Int)->Double3): IConfigChangeListener { + val models = Array(num) { init(it) } + override fun onConfigChange() { (0..num-1).forEach { models[it] = init(it) } } + operator fun get(idx: Int) = models[idx % num] +} + +class SimplexNoise() : IWorldLoadListener { + var noise = NoiseGeneratorSimplex() + override fun onWorldLoad(world: World) { noise = NoiseGeneratorSimplex(Random(world.worldInfo.seed)) + } + operator fun get(x: Int, z: Int) = MathHelper.floor_double((noise.func_151605_a(x.toDouble(), z.toDouble()) + 1.0) * 32.0) + operator fun get(pos: Int3) = get(pos.x, pos.z) +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/resource/TextureGenerator.kt b/src/main/kotlin/mods/octarinecore/client/resource/TextureGenerator.kt new file mode 100644 index 0000000..b4c9ef3 --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/resource/TextureGenerator.kt @@ -0,0 +1,88 @@ +package mods.octarinecore.client.resource + +import mods.octarinecore.client.resource.ResourceType.* +import net.minecraft.client.resources.IResource +import net.minecraft.util.ResourceLocation +import java.awt.image.BufferedImage +import java.io.InputStream + +/** Type of generated texture resource */ +enum class ResourceType { + COLOR, // regular diffuse map + METADATA, // texture metadata + NORMAL, // ShadersMod normal map + SPECULAR // ShadersMod specular map +} + +/** + * Generator returning textures based on a single other texture. This texture is located with the + * _dom_ and _path_ parameters of a [ParameterList]. + * + * @param[domain] Resource domain of generator + */ +abstract class TextureGenerator(domain: String) : ParameterBasedGenerator(domain) { + + /** + * Obtain a [ResourceLocation] to a generated texture + * + * @param[iconName] the name of the [TextureAtlasSprite] (not the full location) backing the generated texture + * @param[extraParams] additional parameters of the generated texture + */ + fun generatedResource(iconName: String, vararg extraParams: Pair) = ResourceLocation( + domain, + textureLocation(iconName).let { + ParameterList( + mapOf("dom" to it.resourceDomain, "path" to it.resourcePath) + + extraParams.map { Pair(it.first, it.second.toString()) }, + "generate" + ).toString() + } + ) + + /** + * Get the type and location of the texture resource encoded by the given [ParameterList]. + */ + fun targetResource(params: ParameterList): Pair? { + val baseTexture = + if (listOf("dom", "path").all { it in params }) ResourceLocation(params["dom"]!!, params["path"]!!) + else return null + return when(params.value?.toLowerCase()) { + "generate.png" -> COLOR to baseTexture + ".png" + "generate.png.mcmeta" -> METADATA to baseTexture + ".png.mcmeta" + "generate_n.png" -> NORMAL to baseTexture + "_n.png" + "generate_s.png" -> SPECULAR to baseTexture + "_s.png" + else -> null + } + } + + override fun resourceExists(params: ParameterList) = + targetResource(params)?.second?.let { resourceManager[it] != null } ?: false + + override fun getInputStream(params: ParameterList): InputStream? { + val target = targetResource(params) + return when(target?.first) { + null -> null + METADATA -> resourceManager[target!!.second]?.inputStream + else -> generate(params)?.asStream + } + } + + /** + * Generate image data from the parameter list. + */ + abstract fun generate(params: ParameterList): BufferedImage? + + /** + * Get a texture resource when multiple sizes may exist. + * + * @param[maxSize] Maximum size to consider. This value is progressively halved when searching for smaller versions. + * @param[maskPath] Location of the texture of the given size + * + */ + fun getMultisizeTexture(maxSize: Int, maskPath: (Int)->ResourceLocation): IResource? { + var size = maxSize + val sizes = linkedListOf() + while(size > 2) { sizes.add(size); size /= 2 } + return sizes.map { resourceManager[maskPath(it)] }.filterNotNull().firstOrNull() + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/resource/Utils.kt b/src/main/kotlin/mods/octarinecore/client/resource/Utils.kt new file mode 100644 index 0000000..97ca2d5 --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/resource/Utils.kt @@ -0,0 +1,90 @@ +@file:JvmName("Utils") +package mods.octarinecore.client.resource + +import mods.octarinecore.PI2 +import mods.octarinecore.client.render.HSB +import mods.octarinecore.tryDefault +import net.minecraft.client.Minecraft +import net.minecraft.client.renderer.texture.TextureAtlasSprite +import net.minecraft.client.resources.IResource +import net.minecraft.client.resources.IResourceManager +import net.minecraft.client.resources.SimpleReloadableResourceManager +import net.minecraft.util.ResourceLocation +import java.awt.image.BufferedImage +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.lang.Math.* +import javax.imageio.ImageIO + +/** Concise getter for the Minecraft resource manager. */ +val resourceManager: SimpleReloadableResourceManager get() = Minecraft.getMinecraft().resourceManager as SimpleReloadableResourceManager + +/** Append a string to the [ResourceLocation]'s path. */ +operator fun ResourceLocation.plus(str: String) = ResourceLocation(resourceDomain, resourcePath + str) + +/** Index operator to get a resource. */ +operator fun IResourceManager.get(domain: String, path: String): IResource? = get(ResourceLocation(domain, path)) +/** Index operator to get a resource. */ +operator fun IResourceManager.get(location: ResourceLocation): IResource? = tryDefault(null) { getResource(location) } + +/** Load an image resource. */ +fun IResource.loadImage() = ImageIO.read(this.inputStream) + +/** Get the lines of a text resource. */ +fun IResource.getLines(): List { + val result = arrayListOf() + inputStream.bufferedReader().useLines { it.forEach { result.add(it) } } + return result +} + +/** Index operator to get the RGB value of a pixel. */ +operator fun BufferedImage.get(x: Int, y: Int) = this.getRGB(x, y) +/** Index operator to set the RGB value of a pixel. */ +operator fun BufferedImage.set(x: Int, y: Int, value: Int) = this.setRGB(x, y, value) + +/** Get an [InputStream] to an image object in PNG format. */ +val BufferedImage.asStream: InputStream get() = + ByteArrayInputStream(ByteArrayOutputStream().let { ImageIO.write(this, "PNG", it); it.toByteArray() }) + +/** + * Calculate the average color of a texture. + * + * Only non-transparent pixels are considered. Averages are taken in the HSB color space (note: Hue is a circular average), + * and the result transformed back to the RGB color space. + */ +val TextureAtlasSprite.averageColor: Int? get() { + val locationNoDirs = ResourceLocation(iconName) + val locationWithDirs = ResourceLocation(locationNoDirs.resourceDomain, "textures/blocks/%s.png".format(locationNoDirs.resourcePath)) + val image = resourceManager[locationWithDirs]?.loadImage() ?: return null + + var numOpaque = 0 + var sumHueX = 0.0 + var sumHueY = 0.0 + var sumSaturation = 0.0f + var sumBrightness = 0.0f + for (x in 0..image.width - 1) + for (y in 0..image.height - 1) { + val pixel = image[x, y] + val alpha = (pixel shr 24) and 255 + val hsb = HSB.fromColor(pixel) + if (alpha == 255) { + numOpaque++ + sumHueX += cos((hsb.hue.toDouble() - 0.5) * PI2) + sumHueY += sin((hsb.hue.toDouble() - 0.5) * PI2) + sumSaturation += hsb.saturation + sumBrightness += hsb.brightness + } + } + + // circular average - transform sum vector to polar angle + val avgHue = (atan2(sumHueY.toDouble(), sumHueX.toDouble()) / PI2 + 0.5).toFloat() + return HSB(avgHue, sumSaturation / numOpaque.toFloat(), sumBrightness / numOpaque.toFloat()).asColor +} + +/** + * Get the actual location of a texture from the name of its [TextureAtlasSprite]. + */ +fun textureLocation(iconName: String) = ResourceLocation(iconName).let { + ResourceLocation(it.resourceDomain, "textures/blocks/${it.resourcePath}") +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/config/DelegatingConfig.kt b/src/main/kotlin/mods/octarinecore/config/DelegatingConfig.kt new file mode 100644 index 0000000..ab6f89a --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/config/DelegatingConfig.kt @@ -0,0 +1,232 @@ +package mods.octarinecore.config + +import com.google.common.collect.LinkedListMultimap +import cpw.mods.fml.client.config.GuiConfigEntries +import cpw.mods.fml.client.config.IConfigElement +import cpw.mods.fml.client.event.ConfigChangedEvent +import cpw.mods.fml.common.FMLCommonHandler +import cpw.mods.fml.common.eventhandler.SubscribeEvent +import mods.octarinecore.metaprog.reflectField +import mods.octarinecore.metaprog.reflectFieldsOfType +import mods.octarinecore.metaprog.reflectNestedObjects +import net.minecraftforge.common.config.ConfigElement +import net.minecraftforge.common.config.Configuration +import net.minecraftforge.common.config.Property +import kotlin.reflect.KProperty + +// ============================ +// Configuration object base +// ============================ +/** + * Base class for declarative configuration handling. + * + * Subclasses should be singleton objects, containing one layer of further singleton objects representing + * config categories (nesting is not supported). + * + * Both the root object (maps to the category _global_) and category objects can contain [ConfigPropertyBase] + * instances (either directly or as a delegate), which handle the Forge [Configuration] itself. + * + * Config properties map to language keys by their field names. + * + * @param[modId] mod ID this configuration is linked to + * @param[langPrefix] prefix to use for language keys + */ +abstract class DelegatingConfig(val modId: String, val langPrefix: String) { + + init { FMLCommonHandler.instance().bus().register(this) } + + /** The [Configuration] backing this config object. */ + var config: Configuration? = null + val rootGuiElements = linkedListOf>() + + /** Attach this config object to the given [Configuration] and update all properties. */ + fun attach(config: Configuration) { + this.config = config + val subProperties = LinkedListMultimap.create() + rootGuiElements.clear() + + forEachProperty { category, name, property -> + property.lang = property.lang ?: "$category.$name" + property.attach(config, langPrefix, category, name) + property.guiProperties.forEach { guiProperty -> + property.guiClass?.let { guiProperty.setConfigEntryClass(it) } + if (category == "global") rootGuiElements.add(ConfigElement.getTypedElement(guiProperty)) + else subProperties.put(category, guiProperty.name) + } + } + for (category in subProperties.keySet()) { + val configCategory = config.getCategory(category) + configCategory.setLanguageKey("$langPrefix.$category") + configCategory.setPropertyOrder(subProperties[category]) + rootGuiElements.add(ConfigElement(configCategory)) + } + save() + } + + /** + * Execute the given lambda for all config properties. + * Lambda params: (category name, property name, property instance) + */ + inline fun forEachProperty(init: (String, String, ConfigPropertyBase)->Unit) { + reflectFieldsOfType(ConfigPropertyBase::class.java).forEach { property -> + init("global", property.first.split("$")[0], property.second as ConfigPropertyBase) + } + for (category in reflectNestedObjects) { + category.second.reflectFieldsOfType(ConfigPropertyBase::class.java).forEach { property -> + init(category.first, property.first.split("$")[0], property.second as ConfigPropertyBase) + } + } + } + + /** Save changes to the [Configuration]. */ + fun save() { if (config?.hasChanged() ?: false) config!!.save() } + + /** + * Returns true if any of the given configuration elements have changed. + * Supports both categories and + */ + fun hasChanged(vararg elements: Any?): Boolean { + reflectNestedObjects.forEach { category -> + if (category.second in elements && config?.getCategory(category.first)?.hasChanged() ?: false) return true + } + forEachProperty { category, name, property -> + if (property in elements && property.hasChanged) return true + } + return false + } + + /** Called when the configuration for the mod changes. */ + open fun onChange(event: ConfigChangedEvent.OnConfigChangedEvent) { + save() + forEachProperty { c, n, prop -> prop.read() } + } + + @SubscribeEvent + fun handleConfigChange(event: ConfigChangedEvent.OnConfigChangedEvent) { if (event.modID == modId) onChange(event) } + + /** Extension to get the underlying delegate of a field */ + operator fun Any.get(name: String) = this.reflectField("$name\$delegate") +} + +// ============================ +// Property delegates +// ============================ + +/** Base class for config property delegates. */ +abstract class ConfigPropertyBase { + /** Language key of the property. */ + var lang: String? = null + + /** GUI class to use. */ + var guiClass: Class>? = null + + /** @return true if the property has changed. */ + abstract val hasChanged: Boolean + + /** Attach this delegate to a Forge [Configuration]. */ + abstract fun attach(target: Configuration, langPrefix: String, categoryName: String, propertyName: String) + + /** List of [Property] instances backing this delegate. */ + abstract val guiProperties: List + + /** Re-read the property value from the [Configuration]. */ + open fun read() {} +} + +/** Delegate for a property backed by a single [Property] instance. */ +abstract class ConfigPropertyDelegate() : ConfigPropertyBase() { + /** Cached value of the property. */ + var cached: T? = null + /** The [Property] backing this delegate. */ + var property: Property? = null + + override val guiProperties: List get() = listOf(property!!) + override val hasChanged: Boolean get() = property?.hasChanged() ?: false + + /** Chained setter for the language key. */ + fun lang(lang: String) = apply { this.lang = lang } + + /** Read the backing [Property] instance. */ + abstract fun Property.read(): T + + /** Write the backing [Property] instance. */ + abstract fun Property.write(value: T) + + /** Get the backing [Property] instance. */ + abstract fun resolve(target: Configuration, category: String, name: String): Property + + /** Kotlin deleagation implementation. */ + operator fun getValue(thisRef: Any, delegator: KProperty<*>): T { + if (cached != null) return cached!! + cached = property!!.read() + return cached!! + } + + /** Kotlin deleagation implementation. */ + operator fun setValue(thisRef: Any, delegator: KProperty<*>, value: T) { + cached = value + property!!.write(value) + } + + override fun read() { cached = null } + + override fun attach(target: Configuration, langPrefix: String, categoryName: String, propertyName: String) { + cached = null + property = resolve(target, categoryName, propertyName) + property!!.setLanguageKey("$langPrefix.$lang") + } +} + +/** [Double]-typed property delegate. */ +class ConfigPropertyDouble(val min: Double, val max: Double, val default: Double) : + ConfigPropertyDelegate() { + override fun resolve(target: Configuration, category: String, name: String) = + target.get(category, name, default, null).apply { setMinValue(min); setMaxValue(max) } + override fun Property.read() = property!!.double + override fun Property.write(value: Double) = property!!.set(value) +} + +/** [Float]-typed property delegate. */ +class ConfigPropertyFloat(val min: Double, val max: Double, val default: Double) : + ConfigPropertyDelegate() { + override fun resolve(target: Configuration, category: String, name: String) = + target.get(category, name, default, null).apply { setMinValue(min); setMaxValue(max) } + override fun Property.read() = property!!.double.toFloat() + override fun Property.write(value: Float) = property!!.set(value.toDouble()) +} + +/** [Int]-typed property delegate. */ +class ConfigPropertyInt(val min: Int, val max: Int, val default: Int) : + ConfigPropertyDelegate() { + override fun resolve(target: Configuration, category: String, name: String) = + target.get(category, name, default, null).apply { setMinValue(min); setMaxValue(max) } + override fun Property.read() = property!!.int + override fun Property.write(value: Int) = property!!.set(value) +} + +/** [Boolean]-typed property delegate. */ +class ConfigPropertyBoolean(val default: Boolean) : + ConfigPropertyDelegate() { + override fun resolve(target: Configuration, category: String, name: String) = + target.get(category, name, default, null) + override fun Property.read() = property!!.boolean + override fun Property.write(value: Boolean) = property!!.set(value) +} + +/** [Int] array typed property delegate. */ +class ConfigPropertyIntList(val defaults: ()->Array) : + ConfigPropertyDelegate>() { + override fun resolve(target: Configuration, category: String, name: String) = + target.get(category, name, defaults().toIntArray(), null) + override fun Property.read() = property!!.intList.toTypedArray() + override fun Property.write(value: Array) = property!!.set(value.toIntArray()) +} + +// ============================ +// Delegate factory methods +// ============================ +fun double(min: Double = 0.0, max: Double = 1.0, default: Double) = ConfigPropertyDouble(min, max, default) +fun float(min: Double = 0.0, max: Double = 1.0, default: Double) = ConfigPropertyFloat(min, max, default) +fun int(min: Int = 0, max: Int, default: Int) = ConfigPropertyInt(min, max, default) +fun intList(defaults: ()->Array) = ConfigPropertyIntList(defaults) +fun boolean(default: Boolean) = ConfigPropertyBoolean(default) \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/metaprog/Reflection.kt b/src/main/kotlin/mods/octarinecore/metaprog/Reflection.kt new file mode 100644 index 0000000..3cf4e85 --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/metaprog/Reflection.kt @@ -0,0 +1,166 @@ +@file:JvmName("Reflection") +package mods.octarinecore.metaprog + +import java.lang.reflect.Field +import java.lang.reflect.Method +import mods.octarinecore.metaprog.Namespace.* +import mods.octarinecore.tryDefault + +/** Get a Java class with the given name. */ +fun getJavaClass(name: String) = tryDefault(null) { Class.forName(name) } + +/** Get the field with the given name and type using reflection. */ +inline fun Any.reflectField(field: String): T? = + tryDefault(null) { this.javaClass.getDeclaredField(field) }?.let { + it.isAccessible = true + it.get(this) as T + } + +/** Get the static field with the given name and type using reflection. */ +inline fun Class<*>.reflectStaticField(field: String): T? = + tryDefault(null) { this.getDeclaredField(field) }?.let { + it.isAccessible = true + it.get(null) as T + } + +/** + * Get all nested _object_s of this _object_ with reflection. + * + * @return [Pair]s of (name, instance) + */ +val Any.reflectNestedObjects: List> get() = this.javaClass.declaredClasses.map { + tryDefault(null) { it.name.split("$")[1] to it.getField("INSTANCE").get(null) } +}.filterNotNull() + +/** + * Get all fields of this instance that match (or subclass) any of the given classes. + * + * @param[types] classes to look for + * @return [Pair]s of (field name, instance) + */ +fun Any.reflectFieldsOfType(vararg types: Class<*>) = this.javaClass.declaredFields + .filter { field -> types.any { it.isAssignableFrom(field.type) } } + .map { field -> field.name to field.let { it.isAccessible = true; it.get(this) } } + .filterNotNull() + +enum class Namespace { OBF, SRG, MCP } + +abstract class Resolvable { + abstract fun resolve(): T? + val element: T? by lazy { resolve() } +} + +/** Return true if all given elements are found. */ +fun allAvailable(vararg codeElement: Resolvable<*>) = codeElement.all { it.element != null } + +/** + * Reference to a class. + * + * @param[mcpName] MCP name of the class + * @param[obfName] obfuscated name of the class + */ +open class ClassRef(val mcpName: String, val obfName: String) : Resolvable>() { + constructor(mcpName: String) : this(mcpName, mcpName) + + companion object { + val int = ClassRefPrimitive("I", Int::class.java) + val float = ClassRefPrimitive("F", Float::class.java) + val boolean = ClassRefPrimitive("Z", Boolean::class.java) + val void = ClassRefPrimitive("V", null) + } + + fun name(namespace: Namespace) = if (namespace == Namespace.OBF) obfName else mcpName + open fun asmDescriptor(namespace: Namespace) = "L${name(namespace).replace(".", "/")};" + + override fun resolve() = listOf(mcpName, obfName).map { getJavaClass(it) }.filterNotNull().firstOrNull() +} + +/** + * Reference to a primitive type. + * + * @param[name] ASM descriptor of this primitive type + * @param[clazz] class of this primitive type + */ +class ClassRefPrimitive(name: String, val clazz: Class<*>?) : ClassRef(name) { + override fun asmDescriptor(namespace: Namespace) = mcpName + override fun resolve() = clazz +} + +/** + * Reference to a method. + * + * @param[parentClass] reference to the class containing the method + * @param[mcpName] MCP name of the method + * @param[srgName] SRG name of the method + * @param[obfName] obfuscated name of the method + * @param[returnType] reference to the return type + * @param[returnType] references to the argument types + */ +class MethodRef(val parentClass: ClassRef, + val mcpName: String, + val srgName: String?, + val obfName: String?, + val returnType: ClassRef, + vararg argTypes: ClassRef +) : Resolvable() { + constructor(parentClass: ClassRef, mcpName: String, returnType: ClassRef, vararg argTypes: ClassRef) : + this(parentClass, mcpName, mcpName, mcpName, returnType, *argTypes) + + val argTypes = argTypes + + fun name(namespace: Namespace) = when(namespace) { OBF -> obfName!!; SRG -> srgName!!; MCP -> mcpName } + fun asmDescriptor(namespace: Namespace) = "(${argTypes.map { it.asmDescriptor(namespace) }.fold(""){ s1, s2 -> s1 + s2 } })${returnType.asmDescriptor(namespace)}" + + override fun resolve(): Method? = + if (parentClass.element == null || argTypes.any { it.element == null }) null + else { + val args = argTypes.map { it.element!! }.toTypedArray() + listOf(srgName!!, mcpName).map { tryDefault(null) { + parentClass.element!!.getDeclaredMethod(it, *args) + }}.filterNotNull().firstOrNull() + ?.apply { isAccessible = true } + } + + /** Invoke this method using reflection. */ + fun invoke(receiver: Any, vararg args: Any) = element?.invoke(receiver, *args) + + /** Invoke this static method using reflection. */ + fun invokeStatic(vararg args: Any) = element?.invoke(null, *args) + +} + +/** + * Reference to a field. + * + * @param[parentClass] reference to the class containing the field + * @param[mcpName] MCP name of the field + * @param[srgName] SRG name of the field + * @param[obfName] obfuscated name of the field + * @param[type] reference to the field type + */ +class FieldRef(val parentClass: ClassRef, + val mcpName: String, + val srgName: String?, + val obfName: String?, + val type: ClassRef? +) : Resolvable() { + constructor(parentClass: ClassRef, mcpName: String, type: ClassRef?) : this(parentClass, mcpName, mcpName, mcpName, type) + + fun name(namespace: Namespace) = when(namespace) { OBF -> obfName!!; SRG -> srgName!!; MCP -> mcpName } + fun asmDescriptor(namespace: Namespace) = type!!.asmDescriptor(namespace) + + override fun resolve(): Field? = + if (parentClass.element == null) null + else { + listOf(srgName!!, mcpName).map { tryDefault(null) { + parentClass.element!!.getDeclaredField(it) + }}.filterNotNull().firstOrNull() + ?.apply{ isAccessible = true } + } + + /** Get this field using reflection. */ + fun get(receiver: Any?) = element?.get(receiver) + + /** Get this static field using reflection. */ + fun getStatic() = get(null) +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/metaprog/Transformation.kt b/src/main/kotlin/mods/octarinecore/metaprog/Transformation.kt new file mode 100644 index 0000000..1ff2d32 --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/metaprog/Transformation.kt @@ -0,0 +1,199 @@ +package mods.octarinecore.metaprog + +import cpw.mods.fml.relauncher.IFMLLoadingPlugin +import net.minecraft.launchwrapper.IClassTransformer +import mods.octarinecore.metaprog.Namespace.* +import org.apache.logging.log4j.LogManager +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.Opcodes +import org.objectweb.asm.tree.* + +@IFMLLoadingPlugin.TransformerExclusions( + "mods.octarinecore.metaprog", + "kotlin" +) +open class ASMPlugin(vararg val classes: Class<*>) : IFMLLoadingPlugin { + override fun getASMTransformerClass() = classes.map { it.canonicalName }.toTypedArray() + override fun getAccessTransformerClass() = null + override fun getModContainerClass() = null + override fun getSetupClass() = null + override fun injectData(data: Map) {} +} + +/** + * Base class for convenient bytecode transformers. + */ +open class Transformer : IClassTransformer { + + val log = LogManager.getLogger(this) + + /** The type of environment we are in. Assume MCP until proven otherwise. */ + var environment: Namespace = MCP + + /** The list of transformers and targets. */ + var transformers: MutableListUnit>> = arrayListOf() + + /** Add a transformation to perform. Call this during instance initialization. + * + * @param[method] the target method of the transformation + * @param[trans] method transformation lambda + */ + fun transformMethod(method: MethodRef, trans: MethodTransformContext.()->Unit) = transformers.add(method to trans) + + override fun transform(name: String?, transformedName: String?, classData: ByteArray?): ByteArray? { + if (classData == null) return null + if (name != transformedName) environment = OBF + + val classNode = ClassNode().apply { val reader = ClassReader(classData); reader.accept(this, 0) } + var workDone = false + + val transformations: ListUnit, MethodNode?>> = transformers.map { transformer -> + if (transformedName != transformer.first.parentClass.mcpName) return@map transformer.second to null + log.debug("Found class: $name -> $transformedName") + log.debug(" searching: ${transformer.first.name(OBF)} ${transformer.first.asmDescriptor(OBF)} -> ${transformer.first.name(MCP)} ${transformer.first.asmDescriptor(MCP)}") + transformer.second to classNode.methods.find { + log.debug(" ${it.name} ${it.desc}") + + it.name == transformer.first.name(MCP) && it.desc == transformer.first.asmDescriptor(MCP) || + it.name == transformer.first.name(OBF) && it.desc == transformer.first.asmDescriptor(OBF) + } + } + + transformations.filter { it.second != null }.forEach { + synchronized(it.second!!) { + try { + val trans = it.first + MethodTransformContext(it.second!!, environment).trans() + workDone = true + } catch (e: Throwable) { + log.warn("Error transforming method ${it.second!!.name} ${it.second!!.desc}") + } + } + } + + return if (!workDone) classData else ClassWriter(0).apply { classNode.accept(this) }.toByteArray() + } +} + +/** + * Allows builder-style declarative definition of transformations. Transformation lambdas are extension + * methods on this class. + * + * @param[method] the [MethodNode] currently being transformed + * @param[environment] the type of environment we are in + */ +class MethodTransformContext(val method: MethodNode, val environment: Namespace) { + /** + * Find the first instruction that matches a predicate. + * + * @param[start] the instruction node to start iterating from + * @param[predicate] the predicate to check + */ + fun find(start: AbstractInsnNode, predicate: (AbstractInsnNode) -> Boolean): AbstractInsnNode? { + var current: AbstractInsnNode? = start + while (current != null && !predicate(current)) current = current.next + return current + } + + /** Find the first instruction in the current [MethodNode] that matches a predicate. */ + fun find(predicate: (AbstractInsnNode)->Boolean): AbstractInsnNode? = find(method.instructions.first, predicate) + + /** Find the first instruction in the current [MethodNode] with the given opcode. */ + fun find(opcode: Int) = find { it.opcode == opcode } + + /** + * Insert new instructions after this one. + * + * @param[init] builder-style lambda to assemble instruction list + */ + fun AbstractInsnNode.insertAfter(init: InstructionList.()->Unit) = InstructionList(environment).apply{ + this.init(); list.reversed().forEach { method.instructions.insert(this@insertAfter, it) } + } + + /** + * Insert new instructions before this one. + * + * @param[init] builder-style lambda to assemble instruction list + */ + fun AbstractInsnNode.insertBefore(init: InstructionList.()->Unit) = InstructionList(environment).apply{ + this.init(); list.forEach { method.instructions.insertBefore(this@insertBefore, it) } + } + + /** Remove all isntructiuons between the given two (inclusive). */ + fun Pair.remove() { + var current: AbstractInsnNode? = first + while (current != null && current != second) { + val next = current.next + method.instructions.remove(current) + current = next + } + if (current != null) method.instructions.remove(current) + } + + /** + * Replace all isntructiuons between the given two (inclusive) with the specified instruction list. + * + * @param[init] builder-style lambda to assemble instruction list + */ + fun Pair.replace(init: InstructionList.()->Unit) { + val beforeInsn = first.previous + remove() + beforeInsn.insertAfter(init) + } + + /** + * Matches variable instructions. + * + * @param[opcode] instruction opcode + * @param[idx] variable the opcode references + */ + fun varinsn(opcode: Int, idx: Int): (AbstractInsnNode)->Boolean = { insn -> + insn.opcode == opcode && insn is VarInsnNode && insn.`var` == idx + } +} + +/** + * Allows builder-style declarative definition of instruction lists. + * + * @param[environment] the type of environment we are in + */ +class InstructionList(val environment: Namespace) { + + /** The instruction list being assembled. */ + val list: MutableList = arrayListOf() + + /** + * Adds a variable instruction. + * + * @param[opcode] instruction opcode + * @param[idx] variable the opcode references + */ + fun varinsn(opcode: Int, idx: Int) = list.add(VarInsnNode(opcode, idx)) + + /** + * Adds an INVOKESTATIC instruction. + * + * @param[target] the target method of the instruction + * @param[isInterface] true if the target method is defined by an interface + */ + fun invokeStatic(target: MethodRef, isInterface: Boolean = false) = list.add(MethodInsnNode( + Opcodes.INVOKESTATIC, + target.parentClass.name(environment).replace(".", "/"), + target.name(environment), + target.asmDescriptor(environment), + isInterface + )) + + /** + * Adds a GETFIELD instruction. + * + * @param[target] the target field of the instruction + */ + fun getField(target: FieldRef) = list.add(FieldInsnNode( + Opcodes.GETFIELD, + target.parentClass.name(environment).replace(".", "/"), + target.name(environment), + target.asmDescriptor(environment) + )) +} \ No newline at end of file diff --git a/src/main/resources/assets/betterfoliage/CactusDefault.cfg b/src/main/resources/assets/betterfoliage/CactusDefault.cfg new file mode 100644 index 0000000..b5720e8 --- /dev/null +++ b/src/main/resources/assets/betterfoliage/CactusDefault.cfg @@ -0,0 +1,5 @@ +// Vanilla +net.minecraft.block.BlockCactus + +// TerraFirmaCraft +com.bioxx.tfc.Blocks.Vanilla.BlockCustomCactus \ No newline at end of file diff --git a/src/main/resources/assets/betterfoliage/CropDefault.cfg b/src/main/resources/assets/betterfoliage/CropDefault.cfg new file mode 100644 index 0000000..bd95c37 --- /dev/null +++ b/src/main/resources/assets/betterfoliage/CropDefault.cfg @@ -0,0 +1,35 @@ +// Vanilla +net.minecraft.block.BlockTallGrass +net.minecraft.block.BlockCrops +net.minecraft.block.BlockReed +net.minecraft.block.BlockDoublePlant +-net.minecraft.block.BlockCarrot +-net.minecraft.block.BlockPotato + +// Biomes O'Plenty +biomesoplenty.common.blocks.BlockBOPFlower +biomesoplenty.common.blocks.BlockBOPFlower2 + +// Tinkers' Construct +tconstruct.blocks.slime.SlimeTallGrass + +// Plant Mega Pack +plantmegapack.block.PMPBlockBerrybush +plantmegapack.block.PMPBlockCrops +plantmegapack.block.PMPBlockDesert +plantmegapack.block.PMPBlockFern +plantmegapack.block.PMPBlockFlowerMulti +plantmegapack.block.PMPBlockFlowerSingle +plantmegapack.block.PMPBlockForest +plantmegapack.block.PMPBlockGrass +plantmegapack.block.PMPBlockJungle +plantmegapack.block.PMPBlockMountain +plantmegapack.block.PMPBlockSavanna +plantmegapack.block.PMPBlockShrub +plantmegapack.block.PMPBlockWetlands + +// Pam's HarvestCraft +com.pam.harvestcraft.BlockPamCrop +com.pam.harvestcraft.BlockPamDesertGarden +com.pam.harvestcraft.BlockPamNormalGarden +com.pam.harvestcraft.BlockPamWaterGarden \ No newline at end of file diff --git a/src/main/resources/assets/betterfoliage/DirtDefault.cfg b/src/main/resources/assets/betterfoliage/DirtDefault.cfg new file mode 100644 index 0000000..7322ae7 --- /dev/null +++ b/src/main/resources/assets/betterfoliage/DirtDefault.cfg @@ -0,0 +1,14 @@ +// Vanilla +net.minecraft.block.BlockDirt + +// Biomes O'Plenty +biomesoplenty.common.blocks.BlockNewDirt + +// Enhanced Biomes +enhancedbiomes.blocks.BlockSoilEB + +// TerraFirmaCraft +com.bioxx.tfc.Blocks.Terrain.BlockDirt + +// Aether +net.aetherteam.aether.blocks.natural.BlockAetherDirt \ No newline at end of file diff --git a/src/main/resources/assets/betterfoliage/GrassDefault.cfg b/src/main/resources/assets/betterfoliage/GrassDefault.cfg new file mode 100644 index 0000000..2f44802 --- /dev/null +++ b/src/main/resources/assets/betterfoliage/GrassDefault.cfg @@ -0,0 +1,19 @@ +// Vanilla +net.minecraft.block.BlockGrass + +// Biomes O'Plenty +biomesoplenty.common.blocks.BlockOriginGrass +biomesoplenty.common.blocks.BlockLongGrass +biomesoplenty.common.blocks.BlockNewGrass + +// Tinker's Construct +tconstruct.blocks.slime.SlimeGrass + +// Enhanced Biomes +enhancedbiomes.blocks.BlockGrassEB + +// TerraFirmaCraft +com.bioxx.tfc.Blocks.Terrain.BlockGrass + +// Aether +net.aetherteam.aether.blocks.natural.BlockAetherGrass \ No newline at end of file diff --git a/src/main/resources/assets/betterfoliage/LeavesDefault.cfg b/src/main/resources/assets/betterfoliage/LeavesDefault.cfg new file mode 100644 index 0000000..3dbacee --- /dev/null +++ b/src/main/resources/assets/betterfoliage/LeavesDefault.cfg @@ -0,0 +1,8 @@ +// Vanilla +net.minecraft.block.BlockLeavesBase + +// Forestry +forestry.arboriculture.gadgets.BlockLeaves + +// Thaumcraft +thaumcraft.common.blocks.BlockMagicalLeaves \ No newline at end of file diff --git a/src/main/resources/assets/betterfoliage/LilypadDefault.cfg b/src/main/resources/assets/betterfoliage/LilypadDefault.cfg new file mode 100644 index 0000000..4af6afd --- /dev/null +++ b/src/main/resources/assets/betterfoliage/LilypadDefault.cfg @@ -0,0 +1,5 @@ +// Vanilla +net.minecraft.block.BlockLilyPad + +// TerraFirmaCraft +com.bioxx.tfc.Blocks.Vanilla.BlockCustomLilyPad \ No newline at end of file diff --git a/src/main/resources/assets/betterfoliage/LogDefault.cfg b/src/main/resources/assets/betterfoliage/LogDefault.cfg new file mode 100644 index 0000000..14c2403 --- /dev/null +++ b/src/main/resources/assets/betterfoliage/LogDefault.cfg @@ -0,0 +1,23 @@ +// Vanilla +net.minecraft.block.BlockLog + +// Biomes O'Plenty +biomesoplenty.common.blocks.BlockBOPLog + +// Natura +mods.natura.blocks.trees.DarkTreeBlock +mods.natura.blocks.trees.LogTwoxTwo +mods.natura.blocks.trees.SimpleLog + +// Thaumcraft +thaumcraft.common.blocks.BlockMagicalLog + +// Forestry +forestry.arboriculture.gadgets.BlockLog + +// Extra Biomes XL +-extrabiomes.blocks.BlockMiniLog + +// TerraFirmaCraft +com.bioxx.tfc.Blocks.Flora.BlockLogVert +com.bioxx.tfc.Blocks.Flora.BlockLogNatural \ No newline at end of file diff --git a/src/main/resources/assets/betterfoliage/SandDefault.cfg b/src/main/resources/assets/betterfoliage/SandDefault.cfg new file mode 100644 index 0000000..a1330e8 --- /dev/null +++ b/src/main/resources/assets/betterfoliage/SandDefault.cfg @@ -0,0 +1,5 @@ +// Vanilla +net.minecraft.block.BlockSand + +// TerraFirmaCraft +com.bioxx.tfc.Blocks.Terrain.BlockSand \ No newline at end of file diff --git a/src/main/resources/assets/betterfoliage/lang/en_US.lang b/src/main/resources/assets/betterfoliage/lang/en_US.lang new file mode 100644 index 0000000..ce8dbf1 --- /dev/null +++ b/src/main/resources/assets/betterfoliage/lang/en_US.lang @@ -0,0 +1,212 @@ +key.betterfoliage.gui=Open Settings + +betterfoliage.global.enabled=Enable Mod +betterfoliage.global.enabled.tooltip=If set to false, BetterFoliage will not render anything + +betterfoliage.enabled=Enable +betterfoliage.enabled.tooltip=Is this feature enabled? +betterfoliage.hOffset=Horizontal offset +betterfoliage.hOffset.tooltip=The distance this element is shifted horizontally, in blocks +betterfoliage.vOffset=Vertical offset +betterfoliage.vOffset.tooltip=The distance this element is shifted vertically, in blocks +betterfoliage.size=Size +betterfoliage.size.tooltip=Size of this element +betterfoliage.heightMin=Minimum height +betterfoliage.heightMin.tooltip=Minimum height of element +betterfoliage.heightMax=Maximum height +betterfoliage.heightMax.tooltip=Maximum height of element +betterfoliage.population=Population +betterfoliage.population.tooltip=Chance (N in 64) that an eligible block will have this feature +betterfoliage.shaderWind=Shader wind effects +betterfoliage.shaderWind.tooltip=Apply wind effects from ShaderMod shaders to this element? +betterfoliage.distance=Distance limit +betterfoliage.distance.tooltip=Maximum distance from player at which to render this feature + +betterfoliage.blocks=Block Types +betterfoliage.blocks.tooltip=Configure lists of block classes that will have specific features applied to them + +betterfoliage.blocks.dirtWhitelist=Dirt Whitelist +betterfoliage.blocks.dirtBlacklist=Dirt Blacklist +betterfoliage.blocks.dirtWhitelist.arrayEntry=%d entries +betterfoliage.blocks.dirtBlacklist.arrayEntry=%d entries + +betterfoliage.blocks.grassWhitelist=Grass Whitelist +betterfoliage.blocks.grassBlacklist=Grass Blacklist +betterfoliage.blocks.grassWhitelist.arrayEntry=%d entries +betterfoliage.blocks.grassBlacklist.arrayEntry=%d entries + +betterfoliage.blocks.leavesWhitelist=Leaves Whitelist +betterfoliage.blocks.leavesBlacklist=Leaves Blacklist +betterfoliage.blocks.leavesWhitelist.arrayEntry=%d entries +betterfoliage.blocks.leavesBlacklist.arrayEntry=%d entries + +betterfoliage.blocks.cropsWhitelist=Crop Whitelist +betterfoliage.blocks.cropsBlacklist=Crop Blacklist +betterfoliage.blocks.cropsWhitelist.arrayEntry=%d entries +betterfoliage.blocks.cropsBlacklist.arrayEntry=%d entries + +betterfoliage.blocks.logsWhitelist=Wood Log Whitelist +betterfoliage.blocks.logsBlacklist=Wood Log Blacklist +betterfoliage.blocks.logsWhitelist.arrayEntry=%d entries +betterfoliage.blocks.logsBlacklist.arrayEntry=%d entries + +betterfoliage.blocks.sandWhitelist=Sand Whitelist +betterfoliage.blocks.sandBlacklist=Sand Blacklist +betterfoliage.blocks.sandWhitelist.arrayEntry=%d entries +betterfoliage.blocks.sandBlacklist.arrayEntry=%d entries + +betterfoliage.blocks.lilypadWhitelist=Lilypad Whitelist +betterfoliage.blocks.lilypadBlacklist=Lilypad Blacklist +betterfoliage.blocks.lilypadWhitelist.arrayEntry=%d entries +betterfoliage.blocks.lilypadBlacklist.arrayEntry=%d entries + +betterfoliage.blocks.cactusWhitelist=Cactus Whitelist +betterfoliage.blocks.cactusBlacklist=Cactus Blacklist +betterfoliage.blocks.cactusWhitelist.arrayEntry=%d entries +betterfoliage.blocks.cactusBlacklist.arrayEntry=%d entries + + +betterfoliage.blocks.dirtWhitelist.tooltip=Blocks recognized as Dirt. Has an impact on Reeds, Algae, Connected Grass +betterfoliage.blocks.dirtBlacklist.tooltip=Blocks never accepted as Dirt. Has an impact on Reeds, Algae, Connected Grass +betterfoliage.blocks.grassWhitelist.tooltip=Blocks recognized as Grass. Has an impact on Short Grass, Connected Grass +betterfoliage.blocks.grassBlacklist.tooltip=Blocks never accepted as Grass. Has an impact on Short Grass, Connected Grass +betterfoliage.blocks.leavesWhitelist.tooltip=Blocks recognized as Leaves. Has an impact on Extra Leaves, Falling Leaves. Leaves will render with leaves block ID in shader programs +betterfoliage.blocks.leavesBlacklist.tooltip=Blocks never accepted as Leaves. Has an impact on Extra Leaves, Falling Leaves. Leaves will render with leaves block ID in shader programs +betterfoliage.blocks.cropsWhitelist.tooltip=Blocks recognized as crops. Crops will render with tallgrass block ID in shader programs +betterfoliage.blocks.cropsBlacklist.tooltip=Blocks never accepted as crops. Crops will render with tallgrass block ID in shader programs +betterfoliage.blocks.logsWhitelist.tooltip=Blocks recognized as wooden logs. Has an impact on Rounded Logs +betterfoliage.blocks.logsBlacklist.tooltip=Blocks never accepted as wooden logs. Has an impact on Rounded Logs +betterfoliage.blocks.sandWhitelist.tooltip=Blocks recognized as Sand. Has an impact on Coral +betterfoliage.blocks.sandBlacklist.tooltip=Blocks never accepted Sand. Has an impact on Coral +betterfoliage.blocks.lilypadWhitelist.tooltip=Blocks recognized as Lilypad. Has an impact on Better Lilypad +betterfoliage.blocks.lilypadBlacklist.tooltip=Blocks never accepted Lilypad. Has an impact on Better Lilypad +betterfoliage.blocks.cactusWhitelist.tooltip=Blocks recognized as Cactus. Has an impact on Better Cactus +betterfoliage.blocks.cactusBlacklist.tooltip=Blocks never accepted Cactus. Has an impact on Better Cactus + +betterfoliage.leaves=Extra Leaves +betterfoliage.leaves.tooltip=Extra round leaves on leaf blocks +betterfoliage.leaves.dense=Dense mode +betterfoliage.leaves.dense.tooltip=Dense mode has more round leaves + +betterfoliage.shortGrass=Short Grass & Mycelium +betterfoliage.shortGrass.tooltip=Tufts of grass/mycelium on top of appropriate blocks +betterfoliage.shortGrass.useGenerated=Use generated texture for grass +betterfoliage.shortGrass.useGenerated.tooltip=Generated texture is made by slicing the tallgrass texture from the active resource pack in half +betterfoliage.shortGrass.myceliumEnabled=Enable Mycelium +betterfoliage.shortGrass.myceliumEnabled.tooltip=Is this feature enabled for mycelium blocks? +betterfoliage.shortGrass.grassEnabled=Enable Grass +betterfoliage.shortGrass.grassEnabled.tooltip=Is this feature enabled for grass blocks? +betterfoliage.shortGrass.snowEnabled=Enable under snow +betterfoliage.shortGrass.snowEnabled.tooltip=Enable on snowed grass blocks? +betterfoliage.shortGrass.saturationThreshold=Saturation threshold +betterfoliage.shortGrass.saturationThreshold.tooltip=Color saturation cutoff between "colorless" blocks (using biome color) and "colorful" blocks (using their own specific color) + +betterfoliage.hangingGrass=Hanging Grass +betterfoliage.hangingGrass.tooltip=Grass tufts hanging down from the top edges of grass blocks +betterfoliage.hangingGrass.separation=Separation +betterfoliage.hangingGrass.separation.tooltip=How much the hanging grass stands out from the block + +betterfoliage.cactus=Better Cactus +betterfoliage.cactus.tooltip=Enhance cactus with extra bits and smooth shading +betterfoliage.cactus.sizeVariation=Size variation +betterfoliage.cactus.sizeVariation.tooltip=Amount of random variation on cactus size + +betterfoliage.lilypad=Better Lilypad +betterfoliage.lilypad.tooltip=Enhance lilypad with roots and occasional flowers +betterfoliage.lilypad.flowerChance=Flower chance +betterfoliage.lilypad.flowerChance.tooltip=Chance (N in 64) of a lilypad having a flower on it + +betterfoliage.reed=Reeds +betterfoliage.reed.tooltip=Reeds on dirt blocks in shallow water +betterfoliage.reed.biomes=Biome List +betterfoliage.reed.biomes.tooltip=Configure which biomes reeds are allowed to appear in +betterfoliage.reed.biomes.tooltip.element=Should reeds appear in the %s biome? + +betterfoliage.algae=Algae +betterfoliage.algae.tooltip=Algae on dirt blocks in deep water +betterfoliage.algae.biomes=Biome List +betterfoliage.algae.biomes.tooltip=Configure which biomes algae is allowed to appear in +betterfoliage.algae.biomes.tooltip.element=Should algae appear in the %s biome? + +betterfoliage.coral=Coral +betterfoliage.coral.tooltip=Coral on sand blocks in deep water +betterfoliage.coral.size=Coral size +betterfoliage.coral.size.tooltip=Size of coral bits sticking out +betterfoliage.coral.crustSize=Crust size +betterfoliage.coral.crustSize.tooltip=Size of the flat coral part +betterfoliage.coral.chance=Coral chance +betterfoliage.coral.chance.tooltip=Chance (N in 64) of a specific face of the block to show coral +betterfoliage.coral.biomes=Biome List +betterfoliage.coral.biomes.tooltip=Configure which biomes coral is allowed to appear in +betterfoliage.coral.biomes.tooltip.element=Should coral appear in the %s biome? +betterfoliage.coral.shallowWater=Shallow water coral +betterfoliage.coral.shallowWater.tooltip=Should coral appear in 1 block deep water? + +betterfoliage.netherrack=Netherrack Vines +betterfoliage.netherrack.tooltip=Hanging Vines under netherrack + +betterfoliage.fallingLeaves=Falling leaves +betterfoliage.fallingLeaves.tooltip=Falling leaf particle FX emitted from the bottom of leaf blocks +betterfoliage.fallingLeaves.speed=Particle speed +betterfoliage.fallingLeaves.speed.tooltip=Overall particle speed +betterfoliage.fallingLeaves.windStrength=Wind strength +betterfoliage.fallingLeaves.windStrength.tooltip=Magnitude of wind effects in good weather (spread of normal distribution centered on 0) +betterfoliage.fallingLeaves.stormStrength=Storm strength +betterfoliage.fallingLeaves.stormStrength.tooltip=Additional magnitude of wind effects in rainy weather (spread of normal distribution centered on 0) +betterfoliage.fallingLeaves.size=Particle size +betterfoliage.fallingLeaves.chance=Particle chance +betterfoliage.fallingLeaves.chance.tooltip=Chance of each random render tick hitting a leaf block to spawn a particle +betterfoliage.fallingLeaves.perturb=Perturbation +betterfoliage.fallingLeaves.perturb.tooltip=Magnitude of perturbation effect. Adds a corkscrew-like motion to the particle synchronized to its rotation +betterfoliage.fallingLeaves.lifetime=Maximum lifetime +betterfoliage.fallingLeaves.lifetime.tooltip=Maximum lifetime of particle in seconds. Minimum lifetime is 60%% of this value +betterfoliage.fallingLeaves.opacityHack=Opaque particles +betterfoliage.fallingLeaves.opacityHack.tooltip=Stop transparent blocks obscuring particles even when particle is in front. WARNING: may cause glitches. + +betterfoliage.risingSoul=Rising souls +betterfoliage.risingSoul.tooltip=Rising soul particle FX emitted from the top of soulsand blocks +betterfoliage.risingSoul.chance=Particle chance +betterfoliage.risingSoul.chance.tooltip=Chance of each random render tick hitting a soulsand block to spawn a particle +betterfoliage.risingSoul.speed=Particle speed +betterfoliage.risingSoul.speed.tooltip=Vertical speed of soul particles +betterfoliage.risingSoul.perturb=Perturbation +betterfoliage.risingSoul.perturb.tooltip=Magnitude of perturbation effect. Adds a corkscrew-like motion to the particle +betterfoliage.risingSoul.headSize=Soul size +betterfoliage.risingSoul.headSize.tooltip=Size of the soul particle +betterfoliage.risingSoul.trailSize=Trail size +betterfoliage.risingSoul.trailSize.tooltip=Initial size of the particle trail +betterfoliage.risingSoul.opacity=Opacity +betterfoliage.risingSoul.opacity.tooltip=Opacity of the particle effect +betterfoliage.risingSoul.sizeDecay=Size decay +betterfoliage.risingSoul.sizeDecay.tooltip=Trail particle size relative to its size in the previous tick +betterfoliage.risingSoul.opacityDecay=Opacity decay +betterfoliage.risingSoul.opacityDecay.tooltip=Trail particle opacity relative to its opacity in the previous tick +betterfoliage.risingSoul.lifetime=Maximum lifetime +betterfoliage.risingSoul.lifetime.tooltip=Maximum lifetime of particle effect in seconds. Minimum lifetime is 60%% of this value +betterfoliage.risingSoul.trailLength=Trail length +betterfoliage.risingSoul.trailLength.tooltip=Number of previous positions the particle remembers in ticks +betterfoliage.risingSoul.trailDensity=Trail density +betterfoliage.risingSoul.trailDensity.tooltip=Render every Nth previous position in the particle trail + +betterfoliage.connectedGrass=Connected grass textures +betterfoliage.connectedGrass.enabled=Enable +betterfoliage.connectedGrass.enabled.tooltip=If there is a grass block on top of a dirt block: draw grass top texture on all grass block sides, + +betterfoliage.roundLogs=Round Logs +betterfoliage.roundLogs.tooltip=Connect round blocks to solid full blocks? +betterfoliage.roundLogs.connectSolids=Connect to solid +betterfoliage.roundLogs.connectSolids.tooltip=Connect round blocks to solid full blocks? +betterfoliage.roundLogs.connectPerpendicular=Connect to perpendicular logs +betterfoliage.roundLogs.connectPerpendicular.tooltip=Connect round logs to perpendicular logs alons its axis? +betterfoliage.roundLogs.lenientConnect=Lenient rounding +betterfoliage.roundLogs.lenientConnect.tooltip=Connect parallel round logs in an L-shape too, not just 2x2 +betterfoliage.roundLogs.connectGrass=Connect Grass +betterfoliage.roundLogs.connectGrass.tooltip=Render grass block under trees instead of dirt if there is grass nearby +betterfoliage.roundLogs.radiusSmall=Chamfer radius +betterfoliage.roundLogs.radiusSmall.tooltip=How much to chop off from the log corner +betterfoliage.roundLogs.radiusLarge=Connected chamfer radius +betterfoliage.roundLogs.radiusLarge.tooltip=How much to chop off from the outer corner of connected logs +betterfoliage.roundLogs.dimming=Dimming +betterfoliage.roundLogs.dimming.tooltip=Amount to darken onscured log faces +betterfoliage.roundLogs.zProtection=Z-Protection +betterfoliage.roundLogs.zProtection.tooltip=Amount to scale parallel log connection bits to stop Z-fighting (flickering). Try to set it as high as possible without having glitches. \ No newline at end of file diff --git a/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_default_0.png b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_default_0.png new file mode 100644 index 0000000000000000000000000000000000000000..5d1143e0e78f387e008a38bd91a36490124b0304 GIT binary patch literal 281 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xc6+)whE&|D={MwRb`Wt%7w5Ll)yZiQIkhX{q3D~UCK)65 z4SX+^%#^$~Y`AbE|BT585^vA)L&T}oP~h1R`J zl3*(+wO;tXTE)|8-t*i5=~b(m-v55UUfGg4%cM_v*E;7wk&ed_jsgd^<$gH-d#CNK zg>!?vd@i$`-ZWvErtvv($MrwEj^7e*dw5N^#Ub|k{+8;?8!97bwEb7%of5>edd9p> d%hxk7Gd%3iTB;t@(hBqjgQu&X%Q~loCID^Fa4Y}- literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_default_1.png b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_default_1.png new file mode 100644 index 0000000000000000000000000000000000000000..118562d76c22889550da8f71b98937c902b19e7a GIT binary patch literal 275 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5XwtBiahE&|D={MwRb`Ws=ud|Zp(ISOwf?`{EI6m6UQ*!ru z%xcq=prw1|n3o8Twpp2UIj3f=f$`D*pW73?H0NApwSToM?6ARsLYabdfg-FgJuBy} zE)PwdmF^PAqIl$fiG$B}_gPXK-&glM?%1TGxbzBJs^6B}Y_r_kZkJ7VL`^eP^<;W> zO8r}{y6o;;=|>iAuPcA(aPL~j{P??glfr>r>q=Atrlpp2lyE!$*vEfq{hW`Y2@JOT WBN?Wd?@R%DfWgz%&t;ucLK6VyZfDv6 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_default_2.png b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_default_2.png new file mode 100644 index 0000000000000000000000000000000000000000..6c3fcee11c0783cd4549c8c4dd5c7a03f73bb543 GIT binary patch literal 273 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5XHha1_hE&|D@w4V?b`Y5}^)0WWgiXVusS}jiKe$=;uzLvZ zX=#esF;$@~%$vp8=HO|+q@c71wQZ93r2Sn!tT&N4wMoQW74d>xN+t> zXPM)Ntmfn!9DBgMUW04rdE;5j4x0GR5ptb)rSz|1OJawK@5ODpYql!Lw>8fCQmcRK zd#%m1r|DK~j1?0-Cb6tqwQ75HZ1|bk58@hZ&KED;u9GF>-o~OIRPtV=cn^yN!~ccp Vhq(D&3V_aM@O1TaS?83{1OQQjYEJ+F literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_default_3.png b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_default_3.png new file mode 100644 index 0000000000000000000000000000000000000000..59f003fc26985b56e09c3d961ab564ae660c84fe GIT binary patch literal 254 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5X7I?ZihE&{&>D$QLY#`ujA68Y~cc)GDkkh@UH;par1&6s@ zc9c3gyPOJm$f;L8ae~jjFYm?czSn9kZQ1v}b8$8LOY-Ilmu-Rl4^ z$>a|=&CY!0S!?=lsaf3~BNJcYqH`baUFNLN<<2Yp+w8=#;o3au8&hk7o%**e6*^EX zqn)|SoL6d_2j8?kCnhK_?cDAeC9$TjZ9M}s!y^g*9SO?TYCyL#c)I$ztaD0e0syY> BWNiQd literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_jungle_0.png b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_jungle_0.png new file mode 100644 index 0000000000000000000000000000000000000000..3b3ea399e45e67b5065679f8ed5eec41ca97458a GIT binary patch literal 286 zcmV+(0pb3MP)i-WP zJ}}(4af3lpQj+1-t5*yx2j^=rh?uPfOR%!Cf+f$NKhN;_^JfNAQ&R>e`@|azyLayf zODYTBVbImpWe76Z&7h{H#vmXdz`%0E}0i^0`ZfC1h($07*qoM6N<$f(`L?=>Px# literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_jungle_1.png b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_jungle_1.png new file mode 100644 index 0000000000000000000000000000000000000000..b44a85f5c00cd2521efc7cb0d48025dd88d348eb GIT binary patch literal 265 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5XR(rZQhE&{2`t$$4J+s{brsSBtRbT%fZs(W$@$;u73k%DE zq+7Rc_2|65zW#sxzdM#~)7QrtW@KdDL=2I`Mm72h0=rAcOD~o47>uQN3t&x(?-cL%sQE{!|A zKi}@`fzzi?fB5-1y{$a|zLAEewz9|q>u;hLe3O!rW^i3#bZB5?uru*mHmxTu4CrhI MPgg&ebxsLQ05QOAod5s; literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_jungle_2.png b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_jungle_2.png new file mode 100644 index 0000000000000000000000000000000000000000..102025910f97d3e33215c0fe8ecf24f360b5b5d1 GIT binary patch literal 274 zcmV+t0qy>YP)XW{^{{U{Dsm!@$bQ3RlGB<>kfr?(JK!qDOa5FnoHpivegV!;Kp^z={|_ z4mo=C=>Lx&KQg>}^$KjSs;Vjjh!4^XRsaGMCQSIRrl!Wg&(F_bZEej6b0k~=E&vc< Y0CCf5@w){6rvLx|07*qoM6N<$f*=)dfB*mh literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_jungle_3.png b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_jungle_3.png new file mode 100644 index 0000000000000000000000000000000000000000..8665fcc4ff1215d86997fc68cef72dc1110f2924 GIT binary patch literal 278 zcmV+x0qOpUP)0Ze);@l!TkW^61V9Mjoxn z|Gxq`N{p|-f*?g+US41}Bgmy70CL=k4v-sEECcgtR^*1oM6t##KZ7RIdrLYSml18V;MYM{an^LB{Ts5 Dbt+}p literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_spruce_1.png b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_spruce_1.png new file mode 100644 index 0000000000000000000000000000000000000000..7103dcc681850c4beef4fb28c032a98207a52eb3 GIT binary patch literal 244 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xrg^$JhE&{2`t$$4J+s|`rZYFU<=(EZtE*#UWMpK^&CPY& z(De27_5b$u?_>_24qq=}VrnYM#l(nV8 r9tG_U4}X4s{-6C~Ps<)ofdqz%BG>ib-~MU>x{$%s)z4*}Q$iB}^Y&r5 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_spruce_2.png b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_spruce_2.png new file mode 100644 index 0000000000000000000000000000000000000000..480f4098f9748dbb7ab303d201e998ca3e1b8d91 GIT binary patch literal 231 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_algae_1.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_algae_1.png new file mode 100644 index 0000000000000000000000000000000000000000..2601579c1b2bbe84ef4db8a3e7660be89bda9105 GIT binary patch literal 3034 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_algae_2.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_algae_2.png new file mode 100644 index 0000000000000000000000000000000000000000..5377e8b8435d7cfae7423524587286b2fe053204 GIT binary patch literal 3045 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_algae_3.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_algae_3.png new file mode 100644 index 0000000000000000000000000000000000000000..af1145c07546c5a75abc9325822a016a74193779 GIT binary patch literal 3177 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_cactus.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_cactus.png new file mode 100644 index 0000000000000000000000000000000000000000..cfbf609f9f0f3347ef27587aad9caabc4a220ab9 GIT binary patch literal 4265 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 z0002KNkli$002ov JPDHLkV1k$YdmaD) literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_cactus_arm_1.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_cactus_arm_1.png new file mode 100644 index 0000000000000000000000000000000000000000..b758ab48186d6617702fac0ceea4c65b2f06ed7f GIT binary patch literal 3011 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 FV1n{;nJNGP literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_coral_0.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_coral_0.png new file mode 100644 index 0000000000000000000000000000000000000000..b735f79ed8b4d4695f6edb57bf9cef9f6b1ecfc7 GIT binary patch literal 3033 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 TVR6m;00000NkvXXu0mjfa>9jD literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_coral_3.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_coral_3.png new file mode 100644 index 0000000000000000000000000000000000000000..f8505317b9ef98ddc75cf50a524c740824e05c78 GIT binary patch literal 3020 zcmV;-3p4bIP)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z0002@Nkl(YGni4b*9ZHfo(FC|!%1{1XGy zW-F)?DP4;LF$MrsQHD4-aJN%9g8p>Y8$pgL&)xIMO_k7jwP4|hPG1Lr;qF(Pj4r6w zO9rOR$>`Fh5M>yMF*>-P?VVQ(WsH{&WD-@BDRCdS|A=z~rE5JnxDEgdvkzLXvq}R1 O0000KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z0003QNklRHWkmr6M79N1#_?R@j*2}x8bgNpSJ_$$EG;|D7{ zu$iOH9D1V#_5LeBZ?xd~@s0ox0K7cglOi+-(=v!;ewxEMNy2Xr=(h*FxOruBIA^0X z=BBPara0LpR@CZp+LzgC4VPH?et!c1d_6s5(}4kyKibUEC03Txz5qb0Udwmi?@qXV zcmRcKj{yKLj-a~+VM+~79Vqbg?g3l=ZYct3bjEy{bn~E;?8OmWVkHPudU1qSy@vbi wTNE__Fej%+rG#}_F%OqmnXT4Pt@!_E0Gz9yCiZUa$^ZZW07*qoM6N<$f|M+*d;kCd literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_coral_5.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_coral_5.png new file mode 100644 index 0000000000000000000000000000000000000000..9ea9fe6a1fd0f47245b144f730a0c25af763fe68 GIT binary patch literal 2982 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_crust_0.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_crust_0.png new file mode 100644 index 0000000000000000000000000000000000000000..f1c6ab9de7d0110ce634e9cb1e8be02eb22a3ea3 GIT binary patch literal 3420 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_crust_2.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_crust_2.png new file mode 100644 index 0000000000000000000000000000000000000000..a54af0027a4b89ab79e84f3f315bfa273fb4b657 GIT binary patch literal 4131 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_crust_3.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_crust_3.png new file mode 100644 index 0000000000000000000000000000000000000000..5f383bfb7516ed71ab5dac38f4e78915ebfe36fe GIT binary patch literal 4092 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_long_1.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_long_1.png new file mode 100644 index 0000000000000000000000000000000000000000..76b92eaa95d0b6f5e9521be48ed9fa604b24e09f GIT binary patch literal 3125 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 P00000NkvXXu0mjf>*&di literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_long_2.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_long_2.png new file mode 100644 index 0000000000000000000000000000000000000000..ac9aaf17162d6618d12c25c68e5a90705978ed6c GIT binary patch literal 3023 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 RydD4m002ovPDHLkV1m;LoQePd literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_long_3.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_long_3.png new file mode 100644 index 0000000000000000000000000000000000000000..e8f46df60310a303d542c92ece78d169e3c036c9 GIT binary patch literal 3083 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 ZHvnY6AXpVtv+V!?002ovPDHLkV1nhI!G{0< literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_long_4.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_long_4.png new file mode 100644 index 0000000000000000000000000000000000000000..386e5db11d140ea30db63528e116623865863eb2 GIT binary patch literal 3069 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_short_0.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_short_0.png new file mode 100644 index 0000000000000000000000000000000000000000..5372504794ab47ed81d5e56d2c66783a3da763a8 GIT binary patch literal 2959 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 Z4*(^zlXBrEMkN3M002ovPDHLkV1f=Ghx7md literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_short_2.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_short_2.png new file mode 100644 index 0000000000000000000000000000000000000000..a82a97ecfa9fc1ac3a1afb61a683883f968c2755 GIT binary patch literal 2947 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_side_0.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_side_0.png new file mode 100644 index 0000000000000000000000000000000000000000..978d6c31c63afa0d4cf41224da8f8aa87731a24a GIT binary patch literal 3294 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_snowed_0.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_snowed_0.png new file mode 100644 index 0000000000000000000000000000000000000000..00ffe897817a70add100a3f7ed54021876013516 GIT binary patch literal 3137 zcmV-H48HS;P)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z0004ONkl*Oz5phLq5+eATmh5SCZMD1>ksCUb4*vOCWU?9!4w!@fK&>4oaPeem=ug2 zbb5Q6->1^f{O3+2A&LDsRw79fiKM=@l0@HHC6e~zIL~G8CGjljIrU|_c;-7;Us*8& zYXKNo%Y3zJ?yS{Gx62He^LCq)ZkN3{)}y5Jb{haEF@=Iy=H?iB50KXvqll{{(UY}ow_18}j;%(6XlaS=pmT0U00000NkvXXu0mjfL0H}b literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_snowed_1.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_snowed_1.png new file mode 100644 index 0000000000000000000000000000000000000000..68d2f8f81d8b94bffb1ae707c2a6794003c55c08 GIT binary patch literal 3095 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_leaves_snowed_0.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_leaves_snowed_0.png new file mode 100644 index 0000000000000000000000000000000000000000..33c98974993ee8f6c2394a1928c0171fcddf1479 GIT binary patch literal 3382 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_leaves_snowed_2.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_leaves_snowed_2.png new file mode 100644 index 0000000000000000000000000000000000000000..38ad5ac015e13f387f66df81d49abe20b94daeaa GIT binary patch literal 3422 zcmV-k4WaUhP)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z0007sNklu6^GdyK ztVKWYz*H$*$1yUkN5H-*3u7=90);Us zoeMzQra?_K>H+o(PrDr^N+DJ1pv2pz35Wdue7(laY=)W$sZuD6L8=sN+v4lxW$=Gk zt#C7&Ayq0gEux4g%p?k9VA~ekrU~jl34vu*;eN3Q2~(wlf_%LOfDrU)w?pY%NU&`h zJneS4Uo4O+75qG)7YhI)RVoBsRuxjE@V%;ny22O$K&CYcW5RmAUL#eCGOdv+g@@HD z_zkNAfKsK%pY_-4LGW0Xe3$3!e~oh(etvK0cg)8)_s2McZy1N^F3-6y(X+2rruAQa zPO-umPL)EYHR!P{`LzO{Vwu)_JD(H%&oDZ1a1Cd}NyF$>5+6(c>@a%8jD2F0-zr1z=)agktXoLb_O^DoB_@NXMkS=05LfQDdgk&DgXcg07*qoM6N<$f;}Er ATL1t6 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_lilypad_flower_0.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_lilypad_flower_0.png new file mode 100644 index 0000000000000000000000000000000000000000..655b0166419d5c22479dfea4c5afd937b9806540 GIT binary patch literal 2917 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 P00000NkvXXu0mjffGKWS literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_lilypad_flower_1.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_lilypad_flower_1.png new file mode 100644 index 0000000000000000000000000000000000000000..96db7d21d147c23ad7d9af96f9438439dd59ecd2 GIT binary patch literal 3004 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 FV1ijPf9e1L literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_lilypad_roots_1.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_lilypad_roots_1.png new file mode 100644 index 0000000000000000000000000000000000000000..b2411f435c02519ce3af278eae95433cadf39eb0 GIT binary patch literal 2912 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_mycel_0.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_mycel_0.png new file mode 100644 index 0000000000000000000000000000000000000000..936902e6497ded9428909f37ae37b921d6228c71 GIT binary patch literal 3018 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 M07*qoM6N<$g1)Pt!~g&Q literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_mycel_1.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_mycel_1.png new file mode 100644 index 0000000000000000000000000000000000000000..a4291e3fba4c4005968628a265c3b6b5ce28c7dd GIT binary patch literal 3020 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 XwAHZ7WHrBp00000NkvXXu0mjf>9>G< literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_mycel_side_0.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_mycel_side_0.png new file mode 100644 index 0000000000000000000000000000000000000000..12e9c43902d764fe3b9a66d11ec84a497d92e674 GIT binary patch literal 3221 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 Hu0mjftk(nZ literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_mycel_side_1.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_mycel_side_1.png new file mode 100644 index 0000000000000000000000000000000000000000..6aeab0972e7a9ee2a41c416ed5a2c791458ce8ed GIT binary patch literal 3116 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%0D9+(*rFS!eUz>YZF3gqNx!opyPKqHdI0WmS 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_reed_3.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_reed_3.png new file mode 100644 index 0000000000000000000000000000000000000000..96dcb2ec104329e24033fe453a68cb6445204a3a GIT binary patch literal 3788 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