давайте снова попробуем 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
import com.google.common.collect.ImmutableMap
import com.google.gson.GsonBuilder
import com.google.gson.TypeAdapter
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?) {
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.image.AtlasConfiguration
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.ItemTooltipKind
import ru.dbotthepony.kstarbound.defs.item.LeveledStatusEffect
import ru.dbotthepony.kstarbound.defs.liquid.LiquidDefinition
import ru.dbotthepony.kstarbound.defs.projectile.*
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.json.AABBTypeAdapter
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.Vector2fTypeAdapter
import ru.dbotthepony.kstarbound.io.json.Vector2iTypeAdapter
@ -57,24 +65,24 @@ object Starbound {
/**
* Служит переменной для указания из какой папки происходит чтение asset'а в данном потоке
*/
var readingFolder by ThreadLocal<String>()
var assetFolder by ThreadLocal<String>()
private set
fun readingFolderTransformer(input: String): String {
val readingFolder = readingFolder
require(readingFolder != null) { "Not reading an asset on current thread" }
fun assetFolder(input: String): String {
val assetFolder = assetFolder
require(assetFolder != null) { "Not reading an asset on current thread" }
if (input[0] == '/')
return input
return assetStringInterner.intern("$readingFolder/$input")
return STRING_INTERNER.intern("$assetFolder/$input")
}
fun readingFolderTransformerNullable(input: String?): String? {
require(readingFolder != null) { "Not reading an asset on current thread" }
fun assetFolderNullable(input: String?): String? {
require(assetFolder != null) { "Not reading an asset on current thread" }
if (input != null)
return readingFolderTransformer(input)
return assetFolder(input)
return null
}
@ -83,7 +91,7 @@ object Starbound {
if (input == 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>()
@ -99,7 +107,7 @@ object Starbound {
private val parallax = Object2ObjectOpenHashMap<String, ParallaxPrototype>()
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 liquidByIDAccess: Map<Int, LiquidDefinition> = Collections.unmodifiableMap(liquidByID)
@ -110,21 +118,21 @@ object Starbound {
val projectilesAccess: Map<String, ConfiguredProjectile> = Collections.unmodifiableMap(projectiles)
val parallaxAccess: Map<String, ParallaxPrototype> = Collections.unmodifiableMap(parallax)
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) {
out.value(value)
}
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()
.enableComplexMapKeySerialization()
@ -135,7 +143,7 @@ object Starbound {
.registerTypeAdapter(Color::class.java, ColorTypeAdapter.nullSafe())
// чтоб строки всегда intern'ились
.registerTypeAdapter(stringTypeAdapter)
.registerTypeAdapter(NULLABLE_STRING_ADAPTER)
// math
.registerTypeAdapter(AABBTypeAdapter)
@ -156,12 +164,22 @@ object Starbound {
.also(RenderTemplate::registerGson)
.also(TileDefinition::registerGson)
.also(LiquidDefinition::registerGson)
.also(ItemDefinition::registerGson)
.also(ItemRarity::registerGson)
.also(SpriteReference::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()
@ -170,7 +188,7 @@ object Starbound {
return when (type) {
Float::class.java -> TypeAdapters.FLOAT 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>
Long::class.java -> TypeAdapters.LONG as TypeAdapter<T>
Boolean::class.java -> TypeAdapters.BOOLEAN as TypeAdapter<T>
@ -339,14 +357,14 @@ object Starbound {
}
private fun loadTileMaterials(callback: (String) -> Unit) {
readingFolder = "/tiles/materials"
assetFolder = "/tiles/materials"
for (fs in fileSystems) {
for (listedFile in fs.explore().filter { it.isFile }.filter { it.name.endsWith(".material") }) {
try {
callback("Loading $listedFile")
readingFolder = listedFile.computeDirectory()
assetFolder = listedFile.computeDirectory()
val tileDef = gson.fromJson(listedFile.reader(), TileDefinition::class.java)
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) {
@ -373,7 +391,7 @@ object Starbound {
try {
callback("Loading $listedFile")
readingFolder = listedFile.computeDirectory()
assetFolder = 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!" }
projectiles[def.projectileName] = def
@ -388,7 +406,7 @@ object Starbound {
}
}
readingFolder = null
assetFolder = null
}
private fun loadFunctions(callback: (String) -> Unit) {
@ -397,7 +415,7 @@ object Starbound {
try {
callback("Loading $listedFile")
readingFolder = listedFile.computeDirectory()
assetFolder = listedFile.computeDirectory()
val readObject = JsonParser.parseReader(listedFile.reader()) as JsonObject
for (key in readObject.keySet()) {
@ -414,7 +432,7 @@ object Starbound {
}
}
readingFolder = null
assetFolder = null
}
private fun loadParallax(callback: (String) -> Unit) {
@ -423,7 +441,7 @@ object Starbound {
try {
callback("Loading $listedFile")
readingFolder = listedFile.computeDirectory()
assetFolder = listedFile.computeDirectory()
val def = gson.fromJson(listedFile.reader(), ParallaxPrototype::class.java)
parallax[listedFile.name.substringBefore('.')] = def
} catch(err: Throwable) {
@ -436,18 +454,18 @@ object Starbound {
}
}
readingFolder = null
assetFolder = null
}
private fun loadMaterialModifiers(callback: (String) -> Unit) {
readingFolder = "/tiles/materials"
assetFolder = "/tiles/materials"
for (fs in fileSystems) {
for (listedFile in fs.explore().filter { it.isFile }.filter { it.name.endsWith(".matmod") }) {
try {
callback("Loading $listedFile")
readingFolder = listedFile.computeDirectory()
assetFolder = listedFile.computeDirectory()
val tileDef = gson.fromJson(listedFile.reader(), MaterialModifier::class.java)
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) {
@ -474,7 +492,7 @@ object Starbound {
try {
callback("Loading $listedFile")
readingFolder = listedFile.computeDirectory()
assetFolder = listedFile.computeDirectory()
val liquidDef = gson.fromJson(listedFile.reader(), LiquidDefinition::class.java)
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) {
val files = listOf(".item", ".currency", ".head", ".chest", ".legs", ".activeitem")
val files = listOf(".item", ".currency", ".head", ".chest", ".legs", ".back", ".activeitem")
for (fs in fileSystems) {
for (listedFile in fs.explore().filter { it.isFile }.filter { f -> files.any { f.name.endsWith(it) } }) {
try {
callback("Loading $listedFile")
readingFolder = listedFile.computeDirectory()
ItemDefinition.ADAPTER.currentSymbolicName = listedFile.computeFullPath()
val def = gson.fromJson(listedFile.reader(), ItemDefinition::class.java)
assetFolder = listedFile.computeDirectory()
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) {
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
/**
* Возвращает глубокую неизменяемую копию [input] примитивов/List'ов/Map'ов
* Возвращает глубокую, неизменяемую копию [input] примитивов/List'ов/Map'ов
*/
fun enrollList(input: List<Any>, interner: (String) -> String = String::intern): ImmutableList<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> {
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.JsonToken
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.Vector2dTypeAdapter
import ru.dbotthepony.kvector.vector.ndouble.Vector2d
@ -29,7 +29,7 @@ enum class JsonFunctionInterpolation(vararg aliases: String) : IStringSerializab
}
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 {
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 {
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
override fun acceptJson(json: MutableMap<String, Any>) {

View File

@ -304,7 +304,7 @@ class AtlasConfiguration private constructor(
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) {
gsonBuilder.registerTypeAdapter(ADAPTER)

View File

@ -30,7 +30,7 @@ data class ImageReference(
override fun read(`in`: JsonReader): ImageReference {
if (`in`.peek() == JsonToken.STRING) {
val image = Starbound.readingFolderTransformer(`in`.nextString())
val image = Starbound.assetFolder(`in`.nextString())
if (image.contains(':')) {
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 {
return parse(Starbound.readingFolderTransformer(`in`.nextString()))
return parse(Starbound.assetFolder(`in`.nextString()))
}
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
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(
/**
* Внутреннее имя предмета, как строка
*/
val itemName: String,
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?,
/**
* Цена в пикселях
*/
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)
}
}
}
val json: Map<String, Any>
) : IItemDefinition

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.TypeAdapter
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.registerTypeAdapter
@ -21,12 +21,4 @@ enum class ItemRarity(val canonical: String) : IStringSerializable {
override fun write(out: JsonWriter) {
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.image.ImageReference
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.util.NotNullVar
import ru.dbotthepony.kvector.vector.Color
@ -80,7 +80,7 @@ class ConfigurableProjectile : RawPrototype<ConfigurableProjectile, ConfiguredPr
lightColor = lightColor,
onlyHitTerrain = onlyHitTerrain,
orientationLocked = orientationLocked,
image = ImageReference(Starbound.readingFolderTransformer(requireNotNull(image) { "image is null" })),
image = ImageReference(Starbound.assetFolder(requireNotNull(image) { "image is null" })),
timeToLive = timeToLive,
animationCycle = animationCycle,
bounces = bounces,
@ -123,7 +123,7 @@ class ConfigurableProjectile : RawPrototype<ConfigurableProjectile, ConfiguredPr
fun registerGson(gson: GsonBuilder) {
gson.registerTypeAdapter(ADAPTER)
gson.registerTypeAdapter(CustomEnumTypeAdapter(ProjectilePhysics.values()).nullSafe())
gson.registerTypeAdapter(EnumAdapter(ProjectilePhysics::class).neverNull())
gson.registerTypeAdapter(ActionConfig.ADAPTER)
gson.registerTypeAdapter(ActionProjectile.ADAPTER)
gson.registerTypeAdapter(ActionSound.ADAPTER)

View File

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

View File

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

View File

@ -280,7 +280,7 @@ data class RenderTemplate(
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>()
@ -300,7 +300,7 @@ data class RenderTemplate(
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"
}

View File

@ -6,8 +6,8 @@ import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import org.apache.logging.log4j.LogManager
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.EnumAdapter
import ru.dbotthepony.kvector.vector.Color
import ru.dbotthepony.kvector.vector.ndouble.Vector2d
import kotlin.properties.ReadWriteProperty
@ -43,7 +43,7 @@ class SkyParameters {
gsonBuilder.registerTypeAdapter(SkyParameters::class.java, ADAPTER)
gsonBuilder.registerTypeAdapter(SkyColoringManifold::class.java, SkyColoringManifold.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)
}
}

View File

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

View File

@ -98,7 +98,7 @@ class BuilderAdapter<T : Any> private constructor(
reader = JsonTreeReader(obj)
if (instance is INativeJsonHolder) {
instance.acceptJson(flattenJsonElement(obj.asJsonObject, Starbound.assetStringInterner::intern))
instance.acceptJson(flattenJsonElement(obj.asJsonObject, Starbound.STRING_INTERNER::intern))
} else {
instance.acceptJson(obj.asJsonObject)
}
@ -121,12 +121,16 @@ class BuilderAdapter<T : Any> private constructor(
val peek = reader.peek()
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) {
property.set(instance, null)
reader.nextNull()
} else {
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)
check(missing.remove(property))
}
@ -248,22 +252,39 @@ class BuilderAdapter<T : Any> private constructor(
return this
}
/**
* Автоматически определяет тип свойства и необходимый [TypeAdapter]
*/
fun <V> auto(property: KMutableProperty1<T, V>, configurator: PropertyConfigurator<T, V>.() -> Unit = {}): Builder<T> {
val returnType = property.returnType
val classifier = returnType.classifier as? KClass<*> ?: throw ClassCastException("Unable to cast ${returnType.classifier} to KClass of property ${property.name}!")
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)) {
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 не имеет обозначенного типа
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> {
if (properties.any { it.property.name == 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
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.common.collect.Streams
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
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>() {
private val mapping: ImmutableMap<String, T> = Object2ObjectArrayMap<String, T>().let {
for (value in enum.enumConstants) {
it[value.name] = value
it[value.name.uppercase()] = value
it[value.name.lowercase()] = value
interface IStringSerializable {
fun match(name: String): Boolean
fun write(out: JsonWriter)
}
@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 stitched = value.name.replace("_", "")
it[spaced] = value
it[spaced.uppercase()] = value
it[spaced.lowercase()] = value
builder[spaced] = value
builder[spaced.uppercase()] = value
builder[spaced.lowercase()] = value
it[stitched] = value
it[stitched.uppercase()] = value
it[stitched.lowercase()] = value
builder[stitched] = value
builder[stitched.uppercase()] = value
builder[stitched.lowercase()] = value
}
ImmutableMap.copyOf(it)
mapping = ImmutableMap.copyOf(builder)
}
override fun write(out: JsonWriter, value: T) {
out.value(value.name)
override fun write(out: JsonWriter, value: T?) {
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()
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
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
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)
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()
@ -233,7 +233,7 @@ class FactoryAdapter<T : Any> private constructor(
}
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()
@ -357,6 +357,12 @@ class FactoryAdapter<T : Any> private constructor(
* Позволяет построить класс [FactoryAdapter] на основе заданных параметров
*/
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, *>>()
/**
@ -418,8 +424,8 @@ class FactoryAdapter<T : Any> private constructor(
*
* Список неизменяем (создаётся объект [ImmutableList])
*/
fun <V : Any> list(field: KProperty1<T, List<V>?>, type: Class<V>, transformer: (List<V>?) -> List<V>? = { it }): Builder<T> {
types.add(PackedProperty(field, ListAdapter(type).nullSafe(), transformer = transformer))
fun <V : Any> list(field: KProperty1<T, List<V>?>, type: Class<V>, transformer: (V) -> V = { it }): Builder<T> {
types.add(PackedProperty(field, ListAdapter(type, transformer).nullSafe()))
return this
}
@ -428,8 +434,8 @@ class FactoryAdapter<T : Any> private constructor(
*
* Список неизменяем (создаётся объект [ImmutableList])
*/
inline fun <reified V : Any> autoList(field: KProperty1<T, List<V>?>, noinline transformer: (List<V>?) -> List<V>? = { it }): Builder<T> {
return add(field, ListAdapter(V::class.java).nullSafe())
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, transformer).nullSafe())
}
/**

View File

@ -31,7 +31,7 @@ class String2ObjectAdapter<T>(val adapter: TypeAdapter<T>, val valueTransformer:
reader.beginObject()
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()

View File

@ -5,10 +5,11 @@ import ru.dbotthepony.kbox2d.api.FixtureDef
import ru.dbotthepony.kbox2d.api.Manifold
import ru.dbotthepony.kbox2d.collision.shapes.PolygonShape
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.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 fun beginContact(contact: AbstractContact) {
// тут надо код подбора предмета игроком, если мы начинаем коллизию с окружностью подбора