diff --git a/ADDITIONS.md b/ADDITIONS.md index 4b5da743..b6ac2a13 100644 --- a/ADDITIONS.md +++ b/ADDITIONS.md @@ -1,15 +1,23 @@ ## JSON additions +--------------- + ### Worldgen * Where applicable, Perlin noise now can have custom seed specified - * Change above allows to explicitly specify universe seed (as celestial.config:systemTypePerlin:seed) - * Perlin noise now can be of arbitrary scale (defaults to 512, specified with 'scale' key, integer type, >=16) + * Change above allows to explicitly specify universe seed (as `celestial.config:systemTypePerlin:seed`) + * Perlin noise now can be of arbitrary scale (defaults to `512`, specified with `scale` key, integer type, >=16) #### Terrain - * Nested terrain selectors now get their unique seeds (displacement selector can now properly be nested inside other displacement selector) - * Previously, all nested terrain selectors were based off the same seed - * displacement terrain selector has xClamp added, works like yClamp + * `mix` terrain selector got `mixSeedBias`, `aSeedBias` and `bSeedBias` fields, whose deviate respective selectors seeds (default to `0`) + * `displacement` terrain selector has `seedBias` added, which deviate seed of `source` selector (default to `0`) + * `displacement` terrain selector has `xClamp` added, works like `yClamp` + * `rotate` terrain selector has `rotationWidth` (defaults to `0.5`) and `rotationHeight` (defaults to `0.0`) added, which are multiplied by world's size and world's height respectively to determine rotation point center + * `min` terrain selector added, opposite of existing `max` (json format is the same as `max`) + * `cache` terrain selector removed due it not being documented, and having little practical value + * `perlin` terrain selector now accepts `type`, `frequency` and `amplitude` values (naming inconsistency fix) + * `ridgeblocks` terrain selector now accepts `amplitude` and `frequency` values (naming inconsistency fix); + * `ridgeblocks` has `octaves` added (defaults to `2`), `perlinOctaves` (defaults to `1`) #### Biomes * Tree biome placeables now have `variantsRange` (defaults to `[1, 1]`) and `subVariantsRange` (defaults to `[2, 2]`) @@ -19,6 +27,8 @@ * Also two more properties were added: `sameStemHueShift` (defaults to `true`) and `sameFoliageHueShift` (defaults to `false`), which fixate hue shifts within same "stem-foliage" combination * Original engine always generates two tree types when processing placeable items, new engine however, allows to generate any number of trees. +--------------- + ### player.config * Inventory bags are no longer limited to 255 slots * However, when joining original servers with mod which increase bag size past 255 slots will result in undefined behavior (joining servers with inventory size bag mods will already result in nearly instant desync though, so you may not ever live to see the side effects; and if original server installs said mod, original clients and original server will experience severe desyncs/undefined behavior too) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index 7a48fe04..a3138602 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -13,6 +13,7 @@ import ru.dbotthepony.kstarbound.server.IntegratedStarboundServer import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage import ru.dbotthepony.kstarbound.server.world.ServerUniverse import ru.dbotthepony.kstarbound.server.world.ServerWorld +import ru.dbotthepony.kstarbound.util.random.staticRandomDouble import ru.dbotthepony.kstarbound.world.WorldGeometry import java.io.BufferedInputStream import java.io.ByteArrayInputStream diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt index faa28caf..105ef0cc 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt @@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound import com.google.gson.GsonBuilder import com.google.gson.JsonElement import com.google.gson.JsonObject +import com.google.gson.JsonSyntaxException import com.google.gson.TypeAdapterFactory import com.google.gson.stream.JsonReader import org.apache.logging.log4j.LogManager @@ -39,7 +40,6 @@ import ru.dbotthepony.kstarbound.defs.world.BushVariant import ru.dbotthepony.kstarbound.defs.world.GrassVariant import ru.dbotthepony.kstarbound.defs.world.TreeVariant import ru.dbotthepony.kstarbound.defs.world.BiomeDefinition -import ru.dbotthepony.kstarbound.world.terrain.TerrainSelectorFactory import ru.dbotthepony.kstarbound.world.terrain.TerrainSelectorType import ru.dbotthepony.kstarbound.util.AssetPathStack import java.util.* @@ -78,7 +78,7 @@ object Registries { val monsterTypes = Registry("monster type").also(registriesInternal::add).also { adapters.add(it.adapter()) } val worldObjects = Registry("world object").also(registriesInternal::add).also { adapters.add(it.adapter()) } val biomes = Registry("biome").also(registriesInternal::add).also { adapters.add(it.adapter()) } - val terrainSelectors = Registry>("terrain selector").also(registriesInternal::add).also { adapters.add(it.adapter()) } + val terrainSelectors = Registry>("terrain selector").also(registriesInternal::add).also { adapters.add(it.adapter()) } val grassVariants = Registry("grass variant").also(registriesInternal::add).also { adapters.add(it.adapter()) } val treeStemVariants = Registry("tree stem variant").also(registriesInternal::add).also { adapters.add(it.adapter()) } val treeFoliageVariants = Registry("tree foliage variant").also(registriesInternal::add).also { adapters.add(it.adapter()) } @@ -242,10 +242,12 @@ object Registries { return files.map { listedFile -> Starbound.EXECUTOR.submit { try { - val factory = TerrainSelectorType.createFactory(Starbound.gson.getAdapter(JsonObject::class.java).read(JsonReader(listedFile.reader()).also { it.isLenient = true })) + val json = Starbound.gson.getAdapter(JsonObject::class.java).read(JsonReader(listedFile.reader()).also { it.isLenient = true }) + val name = json["name"]?.asString ?: throw JsonSyntaxException("Missing 'name' field") + val factory = TerrainSelectorType.factory(json) terrainSelectors.add { - terrainSelectors.add(factory.name, factory) + terrainSelectors.add(name, factory) } } catch (err: Exception) { LOGGER.error("Loading terrain selector $listedFile", err) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index 77541220..2459b951 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -125,7 +125,7 @@ object Starbound : ISBFileLocator { }) @JvmField - val EXECUTOR: ExecutorService = ForkJoinPool.commonPool() + val EXECUTOR: ForkJoinPool = ForkJoinPool.commonPool() @JvmField val COROUTINE_EXECUTOR = EXECUTOR.asCoroutineDispatcher() @JvmField diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PerlinNoiseParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PerlinNoiseParameters.kt index bd7090e7..1d293c59 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PerlinNoiseParameters.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PerlinNoiseParameters.kt @@ -1,7 +1,7 @@ package ru.dbotthepony.kstarbound.defs -import com.google.gson.stream.JsonWriter import ru.dbotthepony.kstarbound.json.builder.IStringSerializable +import ru.dbotthepony.kstarbound.json.builder.JsonAlias import ru.dbotthepony.kstarbound.json.builder.JsonFactory @JsonFactory @@ -25,10 +25,12 @@ data class PerlinNoiseParameters( enum class Type(override val jsonName: String) : IStringSerializable { PERLIN("perlin"), BILLOW("billow"), - RIDGED_MULTI("ridgedmulti"); + RIDGED_MULTI("ridgedMulti"); + + private val lower = jsonName.lowercase() override fun match(name: String): Boolean { - return name.lowercase() == jsonName + return name.lowercase() == lower } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt index 7fd0888b..2392292d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt @@ -311,6 +311,7 @@ class Image private constructor( .weigher { key, value -> value.capacity() } .maximumWeight((Runtime.getRuntime().maxMemory() / 4L).coerceIn(1_024L * 1_024L * 32L /* 32 МиБ */, 1_024L * 1_024L * 256L /* 256 МиБ */)) .scheduler(Scheduler.systemScheduler()) + .executor(Starbound.EXECUTOR) .buildAsync(CacheLoader { val getWidth = intArrayOf(0) val getHeight = intArrayOf(0) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrainSelectorParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrainSelectorParameters.kt index b93a76a2..de91509a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrainSelectorParameters.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrainSelectorParameters.kt @@ -2,11 +2,15 @@ package ru.dbotthepony.kstarbound.defs.world import ru.dbotthepony.kstarbound.json.builder.JsonFactory import java.util.random.RandomGenerator +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.sin @JsonFactory data class TerrainSelectorParameters( val worldWidth: Int, val baseHeight: Double, + val worldHeight: Int = 0, val seed: Long = 0L, val commonality: Double = 0.0 ) { @@ -28,4 +32,12 @@ data class TerrainSelectorParameters( fun withRandom(randomGenerator: RandomGenerator): TerrainSelectorParameters { return copy().also { it.random = randomGenerator } } + + fun noiseAngle(x: Int): Pair { + val noiseAngle = (2.0 * PI * x) / worldWidth + val noiseX = (cos(noiseAngle) * worldWidth) / (2.0 * PI) + val noiseY = (sin(noiseAngle) * worldWidth) / (2.0 * PI) + + return noiseX to noiseY + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt index 06ae1d07..80cc254f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt @@ -19,11 +19,11 @@ import ru.dbotthepony.kstarbound.GlobalDefaults import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.world.terrain.AbstractTerrainSelector -import ru.dbotthepony.kstarbound.world.terrain.createNamedTerrainSelector import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.util.ListInterner import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise import ru.dbotthepony.kstarbound.util.random.nextRange +import ru.dbotthepony.kstarbound.world.terrain.TerrainSelectorType import java.util.random.RandomGenerator import kotlin.math.roundToInt import kotlin.properties.Delegates @@ -249,17 +249,17 @@ class WorldLayout { var foregroundCaveSelector: AbstractTerrainSelector<*>? = null var backgroundCaveSelector: AbstractTerrainSelector<*>? = null - val terrainBase = TerrainSelectorParameters(worldSize.x, params.baseHeight.toDouble()) + val terrainBase = TerrainSelectorParameters(worldSize.x, params.baseHeight.toDouble(), worldHeight = worldSize.y) val terrain = terrainBase.withSeed(random.nextLong()) val fg = terrainBase.withSeed(random.nextLong()) val bg = terrainBase.withSeed(random.nextLong()) if (params.terrainSelector != null) - terrainSelector = terrainSelectors.intern(createNamedTerrainSelector(params.terrainSelector, terrain)) + terrainSelector = terrainSelectors.intern(TerrainSelectorType.named(params.terrainSelector, terrain)) if (params.fgCaveSelector != null) - foregroundCaveSelector = terrainSelectors.intern(createNamedTerrainSelector(params.fgCaveSelector, fg)) + foregroundCaveSelector = terrainSelectors.intern(TerrainSelectorType.named(params.fgCaveSelector, fg)) if (params.bgCaveSelector != null) - backgroundCaveSelector = terrainSelectors.intern(createNamedTerrainSelector(params.bgCaveSelector, bg)) + backgroundCaveSelector = terrainSelectors.intern(TerrainSelectorType.named(params.bgCaveSelector, bg)) val subBlockSelector = ArrayList>() val foregroundOreSelector = ArrayList>() @@ -272,18 +272,18 @@ class WorldLayout { if (params.subBlockSelector != null) { for (i in 0 until biome.subBlocks.size) { - subBlockSelector.add(terrainSelectors.intern(createNamedTerrainSelector(params.subBlockSelector, terrainBase.withSeed(random.nextLong())))) + subBlockSelector.add(terrainSelectors.intern(TerrainSelectorType.named(params.subBlockSelector, terrainBase.withSeed(random.nextLong())))) } for ((ore, commonality) in biome.ores) { val oreParams = terrainBase.withCommonality(commonality) if (params.fgOreSelector != null) { - foregroundOreSelector.add(terrainSelectors.intern(createNamedTerrainSelector(params.fgOreSelector, oreParams.withSeed(random.nextLong())))) + foregroundOreSelector.add(terrainSelectors.intern(TerrainSelectorType.named(params.fgOreSelector, oreParams.withSeed(random.nextLong())))) } if (params.bgOreSelector != null) { - backgroundOreSelector.add(terrainSelectors.intern(createNamedTerrainSelector(params.bgOreSelector, oreParams.withSeed(random.nextLong())))) + backgroundOreSelector.add(terrainSelectors.intern(TerrainSelectorType.named(params.bgOreSelector, oreParams.withSeed(random.nextLong())))) } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt index e98e047e..c2391a29 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt @@ -10,9 +10,6 @@ import ru.dbotthepony.kommons.util.XXHash64 import java.util.* import java.util.random.RandomGenerator import java.util.stream.IntStream -import kotlin.NoSuchElementException -import kotlin.collections.List -import kotlin.math.absoluteValue import kotlin.math.ceil import kotlin.math.floor import kotlin.math.ln @@ -79,11 +76,11 @@ fun staticRandom32(vararg values: Any): Int { } fun staticRandomFloat(vararg values: Any): Float { - return staticRandom32(*values).toFloat().absoluteValue / Int.MAX_VALUE.toFloat() + return staticRandom32(*values).ushr(8) * 5.9604645E-8f } fun staticRandomDouble(vararg values: Any): Double { - return staticRandom64(*values).toDouble().absoluteValue / Long.MAX_VALUE.toDouble() + return staticRandom64(*values).ushr(11) * 1.1102230246251565E-16 } fun staticRandom64(vararg values: Any): Long { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/CoordinateMapper.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/CoordinateMapper.kt index 5007f3fd..914718d3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/CoordinateMapper.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/CoordinateMapper.kt @@ -13,6 +13,11 @@ fun positiveModulo(a: Double, b: Int): Double { return if (result < 0.0) result + b else result } +fun positiveModulo(a: Double, b: Double): Double { + val result = a % b + return if (result < 0.0) result + b else result +} + fun positiveModulo(a: Float, b: Int): Float { val result = a % b return if (result < 0f) result + b else result diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/LightCalculator.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/LightCalculator.kt index a91e77ad..45171f0f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/LightCalculator.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/LightCalculator.kt @@ -6,6 +6,7 @@ import ru.dbotthepony.kommons.arrays.Object2DArray import ru.dbotthepony.kommons.util.IStruct4f import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kommons.math.linearInterpolation +import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.world.api.ICellAccess import java.nio.ByteBuffer import java.util.concurrent.Callable @@ -387,7 +388,7 @@ class LightCalculator(val parent: ICellAccess, val width: Int, val height: Int) val thread = Thread.currentThread() // calculate k-means clusters of point lights // to effectively utilize CPU cores - val clusterCount = ForkJoinPool.commonPool().parallelism.coerceAtMost(pointLights.size) + val clusterCount = Starbound.EXECUTOR.parallelism.coerceAtMost(pointLights.size) val clusters = ArrayList(clusterCount) val startingPoints = IntArraySet() // val rand = Random(System.nanoTime()) @@ -464,7 +465,7 @@ class LightCalculator(val parent: ICellAccess, val width: Int, val height: Int) } val tasks = ArrayList>() - clusters.forEach { tasks.add(CompletableFuture.supplyAsync(it, ForkJoinPool.commonPool()).also { it.thenApply { LockSupport.unpark(thread); it } }) } + clusters.forEach { tasks.add(CompletableFuture.supplyAsync(it, Starbound.EXECUTOR).also { it.thenApply { LockSupport.unpark(thread); it } }) } while (tasks.isNotEmpty()) { tasks.removeIf { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index 63d63155..48bbe6d0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -270,7 +270,7 @@ abstract class World, ChunkType : Chunk(val name: String, val config: D, val parameters: TerrainSelectorParameters) { +abstract class AbstractTerrainSelector(val data: D, val parameters: TerrainSelectorParameters) { // Returns a float signifying the "solid-ness" of a block, >= 0.0 should be // considered solid, < 0.0 should be considered open space. abstract operator fun get(x: Int, y: Int): Double @@ -14,9 +14,8 @@ abstract class AbstractTerrainSelector(val name: String, val config: D, fun toJson(): JsonObject { val result = JsonObject() - result["name"] = name result["type"] = type.jsonName - result["config"] = Starbound.gson.toJsonTree(config) + result["config"] = Starbound.gson.toJsonTree(data) result["parameters"] = Starbound.gson.toJsonTree(parameters) return result } @@ -29,12 +28,11 @@ abstract class AbstractTerrainSelector(val name: String, val config: D, return false other as AbstractTerrainSelector<*> - return name == other.name && config == other.config && parameters == other.parameters + return data == other.data && parameters == other.parameters } private val hash by lazy { - var h = name.hashCode() - h = h * 31 + config.hashCode() + var h = data.hashCode() h = h * 31 + parameters.hashCode() h } @@ -44,6 +42,6 @@ abstract class AbstractTerrainSelector(val name: String, val config: D, } override fun toString(): String { - return "${this::class.simpleName}[$name, config=$config, parameters=$parameters]" + return "${this::class.simpleName}[config=$data, parameters=$parameters]" } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/ComparingTerrainSelector.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/ComparingTerrainSelector.kt new file mode 100644 index 00000000..635a13fa --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/ComparingTerrainSelector.kt @@ -0,0 +1,60 @@ +package ru.dbotthepony.kstarbound.world.terrain + +import com.google.common.collect.ImmutableList +import com.google.gson.JsonObject +import ru.dbotthepony.kommons.gson.get +import ru.dbotthepony.kstarbound.defs.world.TerrainSelectorParameters +import ru.dbotthepony.kstarbound.json.builder.JsonFactory + +sealed class ComparingTerrainSelector(data: Data, parameters: TerrainSelectorParameters) : AbstractTerrainSelector(data, parameters) { + @JsonFactory + data class Data(val sources: ImmutableList) + + protected val sources = ArrayList>() + + init { + require(data.sources.isNotEmpty()) { "'sources' array is empty" } + + for (source in data.sources) { + sources.add(TerrainSelectorType.create(source, parameters.withSeed(parameters.seed + source.get("seedBias", 0L)))) + } + } + + class Max(data: Data, parameters: TerrainSelectorParameters) : ComparingTerrainSelector(data, parameters) { + override fun get(x: Int, y: Int): Double { + return sources.maxOf { it[x, y] } + } + + override val type: TerrainSelectorType + get() = TerrainSelectorType.MAX + } + + class Min(data: Data, parameters: TerrainSelectorParameters) : ComparingTerrainSelector(data, parameters) { + override fun get(x: Int, y: Int): Double { + return sources.minOf { it[x, y] } + } + + override val type: TerrainSelectorType + get() = TerrainSelectorType.MIN + } + + class MinMax(data: Data, parameters: TerrainSelectorParameters) : ComparingTerrainSelector(data, parameters) { + override fun get(x: Int, y: Int): Double { + var value = 0.0 + + for (source in sources) { + val sample = source[x, y] + + if (value > 0.0 || sample > 0.0) + value = value.coerceAtLeast(sample) + else + value = value.coerceAtMost(sample) + } + + return value + } + + override val type: TerrainSelectorType + get() = TerrainSelectorType.MIN_MAX + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/ConstantTerrainSelector.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/ConstantTerrainSelector.kt index 6b42e4d3..06258733 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/ConstantTerrainSelector.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/ConstantTerrainSelector.kt @@ -3,7 +3,7 @@ package ru.dbotthepony.kstarbound.world.terrain import ru.dbotthepony.kstarbound.defs.world.TerrainSelectorParameters import ru.dbotthepony.kstarbound.json.builder.JsonFactory -class ConstantTerrainSelector(name: String, data: Data, parameters: TerrainSelectorParameters) : AbstractTerrainSelector(name, data, parameters) { +class ConstantTerrainSelector(data: Data, parameters: TerrainSelectorParameters) : AbstractTerrainSelector(data, parameters) { @JsonFactory data class Data(val value: Double) @@ -11,6 +11,6 @@ class ConstantTerrainSelector(name: String, data: Data, parameters: TerrainSelec get() = TerrainSelectorType.CONSTANT override fun get(x: Int, y: Int): Double { - return config.value + return data.value } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/DisplacementTerrainSelector.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/DisplacementTerrainSelector.kt index 2a80c37e..38052d24 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/DisplacementTerrainSelector.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/DisplacementTerrainSelector.kt @@ -8,7 +8,7 @@ import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise import kotlin.math.roundToInt -class DisplacementTerrainSelector(name: String, data: Data, parameters: TerrainSelectorParameters) : AbstractTerrainSelector(name, data, parameters) { +class DisplacementTerrainSelector(data: Data, parameters: TerrainSelectorParameters) : AbstractTerrainSelector(data, parameters) { @JsonFactory data class Data( val xType: PerlinNoiseParameters.Type, @@ -40,6 +40,7 @@ class DisplacementTerrainSelector(name: String, data: Data, parameters: TerrainS val xClamp: Vector2d? = null, val xClampSmoothing: Double = 0.0, + val seedBias: Long = 0L, val source: JsonObject, ) @@ -50,7 +51,6 @@ class DisplacementTerrainSelector(name: String, data: Data, parameters: TerrainS init { // This allows to have multiple nested displacement selectors with different seeds // original engine isn't capable of this because nested selectors will have the same seed - val parameters = parameters.withRandom() val random = parameters.random() xFn = AbstractPerlinNoise.of(PerlinNoiseParameters( @@ -77,39 +77,39 @@ class DisplacementTerrainSelector(name: String, data: Data, parameters: TerrainS xFn.init(random.nextLong()) yFn.init(random.nextLong()) - source = TerrainSelectorType.create(data.source, parameters) + source = TerrainSelectorType.create(data.source, parameters.withSeed(parameters.seed + data.seedBias)) } override fun get(x: Int, y: Int): Double { - return source[clampX(xFn[x * config.xXInfluence, y * config.xYInfluence]).roundToInt(), clampY(yFn[x * config.yXInfluence, y * config.yYInfluence]).roundToInt()] + return source[clampX(xFn[x * data.xXInfluence, y * data.xYInfluence]).roundToInt(), clampY(yFn[x * data.yXInfluence, y * data.yYInfluence]).roundToInt()] } private fun clampX(v: Double): Double { - if (config.xClamp == null) + if (data.xClamp == null) return v - if (config.xClampSmoothing == 0.0) - return v.coerceIn(config.xClamp.x, config.xClamp.y) + if (data.xClampSmoothing == 0.0) + return v.coerceIn(data.xClamp.x, data.xClamp.y) - return 0.2 * ((v - config.xClampSmoothing).coerceIn(config.xClamp.x, config.xClamp.y) - + (v - 0.5 * config.xClampSmoothing).coerceIn(config.xClamp.x, config.xClamp.y) - + (v).coerceIn(config.xClamp.x, config.xClamp.y) - + (v + 0.5 * config.xClampSmoothing).coerceIn(config.xClamp.x, config.xClamp.y) - + (v + config.xClampSmoothing).coerceIn(config.xClamp.x, config.xClamp.y)) + return 0.2 * ((v - data.xClampSmoothing).coerceIn(data.xClamp.x, data.xClamp.y) + + (v - 0.5 * data.xClampSmoothing).coerceIn(data.xClamp.x, data.xClamp.y) + + (v).coerceIn(data.xClamp.x, data.xClamp.y) + + (v + 0.5 * data.xClampSmoothing).coerceIn(data.xClamp.x, data.xClamp.y) + + (v + data.xClampSmoothing).coerceIn(data.xClamp.x, data.xClamp.y)) } private fun clampY(v: Double): Double { - if (config.yClamp == null) + if (data.yClamp == null) return v - if (config.xClampSmoothing == 0.0) - return v.coerceIn(config.yClamp.x, config.yClamp.y) + if (data.xClampSmoothing == 0.0) + return v.coerceIn(data.yClamp.x, data.yClamp.y) - return 0.2 * ((v - config.yClampSmoothing).coerceIn(config.yClamp.x, config.yClamp.y) - + (v - 0.5 * config.yClampSmoothing).coerceIn(config.yClamp.x, config.yClamp.y) - + (v).coerceIn(config.yClamp.x, config.yClamp.y) - + (v + 0.5 * config.yClampSmoothing).coerceIn(config.yClamp.x, config.yClamp.y) - + (v + config.yClampSmoothing).coerceIn(config.yClamp.x, config.yClamp.y)) + return 0.2 * ((v - data.yClampSmoothing).coerceIn(data.yClamp.x, data.yClamp.y) + + (v - 0.5 * data.yClampSmoothing).coerceIn(data.yClamp.x, data.yClamp.y) + + (v).coerceIn(data.yClamp.x, data.yClamp.y) + + (v + 0.5 * data.yClampSmoothing).coerceIn(data.yClamp.x, data.yClamp.y) + + (v + data.yClampSmoothing).coerceIn(data.yClamp.x, data.yClamp.y)) } override val type: TerrainSelectorType diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/FlatSurfaceTerrainSelector.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/FlatSurfaceTerrainSelector.kt new file mode 100644 index 00000000..5f6bb35a --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/FlatSurfaceTerrainSelector.kt @@ -0,0 +1,18 @@ +package ru.dbotthepony.kstarbound.world.terrain + +import ru.dbotthepony.kstarbound.defs.world.TerrainSelectorParameters +import ru.dbotthepony.kstarbound.json.builder.JsonFactory + +class FlatSurfaceTerrainSelector(data: Data, parameters: TerrainSelectorParameters) : AbstractTerrainSelector(data, parameters) { + @JsonFactory + data class Data(val adjustment: Double = 0.0, val flip: Boolean = false) + + private val flip = if (data.flip) -1.0 else 1.0 + + override fun get(x: Int, y: Int): Double { + return flip * (parameters.baseHeight - (y - data.adjustment)) + } + + override val type: TerrainSelectorType + get() = TerrainSelectorType.FLAT_SURFACE +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/IslandSurfaceTerrainSelector.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/IslandSurfaceTerrainSelector.kt new file mode 100644 index 00000000..2f0ef107 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/IslandSurfaceTerrainSelector.kt @@ -0,0 +1,106 @@ +package ru.dbotthepony.kstarbound.world.terrain + +import com.github.benmanes.caffeine.cache.Caffeine +import com.github.benmanes.caffeine.cache.Scheduler +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters +import ru.dbotthepony.kstarbound.defs.world.TerrainSelectorParameters +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise +import kotlin.math.PI +import kotlin.math.absoluteValue +import kotlin.math.cos +import kotlin.math.sin + +class IslandSurfaceTerrainSelector(data: Data, parameters: TerrainSelectorParameters) : AbstractTerrainSelector(data, parameters) { + @JsonFactory + data class Data( + val islandElevation: Double, + val islandTaperPoint: Double, + val islandHeight: PerlinNoiseParameters, + val islandDepth: PerlinNoiseParameters, + val islandDecision: PerlinNoiseParameters, + ) + + private val islandHeightSeed: Long + private val islandDepthSeed: Long + private val islandDecisionSeed: Long + + init { + val random = parameters.random() + + for (i in 0 .. 7) + random.nextLong() // extra randomness + + islandHeightSeed = random.nextLong() + islandDepthSeed = random.nextLong() + islandDecisionSeed = random.nextLong() + } + + val islandHeight by lazy { + val perlin = AbstractPerlinNoise.of(this.data.islandHeight) + + if (!perlin.isInitialized) { + perlin.init(islandHeightSeed) + } + + perlin + } + + val islandDepth by lazy { + val perlin = AbstractPerlinNoise.of(this.data.islandDepth) + + if (!perlin.isInitialized) { + perlin.init(islandDepthSeed) + } + + perlin + } + + val islandDecision by lazy { + val perlin = AbstractPerlinNoise.of(this.data.islandDecision) + + if (!perlin.isInitialized) { + perlin.init(islandDecisionSeed) + } + + perlin + } + + private fun compute(x: Int): Column { + val (noiseX, noiseY) = parameters.noiseAngle(x) + val thisIslandDecision = islandDecision[noiseX, noiseY] + + if (thisIslandDecision > 0.0) { + val taperFactor = if (thisIslandDecision < data.islandTaperPoint) sin(0.5 * PI * thisIslandDecision) / data.islandTaperPoint else 1.0 + + return Column( + data.islandElevation + parameters.baseHeight + taperFactor * islandHeight[noiseX, noiseY], + data.islandElevation + parameters.baseHeight - taperFactor * islandDepth[noiseX, noiseY], + ) + } else { + return baseHeight + } + } + + private val baseHeight = Column(parameters.baseHeight, parameters.baseHeight) + + private class Column(topLevel: Double, bottomLevel: Double) { + val halfMinus = (topLevel - bottomLevel) / 2.0 + val halfPlus = (topLevel + bottomLevel) / 2.0 + } + + private val cache = Caffeine.newBuilder() + .maximumSize(512L) + .executor(Starbound.EXECUTOR) + .scheduler(Scheduler.systemScheduler()) + .build(::compute) + + override fun get(x: Int, y: Int): Double { + val column = cache.get(x) + return column.halfMinus - (column.halfPlus - y).absoluteValue + } + + override val type: TerrainSelectorType + get() = TerrainSelectorType.ISLAND_SURFACE +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/KarstCaveTerrainSelector.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/KarstCaveTerrainSelector.kt new file mode 100644 index 00000000..60686b4c --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/KarstCaveTerrainSelector.kt @@ -0,0 +1,141 @@ +package ru.dbotthepony.kstarbound.world.terrain + +import com.github.benmanes.caffeine.cache.Caffeine +import com.github.benmanes.caffeine.cache.Scheduler +import ru.dbotthepony.kommons.arrays.Double2DArray +import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters +import ru.dbotthepony.kstarbound.defs.world.TerrainSelectorParameters +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise +import ru.dbotthepony.kstarbound.util.random.random +import ru.dbotthepony.kstarbound.world.positiveModulo +import java.time.Duration +import kotlin.math.PI +import kotlin.math.absoluteValue +import kotlin.math.roundToInt +import kotlin.math.sin + +class KarstCaveTerrainSelector(data: Data, parameters: TerrainSelectorParameters) : AbstractTerrainSelector(data, parameters) { + @JsonFactory + data class Data( + val sectorSize: Int = 64, + val layerPerlinsCacheSize: Int = 32, + val sectorCacheSize: Int = 32, + val layerResolution: Int, + val layerDensity: Double, + val bufferHeight: Int, + val caveTaperPoint: Int, + + val caveDecision: PerlinNoiseParameters, + val layerHeightVariation: PerlinNoiseParameters, + val caveHeightVariation: PerlinNoiseParameters, + val caveFloorVariation: PerlinNoiseParameters, + ) + + private inner class Layer(y: Int) { + val caveSeed: Long + val layerHeightVariationSeed: Long + val caveHeightVariationSeed: Long + val caveFloorVariationSeed: Long + + init { + val random = random(parameters.seed + y) + caveSeed = random.nextLong() + layerHeightVariationSeed = random.nextLong() + caveHeightVariationSeed = random.nextLong() + caveFloorVariationSeed = random.nextLong() + } + + val cave = AbstractPerlinNoise.of(data.caveDecision).also { it.init(caveSeed) } + val layerHeightVariation by lazy { AbstractPerlinNoise.of(data.layerHeightVariation).also { it.init(layerHeightVariationSeed) } } + val caveHeightVariation by lazy { AbstractPerlinNoise.of(data.caveHeightVariation).also { it.init(caveHeightVariationSeed) } } + val caveFloorVariation by lazy { AbstractPerlinNoise.of(data.caveFloorVariation).also { it.init(caveFloorVariationSeed) } } + } + + private val layers = Caffeine.newBuilder() + .maximumSize(data.layerPerlinsCacheSize.toLong()) + .expireAfterAccess(Duration.ofMinutes(5)) + .scheduler(Scheduler.systemScheduler()) + .executor(Starbound.EXECUTOR) + .build(::Layer) + + private inner class Sector(val sector: Vector2i) { + private var maxValue = 0.0 + private val values = Double2DArray.allocate(data.sectorSize, data.sectorSize) + + init { + val random = random(parameters.seed) + + for (y in sector.y - data.bufferHeight until sector.y + data.sectorSize + data.bufferHeight) { + val layerChance = data.layerDensity * data.layerResolution + // determine whether this layer has caves + + if (y % data.layerResolution != 0 && random.nextDouble() > layerChance) + continue + + val layer = layers[y] + + // carve out cave layer + for (x in sector.x until sector.x + data.sectorSize) { + // use wrapping noise + val (noiseX, noiseY) = parameters.noiseAngle(x) + + // determine where caves be at + val isThereACaveHere = layer.cave[noiseX, noiseY] + if (isThereACaveHere <= 0.0) continue + + val taperFactor = if (isThereACaveHere < data.caveTaperPoint) sin((0.5 * PI * isThereACaveHere) / data.caveTaperPoint) else 1.0 + + val baseY = y + layer.layerHeightVariation[noiseX, noiseY] + val ceilingY = baseY + layer.caveHeightVariation[noiseX, noiseY] * taperFactor + val floorY = baseY + layer.caveFloorVariation[noiseX, noiseY] * taperFactor + + val halfHeight = (ceilingY - floorY + 1.0).absoluteValue / 2.0 + val midpointY = (floorY + ceilingY) / 2.0 + + maxValue = maxValue.coerceAtLeast(halfHeight) + + for (pointY in floorY.roundToInt() until ceilingY.roundToInt()) { + if (isInside(x, y)) { + this[x, pointY] = this[x, pointY].coerceAtLeast(halfHeight - (midpointY - pointY).absoluteValue) + } + } + } + } + } + + private fun isInside(x: Int, y: Int): Boolean { + val diffX = x - sector.x + val diffY = y - sector.y + + return diffX in 0 until data.sectorSize && + diffY in 0 until data.sectorSize + } + + operator fun get(x: Int, y: Int): Double { + val get = values[x - sector.x, y - sector.y] + return if (get > 0.0) get else -maxValue + } + + private operator fun set(x: Int, y: Int, value: Double) { + values[x - sector.x, y - sector.y] = value + } + } + + private val sectors = Caffeine.newBuilder() + .maximumSize(data.sectorCacheSize.toLong()) + .expireAfterAccess(Duration.ofMinutes(5)) + .scheduler(Scheduler.systemScheduler()) + .executor(Starbound.EXECUTOR) + .build(::Sector) + + override fun get(x: Int, y: Int): Double { + val sector = Vector2i(x - positiveModulo(x, data.sectorSize), y - positiveModulo(y, data.sectorSize)) + return sectors[sector][x, y] + } + + override val type: TerrainSelectorType + get() = TerrainSelectorType.KARST_CAVE +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/MixTerrainSelector.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/MixTerrainSelector.kt new file mode 100644 index 00000000..818ca9eb --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/MixTerrainSelector.kt @@ -0,0 +1,36 @@ +package ru.dbotthepony.kstarbound.world.terrain + +import com.google.gson.JsonObject +import ru.dbotthepony.kommons.math.linearInterpolation +import ru.dbotthepony.kstarbound.defs.world.TerrainSelectorParameters +import ru.dbotthepony.kstarbound.json.builder.JsonFactory + +class MixTerrainSelector(data: Data, parameters: TerrainSelectorParameters) : AbstractTerrainSelector(data, parameters) { + @JsonFactory + data class Data( + val mixSource: JsonObject, + val mixSeedBias: Long = 0L, + val aSource: JsonObject, + val aSeedBias: Long = 0L, + val bSource: JsonObject, + val bSeedBias: Long = 0L, + ) + + private val mix = TerrainSelectorType.create(data.mixSource, parameters.withSeed(parameters.seed + data.mixSeedBias)) + private val aSource = TerrainSelectorType.create(data.aSource, parameters.withSeed(parameters.seed + data.aSeedBias)) + private val bSource = TerrainSelectorType.create(data.bSource, parameters.withSeed(parameters.seed + data.bSeedBias)) + + override fun get(x: Int, y: Int): Double { + val mix = mix[x, y] + + if (mix <= -1.0) + return aSource[x, y] + else if (mix >= 1.0) + return bSource[x, y] + else + return linearInterpolation(mix * 0.5 + 0.5, aSource[x, y], bSource[x, y]) + } + + override val type: TerrainSelectorType + get() = TerrainSelectorType.MIX +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/PerlinTerrainSelector.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/PerlinTerrainSelector.kt new file mode 100644 index 00000000..0076bf9b --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/PerlinTerrainSelector.kt @@ -0,0 +1,46 @@ +package ru.dbotthepony.kstarbound.world.terrain + +import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters +import ru.dbotthepony.kstarbound.defs.world.TerrainSelectorParameters +import ru.dbotthepony.kstarbound.json.builder.JsonAlias +import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise + +class PerlinTerrainSelector(data: Data, parameters: TerrainSelectorParameters) : AbstractTerrainSelector(data, parameters) { + data class Data( + @JsonAlias("type") + val function: PerlinNoiseParameters.Type, + val octaves: Int, + @JsonAlias("frequency") + val freq: Double, + @JsonAlias("amplitude") + val amp: Double, + val bias: Double = 0.0, + val alpha: Double = 2.0, + val beta: Double = 2.0, + val xInfluence: Double = 1.0, + val yInfluence: Double = 1.0, + ) + + private val seed = parameters.random().nextLong() + private val perlin by lazy { + AbstractPerlinNoise.of( + PerlinNoiseParameters( + type = data.function, + octaves = data.octaves, + frequency = data.freq, + amplitude = data.amp, + bias = data.bias, + alpha = data.alpha, + beta = data.beta, + )).also { + it.init(seed) + } + } + + override fun get(x: Int, y: Int): Double { + return perlin[x * data.xInfluence, y * data.yInfluence] + } + + override val type: TerrainSelectorType + get() = TerrainSelectorType.PERLIN +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/RidgeBlocksTerrainSelector.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/RidgeBlocksTerrainSelector.kt new file mode 100644 index 00000000..33ecc58d --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/RidgeBlocksTerrainSelector.kt @@ -0,0 +1,85 @@ +package ru.dbotthepony.kstarbound.world.terrain + +import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters +import ru.dbotthepony.kstarbound.defs.world.TerrainSelectorParameters +import ru.dbotthepony.kstarbound.json.builder.JsonAlias +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise + +class RidgeBlocksTerrainSelector(data: Data, parameters: TerrainSelectorParameters) : AbstractTerrainSelector(data, parameters) { + @JsonFactory + data class Data( + val amplitude: Double, + val frequency: Double, + val bias: Double, + @JsonAlias("amplitude") + val noiseAmplitude: Double, + @JsonAlias("frequency") + val noiseFrequency: Double, + val octaves: Int = 2, + val perlinOctaves: Int = 1, + ) + + private val ridge1Seed: Long + private val ridge2Seed: Long + private val perlinSeed: Long + + init { + val random = parameters.random() + ridge1Seed = random.nextLong() + ridge2Seed = random.nextLong() + perlinSeed = random.nextLong() + } + + val ridge1 by lazy { + AbstractPerlinNoise.of(PerlinNoiseParameters( + type = PerlinNoiseParameters.Type.RIDGED_MULTI, + octaves = data.octaves, + frequency = data.frequency, + amplitude = data.amplitude, + bias = 0.0, + alpha = 2.0, + beta = 2.0, + seed = ridge1Seed + )) + } + + val ridge2 by lazy { + AbstractPerlinNoise.of(PerlinNoiseParameters( + type = PerlinNoiseParameters.Type.RIDGED_MULTI, + octaves = data.octaves, + frequency = data.frequency, + amplitude = data.amplitude, + bias = 0.0, + alpha = 2.0, + beta = 2.0, + seed = ridge2Seed + )) + } + + val perlin by lazy { + AbstractPerlinNoise.of(PerlinNoiseParameters( + type = PerlinNoiseParameters.Type.PERLIN, + octaves = data.perlinOctaves, + frequency = data.frequency, + amplitude = data.amplitude, + bias = 0.0, + alpha = 1.0, + beta = 2.0, + seed = perlinSeed + )) + } + + override fun get(x: Int, y: Int): Double { + if (parameters.commonality <= 0.0) { + return 0.0 + } else { + val x = perlin[x.toDouble(), y.toDouble()] + val y = perlin[y.toDouble(), x] + return (ridge1[x, y] - ridge2[x, y]) * parameters.commonality + data.bias + } + } + + override val type: TerrainSelectorType + get() = TerrainSelectorType.RIDGE_BLOCKS +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/RotateTerrainSelector.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/RotateTerrainSelector.kt new file mode 100644 index 00000000..6bf27e5e --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/RotateTerrainSelector.kt @@ -0,0 +1,29 @@ +package ru.dbotthepony.kstarbound.world.terrain + +import com.google.gson.JsonObject +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.defs.world.TerrainSelectorParameters +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import kotlin.math.roundToInt + +class RotateTerrainSelector(data: Data, parameters: TerrainSelectorParameters) : AbstractTerrainSelector(data, parameters) { + @JsonFactory + data class Data( + val rotation: Double, + val rotationWidth: Double = 0.5, + val rotationHeight: Double = 0.0, + val source: JsonObject, + ) + + private val source = TerrainSelectorType.create(data.source) + private val deltaX = parameters.worldWidth * data.rotationWidth + private val deltaY = parameters.worldHeight * data.rotationHeight + + override fun get(x: Int, y: Int): Double { + val newPos = Vector2d(x - deltaX, y - deltaY).rotate(data.rotation) + return source[(newPos.x + deltaX).roundToInt(), (newPos.y + deltaY).roundToInt()] + } + + override val type: TerrainSelectorType + get() = TerrainSelectorType.ROTATE +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/TerrainSelectorFactory.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/TerrainSelectorFactory.kt index efa82cb1..49fc0918 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/TerrainSelectorFactory.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/TerrainSelectorFactory.kt @@ -2,8 +2,8 @@ package ru.dbotthepony.kstarbound.world.terrain import ru.dbotthepony.kstarbound.defs.world.TerrainSelectorParameters -class TerrainSelectorFactory>(val name: String, private val data: D, private val factory: (String, D, TerrainSelectorParameters) -> T) { +class TerrainSelectorFactory>(private val data: D, private val factory: (D, TerrainSelectorParameters) -> T) { fun create(parameters: TerrainSelectorParameters): T { - return factory(name, data, parameters) + return factory(data, parameters) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/TerrainSelectorType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/TerrainSelectorType.kt index d5ff7a33..5e06eec8 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/TerrainSelectorType.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/TerrainSelectorType.kt @@ -14,15 +14,38 @@ import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.world.TerrainSelectorParameters -fun createNamedTerrainSelector(name: String, parameters: TerrainSelectorParameters): AbstractTerrainSelector<*> { - return Registries.terrainSelectors.getOrThrow(name).value.create(parameters) +private inline fun > data(noinline factory: (D, TerrainSelectorParameters) -> T): Data { + return Data(TypeToken.get(D::class.java), factory) } -enum class TerrainSelectorType(val jsonName: String) { - CONSTANT("constant"), - DISPLACEMENT("displacement"), +private data class Data>(val token: TypeToken, val factory: (D, TerrainSelectorParameters) -> T) { + val adapter: TypeAdapter by lazy { Starbound.gson.getAdapter(token) } +} + +enum class TerrainSelectorType(val jsonName: String, private val data: Data<*, *>) { + CONSTANT("constant", data(::ConstantTerrainSelector)), + DISPLACEMENT("displacement", data(::DisplacementTerrainSelector)), + FLAT_SURFACE("flatSurface", data(::FlatSurfaceTerrainSelector)), + ISLAND_SURFACE("islandSurface", data(::IslandSurfaceTerrainSelector)), + MAX("max", data(ComparingTerrainSelector::Max)), + MIN("min", data(ComparingTerrainSelector::Min)), + MIN_MAX("minmax", data(ComparingTerrainSelector::MinMax)), + PERLIN("perlin", data(::PerlinTerrainSelector)), + KARST_CAVE("karstcave", data(::KarstCaveTerrainSelector)), + MIX("mix", data(::MixTerrainSelector)), + ROTATE("rotate", data(::RotateTerrainSelector)), + RIDGE_BLOCKS("ridgeblocks", data(::RidgeBlocksTerrainSelector)), + WORM_CAVE("wormcave", data(::WormCaveTerrainSelector)), ; + private val lowercase = jsonName.lowercase() + + data class Factory>(private val data: D, private val factory: (D, TerrainSelectorParameters) -> T) { + fun create(parameters: TerrainSelectorParameters): T { + return factory(data, parameters) + } + } + companion object : TypeAdapter>(), TypeAdapterFactory { override fun create(gson: Gson, type: TypeToken): TypeAdapter? { if (AbstractTerrainSelector::class.java.isAssignableFrom(type.rawType)) { @@ -41,6 +64,10 @@ enum class TerrainSelectorType(val jsonName: String) { private val objects by lazy { Starbound.gson.getAdapter(JsonObject::class.java) } + fun named(name: String, parameters: TerrainSelectorParameters): AbstractTerrainSelector<*> { + return Registries.terrainSelectors.getOrThrow(name).value.create(parameters) + } + override fun read(`in`: JsonReader): AbstractTerrainSelector<*>? { if (`in`.consumeNull()) return null @@ -48,29 +75,24 @@ enum class TerrainSelectorType(val jsonName: String) { return create(objects.read(`in`)) } - fun createFactory(json: JsonObject): TerrainSelectorFactory<*, *> { - val name = json["name"]?.asString ?: "" + fun factory(json: JsonObject): Factory<*, *> { val type = json["type"]?.asString?.lowercase() ?: throw JsonSyntaxException("Missing 'type' element of terrain json") - when (type) { - CONSTANT.jsonName -> { - return TerrainSelectorFactory(name, Starbound.gson.fromJson(json, ConstantTerrainSelector.Data::class.java), ::ConstantTerrainSelector) + for (value in entries) { + if (value.lowercase == type) { + return Factory(value.data.adapter.fromJsonTree(json), value.data.factory as ((Any, TerrainSelectorParameters) -> AbstractTerrainSelector)) } - - DISPLACEMENT.jsonName -> { - return TerrainSelectorFactory(name, Starbound.gson.fromJson(json, DisplacementTerrainSelector.Data::class.java), ::DisplacementTerrainSelector) - } - - else -> throw IllegalArgumentException("Unknown terrain selector type $type") } + + throw IllegalArgumentException("Unknown terrain selector type $type") } fun create(json: JsonObject): AbstractTerrainSelector<*> { - return createFactory(json).create(Starbound.gson.fromJson(json["parameters"] ?: throw JsonSyntaxException("Missing 'parameters' element of terrain json"), TerrainSelectorParameters::class.java)) + return factory(json).create(Starbound.gson.fromJson(json["parameters"] ?: throw JsonSyntaxException("Missing 'parameters' element of terrain json"), TerrainSelectorParameters::class.java)) } fun create(json: JsonObject, parameters: TerrainSelectorParameters): AbstractTerrainSelector<*> { - return createFactory(json).create(parameters) + return factory(json).create(parameters) } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/WormCaveTerrainSelector.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/WormCaveTerrainSelector.kt new file mode 100644 index 00000000..1d5abaec --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/WormCaveTerrainSelector.kt @@ -0,0 +1,194 @@ +package ru.dbotthepony.kstarbound.world.terrain + +import com.github.benmanes.caffeine.cache.Caffeine +import com.github.benmanes.caffeine.cache.Scheduler +import ru.dbotthepony.kommons.arrays.Double2DArray +import ru.dbotthepony.kommons.math.linearInterpolation +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.world.TerrainSelectorParameters +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.util.random.nextRange +import ru.dbotthepony.kstarbound.util.random.random +import ru.dbotthepony.kstarbound.util.random.staticRandom64 +import ru.dbotthepony.kstarbound.util.random.staticRandomDouble +import ru.dbotthepony.kstarbound.world.positiveModulo +import java.time.Duration +import kotlin.math.PI +import kotlin.math.absoluteValue +import kotlin.math.ceil +import kotlin.math.cos +import kotlin.math.sign +import kotlin.math.sin +import kotlin.math.sqrt + +class WormCaveTerrainSelector(data: Data, parameters: TerrainSelectorParameters) : AbstractTerrainSelector(data, parameters) { + @JsonFactory + data class Data( + val sectorSize: Int = 64, + val lruCacheSize: Int = 32, + val numberOfWormsPerSectorRange: Vector2d, + val wormSizeRange: Vector2d, + val wormLengthRange: Vector2d, + val wormTaperDistance: Double, + val wormAngleRange: Vector2d, + val wormTurnChance: Double, + val wormTurnRate: Double, + val wormSpeed: Double = 1.0, + val sectorRadius: Int, + ) + + private data class Worm( + var pos: Vector2d, + var angle: Double, + var goalAngle: Double, + val size: Double, + var length: Double = 0.0, + val goalLength: Double, + ) + + private inner class Sector(val sector: Vector2i) { + private val values = Double2DArray.allocate(data.sectorSize, data.sectorSize) + private var maxValue = data.wormSizeRange.y / 2.0 + + init { + val worms = ArrayList() + + // determine worms for the neighbouring sectors + val sectorRadius = data.sectorRadius * data.sectorSize + + // wormy + for (x in (sector.x - sectorRadius .. sector.x + sectorRadius).step(data.sectorSize)) { + for (y in (sector.y - sectorRadius .. sector.y + sectorRadius).step(data.sectorSize)) { + // worm + val random = random(staticRandom64(x, y, parameters.seed)) + val numberOfWorms = random.nextRange(data.numberOfWormsPerSectorRange) * parameters.commonality + var intNumberOfWorms = numberOfWorms.toInt() + + if (random.nextDouble() < numberOfWorms - intNumberOfWorms) + intNumberOfWorms++ + + for (n in 0 until intNumberOfWorms) { + // wurm + worms.add(Worm( + pos = Vector2d(x + random.nextDouble(0.0, data.sectorSize.toDouble()), y + random.nextDouble(0.0, data.sectorSize.toDouble())), + angle = random.nextRange(data.wormAngleRange), + goalAngle = random.nextRange(data.wormAngleRange), + size = random.nextRange(data.wormSizeRange) * parameters.commonality, + goalLength = random.nextRange(data.wormLengthRange) * parameters.commonality + )) + } + } + } + + while (worms.isNotEmpty()) { + val itr = worms.iterator() + + for (worm in itr) { + // taper size + var wormRadius = worm.size / 2.0 + + val taperFactor = if (worm.length < data.wormTaperDistance) + sin(0.5 * PI * worm.length / data.wormTaperDistance) + else if (worm.goalLength - worm.length < data.wormTaperDistance) + sin(0.5 * PI * (worm.goalLength - worm.length) / data.wormTaperDistance) + else + 1.0 + + wormRadius *= taperFactor + + // carve out worm area + val size = ceil(wormRadius) + var dx = -size + var dy = -size + + while (dx <= size) { + while (dy <= size) { + val m = sqrt(dx * dx + dy * dy) + + if (m <= wormRadius) { + // TODO: maybe roundToInt()? + val x = (dx + worm.pos.x).toInt() + val y = (dy + worm.pos.y).toInt() + + if (isInside(x, y)) { + this[x, y] = this[x, y].coerceAtLeast(wormRadius - m) + } + } + + dy += 1.0 + } + + dx += 1.0 + } + + // move the worm, slowing down a bit as we + // reach the ends to reduce stutter + val thisSpeed = (data.wormSpeed * taperFactor).coerceAtLeast(0.75) + worm.length += thisSpeed + worm.pos += Vector2d(cos(worm.angle) * thisSpeed, sin(worm.angle) * thisSpeed) + + // maybe set new goal angle + // wormy-o! + if (staticRandomDouble(worm.pos.x, worm.pos.y, parameters.seed, 1) < data.wormTurnChance * thisSpeed) { + worm.goalAngle = positiveModulo(linearInterpolation(staticRandomDouble(worm.pos.x, worm.pos.y, parameters.seed, 2), data.wormAngleRange.x, data.wormAngleRange.y), PI * 2.0) + } + + // vroom? + if (worm.angle != worm.goalAngle) { + // turn the worm toward goal angle + var angleDiff = worm.goalAngle - worm.angle + + // stop if we're close enough + if (angleDiff.absoluteValue < data.wormTurnRate * thisSpeed) + worm.angle = worm.goalAngle + else { + // turn the shortest angular distance + if (angleDiff.absoluteValue > PI * 2.0) + angleDiff = -angleDiff + + worm.angle = positiveModulo(worm.angle + data.wormTurnRate * thisSpeed * angleDiff.sign, 2.0 * PI) + } + } + + if (worm.length >= worm.goalLength) { + itr.remove() + } + } + } + } + + private fun isInside(x: Int, y: Int): Boolean { + val diffX = x - sector.x + val diffY = y - sector.y + + return diffX in 0 until data.sectorSize && + diffY in 0 until data.sectorSize + } + + operator fun get(x: Int, y: Int): Double { + val get = values[x - sector.x, y - sector.y] + return if (get > 0.0) get else -maxValue + } + + private operator fun set(x: Int, y: Int, value: Double) { + values[x - sector.x, y - sector.y] = value + } + } + + private val sectors = Caffeine.newBuilder() + .maximumSize(data.lruCacheSize.toLong()) + .expireAfterAccess(Duration.ofMinutes(5)) + .scheduler(Scheduler.systemScheduler()) + .executor(Starbound.EXECUTOR) + .build(::Sector) + + override fun get(x: Int, y: Int): Double { + val sector = Vector2i(x - positiveModulo(x, data.sectorSize), y - positiveModulo(y, data.sectorSize)) + return sectors[sector][x, y] + } + + override val type: TerrainSelectorType + get() = TerrainSelectorType.WORM_CAVE +}