Skip to content

Instantly share code, notes, and snippets.

@copygirl
Last active June 9, 2019 08:12
Show Gist options
  • Select an option

  • Save copygirl/836c43b843cf7ed8ab343e852e0a0213 to your computer and use it in GitHub Desktop.

Select an option

Save copygirl/836c43b843cf7ed8ab343e852e0a0213 to your computer and use it in GitHub Desktop.
package net.mcft.copy.wearables.api;
import java.util.Collection;
import java.util.Collections;
import java.util.stream.Stream;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EquipmentSlot;
import net.minecraft.entity.LivingEntity;
/**
* Implemented by all entities extending {@link LivingEntity} through mixin.
* Allows accessing an entity's {@link IWearableSlot IWearableSlots}, getting
* and setting which items are equipped through Wearables.
* <p>
* Use {@link IWearablesEntity#is} to test whether an entity may support Wearables and
* {@link IWearablesEntity#from} to safely get an IWearablesEntity. This is to account
* for the later possibility of implementing Wearables using component-style properties.
*/
public interface IWearablesEntity
{
/** Returns whether this entity has any items equipped
* in Wearables slots, including in Vanilla armor slots. */
public boolean hasWearables();
/**
* Returns a stream of {@link IWearablesSlot} in which this entity has equipped Wearables.
* <p>
* May also include items worn in invalid slots. See {@link IWearablesSlot#isValid}.
*
* @return A stream of slots in which there is a worn item, or an empty
* stream if none, or the entity doesn't support Wearables.
*/
public Stream<IWearablesSlot> getEquippedWearables();
/**
* Returns a stream of {@link IWearablesSlot} in the specified
* {@link WearablesRegion} in which this entity has equipped Wearables.
* <p>
* May also include items worn in invalid slots. See {@link IWearablesSlot#isValid}.
*
* @param region A region such {@link WearablesRegion#CHEST} or {@link WearablesRegion#BACK}.
* @throws IllegalArgumentException Thrown if region is {@code null}.
* @return A stream of slots in which there is a worn item, or an empty
* stream if none, or the entity doesn't support Wearables.
*/
public Stream<IWearablesSlot> getEquippedWearables(WearablesRegion region);
/**
* Returns a stream of {@link IWearablesSlot} in slots with the specified
* {@link WearablesSlotType} in which this entity has equipped Wearables.
* <p>
* May also include items worn in invalid slots. See {@link IWearablesSlot#isValid}.
*
* @param slotType A slot type such as {@link WearablesSlotType#CHESTPLATE}.
* @throws IllegalArgumentException Thrown if slotType is {@code null}.
* @return A stream of slots in which there is a worn item, or an empty
* stream if none, or the entity doesn't support Wearables.
*/
public Stream<IWearablesSlot> getEquippedWearables(WearablesSlotType slotType);
/**
* Returns this entity's supported regions, such as
* {@link WearablesRegion#CHEST} or {@link WearablesRegion#BACK}.
*
* @return A collection of regions into which Wearables may be worn for this entity,
* or an empty collection if none or the entity doesn't support Wearables.
*/
public Collection<WearablesRegion> getSupportedWearablesRegions();
/**
* Returns this entity's supported {@link WearablesSlotType WearablesSlotTypes},
* such as {@link WearablesSlotType#CHESTPLATE}.
*
* @return A collection of slot types for any region into which Wearables may be worn for
* this entity, or an empty collection if none or the entity doesn't support Wearables.
*/
public Collection<WearablesSlotType> getSupportedWearablesSlotTypes();
/**
* Returns this entity's supported {@link WearablesSlotType WearablesSlotTypes}
* for the specified region, such as {@link WearablesSlotType#CHESTPLATE}.
*
* @throws IllegalArgumentException Thrown if region is {@code null}.
* @return A collection of slot types for the specified region into which
* Wearables may be worn for this entity, or an empty collection if
* none, the entity doesn't support Wearables or the specified region.
*/
public Collection<WearablesSlotType> getSupportedWearablesSlotTypes(WearablesRegion region);
/**
* Returns a stream of {@link IWearablesSlot IWearablesSlots}
* this entity supports. Always includes Vanilla armor slots.
*
* @return A collection of slots supported by this entity, or
* an empty stream if it doesn't support Wearables.
*/
public Collection<IWearablesSlot> getSupportedWearablesSlots();
/**
* Returns a stream of {@link IWearablesSlot IWearablesSlots} in the specified
* region which this entity supports. Always includes Vanilla armor slots.
* <p>
* You may use {@code getSupportedRegions().contains(region)} to test
* whether the specified region is supported for this entity, first.
*
* @throws IllegalArgumentException Thrown if region is {@code null}.
* @return A collection of slots supported by this entity, or an empty stream if
* it doesn't support Wearables at all, or just not the specified region.
*/
public Collection<IWearablesSlot> getSupportedWearableSlots(WearablesRegion region);
/**
* Returns a stream of {@link IWearablesSlot IWearablesSlots} of the specified slot types
* which this entity supports. Always returns {@link IWearablesSlotType#getSlotCount} slots.
* <p>
* You may use {@code getSupportedWearablesSlotTypes().contains(slotType)} to
* test whether the specified slot type is supported for this entity, first.
*
* @throws IllegalArgumentException Thrown if region is {@code null}.
* @return A stream of slots supported by this entity, or an empty stream if it
* doesn't support Wearables at all, or just not the specified slot type.
*/
public Collection<IWearablesSlot> getSupportedWearableSlots(WearablesSlotType slotType);
/**
* Returns a specific {@link IWearablesSlot} of the specified slot type and index.
*
* @param slotType The slot type of which to get a slot from.
* @param index The slot type relative index of which to get a slot from.
* @param force If {@code true}, may return an invalid slot. See {@link IWearablesSlot#isValid}.
* If {@code false}, instead throws an exception in those cases.
*
* @throws IllegalArgumentException Thrown if slotType is {@code null} or index is negative.
* @throws UnsupportedOperationException Thrown if this entity doesn't support Wearables.
* You may test this using {@link IWearablesEntity#is}.
* If force is {@code false}, also thrown if slotType or index is not supported.
* @return A slot for this entity which may be used to get and set the worn Wearable in it.
*/
public IWearablesSlot getWearablesSlot(WearablesSlotType slotType, int index, boolean force);
/**
* Returns a specific {@link IWearablesSlot} of the specified slot type and index.
*
* @param slotType The slot type of which to get a slot from.
* @param index The slot type relative index of which to get a slot from.
*
* @throws IllegalArgumentException Thrown if slotType is {@code null} or index is negative.
* @throws UnsupportedOperationException Thrown if this entity doesn't support Wearables.
* You may test this using {@link IWearablesEntity#is}.
* Also thrown if slotType or index is not supported.
* @return A slot for this entity which may be used to get and set the worn Wearable in it.
*/
public default IWearablesSlot getWearablesSlot(WearablesSlotType slotType, int index)
{ return getWearablesSlot(slotType, index, false); }
/**
* Returns a specific {@link IWearablesSlot} that is associated with the specified Vanilla armor {@link EquipmentSlot}.
*
* @param slot The Vanilla equipment slot which to get an associated Wearables slot for.
* @param force If {@code true}, may return an invalid slot. See {@link IWearablesSlot#isValid}.
* If {@code false}, instead throws an exception in those cases.
*
* @throws IllegalArgumentException Thrown if slot is {@code null} or its type is not {@link EquipmentSlot.Type#ARMOR}.
* @throws UnsupportedOperationException Thrown if this entity doesn't support Wearables.
* You may test this using {@link IWearablesEntity#is}.
* If force is {@code false}, also thrown if the {@link WearablesSlotType}
* associated with the specified slot is not supported for this entity.
* @return A slot for this entity which may be used to get and set the worn Wearable in it.
*/
public default IWearablesSlot getWearablesSlot(EquipmentSlot slot, boolean force)
{ return getWearablesSlot(WearablesSlotType.fromVanillaSlot(slot), 0, force); }
/**
* Returns a specific {@link IWearablesSlot} that is associated with the specified Vanilla armor {@link EquipmentSlot}.
*
* @param slot The Vanilla equipment slot which to get an associated Wearables slot for.
*
* @throws IllegalArgumentException Thrown if slot is {@code null} or its type is not {@link EquipmentSlot.Type#ARMOR}.
* @throws UnsupportedOperationException Thrown if this entity doesn't support Wearables.
* You may test this using {@link IWearablesEntity#is}.
* Also thrown if the {@link IWearablesSlotType} associated
* with the specified slot is not supported for this entity.
* @return A slot for this entity which may be used to get and set the worn Wearable in it.
*/
public default IWearablesSlot getWearablesSlot(EquipmentSlot slot)
{ return getWearablesSlot(slot, false); }
public static final IWearablesEntity DUMMY = new IWearablesEntity()
{
@Override public boolean hasWearables() { return false; }
@Override public Stream<IWearablesSlot> getEquippedWearables() { return Stream.empty(); }
@Override public Stream<IWearablesSlot> getEquippedWearables(WearablesRegion region) { return Stream.empty(); }
@Override public Stream<IWearablesSlot> getEquippedWearables(WearablesSlotType slotType) { return Stream.empty(); }
@Override public Collection<WearablesRegion> getSupportedWearablesRegions() { return Collections.emptyList(); }
@Override public Collection<WearablesSlotType> getSupportedWearablesSlotTypes() { return Collections.emptyList(); }
@Override public Collection<WearablesSlotType> getSupportedWearablesSlotTypes(WearablesRegion region) { return Collections.emptyList(); }
@Override public Collection<IWearablesSlot> getSupportedWearablesSlots() { return Collections.emptyList(); }
@Override public Collection<IWearablesSlot> getSupportedWearableSlots(WearablesRegion region) { return Collections.emptyList(); }
@Override public Collection<IWearablesSlot> getSupportedWearableSlots(WearablesSlotType slotType) { return Collections.emptyList(); }
@Override public IWearablesSlot getWearablesSlot(WearablesSlotType slotType, int index, boolean force)
{ throw new UnsupportedOperationException("Entity doesn't support Wearables"); }
};
/** Returns whether the specified entity may have Wearables equipped. */
public static boolean is(Entity entity)
{ return (entity instanceof IWearablesEntity); }
/** Returns the specified {@link Entity} casted as
* {@link IWearablesEntity}, or {@link #DUMMY} if it isn't. */
public static IWearablesEntity from(Entity entity)
{ return is(entity) ? (IWearablesEntity)entity : DUMMY; }
}
package net.mcft.copy.wearables.api;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
/**
* Implemented by {@link Item Items} to add additional
* control over equipment rules and a tick handler.
*/
public interface IWearablesItem
{
/**
* Returns whether the specified {@link ItemStack} can be equipped in the
* specified {@link IWearablesSlot} by the entity. This doesn't necessarily
* prevent an item from being equipped through other means.
* <p>
* Note that which slots an item can be equipped into is controlled by
* configuration stored in data packs under {@code config/wearables/items/}
* and not just this method.
*
* @param slot The slot to be checked.
* Use {@link IWearablesSlot#getEntity()} to access the entity
* (not guaranteed to be a {@link PlayerEntity}).
* @param stack The exact stack to be checked whether it can be equipped.
*/
public default boolean canEquip(IWearablesSlot slot, ItemStack stack) { return true; }
/**
* Returns whether this item can be unequipped from the specified
* {@link IWearablesSlot} by the entity. This doesn't necessarily prevent
* the item from being unequipped through other means, such as death.
*
* @param slot The slot to be checked.
* Use {@link IWearablesSlot#get()} to get the item to be
* checked whether it can be unequipped.
* Use {@link IWearablesSlot#getEntity()} to access the entity
* (not guaranteed to be a {@link PlayerEntity}).
*/
public default boolean canUnequip(IWearablesSlot slot) { return true; }
/**
* Called right after this item is equipped in the specified {@link IWearablesSlot},
* both on the client (which isn't authoritative) and the server.
*
* @param slot The slot the item got equipped to.
* Use {@link IWearablesSlot#get()} to get the full
* {@link ItemStack} that got equipped.
* Use {@link IWearablesSlot#getEntity()} to access the entity
* (not guaranteed to be a {@link PlayerEntity}).
*/
public default void onEquip(IWearablesSlot slot) { }
/**
* Called right before this item is unequipped from the specified
* {@link IWearablesSlot}. Also called when the item is dropped / broken,
* due to death or taking damage.
*
* @param slot The slot the item is being uneqipped from.
* Use {@link IWearablesSlot#get()} to get the full
* {@link ItemStack} about to be unequipped.
* Use {@link IWearablesSlot#getEntity()} to access the entity
* (not guaranteed to be a {@link PlayerEntity}).
*/
public default void onUnequip(IWearablesSlot slot) { }
/** Returns whether {@link #onEquippedTick} should be called for this item. */
public default boolean doesTick() { return false; }
/**
* Called each tick while this item is equipped in the specified {@link IWearablesSlot}.
* Only called if {@link #doesTick()} returns true (false by default!).
*
* @param slot The slot the item is currently equipped in.
* Use {@link IWearablesSlot#get()} to get the full
* {@link ItemStack} that is currently equipped.
* Use {@link IWearablesSlot#getEntity()} to access the entity
* (not guaranteed to be a {@link PlayerEntity}).
* @param equippedTime The number of ticks (usually 20th of a second) since
* the item got equipped. Can be used to run code only
* every number of ticks: <p>
* {@code if ((equippedTime % 20) != 0) return;}
*/
public default void onEquippedTick(IWearablesSlot slot, int equippedTime) { }
public static final IWearablesItem DUMMY = new IWearablesItem(){};
/** Returns the specified {@link Item} casted as
* {@link IWearablesItem}, or {@link #DUMMY} if it isn't. */
public static IWearablesItem from(Item item)
{ return (item instanceof IWearablesItem) ? (IWearablesItem)item : DUMMY; }
}
package net.mcft.copy.wearables.api;
import net.minecraft.enchantment.EnchantmentHelper;
import net.minecraft.entity.Entity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.ItemStack;
/** Interface for a specific slot on an entity into which a wearable item may be equipped. */
public interface IWearablesSlot
{
/** Gets the entity this slot is attached to. */
public Entity getEntity();
/** Gets the {@link WearablesSlotType} of this slot. */
public WearablesSlotType getSlotType();
/** Gets the numeric index of the slot, differenciating it from other slots of the same type. */
public int getIndex();
/** Gets the {@link WearablesRegion} of this slot. */
public default WearablesRegion getRegion()
{ return getSlotType().getRegion(); }
/**
* Gets the "order" of this slot, whether it's "above" or "below" other slots.
* Smaller number is above, larger is below. Always 0 for Vanilla slots. Default is 500.
* <p>
* TODO: Eventually, order may change on the fly. For example, an amulet could be worn above or below the chestplate.
*/
public int getOrder();
/**
* Gets whether this slot is valid / supported by this entity.
* <p>
* This is {@code false} if the slot type is not supported or the index is
* equal to or larger than {@link IWearablesSlotType#getSlotCount}, which
* can happen if the item is worn in a slot that is later removed from the
* data pack or its count changed. Invalid slots might be returned from
* {@link IWearablesEntity#getEquippedWearables getEquippedWearables} or
* {@link IWearablesEntity#getWearablesSlot getWearablesSlot(..., true)}.
* <p>
* An invalid slot still allows setting the stack, but
* it should only ever be set to {@link ItemStack#EMPTY}.
*/
public default boolean isValid()
{ return IWearablesEntity.from(getEntity()).getSupportedWearablesSlots().contains(this); }
/** Gets the {@link ItemStack} currently contained
* in this slot, or {@link ItemStack#EMPTY} if empty. */
public ItemStack get();
/** Sets the {@link ItemStack} contained in this slot, without
* checking if the specified stack is valid or can be equipped. */
public void set(ItemStack value);
/**
* Returns if the specified {@link ItemStack} can be equipped in this slot.
* Does not check if a currently equipped stack can be unequipped.
* Returns {@code true} if the specified stack is {@code null}.
*/
public default boolean canEquip(ItemStack stack)
{ return stack.isEmpty() || (isValid() && IWearablesItem.from(stack.getItem()).canEquip(this, stack)); }
/** Returns if the {@link ItemStack} currently contained in this slot can
* be unequipped. Returns {@code true} if the current stack is {@code null}. */
public default boolean canUnequip()
{
return get().isEmpty() ||
((getEntity() instanceof PlayerEntity) && ((PlayerEntity)getEntity()).isCreative()) ||
(!EnchantmentHelper.hasBindingCurse(get()) && IWearablesItem.from(get().getItem()).canUnequip(this));
}
}
package net.mcft.copy.wearables.api;
import java.util.regex.Pattern;
public final class WearablesRegion
{
public static final Pattern REGION_REGEX
= Pattern.compile("[a-z]+");
public static final WearablesRegion HEAD = new WearablesRegion("head");
public static final WearablesRegion CHEST = new WearablesRegion("chest");
public static final WearablesRegion LEGS = new WearablesRegion("legs");
public static final WearablesRegion FEET = new WearablesRegion("feet");
public static final WearablesRegion BACK = new WearablesRegion("back");
public static final WearablesRegion ARMS = new WearablesRegion("arms");
public final String name;
public WearablesRegion(String name)
{
if ((name == null) || name.isEmpty())
throw new IllegalArgumentException("name is null or empty");
if (!REGION_REGEX.matcher(name).matches())
throw new IllegalArgumentException("name '" + name + "' is not a valid slot type string");
this.name = name;
}
@Override
public String toString()
{ return name; }
@Override
public boolean equals(Object obj)
{
return (obj instanceof WearablesRegion)
&& ((WearablesRegion)obj).name.equals(this.name);
}
@Override
public int hashCode()
{ return this.name.hashCode(); }
}
package net.mcft.copy.wearables.api;
import java.util.regex.Pattern;
import net.minecraft.entity.EquipmentSlot;
public final class WearablesSlotType
{
public static final Pattern SLOT_TYPE_REGEX
= Pattern.compile("(?<region>[a-z]+):([a-z]+/)*(?<name>[a-z]+)");
public static final WearablesSlotType HELMET = new WearablesSlotType( "head:armor/helmet" );
public static final WearablesSlotType CHESTPLATE = new WearablesSlotType("chest:armor/chestplate");
public static final WearablesSlotType LEGGINGS = new WearablesSlotType( "legs:armor/leggings" );
public static final WearablesSlotType BOOTS = new WearablesSlotType( "feet:armor/boots" );
public final String fullName;
public WearablesSlotType(String fullName)
{
if ((fullName == null) || fullName.isEmpty())
throw new IllegalArgumentException("fullName is null or empty");
if (!SLOT_TYPE_REGEX.matcher(fullName).matches())
throw new IllegalArgumentException("fullName '" + fullName + "' is not a valid slot type string");
this.fullName = fullName;
}
public static WearablesSlotType fromVanillaSlot(EquipmentSlot slot)
{
if (slot == null)
throw new IllegalArgumentException("slot is null");
if (slot.getType() != EquipmentSlot.Type.ARMOR)
throw new IllegalArgumentException("slot's type is not ARMOR");
switch (slot) {
case HEAD: return HELMET;
case CHEST: return CHESTPLATE;
case LEGS: return LEGGINGS;
case FEET: return BOOTS;
default: throw new IllegalArgumentException(
"slot is not HEAD, CHEST, LEGS or FEET (how did this happen?)");
}
}
public WearablesRegion getRegion()
{ return new WearablesRegion(this.fullName.substring(0, this.fullName.indexOf(':'))); }
public String getName()
{
int start = this.fullName.lastIndexOf('/');
if (start < 0) start = this.fullName.indexOf(':');
return this.fullName.substring(start + 1);
}
// TODO: Is this needed? Is this the proper implementation?
// public CharSequence[] getParts()
// {
// int start = 0;
// int numSeparators = 0;
// for (int i = 0; i < this.fullName.length(); i++)
// switch (this.fullName.charAt(i)) {
// case ':': start = i + 1; break;
// case '/': numSeparators++; break;
// }
//
// int index = 0;
// CharSequence[] parts = new CharSequence[numSeparators + 1];
// for (int i = start; i < this.fullName.length(); i++)
// if (this.fullName.charAt(i) == '/') {
// parts[index++] = this.fullName.subSequence(start, i);
// start = i + 1;
// }
// parts[index] = this.fullName.subSequence(start, this.fullName.length() - 1);
// return parts;
// }
@Override
public String toString()
{ return fullName; }
@Override
public boolean equals(Object obj)
{
return (obj instanceof WearablesSlotType)
&& ((WearablesSlotType)obj).fullName.equals(this.fullName);
}
@Override
public int hashCode()
{ return this.fullName.hashCode(); }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment