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 super T> 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);
}
}
}