Implemented all terrain selectors

This commit is contained in:
DBotThePony 2024-03-28 18:32:52 +07:00
parent c8683a15bd
commit 602c21edfc
Signed by: DBot
GPG Key ID: DCC23B5715498507
26 changed files with 844 additions and 78 deletions

View File

@ -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)

View File

@ -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

View File

@ -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<MonsterTypeDefinition>("monster type").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val worldObjects = Registry<ObjectDefinition>("world object").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val biomes = Registry<BiomeDefinition>("biome").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val terrainSelectors = Registry<TerrainSelectorFactory<*, *>>("terrain selector").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val terrainSelectors = Registry<TerrainSelectorType.Factory<*, *>>("terrain selector").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val grassVariants = Registry<GrassVariant.Data>("grass variant").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val treeStemVariants = Registry<TreeVariant.StemData>("tree stem variant").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val treeFoliageVariants = Registry<TreeVariant.FoliageData>("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)

View File

@ -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

View File

@ -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
}
}

View File

@ -311,6 +311,7 @@ class Image private constructor(
.weigher<IStarboundFile, ByteBuffer> { 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)

View File

@ -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<Double, Double> {
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
}
}

View File

@ -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<AbstractTerrainSelector<*>>()
val foregroundOreSelector = ArrayList<AbstractTerrainSelector<*>>()
@ -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()))))
}
}
}

View File

@ -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 {

View File

@ -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

View File

@ -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<TaskCluster>(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<Future<Grid>>()
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 {

View File

@ -270,7 +270,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
ticks++
mailbox.executeQueuedTasks()
ForkJoinPool.commonPool().submit(ParallelPerform(dynamicEntities.spliterator(), { if (!it.isRemote) it.movement.move() })).join()
Starbound.EXECUTOR.submit(ParallelPerform(dynamicEntities.spliterator(), { if (!it.isRemote) it.movement.move() })).join()
mailbox.executeQueuedTasks()
entities.values.forEach { it.think() }

View File

@ -5,7 +5,7 @@ import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.world.TerrainSelectorParameters
abstract class AbstractTerrainSelector<D : Any>(val name: String, val config: D, val parameters: TerrainSelectorParameters) {
abstract class AbstractTerrainSelector<D : Any>(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<D : Any>(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<D : Any>(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<D : Any>(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]"
}
}

View File

@ -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<ComparingTerrainSelector.Data>(data, parameters) {
@JsonFactory
data class Data(val sources: ImmutableList<JsonObject>)
protected val sources = ArrayList<AbstractTerrainSelector<*>>()
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
}
}

View File

@ -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<ConstantTerrainSelector.Data>(name, data, parameters) {
class ConstantTerrainSelector(data: Data, parameters: TerrainSelectorParameters) : AbstractTerrainSelector<ConstantTerrainSelector.Data>(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
}
}

View File

@ -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<DisplacementTerrainSelector.Data>(name, data, parameters) {
class DisplacementTerrainSelector(data: Data, parameters: TerrainSelectorParameters) : AbstractTerrainSelector<DisplacementTerrainSelector.Data>(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

View File

@ -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<FlatSurfaceTerrainSelector.Data>(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
}

View File

@ -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<IslandSurfaceTerrainSelector.Data>(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<Int, Column>(::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
}

View File

@ -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<KarstCaveTerrainSelector.Data>(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<Int, Layer>(::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<Vector2i, Sector>(::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
}

View File

@ -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<MixTerrainSelector.Data>(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
}

View File

@ -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<PerlinTerrainSelector.Data>(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
}

View File

@ -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<RidgeBlocksTerrainSelector.Data>(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
}

View File

@ -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<RotateTerrainSelector.Data>(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
}

View File

@ -2,8 +2,8 @@ package ru.dbotthepony.kstarbound.world.terrain
import ru.dbotthepony.kstarbound.defs.world.TerrainSelectorParameters
class TerrainSelectorFactory<D : Any, out T : AbstractTerrainSelector<D>>(val name: String, private val data: D, private val factory: (String, D, TerrainSelectorParameters) -> T) {
class TerrainSelectorFactory<D : Any, out T : AbstractTerrainSelector<D>>(private val data: D, private val factory: (D, TerrainSelectorParameters) -> T) {
fun create(parameters: TerrainSelectorParameters): T {
return factory(name, data, parameters)
return factory(data, parameters)
}
}

View File

@ -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 <reified D : Any, T : AbstractTerrainSelector<D>> data(noinline factory: (D, TerrainSelectorParameters) -> T): Data<D, T> {
return Data(TypeToken.get(D::class.java), factory)
}
enum class TerrainSelectorType(val jsonName: String) {
CONSTANT("constant"),
DISPLACEMENT("displacement"),
private data class Data<D : Any, out T : AbstractTerrainSelector<D>>(val token: TypeToken<D>, val factory: (D, TerrainSelectorParameters) -> T) {
val adapter: TypeAdapter<D> 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<D : Any, out T : AbstractTerrainSelector<D>>(private val data: D, private val factory: (D, TerrainSelectorParameters) -> T) {
fun create(parameters: TerrainSelectorParameters): T {
return factory(data, parameters)
}
}
companion object : TypeAdapter<AbstractTerrainSelector<*>>(), TypeAdapterFactory {
override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
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<Any>))
}
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)
}
}
}

View File

@ -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<WormCaveTerrainSelector.Data>(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<Worm>()
// 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<Vector2i, Sector>(::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
}