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

644 lines
19 KiB
Kotlin

package ru.dbotthepony.kstarbound
import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.Interner
import com.github.benmanes.caffeine.cache.Scheduler
import com.google.gson.*
import com.google.gson.stream.JsonReader
import it.unimi.dsi.fastutil.objects.ObjectArraySet
import kotlinx.coroutines.asCoroutineDispatcher
import org.apache.logging.log4j.LogManager
import org.classdump.luna.compiler.CompilerChunkLoader
import org.classdump.luna.compiler.CompilerSettings
import org.classdump.luna.load.ChunkFactory
import ru.dbotthepony.kommons.gson.AABBTypeAdapter
import ru.dbotthepony.kommons.gson.AABBiTypeAdapter
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.Vector3dTypeAdapter
import ru.dbotthepony.kommons.gson.Vector3fTypeAdapter
import ru.dbotthepony.kommons.gson.Vector3iTypeAdapter
import ru.dbotthepony.kommons.gson.Vector4dTypeAdapter
import ru.dbotthepony.kommons.gson.Vector4fTypeAdapter
import ru.dbotthepony.kommons.gson.Vector4iTypeAdapter
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.collect.WeightedList
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.TreasurePoolDefinition
import ru.dbotthepony.kstarbound.defs.actor.player.BlueprintLearnList
import ru.dbotthepony.kstarbound.defs.animation.Particle
import ru.dbotthepony.kstarbound.defs.quest.QuestParameter
import ru.dbotthepony.kstarbound.defs.world.CelestialParameters
import ru.dbotthepony.kstarbound.defs.world.VisitableWorldParametersType
import ru.dbotthepony.kstarbound.defs.world.BiomePlaceables
import ru.dbotthepony.kstarbound.defs.world.BiomePlacementDistributionType
import ru.dbotthepony.kstarbound.defs.world.BiomePlacementItemType
import ru.dbotthepony.kstarbound.defs.world.WorldLayout
import ru.dbotthepony.kstarbound.world.terrain.TerrainSelectorType
import ru.dbotthepony.kstarbound.io.*
import ru.dbotthepony.kstarbound.item.ItemRegistry
import ru.dbotthepony.kstarbound.json.factory.MapsTypeAdapterFactory
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.JsonImplementationTypeFactory
import ru.dbotthepony.kstarbound.json.factory.CollectionAdapterFactory
import ru.dbotthepony.kstarbound.json.factory.ImmutableCollectionAdapterFactory
import ru.dbotthepony.kstarbound.json.factory.PairAdapterFactory
import ru.dbotthepony.kstarbound.json.factory.RGBAColorTypeAdapter
import ru.dbotthepony.kstarbound.json.factory.SingletonTypeAdapterFactory
import ru.dbotthepony.kstarbound.server.world.UniverseChunk
import ru.dbotthepony.kstarbound.item.RecipeRegistry
import ru.dbotthepony.kstarbound.json.JsonAdapterTypeFactory
import ru.dbotthepony.kstarbound.json.JsonPatch
import ru.dbotthepony.kstarbound.json.JsonPath
import ru.dbotthepony.kstarbound.json.NativeLegacy
import ru.dbotthepony.kstarbound.util.BlockableEventLoop
import ru.dbotthepony.kstarbound.util.ExecutorWithScheduler
import ru.dbotthepony.kstarbound.util.Directives
import ru.dbotthepony.kstarbound.util.SBPattern
import ru.dbotthepony.kstarbound.util.HashTableInterner
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.*
import java.lang.ref.Cleaner
import java.text.DateFormat
import java.time.Duration
import java.util.Collections
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executor
import java.util.concurrent.ExecutorService
import java.util.concurrent.ForkJoinPool
import java.util.concurrent.Future
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.ThreadFactory
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.LockSupport
import kotlin.NoSuchElementException
import kotlin.collections.ArrayList
object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLocator {
const val ENGINE_VERSION = "0.0.1"
const val NATIVE_PROTOCOL_VERSION = 748
const val LEGACY_PROTOCOL_VERSION = 747
const val TIMESTEP = 1.0 / 60.0
const val SYSTEM_WORLD_TIMESTEP = 1.0 / 20.0
const val TIMESTEP_NANOS = (TIMESTEP * 1_000_000_000L).toLong()
const val SYSTEM_WORLD_TIMESTEP_NANOS = (SYSTEM_WORLD_TIMESTEP * 1_000_000_000L).toLong()
// compile flags. uuuugh
const val DEDUP_CELL_STATES = true
const val USE_CAFFEINE_INTERNER = false
const val USE_INTERNER = true
fun <E : Any> interner(): Interner<E> {
if (!USE_INTERNER)
return Interner { it }
return if (USE_CAFFEINE_INTERNER) Interner.newWeakInterner() else HashTableInterner()
}
fun <E : Any> interner(bits: Int): Interner<E> {
if (!USE_INTERNER)
return Interner { it }
return if (USE_CAFFEINE_INTERNER) Interner.newWeakInterner() else HashTableInterner(bits)
}
private val LOGGER = LogManager.getLogger()
override fun schedule(executor: Executor, command: Runnable, delay: Long, unit: TimeUnit): Future<*> {
return schedule(Runnable { executor.execute(command) }, delay, unit)
}
init {
isDaemon = true
start()
}
private val ioPoolCounter = AtomicInteger()
@JvmField
val IO_EXECUTOR: ExecutorService = ThreadPoolExecutor(0, 64, 30L, TimeUnit.SECONDS, LinkedBlockingQueue(), ThreadFactory {
val thread = Thread(it, "Starbound Storage IO ${ioPoolCounter.getAndIncrement()}")
thread.isDaemon = true
thread.priority = Thread.MIN_PRIORITY
thread.uncaughtExceptionHandler = Thread.UncaughtExceptionHandler { t, e ->
LOGGER.error("I/O thread died due to uncaught exception", e)
}
return@ThreadFactory thread
})
@JvmField
val EXECUTOR: ForkJoinPool = ForkJoinPool.commonPool()
@JvmField
val COROUTINE_EXECUTOR = ExecutorWithScheduler(EXECUTOR, this).asCoroutineDispatcher()
@JvmField
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 }
@JvmField
val STRINGS: Interner<String> = interner(5)
// immeasurably lazy and fragile solution, too bad!
// While having four separate Gson instances look like a (much) better solution (and it indeed could have been!),
// we must not forget the fact that 'Starbound' and 'Consistent data format' are opposites,
// and there are cases of where discStore() calls toJson() on children data, despite it having its own discStore() too.
var IS_LEGACY_JSON: Boolean by ThreadLocal.withInitial { false }
private set
var IS_STORE_JSON: Boolean by ThreadLocal.withInitial { false }
private set
fun legacyJson(data: Any): JsonElement {
try {
IS_LEGACY_JSON = true
return gson.toJsonTree(data)
} finally {
IS_LEGACY_JSON = false
}
}
fun storeJson(data: Any): JsonElement {
try {
IS_STORE_JSON = true
return gson.toJsonTree(data)
} finally {
IS_STORE_JSON = false
}
}
fun legacyStoreJson(data: Any): JsonElement {
try {
IS_STORE_JSON = true
IS_LEGACY_JSON = true
return gson.toJsonTree(data)
} finally {
IS_STORE_JSON = false
IS_LEGACY_JSON = false
}
}
fun <T> legacyJson(block: () -> T): T {
try {
IS_LEGACY_JSON = true
return block.invoke()
} finally {
IS_LEGACY_JSON = false
}
}
fun <T> storeJson(block: () -> T): T {
try {
IS_STORE_JSON = true
return block.invoke()
} finally {
IS_STORE_JSON = false
}
}
fun <T> legacyStoreJson(block: () -> T): T {
try {
IS_STORE_JSON = true
IS_LEGACY_JSON = true
return block.invoke()
} finally {
IS_STORE_JSON = false
IS_LEGACY_JSON = false
}
}
private val loader = CompilerChunkLoader.of(CompilerSettings.defaultNoAccountingSettings(), "sb_lua_")
private val scriptCache = ConcurrentHashMap<String, ChunkFactory>()
private fun loadScript0(path: String): ChunkFactory {
val find = locate(path)
if (!find.exists) {
throw NoSuchElementException("Script $path does not exist")
}
val time = System.nanoTime()
val result = loader.compileTextChunk(path, find.readToString())
LOGGER.debug("Compiled {} in {} ms", path, (System.nanoTime() - time) / 1_000_000L)
return result
}
fun loadScript(path: String): ChunkFactory {
return scriptCache.computeIfAbsent(path, ::loadScript0)
}
fun compileScriptChunk(name: String, chunk: String): ChunkFactory {
return loader.compileTextChunk(name, chunk)
}
val ELEMENTS_ADAPTER = InternedJsonElementAdapter(STRINGS)
val gson: Gson = with(GsonBuilder()) {
// serializeNulls()
setDateFormat(DateFormat.LONG)
setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)
setPrettyPrinting()
registerTypeAdapter(InternedStringAdapter(STRINGS))
registerTypeAdapter(ELEMENTS_ADAPTER)
registerTypeAdapter(ELEMENTS_ADAPTER.arrays)
registerTypeAdapter(ELEMENTS_ADAPTER.objects)
registerTypeAdapter(Nothing::class.java, NothingAdapter)
// Обработчик @JsonImplementation
registerTypeAdapterFactory(JsonImplementationTypeFactory)
registerTypeAdapterFactory(JsonAdapterTypeFactory)
// списки, наборы, т.п.
registerTypeAdapterFactory(CollectionAdapterFactory)
// ImmutableList, ImmutableSet, ImmutableMap
registerTypeAdapterFactory(ImmutableCollectionAdapterFactory(STRINGS))
// fastutil collections
registerTypeAdapterFactory(MapsTypeAdapterFactory(STRINGS))
// все enum'ы без особых настроек
registerTypeAdapterFactory(EnumAdapter.Companion)
// @JsonBuilder
registerTypeAdapterFactory(BuilderAdapter.Factory(STRINGS))
// @JsonFactory
registerTypeAdapterFactory(FactoryAdapter.Factory(STRINGS))
// Either<>
registerTypeAdapterFactory(EitherTypeAdapter)
// KOptional<>
registerTypeAdapterFactory(KOptionalTypeAdapter)
registerTypeAdapterFactory(SingletonTypeAdapterFactory)
// Pair<>
registerTypeAdapterFactory(PairAdapterFactory)
registerTypeAdapterFactory(SBPattern.Companion)
registerTypeAdapterFactory(JsonReference.Companion)
registerTypeAdapter(ColorReplacements.Companion)
registerTypeAdapterFactory(BlueprintLearnList.Companion)
registerTypeAdapter(RGBAColorTypeAdapter)
registerTypeAdapterFactory(NativeLegacy.Companion)
// математические классы
registerTypeAdapter(AABBTypeAdapter.nullSafe())
registerTypeAdapter(AABBiTypeAdapter.nullSafe())
registerTypeAdapter(Vector2dTypeAdapter.nullSafe())
registerTypeAdapter(Vector2fTypeAdapter.nullSafe())
registerTypeAdapter(Vector2iTypeAdapter.nullSafe())
registerTypeAdapter(Vector3dTypeAdapter.nullSafe())
registerTypeAdapter(Vector3fTypeAdapter.nullSafe())
registerTypeAdapter(Vector3iTypeAdapter.nullSafe())
registerTypeAdapter(Vector4iTypeAdapter.nullSafe())
registerTypeAdapter(Vector4dTypeAdapter.nullSafe())
registerTypeAdapter(Vector4fTypeAdapter.nullSafe())
registerTypeAdapterFactory(AbstractPerlinNoise.Companion)
registerTypeAdapterFactory(WeightedList.Companion)
// Функции
registerTypeAdapter(JsonFunction.CONSTRAINT_ADAPTER)
registerTypeAdapter(JsonFunction.INTERPOLATION_ADAPTER)
registerTypeAdapter(JsonFunction.Companion)
registerTypeAdapter(JsonConfigFunction::Adapter)
registerTypeAdapterFactory(Json2Function.Companion)
// Общее
registerTypeAdapterFactory(ThingDescription.Factory(STRINGS))
registerTypeAdapterFactory(TerrainSelectorType.Companion)
registerTypeAdapter(Directives.Companion)
registerTypeAdapter(EnumAdapter(DamageType::class, default = DamageType.DAMAGE))
registerTypeAdapterFactory(AssetPath.Companion)
registerTypeAdapter(SpriteReference.Companion)
registerTypeAdapterFactory(AssetReference.Companion)
registerTypeAdapterFactory(UniverseChunk.Companion)
registerTypeAdapter(Image.Companion)
registerTypeAdapterFactory(Poly.Companion)
registerTypeAdapter(CelestialParameters::Adapter)
registerTypeAdapter(Particle::Adapter)
registerTypeAdapter(QuestParameter::Adapter)
registerTypeAdapterFactory(BiomePlacementDistributionType.DEFINITION_ADAPTER)
registerTypeAdapterFactory(BiomePlacementItemType.DATA_ADAPTER)
registerTypeAdapterFactory(BiomePlacementItemType.DEFINITION_ADAPTER)
registerTypeAdapterFactory(BiomePlaceables.Item.Companion)
// register companion first, so it has lesser priority than dispatching adapter
registerTypeAdapterFactory(VisitableWorldParametersType.Companion)
registerTypeAdapterFactory(VisitableWorldParametersType.ADAPTER)
registerTypeAdapter(WorldLayout.Companion)
Registries.registerAdapters(this)
registerTypeAdapter(LongRangeAdapter)
create()
}
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
var toLoad = 0
private set
var loaded = 0
private set
private val jsonAssetsCache = Caffeine.newBuilder()
.maximumSize(4096L)
.expireAfterAccess(Duration.ofMinutes(5L))
.scheduler(this)
.executor(EXECUTOR)
.build<String, KOptional<JsonElement>>()
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 json = jsonAssetsCache.get(filename) {
val file = locate(it)
if (!file.isFile)
return@get KOptional()
val findPatches = locateAll("$filename.patch")
KOptional(JsonPatch.apply(ELEMENTS_ADAPTER.read(file.jsonReader()), findPatches))
}.orNull() ?: return null
if (jsonPath == null)
return json
return JsonPath.query(jsonPath).get(json)
}
private val fileSystems = ArrayList<IStarboundFile>()
private val toLoadPaks = ObjectArraySet<File>()
private val toLoadPaths = ObjectArraySet<File>()
var loadingProgressText: String = ""
private set
fun addArchive(path: File) {
toLoadPaks.add(path)
}
fun addPath(path: File) {
toLoadPaths.add(path)
}
fun doBootstrap() {
if (!bootstrapped && !bootstrapping) {
bootstrapping = true
} else {
return
}
val fileSystems = ArrayList<Pair<Long, IStarboundFile>>()
for (path in toLoadPaks) {
LOGGER.info("Reading PAK archive $path")
try {
loadingProgressText = "Indexing $path"
val pak = StarboundPak(path) { _, s -> loadingProgressText = "Indexing $path: $s" }
val priority = pak.metadata.get("priority", 0L)
fileSystems.add(priority to pak.root)
} catch (err: Throwable) {
LOGGER.error("Error reading PAK archive $path. Not a PAK archive?")
}
}
for (path in toLoadPaths) {
val metadata: JsonObject
val metadataPath = File(path, "_metadata")
if (metadataPath.exists() && metadataPath.isFile) {
metadata = gson.fromJson(JsonReader(metadataPath.reader()), JsonObject::class.java)
} else {
metadata = JsonObject()
}
val priority = metadata.get("priority", 0L)
fileSystems.add(priority to PhysicalFile(path))
}
fileSystems.sortByDescending { it.first }
for ((_, fs) in fileSystems) {
this.fileSystems.add(fs)
}
LOGGER.info("Finished reading PAK archives")
bootstrapped = true
bootstrapping = false
}
fun bootstrapGame(): CompletableFuture<*> {
return submit { doBootstrap() }
}
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 locateAll(path: String): List<IStarboundFile> {
@Suppress("name_shadowing")
var path = path
if (path[0] == '/') {
path = path.substring(1)
}
val files = ArrayList<IStarboundFile>()
for (fs in fileSystems.asReversed()) {
val file = fs.locate(path)
if (file.exists) {
files.add(file)
}
}
return files
}
private fun doInitialize() {
if (!initializing && !initialized) {
initializing = true
} else {
return
}
doBootstrap()
loadingProgressText = "Building file tree..."
val fileTree = HashMap<String, HashSet<IStarboundFile>>()
val patchTree = HashMap<String, ArrayList<IStarboundFile>>()
// finding assets, assets originating from top-most priority PAKs are overriding
// same assets from other PAKs
fileSystems.forEach {
it.explore { file ->
if (file.isFile)
fileTree.computeIfAbsent(file.name.substringAfterLast('.')) { HashSet() }.add(file)
}
}
// finding asset patches, patches from bottom-most priority PAKs are applied first
fileSystems.asReversed().forEach {
it.explore { file ->
if (file.isFile && file.name.endsWith(".patch"))
patchTree.computeIfAbsent(file.computeFullPath().substringAfterLast('.')) { ArrayList() }.add(file)
}
}
loadingProgressText = "Dispatching load tasks..."
val tasks = ArrayList<Future<*>>()
tasks.addAll(Registries.load(fileTree, patchTree))
tasks.addAll(RecipeRegistry.load(fileTree, patchTree))
tasks.addAll(Globals.load())
tasks.add(VersionRegistry.load(patchTree))
val total = tasks.size.toDouble()
toLoad = tasks.size
while (tasks.isNotEmpty()) {
tasks.removeIf { it.isDone }
loaded = toLoad - tasks.size
loadingProgress = (total - tasks.size) / total
loadingProgressText = "Loading JSON assets, $loaded / $toLoad"
LockSupport.parkNanos(5_000_000L)
}
Registries.finishLoad()
RecipeRegistry.finishLoad()
ItemRegistry.finishLoad()
Registries.validate()
initializing = false
initialized = true
}
fun initializeGame(): CompletableFuture<*> {
return submit { doInitialize() }
}
private var fontPath: File? = null
fun loadFont(): CompletableFuture<File> {
val fontPath = fontPath
if (fontPath != null)
return CompletableFuture.completedFuture(fontPath)
return supplyAsync {
val fontPath = Starbound.fontPath
if (fontPath != null)
return@supplyAsync 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@supplyAsync tempPath
}
}
}
}