mirror of
https://github.com/GTNewHorizons/StructureLib.git
synced 2026-11-11 07:47:46 +00:00
ofChain survival placement had a bug where SKIP (returned when a block already exists) would not stop the chain, causing fallback alternatives to replace already valid blocks , for example: <img width="436" height="477" alt="image" src="https://github.com/user-attachments/assets/7ff3d6fa-de98-4716-823f-3ad2abab7394" /> SKIP now short-circuits the chain, remaining alternatives are skipped because the position is already valid. To keep correct behavior for elements without autobuild (which previously returned SKIP as a "no implementation" signal), a new REJECT_CONTINUE value is added, which: continues silently like SKIP (no error, not counted) in the walker and in the chain tries the next alternative like REJECT Default survivalPlaceBlock and IStructureElementCheckOnly now return REJECT_CONTINUE instead of SKIP for unhandled paths. Tested this with the example above, and fixed the issue: <img width="435" height="498" alt="image" src="https://github.com/user-attachments/assets/d92bce4d-b5ad-4051-af79-ffd8c819f9f7" /> survival placement also still works: <img width="1234" height="916" alt="image" src="https://github.com/user-attachments/assets/632d5556-524d-4dc5-a7f5-75c859cb6b70" /> this should ideally merged before https://github.com/GTNewHorizons/GT5-Unofficial/pull/6922 , to fix this underlying bug, but if possible not be tagged without the other PR, which would cause all NEI previews of multis with water to not show any water anymore --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
382 lines
16 KiB
Java
382 lines
16 KiB
Java
package com.gtnewhorizon.structurelib.structure;
|
|
|
|
import static com.gtnewhorizon.structurelib.StructureLib.LOGGER;
|
|
import static com.gtnewhorizon.structurelib.StructureLib.PANIC_MODE;
|
|
|
|
import java.util.Arrays;
|
|
import java.util.Collections;
|
|
import java.util.List;
|
|
import java.util.function.Consumer;
|
|
import java.util.function.Predicate;
|
|
|
|
import javax.annotation.Nonnull;
|
|
import javax.annotation.Nullable;
|
|
|
|
import net.minecraft.block.Block;
|
|
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;
|
|
|
|
import com.gtnewhorizon.structurelib.StructureLibAPI;
|
|
import com.gtnewhorizon.structurelib.util.ItemStackPredicate;
|
|
import com.gtnewhorizon.structurelib.util.ItemStackPredicate.NBTMode;
|
|
|
|
/**
|
|
* Use StructureUtility to instantiate. These are the building blocks for your {@link IStructureDefinition}. It
|
|
* represents what a particular element can be and how this particular element should be autoplaced.
|
|
*/
|
|
public interface IStructureElement<T> {
|
|
|
|
boolean check(T t, World world, int x, int y, int z);
|
|
|
|
/**
|
|
* Pure (stateless and side effect free) function to test if current block could be valid. Used to give user hints
|
|
* about incorrectly placed blocks given a controller and trigger item. If couldBeValid(...) == false for a set of
|
|
* channels, with those same tiers, check(...) == false. Not required, defaults to true which is always safe but
|
|
* won't give the user error hints.
|
|
*/
|
|
default boolean couldBeValid(T t, World world, int x, int y, int z, ItemStack trigger) {
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Returns a human-readable description of what block(s) this element accepts. Used for diagnostic messages when
|
|
* structure checks fail (e.g. "expected Input Bus at (1, 2, 3)").
|
|
* <p>
|
|
* Unlike {@link #getBlocksToPlace}, which returns the full set of concrete block variants for autoplacing (e.g.
|
|
* every tier of Input Bus), this method returns <b>category-level</b> descriptions suitable for display to the
|
|
* player (e.g. just "Input Bus"). It also requires no parameters, no world, trigger item, or environment, making it
|
|
* usable in contexts where those are unavailable.
|
|
* <p>
|
|
* Implementations should return <b>lang keys</b> (e.g. {@code "tile.blockGlass.name"}) rather than pre-translated
|
|
* display names. The caller is responsible for translating these keys on the client side via
|
|
* {@code StatCollector.translateToLocal()}. If a key has no matching translation, {@code translateToLocal} returns
|
|
* the input string unchanged, so pre-translated fallback strings also work.
|
|
*
|
|
* @param context the context object (e.g. the multiblock controller), used to determine state-dependent
|
|
* descriptions
|
|
* @return a list of accepted block descriptions as lang keys (or display names as fallback), or null if no
|
|
* description is available
|
|
*/
|
|
@Nullable
|
|
default List<String> getDescription(T context) {
|
|
return null;
|
|
}
|
|
|
|
boolean spawnHint(T t, World world, int x, int y, int z, ItemStack trigger);
|
|
|
|
boolean placeBlock(T t, World world, int x, int y, int z, ItemStack trigger);
|
|
|
|
/**
|
|
* Try place the block by taking resource from given IItemSource.
|
|
* <p>
|
|
* 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.
|
|
*
|
|
* @param trigger trigger item
|
|
* @param s drain resources from this place
|
|
* @param actor source of action. for very critical errors you can also just send the error messages here,
|
|
* bypassing any filter that chatter might have.
|
|
* @param chatter send error messages here. Caller will choose an appropriate way to forward it to player if the
|
|
* other fallbacks also fails.
|
|
* @deprecated caller should call the non deprecated overload. implementor for reusable {@link IStructureElement}
|
|
* should still implement this. implementor for private implementations can freely ignore this as long
|
|
* as you know the caller will not call this overload.
|
|
*/
|
|
@Deprecated
|
|
default PlaceResult survivalPlaceBlock(T t, World world, int x, int y, int z, ItemStack trigger, IItemSource s,
|
|
EntityPlayerMP actor, Consumer<IChatComponent> chatter) {
|
|
if (PANIC_MODE) throw new RuntimeException("Panic Tripwire hit");
|
|
if (StructureLibAPI.isDebugEnabled()) LOGGER.error(
|
|
"Default implementation of survivalPlaceBlock hit! Things aren't going to work well! IStructureElement class: {}",
|
|
getClass().getName());
|
|
if (!StructureLibAPI.isBlockTriviallyReplaceable(world, x, y, z, actor)) return PlaceResult.REJECT;
|
|
return PlaceResult.REJECT_CONTINUE;
|
|
}
|
|
|
|
/**
|
|
* A simplified version of survivalPlaceBlock. Return null if not implemented.
|
|
* <p>
|
|
* <b>BY OVERRIDING THIS TO NULL, YOU ACKNOWLEDGE THAT THE {@link #check(Object, World, int, int, int)} OF THIS
|
|
* CLASS IS SIDE EFFECT FREE AND CAN HAVE ITS CHECK CALLED WITHOUT BREAKING ANYTHING. OTHERWISE OVERRIDE THE
|
|
* {@link #survivalPlaceBlock(Object, World, int, int, int, ItemStack, AutoPlaceEnvironment)} TO PROVIDE A
|
|
* SIDEEFFECT FREE {@link PlaceResult#SKIP}.</b>
|
|
* <p>
|
|
* It should be noticed that this filter is only advisory and the actual
|
|
* {@link #survivalPlaceBlock(Object, World, int, int, int, ItemStack, AutoPlaceEnvironment)} or
|
|
* {@link #check(Object, World, int, int, int)} are free to reject/accept/place blocks not contained in this list,
|
|
* e.g. when the element can be placed anywhere in the structure, but no more than 3 overall; when the structure
|
|
* element cannot know all blocks it will accept at the time of calling; when this is backed by a legacy
|
|
* {@link IStructureElement} that just didn't implement this API.
|
|
*
|
|
* @param trigger trigger item
|
|
* @param env autoplacing environment
|
|
* @return null if not implemented, otherwise an instance describing what kind of blocks will be placed by
|
|
* {@link #survivalPlaceBlock(Object, World, int, int, int, ItemStack, AutoPlaceEnvironment)}
|
|
*/
|
|
@Nullable
|
|
default BlocksToPlace getBlocksToPlace(T t, World world, int x, int y, int z, ItemStack trigger,
|
|
AutoPlaceEnvironment env) {
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Try place the block by taking resource from given {@link IItemSource}.
|
|
* <p>
|
|
* 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.
|
|
* <p>
|
|
* The default implementation provided will
|
|
* <ol>
|
|
* <li>Get {@link BlocksToPlace} via
|
|
* {@link #getBlocksToPlace(Object, World, int, int, int, ItemStack, AutoPlaceEnvironment)}. If not null
|
|
* <ol>
|
|
* <li>call {@link #check(Object, World, int, int, int)}. If this returns {@code true}, {@link PlaceResult#SKIP}
|
|
* will be returned without further action.</li>
|
|
* <li>Use the {@link BlocksToPlace} retrieved earlier and passed in {@link IItemSource} to determine an item to
|
|
* place</li>
|
|
* <li>Hand over control to
|
|
* {@link StructureUtility#survivalPlaceBlock(ItemStack, NBTMode, NBTTagCompound, boolean, World, int, int, int, IItemSource, EntityPlayer, Consumer)}</li>
|
|
* </ol>
|
|
* </li>
|
|
* <li>Call legacy
|
|
* {@link #survivalPlaceBlock(Object, World, int, int, int, ItemStack, IItemSource, EntityPlayerMP, Consumer)} if
|
|
* {@code env.getActor()} contains an {@link EntityPlayerMP}</li>
|
|
* <li>Emit warning under debug mode (or throw exception under panic mode), then return
|
|
* {@link PlaceResult#SKIP}</li>
|
|
* </ol>
|
|
* 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
|
|
*/
|
|
default PlaceResult survivalPlaceBlock(T t, World world, int x, int y, int z, ItemStack trigger,
|
|
AutoPlaceEnvironment env) {
|
|
BlocksToPlace e = getBlocksToPlace(t, world, x, y, z, trigger, env);
|
|
IItemSource source = env.getSource();
|
|
EntityPlayer actor = env.getActor();
|
|
Consumer<IChatComponent> chatter = env.getChatter();
|
|
if (e != null) {
|
|
if (check(t, world, x, y, z)) return PlaceResult.SKIP;
|
|
if (e.getStacks() == null) {
|
|
ItemStack taken = source.takeOne(e.getPredicate(), true);
|
|
return StructureUtility.survivalPlaceBlock(
|
|
taken,
|
|
NBTMode.EXACT,
|
|
taken.stackTagCompound,
|
|
true,
|
|
world,
|
|
x,
|
|
y,
|
|
z,
|
|
source,
|
|
actor,
|
|
chatter);
|
|
}
|
|
for (ItemStack stack : e.getStacks()) {
|
|
if (!source.takeOne(stack, true)) continue;
|
|
return StructureUtility.survivalPlaceBlock(
|
|
stack,
|
|
NBTMode.EXACT,
|
|
stack.stackTagCompound,
|
|
true,
|
|
world,
|
|
x,
|
|
y,
|
|
z,
|
|
source,
|
|
actor,
|
|
chatter);
|
|
}
|
|
return PlaceResult.REJECT;
|
|
}
|
|
if (actor instanceof EntityPlayerMP)
|
|
return survivalPlaceBlock(t, world, x, y, z, trigger, source, (EntityPlayerMP) actor, chatter);
|
|
if (PANIC_MODE) throw new RuntimeException("Panic Tripwire hit");
|
|
if (StructureLibAPI.isDebugEnabled()) LOGGER.info(
|
|
"Fallback shim code of survivalPlaceBlock hit! Things aren't going to work well! IStructureElement class: {}",
|
|
getClass().getName());
|
|
return PlaceResult.REJECT_CONTINUE;
|
|
}
|
|
|
|
/**
|
|
* Forget the messed up class dependency graph for now. this is just so convenient.
|
|
*/
|
|
default IStructureElementNoPlacement<T> noPlacement() {
|
|
return new IStructureElementNoPlacement<T>() {
|
|
|
|
@Override
|
|
public boolean check(T t, World world, int x, int y, int z) {
|
|
return IStructureElement.this.check(t, world, x, y, z);
|
|
}
|
|
|
|
@Override
|
|
public boolean couldBeValid(T t, World world, int x, int y, int z, ItemStack trigger) {
|
|
return IStructureElement.this.couldBeValid(t, world, x, y, z, trigger);
|
|
}
|
|
|
|
@Override
|
|
public boolean spawnHint(T t, World world, int x, int y, int z, ItemStack trigger) {
|
|
return IStructureElement.this.spawnHint(t, world, x, y, z, trigger);
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
public List<String> getDescription(T context) {
|
|
return IStructureElement.this.getDescription(context);
|
|
}
|
|
};
|
|
}
|
|
|
|
default int getStepA() {
|
|
return 1;
|
|
}
|
|
|
|
default int getStepB() {
|
|
return 0;
|
|
}
|
|
|
|
default int getStepC() {
|
|
return 0;
|
|
}
|
|
|
|
default boolean resetA() {
|
|
return false;
|
|
}
|
|
|
|
default boolean resetB() {
|
|
return false;
|
|
}
|
|
|
|
default boolean resetC() {
|
|
return false;
|
|
}
|
|
|
|
default boolean isNavigating() {
|
|
return false;
|
|
}
|
|
|
|
enum PlaceResult {
|
|
/**
|
|
* The element's check passed, the position is already valid. No placement is needed. In the walker: continue
|
|
* iteration (no error, not counted as placed). In ofChain: short-circuits, remaining alternatives are skipped.
|
|
*/
|
|
SKIP,
|
|
/**
|
|
* The element cannot handle this position (occupied, missing resources, etc.). In the walker: stop iteration
|
|
* with error particles. In ofChain: try next alternative.
|
|
*/
|
|
REJECT,
|
|
/**
|
|
* This element has no survivalPlaceBlock implementation (default fallback), or otherwise cannot place but is
|
|
* not an error condition. In the walker: continue iteration silently (like SKIP). In ofChain: try next
|
|
* alternative (like REJECT), so a better-suited element can handle it.
|
|
*/
|
|
REJECT_CONTINUE,
|
|
/**
|
|
* Autoplace cannot proceed within this tick. To proceed further would require stopping the placement and wait
|
|
* for next round.
|
|
*/
|
|
STOP,
|
|
/**
|
|
* Element placed successfully.
|
|
*/
|
|
ACCEPT,
|
|
/**
|
|
* Combination of ACCEPT and STOP.
|
|
* <p>
|
|
* Element placed successfully. To proceed further would require stopping the placement and wait for next round.
|
|
*/
|
|
ACCEPT_STOP;
|
|
}
|
|
|
|
final class BlocksToPlace {
|
|
|
|
public static final BlocksToPlace errored = createEmpty();
|
|
private final Predicate<ItemStack> predicate;
|
|
private final Iterable<ItemStack> stacks;
|
|
|
|
public static BlocksToPlace createEmpty() {
|
|
return new BlocksToPlace(s -> false, Collections.emptyList());
|
|
}
|
|
|
|
public static BlocksToPlace create(ItemStack... stacks) {
|
|
return create(Arrays.asList(stacks));
|
|
}
|
|
|
|
public static BlocksToPlace create(Iterable<ItemStack> stacks) {
|
|
Predicate<ItemStack> predicate = null;
|
|
|
|
for (ItemStack stack : stacks) {
|
|
ItemStackPredicate p = ItemStackPredicate.from(stack, NBTMode.EXACT);
|
|
if (predicate == null) predicate = p;
|
|
else predicate = predicate.or(p);
|
|
}
|
|
|
|
return new BlocksToPlace(predicate, stacks);
|
|
}
|
|
|
|
public static BlocksToPlace create(Block block, int meta) {
|
|
Item itemBlock = Item.getItemFromBlock(block);
|
|
if (itemBlock instanceof ISpecialItemBlock) {
|
|
meta = ((ISpecialItemBlock) itemBlock).getItemMetaFromBlockMeta(block, meta);
|
|
}
|
|
return create(itemBlock, meta);
|
|
}
|
|
|
|
public static BlocksToPlace create(Item item, int meta) {
|
|
return new BlocksToPlace(
|
|
ItemStackPredicate.from(item).setMeta(meta),
|
|
Collections.singletonList(new ItemStack(item, 1, meta)));
|
|
}
|
|
|
|
public static BlocksToPlace create(ItemStack itemStack) {
|
|
return new BlocksToPlace(ItemStackPredicate.from(itemStack), Collections.singletonList(itemStack));
|
|
}
|
|
|
|
public static BlocksToPlace create(Predicate<ItemStack> predicate) {
|
|
return new BlocksToPlace(predicate, null);
|
|
}
|
|
|
|
BlocksToPlace(Predicate<ItemStack> predicate, Iterable<ItemStack> stacks) {
|
|
this.predicate = predicate;
|
|
this.stacks = stacks;
|
|
}
|
|
|
|
/**
|
|
* Get the blocks to place filter. This is usually slower as it requires walking through inventories one by one.
|
|
* <p>
|
|
* Suitable for use with {@link IItemSource#takeOne(Predicate, boolean)}
|
|
*
|
|
* @return a predicate. never null.
|
|
*/
|
|
@Nonnull
|
|
public Predicate<ItemStack> getPredicate() {
|
|
return predicate;
|
|
}
|
|
|
|
/**
|
|
* Get the ItemStacks that this one is known to accept. Can be null if this info is not available at the time
|
|
* this instance is constructed.
|
|
* <p>
|
|
* Suitable for use with {@link IItemSource#takeOne(ItemStack, boolean)}.
|
|
* <p>
|
|
* This should be equivalent to the predicate returned from {@link #getPredicate()}
|
|
*/
|
|
@Nullable
|
|
public Iterable<ItemStack> getStacks() {
|
|
return stacks;
|
|
}
|
|
}
|
|
}
|