1258 lines
40 KiB
Kotlin
1258 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.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
|
||
}
|
||
}
|
||
}
|