package com.asteria.game.character.player.serialize; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.nio.file.Paths; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import com.asteria.game.GameConstants; import com.asteria.game.character.MovementQueue; import com.asteria.game.character.combat.weapon.FightType; import com.asteria.game.character.player.Appearance; import com.asteria.game.character.player.Player; import com.asteria.game.character.player.Rights; import com.asteria.game.character.player.content.Spellbook; import com.asteria.game.character.player.skill.Skill; import com.asteria.game.character.player.skill.Skills; import com.asteria.game.item.Item; import com.asteria.game.item.container.Bank; import com.asteria.game.item.container.Equipment; import com.asteria.game.item.container.Inventory; import com.asteria.game.location.Position; import com.asteria.net.login.LoginResponse; import com.asteria.utility.MutableNumber; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; /** * The serializer that will serialize and deserialize character files for * players. * <p> * <p> * Serialization of character files can and should be done on another thread * whenever possible to avoid doing disk I/O on the main game thread. * * @author lare96 <http://github.com/lare96> */ public final class PlayerSerialization { /** * The player serialization cache that will enabled the caching of character * files for later use. */ private static PlayerSerializationCache cache = new PlayerSerializationCache(GameConstants.CLEAN_CACHE); /** * The linked hash collection of tokens that will be serialized and * deserialized. A linked hash set is used here to ensure that there is only * one of each token, and to preserve order. */ private final Set<TokenSerializer> tokens = new LinkedHashSet<>(); /** * The player this serializer is dedicated to. */ private final Player player; /** * The character file that corresponds to this player. */ private final File cf; /** * Creates a new {@link PlayerSerialization}. * * @param player * the player this serializer is dedicated to. */ public PlayerSerialization(Player player) { this.player = player; this.cf = Paths.get("./data/players/" + player.getUsername() + "" + ".json").toFile(); createTokens(); } /** * The function where all of the tokens are added to the linked hash * collection. Add as many tokens here as needed but keep in mind tokens * cannot have the same name. * <p> * The token serialization format is as follows: * <p> * <p> * * <pre> * tokens.add(new TokenSerializer(NAME_OF_TOKEN, SERIALIZATION, DESERIALIZATION)); * </pre> * <p> * For those who are still confused, here is an example. Lets say we want * "deathCount" to be saved to and loaded from the character file: * <p> * <p> * * <pre> * private int deathCount; * * public void setDeathCount(int deathCount) { * this.deathCount = deathCount; * } * * public int getDeathCount() { * return deathCount; * } * </pre> * <p> * We would be able to do it like this: * <p> * <p> * * <pre> * tokens.add(new TokenSerializer("death-count", player.getDeathCount(), n -> player.setDeathCount(n.getAsInt()))); * </pre> */ private void createTokens() { Gson b = new GsonBuilder().create(); Player p = player; tokens.add(new TokenSerializer("username", p.getUsername(), n -> p.setUsername(n.getAsString()))); tokens.add(new TokenSerializer("password", p.getPassword(), n -> p.setPassword(n.getAsString()))); tokens.add(new TokenSerializer("position", p.getPosition(), n -> p.setPosition(b.fromJson(n, Position.class)))); tokens.add(new TokenSerializer("rights", p.getRights(), n -> p.setRights(Rights.valueOf(n.getAsString())))); Appearance appearance = p.getAppearance(); tokens.add(new TokenSerializer("appearance", appearance.getValues(), n -> appearance.setValues(b.fromJson(n, int[].class)))); MovementQueue movement = p.getMovementQueue(); tokens.add(new TokenSerializer("running", movement.isRunning(), n -> movement.setRunning(n.getAsBoolean()))); tokens.add(new TokenSerializer("new-player", p.isNewPlayer(), n -> p.setNewPlayer(n.getAsBoolean()))); Inventory inventory = p.getInventory(); tokens.add(new TokenSerializer("inventory", inventory.container(), n -> inventory.setItems(b.fromJson(n, Item[].class)))); Bank bank = p.getBank(); tokens.add(new TokenSerializer("bank", bank.container(), n -> bank.setItems(b.fromJson(n, Item[].class)))); Equipment equipment = p.getEquipment(); tokens.add(new TokenSerializer("equipment", equipment.container(), n -> equipment.setItems(b.fromJson(n, Item[].class)))); Set<Long> f = p.getFriends(); tokens.add(new TokenSerializer("friends", f.toArray(), n -> Collections.addAll(f, b.fromJson(n, Long[].class)))); Set<Long> i = p.getIgnores(); tokens.add(new TokenSerializer("ignores", i.toArray(), n -> Collections.addAll(i, b.fromJson(n, Long[].class)))); MutableNumber energy = p.getRunEnergy(); tokens.add(new TokenSerializer("run-energy", energy.get(), n -> energy.set(n.getAsInt()))); Spellbook book = p.getSpellbook(); tokens.add(new TokenSerializer("spellbook", book.name(), n -> p.setSpellbook(Spellbook.valueOf(n.getAsString())))); tokens.add(new TokenSerializer("account-banned", p.isBanned(), n -> p.setBanned(n.getAsBoolean()))); tokens.add(new TokenSerializer("auto-retaliate", p.isAutoRetaliate(), n -> p.setAutoRetaliate(n.getAsBoolean()))); FightType type = p.getFightType(); tokens.add(new TokenSerializer("fight-type", type.name(), n -> p.setFightType(FightType.valueOf(n.getAsString())))); MutableNumber skulled = p.getSkullTimer(); tokens.add(new TokenSerializer("skull-timer", skulled.get(), n -> skulled.set(n.getAsInt()))); tokens.add(new TokenSerializer("accept-aid", p.isAcceptAid(), n -> p.setAcceptAid(n.getAsBoolean()))); tokens.add(new TokenSerializer("poison-damage", p.getPoisonDamage().get(), n -> p.getPoisonDamage().set(n.getAsInt()))); MutableNumber teleblocked = p.getTeleblockTimer(); tokens.add(new TokenSerializer("teleblock-timer", teleblocked.get(), n -> teleblocked.set(n.getAsInt()))); MutableNumber percentage = p.getSpecialPercentage(); tokens.add(new TokenSerializer("special-amount", percentage.get(), n -> percentage.set(n.getAsInt()))); Skill[] skills = p.getSkills(); tokens.add(new TokenSerializer("skills", skills, n -> System.arraycopy(b.fromJson(n, Skill[].class), 0, skills, 0, skills.length))); } /** * Serializes the dedicated player into a {@code JSON} file. */ public void serialize() { try { cf.getParentFile().setWritable(true); if (!cf.getParentFile().exists()) { try { cf.getParentFile().mkdirs(); } catch (SecurityException e) { throw new IllegalStateException("Unable to create " + "directory for character files!"); } } try (FileWriter out = new FileWriter(cf)) { Gson gson = new GsonBuilder().setPrettyPrinting().addSerializationExclusionStrategy(new PlayerSerializationFilter()) .create(); JsonObject obj = new JsonObject(); tokens.stream().forEach(t -> obj.add(t.getName(), gson.toJsonTree(t.getToJson()))); out.write(gson.toJson(obj)); cache.add(player.getUsernameHash(), obj); } } catch (Exception e) { e.printStackTrace(); } } /** * Deserializes the dedicated player from a {@code JSON} file. * * @param password * the password that will be used to validate if the player has * the right credentials. * @return the login response determined by what happened before, during, * and after deserialization. */ public LoginResponse deserialize(String password) { try { if (!cf.exists()) { Skills.create(player); return LoginResponse.NORMAL; } Optional<JsonObject> cached = cache.get(player.getUsernameHash()); if (cached.isPresent()) { tokens.stream().filter(t -> cached.get().has(t.getName())).forEach( t -> t.getFromJson().accept(cached.get().get(t.getName()))); } else { cf.setReadable(true); try (FileReader in = new FileReader(cf)) { JsonObject reader = (JsonObject) new JsonParser().parse(in); tokens.stream().filter(t -> reader.has(t.getName())).forEach(t -> t.getFromJson().accept(reader.get(t.getName()))); } } if (!password.equals(player.getPassword())) return LoginResponse.INVALID_CREDENTIALS; if (player.isBanned()) return LoginResponse.ACCOUNT_DISABLED; } catch (Exception e) { e.printStackTrace(); return LoginResponse.COULD_NOT_COMPLETE_LOGIN; } return LoginResponse.NORMAL; } /** * Gets the cache that will enabled the caching of character files for later * use. * * @return the cache for caching character files. */ public static PlayerSerializationCache getCache() { return cache; } /** * The container that represents a token that can be both serialized and * deserialized. * * @author lare96 <http://github.com/lare96> */ private static final class TokenSerializer { /** * The name of this serializable token. */ private final String name; /** * The {@code Object} being serialized by this token. */ private final Object toJson; /** * The deserialization consumer for this token. */ private final Consumer<JsonElement> fromJson; /** * Creates a new {@link TokenSerializer}. * * @param name * the name of this serializable token. * @param toJson * the {@code Object} being serialized by this token. * @param fromJson * the deserialization consumer for this token. */ public TokenSerializer(String name, Object toJson, Consumer<JsonElement> fromJson) { this.name = name; this.toJson = toJson; this.fromJson = fromJson; } @Override public String toString() { return "TOKEN[name= " + name + "]"; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((name == null) ? 0 : name.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (!(obj instanceof TokenSerializer)) return false; TokenSerializer other = (TokenSerializer) obj; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false; return true; } /** * Gets the name of this serializable token. * * @return the name of this token. */ public String getName() { return name; } /** * Gets the {@code Object} being serialized by this token. * * @return the serializable object. */ public Object getToJson() { return toJson; } /** * Gets the deserialization consumer for this token. * * @return the deserialization consumer. */ public Consumer<JsonElement> getFromJson() { return fromJson; } } }