KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt
2023-01-22 20:29:48 +07:00

580 lines
20 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.IArmorItemDefinition
import ru.dbotthepony.kstarbound.defs.item.IFossilItemDefinition
import ru.dbotthepony.kstarbound.defs.item.IItemDefinition
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.factory.ImmutableCollectionAdapterFactory
import ru.dbotthepony.kstarbound.math.*
import ru.dbotthepony.kvector.vector.Color
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<String>()
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<String>?): List<String>? {
if (input == null)
return null
return input.stream().map { assetFolder(it) }.collect(ImmutableList.toImmutableList())
}
private val tiles = Object2ObjectOpenHashMap<String, TileDefinition>()
private val tilesByMaterialID = Int2ObjectOpenHashMap<TileDefinition>()
private val tileModifiers = Object2ObjectOpenHashMap<String, MaterialModifier>()
private val tileModifiersByID = Int2ObjectOpenHashMap<MaterialModifier>()
private val liquid = Object2ObjectOpenHashMap<String, LiquidDefinition>()
private val liquidByID = Int2ObjectOpenHashMap<LiquidDefinition>()
private val projectiles = Object2ObjectOpenHashMap<String, ConfiguredProjectile>()
private val parallax = Object2ObjectOpenHashMap<String, ParallaxPrototype>()
private val functions = Object2ObjectOpenHashMap<String, JsonFunction>()
private val items = Object2ObjectOpenHashMap<String, IItemDefinition>()
val LIQUID: Map<String, LiquidDefinition> = Collections.unmodifiableMap(liquid)
val LIQUID_BY_ID: Map<Int, LiquidDefinition> = Collections.unmodifiableMap(liquidByID)
val TILE_MODIFIER: Map<String, MaterialModifier> = Collections.unmodifiableMap(tileModifiers)
val TILE_MODIFIER_BY_ID: Map<Int, MaterialModifier> = Collections.unmodifiableMap(tileModifiersByID)
val TILE: Map<String, TileDefinition> = Collections.unmodifiableMap(tiles)
val TILE_BY_ID: Map<Int, TileDefinition> = Collections.unmodifiableMap(tilesByMaterialID)
val PROJECTILE: Map<String, ConfiguredProjectile> = Collections.unmodifiableMap(projectiles)
val PARALLAX: Map<String, ParallaxPrototype> = Collections.unmodifiableMap(parallax)
val FUNCTION: Map<String, JsonFunction> = Collections.unmodifiableMap(functions)
val ITEM: Map<String, IItemDefinition> = Collections.unmodifiableMap(items)
val STRING_INTERNER: Interner<String> = Interners.newWeakInterner()
val STRING_ADAPTER: TypeAdapter<String> = object : TypeAdapter<String>() {
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?> = STRING_ADAPTER.nullSafe()
val GSON: Gson = GsonBuilder()
.enableComplexMapKeySerialization()
.serializeNulls()
.setDateFormat(DateFormat.LONG)
.setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)
.setPrettyPrinting()
.registerTypeAdapter(ColorTypeAdapter.nullSafe())
.registerTypeAdapterFactory(ImmutableCollectionAdapterFactory)
// чтоб строки всегда intern'ились
.registerTypeAdapter(NULLABLE_STRING_ADAPTER)
// math
.registerTypeAdapter(AABBTypeAdapter)
.registerTypeAdapter(AABBiTypeAdapter)
.registerTypeAdapter(Vector2dTypeAdapter)
.registerTypeAdapter(Vector2fTypeAdapter)
.registerTypeAdapter(Vector2iTypeAdapter)
.registerTypeAdapter(Vector4iTypeAdapter)
.registerTypeAdapter(PolyTypeAdapter)
.also(ConfigurableProjectile::registerGson)
.also(SkyParameters::registerGson)
.also(DungeonWorldDef::registerGson)
.also(ParallaxPrototype::registerGson)
.also(JsonFunction::registerGson)
.also(MaterialModifier::registerGson)
.also(RenderParameters::registerGson)
.also(RenderTemplate::registerGson)
.also(TileDefinition::registerGson)
.also(LiquidDefinition::registerGson)
.also(SpriteReference::registerGson)
.also(AtlasConfiguration::registerGson)
.registerTypeAdapter(LeveledStatusEffect.ADAPTER)
.registerTypeAdapter(MaterialReference.Companion)
.registerTypeAdapter(ThingDescription.Companion)
.registerTypeAdapter(ItemPrototype.ADAPTER)
.registerTypeAdapter(CurrencyItemPrototype.ADAPTER)
.registerTypeAdapter(ArmorItemPrototype.ADAPTER)
.registerTypeAdapter(MaterialItemPrototype.ADAPTER)
.registerTypeAdapter(LiquidItemPrototype.ADAPTER)
.registerTypeAdapter(IItemDefinition.InventoryIcon.ADAPTER)
.registerTypeAdapter(IFossilItemDefinition.FossilSetDescription.ADAPTER)
.registerTypeAdapter(IArmorItemDefinition.ArmorFrames.ADAPTER)
.registerTypeAdapter(EnumAdapter(DamageType::class, default = DamageType.NORMAL).neverNull())
.registerTypeAdapter(EnumAdapter(ItemRarity::class).neverNull())
.registerTypeAdapter(EnumAdapter(ItemTooltipKind::class).neverNull())
.create()
@Suppress("unchecked_cast")
fun <T> getTypeAdapter(type: Class<T>): TypeAdapter<T> {
return when (type) {
Float::class.java -> TypeAdapters.FLOAT as TypeAdapter<T>
Double::class.java -> TypeAdapters.DOUBLE as TypeAdapter<T>
String::class.java -> NULLABLE_STRING_ADAPTER as TypeAdapter<T>
Int::class.java -> TypeAdapters.INTEGER as TypeAdapter<T>
Long::class.java -> TypeAdapters.LONG as TypeAdapter<T>
Boolean::class.java -> TypeAdapters.BOOLEAN as TypeAdapter<T>
else -> GSON.getAdapter(type)
}
}
var initializing = false
private set
var initialized = false
private set
@Volatile
var terminateLoading = false
private val archivePaths = ArrayList<File>()
private val fileSystems = ArrayList<IStarboundFile>()
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")
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(".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
}
}