KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt

471 lines
16 KiB
Kotlin

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