Custom data stripped when serializing an ItemStack

I’m trying to make a crates plugin that I made be able to interface with a ‘command vouchers’ plugin. The core of both plugins is usable on their own, but I found that when trying to serialize custom data into the file the crates will be stored in, the custom data gets stripped. leaving me with a useless item when it comes out the other end.

DataManipulator:

    package io.github.redrield.commandvouchers.data

    import org.spongepowered.api.Sponge
    import org.spongepowered.api.data.DataContainer
    import org.spongepowered.api.data.DataHolder
    import org.spongepowered.api.data.DataView
    import org.spongepowered.api.data.manipulator.DataManipulatorBuilder
    import org.spongepowered.api.data.manipulator.immutable.common.AbstractImmutableSingleData
    import org.spongepowered.api.data.manipulator.mutable.common.AbstractSingleData
    import org.spongepowered.api.data.merge.MergeFunction
    import org.spongepowered.api.data.persistence.AbstractDataBuilder
    import org.spongepowered.api.data.value.immutable.ImmutableValue
    import org.spongepowered.api.data.value.mutable.Value
    import java.util.Optional

    class VoucherData(command: String) : AbstractSingleData<String, VoucherData, VoucherData.Immutable>(command, VoucherKeys.VOUCHER_COMMAND){
    override fun copy(): VoucherData = VoucherData(value)

    override fun compareTo(other: VoucherData?): Int = 0

    override fun getContentVersion(): Int = 1

    override fun fill(dataHolder: DataHolder, overlap: MergeFunction): Optional<VoucherData> {
        return Optional.of(overlap.merge(this, dataHolder.get(VoucherData::class.java).orElse(null)))
    }

    override fun from(container: DataContainer): Optional<VoucherData> {
        this.value = container.getString(VoucherKeys.VOUCHER_COMMAND.query).orElse(null) ?: return Optional.empty<VoucherData>()
        return Optional.of(this)
    }

    override fun getValueGetter(): Value<*> = Sponge.getRegistry().valueFactory.createValue(VoucherKeys.VOUCHER_COMMAND, value)

    override fun asImmutable(): Immutable = Immutable(value)

    override fun toContainer(): DataContainer = super.toContainer().set(VoucherKeys.VOUCHER_COMMAND, value)

    class Immutable(command: String) : AbstractImmutableSingleData<String, Immutable, VoucherData>(command, VoucherKeys.VOUCHER_COMMAND) {
        override fun getValueGetter(): ImmutableValue<*> = Sponge.getRegistry().valueFactory.createValue(VoucherKeys.VOUCHER_COMMAND, value).asImmutable()

        override fun compareTo(other: Immutable?): Int = 0

        override fun getContentVersion(): Int = 1

        override fun asMutable(): VoucherData = VoucherData(value)

    }

    class Builder : AbstractDataBuilder<VoucherData>(VoucherData::class.java, 1), DataManipulatorBuilder<VoucherData, Immutable> {
        override fun create(): VoucherData = VoucherData("")

        override fun createFrom(dataHolder: DataHolder) = VoucherData("").fill(dataHolder)

        override fun buildContent(container: DataView): Optional<VoucherData> {
            val data = VoucherData("")
            container.getString(VoucherKeys.VOUCHER_COMMAND.query).ifPresent {
                data.value = it
            }
            return Optional.of(data)
        }
    }
}

Reward class:

package io.github.redrield.spongycrates.data

import ninja.leaping.configurate.objectmapping.Setting
import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable
import org.spongepowered.api.item.ItemTypes
import org.spongepowered.api.item.inventory.ItemStack

@ConfigSerializable data class Reward(@Setting("reward") val stack: ItemStack,
                                  @Setting("chance") var percent: Int) {

    constructor(): this(ItemStack.of(ItemTypes.APPLE, 1), 0)
}

Where the data is serialized:

package io.github.redrield.spongycrates.commands

import com.google.common.collect.Lists
import com.google.common.reflect.TypeToken
import io.github.redrield.spongycrates.SpongyCrates
import io.github.redrield.spongycrates.data.Reward
import org.spongepowered.api.command.CommandException
import org.spongepowered.api.command.CommandResult
import org.spongepowered.api.command.CommandSource
import org.spongepowered.api.command.args.CommandContext
import org.spongepowered.api.command.spec.CommandExecutor
import org.spongepowered.api.entity.living.player.Player
import org.spongepowered.api.text.Text
import org.spongepowered.api.text.format.TextColors
import java.util.ArrayList

class AddDropCommand(private val plugin: SpongyCrates) : CommandExecutor {

    @Throws(CommandException::class)
    override fun execute(src: CommandSource, args: CommandContext): CommandResult {
        if(src !is Player) {
            throw CommandException(Text.of(TextColors.RED, "You must be a player to use this command!"))
        }
        val name = args.getOne<String>("name").get()
        val percent = args.getOne<Int>("percent").get()
        if(!src.itemInHand.isPresent) {
            throw CommandException(Text.of(TextColors.RED, "You need to be holding an item in your hand to use this command!"))
        }
        if(plugin.crates.getNode(name).isVirtual) {
            throw CommandException(Text.of(TextColors.RED, "The crate you have specified does not exist!"))
        }
        val reward = Reward(src.itemInHand.get(), percent)
        val rewards = Lists.newArrayList(plugin.crates.getNode(name, "drops").getList(TypeToken.of(Reward::class.java)))
        rewards.add(reward)
        plugin.crates.getNode(name, "drops").setValue(object : TypeToken<ArrayList<Reward>>() {}, rewards)
        plugin.cratesLoader.save(plugin.crates)
        src.sendMessage(Text.of(TextColors.GREEN, "That drop has been added!"))
        return CommandResult.success()
    }

}

(Ignore terrible formatting on the first paste)

1 Like

Interesting… why are you storing in a TypeToken<ArrayList<Reward>> instead of a TypeToken<List<Reward>>?

Lists.newArrayList(...)

Have you registered a DataBuilder for your Reward class? @ConfigSerializable on does the serialization, it doesn’t deal with the deserialization.

I haven’t, I’ll get that done when I have an ide and some time. But it seems to me that the data doesn’t even get serialized. When looking in the configuration file, I couldnt find my custom data.

Oh, I see - my mistake. Um…

I was under the impression that @ConfigSerializable did both directions. The JD and Configurate wiki say so.

It definitley does it for both ways.

Note that Data API does NOT use ConfigSerializable annotations in any way shape or form as it is a separate system. Here is an example plugin that works 100% for the use case of storing data onto an itemstack and verifying that it exists on the item stack and persists between restarts (note there are comments that should be read at the bottom of the gist as it was written with haste).

The data gets stored onto an item just fine. It’s just this edge case. Should I replace ConfigSerializable references with one of the other methods of object serialization?

Ah ha, yes you’ve found it @gabizou

Without implementing DataSerializable Sponge will be able to add the data to the item, but as soon as it tries to serialize it will not working as sponge doesn’t know how to serialize an object that’s not DataSerializable without a DataTranslator.

Change you Reward class to also implement DataSerializable, and override toContainer() so that everything gets saved properly. You’ll also need to use the get/setSerializableX() methods as well to ensure that Sponge tries to deserialize using the correct method.

As mentioned before, you’ll still need to make a DataBuilder to bring that Reward object back after it’s been saved.

oooh, alright. Thanks a ton everyone!

Additionally, unless you’re saving your Reward class directly to a ConfigurationNode, you don’t need to use @ConfigSerializable.

So I have the class now implementing DataSerializable, but I can’t figure out where to get a DataContainer to return? (super.toContainer() isn’t an option because DataSerializable is an interface)

new MemoryDataContainer();
1 Like

I’ve updated the reward class, this is what it looks like

package io.github.redrield.spongycrates.data

import org.spongepowered.api.data.DataContainer
import org.spongepowered.api.data.DataSerializable
import org.spongepowered.api.data.DataView
import org.spongepowered.api.data.MemoryDataContainer
import org.spongepowered.api.data.persistence.AbstractDataBuilder
import org.spongepowered.api.item.inventory.ItemStack
import java.util.Optional

data class Reward(val stack: ItemStack, val percent: Int) : DataSerializable {

    override fun toContainer(): DataContainer {
        return MemoryDataContainer().set(CustomKeys.REWARD_CHANCE, this.percent).set(CustomKeys.REWARD_STACK, this.stack)
    }

    override fun getContentVersion(): Int = 1


    class Builder : AbstractDataBuilder<Reward>(Reward::class.java, 1) {
        override fun buildContent(view: DataView): Optional<Reward> {
            val stack = view.getSerializable(CustomKeys.REWARD_STACK.query, ItemStack::class.java).orElse(null) ?: return Optional.empty()
            val percent = view.getInt(CustomKeys.REWARD_CHANCE.query).orElse(null) ?: return Optional.empty()
            return Optional.of(Reward(stack, percent))
        }
    }
}

And when I make a new ArrayList and add a Reward object into it, it works fine. The problem at this point is that it doesn’t get deserialized correctly, so when I try to retrieve the existing list and add on, the custom data is stripped

Bump, still need some help with this

Can you show the code where you serialize and deserialize? Also, have you registered your builder with DataManager?

The items are originally serialized and put into the config in this class, though they are also deserialized in this listener, I have registered the class as so. Though as I said in the earlier post, when the item is originally serialized, the data is retained. It’s only when the item is deserialized that the data gets removed

Let’s see the Reward class