diff --git a/gradle.properties b/gradle.properties index b5e28fed..0304721e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,7 @@ kotlin.code.style=official org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m kotlinVersion=1.9.0 -kommonsVersion=2.5.0 +kommonsVersion=2.7.16 ffiVersion=2.2.13 lwjglVersion=3.3.0 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index 3f7977ef..b0b45234 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -1,16 +1,20 @@ package ru.dbotthepony.kstarbound +import it.unimi.dsi.fastutil.floats.FloatArrayList import org.apache.logging.log4j.LogManager import org.lwjgl.Version import ru.dbotthepony.kommons.io.BTreeDB6 import ru.dbotthepony.kommons.io.ByteKey +import ru.dbotthepony.kommons.util.AABBi import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.io.BTreeDB5 import ru.dbotthepony.kstarbound.server.IntegratedStarboundServer import ru.dbotthepony.kstarbound.server.world.LegacyChunkSource +import ru.dbotthepony.kstarbound.server.world.ServerUniverse import ru.dbotthepony.kstarbound.server.world.ServerWorld +import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.world.WorldGeometry import ru.dbotthepony.kstarbound.world.entities.ItemEntity import java.io.BufferedInputStream @@ -27,6 +31,22 @@ import java.util.zip.InflaterInputStream private val LOGGER = LogManager.getLogger() fun main() { + Starbound.addPakPath(File("J:\\Steam\\steamapps\\common\\Starbound\\assets\\packed.pak")) + Starbound.doBootstrap() + + if (false) { + val data = ServerUniverse() + + val t = System.nanoTime() + val result = data.scanConstellationLines(AABBi(Vector2i(-100, -100), Vector2i(100, 100))).get() + println(System.nanoTime() - t) + + println(result) + data.close() + + return + } + /*val db0 = BTreeDB5(File("F:\\SteamLibrary\\steamapps\\common\\Starbound - Unstable\\storage\\universe\\389760395_938904237_-238610574_5.world")) val db2 = BTreeDB6.create(File("testdb.bdb"), sync = false) @@ -40,8 +60,8 @@ fun main() { //Thread.sleep(6_000L) - val db = BTreeDB6(File("testdb.bdb")) - //val db = BTreeDB5(File("F:\\SteamLibrary\\steamapps\\common\\Starbound - Unstable\\storage\\universe\\389760395_938904237_-238610574_5.world")) + //val db = BTreeDB6(File("testdb.bdb")) + val db = BTreeDB5(File("F:\\SteamLibrary\\steamapps\\common\\Starbound - Unstable\\storage\\universe\\389760395_938904237_-238610574_5.world")) //val db = BTreeDB(File("world.world")) val meta = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(db.read(ByteKey(0, 0, 0, 0, 0)).get()), Inflater()))) @@ -59,7 +79,6 @@ fun main() { world.thread.start() //Starbound.addFilePath(File("./unpacked_assets/")) - Starbound.addPakPath(File("J:\\Steam\\steamapps\\common\\Starbound\\assets\\packed.pak")) /*for (folder in File("J:\\Steam\\steamapps\\workshop\\content\\211820").list()!!) { val f = File("J:\\Steam\\steamapps\\workshop\\content\\211820\\$folder\\contents.pak") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt index c67dbfef..20b115a1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt @@ -21,7 +21,7 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectMaps import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.util.Either -import ru.dbotthepony.kstarbound.json.consumeNull +import ru.dbotthepony.kommons.gson.consumeNull import java.lang.reflect.ParameterizedType import java.util.concurrent.locks.ReentrantLock import java.util.function.Supplier diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index 4a1d4af9..08cb8884 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -17,6 +17,7 @@ import ru.dbotthepony.kommons.gson.Vector2iTypeAdapter import ru.dbotthepony.kommons.gson.Vector4dTypeAdapter import ru.dbotthepony.kommons.gson.Vector4iTypeAdapter import ru.dbotthepony.kommons.util.MailboxExecutorService +import ru.dbotthepony.kstarbound.collect.WeightedList import ru.dbotthepony.kstarbound.defs.* import ru.dbotthepony.kstarbound.defs.image.Image import ru.dbotthepony.kstarbound.defs.image.SpriteReference @@ -39,10 +40,13 @@ import ru.dbotthepony.kstarbound.json.factory.ArrayListAdapterFactory import ru.dbotthepony.kstarbound.json.factory.ImmutableCollectionAdapterFactory import ru.dbotthepony.kstarbound.json.factory.PairAdapterFactory import ru.dbotthepony.kstarbound.math.* +import ru.dbotthepony.kstarbound.server.world.UniverseChunk import ru.dbotthepony.kstarbound.util.ItemStack import ru.dbotthepony.kstarbound.util.SBPattern import ru.dbotthepony.kstarbound.util.HashTableInterner +import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise import ru.dbotthepony.kstarbound.util.traverseJsonPath +import ru.dbotthepony.kstarbound.world.UniversePos import ru.dbotthepony.kstarbound.world.physics.Poly import java.io.* import java.lang.ref.Cleaner @@ -184,7 +188,10 @@ object Starbound : ISBFileLocator { registerTypeAdapter(Vector2iTypeAdapter) registerTypeAdapter(Vector4iTypeAdapter) registerTypeAdapter(Vector4dTypeAdapter) - registerTypeAdapter(LineF::Adapter) + registerTypeAdapterFactory(Line2d.Companion) + registerTypeAdapterFactory(UniversePos.Companion) + registerTypeAdapterFactory(AbstractPerlinNoise.Companion) + registerTypeAdapterFactory(WeightedList.Companion) // Функции registerTypeAdapter(JsonFunction.CONSTRAINT_ADAPTER) @@ -210,6 +217,8 @@ object Starbound : ISBFileLocator { registerTypeAdapterFactory(ItemReference.Factory(STRINGS)) registerTypeAdapterFactory(TreasurePoolDefinition.Companion) + registerTypeAdapterFactory(UniverseChunk.Companion) + registerTypeAdapter(Image.Companion) registerTypeAdapterFactory(Poly.Companion) @@ -390,7 +399,7 @@ object Starbound : ISBFileLocator { archivePaths.add(pak) } - private fun doBootstrap() { + fun doBootstrap() { if (!bootstrapped && !bootstrapping) { bootstrapping = true } else { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt index 20c50b02..73b60229 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt @@ -368,6 +368,7 @@ class StarboundClient private constructor(val clientID: Int) : Closeable { mailbox.executeQueuedTasks() var next = openglCleanQueue.poll() as CleanRef? + var i = 0 while (next != null) { openglObjectsCleaned++ @@ -383,6 +384,7 @@ class StarboundClient private constructor(val clientID: Int) : Closeable { next.next?.prev = next.prev } + if (++i > 400) break next = openglCleanQueue.poll() as CleanRef? } } @@ -679,7 +681,7 @@ class StarboundClient private constructor(val clientID: Int) : Closeable { blendFunc = BlendFunc.MULTIPLY_WITH_ALPHA } - val spinner = ExecutionSpinner(mailbox, ::renderFrame, Starbound.TICK_TIME_ADVANCE_NANOS) + val spinner = ExecutionSpinner(::executeQueuedTasks, ::renderFrame, Starbound.TICK_TIME_ADVANCE_NANOS) val settings = ClientSettings() val viewportCells: ICellAccess = object : ICellAccess { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/collect/WeightedList.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/collect/WeightedList.kt new file mode 100644 index 00000000..2f7d6ed5 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/collect/WeightedList.kt @@ -0,0 +1,71 @@ +package ru.dbotthepony.kstarbound.collect + +import com.google.common.collect.ImmutableList +import com.google.gson.Gson +import com.google.gson.TypeAdapter +import com.google.gson.TypeAdapterFactory +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import ru.dbotthepony.kommons.util.KOptional +import java.lang.reflect.ParameterizedType +import java.util.random.RandomGenerator +import kotlin.math.absoluteValue + +class WeightedList(val parent: ImmutableList>) { + constructor() : this(ImmutableList.of()) + + val isEmpty: Boolean + get() = parent.isEmpty() + + val isNotEmpty: Boolean + get() = parent.isNotEmpty() + + val sum = parent.stream().mapToDouble { it.first.absoluteValue }.sum() + + fun sample(sampled: Double): KOptional { + if (isEmpty) + return KOptional.empty() + + var sum = 0.0 + + for ((v, e) in parent) { + sum += v + + if (sum >= sampled) + return KOptional(e) + } + + return KOptional(parent.last().second) + } + + fun sample(random: RandomGenerator): KOptional { + if (isEmpty) + return KOptional.empty() + + return sample(random.nextDouble(sum)) + } + + companion object : TypeAdapterFactory { + override fun create(gson: Gson, type: TypeToken): TypeAdapter? { + if (type.rawType == WeightedList::class.java) { + val elemType = (type.type as? ParameterizedType)?.actualTypeArguments?.get(0) ?: return null + + return object : TypeAdapter>() { + private val parent = gson.getAdapter(TypeToken.getParameterized(ImmutableList::class.java, TypeToken.getParameterized(Pair::class.java, java.lang.Double::class.java, elemType).type)) as TypeAdapter>> + + override fun write(out: JsonWriter, value: WeightedList?) { + parent.write(out, value?.parent) + } + + override fun read(`in`: JsonReader): WeightedList? { + val read = parent.read(`in`) ?: return null + return WeightedList(read) + } + } as TypeAdapter + } + + return null + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Celestial.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Celestial.kt index 6ea60274..c8102794 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Celestial.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Celestial.kt @@ -1,12 +1,28 @@ package ru.dbotthepony.kstarbound.defs +import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableMap +import com.google.gson.JsonObject +import it.unimi.dsi.fastutil.ints.Int2ObjectMap import ru.dbotthepony.kommons.io.readVector2i import ru.dbotthepony.kommons.io.writeStruct2i import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kstarbound.collect.WeightedList import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise +import ru.dbotthepony.kstarbound.world.UniversePos import java.io.DataInputStream import java.io.DataOutputStream +@JsonFactory +data class CelestialNames( + val systemNames: WeightedList = WeightedList(), + val systemPrefixNames: WeightedList = WeightedList(), + val systemSuffixNames: WeightedList = WeightedList(), + val planetarySuffixes: ImmutableList = ImmutableList.of(), + val satelliteSuffixes: ImmutableList = ImmutableList.of(), +) + @JsonFactory data class CelestialBaseInformation( val planetOrbitalLevels: Int = 1, @@ -31,3 +47,87 @@ data class CelestialBaseInformation( stream.writeStruct2i(zCoordRange) } } + +@JsonFactory +data class CelestialOrbitRegion( + val regionName: String, + val orbitRange: Vector2i, + val bodyProbability: Double, + val planetaryTypes: WeightedList = WeightedList(), + val satelliteTypes: WeightedList = WeightedList(), +) + +@JsonFactory +data class CelestialPlanet(val parameters: CelestialParameters, val satellites: Int2ObjectMap) + +@JsonFactory +data class CelestialGenerationInformation( + val systemProbability: Double, + val constellationProbability: Double, + val constellationLineCountRange: Vector2i, + val constellationMaxTries: Int, + val maximumConstellationLineLength: Double, + val minimumConstellationLineLength: Double, + val minimumConstellationMagnitude: Double, + val minimumConstellationLineCloseness: Double, + + val systemTypes: ImmutableMap, + val systemTypePerlin: AbstractPerlinNoise, + val systemTypeBins: ImmutableList>, + + val planetaryTypes: ImmutableMap, + val satelliteTypes: ImmutableMap, +) { + init { + systemTypeBins.forEach { + require(it.second.isBlank() || it.second in systemTypes) { "System type is present in systemTypeBins, but not in systemTypes: ${it.second}" } + } + } + + @JsonFactory + data class System( + var typeName: String = "", + /** + * This was present in original code, but never implemented + */ + val constellationCapable: Boolean = true, + val baseParameters: JsonObject = JsonObject(), + val variationParameters: ImmutableList = ImmutableList.of(), + val orbitRegions: ImmutableList = ImmutableList.of(), + ) + + @JsonFactory + data class Planet( + var typeName: String = "", + val satelliteProbability: Double = 0.0, + val maxSatelliteCount: Int? = null, + val baseParameters: JsonObject = JsonObject(), + val variationParameters: ImmutableList = ImmutableList.of(), + val orbitParameters: JsonObject = JsonObject(), + ) + + @JsonFactory + data class Satellite( + var typeName: String = "", + val baseParameters: JsonObject = JsonObject(), + val variationParameters: ImmutableList = ImmutableList.of(), + val orbitParameters: ImmutableMap> = ImmutableMap.of(), + ) + + init { + planetaryTypes.entries.forEach { + it.value.typeName = it.key + } + + systemTypes.entries.forEach { + it.value.typeName = it.key + } + + satelliteTypes.entries.forEach { + it.value.typeName = it.key + } + } +} + +@JsonFactory +data class CelestialParameters(val coordinate: UniversePos, val seed: Long, val name: String, val parameters: JsonObject) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Drawable.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Drawable.kt index ecb66569..e7bc8d4b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Drawable.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Drawable.kt @@ -19,16 +19,16 @@ import ru.dbotthepony.kstarbound.client.gl.vertex.GeometryType import ru.dbotthepony.kstarbound.client.render.IGeometryLayer import ru.dbotthepony.kstarbound.defs.image.SpriteReference import ru.dbotthepony.kstarbound.json.builder.JsonFactory -import ru.dbotthepony.kstarbound.json.consumeNull -import ru.dbotthepony.kstarbound.math.LineF -import ru.dbotthepony.kstarbound.json.contains +import ru.dbotthepony.kommons.gson.consumeNull +import ru.dbotthepony.kommons.gson.contains +import ru.dbotthepony.kstarbound.math.Line2d sealed class Drawable(val position: Vector2f, val color: RGBAColor, val fullbright: Boolean) { @JsonFactory data class Transformations(val centered: Boolean = false, val rotation: Float = 0f, val mirrored: Boolean = false, val scale: Either = Either.left(1f)) class Line( - val line: LineF, + val line: Line2d, val width: Float, position: Vector2f = Vector2f.ZERO, color: RGBAColor = RGBAColor.WHITE, @@ -143,7 +143,7 @@ sealed class Drawable(val position: Vector2f, val color: RGBAColor, val fullbrig } class Adapter(gson: Gson) : TypeAdapter() { - private val lines = gson.getAdapter(LineF::class.java) + private val lines = gson.getAdapter(Line2d::class.java) private val objects = gson.getAdapter(JsonObject::class.java) private val vectors = gson.getAdapter(Vector2f::class.java) private val vectors3 = gson.getAdapter(Vector3f::class.java) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ItemReference.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ItemReference.kt index 6777df37..12d98913 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ItemReference.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ItemReference.kt @@ -13,7 +13,7 @@ import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition import ru.dbotthepony.kstarbound.json.builder.FactoryAdapter import ru.dbotthepony.kstarbound.json.builder.JsonFactory -import ru.dbotthepony.kstarbound.json.consumeNull +import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kstarbound.util.ItemStack /** diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonDriven.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonDriven.kt index 73d003a6..43ff110d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonDriven.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonDriven.kt @@ -8,7 +8,7 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.util.AssetPathStack -import ru.dbotthepony.kstarbound.json.set +import ru.dbotthepony.kommons.gson.set import java.util.function.Consumer import java.util.function.Function import java.util.function.Supplier @@ -215,7 +215,7 @@ abstract class JsonDriven(val path: String) { private val never = Supplier { throw NoSuchElementException() } @JvmStatic - protected fun mergeNoCopy(a: JsonObject, b: JsonObject): JsonObject { + fun mergeNoCopy(a: JsonObject, b: JsonObject): JsonObject { for ((k, v) in b.entrySet()) { val existing = a[k] diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonReference.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonReference.kt index f107b6d9..d61c7dc5 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonReference.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonReference.kt @@ -13,8 +13,8 @@ import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonWriter import ru.dbotthepony.kstarbound.Starbound -import ru.dbotthepony.kstarbound.json.consumeNull -import ru.dbotthepony.kstarbound.json.value +import ru.dbotthepony.kommons.gson.consumeNull +import ru.dbotthepony.kommons.gson.value import ru.dbotthepony.kstarbound.util.AssetPathStack sealed class JsonReference(val path: String?, val fullPath: String?) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PerlinNoiseParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PerlinNoiseParameters.kt new file mode 100644 index 00000000..235b25c3 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PerlinNoiseParameters.kt @@ -0,0 +1,28 @@ +package ru.dbotthepony.kstarbound.defs + +import ru.dbotthepony.kstarbound.json.builder.JsonFactory + +@JsonFactory +data class PerlinNoiseParameters( + val type: Type = Type.PERLIN, + val seed: Long? = null, + val scale: Int = 512, + val octaves: Int = 1, + val gain: Double = 2.0, + val offset: Double = 1.0, + val beta: Double = 1.0, + val alpha: Double = 2.0, + val frequency: Double = 1.0, + val amplitude: Double = 1.0, + val bias: Double = 0.0, +) { + init { + require(scale >= 16) { "Too little perlin noise scale" } + } + + enum class Type { + PERLIN, + BILLOW, + RIDGED_MULTI; + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/StatModifier.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/StatModifier.kt index cc9ceb86..072ee92c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/StatModifier.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/StatModifier.kt @@ -7,9 +7,9 @@ import com.google.gson.JsonSyntaxException import com.google.gson.TypeAdapter import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonWriter -import ru.dbotthepony.kstarbound.json.consumeNull -import ru.dbotthepony.kstarbound.json.contains -import ru.dbotthepony.kstarbound.json.get +import ru.dbotthepony.kommons.gson.consumeNull +import ru.dbotthepony.kommons.gson.contains +import ru.dbotthepony.kommons.gson.get enum class StatModifierType(vararg names: String) { BASE_ADDITION("value", "baseAddition"), diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt index 3f671e43..08e609f3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt @@ -31,10 +31,10 @@ import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.IStarboundFile import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.gl.GLTexture2D -import ru.dbotthepony.kstarbound.json.consumeNull -import ru.dbotthepony.kstarbound.json.contains -import ru.dbotthepony.kstarbound.json.get -import ru.dbotthepony.kstarbound.json.getObject +import ru.dbotthepony.kommons.gson.consumeNull +import ru.dbotthepony.kommons.gson.contains +import ru.dbotthepony.kommons.gson.get +import ru.dbotthepony.kommons.gson.getObject import java.io.BufferedInputStream import java.io.FileNotFoundException import java.lang.ref.Reference diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/SpriteReference.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/SpriteReference.kt index 1ff26e29..dc3051d6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/SpriteReference.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/SpriteReference.kt @@ -6,7 +6,7 @@ import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonWriter import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.AssetPath -import ru.dbotthepony.kstarbound.json.consumeNull +import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.util.SBPattern diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt index 493929cc..f674be08 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt @@ -14,7 +14,7 @@ import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kstarbound.lua.StateMachine import ru.dbotthepony.kstarbound.lua.from import ru.dbotthepony.kstarbound.lua.toJsonObject -import ru.dbotthepony.kstarbound.json.get +import ru.dbotthepony.kommons.gson.get import java.util.function.Supplier fun ItemDescriptor(data: JsonElement): ItemDescriptor { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/TreasurePoolDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/TreasurePoolDefinition.kt index 69d11074..48d3b94b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/TreasurePoolDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/TreasurePoolDefinition.kt @@ -15,7 +15,7 @@ import com.google.gson.stream.JsonWriter import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.defs.ItemReference -import ru.dbotthepony.kstarbound.json.consumeNull +import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kstarbound.json.stream import ru.dbotthepony.kstarbound.util.ItemStack import ru.dbotthepony.kstarbound.util.WriteOnce diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt index 0c382b6e..37626477 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt @@ -21,14 +21,14 @@ import ru.dbotthepony.kstarbound.defs.StatModifier import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig import ru.dbotthepony.kstarbound.json.builder.JsonFactory -import ru.dbotthepony.kstarbound.json.consumeNull +import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kstarbound.json.listAdapter import ru.dbotthepony.kstarbound.json.stream import ru.dbotthepony.kstarbound.math.PeriodicFunction -import ru.dbotthepony.kstarbound.json.contains -import ru.dbotthepony.kstarbound.json.get -import ru.dbotthepony.kstarbound.json.getArray -import ru.dbotthepony.kstarbound.json.set +import ru.dbotthepony.kommons.gson.contains +import ru.dbotthepony.kommons.gson.get +import ru.dbotthepony.kommons.gson.getArray +import ru.dbotthepony.kommons.gson.set data class ObjectDefinition( val objectName: String, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt index ae811d33..8eb5955e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt @@ -10,6 +10,7 @@ import com.google.gson.TypeAdapter import com.google.gson.reflect.TypeToken import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonWriter +import ru.dbotthepony.kommons.gson.clear import ru.dbotthepony.kommons.util.AABB import ru.dbotthepony.kommons.util.AABBi import ru.dbotthepony.kommons.vector.Vector2d @@ -20,13 +21,12 @@ import ru.dbotthepony.kstarbound.client.render.RenderLayer import ru.dbotthepony.kstarbound.defs.Drawable import ru.dbotthepony.kstarbound.defs.JsonReference import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials -import ru.dbotthepony.kstarbound.json.clear -import ru.dbotthepony.kstarbound.json.consumeNull +import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kstarbound.json.listAdapter import ru.dbotthepony.kstarbound.json.setAdapter -import ru.dbotthepony.kstarbound.json.contains -import ru.dbotthepony.kstarbound.json.get -import ru.dbotthepony.kstarbound.json.set +import ru.dbotthepony.kommons.gson.contains +import ru.dbotthepony.kommons.gson.get +import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kstarbound.world.Side import kotlin.math.PI diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/BTreeDB5.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/BTreeDB5.kt index 89c5128f..b22d44f5 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/BTreeDB5.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/BTreeDB5.kt @@ -2,22 +2,16 @@ package ru.dbotthepony.kstarbound.io import it.unimi.dsi.fastutil.io.FastByteArrayInputStream import it.unimi.dsi.fastutil.longs.LongArrayList -import ru.dbotthepony.kommons.io.ByteDataBTreeDB import ru.dbotthepony.kommons.io.ByteKey import ru.dbotthepony.kommons.io.readByteKeyRaw import ru.dbotthepony.kommons.io.readVarInt -import ru.dbotthepony.kommons.util.CarriedExecutor import ru.dbotthepony.kommons.util.KOptional -import ru.dbotthepony.kstarbound.Starbound +import java.io.Closeable import java.io.DataInputStream import java.io.File import java.io.InputStream import java.io.RandomAccessFile import java.util.* -import java.util.concurrent.CompletableFuture -import java.util.concurrent.TimeUnit -import java.util.function.Supplier -import kotlin.collections.ArrayList private fun readHeader(reader: RandomAccessFile, required: Char) { val read = reader.read() @@ -51,7 +45,7 @@ private enum class TreeBlockType(val identity: String) { } } -class BTreeDB5(override val file: File) : ByteDataBTreeDB() { +class BTreeDB5(val file: File) : Closeable { private val reader = RandomAccessFile(file, "r") init { @@ -65,7 +59,7 @@ class BTreeDB5(override val file: File) : ByteDataBTreeDB() { readHeader(reader, '5') } - override val blockSize = reader.readInt() + val blockSize = reader.readInt() val dbNameRaw = ByteArray(16).also { reader.read(it) } val keySize = reader.readInt() val useNodeTwo = reader.readBoolean() @@ -146,17 +140,13 @@ class BTreeDB5(override val file: File) : ByteDataBTreeDB() { reader.close() } - override fun write(key: ByteKey, value: ByteArray, offset: Int, length: Int) { - throw UnsupportedOperationException() - } - - override fun findAllKeys(): List { + fun findAllKeys(): List { val list = ArrayList() doFindAllKeys(rootNodeIndex, list) return list } - override fun contains(key: ByteKey): Boolean { + fun contains(key: ByteKey): Boolean { seekBlock(rootNodeIndex) var blockStream = BlockInputStream() @@ -218,7 +208,7 @@ class BTreeDB5(override val file: File) : ByteDataBTreeDB() { return false } - override fun read(key: ByteKey): KOptional { + fun read(key: ByteKey): KOptional { require(key.size == keySize) { "Key provided is ${key.size} in size, while $keySize is required" } seekBlock(rootNodeIndex) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonReader.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonReader.kt index 95941354..8aca7ceb 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonReader.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonReader.kt @@ -13,6 +13,7 @@ import ru.dbotthepony.kommons.io.readBinaryString import ru.dbotthepony.kommons.io.readSignedVarLong import ru.dbotthepony.kommons.io.readString import ru.dbotthepony.kommons.io.readVarInt +import ru.dbotthepony.kstarbound.Starbound import java.io.DataInputStream import java.io.EOFException import java.io.InputStream @@ -28,7 +29,7 @@ fun DataInputStream.readJsonElement(): JsonElement { BinaryJsonReader.TYPE_DOUBLE -> JsonPrimitive(readDouble()) BinaryJsonReader.TYPE_BOOLEAN -> InternedJsonElementAdapter.of(readBoolean()) BinaryJsonReader.TYPE_INT -> JsonPrimitive(readSignedVarLong()) - BinaryJsonReader.TYPE_STRING -> JsonPrimitive(readBinaryString()) + BinaryJsonReader.TYPE_STRING -> JsonPrimitive(Starbound.STRINGS.intern(readBinaryString())) BinaryJsonReader.TYPE_ARRAY -> readJsonArray() BinaryJsonReader.TYPE_OBJECT -> readJsonObject() else -> throw JsonParseException("Unknown element type $id") @@ -47,7 +48,7 @@ fun DataInputStream.readJsonObject(): JsonObject { for (i in 0 until values) { val key = try { - readBinaryString() + Starbound.STRINGS.intern(readBinaryString()) } catch(err: Throwable) { throw JsonSyntaxException("Reading json object at $i", err) } @@ -216,7 +217,7 @@ class BinaryJsonReader(private val stream: DataInputStream) : JsonReader(unreada } private inner class StringReader(val length: Int) : NothingReader("string", JsonToken.STRING) { - private val value by lazy(LazyThreadSafetyMode.NONE) { stream.readString(length) } + private val value by lazy(LazyThreadSafetyMode.NONE) { Starbound.STRINGS.intern(stream.readString(length)) } override fun skipValue() { popstack(this) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonWriter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonWriter.kt index e4c70cfe..f759d55f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonWriter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonWriter.kt @@ -27,7 +27,17 @@ fun DataOutputStream.writeJsonElement(value: JsonElement) { } else -> { - throw IllegalArgumentException("Unknown number type: ${v::class} ($v)") + // throw IllegalArgumentException("Unknown number type: ${v::class} ($v)") + // hi com.google.gson.internal.LazilyParsedNumber + val doubleBits by lazy { v.toDouble() } + + if (v.toString().contains('.') || doubleBits % 1.0 != 0.0) { + write(BinaryJsonReader.TYPE_DOUBLE) + writeDouble(doubleBits) + } else { + write(BinaryJsonReader.TYPE_INT) + writeSignedVarLong(v.toLong()) + } } } } else if (value.isString) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/FastutilTypeAdapterFactory.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/FastutilTypeAdapterFactory.kt index 08031f65..4e762b2c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/FastutilTypeAdapterFactory.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/FastutilTypeAdapterFactory.kt @@ -8,6 +8,7 @@ import com.google.gson.reflect.TypeToken import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonWriter import it.unimi.dsi.fastutil.objects.* +import ru.dbotthepony.kommons.gson.consumeNull import java.lang.reflect.ParameterizedType class FastutilTypeAdapterFactory(private val interner: Interner) : TypeAdapterFactory { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonUtils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonUtils.kt index 94d157c0..952cb26e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonUtils.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonUtils.kt @@ -17,133 +17,6 @@ import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonWriter import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet -fun TypeAdapter.transformRead(transformer: (T) -> T): TypeAdapter { - return object : TypeAdapter() { - override fun write(out: JsonWriter, value: T) { - return this@transformRead.write(out, value) - } - - override fun read(`in`: JsonReader): T { - return transformer(this@transformRead.read(`in`)) - } - } -} - -fun TypeAdapter.transformWrite(transformer: (T) -> T): TypeAdapter { - return object : TypeAdapter() { - override fun write(out: JsonWriter, value: T) { - return this@transformWrite.write(out, transformer(value)) - } - - override fun read(`in`: JsonReader): T { - return this@transformWrite.read(`in`) - } - } -} - -fun TypeAdapter.transform(read: (In) -> Out, write: (Out) -> In): TypeAdapter { - return object : TypeAdapter() { - override fun write(out: JsonWriter, value: Out) { - return this@transform.write(out, write(value)) - } - - override fun read(`in`: JsonReader): Out { - return read(this@transform.read(`in`)) - } - } -} - -fun TypeAdapter.ifString(reader: (String) -> T): TypeAdapter { - return object : TypeAdapter() { - override fun write(out: JsonWriter, value: T) { - return this@ifString.write(out, value) - } - - override fun read(`in`: JsonReader): T { - if (`in`.peek() == JsonToken.STRING) { - return reader(`in`.nextString()) - } - - return this@ifString.read(`in`) - } - } -} - -fun TypeAdapter.neverNull(): TypeAdapter { - return object : TypeAdapter() { - override fun write(out: JsonWriter, value: T) { - this@neverNull.write(out, value) - } - - override fun read(`in`: JsonReader): T { - val path = `in`.path - return this@neverNull.read(`in`) ?: throw JsonSyntaxException("Value was null near $path") - } - } -} - -fun TypeAdapter.allowNull(): TypeAdapter = nullSafe() - -@Deprecated("Небезопасно, надо использовать другой путь") -fun TypeAdapterFactory.ifString(reader: (String) -> Any): TypeAdapterFactory { - return object : TypeAdapterFactory { - override fun create(gson: Gson, type: TypeToken): TypeAdapter? { - return this@ifString.create(gson, type)?.ifString(reader as (String) -> T) - } - } -} - -fun JsonReader.consumeNull(): Boolean { - if (peek() == JsonToken.NULL) { - nextNull() - return true - } - - return false -} - -fun JsonWriter.value(element: JsonPrimitive) { - if (element.isBoolean) { - value(element.asBoolean) - } else if (element.isNumber) { - value(element.asNumber) - } else if (element.isString) { - value(element.asString) - } else { - throw IllegalArgumentException(element.toString()) - } -} - -fun JsonWriter.value(element: JsonNull) { - nullValue() -} - -fun JsonWriter.value(element: JsonArray) { - beginArray() - for (v in element) value(v) - endArray() -} - -fun JsonWriter.value(element: JsonObject) { - beginObject() - for ((k, v) in element.entrySet()) { - name(k) - value(v) - } - endObject() -} - -fun JsonWriter.value(element: JsonElement?) { - when (element) { - is JsonPrimitive -> value(element) - is JsonNull -> value(element) - is JsonArray -> value(element) - is JsonObject -> value(element) - null -> nullValue() - else -> throw IllegalArgumentException(element.toString()) - } -} - inline fun , reified E> Gson.collectionAdapter(): TypeAdapter { return getAdapter(TypeToken.getParameterized(C::class.java, E::class.java)) as TypeAdapter } @@ -152,6 +25,14 @@ inline fun Gson.listAdapter(): TypeAdapter> { return collectionAdapter() } +inline fun Gson.pairAdapter(): TypeAdapter> { + return getAdapter(TypeToken.getParameterized(Pair::class.java, A::class.java, B::class.java)) as TypeAdapter> +} + +inline fun Gson.pairListAdapter(): TypeAdapter>> { + return getAdapter(TypeToken.getParameterized(ImmutableList::class.java, TypeToken.getParameterized(Pair::class.java, A::class.java, B::class.java).type)) as TypeAdapter>> +} + inline fun Gson.mutableListAdapter(): TypeAdapter> { return collectionAdapter() } @@ -160,275 +41,10 @@ inline fun Gson.setAdapter(): TypeAdapter> { return collectionAdapter() } +inline fun Gson.pairSetAdapter(): TypeAdapter>> { + return getAdapter(TypeToken.getParameterized(ImmutableSet::class.java, TypeToken.getParameterized(Pair::class.java, A::class.java, B::class.java).type)) as TypeAdapter>> +} + inline fun Gson.mutableSetAdapter(): TypeAdapter> { return collectionAdapter() } - -fun JsonArray(elements: Collection): JsonArray { - return JsonArray(elements.size).also { elements.forEach(it::add) } -} - -fun JsonArray.clear() { - while (size() > 0) { - remove(size() - 1) - } -} - -operator fun JsonObject.set(key: String, value: JsonElement?) { add(key, value) } -operator fun JsonObject.set(key: String, value: String) { add(key, JsonPrimitive(value)) } -operator fun JsonObject.set(key: String, value: Int) { add(key, JsonPrimitive(value)) } -operator fun JsonObject.set(key: String, value: Long) { add(key, JsonPrimitive(value)) } -operator fun JsonObject.set(key: String, value: Float) { add(key, JsonPrimitive(value)) } -operator fun JsonObject.set(key: String, value: Double) { add(key, JsonPrimitive(value)) } -operator fun JsonObject.set(key: String, value: Boolean) { add(key, InternedJsonElementAdapter.of(value)) } -operator fun JsonObject.set(key: String, value: Nothing?) { add(key, JsonNull.INSTANCE) } -operator fun JsonObject.contains(key: String): Boolean = has(key) - -fun JsonArray.get(key: Int, default: Boolean): Boolean { - if (key !in 0 until size()) - return default - - val value = this[key] - - if (value !is JsonPrimitive || !value.isBoolean) { - throw JsonSyntaxException("Expected boolean at $key; got $value") - } - - return value.asBoolean -} - -fun JsonObject.get(key: String, default: Boolean): Boolean { - if (!has(key)) - return default - - val value = this[key] - - if (value !is JsonPrimitive || !value.isBoolean) { - throw JsonSyntaxException("Expected boolean at $key; got $value") - } - - return value.asBoolean -} - -fun JsonObject.get(key: String, default: String): String { - if (!has(key)) - return default - - val value = this[key] - - if (value !is JsonPrimitive || !value.isString) { - throw JsonSyntaxException("Expected string at $key; got $value") - } - - return value.asString -} - -fun JsonArray.get(key: Int, default: String): String { - if (key !in 0 until size()) - return default - - val value = this[key] - - if (value !is JsonPrimitive || !value.isString) { - throw JsonSyntaxException("Expected string at $key; got $value") - } - - return value.asString -} - -fun JsonObject.get(key: String, default: Int): Int { - if (!has(key)) - return default - - val value = this[key] - - if (value !is JsonPrimitive || !value.isNumber) { - throw JsonSyntaxException("Expected integer at $key; got $value") - } - - return value.asInt -} - -fun JsonArray.get(key: Int, default: Int): Int { - if (key !in 0 until size()) - return default - - val value = this[key] - - if (value !is JsonPrimitive || !value.isNumber) { - throw JsonSyntaxException("Expected integer at $key; got $value") - } - - return value.asInt -} - -fun JsonObject.get(key: String, default: Long): Long { - if (!has(key)) - return default - - val value = this[key] - - if (value !is JsonPrimitive || !value.isNumber) { - throw JsonSyntaxException("Expected long at $key; got $value") - } - - return value.asLong -} - -fun JsonArray.get(key: Int, default: Long): Long { - if (key !in 0 until size()) - return default - - val value = this[key] - - if (value !is JsonPrimitive || !value.isNumber) { - throw JsonSyntaxException("Expected long at $key; got $value") - } - - return value.asLong -} - -fun JsonObject.get(key: String, default: Double): Double { - if (!has(key)) - return default - - val value = this[key] - - if (value !is JsonPrimitive || !value.isNumber) { - throw JsonSyntaxException("Expected double at $key; got $value") - } - - return value.asDouble -} - -fun JsonArray.get(key: Int, default: Double): Double { - if (key !in 0 until size()) - return default - - val value = this[key] - - if (value !is JsonPrimitive || !value.isNumber) { - throw JsonSyntaxException("Expected double at $key; got $value") - } - - return value.asDouble -} - -fun JsonObject.get(key: String, default: JsonObject): JsonObject { - if (!has(key)) - return default - - val value = this[key] - - if (value !is JsonObject) - throw JsonSyntaxException("Expected json object at $key; got $value") - - return value -} - -inline fun JsonObject.get(key: String, default: () -> T): T { - if (!has(key)) - return default.invoke() - - val value = this[key] - - if (value !is T) - throw JsonSyntaxException("Expected json object at $key; got $value") - - return value -} - -fun JsonArray.get(key: Int, default: JsonObject): JsonObject { - if (key !in 0 until size()) - return default - - val value = this[key] - - if (value !is JsonObject) - throw JsonSyntaxException("Expected json object at $key; got $value") - - return value -} - -inline fun JsonArray.get(key: Int, default: () -> JsonObject): JsonObject { - if (key !in 0 until size()) - return default.invoke() - - val value = this[key] - - if (value !is JsonObject) - throw JsonSyntaxException("Expected json object at $key; got $value") - - return value -} - -fun JsonObject.get(key: String, default: JsonArray): JsonArray { - if (!has(key)) - return default - - val value = this[key] - - if (value !is JsonArray) - throw JsonSyntaxException("Expected json array at $key; got $value") - - return value -} - -fun JsonArray.get(key: Int, default: JsonArray): JsonArray { - if (key !in 0 until size()) - return default - - val value = this[key] - - if (value !is JsonArray) - throw JsonSyntaxException("Expected json array at $key; got $value") - - return value -} - -inline fun JsonArray.get(key: Int, default: () -> T): T { - if (key !in 0 until size()) - return default.invoke() - - val value = this[key] - - if (value !is T) - throw JsonSyntaxException("Expected json array at $key; got $value") - - return value -} - -fun JsonObject.get(key: String, default: JsonPrimitive): JsonElement { - if (!has(key)) - return default - - return this[key] -} - -fun JsonArray.get(key: Int, default: JsonPrimitive): JsonElement { - if (key !in 0 until size()) - return default - - return this[key] -} - -fun JsonObject.get(key: String, type: TypeAdapter): T { - return get(key, type) { throw JsonSyntaxException("Expected value at $key, got nothing") } -} - -inline fun JsonObject.get(key: String, type: TypeAdapter, orElse: () -> T): T { - if (!has(key)) - return orElse.invoke() - - return type.fromJsonTree(this[key]) -} - -fun JsonObject.getArray(key: String): JsonArray { - if (!has(key)) throw JsonSyntaxException("Expected array at $key, got nothing") - return this[key] as? JsonArray ?: throw JsonSyntaxException("Expected array at $key, got ${this[key]}") -} - -fun JsonObject.getObject(key: String): JsonObject { - if (!has(key)) throw JsonSyntaxException("Expected object at $key, got nothing") - return this[key] as? JsonObject ?: throw JsonSyntaxException("Expected object at $key, got ${this[key]}") -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/FactoryAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/FactoryAdapter.kt index 27c2a202..558e671f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/FactoryAdapter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/FactoryAdapter.kt @@ -27,8 +27,8 @@ import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.util.enrollList import ru.dbotthepony.kstarbound.defs.util.enrollMap import ru.dbotthepony.kstarbound.defs.util.flattenJsonElement -import ru.dbotthepony.kstarbound.json.consumeNull -import ru.dbotthepony.kstarbound.json.value +import ru.dbotthepony.kommons.gson.consumeNull +import ru.dbotthepony.kommons.gson.value import java.lang.reflect.Constructor import java.util.Collections import java.util.function.Function diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/PairAdapterFactory.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/PairAdapterFactory.kt index 2d791c8e..0474984e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/PairAdapterFactory.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/PairAdapterFactory.kt @@ -8,7 +8,7 @@ import com.google.gson.reflect.TypeToken import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonWriter -import ru.dbotthepony.kstarbound.json.consumeNull +import ru.dbotthepony.kommons.gson.consumeNull import java.lang.reflect.ParameterizedType object PairAdapterFactory : TypeAdapterFactory { @@ -61,12 +61,12 @@ object PairAdapterFactory : TypeAdapterFactory { while (`in`.hasNext()) { when (`in`.nextName().lowercase()) { - "left", "first", "a" -> { + "left", "first", "a", "weight" -> { left = adapter0.read(`in`) leftPresent = true } - "right", "second", "b" -> { + "right", "second", "b", "item" -> { right = adapter1.read(`in`) rightPresent = true } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/FastMath.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/FastMath.kt deleted file mode 100644 index daff5bd7..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/math/FastMath.kt +++ /dev/null @@ -1,24 +0,0 @@ -package ru.dbotthepony.kstarbound.math - -/** - * Выполняет скалярное умножение между a и векторным произведением b, c на стеке - */ -fun scalarDotWithCross( - ax: Double, - ay: Double, - az: Double, - - bx: Double, - by: Double, - bz: Double, - - cx: Double, - cy: Double, - cz: Double, -): Double { - val crossX = by * cz - bz * cy - val crossY = bz * cx - bx * cz - val crossZ = bx * cy - by * cx - - return ax * crossX + ay * crossY + az * crossZ -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Line2d.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Line2d.kt new file mode 100644 index 00000000..8c0ac1ce --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Line2d.kt @@ -0,0 +1,142 @@ +package ru.dbotthepony.kstarbound.math + +import com.google.gson.Gson +import com.google.gson.TypeAdapter +import com.google.gson.TypeAdapterFactory +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import ru.dbotthepony.kommons.gson.consumeNull +import ru.dbotthepony.kommons.util.KOptional +import ru.dbotthepony.kommons.vector.Vector2d +import kotlin.math.absoluteValue + +private operator fun Vector2d.compareTo(other: Vector2d): Int { + var cmp = x.compareTo(other.x) + if (cmp == 0) cmp = y.compareTo(y) + return cmp +} + +data class Line2d(val a: Vector2d, val b: Vector2d) { + data class Intersection(val intersects: Boolean, val point: KOptional, val t: KOptional, val coincides: Boolean, val glances: Boolean) { + companion object { + val EMPTY = Intersection(false, KOptional(), KOptional(), false, false) + } + } + + fun difference(): Vector2d { + return b - a + } + + fun reverse(): Line2d { + return Line2d(b, a) + } + + // original source of this intersection algorithm: + // https://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect + // article "Intersection of two lines in three-space" by Ronald Goldman, published in Graphics Gems, page 304 + fun intersect(other: Line2d, infinite: Boolean = false): Intersection { + val (c, d) = other + + val ab = difference() + val cd = other.difference() + + val abCross = a.cross(b) + val cdCross = c.cross(d) + + val denominator = ab.cross(cd) + val xNumber = abCross * cd.x - cdCross * ab.x + val yNumber = abCross * cd.y - cdCross * ab.y + + if (denominator.absoluteValue <= NEAR_ZERO) { + if (xNumber.absoluteValue <= NEAR_ZERO && yNumber.absoluteValue <= NEAR_ZERO) { + val intersects = infinite || (a >= c && a <= d) || (c >= a && c <= b) + var point: Vector2d? = null + var t = 0.0 + + if (intersects) { + if (infinite) { + point = Vector2d(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY) + } else { + point = if (a < c) c else a + } + } + + if (a < c) { + if (c.x != a.x) { + t = (c.x - a.x) / ab.x + } else { + t = (c.y - a.y) / ab.y + } + } else if (a > d) { + if (d.x != a.x) { + t = (d.x - a.x) / ab.x + } else { + t = (d.y - a.y) / ab.y + } + } + + return Intersection(intersects, KOptional.ofNullable(point), KOptional(t), true, intersects) + } else { + return Intersection.EMPTY + } + } else { + val ta = (c - a).cross(cd) / denominator + val tb = (c - a).cross(ab) / denominator + + val intersects = infinite || (ta in 0.0 .. 1.0 && tb in 0.0 .. 1.0) + + return Intersection( + intersects = intersects, + t = KOptional(ta), + point = KOptional((b - a) * ta + a), + coincides = false, + glances = !infinite && intersects && (ta <= NEAR_ZERO || ta >= NEAR_ONE || tb <= NEAR_ZERO || tb >= NEAR_ONE) + ) + } + } + + fun project(axis: Vector2d): Double { + val diff = difference() + return ((axis.x - a.x) * diff.x + (axis.y - a.y) * diff.y) / diff.lengthSquared + } + + fun distanceTo(other: Vector2d, infinite: Boolean = false): Double { + var proj = project(other) + + if (!infinite) + proj = proj.coerceIn(0.0, 1.0) + + return (other - a + difference() * proj).length + } + + companion object : TypeAdapterFactory { + const val NEAR_ZERO = Double.MIN_VALUE * 2.0 + const val NEAR_ONE = 1.0 - NEAR_ZERO + + override fun create(gson: Gson, type: TypeToken): TypeAdapter? { + if (type.rawType == Line2d::class.java) { + val pair = gson.getAdapter(TypeToken.getParameterized(Vector2d::class.java, Vector2d::class.java)) as TypeAdapter> + + return object : TypeAdapter() { + override fun write(out: JsonWriter, value: Line2d?) { + if (value == null) + out.nullValue() + else + pair.write(out, value.a to value.b) + } + + override fun read(`in`: JsonReader): Line2d? { + if (`in`.consumeNull()) + return null + + val (a, b) = pair.read(`in`) + return Line2d(a, b) + } + } as TypeAdapter + } + + return null + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/LineF.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/LineF.kt deleted file mode 100644 index a238a250..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/math/LineF.kt +++ /dev/null @@ -1,37 +0,0 @@ -package ru.dbotthepony.kstarbound.math - -import com.google.gson.Gson -import com.google.gson.TypeAdapter -import com.google.gson.stream.JsonReader -import com.google.gson.stream.JsonWriter -import ru.dbotthepony.kommons.vector.Vector2f -import ru.dbotthepony.kstarbound.json.consumeNull - -data class LineF(val start: Vector2f, val end: Vector2f) { - class Adapter(gson: Gson) : TypeAdapter() { - private val vectors = gson.getAdapter(Vector2f::class.java) - - override fun write(out: JsonWriter, value: LineF?) { - if (value == null) { - out.nullValue() - } else { - out.beginArray() - vectors.write(out, value.start) - vectors.write(out, value.end) - out.endArray() - } - } - - override fun read(`in`: JsonReader): LineF? { - if (`in`.consumeNull()) { - return null - } else { - `in`.beginArray() - val start = vectors.read(`in`) - val end = vectors.read(`in`) - `in`.endArray() - return LineF(start, end) - } - } - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/player/QuestInstance.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/player/QuestInstance.kt index 7598fc56..d3635723 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/player/QuestInstance.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/player/QuestInstance.kt @@ -10,7 +10,7 @@ import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.quest.QuestTemplate import ru.dbotthepony.kstarbound.lua.NewLuaState import ru.dbotthepony.kstarbound.util.ItemStack -import ru.dbotthepony.kstarbound.json.set +import ru.dbotthepony.kommons.gson.set import java.util.UUID class QuestInstance( diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt index 812b08e7..54fd03e0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt @@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound.server import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.util.MailboxExecutorService import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.server.world.ServerUniverse import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.util.ExecutionSpinner import java.io.Closeable @@ -26,8 +27,9 @@ sealed class StarboundServer(val root: File) : Closeable { val serverID = threadCounter.getAndIncrement() val mailbox = MailboxExecutorService() - val spinner = ExecutionSpinner(mailbox, ::spin, Starbound.TICK_TIME_ADVANCE_NANOS) + val spinner = ExecutionSpinner(mailbox::executeQueuedTasks, ::spin, Starbound.TICK_TIME_ADVANCE_NANOS) val thread = Thread(spinner, "Starbound Server $serverID") + val universe = ServerUniverse() val settings = ServerSettings() val channels = ServerChannels(this) @@ -64,6 +66,7 @@ sealed class StarboundServer(val root: File) : Closeable { channels.close() worlds.forEach { it.close() } + universe.close() close0() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyChunkSource.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyChunkSource.kt index 25fb229a..36b9a93b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyChunkSource.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyChunkSource.kt @@ -2,13 +2,13 @@ package ru.dbotthepony.kstarbound.server.world import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.arrays.Object2DArray -import ru.dbotthepony.kommons.io.BTreeDB import ru.dbotthepony.kommons.io.ByteKey import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.io.readVarInt import ru.dbotthepony.kommons.util.CarriedExecutor import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.io.BTreeDB5 import ru.dbotthepony.kstarbound.json.VersionedJson import ru.dbotthepony.kstarbound.world.CHUNK_SIZE import ru.dbotthepony.kstarbound.world.ChunkPos @@ -25,7 +25,7 @@ import java.util.function.Supplier import java.util.zip.Inflater import java.util.zip.InflaterInputStream -class LegacyChunkSource(val db: BTreeDB) : IChunkSource { +class LegacyChunkSource(val db: BTreeDB5) : IChunkSource { private val carrier = CarriedExecutor(Starbound.STORAGE_IO_POOL) override fun getTiles(pos: ChunkPos): CompletableFuture>> { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerUniverse.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerUniverse.kt new file mode 100644 index 00000000..f783621a --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerUniverse.kt @@ -0,0 +1,170 @@ +package ru.dbotthepony.kstarbound.server.world + +import com.github.benmanes.caffeine.cache.Cache +import com.github.benmanes.caffeine.cache.Caffeine +import com.github.benmanes.caffeine.cache.Scheduler +import com.google.gson.stream.JsonReader +import it.unimi.dsi.fastutil.io.FastByteArrayInputStream +import it.unimi.dsi.fastutil.objects.ObjectArrayList +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet +import ru.dbotthepony.kommons.collect.chainOptionalFutures +import ru.dbotthepony.kommons.gson.get +import ru.dbotthepony.kommons.io.BTreeDB6 +import ru.dbotthepony.kommons.util.AABBi +import ru.dbotthepony.kommons.util.IStruct2i +import ru.dbotthepony.kommons.util.KOptional +import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.CelestialBaseInformation +import ru.dbotthepony.kstarbound.defs.CelestialGenerationInformation +import ru.dbotthepony.kstarbound.defs.CelestialNames +import ru.dbotthepony.kstarbound.fromJson +import ru.dbotthepony.kstarbound.io.BTreeDB5 +import ru.dbotthepony.kstarbound.util.random.staticRandom64 +import ru.dbotthepony.kstarbound.world.CoordinateMapper +import ru.dbotthepony.kstarbound.world.Universe +import ru.dbotthepony.kstarbound.world.UniversePos +import ru.dbotthepony.kstarbound.world.positiveModulo +import java.io.Closeable +import java.io.File +import java.io.InputStreamReader +import java.time.Duration +import java.util.* +import java.util.concurrent.CompletableFuture +import kotlin.collections.ArrayList + +class ServerUniverse private constructor(marker: Nothing?) : Universe(), Closeable { + constructor() : this(null) { + sources.add(NativeUniverseSource(null, this).generator) + } + + constructor(folder: File) : this(null) { + val nativeFile = File(folder, "universe.kchunks") + val legacyFile = File(folder, "universe.chunks") + + val native = if (!nativeFile.exists()) { + NativeUniverseSource(BTreeDB6.create(nativeFile, sync = false), this) + } else { + NativeUniverseSource(BTreeDB6(nativeFile, sync = false), this) + } + + val legacy = if (legacyFile.exists()) { + LegacyUniverseSource(BTreeDB5(legacyFile)) + } else { + null + } + + sources.add(native.reader) + if (legacy != null) sources.add(legacy) + sources.add(native.generator) + + closeables.add(native) + if (legacy != null) closeables.add(legacy) + } + + override val baseInformation: CelestialBaseInformation + val generationInformation: CelestialGenerationInformation + val celestialNames: CelestialNames + + private val sources = ArrayList() + private val closeables = ArrayList() + + override fun name(pos: UniversePos): CompletableFuture> { + return getChunk(pos).thenApply { + it.flatMap { it.parameters(pos) }.map { it.name } + } + } + + override fun scanSystems(region: AABBi, includedTypes: Set?): CompletableFuture> { + val copy = if (includedTypes != null) ObjectOpenHashSet(includedTypes) else null + val futures = ArrayList>>() + + for (pos in chunkPositions(region)) { + val f = getChunk(pos).thenApply { + it.map> { + val result = ArrayList() + + if (copy == null) { + for (system in it.systems.keys) { + result.add(UniversePos(system)) + } + } else { + for ((system, params) in it.systems) { + if (params.parameters.parameters.get("typeName", "") in copy) { + result.add(UniversePos(system)) + } + } + } + + result + }.orElse(listOf()) + } + + futures.add(f) + } + + return CompletableFuture.allOf(*futures.toTypedArray()) + .thenApply { futures.stream().flatMap { it.get().stream() }.toList() } + } + + override fun scanConstellationLines(region: AABBi): CompletableFuture>> { + val futures = ArrayList>>>() + + for (pos in chunkPositions(region)) { + val f = getChunk(pos).thenApply { + it.map>> { ObjectArrayList(it.constellations) }.orElse(listOf()) + } + + futures.add(f) + } + + return CompletableFuture.allOf(*futures.toTypedArray()) + .thenApply { futures.stream().flatMap { it.get().stream() }.toList() } + } + + override fun scanRegionFullyLoaded(region: AABBi): Boolean { + return true + } + + override fun close() { + closeables.forEach { it.close() } + } + + // Edge case: we load so many chunks AND try to load same chunk twice that it gets loaded/generated twice + // shouldn't cause actual issues though + private val chunkCache: Cache>> = Caffeine + .newBuilder() + .expireAfterAccess(Duration.ofMinutes(10L)) + .maximumSize(1024L) + .softValues() + .scheduler(Scheduler.systemScheduler()) + .build() + + fun getChunk(pos: UniversePos): CompletableFuture> { + return getChunk(world2chunk(Vector2i(pos.location))) + } + + fun getChunk(pos: Vector2i): CompletableFuture> { + return chunkCache.get(pos) { p -> chainOptionalFutures(sources) { it.getChunk(p) } } + } + + init { + val celestial = Starbound.read("/celestial.config") + val stream0 = JsonReader(InputStreamReader(FastByteArrayInputStream(celestial.array()))) + val stream1 = JsonReader(InputStreamReader(FastByteArrayInputStream(celestial.array()))) + + stream0.isLenient = true + stream1.isLenient = true + + baseInformation = Starbound.gson.fromJson(stream0)!! + generationInformation = Starbound.gson.fromJson(stream1)!! + + if (!generationInformation.systemTypePerlin.isInitialized) + generationInformation.systemTypePerlin.init(staticRandom64("SystemTypePerlin")) + + celestialNames = Starbound.gson.fromJson(Starbound.jsonReader("/celestial/names.config"))!! + } + + override val region: AABBi = AABBi(Vector2i(baseInformation.xyCoordRange.x, baseInformation.xyCoordRange.x), Vector2i(baseInformation.xyCoordRange.y, baseInformation.xyCoordRange.y)) + +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt index 86eb6871..b8ef6cd8 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt @@ -70,7 +70,7 @@ class ServerWorld( } } - val spinner = ExecutionSpinner(mailbox, ::spin, Starbound.TICK_TIME_ADVANCE_NANOS) + val spinner = ExecutionSpinner(mailbox::executeQueuedTasks, ::spin, Starbound.TICK_TIME_ADVANCE_NANOS) val thread = Thread(spinner, "Starbound Server World $seed") val ticketListLock = ReentrantLock() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseChunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseChunk.kt new file mode 100644 index 00000000..0aea0f18 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseChunk.kt @@ -0,0 +1,194 @@ +package ru.dbotthepony.kstarbound.server.world + +import com.google.gson.Gson +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import com.google.gson.TypeAdapter +import com.google.gson.TypeAdapterFactory +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import it.unimi.dsi.fastutil.ints.Int2ObjectMap +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet +import ru.dbotthepony.kommons.gson.consumeNull +import ru.dbotthepony.kommons.gson.get +import ru.dbotthepony.kommons.gson.getArray +import ru.dbotthepony.kommons.util.KOptional +import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kommons.vector.Vector3i +import ru.dbotthepony.kstarbound.defs.CelestialParameters +import ru.dbotthepony.kstarbound.defs.CelestialPlanet +import ru.dbotthepony.kstarbound.json.pairAdapter +import ru.dbotthepony.kstarbound.json.pairListAdapter +import ru.dbotthepony.kstarbound.json.pairSetAdapter +import ru.dbotthepony.kstarbound.world.UniversePos + +class UniverseChunk(var chunkPos: Vector2i = Vector2i.ZERO) { + data class System(val parameters: CelestialParameters, val planets: Int2ObjectMap) + + val systems = Object2ObjectOpenHashMap() + val constellations = ObjectOpenHashSet>() + + fun parameters(coordinate: UniversePos): KOptional { + val system = systems[coordinate.location] ?: return KOptional() + + if (coordinate.isSystem) + return KOptional(system.parameters) + else if (coordinate.isPlanet) + return KOptional.ofNullable(system.planets[coordinate.planetOrbit]?.parameters) + else if (coordinate.isSatellite) + return KOptional.ofNullable(system.planets[coordinate.planetOrbit]?.satellites?.get(coordinate.satelliteOrbit)) + else + throw RuntimeException("unreachable code") + } + + class Adapter(gson: Gson) : TypeAdapter() { + private val vectors = gson.getAdapter(Vector2i::class.java) + private val vectors3 = gson.getAdapter(Vector3i::class.java) + private val objects = gson.getAdapter(JsonObject::class.java) + private val systems = gson.pairListAdapter() + private val params = gson.getAdapter(CelestialParameters::class.java) + private val lines = gson.pairAdapter() + + override fun write(out: JsonWriter, value: UniverseChunk?) { + if (value == null) + out.nullValue() + else { + out.beginObject() + + out.name("chunkIndex") + vectors.write(out, value.chunkPos) + + out.name("systemParameters") + out.beginArray() + + for (system in value.systems) { + out.beginArray() + vectors3.write(out, system.key) + params.write(out, system.value.parameters) + out.endArray() + } + + out.endArray() + + out.name("systemObjects") + out.beginArray() + + for (system in value.systems) { + out.beginArray() + vectors3.write(out, system.key) + + out.beginArray() + for ((orbit, planet) in system.value.planets) { + out.beginArray() + out.value(orbit) + + out.beginObject() + out.name("parameters") + params.write(out, planet.parameters) + + out.name("satellites") + out.beginArray() + for ((satelliteOrbit, satellite) in planet.satellites) { + out.beginArray() + out.value(satelliteOrbit) + params.write(out, satellite) + out.endArray() + } + out.endArray() + + out.endObject() + + out.endArray() + } + out.endArray() + + out.endArray() + } + + out.endArray() + + out.name("constellations") + out.beginArray() + out.beginArray() + + for (pair in value.constellations) { + lines.write(out, pair) + } + + out.endArray() + out.endArray() + + out.endObject() + } + } + + override fun read(`in`: JsonReader): UniverseChunk? { + if (`in`.consumeNull()) + return null + + val read = objects.read(`in`) + val chunkPos = read.get("chunkIndex", vectors) + val chunk = UniverseChunk(chunkPos) + + // WARNING: Original game engine stores constellations as List> + // We, on the other hand, don't care and store it as List, since constellations + // are just pairs of system coordinates. + // List> seems to be a leftover from when constellations were supposed to be + // something bigger than just pairs of coordinates, or special processing were required for them. + // (since original game only ever generate a single constellation, or none at all). + // To avoid creating two separate chunk formats, we will save/load constellations as nested list too. + for (sublist in read.getArray("constellations")) { + for (constellation in sublist.asJsonArray) { + chunk.constellations.add(lines.fromJsonTree(constellation)) + } + } + + read.get("systemParameters", systems).forEach { + chunk.systems[it.first] = System(it.second, Int2ObjectOpenHashMap()) + } + + // [[pos, [[orbit, {data}]]]] + val systemObjects = read.getArray("systemObjects") + + for (elem in systemObjects) { + elem as JsonArray + val pos = vectors3.fromJsonTree(elem[0]) + val system = chunk.systems[pos] ?: continue + val planets = elem[1] as JsonArray + + for (planetPair in planets) { + planetPair as JsonArray + val orbit = planetPair[0].asInt + val params = planetPair[1] as JsonObject + + val planet = CelestialPlanet(this.params.fromJsonTree(params["parameters"]), Int2ObjectOpenHashMap()) + val satellitesPairs = params["satellites"] as JsonArray + + for (satellitePair in satellitesPairs) { + satellitePair as JsonArray + val satelliteOrbit = satellitePair[0].asInt + val satelliteData = this.params.fromJsonTree(satellitePair[1]) + planet.satellites[satelliteOrbit] = satelliteData + } + + system.planets[orbit] = planet + } + } + + return chunk + } + } + + companion object : TypeAdapterFactory { + override fun create(gson: Gson, type: TypeToken): TypeAdapter? { + if (type.rawType == UniverseChunk::class.java) { + return Adapter(gson) as TypeAdapter + } + + return null + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseSource.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseSource.kt new file mode 100644 index 00000000..085da532 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseSource.kt @@ -0,0 +1,381 @@ +package ru.dbotthepony.kstarbound.server.world + +import com.google.gson.JsonObject +import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap +import it.unimi.dsi.fastutil.io.FastByteArrayInputStream +import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream +import it.unimi.dsi.fastutil.objects.ObjectArrayList +import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kommons.gson.contains +import ru.dbotthepony.kommons.gson.get +import ru.dbotthepony.kommons.gson.set +import ru.dbotthepony.kommons.io.BTreeDB6 +import ru.dbotthepony.kommons.io.ByteKey +import ru.dbotthepony.kommons.util.CarriedExecutor +import ru.dbotthepony.kommons.util.IStruct2i +import ru.dbotthepony.kommons.util.KOptional +import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kommons.vector.Vector3i +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.CelestialParameters +import ru.dbotthepony.kstarbound.defs.CelestialPlanet +import ru.dbotthepony.kstarbound.defs.JsonDriven +import ru.dbotthepony.kstarbound.io.BTreeDB5 +import ru.dbotthepony.kstarbound.json.readJsonElement +import ru.dbotthepony.kstarbound.json.writeJsonElement +import ru.dbotthepony.kstarbound.math.Line2d +import ru.dbotthepony.kstarbound.util.paddedNumber +import ru.dbotthepony.kstarbound.util.random.random +import ru.dbotthepony.kstarbound.util.random.staticRandom64 +import ru.dbotthepony.kstarbound.world.UniversePos +import java.io.BufferedInputStream +import java.io.BufferedOutputStream +import java.io.Closeable +import java.io.DataInputStream +import java.io.DataOutputStream +import java.util.concurrent.CompletableFuture +import java.util.concurrent.TimeUnit +import java.util.function.Supplier +import java.util.random.RandomGenerator +import java.util.zip.Deflater +import java.util.zip.DeflaterOutputStream +import java.util.zip.InflaterInputStream + +sealed class UniverseSource { + abstract fun getChunk(chunkPos: IStruct2i): CompletableFuture> + + open fun saveChunk(chunkPos: IStruct2i, data: UniverseChunk): Boolean { + return false + } +} + +// legacy chunks are indexed by... 8 byte keys? +// quite strange. +private fun key(chunkPos: IStruct2i): ByteKey { + val (x, y) = chunkPos + val bytes = ByteArray(8) + + bytes[0] = ((x ushr 24) and 0xFF).toByte() + bytes[1] = ((x ushr 16) and 0xFF).toByte() + bytes[2] = ((x ushr 8) and 0xFF).toByte() + bytes[3] = ((x ushr 0) and 0xFF).toByte() + + bytes[4 + 0] = ((y ushr 24) and 0xFF).toByte() + bytes[4 + 1] = ((y ushr 16) and 0xFF).toByte() + bytes[4 + 2] = ((y ushr 8) and 0xFF).toByte() + bytes[4 + 3] = ((y ushr 0) and 0xFF).toByte() + + return ByteKey.wrap(bytes) +} + +class LegacyUniverseSource(private val db: BTreeDB5) : UniverseSource(), Closeable { + private val carried = CarriedExecutor(Starbound.STORAGE_IO_POOL) + + override fun close() { + carried.shutdown() + carried.awaitTermination(Int.MAX_VALUE.toLong(), TimeUnit.MILLISECONDS) + db.close() + } + + override fun getChunk(chunkPos: IStruct2i): CompletableFuture> { + val key = key(chunkPos) + + return CompletableFuture.supplyAsync(Supplier { db.read(key) }, carried).thenApplyAsync { + it.map { + val stream = BufferedInputStream(InflaterInputStream(FastByteArrayInputStream(it))) + UniverseChunk(Vector2i(chunkPos)) + } + } + } +} + +class NativeUniverseSource(private val db: BTreeDB6?, private val universe: ServerUniverse) : Closeable { + private inner class Reader : UniverseSource() { + override fun getChunk(chunkPos: IStruct2i): CompletableFuture> { + if (db == null) return CompletableFuture.completedFuture(KOptional()) + val key = key(chunkPos) + + return CompletableFuture.supplyAsync(Supplier { + db.read(key) + }, carrier).thenApplyAsync { + it.flatMap { + val data = DataInputStream(BufferedInputStream(InflaterInputStream(FastByteArrayInputStream(it)))).readJsonElement() + val expected = Vector2i(chunkPos) + + try { + val chunk = Starbound.gson.fromJson(data, UniverseChunk::class.java) + + if (expected == chunk.chunkPos) { + KOptional(chunk) + } else { + throw IllegalArgumentException("Universe chunk at $chunkPos has ${chunk.chunkPos} stored as position!") + } + } catch (err: Throwable) { + LOGGER.error("Error while deserializing universe chunk from disk storage at $chunkPos, it will be regenerated", err) + KOptional() + } + } + } + } + + override fun saveChunk(chunkPos: IStruct2i, data: UniverseChunk): Boolean { + storeChunk(chunkPos, data) + return true + } + } + + // generator will generate different systems than vanilla + // because of different code flow and ability to use different random number generator + private inner class Generator : UniverseSource() { + override fun getChunk(chunkPos: IStruct2i): CompletableFuture> { + val random = random(staticRandom64(chunkPos.component1(), chunkPos.component2(), "ChunkIndexMix")) + val region = universe.chunkRegion(chunkPos) + + return CompletableFuture.supplyAsync { + val constellationCandidates = ArrayList() + val chunk = UniverseChunk(Vector2i(chunkPos)) + + for (x in region.mins.x until region.maxs.x) { + for (y in region.mins.y until region.maxs.y) { + if (random.nextFloat() < universe.generationInformation.systemProbability) { + val z = random.nextInt(universe.baseInformation.zCoordRange.x, universe.baseInformation.zCoordRange.y) + val pos = Vector3i(x, y, z) + + val system = generateSystem(random, pos) ?: continue + chunk.systems[pos] = system + + if ( + system.parameters.parameters.get("constellationCapable", true) && + system.parameters.parameters.get("magnitude", 0.0) >= universe.generationInformation.minimumConstellationMagnitude + ) { + constellationCandidates.add(Vector2i(x, y)) + } + } + } + } + + for (pair in generateConstellations(random, constellationCandidates)) { + chunk.constellations.add(pair) + } + + storeChunk(chunkPos, chunk) + KOptional(chunk) + } + } + + private fun generateSystem(random: RandomGenerator, location: Vector3i): UniverseChunk.System? { + val typeSelector = universe.generationInformation.systemTypePerlin[location.x.toDouble(), location.y.toDouble()] + + val type = universe.generationInformation.systemTypeBins + .stream() + .sorted { o1, o2 -> o2.first.compareTo(o1.first) } + .filter { it.first <= typeSelector } + .findFirst().map { it.second }.orElse("") + + if (type.isBlank()) + return null + + val system = universe.generationInformation.systemTypes[type]!! + val systemPos = UniversePos(location) + val systemSeed = random.nextLong() + + val prefix = universe.celestialNames.systemPrefixNames.sample(random).orElse("") + val mid = universe.celestialNames.systemNames.sample(random).orElse("missingsystemname $location") + val suffix = universe.celestialNames.systemSuffixNames.sample(random).orElse("") + + val systemName = "$prefix $mid $suffix".trim() + .replace("", random.nextInt(0, 10).toString()) + .replace("", paddedNumber(random.nextInt(0, 100), 2)) + .replace("", paddedNumber(random.nextInt(0, 1000), 3)) + .replace("", paddedNumber(random.nextInt(0, 10000), 4)) + + val systemParams = CelestialParameters( + systemPos, + systemSeed, + systemName, + JsonDriven.mergeNoCopy(system.baseParameters.deepCopy(), system.variationParameters.random(random)) + ) + + if ("typeName" !in systemParams.parameters) { + systemParams.parameters["typeName"] = system.typeName + } + + if ("constellationCapable" !in systemParams.parameters) { + systemParams.parameters["constellationCapable"] = system.constellationCapable + } + + val planets = Int2ObjectArrayMap() + + for (planetOrbitIndex in 1 .. universe.baseInformation.planetOrbitalLevels) { + // this looks dumb at first, but then it makes sense + // in celestial.config, you define orbital region, where planets + // of only specific type appear. + val systemOrbitRegion = system.orbitRegions + .stream() + .filter { planetOrbitIndex in it.orbitRange.x .. it.orbitRange.y } + .findFirst().orElse(null) ?: continue + + if (systemOrbitRegion.bodyProbability > random.nextDouble()) continue + + val planetaryTypeO = systemOrbitRegion.planetaryTypes.sample(random).flatMap { KOptional.ofNullable(universe.generationInformation.planetaryTypes[it]) } + if (!planetaryTypeO.isPresent) continue + val planetaryType = planetaryTypeO.value + + val planetCoordinate = UniversePos(location, planetOrbitIndex) + val planetSeed = random.nextLong() + val planetName = "$systemName ${universe.celestialNames.planetarySuffixes[planetOrbitIndex]}" + + val planetParams = CelestialParameters( + planetCoordinate, + planetSeed, + planetName, + JsonDriven.mergeNoCopy(planetaryType.baseParameters.deepCopy(), planetaryType.variationParameters.random(random)) + ) + + val satellites = Int2ObjectArrayMap() + val maxSatelliteCount = planetaryType.maxSatelliteCount ?: universe.baseInformation.satelliteOrbitalLevels + + if (maxSatelliteCount > 0) { + for (satelliteOrbitIndex in 1 .. universe.baseInformation.satelliteOrbitalLevels) { + if (random.nextDouble() < planetaryType.satelliteProbability) { + val satelliteTypeO = systemOrbitRegion.satelliteTypes.sample(random).flatMap { KOptional.ofNullable(universe.generationInformation.satelliteTypes[it]) } + if (!satelliteTypeO.isPresent) continue + val satelliteType = satelliteTypeO.value + val satelliteSeed = random.nextLong() + val satelliteName = "$planetName ${universe.celestialNames.satelliteSuffixes[satellites.size]}" + val satelliteCoordinate = UniversePos(location, planetOrbitIndex, satelliteOrbitIndex) + + val merge = JsonObject() + JsonDriven.mergeNoCopy(merge, satelliteType.baseParameters) + JsonDriven.mergeNoCopy(merge, satelliteType.variationParameters.random(random)) + + if (systemOrbitRegion.regionName in satelliteType.orbitParameters) { + JsonDriven.mergeNoCopy(merge, satelliteType.orbitParameters[systemOrbitRegion.regionName]!!.random(random)) + } + + satellites[satelliteOrbitIndex] = CelestialParameters( + satelliteCoordinate, + satelliteSeed, + satelliteName, + merge + ) + + if (satellites.size >= maxSatelliteCount) + break + } + } + } + + planets[planetOrbitIndex] = CelestialPlanet(planetParams, satellites) + } + + return UniverseChunk.System(systemParams, planets) + } + + private fun generateConstellations(random: RandomGenerator, candidates: List): List> { + if (candidates.size <= 2 || random.nextDouble() > universe.generationInformation.constellationProbability) return listOf() + + val constellations = ArrayList>() + val constellationPoints = ObjectArrayList() + val constellationLines = ArrayList() + + val target = random.nextInt(universe.generationInformation.constellationLineCountRange.x, universe.generationInformation.constellationLineCountRange.y) + var tries = 0 + + while (constellationLines.size < target && ++tries < universe.generationInformation.constellationMaxTries) { + val start = if (constellationPoints.isEmpty) + candidates.random(random) + else + constellationPoints.random(random) + + val end = candidates.random(random) + + if (start == end) continue + val proposed = Line2d(start.toDoubleVector(), end.toDoubleVector()) + val proposedReversed = proposed.reverse() + + if (proposed in constellationLines || proposedReversed in constellationLines) continue + if (start.distance(end) !in universe.generationInformation.minimumConstellationLineLength .. universe.generationInformation.maximumConstellationLineLength) continue + + var valid = true + + for (existingLine in constellationLines) { + val intersection = proposed.intersect(existingLine) + + if ( + intersection.intersects && + intersection.point.get() != proposed.a && + intersection.point.get() != proposed.b + ) { + valid = false + break + } + + if ( + proposed != existingLine && + proposed.distanceTo(existingLine.a) < universe.generationInformation.minimumConstellationLineCloseness + ) { + valid = false + break + } + + if ( + proposed != existingLine.reverse() && + proposed.distanceTo(existingLine.b) < universe.generationInformation.minimumConstellationLineCloseness + ) { + valid = false + break + } + } + + if (valid) { + // TODO: Original engine generates "single" constellation + // TODO: out of multiple (probably disconnected) point pairs. + // TODO: Should we do the same? It just doesn't seem to make much sense + // Side effect: It is now possible to have chunks where only two stars are connected (one line) + // Original game engine requires at least two lines to be present to form a constellation + constellations.add(start to end) + + constellationLines.add(proposed) + constellationPoints.add(start) + constellationPoints.add(end) + } + } + + return constellations + } + } + + private fun storeChunk(chunkPos: IStruct2i, chunk: UniverseChunk) { + if (db == null) return + val key = key(chunkPos) + + try { + val data = Starbound.gson.toJsonTree(chunk) + val binaryData = FastByteArrayOutputStream() + val stream = DataOutputStream(BufferedOutputStream(DeflaterOutputStream(binaryData, Deflater(2)))) + stream.writeJsonElement(data) + stream.close() + + carrier.execute { + db.write(key, binaryData.array, 0, binaryData.length) + } + } catch (err: Throwable) { + LOGGER.error("Error while saving universe chunk at $chunkPos, it will not persist", err) + } + } + + private val carrier = CarriedExecutor(Starbound.STORAGE_IO_POOL) + val reader: UniverseSource = Reader() + val generator: UniverseSource = Generator() + + override fun close() { + carrier.shutdown() + carrier.awaitTermination(Int.MAX_VALUE.toLong(), TimeUnit.MILLISECONDS) + db?.close() + } + + companion object { + private val LOGGER = LogManager.getLogger() + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/ExecutionSpinner.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/ExecutionSpinner.kt index 43dd1b84..3bdb5a1b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/ExecutionSpinner.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/ExecutionSpinner.kt @@ -8,7 +8,7 @@ import ru.dbotthepony.kstarbound.WindowsBindings import java.util.concurrent.locks.LockSupport import java.util.function.BooleanSupplier -class ExecutionSpinner(private val executor: MailboxExecutorService, private val spinner: BooleanSupplier, private val timeBetweenFrames: Long) : Runnable { +class ExecutionSpinner(private val waiter: Runnable, private val spinner: BooleanSupplier, private val timeBetweenFrames: Long) : Runnable { init { Companion } @@ -54,7 +54,7 @@ class ExecutionSpinner(private val executor: MailboxExecutorService, private val var diff = timeUntilNextFrame() while (diff > 0L) { - executor.executeQueuedTasks() + waiter.run() diff = timeUntilNextFrame() if (diff >= SYSTEM_SCHEDULER_RESOLUTION * 2L) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/ItemStack.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/ItemStack.kt index 5ac9b120..f787dbd9 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/ItemStack.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/ItemStack.kt @@ -12,7 +12,7 @@ import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition -import ru.dbotthepony.kstarbound.json.consumeNull +import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kstarbound.lua.from class ItemStack private constructor(item: Registry.Entry?, count: Long, val parameters: JsonObject, marker: Unit) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt index 23bbadcd..8bc87f06 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt @@ -1,18 +1,10 @@ package ru.dbotthepony.kstarbound.util -import com.google.common.collect.ImmutableList -import com.google.common.collect.ImmutableMap -import com.google.common.collect.ImmutableSet import com.google.gson.JsonArray import com.google.gson.JsonElement import com.google.gson.JsonObject -import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kstarbound.Starbound -import java.lang.ref.Reference import java.util.* -import java.util.concurrent.CompletableFuture -import java.util.function.Consumer -import java.util.stream.Stream fun String.sbIntern(): String { return Starbound.STRINGS.intern(this) @@ -72,3 +64,15 @@ fun UUID.toStarboundString(): String { return builder.toString() } + +fun paddedNumber(number: Int, digits: Int): String { + val str = number.toString() + + if (str.length > digits) { + return str.substring(0, digits) + } else if (str.length == digits) { + return str + } else { + return "0".repeat(digits - str.length) + str + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/AbstractPerlinNoise.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/AbstractPerlinNoise.kt new file mode 100644 index 00000000..25b4d5c3 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/AbstractPerlinNoise.kt @@ -0,0 +1,238 @@ +package ru.dbotthepony.kstarbound.util.random + +import com.google.gson.Gson +import com.google.gson.TypeAdapter +import com.google.gson.TypeAdapterFactory +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import ru.dbotthepony.kommons.arrays.Double2DArray +import ru.dbotthepony.kommons.math.linearInterpolation +import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters +import ru.dbotthepony.kommons.gson.consumeNull +import kotlin.math.floor +import kotlin.math.sqrt + +/** + * Base class for noise generator. + * + * Once [init]ialized, generator is thread-safe, and can be sampled + * concurrently by any amount of threads. + */ +abstract class AbstractPerlinNoise(val parameters: PerlinNoiseParameters) { + abstract operator fun get(x: Double): Double + abstract operator fun get(x: Double, y: Double): Double + abstract operator fun get(x: Double, y: Double, z: Double): Double + + var isInitialized = false + private set + + init { + if (parameters.seed != null) { + init(parameters.seed) + } + } + + val scaleD = parameters.scale.toDouble() + + protected data class Setup(val b0: Int, val b1: Int, val r0: Double, val r1: Double) + + protected val p = IntArray(parameters.scale * 2 + 2) + protected val g1 = DoubleArray(parameters.scale * 2 + 2) + protected val g2 = Double2DArray.allocate(parameters.scale * 2 + 2, 2) + protected val g3 = Double2DArray.allocate(parameters.scale * 2 + 2, 3) + + fun init(seed: Long) { + isInitialized = true + + p.fill(0) + g1.fill(0.0) + g2.fill(0.0) + g3.fill(0.0) + + val random = random(seed) + + for (i in 0 until parameters.scale) { + p[i] = i + + g1[i] = random.nextInt(-parameters.scale, parameters.scale) / scaleD + + g2[i, 0] = random.nextInt(-parameters.scale, parameters.scale) / scaleD + g2[i, 1] = random.nextInt(-parameters.scale, parameters.scale) / scaleD + + g3[i, 0] = random.nextInt(-parameters.scale, parameters.scale) / scaleD + g3[i, 1] = random.nextInt(-parameters.scale, parameters.scale) / scaleD + g3[i, 2] = random.nextInt(-parameters.scale, parameters.scale) / scaleD + + val l2 = sqrt(g2[i, 0] * g2[i, 0] + g2[i, 1] * g2[i, 1]) + val l3 = sqrt(g3[i, 0] * g3[i, 0] + g3[i, 1] * g3[i, 1] + g3[i, 2] * g3[i, 2]) + + if (l2 == 0.0) { + g2[i, 0] = 1.0 + g2[i, 1] = 0.0 + } else { + g2[i, 0] = g2[i, 0] / l2 + g2[i, 1] = g2[i, 1] / l2 + } + + if (l3 == 0.0) { + g3[i, 0] = 1.0 + g3[i, 1] = 0.0 + g3[i, 2] = 0.0 + } else { + g3[i, 0] = g3[i, 0] / l3 + g3[i, 1] = g3[i, 1] / l3 + g3[i, 2] = g3[i, 2] / l3 + } + } + + for (i in parameters.scale downTo 1) { + val k = p[i] + val j = random.nextInt(0, parameters.scale - 1) + p[i] = p[j] + p[j] = k + } + + for (i in 0 until parameters.scale + 2) { + p[parameters.scale + i] = p[i] + g1[parameters.scale + i] = g1[i] + + g2[parameters.scale + i, 0] = g2[i, 0] + g2[parameters.scale + i, 1] = g2[i, 1] + + g3[parameters.scale + i, 0] = g3[i, 0] + g3[parameters.scale + i, 1] = g3[i, 1] + g3[parameters.scale + i, 2] = g3[i, 2] + } + } + + protected fun curve(value: Double): Double { + return value * value * (3.0 - 2.0 * value) + } + + protected fun setup(value: Double): Setup { + val iv: Int = floor(value).toInt() + val fv: Double = value - iv + + val b0 = iv and (parameters.scale - 1) + val b1 = (iv + 1) and (parameters.scale - 1) + val r1 = fv - 1.0 + + return Setup(b0, b1, fv, r1) + } + + protected fun at2(x: Double, y: Double, rx: Double, ry: Double): Double { + return rx * x + ry * y + } + + protected fun at3(x: Double, y: Double, z: Double, rx: Double, ry: Double, rz: Double): Double { + return rx * x + ry * y + rz * z + } + + protected fun noise1(x: Double): Double { + val (bx0, bx1, rx0, rx1) = setup(x) + + val sx = curve(rx0) + val u = rx0 * g1[p[bx0]] + val v = rx1 * g1[p[bx1]] + + return linearInterpolation(sx, u, v) + } + + protected fun noise2(x: Double, y: Double): Double { + val (bx0, bx1, rx0, rx1) = setup(x) + val (by0, by1, ry0, ry1) = setup(y) + + val i = p[bx0] + val j = p[bx1] + + val b00 = p[i + by0] + val b10 = p[j + by0] + val b01 = p[i + by1] + val b11 = p[j + by1] + + val sx = curve(rx0) + val sy = curve(ry0) + + var u = at2(g2[b00, 0], g2[b00, 1], rx0, ry0) + var v = at2(g2[b10, 0], g2[b10, 1], rx1, ry0) + val a = linearInterpolation(sx, u, v) + + u = at2(g2[b01, 0], g2[b01, 1], rx0, ry1) + v = at2(g2[b11, 0], g2[b11, 1], rx1, ry1) + val b = linearInterpolation(sx, u, v) + + return linearInterpolation(sy, a, b) + } + + protected fun noise3(x: Double, y: Double, z: Double): Double { + val (bx0, bx1, rx0, rx1) = setup(x) + val (by0, by1, ry0, ry1) = setup(y) + val (bz0, bz1, rz0, rz1) = setup(z) + + val i = p[bx0]; + val j = p[bx1]; + + val b00 = p[i + by0]; + val b10 = p[j + by0]; + val b01 = p[i + by1]; + val b11 = p[j + by1]; + + val sx = curve(rx0); + val sy = curve(ry0); + val sz = curve(rz0); + + var u = at3(g3[b00 + bz0, 0], g3[b00 + bz0, 1], g3[b00 + bz0, 2], rx0, ry0, rz0) + var v = at3(g3[b10 + bz0, 0], g3[b10 + bz0, 1], g3[b10 + bz0, 2], rx1, ry0, rz0) + var a = linearInterpolation(sx, u, v) + + u = at3(g3[b01 + bz0, 0], g3[b01 + bz0, 1], g3[b01 + bz0, 2], rx0, ry1, rz0) + v = at3(g3[b11 + bz0, 0], g3[b11 + bz0, 1], g3[b11 + bz0, 2], rx1, ry1, rz0) + var b = linearInterpolation(sx, u, v) + + val c = linearInterpolation(sy, a, b) + + u = at3(g3[b00 + bz1, 0], g3[b00 + bz1, 1], g3[b00 + bz1, 2], rx0, ry0, rz1) + v = at3(g3[b10 + bz1, 0], g3[b10 + bz1, 1], g3[b10 + bz1, 2], rx1, ry0, rz1) + a = linearInterpolation(sx, u, v) + + u = at3(g3[b01 + bz1, 0], g3[b01 + bz1, 1], g3[b01 + bz1, 2], rx0, ry1, rz1) + v = at3(g3[b11 + bz1, 0], g3[b11 + bz1, 1], g3[b11 + bz1, 2], rx1, ry1, rz1) + b = linearInterpolation(sx, u, v) + + val d = linearInterpolation(sy, a, b) + + return linearInterpolation(sz, c, d) + } + + companion object : TypeAdapterFactory { + fun of(parameters: PerlinNoiseParameters): AbstractPerlinNoise { + return when (parameters.type) { + PerlinNoiseParameters.Type.PERLIN -> PerlinNoise(parameters) + PerlinNoiseParameters.Type.BILLOW -> BillowNoise(parameters) + PerlinNoiseParameters.Type.RIDGED_MULTI -> RidgedNoise(parameters) + } + } + + override fun create(gson: Gson, type: TypeToken): TypeAdapter? { + if (type.rawType == AbstractPerlinNoise::class.java) { + return object : TypeAdapter() { + private val parent = gson.getAdapter(PerlinNoiseParameters::class.java) + + override fun write(out: JsonWriter, value: AbstractPerlinNoise?) { + return parent.write(out, value?.parameters) + } + + override fun read(`in`: JsonReader): AbstractPerlinNoise? { + if (`in`.consumeNull()) + return null + + return of(parent.read(`in`)!!) + } + } as TypeAdapter + } + + return null + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/BillowNoise.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/BillowNoise.kt new file mode 100644 index 00000000..28425226 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/BillowNoise.kt @@ -0,0 +1,58 @@ +package ru.dbotthepony.kstarbound.util.random + +import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters +import kotlin.math.absoluteValue + +class BillowNoise(parameters: PerlinNoiseParameters) : AbstractPerlinNoise(parameters) { + init { + require(parameters.type == PerlinNoiseParameters.Type.BILLOW) + } + + override fun get(x: Double): Double { + var sum = 0.0 + var p = x * parameters.frequency + var scale = 1.0 + + for (i in 0 until parameters.octaves) { + sum += (2.0 * noise1(p).absoluteValue - 1.0) / scale + scale *= parameters.alpha + p *= parameters.beta + } + + return (sum + 0.5) * parameters.amplitude + parameters.bias + } + + override fun get(x: Double, y: Double): Double { + var sum = 0.0 + var px = x * parameters.frequency + var py = y * parameters.frequency + var scale = 1.0 + + for (i in 0 until parameters.octaves) { + sum += (2.0 * noise2(px, py).absoluteValue - 1.0) / scale + scale *= parameters.alpha + px *= parameters.beta + py *= parameters.beta + } + + return (sum + 0.5) * parameters.amplitude + parameters.bias + } + + override fun get(x: Double, y: Double, z: Double): Double { + var sum = 0.0 + var px = x * parameters.frequency + var py = y * parameters.frequency + var pz = z * parameters.frequency + var scale = 1.0 + + for (i in 0 until parameters.octaves) { + sum += (2.0 * noise3(px, py, pz).absoluteValue - 1.0) / scale + scale *= parameters.alpha + px *= parameters.beta + py *= parameters.beta + pz *= parameters.beta + } + + return (sum + 0.5) * parameters.amplitude + parameters.bias + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/MWCRandom.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/MWCRandom.kt new file mode 100644 index 00000000..10489585 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/MWCRandom.kt @@ -0,0 +1,71 @@ +package ru.dbotthepony.kstarbound.util.random + +import ru.dbotthepony.kstarbound.getValue +import java.util.random.RandomGenerator + +// multiply with carry random number generator, as used by original Starbound +// Game's code SHOULD NOT be tied to this generator, random() global function should be used +// What interesting though, is the size of cycle - 256 (8092 bits). Others use much bigger number - 4096 +class MWCRandom(seed: Long = System.nanoTime(), cycle: Int = 256, windupIterations: Int = 32) : RandomGenerator { + private val data = IntArray(cycle) + private var carry = 0 + private var dataIndex = 0 + + var seed: Long = seed + private set + + init { + require(windupIterations >= 0) { "Negative amount of windup iterations: $windupIterations" } + init(seed, windupIterations) + } + + /** + * re-initialize this MWC generator + */ + fun init(seed: Long, windupIterations: Int = 0) { + this.seed = seed + carry = (seed % MAGIC).toInt() + + data[0] = seed.toInt() + data[1] = seed.ushr(32).toInt() + + for (i in 2 until data.size) { + data[i] = 69069 * data[i - 2] + 362437 + } + + dataIndex = data.size - 1 + + // initial windup + for (i in 0 until windupIterations) { + nextInt() + } + } + + fun addEntropy(seed: Long = System.nanoTime()) { + // Same algo as init, but bitwise xor with existing data + carry = ((carry.toLong().xor(seed)) % MAGIC).toInt() + + data[0] = data[0].xor(seed.toInt()) + data[1] = data[1].xor(seed.ushr(32).xor(seed).toInt()) + + for (i in 2 until data.size) { + data[i] = data[i].xor(69069 * data[i - 2] + 362437) + } + } + + override fun nextLong(): Long { + dataIndex = (dataIndex + 1) % data.size + val t = MAGIC.toLong() * data[dataIndex].toLong() + carry.toLong() + + carry = t.ushr(32).toInt() + data[dataIndex] = t.toInt() + + return t + } + + companion object { + const val MAGIC = 809430660 + + val GLOBAL: MWCRandom by ThreadLocal.withInitial { MWCRandom() } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/PerlinNoise.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/PerlinNoise.kt new file mode 100644 index 00000000..7a028e7f --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/PerlinNoise.kt @@ -0,0 +1,57 @@ +package ru.dbotthepony.kstarbound.util.random + +import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters + +class PerlinNoise(parameters: PerlinNoiseParameters) : AbstractPerlinNoise(parameters) { + init { + require(parameters.type == PerlinNoiseParameters.Type.PERLIN) + } + + override fun get(x: Double): Double { + var sum = 0.0 + var p = x * parameters.frequency + var scale = 1.0 + + for (i in 0 until parameters.octaves) { + sum += noise1(p) / scale + scale *= parameters.alpha + p *= parameters.beta + } + + return sum * parameters.amplitude + parameters.bias + } + + override fun get(x: Double, y: Double): Double { + var sum = 0.0 + var px = x * parameters.frequency + var py = y * parameters.frequency + var scale = 1.0 + + for (i in 0 until parameters.octaves) { + sum += noise2(px, py) / scale + scale *= parameters.alpha + px *= parameters.beta + py *= parameters.beta + } + + return sum * parameters.amplitude + parameters.bias + } + + override fun get(x: Double, y: Double, z: Double): Double { + var sum = 0.0 + var px = x * parameters.frequency + var py = y * parameters.frequency + var pz = z * parameters.frequency + var scale = 1.0 + + for (i in 0 until parameters.octaves) { + sum += noise3(px, py, pz) / scale + scale *= parameters.alpha + px *= parameters.beta + py *= parameters.beta + pz *= parameters.beta + } + + return sum * parameters.amplitude + parameters.bias + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt new file mode 100644 index 00000000..5aa572e6 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt @@ -0,0 +1,209 @@ +package ru.dbotthepony.kstarbound.util.random + +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import it.unimi.dsi.fastutil.bytes.ByteConsumer +import ru.dbotthepony.kommons.util.XXHash32 +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.ceil +import kotlin.math.floor +import kotlin.math.ln +import kotlin.math.sqrt + +/** + * Decouples implementation from semantics. + * Code should construct random generator using this function. + * Replacing generator returned here will affect all random generation code. + */ +fun random(seed: Long = System.nanoTime()): RandomGenerator { + return MWCRandom(seed) +} + +private fun toBytes(accept: ByteConsumer, value: Short) { + accept.accept(value.toByte()) + accept.accept((value.toInt() ushr 8).toByte()) +} + +private fun toBytes(accept: ByteConsumer, value: Int) { + accept.accept(value.toByte()) + accept.accept((value ushr 8).toByte()) + accept.accept((value ushr 16).toByte()) + accept.accept((value ushr 24).toByte()) +} + +private fun toBytes(accept: ByteConsumer, value: Long) { + accept.accept(value.toByte()) + accept.accept((value ushr 8).toByte()) + accept.accept((value ushr 16).toByte()) + accept.accept((value ushr 24).toByte()) + accept.accept((value ushr 32).toByte()) + accept.accept((value ushr 40).toByte()) + accept.accept((value ushr 48).toByte()) + accept.accept((value ushr 56).toByte()) +} + +private fun toBytes(accept: ByteConsumer, value: Double) { + toBytes(accept, value.toBits()) +} + +private fun toBytes(accept: ByteConsumer, value: Float) { + toBytes(accept, value.toBits()) +} + +fun staticRandom32(vararg values: Any): Int { + val digest = XXHash32(2938728349.toInt()) + + for (value in values) { + when (value) { + is String -> digest.update(value.toByteArray()) + is Byte -> digest.update(value) + is Boolean -> digest.update(if (value) 1 else 0) + is Short -> toBytes(digest::update, value) + is Int -> toBytes(digest::update, value) + is Long -> toBytes(digest::update, value) + is Double -> toBytes(digest::update, value) + is Float -> toBytes(digest::update, value) + else -> throw IllegalArgumentException("Can't hash value of type ${value::class.qualifiedName}") + } + } + + return digest.digestAsInt() +} + +fun staticRandom64(vararg values: Any): Long { + val digest = XXHash64(1997293021376312589L) + + for (value in values) { + when (value) { + is String -> digest.update(value.toByteArray()) + is Byte -> digest.update(value) + is Boolean -> digest.update(if (value) 1 else 0) + is Short -> toBytes(digest::update, value) + is Int -> toBytes(digest::update, value) + is Long -> toBytes(digest::update, value) + is Double -> toBytes(digest::update, value) + is Float -> toBytes(digest::update, value) + else -> throw IllegalArgumentException("Can't hash value of type ${value::class.qualifiedName}") + } + } + + return digest.digestAsLong() +} + +// normal distribution via Box-Muller +fun RandomGenerator.nextNormalFloat(stddev: Float, mean: Float): Float { + var rand1: Float + var rand2: Float + var distSqr: Float + + do { + rand1 = 2f * nextFloat() - 1f + rand2 = 2f * nextFloat() - 1f + distSqr = rand1 * rand1 + rand2 * rand2 + } while (distSqr >= 1) + + val mapping = sqrt(-2f * ln(distSqr) / distSqr) + return rand1 * mapping * stddev + mean +} + +// normal distribution via Box-Muller +fun RandomGenerator.nextNormalDouble(stddev: Double, mean: Double): Double { + var rand1: Double + var rand2: Double + var distSqr: Double + + do { + rand1 = 2.0 * nextDouble() - 1.0 + rand2 = 2.0 * nextDouble() - 1.0 + distSqr = rand1 * rand1 + rand2 * rand2 + } while (distSqr >= 1) + + val mapping = sqrt(-2.0 * ln(distSqr) / distSqr) + return rand1 * mapping * stddev + mean +} + +fun RandomGenerator.stochasticRound(value: Double): Long { + val part = value - floor(value) + + if (nextDouble() < part) + return ceil(value).toLong() + else + return floor(value).toLong() +} + +fun RandomGenerator.bytes(): IntStream { + // avoid flatMap since that would create lots of heap garbage + val data = IntArray(4) + var index = 0 + var populated = false + + return IntStream.generate { + if (!populated || index == 4) { + val value = nextInt() + data[0] = value.and(0xFF) + data[1] = value.ushr(8).and(0xFF) + data[2] = value.ushr(16).and(0xFF) + data[3] = value.ushr(24).and(0xFF) + populated = true + index = 0 + } + + data[index++] + } +} + +fun RandomGenerator.nextBytes(buf: ByteArray, offset: Int, length: Int) { + Objects.checkFromIndexSize(offset, buf.size, length) + if (length == 0) return + var offset = offset + + bytes().limit(length.toLong()).forEach { + buf[offset++] = it.toByte() + } +} + +fun RandomGenerator.nextBytes(length: Int): ByteArray { + if (length == 0) return ByteArray(0) + require(length > 0) { "Invalid length: $length" } + val data = ByteArray(length) + var index = 0 + + bytes().limit(length.toLong()).forEach { + data[index++] = it.toByte() + } + + return data +} + +fun Collection.random(random: RandomGenerator): T { + if (isEmpty()) + throw NoSuchElementException("List is empty") + + return elementAt(random.nextInt(size)) +} + +fun JsonArray.random(random: RandomGenerator): JsonElement { + if (isEmpty()) + throw NoSuchElementException("List is empty") + + return elementAt(random.nextInt(size())) +} + +fun Collection.random(random: RandomGenerator, default: () -> T): T { + if (isEmpty()) + return default.invoke() + + return elementAt(random.nextInt(size)) +} + +fun JsonArray.random(random: RandomGenerator, default: () -> JsonElement): JsonElement { + if (isEmpty()) + return default.invoke() + + return elementAt(random.nextInt(size())) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RidgedNoise.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RidgedNoise.kt new file mode 100644 index 00000000..5bb3d33d --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RidgedNoise.kt @@ -0,0 +1,82 @@ +package ru.dbotthepony.kstarbound.util.random + +import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters +import kotlin.math.absoluteValue + +class RidgedNoise(parameters: PerlinNoiseParameters) : AbstractPerlinNoise(parameters) { + init { + require(parameters.type == PerlinNoiseParameters.Type.RIDGED_MULTI) + } + + override fun get(x: Double): Double { + var sum = 0.0 + var p = x * parameters.frequency + var scale = 1.0 + var weight = 1.0 + + for (i in 0 until parameters.octaves) { + var value = noise1(p) + value = parameters.offset - value.absoluteValue + value *= value + value *= weight + + weight = (value * parameters.gain).coerceIn(0.0, 1.0) + + sum += value / scale + scale *= parameters.alpha + p *= parameters.beta + } + + return ((sum * 1.25) - 1.0) * parameters.amplitude + parameters.bias + } + + override fun get(x: Double, y: Double): Double { + var sum = 0.0 + var px = x * parameters.frequency + var py = y * parameters.frequency + var scale = 1.0 + var weight = 1.0 + + for (i in 0 until parameters.octaves) { + var value = noise2(px, py) + value = parameters.offset - value.absoluteValue + value *= value + value *= weight + + weight = (value * parameters.gain).coerceIn(0.0, 1.0) + + sum += value / scale + scale *= parameters.alpha + px *= parameters.beta + py *= parameters.beta + } + + return ((sum * 1.25) - 1.0) * parameters.amplitude + parameters.bias + } + + override fun get(x: Double, y: Double, z: Double): Double { + var sum = 0.0 + var px = x * parameters.frequency + var py = y * parameters.frequency + var pz = z * parameters.frequency + var scale = 1.0 + var weight = 1.0 + + for (i in 0 until parameters.octaves) { + var value = noise3(px, py, pz) + value = parameters.offset - value.absoluteValue + value *= value + value *= weight + + weight = (value * parameters.gain).coerceIn(0.0, 1.0) + + sum += value / scale + scale *= parameters.alpha + px *= parameters.beta + py *= parameters.beta + pz *= parameters.beta + } + + return ((sum * 1.25) - 1.0) * parameters.amplitude + parameters.bias + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Universe.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Universe.kt new file mode 100644 index 00000000..9e68e4ba --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Universe.kt @@ -0,0 +1,66 @@ +package ru.dbotthepony.kstarbound.world + +import com.github.benmanes.caffeine.cache.Caffeine +import ru.dbotthepony.kommons.util.AABBi +import ru.dbotthepony.kommons.util.IStruct2i +import ru.dbotthepony.kommons.util.KOptional +import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kstarbound.defs.CelestialBaseInformation +import java.time.Duration +import java.util.concurrent.CompletableFuture + +abstract class Universe { + abstract val region: AABBi + abstract val baseInformation: CelestialBaseInformation + + abstract fun name(pos: UniversePos): CompletableFuture> + + /** + * Return all valid system coordinates in the given x/y range. All systems + * are guaranteed to have unique x/y coordinates, and are meant to be viewed + * from the top in 2d. The z-coordinate is there simpy as a validation + * parameter. + */ + abstract fun scanSystems(region: AABBi, includedTypes: Set? = null): CompletableFuture> + abstract fun scanConstellationLines(region: AABBi): CompletableFuture>> + + /** + * Returns false if part or all of the specified region is not loaded. This + * is only relevant for calls to scanSystems and scanConstellationLines, and + * does not imply that each individual system in the given region is fully + * loaded with all planets moons etc, only that scanSystem and + * scanConstellationLines are not waiting on missing data. + */ + abstract fun scanRegionFullyLoaded(region: AABBi): Boolean + + fun chunkPositions(region: AABBi): List { + if (region.mins == region.maxs) + return emptyList() + + val result = ArrayList() + + val mins = world2chunk(region.mins) + val maxs = world2chunk(region.maxs - Vector2i.POSITIVE_XY) + + for (x in mins.x .. maxs.x) { + for (y in mins.y .. maxs.y) { + result.add(Vector2i(x, y)) + } + } + + return result + } + + fun world2chunk(pos: IStruct2i): Vector2i { + val (x, y) = pos + + return Vector2i( + (x - positiveModulo(x, baseInformation.chunkSize)) / baseInformation.chunkSize, + (y - positiveModulo(y, baseInformation.chunkSize)) / baseInformation.chunkSize) + } + + fun chunkRegion(chunkPos: IStruct2i): AABBi { + val mins = Vector2i(chunkPos) * baseInformation.chunkSize + return AABBi(mins, mins + Vector2i.POSITIVE_XY * baseInformation.chunkSize) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/UniversePos.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/UniversePos.kt new file mode 100644 index 00000000..5a13962e --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/UniversePos.kt @@ -0,0 +1,133 @@ +package ru.dbotthepony.kstarbound.world + +import com.google.gson.Gson +import com.google.gson.JsonObject +import com.google.gson.JsonSyntaxException +import com.google.gson.TypeAdapter +import com.google.gson.TypeAdapterFactory +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonToken +import com.google.gson.stream.JsonWriter +import ru.dbotthepony.kommons.gson.get +import ru.dbotthepony.kommons.vector.Vector3i +import ru.dbotthepony.kommons.gson.consumeNull + +data class UniversePos(val location: Vector3i = Vector3i.ZERO, val planetOrbit: Int = 0, val satelliteOrbit: Int = 0) { + val isSystem: Boolean + get() = planetOrbit == 0 + + val isPlanet: Boolean + get() = planetOrbit != 0 && satelliteOrbit == 0 + + val isSatellite: Boolean + get() = planetOrbit != 0 && satelliteOrbit == 0 + + val orbitNumber: Int + get() = if (isSatellite) satelliteOrbit else if (isPlanet) planetOrbit else 0 + + fun system(): UniversePos { + if (planetOrbit == 0 && satelliteOrbit == 0) + return this + + return UniversePos(location) + } + + fun planet(): UniversePos { + if (satelliteOrbit == 0) + return this + + return UniversePos(location, planetOrbit) + } + + fun satellite(): UniversePos { + return this + } + + fun parent(): UniversePos { + if (isSatellite) + return planet() + else if (isPlanet) + return system() + else + return this + } + + companion object : TypeAdapterFactory { + private val splitter = Regex("[ _:]") + val ZERO = UniversePos() + + override fun create(gson: Gson, type: TypeToken): TypeAdapter? { + if (type.rawType == UniversePos::class.java) { + val vectors = gson.getAdapter(Vector3i::class.java) + val objects = gson.getAdapter(JsonObject::class.java) + + return object : TypeAdapter() { + override fun write(out: JsonWriter, value: UniversePos?) { + if (value == null) + out.nullValue() + else { + out.beginObject() + + out.name("location") + vectors.write(out, value.location) + + out.name("planet") + out.value(value.planetOrbit) + + out.name("satellite") + out.value(value.satelliteOrbit) + + out.endObject() + } + } + + override fun read(`in`: JsonReader): UniversePos? { + if (`in`.consumeNull()) + return null + + if (`in`.peek() == JsonToken.BEGIN_OBJECT) { + val values = objects.read(`in`)!! + val location = values.get("location", vectors) + val planet = values.get("planet", 0) + val orbit = values.get("orbit", 0) + return UniversePos(location, planet, orbit) + } + + if (`in`.peek() == JsonToken.STRING) { + val read = `in`.nextString().trim() + + if (read == "" || read.lowercase() == "") + return ZERO + else { + try { + val split = read.split(splitter) + val x = split[0].toInt() + val y = split[1].toInt() + val z = split[2].toInt() + + val planet = if (split.size > 3) split[3].toInt() else 0 + val orbit = if (split.size > 4) split[4].toInt() else 0 + + if (planet <= 0) // TODO: ??? Determine, if this is a bug in original code + throw IndexOutOfBoundsException("Planetary orbit: $planet") + + if (orbit < 0) + throw IndexOutOfBoundsException("Satellite orbit: $orbit") + + return UniversePos(Vector3i(x, y, z), planet, orbit) + } catch (err: Throwable) { + throw JsonSyntaxException("Error parsing UniversePos from string", err) + } + } + } + + throw JsonSyntaxException("Invalid data type for UniversePos: ${`in`.peek()}") + } + } as TypeAdapter + } + + return null + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/WorldObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/WorldObject.kt index 51ad84a1..157b926b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/WorldObject.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/WorldObject.kt @@ -20,8 +20,8 @@ import ru.dbotthepony.kstarbound.defs.image.SpriteReference import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition import ru.dbotthepony.kstarbound.defs.`object`.ObjectOrientation import ru.dbotthepony.kstarbound.server.world.ServerWorld -import ru.dbotthepony.kstarbound.json.get -import ru.dbotthepony.kstarbound.json.set +import ru.dbotthepony.kommons.gson.get +import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kstarbound.world.Side import ru.dbotthepony.kstarbound.world.LightCalculator import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNITf diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt index 3dba544e..fd7105ad 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt @@ -17,7 +17,7 @@ import ru.dbotthepony.kommons.util.AABB import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.gl.vertex.GeometryType -import ru.dbotthepony.kstarbound.json.consumeNull +import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kstarbound.json.listAdapter import kotlin.math.absoluteValue import kotlin.math.cos