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

530 lines
16 KiB
Kotlin

package ru.dbotthepony.kstarbound
import com.github.benmanes.caffeine.cache.Interner
import com.google.gson.*
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap
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.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.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.defs.player.BlueprintLearnList
import ru.dbotthepony.kstarbound.defs.player.PlayerDefinition
import ru.dbotthepony.kstarbound.util.JsonArrayCollector
import ru.dbotthepony.kstarbound.io.*
import ru.dbotthepony.kstarbound.json.AABBTypeAdapter
import ru.dbotthepony.kstarbound.json.AABBiTypeAdapter
import ru.dbotthepony.kstarbound.json.ColorTypeAdapter
import ru.dbotthepony.kstarbound.json.EitherTypeAdapter
import ru.dbotthepony.kstarbound.json.FastutilTypeAdapterFactory
import ru.dbotthepony.kstarbound.json.InternedJsonElementAdapter
import ru.dbotthepony.kstarbound.json.InternedStringAdapter
import ru.dbotthepony.kstarbound.json.KOptionalTypeAdapter
import ru.dbotthepony.kstarbound.json.LongRangeAdapter
import ru.dbotthepony.kstarbound.json.NothingAdapter
import ru.dbotthepony.kstarbound.json.OneOfTypeAdapter
import ru.dbotthepony.kstarbound.json.Vector2dTypeAdapter
import ru.dbotthepony.kstarbound.json.Vector2fTypeAdapter
import ru.dbotthepony.kstarbound.json.Vector2iTypeAdapter
import ru.dbotthepony.kstarbound.json.Vector4dTypeAdapter
import ru.dbotthepony.kstarbound.json.Vector4iTypeAdapter
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.ITimeSource
import ru.dbotthepony.kstarbound.util.ItemStack
import ru.dbotthepony.kstarbound.util.JVMTimeSource
import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.util.SBPattern
import ru.dbotthepony.kstarbound.util.HashTableInterner
import ru.dbotthepony.kstarbound.util.MailboxExecutorService
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.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.ForkJoinTask
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
import kotlin.random.Random
object Starbound : ISBFileLocator {
const val TICK_TIME_ADVANCE = 0.01666666666666664
const val TICK_TIME_ADVANCE_NANOS = 16_666_666L
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> = HashTableInterner(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)
// OneOf<>
registerTypeAdapterFactory(OneOfTypeAdapter)
// 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(ru.dbotthepony.kstarbound.json.AABBTypeAdapter)
registerTypeAdapter(ru.dbotthepony.kstarbound.json.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()
}
}
}