Deterministic dungeon containers

This commit is contained in:
DBotThePony 2024-04-18 21:05:16 +07:00
parent 7857b8821e
commit fc6938fc35
Signed by: DBot
GPG Key ID: DCC23B5715498507
4 changed files with 68 additions and 36 deletions

View File

@ -1,7 +1,7 @@
# Modding changes # Modding and behavior changes
This document briefly documents what have been added (or removed) regarding modding capabilities This document briefly documents what have been added (or removed) regarding modding capabilities or engine behavior(s)
## JSON additions ## JSON additions
@ -35,7 +35,6 @@ This document briefly documents what have been added (or removed) regarding modd
* 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.
#### Dungeons #### Dungeons
* All brushes are now deterministic, and will produce _exact_ results given same seed (this fixes dungeons being generated differently on each machine despite players visiting exactly same coordinates in universe)
* `front` and `back` brushes now can properly accept detailed data as json object on second position (e.g. `["front", { "material": ... }]`), with following structure (previously, due to oversight in code, it was impossible to specify this structure through any means, because brush definition itself can't be an object): * `front` and `back` brushes now can properly accept detailed data as json object on second position (e.g. `["front", { "material": ... }]`), with following structure (previously, due to oversight in code, it was impossible to specify this structure through any means, because brush definition itself can't be an object):
```kotlin ```kotlin
val material: Registry.Ref<TileDefinition> = BuiltinMetaMaterials.EMPTY.ref val material: Registry.Ref<TileDefinition> = BuiltinMetaMaterials.EMPTY.ref
@ -101,4 +100,15 @@ val color: TileColor = TileColor.DEFAULT
* Added `animator.sounds(): List<string>` * Added `animator.sounds(): List<string>`
* Added `animator.effects(): List<string>` * Added `animator.effects(): List<string>`
* Added `animator.hasEffect(effect: string): boolean` * Added `animator.hasEffect(effect: string): boolean`
* Added `animator.parts(): List<string>` * Added `animator.parts(): List<string>`
## Behavior
---------------
### Worldgen
* Major dungeon placement on planets is now deterministic
* Container item population in dungeons is now deterministic and is based on dungeon seed
#### Dungeons
* All brushes are now deterministic

View File

@ -484,7 +484,7 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar
val placedObjects = placedObjects.entries val placedObjects = placedObjects.entries
.map { (pos, data) -> .map { (pos, data) ->
WorldObject.create(data.prototype, pos, data.parameters) to data.direction WorldObject.create(data.prototype, pos, data.parameters).also { it?.randomize(random) } to data.direction
} }
.filter { it.first != null } .filter { it.first != null }

View File

@ -35,6 +35,7 @@ import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
import java.util.random.RandomGenerator
class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(config) { class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(config) {
var opened by networkedSignedInt().also { networkGroup.upstream.add(it) } var opened by networkedSignedInt().also { networkGroup.upstream.add(it) }
@ -109,6 +110,45 @@ class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(co
return data return data
} }
private fun randomizeContents(random: RandomGenerator) {
var level = world.template.threatLevel
level = lookupProperty("level") { JsonPrimitive(level) }.asDouble
level += lookupProperty("levelAdjustment") { JsonPrimitive(0.0) }.asDouble
val initialItems = lookupProperty("initialItems")
if (!initialItems.isJsonNull) {
for (item in initialItems.asJsonArray) {
items.add(ItemDescriptor(item).build(level, random.nextLong(), random))
}
}
val treasurePools = lookupProperty("treasurePools")
if (!treasurePools.isJsonNull) {
val get = treasurePools.asJsonArray.random(random).asString
val treasurePool = Registries.treasurePools[get]
if (treasurePool == null) {
LOGGER.error("Unknown treasure pool $get! Can't generate container contents at $tilePosition.")
} else {
for (item in treasurePool.value.evaluate(random, level)) {
val leftover = items.add(item)
if (leftover.isNotEmpty) {
LOGGER.warn("Tried to overfill container at $tilePosition")
lostItems.add(leftover)
}
}
}
}
}
override fun randomize(random: RandomGenerator) {
super.randomize(random)
randomizeContents(random)
}
override fun onJoinWorld(world: World<*, *>) { override fun onJoinWorld(world: World<*, *>) {
if (!isRemote) if (!isRemote)
isInteractive = true isInteractive = true
@ -119,38 +159,12 @@ class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(co
if (isInitialized) return if (isInitialized) return
isInitialized = true isInitialized = true
var level = world.template.threatLevel val seed = lookupProperty("treasureSeed")
val seed = lookupProperty("treasureSeed") { JsonPrimitive(world.random.nextLong()) }.asLong
level = lookupProperty("level") { JsonPrimitive(level) }.asDouble
level += lookupProperty("levelAdjustment") { JsonPrimitive(0.0) }.asDouble
val initialItems = lookupProperty("initialItems") if (seed.isJsonNull) {
randomizeContents(world.random)
if (!initialItems.isJsonNull) { } else {
for (item in initialItems.asJsonArray) { randomizeContents(random(seed.asLong))
items.add(ItemDescriptor(item).build(level, seed))
}
}
val treasurePools = lookupProperty("treasurePools")
if (!treasurePools.isJsonNull) {
val random = random(seed)
val get = treasurePools.asJsonArray.random(random).asString
val treasurePool = Registries.treasurePools[get]
if (treasurePool == null) {
LOGGER.error("Unknown treasure pool $get! Can't generate container contents at $tilePosition.")
} else {
for (item in treasurePool.value.evaluate(random, level)) {
val leftover = items.add(item)
if (leftover.isNotEmpty) {
LOGGER.warn("Tried to overfill container at $tilePosition")
lostItems.add(leftover)
}
}
}
} }
} }
} }

View File

@ -86,6 +86,7 @@ import ru.dbotthepony.kstarbound.world.entities.Animator
import ru.dbotthepony.kstarbound.world.entities.wire.WireConnection import ru.dbotthepony.kstarbound.world.entities.wire.WireConnection
import java.io.DataOutputStream import java.io.DataOutputStream
import java.util.HashMap import java.util.HashMap
import java.util.random.RandomGenerator
open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntity() { open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntity() {
open fun deserialize(data: JsonObject) { open fun deserialize(data: JsonObject) {
@ -134,6 +135,13 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
return into return into
} }
/**
* called by DungeonWorld to deterministically randomize parameters
*/
open fun randomize(random: RandomGenerator) {
}
protected val orientationLazies = ArrayList<ManualLazy<*>>() protected val orientationLazies = ArrayList<ManualLazy<*>>()
protected val parametersLazies = ArrayList<ManualLazy<*>>() protected val parametersLazies = ArrayList<ManualLazy<*>>()
protected val spacesLazies = ArrayList<ManualLazy<*>>() protected val spacesLazies = ArrayList<ManualLazy<*>>()