diff --git a/addon.gradle b/addon.gradle index ee3a49ec..6fd0f883 100644 --- a/addon.gradle +++ b/addon.gradle @@ -6,6 +6,7 @@ javadoc { include "com/gtnewhorizon/structurelib/structure/**" include "com/gtnewhorizon/structurelib/util/**" include "com/gtnewhorizon/structurelib/StructureLibAPI.java" + include "com/gtnewhorizon/structurelib/StructureEvent.java" options { overview "src/main/javadoc/overview.html" diff --git a/src/main/java/com/gtnewhorizon/structurelib/StructureEvent.java b/src/main/java/com/gtnewhorizon/structurelib/StructureEvent.java new file mode 100644 index 00000000..03f9e797 --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/StructureEvent.java @@ -0,0 +1,147 @@ +package com.gtnewhorizon.structurelib; + +import com.gtnewhorizon.structurelib.structure.IStructureElement; +import cpw.mods.fml.common.eventhandler.Event; +import net.minecraft.world.World; +import net.minecraftforge.common.MinecraftForge; + +/** + * Subscribers are required to not keep reference to these events, nor to depend on values of these events after + * the method call returns. + *

+ * All of these events will be fired on the {@link net.minecraftforge.common.MinecraftForge#EVENT_BUS}, if they are + * enabled by {@link StructureLibAPI#enableInstrument(Object)}. + */ +public abstract class StructureEvent extends Event { + private final World world; + private final int x, y, z; + /* + * In case the event is passed to another thread for processing. + */ + private final Object identifier = StructureLibAPI.instrument.get(); + + StructureEvent(World world, int x, int y, int z) { + this.world = world; + this.x = x; + this.y = y; + this.z = z; + } + + /** + * Location of this event. + */ + public World getWorld() { + return world; + } + + /** + * Location of this event. Absolute world coordinate. + */ + public int getX() { + return x; + } + + /** + * Location of this event. Absolute world coordinate. + */ + public int getY() { + return y; + } + + /** + * Location of this event. Absolute world coordinate. + */ + public int getZ() { + return z; + } + + /** + * The instrument identifier. Subscribers should filter by these identifiers to prevent listening to a structure + * event for other people. + */ + public Object getInstrumentIdentifier() { + return identifier; + } + + /** + * Fired just before any structure element is visited. + */ + public static final class StructureElementVisitedEvent extends StructureEvent { + private final int a, b, c; + private final IStructureElement element; + + /** + * Fire this event on {@link MinecraftForge#EVENT_BUS} by populating it with given arguments. + * Do nothing if instrumenting isn't enabled for current thread. + *

+ * Any {@link Exception} thrown by event listeners will be logged and ignored. {@link Error} will be propagated + * as is. + */ + public static void fireEvent( + World world, int x, int y, int z, int a, int b, int c, IStructureElement element) { + if (StructureLibAPI.isInstrumentEnabled()) { + try { + MinecraftForge.EVENT_BUS.post(new StructureElementVisitedEvent(world, x, y, z, a, b, c, element)); + } catch (Exception e) { + // logging is done by event bus, we just ignore the consequence here + } + } + } + + StructureElementVisitedEvent( + World world, int x, int y, int z, int a, int b, int c, IStructureElement element) { + super(world, x, y, z); + this.a = a; + this.b = b; + this.c = c; + this.element = element; + } + + /** + * A position in ABC coordinate, relative to (0,0,0) in the piece, i.e. without baseOffset + *

+ * Submit a feature request if you need the baseOffset. + */ + public int getA() { + return a; + } + + /** + * B position in ABC coordinate, relative to (0,0,0) in the piece, i.e. without baseOffset + *

+ * Submit a feature request if you need the baseOffset. + */ + public int getB() { + return b; + } + + /** + * C position in ABC coordinate, relative to (0,0,0) in the piece, i.e. without baseOffset + *

+ * Submit a feature request if you need the baseOffset. + */ + public int getC() { + return c; + } + + /** + * Element being visited. + */ + public IStructureElement getElement() { + return element; + } + + @Override + public String toString() { + return "StructureElementVisitedEvent{" + "a=" + + a + ", b=" + + b + ", c=" + + c + ", element=" + + element + ", world=" + + getWorld() + ", x=" + + getX() + ", y=" + + getY() + ", z=" + + getZ() + '}'; + } + } +} diff --git a/src/main/java/com/gtnewhorizon/structurelib/StructureLibAPI.java b/src/main/java/com/gtnewhorizon/structurelib/StructureLibAPI.java index 0164289e..0728af5b 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/StructureLibAPI.java +++ b/src/main/java/com/gtnewhorizon/structurelib/StructureLibAPI.java @@ -7,6 +7,7 @@ import com.gtnewhorizon.structurelib.alignment.IAlignmentProvider; import com.gtnewhorizon.structurelib.alignment.enumerable.ExtendedFacing; import com.gtnewhorizon.structurelib.net.AlignmentMessage; import com.gtnewhorizon.structurelib.structure.AutoPlaceEnvironment; +import com.gtnewhorizon.structurelib.structure.IStructureDefinition; import cpw.mods.fml.common.network.NetworkRegistry; import net.minecraft.block.Block; import net.minecraft.entity.player.EntityPlayer; @@ -38,6 +39,54 @@ public class StructureLibAPI { public static final int HINT_BLOCK_META_AIR = 13; public static final int HINT_BLOCK_META_NOT_AIR = 14; public static final int HINT_BLOCK_META_ERROR = 15; + static final ThreadLocal instrument = new ThreadLocal<>(); + + /** + * Start instrumenting for the current thread only. When instrumentation is enabled, + * {@link IStructureDefinition}'s builtin iteration methods will send {@link StructureEvent} and its subclasses when + * appropriate. Otherwise, these events will not be sent at all, regardless of whether a listener for these events + * are registered. + *

+ * Identifiers are required to be value-comparable, i.e. overrides {@link Object#equals(Object)}. + * Suggested identifier includes + *

+ *

+ * It's an API abuse for normal {@link IStructureDefinition} users to depend on this feature. They have full control + * and full knowledge of the {@link IStructureDefinition} they are using. Using events to instrument the structure + * check/autoplace is completely unnecessary and does nothing but add overhead. + *

+ * Instruments are required to call {@link #disableInstrument()} whenever it is done with its business, as there can + * be only one instrument for any given thread. + * + * @param identifier an identifier sent along the actual event. + * @throws IllegalStateException if instrument has already been enabled. + */ + public static void enableInstrument(Object identifier) { + if (isInstrumentEnabled()) throw new IllegalStateException(); + instrument.set(instrument); + } + + /** + * Stop instrumenting. + * + * @throws IllegalStateException if instrumenting hasn't been enabled. + */ + public static void disableInstrument() { + if (!isInstrumentEnabled()) throw new IllegalStateException(); + instrument.set(null); + } + + /** + * Check if instrumenting is enabled. + * @return true if enabled, false otherwise + */ + public static boolean isInstrumentEnabled() { + return instrument.get() != null; + } /** * Start a batch of hinting. All hints particles generated during one batch will be considered to belong to one hologram. @@ -59,12 +108,13 @@ public class StructureLibAPI { /** * Generate a new hint particle on client side at given location using given textures with a tint. - * @param w World to spawn. Usually the client world. - * @param x x coord - * @param y y coord - * @param z z coord + * + * @param w World to spawn. Usually the client world. + * @param x x coord + * @param y y coord + * @param z z coord * @param icons 6 texture. in forge direction order. - * @param RGBa a 4 short array. tint in rgba form. currently alpha channel is ignored, but we might change this later on. + * @param RGBa a 4 short array. tint in rgba form. currently alpha channel is ignored, but we might change this later on. */ public static void hintParticleTinted(World w, int x, int y, int z, IIcon[] icons, short[] RGBa) { proxy.hintParticleTinted(w, x, y, z, icons, RGBa); @@ -72,13 +122,14 @@ public class StructureLibAPI { /** * Generate a new hint particle on client side at given location using textures from given block with a tint. - * @param w World to spawn. Usually the client world. - * @param x x coord - * @param y y coord - * @param z z coord + * + * @param w World to spawn. Usually the client world. + * @param x x coord + * @param y y coord + * @param z z coord * @param block block to take texture from - * @param meta the meta of block to take texture from - * @param RGBa a 4 short array. tint in rgba form. currently alpha channel is ignored, but we might change this later on. + * @param meta the meta of block to take texture from + * @param RGBa a 4 short array. tint in rgba form. currently alpha channel is ignored, but we might change this later on. */ public static void hintParticleTinted(World w, int x, int y, int z, Block block, int meta, short[] RGBa) { proxy.hintParticleTinted(w, x, y, z, block, meta, RGBa); @@ -86,10 +137,11 @@ public class StructureLibAPI { /** * Generate a new hint particle on client side at given location using given textures. - * @param w World to spawn. Usually the client world. - * @param x x coord - * @param y y coord - * @param z z coord + * + * @param w World to spawn. Usually the client world. + * @param x x coord + * @param y y coord + * @param z z coord * @param icons 6 texture. in forge direction order. */ public static void hintParticle(World w, int x, int y, int z, IIcon[] icons) { @@ -98,12 +150,13 @@ public class StructureLibAPI { /** * Generate a new hint particle on client side at given location using textures from given block. - * @param w World to spawn. Usually the client world. - * @param x x coord - * @param y y coord - * @param z z coord + * + * @param w World to spawn. Usually the client world. + * @param x x coord + * @param y y coord + * @param z z coord * @param block block to take texture from - * @param meta the meta of block to take texture from + * @param meta the meta of block to take texture from */ public static void hintParticle(World w, int x, int y, int z, Block block, int meta) { proxy.hintParticle(w, x, y, z, block, meta); @@ -111,7 +164,7 @@ public class StructureLibAPI { /** * Update the tint of given hint particle. Do nothing if particle not found. - * + *

* Can be called on either client side or server side. * Server side will schedule a network message to update it for this player only. * Will do nothing if no hint particle is found at given location. @@ -240,10 +293,11 @@ public class StructureLibAPI { /** * Send chat to player, but throttled. - * @param throttleKey throttle key. Must properly implement {@link Object#hashCode()} and {@link Object#equals(Object)} - * to handle value equality. - * @param player player to send chat to - * @param text chat to send + * + * @param throttleKey throttle key. Must properly implement {@link Object#hashCode()} and {@link Object#equals(Object)} + * to handle value equality. + * @param player player to send chat to + * @param text chat to send * @param intervalRequired interval required before last recorded time to actually send the message. unit in millisecond. * we purposefully chose to not use a bigger data type to limit how long this interval can be */ @@ -254,12 +308,13 @@ public class StructureLibAPI { /** * Send chat to player, but throttled. - * @param throttleKey throttle key. Must properly implement {@link Object#hashCode()} and {@link Object#equals(Object)} - * to handle value equality. - * @param player player to send chat to - * @param text chat to send - * @param intervalRequired interval required before last recorded time to actually send the message. unit in millisecond. - * we purposefully chose to not use a bigger data type to limit how long this interval can be + * + * @param throttleKey throttle key. Must properly implement {@link Object#hashCode()} and {@link Object#equals(Object)} + * to handle value equality. + * @param player player to send chat to + * @param text chat to send + * @param intervalRequired interval required before last recorded time to actually send the message. unit in millisecond. + * we purposefully chose to not use a bigger data type to limit how long this interval can be * @param forceUpdateLastSend if true, always update the last send time, even if the message isn't actually sent. */ public static void addThrottledChat( diff --git a/src/main/java/com/gtnewhorizon/structurelib/structure/IStructureElement.java b/src/main/java/com/gtnewhorizon/structurelib/structure/IStructureElement.java index 06f34c76..553ab3ee 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/structure/IStructureElement.java +++ b/src/main/java/com/gtnewhorizon/structurelib/structure/IStructureElement.java @@ -17,6 +17,7 @@ import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.IChatComponent; import net.minecraft.world.World; @@ -91,10 +92,28 @@ public interface IStructureElement { } /** - * Try place the block by taking resource from given IItemSource. + * Try place the block by taking resource from given {@link IItemSource}. *

* You might want to use {@link StructureUtility#survivalPlaceBlock(Block, int, World, int, int, int, IItemSource, net.minecraft.entity.player.EntityPlayer)} * or its overloads to implement this. + *

+ * The default implementation provided will + *

    + *
  1. Get {@link BlocksToPlace} via {@link #getBlocksToPlace(Object, World, int, int, int, ItemStack, AutoPlaceEnvironment)}. If not null + *
      + *
    1. call {@link #check(Object, World, int, int, int)}. If this returns {@code true}, {@link PlaceResult#SKIP} will be returned without further action.
    2. + *
    3. Use the {@link BlocksToPlace} retrieved earlier and passed in {@link IItemSource} to determine an item to place
    4. + *
    5. Hand over control to {@link StructureUtility#survivalPlaceBlock(ItemStack, NBTMode, NBTTagCompound, boolean, World, int, int, int, IItemSource, EntityPlayer, Consumer)}
    6. + *
    + *
  2. + *
  3. Call legacy {@link #survivalPlaceBlock(Object, World, int, int, int, ItemStack, IItemSource, EntityPlayerMP, Consumer)} if {@code env.getActor()} contains an {@link EntityPlayerMP}
  4. + *
  5. Emit warning under debug mode (or throw exception under panic mode), then return {@link PlaceResult#SKIP}
  6. + *
+ * It should be noticed that the default implementation is likely to cause unexpected issues if the + * {@link #check(Object, World, int, int, int)} is not side effect free, or is not idempotent. + * E.g. the {@link #check(Object, World, int, int, int)} will add current coord to a {@link java.util.List}. As + * this method might be invoked at a location that is already accepted, this will cause the machine to have multiple + * of this coord in its list. * * @param trigger trigger item * @param env autoplacing environment @@ -259,6 +278,7 @@ public interface IStructureElement { * Get the blocks to place filter. This is usually slower as it requires walking through inventories one by one. *

* Suitable for use with {@link IItemSource#takeOne(Predicate, boolean)} + * * @return a predicate. never null. */ @Nonnull diff --git a/src/main/java/com/gtnewhorizon/structurelib/structure/StructureUtility.java b/src/main/java/com/gtnewhorizon/structurelib/structure/StructureUtility.java index 84cef049..b27769c6 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/structure/StructureUtility.java +++ b/src/main/java/com/gtnewhorizon/structurelib/structure/StructureUtility.java @@ -3,6 +3,7 @@ package com.gtnewhorizon.structurelib.structure; import static java.lang.Integer.MIN_VALUE; import com.google.common.collect.ImmutableList; +import com.gtnewhorizon.structurelib.StructureEvent.StructureElementVisitedEvent; import com.gtnewhorizon.structurelib.StructureLib; import com.gtnewhorizon.structurelib.StructureLibAPI; import com.gtnewhorizon.structurelib.alignment.constructable.ChannelDataAccessor; @@ -3030,6 +3031,17 @@ public class StructureUtility { Arrays.toString(abc)); if (world.blockExists(xyz[0], xyz[1], xyz[2])) { + if (StructureLibAPI.isInstrumentEnabled()) { + StructureElementVisitedEvent.fireEvent( + world, + xyz[0], + xyz[1], + xyz[2], + abc[0] - basePositionA, + abc[1] - basePositionB, + abc[2] - basePositionC, + element); + } if (!predicate.visit(element, world, xyz[0], xyz[1], xyz[2], abc[0], abc[1], abc[2])) { if (StructureLibAPI.isDebugEnabled()) { StructureLib.LOGGER.info( diff --git a/src/main/javadoc/overview.html b/src/main/javadoc/overview.html index 3c6a9589..fe8b3f65 100644 --- a/src/main/javadoc/overview.html +++ b/src/main/javadoc/overview.html @@ -97,6 +97,18 @@ StructureLib API documentation. breaking client code.

+

{@link com.gtnewhorizon.structurelib.StructureEvent}

+

+ This class is the base class of all events that will be posted while a IStructureDefinition are visited. + These events are disabled by default, and can be enabled by calling {@link com.gtnewhorizon.structurelib.StructureLibAPI#enableInstrument(Object)}. + To receive these events, register an event handler to {@link net.minecraftforge.common.MinecraftForge#EVENT_BUS}. + Handlers should filter the events received by {@link com.gtnewhorizon.structurelib.StructureEvent#getInstrumentIdentifier()}. +

+

+ These events are intended for third party tools to inspect structure details. Normal users should not use + these at all. +

+

Important Concepts

Context Object

@@ -231,5 +243,13 @@ public class MyHatchElement<T> implements IStructureElement<T> { The code for placeBlock() and spawnHint() will be left as an exercise for the reader. +

Instrumenting StructureLib

+

+ StructureLib support third party tools to poke into structure's internals via events. This is done by listening to {@link com.gtnewhorizon.structurelib.StructureEvent} + and its subclasses on the {@link net.minecraftforge.common.MinecraftForge#EVENT_BUS forge main event bus}. +

+

+ Please refer to {@link com.gtnewhorizon.structurelib.StructureEvent} to see what events are available. +