That's one heck of a commit you've got there...
I may have got a bit behind with version control. A lot behind, in fact. Maybe I'll go back and split this sometime - then again, I probably won't. But hey, at least it's here!
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
package electroblob.wizardry.data;
|
||||
|
||||
import electroblob.wizardry.Wizardry;
|
||||
import electroblob.wizardry.event.SpellCastEvent;
|
||||
import electroblob.wizardry.packet.PacketDispenserCastSpell;
|
||||
import electroblob.wizardry.packet.WizardryPacketHandler;
|
||||
import electroblob.wizardry.registry.Spells;
|
||||
import electroblob.wizardry.spell.None;
|
||||
import electroblob.wizardry.spell.Spell;
|
||||
import electroblob.wizardry.util.SpellModifiers;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.tileentity.TileEntity;
|
||||
import net.minecraft.util.EnumFacing;
|
||||
import net.minecraftforge.common.MinecraftForge;
|
||||
import net.minecraftforge.common.util.INBTSerializable;
|
||||
import net.minecraftforge.fml.common.network.simpleimpl.IMessage;
|
||||
|
||||
/**
|
||||
* Base class for {@link DispenserCastingData}. Originally this was written because command blocks had a similar system,
|
||||
* but that was later removed in favour of spell emitters - however, this class has been kept so that others can use it
|
||||
* for different spellcasting blocks if they wish.
|
||||
*
|
||||
* @since Wizardry 4.2
|
||||
* @author Electroblob
|
||||
*/
|
||||
public abstract class BlockCastingData<T extends TileEntity> implements INBTSerializable<NBTTagCompound> {
|
||||
|
||||
/** The tile entity this BlockCastingData instance belongs to. */
|
||||
protected final T tileEntity;
|
||||
|
||||
/** The continuous spell this tile entity is currently casting, or the {@link None} spell if it is not casting. */
|
||||
protected Spell spell;
|
||||
/** The coordinates of the current continuous spell's origin. */
|
||||
protected double x, y, z;
|
||||
/** The time for which this tile entity has been casting a continuous spell. Increments by 1 each tick. */
|
||||
protected int castingTick;
|
||||
/** SpellModifiers object for the current continuous spell. */
|
||||
protected SpellModifiers modifiers;
|
||||
|
||||
public BlockCastingData(T tileEntity){
|
||||
this.tileEntity = tileEntity;
|
||||
this.spell = Spells.none;
|
||||
this.modifiers = new SpellModifiers();
|
||||
this.castingTick = 0;
|
||||
}
|
||||
|
||||
/** Returns whether this tile entity is currently casting a continuous spell. */
|
||||
public boolean isCasting(){
|
||||
return this.spell != null && this.spell != Spells.none;
|
||||
}
|
||||
|
||||
/** Returns the continuous spell this tile entity is currently casting, or the {@link None} spell if it isn't
|
||||
* casting anything. */
|
||||
public Spell currentlyCasting(){
|
||||
return spell;
|
||||
}
|
||||
|
||||
/** Starts casting the given continuous spell from this tile entity. */
|
||||
protected void startCasting(Spell spell, double x, double y, double z, SpellModifiers modifiers){
|
||||
|
||||
if(!spell.isContinuous){
|
||||
Wizardry.logger.warn("Tried to start casting a continuous spell from a tile entity, but the given spell was not continuous!");
|
||||
return;
|
||||
}
|
||||
|
||||
this.spell = spell;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.castingTick = 0;
|
||||
this.modifiers = modifiers;
|
||||
}
|
||||
|
||||
/** Stops casting the current spell. */
|
||||
protected void stopCasting(){
|
||||
this.spell = Spells.none;
|
||||
this.castingTick = 0;
|
||||
this.modifiers.reset();
|
||||
}
|
||||
|
||||
/** Stops casting the current spell and sends a packet to clients to update them. If called client-side, this just
|
||||
* delegates to {@link BlockCastingData#stopCasting()}. */
|
||||
protected void stopCastingAndNotify(){
|
||||
|
||||
stopCasting();
|
||||
|
||||
if(!tileEntity.getWorld().isRemote){
|
||||
IMessage msg = new PacketDispenserCastSpell.Message(x, y, z, getDirection(), tileEntity.getPos(), spell, 0, modifiers);
|
||||
WizardryPacketHandler.net.sendToDimension(msg, tileEntity.getWorld().provider.getDimension());
|
||||
}
|
||||
}
|
||||
|
||||
/** Called once per tick to update the block casting data. <b>This is not called automatically</b>, subclasses must
|
||||
* do so using their own tick event handlers. */
|
||||
protected void update(){
|
||||
|
||||
if(this.tileEntity.isInvalid()){
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.isCasting() && this.spell.isContinuous){
|
||||
|
||||
// If the dispenser has stopped receiving power, the spell stops immediately.
|
||||
if(!shouldContinueCasting()){
|
||||
this.stopCasting(); // This seems to work fine on both sides, so no point sending a packet
|
||||
return;
|
||||
}
|
||||
|
||||
EnumFacing direction = getDirection();
|
||||
|
||||
if(MinecraftForge.EVENT_BUS.post(new SpellCastEvent.Tick(getSource(), spell, tileEntity.getWorld(),
|
||||
x, y, z, direction, modifiers, castingTick))){
|
||||
// When the event is canceled client-side, this will stop the spell on the client only, as specified in
|
||||
// the javadoc for SpellCastEvent.Tick.
|
||||
this.stopCastingAndNotify();
|
||||
return;
|
||||
}
|
||||
|
||||
this.spell.cast(tileEntity.getWorld(), x, y, z, direction, castingTick, -1, modifiers);
|
||||
|
||||
castingTick++;
|
||||
|
||||
}else{
|
||||
this.castingTick = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the direction to cast the current spell in. */
|
||||
protected abstract EnumFacing getDirection();
|
||||
|
||||
/** Returns the source of spells cast from this block. */
|
||||
protected abstract SpellCastEvent.Source getSource();
|
||||
|
||||
/** Called each tick during continuous spell casting to determine if the spell should continue or stop. */
|
||||
protected abstract boolean shouldContinueCasting();
|
||||
|
||||
@Override
|
||||
public NBTTagCompound serializeNBT(){
|
||||
|
||||
NBTTagCompound nbt = new NBTTagCompound();
|
||||
|
||||
nbt.setInteger("spell", spell.metadata());
|
||||
nbt.setInteger("castingTick", castingTick);
|
||||
nbt.setTag("modifiers", modifiers.toNBT());
|
||||
|
||||
return nbt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deserializeNBT(NBTTagCompound nbt){
|
||||
|
||||
if(nbt != null){
|
||||
|
||||
this.spell = Spell.byMetadata(nbt.getInteger("spell"));
|
||||
this.castingTick = nbt.getInteger("castingTick");
|
||||
this.modifiers = SpellModifiers.fromNBT(nbt.getCompoundTag("modifiers"));
|
||||
}
|
||||
}
|
||||
|
||||
// The two methods below broke EVERYTHING, somehow they made the server think it was the client...
|
||||
|
||||
// // Only fired server-side
|
||||
// @SubscribeEvent
|
||||
// public static void onWorldTickEvent(TickEvent.WorldTickEvent event){
|
||||
//
|
||||
// if(!event.world.isRemote && event.phase == TickEvent.Phase.END){
|
||||
// // This will fire once for each dimension, but since we want dispenser-casting to work in all dimensions,
|
||||
// // this is correct (the loaded tile entity list will of course be different in each case.
|
||||
// this.update();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Only called client-side
|
||||
// @SubscribeEvent
|
||||
// public static void onClientTickEvent(TickEvent.ClientTickEvent event){
|
||||
// World world = net.minecraft.client.Minecraft.getMinecraft().world;
|
||||
// if(event.phase == TickEvent.Phase.END && !net.minecraft.client.Minecraft.getMinecraft().isGamePaused()
|
||||
// && world != null){
|
||||
// this.update();
|
||||
// }
|
||||
// }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package electroblob.wizardry.data;
|
||||
|
||||
import electroblob.wizardry.Wizardry;
|
||||
import electroblob.wizardry.event.SpellCastEvent.Source;
|
||||
import electroblob.wizardry.item.ItemScroll;
|
||||
import electroblob.wizardry.registry.Spells;
|
||||
import electroblob.wizardry.spell.Spell;
|
||||
import electroblob.wizardry.util.SpellModifiers;
|
||||
import net.minecraft.block.BlockDispenser;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.nbt.NBTBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.tileentity.TileEntity;
|
||||
import net.minecraft.tileentity.TileEntityDispenser;
|
||||
import net.minecraft.util.EnumFacing;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
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.event.AttachCapabilitiesEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.gameevent.TickEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Internal capability for attaching data to dispensers. The sole purpose of this class is to keep track of continuous
|
||||
* spell casting for dispensers.
|
||||
* <p></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 4.2
|
||||
* @author Electroblob
|
||||
*/
|
||||
@Mod.EventBusSubscriber
|
||||
public class DispenserCastingData extends BlockCastingData<TileEntityDispenser> {
|
||||
|
||||
/** 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(DispenserCastingData.class)
|
||||
private static final Capability<DispenserCastingData> DISPENSER_CASTING_CAPABILITY = null;
|
||||
|
||||
/** The time for which this dispenser will continue casting a continuous spell. When castingTick exceeds this value,
|
||||
* the dispenser will either stop casting or, if it contains more of the same type of scroll, continue casting and
|
||||
* increase this value by the duration that the spell should be cast for. */
|
||||
private int duration;
|
||||
|
||||
public DispenserCastingData(){
|
||||
this(null); // Nullary constructor for the registration method factory parameter
|
||||
}
|
||||
|
||||
public DispenserCastingData(TileEntityDispenser dispenser){
|
||||
super(dispenser);
|
||||
}
|
||||
|
||||
/** Starts casting the given continuous spell from this dispenser. */
|
||||
public void startCasting(Spell spell, double x, double y, double z, int duration, SpellModifiers modifiers){
|
||||
startCasting(spell, x, y, z, modifiers);
|
||||
this.castingTick = 1; // 1 because we already cast it once in BehaviourSpellDispense
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopCasting(){
|
||||
super.stopCasting();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Source getSource(){
|
||||
return Source.DISPENSER;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected EnumFacing getDirection(){
|
||||
return tileEntity.getWorld().getBlockState(tileEntity.getPos()).getValue(BlockDispenser.FACING);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldContinueCasting(){
|
||||
return tileEntity.getWorld().isBlockPowered(tileEntity.getPos());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(){
|
||||
|
||||
super.update();
|
||||
|
||||
// Check whether enough scrolls are left
|
||||
if(this.isCasting() && this.spell.isContinuous){
|
||||
|
||||
if(castingTick > duration && !tileEntity.getWorld().isRemote){
|
||||
|
||||
if(findNewScroll()){
|
||||
duration += ItemScroll.CASTING_TIME; // Best way to do it for now.
|
||||
}else{
|
||||
this.stopCastingAndNotify();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Searches through the dispenser's inventory for a new stack of scrolls of the same spell that is currently being
|
||||
* cast and returns true if at least one such stack is found. Also consumes one scroll if a stack is found; if more
|
||||
* than one applicable stack is found then one will be chosen at random. */
|
||||
private boolean findNewScroll(){
|
||||
|
||||
if(spell == Spells.none) return false;
|
||||
|
||||
List<Integer> slots = new ArrayList<Integer>();
|
||||
|
||||
for(int i = 0; i < tileEntity.getSizeInventory(); i++){
|
||||
ItemStack stack = tileEntity.getStackInSlot(i);
|
||||
if(stack.getItem() instanceof ItemScroll && stack.getMetadata() == spell.metadata()) slots.add(i);
|
||||
}
|
||||
|
||||
if(slots.isEmpty()) return false; // If no stack was found that matched the current spell
|
||||
|
||||
tileEntity.decrStackSize(slots.get(tileEntity.getWorld().rand.nextInt(slots.size())), 1); // Consumes 1 scroll
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Returns the DispenserCastingData instance for the specified dispenser. */
|
||||
public static DispenserCastingData get(TileEntityDispenser dispenser){
|
||||
return dispenser.getCapability(DISPENSER_CASTING_CAPABILITY, null);
|
||||
}
|
||||
|
||||
/** Called from preInit in the main mod class to register the DispenserCastingData capability. */
|
||||
public static void register(){
|
||||
|
||||
CapabilityManager.INSTANCE.register(DispenserCastingData.class, new IStorage<DispenserCastingData>(){
|
||||
|
||||
@Override
|
||||
public NBTBase writeNBT(Capability<DispenserCastingData> capability, DispenserCastingData instance, EnumFacing side){
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readNBT(Capability<DispenserCastingData> capability, DispenserCastingData instance, EnumFacing side, NBTBase nbt){}
|
||||
|
||||
}, DispenserCastingData::new);
|
||||
}
|
||||
|
||||
// Event handlers
|
||||
|
||||
@SubscribeEvent
|
||||
// The type parameter here has to be SoundLoopSpellDispenser, not TileEntityDispenser, or the event won't get fired.
|
||||
public static void onCapabilityLoad(AttachCapabilitiesEvent<TileEntity> event){
|
||||
|
||||
if(event.getObject() instanceof TileEntityDispenser)
|
||||
event.addCapability(new ResourceLocation(Wizardry.MODID, "casting_data"),
|
||||
new DispenserCastingData.Provider((TileEntityDispenser)event.getObject()));
|
||||
}
|
||||
|
||||
// Only fired server-side
|
||||
@SubscribeEvent
|
||||
public static void onWorldTickEvent(TickEvent.WorldTickEvent event){
|
||||
|
||||
if(event.phase == TickEvent.Phase.END){
|
||||
|
||||
// This will fire once for each dimension, but since we want dispenser-casting to work in all dimensions,
|
||||
// this is correct (the loaded tile entity list will of course be different in each case.
|
||||
|
||||
for(TileEntity tileentity : event.world.loadedTileEntityList){
|
||||
if(tileentity instanceof TileEntityDispenser){
|
||||
if(DispenserCastingData.get((TileEntityDispenser)tileentity) != null){
|
||||
DispenserCastingData.get((TileEntityDispenser)tileentity).update();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a nested class for a few reasons: firstly, it makes sense because instances of this and
|
||||
* DispenserCastingData 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 DISPENSER_CASTING_CAPABILITY while keeping it private.
|
||||
*/
|
||||
public static class Provider implements ICapabilitySerializable<NBTTagCompound> {
|
||||
|
||||
private final DispenserCastingData data;
|
||||
|
||||
public Provider(TileEntityDispenser dispenser){
|
||||
data = new DispenserCastingData(dispenser);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasCapability(Capability<?> capability, EnumFacing facing){
|
||||
return capability == DISPENSER_CASTING_CAPABILITY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getCapability(Capability<T> capability, EnumFacing facing){
|
||||
|
||||
if(capability == DISPENSER_CASTING_CAPABILITY){
|
||||
return DISPENSER_CASTING_CAPABILITY.cast(data);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NBTTagCompound serializeNBT(){
|
||||
return data.serializeNBT();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deserializeNBT(NBTTagCompound nbt){
|
||||
data.deserializeNBT(nbt);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
package electroblob.wizardry.data;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.nbt.*;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraftforge.fml.common.network.ByteBufUtils;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Extension of {@link IVariable} which adds NBT read/write methods. Instances of this interface must be
|
||||
* registered on load using {@link WizardData#registerStoredVariables(IStoredVariable...)} in order for NBT storage
|
||||
* to work. A good place to do this is in spell constructors, if that's where the variable is being used.
|
||||
* <p></p>
|
||||
* This interface is provided for complex cases that require custom NBT handling of some kind. In most cases,
|
||||
* {@link StoredVariable} should be sufficient.
|
||||
* <p></p>
|
||||
* @param <T> The type of variable stored.
|
||||
*/
|
||||
public interface IStoredVariable<T> extends IVariable<T> {
|
||||
|
||||
/** Writes the value to the given NBT tag. */
|
||||
void write(NBTTagCompound nbt, T value);
|
||||
|
||||
/** Reads the value from the given NBT tag. */
|
||||
T read(NBTTagCompound nbt);
|
||||
|
||||
/**
|
||||
* General-purpose implementation of {@link IStoredVariable}. In most cases, this should be sufficient. This class
|
||||
* also contains a number of static methods for common implementations (primitives, {@code String}, {@code UUID},
|
||||
* {@code BlockPos} and {@code ItemStack}).
|
||||
* <p></p>
|
||||
* @param <T> The type of variable stored.
|
||||
* @param <E> The type of NBT tag the variable will be stored as.
|
||||
*/
|
||||
class StoredVariable<T, E extends NBTBase> implements IStoredVariable<T> {
|
||||
|
||||
private final String key;
|
||||
private final Persistence persistence;
|
||||
|
||||
private final Function<T, E> serialiser;
|
||||
private final Function<E, T> deserialiser;
|
||||
|
||||
private boolean synced;
|
||||
|
||||
private BiFunction<EntityPlayer, T, T> ticker;
|
||||
|
||||
/**
|
||||
* Creates a new {@code StoredVariable} with the given key and serialisation behaviour.
|
||||
* @param key The string key used to write the value to NBT (should be unique). This serves no other purpose.
|
||||
* @param serialiser A function used to write the value to NBT.
|
||||
* @param deserialiser A function used to read the value from NBT.
|
||||
*/
|
||||
public StoredVariable(String key, Function<T, E> serialiser, Function<E, T> deserialiser, Persistence persistence){
|
||||
this.key = key;
|
||||
this.serialiser = serialiser;
|
||||
this.deserialiser = deserialiser;
|
||||
this.persistence = persistence;
|
||||
this.ticker = (p, t) -> t; // Initialise this with a do-nothing function, can be overwritten later
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces this variable's update method with the given update function. <i>Beware of auto-unboxing of
|
||||
* primitive types! For lambda expressions, check the second parameter isn't null before operating on it.
|
||||
* For method references, do not reference a method that takes a primitive type. Otherwise, this will cause
|
||||
* a (difficult to debug) {@link NullPointerException} if the key was not stored.</i>
|
||||
* @param ticker A {@link BiFunction} specifying the actions to be performed on this variable each tick. The
|
||||
* {@code BiFunction} returns the new value for this variable.
|
||||
* @return This {@code StoredVariable} object, allowing this method to be chained onto object creation.
|
||||
*/
|
||||
public StoredVariable<T, E> withTicker(BiFunction<EntityPlayer, T, T> ticker){
|
||||
this.ticker = ticker;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds synchronisation to this variable, meaning it will be sent to clients whenever {@link WizardData#sync()}
|
||||
* is called (this always happens on player login, but other than that you'll need to do it yourself).
|
||||
* @return This {@code StoredVariable} object, allowing this method to be chained onto object creation.
|
||||
*/
|
||||
public StoredVariable<T, E> setSynced(){
|
||||
this.synced = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(NBTTagCompound nbt, T value){
|
||||
if(value != null) nbt.setTag(key, serialiser.apply(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked") // Can't check it due to type erasure
|
||||
public T read(NBTTagCompound nbt){
|
||||
// A system allowing any kind of variable to be stored on the fly cannot be made without casting somewhere.
|
||||
// However, doing it like this means we only cast once, below, and proper regulation of access means we
|
||||
// can effectively guarantee the cast is safe.
|
||||
return nbt.hasKey(key) ? deserialiser.apply((E)nbt.getTag(key)) : null; // Still gotta check it ain't null
|
||||
}
|
||||
|
||||
@Override
|
||||
public T update(EntityPlayer player, T value){
|
||||
return ticker.apply(player, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPersistent(boolean respawn){
|
||||
return respawn ? persistence.persistsOnRespawn() : persistence.persistsOnDimensionChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSynced(){
|
||||
return synced;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(ByteBuf buf, T value){
|
||||
if(!synced) return;
|
||||
NBTTagCompound nbt = new NBTTagCompound();
|
||||
write(nbt, value);
|
||||
ByteBufUtils.writeTag(buf, nbt); // Sure, it's not super-efficient, but it's by far the simplest way!
|
||||
}
|
||||
|
||||
@Override
|
||||
public T read(ByteBuf buf){
|
||||
if(!synced) return null; // Better to check in here because this method should only read if it needs to
|
||||
NBTTagCompound nbt = ByteBufUtils.readTag(buf);
|
||||
if(nbt == null) return null;
|
||||
return read(nbt);
|
||||
}
|
||||
|
||||
// Standard implementations to shorten common usages a bit
|
||||
|
||||
/** Creates a new {@code StoredVariable} for a byte value with the given key. */
|
||||
public static StoredVariable<Byte, NBTTagByte> ofByte(String key, Persistence persistence){
|
||||
return new StoredVariable<>(key, NBTTagByte::new, NBTTagByte::getByte, persistence);
|
||||
}
|
||||
|
||||
/** Creates a new {@code StoredVariable} for a boolean value with the given key. As per Minecraft's usual
|
||||
* NBT conventions, the boolean value is stored as an {@link NBTTagByte} (1 = true, 0 = false). */
|
||||
public static StoredVariable<Boolean, NBTTagByte> ofBoolean(String key, Persistence persistence){
|
||||
return new StoredVariable<>(key, b -> new NBTTagByte((byte)(b?1:0)), t -> t.getByte() == 1, persistence);
|
||||
}
|
||||
|
||||
/** Creates a new {@code StoredVariable} for an integer value with the given key. */
|
||||
public static StoredVariable<Integer, NBTTagInt> ofInt(String key, Persistence persistence){
|
||||
return new StoredVariable<>(key, NBTTagInt::new, NBTTagInt::getInt, persistence);
|
||||
}
|
||||
|
||||
// I'm not going to do byte and long arrays here, if you really need them it's pretty obvious how to do it
|
||||
|
||||
/** Creates a new {@code StoredVariable} for an integer array value with the given key. */
|
||||
public static StoredVariable<int[], NBTTagIntArray> ofIntArray(String key, Persistence persistence){
|
||||
return new StoredVariable<>(key, NBTTagIntArray::new, NBTTagIntArray::getIntArray, persistence);
|
||||
}
|
||||
|
||||
/** Creates a new {@code StoredVariable} for a float value with the given key. */
|
||||
public static StoredVariable<Float, NBTTagFloat> ofFloat(String key, Persistence persistence){
|
||||
return new StoredVariable<>(key, NBTTagFloat::new, NBTTagFloat::getFloat, persistence);
|
||||
}
|
||||
|
||||
/** Creates a new {@code StoredVariable} for a double value with the given key. */
|
||||
public static StoredVariable<Double, NBTTagDouble> ofDouble(String key, Persistence persistence){
|
||||
return new StoredVariable<>(key, NBTTagDouble::new, NBTTagDouble::getDouble, persistence);
|
||||
}
|
||||
|
||||
/** Creates a new {@code StoredVariable} for a short value with the given key. */
|
||||
public static StoredVariable<Short, NBTTagShort> ofShort(String key, Persistence persistence){
|
||||
return new StoredVariable<>(key, NBTTagShort::new, NBTTagShort::getShort, persistence);
|
||||
}
|
||||
|
||||
/** Creates a new {@code StoredVariable} for a long value with the given key. */
|
||||
public static StoredVariable<Long, NBTTagLong> ofLong(String key, Persistence persistence){
|
||||
return new StoredVariable<>(key, NBTTagLong::new, NBTTagLong::getLong, persistence);
|
||||
}
|
||||
|
||||
/** Creates a new {@code StoredVariable} for a {@link String} value with the given key. */
|
||||
public static StoredVariable<String, NBTTagString> ofString(String key, Persistence persistence){
|
||||
return new StoredVariable<>(key, NBTTagString::new, NBTTagString::getString, persistence);
|
||||
}
|
||||
|
||||
/** Creates a new {@code StoredVariable} for a {@link BlockPos} value with the given key. */
|
||||
public static StoredVariable<BlockPos, NBTTagCompound> ofBlockPos(String key, Persistence persistence){
|
||||
return new StoredVariable<>(key, NBTUtil::createPosTag, NBTUtil::getPosFromTag, persistence);
|
||||
}
|
||||
|
||||
/** Creates a new {@code StoredVariable} for a {@link UUID} value with the given key. */
|
||||
public static StoredVariable<UUID, NBTTagCompound> ofUUID(String key, Persistence persistence){
|
||||
return new StoredVariable<>(key, NBTUtil::createUUIDTag, NBTUtil::getUUIDFromTag, persistence);
|
||||
}
|
||||
|
||||
/** Creates a new {@code StoredVariable} for an {@link ItemStack} value with the given key. */
|
||||
public static StoredVariable<ItemStack, NBTTagCompound> ofItemStack(String key, Persistence persistence){
|
||||
return new StoredVariable<>(key, ItemStack::serializeNBT, ItemStack::new, persistence);
|
||||
}
|
||||
|
||||
/** Creates a new {@code StoredVariable} for an {@link NBTTagCompound} value with the given key. */
|
||||
public static StoredVariable<NBTTagCompound, NBTTagCompound> ofNBT(String key, Persistence persistence){
|
||||
return new StoredVariable<>(key, t -> t, t -> t, persistence); // No conversion required!
|
||||
}
|
||||
|
||||
// Neither of these work just ignore them
|
||||
|
||||
// /** Creates a new {@code StoredVariable} for an {@link NBTTagCompound} value with the given key which stores the
|
||||
// * given {@code IVariable} for an entity. Entities cannot be stored directly as an {@code IStoredVariable}
|
||||
// * because they require a world instance on construction. */
|
||||
// @SuppressWarnings("unchecked") // Can't check it due to type erasure
|
||||
// public static <T extends Entity> StoredVariable<NBTTagCompound, NBTTagCompound> ofNBTForEntity(String key, Persistence persistence, IVariable<T> toStore){
|
||||
// return ofNBT(key, persistence).withTicker((p, t) -> {
|
||||
// if(WizardData.get(p) != null){
|
||||
// try{
|
||||
// T e = (T)EntityList.createEntityByIDFromName(new ResourceLocation(t.getString("entityType")), p.world);
|
||||
// e.readFromNBT(t);
|
||||
// WizardData.get(p).setVariable(toStore, e);
|
||||
// }catch(ClassCastException e){
|
||||
// Wizardry.logger.error("Error reading entity from NBT: entity not of expected type", e);
|
||||
// }
|
||||
// }
|
||||
// return t;
|
||||
// });
|
||||
// }
|
||||
|
||||
// /** Creates a new {@code StoredVariable} for an {@link Entity} value with the given key. The returned
|
||||
// * {@code StoredVariable} has a ticker which extracts the entity from the given; this functionality will need to be
|
||||
// * replicated in any replacement ticker function. */
|
||||
// @SuppressWarnings("unchecked") // Can't check it due to type erasure
|
||||
// public static <T extends Entity> StoredVariable<T, NBTTagCompound> ofEntity(String key, Persistence persistence, IVariable<NBTTagCompound> storage){
|
||||
// // Well this is horrible
|
||||
// return new IStoredVariable.StoredVariable<>(key,
|
||||
// (T e) -> {
|
||||
// NBTTagCompound nbt = new NBTTagCompound();
|
||||
// nbt.setString("entityType", EntityList.getKey(e).toString());
|
||||
// e.writeToNBT(nbt);
|
||||
// return nbt;
|
||||
// },
|
||||
// t -> null, persistence)
|
||||
// .withTicker((p, e) -> {
|
||||
// if(e == null){
|
||||
// try{
|
||||
// NBTTagCompound nbt = WizardData.get(p).getVariable(storage);
|
||||
// if(nbt == null) return null;
|
||||
// e = (T)EntityList.createEntityByIDFromName(new ResourceLocation(nbt.getString("entityType")), p.world);
|
||||
// e.readFromNBT(nbt);
|
||||
// return e;
|
||||
// }catch(ClassCastException x){
|
||||
// Wizardry.logger.error("Error reading stored variable from NBT: entity not of expected type", x);
|
||||
// }
|
||||
// }
|
||||
// return null;
|
||||
// });
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package electroblob.wizardry.data;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
/**
|
||||
* Instances of this interface act as keys which allow spellData of any type to be stored in {@link WizardData} at
|
||||
* runtime. This means spells (or anything else, for that matter) may define their own storedVariables to be stored
|
||||
* with the player and handle those storedVariables themselves. This prevents {@code WizardData} from being cluttered
|
||||
* with spell-specific fields and allows addon mods to leverage {@code WizardData} for their own spells or other data,
|
||||
* rather than defining their own capability. This system is somewhat similar to {@code DataManager}.
|
||||
* <p></p>
|
||||
* Instances should be created once and stored statically (or pseudo-statically) in some sensible location, such
|
||||
* as a spell class. They can then be used as keys to access the values themselves via {@link WizardData}.
|
||||
* Encapsulation can also be achieved by simply restricting access to the keys.
|
||||
* <p></p>
|
||||
* @param <T> The type of variable stored.
|
||||
*/
|
||||
public interface IVariable<T> {
|
||||
|
||||
// To reiterate, instances of this interface are KEYS. They are both accessors for the data and define how
|
||||
// it is stored and handled, but they DO NOT CONTAIN THE ACTUAL DATA.
|
||||
// Only one instance exists for each thing to be stored, and is shared across instances of WizardData.
|
||||
|
||||
/** Convenience method that allows this variable to define tick behaviour. This is particularly useful for
|
||||
* trivial operations such as decrementing a value, for which a dedicated event handling method would be
|
||||
* unnecessarily verbose. */
|
||||
T update(EntityPlayer player, T value);
|
||||
|
||||
/**
|
||||
* Returns whether this variable persists when data is copied.
|
||||
* @param respawn True if the player died and is respawning, false if they are just travelling between dimensions.
|
||||
* @return True if the variable should be copied over, false if not.
|
||||
*/
|
||||
boolean isPersistent(boolean respawn);
|
||||
|
||||
/**
|
||||
* Returns whether this variable requires syncing with clients.
|
||||
* @return True if the variable should be synced with clients, false if not.
|
||||
*/
|
||||
boolean isSynced();
|
||||
|
||||
/**
|
||||
* Writes this variable's value to the given {@link ByteBuf}.
|
||||
*/
|
||||
void write(ByteBuf buf, T value);
|
||||
|
||||
/**
|
||||
* Reads this variable's value from the given {@link ByteBuf}.
|
||||
*/
|
||||
T read(ByteBuf buf);
|
||||
|
||||
/** If you're storing a lot of data, you can optionally implement this method to define a condition which, if
|
||||
* satisfied, will result in the data being removed from storage, reducing unnecessary syncing and saving. This
|
||||
* is particularly relevant if the value is synced as it reduces packet size. */
|
||||
default boolean canPurge(EntityPlayer player, T value){
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* General-purpose implementation of {@link IVariable} for non-stored variables. These may still, however, persist
|
||||
* across player respawn/dimension change.
|
||||
* <p></p>
|
||||
* @param <T> The type of variable stored.
|
||||
*/
|
||||
class Variable<T> implements IVariable<T> {
|
||||
|
||||
private final Persistence persistence;
|
||||
|
||||
private BiFunction<EntityPlayer, T, T> ticker;
|
||||
|
||||
public Variable(Persistence persistence){
|
||||
this.persistence = persistence;
|
||||
this.ticker = (p, t) -> t;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces this variable's update method with the given update function. <i>Beware of auto-unboxing of
|
||||
* primitive types! For lambda expressions, check the second parameter isn't null before operating on it.
|
||||
* For method references, do not reference a method that takes a primitive type. Otherwise, this will cause
|
||||
* a (difficult to debug) {@link NullPointerException} if the key was not stored.</i>
|
||||
* @param ticker A {@link BiFunction} specifying the actions to be performed on this variable each tick. The
|
||||
* {@code BiFunction} returns the new value for this variable.
|
||||
* @return This {@code Variable} object, allowing this method to be chained onto object creation.
|
||||
*/
|
||||
public Variable<T> withTicker(BiFunction<EntityPlayer, T, T> ticker){
|
||||
this.ticker = ticker;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T update(EntityPlayer player, T value){
|
||||
return ticker.apply(player, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPersistent(boolean respawn){
|
||||
return respawn ? persistence.persistsOnRespawn() : persistence.persistsOnDimensionChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSynced(){
|
||||
return false;// Not implemented for now, maybe we will one day
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(ByteBuf buf, T value){
|
||||
// NYI
|
||||
}
|
||||
|
||||
@Override
|
||||
public T read(ByteBuf buf){
|
||||
return null; // NYI
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package electroblob.wizardry.data;
|
||||
|
||||
/** Enum which defines the circumstances in which an {@link IVariable} persists, i.e. whether its value is carried over. */
|
||||
public enum Persistence {
|
||||
|
||||
NEVER(false, false),
|
||||
DIMENSION_CHANGE(false, true),
|
||||
RESPAWN(true, false),
|
||||
ALWAYS(true, true);
|
||||
|
||||
private boolean persistsOnRespawn, persistsOnDimensionChange;
|
||||
|
||||
Persistence(boolean persistsOnRespawn, boolean persistsOnDimensionChange){
|
||||
this.persistsOnRespawn = persistsOnRespawn;
|
||||
this.persistsOnDimensionChange = persistsOnDimensionChange;
|
||||
}
|
||||
|
||||
public boolean persistsOnRespawn(){
|
||||
return persistsOnRespawn;
|
||||
}
|
||||
|
||||
public boolean persistsOnDimensionChange(){
|
||||
return persistsOnDimensionChange;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package electroblob.wizardry.data;
|
||||
|
||||
import electroblob.wizardry.Wizardry;
|
||||
import electroblob.wizardry.command.SpellEmitter;
|
||||
import electroblob.wizardry.packet.PacketEmitterData;
|
||||
import electroblob.wizardry.packet.WizardryPacketHandler;
|
||||
import electroblob.wizardry.spell.Spell;
|
||||
import electroblob.wizardry.util.NBTExtras;
|
||||
import electroblob.wizardry.util.SpellModifiers;
|
||||
import net.minecraft.entity.player.EntityPlayerMP;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.nbt.NBTTagList;
|
||||
import net.minecraft.util.EnumFacing;
|
||||
import net.minecraft.world.World;
|
||||
import net.minecraft.world.storage.WorldSavedData;
|
||||
import net.minecraftforge.common.util.Constants;
|
||||
import net.minecraftforge.event.world.WorldEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.gameevent.PlayerEvent;
|
||||
import net.minecraftforge.fml.common.gameevent.TickEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Class responsible for storing and keeping track of {@link SpellEmitter}s. Each world has its own instance of
|
||||
* {@code SpellEmitterData} which can be retrieved using {@link SpellEmitterData#get(World)}.<br>
|
||||
* <br>
|
||||
* To add a new {@code SpellEmitter}, use {@link SpellEmitter#add(Spell, World, double, double, double, EnumFacing, int, SpellModifiers)}.
|
||||
*
|
||||
* @since Wizardry 4.2
|
||||
* @author Electroblob
|
||||
*/
|
||||
@Mod.EventBusSubscriber
|
||||
public class SpellEmitterData extends WorldSavedData {
|
||||
|
||||
public static final String NAME = Wizardry.MODID + "_spell_emitters";
|
||||
|
||||
private final List<SpellEmitter> emitters = new ArrayList<>();
|
||||
|
||||
private NBTTagList emitterTags = null;
|
||||
|
||||
// Required constructors
|
||||
public SpellEmitterData(){
|
||||
this(NAME);
|
||||
}
|
||||
|
||||
public SpellEmitterData(String name){
|
||||
super(name);
|
||||
}
|
||||
|
||||
/** Returns the spell emitter data for this world, or creates a new instance if it doesn't exist yet. */
|
||||
public static SpellEmitterData get(World world){
|
||||
|
||||
SpellEmitterData instance = (SpellEmitterData)world.getPerWorldStorage().getOrLoadData(SpellEmitterData.class, NAME);
|
||||
|
||||
if(instance == null){
|
||||
instance = new SpellEmitterData();
|
||||
world.getPerWorldStorage().setData(NAME, instance);
|
||||
}else if(instance.emitters.isEmpty() && instance.emitterTags != null){
|
||||
instance.loadEmitters(world);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
/** Sends the active spell emitters for this world to the specified player's client. */
|
||||
public void sync(EntityPlayerMP player){
|
||||
PacketEmitterData.Message msg = new PacketEmitterData.Message(emitters);
|
||||
WizardryPacketHandler.net.sendTo(msg, player);
|
||||
Wizardry.logger.info("Synchronising spell emitters for " + player.getName());
|
||||
}
|
||||
|
||||
/** Adds the given {@link SpellEmitter} to the list of emitters for this {@code SpellEmitterData}. */
|
||||
public void add(SpellEmitter emitter){
|
||||
emitters.add(emitter);
|
||||
markDirty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readFromNBT(NBTTagCompound nbt){
|
||||
emitterTags = nbt.getTagList("emitters", Constants.NBT.TAG_COMPOUND);
|
||||
}
|
||||
|
||||
private void loadEmitters(World world){
|
||||
emitters.clear();
|
||||
emitters.addAll(NBTExtras.NBTToList(emitterTags, (NBTTagCompound t) -> SpellEmitter.fromNBT(world, t)));
|
||||
emitterTags = null; // Now we know it's loaded
|
||||
}
|
||||
|
||||
@Override
|
||||
public NBTTagCompound writeToNBT(NBTTagCompound compound){
|
||||
compound.setTag("emitters", NBTExtras.listToNBT(emitters, SpellEmitter::toNBT));
|
||||
return compound;
|
||||
}
|
||||
|
||||
public static void update(World world){
|
||||
SpellEmitterData data = SpellEmitterData.get(world);
|
||||
if(!data.emitters.isEmpty()){
|
||||
data.emitters.forEach(SpellEmitter::update);
|
||||
data.emitters.removeIf(SpellEmitter::needsRemoving);
|
||||
data.markDirty(); // Mark dirty if there are changes to be saved
|
||||
}
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void tick(TickEvent.WorldTickEvent event){
|
||||
if(!event.world.isRemote && event.phase == TickEvent.Phase.END){
|
||||
update(event.world);
|
||||
}
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onWorldLoadEvent(WorldEvent.Load event){
|
||||
// Called to initialise the spell emitter data when a world loads, if it isn't already.
|
||||
SpellEmitterData.get(event.getWorld());
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onPlayerChangedDimensionEvent(PlayerEvent.PlayerChangedDimensionEvent event){
|
||||
// Needs to be done here as well as PlayerLoggedInEvent because SpellEmitterData is dimension-specific
|
||||
if(event.player instanceof EntityPlayerMP){
|
||||
SpellEmitterData.get(event.player.world).sync((EntityPlayerMP)event.player);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package electroblob.wizardry.data;
|
||||
|
||||
import electroblob.wizardry.Wizardry;
|
||||
import electroblob.wizardry.packet.PacketGlyphData;
|
||||
import electroblob.wizardry.packet.WizardryPacketHandler;
|
||||
import electroblob.wizardry.spell.Spell;
|
||||
import net.minecraft.entity.player.EntityPlayerMP;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.nbt.NBTTagList;
|
||||
import net.minecraft.world.World;
|
||||
import net.minecraft.world.storage.WorldSavedData;
|
||||
import net.minecraftforge.common.util.Constants.NBT;
|
||||
import net.minecraftforge.event.world.WorldEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Class responsible for generating and storing the randomised spell names and descriptions for each world, which are
|
||||
* displayed as glyphs using the SGA font renderer.
|
||||
*
|
||||
* @since Wizardry 1.1
|
||||
*/
|
||||
@Mod.EventBusSubscriber
|
||||
public class SpellGlyphData extends WorldSavedData {
|
||||
|
||||
public static final String NAME = Wizardry.MODID + "_glyphData";
|
||||
|
||||
public Map<Spell, String> randomNames = new HashMap<>(Spell.getTotalSpellCount());
|
||||
public Map<Spell, String> randomDescriptions = new HashMap<>(Spell.getTotalSpellCount());
|
||||
|
||||
// Required constructors
|
||||
public SpellGlyphData(){
|
||||
this(NAME);
|
||||
}
|
||||
|
||||
public SpellGlyphData(String name){
|
||||
super(name);
|
||||
}
|
||||
|
||||
/** Generates random names and descriptions for any spells which don't already have them. */
|
||||
public void generateGlyphNames(World world){
|
||||
|
||||
for(Spell spell : Spell.getSpells(Spell.allSpells)){
|
||||
if(!randomNames.containsKey(spell)) randomNames.put(spell, generateRandomName(world.rand));
|
||||
}
|
||||
|
||||
for(Spell spell : Spell.getSpells(Spell.allSpells)){
|
||||
if(!randomDescriptions.containsKey(spell))
|
||||
randomDescriptions.put(spell, generateRandomDescription(world.rand));
|
||||
}
|
||||
|
||||
this.markDirty();
|
||||
}
|
||||
|
||||
private String generateRandomName(Random random){
|
||||
|
||||
String name = "";
|
||||
|
||||
for(int i = 0; i < random.nextInt(2) + 2; i++){
|
||||
name = name + RandomStringUtils.random(3 + random.nextInt(5), "abcdefghijklmnopqrstuvwxyz") + " ";
|
||||
}
|
||||
|
||||
return name.trim();
|
||||
}
|
||||
|
||||
private String generateRandomDescription(Random random){
|
||||
|
||||
String name = "";
|
||||
|
||||
for(int i = 0; i < random.nextInt(16) + 8; i++){
|
||||
name = name + RandomStringUtils.random(2 + random.nextInt(7), "abcdefghijklmnopqrstuvwxyz") + " ";
|
||||
}
|
||||
|
||||
return name.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the spell glyph data for this world, or creates a new instance if it doesn't exist yet. Also checks for
|
||||
* any spells that are missing glyph data and adds it accordingly.
|
||||
*/
|
||||
public static SpellGlyphData get(World world){
|
||||
|
||||
SpellGlyphData instance = (SpellGlyphData)world.loadData(SpellGlyphData.class, NAME);
|
||||
|
||||
if(instance == null){
|
||||
instance = new SpellGlyphData();
|
||||
}
|
||||
|
||||
// These two conditions are a bit of backwards compatibility from when I added the descriptions to the
|
||||
// glyph data. Shouldn't be needed in normal operation, but I might as well leave it here.
|
||||
// Edit: More backwards compatibility, this time for the future - should any new spells be added, this now
|
||||
// ensures
|
||||
// existing worlds will generate random names and descriptions for any new spells whilst keeping the old ones.
|
||||
if(instance.randomNames.size() < Spell.getTotalSpellCount()
|
||||
|| instance.randomDescriptions.size() < Spell.getTotalSpellCount()){
|
||||
instance.generateGlyphNames(world);
|
||||
world.setData(NAME, instance);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
/** Sends the random spell names for this world to the specified player's client. */
|
||||
public void sync(EntityPlayerMP player){
|
||||
|
||||
List<String> names = new ArrayList<>();
|
||||
List<String> descriptions = new ArrayList<>();
|
||||
|
||||
int id = 0;
|
||||
|
||||
while(id < Spell.getTotalSpellCount()){
|
||||
Spell spell = Spell.byNetworkID(id + 1); // +1 because the None spell is not included
|
||||
names.add(this.randomNames.get(spell));
|
||||
descriptions.add(this.randomDescriptions.get(spell));
|
||||
id++;
|
||||
}
|
||||
|
||||
PacketGlyphData.Message msg = new PacketGlyphData.Message(names, descriptions);
|
||||
|
||||
WizardryPacketHandler.net.sendTo(msg, player);
|
||||
|
||||
Wizardry.logger.info("Synchronising spell glyph data for " + player.getName());
|
||||
|
||||
}
|
||||
|
||||
/** Helper method to retrieve the random glyph name for the given spell from the map stored in the given world. */
|
||||
public static String getGlyphName(Spell spell, World world){
|
||||
Map<Spell, String> names = SpellGlyphData.get(world).randomNames;
|
||||
return names == null ? "" : names.get(spell);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to retrieve the random glyph description for the given spell from the map stored in the given
|
||||
* world.
|
||||
*/
|
||||
public static String getGlyphDescription(Spell spell, World world){
|
||||
Map<Spell, String> descriptions = SpellGlyphData.get(world).randomDescriptions;
|
||||
return descriptions == null ? "" : descriptions.get(spell);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readFromNBT(NBTTagCompound nbt){
|
||||
|
||||
this.randomNames = new HashMap<>();
|
||||
this.randomDescriptions = new HashMap<>();
|
||||
|
||||
NBTTagList tagList = nbt.getTagList("spellGlyphData", NBT.TAG_COMPOUND);
|
||||
|
||||
for(int i = 0; i < tagList.tagCount(); i++){
|
||||
NBTTagCompound tag = tagList.getCompoundTagAt(i);
|
||||
randomNames.put(Spell.byMetadata(tag.getInteger("spell")), tag.getString("name"));
|
||||
randomDescriptions.put(Spell.byMetadata(tag.getInteger("spell")), tag.getString("description"));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public NBTTagCompound writeToNBT(NBTTagCompound nbt){
|
||||
|
||||
NBTTagList tagList = new NBTTagList();
|
||||
|
||||
for(Spell spell : Spell.getSpells(Spell.allSpells)){
|
||||
// Much like the enchantments tag for items, this stores a list of spell-id-to-name tag pairs
|
||||
// The description is now also included; there's no point in making a second compound tag!
|
||||
NBTTagCompound tag = new NBTTagCompound();
|
||||
tag.setInteger("spell", spell.metadata());
|
||||
tag.setString("name", this.randomNames.get(spell));
|
||||
tag.setString("description", this.randomDescriptions.get(spell));
|
||||
tagList.appendTag(tag);
|
||||
}
|
||||
|
||||
nbt.setTag("spellGlyphData", tagList);
|
||||
|
||||
return nbt;
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onWorldLoadEvent(WorldEvent.Load event){
|
||||
if(!event.getWorld().isRemote && event.getWorld().provider.getDimension() == 0){
|
||||
// Called to initialise the spell glyph data when a world loads, if it isn't already.
|
||||
SpellGlyphData.get(event.getWorld());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,656 @@
|
||||
package electroblob.wizardry.data;
|
||||
|
||||
import com.google.common.collect.EvictingQueue;
|
||||
import electroblob.wizardry.Wizardry;
|
||||
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.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;
|
||||
|
||||
/**
|
||||
* 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></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 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<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;
|
||||
|
||||
/** Internal storage of registered variable keys. This only contains the stored keys. */
|
||||
private static final Set<IStoredVariable> storedVariables = new HashSet<>();
|
||||
|
||||
/** The maximum number of recent spells to track. */
|
||||
public static final int MAX_RECENT_SPELLS = 10;
|
||||
|
||||
/** The player this WizardData instance belongs to. */
|
||||
private final EntityPlayer player;
|
||||
|
||||
/** An instance of {@link Random} which is <i>guaranteed</i> to produce the same number sequence client and server
|
||||
* side <i>provided that it is always called from common code.</i> This can be useful in reducing the number of
|
||||
* packets sent in certain situations.<br>
|
||||
* <br>
|
||||
* 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. <i>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.</i>*/
|
||||
public SpellModifiers itemCastingModifiers;
|
||||
|
||||
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;
|
||||
|
||||
/** 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<IVariable, Object> spellData;
|
||||
|
||||
private Queue<Spell> 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<Imbuement, Integer> 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 10 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<WizardData>(){
|
||||
// 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<WizardData> capability, WizardData instance, EnumFacing side){
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readNBT(Capability<WizardData> 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. Used internally for packet reading. */
|
||||
public static Set<IVariable> getSyncedVariables(){
|
||||
return storedVariables.stream().filter(IVariable::isSynced).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 <T> 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<Number>}, a {@code Number} will
|
||||
* be returned when the value is retrieved.
|
||||
*/
|
||||
// This use of type parameters guarantees that spellData 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 <T> 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 <T> The type of the returned value.
|
||||
* @return The value associated with the given key, or null no such key was stored. <i>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.</i>
|
||||
*/
|
||||
@SuppressWarnings("unchecked") // The spellData map is fully encapsulated so we can be sure that the cast is safe
|
||||
@Nullable
|
||||
public <T> T getVariable(IVariable<T> 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);
|
||||
}
|
||||
|
||||
// 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(spell);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of times the given spell is tracked in this player's recently-cast spells.
|
||||
* @param spell The spell to count casts for.
|
||||
*/
|
||||
public int countRecentCasts(Spell spell){
|
||||
return (int)this.recentSpells.stream().filter(s -> s == spell).count(); // We know this can't be more than 10
|
||||
}
|
||||
|
||||
// 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! (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()){
|
||||
|
||||
NBTTagList enchantmentList = stack.getItem() == Items.ENCHANTED_BOOK ?
|
||||
ItemEnchantedBook.getEnchantments(stack) : stack.getEnchantmentTagList();
|
||||
|
||||
Iterator<NBTBase> 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);
|
||||
}else{
|
||||
// Otherwise, removes the enchantment from the item
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Removes all imbuements from the map that are no longer active
|
||||
this.imbuementDurations.keySet().retainAll(activeImbuements);
|
||||
}
|
||||
|
||||
// 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 spellData which had duplicate
|
||||
// functions, just for different enchantments.
|
||||
updateImbuedItems();
|
||||
updateContinuousSpellCasting();
|
||||
|
||||
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 spellData that should persist over player death. This
|
||||
* is the inverse of the old onPlayerDeath method, which reset the spellData that shouldn't persist.
|
||||
*
|
||||
* @param data The old WizardData whose spellData 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){
|
||||
|
||||
this.allies = data.allies;
|
||||
this.allyNames = data.allyNames;
|
||||
this.selectedMinion = data.selectedMinion;
|
||||
this.spellsDiscovered = data.spellsDiscovered;
|
||||
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();
|
||||
|
||||
properties.setTag("imbuements", NBTExtras.mapToNBT(this.imbuementDurations,
|
||||
imbuement -> new NBTTagInt(Enchantment.getEnchantmentID((Enchantment)imbuement)), NBTTagInt::new));
|
||||
|
||||
// Mmmmmm Java 8....
|
||||
properties.setTag("allies", NBTExtras.listToNBT(this.allies, NBTUtil::createUUIDTag));
|
||||
properties.setTag("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.setTag("recentSpells", NBTExtras.listToNBT(recentSpells, s -> new NBTTagInt(s.metadata())));
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
// Probably won't be null but we may as well just reinitialise it instead of clearing it
|
||||
this.recentSpells = EvictingQueue.create(MAX_RECENT_SPELLS);
|
||||
this.recentSpells.addAll(NBTExtras.NBTToList(nbt.getTagList("recentSpells", NBT.TAG_INT),
|
||||
(NBTTagInt tag) -> Spell.byMetadata(tag.getInt())));
|
||||
|
||||
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<Entity> 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<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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user