давайте снова попробуем builder'ов, но на этот раз с интерфейсами

This commit is contained in:
DBotThePony 2023-01-02 00:08:34 +07:00
parent 78cdc2c886
commit 3da8450a2c
Signed by: DBot
GPG Key ID: DCC23B5715498507
37 changed files with 975 additions and 482 deletions

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound package ru.dbotthepony.kstarbound
import com.google.common.collect.ImmutableMap
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.google.gson.TypeAdapter import com.google.gson.TypeAdapter
import java.util.Arrays import java.util.Arrays
@ -19,3 +20,5 @@ operator fun <T> ThreadLocal<T>.getValue(thisRef: Any, property: KProperty<*>):
operator fun <T> ThreadLocal<T>.setValue(thisRef: Any, property: KProperty<*>, value: T?) { operator fun <T> ThreadLocal<T>.setValue(thisRef: Any, property: KProperty<*>, value: T?) {
set(value) set(value)
} }
operator fun <K, V> ImmutableMap.Builder<K, V>.set(key: K, value: V): ImmutableMap.Builder<K, V> = put(key, value)

View File

@ -17,8 +17,16 @@ import ru.dbotthepony.kstarbound.api.explore
import ru.dbotthepony.kstarbound.defs.* import ru.dbotthepony.kstarbound.defs.*
import ru.dbotthepony.kstarbound.defs.image.AtlasConfiguration import ru.dbotthepony.kstarbound.defs.image.AtlasConfiguration
import ru.dbotthepony.kstarbound.defs.image.SpriteReference import ru.dbotthepony.kstarbound.defs.image.SpriteReference
import ru.dbotthepony.kstarbound.defs.item.ItemDefinition import ru.dbotthepony.kstarbound.defs.item.ArmorItemPrototype
import ru.dbotthepony.kstarbound.defs.item.ArmorPieceType
import ru.dbotthepony.kstarbound.defs.item.CurrencyItemPrototype
import ru.dbotthepony.kstarbound.defs.item.IArmorItemDefinition
import ru.dbotthepony.kstarbound.defs.item.IFossilItemDefinition
import ru.dbotthepony.kstarbound.defs.item.IItemDefinition
import ru.dbotthepony.kstarbound.defs.item.ItemPrototype
import ru.dbotthepony.kstarbound.defs.item.ItemRarity import ru.dbotthepony.kstarbound.defs.item.ItemRarity
import ru.dbotthepony.kstarbound.defs.item.ItemTooltipKind
import ru.dbotthepony.kstarbound.defs.item.LeveledStatusEffect
import ru.dbotthepony.kstarbound.defs.liquid.LiquidDefinition import ru.dbotthepony.kstarbound.defs.liquid.LiquidDefinition
import ru.dbotthepony.kstarbound.defs.projectile.* import ru.dbotthepony.kstarbound.defs.projectile.*
import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier
@ -30,7 +38,7 @@ import ru.dbotthepony.kstarbound.defs.world.dungeon.DungeonWorldDef
import ru.dbotthepony.kstarbound.io.* import ru.dbotthepony.kstarbound.io.*
import ru.dbotthepony.kstarbound.io.json.AABBTypeAdapter import ru.dbotthepony.kstarbound.io.json.AABBTypeAdapter
import ru.dbotthepony.kstarbound.io.json.AABBiTypeAdapter import ru.dbotthepony.kstarbound.io.json.AABBiTypeAdapter
import ru.dbotthepony.kstarbound.io.json.CustomEnumTypeAdapter import ru.dbotthepony.kstarbound.io.json.EnumAdapter
import ru.dbotthepony.kstarbound.io.json.Vector2dTypeAdapter import ru.dbotthepony.kstarbound.io.json.Vector2dTypeAdapter
import ru.dbotthepony.kstarbound.io.json.Vector2fTypeAdapter import ru.dbotthepony.kstarbound.io.json.Vector2fTypeAdapter
import ru.dbotthepony.kstarbound.io.json.Vector2iTypeAdapter import ru.dbotthepony.kstarbound.io.json.Vector2iTypeAdapter
@ -57,24 +65,24 @@ object Starbound {
/** /**
* Служит переменной для указания из какой папки происходит чтение asset'а в данном потоке * Служит переменной для указания из какой папки происходит чтение asset'а в данном потоке
*/ */
var readingFolder by ThreadLocal<String>() var assetFolder by ThreadLocal<String>()
private set private set
fun readingFolderTransformer(input: String): String { fun assetFolder(input: String): String {
val readingFolder = readingFolder val assetFolder = assetFolder
require(readingFolder != null) { "Not reading an asset on current thread" } require(assetFolder != null) { "Not reading an asset on current thread" }
if (input[0] == '/') if (input[0] == '/')
return input return input
return assetStringInterner.intern("$readingFolder/$input") return STRING_INTERNER.intern("$assetFolder/$input")
} }
fun readingFolderTransformerNullable(input: String?): String? { fun assetFolderNullable(input: String?): String? {
require(readingFolder != null) { "Not reading an asset on current thread" } require(assetFolder != null) { "Not reading an asset on current thread" }
if (input != null) if (input != null)
return readingFolderTransformer(input) return assetFolder(input)
return null return null
} }
@ -83,7 +91,7 @@ object Starbound {
if (input == null) if (input == null)
return null return null
return input.stream().map { readingFolderTransformer(it) }.collect(ImmutableList.toImmutableList()) return input.stream().map { assetFolder(it) }.collect(ImmutableList.toImmutableList())
} }
private val tiles = Object2ObjectOpenHashMap<String, TileDefinition>() private val tiles = Object2ObjectOpenHashMap<String, TileDefinition>()
@ -99,7 +107,7 @@ object Starbound {
private val parallax = Object2ObjectOpenHashMap<String, ParallaxPrototype>() private val parallax = Object2ObjectOpenHashMap<String, ParallaxPrototype>()
private val functions = Object2ObjectOpenHashMap<String, JsonFunction>() private val functions = Object2ObjectOpenHashMap<String, JsonFunction>()
private val items = Object2ObjectOpenHashMap<String, ItemDefinition>() private val items = Object2ObjectOpenHashMap<String, IItemDefinition>()
val liquidAccess: Map<String, LiquidDefinition> = Collections.unmodifiableMap(liquid) val liquidAccess: Map<String, LiquidDefinition> = Collections.unmodifiableMap(liquid)
val liquidByIDAccess: Map<Int, LiquidDefinition> = Collections.unmodifiableMap(liquidByID) val liquidByIDAccess: Map<Int, LiquidDefinition> = Collections.unmodifiableMap(liquidByID)
@ -110,21 +118,21 @@ object Starbound {
val projectilesAccess: Map<String, ConfiguredProjectile> = Collections.unmodifiableMap(projectiles) val projectilesAccess: Map<String, ConfiguredProjectile> = Collections.unmodifiableMap(projectiles)
val parallaxAccess: Map<String, ParallaxPrototype> = Collections.unmodifiableMap(parallax) val parallaxAccess: Map<String, ParallaxPrototype> = Collections.unmodifiableMap(parallax)
val functionsAccess: Map<String, JsonFunction> = Collections.unmodifiableMap(functions) val functionsAccess: Map<String, JsonFunction> = Collections.unmodifiableMap(functions)
val itemAccess: Map<String, ItemDefinition> = Collections.unmodifiableMap(items) val itemAccess: Map<String, IItemDefinition> = Collections.unmodifiableMap(items)
val assetStringInterner: Interner<String> = Interners.newStrongInterner() val STRING_INTERNER: Interner<String> = Interners.newStrongInterner()
val nonnullStringTypeAdapter: TypeAdapter<String> = object : TypeAdapter<String>() { val STRING_ADAPTER: TypeAdapter<String> = object : TypeAdapter<String>() {
override fun write(out: JsonWriter, value: String) { override fun write(out: JsonWriter, value: String) {
out.value(value) out.value(value)
} }
override fun read(`in`: JsonReader): String { override fun read(`in`: JsonReader): String {
return assetStringInterner.intern(TypeAdapters.STRING.read(`in`)) return STRING_INTERNER.intern(TypeAdapters.STRING.read(`in`))
} }
} }
val stringTypeAdapter: TypeAdapter<String?> = nonnullStringTypeAdapter.nullSafe() val NULLABLE_STRING_ADAPTER: TypeAdapter<String?> = STRING_ADAPTER.nullSafe()
val gson: Gson = GsonBuilder() val gson: Gson = GsonBuilder()
.enableComplexMapKeySerialization() .enableComplexMapKeySerialization()
@ -135,7 +143,7 @@ object Starbound {
.registerTypeAdapter(Color::class.java, ColorTypeAdapter.nullSafe()) .registerTypeAdapter(Color::class.java, ColorTypeAdapter.nullSafe())
// чтоб строки всегда intern'ились // чтоб строки всегда intern'ились
.registerTypeAdapter(stringTypeAdapter) .registerTypeAdapter(NULLABLE_STRING_ADAPTER)
// math // math
.registerTypeAdapter(AABBTypeAdapter) .registerTypeAdapter(AABBTypeAdapter)
@ -156,12 +164,22 @@ object Starbound {
.also(RenderTemplate::registerGson) .also(RenderTemplate::registerGson)
.also(TileDefinition::registerGson) .also(TileDefinition::registerGson)
.also(LiquidDefinition::registerGson) .also(LiquidDefinition::registerGson)
.also(ItemDefinition::registerGson)
.also(ItemRarity::registerGson)
.also(SpriteReference::registerGson) .also(SpriteReference::registerGson)
.also(AtlasConfiguration::registerGson) .also(AtlasConfiguration::registerGson)
.registerTypeAdapter(DamageType::class.java, CustomEnumTypeAdapter(DamageType.values()).nullSafe()) .registerTypeAdapter(LeveledStatusEffect.ADAPTER)
.registerTypeAdapter(ItemPrototype.ADAPTER)
.registerTypeAdapter(CurrencyItemPrototype.ADAPTER)
.registerTypeAdapter(ArmorItemPrototype.ADAPTER)
.registerTypeAdapter(IItemDefinition.InventoryIcon.ADAPTER)
.registerTypeAdapter(IFossilItemDefinition.FossilSetDescription.ADAPTER)
.registerTypeAdapter(IArmorItemDefinition.ArmorFrames.ADAPTER)
.registerTypeAdapter(EnumAdapter(DamageType::class, default = DamageType.NORMAL).neverNull())
.registerTypeAdapter(EnumAdapter(ItemRarity::class).neverNull())
.registerTypeAdapter(EnumAdapter(ItemTooltipKind::class).neverNull())
.create() .create()
@ -170,7 +188,7 @@ object Starbound {
return when (type) { return when (type) {
Float::class.java -> TypeAdapters.FLOAT as TypeAdapter<T> Float::class.java -> TypeAdapters.FLOAT as TypeAdapter<T>
Double::class.java -> TypeAdapters.DOUBLE as TypeAdapter<T> Double::class.java -> TypeAdapters.DOUBLE as TypeAdapter<T>
String::class.java -> stringTypeAdapter as TypeAdapter<T> String::class.java -> NULLABLE_STRING_ADAPTER as TypeAdapter<T>
Int::class.java -> TypeAdapters.INTEGER as TypeAdapter<T> Int::class.java -> TypeAdapters.INTEGER as TypeAdapter<T>
Long::class.java -> TypeAdapters.LONG as TypeAdapter<T> Long::class.java -> TypeAdapters.LONG as TypeAdapter<T>
Boolean::class.java -> TypeAdapters.BOOLEAN as TypeAdapter<T> Boolean::class.java -> TypeAdapters.BOOLEAN as TypeAdapter<T>
@ -339,14 +357,14 @@ object Starbound {
} }
private fun loadTileMaterials(callback: (String) -> Unit) { private fun loadTileMaterials(callback: (String) -> Unit) {
readingFolder = "/tiles/materials" assetFolder = "/tiles/materials"
for (fs in fileSystems) { for (fs in fileSystems) {
for (listedFile in fs.explore().filter { it.isFile }.filter { it.name.endsWith(".material") }) { for (listedFile in fs.explore().filter { it.isFile }.filter { it.name.endsWith(".material") }) {
try { try {
callback("Loading $listedFile") callback("Loading $listedFile")
readingFolder = listedFile.computeDirectory() assetFolder = listedFile.computeDirectory()
val tileDef = gson.fromJson(listedFile.reader(), TileDefinition::class.java) val tileDef = gson.fromJson(listedFile.reader(), TileDefinition::class.java)
check(tiles[tileDef.materialName] == null) { "Already has material with name ${tileDef.materialName} loaded!" } check(tiles[tileDef.materialName] == null) { "Already has material with name ${tileDef.materialName} loaded!" }
@ -364,7 +382,7 @@ object Starbound {
} }
} }
readingFolder = null assetFolder = null
} }
private fun loadProjectiles(callback: (String) -> Unit) { private fun loadProjectiles(callback: (String) -> Unit) {
@ -373,7 +391,7 @@ object Starbound {
try { try {
callback("Loading $listedFile") callback("Loading $listedFile")
readingFolder = listedFile.computeDirectory() assetFolder = listedFile.computeDirectory()
val def = gson.fromJson(listedFile.reader(), ConfigurableProjectile::class.java).assemble(listedFile.computeDirectory()) val def = gson.fromJson(listedFile.reader(), ConfigurableProjectile::class.java).assemble(listedFile.computeDirectory())
check(projectiles[def.projectileName] == null) { "Already has projectile with ID ${def.projectileName} loaded!" } check(projectiles[def.projectileName] == null) { "Already has projectile with ID ${def.projectileName} loaded!" }
projectiles[def.projectileName] = def projectiles[def.projectileName] = def
@ -388,7 +406,7 @@ object Starbound {
} }
} }
readingFolder = null assetFolder = null
} }
private fun loadFunctions(callback: (String) -> Unit) { private fun loadFunctions(callback: (String) -> Unit) {
@ -397,7 +415,7 @@ object Starbound {
try { try {
callback("Loading $listedFile") callback("Loading $listedFile")
readingFolder = listedFile.computeDirectory() assetFolder = listedFile.computeDirectory()
val readObject = JsonParser.parseReader(listedFile.reader()) as JsonObject val readObject = JsonParser.parseReader(listedFile.reader()) as JsonObject
for (key in readObject.keySet()) { for (key in readObject.keySet()) {
@ -414,7 +432,7 @@ object Starbound {
} }
} }
readingFolder = null assetFolder = null
} }
private fun loadParallax(callback: (String) -> Unit) { private fun loadParallax(callback: (String) -> Unit) {
@ -423,7 +441,7 @@ object Starbound {
try { try {
callback("Loading $listedFile") callback("Loading $listedFile")
readingFolder = listedFile.computeDirectory() assetFolder = listedFile.computeDirectory()
val def = gson.fromJson(listedFile.reader(), ParallaxPrototype::class.java) val def = gson.fromJson(listedFile.reader(), ParallaxPrototype::class.java)
parallax[listedFile.name.substringBefore('.')] = def parallax[listedFile.name.substringBefore('.')] = def
} catch(err: Throwable) { } catch(err: Throwable) {
@ -436,18 +454,18 @@ object Starbound {
} }
} }
readingFolder = null assetFolder = null
} }
private fun loadMaterialModifiers(callback: (String) -> Unit) { private fun loadMaterialModifiers(callback: (String) -> Unit) {
readingFolder = "/tiles/materials" assetFolder = "/tiles/materials"
for (fs in fileSystems) { for (fs in fileSystems) {
for (listedFile in fs.explore().filter { it.isFile }.filter { it.name.endsWith(".matmod") }) { for (listedFile in fs.explore().filter { it.isFile }.filter { it.name.endsWith(".matmod") }) {
try { try {
callback("Loading $listedFile") callback("Loading $listedFile")
readingFolder = listedFile.computeDirectory() assetFolder = listedFile.computeDirectory()
val tileDef = gson.fromJson(listedFile.reader(), MaterialModifier::class.java) val tileDef = gson.fromJson(listedFile.reader(), MaterialModifier::class.java)
check(tileModifiers[tileDef.modName] == null) { "Already has material with name ${tileDef.modName} loaded!" } check(tileModifiers[tileDef.modName] == null) { "Already has material with name ${tileDef.modName} loaded!" }
@ -465,7 +483,7 @@ object Starbound {
} }
} }
readingFolder = null assetFolder = null
} }
private fun loadLiquidDefinitions(callback: (String) -> Unit) { private fun loadLiquidDefinitions(callback: (String) -> Unit) {
@ -474,7 +492,7 @@ object Starbound {
try { try {
callback("Loading $listedFile") callback("Loading $listedFile")
readingFolder = listedFile.computeDirectory() assetFolder = listedFile.computeDirectory()
val liquidDef = gson.fromJson(listedFile.reader(), LiquidDefinition::class.java) val liquidDef = gson.fromJson(listedFile.reader(), LiquidDefinition::class.java)
check(liquid.put(liquidDef.name, liquidDef) == null) { "Already has liquid with name ${liquidDef.name} loaded!" } check(liquid.put(liquidDef.name, liquidDef) == null) { "Already has liquid with name ${liquidDef.name} loaded!" }
@ -490,22 +508,42 @@ object Starbound {
} }
} }
readingFolder = null assetFolder = null
} }
private fun loadItemDefinitions(callback: (String) -> Unit) { private fun loadItemDefinitions(callback: (String) -> Unit) {
val files = listOf(".item", ".currency", ".head", ".chest", ".legs", ".activeitem") val files = listOf(".item", ".currency", ".head", ".chest", ".legs", ".back", ".activeitem")
for (fs in fileSystems) { for (fs in fileSystems) {
for (listedFile in fs.explore().filter { it.isFile }.filter { f -> files.any { f.name.endsWith(it) } }) { for (listedFile in fs.explore().filter { it.isFile }.filter { f -> files.any { f.name.endsWith(it) } }) {
try { try {
callback("Loading $listedFile") callback("Loading $listedFile")
readingFolder = listedFile.computeDirectory() assetFolder = listedFile.computeDirectory()
ItemDefinition.ADAPTER.currentSymbolicName = listedFile.computeFullPath()
val def = gson.fromJson(listedFile.reader(), ItemDefinition::class.java)
check(items.put(def.itemName, def) == null) { "Already has item with name ${def.itemName} loaded!" } if (listedFile.name.endsWith(".item")) {
val def = gson.fromJson(listedFile.reader(), ItemPrototype::class.java)
check(items.put(def.itemName, def.assemble()) == null) { "Already has item with name ${def.itemName} loaded!" }
} else if (listedFile.name.endsWith(".currency")) {
val def = gson.fromJson(listedFile.reader(), CurrencyItemPrototype::class.java)
check(items.put(def.itemName, def.assemble()) == null) { "Already has item with name ${def.itemName} loaded!" }
} else if (listedFile.name.endsWith(".head")) {
val def = gson.fromJson(listedFile.reader(), ArmorItemPrototype::class.java)
def.armorType = ArmorPieceType.HEAD
check(items.put(def.itemName, def.assemble()) == null) { "Already has item with name ${def.itemName} loaded!" }
} else if (listedFile.name.endsWith(".chest")) {
val def = gson.fromJson(listedFile.reader(), ArmorItemPrototype::class.java)
def.armorType = ArmorPieceType.CHEST
check(items.put(def.itemName, def.assemble()) == null) { "Already has item with name ${def.itemName} loaded!" }
} else if (listedFile.name.endsWith(".legs")) {
val def = gson.fromJson(listedFile.reader(), ArmorItemPrototype::class.java)
def.armorType = ArmorPieceType.LEGS
check(items.put(def.itemName, def.assemble()) == null) { "Already has item with name ${def.itemName} loaded!" }
} else if (listedFile.name.endsWith(".back")) {
val def = gson.fromJson(listedFile.reader(), ArmorItemPrototype::class.java)
def.armorType = ArmorPieceType.BACK
check(items.put(def.itemName, def.assemble()) == null) { "Already has item with name ${def.itemName} loaded!" }
}
} catch (err: Throwable) { } catch (err: Throwable) {
LOGGER.error("Loading item definition file $listedFile", err) LOGGER.error("Loading item definition file $listedFile", err)
} }
@ -516,6 +554,6 @@ object Starbound {
} }
} }
readingFolder = null assetFolder = null
} }
} }

View File

@ -0,0 +1,24 @@
package ru.dbotthepony.kstarbound.defs
interface IThingWithDescription {
/**
* Краткое описание штуки. Несмотря на то, что название свойства подразумевает "описание",
* на самом деле данное поле отвечает за название штуки.
*
* Примеры:
* * Microwave Oven
* * Copper Ore
* * Poptop
*/
val shortdescription: String
/**
* Полное описание штуки. Оно отображается игроку, когда последний наводит курсор на штуку.
*
* Примеры:
* * A microwave. For when you're hungry enough to nuke your food.
* * Copper ore. Can be used for smelting.
* * The Poptop hums beautifully to confuse its prey.
*/
val description: String
}

View File

@ -4,7 +4,7 @@ import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableMap
/** /**
* Возвращает глубокую неизменяемую копию [input] примитивов/List'ов/Map'ов * Возвращает глубокую, неизменяемую копию [input] примитивов/List'ов/Map'ов
*/ */
fun enrollList(input: List<Any>, interner: (String) -> String = String::intern): ImmutableList<Any> { fun enrollList(input: List<Any>, interner: (String) -> String = String::intern): ImmutableList<Any> {
val builder = ImmutableList.builder<Any>() val builder = ImmutableList.builder<Any>()
@ -21,7 +21,7 @@ fun enrollList(input: List<Any>, interner: (String) -> String = String::intern):
} }
/** /**
* Возвращает глубокую неизменяемую копию [input] примитивов/List'ов/Map'ов * Возвращает глубокую, неизменяемую копию [input] примитивов/List'ов/Map'ов
*/ */
fun enrollMap(input: Map<String, Any>, interner: (String) -> String = String::intern): ImmutableMap<String, Any> { fun enrollMap(input: Map<String, Any>, interner: (String) -> String = String::intern): ImmutableMap<String, Any> {
val builder = ImmutableMap.builder<String, Any>() val builder = ImmutableMap.builder<String, Any>()

View File

@ -6,7 +6,7 @@ import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kstarbound.io.json.CustomEnumTypeAdapter import ru.dbotthepony.kstarbound.io.json.EnumAdapter
import ru.dbotthepony.kstarbound.io.json.IStringSerializable import ru.dbotthepony.kstarbound.io.json.IStringSerializable
import ru.dbotthepony.kstarbound.io.json.Vector2dTypeAdapter import ru.dbotthepony.kstarbound.io.json.Vector2dTypeAdapter
import ru.dbotthepony.kvector.vector.ndouble.Vector2d import ru.dbotthepony.kvector.vector.ndouble.Vector2d
@ -29,7 +29,7 @@ enum class JsonFunctionInterpolation(vararg aliases: String) : IStringSerializab
} }
companion object { companion object {
val ADAPTER: TypeAdapter<JsonFunctionInterpolation> = CustomEnumTypeAdapter(values()).nullSafe() val ADAPTER: TypeAdapter<JsonFunctionInterpolation> = EnumAdapter(JsonFunctionInterpolation::class).neverNull()
} }
} }
@ -51,7 +51,7 @@ enum class JsonFunctionConstraint(vararg aliases: String) : IStringSerializable
} }
companion object { companion object {
val ADAPTER: TypeAdapter<JsonFunctionConstraint> = CustomEnumTypeAdapter(values()).nullSafe() val ADAPTER: TypeAdapter<JsonFunctionConstraint> = EnumAdapter(JsonFunctionConstraint::class).neverNull()
} }
} }

View File

@ -16,7 +16,7 @@ import ru.dbotthepony.kstarbound.io.json.INativeJsonHolder
*/ */
abstract class RawPrototype<RAW : RawPrototype<RAW, ASSEMBLED>, ASSEMBLED : AssembledPrototype<ASSEMBLED, RAW>> : INativeJsonHolder { abstract class RawPrototype<RAW : RawPrototype<RAW, ASSEMBLED>, ASSEMBLED : AssembledPrototype<ASSEMBLED, RAW>> : INativeJsonHolder {
val json = Object2ObjectArrayMap<String, Any>() val json = Object2ObjectArrayMap<String, Any>()
fun enroll() = enrollMap(json, Starbound.assetStringInterner::intern) fun enroll() = enrollMap(json, Starbound.STRING_INTERNER::intern)
abstract fun assemble(directory: String = ""): ASSEMBLED abstract fun assemble(directory: String = ""): ASSEMBLED
override fun acceptJson(json: MutableMap<String, Any>) { override fun acceptJson(json: MutableMap<String, Any>) {

View File

@ -304,7 +304,7 @@ class AtlasConfiguration private constructor(
return EMPTY return EMPTY
} }
val ADAPTER: TypeAdapter<AtlasConfiguration?> = Starbound.stringTypeAdapter.transform(read = read@{ get(it ?: return@read it as AtlasConfiguration?) }, write = write@{ it?.name }) val ADAPTER: TypeAdapter<AtlasConfiguration?> = Starbound.NULLABLE_STRING_ADAPTER.transform(read = read@{ get(it ?: return@read it as AtlasConfiguration?) }, write = write@{ it?.name })
fun registerGson(gsonBuilder: GsonBuilder) { fun registerGson(gsonBuilder: GsonBuilder) {
gsonBuilder.registerTypeAdapter(ADAPTER) gsonBuilder.registerTypeAdapter(ADAPTER)

View File

@ -30,7 +30,7 @@ data class ImageReference(
override fun read(`in`: JsonReader): ImageReference { override fun read(`in`: JsonReader): ImageReference {
if (`in`.peek() == JsonToken.STRING) { if (`in`.peek() == JsonToken.STRING) {
val image = Starbound.readingFolderTransformer(`in`.nextString()) val image = Starbound.assetFolder(`in`.nextString())
if (image.contains(':')) { if (image.contains(':')) {
throw JsonSyntaxException("Expected atlas/image reference, but got sprite reference: $image") throw JsonSyntaxException("Expected atlas/image reference, but got sprite reference: $image")

View File

@ -30,7 +30,7 @@ data class SpriteReference(
} }
override fun read(`in`: JsonReader): SpriteReference { override fun read(`in`: JsonReader): SpriteReference {
return parse(Starbound.readingFolderTransformer(`in`.nextString())) return parse(Starbound.assetFolder(`in`.nextString()))
} }
fun registerGson(gsonBuilder: GsonBuilder) { fun registerGson(gsonBuilder: GsonBuilder) {

View File

@ -0,0 +1,32 @@
package ru.dbotthepony.kstarbound.defs.item
data class ArmorItemDefinition(
override val shortdescription: String,
override val description: String,
override val itemName: String,
override val price: Long,
override val rarity: ItemRarity,
override val category: String?,
override val inventoryIcon: List<IItemDefinition.IInventoryIcon>?,
override val itemTags: List<String>,
override val learnBlueprintsOnPickup: List<String>,
override val maxStack: Long,
override val eventCategory: String?,
override val consumeOnPickup: Boolean,
override val pickupQuestTemplates: List<String>,
override val scripts: List<String>,
override val tooltipKind: ItemTooltipKind,
override val twoHanded: Boolean,
override val radioMessagesOnPickup: List<String>,
override val fuelAmount: Long?,
override val colorOptions: List<Map<String, String>>,
override val maleFrames: IArmorItemDefinition.IArmorFrames,
override val femaleFrames: IArmorItemDefinition.IArmorFrames,
override val level: Double,
override val leveledStatusEffects: List<ILeveledStatusEffect>,
override val armorType: ArmorPieceType,
val json: Map<String, Any>,
) : IArmorItemDefinition

View File

@ -0,0 +1,67 @@
package ru.dbotthepony.kstarbound.defs.item
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.enrollMap
import ru.dbotthepony.kstarbound.io.json.BuilderAdapter
import ru.dbotthepony.kstarbound.io.json.asJsonObject
import ru.dbotthepony.kstarbound.io.json.asList
import ru.dbotthepony.kstarbound.io.json.neverNull
import ru.dbotthepony.kstarbound.util.NotNullVar
class ArmorItemPrototype : ItemPrototype(), IArmorItemDefinition {
override var colorOptions: List<Map<String, String>> = listOf()
override var maleFrames: IArmorItemDefinition.ArmorFrames by NotNullVar()
override var femaleFrames: IArmorItemDefinition.ArmorFrames by NotNullVar()
override var level: Double = 1.0
override var leveledStatusEffects: List<LeveledStatusEffect> = listOf()
override var armorType: ArmorPieceType by NotNullVar()
init {
maxStack = 1L
}
override fun assemble(): IItemDefinition {
return ArmorItemDefinition(
shortdescription = shortdescription,
description = description,
itemName = itemName,
price = price,
rarity = rarity,
category = category,
inventoryIcon = inventoryIcon,
itemTags = itemTags,
learnBlueprintsOnPickup = learnBlueprintsOnPickup,
maxStack = maxStack,
eventCategory = eventCategory,
consumeOnPickup = consumeOnPickup,
pickupQuestTemplates = pickupQuestTemplates,
scripts = scripts,
tooltipKind = tooltipKind,
twoHanded = twoHanded,
radioMessagesOnPickup = radioMessagesOnPickup,
fuelAmount = fuelAmount,
json = enrollMap(json),
colorOptions = colorOptions,
maleFrames = maleFrames,
femaleFrames = femaleFrames,
level = level,
leveledStatusEffects = leveledStatusEffects,
armorType = armorType,
)
}
companion object {
val ADAPTER = BuilderAdapter.Builder(::ArmorItemPrototype)
.also { addFields(it as BuilderAdapter.Builder<ItemPrototype>) } // безопасность: свойства родительского класса объявлены как final
.add(ArmorItemPrototype::colorOptions, Starbound.NULLABLE_STRING_ADAPTER.neverNull().asJsonObject().asList())
.auto(ArmorItemPrototype::maleFrames)
.auto(ArmorItemPrototype::femaleFrames)
.auto(ArmorItemPrototype::level)
.autoList(ArmorItemPrototype::leveledStatusEffects)
.build()
}
}

View File

@ -0,0 +1,26 @@
package ru.dbotthepony.kstarbound.defs.item
/**
* Тип брони. Более формально, в какой слот надевается данный предмет
*/
enum class ArmorPieceType {
/**
* Шлем
*/
HEAD,
/**
* Нагрудник
*/
CHEST,
/**
* Поножи
*/
LEGS,
/**
* Плащ/рюкзак/прочее
*/
BACK
}

View File

@ -0,0 +1,32 @@
package ru.dbotthepony.kstarbound.defs.item
data class CurrencyItemDefinition(
override val shortdescription: String,
override val description: String,
override val itemName: String,
override val price: Long,
override val rarity: ItemRarity,
override val category: String?,
override val inventoryIcon: List<IItemDefinition.IInventoryIcon>?,
override val itemTags: List<String>,
override val learnBlueprintsOnPickup: List<String>,
override val maxStack: Long,
override val eventCategory: String?,
override val consumeOnPickup: Boolean,
override val pickupQuestTemplates: List<String>,
override val scripts: List<String>,
override val tooltipKind: ItemTooltipKind,
override val twoHanded: Boolean,
override val radioMessagesOnPickup: List<String>,
override val fuelAmount: Long?,
override val pickupSoundsSmall: List<String>,
override val pickupSoundsMedium: List<String>,
override val pickupSoundsLarge: List<String>,
override val smallStackLimit: Long,
override val mediumStackLimit: Long,
override val currency: String,
override val value: Long,
val json: Map<String, Any>,
) : ICurrencyItemDefinition

View File

@ -0,0 +1,65 @@
package ru.dbotthepony.kstarbound.defs.item
import ru.dbotthepony.kstarbound.defs.enrollMap
import ru.dbotthepony.kstarbound.io.json.BuilderAdapter
import ru.dbotthepony.kstarbound.util.NotNullVar
class CurrencyItemPrototype : ItemPrototype(), ICurrencyItemDefinition {
override var pickupSoundsSmall: List<String> = listOf()
override var pickupSoundsMedium: List<String> = listOf()
override var pickupSoundsLarge: List<String> = listOf()
override var smallStackLimit: Long by NotNullVar()
override var mediumStackLimit: Long by NotNullVar()
override var currency: String by NotNullVar()
override var value: Long by NotNullVar()
init {
maxStack = 16777216L
}
override fun assemble(): IItemDefinition {
return CurrencyItemDefinition(
shortdescription = shortdescription,
description = description,
itemName = itemName,
price = price,
rarity = rarity,
category = category,
inventoryIcon = inventoryIcon,
itemTags = itemTags,
learnBlueprintsOnPickup = learnBlueprintsOnPickup,
maxStack = maxStack,
eventCategory = eventCategory,
consumeOnPickup = consumeOnPickup,
pickupQuestTemplates = pickupQuestTemplates,
scripts = scripts,
tooltipKind = tooltipKind,
twoHanded = twoHanded,
radioMessagesOnPickup = radioMessagesOnPickup,
fuelAmount = fuelAmount,
json = enrollMap(json),
pickupSoundsSmall = pickupSoundsSmall,
pickupSoundsMedium = pickupSoundsMedium,
pickupSoundsLarge = pickupSoundsLarge,
smallStackLimit = smallStackLimit,
mediumStackLimit = mediumStackLimit,
currency = currency,
value = value,
)
}
companion object {
val ADAPTER = BuilderAdapter.Builder(::CurrencyItemPrototype)
.also { addFields(it as BuilderAdapter.Builder<ItemPrototype>) } // безопасность: свойства родительского класса объявлены как final
.autoList(CurrencyItemPrototype::pickupSoundsSmall)
.autoList(CurrencyItemPrototype::pickupSoundsMedium)
.autoList(CurrencyItemPrototype::pickupSoundsLarge)
.auto(CurrencyItemPrototype::smallStackLimit)
.auto(CurrencyItemPrototype::mediumStackLimit)
.auto(CurrencyItemPrototype::currency)
.auto(CurrencyItemPrototype::value)
.build()
}
}

View File

@ -0,0 +1,50 @@
package ru.dbotthepony.kstarbound.defs.item
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.io.json.FactoryAdapter
import ru.dbotthepony.kstarbound.io.json.ifString
interface IArmorItemDefinition : ILeveledItemDefinition {
/**
* @see ArmorPieceType
*/
val armorType: ArmorPieceType
/**
* Варианты покраски (???)
*/
val colorOptions: List<Map<String, String>>
/**
* Визуальные кадры анимации, когда надето на гуманоида мужского пола
*/
val maleFrames: IArmorFrames
/**
* Визуальные кадры анимации, когда надето на гуманоида женского пола
*/
val femaleFrames: IArmorFrames
interface IArmorFrames {
val body: String
val backSleeve: String?
val frontSleeve: String?
}
data class ArmorFrames(
override val body: String,
override val backSleeve: String?,
override val frontSleeve: String?,
) : IArmorFrames {
companion object {
val ADAPTER = FactoryAdapter.Builder(
ArmorFrames::class,
ArmorFrames::body,
ArmorFrames::backSleeve,
ArmorFrames::frontSleeve,
)
.build()
.ifString { ArmorFrames(Starbound.assetFolder(it), null, null) }
}
}
}

View File

@ -0,0 +1,38 @@
package ru.dbotthepony.kstarbound.defs.item
interface ICurrencyItemDefinition : IItemDefinition {
/**
* Звуки при поднятии "малого" количества предметов. Не имеет никакого смысла без [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
/**
* ID валюты
*/
val currency: String
/**
* Ценность одного предмета в [currency]
*/
val value: Long
}

View File

@ -0,0 +1,70 @@
package ru.dbotthepony.kstarbound.defs.item
import ru.dbotthepony.kstarbound.defs.IThingWithDescription
import ru.dbotthepony.kstarbound.io.json.FactoryAdapter
import ru.dbotthepony.kvector.vector.ndouble.Vector2d
interface IFossilItemDefinition : IItemDefinition {
/**
* Используется в костях-ископаемых
*/
val race: String
val displayImage: String
val displayoffset: Vector2d
/**
* Используется в костях-ископаемых
*/
val fossilSetName: String
/**
* Используется в костях-ископаемых
*/
val setIndex: Int
/**
* Используется в костях-ископаемых
*/
val setCount: Int
/**
* Используется в костях-ископаемых
*/
val setCollectables: Map<String, String>
/**
* Используется в костях-ископаемых
*/
val completeFossilIcon: String?
/**
* Используется в костях-ископаемых
*/
val completeFossilObject: String?
/**
* Используется в костях-ископаемых
*/
val completeSetDescriptions: IFossilSetDescription?
interface IFossilSetDescription : IThingWithDescription {
/**
* Цена в пикселях
*/
val price: Long
}
data class FossilSetDescription(
override val price: Long = 0L,
override val shortdescription: String = "...",
override val description: String = "..."
) : IFossilSetDescription {
companion object {
val ADAPTER = FactoryAdapter.Builder(
FossilSetDescription::class,
FossilSetDescription::price,
FossilSetDescription::shortdescription,
FossilSetDescription::description).build()
}
}
}

View File

@ -0,0 +1,105 @@
package ru.dbotthepony.kstarbound.defs.item
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.IThingWithDescription
import ru.dbotthepony.kstarbound.defs.image.SpriteReference
import ru.dbotthepony.kstarbound.io.json.FactoryAdapter
import ru.dbotthepony.kstarbound.io.json.ifString
interface IItemDefinition : IThingWithDescription {
/**
* Внутреннее имя предмета (ID).
* Не путать с именем предмета!
*
* @see shortdescription
* @see description
*/
val itemName: String
/**
* Цена в пикселях
*/
val price: Long
/**
* Редкость как [ItemRarity]
*/
val rarity: ItemRarity
/**
* Категория предмета, определяет, в какую вкладку инвентаря оно попадает
*/
val category: String?
/**
* Иконка в инвентаре, относительный и абсолютный пути
*/
val inventoryIcon: List<IInventoryIcon>?
interface IInventoryIcon {
val image: SpriteReference
}
data class InventoryIcon(
override val image: SpriteReference
) : IInventoryIcon {
companion object {
val ADAPTER = FactoryAdapter.Builder(InventoryIcon::class, InventoryIcon::image).build().ifString { InventoryIcon(SpriteReference.parse(Starbound.assetFolder(it))) }
}
}
/**
* Теги предмета
*/
val itemTags: List<String>
/**
* При подборе предмета мгновенно заставляет игрока изучить эти рецепты крафта
*/
val learnBlueprintsOnPickup: List<String>
/**
* Максимальное количество предмета в стопке
*/
val maxStack: Long
/**
* snip
*/
val eventCategory: String?
/**
* Заставляет предмет "использовать" сразу же при подборе
*/
val consumeOnPickup: Boolean
/**
* Запускает следующие квест(ы) при подборе
*/
val pickupQuestTemplates: List<String>
/**
* Lua скрипты для выполнения
*/
val scripts: List<String>
/**
* это где либо ещё применяется кроме брони?
*/
val tooltipKind: ItemTooltipKind
/**
* Занимает ли предмет обе руки
*/
val twoHanded: Boolean
/**
* Заставляет SAIL/прочих болтать при подборе предмета в первый раз
*/
val radioMessagesOnPickup: List<String>
/**
* Топливо корабля
*/
val fuelAmount: Long?
}

View File

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

View File

@ -0,0 +1,26 @@
package ru.dbotthepony.kstarbound.defs.item
import ru.dbotthepony.kstarbound.io.json.FactoryAdapter
interface ILeveledStatusEffect {
val levelFunction: String
val stat: String
val baseMultiplier: Double
val amount: Double
}
data class LeveledStatusEffect(
override val levelFunction: String,
override val stat: String,
override val baseMultiplier: Double = 1.0,
override val amount: Double = 0.0,
) : ILeveledStatusEffect {
companion object {
val ADAPTER = FactoryAdapter.Builder(LeveledStatusEffect::class,
LeveledStatusEffect::levelFunction,
LeveledStatusEffect::stat,
LeveledStatusEffect::baseMultiplier,
LeveledStatusEffect::amount,
).build()
}
}

View File

@ -1,354 +1,24 @@
package ru.dbotthepony.kstarbound.defs.item package ru.dbotthepony.kstarbound.defs.item
import com.google.gson.GsonBuilder
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.image.SpriteReference
import ru.dbotthepony.kstarbound.io.json.FactoryAdapter
import ru.dbotthepony.kstarbound.io.json.ListAdapter
import ru.dbotthepony.kstarbound.io.json.asJsonObject
import ru.dbotthepony.kstarbound.io.json.asList
import ru.dbotthepony.kstarbound.io.json.ifString
import ru.dbotthepony.kstarbound.registerTypeAdapter
import ru.dbotthepony.kvector.vector.ndouble.Vector2d
data class ItemDefinition( data class ItemDefinition(
/** override val shortdescription: String,
* Внутреннее имя предмета, как строка override val description: String,
*/ override val itemName: String,
val itemName: String, override val price: Long,
override val rarity: ItemRarity,
override val category: String?,
override val inventoryIcon: List<IItemDefinition.IInventoryIcon>?,
override val itemTags: List<String>,
override val learnBlueprintsOnPickup: List<String>,
override val maxStack: Long,
override val eventCategory: String?,
override val consumeOnPickup: Boolean,
override val pickupQuestTemplates: List<String>,
override val scripts: List<String>,
override val tooltipKind: ItemTooltipKind,
override val twoHanded: Boolean,
override val radioMessagesOnPickup: List<String>,
override val fuelAmount: Long?,
/** val json: Map<String, Any>
* Цена в пикселях ) : IItemDefinition
*/
val price: Long = 0L,
/**
* Редкость как [ItemRarity]
*/
val rarity: ItemRarity = ItemRarity.COMMON,
/**
* Категория предмета, определяет, в какую вкладку инвентаря оно попадает
*/
val category: String? = null,
/**
* Иконка в инвентаре, относительный и абсолютный пути
*/
val inventoryIcon: List<InventoryIcon>? = null,
/**
* Описание предмета
*/
val description: String = "...",
/**
* Название предмета
*/
val shortdescription: String = "...",
/**
* Теги предмета
*/
val itemTags: List<String> = listOf(),
/**
* При подборе предмета мгновенно заставляет игрока изучить эти рецепты крафта
*/
val learnBlueprintsOnPickup: List<String> = listOf(),
/**
* Максимальное количество предмета в стопке, по умолчанию 9999
*/
val maxStack: Long = 9999L,
/**
* snip
*/
val eventCategory: String? = null,
/**
* Заставляет предмет "использовать" сразу же при подборе
*/
val consumeOnPickup: Boolean = false,
/**
* Запускает следующие квест(ы) при подборе
*/
val pickupQuestTemplates: List<String> = listOf(),
/**
* Используется в костях-ископаемых
*/
val race: String? = null,
val displayImage: String? = null,
val displayoffset: Vector2d? = null,
/**
* Используется в костях-ископаемых
*/
val fossilSetName: String? = null,
/**
* Используется в костях-ископаемых
*/
val setIndex: Int? = null,
/**
* Используется в костях-ископаемых
*/
val setCount: Int? = null,
/**
* Используется в костях-ископаемых
*/
val setCollectables: Map<String, String>? = null,
/**
* Используется в костях-ископаемых
*/
val completeFossilIcon: String? = null,
/**
* Используется в костях-ископаемых
*/
val completeFossilObject: String? = null,
/**
* Используется в костях-ископаемых
*/
val completeSetDescriptions: FossilSetDescription? = null,
/**
* Заставляет SAIL/прочих болтать при подборе предмета в первый раз
*/
val radioMessagesOnPickup: List<String> = listOf(),
/**
* Топливо корабля
*/
val fuelAmount: Long? = null,
// ----------------
// Поля ниже были видны только в файлах валюты
// ----------------
/**
* Звуки при поднятии "малого" количества предметов. Не имеет никакого смысла без [smallStackLimit]
*/
val pickupSoundsSmall: List<String> = listOf(),
/**
* Звуки при поднятии "среднего" количества предметов. Не имеет никакого смысла без [mediumStackLimit]
*/
val pickupSoundsMedium: List<String> = listOf(),
/**
* Звуки при поднятии "большого" количества предметов. Не имеет никакого смысла без [smallStackLimit] и без [mediumStackLimit]
*/
val pickupSoundsLarge: List<String> = listOf(),
/**
* Количество предметов ниже или равному данному значению проиграет звук [pickupSoundsSmall]
*/
val smallStackLimit: Long? = null,
/**
* Количество предметов ниже или равному данному значению (но не меньше [smallStackLimit]) проиграет звук [pickupSoundsMedium]
*/
val mediumStackLimit: Long? = null,
/**
* Превращает предмет в валюту
*/
val currency: String? = null,
/**
* Ценность в [currency]
*/
val value: Long? = null,
// ----------------
// /Валюта
// ----------------
/**
* Lua скрипты для выполнения
*/
val scripts: List<String> = listOf(),
val animationScripts: List<String> = listOf(),
// ----------------
// Броня
// ----------------
/**
* это где либо ещё применяется кроме брони?
*/
val tooltipKind: String? = null,
/**
* Изначальный уровень, может быть изменён позднее чем угодно
*/
val level: Int? = null,
/**
* Эффекты предмета, растущие с уровнем
*/
val leveledStatusEffects: List<StatusEffect> = listOf(),
/**
* Варианты покраски (???)
*/
val colorOptions: List<Map<String, String>> = listOf(),
/**
* Визуальные кадры анимации, когда надето на гуманоида мужского пола
*/
val maleFrames: ArmorFrames? = null,
/**
* Визуальные кадры анимации, когда надето на гуманоида женского пола
*/
val femaleFrames: ArmorFrames? = null,
// ----------------
// /Броня
// ----------------
// ----------------
// activeitem
// ----------------
// TODO: это указатель на структуру
val animation: String? = null,
/**
* Занимает ли предмет обе руки
*/
val twoHanded: Boolean = false,
// ----------------
// /activeitem
// ----------------
/**
* Прототип данного предмета, как JSON структура
*
* Имеет смысл только для Lua скриптов
*/
val json: Map<String, Any>,
) {
data class FossilSetDescription(
val price: Long = 0L,
val shortdescription: String = "...",
val description: String = "..."
)
data class ArmorFrames(
val body: String,
val backSleeve: String?,
val frontSleeve: String?,
)
data class StatusEffect(
val levelFunction: String,
val stat: String,
val baseMultiplier: Double = 1.0,
val amount: Double = 0.0,
)
data class InventoryIcon(
val image: SpriteReference
)
companion object {
val INVENTORY_ICON_ADAPTER = FactoryAdapter.Builder(InventoryIcon::class)
.auto(InventoryIcon::image)
.build()
val ADAPTER = FactoryAdapter.Builder(ItemDefinition::class)
.auto(ItemDefinition::itemName)
.auto(ItemDefinition::price)
.auto(ItemDefinition::rarity)
.auto(ItemDefinition::category)
.add(ItemDefinition::inventoryIcon, ListAdapter(INVENTORY_ICON_ADAPTER).ifString { listOf(InventoryIcon(SpriteReference.parse(Starbound.readingFolderTransformer(it)))) }.nullSafe())
.auto(ItemDefinition::description)
.auto(ItemDefinition::shortdescription)
.autoList(ItemDefinition::itemTags)
.autoList(ItemDefinition::learnBlueprintsOnPickup)
.auto(ItemDefinition::maxStack)
.auto(ItemDefinition::eventCategory)
.auto(ItemDefinition::consumeOnPickup)
.autoList(ItemDefinition::pickupQuestTemplates)
.auto(ItemDefinition::race)
.auto(ItemDefinition::displayImage, transformer = Starbound::readingFolderTransformerNullable)
.auto(ItemDefinition::displayoffset)
.auto(ItemDefinition::fossilSetName)
.auto(ItemDefinition::setIndex)
.auto(ItemDefinition::setCount)
.mapAsObject(ItemDefinition::setCollectables, String::class)
.auto(ItemDefinition::completeFossilIcon)
.auto(ItemDefinition::completeFossilObject)
.auto(ItemDefinition::completeSetDescriptions)
.autoList(ItemDefinition::radioMessagesOnPickup)
.auto(ItemDefinition::fuelAmount)
.autoList(ItemDefinition::pickupSoundsSmall)
.autoList(ItemDefinition::pickupSoundsMedium)
.autoList(ItemDefinition::pickupSoundsLarge)
.auto(ItemDefinition::smallStackLimit)
.auto(ItemDefinition::mediumStackLimit)
.auto(ItemDefinition::currency)
.auto(ItemDefinition::value)
.autoList(ItemDefinition::scripts, transformer = Starbound::readingFolderListTransformer)
.autoList(ItemDefinition::animationScripts, transformer = Starbound::readingFolderListTransformer)
.auto(ItemDefinition::tooltipKind)
.auto(ItemDefinition::level)
.autoList(ItemDefinition::leveledStatusEffects)
.add(ItemDefinition::colorOptions, Starbound.nonnullStringTypeAdapter.asJsonObject().asList())
.auto(ItemDefinition::maleFrames)
.auto(ItemDefinition::femaleFrames)
.auto(ItemDefinition::animation, transformer = Starbound::readingFolderTransformerNullable)
.auto(ItemDefinition::twoHanded)
.storesJson()
.build()
val FOSSIL_ADAPTER = FactoryAdapter.Builder(FossilSetDescription::class)
.auto(FossilSetDescription::price)
.auto(FossilSetDescription::shortdescription)
.auto(FossilSetDescription::description)
.build()
val ARMOR_FRAMES_ADAPTER = FactoryAdapter.Builder(ArmorFrames::class)
.auto(ArmorFrames::body, transformer = Starbound::readingFolderTransformer)
.auto(ArmorFrames::backSleeve, transformer = Starbound::readingFolderTransformerNullable)
.auto(ArmorFrames::frontSleeve, transformer = Starbound::readingFolderTransformerNullable)
.build()
.ifString { ArmorFrames(Starbound.readingFolderTransformer(it), null, null) }
val STATUS_EFFECT_ADAPTER = FactoryAdapter.Builder(StatusEffect::class)
.auto(StatusEffect::levelFunction)
.auto(StatusEffect::stat)
.auto(StatusEffect::baseMultiplier)
.auto(StatusEffect::amount)
.build()
fun registerGson(gsonBuilder: GsonBuilder) {
gsonBuilder.registerTypeAdapter(ADAPTER)
gsonBuilder.registerTypeAdapter(FOSSIL_ADAPTER)
gsonBuilder.registerTypeAdapter(ARMOR_FRAMES_ADAPTER)
gsonBuilder.registerTypeAdapter(STATUS_EFFECT_ADAPTER)
gsonBuilder.registerTypeAdapter(INVENTORY_ICON_ADAPTER)
}
}
}

View File

@ -0,0 +1,87 @@
package ru.dbotthepony.kstarbound.defs.item
import ru.dbotthepony.kstarbound.defs.enrollMap
import ru.dbotthepony.kstarbound.io.json.BuilderAdapter
import ru.dbotthepony.kstarbound.io.json.INativeJsonHolder
import ru.dbotthepony.kstarbound.util.NotNullVar
open class ItemPrototype : IItemDefinition, INativeJsonHolder {
final override var shortdescription: String = "..."
final override var description: String = "..."
final override var itemName: String by NotNullVar()
final override var price: Long = 0L
final override var rarity: ItemRarity = ItemRarity.COMMON
final override var category: String? = null
final override var inventoryIcon: List<IItemDefinition.InventoryIcon>? = null
final override var itemTags: List<String> = listOf()
final override var learnBlueprintsOnPickup: List<String> = listOf()
final override var maxStack: Long = 9999L
final override var eventCategory: String? = null
final override var consumeOnPickup: Boolean = false
final override var pickupQuestTemplates: List<String> = listOf()
final override var scripts: List<String> = listOf()
final override var tooltipKind: ItemTooltipKind = ItemTooltipKind.NORMAL
final override var twoHanded: Boolean = false
final override var radioMessagesOnPickup: List<String> = listOf()
final override var fuelAmount: Long? = null
var json: Map<String, Any> = mapOf()
final override fun acceptJson(json: MutableMap<String, Any>) {
this.json = json
}
open fun assemble(): IItemDefinition {
return ItemDefinition(
shortdescription = shortdescription,
description = description,
itemName = itemName,
price = price,
rarity = rarity,
category = category,
inventoryIcon = inventoryIcon,
itemTags = itemTags,
learnBlueprintsOnPickup = learnBlueprintsOnPickup,
maxStack = maxStack,
eventCategory = eventCategory,
consumeOnPickup = consumeOnPickup,
pickupQuestTemplates = pickupQuestTemplates,
scripts = scripts,
tooltipKind = tooltipKind,
twoHanded = twoHanded,
radioMessagesOnPickup = radioMessagesOnPickup,
fuelAmount = fuelAmount,
json = enrollMap(json),
)
}
companion object {
val ADAPTER = BuilderAdapter.Builder(::ItemPrototype)
.also(::addFields)
.build()
fun addFields(builder: BuilderAdapter.Builder<ItemPrototype>) {
with(builder) {
auto(ItemPrototype::shortdescription)
auto(ItemPrototype::description)
auto(ItemPrototype::itemName)
auto(ItemPrototype::price)
auto(ItemPrototype::rarity)
auto(ItemPrototype::category)
autoNullableList(ItemPrototype::inventoryIcon)
autoList(ItemPrototype::itemTags)
autoList(ItemPrototype::learnBlueprintsOnPickup)
auto(ItemPrototype::maxStack)
auto(ItemPrototype::eventCategory)
auto(ItemPrototype::consumeOnPickup)
autoList(ItemPrototype::pickupQuestTemplates)
autoList(ItemPrototype::scripts)
auto(ItemPrototype::tooltipKind)
auto(ItemPrototype::twoHanded)
autoList(ItemPrototype::radioMessagesOnPickup)
auto(ItemPrototype::fuelAmount)
}
}
}
}

View File

@ -3,7 +3,7 @@ package ru.dbotthepony.kstarbound.defs.item
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.google.gson.TypeAdapter import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kstarbound.io.json.CustomEnumTypeAdapter import ru.dbotthepony.kstarbound.io.json.EnumAdapter
import ru.dbotthepony.kstarbound.io.json.IStringSerializable import ru.dbotthepony.kstarbound.io.json.IStringSerializable
import ru.dbotthepony.kstarbound.registerTypeAdapter import ru.dbotthepony.kstarbound.registerTypeAdapter
@ -21,12 +21,4 @@ enum class ItemRarity(val canonical: String) : IStringSerializable {
override fun write(out: JsonWriter) { override fun write(out: JsonWriter) {
out.value(canonical) out.value(canonical)
} }
companion object {
val ADAPTER: TypeAdapter<ItemRarity> = CustomEnumTypeAdapter(values()).nullSafe()
fun registerGson(gsonBuilder: GsonBuilder) {
gsonBuilder.registerTypeAdapter(ADAPTER)
}
}
} }

View File

@ -0,0 +1,43 @@
package ru.dbotthepony.kstarbound.defs.item
enum class ItemTooltipKind {
/**
* Обычные предметы
*/
NORMAL,
/**
* Улучшение для рюкзака
*/
BASE_AUGMENT,
/**
* Рюкзаки
*/
BACK,
/**
* Броня
*/
ARMOR,
/**
* "Руки" меха
*/
MECH_ARM,
/**
* "Ноги" меха
*/
MECH_LEGS,
/**
* Ускорители меха
*/
MECH_BOOSTER,
/**
* Тело меха
*/
MECH_BODY,
}

View File

@ -9,7 +9,7 @@ import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.* import ru.dbotthepony.kstarbound.defs.*
import ru.dbotthepony.kstarbound.defs.image.ImageReference import ru.dbotthepony.kstarbound.defs.image.ImageReference
import ru.dbotthepony.kstarbound.io.json.BuilderAdapter import ru.dbotthepony.kstarbound.io.json.BuilderAdapter
import ru.dbotthepony.kstarbound.io.json.CustomEnumTypeAdapter import ru.dbotthepony.kstarbound.io.json.EnumAdapter
import ru.dbotthepony.kstarbound.registerTypeAdapter import ru.dbotthepony.kstarbound.registerTypeAdapter
import ru.dbotthepony.kstarbound.util.NotNullVar import ru.dbotthepony.kstarbound.util.NotNullVar
import ru.dbotthepony.kvector.vector.Color import ru.dbotthepony.kvector.vector.Color
@ -80,7 +80,7 @@ class ConfigurableProjectile : RawPrototype<ConfigurableProjectile, ConfiguredPr
lightColor = lightColor, lightColor = lightColor,
onlyHitTerrain = onlyHitTerrain, onlyHitTerrain = onlyHitTerrain,
orientationLocked = orientationLocked, orientationLocked = orientationLocked,
image = ImageReference(Starbound.readingFolderTransformer(requireNotNull(image) { "image is null" })), image = ImageReference(Starbound.assetFolder(requireNotNull(image) { "image is null" })),
timeToLive = timeToLive, timeToLive = timeToLive,
animationCycle = animationCycle, animationCycle = animationCycle,
bounces = bounces, bounces = bounces,
@ -123,7 +123,7 @@ class ConfigurableProjectile : RawPrototype<ConfigurableProjectile, ConfiguredPr
fun registerGson(gson: GsonBuilder) { fun registerGson(gson: GsonBuilder) {
gson.registerTypeAdapter(ADAPTER) gson.registerTypeAdapter(ADAPTER)
gson.registerTypeAdapter(CustomEnumTypeAdapter(ProjectilePhysics.values()).nullSafe()) gson.registerTypeAdapter(EnumAdapter(ProjectilePhysics::class).neverNull())
gson.registerTypeAdapter(ActionConfig.ADAPTER) gson.registerTypeAdapter(ActionConfig.ADAPTER)
gson.registerTypeAdapter(ActionProjectile.ADAPTER) gson.registerTypeAdapter(ActionProjectile.ADAPTER)
gson.registerTypeAdapter(ActionSound.ADAPTER) gson.registerTypeAdapter(ActionSound.ADAPTER)

View File

@ -3,7 +3,7 @@ package ru.dbotthepony.kstarbound.defs.projectile
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kstarbound.io.json.IStringSerializable import ru.dbotthepony.kstarbound.io.json.IStringSerializable
enum class ProjectilePhysics(private vararg val aliases: String) : IStringSerializable { enum class ProjectilePhysics(vararg aliases: String) : IStringSerializable {
GAS, GAS,
LASER, LASER,
BOOMERANG, BOOMERANG,
@ -99,7 +99,12 @@ enum class ProjectilePhysics(private vararg val aliases: String) : IStringSerial
GRENADE_LOW_BOUNCE("GRENADELOWBOUNCE"), GRENADE_LOW_BOUNCE("GRENADELOWBOUNCE"),
GRENADE_NO_BOUNCE("GRENADENOBOUNCE"); GRENADE_NO_BOUNCE("GRENADENOBOUNCE");
private val aliases = Array(aliases.size) { aliases[it].lowercase() }
override fun match(name: String): Boolean { override fun match(name: String): Boolean {
@Suppress("name_shadowing")
val name = name.lowercase()
for (alias in aliases) for (alias in aliases)
if (name == alias) if (name == alias)
return true return true

View File

@ -17,7 +17,7 @@ data class RenderParameters(
val absoluteTexturePath: String val absoluteTexturePath: String
init { init {
val dir = Starbound.readingFolder val dir = Starbound.assetFolder
if (dir == null || texture[0] == '/') { if (dir == null || texture[0] == '/') {
absoluteTexturePath = texture absoluteTexturePath = texture

View File

@ -280,7 +280,7 @@ data class RenderTemplate(
gsonBuilder.registerTypeAdapter(ADAPTER) gsonBuilder.registerTypeAdapter(ADAPTER)
gsonBuilder.registerTypeAdapter(RenderRuleList.Combination::class.java, EnumAdapter(RenderRuleList.Combination::class.java)) gsonBuilder.registerTypeAdapter(EnumAdapter(RenderRuleList.Combination::class.java))
} }
private val cache = ConcurrentHashMap<String, RenderTemplate>() private val cache = ConcurrentHashMap<String, RenderTemplate>()
@ -300,7 +300,7 @@ data class RenderTemplate(
if (path[0] != '/') { if (path[0] != '/') {
// относительный путь // относительный путь
val readingFolder = Starbound.readingFolder ?: throw NullPointerException("Currently read folder is not specified") val readingFolder = Starbound.assetFolder ?: throw NullPointerException("Currently read folder is not specified")
path = "$readingFolder/$path" path = "$readingFolder/$path"
} }

View File

@ -6,8 +6,8 @@ import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.io.ColorTypeAdapter import ru.dbotthepony.kstarbound.io.ColorTypeAdapter
import ru.dbotthepony.kstarbound.io.json.CustomEnumTypeAdapter
import ru.dbotthepony.kstarbound.io.json.BuilderAdapter import ru.dbotthepony.kstarbound.io.json.BuilderAdapter
import ru.dbotthepony.kstarbound.io.json.EnumAdapter
import ru.dbotthepony.kvector.vector.Color import ru.dbotthepony.kvector.vector.Color
import ru.dbotthepony.kvector.vector.ndouble.Vector2d import ru.dbotthepony.kvector.vector.ndouble.Vector2d
import kotlin.properties.ReadWriteProperty import kotlin.properties.ReadWriteProperty
@ -43,7 +43,7 @@ class SkyParameters {
gsonBuilder.registerTypeAdapter(SkyParameters::class.java, ADAPTER) gsonBuilder.registerTypeAdapter(SkyParameters::class.java, ADAPTER)
gsonBuilder.registerTypeAdapter(SkyColoringManifold::class.java, SkyColoringManifold.ADAPTER) gsonBuilder.registerTypeAdapter(SkyColoringManifold::class.java, SkyColoringManifold.ADAPTER)
gsonBuilder.registerTypeAdapter(SkyColoring::class.java, SkyColoring.ADAPTER) gsonBuilder.registerTypeAdapter(SkyColoring::class.java, SkyColoring.ADAPTER)
gsonBuilder.registerTypeAdapter(SkyType::class.java, CustomEnumTypeAdapter(SkyType.values()).nullSafe()) gsonBuilder.registerTypeAdapter(SkyType::class.java, EnumAdapter(SkyType::class))
SkySatellite.registerGson(gsonBuilder) SkySatellite.registerGson(gsonBuilder)
} }
} }

View File

@ -3,8 +3,9 @@ package ru.dbotthepony.kstarbound.defs.world.dungeon
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import ru.dbotthepony.kstarbound.defs.world.SkyParameters import ru.dbotthepony.kstarbound.defs.world.SkyParameters
import ru.dbotthepony.kstarbound.defs.world.WorldProperties import ru.dbotthepony.kstarbound.defs.world.WorldProperties
import ru.dbotthepony.kstarbound.io.json.CustomEnumTypeAdapter
import ru.dbotthepony.kstarbound.io.json.BuilderAdapter import ru.dbotthepony.kstarbound.io.json.BuilderAdapter
import ru.dbotthepony.kstarbound.io.json.EnumAdapter
import ru.dbotthepony.kstarbound.registerTypeAdapter
import kotlin.properties.Delegates import kotlin.properties.Delegates
class DungeonWorldDef { class DungeonWorldDef {
@ -50,8 +51,8 @@ class DungeonWorldDef {
fun registerGson(gsonBuilder: GsonBuilder) { fun registerGson(gsonBuilder: GsonBuilder) {
gsonBuilder.registerTypeAdapter(DungeonWorldDef::class.java, ADAPTER) gsonBuilder.registerTypeAdapter(DungeonWorldDef::class.java, ADAPTER)
gsonBuilder.registerTypeAdapter(BeamUpRule::class.java, CustomEnumTypeAdapter(BeamUpRule.values()).nullSafe()) gsonBuilder.registerTypeAdapter(EnumAdapter(BeamUpRule::class))
gsonBuilder.registerTypeAdapter(DungeonType::class.java, CustomEnumTypeAdapter(DungeonType.values()).nullSafe()) gsonBuilder.registerTypeAdapter(EnumAdapter(DungeonType::class))
} }
} }
} }

View File

@ -98,7 +98,7 @@ class BuilderAdapter<T : Any> private constructor(
reader = JsonTreeReader(obj) reader = JsonTreeReader(obj)
if (instance is INativeJsonHolder) { if (instance is INativeJsonHolder) {
instance.acceptJson(flattenJsonElement(obj.asJsonObject, Starbound.assetStringInterner::intern)) instance.acceptJson(flattenJsonElement(obj.asJsonObject, Starbound.STRING_INTERNER::intern))
} else { } else {
instance.acceptJson(obj.asJsonObject) instance.acceptJson(obj.asJsonObject)
} }
@ -121,12 +121,16 @@ class BuilderAdapter<T : Any> private constructor(
val peek = reader.peek() val peek = reader.peek()
if (!property.returnType.isMarkedNullable && peek == JsonToken.NULL) { if (!property.returnType.isMarkedNullable && peek == JsonToken.NULL) {
throw NullPointerException("Property ${property.name} of ${instance::class.qualifiedName} does not accept nulls") throw NullPointerException("Property ${property.name} of ${instance::class.qualifiedName} does not accept nulls (JSON contains null)")
} else if (peek == JsonToken.NULL) { } else if (peek == JsonToken.NULL) {
property.set(instance, null) property.set(instance, null)
reader.nextNull() reader.nextNull()
} else { } else {
val readValue = property.adapter.read(reader) val readValue = property.adapter.read(reader)
if (!property.returnType.isMarkedNullable && readValue == null)
throw JsonSyntaxException("Property ${property.name} of ${instance::class.qualifiedName} does not accept nulls (Type provider returned null)")
property.set(instance, readValue) property.set(instance, readValue)
check(missing.remove(property)) check(missing.remove(property))
} }
@ -248,22 +252,39 @@ class BuilderAdapter<T : Any> private constructor(
return this return this
} }
/**
* Автоматически определяет тип свойства и необходимый [TypeAdapter]
*/
fun <V> auto(property: KMutableProperty1<T, V>, configurator: PropertyConfigurator<T, V>.() -> Unit = {}): Builder<T> { fun <V> auto(property: KMutableProperty1<T, V>, configurator: PropertyConfigurator<T, V>.() -> Unit = {}): Builder<T> {
val returnType = property.returnType val returnType = property.returnType
val classifier = returnType.classifier as? KClass<*> ?: throw ClassCastException("Unable to cast ${returnType.classifier} to KClass of property ${property.name}!") val classifier = returnType.classifier as? KClass<*> ?: throw ClassCastException("Unable to cast ${returnType.classifier} to KClass of property ${property.name}!")
if (classifier.isSubclassOf(List::class)) { if (classifier.isSubclassOf(List::class)) {
throw IllegalArgumentException("${property.name} is a List, please use autoList() method instead") throw IllegalArgumentException("${property.name} is a List, please use autoList() or directly specify type adapter method instead")
} }
if (classifier.isSubclassOf(Map::class)) { if (classifier.isSubclassOf(Map::class)) {
throw IllegalArgumentException("${property.name} is a Map, please use autoMap() method instead") throw IllegalArgumentException("${property.name} is a Map, please use autoMap() or directly specify type adapter method instead")
} }
@Suppress("unchecked_cast") // classifier.java не имеет обозначенного типа @Suppress("unchecked_cast") // classifier.java не имеет обозначенного типа
return add(property, LazyTypeProvider(classifier.java) as TypeAdapter<V>, configurator) return add(property, LazyTypeProvider(classifier.java) as TypeAdapter<V>, configurator)
} }
/**
* Автоматически создаёт [ListAdapter] для заданного свойства
*/
inline fun <reified V : Any> autoList(property: KMutableProperty1<T, List<V>>, noinline configurator: PropertyConfigurator<T, List<V>>.() -> Unit = {}): Builder<T> {
return add(property, ListAdapter(V::class.java), configurator)
}
/**
* Автоматически создаёт [ListAdapter] для заданного свойства, но в данном случае само свойство может принимать значение null
*/
inline fun <reified V : Any> autoNullableList(property: KMutableProperty1<T, List<V>?>, noinline configurator: PropertyConfigurator<T, List<V>?>.() -> Unit = {}): Builder<T> {
return add(property, ListAdapter(V::class.java).nullSafe(), configurator)
}
fun ignoreKey(name: String): Builder<T> { fun ignoreKey(name: String): Builder<T> {
if (properties.any { it.property.name == name }) { if (properties.any { it.property.name == name }) {
throw IllegalArgumentException("Can not ignore key $name because we have property with this name!") throw IllegalArgumentException("Can not ignore key $name because we have property with this name!")

View File

@ -1,31 +0,0 @@
package ru.dbotthepony.kstarbound.io.json
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
interface IStringSerializable {
fun match(name: String): Boolean
fun write(out: JsonWriter)
}
class CustomEnumTypeAdapter<T : Enum<T>>(private val clazz: Array<T>) : TypeAdapter<T>() {
override fun write(out: JsonWriter, value: T) {
if (value is IStringSerializable)
value.write(out)
else
out.value(value.name)
}
override fun read(`in`: JsonReader): T {
val str = `in`.nextString().uppercase()
for (value in clazz) {
if (value is IStringSerializable && value.match(str) || value.name == str) {
return value
}
}
throw IllegalArgumentException("${clazz[0]::class.qualifiedName} does not have value for $str")
}
}

View File

@ -1,40 +1,133 @@
package ru.dbotthepony.kstarbound.io.json package ru.dbotthepony.kstarbound.io.json
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableMap
import com.google.common.collect.Streams
import com.google.gson.JsonSyntaxException import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap
import ru.dbotthepony.kstarbound.set
import java.util.Arrays
import java.util.stream.Stream
import kotlin.reflect.KClass
import kotlin.reflect.full.isSuperclassOf
class EnumAdapter<T : Enum<T>>(private val enum: Class<T>) : TypeAdapter<T>() { interface IStringSerializable {
private val mapping: ImmutableMap<String, T> = Object2ObjectArrayMap<String, T>().let { fun match(name: String): Boolean
for (value in enum.enumConstants) { fun write(out: JsonWriter)
it[value.name] = value }
it[value.name.uppercase()] = value
it[value.name.lowercase()] = value @Suppress("FunctionName")
inline fun <reified T : Enum<T>>EnumAdapter(values: Stream<T> = Arrays.stream(T::class.java.enumConstants), default: T? = null): EnumAdapter<T> {
return EnumAdapter(T::class, values, default)
}
@Suppress("FunctionName")
inline fun <reified T : Enum<T>>EnumAdapter(values: Iterator<T>, default: T? = null): EnumAdapter<T> {
return EnumAdapter(T::class, Streams.stream(values), default)
}
@Suppress("FunctionName")
inline fun <reified T : Enum<T>>EnumAdapter(values: Array<out T>, default: T? = null): EnumAdapter<T> {
return EnumAdapter(T::class, Arrays.stream(values), default)
}
@Suppress("FunctionName")
inline fun <reified T : Enum<T>>EnumAdapter(values: Collection<T>, default: T? = null): EnumAdapter<T> {
return EnumAdapter(T::class, values.stream(), default)
}
@Suppress("name_shadowing")
class EnumAdapter<T : Enum<T>>(private val enum: KClass<T>, values: Stream<T> = Arrays.stream(enum.java.enumConstants), val default: T? = null) : TypeAdapter<T?>() {
constructor(clazz: Class<T>, values: Stream<T> = Arrays.stream(clazz.enumConstants), default: T? = null) : this(clazz.kotlin, values, default)
constructor(clazz: Class<T>, values: Iterator<T>, default: T? = null) : this(clazz.kotlin, Streams.stream(values), default)
constructor(clazz: Class<T>, values: Array<out T>, default: T? = null) : this(clazz.kotlin, Arrays.stream(values), default)
constructor(clazz: Class<T>, values: Collection<T>, default: T? = null) : this(clazz.kotlin, values.stream(), default)
constructor(clazz: KClass<T>, values: Iterator<T>, default: T? = null) : this(clazz, Streams.stream(values), default)
constructor(clazz: KClass<T>, values: Array<out T>, default: T? = null) : this(clazz, Arrays.stream(values), default)
constructor(clazz: KClass<T>, values: Collection<T>, default: T? = null) : this(clazz, values.stream(), default)
private val values = values.collect(ImmutableList.toImmutableList())
private val mapping: ImmutableMap<String, T>
private val areCustom = IStringSerializable::class.isSuperclassOf(enum)
init {
val builder = Object2ObjectArrayMap<String, T>()
for (value in this.values) {
builder[value.name] = value
builder[value.name.uppercase()] = value
builder[value.name.lowercase()] = value
val spaced = value.name.replace('_', ' ') val spaced = value.name.replace('_', ' ')
val stitched = value.name.replace("_", "") val stitched = value.name.replace("_", "")
it[spaced] = value builder[spaced] = value
it[spaced.uppercase()] = value builder[spaced.uppercase()] = value
it[spaced.lowercase()] = value builder[spaced.lowercase()] = value
it[stitched] = value builder[stitched] = value
it[stitched.uppercase()] = value builder[stitched.uppercase()] = value
it[stitched.lowercase()] = value builder[stitched.lowercase()] = value
} }
ImmutableMap.copyOf(it) mapping = ImmutableMap.copyOf(builder)
} }
override fun write(out: JsonWriter, value: T) { override fun write(out: JsonWriter, value: T?) {
out.value(value.name) if (value == null) {
out.nullValue()
} else {
out.value(value.name)
}
} }
override fun read(`in`: JsonReader): T { @Suppress("unchecked_cast")
override fun read(`in`: JsonReader): T? {
if (`in`.peek() == JsonToken.NULL) {
return null
}
val key = `in`.nextString() val key = `in`.nextString()
return mapping[key] ?: throw JsonSyntaxException("Unable to match '$key' against ${enum.canonicalName}")
if (areCustom) {
for (value in values) {
if ((value as IStringSerializable).match(key)) {
return value
}
}
}
return mapping[key] ?: default
}
/**
* Возвращает [EnumAdapter] с тем же типом и набором [T], которому запрещено возвращать или принимать null'ы
*/
fun neverNull(): TypeAdapter<T> {
return object : TypeAdapter<T>() {
override fun write(out: JsonWriter, value: T) {
return this@EnumAdapter.write(out, value)
}
override fun read(`in`: JsonReader): T {
val key = `in`.nextString()
if (areCustom) {
for (value in values) {
if ((value as IStringSerializable).match(key)) {
return value
}
}
}
return mapping[key] ?: default ?: throw JsonSyntaxException("$key is not a valid ${enum.qualifiedName} value")
}
}
} }
} }

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.io.json package ru.dbotthepony.kstarbound.io.json
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonToken
@ -56,3 +57,18 @@ fun <T> TypeAdapter<T>.ifString(reader: (String) -> T): TypeAdapter<T> {
} }
} }
} }
fun <T> TypeAdapter<T?>.neverNull(): TypeAdapter<T> {
return object : TypeAdapter<T>() {
override fun write(out: JsonWriter, value: T) {
this@neverNull.write(out, value)
}
override fun read(`in`: JsonReader): T {
val path = `in`.path
return this@neverNull.read(`in`) ?: throw JsonSyntaxException("Value was null near $path")
}
}
}
fun <T> TypeAdapter<T>.allowNull(): TypeAdapter<T?> = nullSafe()

View File

@ -188,7 +188,7 @@ class FactoryAdapter<T : Any> private constructor(
} }
reader = JsonTreeReader(readArray) reader = JsonTreeReader(readArray)
readValues[readValues.size - 1] = enrollList(flattenJsonElement(readArray) as List<Any>, Starbound.assetStringInterner::intern) readValues[readValues.size - 1] = enrollList(flattenJsonElement(readArray) as List<Any>, Starbound.STRING_INTERNER::intern)
} }
reader.beginArray() reader.beginArray()
@ -233,7 +233,7 @@ class FactoryAdapter<T : Any> private constructor(
} }
reader = JsonTreeReader(readMap) reader = JsonTreeReader(readMap)
readValues[readValues.size - 1] = enrollMap(flattenJsonElement(readMap) as Map<String, Any>, Starbound.assetStringInterner::intern) readValues[readValues.size - 1] = enrollMap(flattenJsonElement(readMap) as Map<String, Any>, Starbound.STRING_INTERNER::intern)
} }
reader.beginObject() reader.beginObject()
@ -357,6 +357,12 @@ class FactoryAdapter<T : Any> private constructor(
* Позволяет построить класс [FactoryAdapter] на основе заданных параметров * Позволяет построить класс [FactoryAdapter] на основе заданных параметров
*/ */
class Builder<T : Any>(val clazz: KClass<T>) { class Builder<T : Any>(val clazz: KClass<T>) {
constructor(clazz: KClass<T>, vararg fields: KProperty1<T, *>) : this(clazz) {
for (field in fields) {
auto(field)
}
}
private val types = ArrayList<PackedProperty<T, *>>() private val types = ArrayList<PackedProperty<T, *>>()
/** /**
@ -418,8 +424,8 @@ class FactoryAdapter<T : Any> private constructor(
* *
* Список неизменяем (создаётся объект [ImmutableList]) * Список неизменяем (создаётся объект [ImmutableList])
*/ */
fun <V : Any> list(field: KProperty1<T, List<V>?>, type: Class<V>, transformer: (List<V>?) -> List<V>? = { it }): Builder<T> { fun <V : Any> list(field: KProperty1<T, List<V>?>, type: Class<V>, transformer: (V) -> V = { it }): Builder<T> {
types.add(PackedProperty(field, ListAdapter(type).nullSafe(), transformer = transformer)) types.add(PackedProperty(field, ListAdapter(type, transformer).nullSafe()))
return this return this
} }
@ -428,8 +434,8 @@ class FactoryAdapter<T : Any> private constructor(
* *
* Список неизменяем (создаётся объект [ImmutableList]) * Список неизменяем (создаётся объект [ImmutableList])
*/ */
inline fun <reified V : Any> autoList(field: KProperty1<T, List<V>?>, noinline transformer: (List<V>?) -> List<V>? = { it }): Builder<T> { inline fun <reified V : Any> autoList(field: KProperty1<T, List<V>?>, noinline transformer: (V) -> V = { it }): Builder<T> {
return add(field, ListAdapter(V::class.java).nullSafe()) return add(field, ListAdapter(V::class.java, transformer).nullSafe())
} }
/** /**

View File

@ -31,7 +31,7 @@ class String2ObjectAdapter<T>(val adapter: TypeAdapter<T>, val valueTransformer:
reader.beginObject() reader.beginObject()
while (reader.peek() != JsonToken.END_OBJECT) { while (reader.peek() != JsonToken.END_OBJECT) {
builder.put(Starbound.assetStringInterner.intern(reader.nextName()), valueTransformer(adapter.read(reader))) builder.put(Starbound.STRING_INTERNER.intern(reader.nextName()), valueTransformer(adapter.read(reader)))
} }
reader.endObject() reader.endObject()

View File

@ -5,10 +5,11 @@ import ru.dbotthepony.kbox2d.api.FixtureDef
import ru.dbotthepony.kbox2d.api.Manifold import ru.dbotthepony.kbox2d.api.Manifold
import ru.dbotthepony.kbox2d.collision.shapes.PolygonShape import ru.dbotthepony.kbox2d.collision.shapes.PolygonShape
import ru.dbotthepony.kbox2d.dynamics.contact.AbstractContact import ru.dbotthepony.kbox2d.dynamics.contact.AbstractContact
import ru.dbotthepony.kstarbound.defs.item.IItemDefinition
import ru.dbotthepony.kstarbound.defs.item.ItemDefinition import ru.dbotthepony.kstarbound.defs.item.ItemDefinition
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
class ItemEntity(world: World<*, *>, val def: ItemDefinition) : Entity(world) { class ItemEntity(world: World<*, *>, val def: IItemDefinition) : Entity(world) {
override val movement = object : MovementController<ItemEntity>(this) { override val movement = object : MovementController<ItemEntity>(this) {
override fun beginContact(contact: AbstractContact) { override fun beginContact(contact: AbstractContact) {
// тут надо код подбора предмета игроком, если мы начинаем коллизию с окружностью подбора // тут надо код подбора предмета игроком, если мы начинаем коллизию с окружностью подбора