From b3a35920a561d288c8112da4faf0be28acb11565 Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sun, 14 Aug 2022 04:34:10 +0200 Subject: [PATCH] Better player (de)serialization --- .../gameserver/achievements/Achievement.java | 3 + .../gameserver/entity/player/Inventory.java | 11 +- .../gameserver/entity/player/Player.java | 146 ++++++------------ .../entity/player/PlayerConfigFile.java | 139 +++++++++++++++++ .../entity/player/PlayerManager.java | 7 +- .../entity/player/PlayerStatistics.java | 12 +- .../serialization/AchievementSerializer.java | 34 ++++ 7 files changed, 242 insertions(+), 110 deletions(-) create mode 100644 gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerConfigFile.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/serialization/AchievementSerializer.java diff --git a/gameserver/src/main/java/brainwine/gameserver/achievements/Achievement.java b/gameserver/src/main/java/brainwine/gameserver/achievements/Achievement.java index 9d6b630..3713ee5 100644 --- a/gameserver/src/main/java/brainwine/gameserver/achievements/Achievement.java +++ b/gameserver/src/main/java/brainwine/gameserver/achievements/Achievement.java @@ -10,8 +10,10 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo.As; import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.serialization.AchievementSerializer; import brainwine.gameserver.util.MathUtils; @JsonTypeInfo(use = Id.NAME, include = As.PROPERTY, property = "type", defaultImpl = Achievement.class) @@ -27,6 +29,7 @@ import brainwine.gameserver.util.MathUtils; @Type(name = "DiscoveryAchievement", value = DiscoveryAchievement.class), @Type(name = "Journeyman", value = JourneymanAchievement.class) }) +@JsonSerialize(using = AchievementSerializer.class) @JsonIgnoreProperties(ignoreUnknown = true) public abstract class Achievement { diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/Inventory.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/Inventory.java index 14537b7..f177f0b 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/Inventory.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/Inventory.java @@ -7,7 +7,6 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; -import com.fasterxml.jackson.annotation.JsonBackReference; import com.fasterxml.jackson.annotation.JsonIncludeProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonValue; @@ -26,11 +25,11 @@ public class Inventory { // TODO clean up, perhaps just merge with inventory somehow. private final ItemContainer hotbar = new ItemContainer(10); private final ItemContainer accessories = new ItemContainer(20); - - @JsonBackReference private Player player; - public Inventory(Player player) { + protected Inventory() {} + + protected Inventory(Player player) { this.player = player; } @@ -49,6 +48,10 @@ public class Inventory { } } + protected void setPlayer(Player player) { + this.player = player; + } + public void moveItemToContainer(Item item, ContainerType type, int slot) { boolean accessoriesUpdated = false; hotbar.removeItem(item); diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java index 5cdc399..79a6bfa 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java @@ -1,6 +1,5 @@ package brainwine.gameserver.entity.player; -import java.beans.ConstructorProperties; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -13,12 +12,6 @@ import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; -import com.fasterxml.jackson.annotation.JacksonInject; -import com.fasterxml.jackson.annotation.JsonIncludeProperties; -import com.fasterxml.jackson.annotation.JsonManagedReference; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonValue; - import brainwine.gameserver.GameConfiguration; import brainwine.gameserver.GameServer; import brainwine.gameserver.achievements.Achievement; @@ -71,9 +64,6 @@ import brainwine.gameserver.zone.Chunk; import brainwine.gameserver.zone.MetaBlock; import brainwine.gameserver.zone.Zone; -// TODO re-evaluate how we handle saving/loading this thing.. -@JsonIncludeProperties({"name", "email", "password_hash", "auth_tokens", "admin", "experience", "skill_points", "karma", - "crowns", "inventory", "statistics", "ignored_hints", "achievements", "skills", "equipped_clothing", "equipped_colors", "current_zone"}) public class Player extends Entity implements CommandExecutor { public static final int MAX_SKILL_LEVEL = 15; @@ -87,57 +77,22 @@ public class Player extends Entity implements CommandExecutor { public static final float ENTITY_VISIBILITY_RANGE = 40; public static final float BASE_REGEN_AMOUNT = 0.1F; private static int dialogDiscriminator; - - @JacksonInject("documentId") private final String documentId; - - @JsonProperty("email") private String email; - - @JsonProperty("password_hash") private String password; - - @JsonProperty("auth_tokens") - private final List authTokens = new ArrayList<>(); - - @JsonProperty("admin") private boolean admin; - - @JsonProperty("experience") private int experience; - - @JsonProperty("skill_points") private int skillPoints; - - @JsonProperty("karma") private int karma; - - @JsonProperty("crowns") private int crowns; - - @JsonManagedReference - @JsonProperty("inventory") - private final Inventory inventory = new Inventory(this); - - @JsonManagedReference - @JsonProperty("statistics") - private final PlayerStatistics statistics = new PlayerStatistics(this); - - @JsonProperty("ignored_hints") - private final Map ignoredHints = new HashMap(); - - @JsonProperty("achievements") - private final Set achievements = new HashSet<>(); - - @JsonProperty("skills") - private final Map skills = new HashMap<>(); - - @JsonProperty("equipped_clothing") - private final Map clothing = new HashMap<>(); - - @JsonProperty("equipped_colors") - private final Map colors = new HashMap<>(); - + private Inventory inventory; + private PlayerStatistics statistics; + private List authTokens; + private Set achievements; + private Map ignoredHints; + private Map skills; + private Map equippedClothing; + private Map equippedColors; private final Set wardrobe = new HashSet<>(); private final Map settings = new HashMap<>(); private final Set activeChunks = new HashSet<>(); @@ -154,24 +109,41 @@ public class Player extends Entity implements CommandExecutor { private Zone nextZone; private Connection connection; - @ConstructorProperties({"documentId", "name", "current_zone"}) - public Player(@JacksonInject("documentId") String documentId, String name, Zone zone) { + protected Player(String documentId, PlayerConfigFile config) { + super(config.getCurrentZone()); + this.documentId = documentId; + this.name = config.getName(); + this.email = config.getEmail(); + this.password = config.getPasswordHash(); + this.admin = config.isAdmin(); + this.experience = config.getExperience(); + this.skillPoints = config.getSkillPoints(); + this.karma = config.getKarma(); + this.crowns = config.getCrowns(); + this.inventory = config.getInventory(); + this.statistics = config.getStatistics(); + this.authTokens = config.getAuthTokens(); + this.achievements = config.getAchievements(); + this.ignoredHints = config.getIgnoredHints(); + this.skills = config.getSkills(); + this.equippedClothing = config.getEquippedClothing(); + this.equippedColors = config.getEquippedColors(); + inventory.setPlayer(this); + statistics.setPlayer(this); + } + + public Player(String documentId, String name, Zone zone) { super(zone); - - for(Item item : ItemRegistry.getItems()) { - if(item.isClothing()) { - wardrobe.add(item); - } - } - this.documentId = documentId; this.name = name; - settings.put("hotbar_presets", new ArrayList<>()); - Map test = new HashMap<>(); - test.put("fg", true); - test.put("to", true); - test.put("lo", true); - settings.put("appearance", test); + this.inventory = new Inventory(this); + this.statistics = new PlayerStatistics(this); + this.authTokens = new ArrayList<>(); + this.achievements = new HashSet<>(); + this.ignoredHints = new HashMap<>(); + this.skills = new HashMap<>(); + this.equippedClothing = new HashMap<>(); + this.equippedColors = new HashMap<>(); } @Override @@ -843,7 +815,7 @@ public class Player extends Entity implements CommandExecutor { return; } - clothing.put(slot, item); + equippedClothing.put(slot, item); zone.sendMessage(new EntityChangeMessage(id, getAppearanceConfig())); } @@ -856,17 +828,17 @@ public class Player extends Entity implements CommandExecutor { } public Map getEquippedClothing() { - return Collections.unmodifiableMap(clothing); + return Collections.unmodifiableMap(equippedClothing); } public void setColor(ColorSlot slot, String hex) { // TODO check if the string is actually a valid hex color - colors.put(slot, hex); + equippedColors.put(slot, hex); zone.sendMessage(new EntityChangeMessage(id, getAppearanceConfig())); } - public Map getEquippedColors(){ - return Collections.unmodifiableMap(colors); + public Map getEquippedColors() { + return Collections.unmodifiableMap(equippedColors); } public void setSkillLevel(Skill skill, int level) { @@ -1060,36 +1032,14 @@ public class Player extends Entity implements CommandExecutor { return connection != null && connection.isOpen(); } - @JsonValue - public Map getJsonValue() { - Map map = new HashMap<>(); - map.put("email", email); - map.put("password_hash", password); - map.put("auth_tokens", authTokens); - map.put("name", name); - map.put("admin", admin); - map.put("experience", experience); - map.put("skill_points", skillPoints); - map.put("karma", karma); - map.put("crowns", crowns); - map.put("current_zone", zone.getDocumentId()); - map.put("ignored_hints", ignoredHints); - map.put("achievements", achievements); - map.put("skills", skills); - map.put("equipped_colors", colors); - map.put("equipped_clothing", clothing); - map.put("inventory", inventory); - map.put("statistics", statistics); - return map; - } - private Map getAppearanceConfig() { Map appearance = new HashMap<>(); - for(Entry entry : clothing.entrySet()) { + + for(Entry entry : equippedClothing.entrySet()) { appearance.put(entry.getKey().getId(), entry.getValue().getId()); } - for(Entry entry : colors.entrySet()) { + for(Entry entry : equippedColors.entrySet()) { appearance.put(entry.getKey().getId(), entry.getValue()); } diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerConfigFile.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerConfigFile.java new file mode 100644 index 0000000..9a70bf4 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerConfigFile.java @@ -0,0 +1,139 @@ +package brainwine.gameserver.entity.player; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; + +import brainwine.gameserver.achievements.Achievement; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.zone.Zone; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class PlayerConfigFile { + + private String name; + private String email; + private String passwordHash; + private Zone currentZone; + private boolean admin; + private int experience; + private int skillPoints; + private int karma; + private int crowns; + private Inventory inventory = new Inventory(); + private PlayerStatistics statistics = new PlayerStatistics(); + private List authTokens = new ArrayList<>(); + private Set achievements = new HashSet<>(); + private Map ignoredHints = new HashMap<>(); + private Map skills = new HashMap<>(); + private Map equippedClothing = new HashMap<>(); + private Map equippedColors = new HashMap<>(); + + public PlayerConfigFile(Player player) { + this.name = player.getName(); + this.email = player.getEmail(); + this.passwordHash = player.getPassword(); + this.currentZone = player.getZone(); + this.admin = player.isAdmin(); + this.experience = player.getExperience(); + this.skillPoints = player.getSkillPoints(); + this.karma = player.getKarma(); + this.crowns = player.getCrowns(); + this.inventory = player.getInventory(); + this.statistics = player.getStatistics(); + this.authTokens = player.getAuthTokens(); + this.achievements = player.getAchievements(); + this.ignoredHints = player.getIgnoredHints(); + this.skills = player.getSkills(); + this.equippedClothing = player.getEquippedClothing(); + this.equippedColors = player.getEquippedColors(); + } + + @JsonCreator + private PlayerConfigFile() {} + + @JsonSetter(nulls = Nulls.FAIL) + public String getName() { + return name; + } + + public String getEmail() { + return email; + } + + public String getPasswordHash() { + return passwordHash; + } + + public Zone getCurrentZone() { + return currentZone; + } + + public boolean isAdmin() { + return admin; + } + + @JsonSetter(nulls = Nulls.SKIP, contentNulls = Nulls.SKIP) + public List getAuthTokens() { + return authTokens; + } + + public int getExperience() { + return experience; + } + + public int getSkillPoints() { + return skillPoints; + } + + public int getKarma() { + return karma; + } + + public int getCrowns() { + return crowns; + } + + @JsonSetter(nulls = Nulls.SKIP) + public Inventory getInventory() { + return inventory; + } + + @JsonSetter(nulls = Nulls.SKIP) + public PlayerStatistics getStatistics() { + return statistics; + } + + @JsonSetter(nulls = Nulls.SKIP, contentNulls = Nulls.SKIP) + public Set getAchievements() { + return achievements; + } + + @JsonSetter(nulls = Nulls.SKIP, contentNulls = Nulls.SKIP) + public Map getIgnoredHints() { + return ignoredHints; + } + + @JsonSetter(nulls = Nulls.SKIP, contentNulls = Nulls.SKIP) + public Map getSkills() { + return skills; + } + + @JsonSetter(nulls = Nulls.SKIP, contentNulls = Nulls.SKIP) + public Map getEquippedClothing() { + return equippedClothing; + } + + @JsonSetter(nulls = Nulls.SKIP, contentNulls = Nulls.SKIP) + public Map getEquippedColors() { + return equippedColors; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerManager.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerManager.java index d2516f2..08795a4 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerManager.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerManager.java @@ -12,8 +12,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.mindrot.jbcrypt.BCrypt; -import com.fasterxml.jackson.databind.InjectableValues; - import brainwine.gameserver.GameServer; import brainwine.gameserver.server.pipeline.Connection; import brainwine.shared.JsonHelper; @@ -53,7 +51,8 @@ public class PlayerManager { String id = file.getName().replace(".json", ""); try { - Player player = JsonHelper.readValue(file, Player.class, new InjectableValues.Std().addValue("documentId", id)); + PlayerConfigFile configFile = JsonHelper.readValue(file, PlayerConfigFile.class); + Player player = new Player(id, configFile); if(player.getZone() == null) { player.setZone(GameServer.getInstance().getZoneManager().getRandomZone()); @@ -83,7 +82,7 @@ public class PlayerManager { File file = new File("players", player.getDocumentId() + ".json"); try { - JsonHelper.writeValue(file, player); + JsonHelper.writeValue(file, new PlayerConfigFile(player)); } catch(Exception e) { logger.error("Could not save player id {}", player.getDocumentId(), e); } diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerStatistics.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerStatistics.java index e2180d0..65746dd 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerStatistics.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerStatistics.java @@ -7,8 +7,8 @@ import java.util.Map.Entry; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; -import com.fasterxml.jackson.annotation.JsonBackReference; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import brainwine.gameserver.achievements.CraftingAchievement; @@ -39,13 +39,17 @@ public class PlayerStatistics { private int dungeonsRaided; private int deaths; - @JsonBackReference + @JsonIgnore private Player player; @JsonCreator - private PlayerStatistics() {} + protected PlayerStatistics() {} - public PlayerStatistics(Player player) { + protected PlayerStatistics(Player player) { + this.player = player; + } + + protected void setPlayer(Player player) { this.player = player; } diff --git a/gameserver/src/main/java/brainwine/gameserver/serialization/AchievementSerializer.java b/gameserver/src/main/java/brainwine/gameserver/serialization/AchievementSerializer.java new file mode 100644 index 0000000..ca21aa8 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/serialization/AchievementSerializer.java @@ -0,0 +1,34 @@ +package brainwine.gameserver.serialization; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import brainwine.gameserver.achievements.Achievement; + +/** + * Seriously, Jackson has the stupidest problems sometimes. + */ +public class AchievementSerializer extends StdSerializer { + + public static final AchievementSerializer INSTANCE = new AchievementSerializer(); + private static final long serialVersionUID = 7660825036762291561L; + + protected AchievementSerializer() { + super(Achievement.class); + } + + @Override + public void serialize(Achievement achievement, JsonGenerator generator, SerializerProvider provider) throws IOException { + generator.writeString(achievement.getTitle()); + } + + @Override + public void serializeWithType(Achievement achievement, JsonGenerator generator, SerializerProvider provider, + TypeSerializer typeSerializer) throws IOException { + serialize(achievement, generator, provider); + } +}