Items (not finished), Item Registry, performance tweaks, memory improvements, loading performance improvements

This commit is contained in:
DBotThePony 2024-04-17 15:35:32 +07:00
parent 9f52e2314d
commit d755e6cd66
Signed by: DBot
GPG Key ID: DCC23B5715498507
95 changed files with 1674 additions and 1455 deletions

View File

@ -57,6 +57,15 @@ val color: TileColor = TileColor.DEFAULT
### Prototypes ### Prototypes
#### Items
* `inventoryIcon` additions if specified as array:
* `scale`, either as float or as vector (for x and y scales); both in prototype file and in `parameters`.
* `color` (defaults to white `[255, 255, 255, 255]`)
* `rotation` (in degrees, defaults to `0`)
* `mirrored` (defaults to `false`, this is different from setting scale to `-1f` since it obeys center point)
* `centered` (defaults to `true`)
* `fullbright` (defaults to `false`)
#### .matierial #### .matierial
* Implemented `isConnectable`, which was planned by original developers, but scrapped in process (defaults to `true`, by default only next meta-materials have it set to false: `empty`, `null` and `boundary`) * Implemented `isConnectable`, which was planned by original developers, but scrapped in process (defaults to `true`, by default only next meta-materials have it set to false: `empty`, `null` and `boundary`)
* Used by object and plant anchoring code to determine valid placement * Used by object and plant anchoring code to determine valid placement

View File

@ -11,6 +11,7 @@ import ru.dbotthepony.kstarbound.defs.UniverseServerConfig
import ru.dbotthepony.kstarbound.defs.WorldServerConfig import ru.dbotthepony.kstarbound.defs.WorldServerConfig
import ru.dbotthepony.kstarbound.defs.actor.player.PlayerConfig import ru.dbotthepony.kstarbound.defs.actor.player.PlayerConfig
import ru.dbotthepony.kstarbound.defs.item.ItemDropConfig import ru.dbotthepony.kstarbound.defs.item.ItemDropConfig
import ru.dbotthepony.kstarbound.defs.item.ItemGlobalConfig
import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig
import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldsConfig import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldsConfig
import ru.dbotthepony.kstarbound.defs.world.AsteroidWorldsConfig import ru.dbotthepony.kstarbound.defs.world.AsteroidWorldsConfig
@ -25,6 +26,7 @@ import ru.dbotthepony.kstarbound.defs.world.SystemWorldObjectConfig
import ru.dbotthepony.kstarbound.defs.world.WorldTemplateConfig import ru.dbotthepony.kstarbound.defs.world.WorldTemplateConfig
import ru.dbotthepony.kstarbound.json.mapAdapter import ru.dbotthepony.kstarbound.json.mapAdapter
import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.util.AssetPathStack
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ForkJoinTask import java.util.concurrent.ForkJoinTask
import java.util.concurrent.Future import java.util.concurrent.Future
import kotlin.properties.Delegates import kotlin.properties.Delegates
@ -102,30 +104,20 @@ object Globals {
var instanceWorlds by Delegates.notNull<ImmutableMap<String, InstanceWorldConfig>>() var instanceWorlds by Delegates.notNull<ImmutableMap<String, InstanceWorldConfig>>()
private set private set
private object EmptyTask : ForkJoinTask<Unit>() { var itemParameters by Delegates.notNull<ItemGlobalConfig>()
private fun readResolve(): Any = EmptyTask private set
override fun getRawResult() {
}
override fun setRawResult(value: Unit?) { private fun <T> load(path: String, accept: KMutableProperty0<T>, adapter: Lazy<TypeAdapter<T>>): Future<*> {
}
override fun exec(): Boolean {
return true
}
}
private fun <T> load(path: String, accept: KMutableProperty0<T>, adapter: TypeAdapter<T>): Future<*> {
val file = Starbound.loadJsonAsset(path) val file = Starbound.loadJsonAsset(path)
if (file == null) { if (file == null) {
LOGGER.fatal("$path does not exist or is not a file, expect bad things to happen!") LOGGER.fatal("$path does not exist or is not a file, expect bad things to happen!")
return EmptyTask return CompletableFuture.completedFuture(Unit)
} else { } else {
return Starbound.EXECUTOR.submit { return Starbound.EXECUTOR.submit {
try { try {
AssetPathStack("/") { AssetPathStack("/") {
accept.set(adapter.fromJsonTree(file)) accept.set(adapter.value.fromJsonTree(file))
} }
} catch (err: Throwable) { } catch (err: Throwable) {
LOGGER.fatal("Error while reading $path, expect bad things to happen!", err) LOGGER.fatal("Error while reading $path, expect bad things to happen!", err)
@ -136,7 +128,7 @@ object Globals {
} }
private inline fun <reified T> load(path: String, accept: KMutableProperty0<T>): Future<*> { private inline fun <reified T> load(path: String, accept: KMutableProperty0<T>): Future<*> {
return load(path, accept, Starbound.gson.getAdapter(T::class.java)) return load(path, accept, lazy(LazyThreadSafetyMode.NONE) { Starbound.gson.getAdapter(T::class.java) })
} }
fun load(): List<Future<*>> { fun load(): List<Future<*>> {
@ -157,16 +149,17 @@ object Globals {
tasks.add(load("/celestial.config", ::celestialBaseInformation)) tasks.add(load("/celestial.config", ::celestialBaseInformation))
tasks.add(load("/celestial.config", ::celestialConfig)) tasks.add(load("/celestial.config", ::celestialConfig))
tasks.add(load("/celestial/names.config", ::celestialNames)) tasks.add(load("/celestial/names.config", ::celestialNames))
tasks.add(load("/items/defaultParameters.config", ::itemParameters))
tasks.add(load("/plants/grassDamage.config", ::grassDamage)) tasks.add(load("/plants/grassDamage.config", ::grassDamage))
tasks.add(load("/plants/treeDamage.config", ::treeDamage)) tasks.add(load("/plants/treeDamage.config", ::treeDamage))
tasks.add(load("/plants/bushDamage.config", ::bushDamage)) tasks.add(load("/plants/bushDamage.config", ::bushDamage))
tasks.add(load("/tiles/defaultDamage.config", ::tileDamage)) tasks.add(load("/tiles/defaultDamage.config", ::tileDamage))
tasks.add(load("/dungeon_worlds.config", ::dungeonWorlds, Starbound.gson.mapAdapter())) tasks.add(load("/dungeon_worlds.config", ::dungeonWorlds, lazy { Starbound.gson.mapAdapter() }))
tasks.add(load("/currencies.config", ::currencies, Starbound.gson.mapAdapter())) tasks.add(load("/currencies.config", ::currencies, lazy { Starbound.gson.mapAdapter() }))
tasks.add(load("/system_objects.config", ::systemObjects, Starbound.gson.mapAdapter())) tasks.add(load("/system_objects.config", ::systemObjects, lazy { Starbound.gson.mapAdapter() }))
tasks.add(load("/instance_worlds.config", ::instanceWorlds, Starbound.gson.mapAdapter())) tasks.add(load("/instance_worlds.config", ::instanceWorlds, lazy { Starbound.gson.mapAdapter() }))
return tasks return tasks
} }

View File

@ -10,8 +10,15 @@ import java.net.InetSocketAddress
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
fun main() { fun main() {
Starbound.addPakPath(File("J:\\Steam\\steamapps\\common\\Starbound\\assets\\packed.pak")) Starbound.addArchive(File("J:\\Steam\\steamapps\\common\\Starbound\\assets\\packed.pak"))
Starbound.doBootstrap()
/*for (f in File("J:\\Steam\\steamapps\\workshop\\content\\211820").listFiles()!!) {
for (f2 in f.listFiles()!!) {
if (f2.isFile) {
Starbound.addArchive(f2)
}
}
}*/
LOGGER.info("Running LWJGL ${Version.getVersion()}") LOGGER.info("Running LWJGL ${Version.getVersion()}")

View File

@ -19,17 +19,6 @@ import ru.dbotthepony.kstarbound.defs.Species
import ru.dbotthepony.kstarbound.defs.StatusEffectDefinition import ru.dbotthepony.kstarbound.defs.StatusEffectDefinition
import ru.dbotthepony.kstarbound.defs.ThingDescription import ru.dbotthepony.kstarbound.defs.ThingDescription
import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
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.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.MonsterSkillDefinition
import ru.dbotthepony.kstarbound.defs.monster.MonsterTypeDefinition import ru.dbotthepony.kstarbound.defs.monster.MonsterTypeDefinition
import ru.dbotthepony.kstarbound.defs.npc.NpcTypeDefinition import ru.dbotthepony.kstarbound.defs.npc.NpcTypeDefinition
@ -49,6 +38,8 @@ import ru.dbotthepony.kstarbound.defs.world.BushVariant
import ru.dbotthepony.kstarbound.defs.world.GrassVariant import ru.dbotthepony.kstarbound.defs.world.GrassVariant
import ru.dbotthepony.kstarbound.defs.world.TreeVariant import ru.dbotthepony.kstarbound.defs.world.TreeVariant
import ru.dbotthepony.kstarbound.defs.world.BiomeDefinition import ru.dbotthepony.kstarbound.defs.world.BiomeDefinition
import ru.dbotthepony.kstarbound.item.ItemRegistry
import ru.dbotthepony.kstarbound.json.JsonPatch
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.world.terrain.TerrainSelectorType import ru.dbotthepony.kstarbound.world.terrain.TerrainSelectorType
import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.util.AssetPathStack
@ -75,7 +66,6 @@ object Registries {
val species = Registry<Species>("species").also(registriesInternal::add).also { adapters.add(it.adapter()) } val species = Registry<Species>("species").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val statusEffects = Registry<StatusEffectDefinition>("status effect").also(registriesInternal::add).also { adapters.add(it.adapter()) } val statusEffects = Registry<StatusEffectDefinition>("status effect").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val particles = Registry<ParticleConfig>("particle").also(registriesInternal::add).also { adapters.add(it.adapter()) } val particles = Registry<ParticleConfig>("particle").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val items = Registry<IItemDefinition>("item").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val questTemplates = Registry<QuestTemplate>("quest template").also(registriesInternal::add).also { adapters.add(it.adapter()) } val questTemplates = Registry<QuestTemplate>("quest template").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val techs = Registry<TechDefinition>("tech").also(registriesInternal::add).also { adapters.add(it.adapter()) } val techs = Registry<TechDefinition>("tech").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val jsonFunctions = Registry<JsonFunction>("json function").also(registriesInternal::add).also { adapters.add(it.adapter()) } val jsonFunctions = Registry<JsonFunction>("json function").also(registriesInternal::add).also { adapters.add(it.adapter()) }
@ -115,19 +105,18 @@ object Registries {
private inline fun <reified T : Any> loadRegistry( private inline fun <reified T : Any> loadRegistry(
registry: Registry<T>, registry: Registry<T>,
files: List<IStarboundFile>, patches: Map<String, Collection<IStarboundFile>>,
files: Collection<IStarboundFile>,
noinline keyProvider: (T) -> Pair<String, KOptional<Int?>>, noinline keyProvider: (T) -> Pair<String, KOptional<Int?>>,
noinline after: (T, IStarboundFile) -> Unit = { _, _ -> } noinline after: (T, IStarboundFile) -> Unit = { _, _ -> }
): List<Future<*>> { ): List<Future<*>> {
val adapter by lazy { Starbound.gson.getAdapter(T::class.java) } val adapter by lazy { Starbound.gson.getAdapter(T::class.java) }
val elementAdapter by lazy { Starbound.gson.getAdapter(JsonElement::class.java) }
return files.map { listedFile -> return files.map { listedFile ->
Starbound.EXECUTOR.submit { Starbound.EXECUTOR.submit {
try { try {
AssetPathStack(listedFile.computeDirectory()) { AssetPathStack(listedFile.computeDirectory()) {
// TODO: json patch support val elem = JsonPatch.apply(Starbound.ELEMENTS_ADAPTER.read(listedFile.jsonReader()), patches[listedFile.computeFullPath()])
val elem = elementAdapter.read(listedFile.jsonReader())
val read = adapter.fromJsonTree(elem) val read = adapter.fromJsonTree(elem)
val keys = keyProvider(read) val keys = keyProvider(read)
@ -154,91 +143,49 @@ object Registries {
registriesInternal.forEach { it.finishLoad() } registriesInternal.forEach { it.finishLoad() }
} }
fun load(fileTree: Map<String, List<IStarboundFile>>): List<Future<*>> { fun load(fileTree: Map<String, Collection<IStarboundFile>>, patchTree: Map<String, List<IStarboundFile>>): List<Future<*>> {
val tasks = ArrayList<Future<*>>() val tasks = ArrayList<Future<*>>()
tasks.addAll(loadItemDefinitions(fileTree)) tasks.addAll(ItemRegistry.load(fileTree, patchTree))
tasks.addAll(loadTerrainSelectors(fileTree)) tasks.addAll(loadTerrainSelectors(fileTree, patchTree))
tasks.addAll(loadRegistry(tiles, fileTree["material"] ?: listOf(), key(TileDefinition::materialName, TileDefinition::materialId))) tasks.addAll(loadRegistry(tiles, patchTree, fileTree["material"] ?: listOf(), key(TileDefinition::materialName, TileDefinition::materialId)))
tasks.addAll(loadRegistry(tileModifiers, fileTree["matmod"] ?: listOf(), key(TileModifierDefinition::modName, TileModifierDefinition::modId))) tasks.addAll(loadRegistry(tileModifiers, patchTree, fileTree["matmod"] ?: listOf(), key(TileModifierDefinition::modName, TileModifierDefinition::modId)))
tasks.addAll(loadRegistry(liquid, fileTree["liquid"] ?: listOf(), key(LiquidDefinition::name, LiquidDefinition::liquidId))) tasks.addAll(loadRegistry(liquid, patchTree, fileTree["liquid"] ?: listOf(), key(LiquidDefinition::name, LiquidDefinition::liquidId)))
tasks.add(loadMetaMaterials()) tasks.add(loadMetaMaterials())
tasks.addAll(loadRegistry(dungeons, fileTree["dungeon"] ?: listOf(), key(DungeonDefinition::name))) tasks.addAll(loadRegistry(dungeons, patchTree, fileTree["dungeon"] ?: listOf(), key(DungeonDefinition::name)))
tasks.addAll(loadRegistry(worldObjects, fileTree["object"] ?: listOf(), key(ObjectDefinition::objectName))) tasks.addAll(loadRegistry(worldObjects, patchTree, fileTree["object"] ?: listOf(), key(ObjectDefinition::objectName)))
tasks.addAll(loadRegistry(statusEffects, fileTree["statuseffect"] ?: listOf(), key(StatusEffectDefinition::name))) tasks.addAll(loadRegistry(statusEffects, patchTree, fileTree["statuseffect"] ?: listOf(), key(StatusEffectDefinition::name)))
tasks.addAll(loadRegistry(species, fileTree["species"] ?: listOf(), key(Species::kind))) tasks.addAll(loadRegistry(species, patchTree, fileTree["species"] ?: listOf(), key(Species::kind)))
tasks.addAll(loadRegistry(particles, fileTree["particle"] ?: listOf(), { (it.kind ?: throw NullPointerException("Missing 'kind' value")) to KOptional() })) tasks.addAll(loadRegistry(particles, patchTree, fileTree["particle"] ?: listOf(), { (it.kind ?: throw NullPointerException("Missing 'kind' value")) to KOptional() }))
tasks.addAll(loadRegistry(questTemplates, fileTree["questtemplate"] ?: listOf(), key(QuestTemplate::id))) tasks.addAll(loadRegistry(questTemplates, patchTree, fileTree["questtemplate"] ?: listOf(), key(QuestTemplate::id)))
tasks.addAll(loadRegistry(techs, fileTree["tech"] ?: listOf(), key(TechDefinition::name))) tasks.addAll(loadRegistry(techs, patchTree, fileTree["tech"] ?: listOf(), key(TechDefinition::name)))
tasks.addAll(loadRegistry(npcTypes, fileTree["npctype"] ?: listOf(), key(NpcTypeDefinition::type))) tasks.addAll(loadRegistry(npcTypes, patchTree, fileTree["npctype"] ?: listOf(), key(NpcTypeDefinition::type)))
tasks.addAll(loadRegistry(monsterSkills, fileTree["monsterskill"] ?: listOf(), key(MonsterSkillDefinition::name))) tasks.addAll(loadRegistry(monsterSkills, patchTree, fileTree["monsterskill"] ?: listOf(), key(MonsterSkillDefinition::name)))
tasks.addAll(loadRegistry(biomes, fileTree["biome"] ?: listOf(), key(BiomeDefinition::name))) tasks.addAll(loadRegistry(biomes, patchTree, fileTree["biome"] ?: listOf(), key(BiomeDefinition::name)))
tasks.addAll(loadRegistry(grassVariants, fileTree["grass"] ?: listOf(), key(GrassVariant.Data::name))) tasks.addAll(loadRegistry(grassVariants, patchTree, fileTree["grass"] ?: listOf(), key(GrassVariant.Data::name)))
tasks.addAll(loadRegistry(treeStemVariants, fileTree["modularstem"] ?: listOf(), key(TreeVariant.StemData::name))) tasks.addAll(loadRegistry(treeStemVariants, patchTree, fileTree["modularstem"] ?: listOf(), key(TreeVariant.StemData::name)))
tasks.addAll(loadRegistry(treeFoliageVariants, fileTree["modularfoliage"] ?: listOf(), key(TreeVariant.FoliageData::name))) tasks.addAll(loadRegistry(treeFoliageVariants, patchTree, fileTree["modularfoliage"] ?: listOf(), key(TreeVariant.FoliageData::name)))
tasks.addAll(loadRegistry(bushVariants, fileTree["bush"] ?: listOf(), key(BushVariant.Data::name))) tasks.addAll(loadRegistry(bushVariants, patchTree, fileTree["bush"] ?: listOf(), key(BushVariant.Data::name)))
tasks.addAll(loadCombined(jsonFunctions, fileTree["functions"] ?: listOf())) tasks.addAll(loadCombined(jsonFunctions, fileTree["functions"] ?: listOf(), patchTree))
tasks.addAll(loadCombined(json2Functions, fileTree["2functions"] ?: listOf())) tasks.addAll(loadCombined(json2Functions, fileTree["2functions"] ?: listOf(), patchTree))
tasks.addAll(loadCombined(jsonConfigFunctions, fileTree["configfunctions"] ?: listOf())) tasks.addAll(loadCombined(jsonConfigFunctions, fileTree["configfunctions"] ?: listOf(), patchTree))
tasks.addAll(loadCombined(treasurePools, fileTree["treasurepools"] ?: listOf()) { name = it }) tasks.addAll(loadCombined(treasurePools, fileTree["treasurepools"] ?: listOf(), patchTree) { name = it })
return tasks return tasks
} }
private fun loadItemDefinitions(files: Map<String, Collection<IStarboundFile>>): List<Future<*>> { private inline fun <reified T : Any> loadCombined(registry: Registry<T>, files: Collection<IStarboundFile>, patches: Map<String, Collection<IStarboundFile>>, noinline transform: T.(String) -> Unit = {}): List<Future<*>> {
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,
)
val tasks = ArrayList<Future<*>>()
val objects = Starbound.gson.getAdapter(JsonObject::class.java)
for ((ext, clazz) in fileMap) {
val fileList = files[ext] ?: continue
val adapter by lazy { Starbound.gson.getAdapter(clazz) }
for (listedFile in fileList) {
tasks.add(Starbound.EXECUTOR.submit {
try {
val json = objects.read(listedFile.jsonReader())
val def = AssetPathStack(listedFile.computeDirectory()) { adapter.fromJsonTree(json) }
items.add {
items.add(key = def.itemName, value = def, json = json, file = listedFile)
}
} catch (err: Throwable) {
LOGGER.error("Loading item definition file $listedFile", err)
}
})
}
}
return tasks
}
private inline fun <reified T : Any> loadCombined(registry: Registry<T>, files: Collection<IStarboundFile>, noinline transform: T.(String) -> Unit = {}): List<Future<*>> {
val adapter by lazy { Starbound.gson.getAdapter(T::class.java) } val adapter by lazy { Starbound.gson.getAdapter(T::class.java) }
val elementAdapter by lazy { Starbound.gson.getAdapter(JsonObject::class.java) }
return files.map { listedFile -> return files.map { listedFile ->
Starbound.EXECUTOR.submit { Starbound.EXECUTOR.submit {
try { try {
// TODO: json patch support val json = JsonPatch.apply(Starbound.ELEMENTS_ADAPTER.read(listedFile.jsonReader()), patches[listedFile.computeFullPath()]) as JsonObject
val json = elementAdapter.read(JsonReader(listedFile.reader()).also { it.isLenient = true })
for ((k, v) in json.entrySet()) { for ((k, v) in json.entrySet()) {
try { try {
@ -259,9 +206,9 @@ object Registries {
} }
} }
private fun loadTerrainSelector(listedFile: IStarboundFile, type: TerrainSelectorType?) { private fun loadTerrainSelector(listedFile: IStarboundFile, type: TerrainSelectorType?, patches: Map<String, Collection<IStarboundFile>>) {
try { try {
val json = Starbound.gson.getAdapter(JsonObject::class.java).read(JsonReader(listedFile.reader()).also { it.isLenient = true }) val json = JsonPatch.apply(Starbound.ELEMENTS_ADAPTER.read(listedFile.jsonReader()), patches[listedFile.computeFullPath()]) as JsonObject
val name = json["name"]?.asString ?: throw JsonSyntaxException("Missing 'name' field") val name = json["name"]?.asString ?: throw JsonSyntaxException("Missing 'name' field")
val factory = TerrainSelectorType.factory(json, false, type) val factory = TerrainSelectorType.factory(json, false, type)
@ -273,12 +220,12 @@ object Registries {
} }
} }
private fun loadTerrainSelectors(files: Map<String, Collection<IStarboundFile>>): List<Future<*>> { private fun loadTerrainSelectors(files: Map<String, Collection<IStarboundFile>>, patches: Map<String, Collection<IStarboundFile>>): List<Future<*>> {
val tasks = ArrayList<Future<*>>() val tasks = ArrayList<Future<*>>()
tasks.addAll((files["terrain"] ?: listOf()).map { listedFile -> tasks.addAll((files["terrain"] ?: listOf()).map { listedFile ->
Starbound.EXECUTOR.submit { Starbound.EXECUTOR.submit {
loadTerrainSelector(listedFile, null) loadTerrainSelector(listedFile, null, patches)
} }
}) })
@ -286,7 +233,7 @@ object Registries {
for (type in TerrainSelectorType.entries) { for (type in TerrainSelectorType.entries) {
tasks.addAll((files[type.jsonName.lowercase()] ?: listOf()).map { listedFile -> tasks.addAll((files[type.jsonName.lowercase()] ?: listOf()).map { listedFile ->
Starbound.EXECUTOR.submit { Starbound.EXECUTOR.submit {
loadTerrainSelector(listedFile, type) loadTerrainSelector(listedFile, type, patches)
} }
}) })
} }

View File

@ -1,10 +1,11 @@
package ru.dbotthepony.kstarbound package ru.dbotthepony.kstarbound
import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.Interner import com.github.benmanes.caffeine.cache.Interner
import com.github.benmanes.caffeine.cache.Scheduler import com.github.benmanes.caffeine.cache.Scheduler
import com.google.gson.* import com.google.gson.*
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction import com.google.gson.stream.JsonReader
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.ObjectArraySet
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import org.classdump.luna.compiler.CompilerChunkLoader import org.classdump.luna.compiler.CompilerChunkLoader
@ -24,12 +25,12 @@ import ru.dbotthepony.kommons.gson.Vector3iTypeAdapter
import ru.dbotthepony.kommons.gson.Vector4dTypeAdapter import ru.dbotthepony.kommons.gson.Vector4dTypeAdapter
import ru.dbotthepony.kommons.gson.Vector4fTypeAdapter import ru.dbotthepony.kommons.gson.Vector4fTypeAdapter
import ru.dbotthepony.kommons.gson.Vector4iTypeAdapter import ru.dbotthepony.kommons.gson.Vector4iTypeAdapter
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.collect.WeightedList import ru.dbotthepony.kstarbound.collect.WeightedList
import ru.dbotthepony.kstarbound.defs.* import ru.dbotthepony.kstarbound.defs.*
import ru.dbotthepony.kstarbound.defs.image.Image import ru.dbotthepony.kstarbound.defs.image.Image
import ru.dbotthepony.kstarbound.defs.image.SpriteReference import ru.dbotthepony.kstarbound.defs.image.SpriteReference
import ru.dbotthepony.kstarbound.defs.item.api.IArmorItemDefinition
import ru.dbotthepony.kstarbound.defs.item.InventoryIcon
import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition
import ru.dbotthepony.kstarbound.defs.actor.player.BlueprintLearnList import ru.dbotthepony.kstarbound.defs.actor.player.BlueprintLearnList
import ru.dbotthepony.kstarbound.defs.animation.Particle import ru.dbotthepony.kstarbound.defs.animation.Particle
@ -42,6 +43,7 @@ import ru.dbotthepony.kstarbound.defs.world.BiomePlacementItemType
import ru.dbotthepony.kstarbound.defs.world.WorldLayout import ru.dbotthepony.kstarbound.defs.world.WorldLayout
import ru.dbotthepony.kstarbound.world.terrain.TerrainSelectorType import ru.dbotthepony.kstarbound.world.terrain.TerrainSelectorType
import ru.dbotthepony.kstarbound.io.* import ru.dbotthepony.kstarbound.io.*
import ru.dbotthepony.kstarbound.item.ItemRegistry
import ru.dbotthepony.kstarbound.json.factory.MapsTypeAdapterFactory import ru.dbotthepony.kstarbound.json.factory.MapsTypeAdapterFactory
import ru.dbotthepony.kstarbound.json.InternedJsonElementAdapter import ru.dbotthepony.kstarbound.json.InternedJsonElementAdapter
import ru.dbotthepony.kstarbound.json.InternedStringAdapter import ru.dbotthepony.kstarbound.json.InternedStringAdapter
@ -56,8 +58,9 @@ import ru.dbotthepony.kstarbound.json.factory.PairAdapterFactory
import ru.dbotthepony.kstarbound.json.factory.RGBAColorTypeAdapter import ru.dbotthepony.kstarbound.json.factory.RGBAColorTypeAdapter
import ru.dbotthepony.kstarbound.json.factory.SingletonTypeAdapterFactory import ru.dbotthepony.kstarbound.json.factory.SingletonTypeAdapterFactory
import ru.dbotthepony.kstarbound.server.world.UniverseChunk import ru.dbotthepony.kstarbound.server.world.UniverseChunk
import ru.dbotthepony.kstarbound.item.ItemStack import ru.dbotthepony.kstarbound.item.RecipeRegistry
import ru.dbotthepony.kstarbound.json.JsonAdapterTypeFactory import ru.dbotthepony.kstarbound.json.JsonAdapterTypeFactory
import ru.dbotthepony.kstarbound.json.JsonPatch
import ru.dbotthepony.kstarbound.json.JsonPath import ru.dbotthepony.kstarbound.json.JsonPath
import ru.dbotthepony.kstarbound.json.NativeLegacy import ru.dbotthepony.kstarbound.json.NativeLegacy
import ru.dbotthepony.kstarbound.util.BlockableEventLoop import ru.dbotthepony.kstarbound.util.BlockableEventLoop
@ -70,6 +73,8 @@ import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.* import java.io.*
import java.lang.ref.Cleaner import java.lang.ref.Cleaner
import java.text.DateFormat import java.text.DateFormat
import java.time.Duration
import java.util.Collections
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executor import java.util.concurrent.Executor
@ -82,11 +87,6 @@ import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.LockSupport import java.util.concurrent.locks.LockSupport
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.NoSuchElementException
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@ -102,12 +102,19 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
// compile flags. uuuugh // compile flags. uuuugh
const val DEDUP_CELL_STATES = true const val DEDUP_CELL_STATES = true
const val USE_CAFFEINE_INTERNER = false const val USE_CAFFEINE_INTERNER = false
const val USE_INTERNER = true
fun <E : Any> interner(): Interner<E> { fun <E : Any> interner(): Interner<E> {
if (!USE_INTERNER)
return Interner { it }
return if (USE_CAFFEINE_INTERNER) Interner.newWeakInterner() else HashTableInterner() return if (USE_CAFFEINE_INTERNER) Interner.newWeakInterner() else HashTableInterner()
} }
fun <E : Any> interner(bits: Int): Interner<E> { fun <E : Any> interner(bits: Int): Interner<E> {
if (!USE_INTERNER)
return Interner { it }
return if (USE_CAFFEINE_INTERNER) Interner.newWeakInterner() else HashTableInterner(bits) return if (USE_CAFFEINE_INTERNER) Interner.newWeakInterner() else HashTableInterner(bits)
} }
@ -248,6 +255,8 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
return loader.compileTextChunk(name, chunk) return loader.compileTextChunk(name, chunk)
} }
val ELEMENTS_ADAPTER = InternedJsonElementAdapter(STRINGS)
val gson: Gson = with(GsonBuilder()) { val gson: Gson = with(GsonBuilder()) {
// serializeNulls() // serializeNulls()
setDateFormat(DateFormat.LONG) setDateFormat(DateFormat.LONG)
@ -256,11 +265,9 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
registerTypeAdapter(InternedStringAdapter(STRINGS)) registerTypeAdapter(InternedStringAdapter(STRINGS))
InternedJsonElementAdapter(STRINGS).also { registerTypeAdapter(ELEMENTS_ADAPTER)
registerTypeAdapter(it) registerTypeAdapter(ELEMENTS_ADAPTER.arrays)
registerTypeAdapter(it.arrays) registerTypeAdapter(ELEMENTS_ADAPTER.objects)
registerTypeAdapter(it.objects)
}
registerTypeAdapter(Nothing::class.java, NothingAdapter) registerTypeAdapter(Nothing::class.java, NothingAdapter)
@ -336,18 +343,11 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
registerTypeAdapter(EnumAdapter(DamageType::class, default = DamageType.DAMAGE)) registerTypeAdapter(EnumAdapter(DamageType::class, default = DamageType.DAMAGE))
registerTypeAdapter(InventoryIcon.Companion)
registerTypeAdapterFactory(IArmorItemDefinition.Frames.Factory)
registerTypeAdapterFactory(AssetPath.Companion) registerTypeAdapterFactory(AssetPath.Companion)
registerTypeAdapter(SpriteReference.Companion) registerTypeAdapter(SpriteReference.Companion)
registerTypeAdapterFactory(AssetReference.Companion) registerTypeAdapterFactory(AssetReference.Companion)
registerTypeAdapter(ItemStack.Adapter(this@Starbound))
registerTypeAdapterFactory(TreasurePoolDefinition.Companion)
registerTypeAdapterFactory(UniverseChunk.Companion) registerTypeAdapterFactory(UniverseChunk.Companion)
registerTypeAdapter(Image.Companion) registerTypeAdapter(Image.Companion)
@ -375,51 +375,6 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
create() create()
} }
data class ItemConfig(
val directory: String?,
val parameters: JsonObject,
val config: JsonObject
)
fun itemConfig(name: String, parameters: JsonObject, level: Double? = null, seed: Long? = null): ItemConfig {
val obj = Registries.items[name] ?: throw NoSuchElementException("No such item $name")
val directory = obj.file?.computeDirectory()
val parameters = parameters.deepCopy()
TODO()
}
fun item(name: String): ItemStack {
TODO()
}
fun item(name: String, count: Long): ItemStack {
TODO()
}
fun item(name: String, count: Long, parameters: JsonObject): ItemStack {
TODO()
}
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 var initializing = false
private set private set
var initialized = false var initialized = false
@ -435,8 +390,12 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
var loaded = 0 var loaded = 0
private set private set
@Volatile private val jsonAssetsCache = Caffeine.newBuilder()
var terminateLoading = false .maximumSize(4096L)
.expireAfterAccess(Duration.ofMinutes(5L))
.scheduler(this)
.executor(EXECUTOR)
.build<String, KOptional<JsonElement>>()
fun loadJsonAsset(path: String): JsonElement? { fun loadJsonAsset(path: String): JsonElement? {
val filename: String val filename: String
@ -450,24 +409,84 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
jsonPath = null jsonPath = null
} }
val file = locate(filename) val json = jsonAssetsCache.get(filename) {
val file = locate(it)
if (!file.isFile) if (!file.isFile)
return null return@get KOptional()
val findPatches = locateAll("$filename.patch")
KOptional(JsonPatch.apply(ELEMENTS_ADAPTER.read(file.jsonReader()), findPatches))
}.orNull() ?: return null
val pathTraverser = if (jsonPath == null) JsonPath.EMPTY else JsonPath.query(jsonPath) val pathTraverser = if (jsonPath == null) JsonPath.EMPTY else JsonPath.query(jsonPath)
return pathTraverser.get(gson.fromJson(file.reader(), JsonElement::class.java)) return pathTraverser.get(json)
} }
private val archivePaths = ArrayList<File>()
private val fileSystems = ArrayList<IStarboundFile>() private val fileSystems = ArrayList<IStarboundFile>()
private val toLoadPaks = ObjectArraySet<File>()
private val toLoadPaths = ObjectArraySet<File>()
fun addFilePath(path: File) { var loadingProgressText: String = ""
fileSystems.add(PhysicalFile(path)) private set
fun addArchive(path: File) {
toLoadPaks.add(path)
} }
fun addPak(pak: StarboundPak) { fun addPath(path: File) {
fileSystems.add(pak.root) toLoadPaths.add(path)
}
fun doBootstrap() {
if (!bootstrapped && !bootstrapping) {
bootstrapping = true
} else {
return
}
val fileSystems = ArrayList<Pair<Long, IStarboundFile>>()
for (path in toLoadPaks) {
LOGGER.info("Reading PAK archive $path")
try {
loadingProgressText = "Indexing $path"
val pak = StarboundPak(path) { _, s -> loadingProgressText = "Indexing $path: $s" }
val priority = pak.metadata.get("priority", 0L)
fileSystems.add(priority to pak.root)
} catch (err: Throwable) {
LOGGER.error("Error reading PAK archive $path. Not a PAK archive?")
}
}
for (path in toLoadPaths) {
val metadata: JsonObject
val metadataPath = File(path, "_metadata")
if (metadataPath.exists() && metadataPath.isFile) {
metadata = gson.fromJson(JsonReader(metadataPath.reader()), JsonObject::class.java)
} else {
metadata = JsonObject()
}
val priority = metadata.get("priority", 0L)
fileSystems.add(priority to PhysicalFile(path))
}
fileSystems.sortByDescending { it.first }
for ((_, fs) in fileSystems) {
this.fileSystems.add(fs)
}
LOGGER.info("Finished reading PAK archives")
bootstrapped = true
bootstrapping = false
}
fun bootstrapGame(): CompletableFuture<*> {
return submit { doBootstrap() }
} }
override fun exists(path: String): Boolean { override fun exists(path: String): Boolean {
@ -506,40 +525,25 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
return NonExistingFile(path.split("/").last(), fullPath = path) return NonExistingFile(path.split("/").last(), fullPath = path)
} }
fun locate(vararg path: String): IStarboundFile { fun locateAll(path: String): List<IStarboundFile> {
for (p in path) { @Suppress("name_shadowing")
val get = locate(p) var path = path
if (get.exists) { if (path[0] == '/') {
return get path = path.substring(1)
}
val files = ArrayList<IStarboundFile>()
for (fs in fileSystems.asReversed()) {
val file = fs.locate(path)
if (file.exists) {
files.add(file)
} }
} }
return NonExistingFile(path[0].split("/").last(), fullPath = path[0]) return files
}
/**
* Добавляет pak к чтению при initializeGame
*/
fun addPakPath(pak: File) {
archivePaths.add(pak)
}
fun doBootstrap() {
if (!bootstrapped && !bootstrapping) {
bootstrapping = true
} else {
return
}
for (path in archivePaths) {
LOGGER.info("Reading PAK archive $path")
addPak(StarboundPak(path))
}
LOGGER.info("Finished reading PAK archives")
bootstrapped = true
bootstrapping = false
} }
private fun doInitialize() { private fun doInitialize() {
@ -551,46 +555,34 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
doBootstrap() doBootstrap()
val ext2files = fileSystems.parallelStream() loadingProgressText = "Building file tree..."
.flatMap { it.explore() } val fileTree = HashMap<String, HashSet<IStarboundFile>>()
.filter { it.isFile } val patchTree = HashMap<String, ArrayList<IStarboundFile>>()
.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> { // finding assets, assets originating from top-most priority PAKs are overriding
return BiConsumer { t, u -> // same assets from other PAKs
t.computeIfAbsent(u.name.substringAfterLast('.'), Object2ObjectFunction { ArrayList() }).add(u) fileSystems.forEach {
} it.explore { file ->
} if (file.isFile)
fileTree.computeIfAbsent(file.name.substringAfterLast('.')) { HashSet() }.add(file)
}
}
override fun combiner(): BinaryOperator<Object2ObjectOpenHashMap<String, ArrayList<IStarboundFile>>> { // finding asset patches, patches from bottom-most priority PAKs are applied first
return BinaryOperator { t, u -> fileSystems.asReversed().forEach {
for ((k, v) in u) it.explore { file ->
t.computeIfAbsent(k, Object2ObjectFunction { ArrayList() }).addAll(v) if (file.isFile && file.name.endsWith(".patch"))
patchTree.computeIfAbsent(file.computeFullPath().substringAfterLast('.')) { ArrayList() }.add(file)
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)
}
})
loadingProgressText = "Dispatching load tasks..."
val tasks = ArrayList<Future<*>>() val tasks = ArrayList<Future<*>>()
tasks.addAll(Registries.load(ext2files)) tasks.addAll(Registries.load(fileTree, patchTree))
tasks.addAll(RecipeRegistry.load(ext2files)) tasks.addAll(RecipeRegistry.load(fileTree, patchTree))
tasks.addAll(Globals.load()) tasks.addAll(Globals.load())
tasks.add(VersionRegistry.load()) tasks.add(VersionRegistry.load(patchTree))
val total = tasks.size.toDouble() val total = tasks.size.toDouble()
toLoad = tasks.size toLoad = tasks.size
@ -599,11 +591,13 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
tasks.removeIf { it.isDone } tasks.removeIf { it.isDone }
loaded = toLoad - tasks.size loaded = toLoad - tasks.size
loadingProgress = (total - tasks.size) / total loadingProgress = (total - tasks.size) / total
loadingProgressText = "Loading JSON assets, $loaded / $toLoad"
LockSupport.parkNanos(5_000_000L) LockSupport.parkNanos(5_000_000L)
} }
Registries.finishLoad() Registries.finishLoad()
RecipeRegistry.finishLoad() RecipeRegistry.finishLoad()
ItemRegistry.finishLoad()
Registries.validate() Registries.validate()
@ -615,10 +609,6 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
return submit { doInitialize() } return submit { doInitialize() }
} }
fun bootstrapGame(): CompletableFuture<*> {
return submit { doBootstrap() }
}
private var fontPath: File? = null private var fontPath: File? = null
fun loadFont(): CompletableFuture<File> { fun loadFont(): CompletableFuture<File> {

View File

@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound
import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableMap
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import ru.dbotthepony.kstarbound.io.StarboundPak import ru.dbotthepony.kstarbound.io.StarboundPak
import ru.dbotthepony.kstarbound.util.sbIntern
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
@ -48,6 +49,9 @@ fun interface ISBFileLocator {
fun jsonReader(path: String) = locate(path).jsonReader() fun jsonReader(path: String) = locate(path).jsonReader()
} }
/**
* Two files should be considered equal if they have same absolute path
*/
interface IStarboundFile : ISBFileLocator { interface IStarboundFile : ISBFileLocator {
val exists: Boolean val exists: Boolean
val isDirectory: Boolean val isDirectory: Boolean
@ -75,6 +79,11 @@ interface IStarboundFile : ISBFileLocator {
return Stream.concat(Stream.of(this), children.values.stream().flatMap { it.explore() }) return Stream.concat(Stream.of(this), children.values.stream().flatMap { it.explore() })
} }
fun explore(visitor: (IStarboundFile) -> Unit) {
visitor(this)
children?.values?.forEach { it.explore(visitor) }
}
fun computeFullPath(): String { fun computeFullPath(): String {
var path = name var path = name
var parent = parent var parent = parent
@ -158,7 +167,7 @@ interface IStarboundFile : ISBFileLocator {
* @throws IllegalStateException if file is a directory * @throws IllegalStateException if file is a directory
* @throws FileNotFoundException if file does not exist * @throws FileNotFoundException if file does not exist
*/ */
@Deprecated("This does not reflect json patches") @Deprecated("Careful! This does not reflect json patches")
fun jsonReader(): JsonReader = JsonReader(reader()).also { it.isLenient = true } fun jsonReader(): JsonReader = JsonReader(reader()).also { it.isLenient = true }
/** /**
@ -253,29 +262,44 @@ fun getPathFilename(path: String): String {
return path.substringAfterLast('/') return path.substringAfterLast('/')
} }
class PhysicalFile(val real: File) : IStarboundFile { class PhysicalFile(val real: File, override val parent: PhysicalFile? = null) : IStarboundFile {
override val exists: Boolean override val exists: Boolean
get() = real.exists() get() = real.exists()
override val isDirectory: Boolean override val isDirectory: Boolean
get() = real.isDirectory get() = real.isDirectory
override val parent: PhysicalFile?
get() {
return PhysicalFile(real.parentFile ?: return null)
}
override val isFile: Boolean override val isFile: Boolean
get() = real.isFile get() = real.isFile
override val children: Map<String, PhysicalFile>? override val children: Map<String, PhysicalFile>?
get() { get() {
return real.list()?.stream()?.map { it to PhysicalFile(File(it)) }?.collect(ImmutableMap.toImmutableMap({ it.first }, { it.second })) return real.list()?.associate { it to PhysicalFile(File(it), this) }
} }
override val name: String override val name: String
get() = real.name get() = real.name
private val fullPatch by lazy { super.computeFullPath().sbIntern() }
private val directory by lazy { super.computeDirectory().sbIntern() }
override fun computeFullPath(): String {
return fullPatch
}
override fun computeDirectory(): String {
return directory
}
override fun open(): InputStream { override fun open(): InputStream {
return BufferedInputStream(real.inputStream()) return BufferedInputStream(real.inputStream())
} }
override fun equals(other: Any?): Boolean {
return other is IStarboundFile && computeFullPath() == other.computeFullPath()
}
override fun hashCode(): Int {
return computeFullPath().hashCode()
}
override fun toString(): String { override fun toString(): String {
return "PhysicalFile[$real]" return "PhysicalFile[$real]"
} }

View File

@ -31,7 +31,7 @@ object VersionRegistry {
private val adapter by lazy { Starbound.gson.getAdapter(VersionedJson::class.java) } private val adapter by lazy { Starbound.gson.getAdapter(VersionedJson::class.java) }
fun load(): Future<*> { fun load(patchTree: Map<String, List<IStarboundFile>>): Future<*> {
return Starbound.EXECUTOR.submit(Runnable { return Starbound.EXECUTOR.submit(Runnable {
val json = Starbound.loadJsonAsset("/versioning.config") ?: throw NoSuchElementException("Unable to load /versioning.config! Expect HUGE problems!") val json = Starbound.loadJsonAsset("/versioning.config") ?: throw NoSuchElementException("Unable to load /versioning.config! Expect HUGE problems!")
json as JsonObject json as JsonObject

View File

@ -38,9 +38,6 @@ class ClientConnection(val client: StarboundClient, type: ConnectionType) : Conn
client.mailbox.execute { task.invoke(client) } client.mailbox.execute { task.invoke(client) }
} }
override fun inGame() {
}
override fun toString(): String { override fun toString(): String {
val channel = if (hasChannel) channel.remoteAddress().toString() else "<no channel>" val channel = if (hasChannel) channel.remoteAddress().toString() else "<no channel>"
return "ClientConnection[ID=$connectionID channel=$channel]" return "ClientConnection[ID=$connectionID channel=$channel]"

View File

@ -666,7 +666,7 @@ class StarboundClient private constructor(val clientID: Int) : BlockableEventLoo
builder.builder.quad(0f, viewportHeight - 20f, viewportWidth * Starbound.loadingProgress.toFloat(), viewportHeight.toFloat()) { color(RGBAColor.GREEN) } builder.builder.quad(0f, viewportHeight - 20f, viewportWidth * Starbound.loadingProgress.toFloat(), viewportHeight.toFloat()) { color(RGBAColor.GREEN) }
GLFW.glfwSetWindowTitle(window, "KStarbound: Loading JSON assets ${Starbound.loaded} / ${Starbound.toLoad}") GLFW.glfwSetWindowTitle(window, "KStarbound: ${Starbound.loadingProgressText}")
renderedLoadingScreen = true renderedLoadingScreen = true
val runtime = Runtime.getRuntime() val runtime = Runtime.getRuntime()

View File

@ -8,6 +8,7 @@ import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.util.sbIntern
data class AssetPath(val path: String, val fullPath: String) { data class AssetPath(val path: String, val fullPath: String) {
constructor(path: String) : this(path, path) constructor(path: String) : this(path, path)
@ -28,7 +29,7 @@ data class AssetPath(val path: String, val fullPath: String) {
override fun read(`in`: JsonReader): AssetPath? { override fun read(`in`: JsonReader): AssetPath? {
val path = strings.read(`in`) ?: return null val path = strings.read(`in`) ?: return null
if (path == "") return null if (path == "") return null
return AssetPath(path, AssetPathStack.remap(path)) return AssetPath(path.sbIntern(), AssetPathStack.remap(path).sbIntern())
} }
} as TypeAdapter<T> } as TypeAdapter<T>
} }

View File

@ -15,6 +15,7 @@ import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.util.sbIntern
import java.lang.reflect.ParameterizedType import java.lang.reflect.ParameterizedType
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@ -64,7 +65,6 @@ class AssetReference<V> {
private val cache = Collections.synchronizedMap(HashMap<String, Pair<T, JsonElement>>()) private val cache = Collections.synchronizedMap(HashMap<String, Pair<T, JsonElement>>())
private val adapter = gson.getAdapter(TypeToken.get(param.actualTypeArguments[0])) as TypeAdapter<T> private val adapter = gson.getAdapter(TypeToken.get(param.actualTypeArguments[0])) as TypeAdapter<T>
private val strings = gson.getAdapter(String::class.java) private val strings = gson.getAdapter(String::class.java)
private val jsons = gson.getAdapter(JsonElement::class.java)
private val missing = Collections.synchronizedSet(ObjectOpenHashSet<String>()) private val missing = Collections.synchronizedSet(ObjectOpenHashSet<String>())
private val logger = LogManager.getLogger() private val logger = LogManager.getLogger()
@ -84,7 +84,7 @@ class AssetReference<V> {
val get = cache[fullPath] val get = cache[fullPath]
if (get != null) if (get != null)
return AssetReference(path, fullPath, get.first, get.second) return AssetReference(path.sbIntern(), fullPath.sbIntern(), get.first, get.second)
if (fullPath in missing) if (fullPath in missing)
return null return null
@ -92,9 +92,10 @@ class AssetReference<V> {
val json = Starbound.loadJsonAsset(fullPath) val json = Starbound.loadJsonAsset(fullPath)
if (json == null) { if (json == null) {
logger.error("JSON asset does not exist: $fullPath") if (missing.add(fullPath))
missing.add(fullPath) logger.error("JSON asset does not exist: $fullPath")
return AssetReference(path, fullPath, null, null)
return AssetReference(path.sbIntern(), fullPath.sbIntern(), null, null)
} }
val value = AssetPathStack(fullPath.substringBefore(':').substringBeforeLast('/')) { val value = AssetPathStack(fullPath.substringBefore(':').substringBeforeLast('/')) {
@ -103,13 +104,13 @@ class AssetReference<V> {
if (value == null) { if (value == null) {
missing.add(fullPath) missing.add(fullPath)
return AssetReference(path, fullPath, null, json) return AssetReference(path.sbIntern(), fullPath.sbIntern(), null, json)
} }
cache[fullPath] = value to json cache[fullPath] = value to json
return AssetReference(path, fullPath, value, json) return AssetReference(path.sbIntern(), fullPath.sbIntern(), value, json)
} else { } else {
val json = jsons.read(`in`)!! val json = Starbound.ELEMENTS_ADAPTER.read(`in`)
val value = adapter.read(JsonTreeReader(json)) ?: return null val value = adapter.read(JsonTreeReader(json)) ?: return null
return AssetReference(null, null, value, json) return AssetReference(null, null, value, json)
} }

View File

@ -1,9 +1,6 @@
package ru.dbotthepony.kstarbound.defs package ru.dbotthepony.kstarbound.defs
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
data class CurrencyDefinition( data class CurrencyDefinition(
val representativeItem: Registry.Ref<IItemDefinition>, val representativeItem: String,
val playerMax: Long = 999999, val playerMax: Long = 999999,
) )

View File

@ -22,6 +22,9 @@ import ru.dbotthepony.kstarbound.defs.image.SpriteReference
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.gson.contains import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.math.Line2d import ru.dbotthepony.kstarbound.math.Line2d
@JsonAdapter(Drawable.Adapter::class) @JsonAdapter(Drawable.Adapter::class)
@ -43,6 +46,18 @@ sealed class Drawable(val position: Vector2f, val color: RGBAColor, val fullbrig
override fun flop(): Drawable { override fun flop(): Drawable {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override fun boundingBox(cropImages: Boolean): AABB {
TODO("Not yet implemented")
}
override fun scale(scale: Vector2f): Drawable {
TODO("Not yet implemented")
}
override fun translate(translation: Vector2f): Drawable {
TODO("Not yet implemented")
}
} }
class Poly( class Poly(
@ -58,6 +73,18 @@ sealed class Drawable(val position: Vector2f, val color: RGBAColor, val fullbrig
override fun flop(): Drawable { override fun flop(): Drawable {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override fun boundingBox(cropImages: Boolean): AABB {
TODO("Not yet implemented")
}
override fun scale(scale: Vector2f): Drawable {
TODO("Not yet implemented")
}
override fun translate(translation: Vector2f): Drawable {
TODO("Not yet implemented")
}
} }
class Image( class Image(
@ -81,35 +108,75 @@ sealed class Drawable(val position: Vector2f, val color: RGBAColor, val fullbrig
return Image(path, transform.flatMap({ it.copy().scale(-1f, 1f) }, { it.copy(mirrored = !it.mirrored) }), position, color, fullbright) return Image(path, transform.flatMap({ it.copy().scale(-1f, 1f) }, { it.copy(mirrored = !it.mirrored) }), position, color, fullbright)
} }
override fun scale(scale: Vector2f): Drawable {
return Image(path, transform.flatMap({ it.scale(scale) }, { it.copy(scale = Either.right(it.scale.map({ scale * it }, { scale * it }))) }), position, color, fullbright)
}
override fun translate(translation: Vector2f): Drawable {
return Image(path, transform, position + translation, color, fullbright)
}
private fun getTransforms(sprite: ru.dbotthepony.kstarbound.defs.image.Image.Sprite) = transform.map({ it.copy() }, {
val mat = Matrix3f.identity()
it.scale.map({ mat.scale(it / PIXELS_IN_STARBOUND_UNITf, it / PIXELS_IN_STARBOUND_UNITf) }, { mat.scale(it / PIXELS_IN_STARBOUND_UNITf) })
if (it.centered) {
mat.translate(sprite.width / -2f, sprite.height / -2f)
}
if (it.rotation != 0f) {
mat.rotateAroundZ(it.rotation)
}
if (it.mirrored) {
mat.translate(sprite.width.toFloat(), 0f)
mat.scale(-1f, 1f)
}
mat
})
// Original engine here calculates bounding box wrong
// if "cropImages" is false
override fun boundingBox(cropImages: Boolean): AABB {
val sprite = path.sprite ?: return AABB.ZERO
var result: AABB
if (cropImages) {
result = AABB(
Vector2d(sprite.nonEmptyRegion.x.toDouble(), sprite.nonEmptyRegion.y.toDouble()),
Vector2d(sprite.nonEmptyRegion.z.toDouble(), sprite.nonEmptyRegion.w.toDouble()),
)
} else {
result = AABB(
Vector2d.ZERO,
Vector2d(sprite.width.toDouble(), sprite.height.toDouble())
)
}
val transforms = getTransforms(sprite)
result = result.expand((Vector2f(result.mins.x.toFloat(), result.mins.y.toFloat()) * transforms).toDoubleVector())
result = result.expand((Vector2f(result.mins.x.toFloat(), result.maxs.y.toFloat()) * transforms).toDoubleVector())
result = result.expand((Vector2f(result.maxs.x.toFloat(), result.mins.y.toFloat()) * transforms).toDoubleVector())
result = result.expand((Vector2f(result.maxs.x.toFloat(), result.maxs.y.toFloat()) * transforms).toDoubleVector())
result += position.toDoubleVector()
return result
}
override fun render(client: StarboundClient, layer: IGeometryLayer, x: Float, y: Float) { override fun render(client: StarboundClient, layer: IGeometryLayer, x: Float, y: Float) {
val sprite = path.sprite ?: return val sprite = path.sprite ?: return
val texture = path.image!!.texture val texture = path.image!!.texture
val program = if (fullbright) client.programs.positionTexture else client.programs.positionTextureLightmap val program = if (fullbright) client.programs.positionTexture else client.programs.positionTextureLightmap
val mat = transform.map({ it.copy() }, { val mat = getTransforms(sprite)
val mat = Matrix3f.identity()
it.scale.map({ mat.scale(it / PIXELS_IN_STARBOUND_UNITf, it / PIXELS_IN_STARBOUND_UNITf) }, { mat.scale(it / PIXELS_IN_STARBOUND_UNITf) })
if (it.centered) {
mat.translate(sprite.width / -2f, sprite.height / -2f)
}
if (it.rotation != 0f) {
mat.rotateAroundZ(it.rotation)
}
if (it.mirrored) {
mat.translate(sprite.width.toFloat(), 0f)
mat.scale(-1f, 1f)
}
mat
})
val builder = layer.getBuilder(program.config(texture)) val builder = layer.getBuilder(program.config(texture))
mat.preTranslate(x, y) mat.preTranslate(position.x + x, position.y + y)
client.stack.last().mulIntoOther(mat) client.stack.last().mulIntoOther(mat)
builder.mode(GeometryType.QUADS) builder.mode(GeometryType.QUADS)
@ -126,6 +193,18 @@ sealed class Drawable(val position: Vector2f, val color: RGBAColor, val fullbrig
override fun flop(): Drawable { override fun flop(): Drawable {
return this return this
} }
override fun boundingBox(cropImages: Boolean): AABB {
return AABB.ZERO
}
override fun scale(scale: Vector2f): Drawable {
return this
}
override fun translate(translation: Vector2f): Drawable {
return Empty(position + translation, color, fullbright)
}
} }
open fun with(values: (String) -> String?): Drawable { open fun with(values: (String) -> String?): Drawable {
@ -134,6 +213,19 @@ sealed class Drawable(val position: Vector2f, val color: RGBAColor, val fullbrig
abstract fun render(client: StarboundClient = StarboundClient.current(), layer: IGeometryLayer, x: Float = 0f, y: Float = 0f) abstract fun render(client: StarboundClient = StarboundClient.current(), layer: IGeometryLayer, x: Float = 0f, y: Float = 0f)
abstract fun boundingBox(cropImages: Boolean = false): AABB
abstract fun scale(scale: Vector2f): Drawable
fun scale(scale: Float): Drawable {
if (scale == 1f)
return this
return scale(Vector2f(scale, scale))
}
abstract fun translate(translation: Vector2f): Drawable
/** /**
* mirror along X axis * mirror along X axis
*/ */
@ -141,12 +233,12 @@ sealed class Drawable(val position: Vector2f, val color: RGBAColor, val fullbrig
companion object { companion object {
val EMPTY = Empty() val EMPTY = Empty()
val CENTERED = Transformations(true)
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
} }
class Adapter(gson: Gson) : TypeAdapter<Drawable>() { class Adapter(gson: Gson) : TypeAdapter<Drawable>() {
private val lines = gson.getAdapter(Line2d::class.java) private val lines = gson.getAdapter(Line2d::class.java)
private val objects = gson.getAdapter(JsonObject::class.java)
private val vectors = gson.getAdapter(Vector2f::class.java) private val vectors = gson.getAdapter(Vector2f::class.java)
private val vectors3 = gson.getAdapter(Vector3f::class.java) private val vectors3 = gson.getAdapter(Vector3f::class.java)
private val colors = gson.getAdapter(RGBAColor::class.java) private val colors = gson.getAdapter(RGBAColor::class.java)
@ -162,7 +254,7 @@ sealed class Drawable(val position: Vector2f, val color: RGBAColor, val fullbrig
if (`in`.consumeNull()) { if (`in`.consumeNull()) {
return EMPTY return EMPTY
} else { } else {
val value = objects.read(`in`)!! val value = Starbound.ELEMENTS_ADAPTER.objects.read(`in`)!!
val position = value["position"]?.let { vectors.fromJsonTree(it) } ?: Vector2f.ZERO val position = value["position"]?.let { vectors.fromJsonTree(it) } ?: Vector2f.ZERO
val color = value["color"]?.let { colors.fromJsonTree(it) } ?: RGBAColor.WHITE val color = value["color"]?.let { colors.fromJsonTree(it) } ?: RGBAColor.WHITE

View File

@ -229,8 +229,6 @@ class Json2Function(
override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? { override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
if (type.rawType == Json2Function::class.java) { if (type.rawType == Json2Function::class.java) {
return object : TypeAdapter<Json2Function>() { return object : TypeAdapter<Json2Function>() {
val elements = gson.getAdapter(JsonArray::class.java)
override fun write(out: JsonWriter, value: Json2Function?) { override fun write(out: JsonWriter, value: Json2Function?) {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
@ -247,7 +245,7 @@ class Json2Function(
val xRanges = ArrayList<Ranges>() val xRanges = ArrayList<Ranges>()
val yRanges = ArrayList<Ranges>() val yRanges = ArrayList<Ranges>()
val elements = elements.read(reader) val elements = Starbound.ELEMENTS_ADAPTER.arrays.read(reader)
for ((i, row) in elements.withIndex()) { for ((i, row) in elements.withIndex()) {
if (row !is JsonArray) { if (row !is JsonArray) {

View File

@ -4,8 +4,8 @@ import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableSet import com.google.common.collect.ImmutableSet
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.image.SpriteReference import ru.dbotthepony.kstarbound.defs.image.SpriteReference
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
import ru.dbotthepony.kstarbound.defs.actor.player.BlueprintLearnList import ru.dbotthepony.kstarbound.defs.actor.player.BlueprintLearnList
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@JsonFactory @JsonFactory
@ -38,8 +38,8 @@ data class Species(
val characterImage: SpriteReference, val characterImage: SpriteReference,
val hairGroup: String? = null, val hairGroup: String? = null,
val hair: ImmutableSet<String>, val hair: ImmutableSet<String>,
val shirt: ImmutableSet<Registry.Ref<IItemDefinition>>, val shirt: ImmutableSet<ItemDescriptor>,
val pants: ImmutableSet<Registry.Ref<IItemDefinition>>, val pants: ImmutableSet<ItemDescriptor>,
val facialHairGroup: String? = null, val facialHairGroup: String? = null,
val facialHair: ImmutableSet<String> = ImmutableSet.of(), val facialHair: ImmutableSet<String> = ImmutableSet.of(),
val facialMaskGroup: String? = null, val facialMaskGroup: String? = null,

View File

@ -8,7 +8,7 @@ import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@JsonFactory @JsonFactory
data class StatusEffectDefinition( data class StatusEffectDefinition(
val name: String, val name: String,
val defaultDuration: Double, val defaultDuration: Double = 0.0,
val blockingStat: String? = null, val blockingStat: String? = null,
val label: String? = null, val label: String? = null,
val icon: SpriteReference? = null, val icon: SpriteReference? = null,

View File

@ -12,6 +12,7 @@ import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.io.readBinaryString import ru.dbotthepony.kommons.io.readBinaryString
import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.io.readInternedString import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.network.syncher.legacyCodec import ru.dbotthepony.kstarbound.network.syncher.legacyCodec
import ru.dbotthepony.kstarbound.network.syncher.nativeCodec import ru.dbotthepony.kstarbound.network.syncher.nativeCodec
@ -21,8 +22,6 @@ import java.io.DataOutputStream
@JsonAdapter(StatModifier.Adapter::class) @JsonAdapter(StatModifier.Adapter::class)
data class StatModifier(val stat: String, val value: Double, val type: StatModifierType) { data class StatModifier(val stat: String, val value: Double, val type: StatModifierType) {
class Adapter(gson: Gson) : TypeAdapter<StatModifier>() { class Adapter(gson: Gson) : TypeAdapter<StatModifier>() {
private val objects = gson.getAdapter(JsonObject::class.java)
override fun write(out: JsonWriter, value: StatModifier?) { override fun write(out: JsonWriter, value: StatModifier?) {
if (value == null) { if (value == null) {
out.nullValue() out.nullValue()
@ -40,7 +39,7 @@ data class StatModifier(val stat: String, val value: Double, val type: StatModif
if (`in`.consumeNull()) { if (`in`.consumeNull()) {
return null return null
} else { } else {
val read = objects.read(`in`) val read = Starbound.ELEMENTS_ADAPTER.objects.read(`in`)
val stat = read["stat"]?.asString ?: throw JsonSyntaxException("No stat to modify specified") val stat = read["stat"]?.asString ?: throw JsonSyntaxException("No stat to modify specified")

View File

@ -17,9 +17,9 @@ data class BagFilterConfig(
return false return false
if (typeBlacklist != null) { if (typeBlacklist != null) {
return !typeBlacklist.contains(t.config.value!!.category) return !typeBlacklist.contains(t.category)
} else if (typeWhitelist != null) { } else if (typeWhitelist != null) {
return typeWhitelist.contains(t.config.value!!.category) return typeWhitelist.contains(t.category)
} }
return true return true

View File

@ -11,15 +11,14 @@ import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap
import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
class BlueprintLearnList private constructor(private val tiers: Int2ObjectArrayMap<ImmutableList<Entry>>) { class BlueprintLearnList private constructor(private val tiers: Int2ObjectArrayMap<ImmutableList<Entry>>) {
constructor(tiers: Map<Int, List<Entry>>) : this(Int2ObjectArrayMap<ImmutableList<Entry>>().also { for ((k, v) in tiers.entries) it.put(k, ImmutableList.copyOf(v)) }) constructor(tiers: Map<Int, List<Entry>>) : this(Int2ObjectArrayMap<ImmutableList<Entry>>().also { for ((k, v) in tiers.entries) it.put(k, ImmutableList.copyOf(v)) })
@JsonFactory @JsonFactory
data class Entry(val item: Registry.Ref<IItemDefinition>) data class Entry(val item: ItemDescriptor)
operator fun get(tier: Int): List<Entry> { operator fun get(tier: Int): List<Entry> {
return tiers.getOrDefault(tier, ImmutableList.of()) return tiers.getOrDefault(tier, ImmutableList.of())

View File

@ -2,10 +2,9 @@ package ru.dbotthepony.kstarbound.defs.actor.player
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableMap
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.AssetPath import ru.dbotthepony.kstarbound.defs.AssetPath
import ru.dbotthepony.kstarbound.defs.IScriptable import ru.dbotthepony.kstarbound.defs.IScriptable
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@JsonFactory @JsonFactory
@ -13,8 +12,8 @@ data class DeploymentConfig(
override val scripts: ImmutableList<AssetPath>, override val scripts: ImmutableList<AssetPath>,
override val scriptDelta: Int, override val scriptDelta: Int,
val starterMechSet: ImmutableMap<String, Registry.Ref<IItemDefinition>>, val starterMechSet: ImmutableMap<String, ItemDescriptor>,
val speciesStarterMechBody: ImmutableMap<String, Registry.Ref<IItemDefinition>>, val speciesStarterMechBody: ImmutableMap<String, ItemDescriptor>,
val enemyDetectRadius: Double, val enemyDetectRadius: Double,
val enemyDetectTypeNames: ImmutableList<String>, val enemyDetectTypeNames: ImmutableList<String>,

View File

@ -14,7 +14,7 @@ import ru.dbotthepony.kstarbound.defs.Species
import ru.dbotthepony.kstarbound.defs.actor.StatusControllerConfig import ru.dbotthepony.kstarbound.defs.actor.StatusControllerConfig
import ru.dbotthepony.kstarbound.util.SBPattern import ru.dbotthepony.kstarbound.util.SBPattern
import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@JsonFactory @JsonFactory
@ -27,11 +27,11 @@ data class PlayerConfig(
val species: ImmutableSet<Registry.Ref<Species>>, val species: ImmutableSet<Registry.Ref<Species>>,
val nametagColor: RGBAColor, val nametagColor: RGBAColor,
val ageItemsEvery: Int, val ageItemsEvery: Int,
val defaultItems: ImmutableSet<Registry.Ref<IItemDefinition>>, val defaultItems: ImmutableSet<ItemDescriptor>,
val defaultBlueprints: BlueprintLearnList, val defaultBlueprints: BlueprintLearnList,
val defaultCodexes: ImmutableMap<String, ImmutableList<Registry.Ref<IItemDefinition>>>, val defaultCodexes: ImmutableMap<String, ImmutableList<ItemDescriptor>>,
val metaBoundBox: AABB, val metaBoundBox: AABB,
val movementParameters: ActorMovementParameters, val movementParameters: ActorMovementParameters,
val zeroGMovementParameters: ActorMovementParameters, val zeroGMovementParameters: ActorMovementParameters,

View File

@ -9,7 +9,6 @@ import ru.dbotthepony.kstarbound.json.builder.JsonFactory
data class TechDefinition( data class TechDefinition(
val name: String, val name: String,
val type: String, val type: String,
val chipCost: Int,
override val scriptDelta: Int = 1, override val scriptDelta: Int = 1,
override val scripts: ImmutableList<AssetPath> override val scripts: ImmutableList<AssetPath>
) : IScriptable ) : IScriptable

View File

@ -52,7 +52,7 @@ data class AnimatedPartsDefinition(
v.index = index++ v.index = index++
if (v.mode == AnimationMode.TRANSITION && v.transition !in states) { if (v.mode == AnimationMode.TRANSITION && v.transition !in states) {
throw IllegalArgumentException("State $k has specified TRANSITION as mode, however, it points to non-existing state ${v.transition}!") throw IllegalArgumentException("State '$k' has specified TRANSITION as mode, however, it points to non-existing state '${v.transition}'!")
} }
} }
} }

View File

@ -12,6 +12,7 @@ import com.google.gson.stream.JsonWriter
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
@ -47,14 +48,12 @@ abstract class DungeonBrush {
} }
class Adapter(gson: Gson) : TypeAdapter<DungeonBrush>() { class Adapter(gson: Gson) : TypeAdapter<DungeonBrush>() {
private val arrays = gson.getAdapter(JsonArray::class.java)
override fun write(out: JsonWriter, value: DungeonBrush) { override fun write(out: JsonWriter, value: DungeonBrush) {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override fun read(`in`: JsonReader): DungeonBrush { override fun read(`in`: JsonReader): DungeonBrush {
val read = arrays.read(`in`) val read = Starbound.ELEMENTS_ADAPTER.arrays.read(`in`)
// don't delegate to EnumAdapter since this can have bad consequences // don't delegate to EnumAdapter since this can have bad consequences
// such as defaulting to CLEAR action and printing a warning in console // such as defaulting to CLEAR action and printing a warning in console
val type = DungeonBrushType.entries.firstOrNull { it.jsonName == read[0].asString } ?: throw NoSuchElementException("Unknown brush type ${read[0].asString}!") val type = DungeonBrushType.entries.firstOrNull { it.jsonName == read[0].asString } ?: throw NoSuchElementException("Unknown brush type ${read[0].asString}!")

View File

@ -158,10 +158,6 @@ enum class DungeonBrushType(override val jsonName: String) : IStringSerializable
Starbound.gson.getAdapter(DungeonBrush.WorldObject.Extra::class.java) Starbound.gson.getAdapter(DungeonBrush.WorldObject.Extra::class.java)
} }
private val adapterObject by lazy {
Starbound.gson.getAdapter(JsonObject::class.java)
}
override fun createLegacy(json: JsonArray): DungeonBrush { override fun createLegacy(json: JsonArray): DungeonBrush {
if (json.size() > 2) { if (json.size() > 2) {
return DungeonBrush.WorldObject(adapter0.fromJsonTree(json[1]), adapter1.fromJsonTree(json[2])) return DungeonBrush.WorldObject(adapter0.fromJsonTree(json[1]), adapter1.fromJsonTree(json[2]))
@ -183,7 +179,7 @@ enum class DungeonBrushType(override val jsonName: String) : IStringSerializable
if (parameters == null || parameters.isJsonNull) if (parameters == null || parameters.isJsonNull)
parameters = JsonObject() parameters = JsonObject()
else if (parameters is JsonPrimitive) else if (parameters is JsonPrimitive)
parameters = adapterObject.fromJson(parameters.asString) parameters = Starbound.ELEMENTS_ADAPTER.objects.fromJson(parameters.asString)
return DungeonBrush.WorldObject(ref, direction, parameters as JsonObject) return DungeonBrush.WorldObject(ref, direction, parameters as JsonObject)
} }
@ -251,10 +247,6 @@ enum class DungeonBrushType(override val jsonName: String) : IStringSerializable
return DungeonBrush.NPC(json[1].asJsonObject) return DungeonBrush.NPC(json[1].asJsonObject)
} }
private val adapterObject by lazy {
Starbound.gson.getAdapter(JsonObject::class.java)
}
override fun readTiled(json: JsonObject): DungeonBrush? { override fun readTiled(json: JsonObject): DungeonBrush? {
if ("npc" in json) { if ("npc" in json) {
val brush = JsonObject() val brush = JsonObject()
@ -278,7 +270,7 @@ enum class DungeonBrushType(override val jsonName: String) : IStringSerializable
if (parameters == null || parameters.isJsonNull) if (parameters == null || parameters.isJsonNull)
parameters = JsonObject() parameters = JsonObject()
else if (parameters is JsonPrimitive) else if (parameters is JsonPrimitive)
parameters = adapterObject.fromJson(parameters.asString) parameters = Starbound.ELEMENTS_ADAPTER.objects.fromJson(parameters.asString)
brush["parameters"] = parameters brush["parameters"] = parameters
return DungeonBrush.NPC(brush) return DungeonBrush.NPC(brush)
@ -299,7 +291,7 @@ enum class DungeonBrushType(override val jsonName: String) : IStringSerializable
if (parameters == null || parameters.isJsonNull) if (parameters == null || parameters.isJsonNull)
parameters = JsonObject() parameters = JsonObject()
else if (parameters is JsonPrimitive) else if (parameters is JsonPrimitive)
parameters = adapterObject.fromJson(parameters.asString) parameters = Starbound.ELEMENTS_ADAPTER.objects.fromJson(parameters.asString)
brush["parameters"] = parameters brush["parameters"] = parameters
return DungeonBrush.NPC(brush) return DungeonBrush.NPC(brush)
@ -314,10 +306,6 @@ enum class DungeonBrushType(override val jsonName: String) : IStringSerializable
return DungeonBrush.Stagehand(json[1].asJsonObject) return DungeonBrush.Stagehand(json[1].asJsonObject)
} }
private val adapterObject by lazy {
Starbound.gson.getAdapter(JsonObject::class.java)
}
override fun readTiled(json: JsonObject): DungeonBrush? { override fun readTiled(json: JsonObject): DungeonBrush? {
if ("stagehand" in json) { if ("stagehand" in json) {
val brush = JsonObject() val brush = JsonObject()
@ -329,7 +317,7 @@ enum class DungeonBrushType(override val jsonName: String) : IStringSerializable
if (parameters == null || parameters.isJsonNull) if (parameters == null || parameters.isJsonNull)
parameters = JsonObject() parameters = JsonObject()
else if (parameters is JsonPrimitive) else if (parameters is JsonPrimitive)
parameters = adapterObject.fromJson(parameters.asString) parameters = Starbound.ELEMENTS_ADAPTER.objects.fromJson(parameters.asString)
brush["parameters"] = parameters brush["parameters"] = parameters

View File

@ -11,6 +11,7 @@ import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.contains import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.tile.isEmptyLiquid import ru.dbotthepony.kstarbound.defs.tile.isEmptyLiquid
import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid
@ -332,7 +333,6 @@ abstract class DungeonRule {
} }
class Adapter(gson: Gson) : TypeAdapter<DungeonRule>() { class Adapter(gson: Gson) : TypeAdapter<DungeonRule>() {
private val arrays = gson.getAdapter(JsonArray::class.java)
private val types = gson.getAdapter(Type::class.java) private val types = gson.getAdapter(Type::class.java)
override fun write(out: JsonWriter, value: DungeonRule) { override fun write(out: JsonWriter, value: DungeonRule) {
@ -340,7 +340,7 @@ abstract class DungeonRule {
} }
override fun read(`in`: JsonReader): DungeonRule { override fun read(`in`: JsonReader): DungeonRule {
val read = arrays.read(`in`) val read = Starbound.ELEMENTS_ADAPTER.arrays.read(`in`)
if (read.isEmpty) { if (read.isEmpty) {
throw JsonSyntaxException("Empty rule") throw JsonSyntaxException("Empty rule")

View File

@ -98,7 +98,6 @@ data class DungeonTile(
// weird custom parsing rules but ok // weird custom parsing rules but ok
class Adapter(gson: Gson) : TypeAdapter<DungeonTile>() { class Adapter(gson: Gson) : TypeAdapter<DungeonTile>() {
private val objects = gson.getAdapter(JsonObject::class.java)
private val data = gson.getAdapter(BasicData::class.java) private val data = gson.getAdapter(BasicData::class.java)
private val values = gson.getAdapter<Either<Vector4i, String>>() private val values = gson.getAdapter<Either<Vector4i, String>>()
@ -119,7 +118,7 @@ data class DungeonTile(
} }
override fun read(`in`: JsonReader): DungeonTile { override fun read(`in`: JsonReader): DungeonTile {
val read = objects.read(`in`) val read = Starbound.ELEMENTS_ADAPTER.objects.read(`in`)
val (brushes, rules, direction, rawIndex, connector, connectForwardOnly) = data.fromJsonTree(read) val (brushes, rules, direction, rawIndex, connector, connectForwardOnly) = data.fromJsonTree(read)
var connectorIndex = rawIndex var connectorIndex = rawIndex

View File

@ -490,13 +490,17 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar
parent.eventLoop.supplyAsync { parent.eventLoop.supplyAsync {
for ((obj, direction) in placedObjects) { for ((obj, direction) in placedObjects) {
val orientation = obj!!.config.value.findValidOrientation(parent, obj.tilePosition, direction) try {
val orientation = obj!!.config.value.findValidOrientation(parent, obj.tilePosition, direction)
if (orientation != -1) { if (orientation != -1) {
obj.orientationIndex = orientation.toLong() obj.orientationIndex = orientation.toLong()
obj.joinWorld(parent) obj.joinWorld(parent)
} else { } else {
LOGGER.error("Tried to place object ${obj.config.key} at ${obj.tilePosition}, but it can't be placed there!") LOGGER.error("Tried to place object ${obj.config.key} at ${obj.tilePosition}, but it can't be placed there!")
}
} catch (err: Throwable) {
LOGGER.error("Exception while putting dungeon object $obj at ${obj!!.tilePosition}", err)
} }
} }
}.await() }.await()

View File

@ -28,34 +28,18 @@ class ImagePartReader(part: DungeonPart, val images: ImmutableList<Image>) : Par
for ((i, image) in images.withIndex()) { for ((i, image) in images.withIndex()) {
// go around image cache, since image will be loaded exactly once // go around image cache, since image will be loaded exactly once
// and then forgotten // and then forgotten
val (bytes, width, height, channels) = Image.readImageDirect(image.source) val (bytes, width, height) = Image.readImageDirect(image.source)
val tileData = IntArray(width * height) val tileData = IntArray(width * height)
if (channels == 3) { for (x in 0 until image.width) {
// RGB for (y in 0 until image.height) {
for (x in 0 until image.width) { val offset = (x + y * image.width) * 4
for (y in 0 until image.height) {
val offset = (x + y * image.width) * channels
// flip image as we go // flip image as we go
tileData[x + (image.height - y - 1) * image.width] = bytes[offset].toInt().and(0xFF) or tileData[x + (image.height - y - 1) * image.width] = bytes[offset].toInt().and(0xFF) or
bytes[offset + 1].toInt().and(0xFF).shl(8) or bytes[offset + 1].toInt().and(0xFF).shl(8) or
bytes[offset + 2].toInt().and(0xFF).shl(16) or -0x1000000 // leading alpha as 255 bytes[offset + 2].toInt().and(0xFF).shl(16) or
} bytes[offset + 3].toInt().and(0xFF).shl(24)
}
} else if (channels == 4) {
// RGBA
for (x in 0 until image.width) {
for (y in 0 until image.height) {
val offset = (x + y * image.width) * channels
// flip image as we go
tileData[x + (image.height - y - 1) * image.width] = bytes[offset].toInt().and(0xFF) or
bytes[offset + 1].toInt().and(0xFF).shl(8) or
bytes[offset + 2].toInt().and(0xFF).shl(16) or
bytes[offset + 3].toInt().and(0xFF).shl(24)
}
} }
} }

View File

@ -36,6 +36,7 @@ import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.gson.contains import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.getObject import ru.dbotthepony.kommons.gson.getObject
import ru.dbotthepony.kstarbound.json.JsonPatch
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.lang.ref.Reference import java.lang.ref.Reference
@ -53,13 +54,11 @@ class Image private constructor(
val path: String, val path: String,
val width: Int, val width: Int,
val height: Int, val height: Int,
val amountOfChannels: Int,
spritesData: Pair<List<DataSprite>, IStarboundFile>? spritesData: Pair<List<DataSprite>, IStarboundFile>?
) { ) {
init { init {
check(width >= 0) { "Invalid width $width" } check(width >= 0) { "Invalid width $width" }
check(height >= 0) { "Invalid height $height" } check(height >= 0) { "Invalid height $height" }
check(amountOfChannels in 1 .. 4) { "Unknown number of channels $amountOfChannels" }
} }
private val spritesInternal = LinkedHashMap<String, Sprite>() private val spritesInternal = LinkedHashMap<String, Sprite>()
@ -139,17 +138,10 @@ class Image private constructor(
val value = client.named2DTextures0.get(this) { val value = client.named2DTextures0.get(this) {
client.named2DTextures1.get(this) { client.named2DTextures1.get(this) {
val (memFormat, fileFormat) = when (amountOfChannels) { val tex = GLTexture2D(width, height, GL45.GL_RGBA8)
1 -> GL45.GL_R8 to GL45.GL_RED
3 -> GL45.GL_RGB8 to GL45.GL_RGB
4 -> GL45.GL_RGBA8 to GL45.GL_RGBA
else -> throw IllegalArgumentException("Unknown amount of channels in $it: $amountOfChannels")
}
val tex = GLTexture2D(width, height, memFormat)
data.thenApplyAsync({ data.thenApplyAsync({
tex.upload(fileFormat, GL45.GL_UNSIGNED_BYTE, it) tex.upload(GL45.GL_RGBA, GL45.GL_UNSIGNED_BYTE, it)
tex.textureMinFilter = GL45.GL_NEAREST tex.textureMinFilter = GL45.GL_NEAREST
tex.textureMagFilter = GL45.GL_NEAREST tex.textureMagFilter = GL45.GL_NEAREST
@ -211,26 +203,13 @@ class Image private constructor(
operator fun get(x: Int, y: Int): Int { operator fun get(x: Int, y: Int): Int {
require(x in 0 until width && y in 0 until height) { "Position out of bounds: $x $y" } require(x in 0 until width && y in 0 until height) { "Position out of bounds: $x $y" }
val offset = (this.y + y) * this@Image.width * amountOfChannels + (this.x + x) * amountOfChannels val offset = (this.y + y) * this@Image.width * 4 + (this.x + x) * 4
val data = data.join() val data = data.join()
when (amountOfChannels) { return data[offset].toInt().and(0xFF) or // red
4 -> return data[offset].toInt().and(0xFF) or // red data[offset + 1].toInt().and(0xFF).shl(8) or // green
data[offset + 1].toInt().and(0xFF).shl(8) or // green data[offset + 2].toInt().and(0xFF).shl(16) or // blue
data[offset + 2].toInt().and(0xFF).shl(16) or // blue data[offset + 3].toInt().and(0xFF).shl(24) // alpha
data[offset + 3].toInt().and(0xFF).shl(24) // alpha
3 -> return data[offset].toInt().and(0xFF) or
data[offset + 1].toInt().and(0xFF).shl(8) or
data[offset + 2].toInt().and(0xFF).shl(16) or -0x1000000 // leading alpha as 255
2 -> return data[offset].toInt().and(0xFF) or
data[offset + 1].toInt().and(0xFF).shl(8)
1 -> return data[offset].toInt()
else -> throw IllegalStateException()
}
} }
/** /**
@ -248,47 +227,44 @@ class Image private constructor(
fun isTransparent(x: Int, y: Int, flip: Boolean): Boolean { fun isTransparent(x: Int, y: Int, flip: Boolean): Boolean {
if (x !in 0 until width) return true if (x !in 0 until width) return true
if (y !in 0 until height) return true if (y !in 0 until height) return true
if (amountOfChannels != 4) return false
return this[x, y, flip] and -0x1000000 == 0x0 return this[x, y, flip] and -0x1000000 == 0x0
} }
val nonEmptyRegion by lazy { val nonEmptyRegion by lazy {
if (amountOfChannels == 4) { var x0 = 0
var x0 = 0 var y0 = 0
var y0 = 0
search@for (y in 0 until height) { search@for (y in 0 until height) {
for (x in 0 until width) { for (x in 0 until width) {
if (this[x, y] and 0xFF != 0x0) { if (!isTransparent(x, y, false)) {
x0 = x x0 = x
y0 = y y0 = y
break@search break@search
}
} }
} }
var x1 = x0
var y1 = y0
search@for (y in height - 1 downTo y0) {
for (x in width - 1 downTo x0) {
if (this[x, y] and 0xFF != 0x0) {
x1 = x
y1 = y
break@search
}
}
}
return@lazy Vector4i(x0, y0, x1, y1)
} }
Vector4i(0, 0, width, height) var x1 = x0
var y1 = y0
search@for (y in height - 1 downTo y0) {
for (x in width - 1 downTo x0) {
if (!isTransparent(x, y, false)) {
x1 = x
y1 = y
break@search
}
}
}
return@lazy Vector4i(x0, y0, x1, y1)
}
val imageNonEmptyRegion by lazy {
nonEmptyRegion + Vector4i(x, y, x, y)
} }
fun worldSpaces(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): Set<Vector2i> { fun worldSpaces(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): Set<Vector2i> {
if (amountOfChannels != 3 && amountOfChannels != 4) throw IllegalStateException("Can not check world space taken by image with $amountOfChannels color channels")
val minX = pixelOffset.x / PIXELS_IN_STARBOUND_UNITi val minX = pixelOffset.x / PIXELS_IN_STARBOUND_UNITi
val minY = pixelOffset.y / PIXELS_IN_STARBOUND_UNITi val minY = pixelOffset.y / PIXELS_IN_STARBOUND_UNITi
val maxX = (width + pixelOffset.x + PIXELS_IN_STARBOUND_UNITi - 1) / PIXELS_IN_STARBOUND_UNITi val maxX = (width + pixelOffset.x + PIXELS_IN_STARBOUND_UNITi - 1) / PIXELS_IN_STARBOUND_UNITi
@ -335,7 +311,6 @@ class Image private constructor(
companion object : TypeAdapter<Image>() { companion object : TypeAdapter<Image>() {
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
private val objects by lazy { Starbound.gson.getAdapter(JsonObject::class.java) }
private val vectors by lazy { Starbound.gson.getAdapter(Vector4i::class.java) } private val vectors by lazy { Starbound.gson.getAdapter(Vector4i::class.java) }
private val vectors2 by lazy { Starbound.gson.getAdapter(Vector2i::class.java) } private val vectors2 by lazy { Starbound.gson.getAdapter(Vector2i::class.java) }
private val configCache = ConcurrentHashMap<String, Optional<Pair<List<DataSprite>, IStarboundFile>>>() private val configCache = ConcurrentHashMap<String, Optional<Pair<List<DataSprite>, IStarboundFile>>>()
@ -354,7 +329,7 @@ class Image private constructor(
val data = STBImage.stbi_load_from_memory( val data = STBImage.stbi_load_from_memory(
idata, idata,
getWidth, getHeight, getWidth, getHeight,
components, 0 components, 4
) ?: throw IllegalArgumentException("File $file is not an image or it is corrupted") ) ?: throw IllegalArgumentException("File $file is not an image or it is corrupted")
Reference.reachabilityFence(idata) Reference.reachabilityFence(idata)
@ -432,7 +407,7 @@ class Image private constructor(
if (!status) if (!status)
throw IllegalArgumentException("File $file is not an image or it is corrupted") throw IllegalArgumentException("File $file is not an image or it is corrupted")
Optional.of(Image(file, it, getWidth[0], getHeight[0], components[0], getConfig(it))) Optional.of(Image(file, it, getWidth[0], getHeight[0], getConfig(it)))
} catch (err: Exception) { } catch (err: Exception) {
logger.error("Failed to load image at path $it", err) logger.error("Failed to load image at path $it", err)
Optional.empty() Optional.empty()
@ -534,12 +509,17 @@ class Image private constructor(
} }
private fun compute(it: String): Optional<Pair<List<DataSprite>, IStarboundFile>> { private fun compute(it: String): Optional<Pair<List<DataSprite>, IStarboundFile>> {
val find = Starbound.locate("$it.frames") val locate = Starbound.locate("$it.frames")
if (!find.exists) { if (!locate.exists)
return Optional.empty()
val json = JsonPatch.apply(Starbound.ELEMENTS_ADAPTER.read(locate.jsonReader()), Starbound.locateAll("$it.frames.patch"))
if (json !is JsonObject) {
return Optional.empty() return Optional.empty()
} else { } else {
return Optional.of(parseFrames(objects.read(JsonReader(find.reader()).also { it.isLenient = true })) to find) return Optional.of(parseFrames(json) to locate)
} }
} }

View File

@ -10,10 +10,22 @@ import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.util.SBPattern import ru.dbotthepony.kstarbound.util.SBPattern
// TODO: While this class creates proper interface and provides caching for
// image/sprite resolution, it, however, does not match original engine behavior.
// Original engine first resolves entire path as pattern (through String.replaceTags)
// and only then determine what is image path, what is sprite and do we have render directives.
// tl;dr: for next string <image>.png:thing.<frame><directives>
// and next inputs: image=/something frame=4 directives=?hueshift=4
// old and new engines will disagree what to do
// old engine will first resolve tags: /something.png:thing.4?hueshift=4
// and then parse the string (image = /something.png, sprite = thing.4, directives = hueshift=4)
// our engine, however, will first parse the string, and then resolve tags, resulting into
// sprite being "thing.4?hueshift=4" and directives being empty
class SpriteReference private constructor( class SpriteReference private constructor(
val raw: AssetPath, val raw: AssetPath,
val imagePath: SBPattern, val imagePath: SBPattern,
val spritePath: SBPattern?, val spritePath: SBPattern?,
val renderDirectives: SBPattern?,
val image: Image?, val image: Image?,
) { ) {
val sprite by lazy { val sprite by lazy {
@ -28,17 +40,18 @@ class SpriteReference private constructor(
fun with(values: (String) -> String?): SpriteReference { fun with(values: (String) -> String?): SpriteReference {
val imagePath = this.imagePath.with(values) val imagePath = this.imagePath.with(values)
val spritePath = this.spritePath?.with(values) val spritePath = this.spritePath?.with(values)
val renderDirectives = this.renderDirectives?.with(values)
if (imagePath != this.imagePath || spritePath != this.spritePath) { if (imagePath != this.imagePath || spritePath != this.spritePath || renderDirectives != this.renderDirectives) {
if (imagePath != this.imagePath) { if (imagePath != this.imagePath) {
val resolved = imagePath.value val resolved = imagePath.value
if (resolved == null) if (resolved == null)
return SpriteReference(raw, imagePath, spritePath, null) return SpriteReference(raw, imagePath, spritePath, renderDirectives, null)
else else
return SpriteReference(raw, imagePath, spritePath, Image.get(resolved)) return SpriteReference(raw, imagePath, spritePath, renderDirectives, Image.get(resolved))
} else { } else {
return SpriteReference(raw, imagePath, spritePath, image) return SpriteReference(raw, imagePath, spritePath, renderDirectives, image)
} }
} }
@ -58,11 +71,21 @@ class SpriteReference private constructor(
} }
override fun toString(): String { override fun toString(): String {
return "SpriteReference[$imagePath:$spritePath]" val image = imagePath.value ?: imagePath.raw
val sprite = spritePath?.value ?: spritePath?.raw ?: ""
val directives = renderDirectives?.value ?: renderDirectives?.raw ?: ""
if (sprite == "" && directives == "") {
return image
} else if (sprite != "" && directives == "") {
return "$image:$sprite"
} else {
return "$image:$sprite?$directives"
}
} }
companion object : TypeAdapter<SpriteReference>() { companion object : TypeAdapter<SpriteReference>() {
val NEVER = SpriteReference(AssetPath("", ""), SBPattern.EMPTY, null, null) val NEVER = SpriteReference(AssetPath("", ""), SBPattern.EMPTY, null, null, null)
private val strings by lazy { Starbound.gson.getAdapter(String::class.java) } private val strings by lazy { Starbound.gson.getAdapter(String::class.java) }
@ -75,20 +98,52 @@ class SpriteReference private constructor(
if (path == "") if (path == "")
return NEVER return NEVER
val split = path.split(':') val split = path.lowercase().split(':')
if (split.size > 2) { if (split.size > 2) {
throw JsonSyntaxException("Ambiguous image reference: $path") throw JsonSyntaxException("Ambiguous image reference: $path")
} }
val imagePath = if (split.size == 2) SBPattern.of(split[0]) else SBPattern.of(path) val imagePath: SBPattern
val spritePath = if (split.size == 2) SBPattern.of(split[1]) else null
val renderDirectives: SBPattern?
val spritePath: SBPattern?
if (split.size == 2) {
imagePath = SBPattern.of(split[0])
spritePath = if ('?' in split[1]) {
// has render directives
val sprite = split[1].substringBefore('?')
val directives = split[1].substringAfter('?')
renderDirectives = SBPattern.of(directives)
SBPattern.of(sprite)
} else {
// no render directives
renderDirectives = null
SBPattern.of(split[1])
}
} else {
imagePath = if ('?' in split[0]) {
// has render directives
val image = split[0].substringBefore('?')
val directives = split[0].substringAfter('?')
renderDirectives = SBPattern.of(directives)
SBPattern.of(image)
} else {
// no render directives
renderDirectives = null
SBPattern.of(split[0])
}
spritePath = null
}
if (imagePath.isPlainString) { if (imagePath.isPlainString) {
val remapped = AssetPathStack.remap(split[0]) val remapped = AssetPathStack.remap(imagePath.value!!)
return SpriteReference(AssetPath(path, AssetPathStack.remap(path)), SBPattern.raw(remapped), spritePath, Image.get(remapped)) return SpriteReference(AssetPath(path, AssetPathStack.remap(path)), SBPattern.raw(remapped), spritePath, renderDirectives, Image.get(remapped))
} else { } else {
return SpriteReference(AssetPath(path, path), imagePath, spritePath, null) return SpriteReference(AssetPath(path, path), imagePath, spritePath, renderDirectives, null)
} }
} }

View File

@ -1,9 +0,0 @@
package ru.dbotthepony.kstarbound.defs.item
import ru.dbotthepony.kstarbound.defs.image.SpriteReference
import ru.dbotthepony.kstarbound.json.builder.JsonImplementation
@JsonImplementation(InventoryIcon::class)
interface IInventoryIcon {
val image: SpriteReference
}

View File

@ -1,20 +0,0 @@
package ru.dbotthepony.kstarbound.defs.item
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.builder.JsonImplementation
@JsonImplementation(LeveledStatusEffect::class)
interface ILeveledStatusEffect {
val levelFunction: String
val stat: String
val baseMultiplier: Double
val amount: Double
}
@JsonFactory
data class LeveledStatusEffect(
override val levelFunction: String,
override val stat: String,
override val baseMultiplier: Double = 1.0,
override val amount: Double = 0.0,
) : ILeveledStatusEffect

View File

@ -1,41 +0,0 @@
package ru.dbotthepony.kstarbound.defs.item
import com.google.gson.JsonPrimitive
import com.google.gson.TypeAdapter
import com.google.gson.internal.bind.JsonTreeReader
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.image.SpriteReference
import ru.dbotthepony.kstarbound.json.builder.FactoryAdapter
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.util.AssetPathStack
data class InventoryIcon(
override val image: SpriteReference
) : IInventoryIcon {
companion object : TypeAdapter<InventoryIcon>() {
private val adapter by lazy { FactoryAdapter.createFor(InventoryIcon::class, Starbound.gson) }
private val images by lazy { Starbound.gson.getAdapter(SpriteReference::class.java) }
override fun write(out: JsonWriter, value: InventoryIcon?) {
if (value == null)
out.nullValue()
else
adapter.write(out, value)
}
override fun read(`in`: JsonReader): InventoryIcon? {
if (`in`.consumeNull())
return null
if (`in`.peek() == JsonToken.STRING) {
return InventoryIcon(images.read(JsonTreeReader(JsonPrimitive(AssetPathStack.remap(`in`.nextString())))))
}
return adapter.read(`in`)
}
}
}

View File

@ -11,6 +11,7 @@ import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter import com.google.gson.annotations.JsonAdapter
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import org.apache.logging.log4j.LogManager
import org.classdump.luna.ByteString import org.classdump.luna.ByteString
import org.classdump.luna.LuaRuntimeException import org.classdump.luna.LuaRuntimeException
import org.classdump.luna.Table import org.classdump.luna.Table
@ -21,26 +22,29 @@ import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.lua.StateMachine import ru.dbotthepony.kstarbound.lua.StateMachine
import ru.dbotthepony.kstarbound.lua.from import ru.dbotthepony.kstarbound.lua.from
import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.gson.value import ru.dbotthepony.kommons.gson.value
import ru.dbotthepony.kommons.io.StreamCodec import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.io.readVarLong import ru.dbotthepony.kommons.io.readVarLong
import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeVarLong import ru.dbotthepony.kommons.io.writeVarLong
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.VersionRegistry import ru.dbotthepony.kstarbound.VersionRegistry
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
import ru.dbotthepony.kstarbound.io.readInternedString import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.item.ItemRegistry
import ru.dbotthepony.kstarbound.item.ItemStack import ru.dbotthepony.kstarbound.item.ItemStack
import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.json.readJsonElement import ru.dbotthepony.kstarbound.json.readJsonElement
import ru.dbotthepony.kstarbound.json.writeJsonElement import ru.dbotthepony.kstarbound.json.writeJsonElement
import ru.dbotthepony.kstarbound.lua.LuaEnvironment
import ru.dbotthepony.kstarbound.lua.get import ru.dbotthepony.kstarbound.lua.get
import ru.dbotthepony.kstarbound.lua.indexNoYield import ru.dbotthepony.kstarbound.lua.indexNoYield
import ru.dbotthepony.kstarbound.lua.toJson import ru.dbotthepony.kstarbound.lua.toJson
import ru.dbotthepony.kstarbound.lua.toJsonFromLua
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
import java.util.function.Supplier import java.util.function.Supplier
import java.util.random.RandomGenerator
private val EMPTY_JSON = JsonObject() private val EMPTY_JSON = JsonObject()
@ -151,11 +155,9 @@ data class ItemDescriptor(
val count: Long, val count: Long,
val parameters: JsonObject = EMPTY_JSON val parameters: JsonObject = EMPTY_JSON
) { ) {
constructor(ref: Registry.Ref<IItemDefinition>, count: Long, parameters: JsonObject) : this(ref.key.left(), count, parameters)
val isEmpty get() = count <= 0L || name == "" || ref.isEmpty val isEmpty get() = count <= 0L || name == "" || ref.isEmpty
val isNotEmpty get() = !isEmpty val isNotEmpty get() = !isEmpty
val ref by lazy { if (name == "") Registries.items.emptyRef else Registries.items.ref(name) } val ref by lazy { if (name == "") ItemRegistry.AIR else ItemRegistry[name] }
override fun toString(): String { override fun toString(): String {
return "ItemDescriptor[$name, $count, $parameters]" return "ItemDescriptor[$name, $count, $parameters]"
@ -195,6 +197,40 @@ data class ItemDescriptor(
} }
} }
fun build(level: Double? = null, seed: Long? = null, random: RandomGenerator? = null): ItemDescriptor {
val builder = ref.json["builder"]?.asString ?: return this
try {
val lua = LuaEnvironment()
lua.attach(Starbound.loadScript(builder))
lua.random = random ?: lua.random
lua.init(false)
val (config, parameters) = lua.invokeGlobal("build", ref.directory, lua.from(ref.json), lua.from(parameters), level, seed)
// we don't care about "config" here since it is treated equally by code as "parameters"
val jConfig = toJsonFromLua(config).asJsonObject
val jParameters = toJsonFromLua(parameters).asJsonObject
// so, lets promote changed "config" parameters to item parameters
for ((k, v) in jConfig.entrySet()) {
if (ref.json[k] != v) {
if (k !in jParameters) {
jParameters[k] = v
} else {
// existing parameters override changed config parameters
jParameters[k] = mergeJson(v, jParameters[k])
}
}
}
return ItemDescriptor(name, count, jParameters)
} catch (err: Throwable) {
LOGGER.error("Error while generating randomized item '$name' using script $builder", err)
return this
}
}
fun write(stream: DataOutputStream) { fun write(stream: DataOutputStream) {
stream.writeBinaryString(name) stream.writeBinaryString(name)
stream.writeVarLong(count.coerceAtLeast(0L)) stream.writeVarLong(count.coerceAtLeast(0L))
@ -202,8 +238,6 @@ data class ItemDescriptor(
} }
class Adapter(gson: Gson) : TypeAdapter<ItemDescriptor>() { class Adapter(gson: Gson) : TypeAdapter<ItemDescriptor>() {
private val elements = gson.getAdapter(JsonElement::class.java)
override fun write(out: JsonWriter, value: ItemDescriptor) { override fun write(out: JsonWriter, value: ItemDescriptor) {
if (value.isEmpty) if (value.isEmpty)
out.nullValue() out.nullValue()
@ -212,12 +246,13 @@ data class ItemDescriptor(
} }
override fun read(`in`: JsonReader): ItemDescriptor { override fun read(`in`: JsonReader): ItemDescriptor {
return ItemDescriptor(elements.read(`in`)) return ItemDescriptor(Starbound.ELEMENTS_ADAPTER.read(`in`))
} }
} }
companion object { companion object {
val EMPTY = ItemDescriptor("", 0) val EMPTY = ItemDescriptor("", 0)
val CODEC = StreamCodec.Impl(::ItemDescriptor, { a, b -> b.write(a) }) val CODEC = StreamCodec.Impl(::ItemDescriptor, { a, b -> b.write(a) })
private val LOGGER = LogManager.getLogger()
} }
} }

View File

@ -0,0 +1,14 @@
package ru.dbotthepony.kstarbound.defs.item
import com.google.common.collect.ImmutableSet
import ru.dbotthepony.kstarbound.defs.AssetPath
data class ItemGlobalConfig(
val defaultMaxStack: Long,
val defaultPrice: Long,
val pickupSounds: ImmutableSet<AssetPath>,
val defaultTimeToLive: Double,
val missingIcon: AssetPath,
) {
val pickupSoundsAbsolute: ImmutableSet<String> = pickupSounds.stream().map { it.fullPath }.collect(ImmutableSet.toImmutableSet())
}

View File

@ -0,0 +1,31 @@
package ru.dbotthepony.kstarbound.defs.item
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
enum class ItemType(override val jsonName: String, val extension: String?) : IStringSerializable {
GENERIC ("generic", "item"),
LIQUID ("liquid", "liqitem"),
MATERIAL ("material", "matitem"),
OBJECT ("object", null),
CURRENCY ("currency", "currency"),
MINING_TOOL ("miningtool", "miningtool"),
FLASHLIGHT ("flashlight", "flashlight"),
WIRE_TOOL ("wiretool", "wiretool"),
BEAM_MINING_TOOL ("beamminingtool", "beamaxe"),
HARVEST_TOOL ("harvestingtool", "harvestingtool"),
TILLING_TOOL ("tillingtool", "tillingtool"),
PAINTING_BRUSH ("paintingbeamtool", "painttool"),
HEAD_ARMOR ("headarmor", "head"),
CHEST_ARMOR ("chestarmor", "chest"),
LEGS_ARMOR ("legsarmor", "legs"),
BACK_ARMOR ("backarmor", "back"),
CONSUMABLE ("consumable", "consumable"),
BLUEPRINT ("blueprint", "blueprint"),
CODEX ("codex", null),
INSPECTION_TOOL ("inspectiontool", "inspectiontool"),
PLAYING_INSTRUMENT ("instrument", "instrument"),
THROWABLE ("thrownitem", "thrownitem"),
UNLOCK ("unlockitem", "unlock"),
ACTIVE ("activeitem", "activeitem"),
AUGMENT ("augmentitem", "augment");
}

View File

@ -8,19 +8,30 @@ import com.google.gson.JsonPrimitive
import com.google.gson.JsonSyntaxException import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter import com.google.gson.TypeAdapter
import com.google.gson.TypeAdapterFactory import com.google.gson.TypeAdapterFactory
import com.google.gson.annotations.JsonAdapter
import com.google.gson.internal.bind.JsonTreeReader import com.google.gson.internal.bind.JsonTreeReader
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.objects.Object2IntArrayMap
import it.unimi.dsi.fastutil.objects.Object2IntMap
import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.collect.WeightedList
import ru.dbotthepony.kstarbound.json.jsonArrayOf
import ru.dbotthepony.kstarbound.json.stream import ru.dbotthepony.kstarbound.json.stream
import ru.dbotthepony.kstarbound.item.ItemStack
import ru.dbotthepony.kstarbound.util.WriteOnce import ru.dbotthepony.kstarbound.util.WriteOnce
import java.util.Random import ru.dbotthepony.kstarbound.util.random.nextRange
import ru.dbotthepony.kstarbound.util.random.random
import java.util.random.RandomGenerator import java.util.random.RandomGenerator
typealias ItemOrPool = Either<ItemDescriptor, Registry.Ref<TreasurePoolDefinition>>
@JsonAdapter(TreasurePoolDefinition.Adapter::class)
class TreasurePoolDefinition(pieces: List<Piece>) { class TreasurePoolDefinition(pieces: List<Piece>) {
var name: String by WriteOnce() var name: String by WriteOnce()
@ -28,72 +39,83 @@ class TreasurePoolDefinition(pieces: List<Piece>) {
require(pieces.isNotEmpty()) { "Treasure pool is empty" } require(pieces.isNotEmpty()) { "Treasure pool is empty" }
} }
val pieces: ImmutableList<Piece> = pieces.stream().sorted { o1, o2 -> o1.level.compareTo(o2.level) }.collect(ImmutableList.toImmutableList()) val pieces: ImmutableList<Piece> = pieces
.stream()
.sorted { o1, o2 -> o1.startingLevel.compareTo(o2.startingLevel) }
.collect(ImmutableList.toImmutableList())
fun evaluate(random: RandomGenerator, level: Double): List<ItemStack> { fun evaluate(random: RandomGenerator, level: Double, visitedPools: Object2IntMap<String> = Object2IntArrayMap()): List<ItemDescriptor> {
require(level >= 0.0) { "Invalid loot level: $level" } require(level >= 0.0) { "Invalid loot level: $level" }
for (piece in pieces) { val new = visitedPools.getInt(name) + 1
if (level <= piece.level) {
return piece.evaluate(random, level)
}
}
if (pieces.last().level <= level) { if (new >= 50)
return pieces.last().evaluate(random, level) throw IllegalStateException("Too deep treasure pool references, visited treasure pool $name 50 times (visited: $visitedPools)")
} else {
return emptyList() visitedPools.put(name, new)
try {
for (piece in pieces) {
if (level <= piece.startingLevel) {
return piece.evaluate(random, level, visitedPools)
}
}
if (pieces.last().startingLevel <= level) {
return pieces.last().evaluate(random, level, visitedPools)
} else {
return emptyList()
}
} finally {
visitedPools.put(name, visitedPools.getInt(name) - 1)
} }
} }
fun evaluate(seed: Long, level: Double): List<ItemStack> { fun evaluate(seed: Long, level: Double): List<ItemDescriptor> {
return evaluate(Random(seed), level) return evaluate(random(seed), level)
} }
data class Piece( data class Piece(
val level: Double, val startingLevel: Double,
val pool: ImmutableList<PoolEntry> = ImmutableList.of(), val pool: WeightedList<ItemOrPool> = WeightedList(),
val fill: ImmutableList<Either<ItemDescriptor, Registry.Ref<TreasurePoolDefinition>>> = ImmutableList.of(), val fill: ImmutableList<ItemOrPool> = ImmutableList.of(),
val poolRounds: IPoolRounds = OneRound, val poolRounds: IPoolRounds = OneRound,
// TODO: что оно делает? val allowDuplication: Boolean = true,
// оно точно не запрещает ему появляться несколько раз за одну генерацию treasure pool val levelVariance: Vector2d = Vector2d.ZERO
// а так же не "дублирует" содержимое если уровень генерации выше, чем указанный level
val allowDuplication: Boolean = false
) { ) {
val maxWeight = pool.stream().mapToDouble { it.weight }.sum() fun evaluate(random: RandomGenerator, level: Double, visitedPools: Object2IntMap<String>): List<ItemDescriptor> {
val result = ArrayList<ItemDescriptor>()
val previousDescriptors = HashSet<ItemDescriptor>()
fun evaluate(random: RandomGenerator, actualLevel: Double): List<ItemStack> { for (entry in fill) {
val rounds = poolRounds.evaluate(random) entry.map({
if (rounds <= 0) return emptyList() val stack = it.build(level = level + random.nextRange(levelVariance), random = random, seed = random.nextLong())
val result = ArrayList<ItemStack>()
for (round in 0 until rounds) { if (stack.isNotEmpty && (allowDuplication || previousDescriptors.add(stack.copy(count = 1L)))) {
for (entry in fill) { result.add(stack)
entry.map({ }
val stack = it.makeStack() }, {
if (stack.isNotEmpty) result.add(stack) it.value?.evaluate(random, level + random.nextRange(levelVariance), visitedPools)?.forEach {
}, { if (allowDuplication || previousDescriptors.add(it.copy(count = 1L)))
it.value?.evaluate(random, actualLevel) result.add(it)
}) }
} })
}
if (pool.isNotEmpty()) { if (pool.isNotEmpty) {
var chosen = random.nextDouble(maxWeight) for (round in 0 until poolRounds.evaluate(random)) {
pool.sample(random).orThrow { RuntimeException() }.map({
val stack = it.build(level = level + random.nextRange(levelVariance), random = random, seed = random.nextLong())
for (entry in pool) { if (stack.isNotEmpty && (allowDuplication || previousDescriptors.add(stack.copy(count = 1L)))) {
if (chosen <= entry.weight) { result.add(stack)
entry.treasure.map({
val stack = it.makeStack()
if (stack.isNotEmpty) result.add(stack)
}, {
it.value?.evaluate(random, actualLevel)
})
break
} else {
chosen -= entry.weight
} }
} }, {
it.value?.evaluate(random, level + random.nextRange(levelVariance), visitedPools)?.forEach {
if (allowDuplication || previousDescriptors.add(it.copy(count = 1L)))
result.add(it)
}
})
} }
} }
@ -121,139 +143,103 @@ class TreasurePoolDefinition(pieces: List<Piece>) {
} }
} }
data class PoolRounds(val edges: ImmutableList<RoundEdge>) : IPoolRounds { data class WeightedRounds(val edges: WeightedList<Int>) : IPoolRounds {
val maxWeight = edges.stream().mapToDouble { it.weight }.sum()
override fun evaluate(random: RandomGenerator): Int { override fun evaluate(random: RandomGenerator): Int {
val result = random.nextDouble(maxWeight) return edges.sample(random).orElse(1)
var lower = 0.0 }
}
for (edge in edges) { class Adapter(gson: Gson) : TypeAdapter<TreasurePoolDefinition>() {
if (result in lower .. lower + edge.weight) { private val itemAdapter = gson.getAdapter(ItemDescriptor::class.java)
return edge.rounds private val poolAdapter = gson.getAdapter(object : TypeToken<Registry.Ref<TreasurePoolDefinition>>() {})
} else { private val vectors = gson.getAdapter(Vector2d::class.java)
lower += edge.weight
override fun write(out: JsonWriter, value: TreasurePoolDefinition?) {
if (value == null) {
out.nullValue()
} else {
TODO()
}
}
override fun read(`in`: JsonReader): TreasurePoolDefinition? {
if (`in`.consumeNull()) {
return null
}
val pieces = ArrayList<Piece>()
`in`.beginArray()
while (`in`.hasNext()) {
`in`.beginArray()
val level = `in`.nextDouble()
val things = Starbound.ELEMENTS_ADAPTER.objects.read(`in`)
val pool = ImmutableList.Builder<Pair<Double, ItemOrPool>>()
val fill = ImmutableList.Builder<Either<ItemDescriptor, Registry.Ref<TreasurePoolDefinition>>>()
var poolRounds: IPoolRounds = OneRound
val allowDuplication = things["allowDuplication"]?.asBoolean ?: false
things["poolRounds"]?.let {
if (it is JsonPrimitive) {
poolRounds = ConstantRound(it.asInt)
} else if (it is JsonArray) {
poolRounds = WeightedRounds(WeightedList(Starbound.gson.getAdapter(object : TypeToken<ImmutableList<Pair<Double, Int>>>() {}).fromJsonTree(it)))
} else {
throw JsonSyntaxException("Expected either a number or an array for poolRounds, but got ${it::class.simpleName}")
}
} }
}
return edges.last().rounds things["pool"]?.let {
} it as? JsonArray ?: throw JsonSyntaxException("pool must be an array")
}
data class RoundEdge(val weight: Double, val rounds: Int) { for ((i, elem) in it.withIndex()) {
init { elem as? JsonObject ?: throw JsonSyntaxException("Pool element at $i is not an object")
require(weight > 0.0) { "Invalid round weight: $weight" } val weight = (elem["weight"] as? JsonPrimitive)?.asDouble ?: throw JsonSyntaxException("Pool element at $i is missing weight")
require(rounds >= 0) { "Invalid rounds amount: $rounds" }
}
}
data class PoolEntry( if (elem.has("item")) {
val weight: Double, pool.add(weight to Either.left(itemAdapter.read(JsonTreeReader(elem["item"]!!))))
val treasure: Either<ItemDescriptor, Registry.Ref<TreasurePoolDefinition>> } else if (elem.has("pool")) {
) { pool.add(weight to Either.right(poolAdapter.read(JsonTreeReader(elem["pool"]!!))))
init {
require(weight > 0.0) { "Invalid pool entry weight: $weight" }
}
}
companion object : TypeAdapterFactory {
override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
if (type.rawType === TreasurePoolDefinition::class.java) {
return object : TypeAdapter<TreasurePoolDefinition>() {
private val itemAdapter = gson.getAdapter(ItemDescriptor::class.java)
private val poolAdapter = gson.getAdapter(TypeToken.getParameterized(Registry.Ref::class.java, TreasurePoolDefinition::class.java)) as TypeAdapter<Registry.Ref<TreasurePoolDefinition>>
private val objReader = gson.getAdapter(JsonObject::class.java)
override fun write(out: JsonWriter, value: TreasurePoolDefinition?) {
if (value == null) {
out.nullValue()
} else { } else {
TODO() throw JsonSyntaxException("Pool element at $i is missing both 'item' and 'pool' entries")
} }
} }
}
override fun read(`in`: JsonReader): TreasurePoolDefinition? { things["fill"]?.let {
if (`in`.consumeNull()) { it as? JsonArray ?: throw JsonSyntaxException("fill must be an array")
return null
for ((i, elem) in it.withIndex()) {
elem as? JsonObject ?: throw JsonSyntaxException("Fill element at $i is not an object")
if (elem.has("item")) {
fill.add(Either.left(itemAdapter.read(JsonTreeReader(elem["item"]!!))))
} else if (elem.has("pool")) {
fill.add(Either.right(poolAdapter.read(JsonTreeReader(elem["pool"]!!))))
} else {
throw JsonSyntaxException("Fill element at $i is missing both 'item' and 'pool' entries")
} }
val pieces = ArrayList<Piece>()
`in`.beginArray()
while (`in`.hasNext()) {
`in`.beginArray()
val level = `in`.nextDouble()
val things = objReader.read(`in`)
val pool = ImmutableList.Builder<PoolEntry>()
val fill = ImmutableList.Builder<Either<ItemDescriptor, Registry.Ref<TreasurePoolDefinition>>>()
var poolRounds: IPoolRounds = OneRound
val allowDuplication = things["allowDuplication"]?.asBoolean ?: false
things["poolRounds"]?.let {
if (it is JsonPrimitive) {
poolRounds = ConstantRound(it.asInt)
} else if (it is JsonArray) {
poolRounds = PoolRounds(it.stream().map { it as JsonArray; RoundEdge(it[0].asDouble, it[1].asInt) }.collect(ImmutableList.toImmutableList()))
} else {
throw JsonSyntaxException("Expected either a number or an array for poolRounds, but got ${it::class.simpleName}")
}
}
things["pool"]?.let {
it as? JsonArray ?: throw JsonSyntaxException("pool must be an array")
for ((i, elem) in it.withIndex()) {
elem as? JsonObject ?: throw JsonSyntaxException("Pool element at $i is not an object")
val weight = (elem["weight"] as? JsonPrimitive)?.asDouble ?: throw JsonSyntaxException("Pool element at $i is missing weight")
if (elem.has("item")) {
pool.add(PoolEntry(weight, Either.left(itemAdapter.read(JsonTreeReader(elem["item"]!!)))))
} else if (elem.has("pool")) {
pool.add(PoolEntry(weight, Either.right(poolAdapter.read(JsonTreeReader(elem["pool"]!!)))))
} else {
throw JsonSyntaxException("Pool element at $i is missing both 'item' and 'pool' entries")
}
}
}
things["fill"]?.let {
it as? JsonArray ?: throw JsonSyntaxException("fill must be an array")
for ((i, elem) in it.withIndex()) {
elem as? JsonObject ?: throw JsonSyntaxException("Fill element at $i is not an object")
if (elem.has("item")) {
fill.add(Either.left(itemAdapter.read(JsonTreeReader(elem["item"]!!))))
} else if (elem.has("pool")) {
fill.add(Either.right(poolAdapter.read(JsonTreeReader(elem["pool"]!!))))
} else {
throw JsonSyntaxException("Fill element at $i is missing both 'item' and 'pool' entries")
}
}
}
pieces.add(Piece(
pool = pool.build(),
fill = fill.build(),
poolRounds = poolRounds,
level = level,
allowDuplication = allowDuplication
))
`in`.endArray()
}
`in`.endArray()
return TreasurePoolDefinition(pieces)
} }
} as TypeAdapter<T> }
pieces.add(Piece(
pool = WeightedList(pool.build()),
fill = fill.build(),
poolRounds = poolRounds,
startingLevel = level,
allowDuplication = allowDuplication,
levelVariance = vectors.fromJsonTree(things.get("levelVariance", jsonArrayOf(0.0, 0.0)))
))
`in`.endArray()
} }
return null `in`.endArray()
return TreasurePoolDefinition(pieces)
} }
} }
} }

View File

@ -1,71 +0,0 @@
package ru.dbotthepony.kstarbound.defs.item.api
import com.google.gson.Gson
import com.google.gson.TypeAdapter
import com.google.gson.TypeAdapterFactory
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kstarbound.defs.image.SpriteReference
import ru.dbotthepony.kstarbound.json.builder.FactoryAdapter
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.builder.JsonImplementation
interface IArmorItemDefinition : ILeveledItemDefinition, IScriptableItemDefinition {
/**
* Варианты покраски (???)
*/
val colorOptions: List<Map<String, String>>
/**
* Визуальные кадры анимации, когда надето на гуманоида мужского пола
*/
val maleFrames: IFrames
/**
* Визуальные кадры анимации, когда надето на гуманоида женского пола
*/
val femaleFrames: IFrames
@JsonImplementation(Frames::class)
interface IFrames {
val body: SpriteReference
val backSleeve: SpriteReference?
val frontSleeve: SpriteReference?
}
data class Frames(
override val body: SpriteReference,
override val backSleeve: SpriteReference? = null,
override val frontSleeve: SpriteReference? = null,
) : IFrames {
object Factory : TypeAdapterFactory {
override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
if (type.rawType == Frames::class.java) {
return object : TypeAdapter<Frames>() {
private val adapter = FactoryAdapter.createFor(Frames::class, gson)
private val frames = gson.getAdapter(SpriteReference::class.java)
override fun write(out: JsonWriter, value: Frames?) {
adapter.write(out, value)
}
override fun read(`in`: JsonReader): Frames? {
if (`in`.consumeNull())
return null
else if (`in`.peek() == JsonToken.STRING)
return Frames(frames.read(`in`), null, null)
else
return adapter.read(`in`)
}
} as TypeAdapter<T>
}
return null
}
}
}
}

View File

@ -1,16 +0,0 @@
package ru.dbotthepony.kstarbound.defs.item.api
interface ICurrencyItemDefinition : IItemDefinition {
/**
* ID валюты
*/
val currency: String
/**
* Ценность одного предмета в [currency]
*/
val value: Long
override val itemType: String
get() = "currency"
}

View File

@ -1,17 +0,0 @@
package ru.dbotthepony.kstarbound.defs.item.api
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.vector.Vector2d
interface IFlashlightDefinition : IItemDefinition, IItemInHandDefinition {
/**
* Смещение в пикселях
*/
val lightPosition: Vector2d
val lightColor: RGBAColor
val beamLevel: Int
val beamAmbience: Double
}

View File

@ -1,30 +0,0 @@
package ru.dbotthepony.kstarbound.defs.item.api
import ru.dbotthepony.kstarbound.defs.animation.IAnimated
interface IHarvestingToolDefinition : IItemDefinition, IAnimated, IItemInHandDefinition {
/**
* Радиус в тайлах, на какое расстояние действует данный инструмент для сбора
*/
val blockRadius: Int
/**
* Радиус в тайлах, на какое расстояние действует данный инструмент для сбора
*/
val altBlockRadius: Int
/**
* Звуки в бездействии
*/
val idleSound: List<String>
/**
* Звуки при работе
*/
val strikeSounds: List<String>
/**
* Время атаки
*/
val fireTime: Double
}

View File

@ -1,130 +0,0 @@
package ru.dbotthepony.kstarbound.defs.item.api
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.AssetPath
import ru.dbotthepony.kstarbound.defs.IThingWithDescription
import ru.dbotthepony.kstarbound.defs.item.IInventoryIcon
import ru.dbotthepony.kstarbound.defs.item.ItemRarity
import ru.dbotthepony.kstarbound.defs.item.impl.ItemDefinition
import ru.dbotthepony.kstarbound.json.builder.JsonImplementation
@JsonImplementation(ItemDefinition::class)
interface IItemDefinition : IThingWithDescription {
/**
* Внутреннее имя предмета (ID).
* Не путать с именем предмета!
*
* @see shortdescription
* @see description
*/
val itemName: String
/**
* Цена в пикселях
*/
val price: Long
/**
* Редкость предмета
*/
val rarity: ItemRarity
/**
* Категория предмета, определяет, в какую вкладку инвентаря оно попадает
*/
val category: String?
/**
* Иконка в инвентаре, относительный и абсолютный пути
*/
val inventoryIcon: Either<out IInventoryIcon, out List<IInventoryIcon>>?
/**
* Теги предмета
*/
val itemTags: List<String>
/**
* При подборе предмета мгновенно заставляет игрока изучить эти рецепты крафта
*/
val learnBlueprintsOnPickup: List<Registry.Ref<IItemDefinition>>
/**
* Максимальное количество предмета в стопке
*/
val maxStack: Long
/**
* snip
*/
val eventCategory: String?
/**
* Топливо корабля
*/
val fuelAmount: Long
/**
* Заставляет предмет "использовать" сразу же при подборе
*/
val consumeOnPickup: Boolean
/**
* Запускает следующие квест(ы) при подборе
*/
val pickupQuestTemplates: List<String>
/**
* тип tooltip'а
*
* ссылается на конфиг окон
*/
val tooltipKind: String
/**
* Занимает ли предмет обе руки
*/
val twoHanded: Boolean
/**
* Заставляет SAIL/прочих болтать при подборе предмета в первый раз
*/
val radioMessagesOnPickup: List<String>
/**
* Звуки при поднятии "малого" количества предметов. Не имеет никакого смысла без [smallStackLimit]
*/
val pickupSoundsSmall: List<String>?
/**
* Звуки при поднятии "среднего" количества предметов. Не имеет никакого смысла без [mediumStackLimit]
*/
val pickupSoundsMedium: List<String>?
/**
* Звуки при поднятии "большого" количества предметов. Не имеет никакого смысла без [smallStackLimit] и без [mediumStackLimit]
*/
val pickupSoundsLarge: List<String>?
/**
* Количество предметов ниже или равному данному значению проиграет звук [pickupSoundsSmall]
*/
val smallStackLimit: Long?
/**
* Количество предметов ниже или равному данному значению (но не меньше [smallStackLimit]) проиграет звук [pickupSoundsMedium]
*/
val mediumStackLimit: Long?
/**
* Lua script path for item config builder
*/
val builder: AssetPath?
/**
* Тип предмета, используется Lua скриптами
*/
val itemType: String
get() = "item"
}

View File

@ -1,10 +0,0 @@
package ru.dbotthepony.kstarbound.defs.item.api
import ru.dbotthepony.kommons.vector.Vector2d
interface IItemInHandDefinition : IItemDefinition {
/**
* Позиция инструмента в руке (смещение в пикселях)
*/
val handPosition: Vector2d
}

View File

@ -1,15 +0,0 @@
package ru.dbotthepony.kstarbound.defs.item.api
import ru.dbotthepony.kstarbound.defs.item.ILeveledStatusEffect
interface ILeveledItemDefinition : IItemDefinition {
/**
* Изначальный уровень предмета, может быть изменён позднее чем угодно
*/
val level: Double
/**
* Эффекты предмета, растущие с уровнем
*/
val leveledStatusEffects: List<ILeveledStatusEffect>
}

View File

@ -1,13 +0,0 @@
package ru.dbotthepony.kstarbound.defs.item.api
import ru.dbotthepony.kommons.util.Either
interface ILiquidItem : IItemDefinition {
/**
* То, какую жидкость из себя представляет данный предмет
*/
val liquid: Either<Int, String>
override val itemType: String
get() = "liquid"
}

View File

@ -1,13 +0,0 @@
package ru.dbotthepony.kstarbound.defs.item.api
import ru.dbotthepony.kommons.util.Either
interface IMaterialItem : IItemDefinition {
/**
* То, какой материал (блок) из себя представляет данный предмет
*/
val material: Either<Int, String>
override val itemType: String
get() = "material"
}

View File

@ -1,6 +0,0 @@
package ru.dbotthepony.kstarbound.defs.item.api
import ru.dbotthepony.kstarbound.defs.IScriptable
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
interface IScriptableItemDefinition : IItemDefinition, IScriptable

View File

@ -1,49 +0,0 @@
package ru.dbotthepony.kstarbound.defs.item.impl
import com.google.common.collect.ImmutableList
import ru.dbotthepony.kstarbound.defs.IScriptable
import ru.dbotthepony.kstarbound.defs.item.api.IArmorItemDefinition
import ru.dbotthepony.kstarbound.defs.item.LeveledStatusEffect
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.builder.JsonFlat
@JsonFactory(logMisses = false)
data class ArmorItemDefinition(
@JsonFlat
val parent: IItemDefinition,
@JsonFlat
val script: IScriptable.Impl,
override val maxStack: Long = 1L,
override val colorOptions: ImmutableList<Map<String, String>> = ImmutableList.of(),
override val maleFrames: IArmorItemDefinition.Frames,
override val femaleFrames: IArmorItemDefinition.Frames,
override val level: Double = 1.0,
override val leveledStatusEffects: ImmutableList<LeveledStatusEffect> = ImmutableList.of(),
) : IArmorItemDefinition, IItemDefinition by parent, IScriptable by script
@JsonFactory(logMisses = false)
class HeadArmorItemDefinition(@JsonFlat val parent: ArmorItemDefinition) : IArmorItemDefinition by parent {
override val itemType: String
get() = "headarmor"
}
@JsonFactory(logMisses = false)
class ChestArmorItemDefinition(@JsonFlat val parent: ArmorItemDefinition) : IArmorItemDefinition by parent {
override val itemType: String
get() = "chestarmor"
}
@JsonFactory(logMisses = false)
class LegsArmorItemDefinition(@JsonFlat val parent: ArmorItemDefinition) : IArmorItemDefinition by parent {
override val itemType: String
get() = "legsarmor"
}
@JsonFactory(logMisses = false)
class BackArmorItemDefinition(@JsonFlat val parent: ArmorItemDefinition) : IArmorItemDefinition by parent {
override val itemType: String
get() = "backarmor"
}

View File

@ -1,19 +0,0 @@
package ru.dbotthepony.kstarbound.defs.item.impl
import ru.dbotthepony.kstarbound.defs.item.api.ICurrencyItemDefinition
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
import ru.dbotthepony.kstarbound.json.builder.JsonBuilder
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.builder.JsonFlat
@JsonFactory
data class CurrencyItemDefinition(
@JsonFlat
val parent: IItemDefinition,
override val maxStack: Long = 16777216L,
override var currency: String,
override var value: Long,
) : ICurrencyItemDefinition, IItemDefinition by parent {
override val itemType: String
get() = super<ICurrencyItemDefinition>.itemType
}

View File

@ -1,21 +0,0 @@
package ru.dbotthepony.kstarbound.defs.item.impl
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.defs.item.api.IFlashlightDefinition
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.builder.JsonFlat
@JsonFactory(logMisses = false)
class FlashlightDefinition(
@JsonFlat
val parent: IItemDefinition,
override val lightPosition: Vector2d,
override val lightColor: RGBAColor,
override val beamLevel: Int,
override val beamAmbience: Double,
override val handPosition: Vector2d,
override val maxStack: Long = 1L,
) : IItemDefinition by parent, IFlashlightDefinition

View File

@ -1,23 +0,0 @@
package ru.dbotthepony.kstarbound.defs.item.impl
import com.google.common.collect.ImmutableList
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.defs.item.api.IHarvestingToolDefinition
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.builder.JsonFlat
@JsonFactory(logMisses = false)
class HarvestingToolPrototype(
@JsonFlat
val parent: IItemDefinition,
override val frames: Int,
override val animationCycle: Double,
override val blockRadius: Int,
override val altBlockRadius: Int = 0,
override val idleSound: ImmutableList<String> = ImmutableList.of(),
override val strikeSounds: ImmutableList<String> = ImmutableList.of(),
override val handPosition: Vector2d,
override val fireTime: Double,
override val maxStack: Long = 1L,
) : IItemDefinition by parent, IHarvestingToolDefinition

View File

@ -1,42 +0,0 @@
package ru.dbotthepony.kstarbound.defs.item.impl
import com.google.common.collect.ImmutableList
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.AssetPath
import ru.dbotthepony.kstarbound.defs.IThingWithDescription
import ru.dbotthepony.kstarbound.defs.ThingDescription
import ru.dbotthepony.kstarbound.defs.item.IInventoryIcon
import ru.dbotthepony.kstarbound.defs.item.InventoryIcon
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
import ru.dbotthepony.kstarbound.defs.item.ItemRarity
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.builder.JsonFlat
@JsonFactory(logMisses = false)
data class ItemDefinition(
@JsonFlat
val descriptionData: ThingDescription,
override val itemName: String,
override val price: Long = 0,
override val rarity: ItemRarity = ItemRarity.COMMON,
override val category: String? = null,
override val inventoryIcon: Either<InventoryIcon, ImmutableList<IInventoryIcon>>? = null,
override val itemTags: ImmutableList<String> = ImmutableList.of(),
override val learnBlueprintsOnPickup: ImmutableList<Registry.Ref<IItemDefinition>> = ImmutableList.of(),
override val maxStack: Long = 9999L,
override val eventCategory: String? = null,
override val consumeOnPickup: Boolean = false,
override val pickupQuestTemplates: ImmutableList<String> = ImmutableList.of(),
override val tooltipKind: String = "normal",
override val twoHanded: Boolean = false,
override val radioMessagesOnPickup: ImmutableList<String> = ImmutableList.of(),
override val fuelAmount: Long = 0,
override val pickupSoundsSmall: ImmutableList<String> = ImmutableList.of(),
override val pickupSoundsMedium: ImmutableList<String> = ImmutableList.of(),
override val pickupSoundsLarge: ImmutableList<String> = ImmutableList.of(),
override val smallStackLimit: Long? = null,
override val mediumStackLimit: Long? = null,
override val builder: AssetPath? = null,
) : IItemDefinition, IThingWithDescription by descriptionData

View File

@ -1,19 +0,0 @@
package ru.dbotthepony.kstarbound.defs.item.impl
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
import ru.dbotthepony.kstarbound.defs.item.api.ILiquidItem
import ru.dbotthepony.kstarbound.json.builder.JsonAlias
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.builder.JsonFlat
@JsonFactory
data class LiquidItemDefinition(
@JsonFlat
val parent: IItemDefinition,
@JsonAlias("liquidId", "liquidName")
override val liquid: Either<Int, String>,
) : IItemDefinition by parent, ILiquidItem {
override val itemType: String
get() = super<ILiquidItem>.itemType
}

View File

@ -1,19 +0,0 @@
package ru.dbotthepony.kstarbound.defs.item.impl
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
import ru.dbotthepony.kstarbound.defs.item.api.IMaterialItem
import ru.dbotthepony.kstarbound.json.builder.JsonAlias
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.builder.JsonFlat
@JsonFactory
data class MaterialItemDefinition(
@JsonFlat
val parent: IItemDefinition,
@JsonAlias("materialId", "materialName")
override val material: Either<Int, String>
) : IMaterialItem, IItemDefinition by parent {
override val itemType: String
get() = super<IMaterialItem>.itemType
}

View File

@ -30,6 +30,7 @@ import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.getArray import ru.dbotthepony.kommons.gson.getArray
import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.AssetReference import ru.dbotthepony.kstarbound.defs.AssetReference
import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
@ -153,7 +154,6 @@ data class ObjectDefinition(
val biomePlaced: Boolean = false, val biomePlaced: Boolean = false,
) )
private val objects = gson.getAdapter(JsonObject::class.java)
private val objectRef = gson.getAdapter(JsonReference.Object::class.java) private val objectRef = gson.getAdapter(JsonReference.Object::class.java)
private val basic = gson.getAdapter(PlainData::class.java) private val basic = gson.getAdapter(PlainData::class.java)
private val damageConfig = gson.getAdapter(TileDamageConfig::class.java) private val damageConfig = gson.getAdapter(TileDamageConfig::class.java)
@ -174,7 +174,7 @@ data class ObjectDefinition(
if (`in`.consumeNull()) if (`in`.consumeNull())
return null return null
val read = objects.read(`in`) val read = Starbound.ELEMENTS_ADAPTER.objects.read(`in`)
val basic = basic.fromJsonTree(read) val basic = basic.fromJsonTree(read)
val printable = basic.hasObjectItem && read.get("printable", basic.scannable) val printable = basic.hasObjectItem && read.get("printable", basic.scannable)

View File

@ -11,6 +11,7 @@ import com.google.gson.annotations.JsonAdapter
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.clear import ru.dbotthepony.kommons.gson.clear
import ru.dbotthepony.kommons.util.AABB import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.AABBi import ru.dbotthepony.kommons.util.AABBi
@ -30,6 +31,7 @@ import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile
@ -94,6 +96,8 @@ data class ObjectOrientation(
} }
companion object { companion object {
private val LOGGER = LogManager.getLogger()
fun preprocess(json: JsonArray): JsonArray { fun preprocess(json: JsonArray): JsonArray {
val actual = ArrayList<JsonObject>() val actual = ArrayList<JsonObject>()
@ -136,7 +140,6 @@ data class ObjectOrientation(
} }
class Adapter(gson: Gson) : TypeAdapter<ObjectOrientation>() { class Adapter(gson: Gson) : TypeAdapter<ObjectOrientation>() {
private val objects = gson.getAdapter(JsonObject::class.java)
private val vectors = gson.getAdapter(Vector2f::class.java) private val vectors = gson.getAdapter(Vector2f::class.java)
private val vectorsi = gson.getAdapter(Vector2i::class.java) private val vectorsi = gson.getAdapter(Vector2i::class.java)
private val vectorsd = gson.getAdapter(Vector2d::class.java) private val vectorsd = gson.getAdapter(Vector2d::class.java)
@ -161,7 +164,7 @@ data class ObjectOrientation(
if (`in`.consumeNull()) if (`in`.consumeNull())
return null return null
val obj = objects.read(`in`) val obj = Starbound.ELEMENTS_ADAPTER.objects.read(`in`)
val drawables = ArrayList<Drawable>() val drawables = ArrayList<Drawable>()
val flipImages = obj.get("flipImages", false) val flipImages = obj.get("flipImages", false)
val renderLayer = RenderLayer.parse(obj.get("renderLayer", "Object")) val renderLayer = RenderLayer.parse(obj.get("renderLayer", "Object"))
@ -199,12 +202,16 @@ data class ObjectOrientation(
for (drawable in drawables) { for (drawable in drawables) {
if (drawable is Drawable.Image) { if (drawable is Drawable.Image) {
val bound = drawable.path.with { "default" } val bound = drawable.path.with { "default" }
val sprite = bound.sprite ?: throw IllegalStateException("Not a valid sprite reference: ${bound.raw} (${bound.imagePath} / ${bound.spritePath})") val sprite = bound.sprite
val new = ImmutableSet.Builder<Vector2i>() if (sprite != null) {
new.addAll(occupySpaces) val new = ImmutableSet.Builder<Vector2i>()
new.addAll(sprite.worldSpaces(imagePositionI, obj["spaceScan"].asDouble, flipImages)) new.addAll(occupySpaces)
occupySpaces = new.build() new.addAll(sprite.worldSpaces(imagePositionI, obj["spaceScan"].asDouble, flipImages))
occupySpaces = new.build()
} else {
LOGGER.error("Unable to space scan image, not a valid sprite reference: $bound")
}
} }
} }
} catch (err: Throwable) { } catch (err: Throwable) {

View File

@ -20,6 +20,7 @@ import ru.dbotthepony.kommons.io.writeCollection
import ru.dbotthepony.kommons.io.writeStruct2d import ru.dbotthepony.kommons.io.writeStruct2d
import ru.dbotthepony.kommons.io.writeStruct2f import ru.dbotthepony.kommons.io.writeStruct2f
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.actor.Gender import ru.dbotthepony.kstarbound.defs.actor.Gender
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.io.readInternedString import ru.dbotthepony.kstarbound.io.readInternedString
@ -264,7 +265,6 @@ class QuestParameter(
class Adapter(gson: Gson) : TypeAdapter<QuestParameter>() { class Adapter(gson: Gson) : TypeAdapter<QuestParameter>() {
private val details = DETAIL_ADAPTER.create(gson, TypeToken.get(Detail::class.java))!! private val details = DETAIL_ADAPTER.create(gson, TypeToken.get(Detail::class.java))!!
private val objects = gson.getAdapter(JsonObject::class.java)
override fun write(out: JsonWriter, value: QuestParameter?) { override fun write(out: JsonWriter, value: QuestParameter?) {
if (value == null) if (value == null)
@ -287,7 +287,7 @@ class QuestParameter(
if (`in`.consumeNull()) if (`in`.consumeNull())
return null return null
val read = objects.read(`in`) val read = Starbound.ELEMENTS_ADAPTER.objects.read(`in`)
val detail = if (read["type"]?.asString == "json") { val detail = if (read["type"]?.asString == "json") {
JsonData(read) JsonData(read)

View File

@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.defs.quest
import com.google.common.collect.ImmutableSet import com.google.common.collect.ImmutableSet
import com.google.gson.JsonObject import com.google.gson.JsonObject
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.defs.AssetPath import ru.dbotthepony.kstarbound.defs.AssetPath
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@ -10,8 +11,8 @@ data class QuestTemplate(
val id: String, val id: String,
val prerequisites: ImmutableSet<String> = ImmutableSet.of(), val prerequisites: ImmutableSet<String> = ImmutableSet.of(),
val requiredItems: ImmutableSet<String> = ImmutableSet.of(), val requiredItems: ImmutableSet<String> = ImmutableSet.of(),
val script: AssetPath, val script: AssetPath?,
val updateDelta: Int = 10, val updateDelta: Int = 10,
val moneyRange: LongRange, val moneyRange: Vector2i = Vector2i(0, 0),
val scriptConfig: JsonObject = JsonObject() val scriptConfig: JsonObject = JsonObject()
) )

View File

@ -12,7 +12,7 @@ data class RenderParameters(
val multiColored: Boolean = false, val multiColored: Boolean = false,
val occludesBelow: Boolean = false, val occludesBelow: Boolean = false,
val lightTransparent: Boolean = false, val lightTransparent: Boolean = false,
val zLevel: Long, val zLevel: Long = 0,
) { ) {
init { init {
if (texture != null) if (texture != null)

View File

@ -44,7 +44,7 @@ data class TileDefinition(
val collisionKind: CollisionType = CollisionType.BLOCK, val collisionKind: CollisionType = CollisionType.BLOCK,
override val renderTemplate: AssetReference<RenderTemplate>, override val renderTemplate: AssetReference<RenderTemplate> = AssetReference(null),
override val renderParameters: RenderParameters, override val renderParameters: RenderParameters,
val cascading: Boolean = false, val cascading: Boolean = false,

View File

@ -77,7 +77,6 @@ data class BiomePlaceables(
override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? { override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
if (Item::class.java.isAssignableFrom(type.rawType)) { if (Item::class.java.isAssignableFrom(type.rawType)) {
return object : TypeAdapter<Item>() { return object : TypeAdapter<Item>() {
private val arrays = gson.getAdapter(JsonArray::class.java)
private val grassVariant = gson.getAdapter(GrassVariant::class.java) private val grassVariant = gson.getAdapter(GrassVariant::class.java)
private val bushVariant = gson.getAdapter(BushVariant::class.java) private val bushVariant = gson.getAdapter(BushVariant::class.java)
private val trees = gson.listAdapter<TreeVariant>() private val trees = gson.listAdapter<TreeVariant>()
@ -116,7 +115,7 @@ data class BiomePlaceables(
// Truly our hero here. // Truly our hero here.
val obj = when (val type = `in`.nextString()) { val obj = when (val type = `in`.nextString()) {
"treasureBoxSet" -> TreasureBox(`in`.nextString()) "treasureBoxSet" -> TreasureBox(`in`.nextString())
"microDungeon" -> MicroDungeon(arrays.read(`in`).stream().map { Registries.dungeons.ref(it.asString) }.collect(ImmutableSet.toImmutableSet())) "microDungeon" -> MicroDungeon(Starbound.ELEMENTS_ADAPTER.arrays.read(`in`).stream().map { Registries.dungeons.ref(it.asString) }.collect(ImmutableSet.toImmutableSet()))
"grass" -> Grass(grassVariant.read(`in`)) "grass" -> Grass(grassVariant.read(`in`))
"bush" -> Bush(bushVariant.read(`in`)) "bush" -> Bush(bushVariant.read(`in`))
"treePair" -> Tree(trees.read(`in`)) "treePair" -> Tree(trees.read(`in`))

View File

@ -73,8 +73,6 @@ enum class VisitableWorldParametersType(override val jsonName: String, val token
override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? { override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
if (VisitableWorldParameters::class.java.isAssignableFrom(type.rawType)) { if (VisitableWorldParameters::class.java.isAssignableFrom(type.rawType)) {
return object : TypeAdapter<VisitableWorldParameters>() { return object : TypeAdapter<VisitableWorldParameters>() {
private val objects = gson.getAdapter(JsonObject::class.java)
override fun write(out: JsonWriter, value: VisitableWorldParameters?) { override fun write(out: JsonWriter, value: VisitableWorldParameters?) {
if (value == null) if (value == null)
out.nullValue() out.nullValue()
@ -87,7 +85,7 @@ enum class VisitableWorldParametersType(override val jsonName: String, val token
return null return null
val instance = type.rawType.getDeclaredConstructor().newInstance() as VisitableWorldParameters val instance = type.rawType.getDeclaredConstructor().newInstance() as VisitableWorldParameters
instance.fromJson(objects.read(`in`)) instance.fromJson(Starbound.ELEMENTS_ADAPTER.objects.read(`in`))
return instance return instance
} }
} as TypeAdapter<T> } as TypeAdapter<T>

View File

@ -612,8 +612,6 @@ class WorldLayout {
} }
companion object : TypeAdapter<WorldLayout>() { companion object : TypeAdapter<WorldLayout>() {
private val objects by lazy { Starbound.gson.getAdapter(JsonObject::class.java) }
override fun write(out: JsonWriter, value: WorldLayout?) { override fun write(out: JsonWriter, value: WorldLayout?) {
if (value == null) if (value == null)
out.nullValue() out.nullValue()
@ -626,7 +624,7 @@ class WorldLayout {
return null return null
val params = WorldLayout() val params = WorldLayout()
params.fromJson(objects.read(`in`)) params.fromJson(Starbound.ELEMENTS_ADAPTER.objects.read(`in`))
return params return params
} }
} }

View File

@ -2,12 +2,14 @@ package ru.dbotthepony.kstarbound.io
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.io.readBinaryString import ru.dbotthepony.kommons.io.readBinaryString
import ru.dbotthepony.kommons.io.readVarInt import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kommons.io.readVarLong import ru.dbotthepony.kommons.io.readVarLong
import ru.dbotthepony.kstarbound.IStarboundFile import ru.dbotthepony.kstarbound.IStarboundFile
import ru.dbotthepony.kstarbound.getValue import ru.dbotthepony.kstarbound.getValue
import ru.dbotthepony.kstarbound.json.readJsonObject import ru.dbotthepony.kstarbound.json.readJsonObject
import ru.dbotthepony.kstarbound.util.sbIntern
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.Closeable import java.io.Closeable
import java.io.DataInputStream import java.io.DataInputStream
@ -64,12 +66,22 @@ class StarboundPak(val path: File, callback: (finished: Boolean, status: String)
} }
} }
override fun equals(other: Any?): Boolean {
return other is IStarboundFile && computeFullPath() == other.computeFullPath()
}
private val hash = computeFullPath().hashCode()
override fun hashCode(): Int {
return hash
}
override fun open(): InputStream { override fun open(): InputStream {
throw IllegalStateException("${computeFullPath()} is a directory") throw IllegalStateException("${computeFullPath()} is a directory")
} }
override fun toString(): String { override fun toString(): String {
return "SBDirectory[${computeFullPath()} @ $path]" return "SBDirectory[${computeFullPath()} @ ${metadata.get("friendlyName", "")} $path]"
} }
} }
@ -91,6 +103,16 @@ class StarboundPak(val path: File, callback: (finished: Boolean, status: String)
override val children: Map<String, IStarboundFile>? override val children: Map<String, IStarboundFile>?
get() = null get() = null
override fun equals(other: Any?): Boolean {
return other is IStarboundFile && computeFullPath() == other.computeFullPath()
}
private val hash = computeFullPath().hashCode()
override fun hashCode(): Int {
return hash
}
override fun open(): InputStream { override fun open(): InputStream {
return object : InputStream() { return object : InputStream() {
private var innerOffset = 0L private var innerOffset = 0L
@ -132,22 +154,20 @@ class StarboundPak(val path: File, callback: (finished: Boolean, status: String)
if (readMax <= 0) if (readMax <= 0)
return -1 return -1
synchronized(reader) { reader.seek(innerOffset + offset)
reader.seek(innerOffset + offset) val readBytes = reader.read(b, off, readMax)
val readBytes = reader.read(b, off, readMax)
if (readBytes == -1) if (readBytes == -1)
throw RuntimeException("Unexpected EOF, want to read $readMax bytes from starting $offset in $path") throw RuntimeException("Unexpected EOF, want to read $readMax bytes from starting $offset in $path")
innerOffset += readBytes innerOffset += readBytes
return readBytes return readBytes
}
} }
} }
} }
override fun toString(): String { override fun toString(): String {
return "SBFile[${computeFullPath()} @ $path]" return "SBFile[${computeFullPath()} @ ${metadata.get("friendlyName", "")} $path]"
} }
} }
@ -215,13 +235,13 @@ class StarboundPak(val path: File, callback: (finished: Boolean, status: String)
try { try {
callback(false, "Reading index node $i") callback(false, "Reading index node $i")
name = stream.readBinaryString() name = stream.readInternedString()
require(name[0] == '/') { "index node at $i with '$name' appears to be not an absolute filename" } require(name[0] == '/') { "index node at $i with '$name' appears to be not an absolute filename" }
val offset = stream.readLong() val offset = stream.readLong()
val length = stream.readLong() val length = stream.readLong()
if (offset > flength) { if (offset > flength) {
throw IndexOutOfBoundsException("Garbage offset at index $i: ${offset}") throw IndexOutOfBoundsException("Garbage offset at index $i: $offset")
} }
if (length + offset > flength) { if (length + offset > flength) {
@ -233,10 +253,10 @@ class StarboundPak(val path: File, callback: (finished: Boolean, status: String)
var parent = root as SBDirectory var parent = root as SBDirectory
for (splitIndex in 1 until split.size - 1) { for (splitIndex in 1 until split.size - 1) {
parent = parent.subdir(split[splitIndex]) parent = parent.subdir(split[splitIndex].sbIntern())
} }
parent.put(SBFile(split.last(), parent, offset, length)) parent.put(SBFile(split.last().sbIntern(), parent, offset, length))
} catch (err: Throwable) { } catch (err: Throwable) {
if (name == null) { if (name == null) {
throw IOException("Reading index node at $i", err) throw IOException("Reading index node at $i", err)

View File

@ -0,0 +1,159 @@
package ru.dbotthepony.kstarbound.item
import com.google.common.collect.ImmutableSet
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.IStarboundFile
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.AssetPath
import ru.dbotthepony.kstarbound.defs.item.ItemType
import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
import ru.dbotthepony.kstarbound.fromJson
import ru.dbotthepony.kstarbound.json.JsonPatch
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.Future
object ItemRegistry {
class Entry(
val name: String,
val type: ItemType,
val json: JsonObject,
val isEmpty: Boolean,
val friendlyName: String,
val itemTags: ImmutableSet<String>,
val itemAgingScripts: ImmutableSet<AssetPath>,
val file: IStarboundFile?
) {
val isNotEmpty: Boolean
get() = !isEmpty
val directory = file?.computeDirectory() ?: "/"
}
private val LOGGER = LogManager.getLogger()
private val entries = HashMap<String, Entry>()
private val tasks = ConcurrentLinkedQueue<Runnable>()
val AIR = Entry("", ItemType.GENERIC, JsonObject(), true, "air", ImmutableSet.of(), ImmutableSet.of(), null)
init {
entries[""] = AIR
}
operator fun get(name: String): Entry {
return entries[name] ?: AIR
}
@JsonFactory
data class ReadData(
val itemName: String,
val shortdescription: String = "...",
val itemTags: ImmutableSet<String> = ImmutableSet.of(),
val itemAgingScripts: ImmutableSet<AssetPath> = ImmutableSet.of(),
)
@JsonFactory
data class ReadDataObj(
val shortdescription: String = "...",
val itemTags: ImmutableSet<String> = ImmutableSet.of(),
val itemAgingScripts: ImmutableSet<AssetPath> = ImmutableSet.of(),
)
private fun addObjectItem(obj: Registry.Entry<ObjectDefinition>) {
if (obj.key in entries) {
LOGGER.error("Refusing to overwrite item ${obj.key} with generated item for object (old def originate from ${entries[obj.key]!!.file}; new from ${obj.file})")
return
}
val config = obj.jsonObject.deepCopy()
if ("inventoryIcon" !in config) {
config["inventoryIcon"] = Globals.itemParameters.missingIcon.fullPath
LOGGER.warn("inventoryIcon is missing for object item ${obj.key}, using default ${Globals.itemParameters.missingIcon.fullPath}")
}
config["itemName"] = obj.key
if ("tooltipKind" !in config)
config["tooltipKind"] = "object"
// this is required for Lua call to "get item config"
config["printable"] = obj.value.printable
// Don't inherit object scripts. this is kind of a crappy solution to prevent
// ObjectItems (which are firable and therefore scripted) from trying to
// execute scripts intended for objects
config.remove("scripts")
val readData = Starbound.gson.fromJson(config, ReadDataObj::class.java)
entries[obj.key] = Entry(
name = obj.key,
type = ItemType.OBJECT,
json = config,
isEmpty = false,
friendlyName = readData.shortdescription,
itemTags = readData.itemTags,
itemAgingScripts = readData.itemAgingScripts,
file = obj.file,
)
}
fun finishLoad() {
tasks.forEach { it.run() }
tasks.clear()
for (obj in Registries.worldObjects.keys.values) {
if (obj.value.hasObjectItem) {
addObjectItem(obj)
}
}
}
fun load(fileTree: Map<String, Collection<IStarboundFile>>, patches: Map<String, Collection<IStarboundFile>>): Collection<Future<*>> {
val futures = ArrayList<Future<*>>()
val data by lazy { Starbound.gson.getAdapter(ReadData::class.java) }
for (type in ItemType.entries) {
val files = fileTree[type.extension ?: continue] ?: continue
for (file in files) {
futures.add(Starbound.EXECUTOR.submit {
try {
val read = JsonPatch.apply(Starbound.ELEMENTS_ADAPTER.read(file.jsonReader()), patches[file.computeFullPath()]) as JsonObject
val readData = data.fromJsonTree(read)
tasks.add {
if (readData.itemName in entries) {
LOGGER.warn("Overwriting item definition at ${readData.itemName} (old def originate from ${entries[readData.itemName]!!.file}; new from $file)")
}
entries[readData.itemName] = Entry(
name = readData.itemName,
type = type,
json = read,
isEmpty = false,
friendlyName = readData.shortdescription,
itemTags = readData.itemTags,
itemAgingScripts = readData.itemAgingScripts,
file = file,
)
}
} catch (err: Throwable) {
LOGGER.error("Reading item definition $file", err)
}
})
}
}
return futures
}
}

View File

@ -1,43 +1,58 @@
package ru.dbotthepony.kstarbound.item package ru.dbotthepony.kstarbound.item
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableSet
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonNull import com.google.gson.JsonNull
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive import com.google.gson.JsonPrimitive
import com.google.gson.TypeAdapter import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.internal.bind.TypeAdapters import com.google.gson.internal.bind.TypeAdapters
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import org.classdump.luna.Table import org.classdump.luna.Table
import org.classdump.luna.TableFactory import org.classdump.luna.TableFactory
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.value
import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeVarLong import ru.dbotthepony.kommons.io.writeVarLong
import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.vector.Vector2f
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.Drawable
import ru.dbotthepony.kstarbound.defs.image.SpriteReference
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.defs.item.ItemRarity
import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.json.stream
import ru.dbotthepony.kstarbound.json.writeJsonElement import ru.dbotthepony.kstarbound.json.writeJsonElement
import ru.dbotthepony.kstarbound.lua.LuaEnvironment import ru.dbotthepony.kstarbound.lua.LuaEnvironment
import ru.dbotthepony.kstarbound.lua.from import ru.dbotthepony.kstarbound.lua.from
import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.util.ManualLazy
import ru.dbotthepony.kstarbound.util.valueOf
import java.io.DataOutputStream import java.io.DataOutputStream
import java.lang.Math.toRadians
import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicLong
import kotlin.math.max
import kotlin.properties.Delegates
/** /**
* Base class for instanced items in game * Base class for instanced items in game
*/ */
open class ItemStack { @JsonAdapter(ItemStack.Adapter::class)
constructor() { open class ItemStack(descriptor: ItemDescriptor) {
this.config = Registries.items.emptyRef
this.parameters = JsonObject()
}
constructor(descriptor: ItemDescriptor) {
this.config = descriptor.ref
this.size = descriptor.count
this.parameters = descriptor.parameters.deepCopy()
}
/** /**
* unique number utilized to determine whenever stack has changed * unique number utilized to determine whenever stack has changed
*/ */
@ -52,7 +67,7 @@ open class ItemStack {
changeset = CHANGESET.incrementAndGet() changeset = CHANGESET.incrementAndGet()
} }
var size: Long = 0L var size: Long = descriptor.count
set(value) { set(value) {
val newValue = value.coerceAtLeast(0L) val newValue = value.coerceAtLeast(0L)
@ -62,18 +77,21 @@ open class ItemStack {
} }
} }
val config: Registry.Ref<IItemDefinition> val config: ItemRegistry.Entry = descriptor.ref
var parameters: JsonObject var parameters: JsonObject = descriptor.parameters.deepCopy()
protected set protected set
protected val parametersLazies = ObjectArrayList<ManualLazy<*>>() // no CME checks
protected val mergedJson = ManualLazy {
mergeJson(config.json.deepCopy(), parameters)
}.also { parametersLazies.add(it) }
val isEmpty: Boolean val isEmpty: Boolean
get() = size <= 0 || config.isEmpty get() = size <= 0 || config.isEmpty
val isNotEmpty: Boolean val isNotEmpty: Boolean
get() = size > 0 && config.isPresent get() = size > 0 && !config.isEmpty
val maxStackSize: Long
get() = config.value?.maxStack ?: 0L
fun grow(amount: Long) { fun grow(amount: Long) {
size += amount size += amount
@ -83,12 +101,135 @@ open class ItemStack {
size -= amount size -= amount
} }
protected fun lookupProperty(name: String): JsonElement {
return mergedJson.value[name] ?: JsonNull.INSTANCE
}
protected fun lookupProperty(name: String, orElse: JsonElement): JsonElement {
return mergedJson.value[name] ?: orElse
}
protected fun lookupProperty(name: String, orElse: () -> JsonElement): JsonElement {
return mergedJson.value[name] ?: orElse()
}
protected fun <T> lazyProperty(name: String, default: JsonElement, transform: JsonElement.() -> T): ManualLazy<T> {
return ManualLazy {
transform(lookupProperty(name, default))
}.also { parametersLazies.add(it) }
}
protected fun <T> lazyProperty(name: String, transform: JsonElement.() -> T): ManualLazy<T> {
return ManualLazy {
transform(lookupProperty(name))
}.also { parametersLazies.add(it) }
}
protected fun <T> lazyProperty(name: String, default: () -> JsonElement, transform: JsonElement.() -> T): ManualLazy<T> {
return ManualLazy {
transform(lookupProperty(name, default))
}.also { parametersLazies.add(it) }
}
val maxStackSize: Long by lazyProperty("maxStack", JsonPrimitive(Globals.itemParameters.defaultMaxStack)) { asLong }
val shortDescription: String by lazyProperty("shortdescription", JsonPrimitive("")) { asString }
val description: String by lazyProperty("description", JsonPrimitive("")) { asString }
val rarity: ItemRarity by lazyProperty("rarity") { ItemRarity.entries.valueOf(asString) }
val twoHanded: Boolean by lazyProperty("twoHanded", JsonPrimitive(false)) { asBoolean }
val price: Long by lazyProperty("price", JsonPrimitive(Globals.itemParameters.defaultPrice)) { asLong }
val tooltipKind: String by lazyProperty("tooltipKind", JsonPrimitive("")) { asString }
val category: String by lazyProperty("category", JsonPrimitive("")) { asString }
val pickupSounds: Set<String> by lazyProperty("pickupSounds", { JsonArray() }) { if (asJsonArray.isEmpty) Globals.itemParameters.pickupSoundsAbsolute else asJsonArray.stream().map { it.asString }.collect(ImmutableSet.toImmutableSet()) }
val timeToLive: Double by lazyProperty("timeToLive", JsonPrimitive(Globals.itemParameters.defaultTimeToLive)) { asDouble }
val learnBlueprintsOnPickup: Set<ItemDescriptor> by lazyProperty("learnBlueprintsOnPickup", { JsonArray() }) { asJsonArray.stream().map { ItemDescriptor(it) }.collect(ImmutableSet.toImmutableSet()) }
// collection -> entry
val collectablesOnPickup: Map<String, String> by lazyProperty("collectablesOnPickup", { JsonObject() }) { asJsonObject.entrySet().stream().map { it.key to it.value.asString }.collect(ImmutableMap.toImmutableMap({ it.first }, { it.second })) }
var drawables: ImmutableList<Drawable> by Delegates.notNull()
private set
protected fun setIconDrawables(drawables: Collection<Drawable>) {
val drawablesList = ArrayList(drawables)
if (drawablesList.isNotEmpty()) {
var boundingBox = drawablesList.first().boundingBox(true)
for (i in 1 until drawablesList.size) {
boundingBox = boundingBox.combine(drawablesList[i].boundingBox(true))
}
for (drawable in drawablesList.indices) {
drawablesList[drawable] = drawablesList[drawable].translate(boundingBox.centre.toFloatVector())
}
// as original engine puts it:
// TODO: Why 16? Is this the size of the icon container? Shouldn't this be configurable?
// in other news,
// TODO: scaling is always centered on 0.0, 0.0; this also should be configurable.
val zoom = 16.0 / max(boundingBox.width, boundingBox.height)
if (zoom < 1.0) {
for (drawable in drawablesList.indices) {
drawablesList[drawable] = drawablesList[drawable].scale(zoom.toFloat())
}
}
}
this.drawables = ImmutableList.copyOf(drawablesList)
}
init {
val inventoryIcon = lookupProperty("inventoryIcon", JsonPrimitive(Globals.itemParameters.missingIcon.fullPath))
if (inventoryIcon.isJsonArray) {
val drawables = ArrayList<Drawable>()
for (drawable in inventoryIcon.asJsonArray) {
drawable as JsonObject
val image = SpriteReference.create(AssetPathStack.relativeTo(config.directory, drawable["image"].asString))
val position: Vector2f
if ("position" in drawable) {
position = Starbound.gson.fromJson(drawable["position"], Vector2f::class.java)
} else {
position = Vector2f.ZERO
}
val scale = if ("scale" in drawable) {
scales.fromJsonTree(drawable["scale"])
} else {
Either.left(1f)
}
val color = if ("color" in drawable) {
colors.fromJsonTree(drawable["color"])
} else {
RGBAColor.WHITE
}
val rotation = toRadians(drawable.get("rotation", 0.0)).toFloat()
val transforms = Drawable.Transformations(drawable.get("centered", true), rotation, drawable.get("mirrored", false), scale)
drawables.add(Drawable.Image(image, Either.right(transforms), position, color, drawable.get("fullbright", false)))
}
setIconDrawables(drawables)
} else {
val image = SpriteReference.create(AssetPathStack.relativeTo(config.directory, inventoryIcon.asString))
val transforms = Drawable.CENTERED
val drawable = Drawable.Image(image, Either.right(transforms))
setIconDrawables(listOf(drawable))
}
}
data class AgingResult(val new: ItemStack?, val ageUpdated: Boolean) data class AgingResult(val new: ItemStack?, val ageUpdated: Boolean)
private fun config() = config
private val agingScripts: LuaEnvironment? by lazy { private val agingScripts: LuaEnvironment? by lazy {
val config = config().value ?: return@lazy null //val config = config.value ?: return@lazy null
//if (config.itemTags) //if (config.itemTags)
null null
} }
@ -119,7 +260,7 @@ open class ItemStack {
if (isEmpty) if (isEmpty)
return ItemDescriptor.EMPTY return ItemDescriptor.EMPTY
return ItemDescriptor(config.key.left(), size, parameters.deepCopy()) return ItemDescriptor(config.name, size, parameters.deepCopy())
} }
// faster than creating an item descriptor and writing it (because it avoids copying and allocation) // faster than creating an item descriptor and writing it (because it avoids copying and allocation)
@ -129,7 +270,7 @@ open class ItemStack {
stream.writeVarLong(0L) stream.writeVarLong(0L)
stream.writeJsonElement(JsonNull.INSTANCE) stream.writeJsonElement(JsonNull.INSTANCE)
} else { } else {
stream.writeBinaryString(config.key.left()) stream.writeBinaryString(config.name)
stream.writeVarLong(size) stream.writeVarLong(size)
stream.writeJsonElement(parameters) stream.writeJsonElement(parameters)
} }
@ -182,14 +323,14 @@ open class ItemStack {
if (isEmpty) if (isEmpty)
return "ItemStack.EMPTY" return "ItemStack.EMPTY"
return "ItemDescriptor[${config.value?.itemName}, count = $size, params = $parameters]" return "ItemStack[${config.name}, count = $size, params = $parameters]"
} }
fun copy(size: Long = this.size): ItemStack { open fun copy(size: Long = this.size): ItemStack {
if (isEmpty) if (isEmpty)
return this return this
return ItemStack(ItemDescriptor(config, size, parameters.deepCopy())) return ItemStack(ItemDescriptor(config.name, size, parameters.deepCopy()))
} }
fun toJson(): JsonObject? { fun toJson(): JsonObject? {
@ -197,7 +338,7 @@ open class ItemStack {
return null return null
return JsonObject().also { return JsonObject().also {
it.add("name", JsonPrimitive(config.key.left())) it.add("name", JsonPrimitive(config.name))
it.add("count", JsonPrimitive(size)) it.add("count", JsonPrimitive(size))
it.add("parameters", parameters.deepCopy()) it.add("parameters", parameters.deepCopy())
} }
@ -209,27 +350,27 @@ open class ItemStack {
} }
return allocator.newTable(0, 3).also { return allocator.newTable(0, 3).also {
it.rawset("name", config.key.left()) it.rawset("name", config.name)
it.rawset("count", size) it.rawset("count", size)
it.rawset("parameters", allocator.from(parameters)) it.rawset("parameters", allocator.from(parameters))
} }
} }
class Adapter(val starbound: Starbound) : TypeAdapter<ItemStack>() { class Adapter(gson: Gson) : TypeAdapter<ItemStack>() {
override fun write(out: JsonWriter, value: ItemStack?) { override fun write(out: JsonWriter, value: ItemStack?) {
val json = value?.toJson() val json = value?.toJson()
if (json == null) if (json == null)
out.nullValue() out.nullValue()
else else
TypeAdapters.JSON_ELEMENT.write(out, json) out.value(json)
} }
override fun read(`in`: JsonReader): ItemStack { override fun read(`in`: JsonReader): ItemStack {
if (`in`.consumeNull()) if (`in`.consumeNull())
return EMPTY return EMPTY
return starbound.item(TypeAdapters.JSON_ELEMENT.read(`in`)) return create(ItemDescriptor(Starbound.ELEMENTS_ADAPTER.read(`in`)))
} }
} }
@ -237,7 +378,19 @@ open class ItemStack {
private val CHANGESET = AtomicLong() private val CHANGESET = AtomicLong()
@JvmField @JvmField
val EMPTY = ItemStack() val EMPTY = ItemStack(ItemDescriptor.EMPTY)
private val vectors by lazy {
Starbound.gson.getAdapter(object : TypeToken<Vector2f>() {})
}
private val scales by lazy {
Starbound.gson.getAdapter(object : TypeToken<Either<Float, Vector2f>>() {})
}
private val colors by lazy {
Starbound.gson.getAdapter(object : TypeToken<RGBAColor>() {})
}
fun create(descriptor: ItemDescriptor): ItemStack { fun create(descriptor: ItemDescriptor): ItemStack {
if (descriptor.isEmpty) if (descriptor.isEmpty)

View File

@ -1,13 +1,15 @@
package ru.dbotthepony.kstarbound package ru.dbotthepony.kstarbound.item
import com.google.gson.JsonElement import com.google.gson.JsonElement
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.IStarboundFile
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.actor.player.RecipeDefinition import ru.dbotthepony.kstarbound.defs.actor.player.RecipeDefinition
import ru.dbotthepony.kstarbound.json.JsonPatch
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.ExecutorService
import java.util.concurrent.Future import java.util.concurrent.Future
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@ -29,7 +31,7 @@ object RecipeRegistry {
val output2recipes: Map<String, List<Entry>> = Collections.unmodifiableMap(output2recipesBacking) val output2recipes: Map<String, List<Entry>> = Collections.unmodifiableMap(output2recipesBacking)
val input2recipes: Map<String, List<Entry>> = Collections.unmodifiableMap(input2recipesBacking) val input2recipes: Map<String, List<Entry>> = Collections.unmodifiableMap(input2recipesBacking)
private val backlog = ConcurrentLinkedQueue<Entry>() private val tasks = ConcurrentLinkedQueue<Entry>()
private fun add(recipe: Entry) { private fun add(recipe: Entry) {
val value = recipe.value val value = recipe.value
@ -59,26 +61,22 @@ object RecipeRegistry {
} }
fun finishLoad() { fun finishLoad() {
var next = backlog.poll() tasks.forEach { add(it) }
tasks.clear()
while (next != null) {
add(next)
next = backlog.poll()
}
} }
fun load(fileTree: Map<String, List<IStarboundFile>>): List<Future<*>> { fun load(fileTree: Map<String, Collection<IStarboundFile>>, patchTree: Map<String, List<IStarboundFile>>): List<Future<*>> {
val files = fileTree["recipe"] ?: return emptyList() val files = fileTree["recipe"] ?: return emptyList()
val elements = Starbound.gson.getAdapter(JsonElement::class.java) val recipes by lazy { Starbound.gson.getAdapter(RecipeDefinition::class.java) }
val recipes = Starbound.gson.getAdapter(RecipeDefinition::class.java)
return files.map { listedFile -> return files.map { listedFile ->
Starbound.EXECUTOR.submit { Starbound.EXECUTOR.submit {
try { try {
val json = elements.read(listedFile.jsonReader()) val json = JsonPatch.apply(Starbound.ELEMENTS_ADAPTER.read(listedFile.jsonReader()), patchTree[listedFile.computeFullPath()])
val value = recipes.fromJsonTree(json) val value = recipes.fromJsonTree(json)
backlog.add(Entry(value, json, listedFile)) tasks.add(Entry(value, json, listedFile))
} catch (err: Throwable) { } catch (err: Throwable) {
LOGGER.error("Loading recipe definition file $listedFile", err) LOGGER.error("Loading recipe definition file $listedFile", err)
} }

View File

@ -19,12 +19,17 @@ class InternedJsonElementAdapter(val stringInterner: Interner<String>) : TypeAda
return TypeAdapters.JSON_ELEMENT.write(out, value) return TypeAdapters.JSON_ELEMENT.write(out, value)
} }
override fun read(`in`: JsonReader): JsonElement? { override fun read(`in`: JsonReader): JsonElement {
return when (val p = `in`.peek()) { return when (val p = `in`.peek()) {
JsonToken.STRING -> JsonPrimitive(stringInterner.intern(`in`.nextString())) JsonToken.STRING -> JsonPrimitive(stringInterner.intern(`in`.nextString()))
JsonToken.NUMBER -> JsonPrimitive(LazilyParsedNumber(`in`.nextString())) JsonToken.NUMBER -> JsonPrimitive(LazilyParsedNumber(stringInterner.intern(`in`.nextString())))
JsonToken.BOOLEAN -> if (`in`.nextBoolean()) TRUE else FALSE JsonToken.BOOLEAN -> if (`in`.nextBoolean()) TRUE else FALSE
JsonToken.NULL -> JsonNull.INSTANCE
JsonToken.NULL -> {
`in`.nextNull()
JsonNull.INSTANCE
}
JsonToken.BEGIN_ARRAY -> arrays.read(`in`) JsonToken.BEGIN_ARRAY -> arrays.read(`in`)
JsonToken.BEGIN_OBJECT -> objects.read(`in`) JsonToken.BEGIN_OBJECT -> objects.read(`in`)
else -> throw IllegalArgumentException(p.toString()) else -> throw IllegalArgumentException(p.toString())

View File

@ -0,0 +1,133 @@
package ru.dbotthepony.kstarbound.json
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kstarbound.IStarboundFile
import ru.dbotthepony.kstarbound.Starbound
enum class JsonPatch(val key: String) {
TEST("test") {
override fun apply(base: JsonElement, data: JsonObject): JsonElement {
val path = JsonPath.pointer(data.get("path").asString)
val value = data["value"] ?: JsonNull.INSTANCE
val inverse = data.get("inverse", false)
val get = path.find(base) ?: JsonNull.INSTANCE
if (inverse && get == value) {
throw IllegalStateException("Expected $path to not contain $value")
} else if (!inverse && get != value && value != JsonNull.INSTANCE) {
var text = get.toString()
if (text.length > 40) {
text = text.substring(0, 40) + "..."
}
throw IllegalStateException("Expected $path to contain $value, but found $text")
}
return base
}
},
REMOVE("remove") {
override fun apply(base: JsonElement, data: JsonObject): JsonElement {
return JsonPath.pointer(data.get("path").asString).remove(base).first
}
},
ADD("add") {
override fun apply(base: JsonElement, data: JsonObject): JsonElement {
val value = data["value"] ?: throw JsonSyntaxException("Missing 'value' to 'add' operation")
return JsonPath.pointer(data.get("path").asString).add(base, value)
}
},
REPLACE("replace") {
override fun apply(base: JsonElement, data: JsonObject): JsonElement {
val value = data["value"] ?: throw JsonSyntaxException("Missing 'value' to 'add' operation")
val path = JsonPath.pointer(data.get("path").asString)
return path.add(path.remove(base).first, value)
}
},
MOVE("move") {
override fun apply(base: JsonElement, data: JsonObject): JsonElement {
val from = JsonPath.pointer(data.get("from").asString)
val path = JsonPath.pointer(data.get("path").asString)
val (newBase, removed) = from.remove(base)
return path.add(newBase, removed)
}
},
COPY("copy") {
override fun apply(base: JsonElement, data: JsonObject): JsonElement {
val from = JsonPath.pointer(data.get("from").asString)
val path = JsonPath.pointer(data.get("path").asString)
return path.add(base, from.get(base))
}
};
abstract fun apply(base: JsonElement, data: JsonObject): JsonElement
companion object {
private val LOGGER = LogManager.getLogger()
fun apply(base: JsonElement, data: JsonObject): JsonElement {
val op = data["op"]?.asString ?: throw JsonSyntaxException("Missing 'op' in json patch operation")
val operation = entries.firstOrNull { it.key == op } ?: throw JsonSyntaxException("Unknown patch operation $op!")
return operation.apply(base, data)
}
@Suppress("NAME_SHADOWING")
fun apply(base: JsonElement, data: JsonArray, source: IStarboundFile?): JsonElement {
// original engine decides what to do based on first element encountered
// in patch array... let's not.
var base = base
for ((i, element) in data.withIndex()) {
if (element is JsonObject) {
try {
base = apply(base, element)
} catch (err: Throwable) {
LOGGER.error("Error while applying JSON patch from $source at index $i: ${err.message}")
}
} else if (element is JsonArray) {
for ((i2, sub) in element.withIndex()) {
try {
if (sub !is JsonObject)
throw JsonSyntaxException("Expected JSON Object as patch data, got $sub")
base = apply(base, sub)
} catch (err: Throwable) {
LOGGER.error("Error while applying JSON patch from $source at index $i -> $i2: ${err.message}")
break
}
}
} else {
LOGGER.error("Unknown data in $source at index $i")
}
}
return base
}
@Suppress("NAME_SHADOWING")
fun apply(base: JsonElement, source: Collection<IStarboundFile>?): JsonElement {
source ?: return base
var base = base
for (patch in source) {
val read = Starbound.ELEMENTS_ADAPTER.read(patch.jsonReader())
if (read !is JsonArray) {
LOGGER.error("$patch root element is not an array")
} else {
base = apply(base, read, patch)
}
}
return base
}
}
}

View File

@ -7,11 +7,9 @@ import com.google.gson.JsonNull
import com.google.gson.JsonObject import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.objects.ObjectArrayList import it.unimi.dsi.fastutil.objects.ObjectArrayList
import ru.dbotthepony.kommons.gson.contains import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
fun jsonQuery(path: String) = JsonPath.query(path)
fun jsonPointer(path: String) = JsonPath.pointer(path)
class JsonPath private constructor(val pieces: ImmutableList<Piece>) { class JsonPath private constructor(val pieces: ImmutableList<Piece>) {
constructor(path: String) : this(ImmutableList.of(Piece(path))) constructor(path: String) : this(ImmutableList.of(Piece(path)))
@ -66,62 +64,26 @@ class JsonPath private constructor(val pieces: ImmutableList<Piece>) {
return JsonPath(ImmutableList.copyOf(build)) return JsonPath(ImmutableList.copyOf(build))
} }
private fun reconstructPath(at: Int): String { fun reconstructPath(at: Int = pieces.size - 1): String {
if (at == 0) if (at == pieces.size - 1)
return "<root>" return toString()
else
return "/${pieces.joinToString("/", limit = at, truncated = "") { it.value }}" return "/${pieces.joinToString("/", limit = at + 1, truncated = "") { it.value }}"
} }
/** /**
* Attempts to find given element along path, if can't, throws [TraversalException] * Attempts to find given element along path, if can't, throws [TraversalException]
*/ */
fun get(element: JsonElement): JsonElement { fun get(element: JsonElement): JsonElement {
if (isEmpty) { return traverse(element, { self, index ->
return element if (index == null || index !in 0 until self.size()) {
} throw TraversalException("Path at ${reconstructPath()} points at non-existing index")
var current = element
for ((i, piece) in pieces.withIndex()) {
if (current is JsonObject) {
if (piece.value !in current) {
throw TraversalException("Path at ${reconstructPath(i)} points at non-existing element")
}
current = current[piece.value]!!
if (i != pieces.size - 1 && current !is JsonObject && current !is JsonArray) {
throw TraversalException("Path at ${reconstructPath(i)} expects to get index-able element from underlying object (for ${pieces[i + 1]}), but there is $current")
}
} else if (current is JsonArray) {
if (piece.value == "-") {
throw TraversalException("Path at ${reconstructPath(i)} points at non-existent index")
}
val key = piece.int
if (key == null) {
throw TraversalException("Path at ${reconstructPath(i)} can not index an array")
} else if (key < 0) {
throw TraversalException("Path at ${reconstructPath(i)} points at pseudo index")
}
try {
current = current[key]
} catch (err: IndexOutOfBoundsException) {
throw TraversalException("Path at ${reconstructPath(i)} points at non-existing index")
}
if (i != pieces.size - 1 && current !is JsonObject && current !is JsonArray) {
throw TraversalException("Path at ${reconstructPath(i)} expects to get index-able element from underlying array (for ${pieces[i + 1]}), but there is $current")
}
} else {
throw TraversalException("Path at ${reconstructPath(i)} can not index $current")
} }
}
return current self[index]
}, { self, index ->
self[index] ?: throw TraversalException("Path at ${reconstructPath()} points at non-existing element")
}).orElse(element)
} }
/** /**
@ -169,6 +131,121 @@ class JsonPath private constructor(val pieces: ImmutableList<Piece>) {
return find(element) ?: orElse return find(element) ?: orElse
} }
fun <T> traverse(element: JsonElement, arrayOperator: (JsonArray, Int?) -> T, objectOperator: (JsonObject, String) -> T): KOptional<T> {
var current = element
for ((i, piece) in pieces.withIndex()) {
if (current is JsonObject) {
if (i == pieces.size - 1) {
return KOptional(objectOperator(current, piece.value))
} else {
if (piece.value !in current) {
throw TraversalException("Path at ${reconstructPath(i)} points at non-existing element")
}
current = current[piece.value]!!
if (current !is JsonObject && current !is JsonArray) {
throw TraversalException("Path at ${reconstructPath(i)} expects to get index-able element from underlying object (for ${pieces[i + 1]}), but there is $current")
}
}
} else if (current is JsonArray) {
if (piece.value == "-") {
if (i == pieces.size - 1) {
return KOptional(arrayOperator(current, null))
}
throw TraversalException("Path at ${reconstructPath(i)} points at non-existent index")
}
var key = piece.int
if (key == null) {
throw TraversalException("Path at ${reconstructPath(i)} can not index an array")
} else if (key < 0) {
key += current.size()
if (key < 0)
throw TraversalException("Path at ${reconstructPath(i)} points at pseudo index, which is out of range (array size: ${current.size()})")
if (i == pieces.size - 1) {
return KOptional(arrayOperator(current, key))
}
} else if (i == pieces.size - 1) {
return KOptional(arrayOperator(current, key))
} else if (key !in 0 until current.size()) {
throw TraversalException("Path at ${reconstructPath(i)} points at non-existing index")
}
current = current[key]
if (current !is JsonObject && current !is JsonArray) {
throw TraversalException("Path at ${reconstructPath(i)} expects to get index-able element from underlying array (for ${pieces[i + 1]}), but there is $current")
}
} else {
throw TraversalException("Path at ${reconstructPath(i)} can not index $current")
}
}
return KOptional()
}
fun add(base: JsonElement, value: JsonElement): JsonElement {
if (isEmpty) {
check(base.isJsonNull) { "Cannot add a value to the entire document, it is not empty." }
return value
}
traverse(base, { self, index ->
if (index == null)
self.add(value)
else if (index !in 0 .. self.size())
throw IndexOutOfBoundsException("Element at ${reconstructPath()} trying to insert new value into non-existing index (array size: ${self.size()})")
else if (index == self.size())
self.add(value)
else {
// jsonarray does not support insertion
val values = ArrayList<JsonElement>(self.size() - index)
while (self.size() > index) {
values.add(self.remove(self.size() - 1))
}
self.add(value)
while (values.isNotEmpty()) {
self.add(values.removeLast())
}
}
}, { self, index ->
self[index] = value
})
return base
}
fun remove(base: JsonElement): Pair<JsonElement, JsonElement> {
if (isEmpty) {
return JsonNull.INSTANCE to base
}
val removed = traverse(base, { self, index ->
if (index == null)
throw NoSuchElementException("Николай протоген")
else if (index !in 0 until self.size())
throw NoSuchElementException("Element at ${reconstructPath()} does not exist (array size: ${self.size()})")
else
self.remove(index)
}, { self, index ->
if (index !in self)
throw NoSuchElementException("Element at ${reconstructPath()} does not exist")
self.remove(index)
}).orThrow { RuntimeException() }
return base to removed
}
companion object { companion object {
val EMPTY = JsonPath(ImmutableList.of()) val EMPTY = JsonPath(ImmutableList.of())

View File

@ -11,6 +11,7 @@ import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.gson.value import ru.dbotthepony.kommons.gson.value
import ru.dbotthepony.kstarbound.Starbound
inline fun <reified E : Any, reified C : Any> DispatchingAdapter( inline fun <reified E : Any, reified C : Any> DispatchingAdapter(
key: String, key: String,
@ -40,7 +41,6 @@ class DispatchingAdapter<TYPE : Any, ELEMENT : Any>(
private inner class Impl(gson: Gson) : TypeAdapter<ELEMENT>() { private inner class Impl(gson: Gson) : TypeAdapter<ELEMENT>() {
private val typeAdapter = gson.getAdapter(typeClass) private val typeAdapter = gson.getAdapter(typeClass)
private val adapters = types.associateWith { gson.getAdapter(type2value.invoke(it)) } as Map<TYPE, TypeAdapter<ELEMENT>> private val adapters = types.associateWith { gson.getAdapter(type2value.invoke(it)) } as Map<TYPE, TypeAdapter<ELEMENT>>
private val obj = gson.getAdapter(JsonObject::class.java)
override fun write(out: JsonWriter, value: ELEMENT?) { override fun write(out: JsonWriter, value: ELEMENT?) {
if (value == null) if (value == null)
@ -75,7 +75,7 @@ class DispatchingAdapter<TYPE : Any, ELEMENT : Any>(
if (`in`.consumeNull()) if (`in`.consumeNull())
return null return null
val read = obj.read(`in`) val read = Starbound.ELEMENTS_ADAPTER.objects.read(`in`)
val type = typeAdapter.fromJsonTree(read[key] ?: throw JsonSyntaxException("Missing '$key'")) val type = typeAdapter.fromJsonTree(read[key] ?: throw JsonSyntaxException("Missing '$key'"))
val adapter = adapters[type] ?: throw JsonSyntaxException("Unknown type $type (${read[key]})") val adapter = adapters[type] ?: throw JsonSyntaxException("Unknown type $type (${read[key]})")
return adapter.fromJsonTree(read) return adapter.fromJsonTree(read)

View File

@ -432,7 +432,7 @@ class FactoryAdapter<T : Any> private constructor(
stringInterner = stringInterner, stringInterner = stringInterner,
aliases = aliases, aliases = aliases,
logMisses = logMisses, logMisses = logMisses,
elements = gson.getAdapter(JsonElement::class.java) elements = Starbound.ELEMENTS_ADAPTER
) )
} }

View File

@ -1,9 +1,13 @@
package ru.dbotthepony.kstarbound.json.builder package ru.dbotthepony.kstarbound.json.builder
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.TypeAdapter import com.google.gson.TypeAdapter
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.Starbound
import kotlin.properties.Delegates import kotlin.properties.Delegates
import kotlin.reflect.KMutableProperty1 import kotlin.reflect.KMutableProperty1
import kotlin.reflect.KProperty1 import kotlin.reflect.KProperty1
@ -49,6 +53,12 @@ open class ReferencedProperty<T : Any, V>(
} else { } else {
adapter = gson.getAdapter(TypeToken.getParameterized(KOptional.NotNull::class.java, subtype.type!!.javaType)) as TypeAdapter<V> adapter = gson.getAdapter(TypeToken.getParameterized(KOptional.NotNull::class.java, subtype.type!!.javaType)) as TypeAdapter<V>
} }
} else if (token.rawType === JsonElement::class.java) {
adapter = Starbound.ELEMENTS_ADAPTER as TypeAdapter<V>
} else if (token.rawType === JsonArray::class.java) {
adapter = Starbound.ELEMENTS_ADAPTER.arrays as TypeAdapter<V>
} else if (token.rawType === JsonObject::class.java) {
adapter = Starbound.ELEMENTS_ADAPTER.objects as TypeAdapter<V>
} else { } else {
adapter = gson.getAdapter(token) as TypeAdapter<V> adapter = gson.getAdapter(token) as TypeAdapter<V>
} }

View File

@ -4,6 +4,7 @@ import it.unimi.dsi.fastutil.objects.ObjectArraySet
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import org.classdump.luna.ByteString import org.classdump.luna.ByteString
import org.classdump.luna.LuaObject import org.classdump.luna.LuaObject
import org.classdump.luna.LuaRuntimeException
import org.classdump.luna.LuaType import org.classdump.luna.LuaType
import org.classdump.luna.StateContext import org.classdump.luna.StateContext
import org.classdump.luna.Table import org.classdump.luna.Table
@ -27,6 +28,7 @@ import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.AssetPath import ru.dbotthepony.kstarbound.defs.AssetPath
import ru.dbotthepony.kstarbound.lua.bindings.provideRootBindings import ru.dbotthepony.kstarbound.lua.bindings.provideRootBindings
import ru.dbotthepony.kstarbound.lua.bindings.provideUtilityBindings import ru.dbotthepony.kstarbound.lua.bindings.provideUtilityBindings
import ru.dbotthepony.kstarbound.util.random.random
class LuaEnvironment : StateContext { class LuaEnvironment : StateContext {
private var nilMeta: Table? = null private var nilMeta: Table? = null
@ -126,6 +128,7 @@ class LuaEnvironment : StateContext {
val globals: Table = newTable() val globals: Table = newTable()
val executor: DirectCallExecutor = DirectCallExecutor.newExecutor() val executor: DirectCallExecutor = DirectCallExecutor.newExecutor()
var random = random()
init { init {
globals["_G"] = globals globals["_G"] = globals
@ -163,6 +166,39 @@ class LuaEnvironment : StateContext {
StringLib.installInto(this, globals) StringLib.installInto(this, globals)
OsLib.installInto(this, globals, RuntimeEnvironments.system()) OsLib.installInto(this, globals, RuntimeEnvironments.system())
val math = globals["math"] as Table
math["random"] = luaFunction { origin: Number?, bound: Number? ->
if (origin == null && bound == null) {
returnBuffer.setTo(random.nextDouble())
} else if (bound == null) {
val origin = origin!!.toLong()
if (origin > 1L) {
returnBuffer.setTo(random.nextLong(1L, origin))
} else if (origin == 1L) {
returnBuffer.setTo(1L)
} else {
throw LuaRuntimeException("bad argument #1 to 'random' (interval is empty)")
}
} else {
val origin = origin!!.toLong()
val bound = bound.toLong()
if (bound > origin) {
returnBuffer.setTo(random.nextLong(origin, bound))
} else if (bound == origin) {
returnBuffer.setTo(origin)
} else {
throw LuaRuntimeException("bad argument #1 to 'random' (interval is empty)")
}
}
}
math["randomseed"] = luaFunction { seed: Number ->
random = random(seed.toLong())
}
// TODO: NYI, maybe polyfill? // TODO: NYI, maybe polyfill?
Utf8Lib.installInto(this, globals) Utf8Lib.installInto(this, globals)
@ -210,7 +246,7 @@ class LuaEnvironment : StateContext {
errorState = true errorState = true
} }
fun init(): Boolean { fun init(callInit: Boolean = true): Boolean {
check(!initCalled) { "Already called init()" } check(!initCalled) { "Already called init()" }
initCalled = true initCalled = true
@ -227,15 +263,17 @@ class LuaEnvironment : StateContext {
scripts.clear() scripts.clear()
val init = globals["init"] if (callInit) {
val init = globals["init"]
if (init is LuaFunction<*, *, *, *, *>) { if (init is LuaFunction<*, *, *, *, *>) {
try { try {
executor.call(this, init) executor.call(this, init)
} catch (err: Throwable) { } catch (err: Throwable) {
errorState = true errorState = true
LOGGER.error("Exception on init()", err) LOGGER.error("Exception on init()", err)
return false return false
}
} }
} }

View File

@ -1,6 +1,5 @@
package ru.dbotthepony.kstarbound.lua.bindings package ru.dbotthepony.kstarbound.lua.bindings
import it.unimi.dsi.fastutil.longs.LongArrayList
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap
import org.classdump.luna.ByteString import org.classdump.luna.ByteString
import org.classdump.luna.LuaRuntimeException import org.classdump.luna.LuaRuntimeException
@ -9,7 +8,7 @@ import org.classdump.luna.lib.ArgumentIterator
import org.classdump.luna.runtime.ExecutionContext import org.classdump.luna.runtime.ExecutionContext
import org.classdump.luna.runtime.LuaFunction import org.classdump.luna.runtime.LuaFunction
import ru.dbotthepony.kstarbound.Globals import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.RecipeRegistry import ru.dbotthepony.kstarbound.item.RecipeRegistry
import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
@ -143,44 +142,6 @@ private fun recipesForItem(context: ExecutionContext, name: ByteString) {
} }
} }
private fun itemType(context: ExecutionContext, name: ByteString) {
context.returnBuffer.setTo(Registries.items[name.decode()]?.value?.itemType ?: throw NoSuchElementException("No such item $name"))
}
private fun itemTags(context: ExecutionContext, name: ByteString) {
context.returnBuffer.setTo(context.from(Registries.items[name.decode()]?.value?.itemTags ?: throw NoSuchElementException("No such item $name")))
}
private fun itemHasTag(context: ExecutionContext, name: ByteString, tag: ByteString) {
context.returnBuffer.setTo(Registries.items[name.decode()]?.value?.itemTags?.contains(tag.decode()) ?: throw NoSuchElementException("No such item $name"))
}
private fun itemConfig(context: ExecutionContext, args: ArgumentIterator): StateMachine {
val name = args.nextString().decode()
val state = StateMachine()
if (name !in Registries.items) {
context.returnBuffer.setTo()
return state
}
val desc = ItemDescriptor(args.nextTable(), state)
val level = args.nextOptionalFloat()
val seed = args.nextOptionalInteger()
state.add {
val build = Starbound.itemConfig(desc.get().name, desc.get().parameters, level, seed)
context.returnBuffer.setTo(context.newTable(0, 3).also {
it["directory"] = build.directory
it["config"] = context.from(build.config)
it["parameters"] = context.from(build.parameters)
})
}
return state
}
private fun getMatchingTenants(context: ExecutionContext, tags: Table) { private fun getMatchingTenants(context: ExecutionContext, tags: Table) {
val actualTags = Object2IntOpenHashMap<String>() val actualTags = Object2IntOpenHashMap<String>()
@ -400,11 +361,11 @@ fun provideRootBindings(lua: LuaEnvironment) {
table["projectileConfig"] = registryDef(Registries.projectiles) table["projectileConfig"] = registryDef(Registries.projectiles)
table["recipesForItem"] = luaFunction(::recipesForItem) table["recipesForItem"] = luaFunction(::recipesForItem)
table["itemType"] = luaFunction(::itemType) table["itemType"] = luaStub("itemType")
table["itemTags"] = luaFunction(::itemTags) table["itemTags"] = luaStub("itemTags")
table["itemHasTag"] = luaFunction(::itemHasTag) table["itemHasTag"] = luaStub("itemHasTag")
table["itemConfig"] = luaFunctionNS("itemConfig", ::itemConfig) table["itemConfig"] = luaStub("itemConfig")
table["createItem"] = luaStub("createItem") table["createItem"] = luaStub("createItem")
table["tenantConfig"] = registryDef(Registries.tenants) table["tenantConfig"] = registryDef(Registries.tenants)

View File

@ -1,9 +1,11 @@
package ru.dbotthepony.kstarbound.lua.bindings package ru.dbotthepony.kstarbound.lua.bindings
import org.classdump.luna.Table
import ru.dbotthepony.kstarbound.lua.LuaEnvironment import ru.dbotthepony.kstarbound.lua.LuaEnvironment
import ru.dbotthepony.kstarbound.lua.get import ru.dbotthepony.kstarbound.lua.get
import ru.dbotthepony.kstarbound.lua.luaFunction import ru.dbotthepony.kstarbound.lua.luaFunction
import ru.dbotthepony.kstarbound.lua.set import ru.dbotthepony.kstarbound.lua.set
import ru.dbotthepony.kstarbound.lua.toVector2d
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) { fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) {
@ -13,4 +15,12 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) {
callbacks["flyingType"] = luaFunction { callbacks["flyingType"] = luaFunction {
returnBuffer.setTo(self.sky.flyingType.jsonName) returnBuffer.setTo(self.sky.flyingType.jsonName)
} }
callbacks["magnitude"] = luaFunction { arg1: Table, arg2: Table? ->
if (arg2 == null) {
returnBuffer.setTo(toVector2d(arg1).length)
} else {
returnBuffer.setTo(self.geometry.diff(toVector2d(arg1), toVector2d(arg2)).length)
}
}
} }

View File

@ -67,7 +67,7 @@ class LuaRandomGenerator(var random: RandomGenerator) : Userdata<RandomGenerator
self.random = random(seed ?: System.nanoTime()) self.random = random(seed ?: System.nanoTime())
}) })
.add("addEntropy", luaFunction { self: LuaRandomGenerator -> .add("addEntropy", luaFunction { self: LuaRandomGenerator ->
throw UnsupportedOperationException("Adding entropy is not supported on new engine. If you have legitimate usecase for this, please let us know at issue tracker or discord") throw UnsupportedOperationException("Adding entropy is not supported on new engine. If you have legitimate usecase for this, please let us know on issue tracker or discord")
}) })
// TODO: Lua, by definition, has no unsigned numbers, // TODO: Lua, by definition, has no unsigned numbers,
// and before 5.3, there were only doubles, longs (integers) were added // and before 5.3, there were only doubles, longs (integers) were added

View File

@ -79,9 +79,14 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) :
private val legacyValidator = PacketRegistry.LEGACY.Validator(side) private val legacyValidator = PacketRegistry.LEGACY.Validator(side)
private val legacySerializer = PacketRegistry.LEGACY.Serializer(side) private val legacySerializer = PacketRegistry.LEGACY.Serializer(side)
// whenever protocol was negotiated
var isConnected = false var isConnected = false
private set private set
// whenever successfully joined the game
var isReady = false
protected set
open fun setupLegacy() { open fun setupLegacy() {
isConnected = true isConnected = true
LOGGER.info("Handshake successful on ${channel.remoteAddress()}, channel is using legacy protocol") LOGGER.info("Handshake successful on ${channel.remoteAddress()}, channel is using legacy protocol")
@ -150,7 +155,9 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) :
} }
abstract fun inGame() open fun inGame() {
isReady = true
}
fun send(packet: IPacket) { fun send(packet: IPacket) {
if (channel.isOpen && isConnected) { if (channel.isOpen && isConnected) {
@ -172,6 +179,7 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) :
abstract fun disconnect(reason: String = "") abstract fun disconnect(reason: String = "")
override fun close() { override fun close() {
LOGGER.info("Terminating $this")
channel.close() channel.close()
} }

View File

@ -89,7 +89,9 @@ class ServerChannels(val server: StarboundServer) : Closeable {
fun broadcast(packet: IPacket) { fun broadcast(packet: IPacket) {
connections.forEach { connections.forEach {
it.send(packet) if (it.isReady) {
it.send(packet)
}
} }
} }

View File

@ -109,7 +109,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
private var remoteVersion = 0L private var remoteVersion = 0L
override fun flush() { override fun flush() {
if (isConnected) { if (isConnected && isReady) {
val entries = rpc.write() val entries = rpc.write()
if (entries != null || modifiedShipChunks.isNotEmpty() || server2clientGroup.upstream.hasChangedSince(remoteVersion)) { if (entries != null || modifiedShipChunks.isNotEmpty() || server2clientGroup.upstream.hasChangedSince(remoteVersion)) {
@ -424,7 +424,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
} }
fun tick(delta: Double) { fun tick(delta: Double) {
if (!isConnected || !channel.isOpen) if (!isConnected || !channel.isOpen || !isReady)
return return
flush() flush()
@ -452,6 +452,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
flush() flush()
} }
isReady = false
tracker?.remove() tracker?.remove()
tracker = null tracker = null
@ -487,6 +488,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
private var countedTowardsPlayerCount = false private var countedTowardsPlayerCount = false
override fun inGame() { override fun inGame() {
super.inGame()
announcedDisconnect = false announcedDisconnect = false
server.chat.systemMessage("Player '$nickname' connected") server.chat.systemMessage("Player '$nickname' connected")
countedTowardsPlayerCount = true countedTowardsPlayerCount = true

View File

@ -238,6 +238,8 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
// harmless // harmless
} else { } else {
LOGGER.error("Exception while loading chunk $this", err) LOGGER.error("Exception while loading chunk $this", err)
permanent.forEach { it.chunk.completeExceptionally(err) }
temporaryList.forEach { it.chunk.completeExceptionally(err) }
} }
} finally { } finally {
isBusy = false isBusy = false

View File

@ -4,6 +4,7 @@ import com.google.gson.JsonElement
import com.google.gson.JsonObject import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.ints.IntArraySet import it.unimi.dsi.fastutil.ints.IntArraySet
import it.unimi.dsi.fastutil.objects.ObjectArraySet import it.unimi.dsi.fastutil.objects.ObjectArraySet
import kotlinx.coroutines.async
import kotlinx.coroutines.future.asCompletableFuture import kotlinx.coroutines.future.asCompletableFuture
import kotlinx.coroutines.future.await import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -350,7 +351,7 @@ class ServerWorld private constructor(
// everything inside our own thread, not anywhere else // everything inside our own thread, not anywhere else
// This way, external callers can properly wait for preparations to complete // This way, external callers can properly wait for preparations to complete
fun prepare(): CompletableFuture<*> { fun prepare(): CompletableFuture<*> {
return eventLoop.scope.launch { prepare0() }.asCompletableFuture() return eventLoop.scope.async { prepare0() }.asCompletableFuture()
} }
private suspend fun findPlayerStart(hint: Vector2d? = null): Vector2d { private suspend fun findPlayerStart(hint: Vector2d? = null): Vector2d {

View File

@ -16,6 +16,7 @@ import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.getArray import ru.dbotthepony.kommons.gson.getArray
import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kommons.vector.Vector3i import ru.dbotthepony.kommons.vector.Vector3i
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.world.CelestialParameters import ru.dbotthepony.kstarbound.defs.world.CelestialParameters
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.pairAdapter import ru.dbotthepony.kstarbound.json.pairAdapter
@ -59,7 +60,6 @@ class UniverseChunk(var chunkPos: Vector2i = Vector2i.ZERO) {
class Adapter(gson: Gson) : TypeAdapter<UniverseChunk>() { class Adapter(gson: Gson) : TypeAdapter<UniverseChunk>() {
private val vectors = gson.getAdapter(Vector2i::class.java) private val vectors = gson.getAdapter(Vector2i::class.java)
private val vectors3 = gson.getAdapter(Vector3i::class.java) private val vectors3 = gson.getAdapter(Vector3i::class.java)
private val objects = gson.getAdapter(JsonObject::class.java)
private val systems = gson.pairListAdapter<Vector3i, CelestialParameters>() private val systems = gson.pairListAdapter<Vector3i, CelestialParameters>()
private val params = gson.getAdapter(CelestialParameters::class.java) private val params = gson.getAdapter(CelestialParameters::class.java)
private val lines = gson.pairAdapter<Vector2i, Vector2i>() private val lines = gson.pairAdapter<Vector2i, Vector2i>()
@ -141,7 +141,7 @@ class UniverseChunk(var chunkPos: Vector2i = Vector2i.ZERO) {
if (`in`.consumeNull()) if (`in`.consumeNull())
return null return null
val read = objects.read(`in`) val read = Starbound.ELEMENTS_ADAPTER.objects.read(`in`)
val chunkPos = read.get("chunkIndex", vectors) val chunkPos = read.get("chunkIndex", vectors)
val chunk = UniverseChunk(chunkPos) val chunk = UniverseChunk(chunkPos)

View File

@ -55,6 +55,6 @@ object AssetPathStack {
if (path.isNotEmpty() && path[0] == '/') if (path.isNotEmpty() && path[0] == '/')
return path return path
return if (base.endsWith('/')) "$base$path" else "${base.substringBeforeLast('/')}/$path" return if (base.endsWith('/')) "$base$path" else "$base/$path"
} }
} }

View File

@ -230,14 +230,14 @@ class SBPattern private constructor(
if (open == -1) { if (open == -1) {
if (i == 0) if (i == 0)
pieces.add(Piece(contents = raw)) pieces.add(Piece(contents = raw.sbIntern()))
else else
pieces.add(Piece(contents = raw.substring(i))) pieces.add(Piece(contents = raw.substring(i).sbIntern()))
break break
} else { } else {
if (open != i) { if (open != i) {
pieces.add(Piece(contents = raw.substring(i, open))) pieces.add(Piece(contents = raw.substring(i, open).sbIntern()))
} }
val closing = raw.indexOf('>', startIndex = open + 1) val closing = raw.indexOf('>', startIndex = open + 1)
@ -246,7 +246,7 @@ class SBPattern private constructor(
throw IllegalArgumentException("Malformed pattern string: $raw") throw IllegalArgumentException("Malformed pattern string: $raw")
} }
pieces.add(Piece(name = Starbound.STRINGS.intern(raw.substring(open + 1, closing)))) pieces.add(Piece(name = raw.substring(open + 1, closing).sbIntern()))
i = closing + 1 i = closing + 1
} }
} }

View File

@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound.util.random
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonElement import com.google.gson.JsonElement
import it.unimi.dsi.fastutil.bytes.ByteConsumer import it.unimi.dsi.fastutil.bytes.ByteConsumer
import org.classdump.luna.ByteString
import ru.dbotthepony.kommons.util.IStruct2d import ru.dbotthepony.kommons.util.IStruct2d
import ru.dbotthepony.kommons.util.IStruct2i import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kommons.util.XXHash32 import ru.dbotthepony.kommons.util.XXHash32
@ -61,6 +62,7 @@ fun staticRandom32(vararg values: Any?): Int {
for (value in values) { for (value in values) {
when (value) { when (value) {
is String -> digest.update(value.toByteArray()) is String -> digest.update(value.toByteArray())
is ByteString -> digest.update(value.bytes)
is Byte -> digest.update(value) is Byte -> digest.update(value)
is Boolean -> digest.update(if (value) 1 else 0) is Boolean -> digest.update(if (value) 1 else 0)
is Short -> toBytes(digest::update, value) is Short -> toBytes(digest::update, value)
@ -100,6 +102,7 @@ fun staticRandom64(vararg values: Any?): Long {
for (value in values) { for (value in values) {
when (value) { when (value) {
is String -> digest.update(value.toByteArray()) is String -> digest.update(value.toByteArray())
is ByteString -> digest.update(value.bytes)
is Byte -> digest.update(value) is Byte -> digest.update(value)
is Boolean -> digest.update(if (value) 1 else 0) is Boolean -> digest.update(if (value) 1 else 0)
is Short -> toBytes(digest::update, value) is Short -> toBytes(digest::update, value)

View File

@ -166,10 +166,9 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
fun loadCells(source: Object2DArray<out AbstractCell>) { fun loadCells(source: Object2DArray<out AbstractCell>) {
val ours = cells.value val ours = cells.value
source.checkSizeEquals(ours)
for (x in 0 until CHUNK_SIZE) { for (x in 0 until width) {
for (y in 0 until CHUNK_SIZE) { for (y in 0 until height) {
ours[x, y] = source[x, y].immutable() ours[x, y] = source[x, y].immutable()
} }
} }

View File

@ -18,6 +18,7 @@ import ru.dbotthepony.kommons.io.readVector3i
import ru.dbotthepony.kommons.io.writeSignedVarInt import ru.dbotthepony.kommons.io.writeSignedVarInt
import ru.dbotthepony.kommons.io.writeStruct3i import ru.dbotthepony.kommons.io.writeStruct3i
import ru.dbotthepony.kommons.io.writeVarInt import ru.dbotthepony.kommons.io.writeVarInt
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.network.syncher.legacyCodec import ru.dbotthepony.kstarbound.network.syncher.legacyCodec
import ru.dbotthepony.kstarbound.network.syncher.nativeCodec import ru.dbotthepony.kstarbound.network.syncher.nativeCodec
import java.io.DataInputStream import java.io.DataInputStream
@ -122,7 +123,6 @@ data class UniversePos(val location: Vector3i = Vector3i.ZERO, val planetOrbit:
class Adapter(gson: Gson) : TypeAdapter<UniversePos>() { class Adapter(gson: Gson) : TypeAdapter<UniversePos>() {
private val vectors = gson.getAdapter(Vector3i::class.java) private val vectors = gson.getAdapter(Vector3i::class.java)
private val objects = gson.getAdapter(JsonObject::class.java)
override fun write(out: JsonWriter, value: UniversePos) { override fun write(out: JsonWriter, value: UniversePos) {
out.beginObject() out.beginObject()
@ -141,7 +141,7 @@ data class UniversePos(val location: Vector3i = Vector3i.ZERO, val planetOrbit:
override fun read(`in`: JsonReader): UniversePos { override fun read(`in`: JsonReader): UniversePos {
if (`in`.peek() == JsonToken.BEGIN_OBJECT) { if (`in`.peek() == JsonToken.BEGIN_OBJECT) {
val values = objects.read(`in`)!! val values = Starbound.ELEMENTS_ADAPTER.objects.read(`in`)!!
val location = values.get("location", vectors) val location = values.get("location", vectors)
val planet = values.get("planet", 0) val planet = values.get("planet", 0)
val orbit = values.get("orbit", 0) val orbit = values.get("orbit", 0)

View File

@ -31,10 +31,12 @@ import ru.dbotthepony.kstarbound.world.api.AbstractCell
import ru.dbotthepony.kstarbound.world.api.TileView import ru.dbotthepony.kstarbound.world.api.TileView
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.DynamicEntity import ru.dbotthepony.kstarbound.world.entities.DynamicEntity
import ru.dbotthepony.kstarbound.world.entities.MovementController
import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity
import ru.dbotthepony.kstarbound.world.physics.CollisionPoly import ru.dbotthepony.kstarbound.world.physics.CollisionPoly
import ru.dbotthepony.kstarbound.world.physics.CollisionType import ru.dbotthepony.kstarbound.world.physics.CollisionType
import ru.dbotthepony.kstarbound.world.physics.Poly import ru.dbotthepony.kstarbound.world.physics.Poly
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
@ -285,11 +287,32 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
open fun tick(delta: Double) { open fun tick(delta: Double) {
ticks++ ticks++
Starbound.EXECUTOR.submit(ParallelPerform(dynamicEntities.spliterator(), { if (dynamicEntities.size < 128) {
if (!it.isRemote) { dynamicEntities.forEach {
it.movement.move(delta) if (!it.isRemote) {
it.movement.move(delta)
}
} }
})).join() } else {
val tasks = ArrayList<CompletableFuture<*>>()
var batch = ArrayList<MovementController>()
for (entity in dynamicEntities) {
batch.add(entity.movement)
if (batch.size == 32) {
val b = batch
tasks.add(CompletableFuture.runAsync(Runnable { b.forEach { it.move(delta) } }, Starbound.EXECUTOR))
batch = ArrayList()
}
}
if (batch.isNotEmpty()) {
tasks.add(CompletableFuture.runAsync(Runnable { batch.forEach { it.move(delta) } }, Starbound.EXECUTOR))
}
CompletableFuture.allOf(*tasks.toTypedArray()).join()
}
entityList.forEach { entityList.forEach {
try { try {

View File

@ -491,7 +491,7 @@ class Animator() {
for ((part, tags) in config.partTagDefaults) { for ((part, tags) in config.partTagDefaults) {
for ((k, v) in tags) { for ((k, v) in tags) {
setPartTag(part, k, v) partTags.computeIfAbsent(part) { NetworkedMap(InternedStringCodec, InternedStringCodec) }.put(k, v)
} }
} }
@ -602,8 +602,10 @@ class Animator() {
} }
fun setPartTag(partName: String, tagKey: String, tagValue: String) { fun setPartTag(partName: String, tagKey: String, tagValue: String) {
val tags = partTags[partName] ?: throw IllegalArgumentException("Unknown part $partName!") partTags.computeIfAbsent(partName) {
tags[tagKey] = tagValue LOGGER.warn("Creating part tags for $it after initialization, this can cause client-server desyncs")
NetworkedMap(InternedStringCodec, InternedStringCodec)
}.put(tagKey, tagValue)
} }
private fun setupNetworkElements() { private fun setupNetworkElements() {

View File

@ -28,6 +28,7 @@ import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.physics.CollisionPoly import ru.dbotthepony.kstarbound.world.physics.CollisionPoly
import ru.dbotthepony.kstarbound.world.physics.CollisionType import ru.dbotthepony.kstarbound.world.physics.CollisionType
import ru.dbotthepony.kstarbound.world.physics.Poly import ru.dbotthepony.kstarbound.world.physics.Poly
import java.util.LinkedList
import kotlin.math.PI import kotlin.math.PI
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlin.math.acos import kotlin.math.acos

View File

@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.world.entities
import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap
import it.unimi.dsi.fastutil.objects.ObjectArraySet import it.unimi.dsi.fastutil.objects.ObjectArraySet
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.collect.ListenableMap import ru.dbotthepony.kommons.collect.ListenableMap
import ru.dbotthepony.kommons.io.IntValueCodec import ru.dbotthepony.kommons.io.IntValueCodec
import ru.dbotthepony.kommons.io.KOptionalIntValueCodec import ru.dbotthepony.kommons.io.KOptionalIntValueCodec
@ -173,8 +174,17 @@ class StatusController(val entity: ActorEntity, val config: StatusControllerConf
abstract val maxValue: Double? abstract val maxValue: Double?
fun setAsPercentage(percent: Double) { fun setAsPercentage(percent: Double) {
val maxValue = maxValue ?: throw IllegalArgumentException("$name does not have max value") val maxValue = maxValue
this.value = maxValue * percent
if (maxValue == null) {
if (percent == 0.0) {
LOGGER.warn("Preserving odd quirk of original engine: $name requires resource $max to be present to be set as 0% of that, however, $max is missing. Due to flawed `if` branch in original engine, `initialPercentage` specified as `0` falls through condition and is treated as `initialValue`=`0`")
} else {
throw IllegalArgumentException("$name does not have max value")
}
} else {
this.value = maxValue * percent
}
} }
} }
@ -375,5 +385,6 @@ class StatusController(val entity: ActorEntity, val config: StatusControllerConf
companion object { companion object {
private val LEGACY_MODIFIERS_CODEC = StreamCodec.Collection(StatModifier.LEGACY_CODEC, ::ArrayList) private val LEGACY_MODIFIERS_CODEC = StreamCodec.Collection(StatModifier.LEGACY_CODEC, ::ArrayList)
private val NATIVE_MODIFIERS_CODEC = StreamCodec.Collection(StatModifier.CODEC, ::ArrayList) private val NATIVE_MODIFIERS_CODEC = StreamCodec.Collection(StatModifier.CODEC, ::ArrayList)
private val LOGGER = LogManager.getLogger()
} }
} }

View File

@ -5,6 +5,7 @@ import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive import com.google.gson.JsonPrimitive
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.io.readByteArray import ru.dbotthepony.kommons.io.readByteArray
@ -13,6 +14,7 @@ import ru.dbotthepony.kommons.io.writeByteArray
import ru.dbotthepony.kommons.io.writeVarInt import ru.dbotthepony.kommons.io.writeVarInt
import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.InteractAction import ru.dbotthepony.kstarbound.defs.InteractAction
import ru.dbotthepony.kstarbound.defs.InteractRequest import ru.dbotthepony.kstarbound.defs.InteractRequest
@ -28,6 +30,8 @@ import ru.dbotthepony.kstarbound.network.syncher.networkedFloat
import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt
import ru.dbotthepony.kstarbound.util.ManualLazy import ru.dbotthepony.kstarbound.util.ManualLazy
import ru.dbotthepony.kstarbound.util.RelativeClock import ru.dbotthepony.kstarbound.util.RelativeClock
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
@ -36,7 +40,12 @@ class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(co
var opened by networkedSignedInt().also { networkGroup.upstream.add(it) } var opened by networkedSignedInt().also { networkGroup.upstream.add(it) }
var isCrafting by networkedBoolean().also { networkGroup.upstream.add(it) } var isCrafting by networkedBoolean().also { networkGroup.upstream.add(it) }
var craftingProgress by networkedFloat().also { networkGroup.upstream.add(it); it.interpolator = Interpolator.Linear } var craftingProgress by networkedFloat().also { networkGroup.upstream.add(it); it.interpolator = Interpolator.Linear }
val items = Container(0).also { networkGroup.upstream.add(it) } val items = Container(lookupProperty("slotCount").asInt).also { networkGroup.upstream.add(it) }
override fun parametersUpdated() {
super.parametersUpdated()
items.size = lookupProperty("slotCount").asInt
}
// whenever set loot seed, put initial items, etc. // whenever set loot seed, put initial items, etc.
private var isInitialized = false private var isInitialized = false
@ -100,6 +109,52 @@ class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(co
return data return data
} }
override fun onJoinWorld(world: World<*, *>) {
if (!isRemote)
isInteractive = true
super.onJoinWorld(world)
if (!isRemote) {
if (isInitialized) return
isInitialized = true
var level = world.template.threatLevel
val seed = lookupProperty("treasureSeed") { JsonPrimitive(world.random.nextLong()) }.asLong
level = lookupProperty("level") { JsonPrimitive(level) }.asDouble
level += lookupProperty("levelAdjustment") { JsonPrimitive(0.0) }.asDouble
val initialItems = lookupProperty("initialItems")
if (!initialItems.isJsonNull) {
for (item in initialItems.asJsonArray) {
items.add(ItemStack.create(ItemDescriptor(item).build(level, seed)))
}
}
val treasurePools = lookupProperty("treasurePools")
if (!treasurePools.isJsonNull) {
val random = random(seed)
val get = treasurePools.asJsonArray.random(random).asString
val treasurePool = Registries.treasurePools[get]
if (treasurePool == null) {
LOGGER.error("Unknown treasure pool $get! Can't generate container contents at $tilePosition.")
} else {
for (item in treasurePool.value.evaluate(random, level)) {
val leftover = items.add(ItemStack.create(item))
if (leftover.isNotEmpty) {
LOGGER.warn("Tried to overfill container at $tilePosition")
lostItems.add(leftover)
}
}
}
}
}
}
// Networking of this container to legacy clients is incredibly stupid, // Networking of this container to legacy clients is incredibly stupid,
// and networks entire state each time something has changed. // and networks entire state each time something has changed.
inner class Container(size: Int) : NetworkedElement(), IContainer { inner class Container(size: Int) : NetworkedElement(), IContainer {
@ -181,10 +236,7 @@ class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(co
val wrapper = FastByteArrayOutputStream() val wrapper = FastByteArrayOutputStream()
val stream = DataOutputStream(wrapper) val stream = DataOutputStream(wrapper)
stream.writeVarInt(size) stream.writeVarInt(size)
var setItemsSize = items.indexOfLast { it.isNotEmpty } val setItemsSize = items.indexOfLast { it.isNotEmpty } + 1
if (setItemsSize == -1)
setItemsSize = 0
stream.writeVarInt(setItemsSize) stream.writeVarInt(setItemsSize)
@ -256,4 +308,8 @@ class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(co
override fun disableInterpolation() {} override fun disableInterpolation() {}
override fun tickInterpolation(delta: Double) {} override fun tickInterpolation(delta: Double) {}
} }
companion object {
private val LOGGER = LogManager.getLogger()
}
} }

View File

@ -62,8 +62,6 @@ enum class TerrainSelectorType(val jsonName: String, private val data: Data<*, *
out.value(value.toJson()) out.value(value.toJson())
} }
private val objects by lazy { Starbound.gson.getAdapter(JsonObject::class.java) }
fun named(name: String, parameters: TerrainSelectorParameters): AbstractTerrainSelector<*> { fun named(name: String, parameters: TerrainSelectorParameters): AbstractTerrainSelector<*> {
return Registries.terrainSelectors.getOrThrow(name).value.create(parameters) return Registries.terrainSelectors.getOrThrow(name).value.create(parameters)
} }
@ -72,7 +70,7 @@ enum class TerrainSelectorType(val jsonName: String, private val data: Data<*, *
if (`in`.consumeNull()) if (`in`.consumeNull())
return null return null
return load(objects.read(`in`)) return load(Starbound.ELEMENTS_ADAPTER.objects.read(`in`))
} }
fun factory(json: JsonObject, isSerializedForm: Boolean, type: TerrainSelectorType? = null): Factory<*, *> { fun factory(json: JsonObject, isSerializedForm: Boolean, type: TerrainSelectorType? = null): Factory<*, *> {