package ru.dbotthepony.kstarbound import com.google.common.collect.Interner import com.google.common.collect.Interners import com.google.gson.* import com.google.gson.internal.bind.TypeAdapters import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonWriter import it.unimi.dsi.fastutil.objects.Object2ObjectFunction import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import org.apache.logging.log4j.LogManager import ru.dbotthepony.kstarbound.api.ISBFileLocator import ru.dbotthepony.kstarbound.api.IStarboundFile import ru.dbotthepony.kstarbound.api.NonExistingFile import ru.dbotthepony.kstarbound.api.PhysicalFile import ru.dbotthepony.kstarbound.api.explore import ru.dbotthepony.kstarbound.defs.* import ru.dbotthepony.kstarbound.defs.image.AtlasConfiguration import ru.dbotthepony.kstarbound.defs.image.ImageReference import ru.dbotthepony.kstarbound.defs.image.SpriteReference import ru.dbotthepony.kstarbound.defs.item.BackArmorItemPrototype import ru.dbotthepony.kstarbound.defs.item.ChestArmorItemPrototype import ru.dbotthepony.kstarbound.defs.item.CurrencyItemPrototype import ru.dbotthepony.kstarbound.defs.item.FlashlightPrototype import ru.dbotthepony.kstarbound.defs.item.HarvestingToolPrototype import ru.dbotthepony.kstarbound.defs.item.HeadArmorItemPrototype import ru.dbotthepony.kstarbound.defs.item.IArmorItemDefinition import ru.dbotthepony.kstarbound.defs.item.IItemDefinition import ru.dbotthepony.kstarbound.defs.item.ItemPrototype import ru.dbotthepony.kstarbound.defs.item.LegsArmorItemPrototype import ru.dbotthepony.kstarbound.defs.item.LeveledStatusEffect import ru.dbotthepony.kstarbound.defs.item.LiquidItemPrototype import ru.dbotthepony.kstarbound.defs.item.MaterialItemPrototype import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition import ru.dbotthepony.kstarbound.defs.particle.ParticleDefinition import ru.dbotthepony.kstarbound.defs.player.BlueprintLearnList import ru.dbotthepony.kstarbound.defs.player.PlayerDefinition import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier import ru.dbotthepony.kstarbound.defs.tile.TileDefinition import ru.dbotthepony.kstarbound.io.* import ru.dbotthepony.kstarbound.io.json.AABBTypeAdapter import ru.dbotthepony.kstarbound.io.json.AABBiTypeAdapter import ru.dbotthepony.kstarbound.io.json.EitherTypeAdapter import ru.dbotthepony.kstarbound.io.json.Vector2dTypeAdapter import ru.dbotthepony.kstarbound.io.json.Vector2fTypeAdapter import ru.dbotthepony.kstarbound.io.json.Vector2iTypeAdapter import ru.dbotthepony.kstarbound.io.json.Vector4dTypeAdapter import ru.dbotthepony.kstarbound.io.json.Vector4iTypeAdapter import ru.dbotthepony.kstarbound.io.json.builder.EnumAdapter import ru.dbotthepony.kstarbound.io.json.builder.BuilderAdapter import ru.dbotthepony.kstarbound.io.json.builder.FactoryAdapter import ru.dbotthepony.kstarbound.io.json.builder.JsonImplementationTypeFactory import ru.dbotthepony.kstarbound.io.json.factory.ArrayListAdapterFactory import ru.dbotthepony.kstarbound.io.json.factory.ImmutableCollectionAdapterFactory import ru.dbotthepony.kstarbound.math.* import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.util.SBPattern import ru.dbotthepony.kstarbound.util.WriteOnce import java.io.* import java.text.DateFormat import java.util.* 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.collections.ArrayList class Starbound : ISBFileLocator { private val logger = LogManager.getLogger() val stringInterner: Interner = Interners.newWeakInterner() val pathStack = AssetPathStack(stringInterner) private val _tiles = ObjectRegistry("tiles", TileDefinition::materialName, TileDefinition::materialId) val tiles = _tiles.view val tilesByID = _tiles.intView private val _tileModifiers = ObjectRegistry("tile modifiers", MaterialModifier::modName, MaterialModifier::modId) val tileModifiers = _tileModifiers.view val tileModifiersByID = _tileModifiers.intView private val _liquid = ObjectRegistry("liquid", LiquidDefinition::name, LiquidDefinition::liquidId) val liquid = _liquid.view val liquidByID = _liquid.intView private val _species = ObjectRegistry("species", Species::kind) val species = _species.view private val _statusEffects = ObjectRegistry("status effects", StatusEffectDefinition::name) val statusEffects = _statusEffects.view private val _particles = ObjectRegistry("particles", ParticleDefinition::kind) val particles = _particles.view private val _items = ObjectRegistry("items", IItemDefinition::itemName) val items = _items.view val spriteRegistry: SpriteReference.Adapter val gson: Gson = with(GsonBuilder()) { serializeNulls() setDateFormat(DateFormat.LONG) setFieldNamingPolicy(FieldNamingPolicy.IDENTITY) setPrettyPrinting() // чтоб строки всегда intern'ились registerTypeAdapter(object : TypeAdapter() { override fun write(out: JsonWriter, value: String?) { if (value == null) out.nullValue() else out.value(value) } override fun read(`in`: JsonReader): String? { return stringInterner.intern(TypeAdapters.STRING.read(`in`) ?: return null) } }) // Обработчик @JsonImplementation registerTypeAdapterFactory(JsonImplementationTypeFactory) // ImmutableList, ImmutableSet, ImmutableMap registerTypeAdapterFactory(ImmutableCollectionAdapterFactory) // ArrayList registerTypeAdapterFactory(ArrayListAdapterFactory) // все enum'ы без особых настроек registerTypeAdapterFactory(EnumAdapter.Companion) // автоматическое создание BuilderAdapter по @аннотациям registerTypeAdapterFactory(BuilderAdapter.Factory(stringInterner)) // автоматическое создание FactoryAdapter по @аннотациям registerTypeAdapterFactory(FactoryAdapter.Factory(stringInterner)) registerTypeAdapterFactory(EitherTypeAdapter) registerTypeAdapterFactory(SBPattern.Companion) registerTypeAdapter(ColorReplacements.Companion) registerTypeAdapterFactory(BlueprintLearnList.Companion) registerTypeAdapter(ColorTypeAdapter.nullSafe()) // математические классы registerTypeAdapter(AABBTypeAdapter) registerTypeAdapter(AABBiTypeAdapter) registerTypeAdapter(Vector2dTypeAdapter) registerTypeAdapter(Vector2fTypeAdapter) registerTypeAdapter(Vector2iTypeAdapter) registerTypeAdapter(Vector4iTypeAdapter) registerTypeAdapter(Vector4dTypeAdapter) registerTypeAdapter(PolyTypeAdapter) // Функции registerTypeAdapter(JsonFunction.CONSTRAINT_ADAPTER) registerTypeAdapter(JsonFunction.INTERPOLATION_ADAPTER) registerTypeAdapter(JsonFunction.Companion) // Общее registerTypeAdapterFactory(LeveledStatusEffect.ADAPTER) registerTypeAdapter(MaterialReference.Companion) registerTypeAdapterFactory(ThingDescription.Factory(stringInterner)) registerTypeAdapter(EnumAdapter(DamageType::class, default = DamageType.NORMAL)) spriteRegistry = SpriteReference.Adapter(pathStack, this@Starbound::atlasRegistry) registerTypeAdapter(spriteRegistry) registerTypeAdapterFactory(IItemDefinition.InventoryIcon.Factory(pathStack, spriteRegistry)) registerTypeAdapterFactory(IArmorItemDefinition.ArmorFrames.Factory(pathStack, this@Starbound::atlasRegistry)) registerTypeAdapterFactory(DirectAssetReferenceFactory(pathStack)) registerTypeAdapter(ImageReference.Adapter(pathStack, this@Starbound::atlasRegistry)) registerTypeAdapterFactory(AssetReferenceFactory(pathStack, this@Starbound)) registerTypeAdapterFactory(with(RegistryReferenceFactory()) { add(tiles::get) add(tileModifiers::get) add(liquid::get) add(items::get) add(species::get) add(statusEffects::get) add(particles::get) }) .create() } val atlasRegistry = AtlasConfiguration.Registry(this, pathStack, gson) var initializing = false private set var initialized = false private set @Volatile var terminateLoading = false 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 getTileDefinition(name: String) = tiles[name] private val initCallbacks = ArrayList<() -> Unit>() var playerDefinition: PlayerDefinition by WriteOnce() private set private fun loadStage( callback: (Boolean, Boolean, String) -> Unit, loader: ((String) -> Unit) -> Unit, name: String, ) { if (terminateLoading) return val time = System.currentTimeMillis() callback(false, false, "Loading $name...") logger.info("Loading $name...") loader { if (terminateLoading) { throw InterruptedException("Game is terminating") } callback(false, true, it) } callback(false, true, "Loaded $name in ${System.currentTimeMillis() - time}ms") logger.info("Loaded $name in ${System.currentTimeMillis() - time}ms") } private fun loadStage( callback: (Boolean, Boolean, String) -> Unit, clazz: Class, registry: ObjectRegistry, files: List, ) { loadStage(callback, loader = { for (listedFile in files) { try { it("Loading $listedFile") val def = pathStack(listedFile.computeDirectory()) { gson.fromJson(listedFile.reader(), clazz) } registry.add(def, listedFile) } catch (err: Throwable) { logger.error("Loading ${registry.name} definition file $listedFile", err) } if (terminateLoading) { break } } }, registry.name) } private fun doInitialize(callback: (finished: Boolean, replaceStatus: Boolean, status: String) -> Unit) { var time = System.currentTimeMillis() if (archivePaths.isNotEmpty()) { callback(false, false, "Searching for pak archives...".also(logger::info)) for (path in archivePaths) { callback(false, false, "Reading index of ${path}...".also(logger::info)) addPak(StarboundPak(path) { _, status -> callback(false, true, "${path.parent}/${path.name}: $status") }) } } callback(false, false, "Finished reading pak archives in ${System.currentTimeMillis() - time}ms".also(logger::info)) time = System.currentTimeMillis() callback(false, false, "Building file index...".also(logger::info)) val ext2files = fileSystems.parallelStream() .flatMap { it.explore() } .filter { it.isFile } .collect(object : Collector>, Map>>, Supplier>>, BiConsumer>, IStarboundFile>, BinaryOperator>> { override fun accept(t: Object2ObjectOpenHashMap>, u: IStarboundFile) { t.computeIfAbsent(u.name.substringAfterLast('.'), Object2ObjectFunction { ArrayList() }).add(u) } override fun get(): Object2ObjectOpenHashMap> { return Object2ObjectOpenHashMap() } override fun supplier(): Supplier>> { return this } override fun accumulator(): BiConsumer>, IStarboundFile> { return this } override fun apply( t: Object2ObjectOpenHashMap>, u: Object2ObjectOpenHashMap> ): Object2ObjectOpenHashMap> { for ((k, v) in u) t.computeIfAbsent(k, Object2ObjectFunction { ArrayList() }).addAll(v) return t } override fun combiner(): BinaryOperator>> { return this } override fun finisher(): Function>, Map>> { return Function { it } } override fun characteristics(): Set { return setOf(Collector.Characteristics.IDENTITY_FINISH, Collector.Characteristics.UNORDERED) } }) callback(false, false, "Finished building file index in ${System.currentTimeMillis() - time}ms".also(logger::info)) loadStage(callback, this::loadItemDefinitions, "item definitions") loadStage(callback, TileDefinition::class.java, _tiles, ext2files["material"] ?: listOf()) loadStage(callback, MaterialModifier::class.java, _tileModifiers, ext2files["matmod"] ?: listOf()) loadStage(callback, LiquidDefinition::class.java, _liquid, ext2files["liquid"] ?: listOf()) loadStage(callback, StatusEffectDefinition::class.java, _statusEffects, ext2files["statuseffect"] ?: listOf()) loadStage(callback, Species::class.java, _species, ext2files["species"] ?: listOf()) loadStage(callback, ParticleDefinition::class.java, _particles, ext2files["particle"] ?: listOf()) pathStack.block("/") { playerDefinition = gson.fromJson(locate("/player.config").reader(), PlayerDefinition::class.java) } initializing = false initialized = true callback(true, false, "Finished loading in ${System.currentTimeMillis() - time}ms") } fun initializeGame(callback: (finished: Boolean, replaceStatus: Boolean, status: String) -> Unit) { if (initializing) { throw IllegalStateException("Already initializing!") } if (initialized) { throw IllegalStateException("Already initialized!") } initializing = true Thread({ doInitialize(callback) }, "Asset Loader").start() } fun onInitialize(callback: () -> Unit) { if (initialized) { callback() } else { initCallbacks.add(callback) } } fun pollCallbacks() { if (initialized && initCallbacks.isNotEmpty()) { for (callback in initCallbacks) { callback() } initCallbacks.clear() } } private fun loadItemDefinitions(callback: (String) -> Unit) { val files = linkedMapOf( ".item" to ItemPrototype::class.java, ".currency" to CurrencyItemPrototype::class.java, ".liqitem" to LiquidItemPrototype::class.java, ".matitem" to MaterialItemPrototype::class.java, ".flashlight" to FlashlightPrototype::class.java, ".harvestingtool" to HarvestingToolPrototype::class.java, ".head" to HeadArmorItemPrototype::class.java, ".chest" to ChestArmorItemPrototype::class.java, ".legs" to LegsArmorItemPrototype::class.java, ".back" to BackArmorItemPrototype::class.java, ) for (fs in fileSystems) { for (listedFile in fs.explore().filter { it.isFile }.filter { f -> files.keys.any { f.name.endsWith(it) } }) { try { callback("Loading $listedFile") val def: ItemPrototype = pathStack(listedFile.computeDirectory()) { gson.fromJson(listedFile.reader(), files.entries.first { listedFile.name.endsWith(it.key) }.value) } _items.add(def, listedFile) } catch (err: Throwable) { logger.error("Loading item definition file $listedFile", err) } if (terminateLoading) { return } } } } }