package ru.dbotthepony.kstarbound import com.github.benmanes.caffeine.cache.Cache import com.github.benmanes.caffeine.cache.Caffeine import com.github.benmanes.caffeine.cache.Interner import com.google.gson.* import com.google.gson.internal.bind.JsonTreeReader import it.unimi.dsi.fastutil.Hash import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap import it.unimi.dsi.fastutil.objects.Object2ObjectFunction import it.unimi.dsi.fastutil.objects.Object2ObjectOpenCustomHashMap import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import org.apache.logging.log4j.LogManager import org.lwjgl.stb.STBImage 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.AtlasConfiguration import ru.dbotthepony.kstarbound.defs.image.ImageReference import ru.dbotthepony.kstarbound.defs.item.impl.BackArmorItemDefinition import ru.dbotthepony.kstarbound.defs.item.impl.ChestArmorItemDefinition import ru.dbotthepony.kstarbound.defs.item.impl.CurrencyItemDefinition import ru.dbotthepony.kstarbound.defs.item.impl.FlashlightDefinition import ru.dbotthepony.kstarbound.defs.item.impl.HarvestingToolPrototype import ru.dbotthepony.kstarbound.defs.item.impl.HeadArmorItemDefinition import ru.dbotthepony.kstarbound.defs.item.api.IArmorItemDefinition import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition import ru.dbotthepony.kstarbound.defs.item.InventoryIcon import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition import ru.dbotthepony.kstarbound.defs.item.impl.ItemDefinition import ru.dbotthepony.kstarbound.defs.item.impl.LegsArmorItemDefinition import ru.dbotthepony.kstarbound.defs.item.impl.LiquidItemDefinition import ru.dbotthepony.kstarbound.defs.item.impl.MaterialItemDefinition import ru.dbotthepony.kstarbound.defs.monster.MonsterSkillDefinition import ru.dbotthepony.kstarbound.defs.monster.MonsterTypeDefinition import ru.dbotthepony.kstarbound.defs.npc.NpcTypeDefinition import ru.dbotthepony.kstarbound.defs.npc.TenantDefinition 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.player.RecipeDefinition import ru.dbotthepony.kstarbound.defs.player.TechDefinition import ru.dbotthepony.kstarbound.defs.projectile.ProjectileDefinition import ru.dbotthepony.kstarbound.defs.quest.QuestTemplate import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier import ru.dbotthepony.kstarbound.defs.tile.TileDefinition 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.InternedJsonElementAdapter import ru.dbotthepony.kstarbound.io.json.InternedStringAdapter import ru.dbotthepony.kstarbound.io.json.LongRangeAdapter import ru.dbotthepony.kstarbound.io.json.NothingAdapter 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.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.PathStack import ru.dbotthepony.kstarbound.util.SBPattern 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 ru.dbotthepony.kvector.vector.Vector2i import java.io.* import java.lang.ref.ReferenceQueue import java.lang.ref.WeakReference import java.text.DateFormat import java.time.Duration 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 class Starbound : ISBFileLocator { private val logger = LogManager.getLogger() val pathStack = PathStack(STRINGS) 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 private val _questTemplates = ObjectRegistry("quest templates", QuestTemplate::id) val questTemplates = _questTemplates.view private val _techs = ObjectRegistry("techs", TechDefinition::name) val techs = _techs.view private val _jsonFunctions = ObjectRegistry("json functions") val jsonFunctions = _jsonFunctions.view private val _json2Functions = ObjectRegistry("json 2functions") val json2Functions = _json2Functions.view private val _npcTypes = ObjectRegistry("npc types", NpcTypeDefinition::type) val npcTypes = _npcTypes.view private val _projectiles = ObjectRegistry("projectiles", ProjectileDefinition::projectileName) val projectiles = _projectiles.view private val _tenants = ObjectRegistry("tenants", TenantDefinition::name) val tenants = _tenants.view val recipeRegistry = RecipeRegistry() private val _treasurePools = ObjectRegistry("treasure pools", TreasurePoolDefinition::name) val treasurePools = _treasurePools.view private val _monsterSkills = ObjectRegistry("monster skills", MonsterSkillDefinition::name) val monsterSkills = _monsterSkills.view private val _monsterTypes = ObjectRegistry("monster types", MonsterTypeDefinition::type) val monsterTypes = _monsterTypes.view 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)) // ArrayList registerTypeAdapterFactory(ArrayListAdapterFactory) // все enum'ы без особых настроек registerTypeAdapterFactory(EnumAdapter.Companion) // @JsonBuilder registerTypeAdapterFactory(BuilderAdapter.Factory(STRINGS)) // @JsonFactory registerTypeAdapterFactory(FactoryAdapter.Factory(STRINGS)) // Either<> 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(Json2Function.Companion) // Общее registerTypeAdapterFactory(ThingDescription.Factory(STRINGS)) registerTypeAdapter(EnumAdapter(DamageType::class, default = DamageType.NORMAL)) registerTypeAdapterFactory(InventoryIcon.Factory(pathStack)) registerTypeAdapterFactory(IArmorItemDefinition.Frames.Factory) registerTypeAdapterFactory(AssetPathFactory(pathStack)) registerTypeAdapterFactory(ImageReference.Factory({ atlasRegistry.get(it) }, pathStack)) registerTypeAdapterFactory(AssetReferenceFactory(pathStack, this@Starbound)) registerTypeAdapter(ItemStack.Adapter(this@Starbound)) registerTypeAdapterFactory(ItemReference.Factory(STRINGS)) registerTypeAdapterFactory(TreasurePoolDefinition.Companion) registerTypeAdapterFactory(with(RegistryReferenceFactory()) { add(_tiles) add(_tileModifiers) add(_liquid) add(_items) add(_species) add(_statusEffects) add(_particles) add(_questTemplates) add(_techs) add(_jsonFunctions) add(_json2Functions) add(_npcTypes) add(_projectiles) add(_tenants) add(_treasurePools) add(_monsterSkills) add(_monsterTypes) }) registerTypeAdapter(LongRangeAdapter) create() } val atlasRegistry = AtlasConfiguration.Registry(this, pathStack, gson) private val imageCache: Cache = Caffeine.newBuilder() .softValues() .expireAfterAccess(Duration.ofMinutes(20)) .weigher { key, value -> value.data.capacity() } .maximumWeight(1_024L * 1_024L * 256L /* 256 МиБ */) .build() fun item(name: String): ItemStack { return ItemStack(items[name] ?: return ItemStack.EMPTY) } fun item(name: String, count: Long): ItemStack { if (count <= 0L) return ItemStack.EMPTY return ItemStack(items[name] ?: return ItemStack.EMPTY, count = count) } fun item(name: String, count: Long, parameters: JsonObject): ItemStack { if (count <= 0L) return ItemStack.EMPTY return ItemStack(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() } fun imageData(path: String): ImageData { return imageCache.get(path) { val file = locate(path) if (!file.exists) { throw FileNotFoundException("No such file $file") } if (!file.isFile) { throw IllegalStateException("File $file is a directory") } val getWidth = intArrayOf(0) val getHeight = intArrayOf(0) val components = intArrayOf(0) val data = STBImage.stbi_load_from_memory( file.readDirect(), getWidth, getHeight, components, 0 ) ?: throw IllegalArgumentException("File $file is not an image or it is corrupted") ImageData(data, getWidth[0], getHeight[0], components[0]) } } fun imageSize(path: String): Vector2i { val image = imageData(path) return Vector2i(image.width, image.height) } /** * **ПРЕДУПРЕЖДЕНИЕ:** * [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 = 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 = json2Functions[name] ?: throw NoSuchElementException("No such 2function $name") args.push(fn.value.evaluate(args.getDouble(), args.getDouble())) 1 } state.setTableFunction("imageSize", this) {args -> args.lua.push(imageSize(args.getString())) 1 } state.setTableFunction("imageSpaces", this) { args -> // List root.imageSpaces(String imagePath, Vec2F worldPosition, float spaceScan, bool flip) val values = imageData(args.getString()).worldSpaces(args.getVector2i(), args.getDouble(), args.getBool()) 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 -> args.lua.push(imageData(args.getString()).nonEmptyRegion) 1 } state.setTableFunction("npcConfig", this) { args -> // Json root.npcConfig(String npcType) val name = args.getString() args.push(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(projectiles[name]?.copy() ?: 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(items[name]?.value?.itemType ?: throw NoSuchElementException("No such item $name")) 1 } state.setTableFunction("itemTags", this) { args -> val name = args.getString() args.lua.pushStrings(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((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!!.copy() 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() 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(tenants.values .stream() .filter { it.value.test(actualTags) } .sorted { a, b -> b.value.compareTo(a.value) } .map { it.copy() } .collect(JsonArrayCollector)) 1 } state.setTableFunction("liquidStatusEffects", this) { args -> val liquid: LiquidDefinition if (args.isStringAt()) { val name = args.getString() liquid = this.liquid[name]?.value ?: throw NoSuchElementException("No such liquid with name $name") } else { val id = args.getInt() liquid = this.liquidByID[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(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 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(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 = tiles[name] ?: throw NoSuchElementException("No such material $name") if (mod == null) { args.push(mat.value.miningSounds.firstOrNull()) } else { val getMod = 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 = tiles[name] ?: throw NoSuchElementException("No such material $name") if (mod == null) { args.push(mat.value.footstepSound) } else { val getMod = 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 = tiles[name] ?: throw NoSuchElementException("No such material $name") if (mod == null) { args.push(mat.value.health) } else { val getMod = 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(tiles[name]) 1 } state.setTableFunction("modConfig", this) { args -> val name = args.getString() args.pushFull(tileModifiers[name]) 1 } state.setTableFunction("liquidConfig", this) { args -> if (args.isNumberAt()) { val id = args.getLong().toInt() args.pushFull(liquidByID[id]) } else { val name = args.getString() args.pushFull(liquid[name]) } 1 } state.setTableFunction("liquidName", this) { args -> val id = args.getLong().toInt() args.push(liquidByID[id]?.value?.name ?: throw NoSuchElementException("No such liquid with ID $id")) 1 } state.setTableFunction("liquidId", this) { args -> val name = args.getString() args.push(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(monsterSkills[name]?.value?.config?.get(param)) 1 } state.setTableFunction("monsterParameters", this) { args -> val name = args.getString() val monster = 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 = 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 techs) 1 } state.setTableFunction("techType", this) { args -> val name = args.getString() val tech = 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 = 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 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, registry: ObjectRegistry, files: List, ) { loadStage(callback, loader = { for (listedFile in files) { try { it("Loading $listedFile") registry.add(gson, listedFile, pathStack) } 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>> { 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) } }) callback(false, false, "Finished building file index in ${System.currentTimeMillis() - time}ms".also(logger::info)) loadStage(callback, { loadItemDefinitions(it, ext2files) }, "item definitions") loadStage(callback, { loadJsonFunctions(it, ext2files["functions"] ?: listOf()) }, "json functions") loadStage(callback, { loadJson2Functions(it, ext2files["2functions"] ?: listOf()) }, "json 2functions") loadStage(callback, { loadRecipes(it, ext2files["recipe"] ?: listOf()) }, "recipes") loadStage(callback, { loadTreasurePools(it, ext2files["treasurepools"] ?: listOf()) }, "treasure pools") loadStage(callback, _tiles, ext2files["material"] ?: listOf()) loadStage(callback, _tileModifiers, ext2files["matmod"] ?: listOf()) loadStage(callback, _liquid, ext2files["liquid"] ?: listOf()) loadStage(callback, _statusEffects, ext2files["statuseffect"] ?: listOf()) loadStage(callback, _species, ext2files["species"] ?: listOf()) loadStage(callback, _particles, ext2files["particle"] ?: listOf()) loadStage(callback, _questTemplates, ext2files["questtemplate"] ?: listOf()) loadStage(callback, _techs, ext2files["tech"] ?: listOf()) loadStage(callback, _npcTypes, ext2files["npctype"] ?: listOf()) loadStage(callback, _projectiles, ext2files["projectile"] ?: listOf()) loadStage(callback, _tenants, ext2files["tenant"] ?: listOf()) loadStage(callback, _monsterSkills, ext2files["monsterskill"] ?: listOf()) loadStage(callback, _monsterTypes, ext2files["monstertype"] ?: 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").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() } } private fun loadItemDefinitions(callback: (String) -> Unit, files: Map>) { val fileMap = mapOf( "item" to ItemDefinition::class.java, "currency" to CurrencyItemDefinition::class.java, "liqitem" to LiquidItemDefinition::class.java, "matitem" to MaterialItemDefinition::class.java, "flashlight" to FlashlightDefinition::class.java, "harvestingtool" to HarvestingToolPrototype::class.java, "head" to HeadArmorItemDefinition::class.java, "chest" to ChestArmorItemDefinition::class.java, "legs" to LegsArmorItemDefinition::class.java, "back" to BackArmorItemDefinition::class.java, ) for ((ext, clazz) in fileMap) { val fileList = files[ext] ?: continue for (listedFile in fileList) { try { callback("Loading $listedFile") val json = gson.fromJson(listedFile.reader(), JsonObject::class.java) val def: IItemDefinition = pathStack(listedFile.computeDirectory()) { gson.fromJson(JsonTreeReader(json), clazz) } _items.add(def, json, listedFile, gson, pathStack) } catch (err: Throwable) { logger.error("Loading item definition file $listedFile", err) } if (terminateLoading) { return } } } } private fun loadJsonFunctions(callback: (String) -> Unit, files: Collection) { for (listedFile in files) { callback("Loading $listedFile") val json = gson.fromJson(listedFile.reader(), JsonObject::class.java) for ((k, v) in json.entrySet()) { try { callback("Loading $k from $listedFile") val fn = gson.fromJson(JsonTreeReader(v), JsonFunction::class.java) _jsonFunctions.add(fn, v, listedFile, gson, pathStack, k) } catch (err: Throwable) { logger.error("Loading json function definition $k from file $listedFile", err) } } if (terminateLoading) { return } } } private fun loadJson2Functions(callback: (String) -> Unit, files: Collection) { for (listedFile in files) { callback("Loading $listedFile") val json = gson.fromJson(listedFile.reader(), JsonObject::class.java) for ((k, v) in json.entrySet()) { try { callback("Loading $k from $listedFile") val fn = gson.fromJson(JsonTreeReader(v), Json2Function::class.java) _json2Functions.add(fn, v, listedFile, gson, pathStack, k) } catch (err: Throwable) { logger.error("Loading json 2function definition $k from file $listedFile", err) } } if (terminateLoading) { return } } } private fun loadTreasurePools(callback: (String) -> Unit, files: Collection) { for (listedFile in files) { callback("Loading $listedFile") val json = gson.fromJson(listedFile.reader(), JsonObject::class.java) for ((k, v) in json.entrySet()) { try { callback("Loading $k from $listedFile") val result = gson.fromJson(JsonTreeReader(v), TreasurePoolDefinition::class.java) result.name = k _treasurePools.add(result, v, listedFile, gson, pathStack) } catch (err: Throwable) { logger.error("Loading treasure pool definition $k from file $listedFile", err) } } if (terminateLoading) { return } } } private fun loadRecipes(callback: (String) -> Unit, files: Collection) { for (listedFile in files) { try { callback("Loading $listedFile") val json = gson.fromJson(listedFile.reader(), JsonElement::class.java) val value = gson.fromJson(JsonTreeReader(json), RecipeDefinition::class.java) recipeRegistry.add(RegistryObject(value, json, listedFile, gson, pathStack)) } catch (err: Throwable) { logger.error("Loading recipe definition file $listedFile", err) } if (terminateLoading) { return } } } companion object { /** * Глобальный [Interner] для [String] * * Так как нет смысла иметь множество [Interner]'ов для [String], * а так же в силу его поточной безопасности, * данный [Interner] доступен глобально */ @JvmField val STRINGS: Interner = Interner.newWeakInterner() private val polyfill by lazy { loadInternalScript("polyfill") } } } private class StringInterner(private val segmentBits: Int) : Interner, Hash.Strategy { class Ref(referent: String, queue: ReferenceQueue) : WeakReference(referent, queue) { val hash = referent.hashCode() override fun hashCode(): Int { return hash } } override fun equals(a: Any?, b: Any?): Boolean { if (a is String && b is Ref) return a == b.get() if (a is Ref && b is String) return a.get() == b return a === b } override fun hashCode(o: Any): Int { return o.hashCode() } private val queue = ReferenceQueue() private val actualSegmentBits: Int init { var result = 0 for (i in 0 until segmentBits) { result = result or (1.shl(i)) } actualSegmentBits = result } private val cleaner = Runnable { while (true) { val ref = queue.remove() as Ref val segment = segments[ref.hash and actualSegmentBits] synchronized(segment) { val removed = segment.remove(ref) check(removed === ref) { "Expected to remove reference $ref from segment ${ref.hash and actualSegmentBits} (full hash: ${ref.hash}), but we removed $removed (removed hash: ${removed.hashCode()}, removed segment: ${removed.hashCode() and actualSegmentBits})" } } } } private val thread = Thread(cleaner, "String Interner Cleanup Thread") init { thread.priority = 2 thread.isDaemon = true thread.start() } private val segments: Array> = Array(1.shl(segmentBits)) { Object2ObjectOpenCustomHashMap(this) } override fun intern(sample: String): String { val hash = sample.hashCode() val segment = segments[hash and actualSegmentBits] synchronized(segment) { val canonical = (segment[sample] as Ref?)?.get() if (canonical != null) { return canonical } val ref = Ref(sample, queue) segment.put(ref, ref) return sample } } }