Implemented all terrain selectors
This commit is contained in:
parent
c8683a15bd
commit
602c21edfc
ADDITIONS.md
src/main/kotlin/ru/dbotthepony/kstarbound
Main.ktRegistries.ktStarbound.kt
defs
util/random
world
CoordinateMapper.ktLightCalculator.ktWorld.kt
terrain
AbstractTerrainSelector.ktComparingTerrainSelector.ktConstantTerrainSelector.ktDisplacementTerrainSelector.ktFlatSurfaceTerrainSelector.ktIslandSurfaceTerrainSelector.ktKarstCaveTerrainSelector.ktMixTerrainSelector.ktPerlinTerrainSelector.ktRidgeBlocksTerrainSelector.ktRotateTerrainSelector.ktTerrainSelectorFactory.ktTerrainSelectorType.ktWormCaveTerrainSelector.kt
20
ADDITIONS.md
20
ADDITIONS.md
@ -1,15 +1,23 @@
|
|||||||
|
|
||||||
## JSON additions
|
## JSON additions
|
||||||
|
|
||||||
|
---------------
|
||||||
|
|
||||||
### Worldgen
|
### Worldgen
|
||||||
* Where applicable, Perlin noise now can have custom seed specified
|
* Where applicable, Perlin noise now can have custom seed specified
|
||||||
* Change above allows to explicitly specify universe seed (as celestial.config:systemTypePerlin:seed)
|
* 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)
|
* Perlin noise now can be of arbitrary scale (defaults to `512`, specified with `scale` key, integer type, >=16)
|
||||||
|
|
||||||
#### Terrain
|
#### Terrain
|
||||||
* Nested terrain selectors now get their unique seeds (displacement selector can now properly be nested inside other displacement selector)
|
* `mix` terrain selector got `mixSeedBias`, `aSeedBias` and `bSeedBias` fields, whose deviate respective selectors seeds (default to `0`)
|
||||||
* Previously, all nested terrain selectors were based off the same seed
|
* `displacement` terrain selector has `seedBias` added, which deviate seed of `source` selector (default to `0`)
|
||||||
* displacement terrain selector has xClamp added, works like yClamp
|
* `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
|
#### Biomes
|
||||||
* Tree biome placeables now have `variantsRange` (defaults to `[1, 1]`) and `subVariantsRange` (defaults to `[2, 2]`)
|
* 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
|
* 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.
|
* Original engine always generates two tree types when processing placeable items, new engine however, allows to generate any number of trees.
|
||||||
|
|
||||||
|
---------------
|
||||||
|
|
||||||
### player.config
|
### player.config
|
||||||
* Inventory bags are no longer limited to 255 slots
|
* 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)
|
* 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.LegacyWorldStorage
|
||||||
import ru.dbotthepony.kstarbound.server.world.ServerUniverse
|
import ru.dbotthepony.kstarbound.server.world.ServerUniverse
|
||||||
import ru.dbotthepony.kstarbound.server.world.ServerWorld
|
import ru.dbotthepony.kstarbound.server.world.ServerWorld
|
||||||
|
import ru.dbotthepony.kstarbound.util.random.staticRandomDouble
|
||||||
import ru.dbotthepony.kstarbound.world.WorldGeometry
|
import ru.dbotthepony.kstarbound.world.WorldGeometry
|
||||||
import java.io.BufferedInputStream
|
import java.io.BufferedInputStream
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
|
@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound
|
|||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.JsonElement
|
import com.google.gson.JsonElement
|
||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
|
import com.google.gson.JsonSyntaxException
|
||||||
import com.google.gson.TypeAdapterFactory
|
import com.google.gson.TypeAdapterFactory
|
||||||
import com.google.gson.stream.JsonReader
|
import com.google.gson.stream.JsonReader
|
||||||
import org.apache.logging.log4j.LogManager
|
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.GrassVariant
|
||||||
import ru.dbotthepony.kstarbound.defs.world.TreeVariant
|
import ru.dbotthepony.kstarbound.defs.world.TreeVariant
|
||||||
import ru.dbotthepony.kstarbound.defs.world.BiomeDefinition
|
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.world.terrain.TerrainSelectorType
|
||||||
import ru.dbotthepony.kstarbound.util.AssetPathStack
|
import ru.dbotthepony.kstarbound.util.AssetPathStack
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -78,7 +78,7 @@ object Registries {
|
|||||||
val monsterTypes = Registry<MonsterTypeDefinition>("monster type").also(registriesInternal::add).also { adapters.add(it.adapter()) }
|
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 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 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 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 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()) }
|
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 ->
|
return files.map { listedFile ->
|
||||||
Starbound.EXECUTOR.submit {
|
Starbound.EXECUTOR.submit {
|
||||||
try {
|
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 {
|
||||||
terrainSelectors.add(factory.name, factory)
|
terrainSelectors.add(name, factory)
|
||||||
}
|
}
|
||||||
} catch (err: Exception) {
|
} catch (err: Exception) {
|
||||||
LOGGER.error("Loading terrain selector $listedFile", err)
|
LOGGER.error("Loading terrain selector $listedFile", err)
|
||||||
|
@ -125,7 +125,7 @@ object Starbound : ISBFileLocator {
|
|||||||
})
|
})
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
val EXECUTOR: ExecutorService = ForkJoinPool.commonPool()
|
val EXECUTOR: ForkJoinPool = ForkJoinPool.commonPool()
|
||||||
@JvmField
|
@JvmField
|
||||||
val COROUTINE_EXECUTOR = EXECUTOR.asCoroutineDispatcher()
|
val COROUTINE_EXECUTOR = EXECUTOR.asCoroutineDispatcher()
|
||||||
@JvmField
|
@JvmField
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package ru.dbotthepony.kstarbound.defs
|
package ru.dbotthepony.kstarbound.defs
|
||||||
|
|
||||||
import com.google.gson.stream.JsonWriter
|
|
||||||
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
|
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
|
||||||
|
import ru.dbotthepony.kstarbound.json.builder.JsonAlias
|
||||||
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
|
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
|
||||||
|
|
||||||
@JsonFactory
|
@JsonFactory
|
||||||
@ -25,10 +25,12 @@ data class PerlinNoiseParameters(
|
|||||||
enum class Type(override val jsonName: String) : IStringSerializable {
|
enum class Type(override val jsonName: String) : IStringSerializable {
|
||||||
PERLIN("perlin"),
|
PERLIN("perlin"),
|
||||||
BILLOW("billow"),
|
BILLOW("billow"),
|
||||||
RIDGED_MULTI("ridgedmulti");
|
RIDGED_MULTI("ridgedMulti");
|
||||||
|
|
||||||
|
private val lower = jsonName.lowercase()
|
||||||
|
|
||||||
override fun match(name: String): Boolean {
|
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() }
|
.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 МиБ */))
|
.maximumWeight((Runtime.getRuntime().maxMemory() / 4L).coerceIn(1_024L * 1_024L * 32L /* 32 МиБ */, 1_024L * 1_024L * 256L /* 256 МиБ */))
|
||||||
.scheduler(Scheduler.systemScheduler())
|
.scheduler(Scheduler.systemScheduler())
|
||||||
|
.executor(Starbound.EXECUTOR)
|
||||||
.buildAsync(CacheLoader {
|
.buildAsync(CacheLoader {
|
||||||
val getWidth = intArrayOf(0)
|
val getWidth = intArrayOf(0)
|
||||||
val getHeight = intArrayOf(0)
|
val getHeight = intArrayOf(0)
|
||||||
|
@ -2,11 +2,15 @@ package ru.dbotthepony.kstarbound.defs.world
|
|||||||
|
|
||||||
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
|
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
|
||||||
import java.util.random.RandomGenerator
|
import java.util.random.RandomGenerator
|
||||||
|
import kotlin.math.PI
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.sin
|
||||||
|
|
||||||
@JsonFactory
|
@JsonFactory
|
||||||
data class TerrainSelectorParameters(
|
data class TerrainSelectorParameters(
|
||||||
val worldWidth: Int,
|
val worldWidth: Int,
|
||||||
val baseHeight: Double,
|
val baseHeight: Double,
|
||||||
|
val worldHeight: Int = 0,
|
||||||
val seed: Long = 0L,
|
val seed: Long = 0L,
|
||||||
val commonality: Double = 0.0
|
val commonality: Double = 0.0
|
||||||
) {
|
) {
|
||||||
@ -28,4 +32,12 @@ data class TerrainSelectorParameters(
|
|||||||
fun withRandom(randomGenerator: RandomGenerator): TerrainSelectorParameters {
|
fun withRandom(randomGenerator: RandomGenerator): TerrainSelectorParameters {
|
||||||
return copy().also { it.random = randomGenerator }
|
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.Registries
|
||||||
import ru.dbotthepony.kstarbound.Starbound
|
import ru.dbotthepony.kstarbound.Starbound
|
||||||
import ru.dbotthepony.kstarbound.world.terrain.AbstractTerrainSelector
|
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.json.builder.JsonFactory
|
||||||
import ru.dbotthepony.kstarbound.util.ListInterner
|
import ru.dbotthepony.kstarbound.util.ListInterner
|
||||||
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
|
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
|
||||||
import ru.dbotthepony.kstarbound.util.random.nextRange
|
import ru.dbotthepony.kstarbound.util.random.nextRange
|
||||||
|
import ru.dbotthepony.kstarbound.world.terrain.TerrainSelectorType
|
||||||
import java.util.random.RandomGenerator
|
import java.util.random.RandomGenerator
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlin.properties.Delegates
|
import kotlin.properties.Delegates
|
||||||
@ -249,17 +249,17 @@ class WorldLayout {
|
|||||||
var foregroundCaveSelector: AbstractTerrainSelector<*>? = null
|
var foregroundCaveSelector: AbstractTerrainSelector<*>? = null
|
||||||
var backgroundCaveSelector: 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 terrain = terrainBase.withSeed(random.nextLong())
|
||||||
val fg = terrainBase.withSeed(random.nextLong())
|
val fg = terrainBase.withSeed(random.nextLong())
|
||||||
val bg = terrainBase.withSeed(random.nextLong())
|
val bg = terrainBase.withSeed(random.nextLong())
|
||||||
|
|
||||||
if (params.terrainSelector != null)
|
if (params.terrainSelector != null)
|
||||||
terrainSelector = terrainSelectors.intern(createNamedTerrainSelector(params.terrainSelector, terrain))
|
terrainSelector = terrainSelectors.intern(TerrainSelectorType.named(params.terrainSelector, terrain))
|
||||||
if (params.fgCaveSelector != null)
|
if (params.fgCaveSelector != null)
|
||||||
foregroundCaveSelector = terrainSelectors.intern(createNamedTerrainSelector(params.fgCaveSelector, fg))
|
foregroundCaveSelector = terrainSelectors.intern(TerrainSelectorType.named(params.fgCaveSelector, fg))
|
||||||
if (params.bgCaveSelector != null)
|
if (params.bgCaveSelector != null)
|
||||||
backgroundCaveSelector = terrainSelectors.intern(createNamedTerrainSelector(params.bgCaveSelector, bg))
|
backgroundCaveSelector = terrainSelectors.intern(TerrainSelectorType.named(params.bgCaveSelector, bg))
|
||||||
|
|
||||||
val subBlockSelector = ArrayList<AbstractTerrainSelector<*>>()
|
val subBlockSelector = ArrayList<AbstractTerrainSelector<*>>()
|
||||||
val foregroundOreSelector = ArrayList<AbstractTerrainSelector<*>>()
|
val foregroundOreSelector = ArrayList<AbstractTerrainSelector<*>>()
|
||||||
@ -272,18 +272,18 @@ class WorldLayout {
|
|||||||
|
|
||||||
if (params.subBlockSelector != null) {
|
if (params.subBlockSelector != null) {
|
||||||
for (i in 0 until biome.subBlocks.size) {
|
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) {
|
for ((ore, commonality) in biome.ores) {
|
||||||
val oreParams = terrainBase.withCommonality(commonality)
|
val oreParams = terrainBase.withCommonality(commonality)
|
||||||
|
|
||||||
if (params.fgOreSelector != null) {
|
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) {
|
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.*
|
||||||
import java.util.random.RandomGenerator
|
import java.util.random.RandomGenerator
|
||||||
import java.util.stream.IntStream
|
import java.util.stream.IntStream
|
||||||
import kotlin.NoSuchElementException
|
|
||||||
import kotlin.collections.List
|
|
||||||
import kotlin.math.absoluteValue
|
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
import kotlin.math.floor
|
import kotlin.math.floor
|
||||||
import kotlin.math.ln
|
import kotlin.math.ln
|
||||||
@ -79,11 +76,11 @@ fun staticRandom32(vararg values: Any): Int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun staticRandomFloat(vararg values: Any): Float {
|
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 {
|
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 {
|
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
|
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 {
|
fun positiveModulo(a: Float, b: Int): Float {
|
||||||
val result = a % b
|
val result = a % b
|
||||||
return if (result < 0f) result + b else result
|
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.util.IStruct4f
|
||||||
import ru.dbotthepony.kommons.math.RGBAColor
|
import ru.dbotthepony.kommons.math.RGBAColor
|
||||||
import ru.dbotthepony.kommons.math.linearInterpolation
|
import ru.dbotthepony.kommons.math.linearInterpolation
|
||||||
|
import ru.dbotthepony.kstarbound.Starbound
|
||||||
import ru.dbotthepony.kstarbound.world.api.ICellAccess
|
import ru.dbotthepony.kstarbound.world.api.ICellAccess
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.util.concurrent.Callable
|
import java.util.concurrent.Callable
|
||||||
@ -387,7 +388,7 @@ class LightCalculator(val parent: ICellAccess, val width: Int, val height: Int)
|
|||||||
val thread = Thread.currentThread()
|
val thread = Thread.currentThread()
|
||||||
// calculate k-means clusters of point lights
|
// calculate k-means clusters of point lights
|
||||||
// to effectively utilize CPU cores
|
// 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 clusters = ArrayList<TaskCluster>(clusterCount)
|
||||||
val startingPoints = IntArraySet()
|
val startingPoints = IntArraySet()
|
||||||
// val rand = Random(System.nanoTime())
|
// 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>>()
|
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()) {
|
while (tasks.isNotEmpty()) {
|
||||||
tasks.removeIf {
|
tasks.removeIf {
|
||||||
|
@ -270,7 +270,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
|
|||||||
ticks++
|
ticks++
|
||||||
mailbox.executeQueuedTasks()
|
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()
|
mailbox.executeQueuedTasks()
|
||||||
|
|
||||||
entities.values.forEach { it.think() }
|
entities.values.forEach { it.think() }
|
||||||
|
@ -5,7 +5,7 @@ import ru.dbotthepony.kommons.gson.set
|
|||||||
import ru.dbotthepony.kstarbound.Starbound
|
import ru.dbotthepony.kstarbound.Starbound
|
||||||
import ru.dbotthepony.kstarbound.defs.world.TerrainSelectorParameters
|
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
|
// Returns a float signifying the "solid-ness" of a block, >= 0.0 should be
|
||||||
// considered solid, < 0.0 should be considered open space.
|
// considered solid, < 0.0 should be considered open space.
|
||||||
abstract operator fun get(x: Int, y: Int): Double
|
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 {
|
fun toJson(): JsonObject {
|
||||||
val result = JsonObject()
|
val result = JsonObject()
|
||||||
result["name"] = name
|
|
||||||
result["type"] = type.jsonName
|
result["type"] = type.jsonName
|
||||||
result["config"] = Starbound.gson.toJsonTree(config)
|
result["config"] = Starbound.gson.toJsonTree(data)
|
||||||
result["parameters"] = Starbound.gson.toJsonTree(parameters)
|
result["parameters"] = Starbound.gson.toJsonTree(parameters)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@ -29,12 +28,11 @@ abstract class AbstractTerrainSelector<D : Any>(val name: String, val config: D,
|
|||||||
return false
|
return false
|
||||||
|
|
||||||
other as AbstractTerrainSelector<*>
|
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 {
|
private val hash by lazy {
|
||||||
var h = name.hashCode()
|
var h = data.hashCode()
|
||||||
h = h * 31 + config.hashCode()
|
|
||||||
h = h * 31 + parameters.hashCode()
|
h = h * 31 + parameters.hashCode()
|
||||||
h
|
h
|
||||||
}
|
}
|
||||||
@ -44,6 +42,6 @@ abstract class AbstractTerrainSelector<D : Any>(val name: String, val config: D,
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
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.defs.world.TerrainSelectorParameters
|
||||||
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
|
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
|
@JsonFactory
|
||||||
data class Data(val value: Double)
|
data class Data(val value: Double)
|
||||||
|
|
||||||
@ -11,6 +11,6 @@ class ConstantTerrainSelector(name: String, data: Data, parameters: TerrainSelec
|
|||||||
get() = TerrainSelectorType.CONSTANT
|
get() = TerrainSelectorType.CONSTANT
|
||||||
|
|
||||||
override fun get(x: Int, y: Int): Double {
|
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 ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
|
||||||
import kotlin.math.roundToInt
|
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
|
@JsonFactory
|
||||||
data class Data(
|
data class Data(
|
||||||
val xType: PerlinNoiseParameters.Type,
|
val xType: PerlinNoiseParameters.Type,
|
||||||
@ -40,6 +40,7 @@ class DisplacementTerrainSelector(name: String, data: Data, parameters: TerrainS
|
|||||||
val xClamp: Vector2d? = null,
|
val xClamp: Vector2d? = null,
|
||||||
val xClampSmoothing: Double = 0.0,
|
val xClampSmoothing: Double = 0.0,
|
||||||
|
|
||||||
|
val seedBias: Long = 0L,
|
||||||
val source: JsonObject,
|
val source: JsonObject,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -50,7 +51,6 @@ class DisplacementTerrainSelector(name: String, data: Data, parameters: TerrainS
|
|||||||
init {
|
init {
|
||||||
// This allows to have multiple nested displacement selectors with different seeds
|
// 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
|
// original engine isn't capable of this because nested selectors will have the same seed
|
||||||
val parameters = parameters.withRandom()
|
|
||||||
val random = parameters.random()
|
val random = parameters.random()
|
||||||
|
|
||||||
xFn = AbstractPerlinNoise.of(PerlinNoiseParameters(
|
xFn = AbstractPerlinNoise.of(PerlinNoiseParameters(
|
||||||
@ -77,39 +77,39 @@ class DisplacementTerrainSelector(name: String, data: Data, parameters: TerrainS
|
|||||||
|
|
||||||
xFn.init(random.nextLong())
|
xFn.init(random.nextLong())
|
||||||
yFn.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 {
|
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 {
|
private fun clampX(v: Double): Double {
|
||||||
if (config.xClamp == null)
|
if (data.xClamp == null)
|
||||||
return v
|
return v
|
||||||
|
|
||||||
if (config.xClampSmoothing == 0.0)
|
if (data.xClampSmoothing == 0.0)
|
||||||
return v.coerceIn(config.xClamp.x, config.xClamp.y)
|
return v.coerceIn(data.xClamp.x, data.xClamp.y)
|
||||||
|
|
||||||
return 0.2 * ((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 * config.xClampSmoothing).coerceIn(config.xClamp.x, config.xClamp.y)
|
+ (v - 0.5 * data.xClampSmoothing).coerceIn(data.xClamp.x, data.xClamp.y)
|
||||||
+ (v).coerceIn(config.xClamp.x, config.xClamp.y)
|
+ (v).coerceIn(data.xClamp.x, data.xClamp.y)
|
||||||
+ (v + 0.5 * config.xClampSmoothing).coerceIn(config.xClamp.x, config.xClamp.y)
|
+ (v + 0.5 * data.xClampSmoothing).coerceIn(data.xClamp.x, data.xClamp.y)
|
||||||
+ (v + config.xClampSmoothing).coerceIn(config.xClamp.x, config.xClamp.y))
|
+ (v + data.xClampSmoothing).coerceIn(data.xClamp.x, data.xClamp.y))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun clampY(v: Double): Double {
|
private fun clampY(v: Double): Double {
|
||||||
if (config.yClamp == null)
|
if (data.yClamp == null)
|
||||||
return v
|
return v
|
||||||
|
|
||||||
if (config.xClampSmoothing == 0.0)
|
if (data.xClampSmoothing == 0.0)
|
||||||
return v.coerceIn(config.yClamp.x, config.yClamp.y)
|
return v.coerceIn(data.yClamp.x, data.yClamp.y)
|
||||||
|
|
||||||
return 0.2 * ((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 * config.yClampSmoothing).coerceIn(config.yClamp.x, config.yClamp.y)
|
+ (v - 0.5 * data.yClampSmoothing).coerceIn(data.yClamp.x, data.yClamp.y)
|
||||||
+ (v).coerceIn(config.yClamp.x, config.yClamp.y)
|
+ (v).coerceIn(data.yClamp.x, data.yClamp.y)
|
||||||
+ (v + 0.5 * config.yClampSmoothing).coerceIn(config.yClamp.x, config.yClamp.y)
|
+ (v + 0.5 * data.yClampSmoothing).coerceIn(data.yClamp.x, data.yClamp.y)
|
||||||
+ (v + config.yClampSmoothing).coerceIn(config.yClamp.x, config.yClamp.y))
|
+ (v + data.yClampSmoothing).coerceIn(data.yClamp.x, data.yClamp.y))
|
||||||
}
|
}
|
||||||
|
|
||||||
override val type: TerrainSelectorType
|
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
|
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 {
|
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.Starbound
|
||||||
import ru.dbotthepony.kstarbound.defs.world.TerrainSelectorParameters
|
import ru.dbotthepony.kstarbound.defs.world.TerrainSelectorParameters
|
||||||
|
|
||||||
fun createNamedTerrainSelector(name: String, parameters: TerrainSelectorParameters): AbstractTerrainSelector<*> {
|
private inline fun <reified D : Any, T : AbstractTerrainSelector<D>> data(noinline factory: (D, TerrainSelectorParameters) -> T): Data<D, T> {
|
||||||
return Registries.terrainSelectors.getOrThrow(name).value.create(parameters)
|
return Data(TypeToken.get(D::class.java), factory)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class TerrainSelectorType(val jsonName: String) {
|
private data class Data<D : Any, out T : AbstractTerrainSelector<D>>(val token: TypeToken<D>, val factory: (D, TerrainSelectorParameters) -> T) {
|
||||||
CONSTANT("constant"),
|
val adapter: TypeAdapter<D> by lazy { Starbound.gson.getAdapter(token) }
|
||||||
DISPLACEMENT("displacement"),
|
}
|
||||||
|
|
||||||
|
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 {
|
companion object : TypeAdapter<AbstractTerrainSelector<*>>(), TypeAdapterFactory {
|
||||||
override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
|
override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
|
||||||
if (AbstractTerrainSelector::class.java.isAssignableFrom(type.rawType)) {
|
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) }
|
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<*>? {
|
override fun read(`in`: JsonReader): AbstractTerrainSelector<*>? {
|
||||||
if (`in`.consumeNull())
|
if (`in`.consumeNull())
|
||||||
return null
|
return null
|
||||||
@ -48,29 +75,24 @@ enum class TerrainSelectorType(val jsonName: String) {
|
|||||||
return create(objects.read(`in`))
|
return create(objects.read(`in`))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createFactory(json: JsonObject): TerrainSelectorFactory<*, *> {
|
fun factory(json: JsonObject): Factory<*, *> {
|
||||||
val name = json["name"]?.asString ?: ""
|
|
||||||
val type = json["type"]?.asString?.lowercase() ?: throw JsonSyntaxException("Missing 'type' element of terrain json")
|
val type = json["type"]?.asString?.lowercase() ?: throw JsonSyntaxException("Missing 'type' element of terrain json")
|
||||||
|
|
||||||
when (type) {
|
for (value in entries) {
|
||||||
CONSTANT.jsonName -> {
|
if (value.lowercase == type) {
|
||||||
return TerrainSelectorFactory(name, Starbound.gson.fromJson(json, ConstantTerrainSelector.Data::class.java), ::ConstantTerrainSelector)
|
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<*> {
|
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<*> {
|
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