package ru.dbotthepony.kstarbound import com.google.common.collect.ImmutableList 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.ints.Int2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import org.apache.logging.log4j.LogManager 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.SpriteReference import ru.dbotthepony.kstarbound.defs.item.ArmorItemPrototype import ru.dbotthepony.kstarbound.defs.item.ArmorPieceType import ru.dbotthepony.kstarbound.defs.item.CurrencyItemPrototype import ru.dbotthepony.kstarbound.defs.item.HarvestingToolPrototype import ru.dbotthepony.kstarbound.defs.item.IArmorItemDefinition import ru.dbotthepony.kstarbound.defs.item.IFossilItemDefinition import ru.dbotthepony.kstarbound.defs.item.IItemDefinition import ru.dbotthepony.kstarbound.defs.item.ItemDefinition import ru.dbotthepony.kstarbound.defs.item.ItemPrototype import ru.dbotthepony.kstarbound.defs.item.ItemRarity import ru.dbotthepony.kstarbound.defs.item.ItemTooltipKind 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.liquid.LiquidDefinition import ru.dbotthepony.kstarbound.defs.parallax.ParallaxPrototype import ru.dbotthepony.kstarbound.defs.projectile.* import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier import ru.dbotthepony.kstarbound.defs.tile.RenderParameters import ru.dbotthepony.kstarbound.defs.tile.RenderTemplate import ru.dbotthepony.kstarbound.defs.tile.TileDefinition import ru.dbotthepony.kstarbound.defs.world.SkyParameters import ru.dbotthepony.kstarbound.defs.world.dungeon.DungeonWorldDef 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.builder.EnumAdapter 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.Vector4iTypeAdapter 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 java.io.* import java.text.DateFormat import java.util.* import kotlin.collections.ArrayList const val METRES_IN_STARBOUND_UNIT = 0.5 const val METRES_IN_STARBOUND_UNITf = 0.5f const val PIXELS_IN_STARBOUND_UNIT = 8.0 const val PIXELS_IN_STARBOUND_UNITf = 8.0f // class TileDefLoadingException(message: String, cause: Throwable? = null) : IllegalStateException(message, cause) // class ProjectileDefLoadingException(message: String, cause: Throwable? = null) : IllegalStateException(message, cause) fun String.sbIntern(): String = Starbound.STRING_INTERNER.intern(this) object Starbound { private val LOGGER = LogManager.getLogger() /** * Служит переменной для указания из какой папки происходит чтение asset'а в данном потоке */ var assetFolder by ThreadLocal() private set fun assetFolder(input: String): String { val assetFolder = assetFolder require(assetFolder != null) { "Not reading an asset on current thread" } if (input[0] == '/') return input return STRING_INTERNER.intern("$assetFolder/$input") } fun assetFolderNullable(input: String?): String? { require(assetFolder != null) { "Not reading an asset on current thread" } if (input != null) return assetFolder(input) return null } fun readingFolderListTransformer(input: List?): List? { if (input == null) return null return input.stream().map { assetFolder(it) }.collect(ImmutableList.toImmutableList()) } private val tiles = Object2ObjectOpenHashMap() private val tilesByMaterialID = Int2ObjectOpenHashMap() private val tileModifiers = Object2ObjectOpenHashMap() private val tileModifiersByID = Int2ObjectOpenHashMap() private val liquid = Object2ObjectOpenHashMap() private val liquidByID = Int2ObjectOpenHashMap() private val projectiles = Object2ObjectOpenHashMap() private val parallax = Object2ObjectOpenHashMap() private val functions = Object2ObjectOpenHashMap() private val items = Object2ObjectOpenHashMap() val LIQUID: Map = Collections.unmodifiableMap(liquid) val LIQUID_BY_ID: Map = Collections.unmodifiableMap(liquidByID) val TILE_MODIFIER: Map = Collections.unmodifiableMap(tileModifiers) val TILE_MODIFIER_BY_ID: Map = Collections.unmodifiableMap(tileModifiersByID) val TILE: Map = Collections.unmodifiableMap(tiles) val TILE_BY_ID: Map = Collections.unmodifiableMap(tilesByMaterialID) val PROJECTILE: Map = Collections.unmodifiableMap(projectiles) val PARALLAX: Map = Collections.unmodifiableMap(parallax) val FUNCTION: Map = Collections.unmodifiableMap(functions) val ITEM: Map = Collections.unmodifiableMap(items) val STRING_INTERNER: Interner = Interners.newWeakInterner() val STRING_ADAPTER: TypeAdapter = object : TypeAdapter() { override fun write(out: JsonWriter, value: String) { out.value(value) } override fun read(`in`: JsonReader): String { return STRING_INTERNER.intern(TypeAdapters.STRING.read(`in`)) } } val NULLABLE_STRING_ADAPTER: TypeAdapter = STRING_ADAPTER.nullSafe() val GSON: Gson = GsonBuilder() .enableComplexMapKeySerialization() .serializeNulls() .setDateFormat(DateFormat.LONG) .setFieldNamingPolicy(FieldNamingPolicy.IDENTITY) .setPrettyPrinting() // чтоб строки всегда intern'ились .registerTypeAdapter(NULLABLE_STRING_ADAPTER) // Обработчик @JsonImplementation .registerTypeAdapterFactory(JsonImplementationTypeFactory) // ImmutableList, ImmutableSet, ImmutableMap .registerTypeAdapterFactory(ImmutableCollectionAdapterFactory) // ArrayList .registerTypeAdapterFactory(ArrayListAdapterFactory) // все enum'ы без особых настроек .registerTypeAdapterFactory(EnumAdapter.Companion) // автоматическое создание BuilderAdapter по @аннотациям .registerTypeAdapterFactory(BuilderAdapter.Companion) // автоматическое создание FactoryAdapter по @аннотациям .registerTypeAdapterFactory(FactoryAdapter.Companion) .also(::addStarboundJsonAdapters) .create() @Suppress("unchecked_cast") fun getTypeAdapter(type: Class): TypeAdapter { return when (type) { Float::class.java -> TypeAdapters.FLOAT as TypeAdapter Double::class.java -> TypeAdapters.DOUBLE as TypeAdapter String::class.java -> NULLABLE_STRING_ADAPTER as TypeAdapter Int::class.java -> TypeAdapters.INTEGER as TypeAdapter Long::class.java -> TypeAdapters.LONG as TypeAdapter Boolean::class.java -> TypeAdapters.BOOLEAN as TypeAdapter else -> GSON.getAdapter(type) } } 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) } 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 } 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 loadJson(path: String): JsonElement { return JsonParser.parseReader(locate(path).reader()) } fun readDirect(path: String) = locate(path).readDirect() fun getTileDefinition(name: String) = tiles[name] private val initCallbacks = ArrayList<() -> Unit>() private fun loadStage( callback: (Boolean, Boolean, String) -> Unit, loader: ((String) -> Unit) -> Unit, name: String, ) { 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") } 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({ val time = System.currentTimeMillis() if (archivePaths.isNotEmpty()) { callback(false, false, "Reading pak archives...") for (path in archivePaths) { callback(false, false, "Reading ${path.name}...") addPak(StarboundPak(path) { _, status -> callback(false, true, "${path.name}: $status") }) } } loadStage(callback, this::loadFunctions, "functions") loadStage(callback, this::loadTileMaterials, "materials") loadStage(callback, this::loadProjectiles, "projectiles") loadStage(callback, this::loadParallax, "parallax definitions") loadStage(callback, this::loadMaterialModifiers, "material modifier definitions") loadStage(callback, this::loadLiquidDefinitions, "liquid definitions") loadStage(callback, this::loadItemDefinitions, "item definitions") initializing = false initialized = true callback(true, false, "Finished loading in ${System.currentTimeMillis() - time}ms") }, "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 loadTileMaterials(callback: (String) -> Unit) { assetFolder = "/tiles/materials" for (fs in fileSystems) { for (listedFile in fs.explore().filter { it.isFile }.filter { it.name.endsWith(".material") }) { try { callback("Loading $listedFile") assetFolder = listedFile.computeDirectory() val tileDef = GSON.fromJson(listedFile.reader(), TileDefinition::class.java) check(tiles[tileDef.materialName] == null) { "Already has material with name ${tileDef.materialName} loaded!" } check(tilesByMaterialID[tileDef.materialId] == null) { "Already has material with ID ${tileDef.materialId} loaded!" } tilesByMaterialID[tileDef.materialId] = tileDef tiles[tileDef.materialName] = tileDef } catch (err: Throwable) { //throw TileDefLoadingException("Loading tile file $listedFile", err) LOGGER.error("Loading tile file $listedFile", err) } if (terminateLoading) { return } } } assetFolder = null } private fun loadProjectiles(callback: (String) -> Unit) { for (fs in fileSystems) { for (listedFile in fs.explore().filter { it.isFile }.filter { it.name.endsWith(".projectile") }) { try { callback("Loading $listedFile") assetFolder = listedFile.computeDirectory() val def = GSON.fromJson(listedFile.reader(), ConfigurableProjectile::class.java).assemble(listedFile.computeDirectory()) check(projectiles[def.projectileName] == null) { "Already has projectile with ID ${def.projectileName} loaded!" } projectiles[def.projectileName] = def } catch(err: Throwable) { //throw ProjectileDefLoadingException("Loading projectile file $listedFile", err) LOGGER.error("Loading projectile file $listedFile", err) } if (terminateLoading) { return } } } assetFolder = null } private fun loadFunctions(callback: (String) -> Unit) { for (fs in fileSystems) { for (listedFile in fs.explore().filter { it.isFile }.filter { it.name.endsWith(".functions") }) { try { callback("Loading $listedFile") assetFolder = listedFile.computeDirectory() val readObject = JsonParser.parseReader(listedFile.reader()) as JsonObject for (key in readObject.keySet()) { val def = GSON.fromJson(readObject[key], JsonFunction::class.java) functions[key] = def } } catch(err: Throwable) { LOGGER.error("Loading function file $listedFile", err) } if (terminateLoading) { return } } } assetFolder = null } private fun loadParallax(callback: (String) -> Unit) { for (fs in fileSystems) { for (listedFile in fs.explore().filter { it.isFile }.filter { it.name.endsWith(".parallax") }) { try { callback("Loading $listedFile") assetFolder = listedFile.computeDirectory() val def = GSON.fromJson(listedFile.reader(), ParallaxPrototype::class.java) parallax[listedFile.name.substringBefore('.')] = def } catch(err: Throwable) { LOGGER.error("Loading parallax file $listedFile", err) } if (terminateLoading) { return } } } assetFolder = null } private fun loadMaterialModifiers(callback: (String) -> Unit) { assetFolder = "/tiles/materials" for (fs in fileSystems) { for (listedFile in fs.explore().filter { it.isFile }.filter { it.name.endsWith(".matmod") }) { try { callback("Loading $listedFile") assetFolder = listedFile.computeDirectory() val tileDef = GSON.fromJson(listedFile.reader(), MaterialModifier::class.java) check(tileModifiers[tileDef.modName] == null) { "Already has material with name ${tileDef.modName} loaded!" } check(tileModifiersByID[tileDef.modId] == null) { "Already has material with ID ${tileDef.modId} loaded!" } tileModifiersByID[tileDef.modId] = tileDef tileModifiers[tileDef.modName] = tileDef } catch (err: Throwable) { //throw TileDefLoadingException("Loading tile file $listedFile", err) LOGGER.error("Loading tile modifier file $listedFile", err) } if (terminateLoading) { return } } } assetFolder = null } private fun loadLiquidDefinitions(callback: (String) -> Unit) { for (fs in fileSystems) { for (listedFile in fs.explore().filter { it.isFile }.filter { it.name.endsWith(".liquid") }) { try { callback("Loading $listedFile") assetFolder = listedFile.computeDirectory() val liquidDef = GSON.fromJson(listedFile.reader(), LiquidDefinition::class.java) check(liquid.put(liquidDef.name, liquidDef) == null) { "Already has liquid with name ${liquidDef.name} loaded!" } check(liquidByID.put(liquidDef.liquidId, liquidDef) == null) { "Already has liquid with ID ${liquidDef.liquidId} loaded!" } } catch (err: Throwable) { //throw TileDefLoadingException("Loading tile file $listedFile", err) LOGGER.error("Loading liquid definition file $listedFile", err) } if (terminateLoading) { return } } } assetFolder = null } private fun loadItemDefinitions(callback: (String) -> Unit) { val files = listOf(".item", ".currency", ".head", ".chest", ".legs", ".back", ".activeitem", ".matitem", ".liqitem", ".harvestingtool") for (fs in fileSystems) { for (listedFile in fs.explore().filter { it.isFile }.filter { f -> files.any { f.name.endsWith(it) } }) { try { callback("Loading $listedFile") assetFolder = listedFile.computeDirectory() if (listedFile.name.endsWith(".item")) { val def = GSON.fromJson(listedFile.reader(), ItemPrototype::class.java) check(items.put(def.itemName, def.assemble()) == null) { "Already has item with name ${def.itemName} loaded!" } } else if (listedFile.name.endsWith(".harvestingtool")) { val def = GSON.fromJson(listedFile.reader(), HarvestingToolPrototype::class.java) check(items.put(def.itemName, def.assemble()) == null) { "Already has item with name ${def.itemName} loaded!" } } else if (listedFile.name.endsWith(".matitem")) { val def = GSON.fromJson(listedFile.reader(), MaterialItemPrototype::class.java) check(items.put(def.itemName, def.assemble()) == null) { "Already has item with name ${def.itemName} loaded!" } } else if (listedFile.name.endsWith(".liqitem")) { val def = GSON.fromJson(listedFile.reader(), LiquidItemPrototype::class.java) check(items.put(def.itemName, def.assemble()) == null) { "Already has item with name ${def.itemName} loaded!" } } else if (listedFile.name.endsWith(".currency")) { val def = GSON.fromJson(listedFile.reader(), CurrencyItemPrototype::class.java) check(items.put(def.itemName, def.assemble()) == null) { "Already has item with name ${def.itemName} loaded!" } } else if (listedFile.name.endsWith(".head")) { val def = GSON.fromJson(listedFile.reader(), ArmorItemPrototype::class.java) def.armorType = ArmorPieceType.HEAD check(items.put(def.itemName, def.assemble()) == null) { "Already has item with name ${def.itemName} loaded!" } } else if (listedFile.name.endsWith(".chest")) { val def = GSON.fromJson(listedFile.reader(), ArmorItemPrototype::class.java) def.armorType = ArmorPieceType.CHEST check(items.put(def.itemName, def.assemble()) == null) { "Already has item with name ${def.itemName} loaded!" } } else if (listedFile.name.endsWith(".legs")) { val def = GSON.fromJson(listedFile.reader(), ArmorItemPrototype::class.java) def.armorType = ArmorPieceType.LEGS check(items.put(def.itemName, def.assemble()) == null) { "Already has item with name ${def.itemName} loaded!" } } else if (listedFile.name.endsWith(".back")) { val def = GSON.fromJson(listedFile.reader(), ArmorItemPrototype::class.java) def.armorType = ArmorPieceType.BACK check(items.put(def.itemName, def.assemble()) == null) { "Already has item with name ${def.itemName} loaded!" } } } catch (err: Throwable) { LOGGER.error("Loading item definition file $listedFile", err) } if (terminateLoading) { return } } } assetFolder = null } }