KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt

646 lines
19 KiB
Kotlin

package ru.dbotthepony.kstarbound
import com.github.benmanes.caffeine.cache.Interner
import com.google.gson.*
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.asCoroutineDispatcher
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.AABBTypeAdapter
import ru.dbotthepony.kommons.gson.AABBiTypeAdapter
import ru.dbotthepony.kommons.gson.EitherTypeAdapter
import ru.dbotthepony.kommons.gson.KOptionalTypeAdapter
import ru.dbotthepony.kommons.gson.NothingAdapter
import ru.dbotthepony.kommons.gson.Vector2dTypeAdapter
import ru.dbotthepony.kommons.gson.Vector2fTypeAdapter
import ru.dbotthepony.kommons.gson.Vector2iTypeAdapter
import ru.dbotthepony.kommons.gson.Vector3dTypeAdapter
import ru.dbotthepony.kommons.gson.Vector3fTypeAdapter
import ru.dbotthepony.kommons.gson.Vector3iTypeAdapter
import ru.dbotthepony.kommons.gson.Vector4dTypeAdapter
import ru.dbotthepony.kommons.gson.Vector4fTypeAdapter
import ru.dbotthepony.kommons.gson.Vector4iTypeAdapter
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
import ru.dbotthepony.kstarbound.defs.item.api.IArmorItemDefinition
import ru.dbotthepony.kstarbound.defs.item.InventoryIcon
import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition
import ru.dbotthepony.kstarbound.defs.actor.player.BlueprintLearnList
import ru.dbotthepony.kstarbound.defs.animation.Particle
import ru.dbotthepony.kstarbound.defs.quest.QuestParameter
import ru.dbotthepony.kstarbound.defs.world.CelestialParameters
import ru.dbotthepony.kstarbound.defs.world.VisitableWorldParametersType
import ru.dbotthepony.kstarbound.defs.world.BiomePlaceables
import ru.dbotthepony.kstarbound.defs.world.BiomePlacementDistributionType
import ru.dbotthepony.kstarbound.defs.world.BiomePlacementItemType
import ru.dbotthepony.kstarbound.defs.world.WorldLayout
import ru.dbotthepony.kstarbound.world.terrain.TerrainSelectorType
import ru.dbotthepony.kstarbound.io.*
import ru.dbotthepony.kstarbound.json.factory.MapsTypeAdapterFactory
import ru.dbotthepony.kstarbound.json.InternedJsonElementAdapter
import ru.dbotthepony.kstarbound.json.InternedStringAdapter
import ru.dbotthepony.kstarbound.json.LongRangeAdapter
import ru.dbotthepony.kstarbound.json.builder.EnumAdapter
import ru.dbotthepony.kstarbound.json.builder.BuilderAdapter
import ru.dbotthepony.kstarbound.json.builder.FactoryAdapter
import ru.dbotthepony.kstarbound.json.JsonImplementationTypeFactory
import ru.dbotthepony.kstarbound.json.factory.CollectionAdapterFactory
import ru.dbotthepony.kstarbound.json.factory.ImmutableCollectionAdapterFactory
import ru.dbotthepony.kstarbound.json.factory.PairAdapterFactory
import ru.dbotthepony.kstarbound.json.factory.RGBAColorTypeAdapter
import ru.dbotthepony.kstarbound.json.factory.SingletonTypeAdapterFactory
import ru.dbotthepony.kstarbound.server.world.UniverseChunk
import ru.dbotthepony.kstarbound.item.ItemStack
import ru.dbotthepony.kstarbound.json.JsonAdapterTypeFactory
import ru.dbotthepony.kstarbound.json.JsonPath
import ru.dbotthepony.kstarbound.json.NativeLegacy
import ru.dbotthepony.kstarbound.util.Directives
import ru.dbotthepony.kstarbound.util.ExceptionLogger
import ru.dbotthepony.kstarbound.util.SBPattern
import ru.dbotthepony.kstarbound.util.HashTableInterner
import ru.dbotthepony.kstarbound.util.MailboxExecutorService
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.*
import java.lang.ref.Cleaner
import java.text.DateFormat
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ExecutorService
import java.util.concurrent.ForkJoinPool
import java.util.concurrent.Future
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.ThreadFactory
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.LockSupport
import java.util.function.BiConsumer
import java.util.function.BinaryOperator
import java.util.function.Function
import java.util.function.Supplier
import java.util.stream.Collector
import kotlin.NoSuchElementException
import kotlin.collections.ArrayList
object Starbound : ISBFileLocator {
const val ENGINE_VERSION = "0.0.1"
const val NATIVE_PROTOCOL_VERSION = 748
const val LEGACY_PROTOCOL_VERSION = 747
const val TIMESTEP = 1.0 / 60.0
const val TIMESTEP_NANOS = (TIMESTEP * 1_000_000_000L).toLong()
// compile flags. uuuugh
const val DEDUP_CELL_STATES = true
const val USE_CAFFEINE_INTERNER = false
fun <E : Any> interner(): Interner<E> {
return if (USE_CAFFEINE_INTERNER) Interner.newWeakInterner() else HashTableInterner()
}
fun <E : Any> interner(bits: Int): Interner<E> {
return if (USE_CAFFEINE_INTERNER) Interner.newWeakInterner() else HashTableInterner(bits)
}
private val LOGGER = LogManager.getLogger()
val thread = Thread(::universeThread, "Universe Thread")
val mailbox = MailboxExecutorService(thread).also { it.exceptionHandler = ExceptionLogger(LOGGER) }
val mailboxBootstrapped = MailboxExecutorService(thread).also { it.exceptionHandler = ExceptionLogger(LOGGER) }
val mailboxInitialized = MailboxExecutorService(thread).also { it.exceptionHandler = ExceptionLogger(LOGGER) }
init {
thread.isDaemon = true
thread.start()
}
private val ioPoolCounter = AtomicInteger()
@JvmField
val IO_EXECUTOR: ExecutorService = ThreadPoolExecutor(0, 64, 30L, TimeUnit.SECONDS, LinkedBlockingQueue(), ThreadFactory {
val thread = Thread(it, "Starbound Storage IO ${ioPoolCounter.getAndIncrement()}")
thread.isDaemon = true
thread.priority = Thread.MIN_PRIORITY
thread.uncaughtExceptionHandler = Thread.UncaughtExceptionHandler { t, e ->
LOGGER.error("I/O thread died due to uncaught exception", e)
}
return@ThreadFactory thread
})
@JvmField
val EXECUTOR: ForkJoinPool = ForkJoinPool.commonPool()
@JvmField
val COROUTINE_EXECUTOR = EXECUTOR.asCoroutineDispatcher()
@JvmField
val COROUTINES = CoroutineScope(COROUTINE_EXECUTOR)
@JvmField
val CLEANER: Cleaner = Cleaner.create {
val t = Thread(it, "Starbound Global Cleaner")
t.isDaemon = true
t.priority = 2
t
}
// currently Caffeine one saves only 4 megabytes of RAM on pretty big modpack
// Hrm.
// val strings: Interner<String> = Interner.newWeakInterner()
// val strings: Interner<String> = Interner { it }
@JvmField
val STRINGS: Interner<String> = interner(5)
// immeasurably lazy and fragile solution, too bad!
// While having four separate Gson instances look like a (much) better solution (and it indeed could have been!),
// we must not forget the fact that 'Starbound' and 'Consistent data format' are opposites,
// and there are cases of where discStore() calls toJson() on children data, despite it having its own discStore() too.
var IS_LEGACY_JSON: Boolean by ThreadLocal.withInitial { false }
private set
var IS_STORE_JSON: Boolean by ThreadLocal.withInitial { false }
private set
fun legacyJson(data: Any): JsonElement {
try {
IS_LEGACY_JSON = true
return gson.toJsonTree(data)
} finally {
IS_LEGACY_JSON = false
}
}
fun storeJson(data: Any): JsonElement {
try {
IS_STORE_JSON = true
return gson.toJsonTree(data)
} finally {
IS_STORE_JSON = false
}
}
fun legacyStoreJson(data: Any): JsonElement {
try {
IS_STORE_JSON = true
IS_LEGACY_JSON = true
return gson.toJsonTree(data)
} finally {
IS_STORE_JSON = false
IS_LEGACY_JSON = false
}
}
fun <T> legacyJson(block: () -> T): T {
try {
IS_LEGACY_JSON = true
return block.invoke()
} finally {
IS_LEGACY_JSON = false
}
}
fun <T> storeJson(block: () -> T): T {
try {
IS_STORE_JSON = true
return block.invoke()
} finally {
IS_STORE_JSON = false
}
}
fun <T> legacyStoreJson(block: () -> T): T {
try {
IS_STORE_JSON = true
IS_LEGACY_JSON = true
return block.invoke()
} finally {
IS_STORE_JSON = false
IS_LEGACY_JSON = false
}
}
val gson: Gson = with(GsonBuilder()) {
// serializeNulls()
setDateFormat(DateFormat.LONG)
setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)
setPrettyPrinting()
registerTypeAdapter(InternedStringAdapter(STRINGS))
InternedJsonElementAdapter(STRINGS).also {
registerTypeAdapter(it)
registerTypeAdapter(it.arrays)
registerTypeAdapter(it.objects)
}
registerTypeAdapter(Nothing::class.java, NothingAdapter)
// Обработчик @JsonImplementation
registerTypeAdapterFactory(JsonImplementationTypeFactory)
registerTypeAdapterFactory(JsonAdapterTypeFactory)
// списки, наборы, т.п.
registerTypeAdapterFactory(CollectionAdapterFactory)
// ImmutableList, ImmutableSet, ImmutableMap
registerTypeAdapterFactory(ImmutableCollectionAdapterFactory(STRINGS))
// fastutil collections
registerTypeAdapterFactory(MapsTypeAdapterFactory(STRINGS))
// все enum'ы без особых настроек
registerTypeAdapterFactory(EnumAdapter.Companion)
// @JsonBuilder
registerTypeAdapterFactory(BuilderAdapter.Factory(STRINGS))
// @JsonFactory
registerTypeAdapterFactory(FactoryAdapter.Factory(STRINGS))
// Either<>
registerTypeAdapterFactory(EitherTypeAdapter)
// KOptional<>
registerTypeAdapterFactory(KOptionalTypeAdapter)
registerTypeAdapterFactory(SingletonTypeAdapterFactory)
// Pair<>
registerTypeAdapterFactory(PairAdapterFactory)
registerTypeAdapterFactory(SBPattern.Companion)
registerTypeAdapterFactory(JsonReference.Companion)
registerTypeAdapter(ColorReplacements.Companion)
registerTypeAdapterFactory(BlueprintLearnList.Companion)
registerTypeAdapter(RGBAColorTypeAdapter)
registerTypeAdapterFactory(NativeLegacy.Companion)
// математические классы
registerTypeAdapter(AABBTypeAdapter.nullSafe())
registerTypeAdapter(AABBiTypeAdapter.nullSafe())
registerTypeAdapter(Vector2dTypeAdapter.nullSafe())
registerTypeAdapter(Vector2fTypeAdapter.nullSafe())
registerTypeAdapter(Vector2iTypeAdapter.nullSafe())
registerTypeAdapter(Vector3dTypeAdapter.nullSafe())
registerTypeAdapter(Vector3fTypeAdapter.nullSafe())
registerTypeAdapter(Vector3iTypeAdapter.nullSafe())
registerTypeAdapter(Vector4iTypeAdapter.nullSafe())
registerTypeAdapter(Vector4dTypeAdapter.nullSafe())
registerTypeAdapter(Vector4fTypeAdapter.nullSafe())
registerTypeAdapterFactory(AbstractPerlinNoise.Companion)
registerTypeAdapterFactory(WeightedList.Companion)
// Функции
registerTypeAdapter(JsonFunction.CONSTRAINT_ADAPTER)
registerTypeAdapter(JsonFunction.INTERPOLATION_ADAPTER)
registerTypeAdapter(JsonFunction.Companion)
registerTypeAdapter(JsonConfigFunction::Adapter)
registerTypeAdapterFactory(Json2Function.Companion)
// Общее
registerTypeAdapterFactory(ThingDescription.Factory(STRINGS))
registerTypeAdapterFactory(TerrainSelectorType.Companion)
registerTypeAdapter(Directives.Companion)
registerTypeAdapter(EnumAdapter(DamageType::class, default = DamageType.DAMAGE))
registerTypeAdapter(InventoryIcon.Companion)
registerTypeAdapterFactory(IArmorItemDefinition.Frames.Factory)
registerTypeAdapterFactory(AssetPath.Companion)
registerTypeAdapter(SpriteReference.Companion)
registerTypeAdapterFactory(AssetReference.Companion)
registerTypeAdapter(ItemStack.Adapter(this@Starbound))
registerTypeAdapterFactory(TreasurePoolDefinition.Companion)
registerTypeAdapterFactory(UniverseChunk.Companion)
registerTypeAdapter(Image.Companion)
registerTypeAdapterFactory(Poly.Companion)
registerTypeAdapter(CelestialParameters::Adapter)
registerTypeAdapter(Particle::Adapter)
registerTypeAdapter(QuestParameter::Adapter)
registerTypeAdapterFactory(BiomePlacementDistributionType.DEFINITION_ADAPTER)
registerTypeAdapterFactory(BiomePlacementItemType.DATA_ADAPTER)
registerTypeAdapterFactory(BiomePlacementItemType.DEFINITION_ADAPTER)
registerTypeAdapterFactory(BiomePlaceables.Item.Companion)
// register companion first, so it has lesser priority than dispatching adapter
registerTypeAdapterFactory(VisitableWorldParametersType.Companion)
registerTypeAdapterFactory(VisitableWorldParametersType.ADAPTER)
registerTypeAdapter(WorldLayout.Companion)
Registries.registerAdapters(this)
registerTypeAdapter(LongRangeAdapter)
create()
}
data class ItemConfig(
val directory: String?,
val parameters: JsonObject,
val config: JsonObject
)
fun itemConfig(name: String, parameters: JsonObject, level: Double? = null, seed: Long? = null): ItemConfig {
val obj = Registries.items[name] ?: throw NoSuchElementException("No such item $name")
val directory = obj.file?.computeDirectory()
val parameters = parameters.deepCopy()
TODO()
}
fun item(name: String): ItemStack {
TODO()
}
fun item(name: String, count: Long): ItemStack {
TODO()
}
fun item(name: String, count: Long, parameters: JsonObject): ItemStack {
TODO()
}
fun item(descriptor: JsonObject): ItemStack {
return item(
(descriptor["name"] as? JsonPrimitive)?.asString ?: return ItemStack.EMPTY,
descriptor["count"]?.asLong ?: return ItemStack.EMPTY,
(descriptor["parameters"] as? JsonObject)?.deepCopy() ?: JsonObject()
)
}
fun item(descriptor: JsonElement?): ItemStack {
if (descriptor is JsonPrimitive) {
return item(descriptor.asString)
} else if (descriptor is JsonObject) {
return item(descriptor)
} else {
return ItemStack.EMPTY
}
}
var initializing = false
private set
var initialized = false
private set
var bootstrapping = false
private set
var bootstrapped = false
private set
var loadingProgress = 0.0
private set
var toLoad = 0
private set
var loaded = 0
private set
@Volatile
var terminateLoading = false
fun loadJsonAsset(path: String): JsonElement? {
val filename: String
val jsonPath: String?
if (path.contains(':')) {
filename = path.substringBefore(':')
jsonPath = path.substringAfter(':')
} else {
filename = path
jsonPath = null
}
val file = locate(filename)
if (!file.isFile)
return null
val pathTraverser = if (jsonPath == null) JsonPath.EMPTY else JsonPath.query(jsonPath)
return pathTraverser.get(gson.fromJson(file.reader(), JsonElement::class.java))
}
private val archivePaths = ArrayList<File>()
private val fileSystems = ArrayList<IStarboundFile>()
fun addFilePath(path: File) {
fileSystems.add(PhysicalFile(path))
}
fun addPak(pak: StarboundPak) {
fileSystems.add(pak.root)
}
override fun exists(path: String): Boolean {
@Suppress("name_shadowing")
var path = path
if (path[0] == '/') {
path = path.substring(1)
}
for (fs in fileSystems) {
if (fs.locate(path).exists) {
return true
}
}
return false
}
override fun locate(path: String): IStarboundFile {
@Suppress("name_shadowing")
var path = path
if (path[0] == '/') {
path = path.substring(1)
}
for (fs in fileSystems) {
val file = fs.locate(path)
if (file.exists) {
return file
}
}
return NonExistingFile(path.split("/").last(), fullPath = path)
}
fun locate(vararg path: String): IStarboundFile {
for (p in path) {
val get = locate(p)
if (get.exists) {
return get
}
}
return NonExistingFile(path[0].split("/").last(), fullPath = path[0])
}
/**
* Добавляет pak к чтению при initializeGame
*/
fun addPakPath(pak: File) {
archivePaths.add(pak)
}
fun doBootstrap() {
if (!bootstrapped && !bootstrapping) {
bootstrapping = true
} else {
return
}
for (path in archivePaths) {
LOGGER.info("Reading PAK archive $path")
addPak(StarboundPak(path))
}
LOGGER.info("Finished reading PAK archives")
bootstrapped = true
bootstrapping = false
checkMailbox()
}
private fun doInitialize() {
if (!initializing && !initialized) {
initializing = true
} else {
return
}
doBootstrap()
val ext2files = fileSystems.parallelStream()
.flatMap { it.explore() }
.filter { it.isFile }
.collect(object :
Collector<IStarboundFile, Object2ObjectOpenHashMap<String, ArrayList<IStarboundFile>>, Map<String, List<IStarboundFile>>>
{
override fun supplier(): Supplier<Object2ObjectOpenHashMap<String, ArrayList<IStarboundFile>>> {
return Supplier { Object2ObjectOpenHashMap() }
}
override fun accumulator(): BiConsumer<Object2ObjectOpenHashMap<String, ArrayList<IStarboundFile>>, IStarboundFile> {
return BiConsumer { t, u ->
t.computeIfAbsent(u.name.substringAfterLast('.'), Object2ObjectFunction { ArrayList() }).add(u)
}
}
override fun combiner(): BinaryOperator<Object2ObjectOpenHashMap<String, ArrayList<IStarboundFile>>> {
return BinaryOperator { t, u ->
for ((k, v) in u)
t.computeIfAbsent(k, Object2ObjectFunction { ArrayList() }).addAll(v)
t
}
}
override fun finisher(): Function<Object2ObjectOpenHashMap<String, ArrayList<IStarboundFile>>, Map<String, List<IStarboundFile>>> {
return Function { it }
}
override fun characteristics(): Set<Collector.Characteristics> {
return setOf(Collector.Characteristics.IDENTITY_FINISH)
}
})
checkMailbox()
val tasks = ArrayList<Future<*>>()
tasks.addAll(Registries.load(ext2files))
tasks.addAll(RecipeRegistry.load(ext2files))
tasks.addAll(Globals.load())
tasks.add(VersionRegistry.load())
val total = tasks.size.toDouble()
toLoad = tasks.size
while (tasks.isNotEmpty()) {
tasks.removeIf { it.isDone }
checkMailbox()
loaded = toLoad - tasks.size
loadingProgress = (total - tasks.size) / total
LockSupport.parkNanos(5_000_000L)
}
Registries.finishLoad()
RecipeRegistry.finishLoad()
Registries.validate()
initializing = false
initialized = true
}
fun initializeGame(): Future<*> {
return mailbox.submit { doInitialize() }
}
fun bootstrapGame(): Future<*> {
return mailbox.submit { doBootstrap() }
}
private fun checkMailbox() {
mailbox.executeQueuedTasks()
if (bootstrapped)
mailboxBootstrapped.executeQueuedTasks()
if (initialized)
mailboxInitialized.executeQueuedTasks()
}
private var fontPath: File? = null
fun loadFont(): CompletableFuture<File> {
val fontPath = fontPath
if (fontPath != null)
return CompletableFuture.completedFuture(fontPath)
return CompletableFuture.supplyAsync(Supplier {
val fontPath = Starbound.fontPath
if (fontPath != null)
return@Supplier fontPath
val file = locate("/hobo.ttf")
if (!file.exists)
throw FileNotFoundException("Unable to locate font file /hobo.ttf")
else if (!file.isFile)
throw FileNotFoundException("/hobo.ttf is not a file!")
else {
val tempPath = File(System.getProperty("java.io.tmpdir"), "sb-hobo.ttf")
tempPath.writeBytes(file.read().array())
Starbound.fontPath = tempPath
return@Supplier tempPath
}
}, mailboxBootstrapped)
}
private fun universeThread() {
while (true) {
checkMailbox()
LockSupport.park()
}
}
}