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

1280 lines
40 KiB
Kotlin

package ru.dbotthepony.kstarbound
import com.github.benmanes.caffeine.cache.Cache
import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.Interner
import com.google.gson.*
import com.google.gson.internal.bind.JsonTreeReader
import it.unimi.dsi.fastutil.Hash
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenCustomHashMap
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import org.apache.logging.log4j.LogManager
import org.lwjgl.stb.STBImage
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.ImageReference
import ru.dbotthepony.kstarbound.defs.item.impl.BackArmorItemDefinition
import ru.dbotthepony.kstarbound.defs.item.impl.ChestArmorItemDefinition
import ru.dbotthepony.kstarbound.defs.item.impl.CurrencyItemDefinition
import ru.dbotthepony.kstarbound.defs.item.impl.FlashlightDefinition
import ru.dbotthepony.kstarbound.defs.item.impl.HarvestingToolPrototype
import ru.dbotthepony.kstarbound.defs.item.impl.HeadArmorItemDefinition
import ru.dbotthepony.kstarbound.defs.item.api.IArmorItemDefinition
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
import ru.dbotthepony.kstarbound.defs.item.InventoryIcon
import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition
import ru.dbotthepony.kstarbound.defs.item.impl.ItemDefinition
import ru.dbotthepony.kstarbound.defs.item.impl.LegsArmorItemDefinition
import ru.dbotthepony.kstarbound.defs.item.impl.LiquidItemDefinition
import ru.dbotthepony.kstarbound.defs.item.impl.MaterialItemDefinition
import ru.dbotthepony.kstarbound.defs.monster.MonsterSkillDefinition
import ru.dbotthepony.kstarbound.defs.monster.MonsterTypeDefinition
import ru.dbotthepony.kstarbound.defs.npc.NpcTypeDefinition
import ru.dbotthepony.kstarbound.defs.npc.TenantDefinition
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.particle.ParticleDefinition
import ru.dbotthepony.kstarbound.defs.player.BlueprintLearnList
import ru.dbotthepony.kstarbound.defs.player.PlayerDefinition
import ru.dbotthepony.kstarbound.defs.player.RecipeDefinition
import ru.dbotthepony.kstarbound.defs.player.TechDefinition
import ru.dbotthepony.kstarbound.defs.projectile.ProjectileDefinition
import ru.dbotthepony.kstarbound.defs.quest.QuestTemplate
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
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.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.WriteOnce
import ru.dbotthepony.kstarbound.util.filterNotNull
import ru.dbotthepony.kstarbound.util.set
import ru.dbotthepony.kstarbound.util.traverseJsonPath
import ru.dbotthepony.kvector.vector.Vector2i
import java.io.*
import java.lang.ref.ReferenceQueue
import java.lang.ref.WeakReference
import java.text.DateFormat
import java.time.Duration
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 SECONDS_IN_TICK = 1.0 / 60.0
val strings: Interner<String> = Interner.newWeakInterner()
private val polyfill by lazy { loadInternalScript("polyfill") }
private val logger = LogManager.getLogger()
private val _tiles = ObjectRegistry("tiles", TileDefinition::materialName, TileDefinition::materialId)
val tiles = _tiles.view
val tilesByID = _tiles.intView
private val _tileModifiers = ObjectRegistry("tile modifiers", MaterialModifier::modName, MaterialModifier::modId)
val tileModifiers = _tileModifiers.view
val tileModifiersByID = _tileModifiers.intView
private val _liquid = ObjectRegistry("liquid", LiquidDefinition::name, LiquidDefinition::liquidId)
val liquid = _liquid.view
val liquidByID = _liquid.intView
private val _species = ObjectRegistry("species", Species::kind)
val species = _species.view
private val _statusEffects = ObjectRegistry("status effects", StatusEffectDefinition::name)
val statusEffects = _statusEffects.view
private val _particles = ObjectRegistry("particles", ParticleDefinition::kind)
val particles = _particles.view
private val _items = ObjectRegistry("items", IItemDefinition::itemName)
val items = _items.view
private val _questTemplates = ObjectRegistry("quest templates", QuestTemplate::id)
val questTemplates = _questTemplates.view
private val _techs = ObjectRegistry("techs", TechDefinition::name)
val techs = _techs.view
private val _jsonFunctions = ObjectRegistry<JsonFunction>("json functions")
val jsonFunctions = _jsonFunctions.view
private val _json2Functions = ObjectRegistry<Json2Function>("json 2functions")
val json2Functions = _json2Functions.view
private val _npcTypes = ObjectRegistry("npc types", NpcTypeDefinition::type)
val npcTypes = _npcTypes.view
private val _projectiles = ObjectRegistry("projectiles", ProjectileDefinition::projectileName)
val projectiles = _projectiles.view
private val _tenants = ObjectRegistry("tenants", TenantDefinition::name)
val tenants = _tenants.view
val recipeRegistry = RecipeRegistry()
private val _treasurePools = ObjectRegistry("treasure pools", TreasurePoolDefinition::name)
val treasurePools = _treasurePools.view
private val _monsterSkills = ObjectRegistry("monster skills", MonsterSkillDefinition::name)
val monsterSkills = _monsterSkills.view
private val _monsterTypes = ObjectRegistry("monster types", MonsterTypeDefinition::type)
val monsterTypes = _monsterTypes.view
private val _worldObjects = ObjectRegistry("objects", ObjectDefinition::objectName)
val worldObjects = _worldObjects.view
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)
// 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(ImageReference.Companion)
registerTypeAdapterFactory(AssetReference.Companion)
registerTypeAdapter(ItemStack.Adapter(this@Starbound))
registerTypeAdapterFactory(ItemReference.Factory(strings))
registerTypeAdapterFactory(TreasurePoolDefinition.Companion)
registerTypeAdapterFactory(with(RegistryReferenceFactory()) {
add(_tiles)
add(_tileModifiers)
add(_liquid)
add(_items)
add(_species)
add(_statusEffects)
add(_particles)
add(_questTemplates)
add(_techs)
add(_jsonFunctions)
add(_json2Functions)
add(_npcTypes)
add(_projectiles)
add(_tenants)
add(_treasurePools)
add(_monsterSkills)
add(_monsterTypes)
add(_worldObjects)
})
registerTypeAdapter(LongRangeAdapter)
create()
}
init {
val f = NonExistingFile("/metamaterials.config")
for (material in BuiltinMetaMaterials.MATERIALS) {
_tiles.add(material, JsonNull.INSTANCE, f)
}
}
private val imageCache: Cache<String, ImageData> = Caffeine.newBuilder()
.softValues()
.expireAfterAccess(Duration.ofMinutes(20))
.weigher<String, ImageData> { key, value -> value.data.capacity() }
.maximumWeight(1_024L * 1_024L * 256L /* 256 МиБ */)
.build()
fun item(name: String): ItemStack {
return ItemStack(items[name] ?: return ItemStack.EMPTY)
}
fun item(name: String, count: Long): ItemStack {
if (count <= 0L)
return ItemStack.EMPTY
return ItemStack(items[name] ?: return ItemStack.EMPTY, count = count)
}
fun item(name: String, count: Long, parameters: JsonObject): ItemStack {
if (count <= 0L)
return ItemStack.EMPTY
return ItemStack(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()
}
fun imageData(path: String): ImageData {
return imageCache.get(path) {
val file = locate(path)
if (!file.exists) {
throw FileNotFoundException("No such file $file")
}
if (!file.isFile) {
throw FileNotFoundException("File $file is a directory")
}
val getWidth = intArrayOf(0)
val getHeight = intArrayOf(0)
val components = intArrayOf(0)
val data = STBImage.stbi_load_from_memory(
file.readDirect(),
getWidth, getHeight,
components, 0
) ?: throw IllegalArgumentException("File $file is not an image or it is corrupted")
ImageData(data, getWidth[0], getHeight[0], components[0])
}
}
fun imageSize(path: String): Vector2i {
val image = imageData(path)
return Vector2i(image.width, image.height)
}
/**
* **ПРЕДУПРЕЖДЕНИЕ:**
* [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 = 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 = json2Functions[name] ?: throw NoSuchElementException("No such 2function $name")
args.push(fn.value.evaluate(args.getDouble(), args.getDouble()))
1
}
state.setTableFunction("imageSize", this) {args ->
args.lua.push(imageSize(args.getString()))
1
}
state.setTableFunction("imageSpaces", this) { args ->
// List<Vec2I> root.imageSpaces(String imagePath, Vec2F worldPosition, float spaceScan, bool flip)
val values = imageData(args.getString()).worldSpaces(args.getVector2i(), args.getDouble(), args.getBool())
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 ->
args.lua.push(imageData(args.getString()).nonEmptyRegion)
1
}
state.setTableFunction("npcConfig", this) { args ->
// Json root.npcConfig(String npcType)
val name = args.getString()
args.push(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(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(items[name]?.value?.itemType ?: throw NoSuchElementException("No such item $name"))
1
}
state.setTableFunction("itemTags", this) { args ->
val name = args.getString()
args.lua.pushStrings(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((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()
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(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 = this.liquid[name]?.value ?: throw NoSuchElementException("No such liquid with name $name")
} else {
val id = args.getInt()
liquid = this.liquidByID[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(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 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(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 = tiles[name] ?: throw NoSuchElementException("No such material $name")
if (mod == null) {
args.push(mat.value.miningSounds.firstOrNull())
} else {
val getMod = 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 = tiles[name] ?: throw NoSuchElementException("No such material $name")
if (mod == null) {
args.push(mat.value.footstepSound)
} else {
val getMod = 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 = tiles[name] ?: throw NoSuchElementException("No such material $name")
if (mod == null) {
args.push(mat.value.health)
} else {
val getMod = 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(tiles[name])
1
}
state.setTableFunction("modConfig", this) { args ->
val name = args.getString()
args.pushFull(tileModifiers[name])
1
}
state.setTableFunction("liquidConfig", this) { args ->
if (args.isNumberAt()) {
val id = args.getLong().toInt()
args.pushFull(liquidByID[id])
} else {
val name = args.getString()
args.pushFull(liquid[name])
}
1
}
state.setTableFunction("liquidName", this) { args ->
val id = args.getLong().toInt()
args.push(liquidByID[id]?.value?.name ?: throw NoSuchElementException("No such liquid with ID $id"))
1
}
state.setTableFunction("liquidId", this) { args ->
val name = args.getString()
args.push(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(monsterSkills[name]?.value?.config?.get(param))
1
}
state.setTableFunction("monsterParameters", this) { args ->
val name = args.getString()
val monster = 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 = 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 techs)
1
}
state.setTableFunction("techType", this) { args ->
val name = args.getString()
val tech = 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 = 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 loadStage(
callback: (Boolean, Boolean, String) -> Unit,
loader: ((String) -> Unit) -> Unit,
name: String,
) {
if (terminateLoading)
return
val time = System.currentTimeMillis()
callback(false, false, "Loading $name...")
logger.info("Loading $name...")
loader {
if (terminateLoading) {
throw InterruptedException("Game is terminating")
}
callback(false, true, it)
}
callback(false, true, "Loaded $name in ${System.currentTimeMillis() - time}ms")
logger.info("Loaded $name in ${System.currentTimeMillis() - time}ms")
}
private fun <T : Any> loadStage(
callback: (Boolean, Boolean, String) -> Unit,
registry: ObjectRegistry<T>,
files: List<IStarboundFile>,
) {
loadStage(callback, loader = {
for (listedFile in files) {
try {
it("Loading $listedFile")
registry.add(listedFile)
} catch (err: Throwable) {
logger.error("Loading ${registry.name} definition file $listedFile", err)
}
if (terminateLoading) {
break
}
}
}, registry.name)
}
private fun doInitialize(callback: (finished: Boolean, replaceStatus: Boolean, status: String) -> Unit) {
var time = System.currentTimeMillis()
if (archivePaths.isNotEmpty()) {
callback(false, false, "Searching for pak archives...".also(logger::info))
for (path in archivePaths) {
callback(false, false, "Reading index of ${path}...".also(logger::info))
addPak(StarboundPak(path) { _, status ->
callback(false, true, "${path.parent}/${path.name}: $status")
})
}
}
callback(false, false, "Finished reading pak archives in ${System.currentTimeMillis() - time}ms".also(logger::info))
time = System.currentTimeMillis()
callback(false, false, "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)
}
})
callback(false, false, "Finished building file index in ${System.currentTimeMillis() - time}ms".also(logger::info))
loadStage(callback, { loadItemDefinitions(it, ext2files) }, "item definitions")
loadStage(callback, { loadJsonFunctions(it, ext2files["functions"] ?: listOf()) }, "json functions")
loadStage(callback, { loadJson2Functions(it, ext2files["2functions"] ?: listOf()) }, "json 2functions")
loadStage(callback, { loadRecipes(it, ext2files["recipe"] ?: listOf()) }, "recipes")
loadStage(callback, { loadTreasurePools(it, ext2files["treasurepools"] ?: listOf()) }, "treasure pools")
loadStage(callback, _tiles, ext2files["material"] ?: listOf())
loadStage(callback, _tileModifiers, ext2files["matmod"] ?: listOf())
loadStage(callback, _liquid, ext2files["liquid"] ?: listOf())
loadStage(callback, _worldObjects, ext2files["object"] ?: listOf())
loadStage(callback, _statusEffects, ext2files["statuseffect"] ?: listOf())
loadStage(callback, _species, ext2files["species"] ?: listOf())
loadStage(callback, _particles, ext2files["particle"] ?: listOf())
loadStage(callback, _questTemplates, ext2files["questtemplate"] ?: listOf())
loadStage(callback, _techs, ext2files["tech"] ?: listOf())
loadStage(callback, _npcTypes, ext2files["npctype"] ?: listOf())
//loadStage(callback, _projectiles, ext2files["projectile"] ?: listOf())
//loadStage(callback, _tenants, ext2files["tenant"] ?: listOf())
loadStage(callback, _monsterSkills, ext2files["monsterskill"] ?: listOf())
//loadStage(callback, _monsterTypes, ext2files["monstertype"] ?: listOf())
AssetPathStack.block("/") {
//playerDefinition = gson.fromJson(locate("/player.config").reader(), PlayerDefinition::class.java)
}
initializing = false
initialized = true
callback(true, false, "Finished loading in ${System.currentTimeMillis() - time}ms")
}
fun initializeGame(callback: (finished: Boolean, replaceStatus: Boolean, status: String) -> Unit) {
if (initializing) {
throw IllegalStateException("Already initializing!")
}
if (initialized) {
throw IllegalStateException("Already initialized!")
}
initializing = true
Thread({ doInitialize(callback) }, "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()
}
}
private fun loadItemDefinitions(callback: (String) -> Unit, files: Map<String, Collection<IStarboundFile>>) {
val fileMap = mapOf(
"item" to ItemDefinition::class.java,
"currency" to CurrencyItemDefinition::class.java,
"liqitem" to LiquidItemDefinition::class.java,
"matitem" to MaterialItemDefinition::class.java,
"flashlight" to FlashlightDefinition::class.java,
"harvestingtool" to HarvestingToolPrototype::class.java,
"head" to HeadArmorItemDefinition::class.java,
"chest" to ChestArmorItemDefinition::class.java,
"legs" to LegsArmorItemDefinition::class.java,
"back" to BackArmorItemDefinition::class.java,
)
for ((ext, clazz) in fileMap) {
val fileList = files[ext] ?: continue
for (listedFile in fileList) {
try {
callback("Loading $listedFile")
val json = gson.fromJson(listedFile.reader(), JsonObject::class.java)
val def: IItemDefinition = AssetPathStack(listedFile.computeDirectory()) { gson.fromJson(JsonTreeReader(json), clazz) }
_items.add(def, json, listedFile)
} catch (err: Throwable) {
logger.error("Loading item definition file $listedFile", err)
}
if (terminateLoading) {
return
}
}
}
}
private fun loadJsonFunctions(callback: (String) -> Unit, files: Collection<IStarboundFile>) {
for (listedFile in files) {
callback("Loading $listedFile")
val json = gson.fromJson(listedFile.reader(), JsonObject::class.java)
for ((k, v) in json.entrySet()) {
try {
callback("Loading $k from $listedFile")
val fn = gson.fromJson<JsonFunction>(JsonTreeReader(v), JsonFunction::class.java)
_jsonFunctions.add(fn, v, listedFile, k)
} catch (err: Throwable) {
logger.error("Loading json function definition $k from file $listedFile", err)
}
}
if (terminateLoading) {
return
}
}
}
private fun loadJson2Functions(callback: (String) -> Unit, files: Collection<IStarboundFile>) {
for (listedFile in files) {
callback("Loading $listedFile")
val json = gson.fromJson(listedFile.reader(), JsonObject::class.java)
for ((k, v) in json.entrySet()) {
try {
callback("Loading $k from $listedFile")
val fn = gson.fromJson<Json2Function>(JsonTreeReader(v), Json2Function::class.java)
_json2Functions.add(fn, v, listedFile, k)
} catch (err: Throwable) {
logger.error("Loading json 2function definition $k from file $listedFile", err)
}
}
if (terminateLoading) {
return
}
}
}
private fun loadTreasurePools(callback: (String) -> Unit, files: Collection<IStarboundFile>) {
for (listedFile in files) {
callback("Loading $listedFile")
val json = gson.fromJson(listedFile.reader(), JsonObject::class.java)
for ((k, v) in json.entrySet()) {
try {
callback("Loading $k from $listedFile")
val result = gson.fromJson<TreasurePoolDefinition>(JsonTreeReader(v), TreasurePoolDefinition::class.java)
result.name = k
_treasurePools.add(result, v, listedFile)
} catch (err: Throwable) {
logger.error("Loading treasure pool definition $k from file $listedFile", err)
}
}
if (terminateLoading) {
return
}
}
}
private fun loadRecipes(callback: (String) -> Unit, files: Collection<IStarboundFile>) {
for (listedFile in files) {
try {
callback("Loading $listedFile")
val json = gson.fromJson(listedFile.reader(), JsonElement::class.java)
val value = gson.fromJson<RecipeDefinition>(JsonTreeReader(json), RecipeDefinition::class.java)
recipeRegistry.add(RegistryObject(value, json, listedFile))
} catch (err: Throwable) {
logger.error("Loading recipe definition file $listedFile", err)
}
if (terminateLoading) {
return
}
}
}
}
private class StringInterner(private val segmentBits: Int) : Interner<String>, Hash.Strategy<Any> {
class Ref(referent: String, queue: ReferenceQueue<String>) : WeakReference<String>(referent, queue) {
val hash = referent.hashCode()
override fun hashCode(): Int {
return hash
}
}
override fun equals(a: Any?, b: Any?): Boolean {
if (a is String && b is Ref) return a == b.get()
if (a is Ref && b is String) return a.get() == b
return a === b
}
override fun hashCode(o: Any): Int {
return o.hashCode()
}
private val queue = ReferenceQueue<String>()
private val actualSegmentBits: Int
init {
var result = 0
for (i in 0 until segmentBits) {
result = result or (1.shl(i))
}
actualSegmentBits = result
}
private val cleaner = Runnable {
while (true) {
val ref = queue.remove() as Ref
val segment = segments[ref.hash and actualSegmentBits]
synchronized(segment) {
val removed = segment.remove(ref)
check(removed === ref) { "Expected to remove reference $ref from segment ${ref.hash and actualSegmentBits} (full hash: ${ref.hash}), but we removed $removed (removed hash: ${removed.hashCode()}, removed segment: ${removed.hashCode() and actualSegmentBits})" }
}
}
}
private val thread = Thread(cleaner, "String Interner Cleanup Thread")
init {
thread.priority = 2
thread.isDaemon = true
thread.start()
}
private val segments: Array<Object2ObjectOpenCustomHashMap<Any, Any>> = Array(1.shl(segmentBits)) { Object2ObjectOpenCustomHashMap(this) }
override fun intern(sample: String): String {
val hash = sample.hashCode()
val segment = segments[hash and actualSegmentBits]
synchronized(segment) {
val canonical = (segment[sample] as Ref?)?.get()
if (canonical != null) {
return canonical
}
val ref = Ref(sample, queue)
segment.put(ref, ref)
return sample
}
}
}