package ru.dbotthepony.kstarbound import com.github.benmanes.caffeine.cache.Interner import com.google.gson.* import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap 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.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.`object`.ObjectDefinition import ru.dbotthepony.kstarbound.defs.`object`.ObjectOrientation import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition import ru.dbotthepony.kstarbound.defs.player.BlueprintLearnList import ru.dbotthepony.kstarbound.defs.player.PlayerDefinition import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials import ru.dbotthepony.kstarbound.util.JsonArrayCollector 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.ColorTypeAdapter import ru.dbotthepony.kstarbound.io.json.EitherTypeAdapter import ru.dbotthepony.kstarbound.io.json.FastutilTypeAdapterFactory import ru.dbotthepony.kstarbound.io.json.InternedJsonElementAdapter import ru.dbotthepony.kstarbound.io.json.InternedStringAdapter import ru.dbotthepony.kstarbound.io.json.KOptionalTypeAdapter import ru.dbotthepony.kstarbound.io.json.LongRangeAdapter import ru.dbotthepony.kstarbound.io.json.NothingAdapter import ru.dbotthepony.kstarbound.io.json.OneOfTypeAdapter 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.io.json.factory.PairAdapterFactory import ru.dbotthepony.kstarbound.lua.LuaState import ru.dbotthepony.kstarbound.lua.loadInternalScript import ru.dbotthepony.kstarbound.math.* import ru.dbotthepony.kstarbound.util.ITimeSource import ru.dbotthepony.kstarbound.util.ItemStack import ru.dbotthepony.kstarbound.util.JVMTimeSource import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.util.SBPattern import ru.dbotthepony.kstarbound.util.HashTableInterner import ru.dbotthepony.kstarbound.util.WriteOnce import ru.dbotthepony.kstarbound.util.filterNotNull import ru.dbotthepony.kstarbound.util.set import ru.dbotthepony.kstarbound.util.traverseJsonPath import java.io.* import java.lang.ref.Cleaner import java.text.DateFormat import java.util.concurrent.ForkJoinPool import java.util.concurrent.ForkJoinTask 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 import kotlin.random.Random object Starbound : ISBFileLocator { const val TICK_TIME_ADVANCE = 0.01666666666666664 const val TICK_TIME_ADVANCE_NANOS = 16_666_666L val CLEANER: Cleaner = Cleaner.create { val t = Thread(it, "Starbound Global Cleaner Thread") 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 } val STRINGS: Interner = HashTableInterner(5) private val polyfill by lazy { loadInternalScript("polyfill") } private val LOGGER = LogManager.getLogger() 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) // ImmutableList, ImmutableSet, ImmutableMap registerTypeAdapterFactory(ImmutableCollectionAdapterFactory(STRINGS)) // fastutil collections registerTypeAdapterFactory(FastutilTypeAdapterFactory(STRINGS)) // ArrayList registerTypeAdapterFactory(ArrayListAdapterFactory) // все enum'ы без особых настроек registerTypeAdapterFactory(EnumAdapter.Companion) // @JsonBuilder registerTypeAdapterFactory(BuilderAdapter.Factory(STRINGS)) // @JsonFactory registerTypeAdapterFactory(FactoryAdapter.Factory(STRINGS)) // Either<> registerTypeAdapterFactory(EitherTypeAdapter) // OneOf<> registerTypeAdapterFactory(OneOfTypeAdapter) // KOptional<> registerTypeAdapterFactory(KOptionalTypeAdapter) // Pair<> registerTypeAdapterFactory(PairAdapterFactory) registerTypeAdapterFactory(SBPattern.Companion) registerTypeAdapterFactory(JsonReference.Companion) registerTypeAdapter(ColorReplacements.Companion) registerTypeAdapterFactory(BlueprintLearnList.Companion) registerTypeAdapter(ColorTypeAdapter.nullSafe()) registerTypeAdapter(Drawable::Adapter) registerTypeAdapter(ObjectOrientation::Adapter) registerTypeAdapter(ObjectDefinition::Adapter) registerTypeAdapter(StatModifier::Adapter) // математические классы registerTypeAdapter(AABBTypeAdapter) registerTypeAdapter(AABBiTypeAdapter) registerTypeAdapter(Vector2dTypeAdapter) registerTypeAdapter(Vector2fTypeAdapter) registerTypeAdapter(Vector2iTypeAdapter) registerTypeAdapter(Vector4iTypeAdapter) registerTypeAdapter(Vector4dTypeAdapter) registerTypeAdapter(PolyTypeAdapter) registerTypeAdapter(LineF::Adapter) // Функции registerTypeAdapter(JsonFunction.CONSTRAINT_ADAPTER) registerTypeAdapter(JsonFunction.INTERPOLATION_ADAPTER) registerTypeAdapter(JsonFunction.Companion) registerTypeAdapterFactory(Json2Function.Companion) // Общее registerTypeAdapterFactory(ThingDescription.Factory(STRINGS)) registerTypeAdapter(EnumAdapter(DamageType::class, default = DamageType.NORMAL)) registerTypeAdapter(InventoryIcon.Companion) registerTypeAdapterFactory(IArmorItemDefinition.Frames.Factory) registerTypeAdapterFactory(AssetPath.Companion) registerTypeAdapter(SpriteReference.Companion) registerTypeAdapterFactory(AssetReference.Companion) registerTypeAdapter(ItemStack.Adapter(this@Starbound)) registerTypeAdapterFactory(ItemReference.Factory(STRINGS)) registerTypeAdapterFactory(TreasurePoolDefinition.Companion) registerTypeAdapter(Image.Companion) registerTypeAdapterFactory(with(RegistryReferenceFactory()) { add(Registries.tiles) add(Registries.tileModifiers) add(Registries.liquid) add(Registries.items) add(Registries.species) add(Registries.statusEffects) add(Registries.particles) add(Registries.questTemplates) add(Registries.techs) add(Registries.jsonFunctions) add(Registries.json2Functions) add(Registries.npcTypes) add(Registries.projectiles) add(Registries.tenants) add(Registries.treasurePools) add(Registries.monsterSkills) add(Registries.monsterTypes) add(Registries.worldObjects) }) registerTypeAdapter(LongRangeAdapter) create() } init { val f = NonExistingFile("/metamaterials.config") for (material in BuiltinMetaMaterials.MATERIALS) { Registries.tiles.add(material, JsonNull.INSTANCE, f) } } fun item(name: String): ItemStack { return ItemStack(Registries.items[name] ?: return ItemStack.EMPTY) } fun item(name: String, count: Long): ItemStack { if (count <= 0L) return ItemStack.EMPTY return ItemStack(Registries.items[name] ?: return ItemStack.EMPTY, count = count) } fun item(name: String, count: Long, parameters: JsonObject): ItemStack { if (count <= 0L) return ItemStack.EMPTY return ItemStack(Registries.items[name] ?: return ItemStack.EMPTY, count = count, parameters = parameters) } 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 @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 } return traverseJsonPath(jsonPath, gson.fromJson(file.reader(), JsonElement::class.java)) } private fun luaRequire(it: LuaState, args: LuaState.ArgStack) { val name = args.getString() val file = locate(name) if (!file.exists) { throw FileNotFoundException("File $name does not exist") } if (!file.isFile) { throw FileNotFoundException("File $name is a directory") } val read = file.readToString() it.load(read, chunkName = "@" + file.computeFullPath()) it.call() } // wrapping for Lua refs private fun getImage(path: String) = Image.get(path) /** * **ПРЕДУПРЕЖДЕНИЕ:** * [time] не должен иметь ссылок (прямые или непрямые) на [state], иначе произойдёт утечка памяти! */ fun pushLuaAPI(state: LuaState, time: ITimeSource = JVMTimeSource.INSTANCE) { state.pushWeak(this) { args -> luaRequire(args.lua, args) 0 } state.storeGlobal("require") state.pushTable() state.storeGlobal("root") state.loadGlobal("root") state.setTableFunction("assetJson", this) {args -> args.lua.push(loadJsonAsset(args.getString())) 1 } state.setTableFunction("makeCurrentVersionedJson", this) {args -> TODO("makeCurrentVersionedJson") } state.setTableFunction("loadVersionedJson", this) {args -> TODO("loadVersionedJson") } state.setTableFunction("evalFunction", this) {args -> val name = args.getString() val fn = Registries.jsonFunctions[name] ?: throw NoSuchElementException("No such function $name") args.push(fn.value.evaluate(args.getDouble())) 1 } state.setTableFunction("evalFunction2", this) {args -> val name = args.getString() val fn = Registries.json2Functions[name] ?: throw NoSuchElementException("No such 2function $name") args.push(fn.value.evaluate(args.getDouble(), args.getDouble())) 1 } state.setTableFunction("imageSize", this) {args -> val name = args.getString() args.lua.push(getImage(name)?.size ?: throw FileNotFoundException("No such file $name")) 1 } state.setTableFunction("imageSpaces", this) { args -> // List root.imageSpaces(String imagePath, Vec2F worldPosition, float spaceScan, bool flip) val name = args.getString() val values = getImage(name)?.worldSpaces(args.getVector2i(), args.getDouble(), args.getBool()) ?: throw FileNotFoundException("No such file $name") args.lua.pushTable(arraySize = values.size) val table = args.lua.stackTop for ((i, value) in values.withIndex()) { args.lua.push(i + 1) args.lua.push(value) args.lua.setTableValue(table) } 1 } state.setTableFunction("nonEmptyRegion", this) { args -> val name = args.getString() args.lua.push(getImage(name)?.nonEmptyRegion ?: throw FileNotFoundException("No such file $name")) 1 } state.setTableFunction("npcConfig", this) { args -> // Json root.npcConfig(String npcType) val name = args.getString() args.push(Registries.npcTypes[name] ?: throw NoSuchElementException("No such NPC type $name")) 1 } state.setTableFunction("npcVariant", this) { args -> // Json root.npcVariant(String species, String npcType, float level, [unsigned seed], [Json parameters]) TODO("npcVariant") } state.setTableFunction("projectileGravityMultiplier", this) { args -> // float root.projectileGravityMultiplier(String projectileName) TODO("projectileGravityMultiplier") } state.setTableFunction("projectileConfig", this) { args -> // Json root.projectileConfig(String projectileName) val name = args.getString() args.lua.push(Registries.projectiles[name]?.json ?: throw kotlin.NoSuchElementException("No such Projectile type $name")) 1 } state.setTableFunction("recipesForItem", this) { args -> args.lua.push(JsonArray().also { a -> RecipeRegistry.output2recipes[args.getString()]?.stream()?.map { it.toJson() }?.forEach { a.add(it) } }) 1 } state.setTableFunction("itemType", this) { args -> val name = args.getString() args.lua.push(Registries.items[name]?.value?.itemType ?: throw NoSuchElementException("No such item $name")) 1 } state.setTableFunction("itemTags", this) { args -> val name = args.getString() args.lua.pushStrings(Registries.items[name]?.value?.itemTags ?: throw NoSuchElementException("No such item $name")) 1 } state.setTableFunction("itemHasTag", this) { args -> val name = args.getString() val tag = args.getString() args.push((Registries.items[name]?.value?.itemTags ?: throw NoSuchElementException("No such item $name")).contains(tag)) 1 } // TODO: генерация state.setTableFunction("itemConfig", this) { args -> // Json root.itemConfig(ItemDescriptor descriptor, [float level], [unsigned seed]) val item = item(args.getValue()) val level = if (args.hasSomethingAt()) args.getDouble() else null val seed = if (args.hasSomethingAt()) args.getLong() else null if (item.isEmpty) { args.push() } else { args.push(JsonObject().also { it["directory"] = item.item!!.file.computeDirectory() it["config"] = item.item!!.json it["parameters"] = item.parameters }) } 1 } // TODO: генерация state.setTableFunction("createItem", this) { args -> // ItemDescriptor root.createItem(ItemDescriptor descriptor, [float level], [unsigned seed]) val item = item(args.getValue()) val level = if (args.hasSomethingAt()) args.getDouble() else null val seed = if (args.hasSomethingAt()) args.getLong() else null if (item.isEmpty) { args.push() return@setTableFunction 1 } if (item.maxStackSize < item.size) { item.size = item.maxStackSize } args.push(gson.toJsonTree(item)) 1 } state.setTableFunction("tenantConfig", this) { args -> // Json root.tenantConfig(String tenantName) val name = args.getString() Registries.tenants[name]?.push(args) ?: throw NoSuchElementException("No such tenant $name") 1 } state.setTableFunction("getMatchingTenants", this) { args -> // Json root.tenantConfig(String tenantName) val tags = args.getTable() val actualTags = Object2IntOpenHashMap() for ((k, v) in tags.entrySet()) { if (v is JsonPrimitive && v.isNumber) { actualTags[k] = v.asInt } } args.push(Registries.tenants.values .stream() .filter { it.value.test(actualTags) } .sorted { a, b -> b.value.compareTo(a.value) } .map { it.toJson() } .collect(JsonArrayCollector)) 1 } state.setTableFunction("liquidStatusEffects", this) { args -> val liquid: LiquidDefinition if (args.isStringAt()) { val name = args.getString() liquid = Registries.liquid[name]?.value ?: throw NoSuchElementException("No such liquid with name $name") } else { val id = args.getInt() liquid = Registries.liquid[id]?.value ?: throw NoSuchElementException("No such liquid with ID $id") } args.lua.pushStrings(liquid.statusEffects.stream().map { it.value?.value?.name }.filterNotNull().toList()) 1 } state.setTableFunction("generateName", this) { args -> val assetName = args.getString() val seed = if (args.hasSomethingAt()) args.getLong() else time.nanos val names = loadJsonAsset(assetName) ?: throw NoSuchElementException("No such JSON asset $assetName") if (names !is JsonArray) { var possibleName: String? = null if (names is JsonObject) { for ((k, v) in names.entrySet()) { if (v is JsonArray && !v.isEmpty) { if (possibleName == null || (names[possibleName] as JsonArray).size() < v.size()) possibleName = k } } } if (possibleName != null) { if (assetName.contains(':')) { throw IllegalArgumentException("JSON asset $assetName is not an array, did you mean $assetName.$possibleName?") } else { throw IllegalArgumentException("JSON asset $assetName is not an array, did you mean $assetName:$possibleName?") } } else { throw IllegalArgumentException("JSON asset $assetName is not an array") } } if (names.isEmpty) { throw IllegalStateException("JSON array $assetName is empty") } args.push(names[Random(seed).nextInt(0, names.size())]) 1 } state.setTableFunction("questConfig", this) { args -> val name = args.getString() args.push(Registries.questTemplates[name] ?: throw NoSuchElementException("No such quest template $name")) 1 } state.setTableFunction("npcPortrait", this) { args -> // JsonArray root.npcPortrait(String portraitMode, String species, String npcType, float level, [unsigned seed], [Json parameters]) // Generates an NPC with the specified type, level, seed and parameters and returns a portrait in the given portraitMode as a list of drawables. TODO("npcPortrait") } state.setTableFunction("monsterPortrait", this) { args -> // JsonArray root.monsterPortrait(String typeName, [Json parameters]) // Generates a monster of the given type with the given parameters and returns its portrait as a list of drawables. TODO("monsterPortrait") } state.setTableFunction("isTreasurePool", this) { args -> args.push(args.getString() in Registries.treasurePools) 1 } state.setTableFunction("createTreasure", this) { args -> val name = args.getString() val level = args.getDouble() val rand = if (args.hasSomethingAt()) java.util.Random(args.getLong()) else java.util.Random() args.push(Registries.treasurePools[name]?.value?.evaluate(rand, level)?.stream()?.map { it.toJson() }?.filterNotNull()?.collect(JsonArrayCollector) ?: throw NoSuchElementException("No such treasure pool $name")) 1 } state.setTableFunction("materialMiningSound", this) { args -> val name = args.getString() val mod = if (args.hasSomethingAt()) args.getString() else null val mat = Registries.tiles[name] ?: throw NoSuchElementException("No such material $name") if (mod == null) { args.push(mat.value.miningSounds.firstOrNull()) } else { val getMod = Registries.tileModifiers[mod] ?: throw NoSuchElementException("No such material modifier $mod") args.push(getMod.value.miningSounds.firstOrNull() ?: mat.value.miningSounds.firstOrNull()) } 1 } state.setTableFunction("materialFootstepSound", this) { args -> val name = args.getString() val mod = if (args.hasSomethingAt()) args.getString() else null val mat = Registries.tiles[name] ?: throw NoSuchElementException("No such material $name") if (mod == null) { args.push(mat.value.footstepSound) } else { val getMod = Registries.tileModifiers[mod] ?: throw NoSuchElementException("No such material modifier $mod") args.push(getMod.value.footstepSound ?: mat.value.footstepSound) } 1 } state.setTableFunction("materialHealth", this) { args -> val name = args.getString() val mod = if (args.hasSomethingAt()) args.getString() else null val mat = Registries.tiles[name] ?: throw NoSuchElementException("No such material $name") if (mod == null) { args.push(mat.value.health) } else { val getMod = Registries.tileModifiers[mod] ?: throw NoSuchElementException("No such material modifier $mod") args.push(getMod.value.health + mat.value.health) } 1 } state.setTableFunction("materialConfig", this) { args -> val name = args.getString() args.pushFull(Registries.tiles[name]) 1 } state.setTableFunction("modConfig", this) { args -> val name = args.getString() args.pushFull(Registries.tileModifiers[name]) 1 } state.setTableFunction("liquidConfig", this) { args -> if (args.isNumberAt()) { val id = args.getLong().toInt() args.pushFull(Registries.liquid[id]) } else { val name = args.getString() args.pushFull(Registries.liquid[name]) } 1 } state.setTableFunction("liquidName", this) { args -> val id = args.getLong().toInt() args.push(Registries.liquid[id]?.value?.name ?: throw NoSuchElementException("No such liquid with ID $id")) 1 } state.setTableFunction("liquidId", this) { args -> val name = args.getString() args.push(Registries.liquid[name]?.value?.name ?: throw NoSuchElementException("No such liquid $name")) 1 } state.setTableFunction("monsterSkillParameter", this) { args -> val name = args.getString() val param = args.getString() // parity: если скила не существует, то оригинальный движок просто возвращает nil args.push(Registries.monsterSkills[name]?.value?.config?.get(param)) 1 } state.setTableFunction("monsterParameters", this) { args -> val name = args.getString() val monster = Registries.monsterTypes[name] ?: throw NoSuchElementException("No such monster type $name") args.push(monster.traverseJsonPath("baseParameters")) 1 } state.setTableFunction("monsterMovementSettings", this) { args -> val name = args.getString() val monster = Registries.monsterTypes[name] ?: throw NoSuchElementException("No such monster type $name") args.push(gson.toJsonTree(monster.value.baseParameters.movementSettings)) 1 } state.setTableFunction("createBiome", this) { args -> TODO("createBiome") } state.setTableFunction("hasTech", this) { args -> args.push(args.getString() in Registries.techs) 1 } state.setTableFunction("techType", this) { args -> val name = args.getString() val tech = Registries.techs[name] ?: throw NoSuchElementException("No such tech $name") args.push(tech.value.type) 1 } state.setTableFunction("techConfig", this) { args -> val name = args.getString() val tech = Registries.techs[name] ?: throw NoSuchElementException("No such tech $name") tech.push(args) 1 } state.setTableFunction("treeStemDirectory", this) { args -> // String root.treeStemDirectory(String stemName) TODO("treeStemDirectory") } state.setTableFunction("treeFoliageDirectory", this) { args -> // String root.treeFoliageDirectory(String foliageName) TODO("treeFoliageDirectory") } state.setTableFunction("collection", this) { args -> // Collection root.collection(String collectionName) TODO("collection") } state.setTableFunction("collectables", this) { args -> // List root.collectables(String collectionName) TODO("collectables") } state.setTableFunction("elementalResistance", this) { args -> // String root.elementalResistance(String elementalType) TODO("elementalResistance") } state.setTableFunction("dungeonMetadata", this) { args -> // Json root.dungeonMetadata(String dungeonName) TODO("dungeonMetadata") } state.setTableFunction("behavior", this) { args -> // BehaviorState root.behavior(`LuaTable` context, Json config, `JsonObject` parameters) TODO("behavior") } state.pop() state.load(polyfill, "@starbound.jar!/scripts/polyfill.lua") state.call() } 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) } private val initCallbacks = ArrayList<() -> Unit>() var playerDefinition: PlayerDefinition by WriteOnce() private set private fun doInitialize(log: ILoadingLog, parallel: Boolean) { var time = System.currentTimeMillis() if (archivePaths.isNotEmpty()) { log.line("Searching for pak archives...".also(LOGGER::info)) for (path in archivePaths) { val line = log.line("Reading index of ${path}...".also(LOGGER::info)) addPak(StarboundPak(path) { _, status -> line.text = ("${path.parent}/${path.name}: $status") }) } } log.line("Finished reading pak archives in ${System.currentTimeMillis() - time}ms".also(LOGGER::info)) time = System.currentTimeMillis() log.line("Building file index...".also(LOGGER::info)) 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) } }) log.line("Finished building file index in ${System.currentTimeMillis() - time}ms".also(LOGGER::info)) val tasks = ArrayList>() val pool = if (parallel) ForkJoinPool.commonPool() else ForkJoinPool(1) tasks.addAll(Registries.load(log, ext2files, pool)) tasks.addAll(RecipeRegistry.load(log, ext2files, pool)) tasks.addAll(GlobalDefaults.load(log, pool)) AssetPathStack.block("/") { //playerDefinition = gson.fromJson(locate("/player.config").reader(), PlayerDefinition::class.java) } tasks.forEach { it.join() } if (!parallel) pool.shutdown() initializing = false initialized = true log.line("Finished loading in ${System.currentTimeMillis() - time}ms") } fun initializeGame(log: ILoadingLog, parallel: Boolean = true) { if (initializing) { throw IllegalStateException("Already initializing!") } if (initialized) { throw IllegalStateException("Already initialized!") } initializing = true Thread({ doInitialize(log, parallel) }, "Asset Loader").also { it.isDaemon = true it.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() } } }