Implemented all terrain selectors
This commit is contained in:
parent
c8683a15bd
commit
602c21edfc
20
ADDITIONS.md
20
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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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()))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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() }
|
||||
|
@ -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]"
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue
Block a user