Add implementation for variety of attribute and base attribute hierarchies, add in JSON encoding for itemstack-item pairing to hold stats and information about items without unintuitive print & parse methods. Temporarily disable donation related commands in DonationManager for testing, as it was presenting compilation issues. Introduce further implementation of LegendaryItems. Add various utility methods for JSON encoding (utilizes GSON). Fix various bugs seen in internal testing.

This commit is contained in:
Ty Sayers 2015-05-25 14:22:06 -04:00
parent a8a8d4961a
commit 167549c9e8
35 changed files with 831 additions and 52 deletions

View File

@ -40,9 +40,10 @@ public class DonationManager extends MiniDbClientPlugin<Donor>
@Override @Override
public void addCommands() public void addCommands()
{ {
addCommand(new GemCommand(this)); // TODO: Re-add commands? Where are command implementations, seen as missing at the moment.
addCommand(new CoinCommand(this)); //addCommand(new GemCommand(this));
addCommand(new GoldCommand(this)); //addCommand(new CoinCommand(this));
//addCommand(new GoldCommand(this));
} }
@EventHandler @EventHandler

View File

@ -8,5 +8,6 @@
<classpathentry combineaccessrules="false" kind="src" path="/Mineplex.Minecraft.Game.ClassCombat"/> <classpathentry combineaccessrules="false" kind="src" path="/Mineplex.Minecraft.Game.ClassCombat"/>
<classpathentry combineaccessrules="false" kind="src" path="/Mineplex.Minecraft.Game.Core"/> <classpathentry combineaccessrules="false" kind="src" path="/Mineplex.Minecraft.Game.Core"/>
<classpathentry kind="var" path="REPO_DIR/Plugins/Libraries/craftbukkit.jar" sourcepath="/REPO_DIR/GitHubLibraries/CraftBukkit/src"/> <classpathentry kind="var" path="REPO_DIR/Plugins/Libraries/craftbukkit.jar" sourcepath="/REPO_DIR/GitHubLibraries/CraftBukkit/src"/>
<classpathentry kind="var" path="REPO_DIR/Plugins/Libraries/gson-2.2.1.jar"/>
<classpathentry kind="output" path="bin"/> <classpathentry kind="output" path="bin"/>
</classpath> </classpath>

View File

@ -17,6 +17,7 @@ import mineplex.core.itemstack.ItemStackFactory;
import mineplex.core.memory.MemoryFix; import mineplex.core.memory.MemoryFix;
import mineplex.core.message.MessageManager; import mineplex.core.message.MessageManager;
import mineplex.core.monitor.LagMeter; import mineplex.core.monitor.LagMeter;
import mineplex.core.packethandler.PacketHandler;
import mineplex.core.portal.Portal; import mineplex.core.portal.Portal;
import mineplex.core.preferences.PreferencesManager; import mineplex.core.preferences.PreferencesManager;
import mineplex.core.punish.Punish; import mineplex.core.punish.Punish;
@ -94,7 +95,9 @@ public class Clans extends JavaPlugin
new BuildingShop(clans, _clientManager, _donationManager); new BuildingShop(clans, _clientManager, _donationManager);
new PvpShop(clans, _clientManager, _donationManager); new PvpShop(clans, _clientManager, _donationManager);
GearManager customGear = new GearManager(this); // Enable custom-gear related managers
PacketHandler packetHandler = new PacketHandler(this);
GearManager customGear = new GearManager(this, packetHandler);
//Updates //Updates
getServer().getScheduler().scheduleSyncRepeatingTask(this, new Updater(this), 1, 1); getServer().getScheduler().scheduleSyncRepeatingTask(this, new Updater(this), 1, 1);

View File

@ -0,0 +1,28 @@
package mineplex.game.clans.clans;
import org.bukkit.ChatColor;
import org.bukkit.Color;
public class ChunkData
{
private int _x;
public int getX() { return _x; }
private int _z;
public int getZ() { return _z; }
private ChatColor _color;
public ChatColor getColor() { return _color; }
private String _clanName;
public String getClanName() { return _clanName; }
public ChunkData(int x, int z, ChatColor color, String clanName)
{
_x = x;
_z = z;
_color = color;
_clanName = clanName;
}
}

View File

@ -1,11 +1,15 @@
package mineplex.game.clans.clans; package mineplex.game.clans.clans;
import java.awt.Color;
import java.util.ArrayList;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List;
import java.util.Map.Entry; import java.util.Map.Entry;
import org.bukkit.ChatColor; import org.bukkit.ChatColor;
import org.bukkit.Chunk; import org.bukkit.Chunk;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import mineplex.core.common.util.C; import mineplex.core.common.util.C;
@ -33,7 +37,49 @@ public class ClansUtility
ADMIN, ADMIN,
SAFE SAFE
} }
/**
*
* @param location
* @param radius
* @return a 2D array of {@link ClanTerritory} with uniform dimension of ({@code radius} * 2 + 1). The region represented by
* the array of territories is centered on {@code location} chunk with a given chunk {@code radius}.
*/
public List<ChunkData> getTerritory(Location location, int radius, ClanInfo surveyorClan)
{
World world = location.getWorld();
Chunk chunk = location.getChunk();
int chunkX = chunk.getX();
int chunkZ = chunk.getZ();
int width = radius*2 + 1;
List<ChunkData> chunks = new ArrayList<ChunkData>();
for (int x = 0; x < width; x++)
{
for (int z = 0; z < width; z++)
{
int territoryX = chunkX - radius + x;
int territoryZ = chunkZ - radius + z;
ClanTerritory territory = getClaim(world.getChunkAt(territoryX, territoryZ));
if (territory != null)
{
ClanInfo clan = getOwner(territory);
String clanName = territory.Owner;
ClanRelation relationship = rel(surveyorClan, clan);
ChatColor color = relChatColor(relationship, false);
ChunkData data = new ChunkData(territoryX, territoryZ, color, clanName);
chunks.add(data);
}
}
}
return chunks;
}
public ClanInfo searchClanPlayer(Player caller, String name, boolean inform) public ClanInfo searchClanPlayer(Player caller, String name, boolean inform)
{ {
//CLAN //CLAN
@ -197,11 +243,16 @@ public class ClansUtility
return clan.getHome().getChunk().equals(chunk); return clan.getHome().getChunk().equals(chunk);
} }
public ClanTerritory getClaim(Chunk chunk)
{
String chunkTag = UtilWorld.chunkToStr(chunk);
return Clans.getClaimMap().get(chunkTag);
}
public ClanTerritory getClaim(Location loc) public ClanTerritory getClaim(Location loc)
{ {
String chunk = UtilWorld.chunkToStr(loc.getChunk()); return getClaim(loc.getChunk());
return Clans.getClaimMap().get(chunk);
} }
public ClanTerritory getClaim(String chunk) public ClanTerritory getClaim(String chunk)

View File

@ -4,12 +4,15 @@ import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import mineplex.game.clans.items.attributes.ItemAttribute; import mineplex.game.clans.items.attributes.ItemAttribute;
import org.bukkit.Material;
import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
/** /**
* Represents a customizable wrapper for an {@link ItemStack}, enabling the possession * Represents a customizable wrapper for an {@link ItemStack}, enabling the possession
@ -20,11 +23,34 @@ import org.bukkit.inventory.ItemStack;
public class CustomItem public class CustomItem
{ {
private ItemAttribute superPrefix; private ItemAttribute _superPrefix;
private ItemAttribute prefix; public void setSuperPrefix(ItemAttribute attribute) { _superPrefix = attribute; }
private ItemAttribute suffix;
private ItemAttribute _prefix;
public void setPrefix(ItemAttribute attribute) { _prefix = attribute; }
private ItemAttribute _suffix;
public void setSuffix(ItemAttribute attribute) { _suffix = attribute; }
private String _displayName; private String _displayName;
private String _description;
private Material _material;
private String _uuid;
public String getUuid() { return _uuid; }
public CustomItem(String displayName, String description, Material material)
{
_displayName = displayName;
_description = description;
_material = material;
_uuid = UUID.randomUUID().toString();
}
public CustomItem(Material material)
{
this(material.toString(), null, material); // TODO: Prettify item materal name
}
/** /**
* @return the name displayed to players for the item. * @return the name displayed to players for the item.
@ -34,19 +60,19 @@ public class CustomItem
// Concatenate attribute prefixes/suffixes to display name. // Concatenate attribute prefixes/suffixes to display name.
String display = _displayName; String display = _displayName;
if (prefix != null) if (_prefix != null)
{ {
display = prefix.getDisplayName() + " " + display; display = _prefix.getDisplayName() + " " + display;
} }
if (superPrefix != null) if (_superPrefix != null)
{ {
display = superPrefix.getDisplayName() + " " + display; display = _superPrefix.getDisplayName() + " " + display;
} }
if (suffix != null) if (_suffix != null)
{ {
display += " of " + suffix.getDisplayName(); display += " of " + _suffix.getDisplayName();
} }
return display; return display;
@ -54,22 +80,46 @@ public class CustomItem
public List<String> getLore() public List<String> getLore()
{ {
String serialization = GearManager.getItemSerialization(this);
List<String> lore = new ArrayList<String>(); List<String> lore = new ArrayList<String>();
// TODO: Add decorative stat lore (and hidden json-encoding/uuid info) if (_description != null)
lore.add("Custom stat lore string!"); {
lore.add(_description);
}
// Display attribute descriptions and stats in lore
for (ItemAttribute attribute : getAttributes())
{
String attributeLine = attribute.getDisplayName() + " - " + attribute.getDescription();
lore.add(attributeLine);
}
// Tack on serialized JSON encoded line for utility purposes. (Not seen by user)
List<String> serializedLines = new ArrayList<String>();
String[] seri = serialization.split("\n");
for (String line : seri)
{
serializedLines.add(line);
}
lore.addAll(serializedLines);
return lore; return lore;
} }
public ItemStack toItemStack() public ItemStack toItemStack(int amount)
{ {
// TODO: Generate an item stack representing this CustomItem ItemStack item = new ItemStack(_material, amount);
return null; update(item);
// TODO: Add non-descript enchantment for glowing efect?
return item;
} }
public void onInteract(PlayerInteractEvent event) public void onInteract(PlayerInteractEvent event)
{ {
System.out.println("Triggered interact!");
for (ItemAttribute attribute : getAttributes()) for (ItemAttribute attribute : getAttributes())
{ {
attribute.onInteract(event); attribute.onInteract(event);
@ -78,6 +128,7 @@ public class CustomItem
public void onAttack(EntityDamageByEntityEvent event) public void onAttack(EntityDamageByEntityEvent event)
{ {
System.out.println("Triggered attack!");
for (ItemAttribute attribute : getAttributes()) for (ItemAttribute attribute : getAttributes())
{ {
attribute.onAttack(event); attribute.onAttack(event);
@ -86,6 +137,7 @@ public class CustomItem
public void onAttacked(EntityDamageByEntityEvent event) public void onAttacked(EntityDamageByEntityEvent event)
{ {
System.out.println("Triggered damage!");
for (ItemAttribute attribute : getAttributes()) for (ItemAttribute attribute : getAttributes())
{ {
attribute.onAttacked(event); attribute.onAttacked(event);
@ -98,9 +150,36 @@ public class CustomItem
public Set<ItemAttribute> getAttributes() public Set<ItemAttribute> getAttributes()
{ {
Set<ItemAttribute> attributes = new HashSet<ItemAttribute>(); Set<ItemAttribute> attributes = new HashSet<ItemAttribute>();
attributes.add(superPrefix); if (_superPrefix != null) attributes.add(_superPrefix);
attributes.add(prefix); if (_prefix != null) attributes.add(_prefix);
attributes.add(suffix); if (_suffix != null) attributes.add(_suffix);
return attributes; return attributes;
} }
/**
* @param item - the item to check for a matching link
* @return true, if {@code item} matches this CustomItem via UUID, false otherwise.
*/
public boolean matches(CustomItem item)
{
return item.getUuid().equals(_uuid);
}
/**
* Update {@code item} with the proper meta properties suited for this
* {@link CustomItem}.
* @param item - the item whose meta properties are being updated to become a version of this updated custom item.
*/
public void update(ItemStack item)
{
ItemMeta meta = item.getItemMeta();
String displayName = getDisplayName();
List<String> lore = getLore();
meta.setDisplayName(displayName);
meta.setLore(lore);
item.setItemMeta(meta);
}
} }

View File

@ -1,56 +1,101 @@
package mineplex.game.clans.items; package mineplex.game.clans.items;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import mineplex.core.MiniPlugin; import mineplex.core.MiniPlugin;
import mineplex.core.account.CoreClientManager; import mineplex.core.account.CoreClientManager;
import mineplex.core.common.util.UtilServer;
import mineplex.core.packethandler.IPacketHandler;
import mineplex.core.packethandler.PacketHandler;
import mineplex.core.packethandler.PacketInfo;
import mineplex.core.portal.TransferHandler; import mineplex.core.portal.TransferHandler;
import mineplex.core.portal.Commands.SendCommand; import mineplex.core.portal.Commands.SendCommand;
import mineplex.core.portal.Commands.ServerCommand; import mineplex.core.portal.Commands.ServerCommand;
import mineplex.game.clans.items.attributes.ItemAttribute;
import mineplex.game.clans.items.attributes.weapon.FlamingAttribute;
import mineplex.game.clans.items.attributes.weapon.FrostedAttribute;
import mineplex.game.clans.items.attributes.weapon.SharpAttribute;
import mineplex.game.clans.items.commands.GearCommand; import mineplex.game.clans.items.commands.GearCommand;
import mineplex.game.clans.items.generation.Weight; import mineplex.game.clans.items.generation.Weight;
import mineplex.game.clans.items.generation.WeightSet; import mineplex.game.clans.items.generation.WeightSet;
import mineplex.game.clans.items.legendaries.AlligatorsTooth;
import mineplex.game.clans.items.legendaries.WindBlade;
import mineplex.game.clans.items.smelting.SmeltingListener;
import mineplex.serverdata.Region; import mineplex.serverdata.Region;
import mineplex.serverdata.Utility; import mineplex.serverdata.Utility;
import mineplex.serverdata.commands.ServerCommandManager; import mineplex.serverdata.commands.ServerCommandManager;
import mineplex.serverdata.commands.TransferCommand; import mineplex.serverdata.commands.TransferCommand;
import mineplex.serverdata.serialization.RuntimeTypeAdapterFactory;
import mineplex.serverdata.servers.ServerManager; import mineplex.serverdata.servers.ServerManager;
import net.minecraft.server.v1_7_R4.Packet;
import net.minecraft.server.v1_7_R4.PacketPlayOutSetSlot;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/** /**
* Manages creation and retrieval of associated {@link PlayerGear}s with online players, as well * Manages creation and retrieval of associated {@link PlayerGear}s with online players, as well
* as offering methods for parsing and handling {@link CustomItem}s. * as offering methods for parsing and handling {@link CustomItem}s.
* @author MrTwiggy * @author MrTwiggy
* *
*/ */
public class GearManager extends MiniPlugin public class GearManager extends MiniPlugin implements IPacketHandler
{ {
private static final String ITEM_SERIALIZATION_TAG = "-JSON-";
private static GearManager _instance; // Singleton instance private static GearManager _instance; // Singleton instance
private Map<String, PlayerGear> playerGears; // Mapping of player names (key) to cached gear set (value). private Map<String, PlayerGear> playerGears; // Mapping of player names (key) to cached gear set (value).
private WeightSet<Integer> _attributeWeights; // Weightings for randomly selecting number of attributes (1, 2, 3) private WeightSet<Integer> _attributeWeights; // Weightings for randomly selecting number of attributes (1, 2, 3)
private WeightSet<Boolean> _itemWeights; // Weightings for randomly selecting item type (legendary/rare) private WeightSet<Boolean> _itemWeights; // Weightings for randomly selecting item type (legendary/rare)
private WeightSet<Boolean> _gearWeights; // Weightings for randomly selecting gear type (armour/weapon) private WeightSet<Boolean> _gearWeights; // Weightings for randomly selecting gear type (armour/weapon)
private static Gson _gson;
public GearManager(JavaPlugin plugin) public GearManager(JavaPlugin plugin, PacketHandler packetHandler)
{ {
super("CustomGear", plugin); super("CustomGear", plugin);
_instance = this; _instance = this;
playerGears = new HashMap<String, PlayerGear>(); playerGears = new HashMap<String, PlayerGear>();
// TODO: Introduce configurable non-hardcoded values for generation weights?
_attributeWeights = new WeightSet<Integer>(new Weight<Integer>(3, 3), new Weight<Integer>(20, 2), new Weight<Integer>(77, 1)); _attributeWeights = new WeightSet<Integer>(new Weight<Integer>(3, 3), new Weight<Integer>(20, 2), new Weight<Integer>(77, 1));
_itemWeights = new WeightSet<Boolean>(new Weight<Boolean>(90, true), new Weight<Boolean>(10, false)); _itemWeights = new WeightSet<Boolean>(new Weight<Boolean>(90, true), new Weight<Boolean>(10, false));
_itemWeights = new WeightSet<Boolean>(new Weight<Boolean>(50, true), new Weight<Boolean>(50, false)); _itemWeights = new WeightSet<Boolean>(new Weight<Boolean>(50, true), new Weight<Boolean>(50, false));
System.out.println("-Testting-testing"); System.out.println("-Testting-testing");
System.out.println(Utility.currentTimeSeconds()); System.out.println(Utility.currentTimeSeconds());
// Register listeners
UtilServer.getServer().getPluginManager().registerEvents(new ItemListener(), getPlugin());
UtilServer.getServer().getPluginManager().registerEvents(new SmeltingListener(), getPlugin());
// adding all different container classes with their flag
RuntimeTypeAdapterFactory<ItemAttribute> typeFactory = RuntimeTypeAdapterFactory
.of(ItemAttribute.class, "AttributeType")
.registerSubtype(SharpAttribute.class)
.registerSubtype(FrostedAttribute.class)
.registerSubtype(FlamingAttribute.class); // TODO: Register all item attributes automatically
RuntimeTypeAdapterFactory<CustomItem> customItemType = RuntimeTypeAdapterFactory
.of(CustomItem.class, "ItemType")
.registerSubtype(AlligatorsTooth.class)
.registerSubtype(WindBlade.class); // TODO: Register all legendary weapons automatically
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapterFactory(typeFactory);
builder.registerTypeAdapterFactory(customItemType);
_gson = builder.create();
packetHandler.addPacketHandler(this);
} }
@Override @Override
@ -88,12 +133,94 @@ public class GearManager extends MiniPlugin
public static CustomItem parseItem(ItemStack item) public static CustomItem parseItem(ItemStack item)
{ {
return null; // TODO: Parse CustomItem from hidden JSON-encoded lore string in item passed in String serialization = getItemSerialization(item);
if (serialization != null)
{
CustomItem customItem = deserialize(serialization);
return customItem;
}
return null; // No serialization found in item's lore, not a custom item!
} }
public static boolean isCustomItem(ItemStack item) public static boolean isCustomItem(ItemStack item)
{ {
return parseItem(item) != null; // TODO: Check for JSON-encoded lore string instead of deserializing? return getItemSerialization(item) != null;
}
public static String getItemSerialization(CustomItem customItem)
{
String tempSeri = serialize(customItem);
String serialization = ITEM_SERIALIZATION_TAG;
for (int i = 0; i < tempSeri.length(); i++)
{
if (i % 40 == 39)
{
serialization += "\n";
}
serialization += tempSeri.charAt(i);
}
return serialization;
}
private static String getItemSerialization(ItemStack item)
{
if (item == null || item.getItemMeta() == null
|| item.getItemMeta().getLore() == null) return null;
ItemMeta meta = item.getItemMeta();
/*for (String lore : meta.getLore())
{
if (lore.startsWith(ITEM_SERIALIZATION_TAG)) // Found serialization lore-line
{
int tagLength = ITEM_SERIALIZATION_TAG.length();
String serialization = lore.substring(tagLength);
return serialization;
}
}*/
// TODO: Implement packet intercepting to hide json encoded lore
List<String> lore = meta.getLore();
boolean serialized = false;
String serialization = "";
for (int i = 0; i < lore.size(); i++)
{
String line = lore.get(i);
if (line.startsWith(ITEM_SERIALIZATION_TAG))
{
serialized = true;
serialization += line.substring(ITEM_SERIALIZATION_TAG.length());
}
else if (serialized)
{
serialization += line;
}
}
if (serialized)
{
return serialization;
}
return null; // Unable to find any serialized lore lines, hence not a CustomItem.
}
public static String serialize(CustomItem customItem)
{
return _gson.toJson(customItem);
}
public static CustomItem deserialize(String serialization)
{
System.out.println("Serialization: " + serialization);
return _gson.fromJson(serialization, CustomItem.class);
} }
/** /**
@ -103,4 +230,18 @@ public class GearManager extends MiniPlugin
{ {
return _instance; return _instance;
} }
@Override
public void handle(PacketInfo packetInfo)
{
Packet packet = packetInfo.getPacket();
if (packet instanceof PacketPlayOutSetSlot)
{
System.out.println("Item slot packet!");
PacketPlayOutSetSlot slotPacket = (PacketPlayOutSetSlot) packet;
}
}
} }

View File

@ -6,7 +6,9 @@ import org.bukkit.event.Listener;
import org.bukkit.event.block.Action; import org.bukkit.event.block.Action;
import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDamageEvent; import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerItemHeldEvent;
/** /**
* Listens for item-related trigger events and accordingly triggers appropriate * Listens for item-related trigger events and accordingly triggers appropriate
@ -17,6 +19,31 @@ import org.bukkit.event.player.PlayerInteractEvent;
public class ItemListener implements Listener public class ItemListener implements Listener
{ {
/**
* Handle players shuffling CustomItems around by properly updating
* and managing their movement.
* @param event
*/
@EventHandler
public void onInventoryClick(InventoryClickEvent event)
{
// TODO: Update any custom-items that are selected/moved to save proper stats if they
// TODO: are active. (IE: PlayerGear possesses it as armor slot or weapon)
}
/**
* Handle updated CustomItem stats and lore upon player
* switching items.
* @param event
*/
@EventHandler
public void onItemHeldChanged(PlayerItemHeldEvent event)
{
Player player = event.getPlayer();
PlayerGear gear = getGear(player);
gear.onItemHeldChanged(event);
}
/** /**
* Handle the trigger of custom gear related effects and abilities. * Handle the trigger of custom gear related effects and abilities.

View File

@ -7,7 +7,9 @@ import org.bukkit.Bukkit;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerItemHeldEvent;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
/** /**
* PlayerGear caches and manages a players set of {@link CustomItem}s that * PlayerGear caches and manages a players set of {@link CustomItem}s that
@ -17,7 +19,6 @@ import org.bukkit.inventory.ItemStack;
*/ */
public class PlayerGear public class PlayerGear
{ {
private String _playerName; // Name of player who owns the gear private String _playerName; // Name of player who owns the gear
// Cached custom item information for player's gear // Cached custom item information for player's gear
@ -44,6 +45,14 @@ public class PlayerGear
return Bukkit.getPlayer(_playerName); return Bukkit.getPlayer(_playerName);
} }
/**
* @return the {@link PlayerInventory} associated with the owner of this {@link PlayerGear}.
*/
public PlayerInventory getInventory()
{
return getPlayer().getInventory();
}
/** /**
* Trigger interact events for the set of equipped {@link CustomItem}s in gear set. * Trigger interact events for the set of equipped {@link CustomItem}s in gear set.
* @param event - the triggering interact event * @param event - the triggering interact event
@ -80,6 +89,21 @@ public class PlayerGear
} }
} }
/**
* Update appropriate gear status and item lores.
* @param event - the triggering item held change event.
*/
public void onItemHeldChanged(PlayerItemHeldEvent event)
{
ItemStack item = getPlayer().getItemInHand();
CustomItem weapon = getWeapon();
if (weapon != null)
{
weapon.update(item); // Update held-item's stats.
}
}
public CustomItem getWeapon() public CustomItem getWeapon()
{ {
ItemStack weaponItem = getPlayer().getInventory().getItemInHand(); ItemStack weaponItem = getPlayer().getInventory().getItemInHand();
@ -146,11 +170,11 @@ public class PlayerGear
public Set<CustomItem> getGear() public Set<CustomItem> getGear()
{ {
Set<CustomItem> items = new HashSet<CustomItem>(); Set<CustomItem> items = new HashSet<CustomItem>();
items.add(getWeapon()); if (getWeapon() != null) items.add(getWeapon());
items.add(getHelmet()); if (getHelmet() != null) items.add(getHelmet());
items.add(getChestplate()); if (getChestplate() != null) items.add(getChestplate());
items.add(getLeggings()); if (getLeggings() != null) items.add(getLeggings());
items.add(getBoots()); if (getBoots() != null) items.add(getBoots());
return items; return items;
} }
@ -158,8 +182,13 @@ public class PlayerGear
{ {
if (customItem == null || item == null) return false; if (customItem == null || item == null) return false;
// TODO: Implement more sophisticated match-checking that checks hidden UUID if (GearManager.isCustomItem(item))
return customItem.getDisplayName().equals(item.getItemMeta().getDisplayName()); {
CustomItem customItem2 = GearManager.parseItem(item);
return customItem2.matches(customItem);
}
return false;
} }
private CustomItem parseItem(ItemStack item) private CustomItem parseItem(ItemStack item)

View File

@ -18,6 +18,12 @@ public abstract class ItemAttribute
* @return the attribute name display to players. * @return the attribute name display to players.
*/ */
public abstract String getDisplayName(); public abstract String getDisplayName();
/**
* @return a user-friendly description of this attribute, entailing it's effects
* and current associated values.
*/
public String getDescription() { return "???IMPLEMENT"; }
public void onInteract(PlayerInteractEvent event) public void onInteract(PlayerInteractEvent event)
{ {

View File

@ -6,6 +6,7 @@ public abstract class FlatReductionAttribute extends ReductionAttribute
{ {
private double _reduction; private double _reduction;
public double getFlatReduction() { return _reduction; }
public FlatReductionAttribute(ValueDistribution reductionGen, ReductionConfig config) public FlatReductionAttribute(ValueDistribution reductionGen, ReductionConfig config)
{ {

View File

@ -23,4 +23,9 @@ public class LavaAttribute extends PercentReductionAttribute
return "Lava Forged"; return "Lava Forged";
} }
@Override
public String getDescription()
{
return String.format("Reduce fire-related damage by %.2f percent.", getReductionPercent());
}
} }

View File

@ -21,5 +21,10 @@ public class PaddedAttribute extends FlatReductionAttribute
{ {
return "Padded"; return "Padded";
} }
@Override
public String getDescription()
{
return String.format("Reduce fall damage by %.2f half-hearts.", getFlatReduction());
}
} }

View File

@ -4,8 +4,8 @@ import mineplex.game.clans.items.generation.ValueDistribution;
public abstract class PercentReductionAttribute extends ReductionAttribute public abstract class PercentReductionAttribute extends ReductionAttribute
{ {
private double _reductionPercent; private double _reductionPercent;
public double getReductionPercent() { return _reductionPercent; }
public PercentReductionAttribute(ValueDistribution reductionGen, ReductionConfig config) public PercentReductionAttribute(ValueDistribution reductionGen, ReductionConfig config)
{ {

View File

@ -24,4 +24,9 @@ public class ProtectionAttribute extends FlatReductionAttribute
return "Protection"; return "Protection";
} }
@Override
public String getDescription()
{
return String.format("Reduce incoming attack damage by %.2f half-hearts.", getFlatReduction());
}
} }

View File

@ -21,4 +21,9 @@ public class ReinforcedAttribute extends FlatReductionAttribute
return "Reinforced"; return "Reinforced";
} }
@Override
public String getDescription()
{
return String.format("Reduce incoming enemy attacks by %.2f half-hearts.", getFlatReduction());
}
} }

View File

@ -22,4 +22,9 @@ public class SlantedAttribute extends FlatReductionAttribute
return "Slanted"; return "Slanted";
} }
@Override
public String getDescription()
{
return String.format("Reduce arrow damage by %.2f half-hearts.", getFlatReduction());
}
} }

View File

@ -15,6 +15,8 @@ public abstract class AttackAttribute extends ItemAttribute
{ {
private int _attackLimit; private int _attackLimit;
public int getAttackLimit() { return _attackLimit; }
private int _attackCount; private int _attackCount;
public AttackAttribute(int attackLimit) public AttackAttribute(int attackLimit)

View File

@ -21,6 +21,12 @@ public class ConqueringAttribute extends DamageAttribute
return "Conquering"; // TODO: Fill in name return "Conquering"; // TODO: Fill in name
} }
@Override
public String getDescription()
{
return String.format("Deal an extra %.2f hearts of damage to mobs.", getBonusDamage());
}
@Override @Override
public boolean grantBonusDamage(Entity entity) public boolean grantBonusDamage(Entity entity)
{ {

View File

@ -9,6 +9,7 @@ import org.bukkit.event.entity.EntityDamageByEntityEvent;
public abstract class DamageAttribute extends ItemAttribute public abstract class DamageAttribute extends ItemAttribute
{ {
private double _bonusDamage; private double _bonusDamage;
public double getBonusDamage() { return _bonusDamage; }
public DamageAttribute(ValueDistribution damageGen) public DamageAttribute(ValueDistribution damageGen)
{ {

View File

@ -24,6 +24,12 @@ public class FlamingAttribute extends AttackAttribute
return "Flaming"; // TODO: Fill in name return "Flaming"; // TODO: Fill in name
} }
@Override
public String getDescription()
{
return String.format("Enemies catch fire for %d ticks every %d attacks.", _fireDuration, getAttackLimit());
}
@Override @Override
public void triggerAttack(Entity attacker, Entity defender) public void triggerAttack(Entity attacker, Entity defender)
{ {

View File

@ -16,12 +16,11 @@ import org.bukkit.potion.PotionEffectType;
*/ */
public class FrostedAttribute extends ItemAttribute public class FrostedAttribute extends ItemAttribute
{ {
public final int TICKS_PER_SECOND = 20; // Number of ticks per second for in-game logic.
private static ValueDistribution amountGen = generateDistribution(0, 3); // Value generator for slow amount range private static ValueDistribution amountGen = generateDistribution(0, 3); // Value generator for slow amount range
private static ValueDistribution durationGen = generateDistribution(1, 3); // Value generator for slow duration range private static ValueDistribution durationGen = generateDistribution(20, 60); // Value generator for slow duration range
private int _slowAmount; // The slowness level/amplifier private int _slowAmount; // The slowness level/amplifier
private double _slowDuration; // The duration (in seconds) of slow effect private int _slowDuration; // The duration (in ticks) of slow effect
/** /**
* Class constructor * Class constructor
@ -29,7 +28,7 @@ public class FrostedAttribute extends ItemAttribute
public FrostedAttribute() public FrostedAttribute()
{ {
_slowAmount = amountGen.generateIntValue(); _slowAmount = amountGen.generateIntValue();
_slowDuration = durationGen.generateValue(); _slowDuration = durationGen.generateIntValue();
} }
@Override @Override
@ -38,6 +37,12 @@ public class FrostedAttribute extends ItemAttribute
return "Frosted"; return "Frosted";
} }
@Override
public String getDescription()
{
return String.format("Apply slowness %d for %d ticks to enemies.", _slowAmount, _slowDuration);
}
@Override @Override
public void onAttacked(EntityDamageByEntityEvent event) public void onAttacked(EntityDamageByEntityEvent event)
{ {
@ -51,7 +56,6 @@ public class FrostedAttribute extends ItemAttribute
private PotionEffect generateSlowEffect() private PotionEffect generateSlowEffect()
{ {
int duration = (int) (_slowDuration / TICKS_PER_SECOND); return new PotionEffect(PotionEffectType.SLOW, _slowDuration, _slowAmount);
return new PotionEffect(PotionEffectType.SLOW, duration, _slowAmount);
} }
} }

View File

@ -32,6 +32,12 @@ public class HasteAttribute extends AttackAttribute
return "Haste"; // TODO: Fill in name return "Haste"; // TODO: Fill in name
} }
@Override
public String getDescription()
{
return String.format("Gain speed %d for %d ticks every %d attacks.", _speedAmount, _speedDuration, getAttackLimit());
}
@Override @Override
public void triggerAttack(Entity attacker, Entity defender) public void triggerAttack(Entity attacker, Entity defender)
{ {

View File

@ -21,4 +21,5 @@ public class HeavyAttribute extends ItemAttribute
return ""; // TODO: Fill in name return ""; // TODO: Fill in name
} }
} }

View File

@ -21,6 +21,12 @@ public class JaggedAttribute extends AttackAttribute
return "Jagged"; // TODO: Fill in name return "Jagged"; // TODO: Fill in name
} }
@Override
public String getDescription()
{
return String.format("Temporarily halt enemies every %d attacks.", getAttackLimit());
}
@Override @Override
public void triggerAttack(Entity attacker, Entity defender) public void triggerAttack(Entity attacker, Entity defender)
{ {

View File

@ -20,6 +20,12 @@ public class SharpAttribute extends DamageAttribute
return "Sharp"; // TODO: Fill in name return "Sharp"; // TODO: Fill in name
} }
@Override
public String getDescription()
{
return String.format("Deal an extra %.2f damage.", getBonusDamage());
}
@Override @Override
public boolean grantBonusDamage(Entity defender) public boolean grantBonusDamage(Entity defender)
{ {

View File

@ -24,6 +24,12 @@ public class VampiricAttribute extends ItemAttribute
return "Vampiric"; // TODO: Fill in name return "Vampiric"; // TODO: Fill in name
} }
@Override
public String getDescription()
{
return String.format("Heal yourself for %d percentage of damage dealt to enemy players.", _healPercent);
}
@Override @Override
public void onAttack(EntityDamageByEntityEvent event) public void onAttack(EntityDamageByEntityEvent event)
{ {

View File

@ -1,9 +1,13 @@
package mineplex.game.clans.items.commands; package mineplex.game.clans.items.commands;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
import org.bukkit.Chunk; import org.bukkit.Chunk;
import org.bukkit.Material;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import mineplex.core.command.CommandBase; import mineplex.core.command.CommandBase;
import mineplex.core.common.Rank; import mineplex.core.common.Rank;
@ -22,7 +26,13 @@ import mineplex.game.clans.clans.ClanRole;
import mineplex.game.clans.clans.ClansManager; import mineplex.game.clans.clans.ClansManager;
import mineplex.game.clans.clans.ClansUtility.ClanRelation; import mineplex.game.clans.clans.ClansUtility.ClanRelation;
import mineplex.game.clans.clans.ClientClan; import mineplex.game.clans.clans.ClientClan;
import mineplex.game.clans.items.CustomItem;
import mineplex.game.clans.items.GearManager; import mineplex.game.clans.items.GearManager;
import mineplex.game.clans.items.attributes.weapon.FlamingAttribute;
import mineplex.game.clans.items.attributes.weapon.FrostedAttribute;
import mineplex.game.clans.items.attributes.weapon.SharpAttribute;
import mineplex.game.clans.items.legendaries.LegendaryItem;
import mineplex.game.clans.items.legendaries.WindBlade;
public class GearCommand extends CommandBase<GearManager> public class GearCommand extends CommandBase<GearManager>
{ {
@ -36,6 +46,30 @@ public class GearCommand extends CommandBase<GearManager>
{ {
UtilPlayer.message(caller, F.main("Gear", "Opening custom gear GUI!")); UtilPlayer.message(caller, F.main("Gear", "Opening custom gear GUI!"));
if (caller.getItemInHand() != null && caller.getItemInHand().getType() != Material.AIR)
{
ItemStack item = caller.getItemInHand();
ItemMeta meta = item.getItemMeta();
List<String> lore = meta.getLore();
lore.set(0, "Test");
meta.setLore(lore);
item.setItemMeta(meta);
System.out.println("aaaaaaa");
return;
}
else
{
LegendaryItem legendary = new WindBlade();
caller.setItemInHand(legendary.toItemStack(1));
return;
}
/*CustomItem customItem = new CustomItem();
customItem.setPrefix(new FrostedAttribute());
customItem.setSuperPrefix(new SharpAttribute());
customItem.setSuffix(new FlamingAttribute());
ItemStack sword = customItem.toItemStack(1);
caller.setItemInHand(sword);*/
// TODO: Open custom gear GUI here // TODO: Open custom gear GUI here
} }

View File

@ -3,19 +3,26 @@ package mineplex.game.clans.items.legendaries;
import mineplex.game.clans.items.attributes.ItemAttribute; import mineplex.game.clans.items.attributes.ItemAttribute;
import mineplex.game.clans.items.generation.ValueDistribution; import mineplex.game.clans.items.generation.ValueDistribution;
import org.bukkit.Material;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.util.Vector;
public class AlligatorsTooth extends LegendaryItem public class AlligatorsTooth extends LegendaryItem
{ {
private static ValueDistribution damageGen = generateDistribution(1.0d, 5.0d); private static ValueDistribution boostGen = generateDistribution(0.6d, 1.2d);
private static ValueDistribution damageGen = generateDistribution(1.0d, 6.0d);
private double _damageBonus; private double _damageBonus;
private double _swimSpeed;
public AlligatorsTooth() public AlligatorsTooth()
{ {
super("Alligators Tooth", "Grants bonus damage in water and special ability to swim fast!", Material.RAW_FISH);
_damageBonus = damageGen.generateValue(); _damageBonus = damageGen.generateValue();
_swimSpeed = boostGen.generateValue();
} }
@Override @Override
@ -41,7 +48,9 @@ public class AlligatorsTooth extends LegendaryItem
private void propelPlayer(Player player) private void propelPlayer(Player player)
{ {
// TODO: Propel player forward with 0.6 to 1.2 velocity Vector direction = player.getLocation().getDirection().normalize();
direction.multiply(_swimSpeed);
player.setVelocity(direction);
} }
private boolean isInWater(Player player) private boolean isInWater(Player player)

View File

@ -3,6 +3,7 @@ package mineplex.game.clans.items.legendaries;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import org.bukkit.Material;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffect;
@ -11,6 +12,11 @@ import org.bukkit.potion.PotionEffectType;
public class GiantsBroadsword extends LegendaryItem public class GiantsBroadsword extends LegendaryItem
{ {
public GiantsBroadsword()
{
super("Giants Broadsword", "Deal huge damage and block to gain defensive abilities!", Material.PAPER);
}
@Override @Override
public void update(Player wielder) public void update(Player wielder)
{ {
@ -24,6 +30,12 @@ public class GiantsBroadsword extends LegendaryItem
public void onAttack(EntityDamageByEntityEvent event, Player wielder) public void onAttack(EntityDamageByEntityEvent event, Player wielder)
{ {
// TODO: Buff knockback and damage values (What are specific values?) // TODO: Buff knockback and damage values (What are specific values?)
// Chiss: Make 'em up
double bonusDamage = 2.0d; // Too much?
event.setDamage(event.getDamage() + bonusDamage);
// TODO: Apply knockback bonus using previous Clans Custom combat events
} }
private void buffPlayer(Player player) private void buffPlayer(Player player)

View File

@ -1,5 +1,6 @@
package mineplex.game.clans.items.legendaries; package mineplex.game.clans.items.legendaries;
import org.bukkit.Material;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.block.Action; import org.bukkit.event.block.Action;
import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.entity.EntityDamageByEntityEvent;
@ -19,6 +20,13 @@ public class LegendaryItem extends CustomItem
public LegendaryItem() public LegendaryItem()
{ {
this(null, null, null); // TODO: Never used, just to skip base implementation for testing.
}
public LegendaryItem(String name, String description, Material material)
{
super(name, description, material);
_lastBlock = 0l; _lastBlock = 0l;
} }

View File

@ -3,34 +3,52 @@ package mineplex.game.clans.items.legendaries;
import mineplex.game.clans.items.attributes.ItemAttribute; import mineplex.game.clans.items.attributes.ItemAttribute;
import mineplex.game.clans.items.generation.ValueDistribution; import mineplex.game.clans.items.generation.ValueDistribution;
import org.bukkit.Material;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.util.Vector;
public class WindBlade extends LegendaryItem public class WindBlade extends LegendaryItem
{ {
public static final double FLIGHT_VELOCITY = 0.25d;
public static final int MAX_FLIGHT_TIME = 80; // Max flight of 80 ticks public static final int MAX_FLIGHT_TIME = 80; // Max flight of 80 ticks
private long _flightTime; // Time (in ticks) since last touching ground and flying private long _flightTime; // Time (in ticks) since last touching ground and flying
public WindBlade() public WindBlade()
{ {
super("Wind Blade", "Activate flying ability to take flight for 80 ticks before landing!", Material.STICK); // TODO: Configurable?
_flightTime = 0; _flightTime = 0;
} }
@Override @Override
public void update(Player wielder) public void update(Player wielder)
{ {
Entity entity = (Entity) wielder;
// Check if player is attempting to fly and activate
if (isHoldingRightClick() && canPropel()) if (isHoldingRightClick() && canPropel())
{ {
propelPlayer(wielder); propelPlayer(wielder);
} }
// Check if player has touched down
if (entity.isOnGround())
{
_flightTime = 0;
}
} }
private void propelPlayer(Player player) private void propelPlayer(Player player)
{ {
_flightTime++; _flightTime++;
// TODO: Propel player forward with ??? velocity Vector direction = player.getLocation().getDirection().normalize();
direction.multiply(FLIGHT_VELOCITY); // Set velocity magnitude
player.setVelocity(direction);
} }
private boolean canPropel() private boolean canPropel()

View File

@ -1,5 +1,7 @@
package mineplex.game.clans.items.smelting; package mineplex.game.clans.items.smelting;
import mineplex.game.clans.items.GearManager;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
@ -9,21 +11,32 @@ public class Smelter
public static void smeltItemInHand(Player player) public static void smeltItemInHand(Player player)
{ {
// Smelt item in hand for player
ItemStack item = player.getInventory().getItemInHand(); ItemStack item = player.getInventory().getItemInHand();
if (item != null) if (isSmeltable(item))
{ {
ItemStack returns = smeltItem(item); ItemStack returns = smeltItem(item);
player.getInventory().setItemInHand(returns); player.getInventory().setItemInHand(returns);
} }
// TODO: Notify player of smelt success/failure?
} }
public static ItemStack smeltItem(ItemStack item) public static ItemStack smeltItem(ItemStack item)
{ {
Material material = getSmeltedType(item.getType()); Material material = getSmeltedType(item.getType());
int maxAmount = getSmeltAmount(item.getType()); int maxAmount = getSmeltAmount(item.getType());
int amount = maxAmount; // TODO: Determine proportional return on smelt depending on type/durability int amount = maxAmount;
if (!GearManager.isCustomItem(item))
{
short maxDurability = item.getType().getMaxDurability();
int durability = maxDurability - item.getDurability();
double percent = durability / (double) maxDurability;
System.out.println("Durability: " + item.getDurability() + " -- max: " + item.getType().getMaxDurability() + " --- percent: " + percent);
amount = Math.max(1, (int) (maxAmount * percent));
}
return new ItemStack(material, amount); return new ItemStack(material, amount);
} }
@ -60,6 +73,13 @@ public class Smelter
} }
} }
private static boolean isSmeltable(ItemStack item)
{
if (item == null) return false;
return getSmeltedType(item.getType()) != null;
}
private static Material getSmeltedType(Material itemType) private static Material getSmeltedType(Material itemType)
{ {
switch (itemType) switch (itemType)

View File

@ -41,6 +41,8 @@ public class Utility
*/ */
public static <T> T deserialize(String serializedData, Class<T> type) public static <T> T deserialize(String serializedData, Class<T> type)
{ {
if (serializedData == null) return null;
return _gson.fromJson(serializedData, type); return _gson.fromJson(serializedData, type);
} }

View File

@ -0,0 +1,240 @@
/*
* Copyright (C) 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package mineplex.serverdata.serialization;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.Streams;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
/**
* Adapts values whose runtime type may differ from their declaration type. This
* is necessary when a field's type is not the same type that GSON should create
* when deserializing that field. For example, consider these types:
* <pre> {@code
* abstract class Shape {
* int x;
* int y;
* }
* class Circle extends Shape {
* int radius;
* }
* class Rectangle extends Shape {
* int width;
* int height;
* }
* class Diamond extends Shape {
* int width;
* int height;
* }
* class Drawing {
* Shape bottomShape;
* Shape topShape;
* }
* }</pre>
* <p>Without additional type information, the serialized JSON is ambiguous. Is
* the bottom shape in this drawing a rectangle or a diamond? <pre> {@code
* {
* "bottomShape": {
* "width": 10,
* "height": 5,
* "x": 0,
* "y": 0
* },
* "topShape": {
* "radius": 2,
* "x": 4,
* "y": 1
* }
* }}</pre>
* This class addresses this problem by adding type information to the
* serialized JSON and honoring that type information when the JSON is
* deserialized: <pre> {@code
* {
* "bottomShape": {
* "type": "Diamond",
* "width": 10,
* "height": 5,
* "x": 0,
* "y": 0
* },
* "topShape": {
* "type": "Circle",
* "radius": 2,
* "x": 4,
* "y": 1
* }
* }}</pre>
* Both the type field name ({@code "type"}) and the type labels ({@code
* "Rectangle"}) are configurable.
*
* <h3>Registering Types</h3>
* Create a {@code RuntimeTypeAdapter} by passing the base type and type field
* name to the {@link #of} factory method. If you don't supply an explicit type
* field name, {@code "type"} will be used. <pre> {@code
* RuntimeTypeAdapter<Shape> shapeAdapter
* = RuntimeTypeAdapter.of(Shape.class, "type");
* }</pre>
* Next register all of your subtypes. Every subtype must be explicitly
* registered. This protects your application from injection attacks. If you
* don't supply an explicit type label, the type's simple name will be used.
* <pre> {@code
* shapeAdapter.registerSubtype(Rectangle.class, "Rectangle");
* shapeAdapter.registerSubtype(Circle.class, "Circle");
* shapeAdapter.registerSubtype(Diamond.class, "Diamond");
* }</pre>
* Finally, register the type adapter in your application's GSON builder:
* <pre> {@code
* Gson gson = new GsonBuilder()
* .registerTypeAdapter(Shape.class, shapeAdapter)
* .create();
* }</pre>
* Like {@code GsonBuilder}, this API supports chaining: <pre> {@code
* RuntimeTypeAdapter<Shape> shapeAdapter = RuntimeTypeAdapterFactory.of(Shape.class)
* .registerSubtype(Rectangle.class)
* .registerSubtype(Circle.class)
* .registerSubtype(Diamond.class);
* }</pre>
*/
public final class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory {
private final Class<?> baseType;
private final String typeFieldName;
private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<String, Class<?>>();
private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<Class<?>, String>();
private RuntimeTypeAdapterFactory(Class<?> baseType, String typeFieldName) {
if (typeFieldName == null || baseType == null) {
throw new NullPointerException();
}
this.baseType = baseType;
this.typeFieldName = typeFieldName;
}
/**
* Creates a new runtime type adapter using for {@code baseType} using {@code
* typeFieldName} as the type field name. Type field names are case sensitive.
*/
public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName) {
return new RuntimeTypeAdapterFactory<T>(baseType, typeFieldName);
}
/**
* Creates a new runtime type adapter for {@code baseType} using {@code "type"} as
* the type field name.
*/
public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType) {
return new RuntimeTypeAdapterFactory<T>(baseType, "type");
}
/**
* Registers {@code type} identified by {@code label}. Labels are case
* sensitive.
*
* @throws IllegalArgumentException if either {@code type} or {@code label}
* have already been registered on this type adapter.
*/
public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, String label) {
if (type == null || label == null) {
throw new NullPointerException();
}
if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) {
throw new IllegalArgumentException("types and labels must be unique");
}
labelToSubtype.put(label, type);
subtypeToLabel.put(type, label);
return this;
}
/**
* Registers {@code type} identified by its {@link Class#getSimpleName simple
* name}. Labels are case sensitive.
*
* @throws IllegalArgumentException if either {@code type} or its simple name
* have already been registered on this type adapter.
*/
public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type) {
return registerSubtype(type, type.getSimpleName());
}
public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {
if (type.getRawType() != baseType) {
return null;
}
final Map<String, TypeAdapter<?>> labelToDelegate
= new LinkedHashMap<String, TypeAdapter<?>>();
final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate
= new LinkedHashMap<Class<?>, TypeAdapter<?>>();
for (Map.Entry<String, Class<?>> entry : labelToSubtype.entrySet()) {
TypeAdapter<?> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue()));
labelToDelegate.put(entry.getKey(), delegate);
subtypeToDelegate.put(entry.getValue(), delegate);
}
return new TypeAdapter<R>() {
@Override public R read(JsonReader in) throws IOException {
JsonElement jsonElement = Streams.parse(in);
JsonElement labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName);
if (labelJsonElement == null) {
throw new JsonParseException("cannot deserialize " + baseType
+ " because it does not define a field named " + typeFieldName);
}
String label = labelJsonElement.getAsString();
@SuppressWarnings("unchecked") // registration requires that subtype extends T
TypeAdapter<R> delegate = (TypeAdapter<R>) labelToDelegate.get(label);
if (delegate == null) {
throw new JsonParseException("cannot deserialize " + baseType + " subtype named "
+ label + "; did you forget to register a subtype?");
}
return delegate.fromJsonTree(jsonElement);
}
@Override public void write(JsonWriter out, R value) throws IOException {
Class<?> srcType = value.getClass();
String label = subtypeToLabel.get(srcType);
@SuppressWarnings("unchecked") // registration requires that subtype extends T
TypeAdapter<R> delegate = (TypeAdapter<R>) subtypeToDelegate.get(srcType);
if (delegate == null) {
throw new JsonParseException("cannot serialize " + srcType.getName()
+ "; did you forget to register a subtype?");
}
JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject();
if (jsonObject.has(typeFieldName)) {
throw new JsonParseException("cannot serialize " + srcType.getName()
+ " because it already defines a field named " + typeFieldName);
}
JsonObject clone = new JsonObject();
clone.add(typeFieldName, new JsonPrimitive(label));
for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) {
clone.add(e.getKey(), e.getValue());
}
Streams.write(clone, out);
}
};
}
}