Compare commits

...

164 Commits

Author SHA1 Message Date
Astrash 772675639e Better error handling + other changes 2023-07-29 20:29:16 +10:00
Astrash 13300861ee Parse precedence via grammar 2023-07-29 12:03:33 +10:00
Astrash 719b9a06f4 Simplify code 2023-07-29 08:57:43 +10:00
Astrash f5b115e618 Formatting & name changes 2023-07-27 12:52:15 +10:00
Astrash e1e4a63517 Add basic user defined function support 2023-07-27 11:27:15 +10:00
Astrash 0dc1492c4d Handle functions in scope 2023-07-25 14:08:09 +10:00
Astrash a184fe40d0 Name changes 2023-07-24 18:52:54 +10:00
Astrash f462b8198b Move inLoop flag to ScopeBuilder 2023-07-24 18:05:43 +10:00
Astrash de3b213deb Refactor some parsing logic 2023-07-24 17:31:06 +10:00
Astrash be444f75b7 Block -> executable 2023-07-24 17:30:37 +10:00
Astrash d98238c262 Remove statement class 2023-07-23 19:33:17 +10:00
Astrash 8e96745a85 checkReturnType -> ensureReturnType 2023-07-23 19:17:50 +10:00
Astrash 802bce40c8 Move statement end handling to parseExpression 2023-07-23 19:17:08 +10:00
Astrash 76728fe593 More refactoring 2023-07-23 17:55:29 +10:00
Astrash f3d1751c87 Terrascript refactor 2023-07-23 16:11:56 +10:00
Astrash 81e354f91c Use tectonic properly 2023-07-18 22:06:08 +10:00
Astrash aab28ff4f9 Bump version to 6.4.0 2023-07-18 14:32:12 +10:00
Astrash 0e3a756011 Bump config-noise-function to v1.1.0 2023-07-18 14:29:54 +10:00
Astrash 02198e1b88 Implement distance sampler 2023-07-18 14:29:28 +10:00
Astrash 00aeb98419 Implement translation sampler 2023-07-18 14:27:36 +10:00
Astrash 1a784b51ac Implement expression normalizer sampler 2023-07-18 14:25:07 +10:00
Astrash 34c0895c1f Make metalist injection error more user friendly 2023-07-16 22:46:23 +10:00
Astrash 379fa601a3 Meta annotate LINEAR_HEIGHTMAP sampler 2023-07-16 17:04:51 +10:00
Astrash fcbf51d80b Allow Range keys to be meta annotated 2023-07-16 11:51:51 +10:00
Astrash 9d83dfd164 Bump version to 6.3.2 2023-07-16 11:49:14 +10:00
Astrashh 72686601ee Merge pull request #406 from PolyhedralDev/ver/6.3.1
Ver/6.3.1
2023-07-15 07:04:10 +10:00
Astrash 73baaec6cd Bump version to 6.3.1 2023-07-11 09:33:57 +10:00
Astrashh 0be7213ee5 Merge pull request #401 from PolyhedralDev/dev/reduce-pipeline-caching
Reduce pipeline v2 caching
2023-06-20 10:10:35 +10:00
Astrash 3f3e2fe97c Reduce pipeline v2 caching 2023-06-20 09:57:43 +10:00
dfsek 549edd11ea update ServerCommandSourceMixin 2023-06-15 17:27:26 -07:00
dfsek 36f89946d4 update SignBlockEntityMixin 2023-06-15 17:26:04 -07:00
dfsek 18644d6100 update fabric dependencies 2023-06-15 17:22:21 -07:00
dfsek 9d38ee4329 repackage NMS 1.20.1 2023-06-15 17:18:36 -07:00
dfsek b75a8f85e4 restore NMSChunkGeneratorDelegate#getBaseColumn 2023-06-15 17:16:20 -07:00
dfsek aad58f5968 update nms imports 2023-06-15 17:14:45 -07:00
dfsek a548c30484 clean up unused bstats relocation 2023-06-15 17:11:53 -07:00
dfsek 9ba46ae3a5 make new bukkit NMS module 2023-06-15 17:11:32 -07:00
dfsek 49efbed6f5 update versions 2023-06-15 17:11:04 -07:00
dfsek 4001a56100 Merge pull request #400 from PolyhedralDev/ver/6.3.0
make forge compile
2023-06-15 15:55:49 -07:00
dfsek f46f35d2ad make forge compile 2023-06-15 15:52:36 -07:00
dfsek 70dbd2f2c0 Merge pull request #399 from PolyhedralDev/ver/6.3.0
Ver/6.3.0
2023-06-15 14:05:02 -07:00
dfsek bf1be62d54 Merge pull request #385 from PolyhedralDev/dev/img-lib
Image library
2023-06-15 14:00:44 -07:00
Astrash a5cbce3667 Optimize cardinal rotations for RotateColorSampler 2023-06-14 12:07:37 +10:00
Astrash d0591f292e Use primitive int over Integer 2023-06-14 11:15:25 +10:00
dfsek 27874ce0a5 Merge pull request #396 from ccorp2002/master
1.19.4 Support...?
2023-06-10 15:55:12 -07:00
dfsek 170687abdb remove unused mixin 2023-06-10 15:53:27 -07:00
dfsek 46b61d841d remove unneeded buildscript 2023-06-10 15:50:45 -07:00
dfsek 41b7021121 fix fabric pack reloading 2023-06-10 15:49:33 -07:00
dfsek 183255140b remove unused RegistrarInstance class 2023-06-10 15:28:46 -07:00
dfsek bea8f97179 fix bukkit pack reloading 2023-06-09 03:33:47 -07:00
dfsek 60fec05e12 Merge remote-tracking branch 'origin/ver/6.3.0'
# Conflicts:
#	build.gradle.kts
#	buildSrc/src/main/kotlin/Versions.kt
#	platforms/bukkit/build.gradle.kts
#	platforms/bukkit/nms/v1_19_R1/build.gradle.kts
#	platforms/bukkit/nms/v1_19_R2/build.gradle.kts
#	platforms/mixin-common/src/main/java/com/dfsek/terra/mod/ModPlatform.java
#	platforms/mixin-common/src/main/java/com/dfsek/terra/mod/generation/TerraBiomeSource.java
#	platforms/mixin-common/src/main/java/com/dfsek/terra/mod/util/PresetUtil.java
#	platforms/mixin-lifecycle/src/main/java/com/dfsek/terra/lifecycle/LifecyclePlatform.java
#	platforms/mixin-lifecycle/src/main/java/com/dfsek/terra/lifecycle/mixin/lifecycle/RegistryLoaderMixin.java
#	platforms/mixin-lifecycle/src/main/java/com/dfsek/terra/lifecycle/util/LifecycleUtil.java
2023-06-09 03:16:18 -07:00
dfsek e79cc21c11 1.19.4 fabric 2023-06-09 03:04:41 -07:00
Astrash 5188477a6d Merge branch 'ver/6.3.0' into dev/img-lib 2023-05-02 14:21:31 +10:00
Astrash 58b743e6e8 Distance transform documentation 2023-05-02 14:15:20 +10:00
Astrash 9fa5307a60 Remove image sampler addon
The image sampler is implemented as the 'CHANNEL' sampler provided
directly by the library-image addon instead.
2023-05-02 13:41:42 +10:00
Astrash b4ea09929c ColorUtil documentation 2023-05-02 13:37:22 +10:00
Astrash 09d847bc5a Add argb method to Channel class 2023-05-02 13:12:23 +10:00
Astrash dacddef5d6 rgb -> argb 2023-05-02 13:04:23 +10:00
Astrash 3c593c7013 Fix method call 2023-05-02 13:03:13 +10:00
Astrash 36d0ef77fb Add image support addon for biome pipeline 2023-05-02 12:58:37 +10:00
Astrash 105be0c346 Implement color parsing from strings 2023-05-02 12:58:29 +10:00
Astrash 39d21fbe08 Implement channel noise sampler 2023-05-01 13:58:56 +10:00
Astrash 50fc589001 Move distance transform to appropriate package 2023-05-01 13:58:18 +10:00
Astrash ffa55cb7a3 Implement premultiplication in ColorUtil 2023-05-01 13:57:07 +10:00
Astrash ff0985bd31 Make imagelib MathUtil 2023-05-01 13:10:58 +10:00
Astrash 622fed96e5 Use colorsampler package name instead of sampler 2023-05-01 12:48:13 +10:00
Astrash c219eff149 Implement distance transform sampler 2023-05-01 12:45:26 +10:00
Astrash 514e7065e2 Update image provider v2 readme 2023-05-01 12:02:53 +10:00
dfsek 0a16453f98 remove devlaunch lazydfu 2023-04-13 18:20:16 -07:00
dfsek ca2fe27fb3 fix preset creation 2023-04-13 18:15:35 -07:00
dfsek 6f2c01ceb3 update biome builder 2023-04-13 17:20:20 -07:00
dfsek 8b74a5dee0 implement biomeStream 2023-04-13 16:45:09 -07:00
dfsek 460a7651bc fix bukkit build 2023-04-13 16:44:01 -07:00
dfsek b7a6b839e6 Merge remote-tracking branch 'origin/dev/1.19.3'
# Conflicts:
#	buildSrc/src/main/kotlin/Versions.kt
#	platforms/bukkit/build.gradle.kts
2023-04-13 16:34:43 -07:00
dfsek 75bff93ecd update mod versions 2023-04-13 16:31:49 -07:00
dfsek 57a45f08f0 re-package bukkit 1.19.4 code 2023-04-13 16:31:41 -07:00
C_Corp2002 ba35b56016 Delete AwfulBukkitHacks.java.txt
Unneeded.
2023-04-13 15:40:13 -07:00
C_Corp2002 8fd10956e4 Update build.gradle.kts 2023-04-12 00:51:49 -07:00
C_Corp2002 9a5c1302ac Fixing...?
We gottem.
2023-04-12 00:51:12 -07:00
C_Corp2002 f918f1ef66 An attempt at 1.19.4 Support
I tried.
2023-04-09 21:08:38 -07:00
Astrash 2afcee28a6 Base key names on internal names 2023-03-01 13:39:09 +11:00
Astrash 6efff02c19 Implement stitched image support 2023-03-01 13:38:36 +11:00
Astrash d3e0831d9e Use config type based image loading
Rename `path` key to `image`
2023-03-01 13:28:45 +11:00
Astrash 345012810a Fix biome-provider-image-v2 addon manifest 2023-03-01 13:22:39 +11:00
Astrash 73e0899e7c Only use WorldEdit relative offset for v2 sponge schematics 2023-02-28 13:05:36 +11:00
Astrash 8deae0480c Add support for WorldEdit schematic relative offsets 2023-02-28 11:58:39 +11:00
Astrash 7b87498751 Don't load same image multiple times 2023-02-07 11:59:03 +11:00
Astrash 33f1aa07d3 Mark BufferedImageLoader as deprecated 2023-02-07 11:38:12 +11:00
Astrash 3ab671827d Re-add old image sampler to config-noise-function w/ deprecation notice 2023-02-07 11:31:23 +11:00
Astrash 4d17edef80 Add deprecation notice for biome-provider-image addon 2023-02-07 11:31:23 +11:00
Astrash 9d5b33f130 Re-add original image biome provider
This is so we aren't breaking existing packs
2023-02-07 11:31:23 +11:00
Astrash 97c0dcad9d Rename image lib biome provider to v2 2023-02-07 11:31:23 +11:00
Astrash ef4fe4eb7a Revert "Bump image provider major version"
This reverts commit f1bf3990c1.
2023-02-07 11:31:23 +11:00
Astrash 9514641e1e Revert "Bump config-noise-function major version"
This reverts commit 68875cc17b.
2023-02-07 11:31:23 +11:00
Astrash e6c51bcfd0 Merge branch 'ver/6.3.0' into dev/img-lib
fogor import
2023-02-07 11:30:54 +11:00
Astrash 084ecb9ad8 Forgot to delet sout 2023-02-06 20:36:28 +11:00
Astrash 5cc58babca Bump version to 6.3.0 2023-02-06 19:18:08 +11:00
Astrash b10130c5c6 Cache loaded BufferedImages 2023-02-06 19:18:08 +11:00
dfsek 5bc34eb626 update version to 6.2.2 2023-02-06 19:18:08 +11:00
dfsek 7d74245109 make forge compile 2023-02-06 19:18:08 +11:00
dfsek 11b6080413 update bukkit to 1.19.3 2023-02-06 19:18:08 +11:00
dfsek 4df23e464b begin working on Forge 2023-02-06 19:18:08 +11:00
dfsek 7ea5747f8e clean up sysout logging 2023-02-06 19:18:08 +11:00
dfsek caad76f6dd add mixin to inject flora 2023-02-06 19:18:08 +11:00
dfsek ba2f24f1f5 1.19.3 launch and enter world 2023-02-06 19:18:08 +11:00
dfsek 57bb6bca94 evil registry hacks 2023-02-06 19:18:08 +11:00
dfsek 4bb09b126a continue registry wrangling 2023-02-06 19:18:08 +11:00
dfsek ae96d8f526 fix inject parameters in NoiseConfigMixin 2023-02-06 19:18:08 +11:00
dfsek 4a918d00a3 allow registry manager to be overwritten 2023-02-06 19:18:08 +11:00
dfsek cd65785de4 fix SaveLoadingMixin 2023-02-06 19:18:08 +11:00
dfsek 51cd4cd4b7 fix RegistryMixin target class 2023-02-06 19:18:08 +11:00
dfsek e7efdd61a6 fabric compiles now 2023-02-06 19:18:08 +11:00
dfsek 0006762ff3 update platform biome delegate logic 2023-02-06 19:18:08 +11:00
dfsek 4e4627d11d update minecraft data config templates 2023-02-06 19:18:08 +11:00
dfsek 228b26f7c4 more work on evil update 2023-02-06 19:18:08 +11:00
dfsek ef846d53ad update MinecraftChunkGeneratorWrapper and its codec 2023-02-06 19:18:08 +11:00
dfsek f6f7529cb5 begin working on fabric 1.19.3 2023-02-06 19:18:08 +11:00
dfsek 606315ea64 update platform versions to 1.19.3 2023-02-06 19:18:08 +11:00
Astrash 46f7c95314 Implement BiomeProvider#getBaseBiome in pipeline v2 2023-01-30 12:57:50 +11:00
dfsek 4d826c880c update version to 6.2.2 2022-12-21 23:49:15 -07:00
dfsek 6da3acc8a1 make forge compile 2022-12-21 23:49:06 -07:00
dfsek 8fff27fddd update bukkit to 1.19.3 2022-12-21 23:00:40 -07:00
dfsek 75673b5b8e begin working on Forge 2022-12-21 21:59:39 -07:00
dfsek 5ded3552d3 clean up sysout logging 2022-12-21 21:38:40 -07:00
dfsek ded308c01c add mixin to inject flora 2022-12-21 21:34:44 -07:00
dfsek ee336b01a6 1.19.3 launch and enter world 2022-12-21 21:14:38 -07:00
dfsek 66465f27ff evil registry hacks 2022-12-21 14:41:43 -07:00
dfsek 764a4fa535 continue registry wrangling 2022-12-19 01:27:35 -07:00
dfsek 5dd5c37055 fix inject parameters in NoiseConfigMixin 2022-12-18 22:54:30 -07:00
dfsek 2e0f892fff allow registry manager to be overwritten 2022-12-18 22:53:10 -07:00
dfsek cad0e4105c fix SaveLoadingMixin 2022-12-18 22:49:59 -07:00
dfsek b10898b837 fix RegistryMixin target class 2022-12-18 22:46:57 -07:00
dfsek 6255ac7379 fabric compiles now 2022-12-18 22:44:53 -07:00
dfsek c90ca076ab update platform biome delegate logic 2022-12-18 22:44:06 -07:00
dfsek 73af05bf09 update minecraft data config templates 2022-12-18 22:39:18 -07:00
dfsek 84eab0de4a more work on evil update 2022-12-18 22:03:00 -07:00
dfsek 393a868e6a update MinecraftChunkGeneratorWrapper and its codec 2022-12-18 20:05:17 -07:00
dfsek 8955e4bb81 begin working on fabric 1.19.3 2022-12-14 19:14:16 -07:00
dfsek bae2af80c8 update platform versions to 1.19.3 2022-12-14 18:28:25 -07:00
Astrashh 6826f44770 Merge pull request #386 from PolyhedralDev/dev/dot-product-slant-again
Refactor Noise3D palettes to optionally use dot product slants
2022-11-29 13:28:23 +11:00
Astrash 1b5095dd36 Refactor Noise3d palettes to support multiple slant methods 2022-11-26 19:18:58 +11:00
Astrash 95992cc49b Don't apply align to single image fallback 2022-11-26 14:43:15 +11:00
Astrash 7b0185ba7c Add translate color sampler 2022-11-26 14:42:12 +11:00
Astrash 6b7fb82202 Put mutator color samplers in their own packages 2022-11-26 14:21:02 +11:00
Astrash f246c8ada3 Put image based color samplers in own packages 2022-11-26 14:14:15 +11:00
Astrash a97273f358 Skip color dist calc if exact match 2022-11-26 14:05:39 +11:00
Astrash 878bede60b Name changes 2022-11-26 13:52:15 +11:00
Astrash b771e108b6 Use color sampler as fallback instead of constant 2022-11-26 13:52:15 +11:00
Astrash 5c916f7758 Add rotate color sampler 2022-11-26 13:52:05 +11:00
Astrash e9db14f52c Wrap BufferedImage access 2022-11-26 13:49:10 +11:00
Astrash b3f072d689 Properly ignore alpha of lookup map 2022-11-25 14:11:16 +11:00
Astrash 4fdef98bd9 Use generic key mapping function 2022-11-25 14:10:07 +11:00
Astrash 8670c4cdf3 ColorPicker -> ColorSampler 2022-11-25 10:17:56 +11:00
Astrash 64c2a41d19 Ignore alpha channel by default with ExactColorConverter 2022-11-24 13:56:11 +11:00
Astrash 4e225a6592 Add utility methods for zero-ing color channels 2022-11-24 13:55:36 +11:00
Astrash c491ac5b24 ColorUtil.Channel getChannel() -> from() 2022-11-24 13:55:16 +11:00
Astrash 68875cc17b Bump config-noise-function major version
* Removed IMAGE sampler
2022-11-24 12:06:51 +11:00
Astrash f1bf3990c1 Bump image provider major version 2022-11-24 12:06:51 +11:00
Astrash 4334b16ded Use color pickers in image sampler 2022-11-24 12:06:51 +11:00
Astrash 274f864d6a Move channels into image lib 2022-11-24 12:06:51 +11:00
Astrash 329d94ba9c Move image sampler to separate addon 2022-11-24 12:06:51 +11:00
Astrash 03ab463723 Rewrite the image biome provider to use the image library 2022-11-24 12:06:51 +11:00
Astrash 3580267532 Image library initial implementation
*Moves the BufferedImage loader into the library, rather than being
a loader implemented in common/implementation
2022-11-24 12:06:51 +11:00
dfsek 358e09d05b Merge pull request #383 from PolyhedralDev/ver/6.2.1
6.2.1 but for real this time
2022-11-16 14:03:21 -07:00
289 changed files with 7316 additions and 2360 deletions
+3 -3
View File
@@ -1,8 +1,8 @@
preRelease(true)
versionProjects(":common:api", version("6.2.1"))
versionProjects(":common:implementation", version("6.2.1"))
versionProjects(":platforms", version("6.2.1"))
versionProjects(":common:api", version("6.4.0"))
versionProjects(":common:implementation", version("6.4.0"))
versionProjects(":platforms", version("6.4.0"))
allprojects {
+9 -11
View File
@@ -4,7 +4,7 @@ object Versions {
const val paralithic = "0.7.0"
const val strata = "1.1.1"
const val cloud = "1.7.0"
const val cloud = "1.8.0"
const val slf4j = "1.7.36"
const val log4j_slf4j_impl = "2.14.1"
@@ -19,40 +19,38 @@ object Versions {
object Fabric {
const val fabricLoader = "0.14.8"
const val fabricAPI = "0.57.0+1.19"
const val fabricAPI = "0.83.1+1.20.1"
}
object Quilt {
const val quiltLoader = "0.17.0"
const val fabricApi = "2.0.0-beta.4+0.57.0-1.19"
const val fabricApi = "6.0.0-beta.3+0.76.0-1.19.4"
}
object Mod {
const val mixin = "0.11.2+mixin.0.8.5"
const val minecraft = "1.19"
const val yarn = "$minecraft+build.1"
const val fabricLoader = "0.14.2"
const val minecraft = "1.20.1"
const val yarn = "$minecraft+build.2"
const val fabricLoader = "0.14.21"
const val architecuryLoom = "0.12.0.290"
const val architecturyPlugin = "3.4-SNAPSHOT"
const val loomQuiltflower = "1.7.1"
const val lazyDfu = "0.1.2"
}
object Forge {
const val forge = "${Mod.minecraft}-41.0.63"
const val forge = "${Mod.minecraft}-47.0.3"
const val burningwave = "12.53.0"
}
object Bukkit {
const val paper = "1.18.2-R0.1-SNAPSHOT"
const val paperLib = "1.0.5"
const val minecraft = "1.19.2"
const val minecraft = "1.20.1"
const val reflectionRemapper = "0.1.0-SNAPSHOT"
const val paperDevBundle = "1.19.2-R0.1-SNAPSHOT"
const val paperDevBundle = "1.20.1-R0.1-SNAPSHOT"
}
object Sponge {
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-2021 Polyhedral Development
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,5 @@
# biome-provider-image-v2
Implements and registers the `IMAGE` biome provider, which
utilizes various config types provided by the `library-image` addon to
distribute biomes based on images.
@@ -0,0 +1,12 @@
version = version("1.0.0")
dependencies {
compileOnlyApi(project(":common:addons:manifest-addon-loader"))
compileOnlyApi(project(":common:addons:library-image"))
implementation("net.jafama", "jafama", Versions.Libraries.Internal.jafama)
testImplementation("net.jafama", "jafama", Versions.Libraries.Internal.jafama)
}
tasks.named<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("shadowJar") {
relocate("net.jafama", "com.dfsek.terra.addons.biome.image.lib.jafama")
}
@@ -0,0 +1,51 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.biome.image.v2;
import java.util.Optional;
import com.dfsek.terra.addons.image.converter.ColorConverter;
import com.dfsek.terra.addons.image.colorsampler.ColorSampler;
import com.dfsek.terra.api.world.biome.Biome;
import com.dfsek.terra.api.world.biome.generation.BiomeProvider;
public class ImageBiomeProvider implements BiomeProvider {
private final int resolution;
private final ColorConverter<Biome> colorConverter;
private final ColorSampler colorSampler;
public ImageBiomeProvider(ColorConverter<Biome> colorConverter, ColorSampler colorSampler, int resolution) {
this.resolution = resolution;
this.colorConverter = colorConverter;
this.colorSampler = colorSampler;
}
@Override
public Biome getBiome(int x, int y, int z, long seed) {
return getBiome(x, z);
}
public Biome getBiome(int x, int z) {
x /= resolution;
z /= resolution;
return colorConverter.apply(colorSampler.apply(x, z));
}
@Override
public Optional<Biome> getBaseBiome(int x, int z, long seed) {
return Optional.of(getBiome(x, z));
}
@Override
public Iterable<Biome> getBiomes() {
return colorConverter.getEntries();
}
}
@@ -0,0 +1,74 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.biome.image.v2;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import java.util.function.Supplier;
import com.dfsek.terra.addons.biome.image.v2.config.ImageProviderTemplate;
import com.dfsek.terra.addons.biome.image.v2.config.converter.ClosestBiomeColorConverterTemplate;
import com.dfsek.terra.addons.biome.image.v2.config.converter.ExactBiomeColorConverterTemplate;
import com.dfsek.terra.addons.biome.image.v2.config.converter.mapping.DefinedBiomeColorMappingTemplate;
import com.dfsek.terra.addons.image.converter.ColorConverter;
import com.dfsek.terra.addons.image.converter.mapping.BiomeDefinedColorMapping;
import com.dfsek.terra.addons.image.converter.mapping.ColorMapping;
import com.dfsek.terra.addons.manifest.api.AddonInitializer;
import com.dfsek.terra.api.Platform;
import com.dfsek.terra.api.addon.BaseAddon;
import com.dfsek.terra.api.event.events.config.pack.ConfigPackPreLoadEvent;
import com.dfsek.terra.api.event.functional.FunctionalEventHandler;
import com.dfsek.terra.api.inject.annotations.Inject;
import com.dfsek.terra.api.registry.CheckedRegistry;
import com.dfsek.terra.api.util.reflection.TypeKey;
import com.dfsek.terra.api.world.biome.Biome;
import com.dfsek.terra.api.world.biome.generation.BiomeProvider;
public class ImageBiomeProviderAddon implements AddonInitializer {
public static final TypeKey<Supplier<ObjectTemplate<BiomeProvider>>> PROVIDER_REGISTRY_KEY = new TypeKey<>() {
};
public static final TypeKey<Supplier<ObjectTemplate<ColorConverter<Biome>>>> BIOME_COLOR_CONVERTER_REGISTRY_KEY = new TypeKey<>() {
};
public static final TypeKey<Supplier<ObjectTemplate<ColorMapping<Biome>>>> BIOME_COLOR_MAPPING_REGISTRY_KEY = new TypeKey<>() {
};
@Inject
private Platform platform;
@Inject
private BaseAddon addon;
@Override
public void initialize() {
platform.getEventManager()
.getHandler(FunctionalEventHandler.class)
.register(addon, ConfigPackPreLoadEvent.class)
.priority(501)
.then(event -> {
CheckedRegistry<Supplier<ObjectTemplate<BiomeProvider>>> providerRegistry = event.getPack().getOrCreateRegistry(
PROVIDER_REGISTRY_KEY);
providerRegistry.register(addon.key("IMAGE"), ImageProviderTemplate::new);
})
.then(event -> {
CheckedRegistry<Supplier<ObjectTemplate<ColorConverter<Biome>>>> biomeColorConverterRegistry = event.getPack().getOrCreateRegistry(
BIOME_COLOR_CONVERTER_REGISTRY_KEY);
biomeColorConverterRegistry.register(addon.key("EXACT"), ExactBiomeColorConverterTemplate::new);
biomeColorConverterRegistry.register(addon.key("CLOSEST"), ClosestBiomeColorConverterTemplate::new);
})
.then(event -> {
CheckedRegistry<Supplier<ObjectTemplate<ColorMapping<Biome>>>> biomeColorMappingRegistry = event.getPack().getOrCreateRegistry(
BIOME_COLOR_MAPPING_REGISTRY_KEY);
biomeColorMappingRegistry.register(addon.key("USE_BIOME_COLORS"), () -> () -> new BiomeDefinedColorMapping<>(event.getPack().getRegistry(Biome.class), b -> b));
biomeColorMappingRegistry.register(addon.key("MAP"), DefinedBiomeColorMappingTemplate::new);
})
.failThrough();
}
}
@@ -0,0 +1,40 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.biome.image.v2.config;
import com.dfsek.tectonic.api.config.template.annotations.Default;
import com.dfsek.tectonic.api.config.template.annotations.Description;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import com.dfsek.terra.addons.biome.image.v2.ImageBiomeProvider;
import com.dfsek.terra.addons.image.converter.ColorConverter;
import com.dfsek.terra.addons.image.colorsampler.ColorSampler;
import com.dfsek.terra.api.world.biome.Biome;
import com.dfsek.terra.api.world.biome.generation.BiomeProvider;
@SuppressWarnings("FieldMayBeFinal")
public class ImageProviderTemplate implements ObjectTemplate<BiomeProvider> {
@Value("resolution")
@Default
@Description("Sets the resolution at which to sample the image.")
private int resolution = 1;
@Value("color-sampler")
private ColorSampler colorSampler;
@Value("color-conversion")
private ColorConverter<Biome> colorConverter;
@Override
public BiomeProvider get() {
return new ImageBiomeProvider(colorConverter, colorSampler, resolution);
}
}
@@ -0,0 +1,19 @@
package com.dfsek.terra.addons.biome.image.v2.config.converter;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.terra.addons.image.config.converter.ClosestColorConverterTemplate;
import com.dfsek.terra.addons.image.converter.mapping.ColorMapping;
import com.dfsek.terra.api.world.biome.Biome;
public class ClosestBiomeColorConverterTemplate extends ClosestColorConverterTemplate<Biome> {
@Value("match")
private ColorMapping<Biome> match;
@Override
protected ColorMapping<Biome> getMapping() {
return match;
}
}
@@ -0,0 +1,37 @@
package com.dfsek.terra.addons.biome.image.v2.config.converter;
import com.dfsek.tectonic.api.config.template.annotations.Default;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.terra.addons.image.config.converter.ExactColorConverterTemplate;
import com.dfsek.terra.addons.image.converter.mapping.ColorMapping;
import com.dfsek.terra.api.world.biome.Biome;
public class ExactBiomeColorConverterTemplate extends ExactColorConverterTemplate<Biome> {
@Value("match")
private ColorMapping<Biome> match;
@Value("else")
private Biome fallback;
@Value("ignore-alpha")
@Default
private boolean ignoreAlpha = true;
@Override
protected ColorMapping<Biome> getMapping() {
return match;
}
@Override
protected Biome getFallback() {
return fallback;
}
@Override
protected boolean ignoreAlpha() {
return ignoreAlpha;
}
}
@@ -0,0 +1,24 @@
package com.dfsek.terra.addons.biome.image.v2.config.converter.mapping;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import java.util.Map;
import com.dfsek.terra.addons.image.config.ColorLoader.ColorString;
import com.dfsek.terra.addons.image.converter.mapping.ColorMapping;
import com.dfsek.terra.addons.image.util.MapUtil;
import com.dfsek.terra.api.world.biome.Biome;
public class DefinedBiomeColorMappingTemplate implements ObjectTemplate<ColorMapping<Biome>> {
@Value("map")
Map<ColorString, Biome> map;
@Override
public ColorMapping<Biome> get() {
var map = MapUtil.mapKeys(this.map, ColorString::getColor);
return () -> map;
}
}
@@ -0,0 +1,14 @@
schema-version: 1
contributors:
- Terra contributors
id: biome-provider-image-v2
version: @VERSION@
entrypoints:
- "com.dfsek.terra.addons.biome.image.v2.ImageBiomeProviderAddon"
website:
issues: https://github.com/PolyhedralDev/Terra/issues
source: https://github.com/PolyhedralDev/Terra
docs: https://terra.polydev.org
license: MIT License
depends:
library-image: "1.+"
@@ -8,6 +8,8 @@
package com.dfsek.terra.addons.biome.image;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.function.Supplier;
@@ -24,6 +26,9 @@ import com.dfsek.terra.api.world.biome.generation.BiomeProvider;
public class ImageBiomeProviderAddon implements AddonInitializer {
private static final Logger logger = LoggerFactory.getLogger(ImageBiomeProviderAddon.class);
public static final TypeKey<Supplier<ObjectTemplate<BiomeProvider>>> PROVIDER_REGISTRY_KEY = new TypeKey<>() {
};
@@ -45,5 +50,6 @@ public class ImageBiomeProviderAddon implements AddonInitializer {
() -> new ImageProviderTemplate(event.getPack().getRegistry(Biome.class)));
})
.failThrough();
logger.warn("The biome-provider-image addon is deprecated and scheduled for removal in Terra 7.0. It is recommended to use the biome-provider-image-v2 addon for future pack development instead.");
}
}
@@ -11,6 +11,7 @@ import net.jafama.FastMath;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.stream.StreamSupport;
@@ -36,7 +37,7 @@ public class PipelineBiomeProvider implements BiomeProvider {
this.noiseAmp = noiseAmp;
this.chunkSize = pipeline.getChunkSize();
this.biomeChunkCache = Caffeine.newBuilder()
.maximumSize(1024)
.maximumSize(64)
.build(pipeline::generateChunk);
Set<PipelineBiome> biomeSet = new HashSet<>();
@@ -97,6 +98,11 @@ public class PipelineBiomeProvider implements BiomeProvider {
return biomes;
}
@Override
public Optional<Biome> getBaseBiome(int x, int z, long seed) {
return Optional.of(getBiome(x, z, seed));
}
@Override
public Column<Biome> getColumn(int x, int z, long seed, int min, int max) {
return new BiomePipelineColumn(this, min, max, x, z, seed);
@@ -54,6 +54,6 @@ public class BiomePipelineTemplate implements ObjectTemplate<BiomeProvider> {
@Override
public BiomeProvider get() {
return new PipelineBiomeProvider(new PipelineImpl(source, stages, resolution, 500), resolution, blendSampler, blendAmplitude);
return new PipelineBiomeProvider(new PipelineImpl(source, stages, resolution, 128), resolution, blendSampler, blendAmplitude);
}
}
@@ -11,9 +11,10 @@ import com.dfsek.terra.addons.chunkgenerator.config.NoiseChunkGeneratorPackConfi
import com.dfsek.terra.addons.chunkgenerator.config.noise.BiomeNoiseConfigTemplate;
import com.dfsek.terra.addons.chunkgenerator.config.noise.BiomeNoiseProperties;
import com.dfsek.terra.addons.chunkgenerator.config.palette.BiomePaletteTemplate;
import com.dfsek.terra.addons.chunkgenerator.config.palette.PaletteInfo;
import com.dfsek.terra.addons.chunkgenerator.config.palette.SlantLayer;
import com.dfsek.terra.addons.chunkgenerator.config.palette.slant.SlantLayerTemplate;
import com.dfsek.terra.addons.chunkgenerator.generation.NoiseChunkGenerator3D;
import com.dfsek.terra.addons.chunkgenerator.palette.BiomePaletteInfo;
import com.dfsek.terra.addons.chunkgenerator.palette.slant.SlantHolder;
import com.dfsek.terra.addons.manifest.api.AddonInitializer;
import com.dfsek.terra.api.Platform;
import com.dfsek.terra.api.addon.BaseAddon;
@@ -36,15 +37,20 @@ public class NoiseChunkGenerator3DAddon implements AddonInitializer {
@Override
public void initialize() {
PropertyKey<PaletteInfo> paletteInfoPropertyKey = Context.create(PaletteInfo.class);
PropertyKey<BiomePaletteInfo> paletteInfoPropertyKey = Context.create(BiomePaletteInfo.class);
PropertyKey<BiomeNoiseProperties> noisePropertiesPropertyKey = Context.create(BiomeNoiseProperties.class);
platform.getEventManager()
.getHandler(FunctionalEventHandler.class)
.register(addon, ConfigPackPreLoadEvent.class)
.priority(1000)
.then(event -> {
event.getPack().applyLoader(SlantHolder.CalculationMethod.class,
(type, o, loader, depthTracker) -> SlantHolder.CalculationMethod.valueOf((String) o));
NoiseChunkGeneratorPackConfigTemplate config = event.loadTemplate(new NoiseChunkGeneratorPackConfigTemplate());
event.getPack().getContext().put(config);
event.getPack()
.getOrCreateRegistry(ChunkGeneratorProvider.class)
.register(addon.key("NOISE_3D"),
@@ -53,7 +59,7 @@ public class NoiseChunkGenerator3DAddon implements AddonInitializer {
config.getVerticalRes(), noisePropertiesPropertyKey,
paletteInfoPropertyKey));
event.getPack()
.applyLoader(SlantLayer.class, SlantLayer::new);
.applyLoader(SlantHolder.Layer.class, SlantLayerTemplate::new);
})
.failThrough();
@@ -62,8 +68,10 @@ public class NoiseChunkGenerator3DAddon implements AddonInitializer {
.register(addon, ConfigurationLoadEvent.class)
.then(event -> {
if(event.is(Biome.class)) {
NoiseChunkGeneratorPackConfigTemplate config = event.getPack().getContext().get(NoiseChunkGeneratorPackConfigTemplate.class);
event.getLoadedObject(Biome.class).getContext().put(paletteInfoPropertyKey,
event.load(new BiomePaletteTemplate(platform)).get());
event.load(new BiomePaletteTemplate(platform, config.getSlantCalculationMethod())).get());
event.getLoadedObject(Biome.class).getContext().put(noisePropertiesPropertyKey,
event.load(new BiomeNoiseConfigTemplate()).get());
}
@@ -4,10 +4,12 @@ import com.dfsek.tectonic.api.config.template.ConfigTemplate;
import com.dfsek.tectonic.api.config.template.annotations.Default;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.terra.addons.chunkgenerator.palette.slant.SlantHolder;
import com.dfsek.terra.api.config.meta.Meta;
import com.dfsek.terra.api.properties.Properties;
public class NoiseChunkGeneratorPackConfigTemplate implements ConfigTemplate {
public class NoiseChunkGeneratorPackConfigTemplate implements ConfigTemplate, Properties {
@Value("blend.terrain.elevation")
@Default
private @Meta int elevationBlend = 4;
@@ -20,6 +22,10 @@ public class NoiseChunkGeneratorPackConfigTemplate implements ConfigTemplate {
@Default
private @Meta int verticalRes = 2;
@Value("slant.calculation-method")
@Default
private SlantHolder.@Meta CalculationMethod slantCalculationMethod = SlantHolder.CalculationMethod.Derivative;
public int getElevationBlend() {
return elevationBlend;
}
@@ -31,4 +37,8 @@ public class NoiseChunkGeneratorPackConfigTemplate implements ConfigTemplate {
public int getVerticalRes() {
return verticalRes;
}
public SlantHolder.CalculationMethod getSlantCalculationMethod() {
return slantCalculationMethod;
}
}
@@ -15,25 +15,23 @@ import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import com.dfsek.terra.addons.chunkgenerator.palette.PaletteHolder;
import com.dfsek.terra.addons.chunkgenerator.palette.PaletteHolderBuilder;
import com.dfsek.terra.addons.chunkgenerator.palette.SlantHolder;
import com.dfsek.terra.addons.chunkgenerator.palette.BiomePaletteInfo;
import com.dfsek.terra.addons.chunkgenerator.palette.slant.SlantHolder;
import com.dfsek.terra.api.Platform;
import com.dfsek.terra.api.block.state.BlockState;
import com.dfsek.terra.api.config.meta.Meta;
import com.dfsek.terra.api.world.chunk.generation.util.Palette;
public class BiomePaletteTemplate implements ObjectTemplate<PaletteInfo> {
public class BiomePaletteTemplate implements ObjectTemplate<BiomePaletteInfo> {
private final Platform platform;
@Value("slant")
@Default
@Description("The slant palettes to use in this biome.")
private @Meta List<@Meta SlantLayer> slant = Collections.emptyList();
private @Meta List<SlantHolder.@Meta Layer> slantLayers = Collections.emptyList();
@Value("slant-depth")
@Default
@@ -63,27 +61,16 @@ public class BiomePaletteTemplate implements ObjectTemplate<PaletteInfo> {
@Default
private @Meta boolean updatePalette = false;
public BiomePaletteTemplate(Platform platform) { this.platform = platform; }
private final SlantHolder.CalculationMethod slantCalculationMethod;
public BiomePaletteTemplate(Platform platform, SlantHolder.CalculationMethod slantCalculationMethod) {
this.platform = platform;
this.slantCalculationMethod = slantCalculationMethod;
}
@Override
public PaletteInfo get() {
PaletteHolderBuilder builder = new PaletteHolderBuilder();
for(Map<Palette, Integer> layer : palettes) {
for(Entry<Palette, Integer> entry : layer.entrySet()) {
builder.add(entry.getValue(), entry.getKey());
}
}
TreeMap<Double, PaletteHolder> slantLayers = new TreeMap<>();
double minThreshold = Double.MAX_VALUE;
for(SlantLayer layer : slant) {
double threshold = layer.getThreshold();
if(threshold < minThreshold) minThreshold = threshold;
slantLayers.put(threshold, layer.getPalette());
}
return new PaletteInfo(builder.build(), SlantHolder.of(slantLayers, minThreshold), oceanPalette, seaLevel, slantDepth,
updatePalette);
public BiomePaletteInfo get() {
return new BiomePaletteInfo(PaletteHolder.of(palettes), SlantHolder.of(slantLayers, slantDepth, slantCalculationMethod),
oceanPalette, seaLevel, updatePalette);
}
}
@@ -1,22 +0,0 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.chunkgenerator.config.palette;
import com.dfsek.terra.addons.chunkgenerator.palette.PaletteHolder;
import com.dfsek.terra.addons.chunkgenerator.palette.SlantHolder;
import com.dfsek.terra.api.properties.Properties;
import com.dfsek.terra.api.world.chunk.generation.util.Palette;
public record PaletteInfo(PaletteHolder paletteHolder,
SlantHolder slantHolder,
Palette ocean,
int seaLevel,
int maxSlantDepth,
boolean updatePaletteWhenCarving) implements Properties {
}
@@ -1,42 +0,0 @@
package com.dfsek.terra.addons.chunkgenerator.config.palette;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import com.dfsek.terra.addons.chunkgenerator.palette.PaletteHolder;
import com.dfsek.terra.addons.chunkgenerator.palette.PaletteHolderBuilder;
import com.dfsek.terra.api.config.meta.Meta;
import com.dfsek.terra.api.world.chunk.generation.util.Palette;
public class SlantLayer implements ObjectTemplate<SlantLayer> {
@Value("threshold")
private @Meta double threshold;
@Value("palette")
private @Meta List<@Meta Map<@Meta Palette, @Meta Integer>> palettes;
@Override
public SlantLayer get() {
return this;
}
public double getThreshold() {
return threshold;
}
public PaletteHolder getPalette() {
PaletteHolderBuilder builder = new PaletteHolderBuilder();
for(Map<Palette, Integer> layer : palettes) {
for(Entry<Palette, Integer> entry : layer.entrySet()) {
builder.add(entry.getValue(), entry.getKey());
}
}
return builder.build();
}
}
@@ -0,0 +1,27 @@
package com.dfsek.terra.addons.chunkgenerator.config.palette.slant;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import java.util.List;
import java.util.Map;
import com.dfsek.terra.addons.chunkgenerator.palette.PaletteHolder;
import com.dfsek.terra.addons.chunkgenerator.palette.slant.SlantHolder;
import com.dfsek.terra.api.config.meta.Meta;
import com.dfsek.terra.api.world.chunk.generation.util.Palette;
public class SlantLayerTemplate implements ObjectTemplate<SlantHolder.Layer> {
@Value("threshold")
private @Meta double threshold;
@Value("palette")
private @Meta List<@Meta Map<@Meta Palette, @Meta Integer>> palettes;
@Override
public SlantHolder.Layer get() {
return new SlantHolder.Layer(PaletteHolder.of(palettes), threshold);
}
}
@@ -12,7 +12,7 @@ import net.jafama.FastMath;
import org.jetbrains.annotations.NotNull;
import com.dfsek.terra.addons.chunkgenerator.config.noise.BiomeNoiseProperties;
import com.dfsek.terra.addons.chunkgenerator.config.palette.PaletteInfo;
import com.dfsek.terra.addons.chunkgenerator.palette.BiomePaletteInfo;
import com.dfsek.terra.addons.chunkgenerator.generation.math.PaletteUtil;
import com.dfsek.terra.addons.chunkgenerator.generation.math.interpolation.LazilyEvaluatedInterpolator;
import com.dfsek.terra.addons.chunkgenerator.generation.math.samplers.Sampler3D;
@@ -40,13 +40,13 @@ public class NoiseChunkGenerator3D implements ChunkGenerator {
private final int carverHorizontalResolution;
private final int carverVerticalResolution;
private final PropertyKey<PaletteInfo> paletteInfoPropertyKey;
private final PropertyKey<BiomePaletteInfo> paletteInfoPropertyKey;
private final PropertyKey<BiomeNoiseProperties> noisePropertiesKey;
public NoiseChunkGenerator3D(ConfigPack pack, Platform platform, int elevationBlend, int carverHorizontalResolution,
int carverVerticalResolution,
PropertyKey<BiomeNoiseProperties> noisePropertiesKey,
PropertyKey<PaletteInfo> paletteInfoPropertyKey) {
PropertyKey<BiomePaletteInfo> paletteInfoPropertyKey) {
this.platform = platform;
this.air = platform.getWorldHandle().air();
this.carverHorizontalResolution = carverHorizontalResolution;
@@ -97,7 +97,7 @@ public class NoiseChunkGenerator3D implements ChunkGenerator {
for(int y = world.getMaxHeight() - 1; y >= world.getMinHeight(); y--) {
Biome biome = biomeColumn.get(y);
PaletteInfo paletteInfo = biome.getContext().get(paletteInfoPropertyKey);
BiomePaletteInfo paletteInfo = biome.getContext().get(paletteInfoPropertyKey);
int sea = paletteInfo.seaLevel();
Palette seaPalette = paletteInfo.ocean();
@@ -131,7 +131,7 @@ public class NoiseChunkGenerator3D implements ChunkGenerator {
Biome biome = biomeProvider.getBiome(x, y, z, world.getSeed());
Sampler3D sampler = samplerCache.get(x, z, world, biomeProvider);
PaletteInfo paletteInfo = biome.getContext().get(paletteInfoPropertyKey);
BiomePaletteInfo paletteInfo = biome.getContext().get(paletteInfoPropertyKey);
int fdX = FastMath.floorMod(x, 16);
int fdZ = FastMath.floorMod(z, 16);
@@ -7,40 +7,23 @@
package com.dfsek.terra.addons.chunkgenerator.generation.math;
import com.dfsek.terra.addons.chunkgenerator.config.palette.PaletteInfo;
import com.dfsek.terra.addons.chunkgenerator.palette.BiomePaletteInfo;
import com.dfsek.terra.addons.chunkgenerator.generation.math.samplers.Sampler3D;
import com.dfsek.terra.addons.chunkgenerator.palette.SlantHolder;
import com.dfsek.terra.addons.chunkgenerator.palette.slant.SlantHolder;
import com.dfsek.terra.api.world.chunk.generation.util.Palette;
public final class PaletteUtil {
/**
* Derivative constant.
*/
private static final double DERIVATIVE_DIST = 0.55;
public static Palette getPalette(int x, int y, int z, Sampler3D sampler, PaletteInfo paletteInfo, int depth) {
SlantHolder slant = paletteInfo.slantHolder();
if(!slant.isEmpty() && depth <= paletteInfo.maxSlantDepth()) {
double slope = derivative(sampler, x, y, z);
if(slope > slant.getMinSlope()) {
return slant.getPalette(slope).getPalette(y);
public static Palette getPalette(int x, int y, int z, Sampler3D sampler, BiomePaletteInfo paletteInfo, int depth) {
SlantHolder slantHolder = paletteInfo.slantHolder();
if(slantHolder.isAboveDepth(depth)) {
double slant = slantHolder.calculateSlant(sampler, x, y, z);
if(slantHolder.isInSlantThreshold(slant)) {
return slantHolder.getPalette(slant).getPalette(y);
}
}
return paletteInfo.paletteHolder().getPalette(y);
}
public static double derivative(Sampler3D sampler, double x, double y, double z) {
double baseSample = sampler.sample(x, y, z);
double xVal1 = (sampler.sample(x + DERIVATIVE_DIST, y, z) - baseSample) / DERIVATIVE_DIST;
double xVal2 = (sampler.sample(x - DERIVATIVE_DIST, y, z) - baseSample) / DERIVATIVE_DIST;
double zVal1 = (sampler.sample(x, y, z + DERIVATIVE_DIST) - baseSample) / DERIVATIVE_DIST;
double zVal2 = (sampler.sample(x, y, z - DERIVATIVE_DIST) - baseSample) / DERIVATIVE_DIST;
double yVal1 = (sampler.sample(x, y + DERIVATIVE_DIST, z) - baseSample) / DERIVATIVE_DIST;
double yVal2 = (sampler.sample(x, y - DERIVATIVE_DIST, z) - baseSample) / DERIVATIVE_DIST;
return Math.sqrt(((xVal2 - xVal1) * (xVal2 - xVal1)) + ((zVal2 - zVal1) * (zVal2 - zVal1)) + ((yVal2 - yVal1) * (yVal2 - yVal1)));
}
}
@@ -0,0 +1,20 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.chunkgenerator.palette;
import com.dfsek.terra.addons.chunkgenerator.palette.slant.SlantHolder;
import com.dfsek.terra.api.properties.Properties;
import com.dfsek.terra.api.world.chunk.generation.util.Palette;
public record BiomePaletteInfo(PaletteHolder paletteHolder,
SlantHolder slantHolder,
Palette ocean,
int seaLevel,
boolean updatePaletteWhenCarving) implements Properties {
}
@@ -7,6 +7,13 @@
package com.dfsek.terra.addons.chunkgenerator.palette;
import net.jafama.FastMath;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import com.dfsek.terra.api.world.chunk.generation.util.Palette;
@@ -27,4 +34,43 @@ public class PaletteHolder {
: palettes[palettes.length - 1]
: palettes[0];
}
public static PaletteHolder of(List<Map<Palette, Integer>> palettes) {
PaletteHolderBuilder builder = new PaletteHolderBuilder();
for(Map<Palette, Integer> layer : palettes) {
for(Entry<Palette, Integer> entry : layer.entrySet()) {
builder.add(entry.getValue(), entry.getKey());
}
}
return builder.build();
}
private static class PaletteHolderBuilder {
private final TreeMap<Integer, Palette> paletteMap = new TreeMap<>();
public PaletteHolderBuilder add(int y, Palette palette) {
paletteMap.put(y, palette);
return this;
}
public PaletteHolder build() {
int min = FastMath.min(paletteMap.keySet().stream().min(Integer::compareTo).orElse(0), 0);
int max = FastMath.max(paletteMap.keySet().stream().max(Integer::compareTo).orElse(255), 255);
Palette[] palettes = new Palette[paletteMap.lastKey() + 1 - min];
for(int y = min; y <= FastMath.max(paletteMap.lastKey(), max); y++) {
Palette d = null;
for(Entry<Integer, Palette> e : paletteMap.entrySet()) {
if(e.getKey() >= y) {
d = e.getValue();
break;
}
}
if(d == null) throw new IllegalArgumentException("No palette for Y=" + y);
palettes[y - min] = d;
}
return new PaletteHolder(palettes, -min);
}
}
}
@@ -1,45 +0,0 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.chunkgenerator.palette;
import net.jafama.FastMath;
import java.util.Map;
import java.util.TreeMap;
import com.dfsek.terra.api.world.chunk.generation.util.Palette;
public class PaletteHolderBuilder {
private final TreeMap<Integer, Palette> paletteMap = new TreeMap<>();
public PaletteHolderBuilder add(int y, Palette palette) {
paletteMap.put(y, palette);
return this;
}
public PaletteHolder build() {
int min = FastMath.min(paletteMap.keySet().stream().min(Integer::compareTo).orElse(0), 0);
int max = FastMath.max(paletteMap.keySet().stream().max(Integer::compareTo).orElse(255), 255);
Palette[] palettes = new Palette[paletteMap.lastKey() + 1 - min];
for(int y = min; y <= FastMath.max(paletteMap.lastKey(), max); y++) {
Palette d = null;
for(Map.Entry<Integer, Palette> e : paletteMap.entrySet()) {
if(e.getKey() >= y) {
d = e.getValue();
break;
}
}
if(d == null) throw new IllegalArgumentException("No palette for Y=" + y);
palettes[y - min] = d;
}
return new PaletteHolder(palettes, -min);
}
}
@@ -1,64 +0,0 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.chunkgenerator.palette;
import java.util.Map.Entry;
import java.util.TreeMap;
public class SlantHolder {
private final TreeMap<Double, PaletteHolder> layers;
private final double minSlope;
private SlantHolder(TreeMap<Double, PaletteHolder> layers, double minSlope) {
this.layers = layers;
this.minSlope = minSlope;
}
public static SlantHolder of(TreeMap<Double, PaletteHolder> layers, double minSlope) {
if(layers.size() == 1) {
Entry<Double, PaletteHolder> firstEntry = layers.firstEntry();
return new Single(firstEntry.getValue(), minSlope);
}
return new SlantHolder(layers, minSlope);
}
public boolean isEmpty() {
return layers.isEmpty();
}
public PaletteHolder getPalette(double slope) {
return layers.floorEntry(slope).getValue();
}
public double getMinSlope() {
return minSlope;
}
private static final class Single extends SlantHolder {
private final PaletteHolder layers;
public Single(PaletteHolder layers, double minSlope) {
super(of(minSlope, layers), minSlope);
this.layers = layers;
}
private static TreeMap<Double, PaletteHolder> of(double v, PaletteHolder layer) {
TreeMap<Double, PaletteHolder> map = new TreeMap<>();
map.put(v, layer);
return map;
}
@Override
public PaletteHolder getPalette(double slope) {
return layers;
}
}
}
@@ -0,0 +1,47 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.chunkgenerator.palette.slant;
import java.util.List;
import java.util.NavigableMap;
import java.util.TreeMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.dfsek.terra.addons.chunkgenerator.palette.PaletteHolder;
public class MultipleSlantHolder extends SlantHolderImpl {
private final NavigableMap<Double, PaletteHolder> layers;
private final double slantThreshold;
MultipleSlantHolder(List<SlantHolder.Layer> slant, int slantDepth, CalculationMethod calculationMethod) {
super(slantDepth, calculationMethod);
NavigableMap<Double, PaletteHolder> layers = new TreeMap<>(slant.stream().collect(Collectors.toMap(SlantHolder.Layer::threshold, SlantHolder.Layer::palette)));
Stream<Double> thresholds = layers.keySet().stream();
double slantThreshold = floorToThreshold ?
thresholds.min(Double::compare).orElseThrow() :
thresholds.max(Double::compare).orElseThrow();
this.layers = layers;
this.slantThreshold = slantThreshold;
}
@Override
protected double getSlantThreshold() {
return slantThreshold;
}
@Override
public PaletteHolder getPalette(double slant) {
return (floorToThreshold ?
layers.floorEntry(slant) :
layers.ceilingEntry(slant)
).getValue();
}
}
@@ -0,0 +1,24 @@
package com.dfsek.terra.addons.chunkgenerator.palette.slant;
import com.dfsek.terra.addons.chunkgenerator.palette.PaletteHolder;
final class SingleSlantHolder extends SlantHolderImpl {
private final SlantHolder.Layer layer;
public SingleSlantHolder(SlantHolder.Layer layer, int slantDepth, CalculationMethod calculationMethod) {
super(slantDepth, calculationMethod);
this.layer = layer;
}
@Override
public PaletteHolder getPalette(double slant) {
return layer.palette();
}
@Override
protected double getSlantThreshold() {
return layer.threshold();
}
}
@@ -0,0 +1,116 @@
package com.dfsek.terra.addons.chunkgenerator.palette.slant;
import java.util.List;
import com.dfsek.terra.addons.chunkgenerator.generation.math.samplers.Sampler3D;
import com.dfsek.terra.addons.chunkgenerator.palette.PaletteHolder;
import com.dfsek.terra.api.util.vector.Vector3;
public interface SlantHolder {
static SlantHolder of(List<SlantHolder.Layer> layers, int slantDepth, CalculationMethod calculationMethod) {
if(layers.isEmpty()) {
return EMPTY;
} else if(layers.size() == 1) {
return new SingleSlantHolder(layers.get(0), slantDepth, calculationMethod);
}
return new MultipleSlantHolder(layers, slantDepth, calculationMethod);
}
double calculateSlant(Sampler3D sampler, double x, double y, double z);
boolean isAboveDepth(int depth);
boolean isInSlantThreshold(double slant);
PaletteHolder getPalette(double slant);
record Layer(PaletteHolder palette, double threshold) {
}
enum CalculationMethod {
DotProduct {
private static final Vector3 DOT_PRODUCT_DIRECTION = Vector3.of(0, 1, 0);
private static final Vector3[] DOT_PRODUCT_SAMPLE_POINTS = {
Vector3.of(0, 0, -DERIVATIVE_DIST),
Vector3.of(0, 0, DERIVATIVE_DIST),
Vector3.of(0, -DERIVATIVE_DIST, 0),
Vector3.of(0, DERIVATIVE_DIST, 0),
Vector3.of(-DERIVATIVE_DIST, 0, 0),
Vector3.of(DERIVATIVE_DIST, 0, 0)
};
@Override
public double slant(Sampler3D sampler, double x, double y, double z) {
Vector3.Mutable normalApproximation = Vector3.Mutable.of(0, 0, 0);
for(Vector3 point : DOT_PRODUCT_SAMPLE_POINTS) {
var scalar = -sampler.sample(x+point.getX(), y+point.getY(), z+point.getZ());
normalApproximation.add(point.mutable().multiply(scalar));
}
return DOT_PRODUCT_DIRECTION.dot(normalApproximation.normalize());
}
@Override
public boolean floorToThreshold() {
return false;
}
},
Derivative {
@Override
public double slant(Sampler3D sampler, double x, double y, double z) {
double baseSample = sampler.sample(x, y, z);
double xVal1 = (sampler.sample(x + DERIVATIVE_DIST, y, z) - baseSample) / DERIVATIVE_DIST;
double xVal2 = (sampler.sample(x - DERIVATIVE_DIST, y, z) - baseSample) / DERIVATIVE_DIST;
double zVal1 = (sampler.sample(x, y, z + DERIVATIVE_DIST) - baseSample) / DERIVATIVE_DIST;
double zVal2 = (sampler.sample(x, y, z - DERIVATIVE_DIST) - baseSample) / DERIVATIVE_DIST;
double yVal1 = (sampler.sample(x, y + DERIVATIVE_DIST, z) - baseSample) / DERIVATIVE_DIST;
double yVal2 = (sampler.sample(x, y - DERIVATIVE_DIST, z) - baseSample) / DERIVATIVE_DIST;
return Math.sqrt(((xVal2 - xVal1) * (xVal2 - xVal1)) + ((zVal2 - zVal1) * (zVal2 - zVal1)) + ((yVal2 - yVal1) * (yVal2 - yVal1)));
}
@Override
public boolean floorToThreshold() {
return true;
}
};
private static final double DERIVATIVE_DIST = 0.55;
public abstract double slant(Sampler3D sampler, double x, double y, double z);
/*
* Controls whether palettes should be applied before or after their respective thresholds.
*
* If true, slant values will map to the palette of the next floor threshold, otherwise they
* will map to the ceiling.
*/
public abstract boolean floorToThreshold();
}
SlantHolder EMPTY = new SlantHolder() {
@Override
public double calculateSlant(Sampler3D sampler, double x, double y, double z) {
throw new UnsupportedOperationException("Empty holder should not calculate slant");
}
@Override
public boolean isAboveDepth(int depth) {
return false;
}
@Override
public boolean isInSlantThreshold(double slant) {
return false;
}
@Override
public PaletteHolder getPalette(double slant) {
throw new UnsupportedOperationException("Empty holder cannot return a palette");
}
};
}
@@ -0,0 +1,38 @@
package com.dfsek.terra.addons.chunkgenerator.palette.slant;
import com.dfsek.terra.addons.chunkgenerator.generation.math.samplers.Sampler3D;
public abstract class SlantHolderImpl implements SlantHolder {
private final SlantHolder.CalculationMethod calculationMethod;
private final int slantDepth;
protected final boolean floorToThreshold;
protected SlantHolderImpl(int slantDepth, CalculationMethod calculationMethod) {
this.floorToThreshold = calculationMethod.floorToThreshold();
this.calculationMethod = calculationMethod;
this.slantDepth = slantDepth;
}
protected abstract double getSlantThreshold();
@Override
public final double calculateSlant(Sampler3D sampler, double x, double y, double z) {
return calculationMethod.slant(sampler, x, y, z);
}
@Override
public final boolean isAboveDepth(int depth) {
return depth <= slantDepth;
}
@Override
public final boolean isInSlantThreshold(double slant) {
return (floorToThreshold ?
slant > getSlantThreshold() :
slant < getSlantThreshold()
);
}
}
@@ -1,6 +1,6 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
version = version("1.0.0")
version = version("1.1.0")
dependencies {
compileOnlyApi(project(":common:addons:manifest-addon-loader"))
@@ -21,8 +21,10 @@ import com.dfsek.terra.addons.noise.config.templates.FunctionTemplate;
import com.dfsek.terra.addons.noise.config.templates.ImageSamplerTemplate;
import com.dfsek.terra.addons.noise.config.templates.KernelTemplate;
import com.dfsek.terra.addons.noise.config.templates.LinearHeightmapSamplerTemplate;
import com.dfsek.terra.addons.noise.config.templates.TranslateSamplerTemplate;
import com.dfsek.terra.addons.noise.config.templates.noise.CellularNoiseTemplate;
import com.dfsek.terra.addons.noise.config.templates.noise.ConstantNoiseTemplate;
import com.dfsek.terra.addons.noise.config.templates.noise.DistanceSamplerTemplate;
import com.dfsek.terra.addons.noise.config.templates.noise.ExpressionFunctionTemplate;
import com.dfsek.terra.addons.noise.config.templates.noise.GaborNoiseTemplate;
import com.dfsek.terra.addons.noise.config.templates.noise.SimpleNoiseTemplate;
@@ -30,6 +32,7 @@ import com.dfsek.terra.addons.noise.config.templates.noise.fractal.BrownianMotio
import com.dfsek.terra.addons.noise.config.templates.noise.fractal.PingPongTemplate;
import com.dfsek.terra.addons.noise.config.templates.noise.fractal.RidgedFractalTemplate;
import com.dfsek.terra.addons.noise.config.templates.normalizer.ClampNormalizerTemplate;
import com.dfsek.terra.addons.noise.config.templates.normalizer.ExpressionNormalizerTemplate;
import com.dfsek.terra.addons.noise.config.templates.normalizer.LinearNormalizerTemplate;
import com.dfsek.terra.addons.noise.config.templates.normalizer.NormalNormalizerTemplate;
import com.dfsek.terra.addons.noise.config.templates.normalizer.PosterizationNormalizerTemplate;
@@ -42,6 +45,7 @@ import com.dfsek.terra.addons.noise.samplers.arithmetic.MinSampler;
import com.dfsek.terra.addons.noise.samplers.arithmetic.MultiplicationSampler;
import com.dfsek.terra.addons.noise.samplers.arithmetic.SubtractionSampler;
import com.dfsek.terra.addons.noise.samplers.noise.CellularSampler;
import com.dfsek.terra.addons.noise.samplers.noise.DistanceSampler;
import com.dfsek.terra.addons.noise.samplers.noise.random.GaussianNoiseSampler;
import com.dfsek.terra.addons.noise.samplers.noise.random.PositiveWhiteNoiseSampler;
import com.dfsek.terra.addons.noise.samplers.noise.random.WhiteNoiseSampler;
@@ -83,6 +87,8 @@ public class NoiseAddon implements AddonInitializer {
(type, o, loader, depthTracker) -> CellularSampler.DistanceFunction.valueOf((String) o))
.applyLoader(CellularSampler.ReturnType.class,
(type, o, loader, depthTracker) -> CellularSampler.ReturnType.valueOf((String) o))
.applyLoader(DistanceSampler.DistanceFunction.class,
(type, o, loader, depthTracker) -> DistanceSampler.DistanceFunction.valueOf((String) o))
.applyLoader(DimensionApplicableNoiseSampler.class, DimensionApplicableNoiseSampler::new)
.applyLoader(FunctionTemplate.class, FunctionTemplate::new);
@@ -92,9 +98,9 @@ public class NoiseAddon implements AddonInitializer {
noiseRegistry.register(addon.key("PROBABILITY"), ProbabilityNormalizerTemplate::new);
noiseRegistry.register(addon.key("SCALE"), ScaleNormalizerTemplate::new);
noiseRegistry.register(addon.key("POSTERIZATION"), PosterizationNormalizerTemplate::new);
noiseRegistry.register(addon.key("IMAGE"), ImageSamplerTemplate::new);
noiseRegistry.register(addon.key("DOMAIN_WARP"), DomainWarpTemplate::new);
noiseRegistry.register(addon.key("FBM"), BrownianMotionTemplate::new);
@@ -116,12 +122,15 @@ public class NoiseAddon implements AddonInitializer {
noiseRegistry.register(addon.key("WHITE_NOISE"), () -> new SimpleNoiseTemplate(WhiteNoiseSampler::new));
noiseRegistry.register(addon.key("POSITIVE_WHITE_NOISE"), () -> new SimpleNoiseTemplate(PositiveWhiteNoiseSampler::new));
noiseRegistry.register(addon.key("GAUSSIAN"), () -> new SimpleNoiseTemplate(GaussianNoiseSampler::new));
noiseRegistry.register(addon.key("DISTANCE"), DistanceSamplerTemplate::new);
noiseRegistry.register(addon.key("CONSTANT"), ConstantNoiseTemplate::new);
noiseRegistry.register(addon.key("KERNEL"), KernelTemplate::new);
noiseRegistry.register(addon.key("LINEAR_HEIGHTMAP"), LinearHeightmapSamplerTemplate::new);
noiseRegistry.register(addon.key("TRANSLATE"), TranslateSamplerTemplate::new);
noiseRegistry.register(addon.key("ADD"), () -> new BinaryArithmeticTemplate<>(AdditionSampler::new));
noiseRegistry.register(addon.key("SUB"), () -> new BinaryArithmeticTemplate<>(SubtractionSampler::new));
@@ -134,7 +143,7 @@ public class NoiseAddon implements AddonInitializer {
Map<String, DimensionApplicableNoiseSampler> packSamplers = new LinkedHashMap<>();
Map<String, FunctionTemplate> packFunctions = new LinkedHashMap<>();
noiseRegistry.register(addon.key("EXPRESSION"), () -> new ExpressionFunctionTemplate(packSamplers, packFunctions));
noiseRegistry.register(addon.key("EXPRESSION_NORMALIZER"), () -> new ExpressionNormalizerTemplate(packSamplers, packFunctions));
NoiseConfigPackTemplate template = event.loadTemplate(new NoiseConfigPackTemplate());
packSamplers.putAll(template.getSamplers());
@@ -8,6 +8,8 @@
package com.dfsek.terra.addons.noise.config.templates;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.image.BufferedImage;
@@ -19,6 +21,10 @@ import com.dfsek.terra.api.noise.NoiseSampler;
@SuppressWarnings({ "unused", "FieldMayBeFinal" })
public class ImageSamplerTemplate extends SamplerTemplate<ImageSampler> {
private static final Logger logger = LoggerFactory.getLogger(ImageSamplerTemplate.class);
private static boolean used = false;
@Value("image")
private @Meta BufferedImage image;
@@ -30,6 +36,12 @@ public class ImageSamplerTemplate extends SamplerTemplate<ImageSampler> {
@Override
public NoiseSampler get() {
if(!used) {
logger.warn("The IMAGE NoiseSampler implemented by the config-noise-function addon is deprecated. " +
"It is recommended to use the IMAGE NoiseSampler implemented by the config-noise-image " +
"addon instead.");
used = true;
}
return new ImageSampler(image, channel, frequency);
}
}
@@ -4,6 +4,7 @@ import com.dfsek.tectonic.api.config.template.annotations.Default;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.terra.addons.noise.samplers.LinearHeightmapSampler;
import com.dfsek.terra.api.config.meta.Meta;
import com.dfsek.terra.api.noise.NoiseSampler;
@@ -11,14 +12,14 @@ import com.dfsek.terra.api.noise.NoiseSampler;
public class LinearHeightmapSamplerTemplate extends SamplerTemplate<LinearHeightmapSampler> {
@Value("sampler")
@Default
private NoiseSampler sampler = NoiseSampler.zero();
private @Meta NoiseSampler sampler = NoiseSampler.zero();
@Value("base")
private double base;
private @Meta double base;
@Value("scale")
@Default
private double scale = 1;
private @Meta double scale = 1;
@Override
public NoiseSampler get() {
@@ -0,0 +1,32 @@
package com.dfsek.terra.addons.noise.config.templates;
import com.dfsek.tectonic.api.config.template.annotations.Default;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.terra.addons.noise.samplers.TranslateSampler;
import com.dfsek.terra.api.config.meta.Meta;
import com.dfsek.terra.api.noise.NoiseSampler;
public class TranslateSamplerTemplate extends SamplerTemplate<TranslateSampler> {
@Value("sampler")
private NoiseSampler sampler;
@Value("x")
@Default
private @Meta double x = 0;
@Value("y")
@Default
private @Meta double y = 0;
@Value("z")
@Default
private @Meta double z = 0;
@Override
public NoiseSampler get() {
return new TranslateSampler(sampler, x, y ,z);
}
}
@@ -0,0 +1,42 @@
package com.dfsek.terra.addons.noise.config.templates.noise;
import com.dfsek.tectonic.api.config.template.annotations.Default;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.terra.addons.noise.config.templates.SamplerTemplate;
import com.dfsek.terra.addons.noise.samplers.noise.DistanceSampler;
import com.dfsek.terra.addons.noise.samplers.noise.DistanceSampler.DistanceFunction;
import com.dfsek.terra.api.config.meta.Meta;
public class DistanceSamplerTemplate extends SamplerTemplate<DistanceSampler> {
@Value("distance-function")
@Default
private DistanceSampler.@Meta DistanceFunction distanceFunction = DistanceFunction.Euclidean;
@Value("point.x")
@Default
private @Meta double x = 0;
@Value("point.y")
@Default
private @Meta double y = 0;
@Value("point.z")
@Default
private @Meta double z = 0;
@Value("normalize")
@Default
private @Meta boolean normalize = false;
@Value("radius")
@Default
private @Meta double normalizeRadius = 100;
@Override
public DistanceSampler get() {
return new DistanceSampler(distanceFunction, x, y, z, normalize, normalizeRadius);
}
}
@@ -8,7 +8,6 @@
package com.dfsek.terra.addons.noise.config.templates.noise;
import com.dfsek.paralithic.eval.tokenizer.ParseException;
import com.dfsek.paralithic.functions.Function;
import com.dfsek.tectonic.api.config.template.annotations.Default;
import com.dfsek.tectonic.api.config.template.annotations.Value;
@@ -19,17 +18,16 @@ import java.util.Map;
import com.dfsek.terra.addons.noise.config.DimensionApplicableNoiseSampler;
import com.dfsek.terra.addons.noise.config.templates.FunctionTemplate;
import com.dfsek.terra.addons.noise.config.templates.SamplerTemplate;
import com.dfsek.terra.addons.noise.paralithic.defined.UserDefinedFunction;
import com.dfsek.terra.addons.noise.paralithic.noise.NoiseFunction2;
import com.dfsek.terra.addons.noise.paralithic.noise.NoiseFunction3;
import com.dfsek.terra.addons.noise.samplers.noise.ExpressionFunction;
import com.dfsek.terra.api.config.meta.Meta;
import com.dfsek.terra.api.noise.NoiseSampler;
import static com.dfsek.terra.addons.noise.paralithic.FunctionUtil.convertFunctionsAndSamplers;
@SuppressWarnings({ "FieldMayBeFinal", "unused" })
public class ExpressionFunctionTemplate extends SamplerTemplate<ExpressionFunction> {
private final Map<String, DimensionApplicableNoiseSampler> otherFunctions;
private final Map<String, DimensionApplicableNoiseSampler> globalSamplers;
private final Map<String, FunctionTemplate> globalFunctions;
@Value("variables")
@Default
@@ -43,44 +41,19 @@ public class ExpressionFunctionTemplate extends SamplerTemplate<ExpressionFuncti
@Default
private @Meta LinkedHashMap<String, @Meta FunctionTemplate> functions = new LinkedHashMap<>();
public ExpressionFunctionTemplate(Map<String, DimensionApplicableNoiseSampler> otherFunctions, Map<String, FunctionTemplate> samplers) {
this.otherFunctions = otherFunctions;
this.globalFunctions = samplers;
public ExpressionFunctionTemplate(Map<String, DimensionApplicableNoiseSampler> globalSamplers, Map<String, FunctionTemplate> globalFunctions) {
this.globalSamplers = globalSamplers;
this.globalFunctions = globalFunctions;
}
@Override
public NoiseSampler get() {
var mergedFunctions = new HashMap<>(globalFunctions); mergedFunctions.putAll(functions);
var mergedSamplers = new HashMap<>(globalSamplers); mergedSamplers.putAll(samplers);
try {
Map<String, Function> noiseFunctionMap = generateFunctions();
return new ExpressionFunction(noiseFunctionMap, expression, vars);
return new ExpressionFunction(convertFunctionsAndSamplers(mergedFunctions, mergedSamplers), expression, vars);
} catch(ParseException e) {
throw new RuntimeException("Failed to parse expression.", e);
}
}
private Map<String, Function> generateFunctions() throws ParseException {
Map<String, Function> noiseFunctionMap = new HashMap<>();
for(Map.Entry<String, FunctionTemplate> entry : globalFunctions.entrySet()) {
noiseFunctionMap.put(entry.getKey(), UserDefinedFunction.newInstance(entry.getValue()));
}
for(Map.Entry<String, FunctionTemplate> entry : functions.entrySet()) {
noiseFunctionMap.put(entry.getKey(), UserDefinedFunction.newInstance(entry.getValue()));
}
otherFunctions.forEach((id, function) -> {
if(function.getDimensions() == 2) {
noiseFunctionMap.put(id, new NoiseFunction2(function.getSampler()));
} else noiseFunctionMap.put(id, new NoiseFunction3(function.getSampler()));
});
samplers.forEach((id, function) -> {
if(function.getDimensions() == 2) {
noiseFunctionMap.put(id, new NoiseFunction2(function.getSampler()));
} else noiseFunctionMap.put(id, new NoiseFunction3(function.getSampler()));
});
return noiseFunctionMap;
}
}
@@ -0,0 +1,63 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.noise.config.templates.normalizer;
import com.dfsek.paralithic.eval.tokenizer.ParseException;
import com.dfsek.tectonic.api.config.template.annotations.Default;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.terra.addons.noise.config.DimensionApplicableNoiseSampler;
import com.dfsek.terra.addons.noise.config.templates.FunctionTemplate;
import com.dfsek.terra.addons.noise.normalizer.ExpressionNormalizer;
import com.dfsek.terra.api.config.meta.Meta;
import com.dfsek.terra.api.noise.NoiseSampler;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import static com.dfsek.terra.addons.noise.paralithic.FunctionUtil.convertFunctionsAndSamplers;
@SuppressWarnings({ "unused", "FieldMayBeFinal" })
public class ExpressionNormalizerTemplate extends NormalizerTemplate<ExpressionNormalizer> {
private final Map<String, DimensionApplicableNoiseSampler> globalSamplers;
private final Map<String, FunctionTemplate> globalFunctions;
@Value("expression")
private @Meta String expression;
@Value("variables")
@Default
private @Meta Map<String, @Meta Double> vars = new HashMap<>();
@Value("samplers")
@Default
private @Meta LinkedHashMap<String, @Meta DimensionApplicableNoiseSampler> samplers = new LinkedHashMap<>();
@Value("functions")
@Default
private @Meta LinkedHashMap<String, @Meta FunctionTemplate> functions = new LinkedHashMap<>();
public ExpressionNormalizerTemplate(Map<String, DimensionApplicableNoiseSampler> globalSamplers, Map<String, FunctionTemplate> globalFunctions) {
this.globalSamplers = globalSamplers;
this.globalFunctions = globalFunctions;
}
@Override
public NoiseSampler get() {
var mergedFunctions = new HashMap<>(globalFunctions); mergedFunctions.putAll(functions);
var mergedSamplers = new HashMap<>(globalSamplers); mergedSamplers.putAll(samplers);
try {
return new ExpressionNormalizer(function, convertFunctionsAndSamplers(mergedFunctions, mergedSamplers), expression, vars);
} catch(ParseException e) {
throw new RuntimeException("Failed to parse expression.", e);
}
}
}
@@ -0,0 +1,33 @@
package com.dfsek.terra.addons.noise.normalizer;
import com.dfsek.paralithic.Expression;
import com.dfsek.paralithic.eval.parser.Parser;
import com.dfsek.paralithic.eval.parser.Scope;
import com.dfsek.paralithic.eval.tokenizer.ParseException;
import com.dfsek.paralithic.functions.Function;
import com.dfsek.terra.api.noise.NoiseSampler;
import java.util.Map;
public class ExpressionNormalizer extends Normalizer {
private final Expression expression;
public ExpressionNormalizer(NoiseSampler sampler, Map<String, Function> functions, String eq, Map<String, Double> vars)
throws ParseException {
super(sampler);
Parser p = new Parser();
Scope scope = new Scope();
scope.addInvocationVariable("in");
vars.forEach(scope::create);
functions.forEach(p::registerFunction);
expression = p.parse(eq, scope);
}
@Override
public double normalize(double in) {
return expression.evaluate(in);
}
}
@@ -0,0 +1,31 @@
package com.dfsek.terra.addons.noise.paralithic;
import com.dfsek.paralithic.eval.tokenizer.ParseException;
import com.dfsek.paralithic.functions.Function;
import com.dfsek.terra.addons.noise.config.DimensionApplicableNoiseSampler;
import com.dfsek.terra.addons.noise.config.templates.FunctionTemplate;
import com.dfsek.terra.addons.noise.paralithic.defined.UserDefinedFunction;
import com.dfsek.terra.addons.noise.paralithic.noise.NoiseFunction2;
import com.dfsek.terra.addons.noise.paralithic.noise.NoiseFunction3;
import java.util.HashMap;
import java.util.Map;
public class FunctionUtil {
private FunctionUtil() {}
public static Map<String, Function> convertFunctionsAndSamplers(Map<String, FunctionTemplate> functions,
Map<String, DimensionApplicableNoiseSampler> samplers) throws ParseException {
Map<String, Function> functionMap = new HashMap<>();
for(Map.Entry<String, FunctionTemplate> entry : functions.entrySet()) {
functionMap.put(entry.getKey(), UserDefinedFunction.newInstance(entry.getValue()));
}
samplers.forEach((id, sampler) -> functionMap.put(id,
sampler.getDimensions() == 2 ?
new NoiseFunction2(sampler.getSampler()) :
new NoiseFunction3(sampler.getSampler())));
return functionMap;
}
}
@@ -0,0 +1,27 @@
package com.dfsek.terra.addons.noise.samplers;
import com.dfsek.terra.api.noise.NoiseSampler;
public class TranslateSampler implements NoiseSampler {
private final NoiseSampler sampler;
private final double dx, dy, dz;
public TranslateSampler(NoiseSampler sampler, double dx, double dy, double dz) {
this.sampler = sampler;
this.dx = dx;
this.dy = dy;
this.dz = dz;
}
@Override
public double noise(long seed, double x, double y) {
return sampler.noise(seed, x - dx, y - dz);
}
@Override
public double noise(long seed, double x, double y, double z) {
return sampler.noise(seed, x - dx, y - dy, z - dz);
}
}
@@ -0,0 +1,66 @@
package com.dfsek.terra.addons.noise.samplers.noise;
public class DistanceSampler extends NoiseFunction {
private final DistanceFunction distanceFunction;
private final double ox, oy, oz;
private final boolean normalize;
private final double radius;
private final double distanceAtRadius;
public DistanceSampler(DistanceFunction distanceFunction, double ox, double oy, double oz, boolean normalize, double radius) {
frequency = 1;
this.distanceFunction = distanceFunction;
this.ox = ox;
this.oy = oy;
this.oz = oz;
this.normalize = normalize;
this.radius = radius;
this.distanceAtRadius = distance2d(distanceFunction, radius, 0); // distance2d and distance3d should return the same value
}
@Override
public double getNoiseRaw(long seed, double x, double y) {
double dx = x - ox;
double dy = y - oz;
if (normalize && (fastAbs(dx) > radius || fastAbs(dy) > radius)) return 1;
double dist = distance2d(distanceFunction, dx, dy);
if (normalize) return fastMin(((2*dist)/distanceAtRadius)-1, 1);
return dist;
}
@Override
public double getNoiseRaw(long seed, double x, double y, double z) {
double dx = x - ox;
double dy = y - oy;
double dz = z - oz;
if(normalize && (fastAbs(dx) > radius || fastAbs(dy) > radius || fastAbs(dz) > radius)) return 1;
double dist = distance3d(distanceFunction, dx, dy, dz);
if (normalize) return fastMin(((2*dist)/distanceAtRadius)-1, 1);
return dist;
}
private static double distance2d(DistanceFunction distanceFunction, double x, double z) {
return switch(distanceFunction) {
case Euclidean -> fastSqrt(x*x + z*z);
case EuclideanSq -> x*x + z*z;
case Manhattan -> fastAbs(x) + fastAbs(z);
};
}
private static double distance3d(DistanceFunction distanceFunction, double x, double y, double z) {
return switch(distanceFunction) {
case Euclidean -> fastSqrt(x*x + y*y + z*z);
case EuclideanSq -> x*x + y*y + z*z;
case Manhattan -> fastAbs(x) + fastAbs(y) + fastAbs(z);
};
}
public enum DistanceFunction {
Euclidean,
EuclideanSq,
Manhattan
}
}
@@ -0,0 +1,8 @@
version = version("1.0.0")
dependencies {
compileOnlyApi(project(":common:addons:manifest-addon-loader"))
implementation("net.jafama", "jafama", Versions.Libraries.Internal.jafama)
testImplementation("net.jafama", "jafama", Versions.Libraries.Internal.jafama)
}
@@ -0,0 +1,84 @@
package com.dfsek.terra.addons.image;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import java.util.function.Supplier;
import com.dfsek.terra.addons.image.config.ColorLoader;
import com.dfsek.terra.addons.image.config.ColorLoader.ColorString;
import com.dfsek.terra.addons.image.config.noisesampler.ChannelNoiseSamplerTemplate;
import com.dfsek.terra.addons.image.config.noisesampler.DistanceTransformNoiseSamplerTemplate;
import com.dfsek.terra.addons.image.config.image.ImageTemplate;
import com.dfsek.terra.addons.image.config.image.StitchedImageTemplate;
import com.dfsek.terra.addons.image.config.colorsampler.ConstantColorSamplerTemplate;
import com.dfsek.terra.addons.image.config.colorsampler.image.SingleImageColorSamplerTemplate;
import com.dfsek.terra.addons.image.config.colorsampler.image.TileImageColorSamplerTemplate;
import com.dfsek.terra.addons.image.config.colorsampler.mutate.RotateColorSamplerTemplate;
import com.dfsek.terra.addons.image.config.colorsampler.mutate.TranslateColorSamplerTemplate;
import com.dfsek.terra.addons.image.image.Image;
import com.dfsek.terra.addons.image.operator.DistanceTransform;
import com.dfsek.terra.addons.image.colorsampler.ColorSampler;
import com.dfsek.terra.addons.manifest.api.AddonInitializer;
import com.dfsek.terra.api.Platform;
import com.dfsek.terra.api.addon.BaseAddon;
import com.dfsek.terra.api.config.ConfigPack;
import com.dfsek.terra.api.event.events.config.pack.ConfigPackPreLoadEvent;
import com.dfsek.terra.api.event.functional.FunctionalEventHandler;
import com.dfsek.terra.api.inject.annotations.Inject;
import com.dfsek.terra.api.noise.NoiseSampler;
import com.dfsek.terra.api.registry.CheckedRegistry;
import com.dfsek.terra.api.util.reflection.TypeKey;
public class ImageLibraryAddon implements AddonInitializer {
public static final TypeKey<Supplier<ObjectTemplate<Image>>> IMAGE_REGISTRY_KEY = new TypeKey<>() {
};
public static final TypeKey<Supplier<ObjectTemplate<ColorSampler>>> COLOR_PICKER_REGISTRY_KEY = new TypeKey<>() {
};
public static final TypeKey<Supplier<ObjectTemplate<NoiseSampler>>> NOISE_SAMPLER_TOKEN = new TypeKey<>() {
};
@Inject
private Platform platform;
@Inject
private BaseAddon addon;
@Override
public void initialize() {
platform.getEventManager()
.getHandler(FunctionalEventHandler.class)
.register(addon, ConfigPackPreLoadEvent.class)
.priority(10)
.then(event -> {
ConfigPack pack = event.getPack();
CheckedRegistry<Supplier<ObjectTemplate<Image>>> imageRegistry = pack.getOrCreateRegistry(IMAGE_REGISTRY_KEY);
imageRegistry.register(addon.key("BITMAP"), () -> new ImageTemplate(pack.getLoader(), pack));
imageRegistry.register(addon.key("STITCHED_BITMAP"), () -> new StitchedImageTemplate(pack.getLoader(), pack));
})
.then(event -> {
event.getPack()
.applyLoader(DistanceTransform.CostFunction.class,
(type, o, loader, depthTracker) -> DistanceTransform.CostFunction.valueOf((String) o))
.applyLoader(DistanceTransform.Normalization.class,
(type, o, loader, depthTracker) -> DistanceTransform.Normalization.valueOf((String) o))
.applyLoader(ColorString.class, new ColorLoader());
CheckedRegistry<Supplier<ObjectTemplate<NoiseSampler>>> noiseRegistry = event.getPack().getOrCreateRegistry(
NOISE_SAMPLER_TOKEN);
noiseRegistry.register(addon.key("DISTANCE_TRANSFORM"), DistanceTransformNoiseSamplerTemplate::new);
noiseRegistry.register(addon.key("CHANNEL"), ChannelNoiseSamplerTemplate::new);
})
.then(event -> {
CheckedRegistry<Supplier<ObjectTemplate<ColorSampler>>> colorSamplerRegistry = event.getPack().getOrCreateRegistry(
COLOR_PICKER_REGISTRY_KEY);
colorSamplerRegistry.register(addon.key("SINGLE_IMAGE"), SingleImageColorSamplerTemplate::new);
colorSamplerRegistry.register(addon.key("TILED_IMAGE"), TileImageColorSamplerTemplate::new);
colorSamplerRegistry.register(addon.key("COLOR"), ConstantColorSamplerTemplate::new);
colorSamplerRegistry.register(addon.key("ROTATE"), RotateColorSamplerTemplate::new);
colorSamplerRegistry.register(addon.key("TRANSLATE"), TranslateColorSamplerTemplate::new);
});
}
}
@@ -0,0 +1,12 @@
package com.dfsek.terra.addons.image.colorsampler;
@FunctionalInterface
public interface ColorSampler {
/**
* @param x World x coordinate
* @param z World z coordinate
* @return Integer representing a web color
*/
int apply(int x, int z);
}
@@ -0,0 +1,29 @@
package com.dfsek.terra.addons.image.colorsampler.image;
import com.dfsek.terra.addons.image.image.Image;
import com.dfsek.terra.addons.image.colorsampler.ColorSampler;
import com.dfsek.terra.addons.image.colorsampler.image.transform.ImageTransformation;
public class SingleImageColorSampler implements ColorSampler {
private final Image image;
private final ColorSampler fallback;
private final ImageTransformation transformation;
public SingleImageColorSampler(Image image, ColorSampler fallback, ImageTransformation transformation) {
this.image = image;
this.fallback = fallback;
this.transformation = transformation;
}
@Override
public int apply(int x, int z) {
var nx = transformation.transformX(image, x);
var nz = transformation.transformZ(image, z);
if(nx < 0 || nz < 0 || nx >= image.getWidth() || nz >= image.getHeight()) return fallback.apply(x, z);
return image.getRGB(nx, nz);
}
}
@@ -0,0 +1,27 @@
package com.dfsek.terra.addons.image.colorsampler.image;
import net.jafama.FastMath;
import com.dfsek.terra.addons.image.image.Image;
import com.dfsek.terra.addons.image.colorsampler.ColorSampler;
import com.dfsek.terra.addons.image.colorsampler.image.transform.ImageTransformation;
public class TileImageColorSampler implements ColorSampler {
private final Image image;
private final ImageTransformation transformation;
public TileImageColorSampler(Image image, ImageTransformation transformation) {
this.image = image;
this.transformation = transformation;
}
@Override
public int apply(int x, int z) {
x = transformation.transformX(image, x);
z = transformation.transformZ(image, z);
return image.getRGB(FastMath.floorMod(x, image.getWidth()), FastMath.floorMod(z, image.getHeight()));
}
}
@@ -0,0 +1,30 @@
package com.dfsek.terra.addons.image.colorsampler.image.transform;
import com.dfsek.terra.addons.image.image.Image;
public enum Alignment implements ImageTransformation {
NONE() {
@Override
public int transformX(Image image, int x) {
return x;
}
@Override
public int transformZ(Image image, int z) {
return z;
}
},
CENTER {
@Override
public int transformX(Image image, int x) {
return x + image.getWidth() / 2;
}
@Override
public int transformZ(Image image, int z) {
return z + image.getHeight() / 2;
}
};
}
@@ -0,0 +1,11 @@
package com.dfsek.terra.addons.image.colorsampler.image.transform;
import com.dfsek.terra.addons.image.image.Image;
public interface ImageTransformation {
int transformX(Image image, int x);
int transformZ(Image image, int z);
}
@@ -0,0 +1,61 @@
package com.dfsek.terra.addons.image.colorsampler.mutate;
import net.jafama.FastMath;
import com.dfsek.terra.addons.image.colorsampler.ColorSampler;
public class RotateColorSampler implements ColorSampler {
private final ColorSampler sampler;
private final double radians;
private final RotationMethod rotationMethod;
public RotateColorSampler(ColorSampler sampler, double degrees) {
this.sampler = sampler;
double normalizedDegrees = degrees % 360.0;
if (normalizedDegrees < 0) normalizedDegrees += 360.0;
if (normalizedDegrees == 0.0)
rotationMethod = RotationMethod.DEG_0;
else if (normalizedDegrees == 90.0)
rotationMethod = RotationMethod.DEG_90;
else if (normalizedDegrees == 180.0)
rotationMethod = RotationMethod.DEG_180;
else if (normalizedDegrees == 270.0)
rotationMethod = RotationMethod.DEG_270;
else
rotationMethod = RotationMethod.RAD_ANY;
this.radians = FastMath.toRadians(degrees);
}
@Override
public int apply(int x, int z) {
int rx = switch(rotationMethod) {
case DEG_0 -> x;
case DEG_90 -> -z;
case DEG_180 -> -x;
case DEG_270 -> z;
case RAD_ANY -> (int) (x * FastMath.cos(radians) - z * FastMath.sin(radians));
};
int rz = switch(rotationMethod) {
case DEG_0 -> z;
case DEG_90 -> x;
case DEG_180 -> -z;
case DEG_270 -> -x;
case RAD_ANY -> (int) (z * FastMath.cos(radians) + x * FastMath.sin(radians));
};
return sampler.apply(rx, rz);
}
private enum RotationMethod {
DEG_0,
DEG_90,
DEG_180,
DEG_270,
RAD_ANY,
}
}
@@ -0,0 +1,21 @@
package com.dfsek.terra.addons.image.colorsampler.mutate;
import com.dfsek.terra.addons.image.colorsampler.ColorSampler;
public class TranslateColorSampler implements ColorSampler {
private final ColorSampler sampler;
private final int translateX, translateZ;
public TranslateColorSampler(ColorSampler sampler, int translateX, int translateZ) {
this.sampler = sampler;
this.translateX = translateX;
this.translateZ = translateZ;
}
@Override
public int apply(int x, int z) {
return sampler.apply(x - translateX, z - translateZ);
}
}
@@ -0,0 +1,92 @@
package com.dfsek.terra.addons.image.config;
import com.dfsek.tectonic.api.depth.DepthTracker;
import com.dfsek.tectonic.api.exception.LoadException;
import com.dfsek.tectonic.api.loader.ConfigLoader;
import com.dfsek.tectonic.api.loader.type.TypeLoader;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.AnnotatedType;
import com.dfsek.terra.addons.image.config.ColorLoader.ColorString;
import com.dfsek.terra.addons.image.util.ColorUtil;
public class ColorLoader implements TypeLoader<ColorString> {
@Override
public ColorString load(@NotNull AnnotatedType annotatedType, @NotNull Object o, @NotNull ConfigLoader configLoader,
DepthTracker depthTracker) throws LoadException {
return new ColorString((String) o);
}
public static class ColorString {
private final int argb;
public ColorString(String string) throws IllegalArgumentException {
this.argb = parse(string);
}
public int getColor() {
return argb;
}
private static int parse(String string) throws IllegalArgumentException {
if (string.length() == 0)
throw new IllegalArgumentException("Empty string cannot be parsed as a valid color");
String[] split = string.split(",");
if (split.length == 1)
return parseHex(string);
else if (split.length == 3)
return parseChannels("255", split[0], split[1], split[2]);
else if (split.length == 4)
return parseChannels(split[0], split[1], split[2], split[3]);
else
throw new IllegalArgumentException("Invalid channels provided, required format RED,GREEN,BLUE or ALPHA,RED,GREEN,BLUE");
}
private static int parseHex(String hex) throws IllegalArgumentException {
if (hex.startsWith("#"))
hex = hex.substring(1);
int alpha = 255;
int red = 0;
int green = 0;
int blue = 0;
try {
if(hex.length() == 8) {
alpha = Integer.parseInt(hex.substring(0, 2), 16);
hex = hex.substring(2);
}
if(hex.length() != 6)
throw new IllegalArgumentException("Invalid color channels, required format AARRGGBB or RRGGBB");
red = Integer.parseInt(hex.substring(0, 2), 16);
green = Integer.parseInt(hex.substring(2, 4), 16);
blue = Integer.parseInt(hex.substring(4, 6), 16);
return ColorUtil.argbValidated(alpha, red, green, blue);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Failed to parse hex color", e);
}
}
private static int parseChannels(String alpha, String red, String green, String blue) throws IllegalArgumentException {
try {
int a = Integer.decode(alpha);
int r = Integer.decode(red);
int g = Integer.decode(green);
int b = Integer.decode(blue);
return ColorUtil.argbValidated(a, r, g, b);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid channel value", e);
}
}
}
}
@@ -0,0 +1,19 @@
package com.dfsek.terra.addons.image.config.colorsampler;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import com.dfsek.terra.addons.image.colorsampler.ColorSampler;
import com.dfsek.terra.addons.image.config.ColorLoader.ColorString;
public class ConstantColorSamplerTemplate implements ObjectTemplate<ColorSampler> {
@Value("color")
private ColorString color;
@Override
public ColorSampler get() {
return ((x, z) -> color.getColor());
}
}
@@ -0,0 +1,21 @@
package com.dfsek.terra.addons.image.config.colorsampler.image;
import com.dfsek.tectonic.api.config.template.annotations.Default;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import com.dfsek.terra.addons.image.image.Image;
import com.dfsek.terra.addons.image.colorsampler.ColorSampler;
import com.dfsek.terra.addons.image.colorsampler.image.transform.Alignment;
public abstract class ImageColorSamplerTemplate implements ObjectTemplate<ColorSampler> {
@Value("image")
protected Image image;
@Value("align")
@Default
protected Alignment alignment = Alignment.NONE;
}
@@ -0,0 +1,17 @@
package com.dfsek.terra.addons.image.config.colorsampler.image;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.terra.addons.image.colorsampler.ColorSampler;
import com.dfsek.terra.addons.image.colorsampler.image.SingleImageColorSampler;
public class SingleImageColorSamplerTemplate extends ImageColorSamplerTemplate {
@Value("outside-sampler")
private ColorSampler fallback;
@Override
public ColorSampler get() {
return new SingleImageColorSampler(image, fallback, alignment);
}
}
@@ -0,0 +1,13 @@
package com.dfsek.terra.addons.image.config.colorsampler.image;
import com.dfsek.terra.addons.image.colorsampler.ColorSampler;
import com.dfsek.terra.addons.image.colorsampler.image.TileImageColorSampler;
public class TileImageColorSamplerTemplate extends ImageColorSamplerTemplate {
@Override
public ColorSampler get() {
return new TileImageColorSampler(image, alignment);
}
}
@@ -0,0 +1,13 @@
package com.dfsek.terra.addons.image.config.colorsampler.mutate;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import com.dfsek.terra.addons.image.colorsampler.ColorSampler;
public abstract class MutateColorSamplerTemplate implements ObjectTemplate<ColorSampler> {
@Value("color-sampler")
protected ColorSampler sampler;
}
@@ -0,0 +1,18 @@
package com.dfsek.terra.addons.image.config.colorsampler.mutate;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.terra.addons.image.colorsampler.ColorSampler;
import com.dfsek.terra.addons.image.colorsampler.mutate.RotateColorSampler;
public class RotateColorSamplerTemplate extends MutateColorSamplerTemplate {
@Value("angle")
private double degrees;
@Override
public ColorSampler get() {
return new RotateColorSampler(sampler, degrees);
}
}
@@ -0,0 +1,21 @@
package com.dfsek.terra.addons.image.config.colorsampler.mutate;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.terra.addons.image.colorsampler.ColorSampler;
import com.dfsek.terra.addons.image.colorsampler.mutate.TranslateColorSampler;
public class TranslateColorSamplerTemplate extends MutateColorSamplerTemplate {
@Value("x")
private int translateX;
@Value("z")
private int translateZ;
@Override
public ColorSampler get() {
return new TranslateColorSampler(sampler, translateX, translateZ);
}
}
@@ -0,0 +1,16 @@
package com.dfsek.terra.addons.image.config.converter;
import com.dfsek.terra.addons.image.converter.ClosestMatchColorConverter;
import com.dfsek.terra.addons.image.converter.ColorConverter;
import com.dfsek.terra.addons.image.converter.mapping.ColorMapping;
public abstract class ClosestColorConverterTemplate<T> implements ColorConverterTemplate<T> {
protected abstract ColorMapping<T> getMapping();
@Override
public ColorConverter<T> get() {
return new ClosestMatchColorConverter<T>(getMapping().get());
}
}
@@ -0,0 +1,9 @@
package com.dfsek.terra.addons.image.config.converter;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import com.dfsek.terra.addons.image.converter.ColorConverter;
public interface ColorConverterTemplate<T> extends ObjectTemplate<ColorConverter<T>> {
}
@@ -0,0 +1,20 @@
package com.dfsek.terra.addons.image.config.converter;
import com.dfsek.terra.addons.image.converter.ColorConverter;
import com.dfsek.terra.addons.image.converter.ExactColorConverter;
import com.dfsek.terra.addons.image.converter.mapping.ColorMapping;
public abstract class ExactColorConverterTemplate<T> implements ColorConverterTemplate<T> {
protected abstract ColorMapping<T> getMapping();
protected abstract T getFallback();
protected abstract boolean ignoreAlpha();
@Override
public ColorConverter<T> get() {
return new ExactColorConverter<T>(getMapping().get(), getFallback(), ignoreAlpha());
}
}
@@ -0,0 +1,46 @@
package com.dfsek.terra.addons.image.config.image;
import javax.imageio.ImageIO;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import com.dfsek.terra.addons.image.image.BufferedImageWrapper;
import com.dfsek.terra.addons.image.image.Image;
import com.dfsek.terra.api.config.ConfigPack;
import com.dfsek.terra.api.config.Loader;
import com.dfsek.terra.api.properties.Properties;
/*
* Cache prevents configs from loading the same image multiple times into memory
*/
record ImageCache(ConcurrentHashMap<String, Image> map) implements Properties {
public static Image load(String path, ConfigPack pack, Loader files) throws IOException {
ImageCache cache;
if(!pack.getContext().has(ImageCache.class)) {
cache = new ImageCache(new ConcurrentHashMap<>());
pack.getContext().put(cache);
} else {
cache = pack.getContext().get(ImageCache.class);
}
if(cache.map.containsKey(path)) {
return cache.map.get(path);
} else {
try {
BufferedImageWrapper image = new BufferedImageWrapper(ImageIO.read(files.get(path)));
cache.map.put(path, image);
return image;
} catch(IllegalArgumentException e) {
throw new IllegalArgumentException("Unable to load image (image might be too large?)", e);
} catch(IOException e) {
if(e instanceof FileNotFoundException) {
// Rethrow using nicer message
throw new IOException("Unable to load image: No such file or directory: " + path, e);
}
throw new IOException("Unable to load image", e);
}
}
}
}
@@ -0,0 +1,35 @@
package com.dfsek.terra.addons.image.config.image;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import java.io.IOException;
import com.dfsek.terra.addons.image.image.Image;
import com.dfsek.terra.api.config.ConfigPack;
import com.dfsek.terra.api.config.Loader;
public class ImageTemplate implements ObjectTemplate<Image> {
@Value("path")
private String path;
private final Loader files;
private final ConfigPack pack;
public ImageTemplate(Loader files, ConfigPack pack) {
this.files = files;
this.pack = pack;
}
@Override
public Image get() {
try {
return ImageCache.load(path, pack, files);
} catch(IOException e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,76 @@
package com.dfsek.terra.addons.image.config.image;
import com.dfsek.tectonic.api.config.template.ValidatedConfigTemplate;
import com.dfsek.tectonic.api.config.template.annotations.Default;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import com.dfsek.tectonic.api.exception.ValidationException;
import java.io.IOException;
import com.dfsek.terra.addons.image.image.Image;
import com.dfsek.terra.addons.image.image.StitchedImage;
import com.dfsek.terra.api.config.ConfigPack;
import com.dfsek.terra.api.config.Loader;
public class StitchedImageTemplate implements ObjectTemplate<Image>, ValidatedConfigTemplate {
@Value("path-format")
private String path;
@Value("rows")
private int rows;
@Value("columns")
private int cols;
@Value("zero-indexed")
@Default
private boolean zeroIndexed = false;
private final Loader files;
private final ConfigPack pack;
public StitchedImageTemplate(Loader files, ConfigPack pack) {
this.files = files;
this.pack = pack;
}
@Override
public Image get() {
Image[][] grid = new Image[rows][cols];
for(int i = 0; i < rows; i++) {
for(int j = 0; j < cols; j++) {
try {
grid[i][j] = ImageCache.load(getFormattedPath(i, j), pack, files);
} catch(IOException e) {
throw new RuntimeException(e);
}
}
}
return new StitchedImage(grid, zeroIndexed);
}
private String getFormattedPath(int row, int column) {
if (!zeroIndexed) {
row++;
column++;
}
return path.replaceFirst("\\{row}", String.valueOf(row)).replaceFirst("\\{column}", String.valueOf(column));
}
@Override
public boolean validate() throws ValidationException {
if(!path.contains("{row}"))
throw new ValidationException("Path format does not contain sequence '{row}'");
if(!path.contains("{column}"))
throw new ValidationException("Path format does not contain sequence '{column}'");
if(rows < 1)
throw new ValidationException("Must have at least one row");
if(cols < 1)
throw new ValidationException("Must have at least one column");
return true;
}
}
@@ -0,0 +1,41 @@
package com.dfsek.terra.addons.image.config.noisesampler;
import com.dfsek.tectonic.api.config.template.annotations.Default;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import com.dfsek.terra.addons.image.colorsampler.ColorSampler;
import com.dfsek.terra.addons.image.noisesampler.ChannelNoiseSampler;
import com.dfsek.terra.addons.image.util.ColorUtil.Channel;
import com.dfsek.terra.api.noise.NoiseSampler;
public class ChannelNoiseSamplerTemplate implements ObjectTemplate<NoiseSampler> {
@Value("color-sampler")
private ColorSampler colorSampler;
@Value("channel")
private Channel channel;
/*
* If the channel should be normalized to range [-1, 1] or not
*/
@Value("normalize")
@Default
private boolean normalize = true;
/*
* Whether to multiply color channels by the alpha channel or not. If users
* are expecting pixel transparency to reduce the output value then this should
* be set to true.
*/
@Value("premultiply")
@Default
private boolean premultiply = false;
@Override
public NoiseSampler get() {
return new ChannelNoiseSampler(colorSampler, channel, normalize, premultiply);
}
}
@@ -0,0 +1,74 @@
package com.dfsek.terra.addons.image.config.noisesampler;
import com.dfsek.tectonic.api.config.template.annotations.Default;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import com.dfsek.terra.addons.image.image.Image;
import com.dfsek.terra.addons.image.operator.DistanceTransform;
import com.dfsek.terra.addons.image.operator.DistanceTransform.CostFunction;
import com.dfsek.terra.addons.image.operator.DistanceTransform.Normalization;
import com.dfsek.terra.addons.image.util.ColorUtil.Channel;
import com.dfsek.terra.api.noise.NoiseSampler;
public class DistanceTransformNoiseSamplerTemplate implements ObjectTemplate<NoiseSampler> {
@Value("image")
private Image image;
/**
* The threshold value applied to the channel specified in the 'channel' parameter that splits
* the image into a binary image. This parameter is only used for cost functions that utilize
* a binary image.
*/
@Value("threshold")
@Default
private int threshold = 127;
/**
* If set to true, distances calculated will be clamped to stay above the largest
* distance calculated on the edges of the image. This ensures output values do not
* appear to be cut off at the image boundaries. It is recommended to leave padding
* around the image if this is in use, such that larger evaluated distances do not
* get cut out by smaller evaluated distances close to borders. Doing so will yield
* better results.
*/
@Value("clamp-to-max-edge")
@Default
private boolean clampToEdge = false;
/**
* The target channel to run distance calculations on.
*/
@Value("channel")
@Default
private Channel channel = Channel.GRAYSCALE;
/**
* The method of image processing applied to the specified image prior to calculating
* distances.
*/
@Value("cost-function")
@Default
private CostFunction costFunction = CostFunction.Channel;
/**
* Inverts the resulting binary image that may be used as a cost function.
*/
@Value("invert-threshold")
@Default
private boolean invertThreshold = false;
/**
* How the final distance calculation should be redistributed.
*/
@Value("normalization")
@Default
private Normalization normalization = Normalization.None;
@Override
public NoiseSampler get() {
return new DistanceTransform.Noise(new DistanceTransform(image, channel, threshold, clampToEdge, costFunction, invertThreshold), normalization);
}
}
@@ -0,0 +1,40 @@
package com.dfsek.terra.addons.image.converter;
import java.util.Map;
import com.dfsek.terra.addons.image.util.ColorUtil;
public class ClosestMatchColorConverter<T> implements ColorConverter<T> {
private final Map<Integer, T> map;
private final Integer[] colors;
public ClosestMatchColorConverter(Map<Integer, T> map) {
this.map = map;
this.colors = map.keySet().toArray(new Integer[0]);
}
@Override
public T apply(int color) {
int closest = 0;
int smallestDistance = Integer.MAX_VALUE;
for(int compare : colors) {
if(color == compare) {
closest = compare;
break;
}
int distance = ColorUtil.distance(color, compare);
if(distance < smallestDistance) {
smallestDistance = distance;
closest = compare;
}
}
return map.get(closest);
}
@Override
public Iterable<T> getEntries() {
return map.values();
}
}
@@ -0,0 +1,8 @@
package com.dfsek.terra.addons.image.converter;
public interface ColorConverter<T> {
T apply(int color);
Iterable<T> getEntries();
}
@@ -0,0 +1,42 @@
package com.dfsek.terra.addons.image.converter;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import com.dfsek.terra.addons.image.util.ColorUtil;
import com.dfsek.terra.addons.image.util.MapUtil;
public class ExactColorConverter<T> implements ColorConverter<T> {
private final Map<Integer, T> map;
private final T fallback;
private final boolean ignoreAlpha;
public ExactColorConverter(Map<Integer, T> map, T fallback, boolean ignoreAlpha) {
if (ignoreAlpha) {
map = MapUtil.mapKeys(map, ColorUtil::zeroAlpha);
}
this.map = map;
this.fallback = fallback;
this.ignoreAlpha = ignoreAlpha;
}
@Override
public T apply(int color) {
if (ignoreAlpha) {
color = ColorUtil.zeroAlpha(color);
}
T lookup = map.get(color);
return lookup != null ? lookup : fallback;
}
@Override
public Iterable<T> getEntries() {
Set<T> entries = new HashSet<>(map.values());
entries.add(fallback);
return entries;
}
}
@@ -0,0 +1,38 @@
package com.dfsek.terra.addons.image.converter.mapping;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;
import com.dfsek.terra.api.registry.Registry;
import com.dfsek.terra.api.world.biome.Biome;
public class BiomeDefinedColorMapping<T> implements ColorMapping<T> {
Registry<Biome> biomeRegistry;
Function<Biome, T> converter;
public BiomeDefinedColorMapping(Registry<Biome> biomeRegistry, Function<Biome, T> converter) {
this.biomeRegistry = biomeRegistry;
this.converter = converter;
}
@Override
public Map<Integer, T> get() {
Map<Biome, Integer> colorMap = new HashSet<>(biomeRegistry.entries()).stream().collect(Collectors.toMap(b -> b, Biome::getColor));
Map<Integer, Biome> output = new HashMap<>();
colorMap.forEach(((biome, color) -> {
if(!output.containsKey(color)) {
output.put(color, biome);
} else {
throw new IllegalArgumentException(String.format("Biome %s has same color as %s: %x", biome.getID(), output.get(color).getID(), color));
}
}));
return output.entrySet().stream().collect(Collectors.toMap(Entry::getKey, e -> converter.apply(e.getValue())));
}
}
@@ -0,0 +1,8 @@
package com.dfsek.terra.addons.image.converter.mapping;
import java.util.Map;
import java.util.function.Supplier;
public interface ColorMapping<T> extends Supplier<Map<Integer, T>> {
}
@@ -0,0 +1,28 @@
package com.dfsek.terra.addons.image.image;
import java.awt.image.BufferedImage;
public class BufferedImageWrapper implements Image {
private final BufferedImage image;
public BufferedImageWrapper(BufferedImage image) {
this.image = image;
}
@Override
public int getRGB(int x, int y) {
return image.getRGB(x, y);
}
@Override
public int getWidth() {
return image.getWidth();
}
@Override
public int getHeight() {
return image.getHeight();
}
}
@@ -0,0 +1,9 @@
package com.dfsek.terra.addons.image.image;
public interface Image {
int getRGB(int x, int y);
int getWidth();
int getHeight();
}
@@ -0,0 +1,74 @@
package com.dfsek.terra.addons.image.image;
public class StitchedImage implements Image {
private final Image[][] images;
private final int[] rowOffsets, columnOffsets;
private final int width, height;
public StitchedImage(Image[][] images, boolean zeroIndexed) throws IllegalArgumentException {
int width = 0;
int height = 0;
int rows = images.length;
int columns = images[0].length;
this.rowOffsets = new int[rows];
this.columnOffsets = new int[columns];
for(int i = 0; i < rows; i++) {
int rowHeight = images[i][0].getHeight();
rowOffsets[i] = height;
height += rowHeight;
for(int j = 1; j < columns; j++) {
if(images[i][j].getHeight() != rowHeight)
throw new IllegalArgumentException("Image heights in row " + (i + (zeroIndexed ? 0 : 1)) + " do not match");
}
}
for(int i = 0; i < columns; i++) {
int columnWidth = images[0][i].getWidth();
columnOffsets[i] = width;
width += columnWidth;
for(int j = 1; j < rows; j++) {
if(images[i][j].getWidth() != columnWidth)
throw new IllegalArgumentException("Image widths in column " + (i + (zeroIndexed ? 0 : 1)) + " do not match");
}
}
this.width = width;
this.height = height;
this.images = images;
}
private int getColumn(int x) {
for(int i = columnOffsets.length-1; i > 0; i--) {
if(x >= columnOffsets[i])
return i;
}
return 0;
}
private int getRow(int y) {
for(int i = rowOffsets.length-1; i > 0; i--) {
if(y >= rowOffsets[i])
return i;
}
return 0;
}
@Override
public int getRGB(int x, int y) {
int row = getRow(y);
int column = getColumn(x);
return images[row][column].getRGB(x-columnOffsets[column], y-rowOffsets[row]);
}
@Override
public int getWidth() {
return width;
}
@Override
public int getHeight() {
return height;
}
}
@@ -0,0 +1,40 @@
package com.dfsek.terra.addons.image.noisesampler;
import com.dfsek.terra.addons.image.colorsampler.ColorSampler;
import com.dfsek.terra.addons.image.util.ColorUtil;
import com.dfsek.terra.addons.image.util.ColorUtil.Channel;
import com.dfsek.terra.api.noise.NoiseSampler;
import static com.dfsek.terra.addons.image.util.MathUtil.lerp;
public class ChannelNoiseSampler implements NoiseSampler {
private final ColorSampler colorSampler;
private final Channel channel;
private final boolean normalize;
private final boolean premultiply;
public ChannelNoiseSampler(ColorSampler colorSampler, Channel channel, boolean normalize, boolean premultiply) {
this.colorSampler = colorSampler;
this.channel = channel;
this.normalize = normalize;
this.premultiply = premultiply;
}
@Override
public double noise(long seed, double x, double y) {
int sample = colorSampler.apply((int) x, (int) y);
int premultiplied = premultiply ? ColorUtil.premultiply(sample) : sample;
double channelValue = channel.from(premultiplied);
return normalize ? lerp(channelValue, 0, -1, 255, 1) : channelValue;
}
@Override
public double noise(long seed, double x, double y, double z) {
return noise(seed, x, z);
}
}
@@ -0,0 +1,242 @@
package com.dfsek.terra.addons.image.operator;
import net.jafama.FastMath;
import com.dfsek.terra.addons.image.image.Image;
import com.dfsek.terra.addons.image.util.ColorUtil;
import com.dfsek.terra.addons.image.util.ColorUtil.Channel;
import com.dfsek.terra.api.noise.NoiseSampler;
import static com.dfsek.terra.addons.image.util.MathUtil.lerp;
/**
* Computes a 2D distance transform of a given image and stores the result in a 2D array of distances.
* Implementation based on the algorithm described in the paper
* <a href="https://cs.brown.edu/people/pfelzens/papers/dt-final.pdf">Distance Transforms of Sampled Functions</a>
* by Pedro F. Felzenszwalb and Daniel P. Huttenlocher.
*/
public class DistanceTransform {
private final double[][] distances;
/**
* Size bounds matching the provided image.
*/
private final int width, height;
/**
* Min and max distances of the distance computation. These may change after {@link #normalize(Normalization)} calls.
*/
private double minDistance, maxDistance;
private static final double MAX_DISTANCE_CAP = 10_000_000; // Arbitrarily large value, doubtful someone would
// ever use an image large enough to exceed this.
public DistanceTransform(Image image, Channel channel, int threshold, boolean clampToMaxEdgeDistance, CostFunction costFunction, boolean invertThreshold) {
// Construct binary image based on threshold value
boolean[][] binaryImage = new boolean[image.getWidth()][image.getHeight()];
for(int x = 0; x < image.getWidth(); x++) {
for(int y = 0; y < image.getHeight(); y++) {
binaryImage[x][y] = ColorUtil.getChannel(image.getRGB(x, y), channel) > threshold ^ invertThreshold;
}
}
// Get edges of binary image
boolean[][] binaryImageEdge = new boolean[image.getWidth()][image.getHeight()];
for(int x = 0; x < image.getWidth(); x++) {
for(int y = 0; y < image.getHeight(); y++) {
if(!binaryImage[x][y])
binaryImageEdge[x][y] = false;
else
// If cell borders any false cell
binaryImageEdge[x][y] = x > 0 && !binaryImage[x-1][y] ||
y > 0 && !binaryImage[x][y-1] ||
x < image.getWidth ()-1 && !binaryImage[x+1][y] ||
y < image.getHeight()-1 && !binaryImage[x][y+1];
}
}
double[][] function = new double[image.getWidth()][image.getHeight()];
for(int x = 0; x < image.getWidth(); x++) {
for(int y = 0; y < image.getHeight(); y++) {
function[x][y] = switch (costFunction) {
case Channel -> ColorUtil.getChannel(image.getRGB(x, y), channel);
case Threshold -> binaryImage[x][y] ? MAX_DISTANCE_CAP : 0;
case ThresholdEdge, ThresholdEdgeSigned -> binaryImageEdge[x][y] ? 0 : MAX_DISTANCE_CAP;
};
}
}
distances = calculateDistance2D(function);
if(costFunction == CostFunction.ThresholdEdgeSigned) {
for(int x = 0; x < image.getWidth(); x++) {
for(int y = 0; y < image.getHeight(); y++) {
distances[x][y] *= binaryImage[x][y] ? 1 : -1;
}
}
}
if(clampToMaxEdgeDistance) {
// Find largest value on the edge of the image
double max = Double.NEGATIVE_INFINITY;
for(int x = 0; x < image.getWidth(); x++) {
max = Math.max(max, distances[x][0]);
max = Math.max(max, distances[x][image.getHeight()-1]);
}
for(int y = 0; y < image.getHeight(); y++) {
max = Math.max(max, distances[0][y]);
max = Math.max(max, distances[image.getWidth()-1][y]);
}
// Clamp to that largest value
for(int x = 0; x < image.getWidth(); x++) {
for(int y = 0; y < image.getHeight(); y++) {
distances[x][y] = Math.max(max, distances[x][y]);
}
}
}
this.width = image.getWidth();
this.height = image.getHeight();
setOutputRange();
}
private double[][] calculateDistance2D(double[][] f) {
double[][] d = new double[f.length][f[0].length];
// Distance pass for each column
for(int x = 0; x < f.length; x++) {
d[x] = calculateDistance1D(f[x]);
}
// Distance pass for each row
double[] row = new double[f.length];
for(int y = 0; y < f[0].length; y++) {
for(int x = 0; x < f[0].length; x++)
row[x] = d[x][y];
row = calculateDistance1D(row);
for(int x = 0; x < f[0].length; x++) {
d[x][y] = FastMath.sqrt(row[x]);
}
}
return d;
}
private double[] calculateDistance1D(double[] f) {
double[] d = new double[f.length];
int[] v = new int[f.length];
double[] z = new double[f.length+1];
int k = 0;
v[0] = 0;
z[0] = Integer.MIN_VALUE;
z[1] = Integer.MAX_VALUE;
for(int q = 1; q <= f.length-1; q++) {
double s = ((f[q]+FastMath.pow2(q))-(f[v[k]]+FastMath.pow2(v[k])))/(2*q-2*v[k]);
while (s <= z[k]) {
k--;
s = ((f[q]+FastMath.pow2(q))-(f[v[k]]+FastMath.pow2(v[k])))/(2*q-2*v[k]);
}
k++;
v[k] = q;
z[k] = s;
z[k+1] = Integer.MAX_VALUE;
}
k = 0;
for(int q = 0; q <= f.length-1; q++) {
while(z[k+1] < q)
k++;
d[q] = FastMath.pow2(q-v[k]) + f[v[k]];
}
return d;
}
/**
* Redistributes the stored distance computation according to the provided {@link Normalization} method.
*/
private void normalize(Normalization normalization) {
for(int x = 0; x < width; x++) {
for(int y = 0; y < height; y++) {
double d = distances[x][y];
distances[x][y] = switch(normalization) {
case None -> distances[x][y];
case Linear -> lerp(d, minDistance, -1, maxDistance, 1);
case SmoothPreserveZero -> {
if(minDistance > 0 || maxDistance < 0) {
// Can't preserve zero if it is not contained in range so just lerp
yield lerp(distances[x][y], minDistance, -1, maxDistance, 1);
} else {
if(d > 0) {
yield FastMath.pow2(d/maxDistance);
} else if(d < 0) {
yield -FastMath.pow2(d/minDistance);
} else {
yield 0;
}
}
}
};
}
}
setOutputRange();
}
private void setOutputRange() {
double minDistance = Double.POSITIVE_INFINITY;
double maxDistance = Double.NEGATIVE_INFINITY;
for(int x = 0; x < width; x++) {
for(int y = 0; y < height; y++) {
minDistance = Math.min(minDistance, distances[x][y]);
maxDistance = Math.max(maxDistance, distances[x][y]);
}
}
this.minDistance = minDistance;
this.maxDistance = maxDistance;
}
public enum CostFunction {
Channel,
Threshold,
ThresholdEdge,
ThresholdEdgeSigned,
}
public enum Normalization {
/**
* Return the raw calculated distances.
*/
None,
/**
* Redistribute the output values to fit in the range [-1, 1]
*/
Linear,
/**
* Redistributes smoothly to the range [-1, 1], such that areas where distance = 0 stay 0.
* This is only really applicable to signed distance calculations, and will fall back to linear
* redistribution if the input range does not contain both positive and negative values.
*/
SmoothPreserveZero,
}
public static class Noise implements NoiseSampler {
private final DistanceTransform transform;
public Noise(DistanceTransform transform, Normalization normalization) {
this.transform = transform;
transform.normalize(normalization);
}
@Override
public double noise(long seed, double x, double y) {
if(x<0 || y<0 || x>=transform.width || y>=transform.height) return transform.minDistance;
return transform.distances[FastMath.floorToInt(x)][FastMath.floorToInt(y)];
}
@Override
public double noise(long seed, double x, double y, double z) {
return noise(seed, x, z);
}
}
}
@@ -0,0 +1,322 @@
package com.dfsek.terra.addons.image.util;
import net.jafama.FastMath;
/**
* Utility class for manipulating 8 bit ARGB colors
*/
public class ColorUtil {
private ColorUtil() {}
public static int distance(int a, int b) {
return FastMath.abs(getRed(a) - getRed(b)) +
FastMath.abs(getGreen(a) - getGreen(b)) +
FastMath.abs(getBlue(a) - getBlue(b));
}
/**
* Returns the red channel value of a given ARGB color value.
*
* @param argb the ARGB color value to extract the red channel value from
* @return the red channel value of the given ARGB color value, in the range 0-255
*/
public static int getRed(int argb) {
return argb >> 16 & 255;
}
/**
* Returns the green channel value of a given ARGB color value.
*
* @param argb the ARGB color value to extract the green channel value from
* @return the green channel value of the given ARGB color value, in the range 0-255
*/
public static int getGreen(int argb) {
return argb >> 8 & 255;
}
/**
* Returns the blue channel value of a given ARGB color value.
*
* @param argb the ARGB color value to extract the blue channel value from
* @return the blue channel value of the given ARGB color value, in the range 0-255
*/
public static int getBlue(int argb) {
return argb & 255;
}
/**
* Returns the alpha channel value of a given ARGB color value.
*
* @param argb the ARGB color value to extract the blue channel value from
* @return the alpha channel value of the given ARGB color value, in the range 0-255
*/
public static int getAlpha(int argb) {
return argb >> 24 & 255;
}
/**
* Returns the grayscale value of a given ARGB color value.
*
* @param argb the ARGB color value to convert to grayscale
* @return the grayscale value of the given ARGB color value, in the range 0-255
*/
public static int getGrayscale(int argb) {
return (getRed(argb) + getGreen(argb) + getBlue(argb)) / 3;
}
/**
* Returns the value of the specified channel for a given ARGB color value.
*
* @param argb the ARGB color value to extract the channel value from
* @param channel the channel to extract the value from
* @return the value of the specified channel for the given ARGB color value, in the range 0-255
*/
public static int getChannel(int argb, Channel channel) {
return channel.from(argb);
}
/**
* Sets the red channel value of a given ARGB color value to zero.
*
* @param argb the ARGB color value to zero the red channel of
* @return the resulting ARGB color value with the red channel set to zero
*/
public static int zeroRed(int argb) {
return argb & ~0x00FF0000;
}
/**
* Sets the green channel value of a given ARGB color value to zero.
*
* @param argb the ARGB color value to zero the green channel of
* @return the resulting ARGB color value with the green channel set to zero
*/
public static int zeroGreen(int argb) {
return argb & ~0x0000FF00;
}
/**
* Sets the blue channel value of a given ARGB color value to zero.
*
* @param argb the ARGB color value to zero the blue channel of
* @return the resulting ARGB color value with the blue channel set to zero
*/
public static int zeroBlue(int argb) {
return argb & ~0x000000FF;
}
/**
* Sets the alpha channel value of a given ARGB color value to zero.
* This is the same as setting the color to fully transparent.
*
* @param argb the ARGB color value to zero the alpha channel of
* @return the resulting ARGB color value with the alpha channel set to zero
*/
public static int zeroAlpha(int argb) {
return argb & ~0xFF000000;
}
/**
* Sets the color channels of a given ARGB color value to zero.
* This is the same as setting the color to black, while preserving the alpha.
*
* @param argb the ARGB color value to zero the color channel of
* @return the resulting ARGB color value with the color channels set to zero
*/
public static int zeroGrayscale(int argb) {
return argb & ~0x00FFFFFF;
}
/**
* Sets the specified channel value of a given ARGB color value to zero.
*
* @param argb the ARGB color value to zero the specified channel of
* @param channel the channel to zero the value of
* @return the resulting ARGB color value with the specified channel value set to zero
*/
public static int zeroChannel(int argb, Channel channel) {
return channel.zero(argb);
}
/**
* Multiply the RGB channels of a given ARGB color value by its alpha channel value.
*
* @param argb the ARGB color value to premultiply the RGB channels of
* @return the resulting premultiplied ARGB color value
*/
public static int premultiply(int argb) {
int alpha = getAlpha(argb);
int red = (getRed(argb) * alpha + 127) / 255;
int green = (getGreen(argb) * alpha + 127) / 255;
int blue = (getBlue(argb) * alpha + 127) / 255;
return argb(alpha, red, green, blue);
}
/**
* Returns an ARGB color value with the specified values for alpha, red, green, and blue channels.
*
* @param alpha the alpha value, between 0 and 255, to set in the ARGB color value
* @param red the red value, between 0 and 255, to set in the ARGB color value
* @param green the green value, between 0 and 255, to set in the ARGB color value
* @param blue the blue value, between 0 and 255, to set in the ARGB color value
* @return the resulting ARGB color value with the specified values for alpha, red, green, and blue channels
*/
public static int argb(int alpha, int red, int green, int blue) {
return argbAlpha(alpha) | argbRed(red) | argbGreen(green) | argbBlue(blue);
}
/**
* Returns an ARGB color value with the specified values for alpha, red, green, and blue channels,
* after validating that each channel value is in the range 0-255.
*
* @param alpha the alpha value, between 0 and 255, to set in the ARGB color value
* @param red the red value, between 0 and 255, to set in the ARGB color value
* @param green the green value, between 0 and 255, to set in the ARGB color value
* @param blue the blue value, between 0 and 255, to set in the ARGB color value
* @return the resulting ARGB color value with the specified values for alpha, red, green, and blue channels
* @throws IllegalArgumentException if any channel value is outside the range 0-255
*/
public static int argbValidated(int alpha, int red, int green, int blue) throws IllegalArgumentException {
if (alpha < 0 || alpha > 255 ||
red < 0 || red > 255 ||
green < 0 || green > 255 ||
blue < 0 || blue > 255
) throw new IllegalArgumentException("Channel values must be in range 0-255");
return argb(alpha, red, green, blue);
}
/**
* Returns the ARGB color value with the specified alpha channel value and zero
* for the red, green, and blue channels.
*
* @param alpha the alpha channel value to set in the ARGB color value
* @return the resulting ARGB color value
*/
public static int argbAlpha(int alpha) { return alpha << 24; }
/**
* Returns the ARGB color value with the specified red channel value and zero
* for the alpha, green, and blue channels.
*
* @param red the red channel value to set in the ARGB color value
* @return the resulting ARGB color value
*/
public static int argbRed(int red) { return red << 16; }
/**
* Returns the ARGB color value with the specified red channel value and zero
* for the alpha, red, and blue channels.
*
* @param green the green channel value to set in the ARGB color value
* @return the resulting ARGB color value
*/
public static int argbGreen(int green) { return green << 8; }
/**
* Returns the ARGB color value with the specified blue channel value and zero
* for the alpha, red, and green channels.
*
* @param blue the blue channel value to set in the ARGB color value
* @return the resulting ARGB color value
*/
public static int argbBlue(int blue) { return blue; }
/**
* Returns an ARGB color value with the specified grayscale value for all four channels.
*
* @param value the grayscale value to set in all four channels of the ARGB color value
* @return the resulting ARGB color value with the specified grayscale value for all four channels
*/
public static int argbGrayscale(int value) { return argb(value, value, value, value); }
public enum Channel {
RED {
@Override
public int from(int argb) {
return getRed(argb);
}
@Override
public int zero(int argb) {
return zeroRed(argb);
}
@Override
public int argb(int value) {
return argbRed(value);
}
},
GREEN {
@Override
public int from(int argb) {
return getGreen(argb);
}
@Override
public int zero(int argb) {
return zeroGreen(argb);
}
@Override
public int argb(int value) {
return argbGreen(value);
}
},
BLUE {
@Override
public int from(int argb) {
return getBlue(argb);
}
@Override
public int zero(int argb) {
return zeroBlue(argb);
}
@Override
public int argb(int value) {
return argbBlue(value);
}
},
GRAYSCALE {
@Override
public int from(int argb) {
return getGrayscale(argb);
}
@Override
public int zero(int argb) {
return zeroGrayscale(argb);
}
@Override
public int argb(int value) {
return argbAlpha(value);
}
},
ALPHA {
@Override
public int from(int argb) {
return getAlpha(argb);
}
@Override
public int zero(int argb) {
return zeroAlpha(argb);
}
@Override
public int argb(int value) {
return argbAlpha(value);
}
};
public abstract int from(int argb);
public abstract int zero(int argb);
public abstract int argb(int value);
}
}
@@ -0,0 +1,25 @@
package com.dfsek.terra.addons.image.util;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;
public class MapUtil {
private MapUtil() {}
/**
* Utility method for applying transformations on a map's keys.
*/
public static <O, N, T> Map<N, T> mapKeys(Map<O, T> map, Function<O, N> mappingFunction) {
return map
.entrySet()
.stream()
.collect(Collectors.toMap(
e -> mappingFunction.apply(e.getKey()),
Entry::getValue
));
}
}
@@ -0,0 +1,9 @@
package com.dfsek.terra.addons.image.util;
public class MathUtil {
private MathUtil() {}
public static double lerp(double x, double x1, double y1, double x2, double y2) {
return (((y1-y2)*(x-x1))/(x1-x2))+y1;
}
}
@@ -0,0 +1,12 @@
schema-version: 1
contributors:
- Terra contributors
id: library-image
version: @VERSION@
entrypoints:
- "com.dfsek.terra.addons.image.ImageLibraryAddon"
website:
issues: https://github.com/PolyhedralDev/Terra/issues
source: https://github.com/PolyhedralDev/Terra
docs: https://terra.polydev.org
license: MIT License
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-2023 Polyhedral Development
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,7 @@
version = version("1.0.0")
dependencies {
compileOnlyApi(project(":common:addons:manifest-addon-loader"))
compileOnlyApi(project(":common:addons:biome-provider-pipeline-v2"))
compileOnlyApi(project(":common:addons:library-image"))
}
@@ -0,0 +1,29 @@
package com.dfsek.terra.addons.biome.pipeline.image;
import com.dfsek.terra.addons.biome.pipeline.v2.api.Source;
import com.dfsek.terra.addons.biome.pipeline.v2.api.biome.PipelineBiome;
import com.dfsek.terra.addons.image.colorsampler.ColorSampler;
import com.dfsek.terra.addons.image.converter.ColorConverter;
public class ImageSource implements Source {
private final ColorSampler colorSampler;
private final ColorConverter<PipelineBiome> colorConverter;
public ImageSource(ColorSampler colorSampler, ColorConverter<PipelineBiome> colorConverter) {
this.colorSampler = colorSampler;
this.colorConverter = colorConverter;
}
@Override
public PipelineBiome get(long seed, int x, int z) {
return colorConverter.apply(colorSampler.apply(x, z));
}
@Override
public Iterable<PipelineBiome> getBiomes() {
return colorConverter.getEntries();
}
}
@@ -0,0 +1,68 @@
package com.dfsek.terra.addons.biome.pipeline.image;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import java.util.function.Supplier;
import com.dfsek.terra.addons.biome.pipeline.image.config.ImageSourceTemplate;
import com.dfsek.terra.addons.biome.pipeline.image.config.converter.ClosestPipelineBiomeColorConverterTemplate;
import com.dfsek.terra.addons.biome.pipeline.image.config.converter.ExactPipelineBiomeColorConverterTemplate;
import com.dfsek.terra.addons.biome.pipeline.image.config.converter.mapping.DefinedPipelineBiomeColorMappingTemplate;
import com.dfsek.terra.addons.biome.pipeline.v2.BiomePipelineAddon;
import com.dfsek.terra.addons.biome.pipeline.v2.api.Source;
import com.dfsek.terra.addons.biome.pipeline.v2.api.biome.DelegatedPipelineBiome;
import com.dfsek.terra.addons.biome.pipeline.v2.api.biome.PipelineBiome;
import com.dfsek.terra.addons.image.converter.ColorConverter;
import com.dfsek.terra.addons.image.converter.mapping.BiomeDefinedColorMapping;
import com.dfsek.terra.addons.image.converter.mapping.ColorMapping;
import com.dfsek.terra.addons.manifest.api.AddonInitializer;
import com.dfsek.terra.api.Platform;
import com.dfsek.terra.api.addon.BaseAddon;
import com.dfsek.terra.api.event.events.config.pack.ConfigPackPreLoadEvent;
import com.dfsek.terra.api.event.functional.FunctionalEventHandler;
import com.dfsek.terra.api.inject.annotations.Inject;
import com.dfsek.terra.api.registry.CheckedRegistry;
import com.dfsek.terra.api.util.reflection.TypeKey;
import com.dfsek.terra.api.world.biome.Biome;
public class PipelineImageAddon implements AddonInitializer {
public static final TypeKey<Supplier<ObjectTemplate<ColorConverter<PipelineBiome>>>> PIPELINE_BIOME_COLOR_CONVERTER_REGISTRY_KEY = new TypeKey<>() {
};
public static final TypeKey<Supplier<ObjectTemplate<ColorMapping<PipelineBiome>>>> PIPELINE_BIOME_COLOR_MAPPING_REGISTRY_KEY = new TypeKey<>() {
};
@Inject
private Platform platform;
@Inject
private BaseAddon addon;
@Override
public void initialize() {
platform.getEventManager()
.getHandler(FunctionalEventHandler.class)
.register(addon, ConfigPackPreLoadEvent.class)
.priority(500)
.then(event -> {
CheckedRegistry<Supplier<ObjectTemplate<ColorConverter<PipelineBiome>>>> biomeColorConverterRegistry = event.getPack().getOrCreateRegistry(
PIPELINE_BIOME_COLOR_CONVERTER_REGISTRY_KEY);
biomeColorConverterRegistry.register(addon.key("EXACT"), ExactPipelineBiomeColorConverterTemplate::new);
biomeColorConverterRegistry.register(addon.key("CLOSEST"), ClosestPipelineBiomeColorConverterTemplate::new);
})
.then(event -> {
CheckedRegistry<Supplier<ObjectTemplate<Source>>> sourceRegistry = event.getPack().getOrCreateRegistry(BiomePipelineAddon.SOURCE_REGISTRY_KEY);
sourceRegistry.register(addon.key("IMAGE"), ImageSourceTemplate::new);
})
.then(event -> {
CheckedRegistry<Supplier<ObjectTemplate<ColorMapping<PipelineBiome>>>> biomeColorMappingRegistry = event.getPack().getOrCreateRegistry(
PIPELINE_BIOME_COLOR_MAPPING_REGISTRY_KEY);
biomeColorMappingRegistry.register(addon.key("USE_BIOME_COLORS"), () -> () -> new BiomeDefinedColorMapping<>(event.getPack().getRegistry(Biome.class), DelegatedPipelineBiome::new));
biomeColorMappingRegistry.register(addon.key("MAP"), DefinedPipelineBiomeColorMappingTemplate::new);
})
.failThrough();
}
}
@@ -0,0 +1,25 @@
package com.dfsek.terra.addons.biome.pipeline.image.config;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import com.dfsek.terra.addons.biome.pipeline.image.ImageSource;
import com.dfsek.terra.addons.biome.pipeline.v2.api.Source;
import com.dfsek.terra.addons.biome.pipeline.v2.api.biome.PipelineBiome;
import com.dfsek.terra.addons.image.colorsampler.ColorSampler;
import com.dfsek.terra.addons.image.converter.ColorConverter;
public class ImageSourceTemplate implements ObjectTemplate<Source> {
@Value("color-sampler")
private ColorSampler colorSampler;
@Value("color-conversion")
private ColorConverter<PipelineBiome> colorConverter;
@Override
public Source get() {
return new ImageSource(colorSampler, colorConverter);
}
}
@@ -0,0 +1,19 @@
package com.dfsek.terra.addons.biome.pipeline.image.config.converter;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.terra.addons.biome.pipeline.v2.api.biome.PipelineBiome;
import com.dfsek.terra.addons.image.config.converter.ClosestColorConverterTemplate;
import com.dfsek.terra.addons.image.converter.mapping.ColorMapping;
public class ClosestPipelineBiomeColorConverterTemplate extends ClosestColorConverterTemplate<PipelineBiome> {
@Value("match")
private ColorMapping<PipelineBiome> match;
@Override
protected ColorMapping<PipelineBiome> getMapping() {
return match;
}
}
@@ -0,0 +1,37 @@
package com.dfsek.terra.addons.biome.pipeline.image.config.converter;
import com.dfsek.tectonic.api.config.template.annotations.Default;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.terra.addons.biome.pipeline.v2.api.biome.PipelineBiome;
import com.dfsek.terra.addons.image.config.converter.ExactColorConverterTemplate;
import com.dfsek.terra.addons.image.converter.mapping.ColorMapping;
public class ExactPipelineBiomeColorConverterTemplate extends ExactColorConverterTemplate<PipelineBiome> {
@Value("match")
private ColorMapping<PipelineBiome> match;
@Value("else")
private PipelineBiome fallback;
@Value("ignore-alpha")
@Default
private boolean ignoreAlpha = true;
@Override
protected ColorMapping<PipelineBiome> getMapping() {
return match;
}
@Override
protected PipelineBiome getFallback() {
return fallback;
}
@Override
protected boolean ignoreAlpha() {
return ignoreAlpha;
}
}
@@ -0,0 +1,24 @@
package com.dfsek.terra.addons.biome.pipeline.image.config.converter.mapping;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import java.util.Map;
import com.dfsek.terra.addons.biome.pipeline.v2.api.biome.PipelineBiome;
import com.dfsek.terra.addons.image.config.ColorLoader.ColorString;
import com.dfsek.terra.addons.image.converter.mapping.ColorMapping;
import com.dfsek.terra.addons.image.util.MapUtil;
public class DefinedPipelineBiomeColorMappingTemplate implements ObjectTemplate<ColorMapping<PipelineBiome>> {
@Value("map")
Map<ColorString, PipelineBiome> map;
@Override
public ColorMapping<PipelineBiome> get() {
var map = MapUtil.mapKeys(this.map, ColorString::getColor);
return () -> map;
}
}
@@ -0,0 +1,15 @@
schema-version: 1
contributors:
- Terra contributors
id: pipeline-image
version: @VERSION@
entrypoints:
- "com.dfsek.terra.addons.biome.pipeline.image.PipelineImageAddon"
website:
issues: https://github.com/PolyhedralDev/Terra/issues
source: https://github.com/PolyhedralDev/Terra
docs: https://terra.polydev.org
license: MIT License
depends:
library-image: "1.+"
biome-provider-pipeline-v2: "1.+"
@@ -30,6 +30,7 @@ import com.dfsek.terra.api.inject.annotations.Inject;
import com.dfsek.terra.api.registry.CheckedRegistry;
import com.dfsek.terra.api.structure.Structure;
import com.dfsek.terra.api.util.StringUtil;
import com.dfsek.terra.api.util.vector.Vector3Int;
public class SpongeSchematicAddon implements AddonInitializer {
@@ -71,9 +72,36 @@ public class SpongeSchematicAddon implements AddonInitializer {
public SpongeStructure convert(InputStream in, String id) {
try {
CompoundTag baseTag = (CompoundTag) new NBTDeserializer(false).fromStream(detectDecompression(in)).getTag();
int ver = baseTag.getInt("Version");
int wid = baseTag.getShort("Width");
int len = baseTag.getShort("Length");
int hei = baseTag.getShort("Height");
CompoundTag metadata = baseTag.getCompoundTag("Metadata");
Vector3Int offset = switch(ver) {
case 2 -> {
// Use WorldEdit defined legacy relative offset if it exists in schematic metadata
IntTag worldEditOffsetX = metadata.getIntTag("WEOffsetX");
IntTag worldEditOffsetY = metadata.getIntTag("WEOffsetY");
IntTag worldEditOffsetZ = metadata.getIntTag("WEOffsetZ");
if(worldEditOffsetX != null || worldEditOffsetY != null || worldEditOffsetZ != null) {
if(worldEditOffsetX == null || worldEditOffsetY == null || worldEditOffsetZ == null) {
throw new IllegalArgumentException("Failed to parse Sponge schematic: Malformed WorldEdit offset");
}
yield Vector3Int.of(worldEditOffsetX.asInt(), worldEditOffsetY.asInt(), worldEditOffsetZ.asInt());
} else {
// Relative offset handling via 'Offset' field is ambiguous in spec 2 so just apply no offset
yield Vector3Int.zero();
}
}
case 3 -> {
// Relative offset is more concretely defined in spec 3 to use 'Offset' field
int[] offsetArray = baseTag.getIntArray("Offset");
yield Vector3Int.of(offsetArray[0], offsetArray[1], offsetArray[2]);
}
default -> throw new IllegalArgumentException("Failed to parse Sponge schematic: Unsupported format version: " + ver);
};
ByteArrayTag blocks = baseTag.getByteArrayTag("BlockData");
@@ -97,7 +125,7 @@ public class SpongeSchematicAddon implements AddonInitializer {
}
}
return new SpongeStructure(states, addon.key(id));
return new SpongeStructure(states, offset, addon.key(id));
} catch(IOException e) {
throw new IllegalArgumentException("Failed to parse Sponge schematic: ", e);
}
@@ -23,10 +23,15 @@ public class SpongeStructure implements Structure, Keyed<SpongeStructure> {
private final BlockState[][][] blocks;
private final int offsetX, offsetY, offsetZ;
private final RegistryKey id;
public SpongeStructure(BlockState[][][] blocks, RegistryKey id) {
public SpongeStructure(BlockState[][][] blocks, Vector3Int offset, RegistryKey id) {
this.blocks = blocks;
this.offsetX = offset.getX();
this.offsetY = offset.getY();
this.offsetZ = offset.getZ();
this.id = id;
}
@@ -37,13 +42,15 @@ public class SpongeStructure implements Structure, Keyed<SpongeStructure> {
int bZ = location.getZ();
for(int x = 0; x < blocks.length; x++) {
for(int z = 0; z < blocks[x].length; z++) {
Vector2Int r = Vector2Int.of(x, z).rotate(rotation);
int oX = x + offsetX;
int oZ = z + offsetZ;
Vector2Int r = Vector2Int.of(oX, oZ).rotate(rotation);
int rX = r.getX();
int rZ = r.getZ();
for(int y = 0; y < blocks[x][z].length; y++) {
BlockState state = blocks[x][z][y];
if(state == null) continue;
world.setBlockState(bX + rX, bY + y, bZ + rZ, state);
world.setBlockState(bX + rX, bY + y + offsetY, bZ + rZ, state);
}
}
}
@@ -5,18 +5,19 @@
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.terrascript.tokenizer;
package com.dfsek.terra.addons.terrascript.lexer;
import java.util.Objects;
public class Char {
private final char character;
private final int index;
private final int line;
private final SourcePosition position;
public Char(char character, int index, int line) {
public Char(char character, SourcePosition position) {
this.character = character;
this.index = index;
this.line = line;
this.position = position;
}
public boolean is(char... tests) {
@@ -33,18 +34,23 @@ public class Char {
return Character.toString(character);
}
@Override
public boolean equals(Object o) {
if(this == o) return true;
if(o == null || getClass() != o.getClass()) return false;
Char other = (Char) o;
return character == other.character && Objects.equals(position, other.position);
}
@Override
public int hashCode() {
return Objects.hash(character, position);
}
public char getCharacter() {
return character;
}
public int getIndex() {
return index;
}
public int getLine() {
return line;
}
public boolean isWhitespace() {
return Character.isWhitespace(character);
}
@@ -0,0 +1,256 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.terrascript.lexer;
import com.google.common.collect.Sets;
import java.util.Arrays;
import java.util.Set;
import java.util.Stack;
import com.dfsek.terra.addons.terrascript.lexer.Token.TokenType;
import com.dfsek.terra.addons.terrascript.lexer.exceptions.EOFException;
import com.dfsek.terra.addons.terrascript.lexer.exceptions.FormatException;
import com.dfsek.terra.addons.terrascript.lexer.exceptions.TokenizerException;
import com.dfsek.terra.addons.terrascript.parser.exceptions.ParseException;
public class Lexer {
public static final Set<Character> syntaxSignificant = Sets.newHashSet(';', '(', ')', '"', ',', '\\', '=', '{', '}', '+', '-', '*', '/',
'>', '<', '!'); // Reserved chars
private final LookaheadStream reader;
private final Stack<Token> bracketStack = new Stack<>();
private Token current;
public Lexer(String data) {
reader = new LookaheadStream(data + '\0');
current = tokenize();
}
/**
* Get the first token.
*
* @return First token
*
* @throws ParseException If token does not exist
*/
public Token current() {
return current;
}
/**
* Consume (get and remove) the first token.
*
* @return First token
*
* @throws ParseException If token does not exist
*/
public Token consume(String wrongTypeMessage, TokenType expected, TokenType... more) {
if(!current.isType(expected) && Arrays.stream(more).noneMatch(t -> t == current.getType())) throw new ParseException(wrongTypeMessage, current.getPosition());
return consumeUnchecked();
}
public Token consumeUnchecked() {
if(current.getType() == TokenType.END_OF_FILE) return current;
Token temp = current;
current = tokenize();
return temp;
}
/**
* Whether this {@code Tokenizer} contains additional tokens.
*
* @return {@code true} if more tokens are present, otherwise {@code false}
*/
public boolean hasNext() {
return current.getType() != TokenType.END_OF_FILE;
}
private Token tokenize() throws TokenizerException {
consumeWhitespace();
SourcePosition position = reader.getPosition();
// Skip line if comment
while(reader.matchesString("//", true)) skipLine();
// Skip multi line comment
if(reader.matchesString("/*", true)) skipTo("*/");
// Reached end of file
if(reader.current().isEOF()) {
if(!bracketStack.isEmpty()) throw new ParseException("Dangling open brace", bracketStack.peek().getPosition());
return new Token(reader.consume().toString(), TokenType.END_OF_FILE, position);
}
// Check if operator token
if(reader.matchesString("==", true))
return new Token("==", TokenType.EQUALS_EQUALS, position);
if(reader.matchesString("!=", true))
return new Token("!=", TokenType.BANG_EQUALS, position);
if(reader.matchesString(">=", true))
return new Token(">=", TokenType.GREATER_EQUAL, position);
if(reader.matchesString("<=", true))
return new Token("<=", TokenType.LESS_EQUALS, position);
if(reader.matchesString(">", true))
return new Token(">", TokenType.GREATER, position);
if(reader.matchesString("<", true))
return new Token("<", TokenType.LESS, position);
// Check if logical operator
if(reader.matchesString("||", true))
return new Token("||", TokenType.BOOLEAN_OR, position);
if(reader.matchesString("&&", true))
return new Token("&&", TokenType.BOOLEAN_AND, position);
// Check if number
if(isNumberStart()) {
StringBuilder num = new StringBuilder();
while(!reader.current().isEOF() && isNumberLike()) {
num.append(reader.consume().getCharacter());
}
return new Token(num.toString(), TokenType.NUMBER, position);
}
// Check if string literal
if(reader.current().is('"')) {
reader.consume(); // Consume first quote
StringBuilder string = new StringBuilder();
boolean ignoreNext = false;
while((!reader.current().is('"')) || ignoreNext) {
if(reader.current().is('\\') && !ignoreNext) {
ignoreNext = true;
reader.consume();
continue;
} else ignoreNext = false;
if(reader.current().isEOF())
throw new FormatException("No end of string literal found. ", position);
string.append(reader.consume());
}
reader.consume(); // Consume last quote
return new Token(string.toString(), TokenType.STRING, position);
}
if(reader.current().is('('))
return new Token(reader.consume().toString(), TokenType.OPEN_PAREN, position);
if(reader.current().is(')'))
return new Token(reader.consume().toString(), TokenType.CLOSE_PAREN, position);
if(reader.current().is(';'))
return new Token(reader.consume().toString(), TokenType.STATEMENT_END, position);
if(reader.current().is(','))
return new Token(reader.consume().toString(), TokenType.SEPARATOR, position);
if(reader.current().is('{')) {
Token token = new Token(reader.consume().toString(), TokenType.BLOCK_BEGIN, position);
bracketStack.push(token);
return token;
}
if(reader.current().is('}')) {
if(bracketStack.isEmpty()) throw new ParseException("Dangling close brace", position);
bracketStack.pop();
return new Token(reader.consume().toString(), TokenType.BLOCK_END, position);
}
if(reader.current().is('='))
return new Token(reader.consume().toString(), TokenType.ASSIGNMENT, position);
if(reader.current().is('+'))
return new Token(reader.consume().toString(), TokenType.PLUS, position);
if(reader.current().is('-'))
return new Token(reader.consume().toString(), TokenType.MINUS,
position);
if(reader.current().is('*'))
return new Token(reader.consume().toString(), TokenType.STAR,
position);
if(reader.current().is('/'))
return new Token(reader.consume().toString(), TokenType.FORWARD_SLASH, position);
if(reader.current().is('%'))
return new Token(reader.consume().toString(), TokenType.MODULO_OPERATOR, position);
if(reader.current().is('!'))
return new Token(reader.consume().toString(), TokenType.BANG, position);
// Read word
StringBuilder token = new StringBuilder();
while(!reader.current().isEOF() && !isSyntaxSignificant(reader.current().getCharacter())) {
Char c = reader.consume();
if(c.isWhitespace()) break;
token.append(c.getCharacter());
}
String tokenString = token.toString();
// Check if word is a keyword
if(tokenString.equals("true"))
return new Token(tokenString, TokenType.BOOLEAN, position);
if(tokenString.equals("false"))
return new Token(tokenString, TokenType.BOOLEAN, position);
if(tokenString.equals("num"))
return new Token(tokenString, TokenType.TYPE_NUMBER, position);
if(tokenString.equals("str"))
return new Token(tokenString, TokenType.TYPE_STRING, position);
if(tokenString.equals("bool"))
return new Token(tokenString, TokenType.TYPE_BOOLEAN, position);
if(tokenString.equals("void"))
return new Token(tokenString, TokenType.TYPE_VOID, position);
if(tokenString.equals("if"))
return new Token(tokenString, TokenType.IF_STATEMENT, position);
if(tokenString.equals("else"))
return new Token(tokenString, TokenType.ELSE, position);
if(tokenString.equals("while"))
return new Token(tokenString, TokenType.WHILE_LOOP, position);
if(tokenString.equals("for"))
return new Token(tokenString, TokenType.FOR_LOOP, position);
if(tokenString.equals("return"))
return new Token(tokenString, TokenType.RETURN, position);
if(tokenString.equals("continue"))
return new Token(tokenString, TokenType.CONTINUE, position);
if(tokenString.equals("break"))
return new Token(tokenString, TokenType.BREAK, position);
if(tokenString.equals("fail"))
return new Token(tokenString, TokenType.FAIL, position);
// If not keyword, assume it is an identifier
return new Token(tokenString, TokenType.IDENTIFIER, position);
}
private void skipLine() {
while(!reader.current().isEOF() && !reader.current().isNewLine()) reader.consume();
consumeWhitespace();
}
private void consumeWhitespace() {
while(!reader.current().isEOF() && reader.current().isWhitespace()) reader.consume(); // Consume whitespace.
}
private void skipTo(String s) throws EOFException {
SourcePosition begin = reader.getPosition();
while(!reader.current().isEOF()) {
if(reader.matchesString(s, true)) {
consumeWhitespace();
return;
}
reader.consume();
}
throw new EOFException("No end of expression found.", begin);
}
private boolean isNumberLike() {
return reader.current().isDigit()
|| reader.current().is('_', '.', 'E');
}
private boolean isNumberStart() {
return reader.current().isDigit()
|| reader.current().is('.') && reader.peek().isDigit();
}
public boolean isSyntaxSignificant(char c) {
return syntaxSignificant.contains(c);
}
}
@@ -0,0 +1,84 @@
package com.dfsek.terra.addons.terrascript.lexer;
public class LookaheadStream {
private final String source;
private int index;
private SourcePosition position = new SourcePosition(1, 1);
public LookaheadStream(String source) {
this.source = source;
}
/**
* Get the current character without consuming it.
*
* @return current character
*/
public Char current() {
return new Char(source.charAt(index), position);
}
/**
* Consume and return one character.
*
* @return Character that was consumed.
*/
public Char consume() {
Char consumed = current();
incrementIndex(1);
return consumed;
}
/**
* @return The next character in sequence.
*/
public Char peek() {
int index = this.index + 1;
if (index + 1 >= source.length()) return null;
return new Char(source.charAt(index), getPositionAfter(1));
}
/**
* Determines if the contained sequence of characters matches the string
*
* @param check Input string to check against
* @param consumeIfMatched Whether to consume the string if there is a match
* @return If the string matches
*/
public boolean matchesString(String check, boolean consumeIfMatched) {
boolean matches = check.equals(source.substring(index, Math.min(index + check.length(), source.length())));
if (matches && consumeIfMatched) incrementIndex(check.length());
return matches;
}
/**
* @return Current position within the source file
*/
public SourcePosition getPosition() {
return position;
}
private void incrementIndex(int amount) {
position = getPositionAfter(amount);
index = Math.min(index + amount, source.length() - 1);
}
private SourcePosition getPositionAfter(int chars) {
if (chars < 0) throw new IllegalArgumentException("Negative values are not allowed");
int line = position.line();
int column = position.column();
for (int i = index; i < Math.min(index + chars, source.length() - 1); i++) {
if (source.charAt(i) == '\n') {
line++;
column = 0;
}
column++;
}
return new SourcePosition(line, column);
}
}
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.terrascript.lexer;
import java.util.Objects;
public record SourcePosition(int line, int column) {
@Override
public String toString() {
return "line " + line + ", column " + column;
}
@Override
public boolean equals(Object o) {
if(this == o) return true;
if(o == null || getClass() != o.getClass()) return false;
SourcePosition that = (SourcePosition) o;
return line == that.line && column == that.column;
}
@Override
public int hashCode() {
return Objects.hash(line, column);
}
}
@@ -5,14 +5,14 @@
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.terrascript.tokenizer;
package com.dfsek.terra.addons.terrascript.lexer;
public class Token {
private final String content;
private final Type type;
private final Position start;
private final TokenType type;
private final SourcePosition start;
public Token(String content, Type type, Position start) {
public Token(String content, TokenType type, SourcePosition start) {
this.content = content;
this.type = type;
this.start = start;
@@ -23,7 +23,7 @@ public class Token {
return type + ": '" + content + "'";
}
public Type getType() {
public TokenType getType() {
return type;
}
@@ -31,63 +31,68 @@ public class Token {
return content;
}
public Position getPosition() {
public SourcePosition getPosition() {
return start;
}
public boolean isConstant() {
return this.type.equals(Type.NUMBER) || this.type.equals(Type.STRING) || this.type.equals(Type.BOOLEAN);
return this.type.equals(TokenType.NUMBER) || this.type.equals(TokenType.STRING) || this.type.equals(TokenType.BOOLEAN);
}
public boolean isType(TokenType type) {
return type == getType();
}
public boolean isType(TokenType... types) {
for (TokenType t : types) if (isType(t)) return true;
return false;
}
public boolean isBinaryOperator() {
return type.equals(Type.ADDITION_OPERATOR)
|| type.equals(Type.SUBTRACTION_OPERATOR)
|| type.equals(Type.MULTIPLICATION_OPERATOR)
|| type.equals(Type.DIVISION_OPERATOR)
|| type.equals(Type.EQUALS_OPERATOR)
|| type.equals(Type.NOT_EQUALS_OPERATOR)
|| type.equals(Type.LESS_THAN_OPERATOR)
|| type.equals(Type.GREATER_THAN_OPERATOR)
|| type.equals(Type.LESS_THAN_OR_EQUALS_OPERATOR)
|| type.equals(Type.GREATER_THAN_OR_EQUALS_OPERATOR)
|| type.equals(Type.BOOLEAN_OR)
|| type.equals(Type.BOOLEAN_AND)
|| type.equals(Type.MODULO_OPERATOR);
return type.equals(TokenType.PLUS)
|| type.equals(TokenType.MINUS)
|| type.equals(TokenType.STAR)
|| type.equals(TokenType.FORWARD_SLASH)
|| type.equals(TokenType.EQUALS_EQUALS)
|| type.equals(TokenType.BANG_EQUALS)
|| type.equals(TokenType.LESS)
|| type.equals(TokenType.GREATER)
|| type.equals(TokenType.LESS_EQUALS)
|| type.equals(TokenType.GREATER_EQUAL)
|| type.equals(TokenType.BOOLEAN_OR)
|| type.equals(TokenType.BOOLEAN_AND)
|| type.equals(TokenType.MODULO_OPERATOR);
}
public boolean isStrictNumericOperator() {
return type.equals(Type.SUBTRACTION_OPERATOR)
|| type.equals(Type.MULTIPLICATION_OPERATOR)
|| type.equals(Type.DIVISION_OPERATOR)
|| type.equals(Type.GREATER_THAN_OPERATOR)
|| type.equals(Type.LESS_THAN_OPERATOR)
|| type.equals(Type.LESS_THAN_OR_EQUALS_OPERATOR)
|| type.equals(Type.GREATER_THAN_OR_EQUALS_OPERATOR)
|| type.equals(Type.MODULO_OPERATOR);
return type.equals(TokenType.MINUS)
|| type.equals(TokenType.STAR)
|| type.equals(TokenType.FORWARD_SLASH)
|| type.equals(TokenType.GREATER)
|| type.equals(TokenType.LESS)
|| type.equals(TokenType.LESS_EQUALS)
|| type.equals(TokenType.GREATER_EQUAL)
|| type.equals(TokenType.MODULO_OPERATOR);
}
public boolean isStrictBooleanOperator() {
return type.equals(Type.BOOLEAN_AND)
|| type.equals(Type.BOOLEAN_OR);
return type.equals(TokenType.BOOLEAN_AND)
|| type.equals(TokenType.BOOLEAN_OR);
}
public boolean isVariableDeclaration() {
return type.equals(Type.STRING_VARIABLE)
|| type.equals(Type.BOOLEAN_VARIABLE)
|| type.equals(Type.NUMBER_VARIABLE);
return type.equals(TokenType.TYPE_STRING)
|| type.equals(TokenType.TYPE_BOOLEAN)
|| type.equals(TokenType.TYPE_NUMBER);
}
public boolean isLoopLike() {
return type.equals(Type.IF_STATEMENT)
|| type.equals(Type.WHILE_LOOP)
|| type.equals(Type.FOR_LOOP);
public boolean isControlStructure() {
return type.equals(TokenType.IF_STATEMENT)
|| type.equals(TokenType.WHILE_LOOP)
|| type.equals(TokenType.FOR_LOOP);
}
public boolean isIdentifier() {
return type.equals(Type.IDENTIFIER);
}
public enum Type {
public enum TokenType {
/**
* Function identifier or language keyword
*/
@@ -108,11 +113,11 @@ public class Token {
/**
* Beginning of group
*/
GROUP_BEGIN,
OPEN_PAREN,
/**
* Ending of group
*/
GROUP_END,
CLOSE_PAREN,
/**
* End of statement
*/
@@ -136,43 +141,43 @@ public class Token {
/**
* Boolean equals operator
*/
EQUALS_OPERATOR,
EQUALS_EQUALS,
/**
* Boolean not equals operator
*/
NOT_EQUALS_OPERATOR,
BANG_EQUALS,
/**
* Boolean greater than operator
*/
GREATER_THAN_OPERATOR,
GREATER,
/**
* Boolean less than operator
*/
LESS_THAN_OPERATOR,
LESS,
/**
* Boolean greater than or equal to operator
*/
GREATER_THAN_OR_EQUALS_OPERATOR,
GREATER_EQUAL,
/**
* Boolean less than or equal to operator
*/
LESS_THAN_OR_EQUALS_OPERATOR,
LESS_EQUALS,
/**
* Addition/concatenation operator
*/
ADDITION_OPERATOR,
PLUS,
/**
* Subtraction operator
*/
SUBTRACTION_OPERATOR,
MINUS,
/**
* Multiplication operator
*/
MULTIPLICATION_OPERATOR,
STAR,
/**
* Division operator
*/
DIVISION_OPERATOR,
FORWARD_SLASH,
/**
* Modulo operator.
*/
@@ -180,7 +185,7 @@ public class Token {
/**
* Boolean not operator
*/
BOOLEAN_NOT,
BANG,
/**
* Boolean or
*/
@@ -192,15 +197,19 @@ public class Token {
/**
* Numeric variable declaration
*/
NUMBER_VARIABLE,
TYPE_NUMBER,
/**
* String variable declaration
*/
STRING_VARIABLE,
TYPE_STRING,
/**
* Boolean variable declaration
*/
BOOLEAN_VARIABLE,
TYPE_BOOLEAN,
/**
* Void type declaration
*/
TYPE_VOID,
/**
* If statement declaration
*/
@@ -232,6 +241,10 @@ public class Token {
/**
* Else keyword
*/
ELSE
ELSE,
/**
* End of file
*/
END_OF_FILE
}
}
@@ -5,11 +5,11 @@
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.terrascript.tokenizer.exceptions;
package com.dfsek.terra.addons.terrascript.lexer.exceptions;
import java.io.Serial;
import com.dfsek.terra.addons.terrascript.tokenizer.Position;
import com.dfsek.terra.addons.terrascript.lexer.SourcePosition;
public class EOFException extends TokenizerException {
@@ -17,11 +17,11 @@ public class EOFException extends TokenizerException {
@Serial
private static final long serialVersionUID = 3980047409902809440L;
public EOFException(String message, Position position) {
public EOFException(String message, SourcePosition position) {
super(message, position);
}
public EOFException(String message, Position position, Throwable cause) {
public EOFException(String message, SourcePosition position, Throwable cause) {
super(message, position, cause);
}
}
@@ -5,11 +5,11 @@
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.terrascript.tokenizer.exceptions;
package com.dfsek.terra.addons.terrascript.lexer.exceptions;
import java.io.Serial;
import com.dfsek.terra.addons.terrascript.tokenizer.Position;
import com.dfsek.terra.addons.terrascript.lexer.SourcePosition;
public class FormatException extends TokenizerException {
@@ -17,11 +17,11 @@ public class FormatException extends TokenizerException {
@Serial
private static final long serialVersionUID = -791308012940744455L;
public FormatException(String message, Position position) {
public FormatException(String message, SourcePosition position) {
super(message, position);
}
public FormatException(String message, Position position, Throwable cause) {
public FormatException(String message, SourcePosition position, Throwable cause) {
super(message, position, cause);
}
}

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