package electroblob.wizardry; import java.lang.ref.WeakReference; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.UUID; import electroblob.wizardry.constants.Element; import electroblob.wizardry.enchantment.Imbuement; import electroblob.wizardry.entity.EntityShield; import electroblob.wizardry.entity.living.ISummonedCreature; import electroblob.wizardry.event.SpellCastEvent; import electroblob.wizardry.event.SpellCastEvent.Source; import electroblob.wizardry.integration.DamageSafetyChecker; import electroblob.wizardry.packet.PacketCastContinuousSpell; import electroblob.wizardry.packet.PacketPlayerSync; import electroblob.wizardry.packet.PacketTransportation; import electroblob.wizardry.packet.WizardryPacketHandler; import electroblob.wizardry.registry.Spells; import electroblob.wizardry.registry.WizardryAdvancementTriggers; import electroblob.wizardry.spell.None; import electroblob.wizardry.spell.Spell; import electroblob.wizardry.util.MagicDamage; import electroblob.wizardry.util.MagicDamage.DamageType; import electroblob.wizardry.util.SpellModifiers; import electroblob.wizardry.util.WizardryUtilities; import net.minecraft.enchantment.Enchantment; import net.minecraft.enchantment.EnchantmentHelper; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.init.Items; import net.minecraft.init.MobEffects; import net.minecraft.init.SoundEvents; import net.minecraft.item.ItemEnchantedBook; import net.minecraft.item.ItemStack; import net.minecraft.nbt.*; import net.minecraft.potion.PotionEffect; import net.minecraft.util.EnumFacing; import net.minecraft.util.EnumHand; import net.minecraft.util.ResourceLocation; import net.minecraft.util.math.BlockPos; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.capabilities.Capability; import net.minecraftforge.common.capabilities.CapabilityInject; 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; /** * 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; /** The player this WizardData instance belongs to. */ private final EntityPlayer player; // This one is still necessary, because I can't override the equip animation for items that aren't from Wizardry. private Map imbuementDurations; public boolean hasSpiritWolf; public boolean hasSpiritHorse; /** * Whether this player is currently casting a continuous spell via commands. Not saved over world reload and reset * on player death. */ private Spell currentlyCasting; /** * 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 castingTick; /** * SpellModifiers object for the current continuous spell cast via commands. Not saved over world reload and reset * on player death. */ private SpellModifiers spellModifiers; /** Coordinates for the saved transportation stone circle location. Will be null if no location is saved. */ private BlockPos stoneCircleLocation; /** Dimension id which the saved stone circle is in. */ private int stoneCircleDimension; /** Time left until the player teleports under the effect of transportation */ private int tpCountdown; /** Coordinates for the saved clairvoyance location. Will be null if no location is saved. */ private BlockPos clairvoyanceLocation; /** Dimension id which the saved clairvoyance point is in. */ private int clairvoyanceDimension; public EntityShield shield; 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; 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; private Set soulboundCreatures; public WizardData(){ this(null); // Nullary constructor for the registration method factory parameter } public WizardData(EntityPlayer player){ this.player = player; 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 initalised) this.spellsDiscovered.add(Spells.magic_missile); this.hasSpiritWolf = false; this.hasSpiritHorse = false; this.currentlyCasting = Spells.none; this.spellModifiers = new SpellModifiers(); this.castingTick = 0; this.stoneCircleDimension = 0; this.clairvoyanceDimension = 0; this.setTpCountdown(0); this.allies = new HashSet(); this.allyNames = new HashSet(); this.soulboundCreatures = new HashSet(); } 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 if(!spellsDiscovered.add(spell)) return false; // If the spell had not already been discovered, achievements can be triggered and the method returns true if(spellsDiscovered.containsAll(Spell.getSpells(Spell::isEnabled))){ WizardryAdvancementTriggers.all_spells.triggerFor(this.player); } for(Element element : Element.values()){ if(element != Element.MAGIC && spellsDiscovered.containsAll(Spell.getSpells(new Spell.TierElementFilter(null, element)))){ WizardryAdvancementTriggers.element_master.triggerFor(this.player); } } return true; } /** Sets the player's saved transportation stone location and dimension. */ public void setStoneCircleLocation(BlockPos pos, int dimensionID){ this.stoneCircleLocation = pos; this.stoneCircleDimension = dimensionID; } /** Returns the coordinates of the associated player's saved transportation stone circle. */ public BlockPos getStoneCircleLocation(){ return stoneCircleLocation; } /** Returns the dimension ID of the associated player's saved transportation stone circle. */ public int getStoneCircleDimension(){ return stoneCircleDimension; } public int getTpCountdown(){ return tpCountdown; } public void setTpCountdown(int tpCountdown){ this.tpCountdown = tpCountdown; } /** Sets the player's saved clairvoyance location. */ public void setClairvoyancePoint(BlockPos pos, int dimensionID){ this.clairvoyanceLocation = pos; this.clairvoyanceDimension = dimensionID; } /** Returns the coordinates for the saved clairvoyance location. Will be null if no location is saved. */ public BlockPos getClairvoyanceLocation(){ return clairvoyanceLocation; } /** Returns the dimension ID for the saved clairvoyance location. Will be null if no location is saved. */ public int getClairvoyanceDimension(){ return clairvoyanceDimension; } /** * 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! (This exception has been thrown now to prevent a ClassCastException from occurring later.)"); } } /** * 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(){ Set activeImbuements = new HashSet(); // For each item in the player's inventory for(ItemStack stack : player.inventory.mainInventory){ 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 - 1); // Adds this imbuement to the set of imbuements that need to be kept activeImbuements.add((Imbuement)enchantment); // Otherwise: }else{ // Removes the enchantment from the item iterator.remove(); } } } } } // Removes all imbuements from the map that are no longer active this.imbuementDurations.keySet().retainAll(activeImbuements); } /** * 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); } /** Adds the given entity to this player's list of soulbound creatures, and returns whether it succeeded. */ public boolean soulbind(EntityLivingBase target){ return this.soulboundCreatures.add(target.getUniqueID()); } /** Returns whether the given entity has been soulbound to this player. */ public boolean isCreatureSoulbound(EntityPlayer target){ return this.soulboundCreatures.contains(target.getUniqueID()); } /** * Damages all creatures soulbound to this player by the given amount, and removes from the list any that no longer * exist. */ public void damageAllSoulboundCreatures(float damage, String originalSourceName){ for(Iterator iterator = this.soulboundCreatures.iterator(); iterator.hasNext();){ Entity entity = WizardryUtilities.getEntityByUUID(this.player.world, iterator.next()); if(entity == null) iterator.remove(); if(entity instanceof EntityLivingBase){ // Retaliatory effect if(DamageSafetyChecker.attackEntitySafely(entity, MagicDamage.causeDirectMagicDamage(this.player, DamageType.MAGIC, true), damage, originalSourceName)){ // Sound only plays if the damage succeeds player.playSound(SoundEvents.ENTITY_WITHER_HURT, 1.0F, player.world.rand.nextFloat() * 0.2F + 1.0F); } } } } /** Starts casting the given spell with the given modifiers. */ public void startCastingContinuousSpell(Spell spell, SpellModifiers modifiers){ this.currentlyCasting = spell; this.spellModifiers = modifiers; if(!this.player.world.isRemote){ PacketCastContinuousSpell.Message message = new PacketCastContinuousSpell.Message(this.player.getEntityId(), spell.id(), this.spellModifiers); WizardryPacketHandler.net.sendToDimension(message, this.player.world.provider.getDimension()); } } /** Stops casting the current spell. */ public void stopCastingContinuousSpell(){ this.currentlyCasting = Spells.none; this.castingTick = 0; this.spellModifiers.reset(); if(!this.player.world.isRemote){ PacketCastContinuousSpell.Message message = new PacketCastContinuousSpell.Message(this.player.getEntityId(), Spells.none.id(), this.spellModifiers); WizardryPacketHandler.net.sendToDimension(message, this.player.world.provider.getDimension()); } } /** Casts the current continuous spell, fires relevant events and updates the castingTick field. */ public void updateContinuousSpellCasting(){ if(this.currentlyCasting != null && this.currentlyCasting.isContinuous){ if(MinecraftForge.EVENT_BUS.post( new SpellCastEvent.Tick(player, currentlyCasting, spellModifiers, Source.COMMAND, castingTick))){ this.stopCastingContinuousSpell(); return; } if(this.currentlyCasting.cast(player.world, player, EnumHand.MAIN_HAND, castingTick, this.spellModifiers) && this.castingTick == 0){ // On the first tick casting a continuous spell via commands, SpellCastEvent.Post is fired. MinecraftForge.EVENT_BUS .post(new SpellCastEvent.Post(player, currentlyCasting, spellModifiers, Source.COMMAND)); } castingTick++; }else{ // Why is this here? Surely castingTick will always be 0 if currentlyCasting is null? this.castingTick = 0; } } /** Returns whether this player is currently casting a continuous spell via commands. */ public boolean isCasting(){ return this.currentlyCasting != null && this.currentlyCasting != 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 currentlyCasting; } /** Called each time the associated player is updated. */ private void update(){ if(this.selectedMinion != null && this.selectedMinion.get() == null) this.selectedMinion = null; // This new system removes a lot of repetitive event handler code and inflexible variables which had duplicate // functions, just for different enchantments. updateImbuedItems(); if(!player.world.isRemote){ if(getTpCountdown() == 1){ player.setPositionAndUpdate(this.stoneCircleLocation.getX() + 0.5, this.stoneCircleLocation.getY(), this.stoneCircleLocation.getZ() + 0.5); player.addPotionEffect(new PotionEffect(MobEffects.BLINDNESS, 50, 0)); IMessage msg = new PacketTransportation.Message(player.getEntityId()); WizardryPacketHandler.net.sendToDimension(msg, player.world.provider.getDimension()); } if(getTpCountdown() > 0){ setTpCountdown(getTpCountdown() - 1); } } updateContinuousSpellCasting(); } /** * Returns the WizardData instance for the specified player. */ public static final WizardData get(EntityPlayer player){ return player.getCapability(WIZARD_DATA_CAPABILITY, null); } /** * 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 variables that should persist over player death. This * is the inverse of the old onPlayerDeath method, which reset the variables that shouldn't persist. * * @param data The old WizardData whose variables are 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){ // TODO: What happens with spirit wolf and spirit horse? this.hasSpiritHorse = data.hasSpiritHorse; this.hasSpiritWolf = data.hasSpiritWolf; this.allies = data.allies; this.allyNames = data.allyNames; this.clairvoyanceDimension = data.clairvoyanceDimension; this.clairvoyanceLocation = data.clairvoyanceLocation; this.selectedMinion = data.selectedMinion; // Curse of soulbinding is lifted when the caster dies, but not when they switch dimensions. if(!respawn) this.soulboundCreatures = data.soulboundCreatures; this.spellsDiscovered = data.spellsDiscovered; this.stoneCircleDimension = data.stoneCircleDimension; this.stoneCircleLocation = data.stoneCircleLocation; // 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. // tpCountdown is reset both when the player dies and when they switch dimensions. } /** 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(); IMessage msg = new PacketPlayerSync.Message(this.spellsDiscovered, id); WizardryPacketHandler.net.sendTo(msg, (EntityPlayerMP)this.player); } } @Override public NBTTagCompound serializeNBT(){ NBTTagCompound properties = new NBTTagCompound(); properties.setTag("imbuements", WizardryUtilities.mapToNBT(this.imbuementDurations, imbuement -> new NBTTagInt(Enchantment.getEnchantmentID((Enchantment)imbuement)), NBTTagInt::new)); properties.setBoolean("hasSpiritWolf", this.hasSpiritWolf); properties.setBoolean("hasSpiritHorse", this.hasSpiritHorse); if(this.stoneCircleLocation != null) properties.setLong("stoneCircleLocation", this.stoneCircleLocation.toLong()); properties.setInteger("stoneCircleDimension", this.stoneCircleDimension); properties.setInteger("tpCountdown", this.tpCountdown); if(this.clairvoyanceLocation != null) properties.setLong("clairvoyanceLocation", this.clairvoyanceLocation.toLong()); properties.setInteger("clairvoyanceDimension", this.getClairvoyanceDimension()); // Mmmmmm Java 8.... properties.setTag("allies", WizardryUtilities.listToNBT(this.allies, WizardryUtilities::UUIDtoTagCompound)); properties.setTag("allyNames", WizardryUtilities.listToNBT(this.allyNames, NBTTagString::new)); properties.setTag("soulboundCreatures", WizardryUtilities.listToNBT(this.soulboundCreatures, WizardryUtilities::UUIDtoTagCompound)); // 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.id(); i++; } properties.setIntArray("discoveredSpells", spells); return properties; } @Override public void deserializeNBT(NBTTagCompound nbt){ if(nbt != null){ this.imbuementDurations = WizardryUtilities.NBTToMap(nbt.getTagList("imbuements", NBT.TAG_COMPOUND), (NBTTagInt tag) -> (Imbuement)Enchantment.getEnchantmentByID(tag.getInt()), NBTTagInt::getInt); this.hasSpiritWolf = nbt.getBoolean("hasSpiritWolf"); this.hasSpiritHorse = nbt.getBoolean("hasSpiritHorse"); this.stoneCircleLocation = BlockPos.fromLong(nbt.getLong("stoneCircleLocation")); this.stoneCircleDimension = nbt.getInteger("stoneCircleDimension"); this.tpCountdown = nbt.getInteger("tpCountdown"); this.clairvoyanceLocation = BlockPos.fromLong(nbt.getLong("clairvoyanceLocation")); this.clairvoyanceDimension = nbt.getInteger("clairvoyanceDimension"); this.allies = new HashSet(WizardryUtilities.NBTToList(nbt.getTagList("allies", NBT.TAG_COMPOUND), WizardryUtilities::tagCompoundToUUID)); this.allyNames = new HashSet( WizardryUtilities.NBTToList(nbt.getTagList("allyNames", NBT.TAG_STRING), NBTTagString::getString)); this.soulboundCreatures = new HashSet(WizardryUtilities.NBTToList( nbt.getTagList("soulboundCreatures", NBT.TAG_COMPOUND), WizardryUtilities::tagCompoundToUUID)); this.spellsDiscovered = new HashSet(); for(int id : nbt.getIntArray("discoveredSpells")){ spellsDiscovered.add(Spell.get(id)); } } } // 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())); // This demonstrates why capabilities are badly structured: The following code compiles, but what it does is put // a player into a CapabilityDispatcher, which is in turn stored in that very same player, which makes no sense // at all! // event.addCapability(new ResourceLocation(Wizardry.MODID, "WizardData"), 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()); } @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(); } } } /** * 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); } } }