package gregtech.api.util; import static com.gtnewhorizon.structurelib.structure.StructureUtility.ofBlock; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.EnumSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.BiFunction; import java.util.function.BiPredicate; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; import net.minecraft.block.Block; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.item.ItemStack; import net.minecraft.tileentity.TileEntity; import net.minecraft.util.ChatComponentTranslation; import net.minecraft.util.IChatComponent; import net.minecraft.world.World; import net.minecraftforge.common.util.ForgeDirection; import com.gtnewhorizon.structurelib.StructureLibAPI; import com.gtnewhorizon.structurelib.structure.AutoPlaceEnvironment; import com.gtnewhorizon.structurelib.structure.IItemSource; import com.gtnewhorizon.structurelib.structure.IStructureElement; import com.gtnewhorizon.structurelib.structure.IStructureElementChain; import com.gtnewhorizon.structurelib.structure.IStructureElementNoPlacement; import com.gtnewhorizon.structurelib.structure.StructureUtility; import com.gtnewhorizon.structurelib.util.ItemStackPredicate; import gnu.trove.TIntCollection; import gnu.trove.list.array.TIntArrayList; import gnu.trove.set.hash.TIntHashSet; import gregtech.api.interfaces.IHatchElement; import gregtech.api.interfaces.metatileentity.IMetaTileEntity; import gregtech.api.interfaces.tileentity.IGregTechTileEntity; import gregtech.common.blocks.GT_Item_Machines; public class GT_HatchElementBuilder { private interface Builtin { } private IGT_HatchAdder mAdder; private int mCasingIndex = -1; private int mDot = -1; private BiPredicate mShouldSkip; private BiFunction> mHatchItemFilter; private Supplier mHatchItemType; private Predicate mReject; private boolean mCacheHint; private boolean mNoStop; private EnumSet mDisallowedDirection = EnumSet.noneOf(ForgeDirection.class); private GT_HatchElementBuilder() {} public static GT_HatchElementBuilder builder() { return new GT_HatchElementBuilder<>(); } // region composite /** * Set all of adder, hint and hatchItemFilter. Provide a reasonable default for shouldSkip. TODO add doc */ @SafeVarargs public final GT_HatchElementBuilder anyOf(IHatchElement... elements) { if (elements == null || elements.length == 0) throw new IllegalArgumentException(); return adder( Arrays.stream(elements) .map( e -> e.adder() .rebrand()) .reduce(IGT_HatchAdder::orElse) .get()).hatchClasses( Arrays.stream(elements) .map(IHatchElement::mteClasses) .flatMap(Collection::stream) .collect(Collectors.toList())) .cacheHint( () -> Arrays.stream(elements) .map(IHatchElement::name) .sorted() .collect(Collectors.joining(" or ", "of type ", ""))); } /** * Set all of adder, hint and hatchItemFilter. Provide a reasonable default for shouldSkip. *

* Will rotate through all elements TODO add doc */ @SafeVarargs public final GT_HatchElementBuilder atLeast(IHatchElement... elements) { if (elements == null || elements.length == 0) throw new IllegalArgumentException(); return atLeast( Arrays.stream(elements) .collect(Collectors.groupingBy(Function.identity(), LinkedHashMap::new, Collectors.counting()))); } /** * Set all of adder, hint and hatchItemFilter. Provide a reasonable default for shouldSkip. *

* Will rotate through all elements TODO add doc */ public final GT_HatchElementBuilder atLeastList(List> elements) { if (elements == null || elements.isEmpty()) throw new IllegalArgumentException(); return atLeast( elements.stream() .collect(Collectors.groupingBy(Function.identity(), LinkedHashMap::new, Collectors.counting()))); } /** * Set all of adder, hint and hatchItemFilter. Provide a reasonable default for shouldSkip. TODO add doc */ public final GT_HatchElementBuilder atLeast(Map, ? extends Number> elements) { if (elements == null || elements.isEmpty() || elements.containsKey(null) || elements.containsValue(null)) throw new IllegalArgumentException(); List> list = elements.keySet() .stream() .map(IHatchElement::mteClasses) .flatMap(Collection::stream) .collect(Collectors.toList()); // map cannot be null or empty, so assert Optional isPresent return adder( elements.keySet() .stream() .map( e -> e.adder() .rebrand()) .reduce(IGT_HatchAdder::orElse) .orElseThrow(AssertionError::new)) .hatchItemFilter( obj -> GT_StructureUtility.filterByMTEClass( elements.entrySet() .stream() .filter( entry -> entry.getKey() .count(obj) < entry.getValue() .longValue()) .flatMap( entry -> entry.getKey() .mteClasses() .stream()) .collect(Collectors.toList()))) .shouldReject( obj -> elements.entrySet() .stream() .allMatch( e -> e.getKey() .count(obj) >= e.getValue() .longValue())) .shouldSkip( (BiPredicate & Builtin) (c, t) -> t != null && list.stream() .anyMatch(clazz -> clazz.isInstance(t.getMetaTileEntity()))) .cacheHint( () -> elements.keySet() .stream() .map(IHatchElement::name) .sorted() .collect(Collectors.joining(" or ", "of type ", ""))); } // endregion // region primitives public GT_HatchElementBuilder adder(IGT_HatchAdder aAdder) { if (aAdder == null) throw new IllegalArgumentException(); mAdder = aAdder; return this; } public GT_HatchElementBuilder casingIndex(int aCasingIndex) { if (aCasingIndex <= 0) throw new IllegalArgumentException(); mCasingIndex = aCasingIndex; return this; } public GT_HatchElementBuilder dot(int aDot) { if (aDot <= 0) throw new IllegalArgumentException(); mDot = aDot; return this; } public GT_HatchElementBuilder shouldSkip(BiPredicate aShouldSkip) { if (!(aShouldSkip instanceof Builtin) || mShouldSkip != null) { if (!(mShouldSkip instanceof Builtin) && mShouldSkip != null) throw new IllegalStateException(); if (aShouldSkip == null) throw new IllegalArgumentException(); } mShouldSkip = aShouldSkip; return this; } public GT_HatchElementBuilder shouldReject(Predicate aShouldReject) { if (aShouldReject == null) throw new IllegalArgumentException(); mReject = aShouldReject; return this; } public GT_HatchElementBuilder hatchItemFilter( Function> aHatchItemFilter) { if (aHatchItemFilter == null) throw new IllegalArgumentException(); mHatchItemFilter = (t, s) -> aHatchItemFilter.apply(t); return this; } public GT_HatchElementBuilder hatchItemFilterAnd( Function> aHatchItemFilter) { if (aHatchItemFilter == null) throw new IllegalArgumentException(); BiFunction> tOldFilter = mHatchItemFilter; mHatchItemFilter = (t, s) -> tOldFilter.apply(t, s) .and(aHatchItemFilter.apply(t)); return this; } public GT_HatchElementBuilder hatchItemFilter( BiFunction> aHatchItemFilter) { if (aHatchItemFilter == null) throw new IllegalArgumentException(); mHatchItemFilter = aHatchItemFilter; return this; } public GT_HatchElementBuilder hatchItemFilterAnd( BiFunction> aHatchItemFilter) { if (aHatchItemFilter == null) throw new IllegalArgumentException(); BiFunction> tOldFilter = mHatchItemFilter; mHatchItemFilter = (t, s) -> tOldFilter.apply(t, s) .and(aHatchItemFilter.apply(t, s)); return this; } // region hint public GT_HatchElementBuilder hint(Supplier aSupplier) { if (aSupplier == null) throw new IllegalArgumentException(); mHatchItemType = aSupplier; mCacheHint = false; return this; } public GT_HatchElementBuilder cacheHint(Supplier aSupplier) { if (aSupplier == null) throw new IllegalArgumentException(); mHatchItemType = aSupplier; mCacheHint = true; return this; } public GT_HatchElementBuilder cacheHint() { if (mHatchItemType == null) throw new IllegalStateException(); mCacheHint = true; return this; } // endregion public GT_HatchElementBuilder continueIfSuccess() { mNoStop = true; return this; } public GT_HatchElementBuilder stopIfSuccess() { mNoStop = false; return this; } /** * Help automatic hatch side determination code by ruling out some directions. Note the automatic hatch side * determination code will choose to use the default facing if the final allowed facing set is empty. *

* This will clear the sides set by previous call to this or {@link #allowOnly(ForgeDirection...)} *

* Usually mandatory for multis with multiple slices, and otherwise not needed if it contains a single slice only. * * @param facings disallowed direction in ABC coordinate system */ public GT_HatchElementBuilder disallowOnly(ForgeDirection... facings) { if (facings == null) throw new IllegalArgumentException(); mDisallowedDirection = EnumSet.copyOf(Arrays.asList(facings)); return this; } /** * Help automatic hatch side determination code by allowing only some directions. Note the automatic hatch side * determination code will choose to use the default facing if the final allowed facing set is empty. *

* This will clear the sides set by previous call to this or {@link #disallowOnly(ForgeDirection...)} *

* Usually mandatory for multis with multiple slices, and otherwise not needed if it contains a single slice only. * * @param facings allowed direction in ABC coordinate system */ public GT_HatchElementBuilder allowOnly(ForgeDirection... facings) { if (facings == null) throw new IllegalArgumentException(); mDisallowedDirection = EnumSet.complementOf(EnumSet.copyOf(Arrays.asList(facings))); mDisallowedDirection.remove(ForgeDirection.UNKNOWN); return this; } // endregion // region intermediate public GT_HatchElementBuilder hatchClass(Class clazz) { return hatchItemFilter(c -> is -> clazz.isInstance(GT_Item_Machines.getMetaTileEntity(is))) .cacheHint(() -> "of class " + clazz.getSimpleName()) .shouldSkip( (BiPredicate & Builtin) (c, t) -> clazz .isInstance(t.getMetaTileEntity())); } @SafeVarargs public final GT_HatchElementBuilder hatchClasses(Class... classes) { return hatchClasses(Arrays.asList(classes)); } public final GT_HatchElementBuilder hatchClasses(List> classes) { List> list = new ArrayList<>(classes); return hatchItemFilter(obj -> GT_StructureUtility.filterByMTEClass(list)).cacheHint( () -> list.stream() .map(Class::getSimpleName) .sorted() .collect(Collectors.joining(" or ", "of class ", ""))) .shouldSkip( (BiPredicate & Builtin) (c, t) -> t != null && list.stream() .anyMatch(clazz -> clazz.isInstance(t.getMetaTileEntity()))); } public GT_HatchElementBuilder hatchId(int aId) { return hatchItemFilter( c -> is -> GT_Utility.isStackValid(is) && is.getItem() instanceof GT_Item_Machines && is.getItemDamage() == aId).cacheHint(() -> "of id " + aId) .shouldSkip( (BiPredicate & Builtin) (c, t) -> t != null && t.getMetaTileID() == aId); } public GT_HatchElementBuilder hatchIds(int... aIds) { if (aIds == null || aIds.length == 0) throw new IllegalArgumentException(); if (aIds.length == 1) return hatchId(aIds[0]); TIntCollection coll = aIds.length < 16 ? new TIntArrayList(aIds) : new TIntHashSet(aIds); return hatchItemFilter( c -> is -> GT_Utility.isStackValid(is) && is.getItem() instanceof GT_Item_Machines && coll.contains(is.getItemDamage())).cacheHint( () -> Arrays.stream(coll.toArray()) .sorted() .mapToObj(String::valueOf) .collect(Collectors.joining(" or ", "of id ", ""))) .shouldSkip( (BiPredicate & Builtin) (c, t) -> t != null && coll.contains(t.getMetaTileID())); } // endregion @SuppressWarnings("unchecked") @SafeVarargs public final IStructureElementChain buildAndChain(IStructureElement... elements) { List> l = new ArrayList<>(); l.add(build()); l.addAll(Arrays.asList(elements)); IStructureElement[] array = l.toArray(new IStructureElement[0]); return () -> array; } public final IStructureElementChain buildAndChain(Block block, int meta) { return buildAndChain(ofBlock(block, meta)); } public IStructureElement build() { if (mAdder == null || mCasingIndex == -1 || mDot == -1) { throw new IllegalArgumentException(); } if (mHatchItemFilter == null) { // no item filter -> no placement return new IStructureElementNoPlacement<>() { @Override public boolean check(T t, World world, int x, int y, int z) { TileEntity tileEntity = world.getTileEntity(x, y, z); return tileEntity instanceof IGregTechTileEntity && mAdder.apply(t, (IGregTechTileEntity) tileEntity, (short) mCasingIndex); } @Override public boolean spawnHint(T t, World world, int x, int y, int z, ItemStack trigger) { StructureLibAPI.hintParticle(world, x, y, z, StructureLibAPI.getBlockHint(), mDot - 1); return true; } }; } return new IStructureElement<>() { private String mHint = mHatchItemType == null ? "unspecified GT hatch" : mHatchItemType.get(); @Override public boolean check(T t, World world, int x, int y, int z) { TileEntity tileEntity = world.getTileEntity(x, y, z); return tileEntity instanceof IGregTechTileEntity && mAdder.apply(t, (IGregTechTileEntity) tileEntity, (short) mCasingIndex); } @Override public boolean spawnHint(T t, World world, int x, int y, int z, ItemStack trigger) { StructureLibAPI.hintParticle(world, x, y, z, StructureLibAPI.getBlockHint(), mDot - 1); return true; } @Override public boolean placeBlock(T t, World world, int i, int i1, int i2, ItemStack itemStack) { // TODO return false; } private String getHint() { if (mHint != null) return mHint; String tHint = mHatchItemType.get(); if (tHint == null) return "?"; // TODO move this to some .lang instead of half ass it into the crappy gt lang file tHint = GT_LanguageManager.addStringLocalization("Hatch_Type_" + tHint.replace(' ', '_'), tHint); if (mCacheHint) { mHint = tHint; if (mHint != null) // yeet the getter, since its product is retrieved and cached mHatchItemType = null; } return tHint; } @Override public BlocksToPlace getBlocksToPlace(T t, World world, int x, int y, int z, ItemStack trigger, AutoPlaceEnvironment env) { return BlocksToPlace.create(mHatchItemFilter.apply(t, trigger)); } @Deprecated @Override public PlaceResult survivalPlaceBlock(T t, World world, int x, int y, int z, ItemStack trigger, IItemSource s, EntityPlayerMP actor, Consumer chatter) { return survivalPlaceBlock( t, world, x, y, z, trigger, AutoPlaceEnvironment.fromLegacy(s, actor, chatter)); } @Override public PlaceResult survivalPlaceBlock(T t, World world, int x, int y, int z, ItemStack trigger, AutoPlaceEnvironment env) { if (mShouldSkip != null) { TileEntity tileEntity = world.getTileEntity(x, y, z); if (tileEntity instanceof IGregTechTileEntity && mShouldSkip.test(t, (IGregTechTileEntity) tileEntity)) return PlaceResult.SKIP; } if (!StructureLibAPI.isBlockTriviallyReplaceable(world, x, y, z, env.getActor())) return PlaceResult.REJECT; if (mReject != null && mReject.test(t)) return PlaceResult.REJECT; ItemStack taken = env.getSource() .takeOne(mHatchItemFilter.apply(t, trigger), true); if (GT_Utility.isStackInvalid(taken)) { String type = getHint(); env.getChatter() .accept(new ChatComponentTranslation("GT5U.autoplace.error.no_hatch", type)); return PlaceResult.REJECT; } if (StructureUtility.survivalPlaceBlock( taken, ItemStackPredicate.NBTMode.IGNORE, null, true, world, x, y, z, env.getSource(), env.getActor()) != PlaceResult.ACCEPT) { return PlaceResult.REJECT; } // try to infer facing EnumSet allowed = EnumSet.noneOf(ForgeDirection.class); // first find which face of block is not contained in structure if (env.getAPILevel() == AutoPlaceEnvironment.APILevel.Legacy) { // a legacy decorator isn't passing down necessary information // in that case, we just assume all facing is allowed allowed.addAll(Arrays.asList(ForgeDirection.VALID_DIRECTIONS)); } else { for (ForgeDirection direction : ForgeDirection.VALID_DIRECTIONS) { // as noted on getWorldDirection Y axis should be flipped before use if (env.isContainedInPiece(direction.offsetX, -direction.offsetY, direction.offsetZ)) continue; // explicitly rejected, probably obstructed by another slice if (mDisallowedDirection.contains(direction)) continue; ForgeDirection rotated = env.getFacing() .getWorldDirection( (direction.flag & (ForgeDirection.UP.flag | ForgeDirection.DOWN.flag)) != 0 ? direction.getOpposite() : direction); allowed.add(rotated); } } if (!allowed.isEmpty()) { TileEntity tileEntity = world.getTileEntity(x, y, z); if (tileEntity instanceof IGregTechTileEntity) { ForgeDirection result = null; // find the first facing available, but prefer a facing that isn't up/down for (ForgeDirection facing : allowed) { result = facing; if ((facing.flag & (ForgeDirection.UP.flag | ForgeDirection.DOWN.flag)) == 0) break; // Horizontal } assert result != null; ((IGregTechTileEntity) tileEntity).setFrontFacing(result); } } return mNoStop ? PlaceResult.ACCEPT : PlaceResult.ACCEPT_STOP; } }; } }