Search code examples
javaserializationgsonspigot

Serialization of Custom Consumer


I am working on a statue plugin for Spigot 1.20.4 (Java) that creates NPC statues. Currently, when you create a statue, you can use a consumer to define what happens when said NPC is right clicked.

Statue.StatueBuilder builder = new Statue.StatueBuilder(entityType)
.setName("Test")
.setOnClick(connectionPlayer -> System.out.println("TEST " + new Random().nextInt()));
 Statue statue = builder.build();

The #setOnClick takes in a StatueAction, which is defined as:

public interface StatueAction {
    void perform(ConnectionPlayer player);
}

I need to save this data (either yml, json, etc) so when the server restarts, the statues will be spawned again. However, I cant serialize the consumer. Tried with YML and JSON. I understand why, but not how to get around it.

Any ideas on how to save the onClick action to a file? Or if this is not a good approach, any ideas for other approaches I might take to achieve a similar goal? I did look into How to serialize a lambda?, however it seems none of those are working correctly (for me), meaning I must be doing something incorrect.

Here is how i am currently trying to serialize it

StatueAction<ConnectionPlayer> r = (connectionPlayer) -> System.out.println("Hello");
Statue.StatueBuilder builder = new Statue.StatueBuilder(entityType).setName("Test").setOnClick(r);
Statue statue = builder.build();
statue.spawn(player.getLocation());
statue.createDefaultAttribs();
System.out.println(statue.serialize());

And the output is

{"name":"Test","entityType":"ARMOR_STAND","entityUUID":"6c2a484c-3168-4fb8-a3ee-8c2d93bddf1d","textDisplaysID":[],"onClick":{}}

The Statue serialization method is defined as

 public static class StatueSerializer implements JsonSerializer<Statue> {
        @Override
        public JsonElement serialize(Statue statue, Type type, JsonSerializationContext jsonSerializationContext) {
            JsonObject jsonObject = new JsonObject();
            jsonObject.addProperty("name", statue.name);
            jsonObject.addProperty("entityType", statue.entityType.name());
            jsonObject.addProperty("entityUUID", statue.getEntityUUID().toString());
            jsonObject.add("textDisplaysID", jsonSerializationContext.serialize(statue.getTextDisplaysID()));

            // Serialize the CustomAction using its own serialization logic
            jsonObject.add("onClick", jsonSerializationContext.serialize(statue.getOnClick()));

            return jsonObject;
        }
    }

public String serialize() {
    Gson gson = new GsonBuilder()
            .registerTypeAdapter(Statue.class, new Statue.StatueSerializer())
            .create();
    return gson.toJson(this);
}

Solution

  • If you only have a static amount of possible actions, then instead of having an interface you could use an enum whose constants represent these actions, and then only serialize the name of the action / the constant name.
    This would be similar to how Minecraft itself serializes the clickEvent and hoverEvent actions of text components.

    Another alternative could be to add a serialize method to your StatueAction interface so that implementations can serialize all necessary data. But you would also have to include some identifier then, and have some 'registry' where on deserialization the action class is looked up based on the identifier in the JSON data. See other questions about Gson and polymorphism.
    Avoid using Class.forName for deserialization even if it may seem convenient, because if the JSON data can be influenced by an untrusted user, it would allow them to create instances of arbitrary classes, which could be exploited.