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

939 lines
29 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.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.util.JsonArrayCollector
import ru.dbotthepony.kstarbound.io.*
import ru.dbotthepony.kstarbound.io.json.AABBTypeAdapter
import ru.dbotthepony.kstarbound.io.json.AABBiTypeAdapter
import ru.dbotthepony.kstarbound.io.json.ColorTypeAdapter
import ru.dbotthepony.kstarbound.io.json.EitherTypeAdapter
import ru.dbotthepony.kstarbound.io.json.FastutilTypeAdapterFactory
import ru.dbotthepony.kstarbound.io.json.InternedJsonElementAdapter
import ru.dbotthepony.kstarbound.io.json.InternedStringAdapter
import ru.dbotthepony.kstarbound.io.json.KOptionalTypeAdapter
import ru.dbotthepony.kstarbound.io.json.LongRangeAdapter
import ru.dbotthepony.kstarbound.io.json.NothingAdapter
import ru.dbotthepony.kstarbound.io.json.OneOfTypeAdapter
import ru.dbotthepony.kstarbound.io.json.Vector2dTypeAdapter
import ru.dbotthepony.kstarbound.io.json.Vector2fTypeAdapter
import ru.dbotthepony.kstarbound.io.json.Vector2iTypeAdapter
import ru.dbotthepony.kstarbound.io.json.Vector4dTypeAdapter
import ru.dbotthepony.kstarbound.io.json.Vector4iTypeAdapter
import ru.dbotthepony.kstarbound.io.json.builder.EnumAdapter
import ru.dbotthepony.kstarbound.io.json.builder.BuilderAdapter
import ru.dbotthepony.kstarbound.io.json.builder.FactoryAdapter
import ru.dbotthepony.kstarbound.io.json.builder.JsonImplementationTypeFactory
import ru.dbotthepony.kstarbound.io.json.factory.ArrayListAdapterFactory
import ru.dbotthepony.kstarbound.io.json.factory.ImmutableCollectionAdapterFactory
import ru.dbotthepony.kstarbound.io.json.factory.PairAdapterFactory
import ru.dbotthepony.kstarbound.lua.LuaState
import ru.dbotthepony.kstarbound.lua.loadInternalScript
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.WriteOnce
import ru.dbotthepony.kstarbound.util.filterNotNull
import ru.dbotthepony.kstarbound.util.set
import ru.dbotthepony.kstarbound.util.traverseJsonPath
import java.io.*
import java.lang.ref.Cleaner
import java.text.DateFormat
import java.util.concurrent.ForkJoinPool
import java.util.concurrent.ForkJoinTask
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 CLEANER: Cleaner = Cleaner.create {
val t = Thread(it, "Starbound Global Cleaner Thread")
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 polyfill by lazy { loadInternalScript("polyfill") }
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(AABBTypeAdapter)
registerTypeAdapter(AABBiTypeAdapter)
registerTypeAdapter(Vector2dTypeAdapter)
registerTypeAdapter(Vector2fTypeAdapter)
registerTypeAdapter(Vector2iTypeAdapter)
registerTypeAdapter(Vector4iTypeAdapter)
registerTypeAdapter(Vector4dTypeAdapter)
registerTypeAdapter(PolyTypeAdapter)
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(with(RegistryReferenceFactory()) {
add(Registries.tiles)
add(Registries.tileModifiers)
add(Registries.liquid)
add(Registries.items)
add(Registries.species)
add(Registries.statusEffects)
add(Registries.particles)
add(Registries.questTemplates)
add(Registries.techs)
add(Registries.jsonFunctions)
add(Registries.json2Functions)
add(Registries.npcTypes)
add(Registries.projectiles)
add(Registries.tenants)
add(Registries.treasurePools)
add(Registries.monsterSkills)
add(Registries.monsterTypes)
add(Registries.worldObjects)
})
registerTypeAdapter(LongRangeAdapter)
create()
}
init {
val f = NonExistingFile("/metamaterials.config")
for (material in BuiltinMetaMaterials.MATERIALS) {
Registries.tiles.add(material, JsonNull.INSTANCE, f)
}
}
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
@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 fun luaRequire(it: LuaState, args: LuaState.ArgStack) {
val name = args.getString()
val file = locate(name)
if (!file.exists) {
throw FileNotFoundException("File $name does not exist")
}
if (!file.isFile) {
throw FileNotFoundException("File $name is a directory")
}
val read = file.readToString()
it.load(read, chunkName = "@" + file.computeFullPath())
it.call()
}
// wrapping for Lua refs
private fun getImage(path: String) = Image.get(path)
/**
* **ПРЕДУПРЕЖДЕНИЕ:**
* [time] не должен иметь ссылок (прямые или непрямые) на [state], иначе произойдёт утечка памяти!
*/
fun pushLuaAPI(state: LuaState, time: ITimeSource = JVMTimeSource.INSTANCE) {
state.pushWeak(this) { args ->
luaRequire(args.lua, args)
0
}
state.storeGlobal("require")
state.pushTable()
state.storeGlobal("root")
state.loadGlobal("root")
state.setTableFunction("assetJson", this) {args ->
args.lua.push(loadJsonAsset(args.getString()))
1
}
state.setTableFunction("makeCurrentVersionedJson", this) {args ->
TODO("makeCurrentVersionedJson")
}
state.setTableFunction("loadVersionedJson", this) {args ->
TODO("loadVersionedJson")
}
state.setTableFunction("evalFunction", this) {args ->
val name = args.getString()
val fn = Registries.jsonFunctions[name] ?: throw NoSuchElementException("No such function $name")
args.push(fn.value.evaluate(args.getDouble()))
1
}
state.setTableFunction("evalFunction2", this) {args ->
val name = args.getString()
val fn = Registries.json2Functions[name] ?: throw NoSuchElementException("No such 2function $name")
args.push(fn.value.evaluate(args.getDouble(), args.getDouble()))
1
}
state.setTableFunction("imageSize", this) {args ->
val name = args.getString()
args.lua.push(getImage(name)?.size ?: throw FileNotFoundException("No such file $name"))
1
}
state.setTableFunction("imageSpaces", this) { args ->
// List<Vec2I> root.imageSpaces(String imagePath, Vec2F worldPosition, float spaceScan, bool flip)
val name = args.getString()
val values = getImage(name)?.worldSpaces(args.getVector2i(), args.getDouble(), args.getBool()) ?: throw FileNotFoundException("No such file $name")
args.lua.pushTable(arraySize = values.size)
val table = args.lua.stackTop
for ((i, value) in values.withIndex()) {
args.lua.push(i + 1)
args.lua.push(value)
args.lua.setTableValue(table)
}
1
}
state.setTableFunction("nonEmptyRegion", this) { args ->
val name = args.getString()
args.lua.push(getImage(name)?.nonEmptyRegion ?: throw FileNotFoundException("No such file $name"))
1
}
state.setTableFunction("npcConfig", this) { args ->
// Json root.npcConfig(String npcType)
val name = args.getString()
args.push(Registries.npcTypes[name] ?: throw NoSuchElementException("No such NPC type $name"))
1
}
state.setTableFunction("npcVariant", this) { args ->
// Json root.npcVariant(String species, String npcType, float level, [unsigned seed], [Json parameters])
TODO("npcVariant")
}
state.setTableFunction("projectileGravityMultiplier", this) { args ->
// float root.projectileGravityMultiplier(String projectileName)
TODO("projectileGravityMultiplier")
}
state.setTableFunction("projectileConfig", this) { args ->
// Json root.projectileConfig(String projectileName)
val name = args.getString()
args.lua.push(Registries.projectiles[name]?.json ?: throw kotlin.NoSuchElementException("No such Projectile type $name"))
1
}
state.setTableFunction("recipesForItem", this) { args ->
args.lua.push(JsonArray().also { a ->
RecipeRegistry.output2recipes[args.getString()]?.stream()?.map { it.toJson() }?.forEach {
a.add(it)
}
})
1
}
state.setTableFunction("itemType", this) { args ->
val name = args.getString()
args.lua.push(Registries.items[name]?.value?.itemType ?: throw NoSuchElementException("No such item $name"))
1
}
state.setTableFunction("itemTags", this) { args ->
val name = args.getString()
args.lua.pushStrings(Registries.items[name]?.value?.itemTags ?: throw NoSuchElementException("No such item $name"))
1
}
state.setTableFunction("itemHasTag", this) { args ->
val name = args.getString()
val tag = args.getString()
args.push((Registries.items[name]?.value?.itemTags ?: throw NoSuchElementException("No such item $name")).contains(tag))
1
}
// TODO: генерация
state.setTableFunction("itemConfig", this) { args ->
// Json root.itemConfig(ItemDescriptor descriptor, [float level], [unsigned seed])
val item = item(args.getValue())
val level = if (args.hasSomethingAt()) args.getDouble() else null
val seed = if (args.hasSomethingAt()) args.getLong() else null
if (item.isEmpty) {
args.push()
} else {
args.push(JsonObject().also {
it["directory"] = item.item!!.file.computeDirectory()
it["config"] = item.item!!.json
it["parameters"] = item.parameters
})
}
1
}
// TODO: генерация
state.setTableFunction("createItem", this) { args ->
// ItemDescriptor root.createItem(ItemDescriptor descriptor, [float level], [unsigned seed])
val item = item(args.getValue())
val level = if (args.hasSomethingAt()) args.getDouble() else null
val seed = if (args.hasSomethingAt()) args.getLong() else null
if (item.isEmpty) {
args.push()
return@setTableFunction 1
}
if (item.maxStackSize < item.size) {
item.size = item.maxStackSize
}
args.push(gson.toJsonTree(item))
1
}
state.setTableFunction("tenantConfig", this) { args ->
// Json root.tenantConfig(String tenantName)
val name = args.getString()
Registries.tenants[name]?.push(args) ?: throw NoSuchElementException("No such tenant $name")
1
}
state.setTableFunction("getMatchingTenants", this) { args ->
// Json root.tenantConfig(String tenantName)
val tags = args.getTable()
val actualTags = Object2IntOpenHashMap<String>()
for ((k, v) in tags.entrySet()) {
if (v is JsonPrimitive && v.isNumber) {
actualTags[k] = v.asInt
}
}
args.push(Registries.tenants.values
.stream()
.filter { it.value.test(actualTags) }
.sorted { a, b -> b.value.compareTo(a.value) }
.map { it.toJson() }
.collect(JsonArrayCollector))
1
}
state.setTableFunction("liquidStatusEffects", this) { args ->
val liquid: LiquidDefinition
if (args.isStringAt()) {
val name = args.getString()
liquid = Registries.liquid[name]?.value ?: throw NoSuchElementException("No such liquid with name $name")
} else {
val id = args.getInt()
liquid = Registries.liquid[id]?.value ?: throw NoSuchElementException("No such liquid with ID $id")
}
args.lua.pushStrings(liquid.statusEffects.stream().map { it.value?.value?.name }.filterNotNull().toList())
1
}
state.setTableFunction("generateName", this) { args ->
val assetName = args.getString()
val seed = if (args.hasSomethingAt()) args.getLong() else time.nanos
val names = loadJsonAsset(assetName) ?: throw NoSuchElementException("No such JSON asset $assetName")
if (names !is JsonArray) {
var possibleName: String? = null
if (names is JsonObject) {
for ((k, v) in names.entrySet()) {
if (v is JsonArray && !v.isEmpty) {
if (possibleName == null || (names[possibleName] as JsonArray).size() < v.size())
possibleName = k
}
}
}
if (possibleName != null) {
if (assetName.contains(':')) {
throw IllegalArgumentException("JSON asset $assetName is not an array, did you mean $assetName.$possibleName?")
} else {
throw IllegalArgumentException("JSON asset $assetName is not an array, did you mean $assetName:$possibleName?")
}
} else {
throw IllegalArgumentException("JSON asset $assetName is not an array")
}
}
if (names.isEmpty) {
throw IllegalStateException("JSON array $assetName is empty")
}
args.push(names[Random(seed).nextInt(0, names.size())])
1
}
state.setTableFunction("questConfig", this) { args ->
val name = args.getString()
args.push(Registries.questTemplates[name] ?: throw NoSuchElementException("No such quest template $name"))
1
}
state.setTableFunction("npcPortrait", this) { args ->
// JsonArray root.npcPortrait(String portraitMode, String species, String npcType, float level, [unsigned seed], [Json parameters])
// Generates an NPC with the specified type, level, seed and parameters and returns a portrait in the given portraitMode as a list of drawables.
TODO("npcPortrait")
}
state.setTableFunction("monsterPortrait", this) { args ->
// JsonArray root.monsterPortrait(String typeName, [Json parameters])
// Generates a monster of the given type with the given parameters and returns its portrait as a list of drawables.
TODO("monsterPortrait")
}
state.setTableFunction("isTreasurePool", this) { args ->
args.push(args.getString() in Registries.treasurePools)
1
}
state.setTableFunction("createTreasure", this) { args ->
val name = args.getString()
val level = args.getDouble()
val rand = if (args.hasSomethingAt()) java.util.Random(args.getLong()) else java.util.Random()
args.push(Registries.treasurePools[name]?.value?.evaluate(rand, level)?.stream()?.map { it.toJson() }?.filterNotNull()?.collect(JsonArrayCollector) ?: throw NoSuchElementException("No such treasure pool $name"))
1
}
state.setTableFunction("materialMiningSound", this) { args ->
val name = args.getString()
val mod = if (args.hasSomethingAt()) args.getString() else null
val mat = Registries.tiles[name] ?: throw NoSuchElementException("No such material $name")
if (mod == null) {
args.push(mat.value.miningSounds.firstOrNull())
} else {
val getMod = Registries.tileModifiers[mod] ?: throw NoSuchElementException("No such material modifier $mod")
args.push(getMod.value.miningSounds.firstOrNull() ?: mat.value.miningSounds.firstOrNull())
}
1
}
state.setTableFunction("materialFootstepSound", this) { args ->
val name = args.getString()
val mod = if (args.hasSomethingAt()) args.getString() else null
val mat = Registries.tiles[name] ?: throw NoSuchElementException("No such material $name")
if (mod == null) {
args.push(mat.value.footstepSound)
} else {
val getMod = Registries.tileModifiers[mod] ?: throw NoSuchElementException("No such material modifier $mod")
args.push(getMod.value.footstepSound ?: mat.value.footstepSound)
}
1
}
state.setTableFunction("materialHealth", this) { args ->
val name = args.getString()
val mod = if (args.hasSomethingAt()) args.getString() else null
val mat = Registries.tiles[name] ?: throw NoSuchElementException("No such material $name")
if (mod == null) {
args.push(mat.value.health)
} else {
val getMod = Registries.tileModifiers[mod] ?: throw NoSuchElementException("No such material modifier $mod")
args.push(getMod.value.health + mat.value.health)
}
1
}
state.setTableFunction("materialConfig", this) { args ->
val name = args.getString()
args.pushFull(Registries.tiles[name])
1
}
state.setTableFunction("modConfig", this) { args ->
val name = args.getString()
args.pushFull(Registries.tileModifiers[name])
1
}
state.setTableFunction("liquidConfig", this) { args ->
if (args.isNumberAt()) {
val id = args.getLong().toInt()
args.pushFull(Registries.liquid[id])
} else {
val name = args.getString()
args.pushFull(Registries.liquid[name])
}
1
}
state.setTableFunction("liquidName", this) { args ->
val id = args.getLong().toInt()
args.push(Registries.liquid[id]?.value?.name ?: throw NoSuchElementException("No such liquid with ID $id"))
1
}
state.setTableFunction("liquidId", this) { args ->
val name = args.getString()
args.push(Registries.liquid[name]?.value?.name ?: throw NoSuchElementException("No such liquid $name"))
1
}
state.setTableFunction("monsterSkillParameter", this) { args ->
val name = args.getString()
val param = args.getString()
// parity: если скила не существует, то оригинальный движок просто возвращает nil
args.push(Registries.monsterSkills[name]?.value?.config?.get(param))
1
}
state.setTableFunction("monsterParameters", this) { args ->
val name = args.getString()
val monster = Registries.monsterTypes[name] ?: throw NoSuchElementException("No such monster type $name")
args.push(monster.traverseJsonPath("baseParameters"))
1
}
state.setTableFunction("monsterMovementSettings", this) { args ->
val name = args.getString()
val monster = Registries.monsterTypes[name] ?: throw NoSuchElementException("No such monster type $name")
args.push(gson.toJsonTree(monster.value.baseParameters.movementSettings))
1
}
state.setTableFunction("createBiome", this) { args ->
TODO("createBiome")
}
state.setTableFunction("hasTech", this) { args ->
args.push(args.getString() in Registries.techs)
1
}
state.setTableFunction("techType", this) { args ->
val name = args.getString()
val tech = Registries.techs[name] ?: throw NoSuchElementException("No such tech $name")
args.push(tech.value.type)
1
}
state.setTableFunction("techConfig", this) { args ->
val name = args.getString()
val tech = Registries.techs[name] ?: throw NoSuchElementException("No such tech $name")
tech.push(args)
1
}
state.setTableFunction("treeStemDirectory", this) { args ->
// String root.treeStemDirectory(String stemName)
TODO("treeStemDirectory")
}
state.setTableFunction("treeFoliageDirectory", this) { args ->
// String root.treeFoliageDirectory(String foliageName)
TODO("treeFoliageDirectory")
}
state.setTableFunction("collection", this) { args ->
// Collection root.collection(String collectionName)
TODO("collection")
}
state.setTableFunction("collectables", this) { args ->
// List<Collectable> root.collectables(String collectionName)
TODO("collectables")
}
state.setTableFunction("elementalResistance", this) { args ->
// String root.elementalResistance(String elementalType)
TODO("elementalResistance")
}
state.setTableFunction("dungeonMetadata", this) { args ->
// Json root.dungeonMetadata(String dungeonName)
TODO("dungeonMetadata")
}
state.setTableFunction("behavior", this) { args ->
// BehaviorState root.behavior(`LuaTable` context, Json config, `JsonObject` parameters)
TODO("behavior")
}
state.pop()
state.load(polyfill, "@starbound.jar!/scripts/polyfill.lua")
state.call()
}
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 val initCallbacks = ArrayList<() -> Unit>()
var playerDefinition: PlayerDefinition by WriteOnce()
private set
private fun doInitialize(log: ILoadingLog, parallel: Boolean) {
var time = System.currentTimeMillis()
if (archivePaths.isNotEmpty()) {
log.line("Searching for pak archives...".also(LOGGER::info))
for (path in archivePaths) {
val line = log.line("Reading index of ${path}...".also(LOGGER::info))
addPak(StarboundPak(path) { _, status ->
line.text = ("${path.parent}/${path.name}: $status")
})
}
}
log.line("Finished reading pak archives in ${System.currentTimeMillis() - time}ms".also(LOGGER::info))
time = System.currentTimeMillis()
log.line("Building file index...".also(LOGGER::info))
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)
}
})
log.line("Finished building file index in ${System.currentTimeMillis() - time}ms".also(LOGGER::info))
val tasks = ArrayList<ForkJoinTask<*>>()
val pool = if (parallel) ForkJoinPool.commonPool() else ForkJoinPool(1)
tasks.addAll(Registries.load(log, ext2files, pool))
tasks.addAll(RecipeRegistry.load(log, ext2files, pool))
tasks.addAll(GlobalDefaults.load(log, pool))
AssetPathStack.block("/") {
//playerDefinition = gson.fromJson(locate("/player.config").reader(), PlayerDefinition::class.java)
}
tasks.forEach { it.join() }
if (!parallel)
pool.shutdown()
initializing = false
initialized = true
log.line("Finished loading in ${System.currentTimeMillis() - time}ms")
}
fun initializeGame(log: ILoadingLog, parallel: Boolean = true) {
if (initializing) {
throw IllegalStateException("Already initializing!")
}
if (initialized) {
throw IllegalStateException("Already initialized!")
}
initializing = true
Thread({ doInitialize(log, parallel) }, "Asset Loader").also {
it.isDaemon = true
it.start()
}
}
fun onInitialize(callback: () -> Unit) {
if (initialized) {
callback()
} else {
initCallbacks.add(callback)
}
}
fun pollCallbacks() {
if (initialized && initCallbacks.isNotEmpty()) {
for (callback in initCallbacks) {
callback()
}
initCallbacks.clear()
}
}
}