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

1258 lines
40 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.AtlasConfiguration
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.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.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.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.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.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.PathStack
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
class Starbound : ISBFileLocator {
private val logger = LogManager.getLogger()
val pathStack = PathStack(STRINGS)
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
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))
// ArrayList
registerTypeAdapterFactory(ArrayListAdapterFactory)
// все enum'ы без особых настроек
registerTypeAdapterFactory(EnumAdapter.Companion)
// @JsonBuilder
registerTypeAdapterFactory(BuilderAdapter.Factory(STRINGS))
// @JsonFactory
registerTypeAdapterFactory(FactoryAdapter.Factory(STRINGS))
// Either<>
registerTypeAdapterFactory(EitherTypeAdapter)
registerTypeAdapterFactory(SBPattern.Companion)
registerTypeAdapter(ColorReplacements.Companion)
registerTypeAdapterFactory(BlueprintLearnList.Companion)
registerTypeAdapter(ColorTypeAdapter.nullSafe())
// математические классы
registerTypeAdapter(AABBTypeAdapter)
registerTypeAdapter(AABBiTypeAdapter)
registerTypeAdapter(Vector2dTypeAdapter)
registerTypeAdapter(Vector2fTypeAdapter)
registerTypeAdapter(Vector2iTypeAdapter)
registerTypeAdapter(Vector4iTypeAdapter)
registerTypeAdapter(Vector4dTypeAdapter)
registerTypeAdapter(PolyTypeAdapter)
// Функции
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))
registerTypeAdapterFactory(InventoryIcon.Factory(pathStack))
registerTypeAdapterFactory(IArmorItemDefinition.Frames.Factory)
registerTypeAdapterFactory(AssetPathFactory(pathStack))
registerTypeAdapterFactory(ImageReference.Factory({ atlasRegistry.get(it) }, pathStack))
registerTypeAdapterFactory(AssetReferenceFactory(pathStack, this@Starbound))
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)
})
registerTypeAdapter(LongRangeAdapter)
create()
}
val atlasRegistry = AtlasConfiguration.Registry(this, pathStack, gson)
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 IllegalStateException("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]?.copy() ?: 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!!.copy()
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.copy() }
.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(gson, listedFile, pathStack)
} 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, _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())
pathStack.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 = pathStack(listedFile.computeDirectory()) { gson.fromJson(JsonTreeReader(json), clazz) }
_items.add(def, json, listedFile, gson, pathStack)
} 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, gson, pathStack, 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, gson, pathStack, 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, gson, pathStack)
} 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, gson, pathStack))
} catch (err: Throwable) {
logger.error("Loading recipe definition file $listedFile", err)
}
if (terminateLoading) {
return
}
}
}
companion object {
/**
* Глобальный [Interner] для [String]
*
* Так как нет смысла иметь множество [Interner]'ов для [String],
* а так же в силу его поточной безопасности,
* данный [Interner] доступен глобально
*/
@JvmField
val STRINGS: Interner<String> = Interner.newWeakInterner()
private val polyfill by lazy { loadInternalScript("polyfill") }
}
}
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
}
}
}