KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt
2024-02-10 16:49:58 +07:00

526 lines
15 KiB
Kotlin

package ru.dbotthepony.kstarbound
import com.github.benmanes.caffeine.cache.Interner
import com.google.gson.*
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.AABBTypeAdapter
import ru.dbotthepony.kommons.gson.AABBiTypeAdapter
import ru.dbotthepony.kommons.gson.ColorTypeAdapter
import ru.dbotthepony.kommons.gson.EitherTypeAdapter
import ru.dbotthepony.kommons.gson.KOptionalTypeAdapter
import ru.dbotthepony.kommons.gson.NothingAdapter
import ru.dbotthepony.kommons.gson.Vector2dTypeAdapter
import ru.dbotthepony.kommons.gson.Vector2fTypeAdapter
import ru.dbotthepony.kommons.gson.Vector2iTypeAdapter
import ru.dbotthepony.kommons.gson.Vector4dTypeAdapter
import ru.dbotthepony.kommons.gson.Vector4iTypeAdapter
import ru.dbotthepony.kommons.util.MailboxExecutorService
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.player.BlueprintLearnList
import ru.dbotthepony.kstarbound.io.*
import ru.dbotthepony.kstarbound.json.FastutilTypeAdapterFactory
import ru.dbotthepony.kstarbound.json.InternedJsonElementAdapter
import ru.dbotthepony.kstarbound.json.InternedStringAdapter
import ru.dbotthepony.kstarbound.json.LongRangeAdapter
import ru.dbotthepony.kstarbound.json.builder.EnumAdapter
import ru.dbotthepony.kstarbound.json.builder.BuilderAdapter
import ru.dbotthepony.kstarbound.json.builder.FactoryAdapter
import ru.dbotthepony.kstarbound.json.builder.JsonImplementationTypeFactory
import ru.dbotthepony.kstarbound.json.factory.ArrayListAdapterFactory
import ru.dbotthepony.kstarbound.json.factory.ImmutableCollectionAdapterFactory
import ru.dbotthepony.kstarbound.json.factory.PairAdapterFactory
import ru.dbotthepony.kstarbound.math.*
import ru.dbotthepony.kstarbound.util.ItemStack
import ru.dbotthepony.kstarbound.util.SBPattern
import ru.dbotthepony.kstarbound.util.HashTableInterner
import ru.dbotthepony.kstarbound.util.traverseJsonPath
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.*
import java.lang.ref.Cleaner
import java.text.DateFormat
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executors
import java.util.concurrent.ForkJoinPool
import java.util.concurrent.Future
import java.util.concurrent.locks.LockSupport
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
object Starbound : ISBFileLocator {
const val ENGINE_VERSION = "0.0.1"
const val NATIVE_PROTOCOL_VERSION = 748
const val LEGACY_PROTOCOL_VERSION = 747
const val TICK_TIME_ADVANCE = 1.0 / 60.0
const val TICK_TIME_ADVANCE_NANOS = (TICK_TIME_ADVANCE * 1_000_000_000L).toLong()
// compile flags. uuuugh
const val DEDUP_CELL_STATES = true
const val USE_CAFFEINE_INTERNER = false
fun <E : Any> interner(): Interner<E> {
return if (USE_CAFFEINE_INTERNER) Interner.newWeakInterner() else HashTableInterner()
}
fun <E : Any> interner(bits: Int): Interner<E> {
return if (USE_CAFFEINE_INTERNER) Interner.newWeakInterner() else HashTableInterner(bits)
}
val thread = Thread(::universeThread, "Starbound Universe")
val mailbox = MailboxExecutorService(thread)
val mailboxBootstrapped = MailboxExecutorService(thread)
val mailboxInitialized = MailboxExecutorService(thread)
init {
thread.isDaemon = true
thread.start()
}
val CLEANER: Cleaner = Cleaner.create {
val t = Thread(it, "Starbound Global Cleaner")
t.isDaemon = true
t.priority = 2
t
}
// currently Caffeine one saves only 4 megabytes of RAM on pretty big modpack
// Hrm.
// val strings: Interner<String> = Interner.newWeakInterner()
// val strings: Interner<String> = Interner { it }
val STRINGS: Interner<String> = interner(5)
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)
// 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(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(Poly.Companion)
registerTypeAdapterFactory(Registries.tiles.adapter())
registerTypeAdapterFactory(Registries.tileModifiers.adapter())
registerTypeAdapterFactory(Registries.liquid.adapter())
registerTypeAdapterFactory(Registries.items.adapter())
registerTypeAdapterFactory(Registries.species.adapter())
registerTypeAdapterFactory(Registries.statusEffects.adapter())
registerTypeAdapterFactory(Registries.particles.adapter())
registerTypeAdapterFactory(Registries.questTemplates.adapter())
registerTypeAdapterFactory(Registries.techs.adapter())
registerTypeAdapterFactory(Registries.jsonFunctions.adapter())
registerTypeAdapterFactory(Registries.json2Functions.adapter())
registerTypeAdapterFactory(Registries.npcTypes.adapter())
registerTypeAdapterFactory(Registries.projectiles.adapter())
registerTypeAdapterFactory(Registries.tenants.adapter())
registerTypeAdapterFactory(Registries.treasurePools.adapter())
registerTypeAdapterFactory(Registries.monsterSkills.adapter())
registerTypeAdapterFactory(Registries.monsterTypes.adapter())
registerTypeAdapterFactory(Registries.worldObjects.adapter())
registerTypeAdapter(LongRangeAdapter)
create()
}
data class ItemConfig(
val directory: String?,
val parameters: JsonObject,
val config: JsonObject
)
fun itemConfig(name: String, parameters: JsonObject, level: Double? = null, seed: Long? = null): ItemConfig {
val obj = Registries.items[name] ?: throw NoSuchElementException("No such item $name")
val directory = obj.file?.computeDirectory()
val parameters = parameters.deepCopy()
TODO()
}
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
var bootstrapping = false
private set
var bootstrapped = false
private set
var loadingProgress = 0.0
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 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)
}
private fun doBootstrap() {
if (!bootstrapped && !bootstrapping) {
bootstrapping = true
} else {
return
}
for (path in archivePaths) {
LOGGER.info("Reading PAK archive $path")
addPak(StarboundPak(path))
}
LOGGER.info("Finished reading PAK archives")
bootstrapped = true
bootstrapping = false
checkMailbox()
}
private fun doInitialize(parallel: Boolean) {
if (!initializing && !initialized) {
initializing = true
} else {
return
}
doBootstrap()
val ext2files = fileSystems.parallelStream()
.flatMap { it.explore() }
.filter { it.isFile }
.collect(object :
Collector<IStarboundFile, Object2ObjectOpenHashMap<String, ArrayList<IStarboundFile>>, Map<String, List<IStarboundFile>>>
{
override fun supplier(): Supplier<Object2ObjectOpenHashMap<String, ArrayList<IStarboundFile>>> {
return Supplier { Object2ObjectOpenHashMap() }
}
override fun accumulator(): BiConsumer<Object2ObjectOpenHashMap<String, ArrayList<IStarboundFile>>, IStarboundFile> {
return BiConsumer { t, u ->
t.computeIfAbsent(u.name.substringAfterLast('.'), Object2ObjectFunction { ArrayList() }).add(u)
}
}
override fun combiner(): BinaryOperator<Object2ObjectOpenHashMap<String, ArrayList<IStarboundFile>>> {
return BinaryOperator { t, u ->
for ((k, v) in u)
t.computeIfAbsent(k, Object2ObjectFunction { ArrayList() }).addAll(v)
t
}
}
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)
}
})
checkMailbox()
val tasks = ArrayList<Future<*>>()
val pool = if (parallel) ForkJoinPool.commonPool() else Executors.newFixedThreadPool(1)
tasks.addAll(Registries.load(ext2files, pool))
tasks.addAll(RecipeRegistry.load(ext2files, pool))
tasks.addAll(GlobalDefaults.load(pool))
val total = tasks.size.toDouble()
while (tasks.isNotEmpty()) {
tasks.removeIf { it.isDone }
checkMailbox()
loadingProgress = (total - tasks.size) / total
LockSupport.parkNanos(5_000_000L)
}
if (!parallel)
pool.shutdown()
Registries.finishLoad()
RecipeRegistry.finishLoad()
Registries.validate()
initializing = false
initialized = true
}
fun initializeGame(parallel: Boolean = true) {
mailbox.submit { doInitialize(parallel) }
}
fun bootstrapGame() {
mailbox.submit { doBootstrap() }
}
private fun checkMailbox() {
mailbox.executeQueuedTasks()
if (bootstrapped)
mailboxBootstrapped.executeQueuedTasks()
if (initialized)
mailboxInitialized.executeQueuedTasks()
}
private var fontPath: File? = null
fun loadFont(): CompletableFuture<File> {
val fontPath = fontPath
if (fontPath != null)
return CompletableFuture.completedFuture(fontPath)
return CompletableFuture.supplyAsync(Supplier {
val fontPath = Starbound.fontPath
if (fontPath != null)
return@Supplier fontPath
val file = locate("/hobo.ttf")
if (!file.exists)
throw FileNotFoundException("Unable to locate font file /hobo.ttf")
else if (!file.isFile)
throw FileNotFoundException("/hobo.ttf is not a file!")
else {
val tempPath = File(System.getProperty("java.io.tmpdir"), "sb-hobo.ttf")
tempPath.writeBytes(file.read().array())
Starbound.fontPath = tempPath
return@Supplier tempPath
}
}, mailboxBootstrapped)
}
private fun universeThread() {
while (true) {
checkMailbox()
LockSupport.park()
}
}
}