712 lines
29 KiB
Java
712 lines
29 KiB
Java
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.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.WizardryAchievements;
|
|
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.MobEffects;
|
|
import net.minecraft.init.SoundEvents;
|
|
import net.minecraft.item.ItemStack;
|
|
import net.minecraft.nbt.NBTTagCompound;
|
|
import net.minecraft.nbt.NBTTagInt;
|
|
import net.minecraft.nbt.NBTTagString;
|
|
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.
|
|
* <p>
|
|
* 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 1.2
|
|
* @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<NBTTagCompound> {
|
|
|
|
/** 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<WizardData> 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<Imbuement, Integer> 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<ISummonedCreature> selectedMinion;
|
|
|
|
/**
|
|
* Set of this player's discovered spells. <b>Do not write to this list directly</b>, use
|
|
* {@link WizardData#discoverSpell(Spell)} instead.
|
|
*/
|
|
public Set<Spell> spellsDiscovered;
|
|
|
|
private Set<UUID> 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. <b> Do not use this for any other
|
|
* purpose than displaying the names!
|
|
*/
|
|
public Set<String> allyNames;
|
|
|
|
private Set<UUID> soulboundCreatures;
|
|
|
|
public WizardData(EntityPlayer player){
|
|
this.player = player;
|
|
this.imbuementDurations = new HashMap<Imbuement, Integer>();
|
|
this.spellsDiscovered = new HashSet<Spell>();
|
|
// 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<UUID>();
|
|
this.allyNames = new HashSet<String>();
|
|
this.soulboundCreatures = new HashSet<UUID>();
|
|
}
|
|
|
|
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<Spell>();
|
|
}
|
|
// 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))){
|
|
this.player.addStat(WizardryAchievements.all_spells);
|
|
}
|
|
|
|
for(Element element : Element.values()){
|
|
if(element != Element.MAGIC
|
|
&& spellsDiscovered.containsAll(Spell.getSpells(new Spell.TierElementFilter(null, element)))){
|
|
this.player.addStat(WizardryAchievements.element_master);
|
|
}
|
|
}
|
|
|
|
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<Imbuement> activeImbuements = new HashSet<Imbuement>();
|
|
|
|
// For each item in the player's inventory
|
|
for(ItemStack stack : player.inventory.mainInventory){
|
|
if(stack.isItemEnchanted()){
|
|
|
|
Map<Enchantment, Integer> enchantments = EnchantmentHelper.getEnchantments(stack);
|
|
|
|
Iterator<Entry<Enchantment, Integer>> iterator = enchantments.entrySet().iterator();
|
|
// For each of the item's enchantments
|
|
while(iterator.hasNext()){
|
|
Enchantment enchantment = iterator.next().getKey();
|
|
// 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 enchantment map
|
|
iterator.remove();
|
|
// Applies the new enchantment map to the item
|
|
EnchantmentHelper.setEnchantments(enchantments, stack);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// 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){
|
|
|
|
for(Iterator<UUID> 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(entity.attackEntityFrom(MagicDamage.causeDirectMagicDamage(this.player, DamageType.MAGIC, true),
|
|
damage)){
|
|
// 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();
|
|
|
|
// ...so Java 8 allows you to do stuff like this:
|
|
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());
|
|
|
|
// THIS is why I wrote the list/map <-> NBT methods. Look how neat this is!
|
|
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<UUID>(WizardryUtilities.NBTToList(nbt.getTagList("allies", NBT.TAG_COMPOUND),
|
|
WizardryUtilities::tagCompoundToUUID));
|
|
|
|
this.allyNames = new HashSet<String>(
|
|
WizardryUtilities.NBTToList(nbt.getTagList("allyNames", NBT.TAG_STRING), NBTTagString::getString));
|
|
|
|
this.soulboundCreatures = new HashSet<UUID>(WizardryUtilities.NBTToList(
|
|
nbt.getTagList("soulboundCreatures", NBT.TAG_COMPOUND), WizardryUtilities::tagCompoundToUUID));
|
|
|
|
this.spellsDiscovered = new HashSet<Spell>();
|
|
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<Entity> 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<NBTTagCompound> {
|
|
|
|
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> T getCapability(Capability<T> 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);
|
|
}
|
|
|
|
}
|
|
|
|
// Ended up deleting IWizardData because it was unnecessary. This is the comment that was at the start of it:
|
|
|
|
/* I'm not going to lie, I will never find the capabilities system even remotely intuitive so this is a bare-minimum
|
|
* approach just to get things working (four classes where one would have done?!) At one point I considered simply
|
|
* wrapping my old IEEP inside a single-field capability, but I eventually decided I would at least *try* to do it
|
|
* properly.
|
|
*
|
|
* "...without having to directly implement many interfaces." - Forge Docs. I still can't see what's wrong with
|
|
* implementing many interfaces; surely that's what Java interfaces are designed for?
|
|
*
|
|
* Other things I find annoying: - IStorage. It's completely redundant in the majority of cases, and I don't
|
|
* understand why we need yet another separate class. - Making an interface, only to implement it once and once
|
|
* only. This completely defeats the point of interfaces. - The EnumFacing parameter, which is again redundant for
|
|
* everything that isn't a tile entity. So much for a clean, neat system.
|
|
*
|
|
* What Forge has effectively done is conflated two different functions: attaching data to stuff and cross-mod
|
|
* integration/soft dependencies. I think this is bad design; it would have been better to keep the two features
|
|
* separate.
|
|
*
|
|
* Here's my current understanding of how the capability system works: - You make an interface which defines the
|
|
* things your capability can do (this class). I will call this the TEMPLATE. - You implement that interface with
|
|
* your default implementation (WizardData). This is the closest analog to your old IEEP implementation class. THIS
|
|
* CLASS STORES ALL THE VARIABLES, and hence has one instance for each instance of whatever it is attached to. I
|
|
* will call this the DATA. - The DATA class implements INBTSerializable (assuming you want it to be saved, which is
|
|
* nearly always the case) - Despite its name, Capability<T> does NOT represent a capability itself. Instead, it
|
|
* acts as a sort of identifier/key, the idea being that you can access a particular instance of your DATA given the
|
|
* key (which tells forge that you want a capability of type TEMPLATE) and the object you want the DATA for. This is
|
|
* what Entity.getCapability(...) does.
|
|
*
|
|
* To really understand what's going on though, you need to sift through Forge's verbose data structures and find
|
|
* where capabilities are actually hooked into vanilla: - Anything that implements ICapabilityProvider will have a
|
|
* private CapabilityDispatcher field. This holds other ICapabilityProviders. (I know. This inheritance pattern DOES
|
|
* NOT MAKE SENSE, because these could, in theory, be OTHER ENTITIES!) - This field is assigned a value through
|
|
* Forge's event factory, which, as we are all familiar with, calls all the methods marked with @SubscribeEvent.
|
|
* These methods add individual ICapabilityProviders to a Map stored in the event, which the event factory then
|
|
* wraps in a CapabilityDispatcher (which is itself an ICapabilityProvider) for the object that called it. - In your
|
|
* event handler, you return a custom ICapabilityProvider which is effectively bolted on to the player, and
|
|
* duplicates the ICapabilityProvider methods so you can hook into them and return an instance of your DATA class. -
|
|
* Where before there was a simple collection of IEEPs stored in the player, there is now a tree of
|
|
* ICapabilityProviders:
|
|
*
|
|
* - Entity/TileEntity/ItemStack - Vanilla ICapabilityProviders, mostly IItemHandlers, stored as fields. -
|
|
* CapabilityDispatcher, stored as a field. - Custom ICapabilityProviders - Custom CapabilityDispatchers - ...
|
|
*
|
|
* Most importantly, EACH PLAYER HOLDS THEIR OWN INSTANCE OF THIS TREE.
|
|
*
|
|
* When a capability is retrieved, the following process happens: 1. For the Entity/TileEntity/ItemStack instance,
|
|
* ICapabilityProvider.getCapability(...) is called. 2. The request propogates through the tree and finds the
|
|
* requested capability. */
|
|
|
|
}
|