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
#### 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
* 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

View File

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

View File

@ -10,8 +10,15 @@ import java.net.InetSocketAddress
private val LOGGER = LogManager.getLogger()
fun main() {
Starbound.addPakPath(File("J:\\Steam\\steamapps\\common\\Starbound\\assets\\packed.pak"))
Starbound.doBootstrap()
Starbound.addArchive(File("J:\\Steam\\steamapps\\common\\Starbound\\assets\\packed.pak"))
/*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()}")

View File

@ -19,17 +19,6 @@ import ru.dbotthepony.kstarbound.defs.Species
import ru.dbotthepony.kstarbound.defs.StatusEffectDefinition
import ru.dbotthepony.kstarbound.defs.ThingDescription
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.MonsterTypeDefinition
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.TreeVariant
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.world.terrain.TerrainSelectorType
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 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 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 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()) }
@ -115,19 +105,18 @@ object Registries {
private inline fun <reified T : Any> loadRegistry(
registry: Registry<T>,
files: List<IStarboundFile>,
patches: Map<String, Collection<IStarboundFile>>,
files: Collection<IStarboundFile>,
noinline keyProvider: (T) -> Pair<String, KOptional<Int?>>,
noinline after: (T, IStarboundFile) -> Unit = { _, _ -> }
): List<Future<*>> {
val adapter by lazy { Starbound.gson.getAdapter(T::class.java) }
val elementAdapter by lazy { Starbound.gson.getAdapter(JsonElement::class.java) }
return files.map { listedFile ->
Starbound.EXECUTOR.submit {
try {
AssetPathStack(listedFile.computeDirectory()) {
// TODO: json patch support
val elem = elementAdapter.read(listedFile.jsonReader())
val elem = JsonPatch.apply(Starbound.ELEMENTS_ADAPTER.read(listedFile.jsonReader()), patches[listedFile.computeFullPath()])
val read = adapter.fromJsonTree(elem)
val keys = keyProvider(read)
@ -154,91 +143,49 @@ object Registries {
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<*>>()
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(tileModifiers, fileTree["matmod"] ?: listOf(), key(TileModifierDefinition::modName, TileModifierDefinition::modId)))
tasks.addAll(loadRegistry(liquid, fileTree["liquid"] ?: listOf(), key(LiquidDefinition::name, LiquidDefinition::liquidId)))
tasks.addAll(loadRegistry(tiles, patchTree, fileTree["material"] ?: listOf(), key(TileDefinition::materialName, TileDefinition::materialId)))
tasks.addAll(loadRegistry(tileModifiers, patchTree, fileTree["matmod"] ?: listOf(), key(TileModifierDefinition::modName, TileModifierDefinition::modId)))
tasks.addAll(loadRegistry(liquid, patchTree, fileTree["liquid"] ?: listOf(), key(LiquidDefinition::name, LiquidDefinition::liquidId)))
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(statusEffects, fileTree["statuseffect"] ?: listOf(), key(StatusEffectDefinition::name)))
tasks.addAll(loadRegistry(species, 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(questTemplates, fileTree["questtemplate"] ?: listOf(), key(QuestTemplate::id)))
tasks.addAll(loadRegistry(techs, fileTree["tech"] ?: listOf(), key(TechDefinition::name)))
tasks.addAll(loadRegistry(npcTypes, fileTree["npctype"] ?: listOf(), key(NpcTypeDefinition::type)))
tasks.addAll(loadRegistry(monsterSkills, fileTree["monsterskill"] ?: listOf(), key(MonsterSkillDefinition::name)))
tasks.addAll(loadRegistry(biomes, fileTree["biome"] ?: listOf(), key(BiomeDefinition::name)))
tasks.addAll(loadRegistry(grassVariants, fileTree["grass"] ?: listOf(), key(GrassVariant.Data::name)))
tasks.addAll(loadRegistry(treeStemVariants, fileTree["modularstem"] ?: listOf(), key(TreeVariant.StemData::name)))
tasks.addAll(loadRegistry(treeFoliageVariants, fileTree["modularfoliage"] ?: listOf(), key(TreeVariant.FoliageData::name)))
tasks.addAll(loadRegistry(bushVariants, fileTree["bush"] ?: listOf(), key(BushVariant.Data::name)))
tasks.addAll(loadRegistry(worldObjects, patchTree, fileTree["object"] ?: listOf(), key(ObjectDefinition::objectName)))
tasks.addAll(loadRegistry(statusEffects, patchTree, fileTree["statuseffect"] ?: listOf(), key(StatusEffectDefinition::name)))
tasks.addAll(loadRegistry(species, patchTree, fileTree["species"] ?: listOf(), key(Species::kind)))
tasks.addAll(loadRegistry(particles, patchTree, fileTree["particle"] ?: listOf(), { (it.kind ?: throw NullPointerException("Missing 'kind' value")) to KOptional() }))
tasks.addAll(loadRegistry(questTemplates, patchTree, fileTree["questtemplate"] ?: listOf(), key(QuestTemplate::id)))
tasks.addAll(loadRegistry(techs, patchTree, fileTree["tech"] ?: listOf(), key(TechDefinition::name)))
tasks.addAll(loadRegistry(npcTypes, patchTree, fileTree["npctype"] ?: listOf(), key(NpcTypeDefinition::type)))
tasks.addAll(loadRegistry(monsterSkills, patchTree, fileTree["monsterskill"] ?: listOf(), key(MonsterSkillDefinition::name)))
tasks.addAll(loadRegistry(biomes, patchTree, fileTree["biome"] ?: listOf(), key(BiomeDefinition::name)))
tasks.addAll(loadRegistry(grassVariants, patchTree, fileTree["grass"] ?: listOf(), key(GrassVariant.Data::name)))
tasks.addAll(loadRegistry(treeStemVariants, patchTree, fileTree["modularstem"] ?: listOf(), key(TreeVariant.StemData::name)))
tasks.addAll(loadRegistry(treeFoliageVariants, patchTree, fileTree["modularfoliage"] ?: listOf(), key(TreeVariant.FoliageData::name)))
tasks.addAll(loadRegistry(bushVariants, patchTree, fileTree["bush"] ?: listOf(), key(BushVariant.Data::name)))
tasks.addAll(loadCombined(jsonFunctions, fileTree["functions"] ?: listOf()))
tasks.addAll(loadCombined(json2Functions, fileTree["2functions"] ?: listOf()))
tasks.addAll(loadCombined(jsonConfigFunctions, fileTree["configfunctions"] ?: listOf()))
tasks.addAll(loadCombined(treasurePools, fileTree["treasurepools"] ?: listOf()) { name = it })
tasks.addAll(loadCombined(jsonFunctions, fileTree["functions"] ?: listOf(), patchTree))
tasks.addAll(loadCombined(json2Functions, fileTree["2functions"] ?: listOf(), patchTree))
tasks.addAll(loadCombined(jsonConfigFunctions, fileTree["configfunctions"] ?: listOf(), patchTree))
tasks.addAll(loadCombined(treasurePools, fileTree["treasurepools"] ?: listOf(), patchTree) { name = it })
return tasks
}
private fun loadItemDefinitions(files: Map<String, Collection<IStarboundFile>>): 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<*>> {
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 adapter by lazy { Starbound.gson.getAdapter(T::class.java) }
val elementAdapter by lazy { Starbound.gson.getAdapter(JsonObject::class.java) }
return files.map { listedFile ->
Starbound.EXECUTOR.submit {
try {
// TODO: json patch support
val json = elementAdapter.read(JsonReader(listedFile.reader()).also { it.isLenient = true })
val json = JsonPatch.apply(Starbound.ELEMENTS_ADAPTER.read(listedFile.jsonReader()), patches[listedFile.computeFullPath()]) as JsonObject
for ((k, v) in json.entrySet()) {
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 {
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 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<*>>()
tasks.addAll((files["terrain"] ?: listOf()).map { listedFile ->
Starbound.EXECUTOR.submit {
loadTerrainSelector(listedFile, null)
loadTerrainSelector(listedFile, null, patches)
}
})
@ -286,7 +233,7 @@ object Registries {
for (type in TerrainSelectorType.entries) {
tasks.addAll((files[type.jsonName.lowercase()] ?: listOf()).map { listedFile ->
Starbound.EXECUTOR.submit {
loadTerrainSelector(listedFile, type)
loadTerrainSelector(listedFile, type, patches)
}
})
}

View File

@ -1,10 +1,11 @@
package ru.dbotthepony.kstarbound
import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.Interner
import com.github.benmanes.caffeine.cache.Scheduler
import com.google.gson.*
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import com.google.gson.stream.JsonReader
import it.unimi.dsi.fastutil.objects.ObjectArraySet
import kotlinx.coroutines.asCoroutineDispatcher
import org.apache.logging.log4j.LogManager
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.Vector4fTypeAdapter
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.defs.*
import ru.dbotthepony.kstarbound.defs.image.Image
import ru.dbotthepony.kstarbound.defs.image.SpriteReference
import ru.dbotthepony.kstarbound.defs.item.api.IArmorItemDefinition
import ru.dbotthepony.kstarbound.defs.item.InventoryIcon
import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition
import ru.dbotthepony.kstarbound.defs.actor.player.BlueprintLearnList
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.world.terrain.TerrainSelectorType
import ru.dbotthepony.kstarbound.io.*
import ru.dbotthepony.kstarbound.item.ItemRegistry
import ru.dbotthepony.kstarbound.json.factory.MapsTypeAdapterFactory
import ru.dbotthepony.kstarbound.json.InternedJsonElementAdapter
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.SingletonTypeAdapterFactory
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.JsonPatch
import ru.dbotthepony.kstarbound.json.JsonPath
import ru.dbotthepony.kstarbound.json.NativeLegacy
import ru.dbotthepony.kstarbound.util.BlockableEventLoop
@ -70,6 +73,8 @@ import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.*
import java.lang.ref.Cleaner
import java.text.DateFormat
import java.time.Duration
import java.util.Collections
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executor
@ -82,11 +87,6 @@ import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
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.collections.ArrayList
@ -102,12 +102,19 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
// compile flags. uuuugh
const val DEDUP_CELL_STATES = true
const val USE_CAFFEINE_INTERNER = false
const val USE_INTERNER = true
fun <E : Any> interner(): Interner<E> {
if (!USE_INTERNER)
return Interner { it }
return if (USE_CAFFEINE_INTERNER) Interner.newWeakInterner() else HashTableInterner()
}
fun <E : Any> interner(bits: Int): Interner<E> {
if (!USE_INTERNER)
return Interner { it }
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)
}
val ELEMENTS_ADAPTER = InternedJsonElementAdapter(STRINGS)
val gson: Gson = with(GsonBuilder()) {
// serializeNulls()
setDateFormat(DateFormat.LONG)
@ -256,11 +265,9 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
registerTypeAdapter(InternedStringAdapter(STRINGS))
InternedJsonElementAdapter(STRINGS).also {
registerTypeAdapter(it)
registerTypeAdapter(it.arrays)
registerTypeAdapter(it.objects)
}
registerTypeAdapter(ELEMENTS_ADAPTER)
registerTypeAdapter(ELEMENTS_ADAPTER.arrays)
registerTypeAdapter(ELEMENTS_ADAPTER.objects)
registerTypeAdapter(Nothing::class.java, NothingAdapter)
@ -336,18 +343,11 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
registerTypeAdapter(EnumAdapter(DamageType::class, default = DamageType.DAMAGE))
registerTypeAdapter(InventoryIcon.Companion)
registerTypeAdapterFactory(IArmorItemDefinition.Frames.Factory)
registerTypeAdapterFactory(AssetPath.Companion)
registerTypeAdapter(SpriteReference.Companion)
registerTypeAdapterFactory(AssetReference.Companion)
registerTypeAdapter(ItemStack.Adapter(this@Starbound))
registerTypeAdapterFactory(TreasurePoolDefinition.Companion)
registerTypeAdapterFactory(UniverseChunk.Companion)
registerTypeAdapter(Image.Companion)
@ -375,51 +375,6 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
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
private set
var initialized = false
@ -435,8 +390,12 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
var loaded = 0
private set
@Volatile
var terminateLoading = false
private val jsonAssetsCache = Caffeine.newBuilder()
.maximumSize(4096L)
.expireAfterAccess(Duration.ofMinutes(5L))
.scheduler(this)
.executor(EXECUTOR)
.build<String, KOptional<JsonElement>>()
fun loadJsonAsset(path: String): JsonElement? {
val filename: String
@ -450,24 +409,84 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
jsonPath = null
}
val file = locate(filename)
val json = jsonAssetsCache.get(filename) {
val file = locate(it)
if (!file.isFile)
return null
if (!file.isFile)
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)
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 toLoadPaks = ObjectArraySet<File>()
private val toLoadPaths = ObjectArraySet<File>()
fun addFilePath(path: File) {
fileSystems.add(PhysicalFile(path))
var loadingProgressText: String = ""
private set
fun addArchive(path: File) {
toLoadPaks.add(path)
}
fun addPak(pak: StarboundPak) {
fileSystems.add(pak.root)
fun addPath(path: File) {
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 {
@ -506,40 +525,25 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
return NonExistingFile(path.split("/").last(), fullPath = path)
}
fun locate(vararg path: String): IStarboundFile {
for (p in path) {
val get = locate(p)
fun locateAll(path: String): List<IStarboundFile> {
@Suppress("name_shadowing")
var path = path
if (get.exists) {
return get
if (path[0] == '/') {
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])
}
/**
* Добавляет 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
return files
}
private fun doInitialize() {
@ -551,46 +555,34 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
doBootstrap()
val ext2files = fileSystems.parallelStream()
.flatMap { it.explore() }
.filter { it.isFile }
.collect(object :
Collector<IStarboundFile, Object2ObjectOpenHashMap<String, ArrayList<IStarboundFile>>, Map<String, List<IStarboundFile>>>
{
override fun supplier(): Supplier<Object2ObjectOpenHashMap<String, ArrayList<IStarboundFile>>> {
return Supplier { Object2ObjectOpenHashMap() }
}
loadingProgressText = "Building file tree..."
val fileTree = HashMap<String, HashSet<IStarboundFile>>()
val patchTree = HashMap<String, ArrayList<IStarboundFile>>()
override fun accumulator(): BiConsumer<Object2ObjectOpenHashMap<String, ArrayList<IStarboundFile>>, IStarboundFile> {
return BiConsumer { t, u ->
t.computeIfAbsent(u.name.substringAfterLast('.'), Object2ObjectFunction { ArrayList() }).add(u)
}
}
// finding assets, assets originating from top-most priority PAKs are overriding
// same assets from other PAKs
fileSystems.forEach {
it.explore { file ->
if (file.isFile)
fileTree.computeIfAbsent(file.name.substringAfterLast('.')) { HashSet() }.add(file)
}
}
override fun combiner(): BinaryOperator<Object2ObjectOpenHashMap<String, ArrayList<IStarboundFile>>> {
return BinaryOperator { t, u ->
for ((k, v) in u)
t.computeIfAbsent(k, Object2ObjectFunction { ArrayList() }).addAll(v)
t
}
}
override fun finisher(): Function<Object2ObjectOpenHashMap<String, ArrayList<IStarboundFile>>, Map<String, List<IStarboundFile>>> {
return Function { it }
}
override fun characteristics(): Set<Collector.Characteristics> {
return setOf(Collector.Characteristics.IDENTITY_FINISH)
}
})
// finding asset patches, patches from bottom-most priority PAKs are applied first
fileSystems.asReversed().forEach {
it.explore { file ->
if (file.isFile && file.name.endsWith(".patch"))
patchTree.computeIfAbsent(file.computeFullPath().substringAfterLast('.')) { ArrayList() }.add(file)
}
}
loadingProgressText = "Dispatching load tasks..."
val tasks = ArrayList<Future<*>>()
tasks.addAll(Registries.load(ext2files))
tasks.addAll(RecipeRegistry.load(ext2files))
tasks.addAll(Registries.load(fileTree, patchTree))
tasks.addAll(RecipeRegistry.load(fileTree, patchTree))
tasks.addAll(Globals.load())
tasks.add(VersionRegistry.load())
tasks.add(VersionRegistry.load(patchTree))
val total = tasks.size.toDouble()
toLoad = tasks.size
@ -599,11 +591,13 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
tasks.removeIf { it.isDone }
loaded = toLoad - tasks.size
loadingProgress = (total - tasks.size) / total
loadingProgressText = "Loading JSON assets, $loaded / $toLoad"
LockSupport.parkNanos(5_000_000L)
}
Registries.finishLoad()
RecipeRegistry.finishLoad()
ItemRegistry.finishLoad()
Registries.validate()
@ -615,10 +609,6 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
return submit { doInitialize() }
}
fun bootstrapGame(): CompletableFuture<*> {
return submit { doBootstrap() }
}
private var fontPath: File? = null
fun loadFont(): CompletableFuture<File> {

View File

@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound
import com.google.common.collect.ImmutableMap
import com.google.gson.stream.JsonReader
import ru.dbotthepony.kstarbound.io.StarboundPak
import ru.dbotthepony.kstarbound.util.sbIntern
import java.io.BufferedInputStream
import java.io.File
import java.io.FileNotFoundException
@ -48,6 +49,9 @@ fun interface ISBFileLocator {
fun jsonReader(path: String) = locate(path).jsonReader()
}
/**
* Two files should be considered equal if they have same absolute path
*/
interface IStarboundFile : ISBFileLocator {
val exists: Boolean
val isDirectory: Boolean
@ -75,6 +79,11 @@ interface IStarboundFile : ISBFileLocator {
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 {
var path = name
var parent = parent
@ -158,7 +167,7 @@ interface IStarboundFile : ISBFileLocator {
* @throws IllegalStateException if file is a directory
* @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 }
/**
@ -253,29 +262,44 @@ fun getPathFilename(path: String): String {
return path.substringAfterLast('/')
}
class PhysicalFile(val real: File) : IStarboundFile {
class PhysicalFile(val real: File, override val parent: PhysicalFile? = null) : IStarboundFile {
override val exists: Boolean
get() = real.exists()
override val isDirectory: Boolean
get() = real.isDirectory
override val parent: PhysicalFile?
get() {
return PhysicalFile(real.parentFile ?: return null)
}
override val isFile: Boolean
get() = real.isFile
override val children: Map<String, PhysicalFile>?
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
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 {
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 {
return "PhysicalFile[$real]"
}

View File

@ -31,7 +31,7 @@ object VersionRegistry {
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 {
val json = Starbound.loadJsonAsset("/versioning.config") ?: throw NoSuchElementException("Unable to load /versioning.config! Expect HUGE problems!")
json as JsonObject

View File

@ -38,9 +38,6 @@ class ClientConnection(val client: StarboundClient, type: ConnectionType) : Conn
client.mailbox.execute { task.invoke(client) }
}
override fun inGame() {
}
override fun toString(): String {
val channel = if (hasChannel) channel.remoteAddress().toString() else "<no 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) }
GLFW.glfwSetWindowTitle(window, "KStarbound: Loading JSON assets ${Starbound.loaded} / ${Starbound.toLoad}")
GLFW.glfwSetWindowTitle(window, "KStarbound: ${Starbound.loadingProgressText}")
renderedLoadingScreen = true
val runtime = Runtime.getRuntime()

View File

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

View File

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

View File

@ -1,9 +1,6 @@
package ru.dbotthepony.kstarbound.defs
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
data class CurrencyDefinition(
val representativeItem: Registry.Ref<IItemDefinition>,
val representativeItem: String,
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.kommons.gson.consumeNull
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
@JsonAdapter(Drawable.Adapter::class)
@ -43,6 +46,18 @@ sealed class Drawable(val position: Vector2f, val color: RGBAColor, val fullbrig
override fun flop(): Drawable {
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(
@ -58,6 +73,18 @@ sealed class Drawable(val position: Vector2f, val color: RGBAColor, val fullbrig
override fun flop(): Drawable {
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(
@ -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)
}
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) {
val sprite = path.sprite ?: return
val texture = path.image!!.texture
val program = if (fullbright) client.programs.positionTexture else client.programs.positionTextureLightmap
val mat = 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
})
val mat = getTransforms(sprite)
val builder = layer.getBuilder(program.config(texture))
mat.preTranslate(x, y)
mat.preTranslate(position.x + x, position.y + y)
client.stack.last().mulIntoOther(mat)
builder.mode(GeometryType.QUADS)
@ -126,6 +193,18 @@ sealed class Drawable(val position: Vector2f, val color: RGBAColor, val fullbrig
override fun flop(): Drawable {
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 {
@ -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 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
*/
@ -141,12 +233,12 @@ sealed class Drawable(val position: Vector2f, val color: RGBAColor, val fullbrig
companion object {
val EMPTY = Empty()
val CENTERED = Transformations(true)
private val LOGGER = LogManager.getLogger()
}
class Adapter(gson: Gson) : TypeAdapter<Drawable>() {
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 vectors3 = gson.getAdapter(Vector3f::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()) {
return EMPTY
} 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 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>? {
if (type.rawType == Json2Function::class.java) {
return object : TypeAdapter<Json2Function>() {
val elements = gson.getAdapter(JsonArray::class.java)
override fun write(out: JsonWriter, value: Json2Function?) {
TODO("Not yet implemented")
}
@ -247,7 +245,7 @@ class Json2Function(
val xRanges = 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()) {
if (row !is JsonArray) {

View File

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

View File

@ -8,7 +8,7 @@ import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@JsonFactory
data class StatusEffectDefinition(
val name: String,
val defaultDuration: Double,
val defaultDuration: Double = 0.0,
val blockingStat: String? = null,
val label: String? = 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.io.readBinaryString
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.network.syncher.legacyCodec
import ru.dbotthepony.kstarbound.network.syncher.nativeCodec
@ -21,8 +22,6 @@ import java.io.DataOutputStream
@JsonAdapter(StatModifier.Adapter::class)
data class StatModifier(val stat: String, val value: Double, val type: StatModifierType) {
class Adapter(gson: Gson) : TypeAdapter<StatModifier>() {
private val objects = gson.getAdapter(JsonObject::class.java)
override fun write(out: JsonWriter, value: StatModifier?) {
if (value == null) {
out.nullValue()
@ -40,7 +39,7 @@ data class StatModifier(val stat: String, val value: Double, val type: StatModif
if (`in`.consumeNull()) {
return null
} 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")

View File

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

View File

@ -11,15 +11,14 @@ import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
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)) })
@JsonFactory
data class Entry(val item: Registry.Ref<IItemDefinition>)
data class Entry(val item: ItemDescriptor)
operator fun get(tier: Int): List<Entry> {
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.ImmutableMap
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.AssetPath
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
@JsonFactory
@ -13,8 +12,8 @@ data class DeploymentConfig(
override val scripts: ImmutableList<AssetPath>,
override val scriptDelta: Int,
val starterMechSet: ImmutableMap<String, Registry.Ref<IItemDefinition>>,
val speciesStarterMechBody: ImmutableMap<String, Registry.Ref<IItemDefinition>>,
val starterMechSet: ImmutableMap<String, ItemDescriptor>,
val speciesStarterMechBody: ImmutableMap<String, ItemDescriptor>,
val enemyDetectRadius: Double,
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.util.SBPattern
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
@JsonFactory
@ -27,11 +27,11 @@ data class PlayerConfig(
val species: ImmutableSet<Registry.Ref<Species>>,
val nametagColor: RGBAColor,
val ageItemsEvery: Int,
val defaultItems: ImmutableSet<Registry.Ref<IItemDefinition>>,
val defaultItems: ImmutableSet<ItemDescriptor>,
val defaultBlueprints: BlueprintLearnList,
val defaultCodexes: ImmutableMap<String, ImmutableList<Registry.Ref<IItemDefinition>>>,
val defaultCodexes: ImmutableMap<String, ImmutableList<ItemDescriptor>>,
val metaBoundBox: AABB,
val movementParameters: ActorMovementParameters,
val zeroGMovementParameters: ActorMovementParameters,

View File

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

View File

@ -52,7 +52,7 @@ data class AnimatedPartsDefinition(
v.index = index++
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 ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
@ -47,14 +48,12 @@ abstract class DungeonBrush {
}
class Adapter(gson: Gson) : TypeAdapter<DungeonBrush>() {
private val arrays = gson.getAdapter(JsonArray::class.java)
override fun write(out: JsonWriter, value: DungeonBrush) {
TODO("Not yet implemented")
}
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
// 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}!")

View File

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

View File

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

View File

@ -98,7 +98,6 @@ data class DungeonTile(
// weird custom parsing rules but ok
class Adapter(gson: Gson) : TypeAdapter<DungeonTile>() {
private val objects = gson.getAdapter(JsonObject::class.java)
private val data = gson.getAdapter(BasicData::class.java)
private val values = gson.getAdapter<Either<Vector4i, String>>()
@ -119,7 +118,7 @@ data class 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)
var connectorIndex = rawIndex

View File

@ -490,13 +490,17 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar
parent.eventLoop.supplyAsync {
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) {
obj.orientationIndex = orientation.toLong()
obj.joinWorld(parent)
} else {
LOGGER.error("Tried to place object ${obj.config.key} at ${obj.tilePosition}, but it can't be placed there!")
if (orientation != -1) {
obj.orientationIndex = orientation.toLong()
obj.joinWorld(parent)
} else {
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()

View File

@ -28,34 +28,18 @@ class ImagePartReader(part: DungeonPart, val images: ImmutableList<Image>) : Par
for ((i, image) in images.withIndex()) {
// go around image cache, since image will be loaded exactly once
// 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)
if (channels == 3) {
// RGB
for (x in 0 until image.width) {
for (y in 0 until image.height) {
val offset = (x + y * image.width) * channels
for (x in 0 until image.width) {
for (y in 0 until image.height) {
val offset = (x + y * image.width) * 4
// 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 -0x1000000 // leading alpha as 255
}
}
} 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)
}
// 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.get
import ru.dbotthepony.kommons.gson.getObject
import ru.dbotthepony.kstarbound.json.JsonPatch
import java.io.BufferedInputStream
import java.io.FileNotFoundException
import java.lang.ref.Reference
@ -53,13 +54,11 @@ class Image private constructor(
val path: String,
val width: Int,
val height: Int,
val amountOfChannels: Int,
spritesData: Pair<List<DataSprite>, IStarboundFile>?
) {
init {
check(width >= 0) { "Invalid width $width" }
check(height >= 0) { "Invalid height $height" }
check(amountOfChannels in 1 .. 4) { "Unknown number of channels $amountOfChannels" }
}
private val spritesInternal = LinkedHashMap<String, Sprite>()
@ -139,17 +138,10 @@ class Image private constructor(
val value = client.named2DTextures0.get(this) {
client.named2DTextures1.get(this) {
val (memFormat, fileFormat) = when (amountOfChannels) {
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)
val tex = GLTexture2D(width, height, GL45.GL_RGBA8)
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.textureMagFilter = GL45.GL_NEAREST
@ -211,26 +203,13 @@ class Image private constructor(
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" }
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()
when (amountOfChannels) {
4 -> return data[offset].toInt().and(0xFF) or // red
data[offset + 1].toInt().and(0xFF).shl(8) or // green
data[offset + 2].toInt().and(0xFF).shl(16) or // blue
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()
}
return data[offset].toInt().and(0xFF) or // red
data[offset + 1].toInt().and(0xFF).shl(8) or // green
data[offset + 2].toInt().and(0xFF).shl(16) or // blue
data[offset + 3].toInt().and(0xFF).shl(24) // alpha
}
/**
@ -248,47 +227,44 @@ class Image private constructor(
fun isTransparent(x: Int, y: Int, flip: Boolean): Boolean {
if (x !in 0 until width) return true
if (y !in 0 until height) return true
if (amountOfChannels != 4) return false
return this[x, y, flip] and -0x1000000 == 0x0
}
val nonEmptyRegion by lazy {
if (amountOfChannels == 4) {
var x0 = 0
var y0 = 0
var x0 = 0
var y0 = 0
search@for (y in 0 until height) {
for (x in 0 until width) {
if (this[x, y] and 0xFF != 0x0) {
x0 = x
y0 = y
break@search
}
search@for (y in 0 until height) {
for (x in 0 until width) {
if (!isTransparent(x, y, false)) {
x0 = x
y0 = y
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> {
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 minY = pixelOffset.y / 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>() {
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 vectors2 by lazy { Starbound.gson.getAdapter(Vector2i::class.java) }
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(
idata,
getWidth, getHeight,
components, 0
components, 4
) ?: throw IllegalArgumentException("File $file is not an image or it is corrupted")
Reference.reachabilityFence(idata)
@ -432,7 +407,7 @@ class Image private constructor(
if (!status)
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) {
logger.error("Failed to load image at path $it", err)
Optional.empty()
@ -534,12 +509,17 @@ class Image private constructor(
}
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()
} 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.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(
val raw: AssetPath,
val imagePath: SBPattern,
val spritePath: SBPattern?,
val renderDirectives: SBPattern?,
val image: Image?,
) {
val sprite by lazy {
@ -28,17 +40,18 @@ class SpriteReference private constructor(
fun with(values: (String) -> String?): SpriteReference {
val imagePath = this.imagePath.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) {
val resolved = imagePath.value
if (resolved == null)
return SpriteReference(raw, imagePath, spritePath, null)
return SpriteReference(raw, imagePath, spritePath, renderDirectives, null)
else
return SpriteReference(raw, imagePath, spritePath, Image.get(resolved))
return SpriteReference(raw, imagePath, spritePath, renderDirectives, Image.get(resolved))
} 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 {
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>() {
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) }
@ -75,20 +98,52 @@ class SpriteReference private constructor(
if (path == "")
return NEVER
val split = path.split(':')
val split = path.lowercase().split(':')
if (split.size > 2) {
throw JsonSyntaxException("Ambiguous image reference: $path")
}
val imagePath = if (split.size == 2) SBPattern.of(split[0]) else SBPattern.of(path)
val spritePath = if (split.size == 2) SBPattern.of(split[1]) else null
val imagePath: SBPattern
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) {
val remapped = AssetPathStack.remap(split[0])
return SpriteReference(AssetPath(path, AssetPathStack.remap(path)), SBPattern.raw(remapped), spritePath, Image.get(remapped))
val remapped = AssetPathStack.remap(imagePath.value!!)
return SpriteReference(AssetPath(path, AssetPathStack.remap(path)), SBPattern.raw(remapped), spritePath, renderDirectives, Image.get(remapped))
} 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.stream.JsonReader
import com.google.gson.stream.JsonWriter
import org.apache.logging.log4j.LogManager
import org.classdump.luna.ByteString
import org.classdump.luna.LuaRuntimeException
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.from
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.gson.value
import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.io.readVarLong
import ru.dbotthepony.kommons.io.writeBinaryString
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.VersionRegistry
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.item.ItemRegistry
import ru.dbotthepony.kstarbound.item.ItemStack
import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.json.readJsonElement
import ru.dbotthepony.kstarbound.json.writeJsonElement
import ru.dbotthepony.kstarbound.lua.LuaEnvironment
import ru.dbotthepony.kstarbound.lua.get
import ru.dbotthepony.kstarbound.lua.indexNoYield
import ru.dbotthepony.kstarbound.lua.toJson
import ru.dbotthepony.kstarbound.lua.toJsonFromLua
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.function.Supplier
import java.util.random.RandomGenerator
private val EMPTY_JSON = JsonObject()
@ -151,11 +155,9 @@ data class ItemDescriptor(
val count: Long,
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 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 {
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) {
stream.writeBinaryString(name)
stream.writeVarLong(count.coerceAtLeast(0L))
@ -202,8 +238,6 @@ data class ItemDescriptor(
}
class Adapter(gson: Gson) : TypeAdapter<ItemDescriptor>() {
private val elements = gson.getAdapter(JsonElement::class.java)
override fun write(out: JsonWriter, value: ItemDescriptor) {
if (value.isEmpty)
out.nullValue()
@ -212,12 +246,13 @@ data class ItemDescriptor(
}
override fun read(`in`: JsonReader): ItemDescriptor {
return ItemDescriptor(elements.read(`in`))
return ItemDescriptor(Starbound.ELEMENTS_ADAPTER.read(`in`))
}
}
companion object {
val EMPTY = ItemDescriptor("", 0)
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.TypeAdapter
import com.google.gson.TypeAdapterFactory
import com.google.gson.annotations.JsonAdapter
import com.google.gson.internal.bind.JsonTreeReader
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
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.kstarbound.Registry
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.item.ItemStack
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
typealias ItemOrPool = Either<ItemDescriptor, Registry.Ref<TreasurePoolDefinition>>
@JsonAdapter(TreasurePoolDefinition.Adapter::class)
class TreasurePoolDefinition(pieces: List<Piece>) {
var name: String by WriteOnce()
@ -28,72 +39,83 @@ class TreasurePoolDefinition(pieces: List<Piece>) {
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" }
for (piece in pieces) {
if (level <= piece.level) {
return piece.evaluate(random, level)
}
}
val new = visitedPools.getInt(name) + 1
if (pieces.last().level <= level) {
return pieces.last().evaluate(random, level)
} else {
return emptyList()
if (new >= 50)
throw IllegalStateException("Too deep treasure pool references, visited treasure pool $name 50 times (visited: $visitedPools)")
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> {
return evaluate(Random(seed), level)
fun evaluate(seed: Long, level: Double): List<ItemDescriptor> {
return evaluate(random(seed), level)
}
data class Piece(
val level: Double,
val pool: ImmutableList<PoolEntry> = ImmutableList.of(),
val fill: ImmutableList<Either<ItemDescriptor, Registry.Ref<TreasurePoolDefinition>>> = ImmutableList.of(),
val startingLevel: Double,
val pool: WeightedList<ItemOrPool> = WeightedList(),
val fill: ImmutableList<ItemOrPool> = ImmutableList.of(),
val poolRounds: IPoolRounds = OneRound,
// TODO: что оно делает?
// оно точно не запрещает ему появляться несколько раз за одну генерацию treasure pool
// а так же не "дублирует" содержимое если уровень генерации выше, чем указанный level
val allowDuplication: Boolean = false
val allowDuplication: Boolean = true,
val levelVariance: Vector2d = Vector2d.ZERO
) {
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> {
val rounds = poolRounds.evaluate(random)
if (rounds <= 0) return emptyList()
val result = ArrayList<ItemStack>()
for (entry in fill) {
entry.map({
val stack = it.build(level = level + random.nextRange(levelVariance), random = random, seed = random.nextLong())
for (round in 0 until rounds) {
for (entry in fill) {
entry.map({
val stack = it.makeStack()
if (stack.isNotEmpty) result.add(stack)
}, {
it.value?.evaluate(random, actualLevel)
})
}
if (stack.isNotEmpty && (allowDuplication || previousDescriptors.add(stack.copy(count = 1L)))) {
result.add(stack)
}
}, {
it.value?.evaluate(random, level + random.nextRange(levelVariance), visitedPools)?.forEach {
if (allowDuplication || previousDescriptors.add(it.copy(count = 1L)))
result.add(it)
}
})
}
if (pool.isNotEmpty()) {
var chosen = random.nextDouble(maxWeight)
if (pool.isNotEmpty) {
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 (chosen <= entry.weight) {
entry.treasure.map({
val stack = it.makeStack()
if (stack.isNotEmpty) result.add(stack)
}, {
it.value?.evaluate(random, actualLevel)
})
break
} else {
chosen -= entry.weight
if (stack.isNotEmpty && (allowDuplication || previousDescriptors.add(stack.copy(count = 1L)))) {
result.add(stack)
}
}
}, {
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 {
val maxWeight = edges.stream().mapToDouble { it.weight }.sum()
data class WeightedRounds(val edges: WeightedList<Int>) : IPoolRounds {
override fun evaluate(random: RandomGenerator): Int {
val result = random.nextDouble(maxWeight)
var lower = 0.0
return edges.sample(random).orElse(1)
}
}
for (edge in edges) {
if (result in lower .. lower + edge.weight) {
return edge.rounds
} else {
lower += edge.weight
class Adapter(gson: Gson) : TypeAdapter<TreasurePoolDefinition>() {
private val itemAdapter = gson.getAdapter(ItemDescriptor::class.java)
private val poolAdapter = gson.getAdapter(object : TypeToken<Registry.Ref<TreasurePoolDefinition>>() {})
private val vectors = gson.getAdapter(Vector2d::class.java)
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) {
init {
require(weight > 0.0) { "Invalid round weight: $weight" }
require(rounds >= 0) { "Invalid rounds amount: $rounds" }
}
}
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")
data class PoolEntry(
val weight: Double,
val treasure: Either<ItemDescriptor, Registry.Ref<TreasurePoolDefinition>>
) {
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()
if (elem.has("item")) {
pool.add(weight to Either.left(itemAdapter.read(JsonTreeReader(elem["item"]!!))))
} else if (elem.has("pool")) {
pool.add(weight to Either.right(poolAdapter.read(JsonTreeReader(elem["pool"]!!))))
} else {
TODO()
throw JsonSyntaxException("Pool element at $i is missing both 'item' and 'pool' entries")
}
}
}
override fun read(`in`: JsonReader): TreasurePoolDefinition? {
if (`in`.consumeNull()) {
return null
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")
}
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.set
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.AssetReference
import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
@ -153,7 +154,6 @@ data class ObjectDefinition(
val biomePlaced: Boolean = false,
)
private val objects = gson.getAdapter(JsonObject::class.java)
private val objectRef = gson.getAdapter(JsonReference.Object::class.java)
private val basic = gson.getAdapter(PlainData::class.java)
private val damageConfig = gson.getAdapter(TileDamageConfig::class.java)
@ -174,7 +174,7 @@ data class ObjectDefinition(
if (`in`.consumeNull())
return null
val read = objects.read(`in`)
val read = Starbound.ELEMENTS_ADAPTER.objects.read(`in`)
val basic = basic.fromJsonTree(read)
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.stream.JsonReader
import com.google.gson.stream.JsonWriter
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.clear
import ru.dbotthepony.kommons.util.AABB
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.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile
@ -94,6 +96,8 @@ data class ObjectOrientation(
}
companion object {
private val LOGGER = LogManager.getLogger()
fun preprocess(json: JsonArray): JsonArray {
val actual = ArrayList<JsonObject>()
@ -136,7 +140,6 @@ data class 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 vectorsi = gson.getAdapter(Vector2i::class.java)
private val vectorsd = gson.getAdapter(Vector2d::class.java)
@ -161,7 +164,7 @@ data class ObjectOrientation(
if (`in`.consumeNull())
return null
val obj = objects.read(`in`)
val obj = Starbound.ELEMENTS_ADAPTER.objects.read(`in`)
val drawables = ArrayList<Drawable>()
val flipImages = obj.get("flipImages", false)
val renderLayer = RenderLayer.parse(obj.get("renderLayer", "Object"))
@ -199,12 +202,16 @@ data class ObjectOrientation(
for (drawable in drawables) {
if (drawable is Drawable.Image) {
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>()
new.addAll(occupySpaces)
new.addAll(sprite.worldSpaces(imagePositionI, obj["spaceScan"].asDouble, flipImages))
occupySpaces = new.build()
if (sprite != null) {
val new = ImmutableSet.Builder<Vector2i>()
new.addAll(occupySpaces)
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) {

View File

@ -20,6 +20,7 @@ import ru.dbotthepony.kommons.io.writeCollection
import ru.dbotthepony.kommons.io.writeStruct2d
import ru.dbotthepony.kommons.io.writeStruct2f
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.actor.Gender
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.io.readInternedString
@ -264,7 +265,6 @@ class QuestParameter(
class Adapter(gson: Gson) : TypeAdapter<QuestParameter>() {
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?) {
if (value == null)
@ -287,7 +287,7 @@ class QuestParameter(
if (`in`.consumeNull())
return null
val read = objects.read(`in`)
val read = Starbound.ELEMENTS_ADAPTER.objects.read(`in`)
val detail = if (read["type"]?.asString == "json") {
JsonData(read)

View File

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

View File

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

View File

@ -44,7 +44,7 @@ data class TileDefinition(
val collisionKind: CollisionType = CollisionType.BLOCK,
override val renderTemplate: AssetReference<RenderTemplate>,
override val renderTemplate: AssetReference<RenderTemplate> = AssetReference(null),
override val renderParameters: RenderParameters,
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>? {
if (Item::class.java.isAssignableFrom(type.rawType)) {
return object : TypeAdapter<Item>() {
private val arrays = gson.getAdapter(JsonArray::class.java)
private val grassVariant = gson.getAdapter(GrassVariant::class.java)
private val bushVariant = gson.getAdapter(BushVariant::class.java)
private val trees = gson.listAdapter<TreeVariant>()
@ -116,7 +115,7 @@ data class BiomePlaceables(
// Truly our hero here.
val obj = when (val type = `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`))
"bush" -> Bush(bushVariant.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>? {
if (VisitableWorldParameters::class.java.isAssignableFrom(type.rawType)) {
return object : TypeAdapter<VisitableWorldParameters>() {
private val objects = gson.getAdapter(JsonObject::class.java)
override fun write(out: JsonWriter, value: VisitableWorldParameters?) {
if (value == null)
out.nullValue()
@ -87,7 +85,7 @@ enum class VisitableWorldParametersType(override val jsonName: String, val token
return null
val instance = type.rawType.getDeclaredConstructor().newInstance() as VisitableWorldParameters
instance.fromJson(objects.read(`in`))
instance.fromJson(Starbound.ELEMENTS_ADAPTER.objects.read(`in`))
return instance
}
} as TypeAdapter<T>

View File

@ -612,8 +612,6 @@ class WorldLayout {
}
companion object : TypeAdapter<WorldLayout>() {
private val objects by lazy { Starbound.gson.getAdapter(JsonObject::class.java) }
override fun write(out: JsonWriter, value: WorldLayout?) {
if (value == null)
out.nullValue()
@ -626,7 +624,7 @@ class WorldLayout {
return null
val params = WorldLayout()
params.fromJson(objects.read(`in`))
params.fromJson(Starbound.ELEMENTS_ADAPTER.objects.read(`in`))
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.Object2ObjectLinkedOpenHashMap
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.io.readBinaryString
import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kommons.io.readVarLong
import ru.dbotthepony.kstarbound.IStarboundFile
import ru.dbotthepony.kstarbound.getValue
import ru.dbotthepony.kstarbound.json.readJsonObject
import ru.dbotthepony.kstarbound.util.sbIntern
import java.io.BufferedInputStream
import java.io.Closeable
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 {
throw IllegalStateException("${computeFullPath()} is a directory")
}
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>?
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 {
return object : InputStream() {
private var innerOffset = 0L
@ -132,22 +154,20 @@ class StarboundPak(val path: File, callback: (finished: Boolean, status: String)
if (readMax <= 0)
return -1
synchronized(reader) {
reader.seek(innerOffset + offset)
val readBytes = reader.read(b, off, readMax)
reader.seek(innerOffset + offset)
val readBytes = reader.read(b, off, readMax)
if (readBytes == -1)
throw RuntimeException("Unexpected EOF, want to read $readMax bytes from starting $offset in $path")
if (readBytes == -1)
throw RuntimeException("Unexpected EOF, want to read $readMax bytes from starting $offset in $path")
innerOffset += readBytes
return readBytes
}
innerOffset += readBytes
return readBytes
}
}
}
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 {
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" }
val offset = stream.readLong()
val length = stream.readLong()
if (offset > flength) {
throw IndexOutOfBoundsException("Garbage offset at index $i: ${offset}")
throw IndexOutOfBoundsException("Garbage offset at index $i: $offset")
}
if (length + offset > flength) {
@ -233,10 +253,10 @@ class StarboundPak(val path: File, callback: (finished: Boolean, status: String)
var parent = root as SBDirectory
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) {
if (name == null) {
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
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.JsonObject
import com.google.gson.JsonPrimitive
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.internal.bind.TypeAdapters
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import org.classdump.luna.Table
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.contains
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.value
import ru.dbotthepony.kommons.io.writeBinaryString
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.lua.LuaEnvironment
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.lang.Math.toRadians
import java.util.concurrent.atomic.AtomicLong
import kotlin.math.max
import kotlin.properties.Delegates
/**
* Base class for instanced items in game
*/
open class ItemStack {
constructor() {
this.config = Registries.items.emptyRef
this.parameters = JsonObject()
}
constructor(descriptor: ItemDescriptor) {
this.config = descriptor.ref
this.size = descriptor.count
this.parameters = descriptor.parameters.deepCopy()
}
@JsonAdapter(ItemStack.Adapter::class)
open class ItemStack(descriptor: ItemDescriptor) {
/**
* unique number utilized to determine whenever stack has changed
*/
@ -52,7 +67,7 @@ open class ItemStack {
changeset = CHANGESET.incrementAndGet()
}
var size: Long = 0L
var size: Long = descriptor.count
set(value) {
val newValue = value.coerceAtLeast(0L)
@ -62,18 +77,21 @@ open class ItemStack {
}
}
val config: Registry.Ref<IItemDefinition>
var parameters: JsonObject
val config: ItemRegistry.Entry = descriptor.ref
var parameters: JsonObject = descriptor.parameters.deepCopy()
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
get() = size <= 0 || config.isEmpty
val isNotEmpty: Boolean
get() = size > 0 && config.isPresent
val maxStackSize: Long
get() = config.value?.maxStack ?: 0L
get() = size > 0 && !config.isEmpty
fun grow(amount: Long) {
size += amount
@ -83,12 +101,135 @@ open class ItemStack {
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)
private fun config() = config
private val agingScripts: LuaEnvironment? by lazy {
val config = config().value ?: return@lazy null
//val config = config.value ?: return@lazy null
//if (config.itemTags)
null
}
@ -119,7 +260,7 @@ open class ItemStack {
if (isEmpty)
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)
@ -129,7 +270,7 @@ open class ItemStack {
stream.writeVarLong(0L)
stream.writeJsonElement(JsonNull.INSTANCE)
} else {
stream.writeBinaryString(config.key.left())
stream.writeBinaryString(config.name)
stream.writeVarLong(size)
stream.writeJsonElement(parameters)
}
@ -182,14 +323,14 @@ open class ItemStack {
if (isEmpty)
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)
return this
return ItemStack(ItemDescriptor(config, size, parameters.deepCopy()))
return ItemStack(ItemDescriptor(config.name, size, parameters.deepCopy()))
}
fun toJson(): JsonObject? {
@ -197,7 +338,7 @@ open class ItemStack {
return null
return JsonObject().also {
it.add("name", JsonPrimitive(config.key.left()))
it.add("name", JsonPrimitive(config.name))
it.add("count", JsonPrimitive(size))
it.add("parameters", parameters.deepCopy())
}
@ -209,27 +350,27 @@ open class ItemStack {
}
return allocator.newTable(0, 3).also {
it.rawset("name", config.key.left())
it.rawset("name", config.name)
it.rawset("count", size)
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?) {
val json = value?.toJson()
if (json == null)
out.nullValue()
else
TypeAdapters.JSON_ELEMENT.write(out, json)
out.value(json)
}
override fun read(`in`: JsonReader): ItemStack {
if (`in`.consumeNull())
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()
@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 {
if (descriptor.isEmpty)

View File

@ -1,13 +1,15 @@
package ru.dbotthepony.kstarbound
package ru.dbotthepony.kstarbound.item
import com.google.gson.JsonElement
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
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.json.JsonPatch
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.ExecutorService
import java.util.concurrent.Future
import kotlin.collections.ArrayList
@ -29,7 +31,7 @@ object RecipeRegistry {
val output2recipes: Map<String, List<Entry>> = Collections.unmodifiableMap(output2recipesBacking)
val input2recipes: Map<String, List<Entry>> = Collections.unmodifiableMap(input2recipesBacking)
private val backlog = ConcurrentLinkedQueue<Entry>()
private val tasks = ConcurrentLinkedQueue<Entry>()
private fun add(recipe: Entry) {
val value = recipe.value
@ -59,26 +61,22 @@ object RecipeRegistry {
}
fun finishLoad() {
var next = backlog.poll()
while (next != null) {
add(next)
next = backlog.poll()
}
tasks.forEach { add(it) }
tasks.clear()
}
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 elements = Starbound.gson.getAdapter(JsonElement::class.java)
val recipes = Starbound.gson.getAdapter(RecipeDefinition::class.java)
val recipes by lazy { Starbound.gson.getAdapter(RecipeDefinition::class.java) }
return files.map { listedFile ->
Starbound.EXECUTOR.submit {
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)
backlog.add(Entry(value, json, listedFile))
tasks.add(Entry(value, json, listedFile))
} catch (err: Throwable) {
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)
}
override fun read(`in`: JsonReader): JsonElement? {
override fun read(`in`: JsonReader): JsonElement {
return when (val p = `in`.peek()) {
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.NULL -> JsonNull.INSTANCE
JsonToken.NULL -> {
`in`.nextNull()
JsonNull.INSTANCE
}
JsonToken.BEGIN_ARRAY -> arrays.read(`in`)
JsonToken.BEGIN_OBJECT -> objects.read(`in`)
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 it.unimi.dsi.fastutil.objects.ObjectArrayList
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.set
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>) {
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))
}
private fun reconstructPath(at: Int): String {
if (at == 0)
return "<root>"
else
return "/${pieces.joinToString("/", limit = at, truncated = "") { it.value }}"
fun reconstructPath(at: Int = pieces.size - 1): String {
if (at == pieces.size - 1)
return toString()
return "/${pieces.joinToString("/", limit = at + 1, truncated = "") { it.value }}"
}
/**
* Attempts to find given element along path, if can't, throws [TraversalException]
*/
fun get(element: JsonElement): JsonElement {
if (isEmpty) {
return element
}
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 traverse(element, { self, index ->
if (index == null || index !in 0 until self.size()) {
throw TraversalException("Path at ${reconstructPath()} points at non-existing index")
}
}
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
}
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 {
val EMPTY = JsonPath(ImmutableList.of())

View File

@ -11,6 +11,7 @@ import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.gson.value
import ru.dbotthepony.kstarbound.Starbound
inline fun <reified E : Any, reified C : Any> DispatchingAdapter(
key: String,
@ -40,7 +41,6 @@ class DispatchingAdapter<TYPE : Any, ELEMENT : Any>(
private inner class Impl(gson: Gson) : TypeAdapter<ELEMENT>() {
private val typeAdapter = gson.getAdapter(typeClass)
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?) {
if (value == null)
@ -75,7 +75,7 @@ class DispatchingAdapter<TYPE : Any, ELEMENT : Any>(
if (`in`.consumeNull())
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 adapter = adapters[type] ?: throw JsonSyntaxException("Unknown type $type (${read[key]})")
return adapter.fromJsonTree(read)

View File

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

View File

@ -1,9 +1,13 @@
package ru.dbotthepony.kstarbound.json.builder
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.reflect.TypeToken
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.Starbound
import kotlin.properties.Delegates
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.KProperty1
@ -49,6 +53,12 @@ open class ReferencedProperty<T : Any, V>(
} else {
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 {
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.classdump.luna.ByteString
import org.classdump.luna.LuaObject
import org.classdump.luna.LuaRuntimeException
import org.classdump.luna.LuaType
import org.classdump.luna.StateContext
import org.classdump.luna.Table
@ -27,6 +28,7 @@ import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.AssetPath
import ru.dbotthepony.kstarbound.lua.bindings.provideRootBindings
import ru.dbotthepony.kstarbound.lua.bindings.provideUtilityBindings
import ru.dbotthepony.kstarbound.util.random.random
class LuaEnvironment : StateContext {
private var nilMeta: Table? = null
@ -126,6 +128,7 @@ class LuaEnvironment : StateContext {
val globals: Table = newTable()
val executor: DirectCallExecutor = DirectCallExecutor.newExecutor()
var random = random()
init {
globals["_G"] = globals
@ -163,6 +166,39 @@ class LuaEnvironment : StateContext {
StringLib.installInto(this, globals)
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?
Utf8Lib.installInto(this, globals)
@ -210,7 +246,7 @@ class LuaEnvironment : StateContext {
errorState = true
}
fun init(): Boolean {
fun init(callInit: Boolean = true): Boolean {
check(!initCalled) { "Already called init()" }
initCalled = true
@ -227,15 +263,17 @@ class LuaEnvironment : StateContext {
scripts.clear()
val init = globals["init"]
if (callInit) {
val init = globals["init"]
if (init is LuaFunction<*, *, *, *, *>) {
try {
executor.call(this, init)
} catch (err: Throwable) {
errorState = true
LOGGER.error("Exception on init()", err)
return false
if (init is LuaFunction<*, *, *, *, *>) {
try {
executor.call(this, init)
} catch (err: Throwable) {
errorState = true
LOGGER.error("Exception on init()", err)
return false
}
}
}

View File

@ -1,6 +1,5 @@
package ru.dbotthepony.kstarbound.lua.bindings
import it.unimi.dsi.fastutil.longs.LongArrayList
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap
import org.classdump.luna.ByteString
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.LuaFunction
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.Registry
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) {
val actualTags = Object2IntOpenHashMap<String>()
@ -400,11 +361,11 @@ fun provideRootBindings(lua: LuaEnvironment) {
table["projectileConfig"] = registryDef(Registries.projectiles)
table["recipesForItem"] = luaFunction(::recipesForItem)
table["itemType"] = luaFunction(::itemType)
table["itemTags"] = luaFunction(::itemTags)
table["itemHasTag"] = luaFunction(::itemHasTag)
table["itemType"] = luaStub("itemType")
table["itemTags"] = luaStub("itemTags")
table["itemHasTag"] = luaStub("itemHasTag")
table["itemConfig"] = luaFunctionNS("itemConfig", ::itemConfig)
table["itemConfig"] = luaStub("itemConfig")
table["createItem"] = luaStub("createItem")
table["tenantConfig"] = registryDef(Registries.tenants)

View File

@ -1,9 +1,11 @@
package ru.dbotthepony.kstarbound.lua.bindings
import org.classdump.luna.Table
import ru.dbotthepony.kstarbound.lua.LuaEnvironment
import ru.dbotthepony.kstarbound.lua.get
import ru.dbotthepony.kstarbound.lua.luaFunction
import ru.dbotthepony.kstarbound.lua.set
import ru.dbotthepony.kstarbound.lua.toVector2d
import ru.dbotthepony.kstarbound.world.World
fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) {
@ -13,4 +15,12 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) {
callbacks["flyingType"] = luaFunction {
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())
})
.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,
// 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 legacySerializer = PacketRegistry.LEGACY.Serializer(side)
// whenever protocol was negotiated
var isConnected = false
private set
// whenever successfully joined the game
var isReady = false
protected set
open fun setupLegacy() {
isConnected = true
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) {
if (channel.isOpen && isConnected) {
@ -172,6 +179,7 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) :
abstract fun disconnect(reason: String = "")
override fun close() {
LOGGER.info("Terminating $this")
channel.close()
}

View File

@ -89,7 +89,9 @@ class ServerChannels(val server: StarboundServer) : Closeable {
fun broadcast(packet: IPacket) {
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
override fun flush() {
if (isConnected) {
if (isConnected && isReady) {
val entries = rpc.write()
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) {
if (!isConnected || !channel.isOpen)
if (!isConnected || !channel.isOpen || !isReady)
return
flush()
@ -452,6 +452,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
flush()
}
isReady = false
tracker?.remove()
tracker = null
@ -487,6 +488,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
private var countedTowardsPlayerCount = false
override fun inGame() {
super.inGame()
announcedDisconnect = false
server.chat.systemMessage("Player '$nickname' connected")
countedTowardsPlayerCount = true

View File

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

View File

@ -4,6 +4,7 @@ import com.google.gson.JsonElement
import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.ints.IntArraySet
import it.unimi.dsi.fastutil.objects.ObjectArraySet
import kotlinx.coroutines.async
import kotlinx.coroutines.future.asCompletableFuture
import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
@ -350,7 +351,7 @@ class ServerWorld private constructor(
// everything inside our own thread, not anywhere else
// This way, external callers can properly wait for preparations to complete
fun prepare(): CompletableFuture<*> {
return eventLoop.scope.launch { prepare0() }.asCompletableFuture()
return eventLoop.scope.async { prepare0() }.asCompletableFuture()
}
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.vector.Vector2i
import ru.dbotthepony.kommons.vector.Vector3i
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.world.CelestialParameters
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.pairAdapter
@ -59,7 +60,6 @@ class UniverseChunk(var chunkPos: Vector2i = Vector2i.ZERO) {
class Adapter(gson: Gson) : TypeAdapter<UniverseChunk>() {
private val vectors = gson.getAdapter(Vector2i::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 params = gson.getAdapter(CelestialParameters::class.java)
private val lines = gson.pairAdapter<Vector2i, Vector2i>()
@ -141,7 +141,7 @@ class UniverseChunk(var chunkPos: Vector2i = Vector2i.ZERO) {
if (`in`.consumeNull())
return null
val read = objects.read(`in`)
val read = Starbound.ELEMENTS_ADAPTER.objects.read(`in`)
val chunkPos = read.get("chunkIndex", vectors)
val chunk = UniverseChunk(chunkPos)

View File

@ -55,6 +55,6 @@ object AssetPathStack {
if (path.isNotEmpty() && path[0] == '/')
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 (i == 0)
pieces.add(Piece(contents = raw))
pieces.add(Piece(contents = raw.sbIntern()))
else
pieces.add(Piece(contents = raw.substring(i)))
pieces.add(Piece(contents = raw.substring(i).sbIntern()))
break
} else {
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)
@ -246,7 +246,7 @@ class SBPattern private constructor(
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
}
}

View File

@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound.util.random
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import it.unimi.dsi.fastutil.bytes.ByteConsumer
import org.classdump.luna.ByteString
import ru.dbotthepony.kommons.util.IStruct2d
import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kommons.util.XXHash32
@ -61,6 +62,7 @@ fun staticRandom32(vararg values: Any?): Int {
for (value in values) {
when (value) {
is String -> digest.update(value.toByteArray())
is ByteString -> digest.update(value.bytes)
is Byte -> digest.update(value)
is Boolean -> digest.update(if (value) 1 else 0)
is Short -> toBytes(digest::update, value)
@ -100,6 +102,7 @@ fun staticRandom64(vararg values: Any?): Long {
for (value in values) {
when (value) {
is String -> digest.update(value.toByteArray())
is ByteString -> digest.update(value.bytes)
is Byte -> digest.update(value)
is Boolean -> digest.update(if (value) 1 else 0)
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>) {
val ours = cells.value
source.checkSizeEquals(ours)
for (x in 0 until CHUNK_SIZE) {
for (y in 0 until CHUNK_SIZE) {
for (x in 0 until width) {
for (y in 0 until height) {
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.writeStruct3i
import ru.dbotthepony.kommons.io.writeVarInt
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.network.syncher.legacyCodec
import ru.dbotthepony.kstarbound.network.syncher.nativeCodec
import java.io.DataInputStream
@ -122,7 +123,6 @@ data class UniversePos(val location: Vector3i = Vector3i.ZERO, val planetOrbit:
class Adapter(gson: Gson) : TypeAdapter<UniversePos>() {
private val vectors = gson.getAdapter(Vector3i::class.java)
private val objects = gson.getAdapter(JsonObject::class.java)
override fun write(out: JsonWriter, value: UniversePos) {
out.beginObject()
@ -141,7 +141,7 @@ data class UniversePos(val location: Vector3i = Vector3i.ZERO, val planetOrbit:
override fun read(`in`: JsonReader): UniversePos {
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 planet = values.get("planet", 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.entities.AbstractEntity
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.physics.CollisionPoly
import ru.dbotthepony.kstarbound.world.physics.CollisionType
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicInteger
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) {
ticks++
Starbound.EXECUTOR.submit(ParallelPerform(dynamicEntities.spliterator(), {
if (!it.isRemote) {
it.movement.move(delta)
if (dynamicEntities.size < 128) {
dynamicEntities.forEach {
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 {
try {

View File

@ -491,7 +491,7 @@ class Animator() {
for ((part, tags) in config.partTagDefaults) {
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) {
val tags = partTags[partName] ?: throw IllegalArgumentException("Unknown part $partName!")
tags[tagKey] = tagValue
partTags.computeIfAbsent(partName) {
LOGGER.warn("Creating part tags for $it after initialization, this can cause client-server desyncs")
NetworkedMap(InternedStringCodec, InternedStringCodec)
}.put(tagKey, tagValue)
}
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.CollisionType
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.util.LinkedList
import kotlin.math.PI
import kotlin.math.absoluteValue
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.objects.ObjectArraySet
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.collect.ListenableMap
import ru.dbotthepony.kommons.io.IntValueCodec
import ru.dbotthepony.kommons.io.KOptionalIntValueCodec
@ -173,8 +174,17 @@ class StatusController(val entity: ActorEntity, val config: StatusControllerConf
abstract val maxValue: Double?
fun setAsPercentage(percent: Double) {
val maxValue = maxValue ?: throw IllegalArgumentException("$name does not have max value")
this.value = maxValue * percent
val maxValue = maxValue
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 {
private val LEGACY_MODIFIERS_CODEC = StreamCodec.Collection(StatModifier.LEGACY_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 it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.set
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.util.getValue
import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.InteractAction
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.util.ManualLazy
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 java.io.DataInputStream
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 isCrafting by networkedBoolean().also { networkGroup.upstream.add(it) }
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.
private var isInitialized = false
@ -100,6 +109,52 @@ class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(co
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,
// and networks entire state each time something has changed.
inner class Container(size: Int) : NetworkedElement(), IContainer {
@ -181,10 +236,7 @@ class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(co
val wrapper = FastByteArrayOutputStream()
val stream = DataOutputStream(wrapper)
stream.writeVarInt(size)
var setItemsSize = items.indexOfLast { it.isNotEmpty }
if (setItemsSize == -1)
setItemsSize = 0
val setItemsSize = items.indexOfLast { it.isNotEmpty } + 1
stream.writeVarInt(setItemsSize)
@ -256,4 +308,8 @@ class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(co
override fun disableInterpolation() {}
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())
}
private val objects by lazy { Starbound.gson.getAdapter(JsonObject::class.java) }
fun named(name: String, parameters: TerrainSelectorParameters): AbstractTerrainSelector<*> {
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())
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<*, *> {