530 lines
16 KiB
Kotlin
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()
|
|
}
|
|
}
|
|
}
|
|
|