Configurate & Polymorphism

Hello! I’m working on a Quest plugin, where my aim is to serialize an object of type Quest, and potentially store this directly on the player using Custom Data. Either that, or I’ll just shove it into a database of my own. Either way, the point is that Players are associated with a number of quests, each of which is meant to be fully serializable via Configurate.

I’m going to go through some details of my Quest plugin I am working on, in the hopes that I can get some better help or advice.

Now, I’m not going to detail literally every part of it since that would make this post impractically large, so instead I’ll just focus on 1 element I’m trying to work with.

To start off with, a Quest looks something like this:

image

I’m going to focus on the class structure behind Objectives, but it should be noted that Requirements and Rewards have similar structures. It is as follows:

image

Obviously, I’m omitting some methods ( like the getters and setters ) in the diagrams, they’re for visualization purposes only, not really meant for accuracy.

It should also be noted that I have annotated both the interface and the implementing classes as @ConfigSerializable and all the relevant fields have been annotated with @Setting as well. They have empty constructors available as well.

So, with all that being said, this is what the output from the following code looks like:

Quest dummyQuest;

//initi dummyQuest...

GsonConfigurationLoader loader = GsonConfigurationLoader.builder().build();
ConfigurationNode node = loader.createEmptyNode( ConfigurationOptions.defaults() );
node.setValue(TypeToken.of(Quest.class), dummyQuest);

//purely demonstrational
loader.saveInternal(node, System.console().writer());

Output:

{  
   "requirements":[],
   "name":{  
      "text":"This is a dummy quest."
   },
   "description":{  
      "text":"The purpose of this quest is to demonstrate that quests work. So uhh.. kill 3 unnamed creepers and 4 unnamed zombies. Also speak to the king at the end there. You'll get a magical anvil at the end for it."
   },
   "objectives":[  
      {  
         "__class__":"com.atherys.quests.quest.objective.Objective",
         "entityName":"creeper",
         "progress":3,
         "started":false,
         "requirement":3,
         "complete":false
      },
      {  
         "__class__":"com.atherys.quests.quest.objective.Objective",
         "entityName":"zombie",
         "progress":4,
         "started":false,
         "requirement":4,
         "complete":false
      },
      {  
         "__class__":"com.atherys.quests.quest.objective.Objective",
         "requiredDialogTree":"theKingSpeech",
         "description":{  
            "text":"Speak to the king."
         },
         "requiredDialogNode":14,
         "started":false,
         "complete":false
      }
   ],
   "started":false,
   "id":"dummyQuest",
   "complete":false,
   "version":1,
   "rewards":[  
      {  
         "__class__":"com.atherys.quests.quest.reward.Reward",
         "item":{  
            "ContentVersion":1,
            "ItemType":"minecraft:anvil",
            "Count":1,
            "UnsafeDamage":0,
            "UnsafeData":{  
               "display":{  
                  "Name":"The Magical Anvil"
               }
            }
         }
      }
   ]
}

Must say, that worked better than I thought it would at first. Everything was serialized correctly, with one notable exception. For Objectives ( and in fact, this is also the case for Rewards and Requirements ), the "__class__" field contains the name of the interface, rather than the implementing class.

When trying to deserialize the above Json, Configurate throws an exception with the following error:

[12:19:54] [Server thread/ERROR] [Sponge]: Could not pass FMLServerStartedEvent to Plugin{id=atherysquests, name=A'therys Quests, version=1.0.0b, description=A quest plugin written for the A'therys Horizons server., source=/home/container/mods/AtherysQuests.jar}
com.google.common.util.concurrent.UncheckedExecutionException: java.lang.NullPointerException
at com.google.common.cache.LocalCache$Segment.get(LocalCache.java:2217) ~[minecraft_server.1.12.2.jar:?]
at com.google.common.cache.LocalCache.get(LocalCache.java:4154) ~[minecraft_server.1.12.2.jar:?]
at com.google.common.cache.LocalCache.getOrLoad(LocalCache.java:4158) ~[minecraft_server.1.12.2.jar:?]
at com.google.common.cache.LocalCache$LocalLoadingCache.get(LocalCache.java:5147) ~[minecraft_server.1.12.2.jar:?]
at ninja.leaping.configurate.objectmapping.DefaultObjectMapperFactory.getMapper(DefaultObjectMapperFactory.java:44) ~[spongeforge-1.12.2-2586-7.1.0-BETA-2887.jar:1.12.2-2586-7.1.0-BETA-2887]
at ninja.leaping.configurate.objectmapping.serialize.TypeSerializers$AnnotatedObjectSerializer.deserialize(TypeSerializers.java:258) ~[spongeforge-1.12.2-2586-7.1.0-BETA-2887.jar:1.12.2-2586-7.1.0-BETA-2887]
at ninja.leaping.configurate.objectmapping.serialize.TypeSerializers$ListSerializer.deserialize(TypeSerializers.java:225) ~[spongeforge-1.12.2-2586-7.1.0-BETA-2887.jar:1.12.2-2586-7.1.0-BETA-2887]
at ninja.leaping.configurate.objectmapping.serialize.TypeSerializers$ListSerializer.deserialize(TypeSerializers.java:208) ~[spongeforge-1.12.2-2586-7.1.0-BETA-2887.jar:1.12.2-2586-7.1.0-BETA-2887]
at ninja.leaping.configurate.objectmapping.ObjectMapper$FieldData.deserializeFrom(ObjectMapper.java:85) ~[spongeforge-1.12.2-2586-7.1.0-BETA-2887.jar:1.12.2-2586-7.1.0-BETA-2887]
at ninja.leaping.configurate.objectmapping.ObjectMapper$BoundInstance.populate(ObjectMapper.java:148) ~[spongeforge-1.12.2-2586-7.1.0-BETA-2887.jar:1.12.2-2586-7.1.0-BETA-2887]
at ninja.leaping.configurate.objectmapping.serialize.TypeSerializers$AnnotatedObjectSerializer.deserialize(TypeSerializers.java:258) ~[spongeforge-1.12.2-2586-7.1.0-BETA-2887.jar:1.12.2-2586-7.1.0-BETA-2887]
at ninja.leaping.configurate.SimpleConfigurationNode.getValue(SimpleConfigurationNode.java:226) ~[spongeforge-1.12.2-2586-7.1.0-BETA-2887.jar:1.12.2-2586-7.1.0-BETA-2887]
at ninja.leaping.configurate.ConfigurationNode.getValue(ConfigurationNode.java:356) ~[spongeforge-1.12.2-2586-7.1.0-BETA-2887.jar:1.12.2-2586-7.1.0-BETA-2887]
at com.atherys.quests.AtherysQuests.start(AtherysQuests.java:98) ~[AtherysQuests.class:?]
at com.atherys.quests.AtherysQuests.onStart(AtherysQuests.java:133) ~[AtherysQuests.class:?]
at org.spongepowered.common.event.listener.GameStartedServerEventListener_AtherysQuests_onStart9.handle(Unknown Source) ~[?:?]
at org.spongepowered.common.event.RegisteredListener.handle(RegisteredListener.java:95) ~[RegisteredListener.class:1.12.2-2586-7.1.0-BETA-2887]
at org.spongepowered.mod.event.SpongeModEventManager.post(SpongeModEventManager.java:338) [SpongeModEventManager.class:1.12.2-2586-7.1.0-BETA-2887]
at org.spongepowered.mod.event.SpongeModEventManager.post(SpongeModEventManager.java:371) [SpongeModEventManager.class:1.12.2-2586-7.1.0-BETA-2887]
at org.spongepowered.common.SpongeImpl.postEvent(SpongeImpl.java:213) [SpongeImpl.class:1.12.2-2586-7.1.0-BETA-2887]
at org.spongepowered.mod.SpongeMod.onStateEvent(SpongeMod.java:232) [SpongeMod.class:1.12.2-2586-7.1.0-BETA-2887]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_151]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_151]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_151]
at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_151]
at com.google.common.eventbus.Subscriber.invokeSubscriberMethod(Subscriber.java:91) [minecraft_server.1.12.2.jar:?]
at com.google.common.eventbus.Subscriber$SynchronizedSubscriber.invokeSubscriberMethod(Subscriber.java:150) [minecraft_server.1.12.2.jar:?]
at com.google.common.eventbus.Subscriber$1.run(Subscriber.java:76) [minecraft_server.1.12.2.jar:?]
at com.google.common.util.concurrent.MoreExecutors$DirectExecutor.execute(MoreExecutors.java:399) [minecraft_server.1.12.2.jar:?]
at com.google.common.eventbus.Subscriber.dispatchEvent(Subscriber.java:71) [minecraft_server.1.12.2.jar:?]
at com.google.common.eventbus.Dispatcher$PerThreadQueuedDispatcher.dispatch(Dispatcher.java:116) [minecraft_server.1.12.2.jar:?]
at com.google.common.eventbus.EventBus.post(EventBus.java:217) [minecraft_server.1.12.2.jar:?]
at net.minecraftforge.fml.common.LoadController.sendEventToModContainer(LoadController.java:253) [LoadController.class:?]
at net.minecraftforge.fml.common.LoadController.propogateStateMessage(LoadController.java:231) [LoadController.class:?]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_151]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_151]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_151]
at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_151]
at com.google.common.eventbus.Subscriber.invokeSubscriberMethod(Subscriber.java:91) [minecraft_server.1.12.2.jar:?]
at com.google.common.eventbus.Subscriber$SynchronizedSubscriber.invokeSubscriberMethod(Subscriber.java:150) [minecraft_server.1.12.2.jar:?]
at com.google.common.eventbus.Subscriber$1.run(Subscriber.java:76) [minecraft_server.1.12.2.jar:?]
at com.google.common.util.concurrent.MoreExecutors$DirectExecutor.execute(MoreExecutors.java:399) [minecraft_server.1.12.2.jar:?]
at com.google.common.eventbus.Subscriber.dispatchEvent(Subscriber.java:71) [minecraft_server.1.12.2.jar:?]
at com.google.common.eventbus.Dispatcher$PerThreadQueuedDispatcher.dispatch(Dispatcher.java:116) [minecraft_server.1.12.2.jar:?]
at com.google.common.eventbus.EventBus.post(EventBus.java:217) [minecraft_server.1.12.2.jar:?]
at net.minecraftforge.fml.common.LoadController.redirect$onPost$zza000(LoadController.java:560) [LoadController.class:?]
at net.minecraftforge.fml.common.LoadController.distributeStateMessage(LoadController.java:148) [LoadController.class:?]
at net.minecraftforge.fml.common.Loader.serverStarted(Loader.java:782) [Loader.class:?]
at net.minecraftforge.fml.common.FMLCommonHandler.handleServerStarted(FMLCommonHandler.java:300) [FMLCommonHandler.class:?]
at net.minecraft.server.MinecraftServer.run(MinecraftServer.java:486) [MinecraftServer.class:?]
at java.lang.Thread.run(Thread.java:748) [?:1.8.0_151]
Caused by: java.lang.NullPointerException
at ninja.leaping.configurate.objectmapping.ObjectMapper.<init>(ObjectMapper.java:194) ~[spongeforge-1.12.2-2586-7.1.0-BETA-2887.jar:1.12.2-2586-7.1.0-BETA-2887]
at ninja.leaping.configurate.objectmapping.DefaultObjectMapperFactory$1.load(DefaultObjectMapperFactory.java:35) ~[spongeforge-1.12.2-2586-7.1.0-BETA-2887.jar:1.12.2-2586-7.1.0-BETA-2887]
at ninja.leaping.configurate.objectmapping.DefaultObjectMapperFactory$1.load(DefaultObjectMapperFactory.java:32) ~[spongeforge-1.12.2-2586-7.1.0-BETA-2887.jar:1.12.2-2586-7.1.0-BETA-2887]
at com.google.common.cache.LocalCache$LoadingValueReference.loadFuture(LocalCache.java:3716) ~[minecraft_server.1.12.2.jar:?]
at com.google.common.cache.LocalCache$Segment.loadSync(LocalCache.java:2424) ~[minecraft_server.1.12.2.jar:?]
at com.google.common.cache.LocalCache$Segment.lockedGetOrLoad(LocalCache.java:2298) ~[minecraft_server.1.12.2.jar:?]
at com.google.common.cache.LocalCache$Segment.get(LocalCache.java:2211) ~[minecraft_server.1.12.2.jar:?]
... 50 more

So here’s the million dollar question. Is this a failing of Configurate, is it my design, or am I just missing a crucial step in making all this work? The existence of the __class__ field leads me to believe that Configurate has some sort of support for Polymorphism, but I would appreciate some clarification on this.

Thank you in advance!

@zml Mind taking a look at this, please? :slight_smile:

It’s a bug with configurate. You could try this work-around TypeSerializer (or write something similar for each specific interface):

1 Like

This looks very promising.

My solution was to instead go for using Gson and I wrote a TypeAdapter for Configurate: