Skip to content

Making Your Own SerializableInventory

CmdrNorthpaw edited this page Apr 15, 2021 · 5 revisions

KInventory provides a few default implementations for Minecraft's more useful inventories, but sometimes you need to make your own inventories, because of course I can't be expected to prepare for every use case. To that end, I've provided some tools to make your own SerializableInventorys so that you can serialize your own inventories, or perhaps some of Minecraft's that I've overlooked (if that's the case, please make a pull request!). This guide will show you how to do that.

Making the Inventory

For this guide, let's assume that you want to serialize an inventory called FurnaceInventory that looks something like this:

class FurnaceInventory(
  val input: ItemStack,
  val output: ItemStack,
  val fuel: ItemStack,
) : Inventory {
  /*...*/
}

The class may have other variables, but since the ones in the constructor are the ones that are needed to make the inventory, let's focus on them.

So now that we have a class to serialize, let's do just that: serialize it! We're going to create a serializable version of FurnaceInventory that extends SerializableInventory like so:

@Serializable
class SerializableFurnaceInventory (
  val input: SerializableItemStack,
  val output: SeriailizableItemStack,
  val fuel: SerializableItemStack
) : SerializableInventory<FurnaceInventory>(listOf(input, output, fuel))

Here, we have serializable versions of the input, output, and fuel parameters from FurnaceInventory. We're also extending SerializableInventory and giving it FurnaceInventory for the type parameter (remember, T is the non-serializable version of the inventory that the serializable version needs to be able to convert to). We then concatenate input, output and fuel into a list before passing it to SerializableInventory`'s constructor.

To make the mod compile, we also need to make sure that the type argument is serializable. However, when working with inventories, you generally don't need to serialize the type argument itself, you just need to build an object of that type from serializable data, and Kotlinx.serialization doesn't recognise this yet (though there is an open issue on the subject on the GitHub), so as a workaround I've created a dummy serializer called NotSerializable. To the compiler, NotSerializable looks like it should work as a serializer, but if you actually try to use it it will throw an error.

Annotate the type variable like so:

@Serializable
class SerializableFurnaceInventory (
  val input: SerializableItemStack,
  val output: SeriailizableItemStack,
  val fuel: SerializableItemStack
) : SerializableInventory<@kotlinx.serialization.Serializable(NotSerializable::class) FurnaceInventory>(listOf(input, output, fuel))

Now, if you remember from the page about how KInventory is structured (go read that if you haven't already), every SerializableInventory needs to implement the abstract function toInventory(), which will convert the SerializableInventory to T. Let's implement that function now.

override fun toInventory() {
  return FurnaceInventory(input.toItemStack(), output.toItemStack(), fuel.toItemStack())
}

And there we go! That's all you need to create your very own SerializableInventory. If you would like to use KInventory's tools to create a serializer for KInventory, though, then keep reading.

Making the serializer

This step is optional, but if you'd like to be able to serialize your inventories without needing to convert them to serializable form first, you're going to need to do a bit of extra work. You'll need two things a SurrogateInventorySerializer and a SerializableInventoryCompanion

SerializableInventoryCompanion

SerializableInventoryCompanion is an interface with a method to create a SerializableInventory from an Inventory. It has two type parameters, I and S. I is the inventory that should be converted, and S is I's serializable counterpart. The interface has one function: getSerializable(from: I): S, which, as mentioned earlier, creates a SerializableInventory from an Inventory.

While you can of course implement this anywhere you want, the recommended place to put it is on the companion object of a SerializableInventory. For instance, in our furnace example:

class SerializableFurnaceInventory(/*...*/) {
  companion object : SerializableInventoryCompanion<FurnaceInventory, SerializableFurnaceInventory> {
    override fun getSerializable(from: FurnaceInventory): SerializableFurnaceInventory {
      return SerializableFurnaceInventory(from.input.serializable(), from.output.serializable(), from.fuel.serializable())
    }
  }
}   

Now that you have a companion for your serializable inventory, it's time for the main event: let's make a serializer.

SurrogateInventorySerializer

KInventory's automatic serializer creation works on the principle of surrogate serialization. Kotlin's wiki page explains it a lot better than I can, but in essence a surrogate serializer is one where a class is converted into its serializable form automatically by its serializer, rather than by the user. It then serializes it using the serializer which is automatically created by the @Serializable annotation on the serializable version of the class.

To make your own serializer, you should get an instance of SurrogateInventorySerializer that relates to your SerializableInventory. While you can just construct the class normally, I recommend making an object extend it so that you can easily reference it instead of having to make a new instance each time. The class is open for that purpose.

object FurnaceInventorySerializer : SurrogateInventorySerializer<I, S>(/*...*/)

Like SerializableInventoryCompanion, SurrogateInventorySerializer has two type parameters, I and S, which refer to an inventory and said inventory's serializable counterpart respectively. So for the furnace, we should use:

object FurnaceInventorySerializer : SurrogateInventorySerializer<FurnaceInventory, SerializableFurnaceInventory>(/*...*/)

The serializer also has two constructor parameters.

  • surrogate is the KSerializer of S. While it is possible to get a serializer from a type argument, it's not really a good idea and it's quite inflexible as well. So, the user needs to pass in the serializer by calling the S.serializer() function (the code can't call it because it's an extension function).
  • surrogateCompanion, is the SerializableInventoryCompanion of I and S. If, as I suggested, you implemented it on the companion object of S, then you'll just be able to pass in S.Companion`.

Putting all that together, we get this object:

object FurnaceInventorySerializer : SurrogateInventorySerializer<FurnaceInventory, SerializableFurnaceInventory>(
  SerializableFurnaceInventory.serializer(), SerializableFurnaceInventory.Companion
)

Type variable serializing

Now that you have a custom serializer for the inventory type you're serializing, you may as well annotate it's type argument in SerializableInventory with your new serializer, just in case someone actually tries to serialize it:

@Serializable
class SerializableFurnaceInventory (
  val input: SerializableItemStack,
  val output: SeriailizableItemStack,
  val fuel: SerializableItemStack
) : SerializableInventory<@kotlinx.serialization.Serializable(FurnaceInventorySerializer::class) FurnaceInventory>(listOf(input, output, fuel))

And there we have it! One custom serializable inventory complete with a surrogate serializer. Well done. Now go forth and serialize! (or read the page about common errors).