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 interner(): Interner { return if (USE_CAFFEINE_INTERNER) Interner.newWeakInterner() else HashTableInterner() } fun interner(bits: Int): Interner { 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 = Interner.newWeakInterner() // val strings: Interner = Interner { it } @JvmField val STRINGS: Interner = 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 legacyJson(block: () -> T): T { try { IS_LEGACY_JSON = true return block.invoke() } finally { IS_LEGACY_JSON = false } } fun storeJson(block: () -> T): T { try { IS_STORE_JSON = true return block.invoke() } finally { IS_STORE_JSON = false } } fun 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() private val fileSystems = ArrayList() 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>, Map>> { override fun supplier(): Supplier>> { return Supplier { Object2ObjectOpenHashMap() } } override fun accumulator(): BiConsumer>, IStarboundFile> { return BiConsumer { t, u -> t.computeIfAbsent(u.name.substringAfterLast('.'), Object2ObjectFunction { ArrayList() }).add(u) } } override fun combiner(): BinaryOperator>> { return BinaryOperator { t, u -> for ((k, v) in u) t.computeIfAbsent(k, Object2ObjectFunction { ArrayList() }).addAll(v) t } } override fun finisher(): Function>, Map>> { return Function { it } } override fun characteristics(): Set { return setOf(Collector.Characteristics.IDENTITY_FINISH) } }) checkMailbox() val tasks = ArrayList>() 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 { 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() } } }