Compare commits

...

275 Commits

Author SHA1 Message Date
octarine-noise
178a014a6b bump version 2021-07-30 15:57:56 +02:00
octarine-noise
0b802663dc fix ignored snowed leaves config 2021-07-26 23:32:02 +02:00
octarine-noise
d2b9326ced mod support: BOP, BYG, Environmental, Desolation 2021-07-26 23:29:58 +02:00
octarine-noise
31eddf682d get leaf particle type from rules 2021-07-26 22:43:42 +02:00
octarine-noise
e689a44687 simpler parser, allow for negated conditions 2021-07-26 22:42:54 +02:00
octarine-noise
b4824b77ae fix leaf block & leaf particle colors 2021-07-26 14:52:26 +02:00
octarine-noise
4c08354d74 Leaf shader wind integration 2021-07-26 12:15:38 +02:00
octarine-noise
0518b01b50 Optifine compatibility for mixin 2021-07-26 12:14:57 +02:00
octarine-noise
563a67f213 fix wrong package name for Refs 2021-07-26 12:13:06 +02:00
octarine-noise
4637e282ce cleaner rule matching implementation based on Either 2021-07-15 00:53:24 +02:00
octarine-noise
8ef84718b5 get tint index directly from model 2021-07-13 20:06:45 +02:00
octarine-noise
1e69081a2f calculate average texture color without atlas 2021-07-13 20:05:15 +02:00
octarine-noise
c418e001b0 change overlay layer to use Maps as storage 2021-07-13 17:07:06 +02:00
octarine-noise
4ce7eda78b fix snowed short grass color 2021-07-13 17:05:57 +02:00
octarine-noise
145e07363c support for Nylium blocks 2021-07-12 21:47:06 +02:00
octarine-noise
54e245bcd4 add rules for crops 2021-07-12 20:49:45 +02:00
octarine-noise
d2485cd323 remove old block config 2021-07-12 20:27:55 +02:00
octarine-noise
257593d231 use new config parser
+BakeWrapperManager no longer singleton
2021-07-12 20:21:26 +02:00
octarine-noise
c8e79c22ff [WIP] Config parser 2021-07-12 19:13:42 +02:00
octarine-noise
29ab544269 Multi-layer rendering support 2021-07-06 00:06:11 +02:00
octarine-noise
78c7b53595 bump version to 2.6.5 2021-06-02 16:41:26 +02:00
octarine-noise
3a04099fc2 fix chinese lang file 2021-05-29 23:47:13 +02:00
octarine-noise
d6038dd072 add PackNameDecorator to avoid NPE
Forge allows pack hiding, but just in case
2021-05-29 23:43:14 +02:00
octarine-noise
4ebb7f2d35 fix blocks with flat lighting 2021-05-29 23:27:20 +02:00
octarine-noise
14a8600552 fix rising soul particle texture 2021-05-29 16:24:05 +02:00
octarine-noise
b3ffb7e4d6 Merge remote-tracking branch 'local/forge-1.15' into forge-1.16
# Conflicts:
#	src/main/kotlin/mods/betterfoliage/BetterFoliageMod.kt
2021-05-22 19:22:10 +02:00
octarine-noise
fc7f2be15c language key fixes 2021-05-22 19:20:34 +02:00
octarine-noise
b7bdd438e4 upgrade to Cloth Config v4 2021-05-22 19:00:54 +02:00
octarine-noise
65c9596a14 Merge remote-tracking branch 'local/forge-1.15' into forge-1.16
# Conflicts:
#	build.gradle.kts
#	src/main/kotlin/mods/betterfoliage/BetterFoliageMod.kt
#	src/main/kotlin/mods/betterfoliage/config/Config.kt
#	src/main/resources/assets/betterfoliage/lang/en_us.lang
2021-05-22 18:38:10 +02:00
octarine-noise
25b8896a25 log config GUI registration 2021-05-22 18:34:18 +02:00
octarine-noise
f1f811219e Merge remote-tracking branch 'local/forge-1.14' into forge-1.15
# Conflicts:
#	build.gradle.kts
#	gradle.properties
#	src/main/kotlin/mods/betterfoliage/BetterFoliageMod.kt
#	src/main/kotlin/mods/betterfoliage/client/Client.kt
#	src/main/kotlin/mods/betterfoliage/client/integration/ForestryIntegration.kt
#	src/main/kotlin/mods/betterfoliage/client/integration/RubberIntegration.kt
#	src/main/kotlin/mods/betterfoliage/client/render/EntityRisingSoulFX.kt
#	src/main/kotlin/mods/betterfoliage/client/render/RenderCactus.kt
#	src/main/kotlin/mods/betterfoliage/client/render/RenderGrass.kt
#	src/main/kotlin/mods/betterfoliage/client/render/RenderLeaves.kt
#	src/main/kotlin/mods/betterfoliage/client/render/RenderLilypad.kt
#	src/main/kotlin/mods/betterfoliage/client/render/RenderLog.kt
#	src/main/kotlin/mods/betterfoliage/client/render/RenderMycelium.kt
#	src/main/kotlin/mods/betterfoliage/client/render/RenderNetherrack.kt
#	src/main/kotlin/mods/betterfoliage/client/render/RenderReeds.kt
#	src/main/kotlin/mods/betterfoliage/client/texture/GrassRegistry.kt
#	src/main/kotlin/mods/betterfoliage/client/texture/LeafParticleRegistry.kt
#	src/main/kotlin/mods/betterfoliage/client/texture/LeafRegistry.kt
#	src/main/kotlin/mods/betterfoliage/client/texture/Utils.kt
#	src/main/kotlin/mods/betterfoliage/config/Config.kt
#	src/main/kotlin/mods/betterfoliage/config/DelegatingConfig.kt
#	src/main/kotlin/mods/betterfoliage/config/MainConfig.kt
#	src/main/kotlin/mods/betterfoliage/resource/discovery/Delegate.kt
#	src/main/kotlin/mods/betterfoliage/resource/generated/CenteringTextureGenerator.kt
#	src/main/kotlin/mods/betterfoliage/resource/generated/GeneratedGrass.kt
#	src/main/kotlin/mods/octarinecore/client/resource/AsyncSpriteProviderManager.kt
#	src/main/kotlin/mods/octarinecore/client/resource/ModelDiscovery.kt
#	src/main/kotlin/mods/octarinecore/client/resource/ResourceGeneration.kt
#	src/main/kotlin/mods/octarinecore/client/resource/ResourceHandler.kt
2021-05-22 18:07:32 +02:00
octarine-noise
57dc83f1af config GUI misc fixes 2021-05-22 17:49:57 +02:00
octarine-noise
180c2bf230 bump version to 2.5.2 2021-05-22 17:29:13 +02:00
octarine-noise
ff89aa7a13 add client-only version tester 2021-05-22 17:27:51 +02:00
octarine-noise
25cea8633c Integrate ClothConfig for Forge 2021-05-22 17:27:28 +02:00
octarine-noise
eeabc1922e translations JSON conversion
+ some cleanup
2021-05-22 17:25:48 +02:00
XiLaiTL
d7e16d603f Add files via upload
(cherry picked from commit ce8afacb019336962848ade302e7fe76c2654ebd)
2021-05-22 16:39:09 +02:00
octarine-noise
b1a08ab500 Merge remote-tracking branch 'local/forge-1.15' into forge-1.16
# Conflicts:
#	gradle.properties
#	src/main/kotlin/mods/betterfoliage/integration/ShadersModIntegration.kt
#	src/main/kotlin/mods/betterfoliage/model/HalfBaked.kt
#	src/main/kotlin/mods/betterfoliage/render/lighting/VanillaAoCalculation.kt
2021-05-16 12:30:07 +02:00
octarine-noise
fae9e9dfa9 adopt AO calculation tweak from Indigo 2021-05-16 12:02:42 +02:00
octarine-noise
512cd786f7 fix diffuse shading being too dark with shaders 2021-05-16 12:01:59 +02:00
octarine-noise
b96a17fdb9 clean up access transformer 2021-05-16 11:49:04 +02:00
octarine-noise
3d78ecce22 fix unbaked block model coords not being zero-centered 2021-05-16 11:48:34 +02:00
octarine-noise
6219e9353d fix crash with Abnormals Core
change random display tick mixin injection type from @Redirect to @Inject
2021-05-14 18:55:10 +02:00
octarine-noise
e3eb222d93 deburr build script 2021-05-14 18:52:22 +02:00
octarine-noise
8f82fefbb7 Port to 1.16.5
Kottle -> KotlinForForge
2021-05-14 14:44:29 +02:00
octarine-noise
8a303a1a29 update Gradle wrapper to 6.8.1
ForgeGradle dependency for FG4 onwards
2021-05-14 14:40:16 +02:00
octarine-noise
a97a575dd5 fix missing Optifine shader wind effects
+minor cleanup
2021-05-14 14:36:48 +02:00
octarine-noise
a5a5d53341 bump version 2021-05-13 21:13:34 +02:00
octarine-noise
4174301ff7 [WIP] finishing touches
+bunch of renames to bring the 2 version closer
2021-05-13 21:11:47 +02:00
octarine-noise
9899816029 [WIP] Cactus, netherrack, round logs work
+ lots more cleanup
+ Optifine x-ray fix
2021-05-13 10:40:18 +02:00
octarine-noise
dbc421c18e [WIP] fix falling leaf color 2021-05-11 16:53:08 +02:00
octarine-noise
a917d5b3db [WIP] Lilypad working
+ shader integration
2021-05-11 16:18:58 +02:00
octarine-noise
835bf45f13 [WIP] Falling leaves working
+ more cleanup
+ fix double-tinted leaves
2021-05-11 15:08:28 +02:00
octarine-noise
7168caded1 [WIP] algae, reeds, mycelium, coral working
+ lots of cleanup, reorganizing
2021-05-07 19:08:00 +02:00
octarine-noise
f44d2a7a50 [WIP] major rewrite, grass and leaves working already 2021-05-06 22:40:32 +02:00
octarine-noise
09ccb83e8b [WIP] start 1.15 port
reorganize packages to match Fabric version
use util classes from Fabric version
2021-05-01 13:52:21 +02:00
octarine-noise
49d4f8aa31 remove some Fabric aliases 2021-04-30 12:28:34 +02:00
octarine-noise
9566ae8341 fix round log x-ray bug 2021-04-29 11:49:31 +02:00
octarine-noise
dac7fa0b42 fix Optifine Shaders integration
use the low-level call on SVertexBuilder to set apparent block ID for the current draw
2021-04-26 17:07:31 +02:00
octarine-noise
c668713051 fix IModelData being swallowed by renderer 2021-04-26 13:39:09 +02:00
octarine-noise
8a82f3772f fix version information in legacy metadata 2021-04-26 13:38:38 +02:00
octarine-noise
3b728cffcd update Forge version 2021-04-26 13:38:12 +02:00
octarine-noise
aa91aed58e fix Mixin annotation processor requirements 2020-01-17 17:34:16 +01:00
octarine-noise
802862f151 [WIP] more fixes and final touch-up 2020-01-17 17:33:32 +01:00
octarine-noise
2252fb3b42 [WIP] Optifine and Shaders fixes 2020-01-06 20:51:51 +01:00
octarine-noise
4efa831296 [WIP] more async texture loading
keep stitch() call in original method body, in case others want to mod it too
2020-01-06 18:15:17 +01:00
octarine-noise
a3d99c3076 [WIP] fix Mixin annotation processing 2020-01-05 16:33:12 +01:00
octarine-noise
c4ee768025 [WIP] async block texture reloading 2020-01-05 16:32:45 +01:00
octarine-noise
b4f18c1e1d switch to Gradle Kotlin DSL 2020-01-03 21:36:46 +01:00
octarine-noise
2a06c18884 [WIP] Roll all rendering parameters into a single context
+ split project into platform-dependent and -independent parts
2020-01-03 21:36:08 +01:00
octarine-noise
2ba99f40e7 Merge branch 'kotlin-1.12' into forge-1.14 2020-01-01 17:15:13 +01:00
octarine-noise
46cbe64328 [WIP] 1.14.4 port 2020-01-01 16:57:47 +01:00
octarine-noise
7b739c172f Bump version 2020-01-01 16:12:57 +01:00
octarine-noise
8319d721c7 Update to work with OptiFine_1.12.2_HD_U_F6_pre1 2020-01-01 15:15:44 +01:00
octarine-noise
1b0e93b050 Array bounds check for overlay layer 2020-01-01 15:14:19 +01:00
octarine-noise
1ea2b6b946 Model loading rework (1.14 prep)
remove unnecessary complexity
access sprites only at PostStitch
2019-12-30 17:35:52 +01:00
octarine-noise
558c9a2c34 Bump self and dependency versions 2019-12-21 15:10:32 +01:00
octarine-noise
f102bf4bda add Gradle wrapper 2019-12-21 15:10:09 +01:00
octarine-noise
f5cbf48dfa Try to cope with other mods wrapping IModel instances in a generic way 2019-12-21 15:09:51 +01:00
octarine-noise
d9cc03511a Cache round log neighborhood data for faster chunk re-rendering 2019-12-21 15:08:29 +01:00
octarine-noise
d0265483d2 Option to skip rendering of hidden extra leaves 2019-12-21 14:37:20 +01:00
octarine-noise
20da2a27bd Fully remove Optifine CTM code 2019-12-21 14:16:28 +01:00
octarine-noise
02509fa44d remove useless config option: distance limit 2019-09-04 11:29:50 +02:00
octarine-noise
6801304bd1 do not access TileEntities in unloaded chunks 2019-09-03 14:24:26 +02:00
octarine-noise
d96ac1c94c update Forge / Forgelin / Kotlin versions 2019-09-03 14:23:44 +02:00
octarine-noise
ac015b12df disable Optifine CTM support, fix custom colors 2019-04-13 09:37:57 +02:00
octarine-noise
71f0be0c93 fix shader mod integration 2019-03-02 13:27:39 +01:00
octarine-noise
bcc1b04e6b Optifine dev wrapper must be written in Java 2019-03-02 12:00:28 +01:00
octarine-noise
cbee4916ed Merge branch 'kotlin-1.10' into kotlin-1.12 2018-10-26 17:38:14 +02:00
octarine-noise
418d955f54 add Forgelin as mod dependency 2018-10-26 17:37:16 +02:00
octarine-noise
3121542c07 fix operand stack overflow in RenderChunk class transformation with recent Forge versions 2018-10-25 14:17:57 +02:00
octarine-noise
ab08457ae7 Merge branch 'kotlin-1.10' into kotlin-1.12 2018-10-25 14:15:06 +02:00
octarine-noise
5b1b35a891 update to latest recommended Forge 2018-10-25 14:14:30 +02:00
octarine-noise
787b3993b5 allow access to ClassWriter flags in class transformer context 2018-10-25 14:05:13 +02:00
octarine-noise
c593ff9bcb Merge branch 'kotlin-1.10' into kotlin-1.12 2018-10-25 13:46:29 +02:00
octarine-noise
afa619f838 use Forgelin 2018-10-25 11:11:14 +02:00
octarine-noise
e704a0af94 update to latest recommended Forge 2018-10-25 11:10:44 +02:00
octarine-noise
0997b83367 move values to properties file 2018-10-25 10:03:56 +02:00
octarine-noise
fe3030ef77 bump version to 2.1.11 2017-10-01 12:31:33 +02:00
octarine-noise
7aa510189a fix gray blocks with Optifine custom colors enabled 2017-09-30 13:37:43 +02:00
octarine-noise
36a3a38ff1 Merge branch 'kotlin-1.11.2' into kotlin-1.12 2017-09-10 01:18:47 +02:00
octarine-noise
7f4ee4b0a3 support MC versions 1.12 and 1.12.1 as well 2017-09-10 01:15:33 +02:00
octarine-noise
a4c6d1eecd Merge branch 'kotlin-1.10' into kotlin-1.11.2 2017-09-10 01:13:41 +02:00
octarine-noise
73223c15c3 bump version to 2.1.10 2017-09-10 01:12:32 +02:00
octarine-noise
b55d90802f fix broken serverside detection 2017-09-10 01:11:27 +02:00
octarine-noise
c5261216b2 Merge branch 'kotlin-1.11.2' into kotlin-1.12 2017-08-12 19:12:46 +02:00
octarine-noise
50399052b0 Merge branch 'kotlin-1.10' into kotlin-1.11.2 2017-08-12 18:57:10 +02:00
octarine-noise
e29d224df4 update to more recent Forge (GUI factory changes) 2017-08-12 18:19:25 +02:00
octarine-noise
3b7c44b062 bump version to 2.1.8 2017-08-12 18:15:18 +02:00
octarine-noise
32bf60492d add OptiFine custom color support 2017-08-12 17:58:24 +02:00
octarine-noise
9685971fd4 update lang file 2017-08-12 16:37:47 +02:00
octarine-noise
dacc63b11a minor code cleanup 2017-08-12 16:37:27 +02:00
octarine-noise
e20a3fbc54 move nVidia compatibility to a config option 2017-08-12 16:33:22 +02:00
octarine-noise
39df1951df add default model mappings for Lithos Core 2017-08-12 16:09:21 +02:00
octarine-noise
4761eb266f add support for block variants and multiple wood log bark textures 2017-08-12 15:59:22 +02:00
octarine-noise
25c302ecb6 add support for modded netherrack and mycelium
add support for NetherEx
2017-08-12 10:46:42 +02:00
octarine-noise
dec1f6ff03 Merge branch 'kotlin-1.11.2' into kotlin-1.12 2017-07-10 17:30:30 +02:00
octarine-noise
f145ff221e Merge branch 'kotlin-1.10' into kotlin-1.11.2 2017-07-10 17:27:34 +02:00
octarine-noise
c865c8e5ad bump version to 2.1.7 2017-07-10 17:20:32 +02:00
octarine-noise
47781cad91 fixed all rendering layer woes (hopefully) 2017-07-10 17:18:26 +02:00
octarine-noise
e329ce0270 remove unnecessary mirroring of round log endcap texture 2017-07-10 15:29:21 +02:00
octarine-noise
7cae04d7b4 fix round log texture glitch on non-nVidia cards 2017-07-10 15:28:52 +02:00
octarine-noise
0bbf206569 fix compatibility with OptiFine AA & AF 2017-07-10 15:14:07 +02:00
octarine-noise
55095e7252 Merge branch 'kotlin-1.11.2' into kotlin-1.12 2017-07-06 11:01:59 +02:00
octarine-noise
8eebb98c9d Merge branch 'kotlin-1.10' into kotlin-1.11.2 2017-07-06 11:00:32 +02:00
octarine-noise
2a8a9c2703 fix broken OptiFine integration 2017-07-06 10:59:52 +02:00
octarine-noise
7da89e24f1 update rubber integration for IC2 and TR 1.12 2017-07-06 10:12:46 +02:00
octarine-noise
fb078ab365 port to MC 1.12 2017-07-06 09:53:50 +02:00
octarine-noise
b5d87bb148 fix rubber tree integration to work with single resource loading pass 2017-07-06 09:10:38 +02:00
octarine-noise
6c98940d3e Merge branch 'kotlin-1.10' into kotlin-1.11.2 2017-07-05 15:09:28 +02:00
octarine-noise
813719c7f2 bump version to 2.1.6 2017-07-05 15:07:41 +02:00
octarine-noise
800fb4db9f add Better With Mods stumps to the log whitelist 2017-07-05 15:07:07 +02:00
octarine-noise
38b35c910b allow custom renderers to draw on multiple layers
fix extra leaves & fast graphics xray bug
2017-07-05 11:34:07 +02:00
octarine-noise
61076789db fix build errors after merge 2017-05-06 09:47:08 +02:00
octarine-noise
fde6c47ed3 Merge branch 'kotlin-1.10' into kotlin-1.11.2 2017-05-06 09:38:10 +02:00
octarine-noise
a9fba1a18e bump version to 2.1.5 2017-05-06 09:36:47 +02:00
octarine-noise
81ef954524 add support for multipart models 2017-05-06 09:36:32 +02:00
octarine-noise
674d22fdbb add AbyssalCraft grass blocks to default whitelist 2017-05-05 12:10:25 +02:00
octarine-noise
381b154413 change short grass override color to work better with dark textures 2017-05-05 12:08:55 +02:00
octarine-noise
568549e260 added population option to short grass as well (for non-full coverage) 2017-05-05 11:15:55 +02:00
octarine-noise
6d62cb9ac0 dev-only debug code for ASM transformer incompatibilities 2017-05-05 10:58:48 +02:00
octarine-noise
5bea5cde99 error reporting for unexpected registry misses 2017-05-05 10:57:34 +02:00
octarine-noise
28cead464e Merge branch 'kotlin-1.10' into kotlin-1.11.2 2017-04-09 12:07:25 +02:00
octarine-noise
8ffca417fb get rid of obfuscated namespace support, rely on FML deobfuscation instead 2017-04-09 11:30:35 +02:00
octarine-noise
8f9a35f40e bump version to 2.1.4 2017-04-09 10:26:56 +02:00
octarine-noise
ef90adf577 remove reeds and double plants from the crop list 2017-04-08 18:58:38 +02:00
octarine-noise
370e2bb38c don't render hidden faces on connected grass 2017-04-08 18:24:24 +02:00
octarine-noise
fefd5e5633 added compatibility with FoamFix anarchy parallel model baking 2017-04-08 17:58:54 +02:00
octarine-noise
bae81e8085 improve class transformer to work with SRG namespace as well 2017-04-08 17:57:52 +02:00
octarine-noise
479e4cadfa Switch to Kotlin 1.1.1, use typealias feature 2017-04-08 13:03:47 +02:00
octarine-noise
821d618395 update Forestry log support 2017-02-18 10:54:53 +01:00
octarine-noise
eb6058c4ee port to MC 1.11.2 2017-01-29 10:11:53 +01:00
octarine-noise
625a3bd543 change lang file names to lowercase 2016-12-22 00:34:42 +01:00
octarine-noise
37d091daed Merge branch 'kotlin-1.10' into kotlin-1.11 2016-12-22 00:28:58 +01:00
octarine-noise
d852faad96 bump version to 2.1.3 2016-12-22 00:01:14 +01:00
octarine-noise
da8d7ec237 add support for IC2 and TechReborn rubber logs and leaves 2016-12-21 23:57:28 +01:00
octarine-noise
31f64749b1 add some SideOnly annotations where appropriate 2016-12-21 19:54:49 +01:00
octarine-noise
66558932a9 move log block axis into the info object 2016-12-21 19:36:40 +01:00
octarine-noise
ceb3e5b116 port to MC 1.11 2016-12-11 17:02:54 +01:00
octarine-noise
1a1aa81c0f fixed leaf particle fallback type 2016-12-11 16:43:48 +01:00
octarine-noise
522fc1de33 create log directory if missing 2016-12-11 16:14:32 +01:00
octarine-noise
90c13a3a8e prefix log messages by mod name 2016-12-11 16:14:09 +01:00
octarine-noise
66b4df2850 bump version to 2.1.2 2016-12-05 00:32:15 +01:00
octarine-noise
7df142cf50 support for BOP flowering oak leaves
cleanup default models configs
2016-12-04 23:00:34 +01:00
octarine-noise
2b7582c5af fix modid mismatch between annotation and mcmod.info 2016-12-04 22:45:07 +01:00
octarine-noise
56e3dc5d24 fix color for connected grass blocks 2016-12-04 22:02:14 +01:00
octarine-noise
f7044e5225 update Aether II support 2016-12-04 20:19:05 +01:00
octarine-noise
59e4d0c602 fix saturation threshold not being respected for grass textures 2016-12-04 20:12:18 +01:00
octarine-noise
931dca6f3f add Agricultural Revolution log support
fixes #107
2016-12-04 19:21:15 +01:00
octarine-noise
c356c3ef57 defend against CME in Forestry leaf support module
fixes #106
2016-12-04 18:54:25 +01:00
octarine-noise
468d0f34b6 update Natura support
fixes #101
2016-12-04 18:50:02 +01:00
octarine-noise
6f42152cde get rid of some metadata warnings (future-proofing) 2016-12-04 18:47:25 +01:00
octarine-noise
44bfc93e1b preemptively create detail log file 2016-12-04 18:47:25 +01:00
octarine-noise
70591a484e null protection when fetching LeafInfo for leaf block 2016-12-04 18:47:24 +01:00
octarine-noise
fdc14595db add support for Forestry decorative leaves 2016-12-04 18:47:24 +01:00
Forstride
aa8226b46b Updated default block listings for Biomes O' Plenty support (#105)
Squashed 6 commits from branch kotlin-1.10 on Forstride/BetterFoliage

* Update CropDefault.cfg
* Update DirtDefault.cfg
* Update GrassBlocksDefault.cfg
* Update LeavesBlocksDefault.cfg
* Update LogBlocksDefault.cfg
* Update LilypadDefault.cfg
2016-12-04 18:46:25 +01:00
octarine-noise
ad78529d2a Merge branch 'kotlin-1.9.4' into kotlin-1.10 2016-09-17 08:33:33 +02:00
octarine-noise
8c922fd2e8 Merge branch 'kotlin-1.9' into kotlin-1.9.4 2016-09-13 21:12:29 +02:00
octarine-noise
9e9666f69f Merge branch 'kotlin-1.8' into kotlin-1.9 2016-09-13 21:08:36 +02:00
octarine-noise
2c0e95ba5b - revert to case-sensitive config
- fix handling of toggle lists
- ignore unknown options in config file
2016-09-13 20:36:35 +02:00
octarine-noise
85a4707494 automatically delete obsolete config properties 2016-09-05 22:45:00 +02:00
octarine-noise
f0a447bbbb switch to recent Forge and Kotlin versions 2016-09-05 22:43:30 +02:00
octarine-noise
4a4d39b523 Add support for Forestry logs
fixes #82
2016-09-05 22:40:13 +02:00
octarine-noise
e00ccd5919 bump version 2016-08-13 00:27:54 +02:00
octarine-noise
f032814d99 fix particle mapping
Forestry support
2016-08-11 11:56:52 +02:00
octarine-noise
62294bb2bb add support for Forestry leaves 2016-08-11 11:14:42 +02:00
octarine-noise
dec1ffd71c fix config change listener 2016-08-11 11:00:32 +02:00
octarine-noise
488078b50f rewrite model and texture detection
expose in mod configuration
2016-08-11 10:58:57 +02:00
octarine-noise
1bd353577f always push shader metadata for block models 2016-08-09 09:23:55 +02:00
octarine-noise
913496473d Merge branch 'kotlin-1.9.4' into kotlin-1.10 2016-07-28 12:18:58 +02:00
octarine-noise
a8e6c6c470 changed Kotlin version to 1.0.2, no more class relocation 2016-07-28 12:16:57 +02:00
octarine-noise
8bdb5ca8fd update modded block support 2016-07-28 12:16:09 +02:00
octarine-noise
5efc974133 Merge branch 'kotlin-1.9' into kotlin-1.9.4 2016-07-23 13:37:05 +02:00
octarine-noise
7f3617ef59 Merge branch 'kotlin-1.8' into kotlin-1.9 2016-07-23 13:32:31 +02:00
octarine-noise
400b965e02 use case insensitive names in config 2016-07-23 12:39:56 +02:00
octarine-noise
f96409f9a1 allow for custom column textures (preparation for rubber log support) 2016-07-23 12:01:28 +02:00
octarine-noise
2ca330fd29 added option to disable snow on extra leaves 2016-07-23 09:34:10 +02:00
octarine-noise
acf477d709 added korean localization 2016-07-23 09:32:11 +02:00
octarine-noise
97d5d6320c Merge pull request #62 from Ghostlyr/patch-1
Small inaccuracy in the russian localization
2016-06-28 00:43:51 +02:00
octarine-noise
6831050c77 port to 1.10 2016-06-27 23:38:37 +02:00
octarine-noise
70ec7e5289 use case insensitive names in config 2016-06-27 23:36:28 +02:00
octarine-noise
f66aabea67 port to 1.9.4 2016-06-21 01:28:43 +02:00
octarine-noise
0635f1e19e update to OptiFine H6 2016-06-15 20:54:58 +02:00
Ghostlyr
44a8abeb4b Small inaccuracy in the russian localization
Sorry, automatic translation =)
2016-06-07 00:32:10 +06:00
octarine-noise
b5af0fe1c5 upgrade to recent Forge and MCP mappings 2016-05-16 12:20:26 +02:00
octarine-noise
69db3d6608 Merge branch 'kotlin-1.8' into kotlin-1.9 2016-05-16 12:16:48 +02:00
octarine-noise
6903bd2de2 bump version 2016-05-16 12:12:43 +02:00
octarine-noise
9a544b1b53 support leaves that don't use the model minecraft:block/leaves 2016-05-16 12:05:54 +02:00
octarine-noise
bbd4df418c add support for blocks with submodels 2016-05-16 12:01:25 +02:00
octarine-noise
626bc69dad fix CTM textures
bump version to 2.0.10
2016-04-25 19:14:16 +02:00
octarine-noise
66ed1c098f do not use deprecated Block.canRenderInLayer() 2016-04-19 21:11:09 +02:00
octarine-noise
f37cb273f1 Merge branch 'kotlin-1.8' into kotlin-1.9 2016-04-19 21:09:52 +02:00
octarine-noise
a6cc354965 fix wrong height range used for algae 2016-04-19 20:56:28 +02:00
octarine-noise
8fe4346922 fix shallow water coral config option 2016-04-19 20:55:14 +02:00
octarine-noise
2f45abcd7c bump version to 2.0.9 2016-04-19 20:47:53 +02:00
octarine-noise
57cae957f8 keep round logs on the SOLID rendering layer 2016-04-19 20:23:07 +02:00
octarine-noise
befb64b8fc animated texture support for generated short grass 2016-04-19 20:21:17 +02:00
octarine-noise
a0aad5d608 fixed startup crash on null block in blockstate 2016-04-19 20:20:58 +02:00
octarine-noise
c0685d829b update to latest Forge and OptiFine
bump version to 2.0.8
2016-04-07 00:20:09 +02:00
octarine-noise
4209d1eea3 bump version to 2.0.7 2016-04-01 19:11:56 +02:00
octarine-noise
ac2001694b fix random display tick not firing 2016-04-01 19:11:28 +02:00
octarine-noise
50c4882855 fix config crash when accessing isOpaqueCube() before postInit 2016-04-01 19:08:06 +02:00
octarine-noise
07dc8888ef minor cosmetic tweaks in log renderer 2016-04-01 18:23:47 +02:00
octarine-noise
efebf29c64 bump version to 2.0.7 2016-04-01 18:21:49 +02:00
octarine-noise
983703133a Merge branch 'kotlin-1.8' into kotlin-1.9 2016-04-01 18:14:10 +02:00
octarine-noise
ee19120651 Copied from pull request #52 from Ghostlyr/kotlin-1.7.10
Add russian localization.
2016-04-01 18:12:27 +02:00
octarine-noise
cbaebfa26a fixed #50 2016-03-29 22:29:32 +02:00
octarine-noise
1ff5d45840 fixed X-ray glitch caused by Round Logs being reported as opaqe blocks 2016-03-29 22:28:49 +02:00
octarine-noise
7e667d483a fixed Log block endcap UV rotations 2016-03-29 22:14:59 +02:00
octarine-noise
6d5c03ba6a fix crash with OptiFine 1.9 (pre A series)
bump to 2.0.5
2016-03-26 09:28:54 +01:00
octarine-noise
f47aedf84d Merge branch 'kotlin-1.8' into kotlin-1.9 2016-03-26 08:08:58 +01:00
octarine-noise
6ee27c2a99 port to MC 1.9 2016-03-21 20:54:06 +01:00
octarine-noise
abf037d8a9 change the OptiFine dev tweaker to work with OF 1.8.x H5 and later 2016-03-21 20:51:37 +01:00
octarine-noise
c0be72bb37 get Optifine in the dev environment 2016-02-27 02:09:14 +01:00
octarine-noise
42c14790af Merge branch 'kotlin-1.8' into kotlin-1.8.8 2016-02-21 12:35:49 +01:00
octarine-noise
087e8d5685 fix Short Grass under snow 2016-02-21 12:34:00 +01:00
octarine-noise
aaca43fe2c Merge branch 'kotlin-1.8' into kotlin-1.8.8 2016-02-21 12:15:09 +01:00
octarine-noise
8e251dc038 make particle type definitions case insensitive 2016-02-21 12:12:55 +01:00
octarine-noise
1a6ffb251b bump version to 2.0.4 2016-02-21 11:57:59 +01:00
octarine-noise
efb49125b8 support Cooking Plus log blocks 2016-02-21 11:09:45 +01:00
octarine-noise
6032a120d8 support Plant Mega Pack log blocks 2016-02-21 11:08:44 +01:00
octarine-noise
f68f0e5edd Merge branch 'kotlin-1.8' into kotlin-1.8.8 2016-02-21 10:39:44 +01:00
octarine-noise
7393fed5fd Switch Kotlin version to 1.0.0 2016-02-21 10:37:37 +01:00
octarine-noise
41b080646a correctly apply smooth shading multipliers for AO data 2016-02-17 23:56:35 +01:00
octarine-noise
111f1b3907 fix custom texture search location 2016-02-13 11:56:48 +01:00
octarine-noise
662b4f50f0 Merge branch 'kotlin-1.8' into kotlin-1.8.8 2016-01-30 13:48:13 +01:00
octarine-noise
d9a042b356 protect against render buffer underruns 2016-01-30 13:44:33 +01:00
octarine-noise
ef574a13ac Merge branch 'kotlin-1.8' into kotlin-1.8.8 2016-01-30 13:05:30 +01:00
octarine-noise
b246c7acd0 simplified ModelDataInspector 2016-01-30 12:22:29 +01:00
octarine-noise
35ce80c602 added config option for rendering Round Logs with unknown axis
added Tangle Logs to the default whitelist
2016-01-30 12:20:13 +01:00
octarine-noise
4fdabbaf69 fixed lack of generic signature in 1.8 mappings 2016-01-30 11:57:34 +01:00
octarine-noise
0a6c433530 Merge branch 'kotlin-1.8' into kotlin-1.8.8 2016-01-30 00:58:41 +01:00
octarine-noise
3e6f98885f fixed & improved Round Log shading 2016-01-30 00:56:33 +01:00
octarine-noise
4720667f53 updated Thaumcraft Log block class 2016-01-30 00:52:35 +01:00
octarine-noise
5616a68f5a changed Kotlin version 2016-01-30 00:49:59 +01:00
octarine-noise
d1480ed3be improved Log block axis detection 2016-01-30 00:46:15 +01:00
octarine-noise
023e286fd4 Merge branch 'kotlin-1.8' into kotlin-1.8.8 2016-01-29 11:32:33 +01:00
octarine-noise
6c8f40f4e2 Optifine CTM support 2016-01-28 01:18:18 +01:00
octarine-noise
6306a5b03b fix accepted MC versions 2016-01-28 00:21:37 +01:00
octarine-noise
b0193bb108 update version 2016-01-18 14:31:22 +01:00
octarine-noise
eafd36b4b1 fixed OptiFine compatibility 2016-01-18 13:46:56 +01:00
octarine-noise
dac7033e18 Merge branch 'kotlin-1.8' into kotlin-1.8.8 2016-01-18 12:51:21 +01:00
octarine-noise
36c6b775db changed Extra Leaves brightness calculation 2016-01-18 12:50:08 +01:00
octarine-noise
0f985e996b port to MC 1.8.8 2016-01-18 12:47:23 +01:00
octarine-noise
e926f018e7 actually fix #36 2016-01-16 08:35:30 +01:00
octarine-noise
3716804ffb fix Round Log top and bottom texture rotation 2016-01-14 00:07:03 +01:00
octarine-noise
2d70de00e7 changed Round Log tapering rules 2016-01-13 23:12:04 +01:00
octarine-noise
9b717b549b fix for fix of #36 2016-01-13 23:00:19 +01:00
octarine-noise
81abad82e9 fixed #36 2016-01-13 22:55:07 +01:00
octarine-noise
601679f070 switch to new Forge and ForgeGradle version 2016-01-09 14:12:03 +01:00
octarine-noise
f0073b0f2a consolidate AccessTransformer 2016-01-09 14:11:28 +01:00
octarine-noise
5bdbb366eb remove nasty class visibility hack 2016-01-09 14:11:00 +01:00
octarine-noise
2e09f1f10a fixed language file typos 2016-01-09 13:36:55 +01:00
octarine-noise
8460103030 port to MC 1.8 2016-01-09 12:55:52 +01:00
octarine-noise
f44043bb0b first Kotlin version 2016-01-09 12:27:42 +01:00
176 changed files with 8211 additions and 0 deletions

11
.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
.idea/
*.iml
*.ipr
*.iws
run/
.gradle/
build/
classes/
temp/
logs
src/main/javacc/mods

73
build.gradle.kts Normal file
View File

@@ -0,0 +1,73 @@
plugins {
kotlin("jvm").version("1.4.20")
id("net.minecraftforge.gradle").version("4.1.12")
id("org.spongepowered.mixin").version("0.7-SNAPSHOT")
id("com.intershop.gradle.javacc").version("4.0.0")
}
repositories {
maven("https://files.minecraftforge.net/maven")
maven("https://repo.spongepowered.org/maven")
maven("https://www.cursemaven.com")
maven("https://thedarkcolour.github.io/KotlinForForge/")
}
dependencies {
"minecraft"("net.minecraftforge:forge:${properties["mcVersion"]}-${properties["forgeVersion"]}")
"implementation"("thedarkcolour:kotlinforforge:1.7.0")
"api"(fg.deobf("curse.maven:clothconfig-348521:3311352"))
}
configurations["annotationProcessor"].extendsFrom(configurations["implementation"])
sourceSets {
get("main").ext["refMap"] = "betterfoliage.refmap.json"
get("main").java.srcDir("src/main/javacc/")
}
minecraft {
mappings(properties["mappingsChannel"] as String, properties["mappingsVersion"] as String)
accessTransformer(file("src/main/resources/META-INF/accesstransformer.cfg"))
runs.create("client") {
workingDirectory(file("run"))
properties["forge.logging.markers"] = "CORE"
properties["forge.logging.console.level"] = "debug"
mods.create("betterfoliage") {
source(sourceSets["main"])
}
}
}
javacc {
configs {
create("blockconfig") {
staticParam = "false"
inputFile = file("src/main/javacc/BlockConfig.jj")
outputDir = file("src/main/javacc/")
packageName = "mods.betterfoliage.config.match.parser"
}
}
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlin {
target.compilations.configureEach {
kotlinOptions.jvmTarget = "1.8"
kotlinOptions.freeCompilerArgs += listOf("-Xno-param-assertions", "-Xno-call-assertions")
}
}
tasks.getByName<Jar>("jar") {
archiveName = "BetterFoliage-${project.version}-Forge-${properties["mcVersion"]}.jar"
manifest {
from(file("src/main/resources/META-INF/MANIFEST.MF"))
attributes["Implementation-Version"] = project.version
}
exclude("net")
filesMatching("META-INF/mods.toml") { expand(project.properties) }
filesMatching("mcmod.info") { expand(project.properties) }
}

14
gradle.properties Normal file
View File

@@ -0,0 +1,14 @@
org.gradle.jvmargs=-Xmx2G
org.gradle.daemon=false
group = com.github.octarine-noise
jarName = BetterFoliage-Forge
version = 2.7.0
mcVersion = 1.16.5
forgeVersion = 36.1.17
mappingsChannel = official
mappingsVersion = 1.16.5
#kottleVersion = 1.4.0

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

185
gradlew vendored Normal file
View File

@@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

89
gradlew.bat vendored Normal file
View File

@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

13
settings.gradle.kts Normal file
View File

@@ -0,0 +1,13 @@
pluginManagement {
repositories {
maven("https://files.minecraftforge.net/maven")
maven("https://repo.spongepowered.org/maven")
gradlePluginPortal()
}
resolutionStrategy {
eachPlugin {
if (requested.id.let { it.namespace == "net.minecraftforge" && it.name == "gradle"} ) useModule("net.minecraftforge.gradle:ForgeGradle:${requested.version}")
if (requested.id.let { it.namespace == "org.spongepowered" && it.name == "mixin"} ) useModule("org.spongepowered:mixingradle:${requested.version}")
}
}
}

View File

@@ -0,0 +1,18 @@
package mods.betterfoliage;
import org.spongepowered.asm.mixin.Mixins;
import org.spongepowered.asm.mixin.connect.IMixinConnector;
public class MixinConnector implements IMixinConnector {
@Override
public void connect() {
Mixins.addConfiguration("betterfoliage.common.mixins.json");
try {
Class.forName("optifine.OptiFineTransformationService");
Mixins.addConfiguration("betterfoliage.optifine.mixins.json");
} catch (ClassNotFoundException e) {
Mixins.addConfiguration("betterfoliage.vanilla.mixins.json");
}
}
}

View File

@@ -0,0 +1,33 @@
package mods.betterfoliage.mixin;
import mods.betterfoliage.Hooks;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.util.Direction;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.shapes.VoxelShape;
import net.minecraft.world.IBlockReader;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
/**
* Mixin overriding the {@link VoxelShape} used for the neighbor block in {@link Block}.shouldSideBeRendered().
*
* This way log blocks can be made to not block rendering, without altering any {@link Block} or
* {@link BlockState} properties with potential gameplay ramifications.
*/
@Mixin(Block.class)
public class MixinBlock {
private static final String shouldSideBeRendered = "Lnet/minecraft/block/Block;shouldRenderFace(Lnet/minecraft/block/BlockState;Lnet/minecraft/world/IBlockReader;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/util/Direction;)Z";
private static final String getVoxelShape = "Lnet/minecraft/block/BlockState;func_215702_a(Lnet/minecraft/world/IBlockReader;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/util/Direction;)Lnet/minecraft/util/math/shapes/VoxelShape;";
private static final String getFaceOcclusionShape = "Lnet/minecraft/block/BlockState;getFaceOcclusionShape(Lnet/minecraft/world/IBlockReader;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/util/Direction;)Lnet/minecraft/util/math/shapes/VoxelShape;";
private static final String isOpaqueCube = "Lnet/minecraft/block/Block;isOpaqueCube(Lnet/minecraft/block/BlockState;Lnet/minecraft/world/IBlockReader;Lnet/minecraft/util/math/BlockPos;)Z";
@Redirect(method = shouldSideBeRendered, at = @At(value = "INVOKE", target = getFaceOcclusionShape, ordinal = 1))
private static VoxelShape getVoxelShapeOverride(BlockState state, IBlockReader reader, BlockPos pos, Direction dir) {
return Hooks.getVoxelShapeOverride(state, reader, pos, dir);
}
}

View File

@@ -0,0 +1,41 @@
package mods.betterfoliage.mixin;
import com.mojang.blaze3d.matrix.MatrixStack;
import com.mojang.blaze3d.vertex.IVertexBuilder;
import mods.betterfoliage.model.SpecialRenderModel;
import mods.betterfoliage.render.pipeline.RenderCtxVanilla;
import net.minecraft.block.BlockState;
import net.minecraft.client.renderer.BlockModelRenderer;
import net.minecraft.client.renderer.model.IBakedModel;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.IBlockDisplayReader;
import net.minecraftforge.client.model.data.IModelData;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import java.util.Random;
@Mixin(BlockModelRenderer.class)
public class MixinBlockModelRenderer {
private static final String renderModel = "Lnet/minecraft/client/renderer/BlockModelRenderer;renderModel(Lnet/minecraft/world/IBlockDisplayReader;Lnet/minecraft/client/renderer/model/IBakedModel;Lnet/minecraft/block/BlockState;Lnet/minecraft/util/math/BlockPos;Lcom/mojang/blaze3d/matrix/MatrixStack;Lcom/mojang/blaze3d/vertex/IVertexBuilder;ZLjava/util/Random;JILnet/minecraftforge/client/model/data/IModelData;)Z";
private static final String renderModelFlat = "Lnet/minecraft/client/renderer/BlockModelRenderer;renderModelFlat(Lnet/minecraft/world/IBlockDisplayReader;Lnet/minecraft/client/renderer/model/IBakedModel;Lnet/minecraft/block/BlockState;Lnet/minecraft/util/math/BlockPos;Lcom/mojang/blaze3d/matrix/MatrixStack;Lcom/mojang/blaze3d/vertex/IVertexBuilder;ZLjava/util/Random;JILnet/minecraftforge/client/model/data/IModelData;)Z";
private static final String renderModelSmooth = "Lnet/minecraft/client/renderer/BlockModelRenderer;renderModelSmooth(Lnet/minecraft/world/IBlockDisplayReader;Lnet/minecraft/client/renderer/model/IBakedModel;Lnet/minecraft/block/BlockState;Lnet/minecraft/util/math/BlockPos;Lcom/mojang/blaze3d/matrix/MatrixStack;Lcom/mojang/blaze3d/vertex/IVertexBuilder;ZLjava/util/Random;JILnet/minecraftforge/client/model/data/IModelData;)Z";
@Redirect(method = renderModel, at = @At(value = "INVOKE", target = renderModelSmooth), remap = false)
public boolean onRenderModelSmooth(BlockModelRenderer renderer, IBlockDisplayReader world, IBakedModel model, BlockState state, BlockPos pos, MatrixStack matrixStack, IVertexBuilder buffer, boolean checkSides, Random random, long rand, int combinedOverlay, IModelData modelData) {
if (model instanceof SpecialRenderModel)
return RenderCtxVanilla.render(renderer, world, (SpecialRenderModel) model, state, pos, matrixStack, buffer, checkSides, random, rand, combinedOverlay, modelData, true);
else
return renderer.renderModelSmooth(world, model, state, pos, matrixStack, buffer, checkSides, random, rand, combinedOverlay, modelData);
}
@Redirect(method = renderModel, at = @At(value = "INVOKE", target = renderModelFlat), remap = false)
public boolean onRenderModelFlat(BlockModelRenderer renderer, IBlockDisplayReader world, IBakedModel model, BlockState state, BlockPos pos, MatrixStack matrixStack, IVertexBuilder buffer, boolean checkSides, Random random, long rand, int combinedOverlay, IModelData modelData) {
if (model instanceof SpecialRenderModel)
return RenderCtxVanilla.render(renderer, world, (SpecialRenderModel) model, state, pos, matrixStack, buffer, checkSides, random, rand, combinedOverlay, modelData, false);
else
return renderer.renderModelFlat(world, model, state, pos, matrixStack, buffer, checkSides, random, rand, combinedOverlay, modelData);
}
}

View File

@@ -0,0 +1,31 @@
package mods.betterfoliage.mixin;
import mods.betterfoliage.Hooks;
import net.minecraft.block.AbstractBlock;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.IBlockReader;
import org.spongepowered.asm.mixin.Debug;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Coerce;
import org.spongepowered.asm.mixin.injection.Redirect;
/**
* Mixin to override the result of {@link BlockState}.getAmbientOcclusionLightValue().
*
* Needed to avoid excessive darkening of Round Logs at the corners, now that they are not full blocks.
*/
@Mixin(AbstractBlock.AbstractBlockState.class)
@SuppressWarnings({"deprecation"})
public class MixinBlockState {
private static final String callFrom = "Lnet/minecraft/block/AbstractBlock$AbstractBlockState;getShadeBrightness(Lnet/minecraft/world/IBlockReader;Lnet/minecraft/util/math/BlockPos;)F";
// why is the INVOKEVIRTUAL target class Block in the bytecode, not AbstractBlock?
private static final String callTo = "Lnet/minecraft/block/Block;getShadeBrightness(Lnet/minecraft/block/BlockState;Lnet/minecraft/world/IBlockReader;Lnet/minecraft/util/math/BlockPos;)F";
@Redirect(method = callFrom, at = @At(value = "INVOKE", target = callTo))
float getAmbientOcclusionValue(Block block, BlockState state, IBlockReader reader, BlockPos pos) {
return Hooks.getAmbientOcclusionLightValueOverride(block.getShadeBrightness(state, reader, pos), state);
}
}

View File

@@ -0,0 +1,43 @@
package mods.betterfoliage.mixin;
import mods.betterfoliage.Hooks;
import net.minecraft.block.BlockState;
import net.minecraft.client.renderer.WorldRenderer;
import net.minecraft.client.world.ClientWorld;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.IBlockReader;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.Random;
@Mixin(ClientWorld.class)
public class MixinClientWorld {
private static final String worldAnimateTick = "Lnet/minecraft/client/world/ClientWorld;doAnimateTick(IIIILjava/util/Random;ZLnet/minecraft/util/math/BlockPos$Mutable;)V";
private static final String blockAnimateTick = "Lnet/minecraft/block/Block;animateTick(Lnet/minecraft/block/BlockState;Lnet/minecraft/world/World;Lnet/minecraft/util/math/BlockPos;Ljava/util/Random;)V";
private static final String worldNotify = "Lnet/minecraft/client/world/ClientWorld;sendBlockUpdated(Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/BlockState;Lnet/minecraft/block/BlockState;I)V";
private static final String rendererNotify = "Lnet/minecraft/client/renderer/WorldRenderer;blockChanged(Lnet/minecraft/world/IBlockReader;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/BlockState;Lnet/minecraft/block/BlockState;I)V";
/**
* Inject a callback to call for every random display tick. Used for adding custom particle effects to blocks.
*/
@Inject(method = worldAnimateTick, at = @At(value = "INVOKE", target = blockAnimateTick))
void onAnimateTick(int x, int y, int z, int range, Random random, boolean doBarrier, BlockPos.Mutable pos, CallbackInfo ci) {
Hooks.onRandomDisplayTick((ClientWorld) (Object) this, pos, random);
}
/**
* Inject callback to get notified of client-side blockstate changes.
* Used to invalidate caches in the {@link mods.betterfoliage.chunk.ChunkOverlayManager}
*/
@Redirect(method = worldNotify, at = @At(value = "INVOKE", target = rendererNotify))
void onClientBlockChanged(WorldRenderer renderer, IBlockReader world, BlockPos pos, BlockState oldState, BlockState newState, int flags) {
Hooks.onClientBlockChanged((ClientWorld) world, pos, oldState, newState, flags);
renderer.blockChanged(world, pos, oldState, newState, flags);
}
}

View File

@@ -0,0 +1,44 @@
package mods.betterfoliage.mixin;
import com.mojang.blaze3d.matrix.MatrixStack;
import mods.betterfoliage.model.SpecialRenderModel;
import mods.betterfoliage.render.pipeline.RenderCtxForge;
import net.minecraft.block.BlockState;
import net.minecraft.client.renderer.model.IBakedModel;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.IBlockDisplayReader;
import net.minecraftforge.client.model.data.IModelData;
import net.minecraftforge.client.model.pipeline.ForgeBlockModelRenderer;
import net.minecraftforge.client.model.pipeline.VertexLighterFlat;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import java.util.Random;
@Mixin(ForgeBlockModelRenderer.class)
public class MixinForgeBlockModelRenderer {
private static final String renderModelFlat = "Lnet/minecraftforge/client/model/pipeline/ForgeBlockModelRenderer;renderModelFlat(Lnet/minecraft/world/IBlockDisplayReader;Lnet/minecraft/client/renderer/model/IBakedModel;Lnet/minecraft/block/BlockState;Lnet/minecraft/util/math/BlockPos;Lcom/mojang/blaze3d/matrix/MatrixStack;Lcom/mojang/blaze3d/vertex/IVertexBuilder;ZLjava/util/Random;JILnet/minecraftforge/client/model/data/IModelData;)Z";
private static final String renderModelSmooth = "Lnet/minecraftforge/client/model/pipeline/ForgeBlockModelRenderer;renderModelSmooth(Lnet/minecraft/world/IBlockDisplayReader;Lnet/minecraft/client/renderer/model/IBakedModel;Lnet/minecraft/block/BlockState;Lnet/minecraft/util/math/BlockPos;Lcom/mojang/blaze3d/matrix/MatrixStack;Lcom/mojang/blaze3d/vertex/IVertexBuilder;ZLjava/util/Random;JILnet/minecraftforge/client/model/data/IModelData;)Z";
private static final String render = "Lnet/minecraftforge/client/model/pipeline/ForgeBlockModelRenderer;render(Lnet/minecraftforge/client/model/pipeline/VertexLighterFlat;Lnet/minecraft/world/IBlockDisplayReader;Lnet/minecraft/client/renderer/model/IBakedModel;Lnet/minecraft/block/BlockState;Lnet/minecraft/util/math/BlockPos;Lcom/mojang/blaze3d/matrix/MatrixStack;ZLjava/util/Random;JLnet/minecraftforge/client/model/data/IModelData;)Z";
@Redirect(method = {renderModelFlat, renderModelSmooth}, at = @At(value = "INVOKE", target = render), remap = false)
public boolean render(
VertexLighterFlat lighter,
IBlockDisplayReader world,
IBakedModel model,
BlockState state,
BlockPos pos,
MatrixStack matrixStack,
boolean checkSides,
Random rand,
long seed,
IModelData modelData
) {
if (model instanceof SpecialRenderModel)
return RenderCtxForge.render(lighter, world, (SpecialRenderModel) model, state, pos, matrixStack, checkSides, rand, seed, modelData);
else
return ForgeBlockModelRenderer.render(lighter, world, model, state, pos, matrixStack, checkSides, rand, seed, modelData);
}
}

View File

@@ -0,0 +1,65 @@
package mods.betterfoliage.mixin;
import mods.betterfoliage.render.lighting.ForgeVertexLighter;
import mods.betterfoliage.render.lighting.ForgeVertexLighterAccess;
import net.minecraftforge.client.model.pipeline.VertexLighterFlat;
import org.jetbrains.annotations.NotNull;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(VertexLighterFlat.class)
abstract public class MixinForgeCustomVertexLighting implements ForgeVertexLighter, ForgeVertexLighterAccess {
private static final String processQuad = "Lnet/minecraftforge/client/model/pipeline/VertexLighterFlat;processQuad()V";
private static final String updateLightmap = "Lnet/minecraftforge/client/model/pipeline/VertexLighterFlat;updateLightmap([F[FFFF)V";
private static final String updateColor = "Lnet/minecraftforge/client/model/pipeline/VertexLighterFlat;updateColor([F[FFFFFI)V";
private static final String resetBlockInfo = "Lnet/minecraftforge/client/model/pipeline/VertexLighterFlat;resetBlockInfo()V";
@NotNull
public ForgeVertexLighter vertexLighter = this;
@NotNull
public ForgeVertexLighter getVertexLighter() {
return vertexLighter;
}
public void setVertexLighter(@NotNull ForgeVertexLighter vertexLighter) {
this.vertexLighter = vertexLighter;
}
@Shadow
protected abstract void updateLightmap(float[] normal, float[] lightmap, float x, float y, float z);
@Shadow
protected abstract void updateColor(float[] normal, float[] color, float x, float y, float z, float tint, int multiplier);
@Override
public void updateVertexLightmap(@NotNull float[] normal, @NotNull float[] lightmap, float x, float y, float z) {
updateLightmap(normal, lightmap, x, y, z);
}
@Override
public void updateVertexColor(@NotNull float[] normal, @NotNull float[] color, float x, float y, float z, float tint, int multiplier) {
updateColor(normal, color, x, y, z, tint, multiplier);
}
@Redirect(method = processQuad, at = @At(value = "INVOKE", target = updateColor), remap = false)
void onUpdateColor(VertexLighterFlat self, float[] normal, float[] color, float x, float y, float z, float tint, int multiplier) {
vertexLighter.updateVertexColor(normal, color, x, y, z, tint, multiplier);
}
@Redirect(method = processQuad, at = @At(value = "INVOKE", target = updateLightmap), remap = false)
void onUpdateLightmap(VertexLighterFlat self, float[] normal, float[] lightmap, float x, float y, float z) {
vertexLighter.updateVertexLightmap(normal, lightmap, x, y, z);
}
@Inject(method = resetBlockInfo, at = @At("RETURN"), remap = false)
void onReset(CallbackInfo ci) {
vertexLighter = this;
}
}

View File

@@ -0,0 +1,44 @@
package mods.betterfoliage.mixin;
import mods.betterfoliage.BetterFoliage;
import mods.betterfoliage.BetterFoliageMod;
import mods.betterfoliage.resource.discovery.BakeWrapperManager;
import mods.betterfoliage.resource.discovery.ModelDefinitionsLoadedEvent;
import net.minecraft.client.renderer.model.*;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.profiler.IProfiler;
import net.minecraft.util.ResourceLocation;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.function.Function;
@Mixin(ModelBakery.class)
abstract public class MixinModelBakery {
private static final String processLoading = "Lnet/minecraft/client/renderer/model/ModelBakery;processLoading(Lnet/minecraft/profiler/IProfiler;I)V";
private static final String stitch = "Lnet/minecraft/client/renderer/texture/AtlasTexture;stitch(Lnet/minecraft/resources/IResourceManager;Ljava/util/stream/Stream;Lnet/minecraft/profiler/IProfiler;I)Lnet/minecraft/client/renderer/texture/AtlasTexture$SheetData;";
private static final String profilerSection = "Lnet/minecraft/profiler/IProfiler;popPush(Ljava/lang/String;)V";
private static final String getBakedModel = "Lnet/minecraft/client/renderer/model/ModelBakery;getBakedModel(Lnet/minecraft/util/ResourceLocation;Lnet/minecraft/client/renderer/model/IModelTransform;Ljava/util/function/Function;)Lnet/minecraft/client/renderer/model/IBakedModel;";
private static final String bakeModel = "Lnet/minecraft/client/renderer/model/IUnbakedModel;bake(Lnet/minecraft/client/renderer/model/ModelBakery;Ljava/util/function/Function;Lnet/minecraft/client/renderer/model/IModelTransform;Lnet/minecraft/util/ResourceLocation;)Lnet/minecraft/client/renderer/model/IBakedModel;";
@Inject(method = processLoading, at = @At(value = "INVOKE", target = profilerSection, ordinal = 4))
void onBeforeTextures(IProfiler profiler, int maxMipmapLevel, CallbackInfo ci) {
profiler.popPush("betterfoliage");
BetterFoliageMod.INSTANCE.getBus().post(new ModelDefinitionsLoadedEvent(ModelBakery.class.cast(this)));
}
@Redirect(method = getBakedModel, at = @At(value = "INVOKE", target = bakeModel))
IBakedModel onBakeModel(
IUnbakedModel unbaked,
ModelBakery bakery,
Function<RenderMaterial, TextureAtlasSprite> spriteGetter,
IModelTransform transform,
ResourceLocation locationIn
) {
return BetterFoliage.INSTANCE.getModelManager().onBake(unbaked, bakery, spriteGetter, transform, locationIn);
}
}

View File

@@ -0,0 +1,38 @@
package mods.betterfoliage.mixin;
import mods.betterfoliage.Hooks;
import net.minecraft.block.BlockState;
import net.minecraft.util.Direction;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.shapes.VoxelShape;
import net.minecraft.world.IBlockReader;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Pseudo;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Coerce;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Pseudo
@Mixin(targets = "net.optifine.util.BlockUtils")
public class MixinOptifineBlockUtils {
private static final String shouldSideBeRendered = "shouldSideBeRendered(Lnet/minecraft/block/BlockState;Lnet/minecraft/world/IBlockReader;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/util/Direction;Lnet/optifine/render/RenderEnv;)Z";
private static final String shouldSideBeRenderedCached = "shouldSideBeRenderedCached(Lnet/minecraft/block/BlockState;Lnet/minecraft/world/IBlockReader;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/util/Direction;Lnet/optifine/render/RenderEnv;Lnet/minecraft/block/BlockState;Lnet/minecraft/util/math/BlockPos;)Z";
private static final String getFaceOcclusionShape = "Lnet/minecraft/block/BlockState;getFaceOcclusionShape(Lnet/minecraft/world/IBlockReader;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/util/Direction;)Lnet/minecraft/util/math/shapes/VoxelShape;";
@SuppressWarnings("UnresolvedMixinReference")
@Redirect(method = shouldSideBeRenderedCached, at = @At(value = "INVOKE", target = getFaceOcclusionShape, ordinal = 1))
private static VoxelShape getVoxelShapeOverride(BlockState state, IBlockReader reader, BlockPos pos, Direction dir) {
return Hooks.getVoxelShapeOverride(state, reader, pos, dir);
}
@SuppressWarnings("UnresolvedMixinReference")
@Inject(method = shouldSideBeRendered, at = @At(value = "HEAD"), cancellable = true)
private static void shouldForceSideRender(BlockState state, IBlockReader reader, BlockPos pos, Direction face, @Coerce Object renderEnv, CallbackInfoReturnable<Boolean> cir) {
if (Hooks.shouldForceSideRenderOF(state, reader, pos, face)) {
cir.setReturnValue(true);
}
}
}

View File

@@ -0,0 +1,57 @@
package mods.betterfoliage.mixin;
import com.mojang.blaze3d.matrix.MatrixStack;
import mods.betterfoliage.render.pipeline.RenderCtxBase;
import net.minecraft.block.BlockState;
import net.minecraft.client.renderer.BlockRendererDispatcher;
import net.minecraft.client.renderer.RegionRenderCacheBuilder;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.chunk.ChunkRenderDispatcher;
import net.minecraft.client.renderer.chunk.VisGraph;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.IBlockDisplayReader;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Coerce;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
import java.util.Iterator;
import java.util.Random;
import java.util.Set;
@Mixin(targets = "net.minecraft.client.renderer.chunk.ChunkRenderDispatcher$ChunkRender$RebuildTask")
public class MixinOptifineChunkRendererDispatcher {
private static final String compile = "Lnet/minecraft/client/renderer/chunk/ChunkRenderDispatcher$ChunkRender$RebuildTask;compile(FFFLnet/minecraft/client/renderer/chunk/ChunkRenderDispatcher$CompiledChunk;Lnet/minecraft/client/renderer/RegionRenderCacheBuilder;)Ljava/util/Set;";
private static final String getBlockStateMcp = "Lnet/optifine/override/ChunkCacheOF;getBlockState(Lnet/minecraft/util/math/BlockPos;)Lnet/minecraft/block/BlockState;";
private static final String getBlockStateSrg = "Lnet/optifine/override/ChunkCacheOF;func_180495_p(Lnet/minecraft/util/math/BlockPos;)Lnet/minecraft/block/BlockState;";
@Inject(method = compile, locals = LocalCapture.CAPTURE_FAILHARD, require = 1, at = {
@At(value = "INVOKE", target = getBlockStateMcp),
@At(value = "INVOKE", target = getBlockStateSrg),
})
void onStartBlockRender(
float xIn, float yIn, float zIn,
ChunkRenderDispatcher.CompiledChunk compiledChunkIn, RegionRenderCacheBuilder builderIn,
CallbackInfoReturnable<Set> cir,
int i, BlockPos blockpos, BlockPos blockpos1, VisGraph visgraph, Set set, MatrixStack matrixstack,
@Coerce IBlockDisplayReader chunkrendercache, RenderType[] singleLayer,
boolean shaders, boolean shadersMidBlock, Random random,
BlockRendererDispatcher blockrendererdispatcher, Iterator var18,
@Coerce BlockPos blockpos2
) {
RenderCtxBase.reset(chunkrendercache, blockrendererdispatcher, blockpos2, random);
}
// @Inject(method = compile, locals = LocalCapture.PRINT, require = 1, at = {
// @At(value = "INVOKE", target = getBlockStateMcp),
// @At(value = "INVOKE", target = getBlockStateSrg),
// })
// void printLocals(
// float p_228940_1_, float p_228940_2_, float p_228940_3_,
// ChunkRenderDispatcher.CompiledChunk p_228940_4_, RegionRenderCacheBuilder p_228940_5_,
// CallbackInfoReturnable<BlockState> ci) {
// }
}

View File

@@ -0,0 +1,46 @@
package mods.betterfoliage.mixin;
import com.mojang.blaze3d.matrix.MatrixStack;
import mods.betterfoliage.render.pipeline.RenderCtxBase;
import net.minecraft.client.renderer.BlockRendererDispatcher;
import net.minecraft.client.renderer.RegionRenderCacheBuilder;
import net.minecraft.client.renderer.chunk.ChunkRenderCache;
import net.minecraft.client.renderer.chunk.ChunkRenderDispatcher;
import net.minecraft.client.renderer.chunk.VisGraph;
import net.minecraft.util.math.BlockPos;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
import java.util.Iterator;
import java.util.Random;
import java.util.Set;
@Mixin(targets = "net.minecraft.client.renderer.chunk.ChunkRenderDispatcher$ChunkRender$RebuildTask")
public class MixinVanillaChunkRendererDispatcher {
private static final String compile = "Lnet/minecraft/client/renderer/chunk/ChunkRenderDispatcher$ChunkRender$RebuildTask;compile(FFFLnet/minecraft/client/renderer/chunk/ChunkRenderDispatcher$CompiledChunk;Lnet/minecraft/client/renderer/RegionRenderCacheBuilder;)Ljava/util/Set;";
private static final String getBlockState = "Lnet/minecraft/client/renderer/chunk/ChunkRenderCache;getBlockState(Lnet/minecraft/util/math/BlockPos;)Lnet/minecraft/block/BlockState;";
@Inject(method = compile, at = @At(value = "INVOKE", target = getBlockState), locals = LocalCapture.CAPTURE_FAILHARD)
void onStartBlockRender(
float p_228940_1_, float p_228940_2_, float p_228940_3_,
ChunkRenderDispatcher.CompiledChunk p_228940_4_, RegionRenderCacheBuilder p_228940_5_,
CallbackInfoReturnable ci,
int i, BlockPos blockpos, BlockPos blockpos1, VisGraph visgraph, Set set,
ChunkRenderCache chunkrendercache, MatrixStack matrixstack,
Random random,
BlockRendererDispatcher blockrendererdispatcher, Iterator var15,
BlockPos blockpos2) {
RenderCtxBase.reset(chunkrendercache, blockrendererdispatcher, blockpos2, random);
}
// @Inject(method = compile, at = @At(value = "INVOKE", target = getBlockState), locals = LocalCapture.PRINT)
// void printLocals(
// float p_228940_1_, float p_228940_2_, float p_228940_3_,
// ChunkRenderDispatcher.CompiledChunk p_228940_4_, RegionRenderCacheBuilder p_228940_5_,
// CallbackInfoReturnable ci) {
// }
}

View File

@@ -0,0 +1,101 @@
PARSER_BEGIN(BlockConfigParser)
package mods.betterfoliage.config.match.parser;
import java.util.List;
import java.util.LinkedList;
import mods.betterfoliage.config.match.*;
public class BlockConfigParser {
public String configFile;
ConfigSource getSource(Token t) {
return new ConfigSource(configFile, t.beginLine, t.beginColumn);
}
}
PARSER_END(BlockConfigParser)
// Whitespace definition
SKIP : { " " | "\n" | "\t" | "\r" }
// Single-line comment
SPECIAL_TOKEN : { <lineComment: "//" (~["\n","\r"])* ("\n"|"\r"|"\r\n")> }
// Lexical state for string literal in quotes
SPECIAL_TOKEN : { < quoteStart : "\"" > : withinQuotes }
<withinQuotes> SPECIAL_TOKEN : { < quoteEnd : "\"" > : DEFAULT }
<withinQuotes> TOKEN : { < stringLiteral : (["a"-"z"] | ["0"-"9"] | "/" | "." | "_" | "-" | ":" )* > }
// Symbol tokens
TOKEN : {
< parenStart : "(" > |
< parenEnd : ")" > |
< dot : "." > |
< comma : "," > |
< exclamation : "!" >
}
List<Node.MatchAll> matchFile() : {
Token t; Node n; List<Node.MatchAll> rules = new LinkedList<Node.MatchAll>();
} {
(
t = "match"
{ List<Node> nodes = new LinkedList<Node>(); }
(n = match() { nodes.add(n); })*
"end"
{ rules.add(new Node.MatchAll(getSource(t), nodes)); }
)*
{ return rules; }
}
Node match() : {
Token t; Token t2; MatchMethod mm; List<Node.Value> values; Node.Value v; Node n;
} {
<exclamation> n = match() { return new Node.Negate(n); }
|
t = "block.class." mm = matchMethod() <parenStart> values = matchValueList() <parenEnd>
{ return new Node.MatchValueList(Node.MatchSource.BLOCK_CLASS, mm, getSource(t), values); }
|
t = "block.name." mm = matchMethod() <parenStart> values = matchValueList() <parenEnd>
{ return new Node.MatchValueList(Node.MatchSource.BLOCK_NAME, mm, getSource(t), values); }
|
t = "model." mm = matchMethod() <parenStart> values = matchValueList() <parenEnd>
{ return new Node.MatchValueList(Node.MatchSource.MODEL_LOCATION, mm, getSource(t), values); }
|
t = "isParam" <parenStart> t2 = <stringLiteral> <comma> values = matchValueList() <parenEnd>
{ return new Node.MatchParam(t2.image, values, getSource(t)); }
|
t = "setParam" <parenStart> t2 = <stringLiteral> <comma> v = matchValue() <parenEnd>
{ return new Node.SetParam(t2.image, v, getSource(t)); }
}
MatchMethod matchMethod() : {} {
"matches" { return MatchMethod.EXACT_MATCH; } |
"extends" { return MatchMethod.EXTENDS; } |
"contains" { return MatchMethod.CONTAINS; }
}
List<Node.Value> matchValueList() : {
List<Node.Value> values = new LinkedList<Node.Value>();
Node.Value v;
} {
v = matchValue() { values.add(v); }
(<comma> v = matchValue() { values.add(v); } )*
{ return values; }
}
Node.Value matchValue() : {
Token t;
} {
t = <stringLiteral>
{ return new Node.Value.Literal(getSource(t), t.image); }
|
"classOf" <parenStart> t = <stringLiteral> <parenEnd>
{ return new Node.Value.ClassOf(getSource(t), t.image); }
|
"model.texture" <parenStart> t = <stringLiteral> <parenEnd>
{ return new Node.Value.Texture(getSource(t), t.image); }
|
"model.tint" <parenStart> t = <stringLiteral> <parenEnd>
{ return new Node.Value.Tint(getSource(t), t.image); }
}

View File

@@ -0,0 +1,107 @@
package mods.betterfoliage
import mods.betterfoliage.chunk.ChunkOverlayManager
import mods.betterfoliage.config.BlockConfig
import mods.betterfoliage.integration.OptifineCustomColors
import mods.betterfoliage.integration.ShadersModIntegration
import mods.betterfoliage.render.block.vanilla.RoundLogOverlayLayer
import mods.betterfoliage.render.block.vanilla.StandardCactusDiscovery
import mods.betterfoliage.render.block.vanilla.StandardCactusModel
import mods.betterfoliage.render.block.vanilla.StandardDirtDiscovery
import mods.betterfoliage.render.block.vanilla.StandardDirtModel
import mods.betterfoliage.render.block.vanilla.StandardGrassDiscovery
import mods.betterfoliage.render.block.vanilla.StandardGrassModel
import mods.betterfoliage.render.block.vanilla.StandardLeafDiscovery
import mods.betterfoliage.render.block.vanilla.StandardLeafModel
import mods.betterfoliage.render.block.vanilla.StandardLilypadDiscovery
import mods.betterfoliage.render.block.vanilla.StandardLilypadModel
import mods.betterfoliage.render.block.vanilla.StandardMyceliumDiscovery
import mods.betterfoliage.render.block.vanilla.StandardMyceliumModel
import mods.betterfoliage.render.block.vanilla.StandardNetherrackDiscovery
import mods.betterfoliage.render.block.vanilla.StandardNetherrackModel
import mods.betterfoliage.render.block.vanilla.StandardRoundLogDiscovery
import mods.betterfoliage.render.block.vanilla.StandardRoundLogModel
import mods.betterfoliage.render.block.vanilla.StandardSandDiscovery
import mods.betterfoliage.render.block.vanilla.StandardSandModel
import mods.betterfoliage.render.lighting.AoSideHelper
import mods.betterfoliage.render.particle.LeafParticleRegistry
import mods.betterfoliage.render.particle.LeafWindTracker
import mods.betterfoliage.render.particle.RisingSoulParticle
import mods.betterfoliage.resource.discovery.BakeWrapperManager
import mods.betterfoliage.resource.discovery.BlockTypeCache
import mods.betterfoliage.resource.discovery.RuleBasedDiscovery
import mods.betterfoliage.resource.generated.GeneratedTexturePack
import mods.betterfoliage.util.resourceManager
import net.minecraft.block.BlockState
/**
* Object responsible for initializing (and holding a reference to) all the infrastructure of the mod
* except for the call hooks.
*/
object BetterFoliage {
/** Resource pack holding generated assets */
val generatedPack = GeneratedTexturePack("bf_gen", "Better Foliage generated assets")
/** List of recognized [BlockState]s */
var blockTypes = BlockTypeCache()
val blockConfig = BlockConfig()
val standardModelSupport = RuleBasedDiscovery().apply {
discoverers["cactus"] = StandardCactusDiscovery
discoverers["dirt"] = StandardDirtDiscovery
discoverers["grass"] = StandardGrassDiscovery
discoverers["leaf"] = StandardLeafDiscovery
discoverers["lilypad"] = StandardLilypadDiscovery
discoverers["mycelium"] = StandardMyceliumDiscovery
discoverers["netherrack"] = StandardNetherrackDiscovery
discoverers["round-log"] = StandardRoundLogDiscovery
discoverers["sand"] = StandardSandDiscovery
}
val modelManager = BakeWrapperManager().apply {
discoverers.add(standardModelSupport)
}
fun init() {
// discoverers
BetterFoliageMod.bus.register(modelManager)
BetterFoliageMod.bus.register(LeafParticleRegistry)
resourceManager.registerReloadListener(LeafParticleRegistry)
ChunkOverlayManager.layers.add(RoundLogOverlayLayer)
listOf(
StandardLeafDiscovery,
StandardSandDiscovery,
StandardRoundLogDiscovery,
).forEach {
}
// init singletons
val singletons = listOf(
AoSideHelper,
ChunkOverlayManager,
LeafWindTracker
)
val modelSingletons = listOf(
StandardLeafModel.Companion,
StandardGrassModel.Companion,
StandardDirtModel.Companion,
StandardMyceliumModel.Companion,
StandardSandModel.Companion,
StandardLilypadModel.Companion,
StandardCactusModel.Companion,
StandardNetherrackModel.Companion,
StandardRoundLogModel.Companion,
RisingSoulParticle.Companion
)
// init mod integrations
val integrations = listOf(
ShadersModIntegration,
OptifineCustomColors
)
}
}

View File

@@ -0,0 +1,76 @@
package mods.betterfoliage
import mods.betterfoliage.config.MainConfig
import mods.betterfoliage.config.clothGuiRoot
import mods.betterfoliage.config.forgeSpecRoot
import mods.betterfoliage.util.tryDefault
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.screen.Screen
import net.minecraft.util.ResourceLocation
import net.minecraftforge.fml.ExtensionPoint.CONFIGGUIFACTORY
import net.minecraftforge.fml.ExtensionPoint.DISPLAYTEST
import net.minecraftforge.fml.ModLoadingContext
import net.minecraftforge.fml.common.Mod
import net.minecraftforge.fml.config.ModConfig
import org.apache.commons.lang3.tuple.Pair
import org.apache.logging.log4j.Level
import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.simple.SimpleLogger
import org.apache.logging.log4j.util.PropertiesUtil
import thedarkcolour.kotlinforforge.forge.MOD_BUS
import java.io.File
import java.io.PrintStream
import java.util.Properties
import java.util.function.BiFunction
import java.util.function.BiPredicate
import java.util.function.Supplier
@Mod(BetterFoliageMod.MOD_ID)
object BetterFoliageMod {
const val MOD_ID = "betterfoliage"
val bus = MOD_BUS
val config = MainConfig()
val detailLogStream = PrintStream(File("logs/betterfoliage.log").apply {
parentFile.mkdirs()
if (!exists()) createNewFile()
})
fun logger(obj: Any) = LogManager.getLogger(obj)
fun detailLogger(obj: Any) = SimpleLogger(
obj::class.java.simpleName, Level.DEBUG, false, true, true, false, "yyyy-MM-dd HH:mm:ss", null, PropertiesUtil(Properties()), detailLogStream
)
init {
val ctx = ModLoadingContext.get()
val configSpec = config.forgeSpecRoot()
ctx.registerConfig(ModConfig.Type.CLIENT, configSpec)
// Add config GUI extension if Cloth Config is available
val clothLoaded = tryDefault(false) { Class.forName("me.shedaniel.clothconfig2.forge.api.ConfigBuilder"); true }
if (clothLoaded) {
logger(this).log(Level.INFO, "Cloth Config found, registering GUI")
ctx.registerExtensionPoint(CONFIGGUIFACTORY) { BiFunction<Minecraft, Screen, Screen> { client, parent ->
config.clothGuiRoot(
parentScreen = parent,
prefix = listOf(MOD_ID),
background = ResourceLocation("minecraft:textures/block/spruce_log.png"),
saveAction = { configSpec.save() }
)
} }
}
// Accept-all version tester (we are client-only)
ctx.registerExtensionPoint(DISPLAYTEST) {
Pair.of(
Supplier { "Honk if you see this!" },
BiPredicate<String, Boolean> { _, _ -> true }
)
}
Minecraft.getInstance().resourcePackRepository.addPackFinder(BetterFoliage.generatedPack.finder)
BetterFoliage.init()
}
}

View File

@@ -0,0 +1,106 @@
package mods.betterfoliage
import mods.betterfoliage.util.ClassRef
import mods.betterfoliage.util.ClassRef.Companion.float
import mods.betterfoliage.util.ClassRef.Companion.void
import mods.betterfoliage.util.FieldRef
import mods.betterfoliage.util.MethodRef
import net.minecraft.block.Block
import net.minecraft.block.BlockState
import net.minecraft.client.renderer.BlockModelRenderer
import net.minecraft.client.renderer.BlockRendererDispatcher
import net.minecraft.client.renderer.BufferBuilder
import net.minecraft.client.renderer.RenderType
import net.minecraft.client.renderer.chunk.ChunkRenderCache
import net.minecraft.client.renderer.model.BakedQuad
import net.minecraft.client.renderer.model.IUnbakedModel
import net.minecraft.client.renderer.model.ModelBakery
import net.minecraft.client.renderer.texture.TextureAtlasSprite
import net.minecraft.util.ResourceLocation
import net.minecraft.util.math.BlockPos
import net.minecraft.world.IBlockDisplayReader
import net.minecraft.world.IBlockReader
import net.minecraftforge.client.model.pipeline.BlockInfo
import net.minecraftforge.client.model.pipeline.VertexLighterFlat
import net.minecraftforge.registries.IRegistryDelegate
import java.util.Random
import java.util.function.Predicate
typealias Sprite = TextureAtlasSprite
// Java
val String = ClassRef<String>("java.lang.String")
val List = ClassRef<List<*>>("java.util.List")
val Random = ClassRef<Random>("java.util.Random")
fun <K, V> mapRef() = ClassRef<Map<K, V>>("java.util.Map")
fun <K, V> mapRefMutable() = ClassRef<MutableMap<K, V>>("java.util.Map")
// Minecraft
val IBlockReader = ClassRef<IBlockReader>("net.minecraft.world.IBlockReader")
val ILightReader = ClassRef<IBlockDisplayReader>("net.minecraft.world.IBlockDisplayReader")
val BlockState = ClassRef<BlockState>("net.minecraft.block.BlockState")
val BlockPos = ClassRef<BlockPos>("net.minecraft.util.math.BlockPos")
val Block = ClassRef<Block>("net.minecraft.block.Block")
val TextureAtlasSprite = ClassRef<TextureAtlasSprite>("net.minecraft.client.renderer.texture.TextureAtlasSprite")
val BufferBuilder = ClassRef<BufferBuilder>("net.minecraft.client.renderer.BufferBuilder")
val BufferBuilder_setSprite = MethodRef(BufferBuilder, "setSprite", void, TextureAtlasSprite)
val BufferBuilder_sVertexBuilder = FieldRef(BufferBuilder, "sVertexBuilder", SVertexBuilder)
val BlockRendererDispatcher = ClassRef<BlockRendererDispatcher>("net.minecraft.client.renderer.BlockRendererDispatcher")
val ChunkRenderCache = ClassRef<ChunkRenderCache>("net.minecraft.client.renderer.chunk.ChunkRenderCache")
val ResourceLocation = ClassRef<ResourceLocation>("net.minecraft.util.ResourceLocation")
val BakedQuad = ClassRef<BakedQuad>("net.minecraft.client.renderer.model.BakedQuad")
val BlockModelRenderer = ClassRef<BlockModelRenderer>("net.minecraft.client.renderer.BlockModelRenderer")
val VertexLighterFlat = ClassRef<VertexLighterFlat>("net.minecraftforge.client.model.pipeline.VertexLighterFlat")
val BlockInfo = ClassRef<BlockInfo>("net.minecraftforge.client.model.pipeline.BlockInfo")
val VertexLighterFlat_blockInfo = FieldRef(VertexLighterFlat, "blockInfo", BlockInfo)
val BlockInfo_shx = FieldRef(BlockInfo, "shx", float)
val BlockInfo_shy = FieldRef(BlockInfo, "shy", float)
val BlockInfo_shz = FieldRef(BlockInfo, "shz", float)
object ModelBakery : ClassRef<ModelBakery>("net.minecraft.client.renderer.model.ModelBakery") {
val unbakedModels = FieldRef(this, "unbakedModels", mapRefMutable<ResourceLocation, IUnbakedModel>())
val topUnbakedModels = FieldRef(this, "topUnbakedModels", mapRefMutable<ResourceLocation, IUnbakedModel>())
}
object RenderTypeLookup : ClassRef<RenderTypeLookup>("net.minecraft.client.renderer.RenderTypeLookup") {
val blockRenderChecks = FieldRef(this, "blockRenderChecks", mapRefMutable<IRegistryDelegate<Block>, Predicate<RenderType>>())
}
// Optifine
val OptifineClassTransformer = ClassRef<Any>("optifine.OptiFineClassTransformer")
val BlockPosM = ClassRef<Any>("net.optifine.BlockPosM")
object ChunkCacheOF : ClassRef<Any>("net.optifine.override.ChunkCacheOF") {
val chunkCache = FieldRef(this, "chunkCache", ChunkRenderCache)
}
object RenderEnv : ClassRef<Any>("net.optifine.render.RenderEnv") {
val reset = MethodRef(this, "reset", void, BlockState, BlockPos)
}
// Optifine custom colors
val IColorizer = ClassRef<Any>("net.optifine.CustomColors\$IColorizer")
object CustomColors : ClassRef<Any>("net.optifine.CustomColors") {
val getColorMultiplier = MethodRef(this, "getColorMultiplier", int, BakedQuad, BlockState, ILightReader, BlockPos, RenderEnv)
}
// Optifine shaders
object Shaders : ClassRef<Any>("net.optifine.shaders.Shaders") {
val shaderPackLoaded = FieldRef(this, "shaderPackLoaded", boolean)
val blockLightLevel05 = FieldRef(this, "blockLightLevel05", float)
val blockLightLevel06 = FieldRef(this, "blockLightLevel06", float)
val blockLightLevel08 = FieldRef(this, "blockLightLevel08", float)
}
object SVertexBuilder : ClassRef<Any>("net.optifine.shaders.SVertexBuilder") {
val pushState = MethodRef(this, "pushEntity", void, long)
val popState = MethodRef(this, "popEntity", void)
}
object BlockAliases : ClassRef<Any>("net.optifine.shaders.BlockAliases") {
val getAliasBlockId = MethodRef(this, "getAliasBlockId", int, BlockState)
}

View File

@@ -0,0 +1,8 @@
package mods.betterfoliage
import net.minecraft.client.renderer.model.ModelBakery
import net.minecraft.client.renderer.texture.AtlasTexture
import net.minecraft.resources.IResourceManager
import net.minecraft.util.ResourceLocation
import net.minecraftforge.eventbus.api.Event

View File

@@ -0,0 +1,77 @@
@file:JvmName("Hooks")
package mods.betterfoliage
import mods.betterfoliage.chunk.ChunkOverlayManager
import mods.betterfoliage.config.Config
import mods.betterfoliage.model.SpecialRenderModel
import mods.betterfoliage.model.WeightedModelWrapper
import mods.betterfoliage.render.block.vanilla.RoundLogKey
import mods.betterfoliage.render.particle.FallingLeafParticle
import mods.betterfoliage.render.particle.LeafBlockModel
import mods.betterfoliage.render.particle.RisingSoulParticle
import net.minecraft.block.Block
import net.minecraft.block.BlockState
import net.minecraft.block.Blocks
import net.minecraft.client.Minecraft
import net.minecraft.client.world.ClientWorld
import net.minecraft.util.Direction
import net.minecraft.util.Direction.DOWN
import net.minecraft.util.Direction.UP
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.shapes.VoxelShape
import net.minecraft.util.math.shapes.VoxelShapes
import net.minecraft.world.IBlockDisplayReader
import net.minecraft.world.IBlockReader
import net.minecraft.world.World
import java.util.Random
fun getAmbientOcclusionLightValueOverride(original: Float, state: BlockState): Float {
if (Config.enabled && Config.roundLogs.enabled && BetterFoliage.blockTypes.hasTyped<RoundLogKey>(state))
return Config.roundLogs.dimming.toFloat()
return original
}
fun onClientBlockChanged(worldClient: ClientWorld, pos: BlockPos, oldState: BlockState, newState: BlockState, flags: Int) {
ChunkOverlayManager.onBlockChange(worldClient, pos)
}
fun onRandomDisplayTick(world: ClientWorld, pos: BlockPos, random: Random) {
val state = world.getBlockState(pos)
if (Config.enabled &&
Config.risingSoul.enabled &&
state.block == Blocks.SOUL_SAND &&
world.getBlockState(pos.relative(UP)).isAir &&
Math.random() < Config.risingSoul.chance) {
RisingSoulParticle(world, pos).addIfValid()
}
if (Config.enabled &&
Config.fallingLeaves.enabled &&
random.nextDouble() < Config.fallingLeaves.chance &&
world.getBlockState(pos.relative(DOWN)).isAir
) {
(getActualRenderModel(world, pos, state, random) as? LeafBlockModel)?.let { leafModel ->
val blockColor = Minecraft.getInstance().blockColors.getColor(state, world, pos, 0)
FallingLeafParticle(world, pos, leafModel.key, blockColor, random).addIfValid()
}
}
}
fun getVoxelShapeOverride(state: BlockState, reader: IBlockReader, pos: BlockPos, dir: Direction): VoxelShape {
if (Config.enabled && Config.roundLogs.enabled && BetterFoliage.blockTypes.hasTyped<RoundLogKey>(state))
return VoxelShapes.empty()
return state.getFaceOcclusionShape(reader, pos, dir)
}
fun shouldForceSideRenderOF(state: BlockState, world: IBlockReader, pos: BlockPos, face: Direction) =
world.getBlockState(pos.relative(face)).let { neighbor -> BetterFoliage.blockTypes.hasTyped<RoundLogKey>(neighbor) }
fun getActualRenderModel(world: IBlockDisplayReader, pos: BlockPos, state: BlockState, random: Random): SpecialRenderModel? {
val model = Minecraft.getInstance().blockRenderer.blockModelShaper.getBlockModel(state) as? SpecialRenderModel
?: return null
if (model is WeightedModelWrapper) {
random.setSeed(state.getSeed(pos))
return model.getModel(random).model
}
return model
}

View File

@@ -0,0 +1,62 @@
package mods.betterfoliage.chunk
import mods.betterfoliage.util.Int3
import mods.betterfoliage.util.offset
import mods.betterfoliage.util.plus
import mods.betterfoliage.util.semiRandom
import net.minecraft.block.Block
import net.minecraft.block.BlockState
import net.minecraft.client.renderer.chunk.ChunkRenderCache
import net.minecraft.util.Direction
import net.minecraft.util.math.BlockPos
import net.minecraft.world.IBlockDisplayReader
import net.minecraft.world.IWorldReader
import net.minecraft.world.biome.Biome
import net.minecraft.world.level.ColorResolver
/**
* Represents the block being rendered. Has properties and methods to query the neighborhood of the block in
* block-relative coordinates.
*/
interface BlockCtx {
val world: IBlockDisplayReader
val pos: BlockPos
val seed: Long get() = state.getSeed(pos)
fun offset(dir: Direction) = offset(dir.offset)
fun offset(offset: Int3): BlockCtx
val state: BlockState get() = world.getBlockState(pos)
fun state(offset: Int3) = world.getBlockState(pos + offset)
fun state(dir: Direction) = state(dir.offset)
fun isAir(offset: Int3) = (pos + offset).let { world.getBlockState(it).isAir(world, it) }
fun isAir(dir: Direction) = isAir(dir.offset)
val biome: Biome? get() =
(world as? IWorldReader)?.getBiome(pos) ?:
(world as? ChunkRenderCache)?.level?.getBiome(pos)
val isFullBlock: Boolean get() = state.isCollisionShapeFullBlock(world, pos)
fun isNeighborSturdy(dir: Direction) = offset(dir).let { it.state.isFaceSturdy(it.world, it.pos, dir.opposite) }
fun shouldSideBeRendered(side: Direction) = Block.shouldRenderFace(state, world, pos, side)
/** Get a semi-random value based on the block coordinate and the given seed. */
fun semiRandom(seed: Int) = pos.semiRandom(seed)
/** Get an array of semi-random values based on the block coordinate. */
fun semiRandomArray(num: Int): Array<Int> = Array(num) { semiRandom(it) }
fun color(resolver: ColorResolver) = world.getBlockTint(pos, resolver)
}
class BasicBlockCtx(
override val world: IBlockDisplayReader,
override val pos: BlockPos
) : BlockCtx {
override val state: BlockState = world.getBlockState(pos)
override fun offset(offset: Int3) = BasicBlockCtx(world, pos + offset)
}

View File

@@ -0,0 +1,95 @@
package mods.betterfoliage.chunk
import mods.betterfoliage.util.get
import mods.betterfoliage.util.isInstance
import mods.betterfoliage.ChunkCacheOF
import net.minecraft.client.renderer.chunk.ChunkRenderCache
import net.minecraft.client.world.ClientWorld
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.ChunkPos
import net.minecraft.world.DimensionType
import net.minecraft.world.IBlockDisplayReader
import net.minecraft.world.IWorldReader
import net.minecraftforge.common.MinecraftForge
import net.minecraftforge.event.world.ChunkEvent
import net.minecraftforge.event.world.WorldEvent
import net.minecraftforge.eventbus.api.SubscribeEvent
import java.util.*
import kotlin.collections.set
val IBlockDisplayReader.dimType: DimensionType get() = when {
this is IWorldReader -> dimensionType()
this is ChunkRenderCache -> level.dimensionType()
this.isInstance(ChunkCacheOF) -> this[ChunkCacheOF.chunkCache].level.dimensionType()
else -> throw IllegalArgumentException("DimensionType of world with class ${this::class.qualifiedName} cannot be determined!")
}
/**
* Represents some form of arbitrary non-persistent data that can be calculated and cached for each block position
*/
abstract class ChunkOverlayLayer<T> {
val dimData = IdentityHashMap<DimensionType, SparseChunkedMap<T>>()
abstract fun calculate(ctx: BlockCtx): T
abstract fun onBlockUpdate(world: IBlockDisplayReader, pos: BlockPos)
operator fun get(ctx: BlockCtx): T {
return dimData
.getOrPut(ctx.world.dimType) { SparseChunkedMap() }
.getOrPut(ctx.pos) { calculate(ctx) }
}
fun remove(world: IBlockDisplayReader, pos: BlockPos) {
dimData[world.dimType]?.remove(pos)
}
}
/**
* Event forwarder for multiple layers of chunk overlay data.
*/
object ChunkOverlayManager {
init { MinecraftForge.EVENT_BUS.register(this) }
val layers = mutableListOf<ChunkOverlayLayer<*>>()
fun onBlockChange(world: ClientWorld, pos: BlockPos) {
layers.forEach { layer -> layer.onBlockUpdate(world, pos) }
}
@SubscribeEvent
fun handleUnloadWorld(event: WorldEvent.Unload) = (event.world as? ClientWorld)?.let { world ->
layers.forEach { layer -> layer.dimData.remove(world.dimType) }
}
@SubscribeEvent
fun handleUnloadChunk(event: ChunkEvent.Unload) = (event.world as? ClientWorld)?.let { world ->
layers.forEach { layer -> layer.dimData[world.dimType]?.removeChunk(event.chunk.pos) }
}
}
interface DoubleMap<K1, K2, V> {
val map1: MutableMap<K1, MutableMap<K2, V>>
fun createMap2(): MutableMap<K2, V>
fun remove(key1: K1) {
map1.remove(key1)
}
fun remove(key1: K1, key2: K2) {
map1[key1]?.remove(key2)
}
fun contains(key1: K1) = map1.contains(key1)
fun getOrSet(key1: K1, key2: K2, factory: () -> V) =
(map1[key1] ?: createMap2().apply { map1[key1] = this }).let { subMap ->
subMap[key2] ?: factory().apply { subMap[key2] = this }
}
}
class SparseChunkedMap<V> {
val map = object : DoubleMap<ChunkPos, BlockPos, V> {
override val map1 = mutableMapOf<ChunkPos, MutableMap<BlockPos, V>>()
override fun createMap2() = mutableMapOf<BlockPos, V>()
}
fun getOrPut(pos: BlockPos, factory: () -> V) = map.getOrSet(ChunkPos(pos), pos, factory)
fun remove(pos: BlockPos) = map.remove(ChunkPos(pos), pos)
fun removeChunk(pos: ChunkPos) = map.map1.remove(pos)
}

View File

@@ -0,0 +1,40 @@
package mods.betterfoliage.config
import mods.betterfoliage.config.match.Node
import mods.betterfoliage.config.match.parser.BlockConfigParser
import mods.betterfoliage.config.match.parser.ParseException
import mods.betterfoliage.config.match.parser.TokenMgrError
import mods.betterfoliage.util.HasLogger
import mods.betterfoliage.util.stripStart
import net.minecraft.resources.IResourceManager
import net.minecraft.util.ResourceLocation
import org.apache.logging.log4j.Level
import org.apache.logging.log4j.Level.ERROR
class BlockConfig : HasLogger() {
lateinit var rules: List<Node.MatchAll>
fun readConfig(manager: IResourceManager) {
val configs = manager.listResources("config/betterfoliage") { it.endsWith(".rules") }
rules = configs.flatMap { configLocation ->
val resource = manager.getResource(configLocation)
val parser = BlockConfigParser(resource.inputStream)
.apply { configFile = configLocation.stripStart("config/betterfoliage/").toString() }
try {
parser.matchFile()
} catch (e: ParseException) {
parseError(e, configLocation)
} catch (e: TokenMgrError) {
parseError(e, configLocation)
}
}
}
fun parseError(e: Throwable, location: ResourceLocation): List<Node.MatchAll> {
"Error parsing block config $location: ${e.message}".let {
logger.log(ERROR, it)
detailLogger.log(ERROR, it)
}
return emptyList()
}
}

View File

@@ -0,0 +1,170 @@
package mods.betterfoliage.config
import me.shedaniel.clothconfig2.forge.api.AbstractConfigListEntry
import me.shedaniel.clothconfig2.forge.api.ConfigBuilder
import me.shedaniel.clothconfig2.forge.api.ConfigEntryBuilder
import me.shedaniel.clothconfig2.forge.gui.entries.SubCategoryListEntry
import mods.betterfoliage.util.asText
import net.minecraft.client.gui.screen.Screen
import net.minecraft.client.resources.I18n
import net.minecraft.util.ResourceLocation
import net.minecraftforge.common.ForgeConfigSpec
import java.util.Optional
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
const val MAX_LINE_LEN = 30
fun DelegatingConfigGroup.forgeSpecRoot() =
ForgeConfigSpec.Builder()
.also { createForgeNode(it) }
.build()
fun DelegatingConfigGroup.clothGuiRoot(
parentScreen: Screen,
prefix: List<String>,
background: ResourceLocation,
saveAction: ()->Unit
) = ConfigBuilder.create()
.setParentScreen(parentScreen)
.setTitle(I18n.get((prefix + "title").joinToString(".")).asText())
.setDefaultBackgroundTexture(background)
.setSavingRunnable(saveAction)
.also { builder ->
createClothNode(prefix).value.forEach { rootCategory ->
builder.getOrCreateCategory("main".asText()).addEntry(rootCategory)
}
}
.build()
sealed class DelegatingConfigNode {
abstract fun createClothNode(path: List<String>): AbstractConfigListEntry<*>
}
abstract class DelegatingConfigValue<T> : DelegatingConfigNode(), ReadOnlyProperty<DelegatingConfigGroup, T> {
lateinit var forgeValue: ForgeConfigSpec.ConfigValue<T>
abstract fun createForgeNode(builder: ForgeConfigSpec.Builder, name: String)
}
open class DelegatingConfigGroup : DelegatingConfigNode() {
val children = mutableMapOf<String, DelegatingConfigNode>()
fun createForgeNode(builder: ForgeConfigSpec.Builder) {
children.forEach { (name, node) ->
when(node) {
is DelegatingConfigGroup -> {
builder.push(name)
node.createForgeNode(builder)
builder.pop()
}
is DelegatingConfigValue<*> -> node.createForgeNode(builder, name)
}
}
}
override fun createClothNode(path: List<String>): SubCategoryListEntry {
val builder = ConfigEntryBuilder.create()
.startSubCategory(path.joinToString(".").translate())
.setTooltip(*path.joinToString(".").translateTooltip())
.setExpanded(false)
children.forEach { (name, node) -> builder.add(node.createClothNode(path + name)) }
return builder.build()
}
}
interface DelegatingConfigGroupFactory<T> {
operator fun provideDelegate(parent: DelegatingConfigGroup, property: KProperty<*>): ReadOnlyProperty<DelegatingConfigGroup, T>
}
fun <T: DelegatingConfigGroup> subNode(factory: ()->T) = object : DelegatingConfigGroupFactory<T> {
override operator fun provideDelegate(parent: DelegatingConfigGroup, property: KProperty<*>): ReadOnlyProperty<DelegatingConfigGroup, T> {
val child = factory()
parent.children[property.name] = child
return ReadOnlyProperty { _, _ -> child }
}
}
interface DelegatingConfigValueFactory<T> {
fun createForgeNode(builder: ForgeConfigSpec.Builder, name: String): ForgeConfigSpec.ConfigValue<T>
fun createClothNode(prop: CachingConfigProperty<T>, path: List<String>): AbstractConfigListEntry<T>
operator fun provideDelegate(parent: DelegatingConfigGroup, property: KProperty<*>): ReadOnlyProperty<DelegatingConfigGroup, T> {
return object : CachingConfigProperty<T>(parent, property) {
override fun createForgeNode(builder: ForgeConfigSpec.Builder, name: String) {
forgeValue = this@DelegatingConfigValueFactory.createForgeNode(builder, name)
}
override fun createClothNode(path: List<String>): AbstractConfigListEntry<*> = createClothNode(this, path)
}.apply { parent.children[property.name] = this }
}
}
abstract class CachingConfigProperty<T>(parent: DelegatingConfigGroup, property: KProperty<*>) : DelegatingConfigValue<T>() {
var value: T? = null
override fun getValue(thisRef: DelegatingConfigGroup, property: KProperty<*>) =
value ?: forgeValue.get().apply { value = this }
}
fun String.translate() = I18n.get(this).asText()
fun String.translateTooltip(lineLength: Int = MAX_LINE_LEN) =
I18n.get("$this.tooltip").splitToSequence(" ").fold(mutableListOf("")) { tooltips, word ->
if (tooltips.last().length + word.length < lineLength) {
tooltips[tooltips.lastIndex] += "$word "
} else {
tooltips.add("$word ")
}
tooltips
}.map { it.trim().asText() }.toTypedArray()
fun boolean(
default: Boolean,
langKey: (List<String>)->String = { it.joinToString(".") },
valueOverride: (Boolean)->Boolean = { it }
) = object : DelegatingConfigValueFactory<Boolean> {
override fun createForgeNode(builder: ForgeConfigSpec.Builder, name: String) =
builder.define(name, default)
override fun createClothNode(prop: CachingConfigProperty<Boolean>, path: List<String>) = ConfigEntryBuilder.create()
.startBooleanToggle(langKey(path).translate(), prop.forgeValue.get())
.setTooltip(langKey(path).let { if (I18n.exists("$it.tooltip")) Optional.of(it.translateTooltip()) else Optional.empty() })
.setSaveConsumer { prop.forgeValue.set(valueOverride(it)); prop.value = null }
.build()
}
fun integer(
default: Int = 0, min: Int = 0, max: Int,
langKey: (List<String>)->String = { it.joinToString(".") },
valueOverride: (Int)->Int = { it }
) = object : DelegatingConfigValueFactory<Int> {
override fun createForgeNode(builder: ForgeConfigSpec.Builder, name: String) =
builder.defineInRange(name, default, min, max)
override fun createClothNode(prop: CachingConfigProperty<Int>, path: List<String>) = ConfigEntryBuilder.create()
.startIntField(langKey(path).translate(), prop.forgeValue.get())
.setTooltip(langKey(path).let { if (I18n.exists("$it.tooltip")) Optional.of(it.translateTooltip()) else Optional.empty() })
.setMin(min).setMax(max)
.setSaveConsumer { prop.forgeValue.set(valueOverride(it)); prop.value = null }
.build()
}
fun double(
default: Double = 0.0, min: Double = 0.0, max: Double = 1.0,
langKey: (List<String>)->String = { it.joinToString(".") },
valueOverride: (Double)->Double = { it }
) = object : DelegatingConfigValueFactory<Double> {
override fun createForgeNode(builder: ForgeConfigSpec.Builder, name: String) =
builder.defineInRange(name, default, min, max)
override fun createClothNode(prop: CachingConfigProperty<Double>, path: List<String>) = ConfigEntryBuilder.create()
.startDoubleField(langKey(path).translate(), prop.forgeValue.get())
.setTooltip(langKey(path).let { if (I18n.exists("$it.tooltip")) Optional.of(it.translateTooltip()) else Optional.empty() })
.setMin(min).setMax(max)
.setSaveConsumer { prop.forgeValue.set(valueOverride(it)); prop.value = null }
.build()
}
val recurring = { path: List<String> -> "${path.first()}.${path.last()}" }
fun fakeCategory(name: String) = { names: List<String> ->
(listOf(names.first(), name) + names.drop(1)).joinToString(".")
}

View File

@@ -0,0 +1,158 @@
package mods.betterfoliage.config
import mods.betterfoliage.BetterFoliageMod
import java.util.Random
fun featureEnable(default: Boolean = true) = boolean(default, langKey = recurring)
val Config get() = BetterFoliageMod.config
abstract class PopulationConfigGroup : DelegatingConfigGroup() {
abstract val enabled: Boolean
abstract val population: Int
fun enabled(random: Random) = random.nextInt(64) < population && enabled
}
class MainConfig : DelegatingConfigGroup() {
val enabled by boolean(true, langKey = { "betterfoliage.global.enabled" })
val nVidia by boolean(false)
val leaves by subNode(::LeavesConfig)
val shortGrass by subNode(::ShortGrassConfig)
val connectedGrass by subNode(::ConnectedGrassConfig)
val roundLogs by subNode(::RoundLogConfig)
val cactus by subNode(::CactusConfig)
val lilypad by subNode(::LilypadConfig)
val reed by subNode(::ReedConfig)
val algae by subNode(::AlgaeConfig)
val coral by subNode(::CoralConfig)
val netherrack by subNode(::NetherrackConfig)
val fallingLeaves by subNode(::FallingLeavesConfig)
val risingSoul by subNode(::RisingSoulConfig)
}
class LeavesConfig : DelegatingConfigGroup() {
val enabled by featureEnable()
val snowEnabled by boolean(true)
val hOffset by double(max=0.4, default=0.2, langKey = recurring)
val vOffset by double(max=0.4, default=0.1, langKey = recurring)
val size by double(min=0.75, max=2.5, default=1.4, langKey = recurring)
val dense by boolean(false)
val hideInternal by boolean(true)
val saturationThreshold by double(default=0.1, langKey = recurring)
}
class ShortGrassConfig : PopulationConfigGroup() {
override val enabled by featureEnable()
val grassEnabled by boolean(true)
val myceliumEnabled by boolean(true)
val snowEnabled by boolean(true)
val hOffset by double(max=0.4, default=0.2, langKey = recurring)
val heightMin by double(min=0.1, max=2.5, default=0.6, langKey = recurring)
val heightMax by double(min=0.1, max=2.5, default=0.8, langKey = recurring)
val size by double(min=0.5, max=1.5, default=1.0, langKey = recurring)
override val population by integer(max=64, default=64, langKey = recurring)
val useGenerated by boolean(false)
val shaderWind by boolean(true, langKey = recurring)
val saturationThreshold by double(default=0.1, langKey = recurring)
}
class ConnectedGrassConfig : DelegatingConfigGroup() {
val enabled by boolean(true)
val snowEnabled by boolean(false)
}
class RoundLogConfig : DelegatingConfigGroup() {
val enabled by featureEnable()
val plantsOnly by boolean(true)
val radiusSmall by double(max=0.5, default=0.25)
val radiusLarge by double(max=0.5, default=0.44)
val dimming by double(default = 0.7)
val connectSolids by boolean(false)
val lenientConnect by boolean(true)
val connectPerpendicular by boolean(true)
val connectGrass by boolean(true)
val defaultY by boolean(false)
val zProtection by double(min = 0.9, default = 0.99)
}
class CactusConfig : DelegatingConfigGroup() {
val enabled by featureEnable()
val size by double(min=0.5, max=1.5, default=0.8, langKey = recurring)
val sizeVariation by double(max=0.5, default=0.1)
val hOffset by double(max=0.5, default=0.1, langKey = recurring)
}
class LilypadConfig : PopulationConfigGroup() {
override val enabled by featureEnable()
val hOffset by double(max=0.25, default=0.1, langKey = recurring)
override val population by integer(max=64, default=16, min=0, langKey = recurring)
val shaderWind by boolean(true, langKey = recurring)
}
class ReedConfig : PopulationConfigGroup() {
override val enabled by featureEnable()
val hOffset by double(max=0.4, default=0.2, langKey = recurring)
val heightMin by double(min=1.5, max=3.5, default=1.7, langKey = recurring)
val heightMax by double(min=1.5, max=3.5, default=2.2, langKey = recurring)
override val population by integer(max=64, default=32, langKey = recurring)
val minBiomeTemp by double(default=0.4)
val minBiomeRainfall by double(default=0.4)
val shaderWind by boolean(true, langKey = recurring)
}
class AlgaeConfig : PopulationConfigGroup() {
override val enabled by featureEnable()
val hOffset by double(max=0.25, default=0.1, langKey = recurring)
val size by double(min=0.5, max=1.5, default=1.0, langKey = recurring)
val heightMin by double(min=0.1, max=1.5, default=0.5, langKey = recurring)
val heightMax by double(min=0.1, max=1.5, default=1.0, langKey = recurring)
override val population by integer(max=64, default=48, langKey = recurring)
val shaderWind by boolean(true, langKey = recurring)
}
class CoralConfig : PopulationConfigGroup() {
override val enabled by featureEnable()
val shallowWater by boolean(false)
val hOffset by double(max=0.4, default=0.2, langKey = recurring)
val vOffset by double(max=0.4, default=0.1, langKey = recurring)
val size by double(min=0.5, max=1.5, default=0.7, langKey = recurring)
val crustSize by double(min=0.5, max=1.5, default=1.4)
val chance by integer(max=64, default=32)
override val population by integer(max=64, default=48, langKey = recurring)
}
class NetherrackConfig : DelegatingConfigGroup() {
val enabled by featureEnable()
val hOffset by double(max=0.4, default=0.2, langKey = recurring)
val heightMin by double(min=0.1, max=1.5, default=0.6, langKey = recurring)
val heightMax by double(min=0.1, max=1.5, default=0.8, langKey = recurring)
val size by double(min=0.5, max=1.5, default=1.0, langKey = recurring)
}
class FallingLeavesConfig : DelegatingConfigGroup() {
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, langKey = recurring)
val chance by double(min=0.001, max=1.0, default=0.02)
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)
}
class RisingSoulConfig : DelegatingConfigGroup() {
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 double(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 double(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 integer(min=2, max=128, default=48)
val trailDensity by integer(min=1, max=16, default=3)
}

View File

@@ -0,0 +1,13 @@
package mods.betterfoliage.config
import net.minecraft.block.BlockState
import net.minecraft.block.Blocks
import net.minecraft.block.material.Material
import net.minecraft.world.biome.Biome
val SALTWATER_BIOMES = listOf(Biome.Category.BEACH, Biome.Category.OCEAN)
val SNOW_MATERIALS = listOf(Material.TOP_SNOW, Material.SNOW)
val BlockState.isSnow: Boolean get() = material in SNOW_MATERIALS
val ACCEPTED_ROUND_LOG_MATERIALS = listOf(Material.WOOD, Material.GRASS)

View File

@@ -0,0 +1,162 @@
package mods.betterfoliage.config.match
enum class MatchMethod {
EXACT_MATCH, EXTENDS, CONTAINS;
fun description(isSuccess: Boolean) = when (this) {
EXACT_MATCH -> if (isSuccess) "matches" else "does not match"
EXTENDS -> if (isSuccess) "extends" else "does not extend"
CONTAINS -> if (isSuccess) "contains" else "does not contain"
}
}
/**
* Basic Either monad implementation
*/
sealed class Either<out L, out R> {
class Left<L>(val left: L) : Either<L, Nothing>()
class Right<R>(val right: R) : Either<Nothing, R>()
fun leftOrNull() = if (this is Left) left else null
fun rightOrNull() = if (this is Right) right else null
fun <R2> map(func: (R) -> R2): Either<L, R2> = when (this) {
is Left<L> -> this
is Right<R> -> Right(func(right))
}
fun <L2> mapLeft(func: (L) -> L2): Either<L2, R> = when (this) {
is Left<L> -> Left(func(left))
is Right<R> -> this
}
fun ifRight(action: (R) -> Unit) {
if (this is Right) action(right)
}
companion object {
fun <L> ofLeft(left: L) = Left(left)
fun <R> ofRight(right: R) = Right(right)
}
}
// this cannot be inside the class for variance reasons
fun <L, R, R2> Either<L, R>.flatMap(func: (R) -> Either<L, R2>) = when (this) {
is Either.Left<L> -> this
is Either.Right<R> -> func(right)
}
fun <L, R, L2> Either<L, R>.flatMapLeft(func: (L) -> Either<L2, R>) = when (this) {
is Either.Left<L> -> func(left)
is Either.Right<R> -> this
}
fun <T> Either<T, T>.flatten() = when (this) {
is Either.Left -> left
is Either.Right -> right
}
interface MAnything<out T> {
val value: T
val immutable: Boolean
}
class MListAll(val list: List<MAnything<Boolean>>) : MAnything<Boolean> {
override val value get() = list.all { it.value }
override val immutable get() = list.all { it.immutable }
}
class MListAny(val list: List<MValue<Boolean>>) : MAnything<Boolean> {
override val value get() = list.any { it.value }
override val immutable get() = list.all { it.immutable }
}
class MNegated(val inner: MAnything<Boolean>) : MAnything<Boolean> {
override val value get() = !inner.value
override val immutable get() = inner.immutable
}
/**
* Value with metadata related to rule matching applied.
*
* @param value the wrapped value
* @param description human-readable description of what the value represents
* @param configSource identifies where the value is described in the config
* @param immutable true if the value never changes
* (another [MValue] constructed in the same way will have the same value)
*
*/
class MValue<out T>(
override val value: T,
val description: String,
val configSource: ConfigSource,
override val immutable: Boolean,
) : MAnything<T> {
companion object {
fun <T> right(value: T, description: String, configSource: ConfigSource, immutable: Boolean = true) =
Either.ofRight(MValue(value, description, configSource, immutable))
fun left(description: String, configSource: ConfigSource, immutable: Boolean = true) =
Either.ofLeft(MValue(false, description, configSource, immutable))
}
}
typealias MEither<T> = Either<MValue<Boolean>, MValue<T>>
val Node.Value.asEither get() = MValue.right(value, value, configSource, true)
fun Node.Value.left(description: String) = MValue.left(description, configSource)
fun Node.invalidTypeFor(type: String) = MValue.left("invalid type for $type: [${this::class.java.name}]", configSource)
fun Node.error(description: String) = MValue.left(description, configSource)
fun <T, R> MEither<T>.mapValue(func: (T) -> R) = map {
MValue(func(it.value), it.description, it.configSource, it.immutable)
}
fun <T> MEither<T>.mapDescription(func: (MValue<T>) -> String) = map {
MValue(it.value, func(it), it.configSource, it.immutable)
}
fun <T, R> MEither<T>.map(
func: (T) -> R,
description: (MValue<T>, R) -> String
) = map { t -> func(t.value).let { r -> MValue(r, description(t, r), t.configSource, t.immutable) } }
fun <T, R> MEither<T>.mapNotNull(
func: (T) -> R?,
dLeft: (MValue<T>) -> String = { it.description },
dRight: (MValue<T>, R) -> String = { m, _ -> m.description }
) = flatMap { t ->
func(t.value)?.let { r ->
MValue.right(r, dRight(t, r), t.configSource, t.immutable)
} ?: MValue.left(dLeft(t), t.configSource, t.immutable)
}
fun <T> MEither<T>.toRight(value: T) =
flatMapLeft { MValue.right(value, it.description, it.configSource, it.immutable) }
data class MComparison<T1, T2>(
private val opTrue: String,
private val opFalse: String,
val testFunc: (T1, T2) -> Boolean
) {
fun compare(value1: MEither<T1>, value2: MEither<T2>) = when {
value1 is Either.Left -> value1
value2 is Either.Left -> value2
else -> {
val isSuccess = testFunc((value1 as Either.Right).right.value, (value2 as Either.Right).right.value)
MValue.right(
isSuccess,
"${value1.right.description} ${if (isSuccess) opTrue else opFalse} ${value2.right.description}",
value2.right.configSource,
value1.right.immutable && value2.right.immutable
)
}
}.flatten()
companion object {
fun <T1, T2> of(matchMethod: MatchMethod, testFunc: (T1, T2) -> Boolean) =
MComparison(matchMethod.description(true), matchMethod.description(false), testFunc)
val equals = of(MatchMethod.EXACT_MATCH) { t1: Any, t2: Any -> t1 == t2 }
}
}

View File

@@ -0,0 +1,42 @@
package mods.betterfoliage.config.match
data class ConfigSource(
val configFile: String,
val line: Int,
val column: Int
) {
override fun toString() = "$configFile @ R$line,C$column"
}
sealed class Node {
enum class MatchSource { BLOCK_CLASS, BLOCK_NAME, MODEL_LOCATION }
abstract val configSource: ConfigSource
class MatchValueList(
val matchSource: MatchSource,
val matchMethod: MatchMethod,
override val configSource: ConfigSource,
val values: List<Value>
) : Node()
class MatchParam(
val name: String,
val values: List<Value>,
override val configSource: ConfigSource,
) : Node()
class Negate(val node: Node) : Node() {
override val configSource get() = node.configSource
}
class SetParam(val name: String, val value: Value, override val configSource: ConfigSource) : Node()
class MatchAll(override val configSource: ConfigSource, val list: List<Node>) : Node()
abstract class Value(override val configSource: ConfigSource, val value: String) : Node() {
class Literal(configSource: ConfigSource, value: String) : Value(configSource, value)
class ClassOf(configSource: ConfigSource, value: String) : Value(configSource, value)
class Texture(configSource: ConfigSource, value: String) : Value(configSource, value)
class Tint(configSource: ConfigSource, value: String) : Value(configSource, value)
}
}

View File

@@ -0,0 +1,172 @@
package mods.betterfoliage.config.match
import mods.betterfoliage.config.match.MatchMethod.CONTAINS
import mods.betterfoliage.config.match.MatchMethod.EXACT_MATCH
import mods.betterfoliage.config.match.MatchMethod.EXTENDS
import mods.betterfoliage.resource.discovery.RuleProcessingContext
import mods.betterfoliage.util.findFirst
import mods.betterfoliage.util.quoted
import mods.betterfoliage.util.tryDefault
import net.minecraft.client.renderer.model.BlockModel
import net.minecraft.util.ResourceLocation
import net.minecraftforge.registries.ForgeRegistries
typealias PartialLocation = Pair<String?, String>
object MatchRules {
fun visitRoot(ctx: RuleProcessingContext, node: Node.MatchAll): MListAll {
val results = mutableListOf<MAnything<Boolean>>()
for (rule in node.list) {
val result = mNode(ctx, rule)
results.add(result)
if (!result.value) break
}
return MListAll(results)
}
fun mNode(ctx: RuleProcessingContext, node: Node): MAnything<Boolean> = when(node) {
is Node.MatchValueList -> mMatchList(ctx, node)
is Node.MatchParam -> mParam(ctx, node)
is Node.SetParam -> mParamSet(ctx, node)
is Node.Negate -> mNegate(ctx, node)
else -> node.error("match type not implemented: ${node::class.java.name.quoted}").left
}
fun mNegate(ctx: RuleProcessingContext, node: Node.Negate) = MNegated(mNode(ctx, node.node))
fun mMatchList(ctx: RuleProcessingContext, node: Node.MatchValueList) = node.values.map { value ->
when (node.matchSource) {
Node.MatchSource.BLOCK_CLASS -> mBlockClass(ctx, node, value)
Node.MatchSource.BLOCK_NAME -> mBlockName(ctx, node, value)
Node.MatchSource.MODEL_LOCATION -> mModel(ctx, node, value)
}
}.let { MListAny(it) }
fun mBlockClass(ctx: RuleProcessingContext, node: Node.MatchValueList, value: Node.Value): MValue<Boolean> {
val blockClass = ctx.discovery.blockState.block::class.java.let {
MValue.right(it, "block class ${it.name.quoted}", node.configSource)
}
val target = when(value) {
is Node.Value.Literal -> value.asEither.mapNotNull(
func = { tryDefault(null) { Class.forName(it) }},
dLeft = { "missing class ${it.value}" },
dRight = { m, _ -> " class ${m.value}" }
)
is Node.Value.ClassOf -> value.asEither.mapValue(::ResourceLocation).mapNotNull(
func = { loc -> ForgeRegistries.BLOCKS.getValue(loc)?.let { it::class.java } },
dLeft = { "missing block ${it.value.toString().quoted}" },
dRight = { m, r -> "class ${r.name.quoted} of block ${m.value}" }
)
else -> value.invalidTypeFor("block class")
}
return when(node.matchMethod) {
EXACT_MATCH -> MComparison.equals.compare(blockClass, target)
EXTENDS -> classExtends.compare(blockClass, target)
CONTAINS -> node.error("invalid match type for block class: contains").left
}
}
fun mBlockName(ctx: RuleProcessingContext, node: Node.MatchValueList, value: Node.Value): MValue<Boolean> {
val blockName = MValue.right(ctx.discovery.blockState.block, "", node.configSource).mapNotNull(
func = { it.registryName }, dLeft = { "missing block name" }, dRight = { _, r -> "block name ${r.toString().quoted}" }
)
val target = when(value) {
is Node.Value.Literal -> value.asEither.map(::splitLocation, ::quoteString)
else -> value.invalidTypeFor("block name")
}
return when(node.matchMethod) {
EXACT_MATCH -> blockNameExact.compare(blockName, target)
CONTAINS -> blockNameContains.compare(blockName, target)
EXTENDS -> node.error("invalid match type for block name: extends").left
}
}
fun mModel(ctx: RuleProcessingContext, node: Node.MatchValueList, value: Node.Value): MValue<Boolean> {
val model = (ctx.discovery.getUnbaked() as? BlockModel)?.let {
MValue.right(it, "model ${it.name.quoted}", node.configSource)
} ?: node.error("unsupported model type: ${ctx.discovery.getUnbaked()::class.java.name.quoted}")
val target = when(value) {
is Node.Value.Literal -> value.asEither.map(::splitLocation, ::quoteString)
else -> value.invalidTypeFor("model")
}
val models = when(node.matchMethod) {
EXTENDS -> model.mapValue { ctx.discovery.loadHierarchy(it).ancestors() }
else -> model.mapValue { listOf(it) }
}
return when(node.matchMethod) {
EXACT_MATCH, EXTENDS -> anyModel(node.matchMethod, ::locationMatches)
CONTAINS -> anyModel(CONTAINS, ::locationContains)
}.compare(models, target)
}
fun mParam(ctx: RuleProcessingContext, node: Node.MatchParam) = node.values.map { value ->
val paramValue = ctx.params[node.name] ?.let {
MValue.right(it, "parameter ${node.name.quoted}", node.configSource, immutable = false)
} ?: node.error("missing parameter ${node.name.quoted}")
val target = when(value) {
is Node.Value.Literal -> value.asEither.mapDescription { it.description.quoted }
else -> value.invalidTypeFor("parameter")
}
MComparison.equals.compare(paramValue, target)
}.let { MListAny(it) }
fun mParamSet(ctx: RuleProcessingContext, node: Node.SetParam): MValue<Boolean> {
val target = when(node.value) {
is Node.Value.Literal -> node.value.asEither
is Node.Value.Texture -> when(val model = ctx.discovery.getUnbaked()) {
is BlockModel -> node.value.asEither.map(
func = { model.getMaterial(it).texture().toString() },
description = { m, r -> "texture \"${m.value}\" = \"$r\" of model ${model.name}"}
)
else -> node.error("unsupported model type: ${model::class.java.name.quoted}")
}
is Node.Value.Tint -> when(val model = ctx.discovery.getUnbaked()) {
is BlockModel -> node.value.asEither.mapNotNull(
func = { model.tintOf(it)?.toString() },
dRight = { m, r -> "tint index $r for sprite ${m.value}" },
dLeft = { m -> "tint index -1 for unused sprite ${m.value}"}
).toRight("-1")
else -> node.error("unsupported model type: ${model::class.java.name.quoted}")
}
else -> node.value.invalidTypeFor("prameter")
}
target.ifRight { ctx.params[node.name] = it.value }
return target.mapDescription { m -> "parameter ${node.name} set to ${m.value}" }.mapValue { true }.flatten()
}
private val classExtends = MComparison.of<Class<*>, Class<*>>(EXTENDS) { c1, c2 -> c2.isAssignableFrom(c1) }
private val blockNameExact = MComparison.of<ResourceLocation, PartialLocation>(EXACT_MATCH) { block, partial ->
locationMatches(block, partial)
}
private val blockNameContains = MComparison.of<ResourceLocation, Pair<String?, String>>(CONTAINS) { block, partial ->
locationContains(block, partial)
}
private fun anyModel(matchMethod: MatchMethod, func: (ResourceLocation, PartialLocation)->Boolean) =
MComparison.of<List<BlockModel>, PartialLocation>(matchMethod) { models, partial ->
models.any { func(ResourceLocation(it.name), partial) }
}
fun locationMatches(loc: ResourceLocation, partial: PartialLocation) =
(partial.first == null || loc.namespace == partial.first) && loc.path == partial.second
fun locationContains(loc: ResourceLocation, partial: PartialLocation) =
(partial.first == null || loc.namespace.contains(partial.first!!)) && loc.path.contains(partial.second)
fun splitLocation(str: String): PartialLocation =
if (str.contains(":")) ResourceLocation(str).let { it.namespace to it.path } else null to str
fun <T, R> quoteString(mValue: MValue<T>, newValue: R) = mValue.description.quoted
fun BlockModel.ancestors(): List<BlockModel> = if (parent == null) listOf(this) else parent!!.ancestors() + this
fun BlockModel.tintOf(spriteName: String) =
elements.findFirst { element ->
element.faces.entries.findFirst { (_, face) ->
if (face.texture == "#$spriteName") face.tintIndex else null
}
}
}

View File

@@ -0,0 +1,54 @@
package mods.betterfoliage.integration
import mods.betterfoliage.chunk.BlockCtx
import mods.betterfoliage.util.ThreadLocalDelegate
import mods.betterfoliage.util.allAvailable
import mods.betterfoliage.util.reflectField
import mods.betterfoliage.BlockPos
import mods.betterfoliage.BlockState
import mods.betterfoliage.CustomColors
import mods.betterfoliage.RenderEnv
import net.minecraft.block.BlockState
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.model.BakedQuad
import net.minecraft.util.Direction.UP
import net.minecraft.util.math.BlockPos
import net.minecraft.world.level.ColorResolver
import org.apache.logging.log4j.Level.INFO
import org.apache.logging.log4j.LogManager
/**
* Integration for OptiFine custom block colors.
*/
@Suppress("UNCHECKED_CAST")
object OptifineCustomColors {
val logger = LogManager.getLogger(this)
val isColorAvailable = allAvailable(CustomColors, CustomColors.getColorMultiplier)
init {
logger.log(INFO, "Optifine custom color support is ${if (isColorAvailable) "enabled" else "disabled" }")
}
val renderEnv by ThreadLocalDelegate { OptifineRenderEnv() }
val fakeQuad = BakedQuad(IntArray(0), 1, UP, null, true)
fun getBlockColor(ctx: BlockCtx, resolver: ColorResolver): Int {
val ofColor = if (isColorAvailable && Minecraft.getInstance().options.reflectField<Boolean>("ofCustomColors") == true) {
renderEnv.reset(ctx.state, ctx.pos)
CustomColors.getColorMultiplier.invokeStatic(fakeQuad, ctx.state, ctx.world, ctx.pos, renderEnv.wrapped) as? Int
} else null
return if (ofColor == null || ofColor == -1) ctx.color(resolver) else ofColor
}
}
class OptifineRenderEnv {
val wrapped: Any = RenderEnv.element!!.getDeclaredConstructor(BlockState.element, BlockPos.element).let {
it.isAccessible = true
it.newInstance(null, null)
}
fun reset(state: BlockState, pos: BlockPos) {
RenderEnv.reset.invoke(wrapped, state, pos)
}
}

View File

@@ -0,0 +1,98 @@
package mods.betterfoliage.integration
import mods.betterfoliage.BetterFoliage
import mods.betterfoliage.BlockAliases
import mods.betterfoliage.BufferBuilder_sVertexBuilder
import mods.betterfoliage.SVertexBuilder
import mods.betterfoliage.Shaders
import mods.betterfoliage.render.pipeline.RenderCtxBase
import mods.betterfoliage.render.pipeline.RenderCtxVanilla
import mods.betterfoliage.util.HasLogger
import mods.betterfoliage.util.allAvailable
import mods.betterfoliage.util.get
import mods.betterfoliage.util.mapArray
import net.minecraft.block.BlockRenderType
import net.minecraft.block.BlockRenderType.MODEL
import net.minecraft.block.BlockState
import net.minecraft.block.Blocks
import net.minecraft.client.renderer.BufferBuilder
import net.minecraft.util.Direction
import net.minecraft.util.Direction.DOWN
import net.minecraft.util.Direction.EAST
import net.minecraft.util.Direction.NORTH
import net.minecraft.util.Direction.SOUTH
import net.minecraft.util.Direction.WEST
import net.minecraft.util.math.BlockPos
import net.minecraft.world.IBlockDisplayReader
import net.minecraftforge.client.model.pipeline.LightUtil
import org.apache.logging.log4j.Level.INFO
/**
* Integration for ShadersMod.
*/
object ShadersModIntegration : HasLogger() {
@JvmStatic val isEffectsAvailable = allAvailable(SVertexBuilder.pushState, SVertexBuilder.popState, BlockAliases.getAliasBlockId)
@JvmStatic val isDiffuseAvailable = allAvailable(Shaders.shaderPackLoaded, Shaders.blockLightLevel05, Shaders.blockLightLevel06, Shaders.blockLightLevel08)
@JvmStatic val defaultLeaves = Blocks.OAK_LEAVES.defaultBlockState()
@JvmStatic val defaultGrass = Blocks.GRASS.defaultBlockState()
@JvmStatic var diffuseShades = Direction.values().mapArray { LightUtil.diffuseLight(it) }
/**
* Called from transformed ShadersMod code.
* @see mods.betterfoliage.loader.BetterFoliageTransformer
*/
@JvmStatic fun getBlockStateOverride(state: BlockState, world: IBlockDisplayReader, pos: BlockPos): BlockState {
if (state in BetterFoliage.blockTypes.leaf) return defaultLeaves
// if (BlockConfig.crops.matchesClass(state.block)) return defaultGrass
return state
}
init {
logger.log(INFO, "ShadersMod diffuse shading integration is ${if (isDiffuseAvailable) "enabled" else "disabled" }")
logger.log(INFO, "ShadersMod vertex shader integration is ${if (isEffectsAvailable) "enabled" else "disabled" }")
// Recalculate the diffuse shading values used when resources are reloaded
if (isDiffuseAvailable) BetterFoliage.modelManager.onInvalidate {
if (Shaders.shaderPackLoaded.getStatic()) {
diffuseShades = Direction.values().mapArray { face ->
when(face) {
DOWN -> Shaders.blockLightLevel05.getStatic()
WEST, EAST -> Shaders.blockLightLevel06.getStatic()
NORTH, SOUTH -> Shaders.blockLightLevel08.getStatic()
else -> LightUtil.diffuseLight(face)
}
}
} else {
diffuseShades = Direction.values().mapArray { LightUtil.diffuseLight(it) }
}
}
}
/** Quads rendered inside this block will use the given block entity data in shader programs. */
inline fun renderAs(buffer: BufferBuilder, state: BlockState, renderType: BlockRenderType, enabled: Boolean = true, func: ()->Unit) {
if (isEffectsAvailable && enabled) {
val aliasBlockId = BlockAliases.getAliasBlockId.invokeStatic(state)
val sVertexBuilder = buffer[BufferBuilder_sVertexBuilder]
SVertexBuilder.pushState.invoke(sVertexBuilder, aliasBlockId)
func()
SVertexBuilder.popState.invoke(sVertexBuilder)
} else {
func()
}
}
/** Quads rendered inside this block will behave as tallgrass blocks in shader programs. */
inline fun grass(ctx: RenderCtxBase, enabled: Boolean = true, func: ()->Unit) =
((ctx as? RenderCtxVanilla)?.buffer as? BufferBuilder)?.let { bufferBuilder ->
renderAs(bufferBuilder, defaultGrass, MODEL, enabled, func)
}
/** Quads rendered inside this block will behave as leaf blocks in shader programs. */
inline fun leaves(ctx: RenderCtxBase, enabled: Boolean = true, func: ()->Unit) =
((ctx as? RenderCtxVanilla)?.buffer as? BufferBuilder)?.let { bufferBuilder ->
renderAs(bufferBuilder, defaultLeaves, MODEL, enabled, func)
}
}

View File

@@ -0,0 +1,133 @@
package mods.betterfoliage.model
import mods.betterfoliage.chunk.BlockCtx
import mods.betterfoliage.render.pipeline.RenderCtxBase
import mods.betterfoliage.render.pipeline.WrappedLayerPredicate
import mods.betterfoliage.render.pipeline.layerPredicate
import mods.betterfoliage.resource.discovery.ModelBakingContext
import mods.betterfoliage.resource.discovery.ModelBakingKey
import mods.betterfoliage.util.Double3
import mods.betterfoliage.util.HasLogger
import mods.betterfoliage.util.directionsAndNull
import mods.betterfoliage.util.mapArray
import net.minecraft.block.Block
import net.minecraft.block.BlockState
import net.minecraft.client.renderer.RenderType
import net.minecraft.client.renderer.RenderTypeLookup
import net.minecraft.client.renderer.model.BakedQuad
import net.minecraft.client.renderer.model.IBakedModel
import net.minecraft.client.renderer.model.SimpleBakedModel
import net.minecraft.client.renderer.vertex.DefaultVertexFormats
import net.minecraft.client.renderer.vertex.VertexFormatElement
import net.minecraftforge.client.model.pipeline.BakedQuadBuilder
import java.util.Random
/**
* Hybrid baked quad implementation, carrying both baked and unbaked information.
* Used to do advanced vertex lighting without unbaking vertex data at lighting time.
*/
data class HalfBakedQuad(
val raw: Quad,
val baked: BakedQuad
)
open class HalfBakedSimpleModelWrapper(baseModel: SimpleBakedModel): IBakedModel by baseModel, SpecialRenderModel {
val baseQuads = baseModel.unbakeQuads()
override fun prepare(ctx: BlockCtx, random: Random) = Unit
override fun renderLayer(ctx: RenderCtxBase, data: Any, layer: RenderType) {
// if the passed data is a BlockState, render on the same layer(s) as that block
val testState = (data as? BlockState) ?: ctx.state
// this could get called for more layers than the underlying model is on
// ignore extra decoration layers
val shouldRender = when(val predicate = testState.block.layerPredicate) {
is WrappedLayerPredicate -> predicate.original.test(layer)
else -> RenderTypeLookup.canRenderInLayer(testState, layer)
}
if (shouldRender) ctx.renderQuads(baseQuads)
}
}
open class HalfBakedSpecialWrapper(val baseModel: SpecialRenderModel): SpecialRenderModel by baseModel {
}
abstract class HalfBakedWrapperKey : ModelBakingKey, HasLogger() {
override fun bake(ctx: ModelBakingContext): IBakedModel? {
val baseModel = super.bake(ctx)
val halfBaked = when(baseModel) {
is SimpleBakedModel -> HalfBakedSimpleModelWrapper(baseModel)
else -> null
}
return if (halfBaked == null) baseModel else bake(ctx, halfBaked)
}
abstract fun bake(ctx: ModelBakingContext, wrapped: SpecialRenderModel): SpecialRenderModel
}
fun List<Quad>.bake(applyDiffuseLighting: Boolean) = map { quad ->
if (quad.sprite == null) throw IllegalStateException("Quad must have a texture assigned before baking")
val builder = BakedQuadBuilder(quad.sprite)
builder.setApplyDiffuseLighting(applyDiffuseLighting)
builder.setQuadOrientation(quad.face())
builder.setQuadTint(quad.colorIndex)
quad.verts.forEach { vertex ->
DefaultVertexFormats.BLOCK.elements.forEachIndexed { idx, element ->
when {
element.usage == VertexFormatElement.Usage.POSITION -> builder.put(idx,
(vertex.xyz.x + 0.5).toFloat(),
(vertex.xyz.y + 0.5).toFloat(),
(vertex.xyz.z + 0.5).toFloat(),
1.0f
)
// don't fill lightmap UV coords
element.usage == VertexFormatElement.Usage.UV && element.type == VertexFormatElement.Type.FLOAT -> builder.put(idx,
quad.sprite.u0 + (quad.sprite.u1 - quad.sprite.u0) * (vertex.uv.u + 0.5).toFloat(),
quad.sprite.v0 + (quad.sprite.v1 - quad.sprite.v0) * (vertex.uv.v + 0.5).toFloat(),
0.0f, 1.0f
)
element.usage == VertexFormatElement.Usage.COLOR -> builder.put(idx,
(vertex.color.red and 255).toFloat() / 255.0f,
(vertex.color.green and 255).toFloat() / 255.0f,
(vertex.color.blue and 255).toFloat() / 255.0f,
(vertex.color.alpha and 255).toFloat() / 255.0f
)
element.usage == VertexFormatElement.Usage.NORMAL -> builder.put(idx,
(vertex.normal ?: quad.normal).x.toFloat(),
(vertex.normal ?: quad.normal).y.toFloat(),
(vertex.normal ?: quad.normal).z.toFloat(),
0.0f
)
else -> builder.put(idx)
}
}
}
HalfBakedQuad(quad, builder.build())
}
fun Array<List<Quad>>.bake(applyDiffuseLighting: Boolean) = mapArray { it.bake(applyDiffuseLighting) }
fun BakedQuad.unbake(): HalfBakedQuad {
val size = DefaultVertexFormats.BLOCK.integerSize
val verts = Array(4) { vIdx ->
val x = java.lang.Float.intBitsToFloat(vertices[vIdx * size + 0]) - 0.5f
val y = java.lang.Float.intBitsToFloat(vertices[vIdx * size + 1]) - 0.5f
val z = java.lang.Float.intBitsToFloat(vertices[vIdx * size + 2]) - 0.5f
val color = vertices[vIdx * size + 3]
val u = java.lang.Float.intBitsToFloat(vertices[vIdx * size + 4])
val v = java.lang.Float.intBitsToFloat(vertices[vIdx * size + 5])
Vertex(Double3(x, y, z), UV(u.toDouble(), v.toDouble()), Color(color))
}
val unbaked = Quad(
verts[0], verts[1], verts[2], verts[3],
colorIndex = if (isTinted) tintIndex else -1,
face = direction
)
return HalfBakedQuad(unbaked, this)
}
fun SimpleBakedModel.unbakeQuads() = directionsAndNull.flatMap { face ->
getQuads(null, face, Random()).map { it.unbake() }
}

View File

@@ -0,0 +1,218 @@
package mods.betterfoliage.model
import mods.betterfoliage.util.Double3
import mods.betterfoliage.util.Rotation
import mods.betterfoliage.util.boxFaces
import mods.betterfoliage.util.get
import mods.betterfoliage.util.minmax
import mods.betterfoliage.util.nearestAngle
import mods.betterfoliage.util.rotate
import mods.betterfoliage.util.times
import mods.betterfoliage.util.vec
import net.minecraft.client.renderer.texture.NativeImage
import net.minecraft.client.renderer.texture.TextureAtlasSprite
import net.minecraft.util.Direction
import java.lang.Math.max
import java.lang.Math.min
import java.util.Random
import kotlin.math.cos
import kotlin.math.sin
/**
* 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] [ModelLighter] instance to use with AO rendering
* @param[flatShader] [ModelLighter] 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 color: Color = Color.white,
val normal: Double3? = null
)
data class Color(val alpha: Int, val red: Int, val green: Int, val blue: Int) {
constructor(combined: Int) : this(
combined shr 24 and 255,
combined shr 16 and 255,
combined shr 8 and 255,
combined and 255
)
val asInt get() = (alpha shl 24) or (red shl 16) or (green shl 8) or blue
val asHSB get() = HSB.fromColor(this)
operator fun times(f: Float) = Color(
alpha,
(f * red.toFloat()).toInt().coerceIn(0 until 256),
(f * green.toFloat()).toInt().coerceIn(0 until 256),
(f * blue.toFloat()).toInt().coerceIn(0 until 256)
)
companion object {
val white get() = Color(255, 255, 255, 255)
}
}
data class HSB(var hue: Float, var saturation: Float, var brightness: Float) {
companion object {
/** Red is assumed to be LSB, see [NativeImage.PixelFormat.RGBA] */
fun fromColorRGBA(color: Int): HSB {
val hsbVals = java.awt.Color.RGBtoHSB(color and 255, (color shr 8) and 255, (color shr 16) and 255, null)
return HSB(hsbVals[0], hsbVals[1], hsbVals[2])
}
fun fromColorBGRA(color: Int): HSB {
val hsbVals = java.awt.Color.RGBtoHSB((color shr 16) and 255, (color shr 8) and 255, color and 255, null)
return HSB(hsbVals[0], hsbVals[1], hsbVals[2])
}
fun fromColor(color: Color): HSB {
val hsbVals = java.awt.Color.RGBtoHSB(color.red, color.green, color.blue, null)
return HSB(hsbVals[0], hsbVals[1], hsbVals[2])
}
}
val asInt: Int get() = java.awt.Color.HSBtoRGB(hue, saturation, brightness)
val asColor: Color get() = Color(asInt)
}
/**
* Intermediate representation of model quad
* Immutable, double-precision
* Zero-centered (both XYZ and UV) coordinates for simpler rotation/mirroring
*/
data class Quad(
val v1: Vertex, val v2: Vertex, val v3: Vertex, val v4: Vertex,
val sprite: TextureAtlasSprite? = null,
val colorIndex: Int = -1,
val face: Direction? = null
) {
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 = copy(
v1 = trans(v1, 0), v2 = trans(v2, 1), v3 = trans(v3, 2), v4 = 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<Double, Direction>) = 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 rotate(rot: Rotation) =
transformV { it.copy(xyz = it.xyz.rotate(rot), normal = it.normal?.rotate(rot)) }.copy(face = face?.rotate(rot))
fun rotateZ(angle: Double) = transformV {
it.copy(
xyz = Double3(
it.xyz.x * cos(angle) + it.xyz.z * sin(angle),
it.xyz.y,
it.xyz.z * cos(angle) - it.xyz.x * sin(angle)
),
normal = it.normal?.let { normal ->
Double3(
normal.x * cos(angle) + normal.z * sin(angle),
normal.y,
normal.z * cos(angle) - normal.x * sin(angle)
)
}
)
}
fun scaleUV(scale: Double) = transformV { it.copy(uv = UV(it.uv.u * scale, it.uv.v * scale)) }
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 scrambleUV(random: Random, canFlipU: Boolean, canFlipV: Boolean, canRotate: Boolean) = this
.mirrorUV(canFlipU && random.nextBoolean(), canFlipV && random.nextBoolean())
.let { if (canRotate) it.rotateUV(random.nextInt(4)) else it }
fun sprite(sprite: TextureAtlasSprite) = copy(sprite = sprite)
fun color(color: Color) = transformV { it.copy(color = color) }
fun color(color: Int) = transformV { it.copy(color = Color(color)) }
fun colorIndex(colorIndex: Int) = copy(colorIndex = colorIndex)
fun colorAndIndex(color: Color?) = color(color ?: Color.white).colorIndex(if (color == null) 0 else -1)
fun face() = face ?: nearestAngle(normal, Direction.values().toList()) { it.vec }.first
val flipped: Quad get() = Quad(v4, v3, v2, v1, sprite, colorIndex)
fun cycleVertices(n: Int) = when (n % 4) {
1 -> Quad(v2, v3, v4, v1)
2 -> Quad(v3, v4, v1, v2)
3 -> Quad(v4, v1, v2, v3)
else -> this.copy()
}
companion object {
fun mix(first: Quad, second: Quad, vertexFactory: (Vertex, Vertex) -> Vertex) = Quad(
v1 = vertexFactory(first.v1, second.v1),
v2 = vertexFactory(first.v2, second.v2),
v3 = vertexFactory(first.v3, second.v3),
v4 = vertexFactory(first.v4, second.v4)
)
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: Direction): Quad {
val base = face.vec * 0.5
val top = boxFaces[face].top * 0.5
val left = boxFaces[face].left * 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)
)
}
}
}

View File

@@ -0,0 +1,57 @@
package mods.betterfoliage.model
import mods.betterfoliage.chunk.BlockCtx
import mods.betterfoliage.render.pipeline.RenderCtxBase
import net.minecraft.client.renderer.RenderType
import net.minecraft.client.renderer.model.IBakedModel
import net.minecraft.client.renderer.model.WeightedBakedModel
import net.minecraft.util.WeightedRandom
import java.util.Random
/**
* Model that makes use of advanced rendering features.
*/
interface SpecialRenderModel : IBakedModel {
/**
* Create custom renderdata object. Called once per block. Result is passed to renderLayer().
*/
fun prepare(ctx: BlockCtx, random: Random): Any
fun renderLayer(ctx: RenderCtxBase, data: Any, layer: RenderType)
/**
* Get the actual model that will be rendered. Useful for container models (like [WeightedBakedModel]).
*/
fun resolve(random: Random) = this
}
interface SpecialRenderData {
fun canRenderInLayer(layer: RenderType) = false
}
class WeightedModelWrapper(
val models: List<WeightedModel>, baseModel: SpecialRenderModel
) : IBakedModel by baseModel, SpecialRenderModel {
class WeightedModel(val model: SpecialRenderModel, weight: Int) : WeightedRandom.Item(weight)
val totalWeight = models.sumBy { it.weight }
fun getModel(random: Random) = WeightedRandom.getWeightedItem(models, random.nextInt(totalWeight))
override fun resolve(random: Random) = getModel(random).model.resolve(random)
override fun prepare(ctx: BlockCtx, random: Random) = getModel(random).model.let { actual ->
WeightedRenderData(actual, actual.prepare(ctx, random))
}
override fun renderLayer(ctx: RenderCtxBase, data: Any, layer: RenderType) = when (data) {
is WeightedRenderData -> data.model.renderLayer(ctx, data.modelRenderData, layer)
else -> getModel(ctx.random).model.renderLayer(ctx, data, layer)
}
}
data class WeightedRenderData(
val model: SpecialRenderModel,
val modelRenderData: Any
) : SpecialRenderData {
override fun canRenderInLayer(layer: RenderType) = (modelRenderData as? SpecialRenderData)?.canRenderInLayer(layer) ?: false
}

View File

@@ -0,0 +1,83 @@
package mods.betterfoliage.model
import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.util.Atlas
import mods.betterfoliage.util.resourceManager
import net.minecraft.client.renderer.texture.MissingTextureSprite
import net.minecraft.client.renderer.texture.TextureAtlasSprite
import net.minecraft.util.ResourceLocation
import net.minecraftforge.client.event.TextureStitchEvent
import net.minecraftforge.eventbus.api.SubscribeEvent
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
interface SpriteSet {
val num: Int
operator fun get(idx: Int): TextureAtlasSprite
}
class FixedSpriteSet(val sprites: List<TextureAtlasSprite>) : SpriteSet {
override val num = sprites.size
override fun get(idx: Int) = sprites[idx % num]
}
class SpriteDelegate(val atlas: Atlas, val idFunc: () -> ResourceLocation) : ReadOnlyProperty<Any, TextureAtlasSprite> {
private lateinit var id: ResourceLocation
private var value: TextureAtlasSprite? = null
init {
BetterFoliageMod.bus.register(this)
}
@SubscribeEvent
fun handlePreStitch(event: TextureStitchEvent.Pre) {
id = idFunc(); value = null
event.addSprite(id)
}
override fun getValue(thisRef: Any, property: KProperty<*>): TextureAtlasSprite {
value?.let { return it }
synchronized(this) {
value?.let { return it }
atlas[id].let { value = it; return it }
}
}
}
class SpriteSetDelegate(
val atlas: Atlas,
val idRegister: (ResourceLocation) -> ResourceLocation = { it },
val idFunc: (Int) -> ResourceLocation
) : ReadOnlyProperty<Any, SpriteSet> {
private var idList: List<ResourceLocation> = emptyList()
private var spriteSet: SpriteSet? = null
init {
BetterFoliageMod.bus.register(this)
}
@SubscribeEvent
fun handlePreStitch(event: TextureStitchEvent.Pre) {
if (event.map.location() != atlas.resourceId) return
spriteSet = null
idList = (0 until 16)
.map(idFunc)
.filter { resourceManager.hasResource(atlas.file(it)) }
.map(idRegister)
idList.forEach { event.addSprite(it) }
}
override fun getValue(thisRef: Any, property: KProperty<*>): SpriteSet {
spriteSet?.let { return it }
synchronized(this) {
spriteSet?.let { return it }
spriteSet = FixedSpriteSet(
idList
.ifEmpty { listOf(MissingTextureSprite.getLocation()) }
.map { atlas[it] }
)
return spriteSet!!
}
}
}

View File

@@ -0,0 +1,107 @@
package mods.betterfoliage.model
import mods.betterfoliage.util.Atlas
import mods.betterfoliage.util.Double3
import mods.betterfoliage.util.PI2
import mods.betterfoliage.util.allDirections
import mods.betterfoliage.util.random
import mods.betterfoliage.util.randomB
import mods.betterfoliage.util.randomD
import mods.betterfoliage.util.randomI
import mods.betterfoliage.util.rot
import mods.betterfoliage.util.vec
import net.minecraft.client.renderer.texture.TextureAtlasSprite
import net.minecraft.util.Direction.UP
import net.minecraft.util.ResourceLocation
import kotlin.math.cos
import kotlin.math.sin
fun xzDisk(modelIdx: Int) = (PI2 * modelIdx.toDouble() / 64.0).let { Double3(cos(it), 0.0, sin(it)) }
data class TuftShapeKey(
val size: Double,
val height: Double,
val offset: Double3,
val flipU1: Boolean,
val flipU2: Boolean
)
fun tuftShapeSet(size: Double, heightMin: Double, heightMax: Double, hOffset: Double): Array<TuftShapeKey> {
return Array(64) { idx ->
TuftShapeKey(
size,
randomD(heightMin, heightMax),
xzDisk(idx) * randomD(hOffset / 2.0, hOffset),
randomB(),
randomB()
)
}
}
fun tuftQuadSingle(size: Double, height: Double, flipU: Boolean) =
Quad.verticalRectangle(
x1 = -0.5 * size,
z1 = 0.5 * size,
x2 = 0.5 * size,
z2 = -0.5 * size,
yBottom = 0.5,
yTop = 0.5 + height
)
.mirrorUV(flipU, false)
fun tuftModelSet(shapes: Array<TuftShapeKey>, color: Color, tint: Int, spriteGetter: (Int) -> TextureAtlasSprite) =
shapes.mapIndexed { idx, shape ->
listOf(
tuftQuadSingle(shape.size, shape.height, shape.flipU1),
tuftQuadSingle(shape.size, shape.height, shape.flipU2).rotate(rot(UP))
).map { it.move(shape.offset) }
.map { it.color(color).colorIndex(tint) }
.map { it.sprite(spriteGetter(idx)) }
}
fun fullCubeTextured(
spriteLocation: ResourceLocation,
tintIndex: Int,
scrambleUV: Boolean = true
): List<HalfBakedQuad> {
val sprite = Atlas.BLOCKS[spriteLocation]
return allDirections.map { Quad.faceQuad(it) }
.map { if (!scrambleUV) it else it.rotateUV(randomI(max = 4)) }
.map { it.sprite(sprite) }
.map { it.colorIndex(tintIndex) }
.bake(true)
}
fun crossModelsRaw(num: Int, size: Double, hOffset: Double, vOffset: Double): List<List<Quad>> {
return (0 until num).map { idx ->
listOf(
Quad.verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = -0.5 * 1.41, yTop = 0.5 * 1.41),
Quad.verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = -0.5 * 1.41, yTop = 0.5 * 1.41)
.rotate(rot(UP))
).map { it.scale(size) }
.map { it.move(xzDisk(idx) * hOffset) }
.map { it.move(UP.vec * randomD(-1.0, 1.0) * vOffset) }
}
}
fun crossModelSingle(base: List<Quad>, sprite: TextureAtlasSprite, color: Color, tint: Int, scrambleUV: Boolean) =
base.map { if (scrambleUV) it.scrambleUV(random, canFlipU = true, canFlipV = true, canRotate = true) else it }
.map { it.color(color).colorIndex(tint) }
.mapIndexed { idx, quad -> quad.sprite(sprite) }
.withOpposites()
.bake(false)
fun crossModelsTextured(
leafBase: Iterable<List<Quad>>,
color: Color, tint: Int,
scrambleUV: Boolean,
spriteGetter: (Int) -> ResourceLocation
) = leafBase.mapIndexed { idx, leaf ->
crossModelSingle(leaf, Atlas.BLOCKS[spriteGetter(idx)], color, tint, scrambleUV)
}.toTypedArray()
fun Iterable<Quad>.withOpposites() = flatMap { listOf(it, it.flipped) }
fun Iterable<List<Quad>>.buildTufts(applyDiffuseLighting: Boolean = false) =
map { it.withOpposites().bake(applyDiffuseLighting) }.toTypedArray()
fun Iterable<List<Quad>>.transform(trans: Quad.(Int)-> Quad) = mapIndexed { idx, qList -> qList.map { it.trans(idx) } }

View File

@@ -0,0 +1,93 @@
package mods.betterfoliage.render.block.vanilla
import mods.betterfoliage.BetterFoliage
import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.chunk.BlockCtx
import mods.betterfoliage.config.Config
import mods.betterfoliage.model.Color
import mods.betterfoliage.model.HalfBakedSpecialWrapper
import mods.betterfoliage.model.HalfBakedWrapperKey
import mods.betterfoliage.model.SpecialRenderModel
import mods.betterfoliage.model.SpriteSetDelegate
import mods.betterfoliage.model.buildTufts
import mods.betterfoliage.model.crossModelsRaw
import mods.betterfoliage.model.crossModelsTextured
import mods.betterfoliage.model.transform
import mods.betterfoliage.model.tuftModelSet
import mods.betterfoliage.model.tuftShapeSet
import mods.betterfoliage.render.lighting.LightingPreferredFace
import mods.betterfoliage.render.lighting.RoundLeafLighting
import mods.betterfoliage.render.pipeline.RenderCtxBase
import mods.betterfoliage.resource.discovery.ModelBakingContext
import mods.betterfoliage.resource.discovery.ModelDiscoveryContext
import mods.betterfoliage.resource.discovery.ParametrizedModelDiscovery
import mods.betterfoliage.util.Atlas
import mods.betterfoliage.util.Rotation
import mods.betterfoliage.util.horizontalDirections
import mods.betterfoliage.util.idx
import mods.betterfoliage.util.lazy
import mods.betterfoliage.util.randomD
import mods.betterfoliage.util.randomI
import net.minecraft.client.renderer.RenderType
import net.minecraft.util.Direction.DOWN
import net.minecraft.util.ResourceLocation
import java.util.Random
object StandardCactusDiscovery : ParametrizedModelDiscovery() {
override fun processModel(ctx: ModelDiscoveryContext, params: Map<String, String>) {
ctx.addReplacement(StandardCactusKey)
ctx.sprites.add(StandardCactusModel.cactusCrossSprite)
}
}
object StandardCactusKey : HalfBakedWrapperKey() {
override fun bake(ctx: ModelBakingContext, wrapped: SpecialRenderModel) = StandardCactusModel(wrapped)
}
class CactusRenderData(val armSide: Int, val armIdx: Int, val crossIdx: Int)
class StandardCactusModel(
wrapped: SpecialRenderModel
) : HalfBakedSpecialWrapper(wrapped) {
override fun prepare(ctx: BlockCtx, random: Random): Any = when {
!Config.enabled || !Config.cactus.enabled -> Unit
else -> CactusRenderData(
armSide = random.nextInt() and 3,
armIdx = random.idx(cactusArmModels),
crossIdx = random.idx(cactusCrossModels)
)
}
val armLighting = horizontalDirections.map { LightingPreferredFace(it) }.toTypedArray()
override fun renderLayer(ctx: RenderCtxBase, data: Any, layer: RenderType) {
super.renderLayer(ctx, data, layer)
if (data is CactusRenderData) {
ctx.vertexLighter = armLighting[data.armSide]
ctx.renderQuads(cactusArmModels[data.armSide][data.armIdx])
ctx.vertexLighter = RoundLeafLighting
ctx.renderQuads(cactusCrossModels[data.crossIdx])
}
}
companion object {
val cactusCrossSprite = ResourceLocation(BetterFoliageMod.MOD_ID, "blocks/better_cactus")
val cactusArmSprites by SpriteSetDelegate(Atlas.BLOCKS) { idx ->
ResourceLocation(BetterFoliageMod.MOD_ID, "blocks/better_cactus_arm_$idx")
}
val cactusArmModels by BetterFoliage.modelManager.lazy {
val shapes = Config.cactus.let { tuftShapeSet(0.8, 0.8, 0.8, 0.2) }
val models = tuftModelSet(shapes, Color.white, -1) { cactusArmSprites[randomI()] }
horizontalDirections.map { side ->
models.transform { move(0.0625 to DOWN).rotate(Rotation.fromUp[side.ordinal]) }.buildTufts()
}.toTypedArray()
}
val cactusCrossModels by BetterFoliage.modelManager.lazy {
val models = Config.cactus.let { config ->
crossModelsRaw(64, config.size, 0.0, 0.0)
.transform { rotateZ(randomD(-config.sizeVariation, config.sizeVariation)) }
}
crossModelsTextured(models, Color.white, -1, true) { cactusCrossSprite }
}
}
}

View File

@@ -0,0 +1,143 @@
package mods.betterfoliage.render.block.vanilla
import mods.betterfoliage.BetterFoliage
import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.chunk.BlockCtx
import mods.betterfoliage.config.Config
import mods.betterfoliage.config.SALTWATER_BIOMES
import mods.betterfoliage.config.isSnow
import mods.betterfoliage.integration.ShadersModIntegration
import mods.betterfoliage.model.Color
import mods.betterfoliage.model.HalfBakedSpecialWrapper
import mods.betterfoliage.model.HalfBakedWrapperKey
import mods.betterfoliage.model.SpecialRenderData
import mods.betterfoliage.model.SpecialRenderModel
import mods.betterfoliage.model.SpriteSetDelegate
import mods.betterfoliage.model.buildTufts
import mods.betterfoliage.model.tuftModelSet
import mods.betterfoliage.model.tuftShapeSet
import mods.betterfoliage.render.lighting.LightingPreferredFace
import mods.betterfoliage.render.pipeline.Layers
import mods.betterfoliage.render.pipeline.RenderCtxBase
import mods.betterfoliage.render.pipeline.extendLayers
import mods.betterfoliage.resource.discovery.ModelBakingContext
import mods.betterfoliage.resource.discovery.ModelDiscoveryContext
import mods.betterfoliage.resource.discovery.ParametrizedModelDiscovery
import mods.betterfoliage.resource.generated.CenteredSprite
import mods.betterfoliage.util.Atlas
import mods.betterfoliage.util.Int3
import mods.betterfoliage.util.getBlockModel
import mods.betterfoliage.util.idxOrNull
import mods.betterfoliage.util.lazy
import mods.betterfoliage.util.offset
import mods.betterfoliage.util.randomI
import net.minecraft.block.material.Material
import net.minecraft.client.renderer.RenderType
import net.minecraft.util.Direction.UP
import net.minecraft.util.ResourceLocation
import java.util.Random
object StandardDirtDiscovery : ParametrizedModelDiscovery() {
override fun processModel(ctx: ModelDiscoveryContext, params: Map<String, String>) {
BetterFoliage.blockTypes.dirt.add(ctx.blockState)
ctx.addReplacement(StandardDirtKey)
ctx.blockState.block.extendLayers()
}
}
object StandardDirtKey : HalfBakedWrapperKey() {
override fun bake(ctx: ModelBakingContext, wrapped: SpecialRenderModel) = StandardDirtModel(wrapped)
}
class DirtRenderData(
val connectedGrassModel: SpecialRenderModel?,
val algaeIdx: Int?,
val reedIdx: Int?
) : SpecialRenderData {
override fun canRenderInLayer(layer: RenderType) = when {
connectedGrassModel != null && layer == Layers.connectedDirt -> true
(algaeIdx != null || reedIdx != null) && layer == Layers.tufts -> true
else -> false
}
}
class StandardDirtModel(
wrapped: SpecialRenderModel
) : HalfBakedSpecialWrapper(wrapped) {
val vanillaTuftLighting = LightingPreferredFace(UP)
override fun prepare(ctx: BlockCtx, random: Random): Any {
if (!Config.enabled) return Unit
val stateUp = ctx.state(UP)
val state2Up = ctx.state(Int3(0, 2, 0))
val isConnectedGrass = Config.connectedGrass.enabled &&
stateUp in BetterFoliage.blockTypes.grass &&
(Config.connectedGrass.snowEnabled || !state2Up.isSnow)
val isWater = stateUp.material == Material.WATER
val isDeepWater = isWater && state2Up.material == Material.WATER
val isShallowWater = isWater && state2Up.isAir
val isSaltWater = isWater && ctx.biome?.biomeCategory in SALTWATER_BIOMES
// get the actual grass model to use for connected grass rendering
// return null if the grass specifically does not want to connect
val connectedGrassModel = if (!isConnectedGrass) null else getBlockModel(stateUp).let { model ->
(model as? SpecialRenderModel)?.resolve(random)?.let { grassModel ->
if ((grassModel as? StandardGrassModel)?.key?.noConnect == true) null else grassModel
}
}
return DirtRenderData(
connectedGrassModel = connectedGrassModel,
algaeIdx = random.idxOrNull(algaeModels) { Config.algae.enabled(random) && isDeepWater && isSaltWater },
reedIdx = random.idxOrNull(reedModels) { Config.reed.enabled(random) && isShallowWater && !isSaltWater }
)
}
override fun renderLayer(ctx: RenderCtxBase, data: Any, layer: RenderType) {
if (data is DirtRenderData) {
if (data.connectedGrassModel != null) {
ctx.renderMasquerade(UP.offset) {
data.connectedGrassModel.renderLayer(ctx, ctx.state(UP), layer)
}
} else {
// render non-connected grass
super.renderLayer(ctx, data, layer)
}
if (layer == Layers.tufts) {
data.algaeIdx?.let {
ctx.vertexLighter = vanillaTuftLighting
ShadersModIntegration.grass(ctx, Config.algae.shaderWind) {
ctx.renderQuads(algaeModels[it])
}
}
data.reedIdx?.let {
ctx.vertexLighter = vanillaTuftLighting
ShadersModIntegration.grass(ctx, Config.reed.shaderWind) {
ctx.renderQuads(reedModels[it])
}
}
}
} else super.renderLayer(ctx, data, layer)
}
companion object {
val algaeSprites by SpriteSetDelegate(Atlas.BLOCKS) { idx ->
ResourceLocation(BetterFoliageMod.MOD_ID, "blocks/better_algae_$idx")
}
val reedSprites by SpriteSetDelegate(
Atlas.BLOCKS,
idFunc = { idx -> ResourceLocation(BetterFoliageMod.MOD_ID, "blocks/better_reed_$idx") },
idRegister = { id -> CenteredSprite(id, aspectHeight = 2).register(BetterFoliage.generatedPack) }
)
val algaeModels by BetterFoliage.modelManager.lazy {
val shapes = Config.algae.let { tuftShapeSet(it.size, it.heightMin, it.heightMax, it.hOffset) }
tuftModelSet(shapes, Color.white, -1) { algaeSprites[randomI()] }.buildTufts()
}
val reedModels by BetterFoliage.modelManager.lazy {
val shapes = Config.reed.let { tuftShapeSet(2.0, it.heightMin, it.heightMax, it.hOffset) }
tuftModelSet(shapes, Color.white, -1) { reedSprites[randomI()] }.buildTufts()
}
}
}

View File

@@ -0,0 +1,148 @@
package mods.betterfoliage.render.block.vanilla
import mods.betterfoliage.BetterFoliage
import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.chunk.BlockCtx
import mods.betterfoliage.config.Config
import mods.betterfoliage.config.isSnow
import mods.betterfoliage.integration.ShadersModIntegration
import mods.betterfoliage.model.Color
import mods.betterfoliage.model.HalfBakedSpecialWrapper
import mods.betterfoliage.model.HalfBakedWrapperKey
import mods.betterfoliage.model.SpecialRenderData
import mods.betterfoliage.model.SpecialRenderModel
import mods.betterfoliage.model.SpriteSetDelegate
import mods.betterfoliage.model.buildTufts
import mods.betterfoliage.model.fullCubeTextured
import mods.betterfoliage.model.tuftModelSet
import mods.betterfoliage.model.tuftShapeSet
import mods.betterfoliage.render.lighting.LightingPreferredFace
import mods.betterfoliage.render.pipeline.Layers
import mods.betterfoliage.render.pipeline.RenderCtxBase
import mods.betterfoliage.render.pipeline.extendLayers
import mods.betterfoliage.resource.discovery.ModelBakingContext
import mods.betterfoliage.resource.discovery.ModelDiscoveryContext
import mods.betterfoliage.resource.discovery.ParametrizedModelDiscovery
import mods.betterfoliage.util.Atlas
import mods.betterfoliage.util.averageHSB
import mods.betterfoliage.util.idxOrNull
import mods.betterfoliage.util.lazy
import mods.betterfoliage.util.lazyMap
import mods.betterfoliage.util.brighten
import mods.betterfoliage.util.logTextureColor
import mods.betterfoliage.util.randomI
import net.minecraft.client.renderer.RenderType
import net.minecraft.util.Direction.DOWN
import net.minecraft.util.Direction.UP
import net.minecraft.util.ResourceLocation
import org.apache.logging.log4j.Level.INFO
import java.util.Random
object StandardGrassDiscovery : ParametrizedModelDiscovery() {
override fun processModel(ctx: ModelDiscoveryContext, params: Map<String, String>) {
val texture = params.location("texture") ?: return
val tint = params.int("tint") ?: -1
val color = Atlas.BLOCKS.file(texture).averageHSB.let {
detailLogger.logTextureColor(INFO, "grass texture \"$texture\"", it)
it.brighten().asColor
}
val noConnect = params["no-connect"] == "true"
ctx.addReplacement(StandardGrassKey(texture, tint, color, noConnect))
BetterFoliage.blockTypes.grass.add(ctx.blockState)
ctx.blockState.block.extendLayers()
}
}
data class StandardGrassKey(
val sprite: ResourceLocation,
val tintIndex: Int,
val avgColor: Color,
val noConnect: Boolean
) : HalfBakedWrapperKey() {
override fun bake(ctx: ModelBakingContext, wrapped: SpecialRenderModel): SpecialRenderModel {
return StandardGrassModel(wrapped, this)
}
}
class GrassRenderData(
val isSnowed: Boolean,
val connectedGrassIdx: Int?,
val tuftIdx: Int?
): SpecialRenderData {
override fun canRenderInLayer(layer: RenderType) = when {
connectedGrassIdx != null && layer == Layers.connectedGrass -> true
tuftIdx != null && layer == Layers.tufts -> true
else -> false
}
}
class StandardGrassModel(
wrapped: SpecialRenderModel,
val key: StandardGrassKey
) : HalfBakedSpecialWrapper(wrapped) {
val tuftNormal by grassTuftMeshesNormal.delegate(key)
val tuftSnowed by grassTuftMeshesSnowed.delegate(key)
val fullBlock by grassFullBlockMeshes.delegate(key)
val tuftLighting = LightingPreferredFace(UP)
override fun prepare(ctx: BlockCtx, random: Random): Any {
if (!Config.enabled) return Unit
val stateBelow = ctx.state(DOWN)
val stateAbove = ctx.state(UP)
val isAir = ctx.isAir(UP)
val isSnowed = stateAbove.isSnow
val connected = !key.noConnect && Config.connectedGrass.enabled &&
(!isSnowed || Config.connectedGrass.snowEnabled) &&
BetterFoliage.blockTypes.run { stateBelow in grass || stateBelow in dirt }
return GrassRenderData(
isSnowed = isSnowed,
connectedGrassIdx = random.idxOrNull(if (isSnowed) snowFullBlockMeshes else fullBlock) { connected },
tuftIdx = random.idxOrNull(if (isSnowed) tuftSnowed else tuftNormal) {
Config.shortGrass.enabled(random) &&
Config.shortGrass.grassEnabled &&
(isAir || isSnowed)
}
)
}
override fun renderLayer(ctx: RenderCtxBase, data: Any, layer: RenderType) {
if (data is GrassRenderData) {
if (data.connectedGrassIdx != null) {
if (layer == Layers.connectedGrass)
ctx.renderQuads((if (data.isSnowed) snowFullBlockMeshes else fullBlock)[data.connectedGrassIdx])
} else {
super.renderLayer(ctx, data, layer)
}
if (data.tuftIdx != null && layer == Layers.tufts) {
ctx.vertexLighter = tuftLighting
ShadersModIntegration.grass(ctx, Config.shortGrass.shaderWind) {
ctx.renderQuads((if (data.isSnowed) tuftSnowed else tuftNormal)[data.tuftIdx])
}
}
} else super.renderLayer(ctx, data, layer)
}
companion object {
val grassTuftSprites by SpriteSetDelegate(Atlas.BLOCKS) { idx ->
ResourceLocation(BetterFoliageMod.MOD_ID, "blocks/better_grass_long_$idx")
}
val grassTuftShapes by BetterFoliage.modelManager.lazy {
Config.shortGrass.let { tuftShapeSet(it.size, it.heightMin, it.heightMax, it.hOffset) }
}
val grassTuftMeshesNormal = BetterFoliage.modelManager.lazyMap { key: StandardGrassKey ->
tuftModelSet(grassTuftShapes, key.avgColor, key.tintIndex) { idx -> grassTuftSprites[randomI()] }.buildTufts()
}
val grassTuftMeshesSnowed = BetterFoliage.modelManager.lazyMap { key: StandardGrassKey ->
tuftModelSet(grassTuftShapes, Color.white, -1) { idx -> grassTuftSprites[randomI()] }.buildTufts()
}
val grassFullBlockMeshes = BetterFoliage.modelManager.lazyMap { key: StandardGrassKey ->
Array(64) { fullCubeTextured(key.sprite, key.tintIndex) }
}
val snowFullBlockMeshes by BetterFoliage.modelManager.lazy {
Array(64) { fullCubeTextured(ResourceLocation("block/snow"), -1) }
}
}
}

View File

@@ -0,0 +1,101 @@
package mods.betterfoliage.render.block.vanilla
import mods.betterfoliage.BetterFoliage
import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.config.Config
import mods.betterfoliage.config.isSnow
import mods.betterfoliage.integration.ShadersModIntegration
import mods.betterfoliage.model.Color
import mods.betterfoliage.model.HalfBakedSpecialWrapper
import mods.betterfoliage.model.HalfBakedWrapperKey
import mods.betterfoliage.model.SpecialRenderModel
import mods.betterfoliage.model.SpriteSetDelegate
import mods.betterfoliage.model.crossModelsRaw
import mods.betterfoliage.model.crossModelsTextured
import mods.betterfoliage.render.lighting.RoundLeafLightingPreferUp
import mods.betterfoliage.render.particle.LeafBlockModel
import mods.betterfoliage.render.particle.LeafParticleKey
import mods.betterfoliage.render.particle.LeafParticleRegistry
import mods.betterfoliage.render.pipeline.RenderCtxBase
import mods.betterfoliage.resource.discovery.ModelBakingContext
import mods.betterfoliage.resource.discovery.ModelDiscoveryContext
import mods.betterfoliage.resource.discovery.ParametrizedModelDiscovery
import mods.betterfoliage.resource.generated.GeneratedLeafSprite
import mods.betterfoliage.util.Atlas
import mods.betterfoliage.util.averageHSB
import mods.betterfoliage.util.lazy
import mods.betterfoliage.util.lazyMap
import mods.betterfoliage.util.brighten
import mods.betterfoliage.util.logTextureColor
import mods.betterfoliage.util.saturate
import net.minecraft.client.renderer.RenderType
import net.minecraft.util.Direction.UP
import net.minecraft.util.ResourceLocation
import org.apache.logging.log4j.Level.INFO
object StandardLeafDiscovery : ParametrizedModelDiscovery() {
override fun processModel(ctx: ModelDiscoveryContext, params: Map<String, String>) {
val texture = params.location("texture") ?: return
val tint = params.int("tint") ?: -1
val color = Atlas.BLOCKS.file(texture).averageHSB.let {
detailLogger.logTextureColor(INFO, "leaf texture \"$texture\"", it)
it.brighten().asColor
}
val leafType = params["particle"] ?: "default"
val generated = GeneratedLeafSprite(texture, leafType)
.register(BetterFoliage.generatedPack)
.apply { ctx.sprites.add(this) }
detailLogger.log(INFO, " particle $leafType")
ctx.addReplacement(StandardLeafKey(generated, leafType, tint, color))
BetterFoliage.blockTypes.leaf.add(ctx.blockState)
}
}
data class StandardLeafKey(
val roundLeafTexture: ResourceLocation,
override val leafType: String,
override val tintIndex: Int,
override val avgColor: Color
) : HalfBakedWrapperKey(), LeafParticleKey {
override fun bake(ctx: ModelBakingContext, wrapped: SpecialRenderModel): SpecialRenderModel {
return StandardLeafModel(wrapped, this)
}
}
class StandardLeafModel(
model: SpecialRenderModel,
override val key: StandardLeafKey
) : HalfBakedSpecialWrapper(model), LeafBlockModel {
val leafNormal by leafModelsNormal.delegate(key)
val leafSnowed by leafModelsSnowed.delegate(key)
override fun renderLayer(ctx: RenderCtxBase, data: Any, layer: RenderType) {
ShadersModIntegration.leaves(ctx, true) {
super.renderLayer(ctx, data, layer)
if (!Config.enabled || !Config.leaves.enabled) return
ctx.vertexLighter = RoundLeafLightingPreferUp
val leafIdx = ctx.random.nextInt(64)
ctx.renderQuads(leafNormal[leafIdx])
if (Config.leaves.snowEnabled && ctx.state(UP).isSnow) ctx.renderQuads(leafSnowed[leafIdx])
}
}
companion object {
val leafSpritesSnowed by SpriteSetDelegate(Atlas.BLOCKS) { idx ->
ResourceLocation(BetterFoliageMod.MOD_ID, "blocks/better_leaves_snowed_$idx")
}
val leafModelsBase by BetterFoliage.modelManager.lazy {
Config.leaves.let { crossModelsRaw(64, it.size, it.hOffset, it.vOffset) }
}
val leafModelsNormal = BetterFoliage.modelManager.lazyMap { key: StandardLeafKey ->
// generated leaf textures naturally carry the color of their source textures
// no need to color the quad a second time
crossModelsTextured(leafModelsBase, Color.white, key.tintIndex, true) { key.roundLeafTexture }
}
val leafModelsSnowed = BetterFoliage.modelManager.lazyMap { key: StandardLeafKey ->
crossModelsTextured(leafModelsBase, Color.white, -1, false) { leafSpritesSnowed[it].name }
}
}
}

View File

@@ -0,0 +1,88 @@
package mods.betterfoliage.render.block.vanilla
import mods.betterfoliage.BetterFoliage
import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.chunk.BlockCtx
import mods.betterfoliage.config.Config
import mods.betterfoliage.integration.ShadersModIntegration
import mods.betterfoliage.model.Color
import mods.betterfoliage.model.HalfBakedSpecialWrapper
import mods.betterfoliage.model.HalfBakedWrapperKey
import mods.betterfoliage.model.SpecialRenderModel
import mods.betterfoliage.model.SpriteSetDelegate
import mods.betterfoliage.model.buildTufts
import mods.betterfoliage.model.transform
import mods.betterfoliage.model.tuftModelSet
import mods.betterfoliage.model.tuftShapeSet
import mods.betterfoliage.render.pipeline.RenderCtxBase
import mods.betterfoliage.resource.discovery.ModelBakingContext
import mods.betterfoliage.resource.discovery.ModelDiscoveryContext
import mods.betterfoliage.resource.discovery.ParametrizedModelDiscovery
import mods.betterfoliage.util.Atlas
import mods.betterfoliage.util.idx
import mods.betterfoliage.util.idxOrNull
import mods.betterfoliage.util.lazy
import net.minecraft.client.renderer.RenderType
import net.minecraft.util.Direction.DOWN
import net.minecraft.util.ResourceLocation
import java.util.Random
object StandardLilypadDiscovery : ParametrizedModelDiscovery() {
override fun processModel(ctx: ModelDiscoveryContext, params: Map<String, String>) {
ctx.addReplacement(StandardLilypadKey)
}
}
object StandardLilypadKey : HalfBakedWrapperKey() {
override fun bake(ctx: ModelBakingContext, wrapped: SpecialRenderModel) = StandardLilypadModel(wrapped)
}
class LilypadRenderData(
val rootIdx: Int,
val flowerIdx: Int?
)
class StandardLilypadModel(
wrapped: SpecialRenderModel
) : HalfBakedSpecialWrapper(wrapped) {
override fun prepare(ctx: BlockCtx, random: Random): Any {
if (!Config.enabled) return Unit
return LilypadRenderData(
rootIdx = random.idx(lilypadRootModels),
flowerIdx = random.idxOrNull(lilypadFlowerModels) { Config.lilypad.enabled(random) }
)
}
override fun renderLayer(ctx: RenderCtxBase, data: Any, layer: RenderType) {
ctx.checkSides = false
super.renderLayer(ctx, data, layer)
if (data is LilypadRenderData) {
data.flowerIdx?.let { ctx.renderQuads(lilypadFlowerModels[it]) }
ShadersModIntegration.grass(ctx, Config.lilypad.shaderWind) {
ctx.renderQuads(lilypadRootModels[data.rootIdx])
}
}
}
companion object {
val lilypadRootSprites by SpriteSetDelegate(Atlas.BLOCKS) { idx ->
ResourceLocation(BetterFoliageMod.MOD_ID, "blocks/better_lilypad_roots_$idx")
}
val lilypadFlowerSprites by SpriteSetDelegate(Atlas.BLOCKS) { idx ->
ResourceLocation(BetterFoliageMod.MOD_ID, "blocks/better_lilypad_flower_$idx")
}
val lilypadRootModels by BetterFoliage.modelManager.lazy {
val shapes = tuftShapeSet(1.0, 1.0, 1.0, Config.lilypad.hOffset)
tuftModelSet(shapes, Color.white, -1) { lilypadRootSprites[it] }
.transform { move(2.0 to DOWN) }
.buildTufts()
}
val lilypadFlowerModels by BetterFoliage.modelManager.lazy {
val shapes = tuftShapeSet(0.5, 0.5, 0.5, Config.lilypad.hOffset)
tuftModelSet(shapes, Color.white, -1) { lilypadFlowerSprites[it] }
.transform { move(1.0 to DOWN) }
.buildTufts()
}
}
}

View File

@@ -0,0 +1,99 @@
package mods.betterfoliage.render.block.vanilla
import mods.betterfoliage.BetterFoliage
import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.chunk.BlockCtx
import mods.betterfoliage.config.Config
import mods.betterfoliage.model.Color
import mods.betterfoliage.model.HalfBakedSpecialWrapper
import mods.betterfoliage.model.HalfBakedWrapperKey
import mods.betterfoliage.model.SpecialRenderData
import mods.betterfoliage.model.SpecialRenderModel
import mods.betterfoliage.model.SpriteSetDelegate
import mods.betterfoliage.model.buildTufts
import mods.betterfoliage.model.tuftModelSet
import mods.betterfoliage.model.tuftShapeSet
import mods.betterfoliage.render.lighting.LightingPreferredFace
import mods.betterfoliage.render.pipeline.Layers
import mods.betterfoliage.render.pipeline.RenderCtxBase
import mods.betterfoliage.render.pipeline.extendLayers
import mods.betterfoliage.resource.discovery.ModelBakingContext
import mods.betterfoliage.resource.discovery.ModelDiscoveryContext
import mods.betterfoliage.resource.discovery.ParametrizedModelDiscovery
import mods.betterfoliage.util.Atlas
import mods.betterfoliage.util.averageHSB
import mods.betterfoliage.util.idxOrNull
import mods.betterfoliage.util.lazy
import mods.betterfoliage.util.lazyMap
import mods.betterfoliage.util.brighten
import mods.betterfoliage.util.randomI
import net.minecraft.client.renderer.RenderType
import net.minecraft.util.Direction
import net.minecraft.util.ResourceLocation
import java.util.Random
object StandardMyceliumDiscovery : ParametrizedModelDiscovery() {
override fun processModel(ctx: ModelDiscoveryContext, params: Map<String, String>) {
val texture = params.location("texture") ?: return
val tint = params.int("tint") ?: -1
val color = Atlas.BLOCKS.file(texture).averageHSB.brighten(multiplier = 1.5f).asColor
ctx.addReplacement(StandardMyceliumKey(texture, tint, color))
ctx.blockState.block.extendLayers()
}
}
data class StandardMyceliumKey(
val sprite: ResourceLocation,
val tintIndex: Int,
val avgColor: Color,
) : HalfBakedWrapperKey() {
override fun bake(ctx: ModelBakingContext, wrapped: SpecialRenderModel): SpecialRenderModel {
return StandardMyceliumModel(wrapped, this)
}
}
class MyceliumRenderData(
val tuftIndex: Int?
) : SpecialRenderData {
override fun canRenderInLayer(layer: RenderType) = tuftIndex != null && layer == Layers.tufts
}
class StandardMyceliumModel(
wrapped: SpecialRenderModel,
key: StandardMyceliumKey
) : HalfBakedSpecialWrapper(wrapped) {
val tuftModels by myceliumTuftModels.delegate(key)
val tuftLighting = LightingPreferredFace(Direction.UP)
override fun prepare(ctx: BlockCtx, random: Random): Any {
if (!Config.enabled) return Unit
return MyceliumRenderData(
random.idxOrNull(tuftModels) {
Config.shortGrass.enabled(random) &&
Config.shortGrass.myceliumEnabled &&
ctx.state(Direction.UP).isAir(ctx.world, ctx.pos)
}
)
}
override fun renderLayer(ctx: RenderCtxBase, data: Any, layer: RenderType) {
super.renderLayer(ctx, data, layer)
if (data is MyceliumRenderData && data.tuftIndex != null && layer == Layers.tufts) {
ctx.vertexLighter = tuftLighting
ctx.renderQuads(tuftModels[data.tuftIndex])
}
}
companion object {
val myceliumTuftSprites by SpriteSetDelegate(Atlas.BLOCKS) { idx ->
ResourceLocation(BetterFoliageMod.MOD_ID, "blocks/better_mycel_$idx")
}
val myceliumTuftShapes by BetterFoliage.modelManager.lazy {
Config.shortGrass.let { tuftShapeSet(it.size, it.heightMin, it.heightMax, it.hOffset) }
}
val myceliumTuftModels = BetterFoliage.modelManager.lazyMap { key: StandardMyceliumKey ->
tuftModelSet(myceliumTuftShapes, key.avgColor, key.tintIndex) { idx -> myceliumTuftSprites[randomI()] }.buildTufts()
}
}
}

View File

@@ -0,0 +1,86 @@
package mods.betterfoliage.render.block.vanilla
import mods.betterfoliage.BetterFoliage
import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.chunk.BlockCtx
import mods.betterfoliage.config.Config
import mods.betterfoliage.model.Color
import mods.betterfoliage.model.HalfBakedSpecialWrapper
import mods.betterfoliage.model.HalfBakedWrapperKey
import mods.betterfoliage.model.SpecialRenderData
import mods.betterfoliage.model.SpecialRenderModel
import mods.betterfoliage.model.SpriteSetDelegate
import mods.betterfoliage.model.buildTufts
import mods.betterfoliage.model.transform
import mods.betterfoliage.model.tuftModelSet
import mods.betterfoliage.model.tuftShapeSet
import mods.betterfoliage.render.lighting.LightingPreferredFace
import mods.betterfoliage.render.pipeline.Layers
import mods.betterfoliage.render.pipeline.RenderCtxBase
import mods.betterfoliage.render.pipeline.extendLayers
import mods.betterfoliage.resource.discovery.ModelBakingContext
import mods.betterfoliage.resource.discovery.ModelDiscoveryContext
import mods.betterfoliage.resource.discovery.ParametrizedModelDiscovery
import mods.betterfoliage.util.Atlas
import mods.betterfoliage.util.Rotation
import mods.betterfoliage.util.idxOrNull
import mods.betterfoliage.util.lazy
import mods.betterfoliage.util.randomI
import net.minecraft.client.renderer.RenderType
import net.minecraft.util.Direction.DOWN
import net.minecraft.util.ResourceLocation
import java.util.Random
object StandardNetherrackDiscovery : ParametrizedModelDiscovery() {
override fun processModel(ctx: ModelDiscoveryContext, params: Map<String, String>) {
ctx.addReplacement(StandardNetherrackKey)
ctx.blockState.block.extendLayers()
}
}
object StandardNetherrackKey : HalfBakedWrapperKey() {
override fun bake(ctx: ModelBakingContext, wrapped: SpecialRenderModel) = StandardNetherrackModel(wrapped)
}
class NetherrackRenderData(
val tuftIndex: Int?
): SpecialRenderData {
override fun canRenderInLayer(layer: RenderType) = tuftIndex != null && layer == Layers.tufts
}
class StandardNetherrackModel(
wrapped: SpecialRenderModel
) : HalfBakedSpecialWrapper(wrapped) {
val tuftLighting = LightingPreferredFace(DOWN)
override fun prepare(ctx: BlockCtx, random: Random): Any {
if (!Config.enabled) return Unit
return NetherrackRenderData(
random.idxOrNull(netherrackTuftModels) {
Config.netherrack.enabled &&
ctx.isAir(DOWN)
}
)
}
override fun renderLayer(ctx: RenderCtxBase, data: Any, layer: RenderType) {
super.renderLayer(ctx, data, layer)
if (data is NetherrackRenderData && data.tuftIndex != null && layer == Layers.tufts) {
ctx.vertexLighter = tuftLighting
ctx.renderQuads(netherrackTuftModels[data.tuftIndex])
}
}
companion object {
val netherrackTuftSprites by SpriteSetDelegate(Atlas.BLOCKS) { idx ->
ResourceLocation(BetterFoliageMod.MOD_ID, "blocks/better_netherrack_$idx")
}
val netherrackTuftModels by BetterFoliage.modelManager.lazy {
val shapes = Config.netherrack.let { tuftShapeSet(it.size, it.heightMin, it.heightMax, it.hOffset) }
tuftModelSet(shapes, Color.white, -1) { netherrackTuftSprites[randomI()] }
.transform { rotate(Rotation.fromUp[DOWN.ordinal]).rotateUV(2) }
.buildTufts()
}
}
}

View File

@@ -0,0 +1,93 @@
package mods.betterfoliage.render.block.vanilla
import mods.betterfoliage.BetterFoliage
import mods.betterfoliage.config.ACCEPTED_ROUND_LOG_MATERIALS
import mods.betterfoliage.config.Config
import mods.betterfoliage.model.HalfBakedWrapperKey
import mods.betterfoliage.model.SpecialRenderModel
import mods.betterfoliage.render.column.ColumnBlockKey
import mods.betterfoliage.render.column.ColumnMeshSet
import mods.betterfoliage.render.column.ColumnModelBase
import mods.betterfoliage.render.column.ColumnRenderLayer
import mods.betterfoliage.resource.discovery.ModelBakingContext
import mods.betterfoliage.resource.discovery.ModelBakingKey
import mods.betterfoliage.resource.discovery.ModelDiscoveryContext
import mods.betterfoliage.resource.discovery.ParametrizedModelDiscovery
import mods.betterfoliage.util.Atlas
import mods.betterfoliage.util.lazyMap
import mods.betterfoliage.util.tryDefault
import net.minecraft.block.BlockState
import net.minecraft.block.RotatedPillarBlock
import net.minecraft.util.Direction.Axis
import net.minecraft.util.ResourceLocation
import org.apache.logging.log4j.Level.INFO
interface RoundLogKey : ColumnBlockKey, ModelBakingKey {
val barkSprite: ResourceLocation
val endSprite: ResourceLocation
}
object RoundLogOverlayLayer : ColumnRenderLayer() {
override fun getColumnKey(state: BlockState) = BetterFoliage.blockTypes.getTypedOrNull<ColumnBlockKey>(state)
override val connectSolids: Boolean get() = Config.roundLogs.connectSolids
override val lenientConnect: Boolean get() = Config.roundLogs.lenientConnect
override val defaultToY: Boolean get() = Config.roundLogs.defaultY
}
object StandardRoundLogDiscovery : ParametrizedModelDiscovery() {
override fun processModel(ctx: ModelDiscoveryContext, params: Map<String, String>) {
val barkSprite = params.location("texture-side") ?: return
val endSprite = params.location("texture-end") ?: return
val axis = getAxis(ctx.blockState)
detailLogger.log(INFO, " axis $axis, material ${ctx.blockState.material}")
if (!Config.roundLogs.plantsOnly || ctx.blockState.material in ACCEPTED_ROUND_LOG_MATERIALS)
ctx.addReplacement(StandardRoundLogKey(axis, barkSprite, endSprite))
}
fun getAxis(state: BlockState): Axis? {
val axis = tryDefault(null) { state.getValue(RotatedPillarBlock.AXIS).toString() } ?:
state.values.entries.find { it.key.name.toLowerCase() == "axis" }?.value?.toString()
return when (axis) {
"x" -> Axis.X
"y" -> Axis.Y
"z" -> Axis.Z
else -> null
}
}
}
data class StandardRoundLogKey(
override val axis: Axis?,
override val barkSprite: ResourceLocation,
override val endSprite: ResourceLocation
) : RoundLogKey, HalfBakedWrapperKey() {
override fun bake(ctx: ModelBakingContext, wrapped: SpecialRenderModel) = StandardRoundLogModel(this, wrapped)
}
class StandardRoundLogModel(
val key: StandardRoundLogKey,
wrapped: SpecialRenderModel
) : ColumnModelBase(wrapped) {
override val enabled: Boolean get() = Config.enabled && Config.roundLogs.enabled
override val overlayLayer: ColumnRenderLayer get() = RoundLogOverlayLayer
override val connectPerpendicular: Boolean get() = Config.roundLogs.connectPerpendicular
val modelSet by modelSets.delegate(key)
override fun getMeshSet(axis: Axis, quadrant: Int) = modelSet
companion object {
val modelSets = BetterFoliage.modelManager.lazyMap { key: StandardRoundLogKey ->
val barkSprite = Atlas.BLOCKS[key.barkSprite]
val endSprite = Atlas.BLOCKS[key.endSprite]
Config.roundLogs.let { config ->
ColumnMeshSet(
config.radiusSmall, config.radiusLarge, config.zProtection,
key.axis ?: Axis.Y,
barkSprite, barkSprite,
endSprite, endSprite
)
}
}
}
}

View File

@@ -0,0 +1,129 @@
package mods.betterfoliage.render.block.vanilla
import mods.betterfoliage.BetterFoliage
import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.chunk.BlockCtx
import mods.betterfoliage.config.Config
import mods.betterfoliage.config.SALTWATER_BIOMES
import mods.betterfoliage.model.Color
import mods.betterfoliage.model.HalfBakedSpecialWrapper
import mods.betterfoliage.model.HalfBakedWrapperKey
import mods.betterfoliage.model.Quad
import mods.betterfoliage.model.SpecialRenderData
import mods.betterfoliage.model.SpecialRenderModel
import mods.betterfoliage.model.SpriteSetDelegate
import mods.betterfoliage.model.bake
import mods.betterfoliage.model.buildTufts
import mods.betterfoliage.model.transform
import mods.betterfoliage.model.tuftModelSet
import mods.betterfoliage.model.tuftShapeSet
import mods.betterfoliage.render.lighting.LightingPreferredFace
import mods.betterfoliage.render.pipeline.Layers
import mods.betterfoliage.render.pipeline.RenderCtxBase
import mods.betterfoliage.render.pipeline.extendLayers
import mods.betterfoliage.resource.discovery.ModelBakingContext
import mods.betterfoliage.resource.discovery.ModelDiscoveryContext
import mods.betterfoliage.resource.discovery.ParametrizedModelDiscovery
import mods.betterfoliage.util.Atlas
import mods.betterfoliage.util.Rotation
import mods.betterfoliage.util.allDirections
import mods.betterfoliage.util.get
import mods.betterfoliage.util.idx
import mods.betterfoliage.util.lazy
import mods.betterfoliage.util.mapArray
import mods.betterfoliage.util.randomB
import mods.betterfoliage.util.randomD
import mods.betterfoliage.util.randomI
import net.minecraft.block.material.Material
import net.minecraft.client.renderer.RenderType
import net.minecraft.util.Direction
import net.minecraft.util.Direction.UP
import net.minecraft.util.ResourceLocation
import java.util.Random
object StandardSandDiscovery : ParametrizedModelDiscovery() {
override fun processModel(ctx: ModelDiscoveryContext, params: Map<String, String>) {
ctx.addReplacement(StandardSandKey)
ctx.blockState.block.extendLayers()
}
}
object StandardSandKey : HalfBakedWrapperKey() {
override fun bake(ctx: ModelBakingContext, wrapped: SpecialRenderModel) = StandardSandModel(wrapped)
}
class SandRenderData(
val crustIdx: Array<Int?>,
val tuftIdx: Array<Int?>
): SpecialRenderData {
override fun canRenderInLayer(layer: RenderType) = when {
(crustIdx.any { it != null } || tuftIdx.any { it != null }) && layer == Layers.coral -> true
else -> false
}
}
class StandardSandModel(
wrapped: SpecialRenderModel
) : HalfBakedSpecialWrapper(wrapped) {
val coralLighting = Direction.values().mapArray { LightingPreferredFace(it) }
override fun prepare(ctx: BlockCtx, random: Random): Any {
if (!Config.enabled) return Unit
if (!Config.coral.enabled(random)) return Unit
if (ctx.biome?.biomeCategory !in SALTWATER_BIOMES) return Unit
val crustIdx = Array<Int?>(6) { null }
val tuftIdx = Array<Int?>(6) { null }
allDirections.filter { random.nextInt(64) < Config.coral.chance }.forEach { face ->
val isWater = ctx.state(face).material == Material.WATER
val isDeepWater = isWater && ctx.offset(face).state(UP).material == Material.WATER
if (isDeepWater) {
crustIdx[face.ordinal] = random.idx(coralCrustModels)
tuftIdx[face.ordinal] = random.idx(coralTuftModels)
}
}
return SandRenderData(crustIdx, tuftIdx)
}
override fun renderLayer(ctx: RenderCtxBase, data: Any, layer: RenderType) {
super.renderLayer(ctx, data, layer)
if (data is SandRenderData && layer == Layers.coral) {
for (face in 0 until 6) {
ctx.vertexLighter = coralLighting[face]
data.crustIdx[face]?.let { ctx.renderQuads(coralCrustModels[face][it]) }
data.tuftIdx[face]?.let { ctx.renderQuads(coralTuftModels[face][it]) }
}
}
}
companion object {
val coralTuftSprites by SpriteSetDelegate(Atlas.BLOCKS) { idx ->
ResourceLocation(BetterFoliageMod.MOD_ID, "blocks/better_coral_$idx")
}
val coralCrustSprites by SpriteSetDelegate(Atlas.BLOCKS) { idx ->
ResourceLocation(BetterFoliageMod.MOD_ID, "blocks/better_crust_$idx")
}
val coralTuftModels by BetterFoliage.modelManager.lazy {
val shapes = Config.coral.let { tuftShapeSet(it.size, 1.0, 1.0, it.hOffset) }
allDirections.mapArray { face ->
tuftModelSet(shapes, Color.white, -1) { coralTuftSprites[randomI()] }
.transform { rotate(Rotation.fromUp[face]) }
.buildTufts()
}
}
val coralCrustModels by BetterFoliage.modelManager.lazy {
allDirections.map { face ->
Array(64) { idx ->
listOf(
Quad.horizontalRectangle(x1 = -0.5, x2 = 0.5, z1 = -0.5, z2 = 0.5, y = 0.0)
.scale(Config.coral.crustSize)
.move(0.5 + randomD(0.01, Config.coral.vOffset) to UP)
.rotate(Rotation.fromUp[face])
.mirrorUV(randomB(), randomB()).rotateUV(randomI(max = 4))
.sprite(coralCrustSprites[idx]).colorAndIndex(null)
).bake(applyDiffuseLighting = false)
}
}.toTypedArray()
}
}
}

View File

@@ -0,0 +1,166 @@
package mods.betterfoliage.render.column
import mods.betterfoliage.config.Config
import mods.betterfoliage.model.Quad
import mods.betterfoliage.model.UV
import mods.betterfoliage.model.Vertex
import mods.betterfoliage.model.bake
import mods.betterfoliage.render.column.ColumnLayerData.SpecialRender.QuadrantType
import mods.betterfoliage.render.column.ColumnLayerData.SpecialRender.QuadrantType.INVISIBLE
import mods.betterfoliage.render.column.ColumnLayerData.SpecialRender.QuadrantType.LARGE_RADIUS
import mods.betterfoliage.render.column.ColumnLayerData.SpecialRender.QuadrantType.SMALL_RADIUS
import mods.betterfoliage.render.column.ColumnLayerData.SpecialRender.QuadrantType.SQUARE
import mods.betterfoliage.util.Double3
import mods.betterfoliage.util.Rotation
import net.minecraft.client.renderer.texture.TextureAtlasSprite
import net.minecraft.util.Direction.Axis
import net.minecraft.util.Direction.EAST
import net.minecraft.util.Direction.SOUTH
import net.minecraft.util.Direction.UP
/**
* Collection of dynamically generated meshes used to render rounded columns.
*/
class ColumnMeshSet(
radiusSmall: Double,
radiusLarge: Double,
zProtection: Double,
val axis: Axis,
val spriteLeft: TextureAtlasSprite,
val spriteRight: TextureAtlasSprite,
val spriteTop: TextureAtlasSprite,
val spriteBottom: TextureAtlasSprite
) {
protected fun sideRounded(radius: Double, yBottom: Double, yTop: Double): List<Quad> {
val halfRadius = radius * 0.5
return listOf(
// left side of the diagonal
Quad.verticalRectangle(0.0, 0.5, 0.5 - radius, 0.5, yBottom, yTop).clampUV(minU = 0.0, maxU = 0.5 - radius),
Quad.verticalRectangle(0.5 - radius, 0.5, 0.5 - halfRadius, 0.5 - halfRadius, yBottom, yTop).clampUV(minU = 0.5 - radius),
// right side of the diagonal
Quad.verticalRectangle(0.5 - halfRadius, 0.5 - halfRadius, 0.5, 0.5 - radius, yBottom, yTop).clampUV(maxU = radius - 0.5),
Quad.verticalRectangle(0.5, 0.5 - radius, 0.5, 0.0, yBottom, yTop).clampUV(minU = radius - 0.5, maxU = 0.0)
)
}
protected fun sideRoundedTransition(radiusBottom: Double, radiusTop: Double, yBottom: Double, yTop: Double): List<Quad> {
val ySplit = 0.5 * (yBottom + yTop)
val modelTop = sideRounded(radiusTop, yBottom, yTop)
val modelBottom = sideRounded(radiusBottom, yBottom, yTop)
return (modelBottom zip modelTop).map { (quadBottom, quadTop) ->
Quad.mix(quadBottom, quadTop) { vBottom, vTop -> if (vBottom.xyz.y < ySplit) vBottom.copy() else vTop.copy() }
}
}
protected fun sideSquare(yBottom: Double, yTop: Double) = listOf(
Quad.verticalRectangle(0.0, 0.5, 0.5, 0.5, yBottom, yTop).clampUV(minU = 0.0),
Quad.verticalRectangle(0.5, 0.5, 0.5, 0.0, yBottom, yTop).clampUV(maxU = 0.0)
)
protected fun lidRounded(radius: Double, y: Double, isBottom: Boolean) = Array(4) { quadrant ->
val rotation = baseRotation(axis) + quadrantRotations[quadrant]
val v1 = Vertex(Double3(0.0, y, 0.0), UV(0.0, 0.0))
val v2 = Vertex(Double3(0.0, y, 0.5), UV(0.0, 0.5))
val v3 = Vertex(Double3(0.5 - radius, y, 0.5), UV(0.5 - radius, 0.5))
val v4 = Vertex(Double3(0.5 - radius * 0.5, y, 0.5 - radius * 0.5), UV(0.5, 0.5))
val v5 = Vertex(Double3(0.5, y, 0.5 - radius), UV(0.5, 0.5 - radius))
val v6 = Vertex(Double3(0.5, y, 0.0), UV(0.5, 0.0))
listOf(Quad(v1, v2, v3, v4), Quad(v1, v4, v5, v6))
.map { it.cycleVertices(if (isBottom xor Config.nVidia) 0 else 1) }
.map { it.rotate(rotation).rotateUV(quadrant) }
.map { it.sprite(if (isBottom) spriteBottom else spriteTop) }
.map { if (isBottom) it.flipped else it }
}
protected fun lidSquare(y: Double, isBottom: Boolean) = Array(4) { quadrant ->
val rotation = baseRotation(axis) + quadrantRotations[quadrant]
listOf(
Quad.horizontalRectangle(x1 = 0.0, x2 = 0.5, z1 = 0.0, z2 = 0.5, y = y).clampUV(minU = 0.0, minV = 0.0)
.rotate(rotation).rotateUV(quadrant)
.sprite(if (isBottom) spriteBottom else spriteTop)
.let { if (isBottom) it.flipped else it }
)
}
protected val zProtectionScale = zProtection.let { Double3(it, 1.0, it) }
protected fun List<Quad>.extendTop(size: Double) = map { q -> q.clampUV(minV = 0.5 - size).transformV { v ->
if (v.xyz.y > 0.501) v.copy(xyz = v.xyz * zProtectionScale) else v }
}
protected fun List<Quad>.extendBottom(size: Double) = map { q -> q.clampUV(maxV = -0.5 + size).transformV { v ->
if (v.xyz.y < -0.501) v.copy(xyz = v.xyz * zProtectionScale) else v }
}
protected fun List<Quad>.buildSides(quadsPerSprite: Int) = Array(4) { quadrant ->
val rotation = baseRotation(axis) + quadrantRotations[quadrant]
this.map { it.rotate(rotation) }
.mapIndexed { idx, q -> if (idx % (2 * quadsPerSprite) >= quadsPerSprite) q.sprite(spriteRight) else q.sprite(spriteLeft) }
.bake(false)
}
companion object {
fun baseRotation(axis: Axis) = when(axis) {
Axis.X -> Rotation.fromUp[EAST.ordinal]
Axis.Y -> Rotation.fromUp[UP.ordinal]
Axis.Z -> Rotation.fromUp[SOUTH.ordinal]
}
val quadrantRotations = Array(4) { Rotation.rot90[UP.ordinal] * it }
}
//
// Mesh definitions
// 4-element arrays hold prebuild meshes for each of the rotations around the axis
//
val sideSquare = sideSquare(-0.5, 0.5).buildSides(quadsPerSprite = 1)
val sideRoundSmall = sideRounded(radiusSmall, -0.5, 0.5).buildSides(quadsPerSprite = 2)
val sideRoundLarge = sideRounded(radiusLarge, -0.5, 0.5).buildSides(quadsPerSprite = 2)
val sideExtendTopSquare = sideSquare(0.5, 0.5 + radiusLarge).extendTop(radiusLarge).buildSides(quadsPerSprite = 1)
val sideExtendTopRoundSmall = sideRounded(radiusSmall, 0.5, 0.5 + radiusLarge).extendTop(radiusLarge).buildSides(quadsPerSprite = 2)
val sideExtendTopRoundLarge = sideRounded(radiusLarge, 0.5, 0.5 + radiusLarge).extendTop(radiusLarge).buildSides(quadsPerSprite = 2)
val sideExtendBottomSquare = sideSquare(-0.5 - radiusLarge, -0.5).extendBottom(radiusLarge).buildSides(quadsPerSprite = 1)
val sideExtendBottomRoundSmall = sideRounded(radiusSmall, -0.5 - radiusLarge, -0.5).extendBottom(radiusLarge).buildSides(quadsPerSprite = 2)
val sideExtendBottomRoundLarge = sideRounded(radiusLarge, -0.5 - radiusLarge, -0.5).extendBottom(radiusLarge).buildSides(quadsPerSprite = 2)
val lidTopSquare = lidSquare(0.5, false).bake(false)
val lidTopRoundSmall = lidRounded(radiusSmall, 0.5, false).bake(false)
val lidTopRoundLarge = lidRounded(radiusLarge, 0.5, false).bake(false)
val lidBottomSquare = lidSquare(-0.5, true).bake(false)
val lidBottomRoundSmall = lidRounded(radiusSmall, -0.5, true).bake(false)
val lidBottomRoundLarge = lidRounded(radiusLarge, -0.5, true).bake(false)
val transitionTop = sideRoundedTransition(radiusLarge, radiusSmall, -0.5, 0.5).buildSides(quadsPerSprite = 2)
val transitionBottom = sideRoundedTransition(radiusSmall, radiusLarge, -0.5, 0.5).buildSides(quadsPerSprite = 2)
//
// Helper fuctions for lids (block ends)
//
fun flatTop(quadrantTypes: Array<QuadrantType>, quadrant: Int) = when(quadrantTypes[quadrant]) {
SMALL_RADIUS -> lidTopRoundSmall[quadrant]
LARGE_RADIUS -> lidTopRoundLarge[quadrant]
SQUARE -> lidTopSquare[quadrant]
INVISIBLE -> lidTopSquare[quadrant]
}
fun flatBottom(quadrantTypes: Array<QuadrantType>, quadrant: Int) = when(quadrantTypes[quadrant]) {
SMALL_RADIUS -> lidBottomRoundSmall[quadrant]
LARGE_RADIUS -> lidBottomRoundLarge[quadrant]
SQUARE -> lidBottomSquare[quadrant]
INVISIBLE -> lidBottomSquare[quadrant]
}
fun extendTop(quadrantTypes: Array<QuadrantType>, quadrant: Int) = when(quadrantTypes[quadrant]) {
SMALL_RADIUS -> sideExtendTopRoundSmall[quadrant]
LARGE_RADIUS -> sideExtendTopRoundLarge[quadrant]
SQUARE -> sideExtendTopSquare[quadrant]
INVISIBLE -> sideExtendTopSquare[quadrant]
}
fun extendBottom(quadrantTypes: Array<QuadrantType>, quadrant: Int) = when(quadrantTypes[quadrant]) {
SMALL_RADIUS -> sideExtendBottomRoundSmall[quadrant]
LARGE_RADIUS -> sideExtendBottomRoundLarge[quadrant]
SQUARE -> sideExtendBottomSquare[quadrant]
INVISIBLE -> sideExtendBottomSquare[quadrant]
}
}

View File

@@ -0,0 +1,114 @@
package mods.betterfoliage.render.column
import mods.betterfoliage.chunk.ChunkOverlayManager
import mods.betterfoliage.model.HalfBakedSpecialWrapper
import mods.betterfoliage.model.SpecialRenderModel
import mods.betterfoliage.render.column.ColumnLayerData.NormalRender
import mods.betterfoliage.render.column.ColumnLayerData.SpecialRender.BlockType.NONSOLID
import mods.betterfoliage.render.column.ColumnLayerData.SpecialRender.BlockType.PARALLEL
import mods.betterfoliage.render.column.ColumnLayerData.SpecialRender.BlockType.PERPENDICULAR
import mods.betterfoliage.render.column.ColumnLayerData.SpecialRender.QuadrantType.INVISIBLE
import mods.betterfoliage.render.column.ColumnLayerData.SpecialRender.QuadrantType.LARGE_RADIUS
import mods.betterfoliage.render.column.ColumnLayerData.SpecialRender.QuadrantType.SMALL_RADIUS
import mods.betterfoliage.render.column.ColumnLayerData.SpecialRender.QuadrantType.SQUARE
import mods.betterfoliage.render.lighting.ColumnLighting
import mods.betterfoliage.render.pipeline.RenderCtxBase
import net.minecraft.client.renderer.RenderType
import net.minecraft.util.Direction.Axis
abstract class ColumnModelBase(
wrapped: SpecialRenderModel
) : HalfBakedSpecialWrapper(wrapped) {
abstract val enabled: Boolean
abstract val overlayLayer: ColumnRenderLayer
abstract val connectPerpendicular: Boolean
abstract fun getMeshSet(axis: Axis, quadrant: Int): ColumnMeshSet
override fun renderLayer(ctx: RenderCtxBase, data: Any, layer: RenderType) {
if (!enabled) return super.renderLayer(ctx, data, layer)
val roundLog = overlayLayer[ctx]
when(roundLog) {
ColumnLayerData.SkipRender -> return
NormalRender -> return super.renderLayer(ctx, data, layer)
ColumnLayerData.ResolveError, null -> {
return super.renderLayer(ctx, data, layer)
}
}
// if log axis is not defined and "Default to vertical" config option is not set, render normally
if ((roundLog as ColumnLayerData.SpecialRender).column.axis == null && !overlayLayer.defaultToY) {
return super.renderLayer(ctx, data, layer)
}
ctx.vertexLighter = ColumnLighting
val axis = roundLog.column.axis ?: Axis.Y
val baseRotation = ColumnMeshSet.baseRotation(axis)
ColumnMeshSet.quadrantRotations.forEachIndexed { idx, quadrantRotation ->
// set rotation for the current quadrant
val rotation = baseRotation + quadrantRotation
val meshSet = getMeshSet(axis, idx)
// disallow sharp discontinuities in the chamfer radius, or tapering-in where inappropriate
if (roundLog.quadrants[idx] == LARGE_RADIUS &&
roundLog.upType == PARALLEL && roundLog.quadrantsTop[idx] != LARGE_RADIUS &&
roundLog.downType == PARALLEL && roundLog.quadrantsBottom[idx] != LARGE_RADIUS) {
roundLog.quadrants[idx] = SMALL_RADIUS
}
// select meshes for current quadrant based on connectivity rules
val sideMesh = when (roundLog.quadrants[idx]) {
SMALL_RADIUS -> meshSet.sideRoundSmall[idx]
LARGE_RADIUS -> if (roundLog.upType == PARALLEL && roundLog.quadrantsTop[idx] == SMALL_RADIUS) meshSet.transitionTop[idx]
else if (roundLog.downType == PARALLEL && roundLog.quadrantsBottom[idx] == SMALL_RADIUS) meshSet.transitionBottom[idx]
else meshSet.sideRoundLarge[idx]
SQUARE -> meshSet.sideSquare[idx]
else -> null
}
val upMesh = when(roundLog.upType) {
NONSOLID -> meshSet.flatTop(roundLog.quadrants, idx)
PERPENDICULAR -> {
if (!connectPerpendicular) {
meshSet.flatTop(roundLog.quadrants, idx)
} else {
meshSet.extendTop(roundLog.quadrants, idx)
}
}
PARALLEL -> {
if (roundLog.quadrants[idx] discontinuousWith roundLog.quadrantsTop[idx] &&
roundLog.quadrants[idx].let { it == SQUARE || it == INVISIBLE } )
meshSet.flatTop(roundLog.quadrants, idx)
else null
}
else -> null
}
val downMesh = when(roundLog.downType) {
NONSOLID -> meshSet.flatBottom(roundLog.quadrants, idx)
PERPENDICULAR -> {
if (!connectPerpendicular) {
meshSet.flatBottom(roundLog.quadrants, idx)
} else {
meshSet.extendBottom(roundLog.quadrants, idx)
}
}
PARALLEL -> {
if (roundLog.quadrants[idx] discontinuousWith roundLog.quadrantsBottom[idx] &&
roundLog.quadrants[idx].let { it == SQUARE || it == INVISIBLE } )
meshSet.flatBottom(roundLog.quadrants, idx)
else null
}
else -> null
}
// render
sideMesh?.let { ctx.renderQuads(it) }
upMesh?.let { ctx.renderQuads(it) }
downMesh?.let { ctx.renderQuads(it) }
}
}
}

View File

@@ -0,0 +1,195 @@
package mods.betterfoliage.render.column
import mods.betterfoliage.chunk.BlockCtx
import mods.betterfoliage.chunk.ChunkOverlayLayer
import mods.betterfoliage.chunk.ChunkOverlayManager
import mods.betterfoliage.chunk.dimType
import mods.betterfoliage.config.Config
import mods.betterfoliage.render.column.ColumnLayerData.SpecialRender.BlockType.NONSOLID
import mods.betterfoliage.render.column.ColumnLayerData.SpecialRender.BlockType.PARALLEL
import mods.betterfoliage.render.column.ColumnLayerData.SpecialRender.BlockType.PERPENDICULAR
import mods.betterfoliage.render.column.ColumnLayerData.SpecialRender.BlockType.SOLID
import mods.betterfoliage.render.column.ColumnLayerData.SpecialRender.QuadrantType
import mods.betterfoliage.render.column.ColumnLayerData.SpecialRender.QuadrantType.INVISIBLE
import mods.betterfoliage.render.column.ColumnLayerData.SpecialRender.QuadrantType.LARGE_RADIUS
import mods.betterfoliage.render.column.ColumnLayerData.SpecialRender.QuadrantType.SMALL_RADIUS
import mods.betterfoliage.render.column.ColumnLayerData.SpecialRender.QuadrantType.SQUARE
import mods.betterfoliage.util.Int3
import mods.betterfoliage.util.Rotation
import mods.betterfoliage.util.allDirections
import mods.betterfoliage.util.face
import mods.betterfoliage.util.plus
import net.minecraft.block.BlockState
import net.minecraft.util.Direction
import net.minecraft.util.Direction.Axis
import net.minecraft.util.math.BlockPos
import net.minecraft.world.IBlockDisplayReader
/** 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
interface ColumnBlockKey {
val axis: Axis?
}
/**
* Sealed class hierarchy for all possible render outcomes
*/
sealed class ColumnLayerData {
/**
* Data structure to cache texture and world neighborhood data relevant to column rendering
*/
@Suppress("ArrayInDataClass") // not used in comparisons anywhere
data class SpecialRender(
val column: ColumnBlockKey,
val upType: BlockType,
val downType: BlockType,
val quadrants: Array<QuadrantType>,
val quadrantsTop: Array<QuadrantType>,
val quadrantsBottom: Array<QuadrantType>
) : ColumnLayerData() {
enum class BlockType { SOLID, NONSOLID, PARALLEL, PERPENDICULAR }
enum class QuadrantType {
SMALL_RADIUS, LARGE_RADIUS, SQUARE, INVISIBLE;
infix fun continuousWith(other: QuadrantType) =
this == other || ((this == SQUARE || this == INVISIBLE) && (other == SQUARE || other == INVISIBLE))
infix fun discontinuousWith(other: QuadrantType) = !continuousWith(other)
}
}
/** Column block should not be rendered at all */
object SkipRender : ColumnLayerData()
/** Column block must be rendered normally */
object NormalRender : ColumnLayerData()
/** Error while resolving render data, column block must be rendered normally */
object ResolveError : ColumnLayerData()
}
abstract class ColumnRenderLayer : ChunkOverlayLayer<ColumnLayerData>() {
abstract val connectSolids: Boolean
abstract val lenientConnect: Boolean
abstract val defaultToY: Boolean
abstract fun getColumnKey(state: BlockState): ColumnBlockKey?
val allNeighborOffsets = (-1..1).flatMap { offsetX -> (-1..1).flatMap { offsetY -> (-1..1).map { offsetZ -> Int3(offsetX, offsetY, offsetZ) }}}
override fun onBlockUpdate(world: IBlockDisplayReader, pos: BlockPos) {
allNeighborOffsets.forEach { offset -> remove(world, pos + offset) }
}
override fun calculate(ctx: BlockCtx): ColumnLayerData {
// TODO detect round logs
if (allDirections.all { dir -> ctx.offset(dir).let { it.isFullBlock } }) return ColumnLayerData.SkipRender
val columnTextures = getColumnKey(ctx.state) ?: return ColumnLayerData.ResolveError
// if log axis is not defined and "Default to vertical" config option is not set, render normally
val logAxis = columnTextures.axis ?: if (defaultToY) Axis.Y else return ColumnLayerData.NormalRender
// check log neighborhood
val baseRotation = Rotation.fromUp[(logAxis to Direction.AxisDirection.POSITIVE).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)
return ColumnLayerData.SpecialRender(columnTextures, upType, downType, quadrants, quadrantsTop, quadrantsBottom)
}
/** Sets the type of the given quadrant only if the new value is "stronger" (larger ordinal). */
inline fun Array<QuadrantType>.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<QuadrantType>.checkNeighbors(ctx: BlockCtx, rotation: Rotation, logAxis: Axis, yOff: Int): Array<QuadrantType> {
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 type of the block at the given offset in a rotated reference frame.
*/
fun BlockCtx.blockType(rotation: Rotation, axis: Axis, offset: Int3): ColumnLayerData.SpecialRender.BlockType {
val offsetRot = offset.rotate(rotation)
val key = getColumnKey(state(offsetRot))
return if (key == null) {
if (offset(offsetRot).isFullBlock) SOLID else NONSOLID
} else {
(key.axis ?: if (Config.roundLogs.defaultY) Axis.Y else null)?.let {
if (it == axis) PARALLEL else PERPENDICULAR
} ?: SOLID
}
}
}

View File

@@ -0,0 +1,138 @@
package mods.betterfoliage.render.lighting
import mods.betterfoliage.util.get
import mods.betterfoliage.util.mapArray
import mods.betterfoliage.util.perpendiculars
import net.minecraft.util.Direction
import net.minecraft.util.Direction.*
typealias BoxCorner = Triple<Direction, Direction, Direction>
fun BoxCorner.equalsUnordered(other: BoxCorner) = contains(other.first) && contains(other.second) && contains(other.third)
fun BoxCorner.contains(dir: Direction) = first == dir || second == dir || third == dir
fun Array<BoxCorner>.findIdx(corner: BoxCorner): Int? {
forEachIndexed { idx, test -> if (test.contains(corner.first) && test.contains(corner.second) && test.contains(corner.third)) return idx }
return null
}
fun Array<BoxCorner>.findIdx(predicate: (BoxCorner)->Boolean): Int? {
forEachIndexed { idx, test -> if (predicate(test)) return idx }
return null
}
class AoSideHelper private constructor(face: Direction) {
val sides = faceSides[face]
val cornerSideDirections = faceCorners[face]
val aoIndex = faceCornersIdx.mapArray { corner ->
boxCornersDirIdx[face][sides[corner.first]][sides[corner.second]]!!
}
companion object {
/**
* Indexing for undirected box corners (component order does not matter).
* Array contains [Direction] triplets fully defining the corner.
*/
@JvmField
val boxCornersUndir = Array(8) { idx -> Triple(
if (idx and 1 != 0) EAST else WEST,
if (idx and 2 != 0) UP else DOWN,
if (idx and 4 != 0) SOUTH else NORTH
) }
/**
* Reverse lookup for [boxCornersUndir]. Index 3 times with the corner's cardinal directions.
* A null value indicates an invalid corner (multiple indexing along the same axis)
*/
@JvmField
val boxCornersUndirIdx = Array(6) { idx1 -> Array(6) { idx2 -> Array(6) { idx3 ->
boxCornersUndir.findIdx(BoxCorner(
Direction.values()[idx1],
Direction.values()[idx2],
Direction.values()[idx3]
))
} } }
/**
* Indexing for directed face sides
* First index is the face, second is index of side on face
*/
@JvmField
val faceSides = Array(6) { faceIdx -> Array(4) { sideIdx ->
Direction.values()[faceIdx].perpendiculars[sideIdx]
} }
/**
* Pairs of [faceSides] side indexes that form a valid pair describing a corner
*/
@JvmField
val faceCornersIdx = arrayOf(0 to 2, 0 to 3, 1 to 2, 1 to 3)
/**
* Indexing for directed face corners
* First index is the face, second is index of corner on face
*/
@JvmField
val faceCorners = Array(6) { faceIdx -> Array(4) { cornerIdx ->
faceCornersIdx[cornerIdx].let { faceSides[faceIdx][it.first] to faceSides[faceIdx][it.second] }
} }
/**
* Indexing scheme for directed box corners.
* The first direction - the face - matters, the other two are unordered.
* 1:1 correspondence with possible AO values.
* Array contains triplets defining the corner fully.
*/
@JvmField
val boxCornersDir = Array(24) { idx ->
val faceIdx = idx / 4; val face = Direction.values()[faceIdx]
val cornerIdx = idx % 4; val corner = faceCorners[faceIdx][cornerIdx]
BoxCorner(face, corner.first, corner.second)
}
/**
* Reverse lookup for [boxCornersDir]. Index 3 times with the corner's cardinal directions.
* The first direction - the face - matters, the other two are unordered.
* A null value indicates an invalid corner (multiple indexing along the same axis)
*/
@JvmField
val boxCornersDirIdx = Array(6) { face -> Array(6) { side1 -> Array(6) { side2 ->
boxCornersDir.findIdx { boxCorner ->
boxCorner.first.ordinal == face && boxCorner.equalsUnordered(BoxCorner(
Direction.values()[face],
Direction.values()[side1],
Direction.values()[side2]
))
}
} } }
/**
* Reverse lookup for [cornersDir].
* 1st index: primary face
* 2nd index: undirected corner index.
* value: directed corner index
* A null value indicates an invalid corner (primary face not shared by corner).
*/
@JvmField
val boxCornersDirFromUndir = Array(6) { faceIdx -> Array(8) { undirIdx ->
val face = Direction.values()[faceIdx]
val corner = boxCornersUndir[undirIdx]
if (!corner.contains(face)) null
else boxCornersDir.findIdx { it.first == face && it.equalsUnordered(corner) }
} }
@JvmField
val forSide = Direction.values().mapArray { AoSideHelper(it) }
/**
* Get corner index for vertex coordinates
*/
@JvmStatic
fun getCornerUndir(x: Double, y: Double, z: Double): Int {
var result = 0
if (x > 0.0) result += 1
if (y > 0.0) result += 2
if (z > 0.0) result += 4
return result
}
}
}

View File

@@ -0,0 +1,10 @@
package mods.betterfoliage.render.lighting
interface ForgeVertexLighterAccess {
var vertexLighter: ForgeVertexLighter
}
interface ForgeVertexLighter {
fun updateVertexLightmap(normal: FloatArray, lightmap: FloatArray, x: Float, y: Float, z: Float)
fun updateVertexColor(normal: FloatArray, color: FloatArray, x: Float, y: Float, z: Float, tint: Float, multiplier: Int)
}

View File

@@ -0,0 +1,152 @@
package mods.betterfoliage.render.lighting
import mods.betterfoliage.chunk.BlockCtx
import net.minecraft.block.BlockState
import net.minecraft.client.renderer.BlockModelRenderer
import net.minecraft.util.Direction
import net.minecraft.util.math.BlockPos
import net.minecraft.world.IBlockDisplayReader
data class LightingData(
@JvmField var packedLight: Int = 0,
@JvmField var colorMultiplier: Float = 1.0f
) {
fun mixFrom(corner: LightingData, side1: LightingData, side2: LightingData, center: LightingData) {
colorMultiplier =
(center.colorMultiplier + side1.colorMultiplier + side2.colorMultiplier + corner.colorMultiplier) * 0.25f
packedLight = (
center.packedLight +
(side1.packedLight.takeUnless { it == 0 } ?: center.packedLight) +
(side2.packedLight.takeUnless { it == 0 } ?: center.packedLight) +
(corner.packedLight.takeUnless { it == 0 } ?: center.packedLight)
).let { sum -> (sum shr 2) and 0xFF00FF }
}
}
// Vanilla has a very suspicious-looking offset here, which Indigo gets rid of and calls it a fix
// Naturally, we're going to believe Indigo, it's a hardcoded option for now
const val OCCLUSION_OFFSET_FIX = true
/**
* Replacement for [BlockModelRenderer.AmbientOcclusionFace]
* This gets called on a LOT, so object instantiation is avoided.
* Not thread-safe, always use a [ThreadLocal] instance
*/
class VanillaAoCalculator {
lateinit var world: IBlockDisplayReader
/** [blockPos] is used to get block-related information (i.e. tint, opacity, etc.)
* [lightPos] is used to get light-related information
* this facilitates masquerade rendering of blocks */
lateinit var blockPos: BlockPos
lateinit var lightPos: BlockPos
private val probe = LightProbe(BlockModelRenderer.CACHE.get())
val isValid = BooleanArray(6)
val aoData = Array(24) { LightingData() }
// scratchpad values used during calculation
private val centerAo = LightingData()
private val sideAo = Array(4) { LightingData() }
private val cornerAo = Array(4) { LightingData() }
private val isOccluded = BooleanArray(4)
fun reset(ctx: BlockCtx) {
world = ctx.world; blockPos = ctx.pos; lightPos = ctx.pos
(0 until 6).forEach { isValid[it] = false }
}
fun fillLightData(lightFace: Direction, isOpaque: Boolean? = null) {
if (!isValid[lightFace.ordinal]) calculate(lightFace, isOpaque)
}
/**
* Replicate [BlockModelRenderer.AmbientOcclusionFace.updateVertexBrightness]
* Does not handle interpolation for non-cubic models, that should be
* done in a [VanillaVertexLighter]
* @param lightFace face of the block to calculate
* @param forceFull force full-block status for lighting calculation, null for auto
*/
private fun calculate(lightFace: Direction, forceFull: Boolean?) {
if (isValid[lightFace.ordinal]) return
val sideHelper = AoSideHelper.forSide[lightFace.ordinal]
// Bit 0 of the bitset in vanilla calculations
// true if the block model is planar with the block boundary
val isFullBlock = forceFull ?: world.getBlockState(blockPos).isCollisionShapeFullBlock(world, blockPos)
val lightOrigin = if (isFullBlock) lightPos.relative(lightFace) else lightPos
// AO calculation for the face center
probe.position { set(lightOrigin) }.writeTo(centerAo)
if (!isFullBlock && !probe.position { move(lightFace) }.state.isSolidRender(world, probe.pos)) {
// if the neighboring block in the lightface direction is
// transparent (non-opaque), use its packed light instead of our own
// (if our block is a full block, we are already using this value)
centerAo.packedLight = probe.packedLight
}
// AO calculation for the 4 sides
sideHelper.sides.forEachIndexed { sideIdx, sideDir ->
// record light data in the block 1 step to the side
probe.position { set(lightOrigin).move(sideDir) }.writeTo(sideAo[sideIdx])
// side is considered occluded if the block 1 step to that side and
// 1 step forward (in the lightface direction) is not fully transparent
if (!OCCLUSION_OFFSET_FIX) probe.position { move(lightFace) }
isOccluded[sideIdx] = probe.isNonTransparent
}
// AO Calculation for the 4 corners
AoSideHelper.faceCornersIdx.forEachIndexed { cornerIdx, sideIndices ->
val bothOccluded = isOccluded[sideIndices.first] && isOccluded[sideIndices.second]
if (bothOccluded) cornerAo[cornerIdx].apply {
// if both sides are occluded, just use the packed light for one of the sides instead
val copyFrom = sideAo[sideIndices.first]
packedLight = copyFrom.packedLight; colorMultiplier = copyFrom.colorMultiplier
}
else {
// lookup actual packed light from the cornering block in the world
probe.position {
set(lightOrigin)
.move(sideHelper.sides[sideIndices.first])
.move(sideHelper.sides[sideIndices.second])
}.writeTo(cornerAo[cornerIdx])
}
}
// Calculate and store final interpolated value for each corner
AoSideHelper.faceCornersIdx.forEachIndexed { cornerIdx, sideIndices ->
val aoIdx = sideHelper.aoIndex[cornerIdx]
aoData[aoIdx].mixFrom(
cornerAo[cornerIdx],
sideAo[sideIndices.first],
sideAo[sideIndices.second],
centerAo
)
}
isValid[lightFace.ordinal] = true
}
inner class LightProbe(
val cache: BlockModelRenderer.Cache
) {
lateinit var state: BlockState
val pos = BlockPos.Mutable()
val packedLight: Int get() = cache.getLightColor(state, world, pos)
val colorMultiplier: Float get() = cache.getShadeBrightness(state, world, pos)
val isNonTransparent: Boolean get() = state.getLightBlock(world, pos) > 0
fun writeTo(data: LightingData) {
data.packedLight = packedLight
data.colorMultiplier = colorMultiplier
}
inline fun position(func: BlockPos.Mutable.() -> Unit): LightProbe {
pos.func()
state = world.getBlockState(pos)
return this
}
}
}

View File

@@ -0,0 +1,210 @@
package mods.betterfoliage.render.lighting
import mods.betterfoliage.integration.ShadersModIntegration
import mods.betterfoliage.model.HalfBakedQuad
import mods.betterfoliage.util.Double3
import mods.betterfoliage.util.EPSILON_ONE
import mods.betterfoliage.util.EPSILON_ZERO
import mods.betterfoliage.util.get
import mods.betterfoliage.util.minBy
import net.minecraft.client.renderer.color.BlockColors
import net.minecraft.util.Direction
import net.minecraft.util.Direction.*
import kotlin.math.abs
class VanillaQuadLighting {
val packedLight = IntArray(4)
val colorMultiplier = FloatArray(4)
val tint = FloatArray(3)
val calc = VanillaAoCalculator()
lateinit var blockColors: BlockColors
fun updateBlockTint(tintIndex: Int) {
if (tintIndex == -1) {
tint[0] = 1.0f; tint[1] = 1.0f; tint[2] = 1.0f
} else {
val state = calc.world.getBlockState(calc.blockPos)
blockColors.getColor(state, calc.world, calc.blockPos, tintIndex).let { blockTint ->
tint[0] = (blockTint shr 16 and 255).toFloat() / 255.0f
tint[1] = (blockTint shr 8 and 255).toFloat() / 255.0f
tint[2] = (blockTint and 255).toFloat() / 255.0f
}
}
}
fun applyDiffuseLighting(face: Direction) {
val factor = ShadersModIntegration.diffuseShades[face]
tint[0] *= factor; tint[1] *= factor; tint[2] *= factor
}
}
abstract class VanillaVertexLighter {
abstract fun updateLightmapAndColor(quad: HalfBakedQuad, lighting: VanillaQuadLighting)
/**
* Update lighting for each vertex with AO values from one of the corners.
* Does not calculate missing AO values!
* @param quad the quad to shade
* @param func selector function from vertex position to directed corner index of desired AO values
*/
inline fun VanillaQuadLighting.updateWithCornerAo(quad: HalfBakedQuad, func: (Double3)->Int?) {
quad.raw.verts.forEachIndexed { idx, vertex ->
func(vertex.xyz)?.let {
packedLight[idx] = calc.aoData[it].packedLight
colorMultiplier[idx] = calc.aoData[it].colorMultiplier
}
}
}
}
/**
* Replicates vanilla shading for full blocks. Interpolation for non-full blocks
* is not implemented.
*/
object VanillaFullBlockLighting : VanillaVertexLighter() {
override fun updateLightmapAndColor(quad: HalfBakedQuad, lighting: VanillaQuadLighting) {
// TODO bounds checking & interpolation
val face = quad.raw.face()
lighting.calc.fillLightData(face, true)
lighting.updateWithCornerAo(quad) { nearestCornerOnFace(it, face) }
lighting.updateBlockTint(quad.baked.tintIndex)
if (quad.baked.isShade) lighting.applyDiffuseLighting(face)
}
}
object RoundLeafLightingPreferUp : VanillaVertexLighter() {
override fun updateLightmapAndColor(quad: HalfBakedQuad, lighting: VanillaQuadLighting) {
val angles = getAngles45(quad)?.let { normalFaces ->
lighting.calc.fillLightData(normalFaces.first)
lighting.calc.fillLightData(normalFaces.second)
if (normalFaces.first != UP && normalFaces.second != UP) lighting.calc.fillLightData(UP)
lighting.updateWithCornerAo(quad) { vertex ->
val isUp = vertex.y > 0.5f
val cornerUndir = AoSideHelper.getCornerUndir(vertex.x, vertex.y, vertex.z)
val preferredFace = if (isUp) UP else normalFaces.minBy { faceDistance(it, vertex) }
AoSideHelper.boxCornersDirFromUndir[preferredFace.ordinal][cornerUndir]
}
lighting.updateBlockTint(quad.baked.tintIndex)
}
}
}
/**
* Lights vertices with the AO values from the nearest corner on either of
* the 2 faces the quad normal points towards.
*/
object RoundLeafLighting : VanillaVertexLighter() {
override fun updateLightmapAndColor(quad: HalfBakedQuad, lighting: VanillaQuadLighting) {
val angles = getAngles45(quad)?.let { normalFaces ->
lighting.calc.fillLightData(normalFaces.first)
lighting.calc.fillLightData(normalFaces.second)
lighting.updateWithCornerAo(quad) { vertex ->
val cornerUndir = AoSideHelper.getCornerUndir(vertex.x, vertex.y, vertex.z)
val preferredFace = normalFaces.minBy { faceDistance(it, vertex) }
AoSideHelper.boxCornersDirFromUndir[preferredFace.ordinal][cornerUndir]
}
lighting.updateBlockTint(quad.baked.tintIndex)
}
}
}
/**
* Lights vertices with the AO values from the nearest corner on the preferred face.
*/
class LightingPreferredFace(val face: Direction) : VanillaVertexLighter() {
override fun updateLightmapAndColor(quad: HalfBakedQuad, lighting: VanillaQuadLighting) {
lighting.calc.fillLightData(face)
lighting.updateWithCornerAo(quad) { nearestCornerOnFace(it, face) }
lighting.updateBlockTint(quad.baked.tintIndex)
}
}
object ColumnLighting : VanillaVertexLighter() {
override fun updateLightmapAndColor(quad: HalfBakedQuad, lighting: VanillaQuadLighting) {
// faces pointing in cardinal directions
getNormalFace(quad)?.let { face ->
lighting.calc.fillLightData(face)
lighting.updateWithCornerAo(quad) { nearestCornerOnFace(it, face) }
lighting.updateBlockTint(quad.baked.tintIndex)
return
}
// faces pointing at 45deg angles
getAngles45(quad)?.let { (face1, face2) ->
lighting.calc.fillLightData(face1)
lighting.calc.fillLightData(face2)
quad.raw.verts.forEachIndexed { idx, vertex ->
val cornerUndir = AoSideHelper.getCornerUndir(vertex.xyz.x, vertex.xyz.y, vertex.xyz.z)
val cornerDir1 = AoSideHelper.boxCornersDirFromUndir[face1.ordinal][cornerUndir]
val cornerDir2 = AoSideHelper.boxCornersDirFromUndir[face2.ordinal][cornerUndir]
if (cornerDir1 == null || cornerDir2 == null) return@let
val ao1 = lighting.calc.aoData[cornerDir1]
val ao2 = lighting.calc.aoData[cornerDir2]
lighting.packedLight[idx] = ((ao1.packedLight + ao2.packedLight) shr 1) and 0xFF00FF
lighting.colorMultiplier[idx] = (ao1.colorMultiplier + ao2.colorMultiplier) * 0.5f
}
lighting.updateBlockTint(quad.baked.tintIndex)
return
}
// something is wrong...
lighting.updateWithCornerAo(quad) { nearestCornerOnFace(it, quad.raw.face()) }
lighting.updateBlockTint(quad.baked.tintIndex)
}
}
/**
* Return the directed box corner index for the corner nearest the given vertex,
* which is on the given face. May return null if the vertex is closest to
* one of the opposite 4 corners
*/
fun nearestCornerOnFace(pos: Double3, face: Direction): Int? {
val cornerUndir = AoSideHelper.getCornerUndir(pos.x, pos.y, pos.z)
return AoSideHelper.boxCornersDirFromUndir[face.ordinal][cornerUndir]
}
/**
* If the quad normal approximately bisects 2 axes at a 45 degree angle,
* and is approximately perpendicular to the third, returns the 2 directions
* the quad normal points towards.
* Returns null otherwise.
*/
fun getAngles45(quad: HalfBakedQuad): Pair<Direction, Direction>? {
val normal = quad.raw.normal
// one of the components must be close to zero
val zeroAxis = when {
abs(normal.x) < EPSILON_ZERO -> Axis.X
abs(normal.y) < EPSILON_ZERO -> Axis.Y
abs(normal.z) < EPSILON_ZERO -> Axis.Z
else -> return null
}
// the other two must be of similar magnitude
val diff = when(zeroAxis) {
Axis.X -> abs(abs(normal.y) - abs(normal.z))
Axis.Y -> abs(abs(normal.x) - abs(normal.z))
Axis.Z -> abs(abs(normal.x) - abs(normal.y))
}
if (diff > EPSILON_ZERO) return null
return when(zeroAxis) {
Axis.X -> Pair(if (normal.y > 0.0f) UP else DOWN, if (normal.z > 0.0f) SOUTH else NORTH)
Axis.Y -> Pair(if (normal.x > 0.0f) EAST else WEST, if (normal.z > 0.0f) SOUTH else NORTH)
Axis.Z -> Pair(if (normal.x > 0.0f) EAST else WEST, if (normal.y > 0.0f) UP else DOWN)
}
}
fun getNormalFace(quad: HalfBakedQuad) = quad.raw.normal.let { normal ->
when {
normal.x > EPSILON_ONE -> EAST
normal.x < -EPSILON_ONE -> WEST
normal.y > EPSILON_ONE -> UP
normal.y < -EPSILON_ONE -> DOWN
normal.z > EPSILON_ONE -> SOUTH
normal.z < -EPSILON_ONE -> NORTH
else -> null
}
}
fun faceDistance(face: Direction, pos: Double3) = when(face) {
WEST -> pos.x; EAST -> 1.0 - pos.x
DOWN -> pos.y; UP -> 1.0 - pos.y
NORTH -> pos.z; SOUTH -> 1.0 - pos.z
}

View File

@@ -0,0 +1,109 @@
package mods.betterfoliage.render.particle
import com.mojang.blaze3d.vertex.IVertexBuilder
import mods.betterfoliage.model.Color
import mods.betterfoliage.model.HSB
import mods.betterfoliage.util.Double3
import net.minecraft.client.Minecraft
import net.minecraft.client.particle.SpriteTexturedParticle
import net.minecraft.client.renderer.ActiveRenderInfo
import net.minecraft.client.renderer.texture.TextureAtlasSprite
import net.minecraft.client.world.ClientWorld
import net.minecraft.util.math.MathHelper
import net.minecraft.util.math.vector.Vector3f
abstract class AbstractParticle(world: ClientWorld, x: Double, y: Double, z: Double) : SpriteTexturedParticle(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 tick() {
super.tick()
currentPos.setTo(x, y, z)
prevPos.setTo(xo, yo, zo)
velocity.setTo(xd, yd, zd)
update()
x = currentPos.x; y = currentPos.y; z = currentPos.z;
xd = velocity.x; yd = velocity.y; zd = velocity.z;
}
/** 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.getInstance().particleEngine.add(this) }
override fun render(vertexBuilder: IVertexBuilder, camera: ActiveRenderInfo, tickDelta: Float) {
super.render(vertexBuilder, camera, tickDelta)
}
/**
* Render a particle quad.
*
* @param[tessellator] the [Tessellator] instance to use
* @param[tickDelta] partial tick time
* @param[currentPos] render position
* @param[prevPos] previous tick position for interpolation
* @param[size] particle size
* @param[currentAngle] viewpoint-dependent particle rotation (64 steps)
* @param[sprite] particle texture
* @param[isMirrored] mirror particle texture along V-axis
* @param[alpha] aplha blending
*/
fun renderParticleQuad(vertexConsumer: IVertexBuilder,
camera: ActiveRenderInfo,
tickDelta: Float,
currentPos: Double3 = this.currentPos,
prevPos: Double3 = this.prevPos,
size: Double = quadSize.toDouble(),
currentAngle: Float = this.roll,
prevAngle: Float = this.oRoll,
sprite: TextureAtlasSprite = this.sprite,
alpha: Float = this.alpha) {
val center = Double3.lerp(tickDelta.toDouble(), prevPos, currentPos)
val angle = MathHelper.lerp(tickDelta, prevAngle, currentAngle)
val rotation = camera.rotation().copy().apply { mul(Vector3f.ZP.rotation(angle)) }
val lightmapCoord = getLightColor(tickDelta)
val coords = arrayOf(
Double3(-1.0, -1.0, 0.0),
Double3(-1.0, 1.0, 0.0),
Double3(1.0, 1.0, 0.0),
Double3(1.0, -1.0, 0.0)
).map { it.rotate(rotation).mul(size).add(center).sub(camera.position.x, camera.position.y, camera.position.z) }
fun renderVertex(vertex: Double3, u: Float, v: Float) = vertexConsumer
.vertex(vertex.x, vertex.y, vertex.z).uv(u, v)
.color(rCol, gCol, bCol, alpha).uv2(lightmapCoord)
.endVertex()
renderVertex(coords[0], sprite.u1, sprite.v1)
renderVertex(coords[1], sprite.u1, sprite.v0)
renderVertex(coords[2], sprite.u0, sprite.v0)
renderVertex(coords[3], sprite.u0, sprite.v1)
}
fun setColor(color: Color) {
rCol = color.red / 256.0f
gCol = color.green / 256.0f
bCol = color.blue / 256.0f
}
/**
* Set particle color to the "stronger" of the given colors, determined by higher color saturation
*/
fun setColor(color1: Color, color2: Color) =
setColor(if (color1.asHSB.saturation > color2.asHSB.saturation) color1 else color2)
}

View File

@@ -0,0 +1,113 @@
package mods.betterfoliage.render.particle
import mods.betterfoliage.config.Config
import mods.betterfoliage.model.Color
import mods.betterfoliage.util.Double3
import mods.betterfoliage.util.PI2
import mods.betterfoliage.util.minmax
import mods.betterfoliage.util.randomB
import mods.betterfoliage.util.randomD
import mods.betterfoliage.util.randomF
import mods.betterfoliage.util.randomI
import net.minecraft.client.particle.IParticleRenderType
import net.minecraft.client.world.ClientWorld
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.MathHelper
import net.minecraft.world.World
import net.minecraftforge.common.MinecraftForge
import net.minecraftforge.event.TickEvent
import net.minecraftforge.eventbus.api.SubscribeEvent
import net.minecraftforge.fml.LogicalSide
import java.util.Random
import kotlin.math.abs
import kotlin.math.cos
import kotlin.math.sin
class FallingLeafParticle(
world: ClientWorld, pos: BlockPos, leaf: LeafParticleKey, blockColor: Int, random: Random
) : AbstractParticle(
world, pos.x.toDouble() + 0.5, pos.y.toDouble(), pos.z.toDouble() + 0.5
) {
companion object {
@JvmStatic val biomeBrightnessMultiplier = 0.5f
}
var rotationSpeed = random.randomF(min = PI2 / 80.0, max = PI2 / 50.0)
val isMirrored = randomB()
var wasCollided = false
init {
roll = random.randomF(max = PI2)
oRoll = roll - rotationSpeed
lifetime = MathHelper.floor(randomD(0.6, 1.0) * Config.fallingLeaves.lifetime * 20.0)
yd = -Config.fallingLeaves.speed
quadSize = Config.fallingLeaves.size.toFloat() * 0.1f
if (leaf.tintIndex == -1) setColor(leaf.avgColor) else setColor(leaf.avgColor, Color(blockColor))
sprite = LeafParticleRegistry[leaf.leafType][randomI(max = 1024)]
}
override val isValid: Boolean get() = (sprite != null)
override fun update() {
if (random.nextFloat() > 0.95f) rotationSpeed *= -1.0f
if (age > lifetime - 20) alpha = 0.05f * (lifetime - age)
if (onGround || wasCollided) {
velocity.setTo(0.0, 0.0, 0.0)
if (!wasCollided) {
age = age.coerceAtLeast(lifetime - 20)
wasCollided = true
}
} else {
val cosRotation = cos(roll).toDouble(); val sinRotation = sin(roll).toDouble()
velocity.setTo(cosRotation, 0.0, sinRotation).mul(Config.fallingLeaves.perturb)
.add(LeafWindTracker.current).add(0.0, -1.0, 0.0).mul(Config.fallingLeaves.speed)
oRoll = roll
roll += rotationSpeed
}
}
override fun getRenderType(): IParticleRenderType = IParticleRenderType.PARTICLE_SHEET_TRANSLUCENT
}
object LeafWindTracker {
var random = Random()
val target = Double3.zero
val current = Double3.zero
var nextChange: Long = 0
init {
MinecraftForge.EVENT_BUS.register(this)
}
fun changeWind(world: World) {
nextChange = world.gameTime + 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.WorldTickEvent) {
if (event.phase == TickEvent.Phase.START && event.side == LogicalSide.CLIENT) event.world.let { world ->
// change target wind speed
if (world.dayTime >= 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.isClientSide) changeWind(event.world) }
}

View File

@@ -0,0 +1,109 @@
package mods.betterfoliage.render.particle
import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.model.Color
import mods.betterfoliage.model.FixedSpriteSet
import mods.betterfoliage.model.SpriteSet
import mods.betterfoliage.resource.VeryEarlyReloadListener
import mods.betterfoliage.util.Atlas
import mods.betterfoliage.util.HasLogger
import mods.betterfoliage.util.get
import mods.betterfoliage.util.getLines
import mods.betterfoliage.util.resourceManager
import mods.betterfoliage.util.stripEnd
import mods.betterfoliage.util.stripStart
import net.minecraft.client.renderer.texture.MissingTextureSprite
import net.minecraft.resources.IResourceManager
import net.minecraft.util.ResourceLocation
import net.minecraftforge.client.event.TextureStitchEvent
import net.minecraftforge.eventbus.api.SubscribeEvent
import org.apache.logging.log4j.Level.INFO
interface LeafBlockModel {
val key: LeafParticleKey
}
interface LeafParticleKey {
val leafType: String
val tintIndex: Int
val avgColor: Color
}
object LeafParticleRegistry : HasLogger(), VeryEarlyReloadListener {
val allTypes = mutableSetOf<String>()
val particles = hashMapOf<String, SpriteSet>()
operator fun get(type: String) = particles[type] ?: particles["default"]!!
override fun onReloadStarted(resourceManager: IResourceManager) {
allTypes.clear()
resourceManager.listResources("textures/particle") { it.startsWith("falling_leaf_") }
.filter { it.namespace == BetterFoliageMod.MOD_ID }
.map { it.stripStart("textures/particle/falling_leaf_").stripEnd(".png") }
.map { it.path.substringBefore("_", "") }
.forEach { leafType -> if (!leafType.isEmpty()) allTypes.add(leafType) }
}
@SubscribeEvent
fun handlePreStitch(event: TextureStitchEvent.Pre) {
if (event.map.location() == Atlas.PARTICLES.resourceId) {
allTypes.forEach { leafType ->
val locations = (0 until 16).map { idx ->
ResourceLocation(BetterFoliageMod.MOD_ID, "particle/falling_leaf_${leafType}_$idx")
}.filter { resourceManager.hasResource(Atlas.PARTICLES.file(it)) }
detailLogger.log(INFO, "Registering sprites for leaf particle type [$leafType], ${locations.size} sprites found")
locations.forEach { event.addSprite(it) }
}
}
}
@SubscribeEvent
fun handlePostStitch(event: TextureStitchEvent.Post) {
if (event.map.location() == Atlas.PARTICLES.resourceId) {
allTypes.forEach { leafType ->
val sprites = (0 until 16).map { idx ->
ResourceLocation(BetterFoliageMod.MOD_ID, "particle/falling_leaf_${leafType}_$idx")
}
.map { event.map.getSprite(it) }
.filter { it !is MissingTextureSprite }
detailLogger.log(INFO, "Leaf particle type [$leafType], ${sprites.size} sprites in atlas")
particles[leafType] = FixedSpriteSet(sprites)
}
}
}
}
class TextureMatcher {
data class Mapping(val domain: String?, val path: String, val type: String) {
fun matches(iconLocation: ResourceLocation): Boolean {
return (domain == null || domain == iconLocation.namespace) &&
iconLocation.path.stripStart("blocks/").contains(path, ignoreCase = true)
}
}
val mappings: MutableList<Mapping> = mutableListOf()
fun getType(resource: ResourceLocation) = mappings.filter { it.matches(resource) }.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[0].trim(),
mapping[1].trim(),
line2[1].trim()
)
)
}
}
}
}
}

View File

@@ -0,0 +1,90 @@
package mods.betterfoliage.render.particle
import com.mojang.blaze3d.vertex.IVertexBuilder
import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.config.Config
import mods.betterfoliage.model.SpriteDelegate
import mods.betterfoliage.model.SpriteSetDelegate
import mods.betterfoliage.util.Atlas
import mods.betterfoliage.util.Double3
import mods.betterfoliage.util.PI2
import mods.betterfoliage.util.forEachPairIndexed
import mods.betterfoliage.util.randomD
import mods.betterfoliage.util.randomI
import net.minecraft.client.particle.IParticleRenderType
import net.minecraft.client.renderer.ActiveRenderInfo
import net.minecraft.client.world.ClientWorld
import net.minecraft.util.ResourceLocation
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.MathHelper
import net.minecraft.world.World
import java.util.Deque
import java.util.LinkedList
import kotlin.math.cos
import kotlin.math.sin
class RisingSoulParticle(
world: ClientWorld, pos: BlockPos
) : AbstractParticle(
world, pos.x.toDouble() + 0.5, pos.y.toDouble() + 1.0, pos.z.toDouble() + 0.5
) {
val particleTrail: Deque<Double3> = LinkedList<Double3>()
val initialPhase = randomD(max = PI2)
init {
yd = 0.1
gravity = 0.0f
sprite = headIcons[randomI(max = 1024)]
lifetime = MathHelper.floor((0.6 + 0.4 * randomD()) * Config.risingSoul.lifetime * 20.0)
}
override val isValid: Boolean get() = true
override fun update() {
val phase = initialPhase + (age.toDouble() * PI2 / 64.0)
val cosPhase = cos(phase);
val sinPhase = sin(phase)
velocity.setTo(Config.risingSoul.perturb.let { Double3(cosPhase * it, 0.1, sinPhase * it) })
particleTrail.addFirst(currentPos.copy())
while (particleTrail.size > Config.risingSoul.trailLength) particleTrail.removeLast()
if (!Config.enabled) remove()
}
override fun render(vertexBuilder: IVertexBuilder, camera: ActiveRenderInfo, tickDelta: Float) {
var alpha = Config.risingSoul.opacity.toFloat()
if (age > lifetime - 40) alpha *= (lifetime - age) / 40.0f
renderParticleQuad(
vertexBuilder, camera, tickDelta,
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.toFloat()
if (idx % Config.risingSoul.trailDensity == 0)
renderParticleQuad(
vertexBuilder, camera, tickDelta,
currentPos = current,
prevPos = previous,
size = scale,
alpha = alpha,
sprite = trackIcon
)
}
}
override fun getRenderType(): IParticleRenderType = IParticleRenderType.PARTICLE_SHEET_TRANSLUCENT
companion object {
val headIcons by SpriteSetDelegate(Atlas.PARTICLES) { idx ->
ResourceLocation(BetterFoliageMod.MOD_ID, "particle/rising_soul_$idx")
}
val trackIcon by SpriteDelegate(Atlas.PARTICLES) { ResourceLocation(BetterFoliageMod.MOD_ID, "particle/soul_track") }
}
}

View File

@@ -0,0 +1,42 @@
package mods.betterfoliage.render.pipeline
import mods.betterfoliage.model.SpecialRenderData
import mods.betterfoliage.RenderTypeLookup
import net.minecraft.block.Block
import net.minecraft.client.renderer.RenderType
import java.util.function.Predicate
object Layers {
val tufts = RenderType.cutout()
val connectedGrass = RenderType.solid()
val connectedDirt = RenderType.cutoutMipped()
val coral = RenderType.cutoutMipped()
}
val defaultLayerBehaviour = Predicate<RenderType> { layer -> layer == RenderType.solid() }
class WrappedLayerPredicate(val original: Predicate<RenderType>, val func: (RenderType, Predicate<RenderType>) -> Boolean) : Predicate<RenderType> {
override fun test(layer: RenderType) = func(layer, original)
}
/**
* Extension method to access the canRenderInLayer() predicate in [RenderTypeLookup]
*/
var Block.layerPredicate : Predicate<RenderType>?
get() = RenderTypeLookup.blockRenderChecks.getStatic()[delegate]
set(value) {
RenderTypeLookup.blockRenderChecks.getStatic()[delegate] = value!!
}
/**
* Add a wrapper to the block's canRenderInLayer() predicate to enable dynamic multi-layer rendering.
* If the render data for the block implements [SpecialRenderData], the layers it enables will be
* rendered _in addition to_ the block's normal layers.
*/
fun Block.extendLayers() {
val original = layerPredicate ?: defaultLayerBehaviour
if (original !is WrappedLayerPredicate) layerPredicate = WrappedLayerPredicate(original) { layer, original ->
original.test(layer) ||
(RenderCtxBase.specialRenderData.get() as? SpecialRenderData)?.canRenderInLayer(layer) ?: false
}
}

View File

@@ -0,0 +1,83 @@
package mods.betterfoliage.render.pipeline
import com.mojang.blaze3d.matrix.MatrixStack
import mods.betterfoliage.chunk.BasicBlockCtx
import mods.betterfoliage.chunk.BlockCtx
import mods.betterfoliage.model.HalfBakedQuad
import mods.betterfoliage.model.SpecialRenderModel
import mods.betterfoliage.render.lighting.VanillaFullBlockLighting
import mods.betterfoliage.render.lighting.VanillaQuadLighting
import mods.betterfoliage.render.lighting.VanillaVertexLighter
import mods.betterfoliage.util.Int3
import mods.betterfoliage.util.plus
import net.minecraft.block.Block
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.BlockRendererDispatcher
import net.minecraft.client.renderer.chunk.ChunkRenderCache
import net.minecraft.util.Direction
import net.minecraft.util.math.BlockPos
import net.minecraft.world.IBlockDisplayReader
import net.minecraftforge.client.model.data.IModelData
import java.util.Random
/**
* Rendering context for drawing [SpecialRenderModel] models.
*
* This class (and others in its constellation) basically form a replacement, highly customizable,
* push-based partial rendering pipeline for [SpecialRenderModel] instances.
*/
abstract class RenderCtxBase(
blockCtx: BlockCtx,
val matrixStack: MatrixStack,
var checkSides: Boolean,
val random: Random,
val modelData: IModelData,
) : BlockCtx by blockCtx {
var hasRendered = false
var modelRenderData: Any? = null
inline fun <reified T> withRenderData(renderFunc: (T)->Boolean) = (modelRenderData as? T?).let {
if (it == null) false else renderFunc(it)
}
val blockModelShapes = Minecraft.getInstance().blockRenderer.blockModelShaper
var vertexLighter: VanillaVertexLighter = VanillaFullBlockLighting
protected val lightingData = RenderCtxBase.lightingData.get().apply {
calc.reset(this@RenderCtxBase)
blockColors = Minecraft.getInstance().blockColors
}
abstract fun renderQuad(quad: HalfBakedQuad)
inline fun Direction?.shouldRender() = this == null || !checkSides || Block.shouldRenderFace(state, world, pos, this)
fun renderQuads(quads: Iterable<HalfBakedQuad>) {
quads.forEach { quad ->
if (quad.raw.face.shouldRender()) {
renderQuad(quad)
hasRendered = true
}
}
}
fun renderMasquerade(offset: Int3, func: () -> Unit) {
lightingData.calc.blockPos += offset
func()
lightingData.calc.blockPos = pos
}
companion object {
@JvmStatic
fun reset(chunkRenderCache: IBlockDisplayReader, blockRendererDispatcher: BlockRendererDispatcher, pos: BlockPos, random: Random) {
// prepare render data
val blockCtx = BasicBlockCtx(chunkRenderCache, pos)
val model = blockRendererDispatcher.getBlockModel(blockCtx.state)
random.setSeed(blockCtx.seed)
val data = if (model is SpecialRenderModel) model.prepare(blockCtx, random) else Unit
specialRenderData.set(data)
}
val lightingData = ThreadLocal.withInitial { VanillaQuadLighting() }
val specialRenderData = ThreadLocal<Any?>()
}
}

View File

@@ -0,0 +1,90 @@
package mods.betterfoliage.render.pipeline
import com.mojang.blaze3d.matrix.MatrixStack
import mods.betterfoliage.chunk.BasicBlockCtx
import mods.betterfoliage.chunk.BlockCtx
import mods.betterfoliage.model.HalfBakedQuad
import mods.betterfoliage.model.SpecialRenderModel
import mods.betterfoliage.render.lighting.ForgeVertexLighter
import mods.betterfoliage.render.lighting.ForgeVertexLighterAccess
import mods.betterfoliage.util.getWithDefault
import net.minecraft.block.BlockState
import net.minecraft.client.renderer.LightTexture
import net.minecraft.util.math.BlockPos
import net.minecraft.world.IBlockDisplayReader
import net.minecraftforge.client.ForgeHooksClient
import net.minecraftforge.client.MinecraftForgeClient
import net.minecraftforge.client.model.data.IModelData
import net.minecraftforge.client.model.pipeline.VertexLighterFlat
import java.util.Random
class RenderCtxForge(
blockCtx: BlockCtx,
val lighter: VertexLighterFlat,
matrixStack: MatrixStack,
checkSides: Boolean,
random: Random,
modelData: IModelData,
) : RenderCtxBase(blockCtx, matrixStack, checkSides, random, modelData), ForgeVertexLighter {
override fun renderQuad(quad: HalfBakedQuad) {
// set Forge lighter AO calculator to us
vertexLighter.updateLightmapAndColor(quad, lightingData)
quad.baked.pipe(lighter)
}
// somewhat ugly hack to pipe lighting values into the Forge pipeline
var vIdx = 0
override fun updateVertexLightmap(normal: FloatArray, lightmap: FloatArray, x: Float, y: Float, z: Float) {
lightingData.packedLight[vIdx].let { packedLight ->
lightmap[0] = LightTexture.block(packedLight) / 0xF.toFloat()
lightmap[1] = LightTexture.sky(packedLight) / 0xF.toFloat()
}
}
override fun updateVertexColor(
normal: FloatArray,
color: FloatArray,
x: Float,
y: Float,
z: Float,
tint: Float,
multiplier: Int
) {
color[0] = lightingData.tint[0] * lightingData.colorMultiplier[vIdx]
color[1] = lightingData.tint[1] * lightingData.colorMultiplier[vIdx]
color[2] = lightingData.tint[2] * lightingData.colorMultiplier[vIdx]
vIdx++
}
companion object {
@JvmStatic
fun render(
lighter: VertexLighterFlat,
world: IBlockDisplayReader,
model: SpecialRenderModel,
state: BlockState,
pos: BlockPos,
matrixStack: MatrixStack,
checkSides: Boolean,
random: Random, seed: Long,
modelData: IModelData
): Boolean {
val blockCtx = BasicBlockCtx(world, pos)
val ctx = RenderCtxForge(blockCtx, lighter, matrixStack, checkSides, random, modelData).apply {
lighter.setWorld(world)
lighter.setState(state)
lighter.setBlockPos(pos)
lighter.updateBlockInfo()
}
// render layer
return ctx.let {
(lighter as ForgeVertexLighterAccess).vertexLighter = it
model.renderLayer(it, specialRenderData.get()!!, MinecraftForgeClient.getRenderLayer())
lighter.resetBlockInfo()
it.hasRendered
}
}
}
}

View File

@@ -0,0 +1,65 @@
package mods.betterfoliage.render.pipeline
import com.mojang.blaze3d.matrix.MatrixStack
import com.mojang.blaze3d.vertex.IVertexBuilder
import mods.betterfoliage.chunk.BasicBlockCtx
import mods.betterfoliage.chunk.BlockCtx
import mods.betterfoliage.model.HalfBakedQuad
import mods.betterfoliage.model.SpecialRenderModel
import mods.betterfoliage.util.getWithDefault
import net.minecraft.block.BlockState
import net.minecraft.client.renderer.BlockModelRenderer
import net.minecraft.util.math.BlockPos
import net.minecraft.world.IBlockDisplayReader
import net.minecraftforge.client.MinecraftForgeClient
import net.minecraftforge.client.model.data.IModelData
import java.util.Random
class RenderCtxVanilla(
val renderer: BlockModelRenderer,
blockCtx: BlockCtx,
val buffer: IVertexBuilder,
val combinedOverlay: Int,
matrixStack: MatrixStack,
checkSides: Boolean,
random: Random,
val randomSeed: Long,
modelData: IModelData,
val useAO: Boolean
): RenderCtxBase(blockCtx, matrixStack, checkSides, random, modelData) {
override fun renderQuad(quad: HalfBakedQuad) {
vertexLighter.updateLightmapAndColor(quad, lightingData)
buffer.putBulkData(
matrixStack.last(), quad.baked,
lightingData.colorMultiplier,
lightingData.tint[0], lightingData.tint[1], lightingData.tint[2],
lightingData.packedLight, combinedOverlay, true
)
}
companion object {
@JvmStatic
fun render(
renderer: BlockModelRenderer,
world: IBlockDisplayReader,
model: SpecialRenderModel,
state: BlockState,
pos: BlockPos,
matrixStack: MatrixStack,
buffer: IVertexBuilder,
checkSides: Boolean,
random: Random,
seed: Long,
combinedOverlay: Int,
modelData: IModelData,
smooth: Boolean
): Boolean {
val blockCtx = BasicBlockCtx(world, pos)
// init context if missing (this is the first render layer)
val ctx = RenderCtxVanilla(renderer, blockCtx, buffer, combinedOverlay, matrixStack, checkSides, random, seed, modelData, smooth)
model.renderLayer(ctx, specialRenderData.get()!!, MinecraftForgeClient.getRenderLayer())
return ctx.hasRendered
}
}
}

View File

@@ -0,0 +1,27 @@
package mods.betterfoliage.resource
import net.minecraft.profiler.IProfiler
import net.minecraft.resources.IFutureReloadListener
import net.minecraft.resources.IResourceManager
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executor
/**
* Catch resource reload extremely early, before any of the reloaders
* have started working.
*/
interface VeryEarlyReloadListener : IFutureReloadListener {
override fun reload(
stage: IFutureReloadListener.IStage,
resourceManager: IResourceManager,
preparationsProfiler: IProfiler,
reloadProfiler: IProfiler,
backgroundExecutor: Executor,
gameExecutor: Executor
): CompletableFuture<Void> {
onReloadStarted(resourceManager)
return stage.wait(null)
}
fun onReloadStarted(resourceManager: IResourceManager) {}
}

View File

@@ -0,0 +1,135 @@
package mods.betterfoliage.resource.discovery
import mods.betterfoliage.BetterFoliage
import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.util.Atlas
import mods.betterfoliage.util.HasLogger
import mods.betterfoliage.util.Invalidator
import mods.betterfoliage.util.resourceManager
import net.minecraft.block.BlockState
import net.minecraft.client.renderer.model.IBakedModel
import net.minecraft.client.renderer.model.IModelTransform
import net.minecraft.client.renderer.model.IUnbakedModel
import net.minecraft.client.renderer.model.ModelBakery
import net.minecraft.client.renderer.model.RenderMaterial
import net.minecraft.client.renderer.texture.TextureAtlasSprite
import net.minecraft.util.ResourceLocation
import net.minecraftforge.client.event.ModelBakeEvent
import net.minecraftforge.client.event.TextureStitchEvent
import net.minecraftforge.eventbus.api.Event
import net.minecraftforge.eventbus.api.EventPriority
import net.minecraftforge.eventbus.api.SubscribeEvent
import net.minecraftforge.fml.loading.progress.StartupMessageManager
import org.apache.logging.log4j.Level.INFO
import org.apache.logging.log4j.Level.WARN
import org.apache.logging.log4j.Logger
import java.lang.ref.WeakReference
import java.util.function.Function
data class ModelDefinitionsLoadedEvent(
val bakery: ModelBakery
) : Event()
interface ModelBakingKey {
fun bake(ctx: ModelBakingContext): IBakedModel? =
ctx.getUnbaked().bake(ctx.bakery, ctx.spriteGetter, ctx.transform, ctx.location)
}
interface ModelDiscovery {
fun onModelsLoaded(
bakery: ModelBakery,
sprites: MutableSet<ResourceLocation>,
replacements: MutableMap<ResourceLocation, ModelBakingKey>
)
}
data class ModelDiscoveryContext(
val bakery: ModelBakery,
val blockState: BlockState,
val modelLocation: ResourceLocation,
val sprites: MutableSet<ResourceLocation>,
val replacements: MutableMap<ResourceLocation, ModelBakingKey>,
val logger: Logger
) {
fun getUnbaked(location: ResourceLocation = modelLocation) = bakery.getModel(location)
fun addReplacement(key: ModelBakingKey, addToStateKeys: Boolean = true) {
replacements[modelLocation] = key
if (addToStateKeys) BetterFoliage.blockTypes.stateKeys[blockState] = key
logger.log(INFO, "Adding model replacement $modelLocation -> $key")
}
fun <T: IUnbakedModel> loadHierarchy(model: T) = model.apply {
getMaterials(this@ModelDiscoveryContext::getUnbaked, mutableSetOf())
}
}
data class ModelBakingContext(
val bakery: ModelBakery,
val spriteGetter: Function<RenderMaterial, TextureAtlasSprite>,
val location: ResourceLocation,
val transform: IModelTransform,
val logger: Logger
) {
fun getUnbaked() = bakery.getModel(location)
fun getBaked() = bakery.getBakedModel(location, transform, spriteGetter)
}
class BakeWrapperManager : Invalidator, HasLogger() {
val discoverers = mutableListOf<ModelDiscovery>()
override val callbacks = mutableListOf<WeakReference<()->Unit>>()
private val replacements = mutableMapOf<ResourceLocation, ModelBakingKey>()
private val sprites = mutableSetOf<ResourceLocation>()
@SubscribeEvent(priority = EventPriority.LOWEST)
fun handleModelLoad(event: ModelDefinitionsLoadedEvent) {
val startTime = System.currentTimeMillis()
invalidate()
BetterFoliage.blockConfig.readConfig(resourceManager)
BetterFoliage.blockTypes = BlockTypeCache()
StartupMessageManager.addModMessage("BetterFoliage: discovering models")
logger.log(INFO, "starting model discovery (${discoverers.size} listeners)")
discoverers.forEach { listener ->
val replacementsLocal = mutableMapOf<ResourceLocation, ModelBakingKey>()
listener.onModelsLoaded(event.bakery, sprites, replacements)
}
val elapsed = System.currentTimeMillis() - startTime
logger.log(INFO, "finished model discovery in $elapsed ms, ${replacements.size} top-level replacements")
}
@SubscribeEvent
fun handleStitch(event: TextureStitchEvent.Pre) {
if (event.map.location() == Atlas.BLOCKS.resourceId) {
logger.log(INFO, "Adding ${sprites.size} sprites to block atlas")
sprites.forEach { event.addSprite(it) }
sprites.clear()
}
}
@SubscribeEvent
fun handleModelBake(event: ModelBakeEvent) {
replacements.clear()
}
fun onBake(
unbaked: IUnbakedModel,
bakery: ModelBakery,
spriteGetter: Function<RenderMaterial, TextureAtlasSprite>,
transform: IModelTransform,
location: ResourceLocation
): IBakedModel? {
val ctx = ModelBakingContext(bakery, spriteGetter, location, transform, detailLogger)
// bake replacement if available
replacements[location]?.let { replacement ->
detailLogger.log(INFO, "Baking replacement for [${unbaked::class.java.simpleName}] $location -> $replacement")
try {
return replacement.bake(ctx)
} catch (e: Exception) {
detailLogger.log(WARN, "Error while baking $replacement", e)
logger.log(WARN, "Error while baking $replacement", e)
}
}
return unbaked.bake(bakery, spriteGetter, transform, location)
}
}

View File

@@ -0,0 +1,14 @@
package mods.betterfoliage.resource.discovery
import net.minecraft.block.BlockState
class BlockTypeCache {
val leaf = mutableSetOf<BlockState>()
val grass = mutableSetOf<BlockState>()
val dirt = mutableSetOf<BlockState>()
val stateKeys = mutableMapOf<BlockState, ModelBakingKey>()
inline fun <reified T> getTypedOrNull(state: BlockState) = stateKeys[state] as? T
inline fun <reified T> hasTyped(state: BlockState) = stateKeys[state] is T
}

View File

@@ -0,0 +1,73 @@
package mods.betterfoliage.resource.discovery
import mods.betterfoliage.util.HasLogger
import mods.betterfoliage.util.getJavaClass
import mods.betterfoliage.util.getLines
import mods.betterfoliage.util.resourceManager
import net.minecraft.block.Block
import net.minecraft.util.ResourceLocation
import org.apache.logging.log4j.Level.INFO
interface IBlockMatcher {
fun matchesClass(block: Block): Boolean
fun matchingClass(block: Block): Class<*>?
}
class SimpleBlockMatcher(vararg val classes: Class<*>) : IBlockMatcher {
override fun matchesClass(block: Block) = matchingClass(block) != null
override fun matchingClass(block: Block): Class<*>? {
val blockClass = block.javaClass
classes.forEach { if (it.isAssignableFrom(blockClass)) return it }
return null
}
}
class ConfigurableBlockMatcher(val location: ResourceLocation) : HasLogger(), IBlockMatcher {
val blackList = mutableListOf<Class<*>>()
val whiteList = mutableListOf<Class<*>>()
override 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
}
override fun matchingClass(block: Block): Class<*>? {
val blockClass = block.javaClass
blackList.forEach { if (it.isAssignableFrom(blockClass)) return null }
whiteList.forEach { if (it.isAssignableFrom(blockClass)) return it }
return null
}
fun readDefaults() {
blackList.clear()
whiteList.clear()
resourceManager.getResources(location).forEach { resource ->
detailLogger.log(INFO, "Reading block class configuration $location from pack ${resource.sourceName}")
resource.getLines().map{ it.trim() }.filter { !it.startsWith("//") && it.isNotEmpty() }.forEach { line ->
if (line.startsWith("-")) getJavaClass(line.substring(1))?.let { blackList.add(it) }
else getJavaClass(line)?.let { whiteList.add(it) }
}
}
}
}
data class ModelTextureList(val modelLocation: ResourceLocation, val textureNames: List<String>) {
constructor(vararg args: String) : this(ResourceLocation(args[0]), listOf(*args).drop(1))
}
class ModelTextureListConfiguration(val location: ResourceLocation) : HasLogger() {
val modelList = mutableListOf<ModelTextureList>()
fun readDefaults() {
resourceManager.getResources(location).forEach { resource ->
detailLogger.log(INFO, "Reading model/texture configuration $location from pack ${resource.sourceName}")
resource.getLines().map{ it.trim() }.filter { !it.startsWith("//") && it.isNotEmpty() }.forEach { line ->
val elements = line.split(",")
modelList.add(ModelTextureList(ResourceLocation(elements.first()), elements.drop(1)))
}
}
}
}

View File

@@ -0,0 +1,108 @@
package mods.betterfoliage.resource.discovery
import com.google.common.base.Joiner
import mods.betterfoliage.util.HasLogger
import net.minecraft.client.renderer.BlockModelShapes
import net.minecraft.client.renderer.model.BlockModel
import net.minecraft.client.renderer.model.ModelBakery
import net.minecraft.client.renderer.model.VariantList
import net.minecraft.client.renderer.texture.MissingTextureSprite
import net.minecraft.util.ResourceLocation
import net.minecraftforge.registries.ForgeRegistries
import org.apache.logging.log4j.Level
abstract class AbstractModelDiscovery : HasLogger(), ModelDiscovery {
override fun onModelsLoaded(
bakery: ModelBakery,
sprites: MutableSet<ResourceLocation>,
replacements: MutableMap<ResourceLocation, ModelBakingKey>
) {
ForgeRegistries.BLOCKS
.flatMap { block -> block.stateDefinition.possibleStates }
.forEach { state ->
val location = BlockModelShapes.stateToModelLocation(state)
val ctx = ModelDiscoveryContext(bakery, state, location, sprites, replacements, detailLogger)
try {
processModel(ctx)
} catch (e: Exception) {
logger.log(Level.WARN, "Discovery error in $location", e)
}
}
}
open fun processModel(ctx: ModelDiscoveryContext) {
processContainerModel(ctx)
}
fun processContainerModel(ctx: ModelDiscoveryContext) {
val model = ctx.getUnbaked()
// built-in support for container models
if (model is VariantList) {
// per-location replacements need to be scoped to the variant list, as replacement models
// may need information from the BlockState which is not available at baking time
val scopedReplacements = mutableMapOf<ResourceLocation, ModelBakingKey>()
model.variants.forEach { variant ->
processModel(ctx.copy(modelLocation = variant.modelLocation, replacements = scopedReplacements))
}
if (scopedReplacements.isNotEmpty()) {
ctx.addReplacement(WeightedUnbakedKey(scopedReplacements), addToStateKeys = false)
}
}
}
}
abstract class ConfigurableModelDiscovery : AbstractModelDiscovery() {
abstract val matchClasses: IBlockMatcher
abstract val modelTextures: List<ModelTextureList>
abstract fun processModel(
ctx: ModelDiscoveryContext,
textureMatch: List<ResourceLocation>
)
override fun processModel(ctx: ModelDiscoveryContext) {
val model = ctx.getUnbaked()
if (model is BlockModel) {
val matchClass = matchClasses.matchingClass(ctx.blockState.block) ?: return
detailLogger.log(Level.INFO, "block state ${ctx.blockState}")
detailLogger.log(Level.INFO, " model ${ctx.modelLocation}")
detailLogger.log(Level.INFO, " class ${ctx.blockState.block.javaClass.name} matches ${matchClass.name}")
val ancestry = ctx.bakery.getAncestry(ctx.modelLocation)
val matches = modelTextures.filter { matcher ->
matcher.modelLocation in ancestry
}
matches.forEach { match ->
detailLogger.log(Level.INFO, " model $model matches ${match.modelLocation}")
val materials = match.textureNames.map { it to model.getMaterial(it) }
val texMapString = Joiner.on(", ").join(materials.map { "${it.first}=${it.second.texture()}" })
detailLogger.log(Level.INFO, " sprites [$texMapString]")
if (materials.all { it.second.texture() != MissingTextureSprite.getLocation() }) {
// found a valid model (all required textures exist)
processModel(ctx, materials.map { it.second.texture() })
}
}
if (matches.isEmpty()) {
detailLogger.log(Level.INFO, " no matches for model ${ctx.modelLocation}, inheritance chain ${ancestry.joinToString(" -> ")}")
}
}
return super.processModel(ctx)
}
}
fun ModelBakery.modelDerivesFrom(model: BlockModel, location: ResourceLocation, target: ResourceLocation): Boolean =
if (location == target) true
else model.parentLocation
?.let { getModel(it) as? BlockModel }
?.let { parent -> modelDerivesFrom(parent, model.parentLocation!!, target) }
?: false
fun ModelBakery.getAncestry(location: ResourceLocation): List<ResourceLocation> {
val model = getModel(location) as? BlockModel ?: return listOf(location)
val parentAncestry = model.parentLocation?.let { getAncestry(it) } ?: emptyList()
return listOf(location) + parentAncestry
}

View File

@@ -0,0 +1,97 @@
package mods.betterfoliage.resource.discovery
import mods.betterfoliage.BetterFoliage
import mods.betterfoliage.config.match.MAnything
import mods.betterfoliage.config.match.MListAll
import mods.betterfoliage.config.match.MListAny
import mods.betterfoliage.config.match.MNegated
import mods.betterfoliage.config.match.MValue
import mods.betterfoliage.config.match.MatchRules
import mods.betterfoliage.util.HasLogger
import net.minecraft.client.renderer.model.BlockModel
import net.minecraft.client.renderer.model.VariantList
import net.minecraft.util.ResourceLocation
import org.apache.logging.log4j.Level
abstract class ParametrizedModelDiscovery : HasLogger() {
abstract fun processModel(ctx: ModelDiscoveryContext, params: Map<String, String>)
fun Map<String, String>.location(key: String): ResourceLocation? {
val result = get(key)?.let { ResourceLocation(it) }
if (result == null) detailLogger.log(Level.WARN, "Cannot find texture parameter \"$key\"")
return result
}
fun Map<String, String>.int(key: String): Int? {
val result = get(key)?.toInt()
if (result == null) detailLogger.log(Level.WARN, "Cannot find integer parameter \"$key\"")
return result
}
}
class RuleProcessingContext(
val discovery: ModelDiscoveryContext
) {
val params = mutableMapOf("type" to "none")
}
class RuleBasedDiscovery : AbstractModelDiscovery() {
val discoverers = mutableMapOf<String, ParametrizedModelDiscovery>()
override fun processModel(ctx: ModelDiscoveryContext) = when(ctx.getUnbaked()) {
is VariantList -> processContainerModel(ctx)
is BlockModel -> processBlockModel(ctx)
else -> Unit
}
fun processBlockModel(ctx: ModelDiscoveryContext) {
val ruleCtx = RuleProcessingContext(ctx)
val rulesToCheck = BetterFoliage.blockConfig.rules.toMutableList()
val ruleResults = mutableListOf<MListAll>()
var previousSize = 0
// stop processing if nothing changes anymore
while (rulesToCheck.size != previousSize) {
previousSize = rulesToCheck.size
val iterator = rulesToCheck.listIterator()
while (iterator.hasNext()) iterator.next().let { rule ->
// process single rule
MatchRules.visitRoot(ruleCtx, rule).let { result ->
ruleResults.add(result)
// remove rule from active list if:
// - rule succeeded (all directives returned success)
// - rule is immutable (result will always be the same)
if (result.value || result.immutable) iterator.remove()
}
}
}
// log result of rule processing
if (ruleResults.any { it.value }) {
detailLogger.log(Level.INFO, "================================")
detailLogger.log(Level.INFO, "block state: ${ctx.blockState}")
detailLogger.log(Level.INFO, "block class: ${ctx.blockState.block::class.java.name}")
detailLogger.log(Level.INFO, "model : ${ctx.modelLocation}")
detailLogger.log(Level.INFO, "--------------------------------")
ruleResults.forEach { logResult(it) }
}
discoverers[ruleCtx.params["type"]]?.processModel(ctx, ruleCtx.params)
}
fun logResult(match: MAnything<Boolean>) {
when(match) {
is MListAll -> if (match.list.any { it.value }) {
var seenInvariantSuccess = false
match.list.forEach { item ->
if (item.immutable && item.value) seenInvariantSuccess = true
if (seenInvariantSuccess) logResult(item)
}
}
is MListAny -> if (match.value) match.list.first { it.value }.let { logResult(it) }
else match.list.forEach { logResult(it) }
is MNegated -> logResult(match.inner)
is MValue<Boolean> -> detailLogger.log(Level.INFO, "[${match.configSource}] ${match.description}")
}
}
}

View File

@@ -0,0 +1,58 @@
package mods.betterfoliage.resource.discovery
import mods.betterfoliage.model.HalfBakedSimpleModelWrapper
import mods.betterfoliage.model.SpecialRenderModel
import mods.betterfoliage.model.WeightedModelWrapper
import mods.betterfoliage.util.HasLogger
import net.minecraft.client.renderer.model.IBakedModel
import net.minecraft.client.renderer.model.SimpleBakedModel
import net.minecraft.client.renderer.model.VariantList
import net.minecraft.util.ResourceLocation
import org.apache.logging.log4j.Level.INFO
import org.apache.logging.log4j.Level.WARN
class WeightedUnbakedKey(
val replacements: Map<ResourceLocation, ModelBakingKey>
) : ModelBakingKey {
override fun bake(ctx: ModelBakingContext): IBakedModel? {
val unbaked = ctx.getUnbaked()
if (unbaked !is VariantList) return super.bake(ctx)
// bake all variants, replace as needed
val bakedModels = unbaked.variants.mapNotNull {
val variantCtx = ctx.copy(location = it.modelLocation, transform = it)
val replacement = replacements[it.modelLocation]
val baked = replacement?.let { replacement ->
ctx.logger.log(INFO, "Baking replacement for variant [${variantCtx.getUnbaked()::class.java.simpleName}] ${variantCtx.location} -> $replacement")
replacement.bake(variantCtx)
} ?: variantCtx.getBaked()
when(baked) {
is SpecialRenderModel -> it to baked
// just in case we replaced some variants in the list, but not others
// this should not realistically happen, this is just a best-effort fallback
is SimpleBakedModel -> it to HalfBakedSimpleModelWrapper(baked)
else -> null
}
}
// something fishy going on, possibly unknown model type
// let it through unchanged
if (bakedModels.isEmpty()) return super.bake(ctx)
if (bakedModels.size < unbaked.variants.size) {
detailLogger.log(
WARN,
"Dropped ${unbaked.variants.size - bakedModels.size} variants from model ${ctx.location}"
)
}
val weightedSpecials = bakedModels.map { (variant, model) ->
WeightedModelWrapper.WeightedModel(model, variant.weight)
}
return WeightedModelWrapper(weightedSpecials, weightedSpecials[0].model)
}
override fun toString() = "[SpecialRenderVariantList, ${replacements.size} replacements]"
companion object : HasLogger()
}

View File

@@ -0,0 +1,39 @@
package mods.betterfoliage.resource.generated
import mods.betterfoliage.util.Atlas
import mods.betterfoliage.util.bytes
import mods.betterfoliage.util.loadSprite
import net.minecraft.resources.IResourceManager
import net.minecraft.util.ResourceLocation
import java.awt.image.BufferedImage
import java.lang.Math.max
data class CenteredSprite(val sprite: ResourceLocation, val aspectHeight: Int = 1, val aspectWidth: Int = 1, val atlas: Atlas = Atlas.BLOCKS) {
fun register(pack: GeneratedTexturePack) = pack.register(atlas, this, this::draw)
fun draw(resourceManager: IResourceManager): ByteArray {
val baseTexture = resourceManager.loadSprite(atlas.file(sprite))
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 until frames) {
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.bytes
}
}

View File

@@ -0,0 +1,55 @@
package mods.betterfoliage.resource.generated
import mods.betterfoliage.util.Atlas
import mods.betterfoliage.util.blendRGB
import mods.betterfoliage.util.bytes
import mods.betterfoliage.util.get
import mods.betterfoliage.util.loadSprite
import mods.betterfoliage.util.set
import net.minecraft.resources.IResourceManager
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
*/
data class GeneratedGrass(val baseSprite: ResourceLocation, val isSnowed: Boolean, val atlas: Atlas = Atlas.BLOCKS) {
constructor(sprite: String, isSnowed: Boolean) : this(ResourceLocation(sprite), isSnowed)
fun register(pack: GeneratedTexturePack) = pack.register(atlas, this, this::draw)
fun draw(resourceManager: IResourceManager): ByteArray {
val baseTexture = resourceManager.loadSprite(atlas.file(baseSprite))
val result = BufferedImage(baseTexture.width, baseTexture.height, BufferedImage.TYPE_4BYTE_ABGR)
val graphics = result.createGraphics()
val size = baseTexture.width
val frames = baseTexture.height / size
// iterate all frames
for (frame in 0 until frames) {
val baseFrame = baseTexture.getSubimage(0, size * frame, size, size)
val grassFrame = BufferedImage(size, size, BufferedImage.TYPE_4BYTE_ABGR)
// draw bottom half of texture
grassFrame.createGraphics().apply {
drawImage(baseFrame, 0, 3 * size / 8, null)
}
// add to animated png
graphics.drawImage(grassFrame, 0, size * frame, null)
}
// blend with white if snowed
if (isSnowed) {
for (x in 0 until result.width) for (y in 0 until result.height) {
result[x, y] = blendRGB(result[x, y], 16777215, 2, 3)
}
}
return result.bytes
}
}

View File

@@ -0,0 +1,92 @@
package mods.betterfoliage.resource.generated
import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.util.Atlas
import mods.betterfoliage.util.bytes
import mods.betterfoliage.util.get
import mods.betterfoliage.util.loadImage
import mods.betterfoliage.util.loadSprite
import mods.betterfoliage.util.resourceManager
import mods.betterfoliage.util.set
import net.minecraft.resources.IResource
import net.minecraft.resources.IResourceManager
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.
*
* Different leaf types may have their own alpha mask.
*
* @param[domain] Resource domain of generator
*/
data class GeneratedLeafSprite(val baseSprite: ResourceLocation, val leafType: String, val atlas: Atlas = Atlas.BLOCKS) {
fun register(pack: GeneratedTexturePack) = pack.register(atlas, this, this::draw)
fun draw(resourceManager: IResourceManager): ByteArray {
val baseTexture = resourceManager.loadSprite(atlas.file(baseSprite))
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 until frames) {
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 (maskTexture != null) {
for (x in 0 until size * 2) for (y in 0 until size * 2) {
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.bytes
}
/**
* 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 ->
Atlas.BLOCKS.file(ResourceLocation(BetterFoliageMod.MOD_ID, "blocks/leafmask_${size}_${type}"))
}
/**
* 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 = mutableListOf<Int>()
while(size > 2) { sizes.add(size); size /= 2 }
return sizes.map { resourceManager[maskPath(it)] }.filterNotNull().firstOrNull()
}
}

View File

@@ -0,0 +1,69 @@
package mods.betterfoliage.resource.generated
import mods.betterfoliage.util.Atlas
import mods.betterfoliage.util.HasLogger
import net.minecraft.client.Minecraft
import net.minecraft.resources.*
import net.minecraft.resources.ResourcePackType.CLIENT_RESOURCES
import net.minecraft.resources.data.IMetadataSectionSerializer
import net.minecraft.util.ResourceLocation
import net.minecraft.util.text.StringTextComponent
import org.apache.logging.log4j.Level.INFO
import java.util.*
import java.util.function.Consumer
import java.util.function.Predicate
import java.util.function.Supplier
/**
* [IResourcePack] containing generated resources
*
* @param[name] Name of the resource pack
* @param[generators] List of resource generators
*/
class GeneratedTexturePack(
val nameSpace: String, val packName: String
) : HasLogger(), IResourcePack {
override fun getName() = packName
override fun getNamespaces(type: ResourcePackType) = setOf(nameSpace)
override fun <T : Any?> getMetadataSection(deserializer: IMetadataSectionSerializer<T>) = null
override fun getRootResource(id: String) = null
override fun getResources(type: ResourcePackType, namespace:String, path: String, maxDepth: Int, filter: Predicate<String>) = emptyList<ResourceLocation>()
override fun close() {}
protected var manager: IResourceManager = Minecraft.getInstance().resourceManager
val identifiers = Collections.synchronizedMap(mutableMapOf<Any, ResourceLocation>())
val resources = Collections.synchronizedMap(mutableMapOf<ResourceLocation, ByteArray>())
fun register(atlas: Atlas, key: Any, func: (IResourceManager)->ByteArray): ResourceLocation {
identifiers[key]?.let { return it }
val id = ResourceLocation(nameSpace, UUID.randomUUID().toString())
val fileName = atlas.file(id)
val resource = func(manager)
identifiers[key] = id
resources[fileName] = resource
detailLogger.log(INFO, "generated resource $key -> $fileName")
return id
}
override fun getResource(type: ResourcePackType, id: ResourceLocation) =
if (type != CLIENT_RESOURCES) null else resources[id]?.inputStream()
override fun hasResource(type: ResourcePackType, id: ResourceLocation) =
type == CLIENT_RESOURCES && resources.containsKey(id)
val finder = object : IPackFinder {
val packInfo = ResourcePackInfo(
packName, true, Supplier { this@GeneratedTexturePack },
StringTextComponent(packName),
StringTextComponent("Generated block textures resource pack"),
PackCompatibility.COMPATIBLE, ResourcePackInfo.Priority.TOP, true, IPackNameDecorator.DEFAULT, true
)
override fun loadPacks(p0: Consumer<ResourcePackInfo>, p1: ResourcePackInfo.IFactory) {
p0.accept(packInfo)
}
}
}

View File

@@ -0,0 +1,67 @@
package mods.betterfoliage.util
import java.lang.ref.WeakReference
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
interface Invalidator {
fun invalidate() {
val iterator = callbacks.iterator()
while(iterator.hasNext()) iterator.next().let { callback ->
callback.get()?.invoke() ?: iterator.remove()
}
}
val callbacks: MutableList<WeakReference<()->Unit>>
fun onInvalidate(callback: ()->Unit) {
callbacks.add(WeakReference(callback))
}
}
class SimpleInvalidator : Invalidator {
override val callbacks = mutableListOf<WeakReference<() -> Unit>>()
}
class LazyInvalidatable<V>(invalidator: Invalidator, val valueFactory: ()->V): ReadOnlyProperty<Any, V> {
init { invalidator.onInvalidate { value = null } }
var value: V? = null
override fun getValue(thisRef: Any, property: KProperty<*>): V {
value?.let { return it }
return synchronized(this) {
value?.let { return it }
valueFactory().apply { value = this }
}
}
}
open class LazyMapInvalidatable<K, V>(val invalidator: Invalidator, val valueFactory: (K)->V) {
init { invalidator.onInvalidate { values.clear() } }
val values = mutableMapOf<K, V>()
operator fun get(key: K): V {
values[key]?.let { return it }
return synchronized(values) {
values[key]?.let { return it }
valueFactory(key).apply { values[key] = this }
}
}
operator fun set(key: K, value: V) { values[key] = value }
fun delegate(key: K) = Delegate(key)
inner class Delegate(val key: K) : ReadOnlyProperty<Any, V> {
init { invalidator.onInvalidate { cached = null } }
private var cached: V? = null
override fun getValue(thisRef: Any, property: KProperty<*>): V {
cached?.let { return it }
get(key).let { cached = it; return it }
}
}
}
fun <V> Invalidator.lazy(valueFactory: ()->V) = LazyInvalidatable(this, valueFactory)
fun <K, V> Invalidator.lazyMap(valueFactory: (K)->V) = LazyMapInvalidatable(this, valueFactory)

View File

@@ -0,0 +1,74 @@
package mods.betterfoliage.util
import com.google.common.collect.ImmutableList
import java.util.*
/**
* Starting with the second element of this [Iterable] until the last, call the supplied lambda with
* the parameters (index, element, previous element).
*/
inline fun <reified T> Iterable<T>.forEachPairIndexed(func: (Int, T, T)->Unit) {
var previous: T? = null
forEachIndexed { idx, current ->
if (previous != null) func(idx, current, previous!!)
previous = current
}
}
inline fun <T, C: Comparable<C>> Pair<T, T>.minBy(func: (T)->C) =
if (func(first) < func(second)) first else second
inline fun <T, C: Comparable<C>> Pair<T, T>.maxBy(func: (T)->C) =
if (func(first) > func(second)) first else second
inline fun <T, C: Comparable<C>> Triple<T, T, T>.maxValueBy(func: (T)->C): C {
var result = func(first)
func(second).let { if (it > result) result = it }
func(third).let { if (it > result) result = it }
return result
}
inline fun <reified T, reified R> Array<T>.mapArray(func: (T)->R) = Array<R>(size) { idx -> func(get(idx)) }
@Suppress("UNCHECKED_CAST")
inline fun <K, V> Map<K, V?>.filterValuesNotNull() = filterValues { it != null } as Map<K, V>
inline fun <reified T, R> Iterable<T>.findFirst(func: (T)->R?): R? {
forEach { func(it)?.let { return it } }
return null
}
inline fun <A1, reified A2, B> List<Pair<A1, B>>.filterIsInstanceFirst(cls: Class<A2>) = filter { it.first is A2 } as List<Pair<A2, B>>
/** Cross product of this [Iterable] with the parameter. */
fun <A, B> Iterable<A>.cross(other: Iterable<B>) = flatMap { a -> other.map { b -> a to b } }
inline fun <C, R, T> Iterable<T>.mapAs(transform: (C) -> R) = map { transform(it as C) }
inline fun <T1, T2> forEachNested(list1: Iterable<T1>, list2: Iterable<T2>, func: (T1, T2)-> Unit) =
list1.forEach { e1 ->
list2.forEach { e2 ->
func(e1, e2)
}
}
/** Mutating version of _map_. Replace each element of the list with the result of the given transformation. */
inline fun <reified T> MutableList<T>.replace(transform: (T) -> T) = forEachIndexed { idx, value -> this[idx] = transform(value) }
/** Exchange the two elements of the list with the given indices */
inline fun <T> MutableList<T>.exchange(idx1: Int, idx2: Int) {
val e = this[idx1]
this[idx1] = this[idx2]
this[idx2] = e
}
/** Return a random element from the array using the provided random generator */
inline operator fun <T> Array<T>.get(random: Random) = get(random.nextInt(Int.MAX_VALUE) % size)
inline fun Random.idx(array: Array<*>) = nextInt(Int.MAX_VALUE) % array.size
inline fun Random.idxOrNull(array: Array<*>, predicate: ()->Boolean) = if (predicate()) idx(array) else null
fun <T> Iterable<T>.toImmutableList() = ImmutableList.builder<T>().let { builder ->
forEach { builder.add(it) }
builder.build()
}

View File

@@ -0,0 +1,15 @@
package mods.betterfoliage.util
import net.minecraft.client.Minecraft
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionStage
import java.util.function.Consumer
import java.util.function.Function
fun completedVoid() = CompletableFuture.completedFuture<Void>(null)!!
fun <T, U> CompletionStage<T>.map(func: (T)->U) = thenApply(Function(func)).toCompletableFuture()!!
fun <T, U> CompletionStage<T>.mapAsync(func: (T)->U) = thenApplyAsync(Function(func), Minecraft.getInstance()).toCompletableFuture()!!
fun <T> CompletionStage<T>.sink(func: (T)->Unit) = thenAccept(Consumer(func)).toCompletableFuture()!!
fun <T> CompletionStage<T>.sinkAsync(func: (T)->Unit) = thenAcceptAsync(Consumer(func), Minecraft.getInstance()).toCompletableFuture()!!

View File

@@ -0,0 +1,252 @@
package mods.betterfoliage.util
import net.minecraft.util.Direction
import net.minecraft.util.Direction.*
import net.minecraft.util.Direction.Axis.*
import net.minecraft.util.Direction.AxisDirection.NEGATIVE
import net.minecraft.util.Direction.AxisDirection.POSITIVE
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.vector.Quaternion
val EPSILON_ZERO = 0.05
val EPSILON_ONE = 0.95
// ================================
// Axes and directions
// ================================
val axes = listOf(X, Y, Z)
val axisDirs = listOf(POSITIVE, NEGATIVE)
val Direction.dir: AxisDirection get() = axisDirection
val AxisDirection.sign: String get() = when(this) { POSITIVE -> "+"; NEGATIVE -> "-" }
val allDirections = Direction.values()
val horizontalDirections = listOf(NORTH, SOUTH, EAST, WEST)
val allDirOffsets = allDirections.map { Int3(it) }
val Pair<Axis, AxisDirection>.face: Direction get() = when(this) {
X to POSITIVE -> EAST; X to NEGATIVE -> WEST;
Y to POSITIVE -> UP; Y to NEGATIVE -> DOWN;
Z to POSITIVE -> SOUTH; else -> NORTH;
}
val directionsAndNull = arrayOf(DOWN, UP, NORTH, SOUTH, WEST, EAST, null)
val Direction.perpendiculars: Array<Direction> get() =
axes.filter { it != this.axis }.flatMap { listOf((it to POSITIVE).face, (it to NEGATIVE).face) }.toTypedArray()
val perpendiculars: Array<Array<Direction>> = Direction.values().map { dir ->
axes.filter { it != dir.axis }
.flatMap { listOf(
(it to POSITIVE).face,
(it to NEGATIVE).face
) }.toTypedArray()
}.toTypedArray()
val Direction.offset: Int3 get() = allDirOffsets[ordinal]
/** Old ForgeDirection rotation matrix yanked from 1.7.10 */
val ROTATION_MATRIX: Array<IntArray> get() = arrayOf(
intArrayOf(0, 1, 4, 5, 3, 2, 6),
intArrayOf(0, 1, 5, 4, 2, 3, 6),
intArrayOf(5, 4, 2, 3, 0, 1, 6),
intArrayOf(4, 5, 2, 3, 1, 0, 6),
intArrayOf(2, 3, 1, 0, 4, 5, 6),
intArrayOf(3, 2, 0, 1, 4, 5, 6)
)
// ================================
// Vectors
// ================================
operator fun Direction.times(scale: Double) =
Double3(normal.x.toDouble() * scale, normal.y.toDouble() * scale, normal.z.toDouble() * scale)
val Direction.vec: Double3 get() = Double3(normal.x.toDouble(), normal.y.toDouble(), normal.z.toDouble())
operator fun BlockPos.plus(other: Int3) = BlockPos(x + other.x, y + other.y, z + other.z)
/** 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: Direction) : this(dir.normal.x.toDouble(), dir.normal.y.toDouble(), dir.normal.z.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)
fun lerp(delta: Double, first: Double3, second: Double3) = first + (second - first) * delta
}
// 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)
)
/** Rotate vector by the given [Quaternion] */
fun rotate(quat: Quaternion) =
quat.copy()
.apply { mul(Quaternion(x.toFloat(), y.toFloat(), z.toFloat(), 0.0F)) }
.apply { mul(quat.copy().apply(Quaternion::conj)) }
.let { Double3(it.i().toDouble(), it.j().toDouble(), it.k().toDouble()) }
// 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: Direction get() = nearestAngle(this, allDirections.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: Direction) : this(dir.normal.x, dir.normal.y, dir.normal.z)
constructor(offset: Pair<Int, Direction>) : this(
offset.first * offset.second.normal.x,
offset.first * offset.second.normal.y,
offset.first * offset.second.normal.z
)
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<Int, Direction>) = Int3(
x + other.first * other.second.normal.x,
y + other.first * other.second.normal.y,
z + other.first * other.second.normal.z
)
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 Direction.rotations: Array<Direction> get() =
Array(6) { idx -> Direction.values()[ROTATION_MATRIX[ordinal][idx]] }
fun Direction.rotate(rot: Rotation) = rot.forward[ordinal]
fun rot(axis: Direction) = 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<Direction>, val reverse: Array<Direction>) {
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: Direction, 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: Direction, 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(allDirections[idx].opposite.rotations, allDirections[idx].rotations) }
val identity = Rotation(allDirections, allDirections)
val fromUp = arrayOf(
rot90[EAST.ordinal] * 2,
identity,
rot90[WEST.ordinal],
rot90[EAST.ordinal],
rot90[SOUTH.ordinal],
rot90[NORTH.ordinal]
)
}
}
// ================================
// Miscellaneous
// ================================
inline operator fun <reified T> Array<T>.get(face: Direction): T = get(face.ordinal)
data class BoxFace(val top: Direction, val left: Direction) {
val allCorners = listOf(top to left, top to left.opposite, top.opposite to left, top.opposite to left.opposite)
}
val boxFaces = allDirections.map { when(it) {
DOWN -> BoxFace(SOUTH, WEST)
UP -> BoxFace(SOUTH, EAST)
NORTH -> BoxFace(WEST, UP)
SOUTH -> BoxFace(UP, WEST)
WEST -> BoxFace(SOUTH, UP)
EAST -> BoxFace(SOUTH, DOWN)
}}.toTypedArray()
/** List of all 12 box edges, represented as a [Pair] of [ForgeDirection]s */
val boxEdges = allDirections.flatMap { face1 -> allDirections.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 <T> nearestPosition(vertex: Double3, objs: Iterable<T>, objPos: (T)-> Double3): Pair<T, Double> =
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 <T> nearestAngle(vector: Double3, objs: Iterable<T>, objAngle: (T)-> Double3): Pair<T, Double> =
objs.map { it to objAngle(it).dot(vector) }.maxBy { it.second }!!

View File

@@ -0,0 +1,25 @@
@file:JvmName("Utils")
package mods.betterfoliage.util
import net.minecraft.util.text.StringTextComponent
import net.minecraft.util.text.Style
import net.minecraft.util.text.TextFormatting
import net.minecraft.util.text.TextFormatting.AQUA
import net.minecraft.util.text.TextFormatting.GRAY
//fun stripTooltipDefaultText(tooltip: MutableList<String>) {
// var defaultRows = false
// val iter = tooltip.iterator()
// while (iter.hasNext()) {
// if (iter.next().startsWith(AQUA.toString())) defaultRows = true
// if (defaultRows) iter.remove()
// }
//}
//
//fun textComponent(msg: String, color: TextFormatting = GRAY): StringTextComponent {
// val style = Style().apply { this.color = color }
// return StringTextComponent(msg).apply { this.style = style }
//}
val styleGray = Style.EMPTY.applyFormats(GRAY)
fun String.asText() = StringTextComponent(this).setStyle(styleGray)

View File

@@ -0,0 +1,89 @@
@file:Suppress("NOTHING_TO_INLINE")
package mods.betterfoliage.util
import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.model.HSB
import net.minecraft.block.BlockState
import net.minecraft.client.Minecraft
import net.minecraft.util.ResourceLocation
import net.minecraft.util.math.BlockPos
import net.minecraft.world.World
import org.apache.logging.log4j.Level
import org.apache.logging.log4j.Logger
import java.lang.Math.*
import kotlin.reflect.KProperty
const val PI2 = 2.0 * PI
/** Strip the given prefix off the start of the string, if present */
inline fun String.stripStart(str: String, ignoreCase: Boolean = true) = if (startsWith(str, ignoreCase)) substring(str.length) else this
inline fun String.stripEnd(str: String, ignoreCase: Boolean = true) = if (endsWith(str, ignoreCase)) substring(0, length - str.length) else this
/** Strip the given prefix off the start of the resource path, if present */
inline fun ResourceLocation.stripStart(str: String) = ResourceLocation(namespace, path.stripStart(str))
inline fun ResourceLocation.stripEnd(str: String) = ResourceLocation(namespace, path.stripEnd(str))
val String.quoted: String get() = "\"$this\""
/**
* Property-level delegate backed by a [ThreadLocal].
*
* @param[init] Lambda to get initial value
*/
class ThreadLocalDelegate<T>(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) }
}
fun <T> ThreadLocal<T?>.getWithDefault(factory: ()->T): T {
get()?.let { return it }
return factory().apply { set(this) }
}
/** Call the supplied lambda and return its result, or the given default value if an exception is thrown. */
fun <T> tryDefault(default: T, work: ()->T) = try { work() } catch (e: Throwable) { default }
/**
* 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))
}
@Suppress("LeakingThis")
abstract class HasLogger {
val logger = BetterFoliageMod.logger(this)
val detailLogger = BetterFoliageMod.detailLogger(this)
}
fun Logger.logTextureColor(level: Level, description: String, avgColor: HSB) {
val rgb = avgColor.asColor
log(level, "$description average color RGB[${rgb.red},${rgb.green},${rgb.blue}], HSB[${avgColor.hue},${avgColor.saturation},${avgColor.brightness}]")
}
fun getBlockModel(state: BlockState) = Minecraft.getInstance().blockRenderer.blockModelShaper.getBlockModel(state)
/**
* Check if the Chunk containing the given [BlockPos] is loaded.
* Works for both [World] and [ChunkCache] (vanilla and OptiFine) instances.
*/
//fun IWorldReader.isBlockLoaded(pos: BlockPos) = when {
// this is World -> isBlockLoaded(pos, false)
// this is RenderChunkCache -> isworld.isBlockLoaded(pos, false)
// Refs.OptifineChunkCache.isInstance(this) -> (Refs.CCOFChunkCache.get(this) as ChunkCache).world.isBlockLoaded(pos, false)
// else -> false
//}
//fun textComponent(msg: String, color: Formatting = Formatting.GRAY): LiteralText {
// val style = Style().apply { this.color = color }
// return LiteralText(msg).apply { this.style = style }
//}

View File

@@ -0,0 +1,23 @@
package mods.betterfoliage.util
import net.minecraft.util.math.BlockPos
import java.util.Random
val random = Random(System.nanoTime())
fun randomB() = random.nextBoolean()
fun randomI(min: Int = 0, max: Int = Int.MAX_VALUE) = min + random.nextInt(max - min)
fun randomF(min: Float = 0.0f, max: Float = 1.0f) = random.randomF(min, max)
fun randomD(min: Double = 0.0, max: Double = 1.0) = random.randomD(min, max)
fun Random.randomF(min: Float = 0.0f, max: Float = 1.0f) = nextFloat() * (max - min) + min
fun Random.randomF(min: Double = 0.0, max: Double = 1.0) = randomF(min.toFloat(), max.toFloat())
fun Random.randomD(min: Double = 0.0, max: Double = 1.0) = nextDouble() * (max - min) + min
fun semiRandom(x: Int, y: Int, z: Int, seed: Int): Int {
var value = (x * x + y * y + z * z + x * y + y * z + z * x + (seed * seed))
value = (3 * x * value + 5 * y * value + 7 * z * value + (11 * seed))
return value shr 4
}
fun BlockPos.semiRandom(seed: Int) = semiRandom(x, y, z, seed)

View File

@@ -0,0 +1,179 @@
@file:JvmName("Reflection")
package mods.betterfoliage.util
import java.lang.reflect.Field
import java.lang.reflect.Method
import mods.betterfoliage.util.Namespace.*
/** 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 <reified T> 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 <reified T> 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<Pair<String, Any>> 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()
fun <T> Any.reflectFields(type: Class<T>) = this.javaClass.declaredFields
.filter { field -> type.isAssignableFrom(field.type) }
.map { field -> field.name to field.let { it.isAccessible = true; it.get(this) as T } }
fun <T> Any.reflectDelegates(type: Class<T>) = this.javaClass.declaredFields
.filter { field -> type.isAssignableFrom(field.type) && field.name.contains("$") }
.map { field -> field.name.split("$")[0] to field.let { it.isAccessible = true; it.get(this) } as T }
enum class Namespace { MCP, SRG }
abstract class Resolvable<T> {
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[name] MCP name of the class
*/
open class ClassRef<T: Any?>(val name: String) : Resolvable<Class<T>>() {
companion object {
val int = ClassRefPrimitive("I", Int::class.java)
val long = ClassRefPrimitive("J", Long::class.java)
val float = ClassRefPrimitive("F", Float::class.java)
val boolean = ClassRefPrimitive("Z", Boolean::class.java)
val void = ClassRefPrimitive("V", Void::class.java)
}
open fun asmDescriptor(namespace: Namespace) = "L${name.replace(".", "/")};"
override fun resolve() = getJavaClass(name) as Class<T>?
fun isInstance(obj: Any) = element?.isInstance(obj) ?: false
}
/**
* Reference to a primitive type.
*
* @param[name] ASM descriptor of this primitive type
* @param[clazz] class of this primitive type
*/
class ClassRefPrimitive<T>(name: String, val clazz: Class<T>?) : ClassRef<T>(name) {
override fun asmDescriptor(namespace: Namespace) = name
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[returnType] reference to the return type
* @param[returnType] references to the argument types
*/
class MethodRef<T: Any?>(val parentClass: ClassRef<*>,
val mcpName: String,
val srgName: String,
val returnType: ClassRef<T>,
vararg val argTypes: ClassRef<*>
) : Resolvable<Method>() {
constructor(parentClass: ClassRef<*>, mcpName: String, returnType: ClassRef<T>, vararg argTypes: ClassRef<*>) :
this(parentClass, mcpName, mcpName, returnType, *argTypes)
fun name(namespace: Namespace) = when(namespace) { 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. */
operator fun invoke(receiver: Any, vararg args: Any?) = element?.invoke(receiver, *args) as T
/** 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[type] reference to the field type
*/
class FieldRef<T>(val parentClass: ClassRef<*>,
val mcpName: String,
val srgName: String,
val type: ClassRef<T>
) : Resolvable<Field>() {
constructor(parentClass: ClassRef<*>, mcpName: String, type: ClassRef<T>) : this(parentClass, mcpName, mcpName, type)
fun name(namespace: Namespace) = when(namespace) { SRG -> srgName; MCP -> mcpName }
fun asmDescriptor(namespace: Namespace) = type.asmDescriptor(namespace)
override fun resolve(): Field? =
if (parentClass.element == null) null
else {
listOf(srgName, mcpName).mapNotNull { tryDefault(null) {
parentClass.element!!.getDeclaredField(it)
} }.firstOrNull()
?.apply{ isAccessible = true }
}
/** Get this field using reflection. */
operator fun get(receiver: Any?) = element?.get(receiver) as T
/** Get this static field using reflection. */
fun getStatic() = get(null)
fun set(receiver: Any?, obj: Any?) { element?.set(receiver, obj) }
}
fun Any.isInstance(cls: ClassRef<*>) = cls.isInstance(this)
interface ReflectionCallable<T> {
operator fun invoke(vararg args: Any): T
}
inline operator fun <reified T> Any.get(field: FieldRef<T>) = field.get(this)
inline operator fun <reified T> Any.set(field: FieldRef<T>, value: T) = field.set(this, value)
inline operator fun <T> Any.get(methodRef: MethodRef<T>) = object : ReflectionCallable<T> {
override fun invoke(vararg args: Any) = methodRef.invoke(this@get, args)
}

View File

@@ -0,0 +1,30 @@
package mods.betterfoliage.util
import net.minecraft.client.Minecraft
import net.minecraft.resources.IReloadableResourceManager
import net.minecraft.resources.IResource
import net.minecraft.resources.IResourceManager
import net.minecraft.util.ResourceLocation
/** Concise getter for the Minecraft resource manager. */
val resourceManager: IReloadableResourceManager
get() = Minecraft.getInstance().resourceManager as IReloadableResourceManager
/** Append a string to the [ResourceLocation]'s path. */
operator fun ResourceLocation.plus(str: String) = ResourceLocation(namespace, path + str)
/** Prepend a string to the [ResourceLocation]'s path. */
fun ResourceLocation.prependLocation(basePath: String) =
ResourceLocation(namespace, basePath.stripEnd("/").let { "$it/$path" })
/** 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) }
/** Get the lines of a text resource. */
fun IResource.getLines(): List<String> {
val result = arrayListOf<String>()
inputStream.bufferedReader().useLines { it.forEach { result.add(it) } }
return result
}

View File

@@ -0,0 +1,137 @@
package mods.betterfoliage.util
import mods.betterfoliage.model.Color
import mods.betterfoliage.model.HSB
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.texture.AtlasTexture
import net.minecraft.client.renderer.texture.TextureAtlasSprite
import net.minecraft.resources.IResource
import net.minecraft.resources.IResourceManager
import net.minecraft.util.ResourceLocation
import org.apache.logging.log4j.Level
import org.apache.logging.log4j.Logger
import java.awt.image.BufferedImage
import java.io.ByteArrayOutputStream
import java.io.IOException
import javax.imageio.ImageIO
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sin
enum class Atlas(val resourceId: ResourceLocation) {
BLOCKS(AtlasTexture.LOCATION_BLOCKS),
PARTICLES(AtlasTexture.LOCATION_PARTICLES);
/** Get the fully-qualified resource name for sprites belonging to this atlas */
fun file(resource: ResourceLocation) = ResourceLocation(resource.namespace, "textures/${resource.path}.png")
/** Reference to the atlas itself */
private val atlas: AtlasTexture get() = Minecraft.getInstance().textureManager.getTexture(resourceId) as AtlasTexture
/** Get a sprite from this atlas */
operator fun get(location: ResourceLocation) = atlas.getSprite(location)
}
//val Spr.atlas: Atlas get() = Atlas.values().find { it.resourceId == atlasLocation } ?: Atlas.BLOCKS
inline operator fun AtlasTexture.get(res: ResourceLocation): TextureAtlasSprite? = this.getSprite(res)
inline operator fun AtlasTexture.get(name: String): TextureAtlasSprite? = get(ResourceLocation(name))
fun IResourceManager.loadSprite(id: ResourceLocation) =
this.get(id)?.loadImage() ?: throw IOException("Cannot load resource $id")
fun IResource.loadImage(): BufferedImage? = ImageIO.read(this.inputStream)
/** 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)
val BufferedImage.bytes: ByteArray get() =
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: HSB get() {
var numOpaque = 0
var sumHueX = 0.0
var sumHueY = 0.0
var sumSaturation = 0.0f
var sumBrightness = 0.0f
for (x in 0 until width)
for (y in 0 until height) {
val pixel = getPixelRGBA(0, x, y)
val alpha = (pixel shr 24) and 255
val hsb = HSB.fromColorRGBA(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, sumHueX) / PI2 + 0.5).toFloat()
return HSB(avgHue, sumSaturation / numOpaque.toFloat(), sumBrightness / numOpaque.toFloat())
}
val ResourceLocation.averageHSB: HSB get() = resourceManager.loadSprite(this).let { image ->
var numOpaque = 0
var sumHueX = 0.0
var sumHueY = 0.0
var sumSaturation = 0.0f
var sumBrightness = 0.0f
for (x in 0 until image.width)
for (y in 0 until image.width) {
val pixel = image[x, y]
val alpha = (pixel shr 24) and 255
val hsb = HSB.fromColorBGRA(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, sumHueX) / PI2 + 0.5).toFloat()
return HSB(avgHue, sumSaturation / numOpaque.toFloat(), sumBrightness / numOpaque.toFloat())
}
fun HSB.brighten(multiplier: Float = 1.5f, floor: Float = 0.1f, ceiling: Float = 0.9f) =
copy(brightness = (brightness * multiplier).coerceAtMost(max(ceiling, brightness)).coerceAtLeast(min(floor, brightness)))
fun HSB.saturate(multiplier: Float = 1.5f, floor: Float = 0.1f, ceiling: Float = 0.9f) =
copy(saturation = (saturation * multiplier).coerceAtMost(max(ceiling, saturation)).coerceAtLeast(min(floor, saturation)))
/** Weighted blend of 2 packed RGB colors */
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)
return result
}
fun logColorOverride(logger: Logger, threshold: Double, hsb: HSB) {
return if (hsb.saturation >= threshold) {
logger.log(Level.INFO, " brightness ${hsb.brightness}")
logger.log(Level.INFO, " saturation ${hsb.saturation} >= ${threshold}, will use texture color")
} else {
logger.log(Level.INFO, " saturation ${hsb.saturation} < ${threshold}, will use block color")
}
}
fun HSB.colorOverride(threshold: Double) =
if (saturation < threshold) null else copy(brightness = (brightness * 2.0f).coerceAtMost(0.9f)).asInt.let { Color(it) }

View File

@@ -0,0 +1,6 @@
Manifest-Version: 1.0
Specification-Title: BetterFoliage
Implementation-Title: BetterFoliage
Specification-Vendor: octarine-noise
Implementation-Vendor: octarine-noise
MixinConnector: mods.betterfoliage.MixinConnector

View File

@@ -0,0 +1,5 @@
public net.minecraft.client.renderer.chunk.ChunkRenderCache field_212408_i #world
public net.minecraft.client.renderer.BlockModelRenderer$Cache
public net.minecraft.client.renderer.BlockModelRenderer field_210267_b
public net.minecraft.client.renderer.BlockModelRenderer field_187499_a

View File

@@ -0,0 +1,14 @@
modLoader="kotlinforforge"
loaderVersion="[1,)"
issueTrackerURL="https://github.com/octarine-noise/BetterFoliage/issues"
license="MIT"
[[mods]]
modId="betterfoliage"
version="${version}"
displayName="Better Foliage"
authors="octarine-noise"
description='''
Leafier leaves and grassier grass.
'''
displayURL="https://www.curseforge.com/minecraft/mc-mods/better-foliage"
side="CLIENT"

View File

@@ -0,0 +1,11 @@
match isParam("type", "leaf")
model.extends("biomesoplenty:block/leaves_overlay")
setParam("texture", model.texture("leaves"))
setParam("tint", model.tint("leaves"))
end
match isParam("type", "grass")
model.extends("biomesoplenty:block/origin_grass_block")
setParam("texture", model.texture("top"))
setParam("tint", model.tint("top"))
end

View File

@@ -0,0 +1,48 @@
// A lot of BYG leaf models are very sloppily made, extending "block/cube" or even "block/block"
// These rules are meh, but there's no better way to do it, there's no method to the madness here
// snowy leaves
match isParam("type", "leaf") block.name.contains("byg:leaves") model.contains("snowy")
setParam("texture", model.texture("up")) setParam("tint", model.tint("up")) end
// list of leaves where texture is "up"
match model.matches(
"byg:block/aspen_leaves",
"byg:block/baobab_leaves",
"byg:block/blue_enchanted_leaves"
) setParam("texture", model.texture("up")) setParam("tint", model.tint("up"))
end
// list of leaves where texture is "top"
match model.matches(
"byg:block/flowering_orchard_leaves",
"byg:block/joshua_leaves",
"byg:block/mahogany_leaves",
"byg:block/maple_leaves",
"byg:block/orchard_leaves",
"byg:block/rainbow_eucalyptus_leaves",
"byg:block/willow_leaves"
) setParam("texture", model.texture("top")) setParam("tint", model.tint("top"))
end
// ripe leaves (tint comes from overlay)
match model.matches(
"byg:block/ripe_joshua_leaves",
"byg:block/ripe_orchard_leaves"
) setParam("texture", model.texture("top")) setParam("tint", model.tint("overlay"))
end
//
// other blocks
//
match block.name.matches("byg:meadow_dirt") setParam("type", "dirt") end
match block.name.matches("byg:overgrown_crimson_blackstone") setParam("type", "mycelium") end
match model.matches("byg:block/meadow_grass_block", "byg:block/overgrown_stone", "byg:block/overgrown_dacite", "byg:block/overgrown_netherrack")
setParam("type", "grass")
setParam("texture", model.texture("top")) setParam("tint", model.tint("top"))
end
match block.name.matches("byg:overgrown_stone", "byg:overgrown_dacite", "byg:overgrown_netherrack", "byg:podzol_dacite")
setParam("no-connect", "true")
end

View File

@@ -0,0 +1 @@
match block.class.extends(classOf("desolation:charred_branches")) setParam("type", "leaf") end

View File

@@ -0,0 +1 @@
match block.class.extends(classOf("environmental:blue_wisteria_leaves")) setParam("type", "leaf") end

View File

@@ -0,0 +1,9 @@
match isParam("type", "leaf")
block.name.contains("spruce", "fir")
setParam("particle", "spruce")
end
match isParam("type", "leaf")
block.name.contains("jungle", "palm")
setParam("particle", "jungle")
end

View File

@@ -0,0 +1,58 @@
// Leaves
match block.class.extends(classOf("minecraft:oak_leaves")) setParam("type", "leaf") end
match isParam("type", "leaf")
model.extends("minecraft:block/leaves", "minecraft:block/cube_all")
setParam("texture", model.texture("all"))
setParam("tint", model.tint("all"))
end
// Podzol
match block.name.matches("minecraft:podzol") setParam("type", "grass") end
// Grass
match block.class.extends(classOf("minecraft:grass_block")) setParam("type", "grass") end
match isParam("type", "grass")
model.extends("minecraft:block/grass_block", "minecraft:block/cube_bottom_top")
setParam("texture", model.texture("top"))
setParam("tint", model.tint("top"))
end
// Mycelium & Nylium
match block.name.matches("minecraft:mycelium", "minecraft:crimson_nylium", "minecraft:warped_nylium") setParam("type", "mycelium") end
match isParam("type", "mycelium")
model.extends("minecraft:block/cube_bottom_top")
setParam("texture", model.texture("top"))
setParam("tint", model.tint("top"))
end
// Dirt
match block.name.matches("minecraft:dirt") setParam("type", "dirt") end
// Wood Log
match block.class.extends(classOf("minecraft:oak_log")) setParam("type", "round-log") end
match isParam("type", "round-log")
model.extends("minecraft:block/cube_column", "minecraft:block/cube_column_horizontal")
setParam("texture-side", model.texture("side"))
setParam("texture-end", model.texture("end"))
end
match isParam("type", "round-log")
model.extends("minecraft:block/cube_all")
setParam("texture-side", model.texture("all"))
setParam("texture-end", model.texture("all"))
end
// Sand & Dirt
match block.name.matches("minecraft:sand", "minecraft:red_sand") setParam("type", "sand") end
// Cactus, Lilypad, Netherrack
match block.name.matches("minecraft:cactus") setParam("type", "cactus") end
match block.name.matches("minecraft:lilypad") setParam("type", "lilypad") end
match block.name.matches("minecraft:netherrack") setParam("type", "netherrack") end
// Crops
match block.class.extends(classOf("minecraft:wheat")) setParam("type", "crop") end

View File

@@ -0,0 +1,177 @@
{
"key.betterfoliage.gui": "Open Settings",
"betterfoliage.title": "Better Foliage configuration",
"betterfoliage.global.enabled": "Enable Mod",
"betterfoliage.global.enabled.tooltip": "If set to false, BetterFoliage will not render anything",
"betterfoliage.nVidia": "nVidia GPU",
"betterfoliage.nVidia.tooltip": "Specify whether you have an nVidia GPU",
"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.saturationThreshold": "Saturation threshold",
"betterfoliage.saturationThreshold.tooltip": "Color saturation cutoff between \"colorless\" blocks (using biome color) and \"colorful\" blocks (using their own specific color)",
"betterfoliage.rendererror": "§a[BetterFoliage]§f Error rendering block %s at position %s",
"betterfoliage.shaders": "Shader configuration",
"betterfoliage.shaders.tooltip": "Configure integration with shaders",
"betterfoliage.shaders.leavesId": "Leaves ID",
"betterfoliage.shaders.leavesId.tooltip": "Block ID reported to shader programs for all kinds of leaves. If your shader uses a §6block.properties§e file, you'll probably need to change this to match the shader's mappings.",
"betterfoliage.shaders.grassId": "Grass ID",
"betterfoliage.shaders.grassId.tooltip": "Block ID reported to shader programs for all grasses and crops. If your shader uses a §6block.properties§e file, you'll probably need to change this to match the shader's mappings.",
"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.leaves.snowEnabled": "Enable snow",
"betterfoliage.leaves.snowEnabled.tooltip": "Enable snow on extra leaves?",
"betterfoliage.leaves.hideInternal": "Hide internal leaves",
"betterfoliage.leaves.hideInternal.tooltip": "Skip rendering extra leaves if leaf block is completely surrounded by other leaves or solid blocks",
"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.connectedGrass": "Connected grass textures",
"betterfoliage.connectedGrass.tooltip": "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.connectedGrass.snowEnabled": "Enable under snow",
"betterfoliage.connectedGrass.snowEnabled.tooltip": "Enable on snowed grass blocks?",
"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.roundLogs": "Round Logs",
"betterfoliage.roundLogs.tooltip": "Round Logs",
"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 along 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 obscured 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.",
"betterfoliage.roundLogs.defaultY": "Default to vertical",
"betterfoliage.roundLogs.defaultY.tooltip": "If true, log blocks where the orientation cannot be determined will be rendered as vertical. Otherwise, they will be rendered as cube blocks.",
"betterfoliage.roundLogs.plantsOnly": "Plants only",
"betterfoliage.roundLogs.plantsOnly.tooltip": "If true, only blocks with plant materials (wood, grass) will be rounded. If false, all column blocks will be rounded, including stone columns."
}

View File

@@ -0,0 +1,155 @@
{
"key.betterfoliage.gui": "설정",
"betterfoliage.global.enabled": "모드 활성화",
"betterfoliage.global.enabled.tooltip": "비활성화 할 경우, 환경강화모드 렌더링이 보이지 않습니다.",
"betterfoliage.enabled": "활성화",
"betterfoliage.enabled.tooltip": "이 기능이 활성화 되어 있습니까?",
"betterfoliage.hOffset": "수평(가로) 상쇄시키다",
"betterfoliage.hOffset.tooltip": "이 성분이 블럭 수평으로 이동된다.",
"betterfoliage.vOffset": "수직(세로) 상쇄시키다",
"betterfoliage.vOffset.tooltip": "이 성분이 블럭 수직으로 이동된다.",
"betterfoliage.size": "사이즈(크기)",
"betterfoliage.size.tooltip": "사이즈(크기)의 최소",
"betterfoliage.heightMin": "최소한 높이",
"betterfoliage.heightMin.tooltip": "높이 최소한의 최소",
"betterfoliage.heightMax": "최대한 높이",
"betterfoliage.heightMax.tooltip": "높이 최대한의 최소",
"betterfoliage.population": "주민",
"betterfoliage.population.tooltip": "자격을 갖춘 블럭의 기능 확률(N 분의 64)",
"betterfoliage.shaderWind": "쉐이더 바람 효과",
"betterfoliage.shaderWind.tooltip": "바람효과를 쉐이더에 적용시키겠습니까?",
"betterfoliage.distance": "거리 제한",
"betterfoliage.distance.tooltip": "이 기능을 렌더링하는 플레이어의 최대거리",
"betterfoliage.saturationThreshold": "채도 임계값",
"betterfoliage.saturationThreshold.tooltip": "(특정 색상을 사용하여)\"무채색\"블록과 (바이옴 색을 사용하여)\"화려한\"블록 사이의 채도 차단",
"betterfoliage.leaves": "잎 추가",
"betterfoliage.leaves.tooltip": "둥글게 나뭇잎을 추가시켜줍니다.",
"betterfoliage.leaves.dense": "조밀한 모드",
"betterfoliage.leaves.dense.tooltip": "조밀한 모드는 둥근 나뭇잎을 더 추가시켜줍니다.",
"betterfoliage.shortGrass": "이쁜 잔디 & 균사체",
"betterfoliage.shortGrass.tooltip": "블록 상단에 잔디 / 균사체",
"betterfoliage.shortGrass.useGenerated": "잔디텍스쳐를 활성화 합니다.",
"betterfoliage.shortGrass.useGenerated.tooltip": "소스팩의 일부분에 활성화 되어있는 큰잔디 텍스쳐를 생성시킵니다.",
"betterfoliage.shortGrass.myceliumEnabled": "균사체 활성화",
"betterfoliage.shortGrass.myceliumEnabled.tooltip": "균사체 블록에 있는 기능을 활성화 시키겠습니까?",
"betterfoliage.shortGrass.grassEnabled": "잔디 활성화",
"betterfoliage.shortGrass.grassEnabled.tooltip": "잔디 블록에 있는 기능을 활성화 시키겠습니까?",
"betterfoliage.shortGrass.snowEnabled": "눈 활성화",
"betterfoliage.shortGrass.snowEnabled.tooltip": "잔디 블록위에 있는 눈을 활성화 시키겠습니까?",
"betterfoliage.hangingGrass": "매달려있는 잔디",
"betterfoliage.hangingGrass.tooltip": "잔디 블록 상단 가장자리에서 아래로 매달려 있는 잔디 다발",
"betterfoliage.hangingGrass.separation": "분리",
"betterfoliage.hangingGrass.separation.tooltip": "잔디블럭에 매달려있는 잔디의 양 ",
"betterfoliage.cactus": "선인장",
"betterfoliage.cactus.tooltip": "선인장의 비트 수와 부드러운 그림자를 추가",
"betterfoliage.cactus.sizeVariation": "사이즈 변화",
"betterfoliage.cactus.sizeVariation.tooltip": "선인장의 사이즈 크기를 무작위 변화 시킵니다.",
"betterfoliage.lilypad": "연꽃잎",
"betterfoliage.lilypad.tooltip": "연꽃의 뿌리와 약간의 꽃 추가",
"betterfoliage.lilypad.flowerChance": "꽃 확률",
"betterfoliage.lilypad.flowerChance.tooltip": "연꽃 위에 있는 꽃 확률(N 분의 64)",
"betterfoliage.reed": "갈대",
"betterfoliage.reed.tooltip": "물 속에서 흙블럭 위에 있는 갈대",
"betterfoliage.reed.biomes": "바이옴 리스트",
"betterfoliage.reed.biomes.tooltip": "갈대를 바이옴에 따라 표시",
"betterfoliage.reed.biomes.tooltip.element": "갈대를 %s 바이옴에 따라 나타내시겠습니까? ",
"betterfoliage.algae": "해조류",
"betterfoliage.algae.tooltip": "깊은 물 속 흙 블럭 위에 있는 해조류",
"betterfoliage.algae.biomes": "바이옴 리스트",
"betterfoliage.algae.biomes.tooltip": "해조류를 바이옴에 따라 표시",
"betterfoliage.algae.biomes.tooltip.element": "해조류를 %s 바이옴에 따라 나타내시겠습니까? ",
"betterfoliage.coral": "산호",
"betterfoliage.coral.tooltip": "깊은 물 속 모래 블럭 위에있는 산호",
"betterfoliage.coral.size": "산호 사이즈(크기)",
"betterfoliage.coral.size.tooltip": "산호 이미지만큼 적용",
"betterfoliage.coral.crustSize": "껍질 크기",
"betterfoliage.coral.crustSize.tooltip": "산호 부분의 크기 ",
"betterfoliage.coral.chance": "산호 확률",
"betterfoliage.coral.chance.tooltip": "산호를 특정블록에 표시하는 확률(N 분의 64)",
"betterfoliage.coral.biomes": "바이옴 리스트",
"betterfoliage.coral.biomes.tooltip": "산호를 바이옴에 따라 표시",
"betterfoliage.coral.biomes.tooltip.element": "산호를 %s 바이옴에 따라 나타내시겠습니까?",
"betterfoliage.coral.shallowWater": "얕은 물 산호",
"betterfoliage.coral.shallowWater.tooltip": "산호를 깊은 물에 표시하시겠습니까?",
"betterfoliage.netherrack": "네더랙 덩굴",
"betterfoliage.netherrack.tooltip": "네더랙에 매달려있는 덩굴",
"betterfoliage.fallingLeaves": "떨어지는 나뭇잎",
"betterfoliage.fallingLeaves.tooltip": "잎 블록에서 바닥으로 떨어지는 잎 파티클",
"betterfoliage.fallingLeaves.speed": "파티클 속도",
"betterfoliage.fallingLeaves.speed.tooltip": "전체 파티클 속도",
"betterfoliage.fallingLeaves.windStrength": "바람 세기",
"betterfoliage.fallingLeaves.windStrength.tooltip": "날씨 바람 추가효과 (0을 중심으로 정규분포의 확산)",
"betterfoliage.fallingLeaves.stormStrength": "폭풍 세기",
"betterfoliage.fallingLeaves.stormStrength.tooltip": "비 날씨에 바람 추가효과 (0을 중심으로 정규분포의 확산)",
"betterfoliage.fallingLeaves.size": "파티클 크기",
"betterfoliage.fallingLeaves.chance": "파티클 확률",
"betterfoliage.fallingLeaves.chance.tooltip": "잎 파티클 랜덤 확률 설정",
"betterfoliage.fallingLeaves.perturb": "움직임",
"betterfoliage.fallingLeaves.perturb.tooltip": "움직임 효과의 크기. 코르크 같은 움직임을 추가합니다.",
"betterfoliage.fallingLeaves.lifetime": "파티클 지속시간",
"betterfoliage.fallingLeaves.lifetime.tooltip": "최대 파티클 지속시간을 설정합니다. 최소 지속시간은 60%입니다.",
"betterfoliage.fallingLeaves.opacityHack": "불투명 파티클",
"betterfoliage.fallingLeaves.opacityHack.tooltip": "파티클이 앞에 있어도 파티클을 가리는 투명블럭을 없앱니다. 경고: 오류주의",
"betterfoliage.risingSoul": "소울 상승",
"betterfoliage.risingSoul.tooltip": "소울 블럭에서 올라오는 파티클",
"betterfoliage.risingSoul.chance": "파티클 수정",
"betterfoliage.risingSoul.chance.tooltip": "소울 파티클 랜덤 확률 설정",
"betterfoliage.risingSoul.speed": "파티클 속도",
"betterfoliage.risingSoul.speed.tooltip": "파티클 속도",
"betterfoliage.risingSoul.perturb": "움직임",
"betterfoliage.risingSoul.perturb.tooltip": "움직임 효과의 크기. 코르크 같은 움직임을 추가합니다.",
"betterfoliage.risingSoul.headSize": "소울 크기",
"betterfoliage.risingSoul.headSize.tooltip": "소울 파티클의 크기",
"betterfoliage.risingSoul.trailSize": "자취 사이즈",
"betterfoliage.risingSoul.trailSize.tooltip": "자취의 크기",
"betterfoliage.risingSoul.opacity": "불투명",
"betterfoliage.risingSoul.opacity.tooltip": "파티클의 불투명",
"betterfoliage.risingSoul.sizeDecay": "크기 감소",
"betterfoliage.risingSoul.sizeDecay.tooltip": "상대적인 자취 파티클 크기",
"betterfoliage.risingSoul.opacityDecay": "불투명 감소",
"betterfoliage.risingSoul.opacityDecay.tooltip": "상대적인 입자의 파티클 불투명도",
"betterfoliage.risingSoul.lifetime": "최대 지속시간",
"betterfoliage.risingSoul.lifetime.tooltip": "최대 파티클 지속시간을 설정합니다. 최소 지속시간은 60%입니다.",
"betterfoliage.risingSoul.trailLength": "자취의 세기",
"betterfoliage.risingSoul.trailLength.tooltip": "이전 파티클 틱으로 자취를 생성합니다.",
"betterfoliage.risingSoul.trailDensity": "자취의 밀도",
"betterfoliage.risingSoul.trailDensity.tooltip": "파티클 자취를 모두 렌더링 합니다.",
"betterfoliage.connectedGrass": "잔디 연결 텍스쳐",
"betterfoliage.connectedGrass.enabled": "활성화",
"betterfoliage.connectedGrass.enabled.tooltip": "잔디블록이 흙블록 위에 있을 경우 잔디블록 윗텍스쳐를 옆텍스쳐에 전부 씌웁니다.",
"betterfoliage.roundLogs": "둥근 나무",
"betterfoliage.roundLogs.tooltip": "둥근부분을 블럭과 연결.",
"betterfoliage.roundLogs.connectSolids": "블럭과 연결",
"betterfoliage.roundLogs.connectSolids.tooltip": "둥근부분을 블럭과 연결.",
"betterfoliage.roundLogs.connectPerpendicular": "나무 세로부분과 연결",
"betterfoliage.roundLogs.connectPerpendicular.tooltip": "나무 세로부분을 나무부분끼리 연결",
"betterfoliage.roundLogs.lenientConnect": "부드럽게 둥글게 연결",
"betterfoliage.roundLogs.lenientConnect.tooltip": "2x2사이즈처럼 나무 평형된 부분끼리 서로 연결합니다.",
"betterfoliage.roundLogs.connectGrass": "잔디 연결 텍스쳐",
"betterfoliage.roundLogs.connectGrass.tooltip": "잔디가 근처에 있을 경우 나무 아래 잔디 블록을 렌더링",
"betterfoliage.roundLogs.radiusSmall": "모서리 깎는 반지름",
"betterfoliage.roundLogs.radiusSmall.tooltip": "나무 모서리 깎는 정도",
"betterfoliage.roundLogs.radiusLarge": "모서리 깎는 부분 연결",
"betterfoliage.roundLogs.radiusLarge.tooltip": "나무 모서리 부분을 연결",
"betterfoliage.roundLogs.dimming": "조광",
"betterfoliage.roundLogs.dimming.tooltip": "나무 표면부분을 어둡게하는 양",
"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.",
"betterfoliage.roundLogs.defaultY": "수직선에대한 기본값",
"betterfoliage.roundLogs.defaultY.tooltip": "true 일경우, 나무 블럭이 수직선에대한 렌더링을 할수가 없습니다. 그렇지 아니하면, 네모난 블럭으로 렌더링 될것임니다."
}

View File

@@ -0,0 +1,154 @@
{
"key.betterfoliage.gui": "Открыть настройки",
"betterfoliage.global.enabled": "Включить мод",
"betterfoliage.global.enabled.tooltip": "Если установлено на false, BetterFoliage не будет ничего рендерить",
"betterfoliage.enabled": "Включить",
"betterfoliage.enabled.tooltip": "Включена ли эта функция?",
"betterfoliage.hOffset": "Горизонтальное смещение",
"betterfoliage.hOffset.tooltip": "Дистанция горизонтального смещения этого элемента в блоках",
"betterfoliage.vOffset": "Вертикальное смещение",
"betterfoliage.vOffset.tooltip": "Дистанция вертикального смещения этого элемента в блоках",
"betterfoliage.size": "Размер",
"betterfoliage.size.tooltip": "Размер этого элемента",
"betterfoliage.heightMin": "Минимальная высота",
"betterfoliage.heightMin.tooltip": "Минимальная высота элемента",
"betterfoliage.heightMax": "Максимальная высота",
"betterfoliage.heightMax.tooltip": "Максимальная высота этого элемента",
"betterfoliage.population": "Популяция",
"betterfoliage.population.tooltip": "Шанс (N к 64), что блок будет иметь эту функцию",
"betterfoliage.shaderWind": "Шейдерные эффекты ветра",
"betterfoliage.shaderWind.tooltip": "Применить эффекты ветра с ShaderMod для этого элемента?",
"betterfoliage.distance": "Лимит дистанции",
"betterfoliage.distance.tooltip": "Максимальное расстояние от игрока для рендеринга этой функции",
"betterfoliage.saturationThreshold": "Порог насыщения",
"betterfoliage.saturationThreshold.tooltip": "Насыщенность цвета разделяется на: \"обесцвеченные\" блоки (используя цвет биома) и \"цветные\" блоки (используя их собственный цвет)",
"betterfoliage.leaves": "Улучшенная листва",
"betterfoliage.leaves.tooltip": "Дополнительное округление листьев на блоках листвы.",
"betterfoliage.leaves.dense": "Плотный режим",
"betterfoliage.leaves.dense.tooltip": "Плотный режим имеет более округлые листья.",
"betterfoliage.shortGrass": "Низкая трава и мицелий",
"betterfoliage.shortGrass.tooltip": "Пучки травы / мицелия на поверхности соответствующих блоков.",
"betterfoliage.shortGrass.useGenerated": "Использовать сгенерированные текстуры для травы.",
"betterfoliage.shortGrass.useGenerated.tooltip": "Сгенерированная текстура создается путём разрезания текстуры высокой травы с активного ресурс-пака пополам.",
"betterfoliage.shortGrass.myceliumEnabled": "Включить мицелий",
"betterfoliage.shortGrass.myceliumEnabled.tooltip": "Включить эту особенность для блоков мицелия?",
"betterfoliage.shortGrass.grassEnabled": "Включить траву",
"betterfoliage.shortGrass.grassEnabled.tooltip": "Включить эту особенность для блоков травы?",
"betterfoliage.shortGrass.snowEnabled": "Включить траву под снегом",
"betterfoliage.shortGrass.snowEnabled.tooltip": "Включить эту особенность для заснеженных блоков травы?",
"betterfoliage.hangingGrass": "Висячая трава",
"betterfoliage.hangingGrass.tooltip": "Пучки травы свисают вниз с верхних краев блока травы.",
"betterfoliage.hangingGrass.separation": "Разделение",
"betterfoliage.hangingGrass.separation.tooltip": "Как долго подвесная трава выделяется из блока?",
"betterfoliage.cactus": "Улучшенные кактусы",
"betterfoliage.cactus.tooltip": "Улучшить кактус с дополнительными частицами и плавными тенями.",
"betterfoliage.cactus.sizeVariation": "Вариации размера",
"betterfoliage.cactus.sizeVariation.tooltip": "Количество случайных изменений в размере кактусов.",
"betterfoliage.lilypad": "Улучшенные кувшинки",
"betterfoliage.lilypad.tooltip": "Добавить кувшинкам корни и цветы.",
"betterfoliage.lilypad.flowerChance": "Шанс появления цветов",
"betterfoliage.lilypad.flowerChance.tooltip": "Шанс (N к 64) появления цветка на кувшинке.",
"betterfoliage.reed": "Камыши",
"betterfoliage.reed.tooltip": "Мелководные камыши на блоках земли.",
"betterfoliage.reed.biomes": "Список биомов",
"betterfoliage.reed.biomes.tooltip": "Настройка биомов, в которых камышам разрешено появляться.",
"betterfoliage.reed.biomes.tooltip.element": "Должны ли камыши встречаться в %s биоме?",
"betterfoliage.algae": "Морские водоросли",
"betterfoliage.algae.tooltip": "Глубоководные водоросли на блоках земли.",
"betterfoliage.algae.biomes": "Список биомов",
"betterfoliage.algae.biomes.tooltip": "Настройка биомов, в которых водорослям разрешено появляться.",
"betterfoliage.algae.biomes.tooltip.element": "Должны ли водоросли встречаться в %s биоме?",
"betterfoliage.coral": "Кораллы",
"betterfoliage.coral.tooltip": "Кораллы на песчаных блоках в глубокой воде.",
"betterfoliage.coral.size": "Размер кораллов",
"betterfoliage.coral.size.tooltip": "Размер торчащих частичек кораллов.",
"betterfoliage.coral.crustSize": "Размер коры",
"betterfoliage.coral.crustSize.tooltip": "Размер плоской части кораллов.",
"betterfoliage.coral.chance": "Шанс кораллов",
"betterfoliage.coral.chance.tooltip": "Шанс (N in 64) появления кораллов на определенном блоке.",
"betterfoliage.coral.biomes": "Список биомов",
"betterfoliage.coral.biomes.tooltip": "Настройка биомов, в которых разрешено появляться кораллам.",
"betterfoliage.coral.biomes.tooltip.element": "Должны ли кораллы появляться в %s биоме?",
"betterfoliage.coral.shallowWater": "Мелководные кораллы",
"betterfoliage.coral.shallowWater.tooltip": "Должны ли появляться кораллы в воде, глубиной в 1 блок?",
"betterfoliage.netherrack": "Адская лоза",
"betterfoliage.netherrack.tooltip": "Висячая лоза под адским камнем",
"betterfoliage.fallingLeaves": "Падающие листья",
"betterfoliage.fallingLeaves.tooltip": "Падение FX частиц листвы исходящие из низа блоков листвы",
"betterfoliage.fallingLeaves.speed": "Скорость частиц",
"betterfoliage.fallingLeaves.speed.tooltip": "Общая скорость частиц",
"betterfoliage.fallingLeaves.windStrength": "Сила ветра",
"betterfoliage.fallingLeaves.windStrength.tooltip": "Величина воздействия ветра в хорошую погоду (распространение нормального распределения сосредоточено на 0)",
"betterfoliage.fallingLeaves.stormStrength": "Сила шторма",
"betterfoliage.fallingLeaves.stormStrength.tooltip": "Дополнительная величина воздействия ветра в ненастную погоду (распространение нормального распределения сосредоточено на 0)",
"betterfoliage.fallingLeaves.size": "Размер частиц",
"betterfoliage.fallingLeaves.chance": "Шанс частиц",
"betterfoliage.fallingLeaves.chance.tooltip": "Вероятность каждого случайного рендеринга в такт (1/20 секунды) опадения частицы блока листвы.",
"betterfoliage.fallingLeaves.perturb": "Возмущение",
"betterfoliage.fallingLeaves.perturb.tooltip": "Величина эффекта возмущений. Добавляет штопорообразное движение к частице синхронизированной с его вращением.",
"betterfoliage.fallingLeaves.lifetime": "Максимальное время жизни",
"betterfoliage.fallingLeaves.lifetime.tooltip": "Максимальное время жизни частиц. Минимальное время жизни - 60%% от этого значения.",
"betterfoliage.fallingLeaves.opacityHack": "Непрозрачные частицы",
"betterfoliage.fallingLeaves.opacityHack.tooltip": "Запретить прозрачным блокам затемнять частицы даже тогда, когда частицы впереди. ВНИМАНИЕ: может спровоцировать баги.",
"betterfoliage.risingSoul": "Адские духи",
"betterfoliage.risingSoul.tooltip": "Количество душ-частиц FX, испускаемых из верхней части блоков песка душ.",
"betterfoliage.risingSoul.chance": "Шанс частиц",
"betterfoliage.risingSoul.chance.tooltip": "Частота генерации частиц на песке душ.",
"betterfoliage.risingSoul.speed": "Скорость частиц",
"betterfoliage.risingSoul.speed.tooltip": "Вертикальная скорость движения частиц духов.",
"betterfoliage.risingSoul.perturb": "Возмущение",
"betterfoliage.risingSoul.perturb.tooltip": "Магнитуда эффекта возмущений. Добавляет штопороподобное движение частиц.",
"betterfoliage.risingSoul.headSize": "Размер духа",
"betterfoliage.risingSoul.headSize.tooltip": "Размер частицы духа",
"betterfoliage.risingSoul.trailSize": "Размер следов",
"betterfoliage.risingSoul.trailSize.tooltip": "Начальный размер следа частиц",
"betterfoliage.risingSoul.opacity": "Прозрачность",
"betterfoliage.risingSoul.opacity.tooltip": "Непрозрачность эффекта частиц",
"betterfoliage.risingSoul.sizeDecay": "Размер распада",
"betterfoliage.risingSoul.sizeDecay.tooltip": "Следующий размер частицы соответствует их же размеру в предыдущем такте (1/20 секунды).",
"betterfoliage.risingSoul.opacityDecay": "Непрозрачность распада",
"betterfoliage.risingSoul.opacityDecay.tooltip": "Следующий уровень прозрачности частицы соответствует их же уровню прозрачности в предыдущем такте (1/20 секунды).",
"betterfoliage.risingSoul.lifetime": "Максимальное время жизни",
"betterfoliage.risingSoul.lifetime.tooltip": "Максимальное время жизни эффекта частиц. Минимальное время жизни равно 60%% от этого числа.",
"betterfoliage.risingSoul.trailLength": "Длина следов",
"betterfoliage.risingSoul.trailLength.tooltip": "Количество предыдущих позиций, которые запомнила частица в тактах (1/20 секунды).",
"betterfoliage.risingSoul.trailDensity": "Плотность следов",
"betterfoliage.risingSoul.trailDensity.tooltip": "Рендер каждой предыдущий Nой позиции в следах частиц.",
"betterfoliage.connectedGrass": "Соединенные текстуры травы",
"betterfoliage.connectedGrass.enabled": "Включить",
"betterfoliage.connectedGrass.enabled.tooltip": "Если блок травы находится над блоком земли: прорисовать верхнюю текстуру травы на всех сторонах блока травы.",
"betterfoliage.roundLogs": "Цилиндрические брёвна",
"betterfoliage.roundLogs.tooltip": "Соединить круглые блоки в сплошные, полные блоки?",
"betterfoliage.roundLogs.connectSolids": "Соединение в крупные брёвна",
"betterfoliage.roundLogs.connectSolids.tooltip": "Соединить круглые блоки в сплошные, полные блоки?",
"betterfoliage.roundLogs.connectPerpendicular": "Соединение в перпендикулярные брёвна",
"betterfoliage.roundLogs.connectPerpendicular.tooltip": "Соединить круглые брёвна к перпендикулярным брёвнам относительно их оси?",
"betterfoliage.roundLogs.lenientConnect": "Мягкое округление",
"betterfoliage.roundLogs.lenientConnect.tooltip": "Соединение в параллельные круглые брёвна L-формы, не только 2х2.",
"betterfoliage.roundLogs.connectGrass": "Соединенная трава",
"betterfoliage.roundLogs.connectGrass.tooltip": "Заменяет землю под деревьями на траву, если она есть поблизости.",
"betterfoliage.roundLogs.radiusSmall": "Радиус фаски",
"betterfoliage.roundLogs.radiusSmall.tooltip": "Радиус обрезки углов от бревна.",
"betterfoliage.roundLogs.radiusLarge": "Радиус соединенной фаски",
"betterfoliage.roundLogs.radiusLarge.tooltip": "Радиус среза внешнего угла соединённых брёвен.",
"betterfoliage.roundLogs.dimming": "Затемнение",
"betterfoliage.roundLogs.dimming.tooltip": "Затемнить неясные длинные грани.",
"betterfoliage.roundLogs.zProtection": "Z-Защита",
"betterfoliage.roundLogs.zProtection.tooltip": "Для масштабирования параллельных битов соединения бревен, чтобы остановить Z-бой (мерцание). Попробуйте установить его как можно выше, для устранения мерцания."
}

View File

@@ -0,0 +1,177 @@
{
"key.betterfoliage.gui": "打开BF设置",
"betterfoliage.global.enabled": "模组使用",
"betterfoliage.global.enabled.tooltip": "如果关闭,BetterFoliage不会呈现任何东西",
"betterfoliage.nVidia": "nVidia GPU",
"betterfoliage.nVidia.tooltip": "明确你是否有一个nVidia GPU",
"betterfoliage.enabled": "启用",
"betterfoliage.enabled.tooltip": "要启用这个功能吗?",
"betterfoliage.hOffset": "水平偏移",
"betterfoliage.hOffset.tooltip": "在方块中这个元素是横向距离移动的",
"betterfoliage.vOffset": "垂直偏移",
"betterfoliage.vOffset.tooltip": "在方块中这个元素是横向距离移动的",
"betterfoliage.size": "尺寸",
"betterfoliage.size.tooltip": "该元素的尺寸",
"betterfoliage.heightMin": "最小高度",
"betterfoliage.heightMin.tooltip": "最小元素的高度",
"betterfoliage.heightMax": "最大高度",
"betterfoliage.heightMax.tooltip": "最大元素的高度",
"betterfoliage.population": "物体密度",
"betterfoliage.population.tooltip": "生成的概率为64分之1",
"betterfoliage.shaderWind": "光影水反中的 风的影响",
"betterfoliage.shaderWind.tooltip": "能适用于光影水反中的\"风的影响\" 这一元素?",
"betterfoliage.distance": "距离限制",
"betterfoliage.distance.tooltip": "离游戏者的最大距离,在这个范围内呈现这个特征",
"betterfoliage.saturationThreshold": "饱和度阈值",
"betterfoliage.saturationThreshold.tooltip": "色彩饱和度将介于\"无色\"方块(使用生物群系的颜色)和\"有色\"方块(使用材质特定颜色)之间",
"betterfoliage.rendererror": "§a[BF更好的叶子]§f 错误:渲染方块 %s 此位置 %s",
"betterfoliage.shaders": "着色器配置",
"betterfoliage.shaders.tooltip": "Configure integration with shaders",
"betterfoliage.shaders.leavesId": "叶子 ID",
"betterfoliage.shaders.leavesId.tooltip": "方块 ID 将作为所有叶片都使用的相同ID提供给光影水反 如果你的光影水反文件使用了一个 §6block.properties§e 文件, 你可能需要修改这个以匹配光影水反的映射",
"betterfoliage.shaders.grassId": "草 ID",
"betterfoliage.shaders.grassId.tooltip": "方块 ID 将作为所有的草和作物都使用的相同ID提供给光影水反 如果你的光影水反文件使用了一个 §6block.properties§e 文件, 你可能需要修改这个以匹配光影水反的映射",
"betterfoliage.leaves": "额外的叶片",
"betterfoliage.leaves.tooltip": "叶方块上额外的密叶",
"betterfoliage.leaves.dense": "浓密模式",
"betterfoliage.leaves.dense.tooltip": "浓密的叶子会有更多额外的叶片",
"betterfoliage.leaves.snowEnabled": "启用雪覆盖叶片",
"betterfoliage.leaves.snowEnabled.tooltip": "是否启用被雪覆盖的额外叶片?",
"betterfoliage.leaves.hideInternal": "隐藏内部的叶片",
"betterfoliage.leaves.hideInternal.tooltip": "如果该叶方块完全被其他叶方块或者固体方块包围,将不渲染额外的叶片",
"betterfoliage.shortGrass": "短草和菌丝",
"betterfoliage.shortGrass.tooltip": "一簇小草或者一丛菌丝长在合适的方块的顶部",
"betterfoliage.shortGrass.useGenerated": "为草使用mod创建的材质",
"betterfoliage.shortGrass.useGenerated.tooltip": "mod创建的材质指的是将已选用的资源包中高草的材质切一半来用",
"betterfoliage.shortGrass.myceliumEnabled": "使用在菌丝上",
"betterfoliage.shortGrass.myceliumEnabled.tooltip": "将这个特性利用到菌丝中吗?",
"betterfoliage.shortGrass.grassEnabled": "使用在草方块上",
"betterfoliage.shortGrass.grassEnabled.tooltip": "将这个特性利用到草方块中吗?",
"betterfoliage.shortGrass.snowEnabled": "使用在雪地里",
"betterfoliage.shortGrass.snowEnabled.tooltip": "使用到被雪覆盖的草方块中去?",
"betterfoliage.shortGrass.saturationThreshold": "饱和度阈值",
"betterfoliage.shortGrass.saturationThreshold.tooltip": "色彩饱和度将介于\"无色\"方块(使用生物群系的颜色)和\"有色\"方块(使用材质特定颜色)之间",
"betterfoliage.connectedGrass": "草的纹理的连接",
"betterfoliage.connectedGrass.tooltip": "使得草方块侧面拥有像顶面的材质",
"betterfoliage.connectedGrass.enabled": "启用",
"betterfoliage.connectedGrass.enabled.tooltip": "如果泥土上有草方块:所有的草方块的侧面换成草方块顶部的材质,",
"betterfoliage.connectedGrass.snowEnabled": "使用在被雪覆盖的草方块上",
"betterfoliage.connectedGrass.snowEnabled.tooltip": "是否应用于被雪覆盖的草方块上?",
"betterfoliage.hangingGrass": "悬挂的草",
"betterfoliage.hangingGrass.tooltip": "草会在边缘的草方块上垂下来",
"betterfoliage.hangingGrass.separation": "垂下来的距离",
"betterfoliage.hangingGrass.separation.tooltip": "有多长的挂草会从草方块上垂下来",
"betterfoliage.cactus": "更好的仙人掌",
"betterfoliage.cactus.tooltip": "用额外的仙人掌刺和圆润的形状来提高仙人掌的真实性",
"betterfoliage.cactus.sizeVariation": "尺寸变化",
"betterfoliage.cactus.sizeVariation.tooltip": "仙人掌的尺寸随机变化的范围",
"betterfoliage.lilypad": "更好的莲叶与荷花",
"betterfoliage.lilypad.tooltip": "用莲叶的根和随机的莲花以提高莲叶的真实性",
"betterfoliage.lilypad.flowerChance": "花的生成概率",
"betterfoliage.lilypad.flowerChance.tooltip": "(64个莲叶中 N 个)莲叶具有花了",
"betterfoliage.reed": "芦苇",
"betterfoliage.reed.tooltip": "芦苇会生成在浅水中的泥土块上",
"betterfoliage.reed.biomes": "生物群系的列表",
"betterfoliage.reed.biomes.tooltip": "配置允许出现芦苇的生物群系",
"betterfoliage.reed.biomes.tooltip.element": "芦苇应该出现在%s生物群落?",
"betterfoliage.algae": "藻类",
"betterfoliage.algae.tooltip": "藻类会生成在深水中的泥土块上",
"betterfoliage.algae.biomes": "生物群系的列表",
"betterfoliage.algae.biomes.tooltip": "配置允许出现藻类的生物群系",
"betterfoliage.algae.biomes.tooltip.element": "藻类应该出现在%s生物群落?",
"betterfoliage.coral": "珊瑚",
"betterfoliage.coral.tooltip": "珊瑚会生成在深水中的沙块上",
"betterfoliage.coral.size": "珊瑚的大小",
"betterfoliage.coral.size.tooltip": "珊瑚伸出的部分的大小",
"betterfoliage.coral.crustSize": "外壳尺寸",
"betterfoliage.coral.crustSize.tooltip": "珊瑚的平的部分的尺寸",
"betterfoliage.coral.chance": "珊瑚的生成概率",
"betterfoliage.coral.chance.tooltip": "64个特殊表面的方块中有N个有机会出现珊瑚",
"betterfoliage.coral.biomes": "生物群落的列表",
"betterfoliage.coral.biomes.tooltip": "配置允许出现珊瑚的生物群系",
"betterfoliage.coral.biomes.tooltip.element": "珊瑚应该出现在%s生物群落?",
"betterfoliage.coral.shallowWater": "浅海珊瑚",
"betterfoliage.coral.shallowWater.tooltip": "珊瑚应该生成在深水1格?",
"betterfoliage.netherrack": "下界岩下面的藤蔓",
"betterfoliage.netherrack.tooltip": "下界岩下面将挂上藤蔓",
"betterfoliage.fallingLeaves": "落叶",
"betterfoliage.fallingLeaves.tooltip": "落叶粒子效果 FX 会从叶方块底部下落",
"betterfoliage.fallingLeaves.speed": "粒子速度",
"betterfoliage.fallingLeaves.speed.tooltip": "总体的粒子速度",
"betterfoliage.fallingLeaves.windStrength": "柔风的强度",
"betterfoliage.fallingLeaves.windStrength.tooltip": "在晴天中体现的风的效果(默认以0为中心散播)",
"betterfoliage.fallingLeaves.stormStrength": "风暴的强度",
"betterfoliage.fallingLeaves.stormStrength.tooltip": "附加在阴天里体现的风的效果(默认以0为中心散播)",
"betterfoliage.fallingLeaves.size": "粒子大小",
"betterfoliage.fallingLeaves.chance": "粒子生成机率",
"betterfoliage.fallingLeaves.chance.tooltip": "随机渲染刻激发叶方块生成粒子的概率",
"betterfoliage.fallingLeaves.perturb": "扰动",
"betterfoliage.fallingLeaves.perturb.tooltip": "扰动的大小.增加了一个与之旋转同步的螺旋状动量",
"betterfoliage.fallingLeaves.lifetime": "最长存在时间",
"betterfoliage.fallingLeaves.lifetime.tooltip": "粒子的最长存在时间,仅在几秒内,最短时间为这个值60%%",
"betterfoliage.fallingLeaves.opacityHack": "粒子不透明",
"betterfoliage.fallingLeaves.opacityHack.tooltip": "阻止透明方块隐藏粒子,即使粒子已经在前面了.警告:可能会导致错误",
"betterfoliage.risingSoul": "灵魂沙上飘起的魂灵",
"betterfoliage.risingSoul.tooltip": "灵魂沙的顶部飘出灵魂粒子 FX",
"betterfoliage.risingSoul.chance": "粒子的生成机率",
"betterfoliage.risingSoul.chance.tooltip": "随机渲染刻激发灵魂沙生成一个粒子的概率",
"betterfoliage.risingSoul.speed": "粒子速度",
"betterfoliage.risingSoul.speed.tooltip": "灵颗粒的垂直速度",
"betterfoliage.risingSoul.perturb": "扰动",
"betterfoliage.risingSoul.perturb.tooltip": "动的大小.增加了一个与之旋转同步的螺旋状动量",
"betterfoliage.risingSoul.headSize": "魂灵的大小",
"betterfoliage.risingSoul.headSize.tooltip": "灵魂粒子的大小",
"betterfoliage.risingSoul.trailSize": "魂灵轨迹的大小",
"betterfoliage.risingSoul.trailSize.tooltip": "灵魂粒子的轨迹的初始大小",
"betterfoliage.risingSoul.opacity": "不透明度",
"betterfoliage.risingSoul.opacity.tooltip": "粒子效果的不透明度",
"betterfoliage.risingSoul.sizeDecay": "大小的衰减",
"betterfoliage.risingSoul.sizeDecay.tooltip": "灵魂轨迹粒子大小与前一刻他的大小相关",
"betterfoliage.risingSoul.opacityDecay": "不透明度的衰减",
"betterfoliage.risingSoul.opacityDecay.tooltip": "灵魂轨迹粒子不透明度与前一刻他的不透明度相关",
"betterfoliage.risingSoul.lifetime": "最长存在时间",
"betterfoliage.risingSoul.lifetime.tooltip": "粒子的最长存在时间,仅在几秒内,最短时间为这个值60%%",
"betterfoliage.risingSoul.trailLength": "魂灵轨迹长度",
"betterfoliage.risingSoul.trailLength.tooltip": "在游戏刻内粒子记录的轨迹位置的数量",
"betterfoliage.risingSoul.trailDensity": "魂灵轨迹密集程度",
"betterfoliage.risingSoul.trailDensity.tooltip": "整条轨迹中渲染每第N个轨迹位置",
"betterfoliage.roundLogs": "圆木",
"betterfoliage.roundLogs.tooltip": "使木方块拥有八角形的横截面",
"betterfoliage.roundLogs.plantsOnly": "仅仅应用于植物性材料",
"betterfoliage.roundLogs.plantsOnly.tooltip": "是否仅仅应用于木质材料和干草堆,而不是所有圆柱方块?",
"betterfoliage.roundLogs.connectSolids": "连接到固体方块上",
"betterfoliage.roundLogs.connectSolids.tooltip": "要使圆木连接到完整的固体方块上吗?",
"betterfoliage.roundLogs.connectPerpendicular": "连接到垂直的圆木上",
"betterfoliage.roundLogs.connectPerpendicular.tooltip": "要使圆木根据它的角度连接到垂直圆木上吗?",
"betterfoliage.roundLogs.lenientConnect": "巨大的圆木",
"betterfoliage.roundLogs.lenientConnect.tooltip": "在L形中也连接成巨大的圆木, 不只是2x2形",
"betterfoliage.roundLogs.connectGrass": "连接到草方块上",
"betterfoliage.roundLogs.connectGrass.tooltip": "如果树木下的泥土附近有草方块,那么它也会被渲染成草方块",
"betterfoliage.roundLogs.radiusSmall": "圆木斜面的半径",
"betterfoliage.roundLogs.radiusSmall.tooltip": "从圆木的外角砍掉多大的部分",
"betterfoliage.roundLogs.radiusLarge": "连接圆木的斜面半径",
"betterfoliage.roundLogs.radiusLarge.tooltip": "从连接着的巨大圆木的外角砍掉多大的部分",
"betterfoliage.roundLogs.dimming": "调光变暗",
"betterfoliage.roundLogs.dimming.tooltip": "将圆木阴暗面变黑的程度",
"betterfoliage.roundLogs.zProtection": "Z-Protection [Z保护]",
"betterfoliage.roundLogs.zProtection.tooltip": "用多少倍放大平行圆木连接处的面 去停止 Z-fighting (闪烁)[斑驳,两个多边形共面所出现的效果].在没有发生故障的情况下,试着将这个值设置得尽可能的高[这是Z-fighting出现的原因:多个多面体的面重叠在一起,会一直闪烁]",
"betterfoliage.roundLogs.defaultY": "默认垂直",
"betterfoliage.roundLogs.defaultY.tooltip": "如果开启, 方向不确定的圆木将会渲染成垂直的.否则, 它们仅仅渲染为cube型方块."
}

View File

@@ -0,0 +1,9 @@
// Vanilla
spruce=spruce
jungle=jungle
// Biomes O' Plenty
fir=spruce
// Forestry
forestry:conifers=spruce

Some files were not shown because too many files have changed in this diff Show More