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.kstarbound.math.AABBTypeAdapter import ru.dbotthepony.kommons.gson.EitherTypeAdapter import ru.dbotthepony.kommons.gson.KOptionalTypeAdapter import ru.dbotthepony.kommons.gson.NothingAdapter import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kstarbound.math.vector.Vector2i 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.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.math.AABBiTypeAdapter import ru.dbotthepony.kstarbound.math.Vector2dTypeAdapter import ru.dbotthepony.kstarbound.math.Vector2fTypeAdapter import ru.dbotthepony.kstarbound.math.Vector2iTypeAdapter import ru.dbotthepony.kstarbound.math.Vector3dTypeAdapter import ru.dbotthepony.kstarbound.math.Vector3fTypeAdapter import ru.dbotthepony.kstarbound.math.Vector3iTypeAdapter import ru.dbotthepony.kstarbound.math.Vector4dTypeAdapter import ru.dbotthepony.kstarbound.math.Vector4fTypeAdapter import ru.dbotthepony.kstarbound.math.Vector4iTypeAdapter 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.util.random.nextRange import ru.dbotthepony.kstarbound.util.random.random 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.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 java.util.random.RandomGenerator 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 interner(): Interner { if (!USE_INTERNER) return Interner { it } return if (USE_CAFFEINE_INTERNER) Interner.newWeakInterner() else HashTableInterner() } fun interner(bits: Int): Interner { 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() // this is required for Caffeine since it ignores scheduler // (and suffers noticeable throughput penalty) in rescheduleCleanUpIfIncomplete() // if executor is specified as ForkJoinPool.commonPool() @JvmField val SCREENED_EXECUTOR: ExecutorService = object : ExecutorService by EXECUTOR {} @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 = Interner.newWeakInterner() // val strings: Interner = Interner { it } @JvmField val STRINGS: Interner = 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 legacyJson(block: () -> T): T { try { IS_LEGACY_JSON = true return block.invoke() } finally { IS_LEGACY_JSON = false } } fun storeJson(block: () -> T): T { try { IS_STORE_JSON = true return block.invoke() } finally { IS_STORE_JSON = false } } fun 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() 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>() 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() private val toLoadPaks = ObjectArraySet() private val toLoadPaths = ObjectArraySet() 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>() 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 { @Suppress("name_shadowing") var path = path if (path[0] == '/') { path = path.substring(1) } val files = ArrayList() 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>() val patchTree = HashMap>() // 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>() 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 { 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 } } } private fun processNameRules(rules: JsonArray, random: RandomGenerator): String { if (rules.isEmpty) return "" val meta: JsonObject val result = StringBuilder() var mode = "alts" var index = 0 var uppercase = false if (rules[0] is JsonObject) { meta = rules[0] as JsonObject mode = meta.get("mode", mode) uppercase = meta.get("titleCase", false) index++ } else { meta = JsonObject() } when (mode) { "serie" -> { while (index < rules.size()) { val entry = rules[index++] if (entry is JsonArray) { result.append(processNameRules(entry, random)) } else { result.append(entry.asString) } } } "alts" -> { val i = if (rules.size() == 1) throw RuntimeException("Николай не протоген") else random.nextInt(index, rules.size()) val entry = rules[i] if (entry is JsonArray) { result.append(processNameRules(entry, random)) } else { result.append(entry.asString) } } "markov" -> { val source = Registries.markovGenerators.getOrThrow(meta.get("source").asString).value val lengthRange = gson.fromJson(meta.get("targetLength"), Vector2i::class.java) val targetLength = random.nextRange(lengthRange) result.append(source.generate(random, targetLength, lengthRange.y)) } else -> throw IllegalArgumentException("Unknown name rule mode: $mode") } if (uppercase) { return result.toString().uppercase() } return result.toString() } fun generateName(asset: String, random: RandomGenerator): String { val load = loadJsonAsset(asset) as? JsonArray ?: return "missingasset" var tries = 500 var result = "" while (tries-- > 0 && (result.isEmpty() || result.lowercase() in Globals.profanityFilter)) result = processNameRules(load, random) return result } fun generateName(asset: String, seed: Long) = generateName(asset, random(seed)) fun generateName(asset: String) = generateName(asset, System.nanoTime()) }