package electroblob.wizardry.data; import com.google.common.collect.EvictingQueue; import electroblob.wizardry.Wizardry; import electroblob.wizardry.constants.Tier; import electroblob.wizardry.enchantment.Imbuement; import electroblob.wizardry.entity.living.ISummonedCreature; import electroblob.wizardry.event.SpellCastEvent; import electroblob.wizardry.event.SpellCastEvent.Source; import electroblob.wizardry.item.ItemWand; import electroblob.wizardry.packet.PacketCastContinuousSpell; import electroblob.wizardry.packet.PacketPlayerSync; import electroblob.wizardry.packet.WizardryPacketHandler; import electroblob.wizardry.registry.Spells; import electroblob.wizardry.spell.None; import electroblob.wizardry.spell.Spell; import electroblob.wizardry.util.NBTExtras; import electroblob.wizardry.util.SpellModifiers; import net.minecraft.enchantment.Enchantment; import net.minecraft.entity.Entity; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.init.Items; import net.minecraft.item.ItemEnchantedBook; import net.minecraft.item.ItemStack; import net.minecraft.nbt.*; import net.minecraft.util.EnumFacing; import net.minecraft.util.EnumHand; import net.minecraft.util.ResourceLocation; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.capabilities.Capability; import net.minecraftforge.common.capabilities.Capability.IStorage; import net.minecraftforge.common.capabilities.CapabilityInject; import net.minecraftforge.common.capabilities.CapabilityManager; import net.minecraftforge.common.capabilities.ICapabilitySerializable; import net.minecraftforge.common.util.Constants.NBT; import net.minecraftforge.common.util.INBTSerializable; import net.minecraftforge.event.AttachCapabilitiesEvent; import net.minecraftforge.event.entity.EntityJoinWorldEvent; import net.minecraftforge.event.entity.living.LivingEvent.LivingUpdateEvent; import net.minecraftforge.event.entity.player.PlayerEvent; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.network.simpleimpl.IMessage; import javax.annotation.Nullable; import java.lang.ref.WeakReference; import java.util.*; import java.util.stream.Collectors; import java.util.AbstractMap.SimpleEntry; /** * Capability-based replacement for the old ExtendedPlayer class from 1.7.10. This has been reworked to leave minimum * external changes (for my own sanity, mainly!). Turns out the only major difference between an internal capability and * an IEEP is a couple of redundant classes and a different way of registering it. *

* Forge seems to have separate classes to hold the Capability<...> instance ('key') and methods for getting the * capability, but in my opinion there are already too many classes to deal with, so I'm not adding any more than are * necessary, meaning those constants and values are kept here instead. * * @since Wizardry 2.1 * @author Electroblob */ // On the plus side, having to rethink this class allowed me to clean it up a lot. @Mod.EventBusSubscriber public class WizardData implements INBTSerializable { /** Static instance of what I like to refer to as the capability key. Private because, well, it's internal! */ // This annotation does some crazy Forge magic behind the scenes and assigns this field a value. @CapabilityInject(WizardData.class) private static final Capability WIZARD_DATA_CAPABILITY = null; /** Internal storage of registered variable keys. This only contains the stored keys. */ private static final Set storedVariables = new HashSet<>(); /** The maximum number of recent spells to track. */ public static int MAX_RECENT_SPELLS; private static final int IMBUEMENT_UPDATE_INTERVAL = 20; /** The player this WizardData instance belongs to. */ private final EntityPlayer player; /** An instance of {@link Random} which is guaranteed to produce the same number sequence client and server * side provided that it is always called from common code. This can be useful in reducing the number of * packets sent in certain situations.
*
* This is achieved by setting the seed to a new random value each time {@link WizardData#sync()} is called and * sending this to the client so it can also set its seed to that value. */ public final Random synchronisedRandom; /** Whether this player is currently casting a continuous spell via commands. Not saved over world reload and reset * on player death. */ private Spell castCommandSpell; /** The time for which this player has been casting a continuous spell via commands. Increments by 1 each tick. Not * saved over world reload and reset on player death. */ private int castCommandTick; /** SpellModifiers object for the current continuous spell cast via commands. Not saved over world reload and reset * on player death. */ private SpellModifiers castCommandModifiers; /** The number of ticks this player's current continuous spell lasts for, or null if there is none. Not saved over * world reload and reset on player death. */ private int castCommandDuration; /** SpellModifiers object for the current continuous spell cast via items. Not saved over world reload and reset * on player death. N.B. Since a player can only use one item at a time, this can be reused for any item that * casts spells, it's not just for wands.*/ public SpellModifiers itemCastingModifiers; public WeakReference selectedMinion; /** Set of this player's discovered spells. Do not write to this list directly, use * {@link WizardData#discoverSpell(Spell)} instead. */ public Set spellsDiscovered; /** The highest {@link Tier} this player has upgraded a wand to, used to apply progression modifiers. */ private Tier maxTierReached = Tier.NOVICE; private Set allies; /** List of usernames of this player's allies. May not be accurate 100% of the time. This is here so that a player * can view the usernames of their allies even when those allies are not online. Do not use this for any other * purpose than displaying the names! */ public Set allyNames; /** Internal storage of custom (spell-specific) data. Note that a {@code Map} cannot specify that its values are of * the same type as the type parameter of its keys, so to ensure this condition always holds, the map must only * be modified via {@link WizardData#setVariable(IVariable, Object)}, which (as a method) is able to enforce it. */ private final Map spellData; private Queue> recentSpells; // This one is still necessary, because I can't override the equip animation for items that aren't from Wizardry. // Leaving this for now because merging it into the spell data system will be more tricky private Map imbuementDurations; /** Stores this player's y velocity from the previous tick; used for the velocity-based fall damage replacement. */ public double prevMotionY; public WizardData(){ this(null); // Nullary constructor for the registration method factory parameter } public WizardData(EntityPlayer player){ this.player = player; this.synchronisedRandom = new Random(); this.imbuementDurations = new HashMap<>(); this.spellsDiscovered = new HashSet<>(); // All players can recognise magic missile. This is not done using discoverSpell because that seems to cause // a crash on load occasionally (probably something to do with achievements being initialised) this.spellsDiscovered.add(Spells.magic_missile); this.recentSpells = EvictingQueue.create(MAX_RECENT_SPELLS); // Only keeps a reference to the last 5 spells cast this.castCommandSpell = Spells.none; this.castCommandModifiers = new SpellModifiers(); this.castCommandTick = 0; this.itemCastingModifiers = new SpellModifiers(); this.allies = new HashSet<>(); this.allyNames = new HashSet<>(); this.spellData = new HashMap<>(); } /** Called from preInit in the main mod class to register the WizardData capability. */ public static void register(){ // Yes - by the looks of it, having an interface is completely unnecessary in this case. CapabilityManager.INSTANCE.register(WizardData.class, new IStorage(){ // These methods are only called by Capability.writeNBT() or Capability.readNBT(), which in turn are // NEVER CALLED. Unless I'm missing some reflective invocation, that means this entire class serves only // to allow capabilities to be saved and loaded manually. What that would be useful for I don't know. // (If an API forces most users to write redundant code for no reason, it's not user friendly, is it?) // ... well, that's my rant for today! @Override public NBTBase writeNBT(Capability capability, WizardData instance, EnumFacing side){ return null; } @Override public void readNBT(Capability capability, WizardData instance, EnumFacing side, NBTBase nbt){} }, WizardData::new); } /** Returns the WizardData instance for the specified player. */ public static WizardData get(EntityPlayer player){ return player.getCapability(WIZARD_DATA_CAPABILITY, null); } // ============================================= Variable Storage ============================================= // This is my answer to having spells define their own player variables. It's not the prettiest system ever, but // I think the ability to add arbitrary data to this class and have it save itself to NBT automatically is pretty // powerful. If it doesn't need saving, this can even be done on the fly - no registration necessary. // The reason we have interfaces here is to allow custom implementations of the NBT read/write methods, for // example, reading/writing multiple keys without having to wrap them in an NBTTagCompound. /** Registers the given {@link IStoredVariable} objects as keys that will be stored to NBT for each {@code WizardData} * instance. */ public static void registerStoredVariables(IStoredVariable... variables){ storedVariables.addAll(Arrays.asList(variables)); } /** Returns a set containing the registered {@link IStoredVariable} objects for which {@link IVariable#isSynced()} * returns true, ordered by their keys obtained from {@link IVariable#getKey()}. Used internally for packets. */ public static Set getSyncedVariablesOrderedByKey(){ Comparator keyComparator = Comparator.comparing(IVariable::getKey); return storedVariables.stream() .filter(IVariable::isSynced) .sorted(keyComparator) .collect(Collectors.toCollection(LinkedHashSet::new)); } /** * Stores the given value under the given key in this {@code WizardData} object. * @param variable The key under which the value is to be stored. See {@link IVariable} for more details. * @param value The value to be stored. * @param The type of the value to be stored. Note that the given variable (key) may be of a supertype of the * stored value itself; however, when the value is retrieved its type will match that of the key. In * other words, if an {@code Integer} is stored under a {@code IVariable}, a {@code Number} will * be returned when the value is retrieved. */ // This use of type parameters guarantees that data may only be stored (and therefore may only be accessed) // using a compatible key. For instance, the following code will not compile: // Number i = 1; // setVariable(StoredVariable.ofInt("key", Persistence.ALWAYS), i); public void setVariable(IVariable variable, T value){ this.spellData.put(variable, value); } /** * Returns the value stored under the given key in this {@code WizardData} object, or null if the key was not * stored. * @param variable The key whose associated value is to be returned. * @param The type of the returned value. * @return The value associated with the given key, or null no such key was stored. Beware of auto-unboxing * of primitive types! Directly assigning the result to a primitive type, as in {@code int i = getVariable(...)}, * will cause a {@link NullPointerException} if the key was not stored. */ @SuppressWarnings("unchecked") // The spellData map is fully encapsulated so we can be sure that the cast is safe @Nullable public T getVariable(IVariable variable){ return (T)spellData.get(variable); } // ============================================== Miscellaneous ============================================== // Spell discovery public boolean hasSpellBeenDiscovered(Spell spell){ return spellsDiscovered.contains(spell) || spell instanceof None; } /** * Adds the given spell to the list of discovered spells for this player. Automatically takes into account whether * the spell has been discovered. Use this method rather than adding directly to the list because it handles * achievements. * * @param spell The spell to be discovered * @return True if the spell had not already been discovered; false otherwise. */ public boolean discoverSpell(Spell spell){ if(spellsDiscovered == null){ spellsDiscovered = new HashSet<>(); } // The 'none' spell cannot be discovered if(spell instanceof None) return false; // Tries to add the spell to the list of discovered spells, and returns false if it was already present return spellsDiscovered.add(spell); } /** * Sets the tier this player has reached to the given tier, if they have not yet reached it. * @param tier The tier the player has reached */ public void setTierReached(Tier tier){ if(!hasReachedTier(tier)) this.maxTierReached = tier; } /** * Returns true if this player has previously upgraded a wand to the given tier. * @param tier The tier to check for * @return True if this player has already upgraded a wand to the given tier, false if not. This does not include * wands that were purchased at the given tier, unless they have since been upgraded. */ public boolean hasReachedTier(Tier tier){ return tier.level >= maxTierReached.level; } // Recent spell tracking /** * Adds the given spell to this player's recently-cast spells. Spells can (and will) be added multiple times, and * will be automatically removed when enough spells are added after them. * @param spell The spell to be tracked. */ public void trackRecentSpell(Spell spell){ this.recentSpells.add(new SimpleEntry<>(spell, player.world.getTotalWorldTime())); } /** * Returns the number of times the given spell is tracked in this player's recently-cast spells. * Only counts spells cast within the configured expiry time. * @param spell The spell to count casts for. */ public int countRecentCasts(Spell spell){ long currentTime = player.world.getTotalWorldTime(); return (int)this.recentSpells.stream() .filter(entry -> entry.getKey() == spell && (currentTime - entry.getValue()) < Wizardry.settings.recentSpellExpiryTime) .count(); // We know this can't be more than 5 } // Imbuements /** * Overwrites the imbuement duration associated with the given imubement for this player, or creates it if there was * none previously. * * @throws IllegalArgumentException if the given {@link Enchantment} is not an {@link Imbuement}. */ public void setImbuementDuration(Enchantment enchantment, int duration){ // It is best to throw an exception here, because otherwise the error would either go unnoticed (if non- // -imbuements were ignored) or cause a ClassCastException later (if non-imbuements were allowed to be added) if(enchantment instanceof Imbuement){ this.imbuementDurations.put((Imbuement)enchantment, duration); }else{ throw new IllegalArgumentException( "Attempted to set an imbuement duration for something that isn't an Imbuement!"); } } /** * Returns the imbuement duration associated with the given imbuement for this player, or 0 if it does not exist. */ @SuppressWarnings("unlikely-arg-type") public int getImbuementDuration(Enchantment enchantment){ // Need to check that i is not null, otherwise it throws an NPE when Java auto-unboxes it. // What's nice here is that the map simply accepts objects as keys, so there's no need to cast or throw // exceptions. Integer i = this.imbuementDurations.get(enchantment); // If i is null, returns 0; otherwise returns i, auto-unboxed to an int. return i == null ? 0 : i; } /** * Decrements the duration for each conjured item by 1, and removes from the map any that are 0 or less or that the * player no longer has. Also deletes the item from the player's inventory if it runs out of time. */ private void updateImbuedItems(){ // FIXME: Iterating through the entire inventory each tick is an AWFUL way of doing this, and is super-slow! // Change the imbuement durations map to be ItemStack -> Integer and just decrement them, adding entries // whenever an item with an imbuement enters the inventory and removing them when the time expires. Set activeImbuements = new HashSet(); // For each item in the player's inventory for(ItemStack stack : player.inventory.mainInventory){ updateImbutedItem(stack, activeImbuements); } for(ItemStack stack : player.inventory.armorInventory){ updateImbutedItem(stack, activeImbuements); } for(ItemStack stack : player.inventory.offHandInventory){ updateImbutedItem(stack, activeImbuements); } // Removes all imbuements from the map that are no longer active this.imbuementDurations.keySet().retainAll(activeImbuements); } private void updateImbutedItem(ItemStack stack, Set activeImbuements){ if(stack.isItemEnchanted()){ NBTTagList enchantmentList = stack.getItem() == Items.ENCHANTED_BOOK ? ItemEnchantedBook.getEnchantments(stack) : stack.getEnchantmentTagList(); Iterator iterator = enchantmentList.iterator(); // For each of the item's enchantments while(iterator.hasNext()){ NBTTagCompound enchantmentTag = (NBTTagCompound) iterator.next(); Enchantment enchantment = Enchantment.getEnchantmentByID(enchantmentTag.getShort("id")); // Ignores the enchantment unless it is an imbuement if(enchantment instanceof Imbuement){ int duration = this.getImbuementDuration(enchantment); // If the imbuement is still active: if(duration > 0){ // Decrements the timer this.imbuementDurations.put((Imbuement)enchantment, duration - IMBUEMENT_UPDATE_INTERVAL); // Adds this imbuement to the set of imbuements that need to be kept activeImbuements.add((Imbuement)enchantment); }else{ // Otherwise, removes the enchantment from the item ((Imbuement) enchantment).onImbuementRemoval(stack); iterator.remove(); // FIXME: Apparently this can cause a CME } } } } } // Ally designation system /** * Adds the given player to the list of allies belonging to the associated player, or removes the player if they are * already in the list of allies. Returns true if the player was added, false if they were removed. */ public boolean toggleAlly(EntityPlayer player){ if(this.isPlayerAlly(player)){ this.allies.remove(player.getUniqueID()); // The remove method uses .equals() rather than == so this will work fine. this.allyNames.remove(player.getName()); return false; }else{ this.allies.add(player.getUniqueID()); this.allyNames.add(player.getName()); return true; } } /** Returns whether the given player is in this player's list of allies, or is on the same team as this player. */ public boolean isPlayerAlly(EntityPlayer player){ return this.allies.contains(player.getUniqueID()) || this.player.isOnSameTeam(player); } /** Returns whether the player with the given UUID is in this player's list of allies. The player to whom the given * UUID belongs need not be logged in. This method is intended for use by owned entities so that their owner's * allies don't accidentally damage them, even when the owner is offline. */ public boolean isPlayerAlly(UUID playerUUID){ // Scoreboard teams use usernames, but since we keep a cache of those... return this.allies.contains(playerUUID) || (this.player.getTeam() != null && this.player.getTeam().getMembershipCollection() != null && this.player.getTeam().getMembershipCollection().stream().anyMatch(allyNames::contains)); } // Command continuous spell casting /** Starts casting the given spell with the given modifiers. */ public void startCastingContinuousSpell(Spell spell, SpellModifiers modifiers, int duration){ this.castCommandSpell = spell; this.castCommandModifiers = modifiers; this.castCommandDuration = duration; if(!this.player.world.isRemote){ PacketCastContinuousSpell.Message message = new PacketCastContinuousSpell.Message(this.player, spell, modifiers, duration); WizardryPacketHandler.net.sendToDimension(message, this.player.world.provider.getDimension()); } } /** Stops casting the current spell. */ public void stopCastingContinuousSpell(){ this.castCommandSpell = Spells.none; this.castCommandTick = 0; this.castCommandModifiers.reset(); if(!this.player.world.isRemote){ PacketCastContinuousSpell.Message message = new PacketCastContinuousSpell.Message(this.player, Spells.none, this.castCommandModifiers, this.castCommandDuration); WizardryPacketHandler.net.sendToDimension(message, this.player.world.provider.getDimension()); } } /** Casts the current continuous spell, fires relevant events and updates the castCommandTick field. */ public void updateContinuousSpellCasting(){ if(this.castCommandSpell != null && this.castCommandSpell.isContinuous){ if(castCommandTick >= castCommandDuration){ this.stopCastingContinuousSpell(); return; } if(MinecraftForge.EVENT_BUS.post(new SpellCastEvent.Tick(Source.COMMAND, castCommandSpell, player, castCommandModifiers, castCommandTick))){ this.stopCastingContinuousSpell(); return; } if(this.castCommandSpell.cast(player.world, player, EnumHand.MAIN_HAND, castCommandTick, this.castCommandModifiers) && this.castCommandTick == 0){ // On the first tick casting a continuous spell via commands, SpellCastEvent.Post is fired. MinecraftForge.EVENT_BUS.post(new SpellCastEvent.Post(Source.COMMAND, castCommandSpell, player, castCommandModifiers)); } castCommandTick++; }else{ // Why is this here? Surely castCommandTick will always be 0 if castCommandSpell is null? this.castCommandTick = 0; } } /** Returns whether this player is currently casting a continuous spell via commands. */ public boolean isCasting(){ return this.castCommandSpell != null && this.castCommandSpell != Spells.none; } /** * Returns the continuous spell this player is currently casting via commands, or the 'none' spell if they aren't * casting anything. */ public Spell currentlyCasting(){ return castCommandSpell; } // ============================================== Data Handling ============================================== /** Called each time the associated player is updated. */ @SuppressWarnings("unchecked") // Again, we know it must be ok private void update(){ if(this.selectedMinion != null && this.selectedMinion.get() == null) this.selectedMinion = null; prevMotionY = player.motionY; // This new system removes a lot of repetitive event handler code and inflexible data which had duplicate // functions, just for different enchantments. if(player.ticksExisted % IMBUEMENT_UPDATE_INTERVAL == 0) updateImbuedItems(); updateContinuousSpellCasting(); // Clean up expired recent spells every 60 ticks (1 second) if(player.ticksExisted % 60 == 0) { long currentTime = player.world.getTotalWorldTime(); this.recentSpells.removeIf(entry -> (currentTime - entry.getValue()) >= Wizardry.settings.recentSpellExpiryTime); } this.spellData.forEach((k, v) -> this.spellData.put(k, k.update(player, v))); this.spellData.keySet().removeIf(k -> k.canPurge(player, this.spellData.get(k))); } /** * Called from the event handler each time the associated player entity is cloned, i.e. on respawn or when * travelling to a different dimension. Used to copy over any data that should persist over player death. This * is the inverse of the old onPlayerDeath method, which reset the data that shouldn't persist. * * @param data The old WizardData whose data is to be copied over. * @param respawn True if the player died and is respawning, false if they are just travelling between dimensions. */ public void copyFrom(WizardData data, boolean respawn){ this.allies = data.allies; this.allyNames = data.allyNames; this.selectedMinion = data.selectedMinion; this.spellsDiscovered = data.spellsDiscovered; this.maxTierReached = data.maxTierReached; this.recentSpells = data.recentSpells; for(IVariable variable : data.spellData.keySet()){ if(variable.isPersistent(respawn)) this.spellData.put(variable, data.spellData.get(variable)); } // Imbuements are lost on death so their durations do not persist. // Command spell casting is reset on death so the associated variables do not persist. } /** Sends a packet to this player's client to synchronise necessary information. Only called server side. */ public void sync(){ if(this.player instanceof EntityPlayerMP){ int id = -1; if(this.selectedMinion != null && this.selectedMinion.get() instanceof Entity) id = ((Entity)this.selectedMinion.get()).getEntityId(); long seed = player.world.rand.nextLong(); this.synchronisedRandom.setSeed(seed); IMessage msg = new PacketPlayerSync.Message(seed, this.spellsDiscovered, id, this.spellData); WizardryPacketHandler.net.sendTo(msg, (EntityPlayerMP)this.player); } } @Override @SuppressWarnings("unchecked") public NBTTagCompound serializeNBT(){ NBTTagCompound properties = new NBTTagCompound(); NBTExtras.storeTagSafely(properties, "imbuements", NBTExtras.mapToNBT(this.imbuementDurations, imbuement -> new NBTTagInt(Enchantment.getEnchantmentID((Enchantment)imbuement)), NBTTagInt::new)); // Mmmmmm Java 8.... NBTExtras.storeTagSafely(properties, "allies", NBTExtras.listToNBT(this.allies, NBTUtil::createUUIDTag)); NBTExtras.storeTagSafely(properties, "allyNames", NBTExtras.listToNBT(this.allyNames, NBTTagString::new)); // Might be worth converting this over to WizardryUtilities.listToNBT. int[] spells = new int[this.spellsDiscovered.size()]; int i = 0; for(Spell spell : this.spellsDiscovered){ spells[i] = spell.metadata(); i++; } properties.setIntArray("discoveredSpells", spells); properties.setInteger("maxTierReached", maxTierReached.ordinal()); // Serialize recent spells with timestamps as compound tags NBTTagList recentSpellsList = new NBTTagList(); for(SimpleEntry entry : recentSpells) { NBTTagCompound spellTag = new NBTTagCompound(); spellTag.setInteger("spellId", entry.getKey().metadata()); spellTag.setLong("timestamp", entry.getValue()); recentSpellsList.appendTag(spellTag); } NBTExtras.storeTagSafely(properties, "recentSpells", recentSpellsList); storedVariables.forEach(k -> k.write(properties, this.spellData.get(k))); return properties; } @Override public void deserializeNBT(NBTTagCompound nbt){ if(nbt != null){ this.imbuementDurations = NBTExtras.NBTToMap(nbt.getTagList("imbuements", NBT.TAG_COMPOUND), (NBTTagInt tag) -> (Imbuement)Enchantment.getEnchantmentByID(tag.getInt()), NBTTagInt::getInt); this.allies = new HashSet<>(NBTExtras.NBTToList(nbt.getTagList("allies", NBT.TAG_COMPOUND), NBTUtil::getUUIDFromTag)); this.allyNames = new HashSet<>(NBTExtras.NBTToList(nbt.getTagList("allyNames", NBT.TAG_STRING), NBTTagString::getString)); this.spellsDiscovered = new HashSet<>(); for(int id : nbt.getIntArray("discoveredSpells")){ spellsDiscovered.add(Spell.byMetadata(id)); } this.maxTierReached = Tier.values()[nbt.getInteger("maxTierReached")]; // Probably won't be null but we may as well just reinitialise it instead of clearing it this.recentSpells = EvictingQueue.create(MAX_RECENT_SPELLS); // Deserialize recent spells with timestamps NBTTagList recentSpellsList = nbt.getTagList("recentSpells", NBT.TAG_COMPOUND); for(int i = 0; i < recentSpellsList.tagCount(); i++) { NBTTagCompound spellTag = recentSpellsList.getCompoundTagAt(i); Spell spell = Spell.byMetadata(spellTag.getInteger("spellId")); long timestamp = spellTag.getLong("timestamp"); this.recentSpells.add(new SimpleEntry<>(spell, timestamp)); } try{ storedVariables.forEach(k -> this.spellData.put(k, k.read(nbt))); }catch(ClassCastException e){ // Should only happen if someone manually edits the save file Wizardry.logger.error("Wizard data NBT tag was not of expected type!", e); } } } // ============================================== Event Handlers ============================================== @SubscribeEvent // The type parameter here has to be Entity, not EntityPlayer, or the event won't get fired. public static void onCapabilityLoad(AttachCapabilitiesEvent event){ if(event.getObject() instanceof EntityPlayer) event.addCapability(new ResourceLocation(Wizardry.MODID, "WizardData"), new WizardData.Provider((EntityPlayer)event.getObject())); } @SubscribeEvent public static void onPlayerCloneEvent(PlayerEvent.Clone event){ WizardData newData = WizardData.get(event.getEntityPlayer()); WizardData oldData = WizardData.get(event.getOriginal()); newData.copyFrom(oldData, event.isWasDeath()); newData.sync(); // In theory this should fix client/server discrepancies (see #69) } @SubscribeEvent public static void onEntityJoinWorld(EntityJoinWorldEvent event){ if(!event.getEntity().world.isRemote && event.getEntity() instanceof EntityPlayerMP){ // Synchronises wizard data after loading. WizardData data = WizardData.get((EntityPlayer)event.getEntity()); if(data != null) data.sync(); } } @SubscribeEvent public static void onLivingUpdateEvent(LivingUpdateEvent event){ if(event.getEntityLiving() instanceof EntityPlayer){ EntityPlayer player = (EntityPlayer)event.getEntityLiving(); if(WizardData.get(player) != null){ WizardData.get(player).update(); } } } // ========================================== Capability Boilerplate ========================================== /** * This is a nested class for a few reasons: firstly, it makes sense because instances of this and WizardData go * hand-in-hand; secondly, it's too short to be worth a separate file; and thirdly (and most importantly) it allows * me to access WIZARD_DATA_CAPABILITY while keeping it private. */ public static class Provider implements ICapabilitySerializable { private final WizardData data; public Provider(EntityPlayer player){ data = new WizardData(player); } @Override public boolean hasCapability(Capability capability, EnumFacing facing){ return capability == WIZARD_DATA_CAPABILITY; } @Override public T getCapability(Capability capability, EnumFacing facing){ if(capability == WIZARD_DATA_CAPABILITY){ return WIZARD_DATA_CAPABILITY.cast(data); } return null; } @Override public NBTTagCompound serializeNBT(){ return data.serializeNBT(); } @Override public void deserializeNBT(NBTTagCompound nbt){ data.deserializeNBT(nbt); } } }