diff --git a/gradle.properties b/gradle.properties index 4535a69..b5ca230 100644 --- a/gradle.properties +++ b/gradle.properties @@ -53,7 +53,7 @@ mod_name=Zontreck's Library Mod # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. mod_license=GPLv3 # The mod version. See https://semver.org/ -mod_version=1201.11.021824.0959 +mod_version=1201.11.022724.1602 # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # This should match the base package used for the mod sources. # See https://maven.apache.org/guides/mini/guide-naming-conventions.html diff --git a/src/main/java/dev/zontreck/libzontreck/edlibmc/Auxiliaries.java b/src/main/java/dev/zontreck/libzontreck/edlibmc/Auxiliaries.java new file mode 100644 index 0000000..eb57f30 --- /dev/null +++ b/src/main/java/dev/zontreck/libzontreck/edlibmc/Auxiliaries.java @@ -0,0 +1,590 @@ +/* + * @file Auxiliaries.java + * @author Stefan Wilhelm (wile) + * @copyright (C) 2020 Stefan Wilhelm + * @license MIT (see https://opensource.org/licenses/MIT) + * + * General commonly used functionality. + */ +package dev.zontreck.libzontreck.edlibmc; + +import com.mojang.blaze3d.platform.InputConstants; +import net.minecraft.ChatFormatting; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Registry; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentUtils; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.shapes.BooleanOp; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.registries.ForgeRegistries; +import org.slf4j.Logger; +import org.lwjgl.glfw.GLFW; + +import javax.annotation.Nullable; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + + +public class Auxiliaries { + private static String modid; + private static Logger logger; + private static Supplier server_config_supplier = CompoundTag::new; + + public static void init(String modid, Logger logger, Supplier server_config_supplier) { + Auxiliaries.modid = modid; + Auxiliaries.logger = logger; + Auxiliaries.server_config_supplier = server_config_supplier; + } + + // ------------------------------------------------------------------------------------------------------------------- + // Mod specific exports + // ------------------------------------------------------------------------------------------------------------------- + + public static String modid() { + return modid; + } + + public static Logger logger() { + return logger; + } + + // ------------------------------------------------------------------------------------------------------------------- + // Sidedness, system/environment, tagging interfaces + // ------------------------------------------------------------------------------------------------------------------- + + public interface IExperimentalFeature { + } + + public static boolean isModLoaded(final String registry_name) { + return ModList.get().isLoaded(registry_name); + } + + public static boolean isDevelopmentMode() { + return SharedConstants.IS_RUNNING_IN_IDE; + } + + @OnlyIn(Dist.CLIENT) + public static boolean isShiftDown() { + return (InputConstants.isKeyDown(SidedProxy.mc().getWindow().getWindow(), GLFW.GLFW_KEY_LEFT_SHIFT) || + InputConstants.isKeyDown(SidedProxy.mc().getWindow().getWindow(), GLFW.GLFW_KEY_RIGHT_SHIFT)); + } + + @OnlyIn(Dist.CLIENT) + public static boolean isCtrlDown() { + return (InputConstants.isKeyDown(SidedProxy.mc().getWindow().getWindow(), GLFW.GLFW_KEY_LEFT_CONTROL) || + InputConstants.isKeyDown(SidedProxy.mc().getWindow().getWindow(), GLFW.GLFW_KEY_RIGHT_CONTROL)); + } + + // ------------------------------------------------------------------------------------------------------------------- + // Logging + // ------------------------------------------------------------------------------------------------------------------- + + public static void logInfo(final String msg) { + logger.info(msg); + } + + public static void logWarn(final String msg) { + logger.warn(msg); + } + + public static void logError(final String msg) { + logger.error(msg); + } + + public static void logDebug(final String msg) { /*logger.debug(msg);*/ } + + // ------------------------------------------------------------------------------------------------------------------- + // Localization, text formatting + // ------------------------------------------------------------------------------------------------------------------- + + /** + * Text localization wrapper, implicitly prepends `MODID` to the + * translation keys. Forces formatting argument, nullable if no special formatting shall be applied.. + */ + public static MutableComponent localizable(String modtrkey, Object... args) { + return Component.translatable((modtrkey.startsWith("block.") || (modtrkey.startsWith("item."))) ? (modtrkey) : (modid + "." + modtrkey), args); + } + + public static MutableComponent localizable(String modtrkey, @Nullable ChatFormatting color, Object... args) { + final MutableComponent tr = Component.translatable(modid + "." + modtrkey, args); + if (color != null) tr.getStyle().applyFormat(color); + return tr; + } + + public static Component localizable(String modtrkey) { + return localizable(modtrkey, new Object[]{}); + } + + public static Component localizable_block_key(String blocksubkey) { + return Component.translatable("block." + modid + "." + blocksubkey); + } + + @OnlyIn(Dist.CLIENT) + public static String localize(String translationKey, Object... args) { + Component tr = Component.translatable(translationKey, args); + tr.getStyle().applyFormat(ChatFormatting.RESET); + final String ft = tr.getString(); + if (ft.contains("${")) { + // Non-recursive, non-argument lang file entry cross referencing. + Pattern pt = Pattern.compile("\\$\\{([^}]+)\\}"); + Matcher mt = pt.matcher(ft); + StringBuffer sb = new StringBuffer(); + while (mt.find()) { + String m = mt.group(1); + if (m.contains("?")) { + String[] kv = m.split("\\?", 2); + String key = kv[0].trim(); + boolean not = key.startsWith("!"); + if (not) key = key.replaceFirst("!", ""); + m = kv[1].trim(); + if (!server_config_supplier.get().contains(key)) { + m = ""; + } else { + boolean r = server_config_supplier.get().getBoolean(key); + if (not) r = !r; + if (!r) m = ""; + } + } + mt.appendReplacement(sb, Matcher.quoteReplacement((Component.translatable(m)).getString().trim())); + } + mt.appendTail(sb); + return sb.toString(); + } else { + return ft; + } + } + + /** + * Returns true if a given key is translated for the current language. + */ + @OnlyIn(Dist.CLIENT) + public static boolean hasTranslation(String key) { + return net.minecraft.client.resources.language.I18n.exists(key); + } + + public static MutableComponent join(Collection components, String separator) { + return ComponentUtils.formatList(components, Component.literal(separator), Function.identity()); + } + + public static MutableComponent join(Component... components) { + final MutableComponent tc = Component.empty(); + for (Component c : components) { + tc.append(c); + } + return tc; + } + + public static boolean isEmpty(Component component) { + return component.getSiblings().isEmpty() && component.getString().isEmpty(); + } + + public static final class Tooltip { + @OnlyIn(Dist.CLIENT) + public static boolean extendedTipCondition() { + return isShiftDown(); + } + + @OnlyIn(Dist.CLIENT) + public static boolean helpCondition() { + return isShiftDown() && isCtrlDown(); + } + + /** + * Adds an extended tooltip or help tooltip depending on the key states of CTRL and SHIFT. + * Returns true if the localisable help/tip was added, false if not (either not CTL/SHIFT or + * no translation found). + */ + @OnlyIn(Dist.CLIENT) + public static boolean addInformation(@Nullable String advancedTooltipTranslationKey, @Nullable String helpTranslationKey, List tooltip, TooltipFlag flag, boolean addAdvancedTooltipHints) { + // Note: intentionally not using keybinding here, this must be `control` or `shift`. + final boolean help_available = (helpTranslationKey != null) && Auxiliaries.hasTranslation(helpTranslationKey + ".help"); + final boolean tip_available = (advancedTooltipTranslationKey != null) && Auxiliaries.hasTranslation(helpTranslationKey + ".tip"); + if ((!help_available) && (!tip_available)) return false; + String tip_text = ""; + if (helpCondition()) { + if (help_available) tip_text = localize(helpTranslationKey + ".help"); + } else if (extendedTipCondition()) { + if (tip_available) tip_text = localize(advancedTooltipTranslationKey + ".tip"); + } else if (addAdvancedTooltipHints) { + if (tip_available) tip_text += localize(modid + ".tooltip.hint.extended") + (help_available ? " " : ""); + if (help_available) tip_text += localize(modid + ".tooltip.hint.help"); + } + if (tip_text.isEmpty()) return false; + String[] tip_list = tip_text.split("\\r?\\n"); + for (String tip : tip_list) { + tooltip.add(Component.literal(tip.replaceAll("\\s+$", "").replaceAll("^\\s+", "")).withStyle(ChatFormatting.GRAY)); + } + return true; + } + + /** + * Adds an extended tooltip or help tooltip for a given stack depending on the key states of CTRL and SHIFT. + * Format in the lang file is (e.g. for items): "item.MODID.REGISTRYNAME.tip" and "item.MODID.REGISTRYNAME.help". + * Return value see method pattern above. + */ + @OnlyIn(Dist.CLIENT) + public static boolean addInformation(ItemStack stack, @Nullable BlockGetter world, List tooltip, TooltipFlag flag, boolean addAdvancedTooltipHints) { + return addInformation(stack.getDescriptionId(), stack.getDescriptionId(), tooltip, flag, addAdvancedTooltipHints); + } + + @OnlyIn(Dist.CLIENT) + public static boolean addInformation(String translation_key, List tooltip) { + if (!Auxiliaries.hasTranslation(translation_key)) return false; + tooltip.add(Component.literal(localize(translation_key).replaceAll("\\s+$", "").replaceAll("^\\s+", "")).withStyle(ChatFormatting.GRAY)); + return true; + } + + } + + @SuppressWarnings("unused") + public static void playerChatMessage(final Player player, final String message) { + player.displayClientMessage(Component.translatable(message.trim()), true); + } + + public static @Nullable Component unserializeTextComponent(String serialized) { + return Component.Serializer.fromJson(serialized); + } + + public static String serializeTextComponent(Component tc) { + return (tc == null) ? ("") : (Component.Serializer.toJson(tc)); + } + + // ------------------------------------------------------------------------------------------------------------------- + // Tag Handling + // ------------------------------------------------------------------------------------------------------------------- + + @SuppressWarnings("deprecation") + public static boolean isInItemTag(Item item, ResourceLocation tag) { + return ForgeRegistries.ITEMS.tags().stream().filter(tg -> tg.getKey().location().equals(tag)).anyMatch(tk -> tk.contains(item)); + } + + @SuppressWarnings("deprecation") + public static boolean isInBlockTag(Block block, ResourceLocation tag) { + return ForgeRegistries.BLOCKS.tags().stream().filter(tg -> tg.getKey().location().equals(tag)).anyMatch(tk -> tk.contains(block)); + } + + @SuppressWarnings("deprecation") + public static ResourceLocation getResourceLocation(Item item) { + return ForgeRegistries.ITEMS.getKey(item); + } + + @SuppressWarnings("deprecation") + public static ResourceLocation getResourceLocation(Block block) { + return ForgeRegistries.BLOCKS.getKey(block); + } + + @SuppressWarnings("deprecation") + public static ResourceLocation getResourceLocation(net.minecraft.world.inventory.MenuType menu) { + return ForgeRegistries.MENU_TYPES.getKey(menu); + } + + @SuppressWarnings("deprecation") + public static ResourceLocation getResourceLocation(net.minecraft.world.level.material.Fluid fluid) { + return ForgeRegistries.FLUIDS.getKey(fluid); + } + + // ------------------------------------------------------------------------------------------------------------------- + // Item NBT data + // ------------------------------------------------------------------------------------------------------------------- + + /** + * Equivalent to getDisplayName(), returns null if no custom name is set. + */ + public static @Nullable Component getItemLabel(ItemStack stack) { + CompoundTag nbt = stack.getTagElement("display"); + if (nbt != null && nbt.contains("Name", 8)) { + try { + Component tc = unserializeTextComponent(nbt.getString("Name")); + if (tc != null) return tc; + nbt.remove("Name"); + } catch (Exception e) { + nbt.remove("Name"); + } + } + return null; + } + + public static ItemStack setItemLabel(ItemStack stack, @Nullable Component name) { + if (name != null) { + CompoundTag nbt = stack.getOrCreateTagElement("display"); + nbt.putString("Name", serializeTextComponent(name)); + } else { + if (stack.hasTag()) stack.removeTagKey("display"); + } + return stack; + } + + // ------------------------------------------------------------------------------------------------------------------- + // Block handling + // ------------------------------------------------------------------------------------------------------------------- + + public static boolean isWaterLogged(BlockState state) { + return state.hasProperty(BlockStateProperties.WATERLOGGED) && state.getValue(BlockStateProperties.WATERLOGGED); + } + + public static AABB getPixeledAABB(double x0, double y0, double z0, double x1, double y1, double z1) { + return new AABB(x0 / 16.0, y0 / 16.0, z0 / 16.0, x1 / 16.0, y1 / 16.0, z1 / 16.0); + } + + public static AABB getRotatedAABB(AABB bb, Direction new_facing) { + return getRotatedAABB(bb, new_facing, false); + } + + public static AABB[] getRotatedAABB(AABB[] bb, Direction new_facing) { + return getRotatedAABB(bb, new_facing, false); + } + + public static AABB getRotatedAABB(AABB bb, Direction new_facing, boolean horizontal_rotation) { + if (!horizontal_rotation) { + switch (new_facing.get3DDataValue()) { + case 0: + return new AABB(1 - bb.maxX, bb.minZ, bb.minY, 1 - bb.minX, bb.maxZ, bb.maxY); // D + case 1: + return new AABB(1 - bb.maxX, 1 - bb.maxZ, 1 - bb.maxY, 1 - bb.minX, 1 - bb.minZ, 1 - bb.minY); // U + case 2: + return new AABB(bb.minX, bb.minY, bb.minZ, bb.maxX, bb.maxY, bb.maxZ); // N --> bb + case 3: + return new AABB(1 - bb.maxX, bb.minY, 1 - bb.maxZ, 1 - bb.minX, bb.maxY, 1 - bb.minZ); // S + case 4: + return new AABB(bb.minZ, bb.minY, 1 - bb.maxX, bb.maxZ, bb.maxY, 1 - bb.minX); // W + case 5: + return new AABB(1 - bb.maxZ, bb.minY, bb.minX, 1 - bb.minZ, bb.maxY, bb.maxX); // E + } + } else { + switch (new_facing.get3DDataValue()) { + case 0: + return new AABB(bb.minX, bb.minY, bb.minZ, bb.maxX, bb.maxY, bb.maxZ); // D --> bb + case 1: + return new AABB(bb.minX, bb.minY, bb.minZ, bb.maxX, bb.maxY, bb.maxZ); // U --> bb + case 2: + return new AABB(bb.minX, bb.minY, bb.minZ, bb.maxX, bb.maxY, bb.maxZ); // N --> bb + case 3: + return new AABB(1 - bb.maxX, bb.minY, 1 - bb.maxZ, 1 - bb.minX, bb.maxY, 1 - bb.minZ); // S + case 4: + return new AABB(bb.minZ, bb.minY, 1 - bb.maxX, bb.maxZ, bb.maxY, 1 - bb.minX); // W + case 5: + return new AABB(1 - bb.maxZ, bb.minY, bb.minX, 1 - bb.minZ, bb.maxY, bb.maxX); // E + } + } + return bb; + } + + public static AABB[] getRotatedAABB(AABB[] bbs, Direction new_facing, boolean horizontal_rotation) { + final AABB[] transformed = new AABB[bbs.length]; + for (int i = 0; i < bbs.length; ++i) transformed[i] = getRotatedAABB(bbs[i], new_facing, horizontal_rotation); + return transformed; + } + + public static AABB getYRotatedAABB(AABB bb, int clockwise_90deg_steps) { + final Direction[] direction_map = new Direction[]{Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST}; + return getRotatedAABB(bb, direction_map[(clockwise_90deg_steps + 4096) & 0x03], true); + } + + public static AABB[] getYRotatedAABB(AABB[] bbs, int clockwise_90deg_steps) { + final AABB[] transformed = new AABB[bbs.length]; + for (int i = 0; i < bbs.length; ++i) transformed[i] = getYRotatedAABB(bbs[i], clockwise_90deg_steps); + return transformed; + } + + public static AABB getMirroredAABB(AABB bb, Direction.Axis axis) { + return switch (axis) { + case X -> new AABB(1 - bb.maxX, bb.minY, bb.minZ, 1 - bb.minX, bb.maxY, bb.maxZ); + case Y -> new AABB(bb.minX, 1 - bb.maxY, bb.minZ, bb.maxX, 1 - bb.minY, bb.maxZ); + case Z -> new AABB(bb.minX, bb.minY, 1 - bb.maxZ, bb.maxX, bb.maxY, 1 - bb.minZ); + }; + } + + public static AABB[] getMirroredAABB(AABB[] bbs, Direction.Axis axis) { + final AABB[] transformed = new AABB[bbs.length]; + for (int i = 0; i < bbs.length; ++i) transformed[i] = getMirroredAABB(bbs[i], axis); + return transformed; + } + + public static VoxelShape getUnionShape(AABB... aabbs) { + VoxelShape shape = Shapes.empty(); + for (AABB aabb : aabbs) shape = Shapes.joinUnoptimized(shape, Shapes.create(aabb), BooleanOp.OR); + return shape; + } + + public static VoxelShape getUnionShape(AABB[]... aabb_list) { + VoxelShape shape = Shapes.empty(); + for (AABB[] aabbs : aabb_list) { + for (AABB aabb : aabbs) shape = Shapes.joinUnoptimized(shape, Shapes.create(aabb), BooleanOp.OR); + } + return shape; + } + + public static AABB[] getMappedAABB(AABB[] bbs, Function mapper) { + final AABB[] transformed = new AABB[bbs.length]; + for (int i = 0; i < bbs.length; ++i) transformed[i] = mapper.apply(bbs[i]); + return transformed; + } + + public static final class BlockPosRange implements Iterable { + private final int x0, x1, y0, y1, z0, z1; + + public BlockPosRange(int x0, int y0, int z0, int x1, int y1, int z1) { + this.x0 = Math.min(x0, x1); + this.x1 = Math.max(x0, x1); + this.y0 = Math.min(y0, y1); + this.y1 = Math.max(y0, y1); + this.z0 = Math.min(z0, z1); + this.z1 = Math.max(z0, z1); + } + + public static BlockPosRange of(AABB range) { + return new BlockPosRange( + (int) Math.floor(range.minX), + (int) Math.floor(range.minY), + (int) Math.floor(range.minZ), + (int) Math.floor(range.maxX - .0625), + (int) Math.floor(range.maxY - .0625), + (int) Math.floor(range.maxZ - .0625) + ); + } + + public int getXSize() { + return x1 - x0 + 1; + } + + public int getYSize() { + return y1 - y0 + 1; + } + + public int getZSize() { + return z1 - z0 + 1; + } + + public int getArea() { + return getXSize() * getZSize(); + } + + public int getHeight() { + return getYSize(); + } + + public int getVolume() { + return getXSize() * getYSize() * getZSize(); + } + + public BlockPos byXZYIndex(int xyz_index) { + final int xsz = getXSize(), ysz = getYSize(), zsz = getZSize(); + xyz_index = xyz_index % (xsz * ysz * zsz); + final int y = xyz_index / (xsz * zsz); + xyz_index -= y * (xsz * zsz); + final int z = xyz_index / xsz; + xyz_index -= z * xsz; + final int x = xyz_index; + return new BlockPos(x0 + x, y0 + y, z0 + z); + } + + public BlockPos byXZIndex(int xz_index, int y_offset) { + final int xsz = getXSize(), zsz = getZSize(); + xz_index = xz_index % (xsz * zsz); + final int z = xz_index / xsz; + xz_index -= z * xsz; + final int x = xz_index; + return new BlockPos(x0 + x, y0 + y_offset, z0 + z); + } + + public static final class BlockRangeIterator implements Iterator { + private final BlockPosRange range_; + private int x, y, z; + + public BlockRangeIterator(BlockPosRange range) { + range_ = range; + x = range.x0; + y = range.y0; + z = range.z0; + } + + @Override + public boolean hasNext() { + return (z <= range_.z1); + } + + @Override + public BlockPos next() { + if (!hasNext()) throw new NoSuchElementException(); + final BlockPos pos = new BlockPos(x, y, z); + ++x; + if (x > range_.x1) { + x = range_.x0; + ++y; + if (y > range_.y1) { + y = range_.y0; + ++z; + } + } + return pos; + } + } + + @Override + public BlockRangeIterator iterator() { + return new BlockRangeIterator(this); + } + + public Stream stream() { + return java.util.stream.StreamSupport.stream(spliterator(), false); + } + } + + // ------------------------------------------------------------------------------------------------------------------- + // JAR resource related + // ------------------------------------------------------------------------------------------------------------------- + + public static String loadResourceText(InputStream is) { + try { + if (is == null) return ""; + BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)); + return br.lines().collect(Collectors.joining("\n")); + } catch (Throwable e) { + return ""; + } + } + + public static String loadResourceText(String path) { + return loadResourceText(Auxiliaries.class.getResourceAsStream(path)); + } + + public static void logGitVersion(String mod_name) { + try { + // Done during construction to have an exact version in case of a crash while registering. + String version = Auxiliaries.loadResourceText("/.gitversion-" + modid).trim(); + logInfo(mod_name + ((version.isEmpty()) ? (" (dev build)") : (" GIT id #" + version)) + "."); + } catch (Throwable e) { + // (void)e; well, then not. Priority is not to get unneeded crashes because of version logging. + } + } +} diff --git a/src/main/java/dev/zontreck/libzontreck/edlibmc/Containers.java b/src/main/java/dev/zontreck/libzontreck/edlibmc/Containers.java new file mode 100644 index 0000000..37a1814 --- /dev/null +++ b/src/main/java/dev/zontreck/libzontreck/edlibmc/Containers.java @@ -0,0 +1,138 @@ +package dev.zontreck.libzontreck.edlibmc; + +import net.minecraft.util.Mth; +import net.minecraft.world.Container; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.function.BiConsumer; + +public class Containers { + // ------------------------------------------------------------------------------------------------------------------- + // Slots + // ------------------------------------------------------------------------------------------------------------------- + + public static class StorageSlot extends Slot { + protected BiConsumer slot_change_action_ = (oldStack, newStack) -> { + }; + protected int stack_limit_ = 64; + public boolean enabled = true; + + public StorageSlot(Container inventory, int index, int x, int y) { + super(inventory, index, x, y); + } + + public StorageSlot setSlotStackLimit(int limit) { + stack_limit_ = Mth.clamp(limit, 1, 64); + return this; + } + + public int getMaxStackSize() { + return stack_limit_; + } + + public StorageSlot setSlotChangeNotifier(BiConsumer action) { + slot_change_action_ = action; + return this; + } + + @Override + public void onQuickCraft(ItemStack oldStack, ItemStack newStack) { + slot_change_action_.accept(oldStack, newStack); + } // no crafting trigger + + @Override + public void set(ItemStack stack) { + if (stack.is(getItem().getItem())) { + super.set(stack); + } else { + final ItemStack before = getItem().copy(); + super.set(stack); // whatever this does else next to setting inventory. + slot_change_action_.accept(before, getItem()); + } + } + + @Override + public boolean mayPlace(ItemStack stack) { + return enabled && this.container.canPlaceItem(this.getSlotIndex(), stack); + } + + @Override + public int getMaxStackSize(ItemStack stack) { + return Math.min(getMaxStackSize(), stack_limit_); + } + + @OnlyIn(Dist.CLIENT) + public boolean isActive() { + return enabled; + } + } + + public static class LockedSlot extends Slot { + protected int stack_limit_ = 64; + public boolean enabled = true; + + public LockedSlot(Container inventory, int index, int x, int y) { + super(inventory, index, x, y); + } + + public LockedSlot setSlotStackLimit(int limit) { + stack_limit_ = Mth.clamp(limit, 1, 64); + return this; + } + + public int getMaxStackSize() { + return stack_limit_; + } + + @Override + public int getMaxStackSize(ItemStack stack) { + return Math.min(getMaxStackSize(), stack_limit_); + } + + @Override + public boolean mayPlace(ItemStack stack) { + return false; + } + + @Override + public boolean mayPickup(Player player) { + return false; + } + + @OnlyIn(Dist.CLIENT) + public boolean isActive() { + return enabled; + } + } + + public static class HiddenSlot extends Slot { + public HiddenSlot(Container inventory, int index) { + super(inventory, index, 0, 0); + } + + @Override + public int getMaxStackSize(ItemStack stack) { + return getMaxStackSize(); + } + + @Override + public boolean mayPlace(ItemStack stack) { + return false; + } + + @Override + public boolean mayPickup(Player player) { + return false; + } + + @OnlyIn(Dist.CLIENT) + public boolean isActive() { + return false; + } + } + +} diff --git a/src/main/java/dev/zontreck/libzontreck/edlibmc/Crafting.java b/src/main/java/dev/zontreck/libzontreck/edlibmc/Crafting.java new file mode 100644 index 0000000..2708733 --- /dev/null +++ b/src/main/java/dev/zontreck/libzontreck/edlibmc/Crafting.java @@ -0,0 +1,440 @@ +/* + * @file Recipes.java + * @author Stefan Wilhelm (wile) + * @copyright (C) 2020 Stefan Wilhelm + * @license MIT (see https://opensource.org/licenses/MIT) + * + * Recipe utility functionality. + */ +package dev.zontreck.libzontreck.edlibmc; + +import net.minecraft.core.NonNullList; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import net.minecraft.util.Tuple; +import net.minecraft.world.Container; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.entity.player.StackedContents; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.CraftingContainer; +import net.minecraft.world.item.EnchantedBookItem; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.*; +import net.minecraft.world.item.enchantment.Enchantment; +import net.minecraft.world.item.enchantment.EnchantmentHelper; +import net.minecraft.world.item.enchantment.EnchantmentInstance; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.ComposterBlock; +import net.minecraftforge.common.ForgeHooks; +import net.minecraftforge.common.brewing.BrewingRecipeRegistry; + +import javax.annotation.Nullable; +import java.util.*; +import java.util.function.BiPredicate; + + +public class Crafting { + // ------------------------------------------------------------------------------------------------------------------- + + /** + * Returns a Crafting recipe by registry name. + */ + public static Optional getCraftingRecipe(Level world, ResourceLocation recipe_id) { + Recipe recipe = world.getRecipeManager().byKey(recipe_id).orElse(null); + return (recipe instanceof CraftingRecipe) ? Optional.of((CraftingRecipe) recipe) : Optional.empty(); + } + + /** + * Returns a list of matching recipes by the first N slots (crafting grid slots) of the given inventory. + */ + public static List get3x3CraftingRecipes(Level world, Container crafting_grid_slots) { + return CraftingGrid.instance3x3.getRecipes(world, crafting_grid_slots); + } + + /** + * Returns a recipe by the first N slots (crafting grid slots). + */ + public static Optional get3x3CraftingRecipe(Level world, Container crafting_grid_slots) { + return get3x3CraftingRecipes(world, crafting_grid_slots).stream().findFirst(); + } + + /** + * Returns the result item of the recipe with the given grid layout. + */ + public static ItemStack get3x3CraftingResult(Level world, Container grid, CraftingRecipe recipe) { + return CraftingGrid.instance3x3.getCraftingResult(world, grid, recipe); + } + + /** + * Returns the items remaining in the grid after crafting 3x3. + */ + public static List get3x3RemainingItems(Level world, Container grid, CraftingRecipe recipe) { + return CraftingGrid.instance3x3.getRemainingItems(world, grid, recipe); + } + + public static List get3x3Placement(Level world, CraftingRecipe recipe, Container item_inventory, @Nullable Container crafting_grid) { + final int width = 3; + final int height = 3; + if (!recipe.canCraftInDimensions(width, height)) return Collections.emptyList(); + List used = new ArrayList<>(); //NonNullList.withSize(width*height); + for (int i = width * height; i > 0; --i) used.add(ItemStack.EMPTY); + Container check_inventory = Inventories.copyOf(item_inventory); + Inventories.InventoryRange source = new Inventories.InventoryRange(check_inventory); + final List ingredients = recipe.getIngredients(); + final List preferred = new ArrayList<>(width * height); + if (crafting_grid != null) { + for (int i = 0; i < crafting_grid.getContainerSize(); ++i) { + ItemStack stack = crafting_grid.getItem(i); + if (stack.isEmpty()) continue; + stack = stack.copy(); + stack.setCount(1); + if (!source.extract(stack).isEmpty()) preferred.add(stack); + } + } + for (int i = 0; i < ingredients.size(); ++i) { + final Ingredient ingredient = ingredients.get(i); + if (ingredient == Ingredient.EMPTY) continue; + ItemStack stack = preferred.stream().filter(ingredient).findFirst().orElse(ItemStack.EMPTY); + if (!stack.isEmpty()) { + preferred.remove(stack); + } else { + stack = source.stream().filter(ingredient).findFirst().orElse(ItemStack.EMPTY); + if (stack.isEmpty()) return Collections.emptyList(); + stack = stack.copy(); + stack.setCount(1); + if (source.extract(stack).isEmpty()) return Collections.emptyList(); + } + used.set(i, stack); + } + if (recipe instanceof ShapedRecipe shaped) { + List placement = NonNullList.withSize(width * height, ItemStack.EMPTY); + for (int row = 0; row < shaped.getRecipeHeight(); ++row) { + for (int col = 0; col < shaped.getRecipeWidth(); ++col) { + placement.set(width * row + col, used.get(row * shaped.getRecipeWidth() + col)); + } + } + return placement; + } else { + return used; + } + } + + /** + * Returns the recipe for a given input stack to smelt, null if there is no recipe + * for the given type (SMELTING,BLASTING,SMOKING, etc). + */ + public static > Optional getFurnaceRecipe(RecipeType recipe_type, Level world, ItemStack input_stack) { + if (input_stack.isEmpty()) { + return Optional.empty(); + } else if (recipe_type == RecipeType.SMELTING) { + SimpleContainer inventory = new SimpleContainer(3); + inventory.setItem(0, input_stack); + SmeltingRecipe recipe = world.getRecipeManager().getRecipeFor(RecipeType.SMELTING, inventory, world).orElse(null); + return (recipe == null) ? Optional.empty() : Optional.of(recipe); + } else if (recipe_type == RecipeType.BLASTING) { + SimpleContainer inventory = new SimpleContainer(3); + inventory.setItem(0, input_stack); + BlastingRecipe recipe = world.getRecipeManager().getRecipeFor(RecipeType.BLASTING, inventory, world).orElse(null); + return (recipe == null) ? Optional.empty() : Optional.of(recipe); + } else if (recipe_type == RecipeType.SMOKING) { + SimpleContainer inventory = new SimpleContainer(3); + inventory.setItem(0, input_stack); + SmokingRecipe recipe = world.getRecipeManager().getRecipeFor(RecipeType.SMOKING, inventory, world).orElse(null); + return (recipe == null) ? Optional.empty() : Optional.of(recipe); + } else { + return Optional.empty(); + } + } + + // ------------------------------------------------------------------------------------------------------------------- + + public static > int getSmeltingTimeNeeded(RecipeType recipe_type, Level world, ItemStack stack) { + if (stack.isEmpty()) return 0; + final int t = getFurnaceRecipe(recipe_type, world, stack).map((AbstractCookingRecipe::getCookingTime)).orElse(0); + return (t <= 0) ? 200 : t; + } + + /** + * Returns the burn time of an item when used as fuel, 0 if it is no fuel. + */ + public static int getFuelBurntime(Level world, ItemStack stack) { + if (stack.isEmpty()) return 0; + int t = ForgeHooks.getBurnTime(stack, null); + return Math.max(t, 0); + } + + /** + * Returns true if an item can be used as fuel. + */ + public static boolean isFuel(Level world, ItemStack stack) { + return (getFuelBurntime(world, stack) > 0) || (stack.getItem() == Items.LAVA_BUCKET); + } + + /** + * Returns burntime and remaining stack then the item shall be used as fuel. + */ + public static Tuple consumeFuel(Level world, ItemStack stack) { + if (stack.isEmpty()) return new Tuple<>(0, stack); + int burnime = getFuelBurntime(world, stack); + if ((stack.getItem() == Items.LAVA_BUCKET)) { + if (burnime <= 0) burnime = 1000 * 20; + return new Tuple<>(burnime, new ItemStack(Items.BUCKET)); + } else if (burnime <= 0) { + return new Tuple<>(0, stack); + } else { + ItemStack left_over = stack.copy(); + left_over.shrink(1); + return new Tuple<>(burnime, left_over); + } + } + + /** + * Returns true if the item can be used as brewing fuel. + */ + public static boolean isBrewingFuel(Level world, ItemStack stack) { + return (stack.getItem() == Items.BLAZE_POWDER) || (stack.getItem() == Items.BLAZE_ROD); + } + + // ------------------------------------------------------------------------------------------------------------------- + + /** + * Returns true if the item can be used as brewing ingredient. + */ + public static boolean isBrewingIngredient(Level world, ItemStack stack) { + return BrewingRecipeRegistry.isValidIngredient(stack); + } + + /** + * Returns true if the item can be used as brewing bottle. + */ + public static boolean isBrewingInput(Level world, ItemStack stack) { + return BrewingRecipeRegistry.isValidInput(stack); + } + + /** + * Returns the burn time for brewing of the given stack. + */ + public static int getBrewingFuelBurntime(Level world, ItemStack stack) { + if (stack.isEmpty()) return 0; + if (stack.getItem() == Items.BLAZE_POWDER) return (400 * 20); + if (stack.getItem() == Items.BLAZE_ROD) return (400 * 40); + return 0; + } + + /** + * Returns brewing burn time and remaining stack if the item shall be used as fuel. + */ + public static Tuple consumeBrewingFuel(Level world, ItemStack stack) { + int burntime = getBrewingFuelBurntime(world, stack); + if (burntime <= 0) return new Tuple<>(0, stack.copy()); + stack = stack.copy(); + stack.shrink(1); + return new Tuple<>(burntime, stack.isEmpty() ? ItemStack.EMPTY : stack); + } + + public static double getCompostingChance(ItemStack stack) { + return ComposterBlock.COMPOSTABLES.getOrDefault(stack.getItem(), 0); + } + + /** + * Returns the enchtments bound to the given stack. + */ + public static Map getEnchantmentsOnItem(Level world, ItemStack stack) { + return (stack.isEmpty() || (stack.getTag() == null)) ? Collections.emptyMap() : EnchantmentHelper.getEnchantments(stack); + } + + // ------------------------------------------------------------------------------------------------------------------- + + /** + * Returns an enchanted book with the given enchantment, emtpy stack if not applicable. + */ + public static ItemStack getEnchantmentBook(Level world, Enchantment enchantment, int level) { + return ((!enchantment.isAllowedOnBooks()) || (level <= 0)) ? ItemStack.EMPTY : EnchantedBookItem.createForEnchantment(new EnchantmentInstance(enchantment, level)); + } + + // ------------------------------------------------------------------------------------------------------------------- + + /** + * Returns the accumulated repair cost for the given enchantments. + */ + public static int getEnchantmentRepairCost(Level world, Map enchantments) { + int repair_cost = 0; + for (Map.Entry e : enchantments.entrySet()) + repair_cost = repair_cost * 2 + 1; // @see: RepairContainer.getNewRepairCost() + return repair_cost; + } + + /** + * Trys to add an enchtment to the given stack, returns boolean success. + */ + public static boolean addEnchantmentOnItem(Level world, ItemStack stack, Enchantment enchantment, int level) { + if (stack.isEmpty() || (level <= 0) || (!stack.isEnchantable()) || (level >= enchantment.getMaxLevel())) + return false; + final Map on_item = getEnchantmentsOnItem(world, stack); + if (on_item.keySet().stream().anyMatch(ench -> ench.isCompatibleWith(enchantment))) return false; + final ItemStack book = EnchantedBookItem.createForEnchantment(new EnchantmentInstance(enchantment, level)); + if ((!(stack.isBookEnchantable(book) && enchantment.isAllowedOnBooks())) && (!stack.canApplyAtEnchantingTable(enchantment)) && (!enchantment.canEnchant(stack))) + return false; + final int existing_level = on_item.getOrDefault(enchantment, 0); + if (existing_level > 0) level = Mth.clamp(level + existing_level, 1, enchantment.getMaxLevel()); + on_item.put(enchantment, level); + EnchantmentHelper.setEnchantments(on_item, stack); + stack.setRepairCost(getEnchantmentRepairCost(world, on_item)); + return true; + } + + /** + * Removes enchantments from a stack, returns the removed enchantments. + */ + public static Map removeEnchantmentsOnItem(Level world, ItemStack stack, BiPredicate filter) { + if (stack.isEmpty()) return Collections.emptyMap(); + final Map on_item = getEnchantmentsOnItem(world, stack); + final Map removed = new HashMap<>(); + for (Map.Entry e : on_item.entrySet()) { + if (filter.test(e.getKey(), e.getValue())) { + removed.put(e.getKey(), e.getValue()); + } + } + for (Enchantment e : removed.keySet()) { + on_item.remove(e); + } + EnchantmentHelper.setEnchantments(on_item, stack); + stack.setRepairCost(getEnchantmentRepairCost(world, on_item)); + return removed; + } + + public static final class CraftingGrid implements CraftingContainer { + private static final CraftingGrid instance3x3 = new CraftingGrid(3, 3); + + final int _width; + + private CraftingGrid(int width, int height) { + _width=width; + } + + private void fill(Container grid) { + for (int i = 0; i < getContainerSize(); ++i) + setItem(i, i >= grid.getContainerSize() ? ItemStack.EMPTY : grid.getItem(i)); + } + + public List getRecipes(Level world, Container grid) { + fill(grid); + return world.getRecipeManager().getRecipesFor(RecipeType.CRAFTING, this, world); + } + + public List getRemainingItems(Level world, Container grid, CraftingRecipe recipe) { + fill(grid); + return recipe.getRemainingItems(this); + } + + public ItemStack getCraftingResult(Level world, Container grid, CraftingRecipe recipe) { + fill(grid); + return recipe.assemble(this, world.registryAccess()); + } + + @Override + public int getWidth() { + return _width; + } + + @Override + public int getHeight() { + return 0; + } + + @Override + public List getItems() { + return null; + } + + @Override + public int getContainerSize() { + return 0; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public ItemStack getItem(int i) { + return null; + } + + @Override + public ItemStack removeItem(int i, int i1) { + return null; + } + + @Override + public ItemStack removeItemNoUpdate(int i) { + return null; + } + + @Override + public void setItem(int i, ItemStack itemStack) { + + } + + @Override + public void setChanged() { + + } + + @Override + public boolean stillValid(Player player) { + return false; + } + + @Override + public void clearContent() { + + } + + @Override + public void fillStackedContents(StackedContents stackedContents) { + + } + } + + public static final class BrewingOutput { + public static final int DEFAULT_BREWING_TIME = 400; + public static final BrewingOutput EMPTY = new BrewingOutput(ItemStack.EMPTY, new SimpleContainer(1), new SimpleContainer(1), 0, 0, DEFAULT_BREWING_TIME); + public final ItemStack item; + public final Container potionInventory; + public final Container ingredientInventory; + public final int potionSlot; + public final int ingredientSlot; + public final int brewTime; + + public BrewingOutput(ItemStack output_potion, Container potion_inventory, Container ingredient_inventory, int potion_slot, int ingredient_slot, int time_needed) { + item = output_potion; + potionInventory = potion_inventory; + ingredientInventory = ingredient_inventory; + potionSlot = potion_slot; + ingredientSlot = ingredient_slot; + brewTime = time_needed; + } + + public static BrewingOutput find(Level world, Container potion_inventory, Container ingredient_inventory) { + for (int potion_slot = 0; potion_slot < potion_inventory.getContainerSize(); ++potion_slot) { + final ItemStack pstack = potion_inventory.getItem(potion_slot); + if (!isBrewingInput(world, pstack)) continue; + for (int ingredient_slot = 0; ingredient_slot < ingredient_inventory.getContainerSize(); ++ingredient_slot) { + final ItemStack istack = ingredient_inventory.getItem(ingredient_slot); + if ((!isBrewingIngredient(world, istack)) || (ingredient_slot == potion_slot) || (isBrewingFuel(world, istack))) + continue; + final ItemStack result = BrewingRecipeRegistry.getOutput(pstack, istack); + if (result.isEmpty()) continue; + return new BrewingOutput(result, potion_inventory, ingredient_inventory, potion_slot, ingredient_slot, DEFAULT_BREWING_TIME); + } + } + return BrewingOutput.EMPTY; + } + } + + +} \ No newline at end of file diff --git a/src/main/java/dev/zontreck/libzontreck/edlibmc/Fluidics.java b/src/main/java/dev/zontreck/libzontreck/edlibmc/Fluidics.java new file mode 100644 index 0000000..6ac9dbd --- /dev/null +++ b/src/main/java/dev/zontreck/libzontreck/edlibmc/Fluidics.java @@ -0,0 +1,485 @@ +/* + * @file Fluidics.java + * @author Stefan Wilhelm (wile) + * @copyright (C) 2020 Stefan Wilhelm + * @license MIT (see https://opensource.org/licenses/MIT) + * + * General fluid handling functionality. + */ +package dev.zontreck.libzontreck.edlibmc; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.util.Mth; +import net.minecraft.util.Tuple; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluid; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.capabilities.ForgeCapabilities; +import net.minecraftforge.common.capabilities.ICapabilityProvider; +import net.minecraft.nbt.Tag; +import net.minecraftforge.common.util.LazyOptional; +import net.minecraftforge.fluids.FluidActionResult; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.FluidUtil; +import net.minecraftforge.fluids.IFluidTank; +import net.minecraftforge.fluids.capability.IFluidHandler; +import net.minecraftforge.fluids.capability.IFluidHandler.FluidAction; +import net.minecraftforge.fluids.capability.IFluidHandlerItem; +import net.minecraftforge.items.IItemHandler; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Predicate; + + +public class Fluidics { + public static class SingleTankFluidHandler implements IFluidHandler { + private final IFluidTank tank_; + + public SingleTankFluidHandler(IFluidTank tank) { + tank_ = tank; + } + + @Override + public int getTanks() { + return 1; + } + + @Override + public FluidStack getFluidInTank(int tank) { + return tank_.getFluid(); + } + + @Override + public int getTankCapacity(int tank) { + return tank_.getCapacity(); + } + + @Override + public boolean isFluidValid(int tank, @Nonnull FluidStack stack) { + return tank_.isFluidValid(stack); + } + + @Override + public int fill(FluidStack resource, FluidAction action) { + return tank_.fill(resource, action); + } + + @Override + public FluidStack drain(FluidStack resource, FluidAction action) { + return tank_.drain(resource, action); + } + + @Override + public FluidStack drain(int maxDrain, FluidAction action) { + return tank_.drain(maxDrain, action); + } + } + + private static class SingleTankOutputFluidHandler implements IFluidHandler { + private final IFluidTank tank_; + + public SingleTankOutputFluidHandler(IFluidTank tank) { + tank_ = tank; + } + + @Override + public int getTanks() { + return 1; + } + + @Override + public FluidStack getFluidInTank(int tank) { + return tank_.getFluid().copy(); + } + + @Override + public int getTankCapacity(int tank) { + return tank_.getCapacity(); + } + + @Override + public boolean isFluidValid(int tank, @Nonnull FluidStack stack) { + return true; + } + + @Override + public int fill(FluidStack resource, FluidAction action) { + return 0; + } + + @Override + public FluidStack drain(FluidStack resource, FluidAction action) { + return tank_.drain(resource, action); + } + + @Override + public FluidStack drain(int maxDrain, FluidAction action) { + return tank_.drain(maxDrain, action); + } + } + + public static class Tank implements IFluidTank { + private Predicate validator_ = ((e) -> true); + private BiConsumer interaction_notifier_ = ((tank, diff) -> { + }); + private FluidStack fluid_ = FluidStack.EMPTY; + private int capacity_; + private int fill_rate_; + private int drain_rate_; + + public Tank(int capacity) { + this(capacity, capacity, capacity); + } + + public Tank(int capacity, int fill_rate, int drain_rate) { + this(capacity, fill_rate, drain_rate, e -> true); + } + + public Tank(int capacity, int fill_rate, int drain_rate, Predicate validator) { + capacity_ = capacity; + setMaxFillRate(fill_rate); + setMaxDrainRate(drain_rate); + setValidator(validator); + } + + public Tank load(CompoundTag nbt) { + if (nbt.contains("tank", Tag.TAG_COMPOUND)) { + setFluid(FluidStack.loadFluidStackFromNBT(nbt.getCompound("tank"))); + } else { + clear(); + } + return this; + } + + public CompoundTag save(CompoundTag nbt) { + if (!isEmpty()) { + nbt.put("tank", fluid_.writeToNBT(new CompoundTag())); + } + return nbt; + } + + public void reset() { + clear(); + } + + public Tank clear() { + setFluid(null); + return this; + } + + public int getCapacity() { + return capacity_; + } + + public Tank setCapacity(int capacity) { + capacity_ = capacity; + return this; + } + + public int getMaxDrainRate() { + return drain_rate_; + } + + public Tank setMaxDrainRate(int rate) { + drain_rate_ = Mth.clamp(rate, 0, capacity_); + return this; + } + + public int getMaxFillRate() { + return fill_rate_; + } + + public Tank setMaxFillRate(int rate) { + fill_rate_ = Mth.clamp(rate, 0, capacity_); + return this; + } + + public Tank setValidator(Predicate validator) { + validator_ = (validator != null) ? validator : ((e) -> true); + return this; + } + + public Tank setInteractionNotifier(BiConsumer notifier) { + interaction_notifier_ = (notifier != null) ? notifier : ((tank, diff) -> { + }); + return this; + } + + public LazyOptional createFluidHandler() { + return LazyOptional.of(() -> new Fluidics.SingleTankFluidHandler(this)); + } + + public LazyOptional createOutputFluidHandler() { + return LazyOptional.of(() -> new Fluidics.SingleTankOutputFluidHandler(this)); + } + + // IFluidTank ------------------------------------------------------------------------------------ + + @Nonnull + public FluidStack getFluid() { + return fluid_; + } + + public void setFluid(@Nullable FluidStack stack) { + fluid_ = (stack == null) ? FluidStack.EMPTY : stack; + } + + public int getFluidAmount() { + return fluid_.getAmount(); + } + + public boolean isEmpty() { + return fluid_.isEmpty(); + } + + public boolean isFull() { + return getFluidAmount() >= getCapacity(); + } + + public boolean isFluidValid(FluidStack stack) { + return validator_.test(stack); + } + + public boolean isFluidEqual(FluidStack stack) { + return (stack == null) ? (fluid_.isEmpty()) : fluid_.isFluidEqual(stack); + } + + @Override + public int fill(FluidStack fs, FluidAction action) { + if ((fs == null) || fs.isEmpty() || (!isFluidValid(fs))) { + return 0; + } else if (action.simulate()) { + if (fluid_.isEmpty()) return Math.min(capacity_, fs.getAmount()); + if (!fluid_.isFluidEqual(fs)) return 0; + return Math.min(capacity_ - fluid_.getAmount(), fs.getAmount()); + } else if (fluid_.isEmpty()) { + fluid_ = new FluidStack(fs, Math.min(capacity_, fs.getAmount())); + return fluid_.getAmount(); + } else if (!fluid_.isFluidEqual(fs)) { + return 0; + } else { + int amount = capacity_ - fluid_.getAmount(); + if (fs.getAmount() < amount) { + fluid_.grow(fs.getAmount()); + amount = fs.getAmount(); + } else { + fluid_.setAmount(capacity_); + } + if (amount != 0) interaction_notifier_.accept(this, amount); + return amount; + } + } + + @Nonnull + public FluidStack drain(int maxDrain) { + return drain(maxDrain, FluidAction.EXECUTE); + } + + @Nonnull + @Override + public FluidStack drain(FluidStack fs, FluidAction action) { + return ((fs.isEmpty()) || (!fs.isFluidEqual(fluid_))) ? FluidStack.EMPTY : drain(fs.getAmount(), action); + } + + @Nonnull + @Override + public FluidStack drain(int maxDrain, FluidAction action) { + final int amount = Math.min(fluid_.getAmount(), maxDrain); + final FluidStack stack = new FluidStack(fluid_, amount); + if ((amount > 0) && action.execute()) { + fluid_.shrink(amount); + if (fluid_.isEmpty()) fluid_ = FluidStack.EMPTY; + if (amount != 0) interaction_notifier_.accept(this, -amount); + } + return stack; + } + } + + // ------------------------------------------------------------------------------------------------------------------- + + public static @Nullable IFluidHandler handler(Level world, BlockPos pos, @Nullable Direction side) { + return FluidUtil.getFluidHandler(world, pos, side).orElse(null); + } + + /** + * Fills or drains items with fluid handlers from or into tile blocks with fluid handlers. + */ + public static boolean manualFluidHandlerInteraction(Level world, BlockPos pos, @Nullable Direction side, Player player, InteractionHand hand) { + return manualTrackedFluidHandlerInteraction(world, pos, side, player, hand) != null; + } + + public static boolean manualFluidHandlerInteraction(Player player, InteractionHand hand, IFluidHandler handler) { + return FluidUtil.interactWithFluidHandler(player, hand, handler); + } + + /** + * Fills or drains items with fluid handlers from or into tile blocks with fluid handlers. + * Returns the fluid and (possibly negative) amount that transferred from the item into the block. + */ + public static @Nullable Tuple manualTrackedFluidHandlerInteraction(Level world, BlockPos pos, @Nullable Direction side, Player player, InteractionHand hand) { + if (world.isClientSide()) return null; + final ItemStack held = player.getItemInHand(hand); + if (held.isEmpty()) return null; + final IFluidHandler fh = handler(world, pos, side); + if (fh == null) return null; + final IItemHandler ih = player.getCapability(ForgeCapabilities.ITEM_HANDLER).orElse(null); + if (ih == null) return null; + FluidActionResult far = FluidUtil.tryFillContainerAndStow(held, fh, ih, Integer.MAX_VALUE, player, true); + if (!far.isSuccess()) far = FluidUtil.tryEmptyContainerAndStow(held, fh, ih, Integer.MAX_VALUE, player, true); + if (!far.isSuccess()) return null; + final ItemStack rstack = far.getResult().copy(); + player.setItemInHand(hand, far.getResult()); + final IFluidHandler fh_before = FluidUtil.getFluidHandler(held).orElse(null); + final IFluidHandler fh_after = FluidUtil.getFluidHandler(rstack).orElse(null); + if ((fh_before == null) || (fh_after == null) || (fh_after.getTanks() != fh_before.getTanks())) + return null; // should not be, but y'never know. + for (int i = 0; i < fh_before.getTanks(); ++i) { + final int vol_before = fh_before.getFluidInTank(i).getAmount(); + final int vol_after = fh_after.getFluidInTank(i).getAmount(); + if (vol_before != vol_after) { + return new Tuple<>( + (vol_before > 0) ? (fh_before.getFluidInTank(i).getFluid()) : (fh_after.getFluidInTank(i).getFluid()), + (vol_before - vol_after) + ); + } + } + return null; + } + + public static boolean manualFluidHandlerInteraction(Player player, InteractionHand hand, Level world, BlockPos pos, @Nullable Direction side) { + return FluidUtil.interactWithFluidHandler(player, hand, world, pos, side); + } + + public static int fill(Level world, BlockPos pos, Direction side, FluidStack fs, FluidAction action) { + IFluidHandler fh = FluidUtil.getFluidHandler(world, pos, side).orElse(null); + return (fh == null) ? (0) : (fh.fill(fs, action)); + } + + public static int fill(Level world, BlockPos pos, Direction side, FluidStack fs) { + return fill(world, pos, side, fs, FluidAction.EXECUTE); + } + + /** + * Fluid tank access when itemized. + */ + public static class FluidContainerItemCapabilityWrapper implements IFluidHandlerItem, ICapabilityProvider { + private final LazyOptional handler_ = LazyOptional.of(() -> this); + private final Function nbt_getter_; + private final BiConsumer nbt_setter_; + private final Predicate validator_; + private final ItemStack container_; + private final int capacity_; + private final int transfer_rate_; + + public FluidContainerItemCapabilityWrapper(ItemStack container, int capacity, int transfer_rate, + Function nbt_getter, + BiConsumer nbt_setter, + Predicate validator) { + container_ = container; + capacity_ = capacity; + transfer_rate_ = transfer_rate; + nbt_getter_ = nbt_getter; + nbt_setter_ = nbt_setter; + validator_ = (validator != null) ? validator : (e -> true); + } + + @Override + public LazyOptional getCapability(Capability capability, @Nullable Direction side) { + return (capability == ForgeCapabilities.FLUID_HANDLER) ? handler_.cast() : LazyOptional.empty(); + } + + protected FluidStack readnbt() { + final CompoundTag nbt = nbt_getter_.apply(container_); + return ((nbt == null) || (nbt.isEmpty())) ? FluidStack.EMPTY : FluidStack.loadFluidStackFromNBT(nbt); + } + + protected void writenbt(FluidStack fs) { + CompoundTag nbt = new CompoundTag(); + if (!fs.isEmpty()) fs.writeToNBT(nbt); + nbt_setter_.accept(container_, nbt); + } + + @Override + public ItemStack getContainer() { + return container_; + } + + @Override + public int getTanks() { + return 1; + } + + @Override + public FluidStack getFluidInTank(int tank) { + return readnbt(); + } + + @Override + public int getTankCapacity(int tank) { + return capacity_; + } + + @Override + public boolean isFluidValid(int tank, FluidStack fs) { + return isFluidValid(fs); + } + + public boolean isFluidValid(FluidStack fs) { + return validator_.test(fs); + } + + @Override + public int fill(FluidStack fs, FluidAction action) { + if ((fs.isEmpty()) || (!isFluidValid(fs) || (container_.getCount() != 1))) return 0; + FluidStack tank = readnbt(); + final int amount = Math.min(Math.min(fs.getAmount(), transfer_rate_), capacity_ - tank.getAmount()); + if (amount <= 0) return 0; + if (tank.isEmpty()) { + if (action.execute()) { + tank = new FluidStack(fs.getFluid(), amount, fs.getTag()); + writenbt(tank); + } + } else { + if (!tank.isFluidEqual(fs)) { + return 0; + } else if (action.execute()) { + tank.grow(amount); + writenbt(tank); + } + } + return amount; + } + + @Override + public FluidStack drain(FluidStack fs, FluidAction action) { + if ((fs.isEmpty()) || (container_.getCount() != 1)) return FluidStack.EMPTY; + final FluidStack tank = readnbt(); + if ((!tank.isEmpty()) && (!tank.isFluidEqual(fs))) return FluidStack.EMPTY; + return drain(fs.getAmount(), action); + } + + @Override + public FluidStack drain(int max, FluidAction action) { + if ((max <= 0) || (container_.getCount() != 1)) return FluidStack.EMPTY; + FluidStack tank = readnbt(); + if (tank.isEmpty()) return FluidStack.EMPTY; + final int amount = Math.min(Math.min(tank.getAmount(), max), transfer_rate_); + final FluidStack fs = tank.copy(); + fs.setAmount(amount); + if (action.execute()) { + tank.shrink(amount); + writenbt(tank); + } + return fs; + } + } + +} diff --git a/src/main/java/dev/zontreck/libzontreck/edlibmc/Guis.java b/src/main/java/dev/zontreck/libzontreck/edlibmc/Guis.java new file mode 100644 index 0000000..1374a19 --- /dev/null +++ b/src/main/java/dev/zontreck/libzontreck/edlibmc/Guis.java @@ -0,0 +1,466 @@ +/* + * @file Guis.java + * @author Stefan Wilhelm (wile) + * @copyright (C) 2020 Stefan Wilhelm + * @license MIT (see https://opensource.org/licenses/MIT) + * + * Gui Wrappers and Widgets. + */ +package dev.zontreck.libzontreck.edlibmc; + +import com.mojang.blaze3d.platform.Window; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.renderer.entity.ItemRenderer; +import net.minecraft.client.sounds.SoundManager; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.Arrays; +import java.util.function.Consumer; +import java.util.function.Function; + + +public class Guis { + // ------------------------------------------------------------------------------------------------------------------- + // Gui base + // ------------------------------------------------------------------------------------------------------------------- + + @OnlyIn(Dist.CLIENT) + public static abstract class ContainerGui extends AbstractContainerScreen { + protected final ResourceLocation background_image_; + protected final Player player_; + protected final Guis.BackgroundImage gui_background_; + protected final TooltipDisplay tooltip_ = new TooltipDisplay(); + + public ContainerGui(T menu, Inventory player_inv, Component title, String background_image, int width, int height) { + super(menu, player_inv, title); + this.background_image_ = new ResourceLocation(Auxiliaries.modid(), background_image); + this.player_ = player_inv.player; + this.imageWidth = width; + this.imageHeight = height; + gui_background_ = new Guis.BackgroundImage(background_image_, width, height, Coord2d.ORIGIN); + } + + public ContainerGui(T menu, Inventory player_inv, Component title, String background_image) { + super(menu, player_inv, title); + this.background_image_ = new ResourceLocation(Auxiliaries.modid(), background_image); + this.player_ = player_inv.player; + gui_background_ = new Guis.BackgroundImage(background_image_, imageWidth, imageHeight, Coord2d.ORIGIN); + } + + @Override + public void init() { + super.init(); + gui_background_.init(this, Coord2d.ORIGIN).show(); + } + + @Override + public void render(GuiGraphics mx, int mouseX, int mouseY, float partialTicks) { + renderBackground(mx); + super.render(mx, mouseX, mouseY, partialTicks); + if (!tooltip_.render(mx, this, mouseX, mouseY)) renderTooltip(mx, mouseX, mouseY); + } + + @Override + protected void renderLabels(GuiGraphics mx, int x, int y) { + } + + @Override + @SuppressWarnings("deprecation") + protected final void renderBg(GuiGraphics mx, float partialTicks, int mouseX, int mouseY) { + RenderSystem.setShader(GameRenderer::getPositionTexShader); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + RenderSystem.enableDepthTest(); + gui_background_.draw(mx, this); + renderBgWidgets(mx, partialTicks, mouseX, mouseY); + RenderSystem.disableBlend(); + } + + public final ResourceLocation getBackgroundImage() { + return background_image_; + } + + protected void renderBgWidgets(GuiGraphics mx, float partialTicks, int mouseX, int mouseY) { + } + + protected void renderItemTemplate(GuiGraphics mx, ItemStack stack, int x, int y) { + final int x0 = getGuiLeft(); + final int y0 = getGuiTop(); + + mx.renderFakeItem(stack, x0 + x, y0 + y); + RenderSystem.disableColorLogicOp(); //RenderSystem.disableColorMaterial(); + RenderSystem.enableDepthTest(); //RenderSystem.enableAlphaTest(); + RenderSystem.defaultBlendFunc(); + RenderSystem.enableBlend(); + RenderSystem.colorMask(true, true, true, true); + RenderSystem.setShaderColor(0.7f, 0.7f, 0.7f, 0.8f); + RenderSystem.setShaderTexture(0, background_image_); + mx.blit(background_image_,x0 + x, y0 + y, x, y, 16, 16); + RenderSystem.setShaderColor(1f, 1f, 1f, 1f); + } + } + + // ------------------------------------------------------------------------------------------------------------------- + // Gui elements + // ------------------------------------------------------------------------------------------------------------------- + + @OnlyIn(Dist.CLIENT) + public static class Coord2d { + public static final Coord2d ORIGIN = new Coord2d(0, 0); + public final int x, y; + + public Coord2d(int x, int y) { + this.x = x; + this.y = y; + } + + public static Coord2d of(int x, int y) { + return new Coord2d(x, y); + } + + public String toString() { + return "[" + x + "," + y + "]"; + } + } + + @OnlyIn(Dist.CLIENT) + public static class UiWidget extends AbstractWidget { + protected static final Component EMPTY_TEXT = Component.literal(""); + protected static final Function NO_TOOLTIP = (uiw) -> EMPTY_TEXT; + + private final Minecraft mc_; + private Function tooltip_ = NO_TOOLTIP; + private Screen parent_; + + public UiWidget(int x, int y, int width, int height, Component title) { + super(x, y, width, height, title); + mc_ = Minecraft.getInstance(); + } + + public UiWidget init(Screen parent) { + this.parent_ = parent; + this.setX(((parent instanceof AbstractContainerScreen) ? ((AbstractContainerScreen) parent).getGuiLeft() : 0)); + this.setY(((parent instanceof AbstractContainerScreen) ? ((AbstractContainerScreen) parent).getGuiTop() : 0)); + return this; + } + + public UiWidget init(Screen parent, Coord2d position) { + this.parent_ = parent; + this.setX(position.x + ((parent instanceof AbstractContainerScreen) ? ((AbstractContainerScreen) parent).getGuiLeft() : 0)); + this.setY(position.y + ((parent instanceof AbstractContainerScreen) ? ((AbstractContainerScreen) parent).getGuiTop() : 0)); + return this; + } + + public final UiWidget tooltip(Function tip) { + tooltip_ = tip; + return this; + } + + public final UiWidget tooltip(Component tip) { + tooltip_ = (o) -> tip; + return this; + } + + public final int getWidth() { + return this.width; + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput narrationElementOutput) { + + } + + public final int getHeight() { + return this.height; + } + + public Coord2d getMousePosition() { + final Window win = mc_.getWindow(); + return Coord2d.of( + Mth.clamp(((int) (mc_.mouseHandler.xpos() * (double) win.getGuiScaledWidth() / (double) win.getScreenWidth())) - this.getX(), -1, this.width + 1), + Mth.clamp(((int) (mc_.mouseHandler.ypos() * (double) win.getGuiScaledHeight() / (double) win.getScreenHeight())) - this.getY(), -1, this.height + 1) + ); + } + + protected final Coord2d screenCoordinates(Coord2d xy, boolean reverse) { + return (reverse) ? (Coord2d.of(xy.x + getX(), xy.y + getY())) : (Coord2d.of(xy.x - getX(), xy.y - getY())); + } + + public UiWidget show() { + visible = true; + return this; + } + + public UiWidget hide() { + visible = false; + return this; + } + + @Override + public void renderWidget(GuiGraphics mxs, int mouseX, int mouseY, float partialTicks) { + //super.renderWidget(mxs, mouseX, mouseY, partialTicks); + if (isHovered) renderToolTip(mxs, mouseX, mouseY); + } + + @SuppressWarnings("all") + public void renderToolTip(GuiGraphics mx, int mouseX, int mouseY) { + if (!visible || (!active) || (tooltip_ == NO_TOOLTIP)) return; + final Component tip = tooltip_.apply(this); + if (tip.getString().trim().isEmpty()) return; + mx.renderTooltip(mc_.font, Arrays.asList(tip.getVisualOrderText()), mouseX, mouseY); + } + } + + @OnlyIn(Dist.CLIENT) + public static class HorizontalProgressBar extends UiWidget { + private final Coord2d texture_position_base_; + private final Coord2d texture_position_filled_; + private final ResourceLocation atlas_; + private double progress_max_ = 100; + private double progress_ = 0; + + public HorizontalProgressBar(ResourceLocation atlas, int width, int height, Coord2d base_texture_xy, Coord2d filled_texture_xy) { + super(0, 0, width, height, EMPTY_TEXT); + atlas_ = atlas; + texture_position_base_ = base_texture_xy; + texture_position_filled_ = filled_texture_xy; + } + + public HorizontalProgressBar setProgress(double progress) { + progress_ = Mth.clamp(progress, 0, progress_max_); + return this; + } + + public double getProgress() { + return progress_; + } + + public HorizontalProgressBar setMaxProgress(double progress) { + progress_max_ = Math.max(progress, 0); + return this; + } + + public double getMaxProgress() { + return progress_max_; + } + + public HorizontalProgressBar show() { + visible = true; + return this; + } + + public HorizontalProgressBar hide() { + visible = false; + return this; + } + + @Override + public void playDownSound(SoundManager handler) { + } + + @Override + public void renderWidget(GuiGraphics mxs, int mouseX, int mouseY, float partialTicks) { + RenderSystem.setShaderTexture(0, atlas_); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, this.alpha); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + RenderSystem.enableDepthTest(); + mxs.blit(atlas_, getX(), getY(), texture_position_base_.x, texture_position_base_.y, width, height); + if ((progress_max_ > 0) && (progress_ > 0)) { + int w = Mth.clamp((int) Math.round((progress_ * width) / progress_max_), 0, width); + mxs.blit(atlas_, getX(), getY(), texture_position_filled_.x, texture_position_filled_.y, w, height); + } + if (isHovered) renderToolTip(mxs, mouseX, mouseY); + } + } + + @OnlyIn(Dist.CLIENT) + public static class BackgroundImage extends UiWidget { + private final ResourceLocation atlas_; + private final Coord2d atlas_position_; + public boolean visible; + + public BackgroundImage(ResourceLocation atlas, int width, int height, Coord2d atlas_position) { + super(0, 0, width, height, EMPTY_TEXT); + atlas_ = atlas; + atlas_position_ = atlas_position; + this.width = width; + this.height = height; + visible = true; + } + + public void draw(GuiGraphics mx, Screen parent) { + if (!visible) return; + RenderSystem.setShaderTexture(0, atlas_); + mx.blit(atlas_, getX(), getY(), atlas_position_.x, atlas_position_.y, width, height); + } + } + + @OnlyIn(Dist.CLIENT) + public static class CheckBox extends UiWidget { + private final Coord2d texture_position_off_; + private final Coord2d texture_position_on_; + private final ResourceLocation atlas_; + private boolean checked_ = false; + private Consumer on_click_ = (checkbox) -> { + }; + + public CheckBox(ResourceLocation atlas, int width, int height, Coord2d atlas_texture_position_off, Coord2d atlas_texture_position_on) { + super(0, 0, width, height, EMPTY_TEXT); + texture_position_off_ = atlas_texture_position_off; + texture_position_on_ = atlas_texture_position_on; + atlas_ = atlas; + } + + public boolean checked() { + return checked_; + } + + public CheckBox checked(boolean on) { + checked_ = on; + return this; + } + + public CheckBox onclick(Consumer action) { + on_click_ = action; + return this; + } + + @Override + public void onClick(double mouseX, double mouseY) { + checked_ = !checked_; + on_click_.accept(this); + } + + @Override + public void renderWidget(GuiGraphics mxs, int mouseX, int mouseY, float partialTicks) { + RenderSystem.setShader(GameRenderer::getPositionTexShader); + RenderSystem.setShaderTexture(0, atlas_); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, this.alpha); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + RenderSystem.enableDepthTest(); + Coord2d pos = checked_ ? texture_position_on_ : texture_position_off_; + mxs.blit(atlas_, getX(), getY(), pos.x, pos.y, width, height); + if (isHovered) renderToolTip(mxs, mouseX, mouseY); + } + } + + @OnlyIn(Dist.CLIENT) + public static class ImageButton extends UiWidget { + private final Coord2d texture_position_; + private final ResourceLocation atlas_; + private Consumer on_click_ = (bt) -> { + }; + + + public ImageButton(ResourceLocation atlas, int width, int height, Coord2d atlas_texture_position) { + super(0, 0, width, height, Component.empty()); + texture_position_ = atlas_texture_position; + atlas_ = atlas; + } + + public ImageButton onclick(Consumer action) { + on_click_ = action; + return this; + } + + @Override + public void onClick(double mouseX, double mouseY) { + on_click_.accept(this); + } + + @Override + public void renderWidget(GuiGraphics mxs, int mouseX, int mouseY, float partialTicks) { + RenderSystem.setShader(GameRenderer::getPositionTexShader); + RenderSystem.setShaderTexture(0, atlas_); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, this.alpha); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + RenderSystem.enableDepthTest(); + Coord2d pos = texture_position_; + mxs.blit(atlas_, getX(), getY(), pos.x, pos.y, width, height); + if (isHovered) renderToolTip(mxs, mouseX, mouseY); + } + } + + @OnlyIn(Dist.CLIENT) + public static class Image extends UiWidget { + private final Coord2d texture_position_; + private final ResourceLocation atlas_; + + public Image(ResourceLocation atlas, int width, int height, Coord2d atlas_texture_position) { + super(0, 0, width, height, Component.empty()); + texture_position_ = atlas_texture_position; + atlas_ = atlas; + } + + @Override + public void onClick(double mouseX, double mouseY) { + } + + @Override + public void renderWidget(GuiGraphics mxs, int mouseX, int mouseY, float partialTicks) { + RenderSystem.setShader(GameRenderer::getPositionTexShader); + RenderSystem.setShaderTexture(0, atlas_); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, this.alpha); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + RenderSystem.enableDepthTest(); + Coord2d pos = texture_position_; + mxs.blit(atlas_, getX(), getY(), pos.x, pos.y, width, height); + if (isHovered) renderToolTip(mxs, mouseX, mouseY); + } + } + + @OnlyIn(Dist.CLIENT) + public static class TextBox extends net.minecraft.client.gui.components.EditBox { + public TextBox(int x, int y, int width, int height, Component title, Font font) { + super(font, x, y, width, height, title); + setBordered(false); + } + + public TextBox withMaxLength(int len) { + super.setMaxLength(len); + return this; + } + + public TextBox withBordered(boolean b) { + super.setBordered(b); + return this; + } + + public TextBox withValue(String s) { + super.setValue(s); + return this; + } + + public TextBox withEditable(boolean e) { + super.setEditable(e); + return this; + } + + public TextBox withResponder(Consumer r) { + super.setResponder(r); + return this; + } + } +} diff --git a/src/main/java/dev/zontreck/libzontreck/edlibmc/Inventories.java b/src/main/java/dev/zontreck/libzontreck/edlibmc/Inventories.java new file mode 100644 index 0000000..e7442e0 --- /dev/null +++ b/src/main/java/dev/zontreck/libzontreck/edlibmc/Inventories.java @@ -0,0 +1,1134 @@ +/* + * @file Inventories.java + * @author Stefan Wilhelm (wile) + * @copyright (C) 2020 Stefan Wilhelm + * @license MIT (see https://opensource.org/licenses/MIT) + * + * General inventory item handling functionality. + */ +package dev.zontreck.libzontreck.edlibmc; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.NonNullList; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.util.Mth; +import net.minecraft.world.*; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.minecraft.nbt.Tag; +import net.minecraftforge.common.capabilities.ForgeCapabilities; +import net.minecraftforge.common.util.LazyOptional; +import net.minecraftforge.items.IItemHandler; +import net.minecraftforge.items.ItemHandlerHelper; +import net.minecraftforge.items.wrapper.InvWrapper; +import net.minecraftforge.items.wrapper.PlayerMainInvWrapper; +import net.minecraftforge.items.wrapper.SidedInvWrapper; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + + +public class Inventories { + public static boolean areItemStacksIdentical(ItemStack a, ItemStack b) { + return (a.getItem() == b.getItem()) && ItemStack.isSameItemSameTags(a, b); + } + + public static boolean areItemStacksDifferent(ItemStack a, ItemStack b) { + return (a.getItem() != b.getItem()) || (!ItemStack.isSameItemSameTags(a, b)); + } + + public static IItemHandler itemhandler(Level world, BlockPos pos, @Nullable Direction side) { + BlockEntity te = world.getBlockEntity(pos); + if (te == null) return null; + IItemHandler ih = te.getCapability(ForgeCapabilities.ITEM_HANDLER, side).orElse(null); + if (ih != null) return ih; + if ((side != null) && (te instanceof WorldlyContainer)) return new SidedInvWrapper((WorldlyContainer) te, side); + if (te instanceof Container) return new InvWrapper((Container) te); + return null; + } + + public static IItemHandler itemhandler(Level world, BlockPos pos, @Nullable Direction side, boolean including_entities) { + IItemHandler ih = itemhandler(world, pos, side); + if (ih != null) return ih; + if (!including_entities) return null; + Entity entity = world.getEntitiesOfClass(Entity.class, new AABB(pos), (e) -> (e instanceof Container)).stream().findFirst().orElse(null); + return (entity == null) ? (null) : (itemhandler(entity, side)); + } + + public static IItemHandler itemhandler(Player player) { + return new PlayerMainInvWrapper(player.getInventory()); + } + + public static IItemHandler itemhandler(Entity entity) { + return itemhandler(entity, null); + } + + public static IItemHandler itemhandler(Entity entity, @Nullable Direction side) { + if (entity == null) return null; + final IItemHandler ih = entity.getCapability(ForgeCapabilities.ITEM_HANDLER, side).orElse(null); + if (ih != null) return ih; + if (entity instanceof Container container) return (new InvWrapper(container)); + return null; + } + + public static boolean insertionPossible(Level world, BlockPos pos, @Nullable Direction side, boolean including_entities) { + return itemhandler(world, pos, side, including_entities) != null; + } + + public static ItemStack insert(IItemHandler handler, ItemStack stack, boolean simulate) { + return ItemHandlerHelper.insertItemStacked(handler, stack, simulate); + } + + public static ItemStack insert(BlockEntity te, @Nullable Direction side, ItemStack stack, boolean simulate) { + if (te == null) return stack; + IItemHandler hnd = te.getCapability(ForgeCapabilities.ITEM_HANDLER, side).orElse(null); + if (hnd == null) { + if ((side != null) && (te instanceof WorldlyContainer)) { + hnd = new SidedInvWrapper((WorldlyContainer) te, side); + } else if (te instanceof Container) { + hnd = new InvWrapper((Container) te); + } + } + return (hnd == null) ? stack : insert(hnd, stack, simulate); + } + + public static ItemStack insert(Level world, BlockPos pos, @Nullable Direction side, ItemStack stack, boolean simulate, boolean including_entities) { + return insert(itemhandler(world, pos, side, including_entities), stack, simulate); + } + + public static ItemStack insert(Level world, BlockPos pos, @Nullable Direction side, ItemStack stack, boolean simulate) { + return insert(world, pos, side, stack, simulate, false); + } + + public static ItemStack extract(IItemHandler inventory, @Nullable ItemStack match, int amount, boolean simulate) { + if ((inventory == null) || (amount <= 0) || ((match != null) && (match.isEmpty()))) return ItemStack.EMPTY; + final int max = inventory.getSlots(); + ItemStack out_stack = ItemStack.EMPTY; + for (int i = 0; i < max; ++i) { + final ItemStack stack = inventory.getStackInSlot(i); + if (stack.isEmpty()) continue; + if (out_stack.isEmpty()) { + if ((match != null) && areItemStacksDifferent(stack, match)) continue; + out_stack = inventory.extractItem(i, amount, simulate); + } else if (areItemStacksIdentical(stack, out_stack)) { + ItemStack es = inventory.extractItem(i, (amount - out_stack.getCount()), simulate); + out_stack.grow(es.getCount()); + } + if (out_stack.getCount() >= amount) break; + } + return out_stack; + } + + private static ItemStack checked(ItemStack stack) { + return stack.isEmpty() ? ItemStack.EMPTY : stack; + } + + public static Container copyOf(Container src) { + final int size = src.getContainerSize(); + SimpleContainer dst = new SimpleContainer(size); + for (int i = 0; i < size; ++i) dst.setItem(i, src.getItem(i).copy()); + return dst; + } + + //-------------------------------------------------------------------------------------------------------------------- + + public static ItemStack insert(InventoryRange[] to_ranges, ItemStack stack) { + ItemStack remaining = stack.copy(); + for (InventoryRange range : to_ranges) { + remaining = range.insert(remaining, false, 0, false, true); + if (remaining.isEmpty()) return remaining; + } + return remaining; + } + + //-------------------------------------------------------------------------------------------------------------------- + + public static class MappedItemHandler implements IItemHandler { + private final BiPredicate extraction_predicate_; + private final BiPredicate insertion_predicate_; + private final BiConsumer insertion_notifier_; + private final BiConsumer extraction_notifier_; + private final List slot_map_; + private final Container inv_; + + public MappedItemHandler(Container inv, BiPredicate extraction_predicate, BiPredicate insertion_predicate, BiConsumer insertion_notifier, BiConsumer extraction_notifier) { + inv_ = inv; + extraction_predicate_ = extraction_predicate; + insertion_predicate_ = insertion_predicate; + insertion_notifier_ = insertion_notifier; + extraction_notifier_ = extraction_notifier; + slot_map_ = IntStream.range(0, inv.getContainerSize()).boxed().collect(Collectors.toList()); + } + + public MappedItemHandler(Container inv, List slot_map, BiPredicate extraction_predicate, BiPredicate insertion_predicate, BiConsumer insertion_notifier, BiConsumer extraction_notifier) { + inv_ = inv; + extraction_predicate_ = extraction_predicate; + insertion_predicate_ = insertion_predicate; + insertion_notifier_ = insertion_notifier; + extraction_notifier_ = extraction_notifier; + slot_map_ = slot_map; + } + + public MappedItemHandler(Container inv, List slot_map, BiPredicate extraction_predicate, BiPredicate insertion_predicate) { + this(inv, slot_map, extraction_predicate, insertion_predicate, (i, s) -> { + }, (i, s) -> { + }); + } + + public MappedItemHandler(Container inv, BiPredicate extraction_predicate, BiPredicate insertion_predicate) { + this(inv, IntStream.range(0, inv.getContainerSize()).boxed().collect(Collectors.toList()), extraction_predicate, insertion_predicate); + } + + public MappedItemHandler(Container inv) { + this(inv, (i, s) -> true, (i, s) -> true); + } + + @Override + public int hashCode() { + return inv_.hashCode(); + } + + @Override + public boolean equals(Object o) { + return (o == this) || ((o != null) && (getClass() == o.getClass()) && (inv_.equals(((MappedItemHandler) o).inv_))); + } + + // IItemHandler ----------------------------------------------------------------------------------------------- + + @Override + public int getSlots() { + return slot_map_.size(); + } + + @Override + @Nonnull + public ItemStack getStackInSlot(int slot) { + return (slot >= slot_map_.size()) ? ItemStack.EMPTY : inv_.getItem(slot_map_.get(slot)); + } + + @Override + public int getSlotLimit(int slot) { + return inv_.getMaxStackSize(); + } + + @Override + public boolean isItemValid(int slot, @Nonnull ItemStack stack) { + if (slot >= slot_map_.size()) return false; + slot = slot_map_.get(slot); + return insertion_predicate_.test(slot, stack) && inv_.canPlaceItem(slot, stack); + } + + @Override + @Nonnull + public ItemStack insertItem(int slot, @Nonnull ItemStack stack, boolean simulate) { + if (stack.isEmpty()) return ItemStack.EMPTY; + if (slot >= slot_map_.size()) return stack; + slot = slot_map_.get(slot); + if (!insertion_predicate_.test(slot, stack)) return stack; + if (!inv_.canPlaceItem(slot, stack)) return stack; + ItemStack sst = inv_.getItem(slot); + final int slot_limit = inv_.getMaxStackSize(); + if (!sst.isEmpty()) { + if (sst.getCount() >= Math.min(sst.getMaxStackSize(), slot_limit)) return stack; + if (!ItemHandlerHelper.canItemStacksStack(stack, sst)) return stack; + final int limit = Math.min(stack.getMaxStackSize(), slot_limit) - sst.getCount(); + if (stack.getCount() <= limit) { + if (!simulate) { + stack = stack.copy(); + stack.grow(sst.getCount()); + inv_.setItem(slot, stack); + inv_.setChanged(); + insertion_notifier_.accept(slot, sst); + } + return ItemStack.EMPTY; + } else { + stack = stack.copy(); + if (simulate) { + stack.shrink(limit); + } else { + final ItemStack diff = stack.split(limit); + sst.grow(diff.getCount()); + inv_.setItem(slot, sst); + inv_.setChanged(); + insertion_notifier_.accept(slot, diff); + } + return stack; + } + } else { + final int limit = Math.min(slot_limit, stack.getMaxStackSize()); + if (stack.getCount() >= limit) { + stack = stack.copy(); + final ItemStack ins = stack.split(limit); + if (!simulate) { + inv_.setItem(slot, ins); + inv_.setChanged(); + insertion_notifier_.accept(slot, ins.copy()); + } + if (stack.isEmpty()) { + stack = ItemStack.EMPTY; + } + return stack; + } else { + if (!simulate) { + inv_.setItem(slot, stack.copy()); + inv_.setChanged(); + insertion_notifier_.accept(slot, stack.copy()); + } + return ItemStack.EMPTY; + } + } + } + + @Override + public ItemStack extractItem(int slot, int amount, boolean simulate) { + if (amount <= 0) return ItemStack.EMPTY; + if (slot >= slot_map_.size()) return ItemStack.EMPTY; + slot = slot_map_.get(slot); + ItemStack stack = inv_.getItem(slot); + if (!extraction_predicate_.test(slot, stack)) return ItemStack.EMPTY; + if (simulate) { + stack = stack.copy(); + if (amount < stack.getCount()) stack.setCount(amount); + } else { + stack = inv_.removeItem(slot, Math.min(stack.getCount(), amount)); + inv_.setChanged(); + extraction_notifier_.accept(slot, stack.copy()); + } + return stack; + } + + // Factories -------------------------------------------------------------------------------------------- + + public static LazyOptional createGenericHandler(Container inv, BiPredicate extraction_predicate, BiPredicate insertion_predicate, BiConsumer insertion_notifier, BiConsumer extraction_notifier) { + return LazyOptional.of(() -> new MappedItemHandler(inv, extraction_predicate, insertion_predicate, insertion_notifier, extraction_notifier)); + } + + public static LazyOptional createGenericHandler(Container inv, BiPredicate extraction_predicate, BiPredicate insertion_predicate, BiConsumer insertion_notifier, BiConsumer extraction_notifier, List slot_map) { + return LazyOptional.of(() -> new MappedItemHandler(inv, slot_map, extraction_predicate, insertion_predicate, insertion_notifier, extraction_notifier)); + } + + public static LazyOptional createGenericHandler(Container inv, BiPredicate extraction_predicate, BiPredicate insertion_predicate, List slot_map) { + return LazyOptional.of(() -> new MappedItemHandler(inv, slot_map, extraction_predicate, insertion_predicate)); + } + + public static LazyOptional createGenericHandler(Container inv, BiPredicate extraction_predicate, BiPredicate insertion_predicate) { + return LazyOptional.of(() -> new MappedItemHandler(inv, extraction_predicate, insertion_predicate)); + } + + public static LazyOptional createGenericHandler(Container inv) { + return LazyOptional.of(() -> new MappedItemHandler(inv)); + } + + public static LazyOptional createExtractionHandler(Container inv, BiPredicate extraction_predicate, List slot_map) { + return LazyOptional.of(() -> new MappedItemHandler(inv, slot_map, extraction_predicate, (i, s) -> false)); + } + + public static LazyOptional createExtractionHandler(Container inv, BiPredicate extraction_predicate) { + return LazyOptional.of(() -> new MappedItemHandler(inv, extraction_predicate, (i, s) -> false)); + } + + public static LazyOptional createExtractionHandler(Container inv, Integer... slots) { + return LazyOptional.of(() -> new MappedItemHandler(inv, Arrays.asList(slots), (i, s) -> true, (i, s) -> false)); + } + + public static LazyOptional createExtractionHandler(Container inv) { + return LazyOptional.of(() -> new MappedItemHandler(inv, (i, s) -> true, (i, s) -> false)); + } + + public static LazyOptional createInsertionHandler(Container inv, BiPredicate insertion_predicate, List slot_map) { + return LazyOptional.of(() -> new MappedItemHandler(inv, slot_map, (i, s) -> false, insertion_predicate)); + } + + public static LazyOptional createInsertionHandler(Container inv, Integer... slots) { + return LazyOptional.of(() -> new MappedItemHandler(inv, Arrays.asList(slots), (i, s) -> false, (i, s) -> true)); + } + + public static LazyOptional createInsertionHandler(Container inv, BiPredicate insertion_predicate) { + return LazyOptional.of(() -> new MappedItemHandler(inv, (i, s) -> false, insertion_predicate)); + } + + public static LazyOptional createInsertionHandler(Container inv) { + return LazyOptional.of(() -> new MappedItemHandler(inv, (i, s) -> false, (i, s) -> true)); + } + } + + //-------------------------------------------------------------------------------------------------------------------- + + public static class InventoryRange implements Container, Iterable { + protected final Container inventory_; + protected final int offset_, size_, num_rows; + protected int max_stack_size_ = 64; + protected BiPredicate validator_ = (index, stack) -> true; + + public static InventoryRange fromPlayerHotbar(Player player) { + return new InventoryRange(player.getInventory(), 0, 9, 1); + } + + public static InventoryRange fromPlayerStorage(Player player) { + return new InventoryRange(player.getInventory(), 9, 27, 3); + } + + public static InventoryRange fromPlayerInventory(Player player) { + return new InventoryRange(player.getInventory(), 0, 36, 4); + } + + public InventoryRange(Container inventory, int offset, int size, int num_rows) { + this.inventory_ = inventory; + this.offset_ = Mth.clamp(offset, 0, inventory.getContainerSize() - 1); + this.size_ = Mth.clamp(size, 0, inventory.getContainerSize() - this.offset_); + this.num_rows = num_rows; + } + + public InventoryRange(Container inventory, int offset, int size) { + this(inventory, offset, size, 1); + } + + public InventoryRange(Container inventory) { + this(inventory, 0, inventory.getContainerSize(), 1); + } + + public final Container inventory() { + return inventory_; + } + + public final int size() { + return size_; + } + + public final int offset() { + return offset_; + } + + public final ItemStack get(int index) { + return inventory_.getItem(offset_ + index); + } + + public final void set(int index, ItemStack stack) { + inventory_.setItem(offset_ + index, stack); + } + + public final InventoryRange setValidator(BiPredicate validator) { + validator_ = validator; + return this; + } + + public final BiPredicate getValidator() { + return validator_; + } + + public final InventoryRange setMaxStackSize(int count) { + max_stack_size_ = Math.max(count, 1); + return this; + } + + // Container ------------------------------------------------------------------------------------------------------ + + @Override + public void clearContent() { + for (int i = 0; i < size_; ++i) setItem(i, ItemStack.EMPTY); + } + + @Override + public int getContainerSize() { + return size_; + } + + @Override + public boolean isEmpty() { + for (int i = 0; i < size_; ++i) + if (!inventory_.getItem(offset_ + i).isEmpty()) { + return false; + } + return true; + } + + @Override + public ItemStack getItem(int index) { + return inventory_.getItem(offset_ + index); + } + + @Override + public ItemStack removeItem(int index, int count) { + return inventory_.removeItem(offset_ + index, count); + } + + @Override + public ItemStack removeItemNoUpdate(int index) { + return inventory_.removeItemNoUpdate(offset_ + index); + } + + @Override + public void setItem(int index, ItemStack stack) { + inventory_.setItem(offset_ + index, stack); + } + + @Override + public int getMaxStackSize() { + return Math.min(max_stack_size_, inventory_.getMaxStackSize()); + } + + @Override + public void setChanged() { + inventory_.setChanged(); + } + + @Override + public boolean stillValid(Player player) { + return inventory_.stillValid(player); + } + + @Override + public void startOpen(Player player) { + inventory_.startOpen(player); + } + + @Override + public void stopOpen(Player player) { + inventory_.stopOpen(player); + } + + @Override + public boolean canPlaceItem(int index, ItemStack stack) { + return validator_.test(offset_ + index, stack) && inventory_.canPlaceItem(offset_ + index, stack); + } + + //------------------------------------------------------------------------------------------------------------------ + + /** + * Iterates using a function (slot, stack) -> bool until the function matches (returns true). + */ + public boolean iterate(BiPredicate fn) { + for (int i = 0; i < size_; ++i) { + if (fn.test(i, getItem(i))) { + return true; + } + } + return false; + } + + public boolean contains(ItemStack stack) { + for (int i = 0; i < size_; ++i) { + if (areItemStacksIdentical(stack, getItem(i))) { + return true; + } + } + return false; + } + + public int indexOf(ItemStack stack) { + for (int i = 0; i < size_; ++i) { + if (areItemStacksIdentical(stack, getItem(i))) { + return i; + } + } + return -1; + } + + public Optional find(BiFunction> fn) { + for (int i = 0; i < size_; ++i) { + Optional r = fn.apply(i, getItem(i)); + if (r.isPresent()) return r; + } + return Optional.empty(); + } + + public List collect(BiFunction> fn) { + List data = new ArrayList<>(); + for (int i = 0; i < size_; ++i) { + fn.apply(i, getItem(i)).ifPresent(data::add); + } + return data; + } + + public Stream stream() { + return java.util.stream.StreamSupport.stream(this.spliterator(), false); + } + + public Iterator iterator() { + return new InventoryRangeIterator(this); + } + + public static class InventoryRangeIterator implements Iterator { + private final InventoryRange parent_; + private int index = 0; + + public InventoryRangeIterator(InventoryRange range) { + parent_ = range; + } + + public boolean hasNext() { + return index < parent_.size_; + } + + public ItemStack next() { + if (index >= parent_.size_) throw new NoSuchElementException(); + return parent_.getItem(index++); + } + } + + //------------------------------------------------------------------------------------------------------------------ + + /** + * Returns the number of stacks that match the given stack with NBT. + */ + public int stackMatchCount(final ItemStack ref_stack) { + int n = 0; // ... std::accumulate() the old school way. + for (int i = 0; i < size_; ++i) { + if (areItemStacksIdentical(ref_stack, getItem(i))) ++n; + } + return n; + } + + public int totalMatchingItemCount(final ItemStack ref_stack) { + int n = 0; + for (int i = 0; i < size_; ++i) { + ItemStack stack = getItem(i); + if (areItemStacksIdentical(ref_stack, stack)) n += stack.getCount(); + } + return n; + } + + //------------------------------------------------------------------------------------------------------------------ + + /** + * Moves as much items from the stack to the slots in range [offset_, end_slot] of the inventory_, + * filling up existing stacks first, then (player inventory_ only) checks appropriate empty slots next + * to stacks that have that item already, and last uses any empty slot that can be found. + * Returns the stack that is still remaining in the referenced `stack`. + */ + public ItemStack insert(final ItemStack input_stack, boolean only_fillup, int limit, boolean reverse, boolean force_group_stacks) { + final ItemStack mvstack = input_stack.copy(); + //final int end_slot = offset_ + size; + if (mvstack.isEmpty()) return checked(mvstack); + int limit_left = (limit > 0) ? (Math.min(limit, mvstack.getMaxStackSize())) : (mvstack.getMaxStackSize()); + boolean[] matches = new boolean[size_]; + boolean[] empties = new boolean[size_]; + int num_matches = 0; + for (int i = 0; i < size_; ++i) { + final int sno = reverse ? (size_ - 1 - i) : (i); + final ItemStack stack = getItem(sno); + if (stack.isEmpty()) { + empties[sno] = true; + } else if (areItemStacksIdentical(stack, mvstack)) { + matches[sno] = true; + ++num_matches; + } + } + // first iteration: fillup existing stacks + for (int i = 0; i < size_; ++i) { + final int sno = reverse ? (size_ - 1 - i) : (i); + if ((empties[sno]) || (!matches[sno])) continue; + final ItemStack stack = getItem(sno); + int nmax = Math.min(limit_left, stack.getMaxStackSize() - stack.getCount()); + if (mvstack.getCount() <= nmax) { + stack.setCount(stack.getCount() + mvstack.getCount()); + setItem(sno, stack); + return ItemStack.EMPTY; + } else { + stack.grow(nmax); + mvstack.shrink(nmax); + setItem(sno, stack); + limit_left -= nmax; + } + } + if (only_fillup) return checked(mvstack); + if ((num_matches > 0) && ((force_group_stacks) || (inventory_ instanceof Inventory))) { + // second iteration: use appropriate empty slots, + // a) between + { + int insert_start = -1; + int insert_end = -1; + int i = 1; + for (; i < size_ - 1; ++i) { + final int sno = reverse ? (size_ - 1 - i) : (i); + if (insert_start < 0) { + if (matches[sno]) insert_start = sno; + } else if (matches[sno]) { + insert_end = sno; + } + } + for (i = insert_start; i < insert_end; ++i) { + final int sno = reverse ? (size_ - 1 - i) : (i); + if ((!empties[sno]) || (!canPlaceItem(sno, mvstack))) continue; + int nmax = Math.min(limit_left, mvstack.getCount()); + ItemStack moved = mvstack.copy(); + moved.setCount(nmax); + mvstack.shrink(nmax); + setItem(sno, moved); + return checked(mvstack); + } + } + // b) before/after + { + for (int i = 1; i < size_ - 1; ++i) { + final int sno = reverse ? (size_ - 1 - i) : (i); + if (!matches[sno]) continue; + int ii = (empties[sno - 1]) ? (sno - 1) : (empties[sno + 1] ? (sno + 1) : -1); + if ((ii >= 0) && (canPlaceItem(ii, mvstack))) { + int nmax = Math.min(limit_left, mvstack.getCount()); + ItemStack moved = mvstack.copy(); + moved.setCount(nmax); + mvstack.shrink(nmax); + setItem(ii, moved); + return checked(mvstack); + } + } + } + } + // third iteration: use any empty slots + for (int i = 0; i < size_; ++i) { + final int sno = reverse ? (size_ - 1 - i) : (i); + if ((!empties[sno]) || (!canPlaceItem(sno, mvstack))) continue; + int nmax = Math.min(limit_left, mvstack.getCount()); + ItemStack placed = mvstack.copy(); + placed.setCount(nmax); + mvstack.shrink(nmax); + setItem(sno, placed); + return checked(mvstack); + } + return checked(mvstack); + } + + public ItemStack insert(final ItemStack stack_to_move) { + return insert(stack_to_move, false, 0, false, true); + } + + public ItemStack insert(final int index, final ItemStack stack_to_move) { + if (stack_to_move.isEmpty()) return stack_to_move; + final ItemStack stack = getItem(index); + final int limit = Math.min(getMaxStackSize(), stack.getMaxStackSize()); + if (stack.isEmpty()) { + setItem(index, stack_to_move.copy()); + return ItemStack.EMPTY; + } else if ((stack.getCount() >= limit) || !areItemStacksIdentical(stack, stack_to_move)) { + return stack_to_move; + } else { + final int amount = Math.min(limit - stack.getCount(), stack_to_move.getCount()); + ItemStack remaining = stack_to_move.copy(); + remaining.shrink(amount); + stack.grow(amount); + return remaining.isEmpty() ? ItemStack.EMPTY : remaining; + } + } + + //------------------------------------------------------------------------------------------------------------------ + + /** + * Extracts maximum amount of items from the inventory_. + * The first non-empty stack defines the item. + */ + public ItemStack extract(int amount) { + return extract(amount, false); + } + + public ItemStack extract(int amount, boolean random) { + ItemStack out_stack = ItemStack.EMPTY; + int offset = random ? (int) (Math.random() * size_) : 0; + for (int k = 0; k < size_; ++k) { + int i = (offset + k) % size_; + final ItemStack stack = getItem(i); + if (stack.isEmpty()) continue; + if (out_stack.isEmpty()) { + if (stack.getCount() < amount) { + out_stack = stack; + setItem(i, ItemStack.EMPTY); + if (!out_stack.isStackable()) break; + amount -= out_stack.getCount(); + } else { + out_stack = stack.split(amount); + break; + } + } else if (areItemStacksIdentical(stack, out_stack)) { + if (stack.getCount() <= amount) { + out_stack.grow(stack.getCount()); + amount -= stack.getCount(); + setItem(i, ItemStack.EMPTY); + } else { + out_stack.grow(amount); + stack.shrink(amount); + if (stack.isEmpty()) setItem(i, ItemStack.EMPTY); + break; + } + } + } + if (!out_stack.isEmpty()) setChanged(); + return out_stack; + } + + /** + * Moves as much items from the slots in range [offset_, end_slot] of the inventory_ into a new stack. + * Implicitly shrinks the inventory_ stacks and the `request_stack`. + */ + public ItemStack extract(final ItemStack request_stack) { + if (request_stack.isEmpty()) return ItemStack.EMPTY; + List matches = new ArrayList<>(); + for (int i = 0; i < size_; ++i) { + final ItemStack stack = getItem(i); + if ((!stack.isEmpty()) && (areItemStacksIdentical(stack, request_stack))) { + if (stack.hasTag()) { + final CompoundTag nbt = stack.getOrCreateTag(); + int n = nbt.size(); + if ((n > 0) && (nbt.contains("Damage"))) --n; + if (n > 0) continue; + } + matches.add(stack); + } + } + matches.sort(Comparator.comparingInt(ItemStack::getCount)); + if (matches.isEmpty()) return ItemStack.EMPTY; + int n_left = request_stack.getCount(); + ItemStack fetched_stack = matches.get(0).split(n_left); + n_left -= fetched_stack.getCount(); + for (int i = 1; (i < matches.size()) && (n_left > 0); ++i) { + ItemStack stack = matches.get(i).split(n_left); + n_left -= stack.getCount(); + fetched_stack.grow(stack.getCount()); + } + return checked(fetched_stack); + } + + //------------------------------------------------------------------------------------------------------------------ + + /** + * Moves items from this inventory_ range to another. Returns true if something was moved + * (if the inventories should be marked dirty). + */ + public boolean move(int index, final InventoryRange target_range, boolean all_identical_stacks, boolean only_fillup, boolean reverse, boolean force_group_stacks) { + final ItemStack source_stack = getItem(index); + if (source_stack.isEmpty()) return false; + if (!all_identical_stacks) { + ItemStack remaining = target_range.insert(source_stack, only_fillup, 0, reverse, force_group_stacks); + setItem(index, remaining); + return (remaining.getCount() != source_stack.getCount()); + } else { + ItemStack remaining = source_stack.copy(); + setItem(index, ItemStack.EMPTY); + final ItemStack ref_stack = remaining.copy(); + ref_stack.setCount(ref_stack.getMaxStackSize()); + for (int i = size_; (i > 0) && (!remaining.isEmpty()); --i) { + remaining = target_range.insert(remaining, only_fillup, 0, reverse, force_group_stacks); + if (!remaining.isEmpty()) break; + remaining = this.extract(ref_stack); + } + if (!remaining.isEmpty()) { + setItem(index, remaining); // put back + } + return (remaining.getCount() != source_stack.getCount()); + } + } + + public boolean move(int index, final InventoryRange target_range) { + return move(index, target_range, false, false, false, true); + } + + /** + * Moves/clears the complete range to another range if possible. Returns true if something was moved + * (if the inventories should be marked dirty). + */ + public boolean move(final InventoryRange target_range, boolean only_fillup, boolean reverse, boolean force_group_stacks) { + boolean changed = false; + for (int i = 0; i < size_; ++i) + changed |= move(i, target_range, false, only_fillup, reverse, force_group_stacks); + return changed; + } + + public boolean move(final InventoryRange target_range, boolean only_fillup) { + return move(target_range, only_fillup, false, true); + } + + public boolean move(final InventoryRange target_range) { + return move(target_range, false, false, true); + } + + } + + //-------------------------------------------------------------------------------------------------------------------- + + public static class StorageInventory implements Container, Iterable { + protected final NonNullList stacks_; + protected final BlockEntity te_; + protected final int size_; + protected final int num_rows_; + protected int stack_limit_ = 64; + protected BiPredicate validator_ = (index, stack) -> true; + protected Consumer open_action_ = (player) -> { + }; + protected Consumer close_action_ = (player) -> { + }; + protected BiConsumer slot_set_action_ = (index, stack) -> { + }; + + public StorageInventory(BlockEntity te, int size) { + this(te, size, 1); + } + + public StorageInventory(BlockEntity te, int size, int num_rows) { + te_ = te; + size_ = Math.max(size, 1); + stacks_ = NonNullList.withSize(size_, ItemStack.EMPTY); + num_rows_ = Mth.clamp(num_rows, 1, size_); + } + + public CompoundTag save(CompoundTag nbt, String key) { + nbt.put(key, save(new CompoundTag(), false)); + return nbt; + } + + public CompoundTag save(CompoundTag nbt) { + return ContainerHelper.saveAllItems(nbt, stacks_); + } + + public CompoundTag save(CompoundTag nbt, boolean save_empty) { + return ContainerHelper.saveAllItems(nbt, stacks_, save_empty); + } + + public CompoundTag save(boolean save_empty) { + return save(new CompoundTag(), save_empty); + } + + public StorageInventory load(CompoundTag nbt, String key) { + if (!nbt.contains("key", Tag.TAG_COMPOUND)) { + stacks_.clear(); + return this; + } else { + return load(nbt.getCompound(key)); + } + } + + public StorageInventory load(CompoundTag nbt) { + stacks_.clear(); + ContainerHelper.loadAllItems(nbt, stacks_); + return this; + } + + public List stacks() { + return stacks_; + } + + public BlockEntity getBlockEntity() { + return te_; + } + + public StorageInventory setOpenAction(Consumer fn) { + open_action_ = fn; + return this; + } + + public StorageInventory setCloseAction(Consumer fn) { + close_action_ = fn; + return this; + } + + public StorageInventory setSlotChangeAction(BiConsumer fn) { + slot_set_action_ = fn; + return this; + } + + public StorageInventory setStackLimit(int max_slot_stack_size) { + stack_limit_ = Math.max(max_slot_stack_size, 1); + return this; + } + + public StorageInventory setValidator(BiPredicate validator) { + validator_ = validator; + return this; + } + + public BiPredicate getValidator() { + return validator_; + } + + // Iterable --------------------------------------------------------------------- + + public Iterator iterator() { + return stacks_.iterator(); + } + + public Stream stream() { + return stacks_.stream(); + } + + // Container ------------------------------------------------------------------------------ + + @Override + public int getContainerSize() { + return size_; + } + + @Override + public boolean isEmpty() { + for (ItemStack stack : stacks_) { + if (!stack.isEmpty()) return false; + } + return true; + } + + @Override + public ItemStack getItem(int index) { + return (index < size_) ? stacks_.get(index) : ItemStack.EMPTY; + } + + @Override + public ItemStack removeItem(int index, int count) { + return ContainerHelper.removeItem(stacks_, index, count); + } + + @Override + public ItemStack removeItemNoUpdate(int index) { + return ContainerHelper.takeItem(stacks_, index); + } + + @Override + public void setItem(int index, ItemStack stack) { + stacks_.set(index, stack); + if ((stack.getCount() != stacks_.get(index).getCount()) || !areItemStacksDifferent(stacks_.get(index), stack)) { + slot_set_action_.accept(index, stack); + } + } + + @Override + public int getMaxStackSize() { + return stack_limit_; + } + + @Override + public void setChanged() { + te_.setChanged(); + } + + @Override + public boolean stillValid(Player player) { + return ((te_.getLevel().getBlockEntity(te_.getBlockPos()) == te_)) && (te_.getBlockPos().distSqr(player.blockPosition()) < 64); + } + + @Override + public void startOpen(Player player) { + open_action_.accept(player); + } + + @Override + public void stopOpen(Player player) { + setChanged(); + close_action_.accept(player); + } + + @Override + public boolean canPlaceItem(int index, ItemStack stack) { + return validator_.test(index, stack); + } + + @Override + public void clearContent() { + stacks_.clear(); + setChanged(); + } + + } + + //-------------------------------------------------------------------------------------------------------------------- + + public static void give(Player entity, ItemStack stack) { + ItemHandlerHelper.giveItemToPlayer(entity, stack); + } + + public static void setItemInPlayerHand(Player player, InteractionHand hand, ItemStack stack) { + if (stack.isEmpty()) stack = ItemStack.EMPTY; + if (hand == InteractionHand.MAIN_HAND) { + player.getInventory().items.set(player.getInventory().selected, stack); + } else { + player.getInventory().offhand.set(0, stack); + } + } + + //-------------------------------------------------------------------------------------------------------------------- + + public static Container readNbtStacks(CompoundTag nbt, String key, Container target) { + NonNullList stacks = Inventories.readNbtStacks(nbt, key, target.getContainerSize()); + for (int i = 0; i < stacks.size(); ++i) target.setItem(i, stacks.get(i)); + return target; + } + + public static NonNullList readNbtStacks(CompoundTag nbt, String key, int size) { + NonNullList stacks = NonNullList.withSize(size, ItemStack.EMPTY); + if ((nbt == null) || (!nbt.contains(key, Tag.TAG_COMPOUND))) return stacks; + CompoundTag stacknbt = nbt.getCompound(key); + ContainerHelper.loadAllItems(stacknbt, stacks); + return stacks; + } + + public static NonNullList readNbtStacks(CompoundTag nbt, int size) { + NonNullList stacks = NonNullList.withSize(size, ItemStack.EMPTY); + if ((nbt == null) || (!nbt.contains("Items", Tag.TAG_LIST))) return stacks; + ContainerHelper.loadAllItems(nbt, stacks); + return stacks; + } + + public static CompoundTag writeNbtStacks(CompoundTag nbt, String key, NonNullList stacks, boolean omit_trailing_empty) { + CompoundTag stacknbt = new CompoundTag(); + if (omit_trailing_empty) { + for (int i = stacks.size() - 1; i >= 0; --i) { + if (!stacks.get(i).isEmpty()) break; + stacks.remove(i); + } + } + ContainerHelper.saveAllItems(stacknbt, stacks); + if (nbt == null) nbt = new CompoundTag(); + nbt.put(key, stacknbt); + return nbt; + } + + public static CompoundTag writeNbtStacks(CompoundTag nbt, String key, NonNullList stacks) { + return writeNbtStacks(nbt, key, stacks, false); + } + + //-------------------------------------------------------------------------------------------------------------------- + + public static void dropStack(Level world, Vec3 pos, ItemStack stack, Vec3 velocity, double position_noise, double velocity_noise) { + if (stack.isEmpty()) return; + if (position_noise > 0) { + position_noise = Math.min(position_noise, 0.8); + pos = pos.add( + position_noise * (world.getRandom().nextDouble() - .5), + position_noise * (world.getRandom().nextDouble() - .5), + position_noise * (world.getRandom().nextDouble() - .5) + ); + } + if (velocity_noise > 0) { + velocity_noise = Math.min(velocity_noise, 1.0); + velocity = velocity.add( + (velocity_noise) * (world.getRandom().nextDouble() - .5), + (velocity_noise) * (world.getRandom().nextDouble() - .5), + (velocity_noise) * (world.getRandom().nextDouble() - .5) + ); + } + ItemEntity e = new ItemEntity(world, pos.x, pos.y, pos.z, stack); + e.setDeltaMovement((float) velocity.x, (float) velocity.y, (float) velocity.z); + e.setDefaultPickUpDelay(); + world.addFreshEntity(e); + } + + public static void dropStack(Level world, Vec3 pos, ItemStack stack, Vec3 velocity) { + dropStack(world, pos, stack, velocity, 0.3, 0.2); + } + + public static void dropStack(Level world, Vec3 pos, ItemStack stack) { + dropStack(world, pos, stack, Vec3.ZERO, 0.3, 0.2); + } + +} diff --git a/src/main/java/dev/zontreck/libzontreck/edlibmc/Networking.java b/src/main/java/dev/zontreck/libzontreck/edlibmc/Networking.java new file mode 100644 index 0000000..67a3540 --- /dev/null +++ b/src/main/java/dev/zontreck/libzontreck/edlibmc/Networking.java @@ -0,0 +1,409 @@ +/* + * @file Networking.java + * @author Stefan Wilhelm (wile) + * @copyright (C) 2020 Stefan Wilhelm + * @license MIT (see https://opensource.org/licenses/MIT) + * + * Main client/server message handling. + */ +package dev.zontreck.libzontreck.edlibmc; + +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraftforge.common.util.FakePlayer; +import net.minecraftforge.network.NetworkDirection; +import net.minecraftforge.network.NetworkEvent; +import net.minecraftforge.network.NetworkRegistry; +import net.minecraftforge.network.simple.SimpleChannel; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Supplier; + + +public class Networking { + private static final String PROTOCOL = "1"; + private static SimpleChannel DEFAULT_CHANNEL; + + public static void init(String modid) { + DEFAULT_CHANNEL = NetworkRegistry.ChannelBuilder + .named(new ResourceLocation(modid, "default_ch")) + .clientAcceptedVersions(PROTOCOL::equals).serverAcceptedVersions(PROTOCOL::equals).networkProtocolVersion(() -> PROTOCOL) + .simpleChannel(); + int discr = -1; + DEFAULT_CHANNEL.registerMessage(++discr, PacketTileNotifyClientToServer.class, PacketTileNotifyClientToServer::compose, PacketTileNotifyClientToServer::parse, PacketTileNotifyClientToServer.Handler::handle); + DEFAULT_CHANNEL.registerMessage(++discr, PacketTileNotifyServerToClient.class, PacketTileNotifyServerToClient::compose, PacketTileNotifyServerToClient::parse, PacketTileNotifyServerToClient.Handler::handle); + DEFAULT_CHANNEL.registerMessage(++discr, PacketContainerSyncClientToServer.class, PacketContainerSyncClientToServer::compose, PacketContainerSyncClientToServer::parse, PacketContainerSyncClientToServer.Handler::handle); + DEFAULT_CHANNEL.registerMessage(++discr, PacketContainerSyncServerToClient.class, PacketContainerSyncServerToClient::compose, PacketContainerSyncServerToClient::parse, PacketContainerSyncServerToClient.Handler::handle); + DEFAULT_CHANNEL.registerMessage(++discr, PacketNbtNotifyClientToServer.class, PacketNbtNotifyClientToServer::compose, PacketNbtNotifyClientToServer::parse, PacketNbtNotifyClientToServer.Handler::handle); + DEFAULT_CHANNEL.registerMessage(++discr, PacketNbtNotifyServerToClient.class, PacketNbtNotifyServerToClient::compose, PacketNbtNotifyServerToClient::parse, PacketNbtNotifyServerToClient.Handler::handle); + DEFAULT_CHANNEL.registerMessage(++discr, OverlayTextMessage.class, OverlayTextMessage::compose, OverlayTextMessage::parse, OverlayTextMessage.Handler::handle); + } + + //-------------------------------------------------------------------------------------------------------------------- + // Tile entity notifications + //-------------------------------------------------------------------------------------------------------------------- + + public interface IPacketTileNotifyReceiver { + default void onServerPacketReceived(CompoundTag nbt) { + } + + default void onClientPacketReceived(Player player, CompoundTag nbt) { + } + } + + public static class PacketTileNotifyClientToServer { + CompoundTag nbt = null; + BlockPos pos = BlockPos.ZERO; + + public static void sendToServer(BlockPos pos, CompoundTag nbt) { + if ((pos != null) && (nbt != null)) + DEFAULT_CHANNEL.sendToServer(new PacketTileNotifyClientToServer(pos, nbt)); + } + + public static void sendToServer(BlockEntity te, CompoundTag nbt) { + if ((te != null) && (nbt != null)) + DEFAULT_CHANNEL.sendToServer(new PacketTileNotifyClientToServer(te, nbt)); + } + + public PacketTileNotifyClientToServer() { + } + + public PacketTileNotifyClientToServer(BlockPos pos, CompoundTag nbt) { + this.nbt = nbt; + this.pos = pos; + } + + public PacketTileNotifyClientToServer(BlockEntity te, CompoundTag nbt) { + this.nbt = nbt; + pos = te.getBlockPos(); + } + + public static PacketTileNotifyClientToServer parse(final FriendlyByteBuf buf) { + return new PacketTileNotifyClientToServer(buf.readBlockPos(), buf.readNbt()); + } + + public static void compose(final PacketTileNotifyClientToServer pkt, final FriendlyByteBuf buf) { + buf.writeBlockPos(pkt.pos); + buf.writeNbt(pkt.nbt); + } + + @SuppressWarnings("all") + public static class Handler { + public static void handle(final PacketTileNotifyClientToServer pkt, final Supplier ctx) { + ctx.get().enqueueWork(() -> { + Player player = ctx.get().getSender(); + if (player == null) return; + Level world = player.level(); + final BlockEntity te = world.getBlockEntity(pkt.pos); + if (!(te instanceof IPacketTileNotifyReceiver)) return; + ((IPacketTileNotifyReceiver) te).onClientPacketReceived(ctx.get().getSender(), pkt.nbt); + }); + ctx.get().setPacketHandled(true); + } + } + } + + public static class PacketTileNotifyServerToClient { + CompoundTag nbt = null; + BlockPos pos = BlockPos.ZERO; + + public static void sendToPlayer(Player player, BlockEntity te, CompoundTag nbt) { + if ((!(player instanceof ServerPlayer)) || (player instanceof FakePlayer) || (te == null) || (nbt == null)) + return; + DEFAULT_CHANNEL.sendTo(new PacketTileNotifyServerToClient(te, nbt), ((ServerPlayer) player).connection.connection, NetworkDirection.PLAY_TO_CLIENT); + } + + public static void sendToPlayers(BlockEntity te, CompoundTag nbt) { + if (te == null || te.getLevel() == null) return; + for (Player player : te.getLevel().players()) sendToPlayer(player, te, nbt); + } + + public PacketTileNotifyServerToClient() { + } + + public PacketTileNotifyServerToClient(BlockPos pos, CompoundTag nbt) { + this.nbt = nbt; + this.pos = pos; + } + + public PacketTileNotifyServerToClient(BlockEntity te, CompoundTag nbt) { + this.nbt = nbt; + pos = te.getBlockPos(); + } + + public static PacketTileNotifyServerToClient parse(final FriendlyByteBuf buf) { + return new PacketTileNotifyServerToClient(buf.readBlockPos(), buf.readNbt()); + } + + public static void compose(final PacketTileNotifyServerToClient pkt, final FriendlyByteBuf buf) { + buf.writeBlockPos(pkt.pos); + buf.writeNbt(pkt.nbt); + } + + public static class Handler { + public static void handle(final PacketTileNotifyServerToClient pkt, final Supplier ctx) { + ctx.get().enqueueWork(() -> { + Level world = SidedProxy.getWorldClientSide(); + if (world == null) return; + final BlockEntity te = world.getBlockEntity(pkt.pos); + if (!(te instanceof IPacketTileNotifyReceiver)) return; + ((IPacketTileNotifyReceiver) te).onServerPacketReceived(pkt.nbt); + }); + ctx.get().setPacketHandled(true); + } + } + } + + //-------------------------------------------------------------------------------------------------------------------- + // (GUI) Container synchronization + //-------------------------------------------------------------------------------------------------------------------- + + public interface INetworkSynchronisableContainer { + void onServerPacketReceived(int windowId, CompoundTag nbt); + + void onClientPacketReceived(int windowId, Player player, CompoundTag nbt); + } + + public static class PacketContainerSyncClientToServer { + int id = -1; + CompoundTag nbt = null; + + public static void sendToServer(int windowId, CompoundTag nbt) { + if (nbt != null) DEFAULT_CHANNEL.sendToServer(new PacketContainerSyncClientToServer(windowId, nbt)); + } + + public static void sendToServer(AbstractContainerMenu container, CompoundTag nbt) { + if (nbt != null) + DEFAULT_CHANNEL.sendToServer(new PacketContainerSyncClientToServer(container.containerId, nbt)); + } + + public PacketContainerSyncClientToServer() { + } + + public PacketContainerSyncClientToServer(int id, CompoundTag nbt) { + this.nbt = nbt; + this.id = id; + } + + public static PacketContainerSyncClientToServer parse(final FriendlyByteBuf buf) { + return new PacketContainerSyncClientToServer(buf.readInt(), buf.readNbt()); + } + + public static void compose(final PacketContainerSyncClientToServer pkt, final FriendlyByteBuf buf) { + buf.writeInt(pkt.id); + buf.writeNbt(pkt.nbt); + } + + public static class Handler { + public static void handle(final PacketContainerSyncClientToServer pkt, final Supplier ctx) { + ctx.get().enqueueWork(() -> { + Player player = ctx.get().getSender(); + if ((player == null) || !(player.containerMenu instanceof INetworkSynchronisableContainer)) return; + if (player.containerMenu.containerId != pkt.id) return; + ((INetworkSynchronisableContainer) player.containerMenu).onClientPacketReceived(pkt.id, player, pkt.nbt); + }); + ctx.get().setPacketHandled(true); + } + } + } + + public static class PacketContainerSyncServerToClient { + int id = -1; + CompoundTag nbt = null; + + public static void sendToPlayer(Player player, int windowId, CompoundTag nbt) { + if ((!(player instanceof ServerPlayer)) || (player instanceof FakePlayer) || (nbt == null)) return; + DEFAULT_CHANNEL.sendTo(new PacketContainerSyncServerToClient(windowId, nbt), ((ServerPlayer) player).connection.connection, NetworkDirection.PLAY_TO_CLIENT); + } + + public static void sendToPlayer(Player player, AbstractContainerMenu container, CompoundTag nbt) { + if ((!(player instanceof ServerPlayer)) || (player instanceof FakePlayer) || (nbt == null)) return; + DEFAULT_CHANNEL.sendTo(new PacketContainerSyncServerToClient(container.containerId, nbt), ((ServerPlayer) player).connection.connection, NetworkDirection.PLAY_TO_CLIENT); + } + + public static + void sendToListeners(Level world, C container, CompoundTag nbt) { + for (Player player : world.players()) { + if (player.containerMenu.containerId != container.containerId) continue; + sendToPlayer(player, container.containerId, nbt); + } + } + + public PacketContainerSyncServerToClient() { + } + + public PacketContainerSyncServerToClient(int id, CompoundTag nbt) { + this.nbt = nbt; + this.id = id; + } + + public static PacketContainerSyncServerToClient parse(final FriendlyByteBuf buf) { + return new PacketContainerSyncServerToClient(buf.readInt(), buf.readNbt()); + } + + public static void compose(final PacketContainerSyncServerToClient pkt, final FriendlyByteBuf buf) { + buf.writeInt(pkt.id); + buf.writeNbt(pkt.nbt); + } + + public static class Handler { + public static void handle(final PacketContainerSyncServerToClient pkt, final Supplier ctx) { + ctx.get().enqueueWork(() -> { + Player player = SidedProxy.getPlayerClientSide(); + if ((player == null) || !(player.containerMenu instanceof INetworkSynchronisableContainer)) return; + if (player.containerMenu.containerId != pkt.id) return; + ((INetworkSynchronisableContainer) player.containerMenu).onServerPacketReceived(pkt.id, pkt.nbt); + }); + ctx.get().setPacketHandled(true); + } + } + } + + //-------------------------------------------------------------------------------------------------------------------- + // World notifications + //-------------------------------------------------------------------------------------------------------------------- + + public static class PacketNbtNotifyClientToServer { + public static final Map> handlers = new HashMap<>(); + final CompoundTag nbt; + + public static void sendToServer(CompoundTag nbt) { + if (nbt != null) DEFAULT_CHANNEL.sendToServer(new PacketNbtNotifyClientToServer(nbt)); + } + + public PacketNbtNotifyClientToServer(CompoundTag nbt) { + this.nbt = nbt; + } + + public static PacketNbtNotifyClientToServer parse(final FriendlyByteBuf buf) { + return new PacketNbtNotifyClientToServer(buf.readNbt()); + } + + public static void compose(final PacketNbtNotifyClientToServer pkt, final FriendlyByteBuf buf) { + buf.writeNbt(pkt.nbt); + } + + public static class Handler { + public static void handle(final PacketNbtNotifyClientToServer pkt, final Supplier ctx) { + ctx.get().enqueueWork(() -> { + final ServerPlayer player = ctx.get().getSender(); + if (player == null) return; + final String hnd = pkt.nbt.getString("hnd"); + if (hnd.isEmpty()) return; + if (handlers.containsKey(hnd)) handlers.get(hnd).accept(player, pkt.nbt); + }); + ctx.get().setPacketHandled(true); + } + } + } + + public static class PacketNbtNotifyServerToClient { + public static final Map> handlers = new HashMap<>(); + final CompoundTag nbt; + + public static void sendToPlayer(Player player, CompoundTag nbt) { + if ((!(player instanceof ServerPlayer)) || (player instanceof FakePlayer) || (nbt == null)) return; + DEFAULT_CHANNEL.sendTo(new PacketNbtNotifyServerToClient(nbt), ((ServerPlayer) player).connection.connection, NetworkDirection.PLAY_TO_CLIENT); + } + + public static void sendToPlayers(Level world, CompoundTag nbt) { + for (Player player : world.players()) sendToPlayer(player, nbt); + } + + public PacketNbtNotifyServerToClient(CompoundTag nbt) { + this.nbt = nbt; + } + + public static PacketNbtNotifyServerToClient parse(final FriendlyByteBuf buf) { + return new PacketNbtNotifyServerToClient(buf.readNbt()); + } + + public static void compose(final PacketNbtNotifyServerToClient pkt, final FriendlyByteBuf buf) { + buf.writeNbt(pkt.nbt); + } + + public static class Handler { + public static void handle(final PacketNbtNotifyServerToClient pkt, final Supplier ctx) { + ctx.get().enqueueWork(() -> { + final String hnd = pkt.nbt.getString("hnd"); + if (hnd.isEmpty()) return; + if (handlers.containsKey(hnd)) handlers.get(hnd).accept(pkt.nbt); + }); + ctx.get().setPacketHandled(true); + } + } + } + + //-------------------------------------------------------------------------------------------------------------------- + // Main window GUI text message + //-------------------------------------------------------------------------------------------------------------------- + + public static class OverlayTextMessage { + public static final int DISPLAY_TIME_MS = 3000; + private static BiConsumer handler_ = null; + private final Component data_; + private int delay_ = DISPLAY_TIME_MS; + + private Component data() { + return data_; + } + + private int delay() { + return delay_; + } + + public static void setHandler(BiConsumer handler) { + if (handler_ == null) handler_ = handler; + } + + public static void sendToPlayer(Player player, Component message, int delay) { + if ((!(player instanceof ServerPlayer)) || (player instanceof FakePlayer) || Auxiliaries.isEmpty(message)) + return; + DEFAULT_CHANNEL.sendTo(new OverlayTextMessage(message, delay), ((ServerPlayer) player).connection.connection, NetworkDirection.PLAY_TO_CLIENT); + } + + public OverlayTextMessage() { + data_ = Component.translatable("[unset]"); + } + + public OverlayTextMessage(final Component tct, int delay) { + data_ = tct.copy(); + delay_ = delay; + } + + public static OverlayTextMessage parse(final FriendlyByteBuf buf) { + try { + return new OverlayTextMessage(buf.readComponent(), DISPLAY_TIME_MS); + } catch (Throwable e) { + return new OverlayTextMessage(Component.translatable("[incorrect translation]"), DISPLAY_TIME_MS); + } + } + + public static void compose(final OverlayTextMessage pkt, final FriendlyByteBuf buf) { + try { + buf.writeComponent(pkt.data()); + } catch (Throwable e) { + Auxiliaries.logger().error("OverlayTextMessage.toBytes() failed: " + e); + } + } + + public static class Handler { + public static void handle(final OverlayTextMessage pkt, final Supplier ctx) { + if (handler_ != null) ctx.get().enqueueWork(() -> handler_.accept(pkt.data(), pkt.delay())); + ctx.get().setPacketHandled(true); + } + } + } + +} diff --git a/src/main/java/dev/zontreck/libzontreck/edlibmc/OptionalRecipeCondition.java b/src/main/java/dev/zontreck/libzontreck/edlibmc/OptionalRecipeCondition.java new file mode 100644 index 0000000..681daff --- /dev/null +++ b/src/main/java/dev/zontreck/libzontreck/edlibmc/OptionalRecipeCondition.java @@ -0,0 +1,191 @@ +/* + * @file OptionalRecipeCondition.java + * @author Stefan Wilhelm (wile) + * @copyright (C) 2020 Stefan Wilhelm + * @license MIT (see https://opensource.org/licenses/MIT) + * + * Recipe condition to enable opt'ing out JSON based recipes. + */ +package dev.zontreck.libzontreck.edlibmc; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.GsonHelper; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.block.Block; +import net.minecraftforge.common.crafting.conditions.ICondition; +import net.minecraftforge.common.crafting.conditions.IConditionSerializer; +import net.minecraftforge.registries.ForgeRegistries; +import net.minecraftforge.registries.IForgeRegistry; +import org.slf4j.Logger; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + + +public class OptionalRecipeCondition implements ICondition { + private static ResourceLocation NAME; + + private final List all_required; + private final List any_missing; + private final List all_required_tags; + private final List any_missing_tags; + private final @Nullable ResourceLocation result; + private final boolean result_is_tag; + private final boolean experimental; + + private static boolean with_experimental = false; + private static boolean without_recipes = false; + private static Predicate block_optouts = (block) -> false; + private static Predicate item_optouts = (item) -> false; + + public static void init(String modid, Logger logger) { + NAME = new ResourceLocation(modid, "optional"); + } + + public static void on_config(boolean enable_experimental, boolean disable_all_recipes, + Predicate block_optout_provider, + Predicate item_optout_provider) { + with_experimental = enable_experimental; + without_recipes = disable_all_recipes; + block_optouts = block_optout_provider; + item_optouts = item_optout_provider; + } + + public OptionalRecipeCondition(ResourceLocation result, List required, List missing, List required_tags, List missing_tags, boolean isexperimental, boolean result_is_tag) { + all_required = required; + any_missing = missing; + all_required_tags = required_tags; + any_missing_tags = missing_tags; + this.result = result; + this.result_is_tag = result_is_tag; + experimental = isexperimental; + } + + @Override + public ResourceLocation getID() { + return NAME; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Optional recipe, all-required: ["); + for (ResourceLocation e : all_required) sb.append(e.toString()).append(","); + for (ResourceLocation e : all_required_tags) sb.append("#").append(e.toString()).append(","); + sb.delete(sb.length() - 1, sb.length()).append("], any-missing: ["); + for (ResourceLocation e : any_missing) sb.append(e.toString()).append(","); + for (ResourceLocation e : any_missing_tags) sb.append("#").append(e.toString()).append(","); + sb.delete(sb.length() - 1, sb.length()).append("]"); + if (experimental) sb.append(" EXPERIMENTAL"); + return sb.toString(); + } + + @Override + public boolean test(IContext context) { + if (without_recipes) return false; + if ((experimental) && (!with_experimental)) return false; + final IForgeRegistry item_registry = ForgeRegistries.ITEMS; + //final Collection item_tags = SerializationTags.getInstance().getOrEmpty(Registry.ITEM_REGISTRY).getAvailableTags(); + if (result != null) { + boolean item_registered = item_registry.containsKey(result); + if (!item_registered) return false; // required result not registered + if (item_optouts.test(item_registry.getValue(result))) return false; + if (ForgeRegistries.BLOCKS.containsKey(result) && block_optouts.test(ForgeRegistries.BLOCKS.getValue(result))) + return false; + } + if (!all_required.isEmpty()) { + for (ResourceLocation rl : all_required) { + if (!item_registry.containsKey(rl)) return false; + } + } + if (!all_required_tags.isEmpty()) { + for (ResourceLocation rl : all_required_tags) { + if (item_registry.tags().getTagNames().noneMatch(tk -> tk.location().equals(rl))) + return false; // if(!item_tags.contains(rl)) return false; + } + } + if (!any_missing.isEmpty()) { + for (ResourceLocation rl : any_missing) { + if (!item_registry.containsKey(rl)) return true; + } + return false; + } + if (!any_missing_tags.isEmpty()) { + for (ResourceLocation rl : any_missing_tags) { + if (item_registry.tags().getTagNames().noneMatch(tk -> tk.location().equals(rl))) + return true; // if(!item_tags.contains(rl)) return true; + } + return false; + } + return true; + } + + public static class Serializer implements IConditionSerializer { + public static final Serializer INSTANCE = new Serializer(); + + @Override + public ResourceLocation getID() { + return OptionalRecipeCondition.NAME; + } + + @Override + public void write(JsonObject json, OptionalRecipeCondition condition) { + JsonArray required = new JsonArray(); + JsonArray missing = new JsonArray(); + for (ResourceLocation e : condition.all_required) required.add(e.toString()); + for (ResourceLocation e : condition.any_missing) missing.add(e.toString()); + json.add("required", required); + json.add("missing", missing); + if (condition.result != null) { + json.addProperty("result", (condition.result_is_tag ? "#" : "") + condition.result); + } + } + + @Override + public OptionalRecipeCondition read(JsonObject json) { + List required = new ArrayList<>(); + List missing = new ArrayList<>(); + List required_tags = new ArrayList<>(); + List missing_tags = new ArrayList<>(); + ResourceLocation result = null; + boolean experimental = false; + boolean result_is_tag = false; + if (json.has("result")) { + String s = json.get("result").getAsString(); + if (s.startsWith("#")) { + result = new ResourceLocation(s.substring(1)); + result_is_tag = true; + } else { + result = new ResourceLocation(s); + } + } + if (json.has("required")) { + for (JsonElement e : GsonHelper.getAsJsonArray(json, "required")) { + String s = e.getAsString(); + if (s.startsWith("#")) { + required_tags.add(new ResourceLocation(s.substring(1))); + } else { + required.add(new ResourceLocation(s)); + } + } + } + if (json.has("missing")) { + for (JsonElement e : GsonHelper.getAsJsonArray(json, "missing")) { + String s = e.getAsString(); + if (s.startsWith("#")) { + missing_tags.add(new ResourceLocation(s.substring(1))); + } else { + missing.add(new ResourceLocation(s)); + } + } + } + if (json.has("experimental")) experimental = json.get("experimental").getAsBoolean(); + return new OptionalRecipeCondition(result, required, missing, required_tags, missing_tags, experimental, result_is_tag); + } + } +} diff --git a/src/main/java/dev/zontreck/libzontreck/edlibmc/Overlay.java b/src/main/java/dev/zontreck/libzontreck/edlibmc/Overlay.java new file mode 100644 index 0000000..43f71cc --- /dev/null +++ b/src/main/java/dev/zontreck/libzontreck/edlibmc/Overlay.java @@ -0,0 +1,165 @@ +/* + * @file Overlay.java + * @author Stefan Wilhelm (wile) + * @copyright (C) 2020 Stefan Wilhelm + * @license MIT (see https://opensource.org/licenses/MIT) + * + * Renders status messages in one line. + */ +package dev.zontreck.libzontreck.edlibmc; + +import com.mojang.blaze3d.platform.Window; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.util.Mth; +import net.minecraft.util.Tuple; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.LightLayer; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.fml.DistExecutor; + +import javax.annotation.Nullable; +import java.util.Optional; + + +public class Overlay { + public static void show(Player player, final Component message) { + Networking.OverlayTextMessage.sendToPlayer(player, message, 3000); + } + + public static void show(Player player, final Component message, int delay) { + Networking.OverlayTextMessage.sendToPlayer(player, message, delay); + } + + public static void show(BlockState state, BlockPos pos) { + show(state, pos, 100); + } + + public static void show(BlockState state, BlockPos pos, int displayTimeoutMs) { + DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> (() -> TextOverlayGui.show(state, pos, displayTimeoutMs))); + } // Only called when client side + + // ----------------------------------------------------------------------------- + // Client side handler + // ----------------------------------------------------------------------------- + + @OnlyIn(Dist.CLIENT) + public static class TextOverlayGui extends Screen { + public static final TextOverlayGui INSTANCE = new TextOverlayGui(); + private static final Component EMPTY_TEXT = Component.literal(""); + private static final BlockState EMPTY_STATE = null; + private static double overlay_y_ = 0.75; + private static int text_color_ = 0x00ffaa00; + private static int border_color_ = 0xaa333333; + private static int background_color1_ = 0xaa333333; + private static int background_color2_ = 0xaa444444; + private static long text_deadline_ = 0; + private static Component text_ = EMPTY_TEXT; + private static long state_deadline_ = 0; + private static @Nullable BlockState state_ = EMPTY_STATE; + private static BlockPos pos_ = BlockPos.ZERO; + + public static void on_config(double overlay_y) { + on_config(overlay_y, 0x00ffaa00, 0xaa333333, 0xaa333333, 0xaa444444); + } + + public static void on_config(double overlay_y, int text_color, int border_color, int background_color1, int background_color2) { + overlay_y_ = overlay_y; + text_color_ = text_color; + border_color_ = border_color; + background_color1_ = background_color1; + background_color2_ = background_color2; + } + + public static synchronized Component text() { + return text_; + } + + public static synchronized long deadline() { + return text_deadline_; + } + + public static synchronized void hide() { + text_deadline_ = 0; + text_ = EMPTY_TEXT; + } + + public static synchronized void show(Component s, int displayTimeoutMs) { + text_ = (s == null) ? (EMPTY_TEXT) : (s.copy()); + text_deadline_ = System.currentTimeMillis() + displayTimeoutMs; + } + + public static synchronized void show(String s, int displayTimeoutMs) { + text_ = ((s == null) || (s.isEmpty())) ? (EMPTY_TEXT) : (Component.literal(s)); + text_deadline_ = System.currentTimeMillis() + displayTimeoutMs; + } + + public static synchronized void show(BlockState state, BlockPos pos, int displayTimeoutMs) { + pos_ = new BlockPos(pos); + state_ = state; + state_deadline_ = System.currentTimeMillis() + displayTimeoutMs; + } + + private static synchronized Optional> state_pos() { + return ((state_deadline_ < System.currentTimeMillis()) || (state_ == EMPTY_STATE)) ? Optional.empty() : Optional.of(new Tuple<>(state_, pos_)); + } + + TextOverlayGui() { + super(Component.literal("")); + } + + public void onRenderGui(final GuiGraphics mxs) { + if (deadline() < System.currentTimeMillis()) return; + if (text() == EMPTY_TEXT) return; + String txt = text().getString(); + if (txt.isEmpty()) return; + final net.minecraft.client.Minecraft mc = net.minecraft.client.Minecraft.getInstance(); + final Window win = mc.getWindow(); + final Font fr = mc.font; + final boolean was_unicode = fr.isBidirectional(); + final int cx = win.getGuiScaledWidth() / 2; + final int cy = (int) (win.getGuiScaledHeight() * overlay_y_); + final int w = fr.width(txt); + final int h = fr.lineHeight; + mxs.fillGradient(cx - (w / 2) - 3, cy - 2, cx + (w / 2) + 2, cy + h + 2, 0xaa333333, 0xaa444444); + mxs.hLine(cx - (w / 2) - 3, cx + (w / 2) + 2, cy - 2, 0xaa333333); + mxs.hLine(cx - (w / 2) - 3, cx + (w / 2) + 2, cy + h + 2, 0xaa333333); + mxs.vLine(cx - (w / 2) - 3, cy - 2, cy + h + 2, 0xaa333333); + mxs.vLine(cx + (w / 2) + 2, cy - 2, cy + h + 2, 0xaa333333); + mxs.drawCenteredString(fr, text(), cx, cy + 1, 0x00ffaa00); + } + + @SuppressWarnings("deprecation") + public void onRenderWorldOverlay(final com.mojang.blaze3d.vertex.PoseStack mxs, final double partialTick) { + final Optional> sp = state_pos(); + if (sp.isEmpty()) return; + final ClientLevel world = Minecraft.getInstance().level; + final LocalPlayer player = Minecraft.getInstance().player; + if ((player == null) || (world == null)) return; + final BlockState state = sp.get().getA(); + final BlockPos pos = sp.get().getB(); + final int light = (world.hasChunkAt(pos)) ? LightTexture.pack(world.getBrightness(LightLayer.BLOCK, pos), world.getBrightness(LightLayer.SKY, pos)) : LightTexture.pack(15, 15); + final MultiBufferSource buffer = Minecraft.getInstance().renderBuffers().bufferSource(); + final double px = Mth.lerp(partialTick, player.xo, player.getX()); + final double py = Mth.lerp(partialTick, player.yo, player.getY()); + final double pz = Mth.lerp(partialTick, player.zo, player.getZ()); + mxs.pushPose(); + mxs.translate((pos.getX() - px), (pos.getY() - py - player.getEyeHeight()), (pos.getZ() - pz)); + Minecraft.getInstance().getBlockRenderer().renderSingleBlock(state, mxs, buffer, light, OverlayTexture.NO_OVERLAY); + mxs.popPose(); + } + + } + +} diff --git a/src/main/java/dev/zontreck/libzontreck/edlibmc/README.md b/src/main/java/dev/zontreck/libzontreck/edlibmc/README.md new file mode 100644 index 0000000..f4bf684 --- /dev/null +++ b/src/main/java/dev/zontreck/libzontreck/edlibmc/README.md @@ -0,0 +1,4 @@ +# Engineer's Decor LibMC +____________ + +As Engineer's Decor has been abandoned and discontinued, i've adopted the LibMC, and split the mod up among my various mods. Engineer's Decor blocks, and items, will now reside in Thresholds. \ No newline at end of file diff --git a/src/main/java/dev/zontreck/libzontreck/edlibmc/Registries.java b/src/main/java/dev/zontreck/libzontreck/edlibmc/Registries.java new file mode 100644 index 0000000..02542bc --- /dev/null +++ b/src/main/java/dev/zontreck/libzontreck/edlibmc/Registries.java @@ -0,0 +1,263 @@ +/* + * @file Registries.java + * @author Stefan Wilhelm (wile) + * @copyright (C) 2020 Stefan Wilhelm + * @license MIT (see https://opensource.org/licenses/MIT) + * + * Common game registry handling. + */ +package dev.zontreck.libzontreck.edlibmc; + +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.flag.FeatureFlagSet; +import net.minecraft.world.flag.FeatureFlags; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.level.ItemLike; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraftforge.eventbus.api.IEventBus; +import net.minecraftforge.registries.DeferredRegister; +import net.minecraftforge.registries.ForgeRegistries; +import net.minecraftforge.registries.RegistryObject; + +import javax.annotation.Nonnull; +import java.util.*; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +public class Registries { + private static String modid = null; + private static String creative_tab_icon = ""; + private static CreativeModeTab creative_tab = null; + private static final Map> registered_block_tag_keys = new HashMap<>(); + private static final Map> registered_item_tag_keys = new HashMap<>(); + + private static final Map> registered_blocks = new HashMap<>(); + private static final Map> registered_items = new HashMap<>(); + private static final Map>> registered_block_entity_types = new HashMap<>(); + private static final Map>> registered_entity_types = new HashMap<>(); + private static final Map>> registered_menu_types = new HashMap<>(); + private static final Map>> recipe_serializers = new HashMap<>(); + private static final Map> registered_tabs = new HashMap<>(); + + public static final List> tabItems = new ArrayList<>(); + + + + private static DeferredRegister BLOCKS; + private static DeferredRegister ITEMS; + private static DeferredRegister TABS; + private static DeferredRegister> BLOCK_ENTITIES; + private static DeferredRegister> MENUS; + private static DeferredRegister> ENTITIES; + private static DeferredRegister> RECIPE_SERIALIZERS; + private static List> MOD_REGISTRIES; + + public static void init(String mod_id, String creative_tab_icon_item_name, IEventBus bus) { + modid = mod_id; + creative_tab_icon = creative_tab_icon_item_name; + BLOCKS = DeferredRegister.create(ForgeRegistries.BLOCKS, modid); + ITEMS = DeferredRegister.create(ForgeRegistries.ITEMS, modid); + BLOCK_ENTITIES = DeferredRegister.create(ForgeRegistries.BLOCK_ENTITY_TYPES, modid); + MENUS = DeferredRegister.create(ForgeRegistries.MENU_TYPES, modid); + ENTITIES = DeferredRegister.create(ForgeRegistries.ENTITY_TYPES, modid); + RECIPE_SERIALIZERS = DeferredRegister.create(ForgeRegistries.RECIPE_SERIALIZERS, modid); + TABS = DeferredRegister.create(net.minecraft.core.registries.Registries.CREATIVE_MODE_TAB, modid); + + Consumer> consumer = (X)->{ + X.register(bus); + }; + + List.of(BLOCKS, ITEMS, BLOCK_ENTITIES, MENUS, ENTITIES, RECIPE_SERIALIZERS, TABS).forEach(consumer); + } + + // ------------------------------------------------------------------------------------------------------------- + + public static Block getBlock(String block_name) { + return registered_blocks.get(block_name).get(); + } + + public static RegistryObject withTab(RegistryObject item) + { + tabItems.add(item); + + return item; + } + + public static Item getItem(String name) { + return registered_items.get(name).get(); + } + + public static EntityType getEntityType(String name) { + return registered_entity_types.get(name).get(); + } + + public static BlockEntityType getBlockEntityType(String block_name) { + return registered_block_entity_types.get(block_name).get(); + } + + public static MenuType getMenuType(String name) { + return registered_menu_types.get(name).get(); + } + + public static RecipeSerializer getRecipeSerializer(String name) { + return recipe_serializers.get(name).get(); + } + + public static BlockEntityType getBlockEntityTypeOfBlock(String block_name) { + return getBlockEntityType("tet_" + block_name); + } + + public static BlockEntityType getBlockEntityTypeOfBlock(Block block) { + return getBlockEntityTypeOfBlock(ForgeRegistries.BLOCKS.getKey(block).getPath()); + } + + public static MenuType getMenuTypeOfBlock(String name) { + return getMenuType("ct_" + name); + } + + public static MenuType getMenuTypeOfBlock(Block block) { + return getMenuTypeOfBlock(ForgeRegistries.BLOCKS.getKey(block).getPath()); + } + + public static TagKey getBlockTagKey(String name) { + return registered_block_tag_keys.get(name); + } + + public static TagKey getItemTagKey(String name) { + return registered_item_tag_keys.get(name); + } + + // ------------------------------------------------------------------------------------------------------------- + + @Nonnull + public static List getRegisteredBlocks() { + return Collections.unmodifiableList(registered_blocks.values().stream().map(RegistryObject::get).toList()); + } + + @Nonnull + public static List getRegisteredItems() { + return Collections.unmodifiableList(registered_items.values().stream().map(RegistryObject::get).toList()); + } + + @Nonnull + public static List> getRegisteredBlockEntityTypes() { + return Collections.unmodifiableList(registered_block_entity_types.values().stream().map(RegistryObject::get).toList()); + } + + @Nonnull + public static List> getRegisteredEntityTypes() { + return Collections.unmodifiableList(registered_entity_types.values().stream().map(RegistryObject::get).toList()); + } + + // ------------------------------------------------------------------------------------------------------------- + + public static void addItem(String registry_name, Supplier supplier) { + registered_items.put(registry_name, withTab(ITEMS.register(registry_name, supplier))); + + } + + public static void addBlock(String registry_name, Supplier block_supplier) { + registered_blocks.put(registry_name, BLOCKS.register(registry_name, block_supplier)); + registered_items.put(registry_name, withTab(ITEMS.register(registry_name, () -> new BlockItem(registered_blocks.get(registry_name).get(), (new Item.Properties()))))); + } + + public static void addCreativeModeTab(String id, Component title, ItemStack icon) + { + registered_tabs.put(id, TABS.register(id, ()-> CreativeModeTab + .builder() + .icon(()->icon) + .title(title) + .withSearchBar() + + .displayItems((display, output) -> tabItems.forEach(it->output.accept(it.get()))) + + .build() + )); + } + + public static void addBlock(String registry_name, Supplier block_supplier, Supplier item_supplier) { + registered_blocks.put(registry_name, BLOCKS.register(registry_name, block_supplier)); + registered_items.put(registry_name, ITEMS.register(registry_name, item_supplier)); + } + + public static void addBlockEntityType(String registry_name, BlockEntityType.BlockEntitySupplier ctor, String... block_names) { + registered_block_entity_types.put(registry_name, BLOCK_ENTITIES.register(registry_name, () -> { + final Block[] blocks = Arrays.stream(block_names).map(s -> { + Block b = BLOCKS.getEntries().stream().filter((ro) -> ro.getId().getPath().equals(s)).findFirst().map(RegistryObject::get).orElse(null); + if (b == null) Auxiliaries.logError("registered_blocks does not encompass '" + s + "'"); + return b; + }).filter(Objects::nonNull).collect(Collectors.toList()).toArray(new Block[]{}); + return BlockEntityType.Builder.of(ctor, blocks).build(null); + })); + } + + public static > void addEntityType(String registry_name, Supplier> supplier) { + registered_entity_types.put(registry_name, ENTITIES.register(registry_name, supplier)); + } + + public static > void addMenuType(String registry_name, MenuType.MenuSupplier supplier, FeatureFlagSet flags) { + registered_menu_types.put(registry_name, MENUS.register(registry_name, () -> new MenuType<>(supplier, flags))); + } + + public static void addRecipeSerializer(String registry_name, Supplier> serializer_supplier) { + recipe_serializers.put(registry_name, RECIPE_SERIALIZERS.register(registry_name, serializer_supplier)); + } + + public static void addOptionalBlockTag(String tag_name, ResourceLocation... default_blocks) { + final Set> default_suppliers = new HashSet<>(); + for (ResourceLocation rl : default_blocks) default_suppliers.add(() -> ForgeRegistries.BLOCKS.getValue(rl)); + final TagKey key = ForgeRegistries.BLOCKS.tags().createOptionalTagKey(new ResourceLocation(modid, tag_name), default_suppliers); + registered_block_tag_keys.put(tag_name, key); + } + + public static void addOptionaItemTag(String tag_name, ResourceLocation... default_items) { + final Set> default_suppliers = new HashSet<>(); + for (ResourceLocation rl : default_items) default_suppliers.add(() -> ForgeRegistries.ITEMS.getValue(rl)); + final TagKey key = ForgeRegistries.ITEMS.tags().createOptionalTagKey(new ResourceLocation(modid, tag_name), default_suppliers); + registered_item_tag_keys.put(tag_name, key); + } + + // ------------------------------------------------------------------------------------------------------------- + + @Deprecated + /** + * This function is to be removed as it cannot add items to a tab anymore + */ + public static void addBlock(String registry_name, Supplier block_supplier, BiFunction item_builder) { + addBlock(registry_name, block_supplier, () -> item_builder.apply(registered_blocks.get(registry_name).get(), (new Item.Properties()))); + } + + public static void addBlock(String registry_name, Supplier block_supplier, BlockEntityType.BlockEntitySupplier block_entity_ctor) { + addBlock(registry_name, block_supplier); + addBlockEntityType("tet_" + registry_name, block_entity_ctor, registry_name); + } + + public static void addBlock(String registry_name, Supplier block_supplier, BiFunction item_builder, BlockEntityType.BlockEntitySupplier block_entity_ctor) { + addBlock(registry_name, block_supplier, item_builder); + addBlockEntityType("tet_" + registry_name, block_entity_ctor, registry_name); + } + + public static void addBlock(String registry_name, Supplier block_supplier, BiFunction item_builder, BlockEntityType.BlockEntitySupplier block_entity_ctor, MenuType.MenuSupplier menu_type_supplier, FeatureFlagSet menuFlags) { + addBlock(registry_name, block_supplier, item_builder); + addBlockEntityType("tet_" + registry_name, block_entity_ctor, registry_name); + addMenuType("ct_" + registry_name, menu_type_supplier, menuFlags); + } + + public static void addBlock(String registry_name, Supplier block_supplier, BlockEntityType.BlockEntitySupplier block_entity_ctor, MenuType.MenuSupplier menu_type_supplier, FeatureFlagSet menuFlags) { + addBlock(registry_name, block_supplier, block_entity_ctor); + addMenuType("ct_" + registry_name, menu_type_supplier, menuFlags); + } + +} diff --git a/src/main/java/dev/zontreck/libzontreck/edlibmc/RfEnergy.java b/src/main/java/dev/zontreck/libzontreck/edlibmc/RfEnergy.java new file mode 100644 index 0000000..8636926 --- /dev/null +++ b/src/main/java/dev/zontreck/libzontreck/edlibmc/RfEnergy.java @@ -0,0 +1,180 @@ +/* + * @file RfEnergy.java + * @author Stefan Wilhelm (wile) + * @copyright (C) 2020 Stefan Wilhelm + * @license MIT (see https://opensource.org/licenses/MIT) + * + * General RF/FE energy handling functionality. + */ +package dev.zontreck.libzontreck.edlibmc; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.util.Mth; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraftforge.common.capabilities.ForgeCapabilities; +import net.minecraftforge.common.util.LazyOptional; +import net.minecraftforge.energy.IEnergyStorage; + +import javax.annotation.Nullable; + +public class RfEnergy { + public static int feed(Level world, BlockPos pos, @Nullable Direction side, int rf_energy) { + final BlockEntity te = world.getBlockEntity(pos); + if (te == null) return 0; + final IEnergyStorage es = te.getCapability(ForgeCapabilities.ENERGY, side).orElse(null); + if (es == null) return 0; + return es.receiveEnergy(rf_energy, false); + } + + public static class Battery implements IEnergyStorage { + protected int capacity_; + protected int charge_rate_; + protected int discharge_rate_; + protected int energy_; + + public Battery(int capacity) { + this(capacity, capacity); + } + + public Battery(int capacity, int transfer_rate) { + this(capacity, transfer_rate, transfer_rate, 0); + } + + public Battery(int capacity, int charge_rate, int discharge_rate) { + this(capacity, charge_rate, discharge_rate, 0); + } + + public Battery(int capacity, int charge_rate, int discharge_rate, int energy) { + capacity_ = Math.max(capacity, 1); + charge_rate_ = Mth.clamp(charge_rate, 0, capacity_); + discharge_rate_ = Mth.clamp(discharge_rate, 0, capacity_); + energy_ = Mth.clamp(energy, 0, capacity_); + } + + // --------------------------------------------------------------------------------------------------- + + public Battery setMaxEnergyStored(int capacity) { + capacity_ = Math.max(capacity, 1); + return this; + } + + public Battery setEnergyStored(int energy) { + energy_ = Mth.clamp(energy, 0, capacity_); + return this; + } + + public Battery setChargeRate(int in_rate) { + charge_rate_ = Mth.clamp(in_rate, 0, capacity_); + return this; + } + + public Battery setDischargeRate(int out_rate) { + discharge_rate_ = Mth.clamp(out_rate, 0, capacity_); + return this; + } + + public int getChargeRate() { + return charge_rate_; + } + + public int getDischargeRate() { + return discharge_rate_; + } + + public boolean isEmpty() { + return energy_ <= 0; + } + + public boolean isFull() { + return energy_ >= capacity_; + } + + public int getSOC() { + return (int) Mth.clamp((100.0 * energy_ / capacity_ + .5), 0, 100); + } + + public int getComparatorOutput() { + return (int) Mth.clamp((15.0 * energy_ / capacity_ + .2), 0, 15); + } + + public boolean draw(int energy) { + if (energy_ < energy) return false; + energy_ -= energy; + return true; + } + + public boolean feed(int energy) { + energy_ = Math.min(energy_ + energy, capacity_); + return energy_ >= capacity_; + } + + public Battery clear() { + energy_ = 0; + return this; + } + + public Battery load(CompoundTag nbt, String key) { + setEnergyStored(nbt.getInt(key)); + return this; + } + + public Battery load(CompoundTag nbt) { + return load(nbt, "Energy"); + } + + public CompoundTag save(CompoundTag nbt, String key) { + nbt.putInt(key, energy_); + return nbt; + } + + public CompoundTag save(CompoundTag nbt) { + return save(nbt, "Energy"); + } + + public LazyOptional createEnergyHandler() { + return LazyOptional.of(() -> this); + } + + // IEnergyStorage ------------------------------------------------------------------------------------ + + @Override + public int receiveEnergy(int feed_energy, boolean simulate) { + if (!canReceive()) return 0; + int e = Math.min(Math.min(charge_rate_, feed_energy), capacity_ - energy_); + if (!simulate) energy_ += e; + return e; + } + + @Override + public int extractEnergy(int draw_energy, boolean simulate) { + if (!canExtract()) return 0; + int e = Math.min(Math.min(discharge_rate_, draw_energy), energy_); + if (!simulate) energy_ -= e; + return e; + } + + @Override + public int getEnergyStored() { + return energy_; + } + + @Override + public int getMaxEnergyStored() { + return capacity_; + } + + @Override + public boolean canExtract() { + return discharge_rate_ > 0; + } + + @Override + public boolean canReceive() { + return charge_rate_ > 0; + } + + } +} diff --git a/src/main/java/dev/zontreck/libzontreck/edlibmc/RsSignals.java b/src/main/java/dev/zontreck/libzontreck/edlibmc/RsSignals.java new file mode 100644 index 0000000..70509bd --- /dev/null +++ b/src/main/java/dev/zontreck/libzontreck/edlibmc/RsSignals.java @@ -0,0 +1,42 @@ +/* + * @file RsSignals.java + * @author Stefan Wilhelm (wile) + * @copyright (C) 2020 Stefan Wilhelm + * @license MIT (see https://opensource.org/licenses/MIT) + * + * General redstone signal related functionality. + */ +package dev.zontreck.libzontreck.edlibmc; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.Container; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.block.state.BlockState; + +import javax.annotation.Nullable; + +public class RsSignals { + + public static boolean hasSignalConnector(BlockState state, BlockGetter world, BlockPos pos, @Nullable Direction realSide) { + return state.isSignalSource(); + } + + public static int fromContainer(@Nullable Container container) { + if (container == null) return 0; + final double max = container.getMaxStackSize(); + if (max <= 0) return 0; + boolean nonempty = false; + double fill_level = 0; + for (int i = 0; i < container.getContainerSize(); ++i) { + ItemStack stack = container.getItem(i); + if (stack.isEmpty() || (stack.getMaxStackSize() <= 0)) continue; + fill_level += ((double) stack.getCount()) / Math.min(max, stack.getMaxStackSize()); + nonempty = true; + } + fill_level /= container.getContainerSize(); + return (int) (Math.floor(fill_level * 14) + (nonempty ? 1 : 0)); // vanilla compliant calculation. + } + +} diff --git a/src/main/java/dev/zontreck/libzontreck/edlibmc/SidedProxy.java b/src/main/java/dev/zontreck/libzontreck/edlibmc/SidedProxy.java new file mode 100644 index 0000000..ba5f04b --- /dev/null +++ b/src/main/java/dev/zontreck/libzontreck/edlibmc/SidedProxy.java @@ -0,0 +1,122 @@ +/* + * @file SidedProxy.java + * @author Stefan Wilhelm (wile) + * @copyright (C) 2020 Stefan Wilhelm + * @license MIT (see https://opensource.org/licenses/MIT) + * + * General client/server sidedness selection proxy. + */ +package dev.zontreck.libzontreck.edlibmc; + +import net.minecraft.client.Minecraft; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraftforge.fml.DistExecutor; + +import javax.annotation.Nullable; +import java.util.Optional; + +public class SidedProxy { + @Nullable + public static Player getPlayerClientSide() { + return proxy.getPlayerClientSide(); + } + + @Nullable + public static Level getWorldClientSide() { + return proxy.getWorldClientSide(); + } + + @Nullable + public static Minecraft mc() { + return proxy.mc(); + } + + public static Optional isCtrlDown() { + return proxy.isCtrlDown(); + } + + public static Optional isShiftDown() { + return proxy.isShiftDown(); + } + + public static Optional getClipboard() { + return proxy.getClipboard(); + } + + public static boolean setClipboard(String text) { + return proxy.setClipboard(text); + } + + // -------------------------------------------------------------------------------------------------------- + + private static final ISidedProxy proxy = DistExecutor.unsafeRunForDist(() -> ClientProxy::new, () -> ServerProxy::new); + + private interface ISidedProxy { + default @Nullable Player getPlayerClientSide() { + return null; + } + + default @Nullable Level getWorldClientSide() { + return null; + } + + default @Nullable Minecraft mc() { + return null; + } + + default Optional isCtrlDown() { + return Optional.empty(); + } + + default Optional isShiftDown() { + return Optional.empty(); + } + + default Optional getClipboard() { + return Optional.empty(); + } + + default boolean setClipboard(String text) { + return false; + } + } + + private static final class ClientProxy implements ISidedProxy { + public @Nullable Player getPlayerClientSide() { + return Minecraft.getInstance().player; + } + + public @Nullable Level getWorldClientSide() { + return Minecraft.getInstance().level; + } + + public @Nullable Minecraft mc() { + return Minecraft.getInstance(); + } + + public Optional isCtrlDown() { + return Optional.of(Auxiliaries.isCtrlDown()); + } + + public Optional isShiftDown() { + return Optional.of(Auxiliaries.isShiftDown()); + } + + public Optional getClipboard() { + return (mc() == null) ? Optional.empty() : Optional.of(net.minecraft.client.gui.font.TextFieldHelper.getClipboardContents(mc())); + } + + public boolean setClipboard(String text) { + if (mc() == null) { + return false; + } + net.minecraft.client.gui.font.TextFieldHelper.setClipboardContents(Minecraft.getInstance(), text); + return true; + } + } + + private static final class ServerProxy implements ISidedProxy { + } + +} diff --git a/src/main/java/dev/zontreck/libzontreck/edlibmc/SlabSliceBlock.java b/src/main/java/dev/zontreck/libzontreck/edlibmc/SlabSliceBlock.java new file mode 100644 index 0000000..40e3401 --- /dev/null +++ b/src/main/java/dev/zontreck/libzontreck/edlibmc/SlabSliceBlock.java @@ -0,0 +1,228 @@ +/* + * @file SlabSliceBlock.java + * @author Stefan Wilhelm (wile) + * @copyright (C) 2020 Stefan Wilhelm + * @license MIT (see https://opensource.org/licenses/MIT) + * + * Half slab ("slab slices") characteristics class. Actually + * it's now a quarter slab, but who cares. + */ +package dev.zontreck.libzontreck.edlibmc; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.network.chat.Component; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.SpawnPlacements; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.SoundType; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.IntegerProperty; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class SlabSliceBlock extends StandardBlocks.WaterLoggable implements StandardBlocks.IStandardBlock { + public static final IntegerProperty PARTS = IntegerProperty.create("parts", 0, 14); + + protected static final VoxelShape[] AABBs = { + Shapes.create(new AABB(0, 0. / 16, 0, 1, 2. / 16, 1)), + Shapes.create(new AABB(0, 0. / 16, 0, 1, 4. / 16, 1)), + Shapes.create(new AABB(0, 0. / 16, 0, 1, 6. / 16, 1)), + Shapes.create(new AABB(0, 0. / 16, 0, 1, 8. / 16, 1)), + Shapes.create(new AABB(0, 0. / 16, 0, 1, 10. / 16, 1)), + Shapes.create(new AABB(0, 0. / 16, 0, 1, 12. / 16, 1)), + Shapes.create(new AABB(0, 0. / 16, 0, 1, 14. / 16, 1)), + Shapes.create(new AABB(0, 0. / 16, 0, 1, 16. / 16, 1)), + Shapes.create(new AABB(0, 2. / 16, 0, 1, 16. / 16, 1)), + Shapes.create(new AABB(0, 4. / 16, 0, 1, 16. / 16, 1)), + Shapes.create(new AABB(0, 6. / 16, 0, 1, 16. / 16, 1)), + Shapes.create(new AABB(0, 8. / 16, 0, 1, 16. / 16, 1)), + Shapes.create(new AABB(0, 10. / 16, 0, 1, 16. / 16, 1)), + Shapes.create(new AABB(0, 12. / 16, 0, 1, 16. / 16, 1)), + Shapes.create(new AABB(0, 14. / 16, 0, 1, 16. / 16, 1)), + Shapes.create(new AABB(0, 0, 0, 1, 1, 1)) // <- with 4bit fill + }; + + protected static final int[] num_slabs_contained_in_parts_ = {1, 2, 3, 4, 5, 6, 7, 8, 7, 6, 5, 4, 3, 2, 1, 0x1}; // <- with 4bit fill + private static boolean with_pickup = false; + + public static void on_config(boolean direct_slab_pickup) { + with_pickup = direct_slab_pickup; + } + + public SlabSliceBlock(long config, BlockBehaviour.Properties builder) { + super(config, builder); + } + + protected boolean is_cube(BlockState state) { + return state.getValue(PARTS) == 0x07; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void appendHoverText(ItemStack stack, @Nullable BlockGetter world, List tooltip, TooltipFlag flag) { + if (!Auxiliaries.Tooltip.addInformation(stack, world, tooltip, flag, true)) return; + if (with_pickup) Auxiliaries.Tooltip.addInformation("engineersdecor.tooltip.slabpickup", tooltip); + } + + @Override + public RenderTypeHint getRenderTypeHint() { + return (((config & StandardBlocks.CFG_TRANSLUCENT) != 0) ? (RenderTypeHint.TRANSLUCENT) : (RenderTypeHint.CUTOUT)); + } + + @Override + public boolean isPossibleToRespawnInThis(BlockState p_279289_) { + return false; + } + + @Override + public boolean isValidSpawn(BlockState state, BlockGetter world, BlockPos pos, SpawnPlacements.Type type, @Nullable EntityType entityType) { + return false; + } + + @Override + public VoxelShape getShape(BlockState state, BlockGetter source, BlockPos pos, CollisionContext selectionContext) { + return AABBs[state.getValue(PARTS) & 0xf]; + } + + @Override + public VoxelShape getCollisionShape(BlockState state, BlockGetter world, BlockPos pos, CollisionContext selectionContext) { + return getShape(state, world, pos, selectionContext); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + super.createBlockStateDefinition(builder); + builder.add(PARTS); + } + + @Override + @Nullable + public BlockState getStateForPlacement(BlockPlaceContext context) { + final BlockPos pos = context.getClickedPos(); + BlockState state = context.getLevel().getBlockState(pos); + if (state.getBlock() == this) { + int parts = state.getValue(PARTS); + if (parts == 7) return null; // -> is already a full block. + parts += (parts < 7) ? 1 : -1; + if (parts == 7) state = state.setValue(WATERLOGGED, false); + return state.setValue(PARTS, parts); + } else { + final Direction face = context.getClickedFace(); + final BlockState placement_state = super.getStateForPlacement(context); // fluid state + if (face == Direction.UP) return placement_state.setValue(PARTS, 0); + if (face == Direction.DOWN) return placement_state.setValue(PARTS, 14); + if (!face.getAxis().isHorizontal()) return placement_state; + final boolean isupper = ((context.getClickLocation().y() - context.getClickedPos().getY()) > 0.5); + return placement_state.setValue(PARTS, isupper ? 14 : 0); + } + } + + @Override + @SuppressWarnings("deprecation") + public boolean canBeReplaced(BlockState state, BlockPlaceContext context) { + if (context.getItemInHand().getItem() != this.asItem()) return false; + if (!context.replacingClickedOnBlock()) return true; + final Direction face = context.getClickedFace(); + final int parts = state.getValue(PARTS); + if (parts == 7) return false; + if ((face == Direction.UP) && (parts < 7)) return true; + if ((face == Direction.DOWN) && (parts > 7)) return true; + if (!face.getAxis().isHorizontal()) return false; + final boolean isupper = ((context.getClickLocation().y() - context.getClickedPos().getY()) > 0.5); + return isupper ? (parts == 0) : (parts == 1); + } + + @Override + @SuppressWarnings("deprecation") + public BlockState rotate(BlockState state, Rotation rot) { + return state; + } + + @Override + @SuppressWarnings("deprecation") + public BlockState mirror(BlockState state, Mirror mirrorIn) { + return state; + } + + @Override + public boolean hasDynamicDropList() { + return true; + } + + @Override + public List dropList(BlockState state, Level world, BlockEntity te, boolean explosion) { + return new ArrayList<>(Collections.singletonList(new ItemStack(this.asItem(), num_slabs_contained_in_parts_[state.getValue(PARTS) & 0xf]))); + } + + @Override + @SuppressWarnings("deprecation") + public void attack(BlockState state, Level world, BlockPos pos, Player player) { + if ((world.isClientSide) || (!with_pickup)) return; + final ItemStack stack = player.getMainHandItem(); + if (stack.isEmpty() || (Block.byItem(stack.getItem()) != this)) return; + if (stack.getCount() >= stack.getMaxStackSize()) return; + Vec3 lv = player.getLookAngle(); + Direction facing = Direction.getNearest((float) lv.x, (float) lv.y, (float) lv.z); + if ((facing != Direction.UP) && (facing != Direction.DOWN)) return; + if (state.getBlock() != this) return; + int parts = state.getValue(PARTS); + if ((facing == Direction.DOWN) && (parts <= 7)) { + if (parts > 0) { + world.setBlock(pos, state.setValue(PARTS, parts - 1), 3); + } else { + world.removeBlock(pos, false); + } + } else if ((facing == Direction.UP) && (parts >= 7)) { + if (parts < 14) { + world.setBlock(pos, state.setValue(PARTS, parts + 1), 3); + } else { + world.removeBlock(pos, false); + } + } else { + return; + } + if (!player.isCreative()) { + stack.grow(1); + if (player.getInventory() != null) player.getInventory().setChanged(); + } + SoundType st = this.getSoundType(state, world, pos, null); + world.playSound(player, pos, st.getPlaceSound(), SoundSource.BLOCKS, (st.getVolume() + 1f) / 2.5f, 0.9f * st.getPitch()); + } + + @Override + public boolean placeLiquid(LevelAccessor world, BlockPos pos, BlockState state, FluidState fluidState) { + return (state.getValue(PARTS) != 14) && (super.placeLiquid(world, pos, state, fluidState)); + } + + @Override + public boolean canPlaceLiquid(BlockGetter world, BlockPos pos, BlockState state, Fluid fluid) { + return (state.getValue(PARTS) != 14) && (super.canPlaceLiquid(world, pos, state, fluid)); + } + +} diff --git a/src/main/java/dev/zontreck/libzontreck/edlibmc/StandardBlocks.java b/src/main/java/dev/zontreck/libzontreck/edlibmc/StandardBlocks.java new file mode 100644 index 0000000..69343b1 --- /dev/null +++ b/src/main/java/dev/zontreck/libzontreck/edlibmc/StandardBlocks.java @@ -0,0 +1,698 @@ +/* + * @file StandardBlocks.java + * @author Stefan Wilhelm (wile) + * @copyright (C) 2020 Stefan Wilhelm + * @license MIT (see https://opensource.org/licenses/MIT) + * + * Common functionality class for decor blocks. + */ +package dev.zontreck.libzontreck.edlibmc; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.SpawnPlacements; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.SimpleWaterloggedBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.level.block.state.properties.DirectionProperty; +import net.minecraft.world.level.block.state.properties.EnumProperty; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.material.Fluids; +import net.minecraft.world.level.material.PushReaction; +import net.minecraft.world.level.pathfinder.PathComputationType; +import net.minecraft.world.level.storage.loot.LootContext; +import net.minecraft.world.level.storage.loot.LootParams; +import net.minecraft.world.level.storage.loot.parameters.LootContextParams; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.shapes.BooleanOp; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import javax.annotation.Nullable; +import java.util.*; +import java.util.function.Function; +import java.util.function.Supplier; + + +@SuppressWarnings("deprecation") +public class StandardBlocks { + public static final long CFG_DEFAULT = 0x0000000000000000L; // no special config + public static final long CFG_CUTOUT = 0x0000000000000001L; // cutout rendering + public static final long CFG_MIPPED = 0x0000000000000002L; // cutout mipped rendering + public static final long CFG_TRANSLUCENT = 0x0000000000000004L; // indicates a block/pane is glass like (transparent, etc) + public static final long CFG_WATERLOGGABLE = 0x0000000000000008L; // The derived block extends IWaterLoggable + public static final long CFG_HORIZIONTAL = 0x0000000000000010L; // horizontal block, affects bounding box calculation at construction time and placement + public static final long CFG_LOOK_PLACEMENT = 0x0000000000000020L; // placed in direction the player is looking when placing. + public static final long CFG_FACING_PLACEMENT = 0x0000000000000040L; // placed on the facing the player has clicked. + public static final long CFG_OPPOSITE_PLACEMENT = 0x0000000000000080L; // placed placed in the opposite direction of the face the player clicked. + public static final long CFG_FLIP_PLACEMENT_IF_SAME = 0x0000000000000100L; // placement direction flipped if an instance of the same class was clicked + public static final long CFG_FLIP_PLACEMENT_SHIFTCLICK = 0x0000000000000200L; // placement direction flipped if player is sneaking + public static final long CFG_STRICT_CONNECTIONS = 0x0000000000000400L; // blocks do not connect to similar blocks around (implementation details may vary a bit) + public static final long CFG_AI_PASSABLE = 0x0000000000000800L; // does not block movement path for AI, needed for non-opaque blocks with collision shapes not thin at the bottom or one side. + + public interface IStandardBlock { + default long config() { + return 0; + } + + default boolean hasDynamicDropList() { + return false; + } + + default List dropList(BlockState state, Level world, @Nullable BlockEntity te, boolean explosion) { + return Collections.singletonList((!world.isClientSide()) ? (new ItemStack(state.getBlock().asItem())) : (ItemStack.EMPTY)); + } + + enum RenderTypeHint {SOLID, CUTOUT, CUTOUT_MIPPED, TRANSLUCENT, TRANSLUCENT_NO_CRUMBLING} + + default RenderTypeHint getRenderTypeHint() { + return getRenderTypeHint(config()); + } + + default RenderTypeHint getRenderTypeHint(long config) { + if ((config & CFG_CUTOUT) != 0) return RenderTypeHint.CUTOUT; + if ((config & CFG_MIPPED) != 0) return RenderTypeHint.CUTOUT_MIPPED; + if ((config & CFG_TRANSLUCENT) != 0) return RenderTypeHint.TRANSLUCENT; + return RenderTypeHint.SOLID; + } + } + + public static class BaseBlock extends Block implements IStandardBlock, SimpleWaterloggedBlock { + public static final BooleanProperty WATERLOGGED = BlockStateProperties.WATERLOGGED; + public final long config; + + public BaseBlock(long conf, BlockBehaviour.Properties properties) { + super(properties); + config = conf; + BlockState state = getStateDefinition().any(); + if ((conf & CFG_WATERLOGGABLE) != 0) state = state.setValue(WATERLOGGED, false); + registerDefaultState(state); + } + + @Override + public long config() { + return config; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void appendHoverText(ItemStack stack, @Nullable BlockGetter world, List tooltip, TooltipFlag flag) { + Auxiliaries.Tooltip.addInformation(stack, world, tooltip, flag, true); + } + + @Override + public RenderTypeHint getRenderTypeHint() { + return getRenderTypeHint(config); + } + + @Override + @SuppressWarnings("deprecation") + public boolean isPathfindable(BlockState state, BlockGetter world, BlockPos pos, PathComputationType type) { + return ((config & CFG_AI_PASSABLE) != 0) && (super.isPathfindable(state, world, pos, type)); + } + + public boolean hasSignalConnector(BlockState state, BlockGetter world, BlockPos pos, @Nullable Direction side) { + return state.isSignalSource(); + } + + @Override + @SuppressWarnings("deprecation") + public void onRemove(BlockState state, Level world, BlockPos pos, BlockState newState, boolean isMoving) { + final boolean rsup = (state.hasBlockEntity() && (state.getBlock() != newState.getBlock())); + super.onRemove(state, world, pos, newState, isMoving); + if (rsup) world.updateNeighbourForOutputSignal(pos, this); + } + + @Override + public boolean propagatesSkylightDown(BlockState state, BlockGetter reader, BlockPos pos) { + return (((config & CFG_WATERLOGGABLE) == 0) || (!state.getValue(WATERLOGGED))) && super.propagatesSkylightDown(state, reader, pos); + } + + @Override + @SuppressWarnings("deprecation") + public FluidState getFluidState(BlockState state) { + return (((config & CFG_WATERLOGGABLE) != 0) && state.getValue(WATERLOGGED)) ? Fluids.WATER.getSource(false) : super.getFluidState(state); + } + + @Override + @SuppressWarnings("deprecation") + public BlockState updateShape(BlockState state, Direction facing, BlockState facingState, LevelAccessor world, BlockPos pos, BlockPos facingPos) { + if (((config & CFG_WATERLOGGABLE) != 0) && (state.getValue(WATERLOGGED))) + world.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(world)); + return state; + } + + @Override // SimpleWaterloggedBlock + public boolean canPlaceLiquid(BlockGetter world, BlockPos pos, BlockState state, Fluid fluid) { + return ((config & CFG_WATERLOGGABLE) != 0) && SimpleWaterloggedBlock.super.canPlaceLiquid(world, pos, state, fluid); + } + + @Override // SimpleWaterloggedBlock + public boolean placeLiquid(LevelAccessor world, BlockPos pos, BlockState state, FluidState fluidState) { + return ((config & CFG_WATERLOGGABLE) != 0) && SimpleWaterloggedBlock.super.placeLiquid(world, pos, state, fluidState); + } + + @Override // SimpleWaterloggedBlock + public ItemStack pickupBlock(LevelAccessor world, BlockPos pos, BlockState state) { + return ((config & CFG_WATERLOGGABLE) != 0) ? (SimpleWaterloggedBlock.super.pickupBlock(world, pos, state)) : (ItemStack.EMPTY); + } + + @Override // SimpleWaterloggedBlock + public Optional getPickupSound() { + return ((config & CFG_WATERLOGGABLE) != 0) ? (SimpleWaterloggedBlock.super.getPickupSound()) : Optional.empty(); + } + } + + public static class Cutout extends BaseBlock implements IStandardBlock { + private final VoxelShape vshape; + + public Cutout(long conf, BlockBehaviour.Properties properties) { + this(conf, properties, Auxiliaries.getPixeledAABB(0, 0, 0, 16, 16, 16)); + } + + public Cutout(long conf, BlockBehaviour.Properties properties, AABB aabb) { + this(conf, properties, Shapes.create(aabb)); + } + + public Cutout(long conf, BlockBehaviour.Properties properties, AABB[] aabbs) { + this(conf, properties, Arrays.stream(aabbs).map(Shapes::create).reduce(Shapes.empty(), (shape, aabb) -> Shapes.joinUnoptimized(shape, aabb, BooleanOp.OR))); + } + + public Cutout(long conf, BlockBehaviour.Properties properties, VoxelShape voxel_shape) { + super(conf, properties); + vshape = voxel_shape; + } + + @Override + @SuppressWarnings("deprecation") + public VoxelShape getShape(BlockState state, BlockGetter source, BlockPos pos, CollisionContext selectionContext) { + return vshape; + } + + @Override + @SuppressWarnings("deprecation") + public VoxelShape getCollisionShape(BlockState state, BlockGetter world, BlockPos pos, CollisionContext selectionContext) { + return vshape; + } + + @Override + @Nullable + public BlockState getStateForPlacement(BlockPlaceContext context) { + BlockState state = super.getStateForPlacement(context); + if ((config & CFG_WATERLOGGABLE) != 0) { + FluidState fs = context.getLevel().getFluidState(context.getClickedPos()); + state = state.setValue(WATERLOGGED, fs.getType() == Fluids.WATER); + } + return state; + } + + + @Override + public boolean isPossibleToRespawnInThis(BlockState p_279289_) { + return false; + } + + @Override + @SuppressWarnings("deprecation") + public PushReaction getPistonPushReaction(BlockState state) { + return PushReaction.NORMAL; + } + + @Override + public boolean propagatesSkylightDown(BlockState state, BlockGetter reader, BlockPos pos) { + if ((config & CFG_WATERLOGGABLE) != 0) { + if (state.getValue(WATERLOGGED)) return false; + } + return super.propagatesSkylightDown(state, reader, pos); + } + + @Override + @SuppressWarnings("deprecation") + public FluidState getFluidState(BlockState state) { + if ((config & CFG_WATERLOGGABLE) != 0) { + return state.getValue(WATERLOGGED) ? Fluids.WATER.getSource(false) : super.getFluidState(state); + } + return super.getFluidState(state); + } + + @Override + @SuppressWarnings("deprecation") + public BlockState updateShape(BlockState state, Direction facing, BlockState facingState, LevelAccessor world, BlockPos pos, BlockPos facingPos) { + if ((config & CFG_WATERLOGGABLE) != 0) { + if (state.getValue(WATERLOGGED)) + world.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(world)); + } + return state; + } + } + + public static class WaterLoggable extends Cutout implements IStandardBlock { + public WaterLoggable(long config, BlockBehaviour.Properties properties) { + super(config | CFG_WATERLOGGABLE, properties); + } + + public WaterLoggable(long config, BlockBehaviour.Properties properties, AABB aabb) { + super(config | CFG_WATERLOGGABLE, properties, aabb); + } + + public WaterLoggable(long config, BlockBehaviour.Properties properties, VoxelShape voxel_shape) { + super(config | CFG_WATERLOGGABLE, properties, voxel_shape); + } + + public WaterLoggable(long config, BlockBehaviour.Properties properties, AABB[] aabbs) { + super(config | CFG_WATERLOGGABLE, properties, aabbs); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + super.createBlockStateDefinition(builder); + builder.add(WATERLOGGED); + } + } + + public static class Directed extends Cutout implements IStandardBlock { + public static final DirectionProperty FACING = BlockStateProperties.FACING; + protected final Map vshapes; + + public Directed(long config, BlockBehaviour.Properties properties, final Function, Map> shape_supplier) { + super(config, properties); + registerDefaultState(super.defaultBlockState().setValue(FACING, Direction.UP)); + vshapes = shape_supplier.apply(getStateDefinition().getPossibleStates()); + } + + public Directed(long config, BlockBehaviour.Properties properties, final Supplier> shape_supplier) { + this(config, properties, (states) -> { + final Map vshapes = new HashMap<>(); + final ArrayList indexed_shapes = shape_supplier.get(); + for (BlockState state : states) + vshapes.put(state, indexed_shapes.get(state.getValue(FACING).get3DDataValue())); + return vshapes; + }); + } + + public Directed(long config, BlockBehaviour.Properties properties, final AABB[] unrotatedAABBs) { + this(config, properties, (states) -> { + final boolean is_horizontal = ((config & CFG_HORIZIONTAL) != 0); + Map vshapes = new HashMap<>(); + for (BlockState state : states) { + vshapes.put(state, Auxiliaries.getUnionShape(Auxiliaries.getRotatedAABB(unrotatedAABBs, state.getValue(FACING), is_horizontal))); + } + return vshapes; + }); + } + + public Directed(long config, BlockBehaviour.Properties properties, final AABB unrotatedAABB) { + this(config, properties, new AABB[]{unrotatedAABB}); + } + + + @Override + public boolean isPossibleToRespawnInThis(BlockState p_279289_) { + return false; + } + + @Override + public boolean isValidSpawn(BlockState state, BlockGetter world, BlockPos pos, SpawnPlacements.Type type, @Nullable EntityType entityType) { + return false; + } + + @Override + public VoxelShape getShape(BlockState state, BlockGetter source, BlockPos pos, CollisionContext selectionContext) { + return vshapes.get(state); + } + + @Override + public VoxelShape getCollisionShape(BlockState state, BlockGetter world, BlockPos pos, CollisionContext selectionContext) { + return getShape(state, world, pos, selectionContext); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + super.createBlockStateDefinition(builder); + builder.add(FACING); + } + + @Override + @Nullable + public BlockState getStateForPlacement(BlockPlaceContext context) { + BlockState state = super.getStateForPlacement(context); + if (state == null) return null; + Direction facing = context.getClickedFace(); + if ((config & (CFG_HORIZIONTAL | CFG_LOOK_PLACEMENT)) == (CFG_HORIZIONTAL | CFG_LOOK_PLACEMENT)) { + // horizontal placement in direction the player is looking + facing = context.getHorizontalDirection(); + } else if ((config & (CFG_HORIZIONTAL | CFG_LOOK_PLACEMENT)) == (CFG_HORIZIONTAL)) { + // horizontal placement on a face + if (((facing == Direction.UP) || (facing == Direction.DOWN))) return null; + } else if ((config & CFG_LOOK_PLACEMENT) != 0) { + // placement in direction the player is looking, with up and down + facing = context.getNearestLookingDirection(); + } + if ((config & CFG_OPPOSITE_PLACEMENT) != 0) facing = facing.getOpposite(); + if (((config & CFG_FLIP_PLACEMENT_SHIFTCLICK) != 0) && (context.getPlayer() != null) && (context.getPlayer().isShiftKeyDown())) + facing = facing.getOpposite(); + return state.setValue(FACING, facing); + } + } + + public static class AxisAligned extends Cutout implements IStandardBlock { + public static final EnumProperty AXIS = BlockStateProperties.AXIS; + protected final ArrayList vshapes; + + public AxisAligned(long config, BlockBehaviour.Properties properties, final Supplier> shape_supplier) { + super(config, properties); + registerDefaultState(super.defaultBlockState().setValue(AXIS, Direction.Axis.X)); + vshapes = shape_supplier.get(); + } + + public AxisAligned(long config, BlockBehaviour.Properties properties, final AABB[] unrotatedAABBs) { + this(config, properties, () -> new ArrayList<>(Arrays.asList( + Auxiliaries.getUnionShape(Auxiliaries.getRotatedAABB(unrotatedAABBs, Direction.EAST, false)), + Auxiliaries.getUnionShape(Auxiliaries.getRotatedAABB(unrotatedAABBs, Direction.UP, false)), + Auxiliaries.getUnionShape(Auxiliaries.getRotatedAABB(unrotatedAABBs, Direction.SOUTH, false)), + Shapes.block() + ))); + } + + public AxisAligned(long config, BlockBehaviour.Properties properties, final AABB unrotatedAABB) { + this(config, properties, new AABB[]{unrotatedAABB}); + } + + + @Override + public boolean isPossibleToRespawnInThis(BlockState p_279289_) { + return false; + } + + @Override + public boolean isValidSpawn(BlockState state, BlockGetter world, BlockPos pos, SpawnPlacements.Type type, @Nullable EntityType entityType) { + return false; + } + + @Override + public VoxelShape getShape(BlockState state, BlockGetter source, BlockPos pos, CollisionContext selectionContext) { + return vshapes.get((state.getValue(AXIS)).ordinal() & 0x3); + } + + @Override + public VoxelShape getCollisionShape(BlockState state, BlockGetter world, BlockPos pos, CollisionContext selectionContext) { + return getShape(state, world, pos, selectionContext); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + super.createBlockStateDefinition(builder); + builder.add(AXIS); + } + + @Override + @Nullable + public BlockState getStateForPlacement(BlockPlaceContext context) { + Direction facing; + if ((config & CFG_LOOK_PLACEMENT) != 0) { + facing = context.getNearestLookingDirection(); + } else { + facing = context.getClickedFace(); + } + return super.getStateForPlacement(context).setValue(AXIS, facing.getAxis()); + } + + @Override + @SuppressWarnings("deprecation") + public BlockState rotate(BlockState state, Rotation rotation) { + switch (rotation) { + case CLOCKWISE_90: + case COUNTERCLOCKWISE_90: + switch (state.getValue(AXIS)) { + case X: + return state.setValue(AXIS, Direction.Axis.Z); + case Z: + return state.setValue(AXIS, Direction.Axis.X); + } + } + return state; + } + } + + public static class Horizontal extends Cutout implements IStandardBlock { + public static final DirectionProperty HORIZONTAL_FACING = BlockStateProperties.HORIZONTAL_FACING; + protected final Map vshapes; + protected final Map cshapes; + + public Horizontal(long config, BlockBehaviour.Properties properties, final Function, Map> shape_supplier) { + super(config | CFG_HORIZIONTAL, properties); + registerDefaultState(super.defaultBlockState().setValue(HORIZONTAL_FACING, Direction.NORTH)); + vshapes = shape_supplier.apply(getStateDefinition().getPossibleStates()); + cshapes = shape_supplier.apply(getStateDefinition().getPossibleStates()); + } + + public Horizontal(long config, BlockBehaviour.Properties properties, final Supplier> shape_supplier) { + this(config, properties, (states) -> { + final Map vshapes = new HashMap<>(); + final ArrayList indexed_shapes = shape_supplier.get(); + for (BlockState state : states) + vshapes.put(state, indexed_shapes.get(state.getValue(HORIZONTAL_FACING).get3DDataValue())); + return vshapes; + }); + } + + public Horizontal(long config, BlockBehaviour.Properties properties, final AABB unrotatedAABB) { + this(config, properties, new AABB[]{unrotatedAABB}); + } + + public Horizontal(long config, BlockBehaviour.Properties properties, final AABB[] unrotatedAABBs) { + this(config, properties, (states) -> { + Map vshapes = new HashMap<>(); + for (BlockState state : states) { + vshapes.put(state, Auxiliaries.getUnionShape(Auxiliaries.getRotatedAABB(unrotatedAABBs, state.getValue(HORIZONTAL_FACING), true))); + } + return vshapes; + }); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + super.createBlockStateDefinition(builder); + builder.add(HORIZONTAL_FACING); + } + + @Override + public VoxelShape getShape(BlockState state, BlockGetter source, BlockPos pos, CollisionContext selectionContext) { + return vshapes.get(state); + } + + @Override + public VoxelShape getCollisionShape(BlockState state, BlockGetter world, BlockPos pos, CollisionContext selectionContext) { + return cshapes.get(state); + } + + @Override + @Nullable + public BlockState getStateForPlacement(BlockPlaceContext context) { + BlockState state = super.getStateForPlacement(context); + if (state == null) return null; + Direction facing = context.getClickedFace(); + if ((config & CFG_LOOK_PLACEMENT) != 0) { + // horizontal placement in direction the player is looking + facing = context.getHorizontalDirection(); + } else { + // horizontal placement on a face + facing = ((facing == Direction.UP) || (facing == Direction.DOWN)) ? (context.getHorizontalDirection()) : facing; + } + if ((config & CFG_OPPOSITE_PLACEMENT) != 0) facing = facing.getOpposite(); + if (((config & CFG_FLIP_PLACEMENT_SHIFTCLICK) != 0) && (context.getPlayer() != null) && (context.getPlayer().isShiftKeyDown())) + facing = facing.getOpposite(); + return state.setValue(HORIZONTAL_FACING, facing); + } + + @Override + @SuppressWarnings("deprecation") + public BlockState rotate(BlockState state, Rotation rot) { + return state.setValue(HORIZONTAL_FACING, rot.rotate(state.getValue(HORIZONTAL_FACING))); + } + + @Override + @SuppressWarnings("deprecation") + public BlockState mirror(BlockState state, Mirror mirrorIn) { + return state.rotate(mirrorIn.getRotation(state.getValue(HORIZONTAL_FACING))); + } + } + + public static class DirectedWaterLoggable extends Directed implements IStandardBlock { + public DirectedWaterLoggable(long config, BlockBehaviour.Properties properties, AABB aabb) { + super(config | CFG_WATERLOGGABLE, properties, aabb); + } + + public DirectedWaterLoggable(long config, BlockBehaviour.Properties properties, AABB[] aabbs) { + super(config | CFG_WATERLOGGABLE, properties, aabbs); + } + + public DirectedWaterLoggable(long config, BlockBehaviour.Properties properties, final Function, Map> shape_supplier) { + super(config | CFG_WATERLOGGABLE, properties, shape_supplier); + } + + public DirectedWaterLoggable(long config, BlockBehaviour.Properties properties, final Supplier> shape_supplier) { + super(config | CFG_WATERLOGGABLE, properties, shape_supplier); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + super.createBlockStateDefinition(builder); + builder.add(WATERLOGGED); + } + } + + public static class AxisAlignedWaterLoggable extends AxisAligned implements IStandardBlock { + public AxisAlignedWaterLoggable(long config, BlockBehaviour.Properties properties, AABB aabb) { + super(config | CFG_WATERLOGGABLE, properties, aabb); + } + + public AxisAlignedWaterLoggable(long config, BlockBehaviour.Properties properties, AABB[] aabbs) { + super(config | CFG_WATERLOGGABLE, properties, aabbs); + } + + public AxisAlignedWaterLoggable(long config, BlockBehaviour.Properties properties, final Supplier> shape_supplier) { + super(config | CFG_WATERLOGGABLE, properties, shape_supplier); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + super.createBlockStateDefinition(builder); + builder.add(WATERLOGGED); + } + } + + public static class HorizontalWaterLoggable extends Horizontal implements IStandardBlock { + public HorizontalWaterLoggable(long config, BlockBehaviour.Properties properties, AABB aabb) { + super(config | CFG_WATERLOGGABLE | CFG_HORIZIONTAL, properties, aabb); + } + + public HorizontalWaterLoggable(long config, BlockBehaviour.Properties properties, AABB[] aabbs) { + super(config | CFG_WATERLOGGABLE | CFG_HORIZIONTAL, properties, aabbs); + } + + public HorizontalWaterLoggable(long config, BlockBehaviour.Properties properties, final Supplier> shape_supplier) { + super(config | CFG_WATERLOGGABLE | CFG_HORIZIONTAL, properties, shape_supplier); + } + + public HorizontalWaterLoggable(long config, BlockBehaviour.Properties properties, final Function, Map> shape_supplier) { + super(config | CFG_WATERLOGGABLE | CFG_HORIZIONTAL, properties, shape_supplier); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + super.createBlockStateDefinition(builder); + builder.add(WATERLOGGED); + } + } + + static public class HorizontalFourWayWaterLoggable extends WaterLoggable implements IStandardBlock { + public static final BooleanProperty NORTH = BlockStateProperties.NORTH; + public static final BooleanProperty EAST = BlockStateProperties.EAST; + public static final BooleanProperty SOUTH = BlockStateProperties.SOUTH; + public static final BooleanProperty WEST = BlockStateProperties.WEST; + protected final Map shapes; + protected final Map collision_shapes; + + public HorizontalFourWayWaterLoggable(long config, BlockBehaviour.Properties properties, AABB base_aabb, final AABB[] side_aabb, int railing_height_extension) { + super(config, properties, base_aabb); + Map build_shapes = new HashMap<>(); + Map build_collision_shapes = new HashMap<>(); + for (BlockState state : getStateDefinition().getPossibleStates()) { + { + VoxelShape shape = ((base_aabb.getXsize() == 0) || (base_aabb.getYsize() == 0) || (base_aabb.getZsize() == 0)) ? Shapes.empty() : Shapes.create(base_aabb); + if (state.getValue(NORTH)) + shape = Shapes.joinUnoptimized(shape, Auxiliaries.getUnionShape(Auxiliaries.getRotatedAABB(side_aabb, Direction.NORTH, true)), BooleanOp.OR); + if (state.getValue(EAST)) + shape = Shapes.joinUnoptimized(shape, Auxiliaries.getUnionShape(Auxiliaries.getRotatedAABB(side_aabb, Direction.EAST, true)), BooleanOp.OR); + if (state.getValue(SOUTH)) + shape = Shapes.joinUnoptimized(shape, Auxiliaries.getUnionShape(Auxiliaries.getRotatedAABB(side_aabb, Direction.SOUTH, true)), BooleanOp.OR); + if (state.getValue(WEST)) + shape = Shapes.joinUnoptimized(shape, Auxiliaries.getUnionShape(Auxiliaries.getRotatedAABB(side_aabb, Direction.WEST, true)), BooleanOp.OR); + if (shape.isEmpty()) shape = Shapes.block(); + build_shapes.put(state.setValue(WATERLOGGED, false), shape); + build_shapes.put(state.setValue(WATERLOGGED, true), shape); + } + { + // how the hack to extend a shape, these are the above with y+4px. + VoxelShape shape = ((base_aabb.getXsize() == 0) || (base_aabb.getYsize() == 0) || (base_aabb.getZsize() == 0)) ? Shapes.empty() : Shapes.create(base_aabb); + if (state.getValue(NORTH)) + shape = Shapes.joinUnoptimized(shape, Auxiliaries.getUnionShape(Auxiliaries.getMappedAABB(Auxiliaries.getRotatedAABB(side_aabb, + Direction.NORTH, true), bb -> bb.expandTowards(0, railing_height_extension, 0))), BooleanOp.OR); + if (state.getValue(EAST)) + shape = Shapes.joinUnoptimized(shape, Auxiliaries.getUnionShape(Auxiliaries.getMappedAABB(Auxiliaries.getRotatedAABB(side_aabb, + Direction.EAST, true), bb -> bb.expandTowards(0, railing_height_extension, 0))), BooleanOp.OR); + if (state.getValue(SOUTH)) + shape = Shapes.joinUnoptimized(shape, Auxiliaries.getUnionShape(Auxiliaries.getMappedAABB(Auxiliaries.getRotatedAABB(side_aabb, + Direction.SOUTH, true), bb -> bb.expandTowards(0, railing_height_extension, 0))), BooleanOp.OR); + if (state.getValue(WEST)) + shape = Shapes.joinUnoptimized(shape, Auxiliaries.getUnionShape(Auxiliaries.getMappedAABB(Auxiliaries.getRotatedAABB(side_aabb, + Direction.WEST, true), bb -> bb.expandTowards(0, railing_height_extension, 0))), BooleanOp.OR); + if (shape.isEmpty()) shape = Shapes.block(); + build_collision_shapes.put(state.setValue(WATERLOGGED, false), shape); + build_collision_shapes.put(state.setValue(WATERLOGGED, true), shape); + } + } + shapes = build_shapes; + collision_shapes = build_collision_shapes; + registerDefaultState(super.defaultBlockState().setValue(NORTH, false).setValue(EAST, false).setValue(SOUTH, false).setValue(WEST, false).setValue(WATERLOGGED, false)); + } + + public HorizontalFourWayWaterLoggable(long config, BlockBehaviour.Properties properties, AABB base_aabb, final AABB side_aabb, int railing_height_extension) { + this(config, properties, base_aabb, new AABB[]{side_aabb}, railing_height_extension); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + super.createBlockStateDefinition(builder); + builder.add(NORTH, EAST, SOUTH, WEST); + } + + @Override + @Nullable + public BlockState getStateForPlacement(BlockPlaceContext context) { + return super.getStateForPlacement(context).setValue(NORTH, false).setValue(EAST, false).setValue(SOUTH, false).setValue(WEST, false); + } + + @Override + public VoxelShape getShape(BlockState state, BlockGetter worldIn, BlockPos pos, CollisionContext context) { + return shapes.getOrDefault(state, Shapes.block()); + } + + @Override + public VoxelShape getCollisionShape(BlockState state, BlockGetter worldIn, BlockPos pos, CollisionContext context) { + return collision_shapes.getOrDefault(state, Shapes.block()); + } + + public static BooleanProperty getDirectionProperty(Direction face) { + return switch (face) { + case EAST -> HorizontalFourWayWaterLoggable.EAST; + case SOUTH -> HorizontalFourWayWaterLoggable.SOUTH; + case WEST -> HorizontalFourWayWaterLoggable.WEST; + default -> HorizontalFourWayWaterLoggable.NORTH; + }; + } + } + +} diff --git a/src/main/java/dev/zontreck/libzontreck/edlibmc/StandardDoorBlock.java b/src/main/java/dev/zontreck/libzontreck/edlibmc/StandardDoorBlock.java new file mode 100644 index 0000000..8c7f10b --- /dev/null +++ b/src/main/java/dev/zontreck/libzontreck/edlibmc/StandardDoorBlock.java @@ -0,0 +1,175 @@ +/* + * @file StandardDoorBlock.java + * @author Stefan Wilhelm (wile) + * @copyright (C) 2020 Stefan Wilhelm + * @license MIT (see https://opensource.org/licenses/MIT) + * + * Door blocks, almost entirely based on vanilla. + */ +package dev.zontreck.libzontreck.edlibmc; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.network.chat.Component; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.SpawnPlacements; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.DoorBlock; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.BlockSetType; +import net.minecraft.world.level.block.state.properties.DoorHingeSide; +import net.minecraft.world.level.block.state.properties.DoubleBlockHalf; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.shapes.BooleanOp; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import javax.annotation.Nullable; +import java.util.List; + + +public class StandardDoorBlock extends DoorBlock implements StandardBlocks.IStandardBlock { + private final long config_; + protected final VoxelShape[][][][] shapes_; + protected final SoundEvent open_sound_; + protected final SoundEvent close_sound_; + + public StandardDoorBlock(long config, BlockBehaviour.Properties properties, AABB[] open_aabbs_top, AABB[] open_aabbs_bottom, AABB[] closed_aabbs_top, AABB[] closed_aabbs_bottom, SoundEvent open_sound, SoundEvent close_sound, BlockSetType blockSetType) { + super(properties, blockSetType); + VoxelShape[][][][] shapes = new VoxelShape[Direction.values().length][2][2][2]; + for (Direction facing : Direction.values()) { + for (boolean open : new boolean[]{false, true}) { + for (DoubleBlockHalf half : new DoubleBlockHalf[]{DoubleBlockHalf.UPPER, DoubleBlockHalf.LOWER}) { + for (boolean hinge_right : new boolean[]{false, true}) { + VoxelShape shape = Shapes.empty(); + if (facing.getAxis() == Direction.Axis.Y) { + shape = Shapes.block(); + } else { + final AABB[] aabbs = (open) ? ((half == DoubleBlockHalf.UPPER) ? open_aabbs_top : open_aabbs_bottom) : ((half == DoubleBlockHalf.UPPER) ? closed_aabbs_top : closed_aabbs_bottom); + for (AABB e : aabbs) { + AABB aabb = Auxiliaries.getRotatedAABB(e, facing, true); + if (!hinge_right) + aabb = Auxiliaries.getMirroredAABB(aabb, facing.getClockWise().getAxis()); + shape = Shapes.join(shape, Shapes.create(aabb), BooleanOp.OR); + } + } + shapes[facing.ordinal()][open ? 1 : 0][hinge_right ? 1 : 0][half == DoubleBlockHalf.UPPER ? 0 : 1] = shape; + } + } + } + } + config_ = config; + shapes_ = shapes; + open_sound_ = open_sound; + close_sound_ = close_sound; + } + + public StandardDoorBlock(long config, BlockBehaviour.Properties properties, AABB open_aabb, AABB closed_aabb, SoundEvent open_sound, SoundEvent close_sound, BlockSetType blockSetType) { + this(config, properties, new AABB[]{open_aabb}, new AABB[]{open_aabb}, new AABB[]{closed_aabb}, new AABB[]{closed_aabb}, open_sound, close_sound, blockSetType); + } + + public StandardDoorBlock(long config, BlockBehaviour.Properties properties, SoundEvent open_sound, SoundEvent close_sound, BlockSetType blockSetType) { + this( + config, properties, + Auxiliaries.getPixeledAABB(13, 0, 0, 16, 16, 16), + Auxiliaries.getPixeledAABB(0, 0, 13, 16, 16, 16), + open_sound, + close_sound, + blockSetType + ); + } + + public StandardDoorBlock(long config, BlockBehaviour.Properties properties) { + this( + config, properties, + Auxiliaries.getPixeledAABB(13, 0, 0, 16, 16, 16), + Auxiliaries.getPixeledAABB(0, 0, 13, 16, 16, 16), + SoundEvents.WOODEN_DOOR_OPEN, + SoundEvents.WOODEN_DOOR_CLOSE, + BlockSetType.OAK + ); + } + + @Override + public long config() { + return config_; + } + + protected void sound(BlockGetter world, BlockPos pos, boolean open) { + if (world instanceof Level) + ((Level) world).playSound(null, pos, open ? open_sound_ : close_sound_, SoundSource.BLOCKS, 0.7f, 1f); + } + + protected void actuate_adjacent_wing(BlockState state, BlockGetter world_ro, BlockPos pos, boolean open) { + if (!(world_ro instanceof final Level world)) return; + final BlockPos adjecent_pos = pos.relative((state.getValue(HINGE) == DoorHingeSide.LEFT) ? (state.getValue(FACING).getClockWise()) : (state.getValue(FACING).getCounterClockWise())); + if (!world.isLoaded(adjecent_pos)) return; + BlockState adjacent_state = world.getBlockState(adjecent_pos); + if (adjacent_state.getBlock() != this) return; + if (adjacent_state.getValue(OPEN) == open) return; + world.setBlock(adjecent_pos, adjacent_state.setValue(OPEN, open), 2 | 10); + } + + @Override + @OnlyIn(Dist.CLIENT) + public void appendHoverText(ItemStack stack, @Nullable BlockGetter world, List tooltip, TooltipFlag flag) { + Auxiliaries.Tooltip.addInformation(stack, world, tooltip, flag, true); + } + + + @Override + public boolean isPossibleToRespawnInThis(BlockState p_279289_) { + return false; + } + + @Override + public boolean isValidSpawn(BlockState state, BlockGetter world, BlockPos pos, SpawnPlacements.Type type, @Nullable EntityType entityType) { + return false; + } + + @Override + public VoxelShape getShape(BlockState state, BlockGetter world, BlockPos pos, CollisionContext context) { + return shapes_[state.getValue(FACING).ordinal()][state.getValue(OPEN) ? 1 : 0][state.getValue(HINGE) == DoorHingeSide.RIGHT ? 1 : 0][state.getValue(HALF) == DoubleBlockHalf.UPPER ? 0 : 1]; + } + + @Override + public InteractionResult use(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { + setOpen(player, world, state, pos, !state.getValue(OPEN)); + return InteractionResult.sidedSuccess(world.isClientSide()); + } + + @Override + public void neighborChanged(BlockState state, Level world, BlockPos pos, Block block, BlockPos fromPos, boolean isMoving) { + boolean powered = world.hasNeighborSignal(pos) || world.hasNeighborSignal(pos.relative(state.getValue(HALF) == DoubleBlockHalf.LOWER ? Direction.UP : Direction.DOWN)); + if ((block == this) || (powered == state.getValue(POWERED))) return; + world.setBlock(pos, state.setValue(POWERED, powered).setValue(OPEN, powered), 2); + actuate_adjacent_wing(state, world, pos, powered); + if (powered != state.getValue(OPEN)) sound(world, pos, powered); + } + + @Override + public void setOpen(@Nullable Entity entity, Level world, BlockState state, BlockPos pos, boolean open) { + if (!state.is(this) || (state.getValue(OPEN) == open)) return; + state = state.setValue(OPEN, open); + world.setBlock(pos, state, 2 | 8); + sound(world, pos, open); + actuate_adjacent_wing(state, world, pos, open); + } + +} diff --git a/src/main/java/dev/zontreck/libzontreck/edlibmc/StandardEntityBlocks.java b/src/main/java/dev/zontreck/libzontreck/edlibmc/StandardEntityBlocks.java new file mode 100644 index 0000000..78bf385 --- /dev/null +++ b/src/main/java/dev/zontreck/libzontreck/edlibmc/StandardEntityBlocks.java @@ -0,0 +1,72 @@ +/* + * @file StandardEntityBlocks.java + * @author Stefan Wilhelm (wile) + * @copyright (C) 2020 Stefan Wilhelm + * @license MIT (see https://opensource.org/licenses/MIT) + * + * Common functionality class for blocks with block entities. + */ +package dev.zontreck.libzontreck.edlibmc; + +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.gameevent.GameEventListener; +import net.minecraftforge.common.util.FakePlayer; + +import javax.annotation.Nullable; + + +public class StandardEntityBlocks { + public interface IStandardEntityBlock extends EntityBlock { + + default boolean isBlockEntityTicking(Level world, BlockState state) { + return false; + } + + default InteractionResult useOpenGui(BlockState state, Level world, BlockPos pos, Player player) { + if (world.isClientSide()) return InteractionResult.SUCCESS; + final BlockEntity te = world.getBlockEntity(pos); + if (!(te instanceof MenuProvider) || ((player instanceof FakePlayer))) return InteractionResult.FAIL; + player.openMenu((MenuProvider) te); + return InteractionResult.CONSUME; + } + + @Override + @Nullable + default BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + BlockEntityType tet = Registries.getBlockEntityTypeOfBlock(state.getBlock()); + return (tet == null) ? null : tet.create(pos, state); + } + + @Override + @Nullable + default BlockEntityTicker getTicker(Level world, BlockState state, BlockEntityType te_type) { + return (world.isClientSide || (!isBlockEntityTicking(world, state))) ? (null) : ((Level w, BlockPos p, BlockState s, T te) -> ((StandardBlockEntity) te).tick()); + } + + @Override + @Nullable + default GameEventListener getListener(ServerLevel world, T te) { + return null; + } + } + + public static abstract class StandardBlockEntity extends BlockEntity { + public StandardBlockEntity(BlockEntityType type, BlockPos pos, BlockState state) { + super(type, pos, state); + } + + public void tick() { + } + } + +} diff --git a/src/main/java/dev/zontreck/libzontreck/edlibmc/StandardFenceBlock.java b/src/main/java/dev/zontreck/libzontreck/edlibmc/StandardFenceBlock.java new file mode 100644 index 0000000..0097d76 --- /dev/null +++ b/src/main/java/dev/zontreck/libzontreck/edlibmc/StandardFenceBlock.java @@ -0,0 +1,203 @@ +/* + * @file StandardFenceBlock.java + * @author Stefan Wilhelm (wile) + * @copyright (C) 2020 Stefan Wilhelm + * @license MIT (see https://opensource.org/licenses/MIT) + * + * Wall blocks. + */ +package dev.zontreck.libzontreck.edlibmc; + +import com.google.common.collect.ImmutableMap; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.SpawnPlacements; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.FenceGateBlock; +import net.minecraft.world.level.block.WallBlock; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.level.block.state.properties.EnumProperty; +import net.minecraft.world.level.block.state.properties.WallSide; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.material.Fluids; +import net.minecraft.world.level.material.PushReaction; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.Map; + + +public class StandardFenceBlock extends WallBlock implements StandardBlocks.IStandardBlock { + public static final BooleanProperty UP = BlockStateProperties.UP; + public static final EnumProperty WALL_EAST = BlockStateProperties.EAST_WALL; + public static final EnumProperty WALL_NORTH = BlockStateProperties.NORTH_WALL; + public static final EnumProperty WALL_SOUTH = BlockStateProperties.SOUTH_WALL; + public static final EnumProperty WALL_WEST = BlockStateProperties.WEST_WALL; + public static final BooleanProperty WATERLOGGED = BlockStateProperties.WATERLOGGED; + private final Map shape_voxels; + private final Map collision_shape_voxels; + private final long config; + + public StandardFenceBlock(long config, BlockBehaviour.Properties properties) { + this(config, properties, 1.5, 16, 1.5, 0, 14, 16); + } + + public StandardFenceBlock(long config, BlockBehaviour.Properties properties, double pole_width, double pole_height, double side_width, double side_min_y, double side_max_low_y, double side_max_tall_y) { + super(properties); + shape_voxels = buildShapes(pole_width, pole_height, side_width, side_min_y, side_max_low_y, side_max_tall_y); + collision_shape_voxels = buildShapes(pole_width, 24, pole_width, 0, 24, 24); + this.config = config; + } + + @Override + public long config() { + return config; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void appendHoverText(ItemStack stack, @Nullable BlockGetter world, List tooltip, TooltipFlag flag) { + Auxiliaries.Tooltip.addInformation(stack, world, tooltip, flag, true); + } + + private static VoxelShape combinedShape(VoxelShape pole, WallSide height, VoxelShape low, VoxelShape high) { + if (height == WallSide.TALL) return Shapes.or(pole, high); + if (height == WallSide.LOW) return Shapes.or(pole, low); + return pole; + } + + protected Map buildShapes(double pole_width, double pole_height, double side_width, double side_min_y, double side_max_low_y, double side_max_tall_y) { + final double px0 = 8.0 - pole_width, px1 = 8.0 + pole_width, sx0 = 8.0 - side_width, sx1 = 8.0 + side_width; + VoxelShape vp = Block.box(px0, 0, px0, px1, pole_height, px1); + VoxelShape vs1 = Block.box(sx0, side_min_y, 0, sx1, side_max_low_y, sx1); + VoxelShape vs2 = Block.box(sx0, side_min_y, sx0, sx1, side_max_low_y, 16); + VoxelShape vs3 = Block.box(0, side_min_y, sx0, sx1, side_max_low_y, sx1); + VoxelShape vs4 = Block.box(sx0, side_min_y, sx0, 16, side_max_low_y, sx1); + VoxelShape vs5 = Block.box(sx0, side_min_y, 0, sx1, side_max_tall_y, sx1); + VoxelShape vs6 = Block.box(sx0, side_min_y, sx0, sx1, side_max_tall_y, 16); + VoxelShape vs7 = Block.box(0, side_min_y, sx0, sx1, side_max_tall_y, sx1); + VoxelShape vs8 = Block.box(sx0, side_min_y, sx0, 16, side_max_tall_y, sx1); + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (Boolean up : UP.getPossibleValues()) { + for (WallSide wh_east : WALL_EAST.getPossibleValues()) { + for (WallSide wh_north : WALL_NORTH.getPossibleValues()) { + for (WallSide wh_west : WALL_WEST.getPossibleValues()) { + for (WallSide wh_south : WALL_SOUTH.getPossibleValues()) { + VoxelShape shape = Shapes.empty(); + shape = combinedShape(shape, wh_east, vs4, vs8); + shape = combinedShape(shape, wh_west, vs3, vs7); + shape = combinedShape(shape, wh_north, vs1, vs5); + shape = combinedShape(shape, wh_south, vs2, vs6); + if (up) shape = Shapes.or(shape, vp); + BlockState bs = defaultBlockState().setValue(UP, up) + .setValue(WALL_EAST, wh_east) + .setValue(WALL_NORTH, wh_north) + .setValue(WALL_WEST, wh_west) + .setValue(WALL_SOUTH, wh_south); + builder.put(bs.setValue(WATERLOGGED, false), shape); + builder.put(bs.setValue(WATERLOGGED, true), shape); + } + } + } + } + } + return builder.build(); + } + + @Override + public VoxelShape getShape(BlockState state, BlockGetter world, BlockPos pos, CollisionContext selectionContext) { + return shape_voxels.getOrDefault(state, Shapes.block()); + } + + @Override + public VoxelShape getCollisionShape(BlockState state, BlockGetter world, BlockPos pos, CollisionContext selectionContext) { + return collision_shape_voxels.getOrDefault(state, Shapes.block()); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + super.createBlockStateDefinition(builder); + } + + protected boolean attachesTo(BlockState facingState, LevelReader world, BlockPos facingPos, Direction side) { + final Block block = facingState.getBlock(); + if ((block instanceof FenceGateBlock) || (block instanceof StandardFenceBlock) || (block instanceof VariantWallBlock)) + return true; + final BlockState oppositeState = world.getBlockState(facingPos.relative(side, 2)); + if (!(oppositeState.getBlock() instanceof StandardFenceBlock)) return false; + return facingState.isRedstoneConductor(world, facingPos) && canSupportCenter(world, facingPos, side); + } + + protected WallSide selectWallHeight(LevelReader world, BlockPos pos, Direction direction) { + return WallSide.LOW; // @todo: implement + } + + public BlockState getStateForPlacement(BlockPlaceContext context) { + LevelReader world = context.getLevel(); + BlockPos pos = context.getClickedPos(); + FluidState fs = context.getLevel().getFluidState(context.getClickedPos()); + boolean n = attachesTo(world.getBlockState(pos.north()), world, pos.north(), Direction.SOUTH); + boolean e = attachesTo(world.getBlockState(pos.east()), world, pos.east(), Direction.WEST); + boolean s = attachesTo(world.getBlockState(pos.south()), world, pos.south(), Direction.NORTH); + boolean w = attachesTo(world.getBlockState(pos.west()), world, pos.west(), Direction.EAST); + boolean not_straight = (!n || !s || e || w) && (n || s || !e || !w); + return defaultBlockState() + .setValue(UP, not_straight) + .setValue(WALL_NORTH, n ? selectWallHeight(world, pos, Direction.NORTH) : WallSide.NONE) + .setValue(WALL_EAST, e ? selectWallHeight(world, pos, Direction.EAST) : WallSide.NONE) + .setValue(WALL_SOUTH, s ? selectWallHeight(world, pos, Direction.SOUTH) : WallSide.NONE) + .setValue(WALL_WEST, w ? selectWallHeight(world, pos, Direction.WEST) : WallSide.NONE) + .setValue(WATERLOGGED, fs.getType() == Fluids.WATER); + } + + @Override + public BlockState updateShape(BlockState state, Direction side, BlockState facingState, LevelAccessor world, BlockPos pos, BlockPos facingPos) { + if (state.getValue(BlockStateProperties.WATERLOGGED)) + world.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(world)); + if (side == Direction.DOWN) return super.updateShape(state, side, facingState, world, pos, facingPos); + boolean n = (side == Direction.NORTH) ? attachesTo(facingState, world, facingPos, side) : (state.getValue(WALL_NORTH) != WallSide.NONE); + boolean e = (side == Direction.EAST) ? attachesTo(facingState, world, facingPos, side) : (state.getValue(WALL_EAST) != WallSide.NONE); + boolean s = (side == Direction.SOUTH) ? attachesTo(facingState, world, facingPos, side) : (state.getValue(WALL_SOUTH) != WallSide.NONE); + boolean w = (side == Direction.WEST) ? attachesTo(facingState, world, facingPos, side) : (state.getValue(WALL_WEST) != WallSide.NONE); + boolean not_straight = (!n || !s || e || w) && (n || s || !e || !w); + return state.setValue(UP, not_straight) + .setValue(WALL_NORTH, n ? selectWallHeight(world, pos, Direction.NORTH) : WallSide.NONE) + .setValue(WALL_EAST, e ? selectWallHeight(world, pos, Direction.EAST) : WallSide.NONE) + .setValue(WALL_SOUTH, s ? selectWallHeight(world, pos, Direction.SOUTH) : WallSide.NONE) + .setValue(WALL_WEST, w ? selectWallHeight(world, pos, Direction.WEST) : WallSide.NONE); + } + + @Override + public boolean isValidSpawn(BlockState state, BlockGetter world, BlockPos pos, SpawnPlacements.Type type, @Nullable EntityType entityType) { + return false; + } + + + @Override + public boolean isPossibleToRespawnInThis(BlockState p_279289_) { + return false; + } + + @Override + @SuppressWarnings("deprecation") + public PushReaction getPistonPushReaction(BlockState state) { + return PushReaction.NORMAL; + } +} diff --git a/src/main/java/dev/zontreck/libzontreck/edlibmc/StandardStairsBlock.java b/src/main/java/dev/zontreck/libzontreck/edlibmc/StandardStairsBlock.java new file mode 100644 index 0000000..64a9f05 --- /dev/null +++ b/src/main/java/dev/zontreck/libzontreck/edlibmc/StandardStairsBlock.java @@ -0,0 +1,59 @@ +/* + * @file StandardStairsBlock.java + * @author Stefan Wilhelm (wile) + * @copyright (C) 2020 Stefan Wilhelm + * @license MIT (see https://opensource.org/licenses/MIT) + * + * Stairs and roof blocks, almost entirely based on vanilla stairs. + */ +package dev.zontreck.libzontreck.edlibmc; + +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.SpawnPlacements; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.block.StairBlock; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.PushReaction; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import javax.annotation.Nullable; +import java.util.List; + + +public class StandardStairsBlock extends StairBlock implements StandardBlocks.IStandardBlock { + private final long config; + + public StandardStairsBlock(long config, java.util.function.Supplier state, BlockBehaviour.Properties properties) { + super(state, properties); + this.config = config; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void appendHoverText(ItemStack stack, @Nullable BlockGetter world, List tooltip, TooltipFlag flag) { + Auxiliaries.Tooltip.addInformation(stack, world, tooltip, flag, true); + } + + + @Override + public boolean isPossibleToRespawnInThis(BlockState p_279289_) { + return false; + } + + @Override + public boolean isValidSpawn(BlockState state, BlockGetter world, BlockPos pos, SpawnPlacements.Type type, @Nullable EntityType entityType) { + return false; + } + + @Override + @SuppressWarnings("deprecation") + public PushReaction getPistonPushReaction(BlockState state) { + return PushReaction.NORMAL; + } +} diff --git a/src/main/java/dev/zontreck/libzontreck/edlibmc/TooltipDisplay.java b/src/main/java/dev/zontreck/libzontreck/edlibmc/TooltipDisplay.java new file mode 100644 index 0000000..76eb760 --- /dev/null +++ b/src/main/java/dev/zontreck/libzontreck/edlibmc/TooltipDisplay.java @@ -0,0 +1,130 @@ +/* + * @file Tooltip.java + * @author Stefan Wilhelm (wile) + * @copyright (C) 2020 Stefan Wilhelm + * @license MIT (see https://opensource.org/licenses/MIT) + * + * Delayed tooltip for a selected area. Constructed with a + * GUI, invoked in `render()`. + */ +package dev.zontreck.libzontreck.edlibmc; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.network.chat.*; +import net.minecraft.util.Mth; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Supplier; + + +@OnlyIn(Dist.CLIENT) +public class TooltipDisplay { + private static long default_delay = 800; + private static int default_max_deviation = 1; + + public static void config(long delay, int max_deviation) { + default_delay = clamp(delay, 500, 5000); + default_max_deviation = Mth.clamp(max_deviation, 1, 5); + } + private static long clamp(long p1, long a, long b) { + return p1 < a ? a : Math.min(p1, b); + } + + // --------------------------------------------------------------------------------------------------- + + public static class TipRange { + public final int x0, y0, x1, y1; + public final Supplier text; + + public TipRange(int x, int y, int w, int h, Component text) { + this(x, y, w, h, () -> text); + } + + public TipRange(int x, int y, int w, int h, Supplier text) { + this.text = text; + this.x0 = x; + this.y0 = y; + this.x1 = x0 + w - 1; + this.y1 = y0 + h - 1; + } + + } + + // --------------------------------------------------------------------------------------------------- + + private List ranges = new ArrayList<>(); + private long delay = default_delay; + private int max_deviation = default_max_deviation; + private int x_last, y_last; + private long t; + private static boolean had_render_exception = false; + + public TooltipDisplay() { + t = System.currentTimeMillis(); + } + + public TooltipDisplay init(List ranges, long delay_ms, int max_deviation_xy) { + this.ranges = ranges; + this.delay = delay_ms; + this.max_deviation = max_deviation_xy; + t = System.currentTimeMillis(); + x_last = y_last = 0; + return this; + } + + public TooltipDisplay init(List ranges) { + return init(ranges, default_delay, default_max_deviation); + } + + public TooltipDisplay init(TipRange... ranges) { + return init(Arrays.asList(ranges), default_delay, default_max_deviation); + } + + public TooltipDisplay delay(int ms) { + delay = (ms <= 0) ? default_delay : ms; + return this; + } + + public void resetTimer() { + t = System.currentTimeMillis(); + } + + public boolean render(GuiGraphics mx, final AbstractContainerScreen gui, int x, int y) { + if (had_render_exception) return false; + if ((Math.abs(x - x_last) > max_deviation) || (Math.abs(y - y_last) > max_deviation)) { + x_last = x; + y_last = y; + resetTimer(); + return false; + } else if (Math.abs(System.currentTimeMillis() - t) < delay) { + return false; + } else if (ranges.stream().noneMatch( + (tip) -> { + if ((x < tip.x0) || (x > tip.x1) || (y < tip.y0) || (y > tip.y1)) return false; + String text = tip.text.get().toString(); + if (text.isEmpty()) return false; + try { + mx.renderComponentTooltip(Minecraft.getInstance().font, tip.text.get().toFlatList(Style.EMPTY), x, y); + } catch (Exception ex) { + had_render_exception = true; + Auxiliaries.logError("Tooltip rendering disabled due to exception: '" + ex.getMessage() + "'"); + return false; + } + return true; + }) + ) { + resetTimer(); + return false; + } else { + return true; + } + } +} diff --git a/src/main/java/dev/zontreck/libzontreck/edlibmc/VariantSlabBlock.java b/src/main/java/dev/zontreck/libzontreck/edlibmc/VariantSlabBlock.java new file mode 100644 index 0000000..4f2eeb7 --- /dev/null +++ b/src/main/java/dev/zontreck/libzontreck/edlibmc/VariantSlabBlock.java @@ -0,0 +1,220 @@ +/* + * @file VariantSlabBlock.java + * @author Stefan Wilhelm (wile) + * @copyright (C) 2020 Stefan Wilhelm + * @license MIT (see https://opensource.org/licenses/MIT) + * + * Standard half block horizontal slab characteristics class. + */ +package dev.zontreck.libzontreck.edlibmc; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.network.chat.Component; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.SpawnPlacements; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.SoundType; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.EnumProperty; +import net.minecraft.world.level.block.state.properties.IntegerProperty; +import net.minecraft.world.level.block.state.properties.SlabType; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + + +public class VariantSlabBlock extends StandardBlocks.WaterLoggable implements StandardBlocks.IStandardBlock { + public static final EnumProperty TYPE = BlockStateProperties.SLAB_TYPE; + public static final IntegerProperty TEXTURE_VARIANT = IntegerProperty.create("tvariant", 0, 3); + + protected static final VoxelShape[] AABBs = { + Shapes.create(new AABB(0, 8. / 16, 0, 1, 16. / 16, 1)), // top slab + Shapes.create(new AABB(0, 0. / 16, 0, 1, 8. / 16, 1)), // bottom slab + Shapes.create(new AABB(0, 0. / 16, 0, 1, 16. / 16, 1)), // both slabs + Shapes.create(new AABB(0, 0. / 16, 0, 1, 16. / 16, 1)) // << 2bit fill + }; + protected static final int[] num_slabs_contained_in_parts_ = {1, 1, 2, 2}; + private static boolean with_pickup = false; + + public static void on_config(boolean direct_slab_pickup) { + with_pickup = direct_slab_pickup; + } + + protected boolean is_cube(BlockState state) { + return state.getValue(TYPE) == SlabType.DOUBLE; + } + + public VariantSlabBlock(long config, BlockBehaviour.Properties builder) { + super(config, builder); + registerDefaultState(defaultBlockState().setValue(TYPE, SlabType.BOTTOM)); + } + + @Override + public RenderTypeHint getRenderTypeHint() { + return (((config & StandardBlocks.CFG_TRANSLUCENT) != 0) ? (RenderTypeHint.TRANSLUCENT) : (RenderTypeHint.CUTOUT)); + } + + @Override + @OnlyIn(Dist.CLIENT) + public void appendHoverText(ItemStack stack, @Nullable BlockGetter world, List tooltip, TooltipFlag flag) { + if (!Auxiliaries.Tooltip.addInformation(stack, world, tooltip, flag, true)) return; + if (with_pickup && Auxiliaries.Tooltip.helpCondition()) + Auxiliaries.Tooltip.addInformation("engineersdecor.tooltip.slabpickup", "engineersdecor.tooltip.slabpickup", tooltip, flag, true); + } + + @Override + @OnlyIn(Dist.CLIENT) + @SuppressWarnings("deprecation") + public boolean skipRendering(BlockState state, BlockState adjacentBlockState, Direction side) { + return (adjacentBlockState == state) || (super.skipRendering(state, adjacentBlockState, side)); + } + + @Override + public boolean isPossibleToRespawnInThis(BlockState state) { + return false; + } + + @Override + public boolean isValidSpawn(BlockState state, BlockGetter world, BlockPos pos, SpawnPlacements.Type type, @Nullable EntityType entityType) { + return false; + } + + @Override + public VoxelShape getShape(BlockState state, BlockGetter source, BlockPos pos, CollisionContext selectionContext) { + return AABBs[state.getValue(TYPE).ordinal() & 0x3]; + } + + @Override + public VoxelShape getCollisionShape(BlockState state, BlockGetter world, BlockPos pos, CollisionContext selectionContext) { + return getShape(state, world, pos, selectionContext); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + super.createBlockStateDefinition(builder); + builder.add(TYPE, TEXTURE_VARIANT); + } + + @Override + @Nullable + public BlockState getStateForPlacement(BlockPlaceContext context) { + BlockPos pos = context.getClickedPos(); + if (context.getLevel().getBlockState(pos).getBlock() == this) + return context.getLevel().getBlockState(pos).setValue(TYPE, SlabType.DOUBLE).setValue(WATERLOGGED, false); + final int rnd = Mth.clamp((int) (Mth.getSeed(context.getClickedPos()) & 0x3), 0, 3); + final Direction face = context.getClickedFace(); + final BlockState placement_state = super.getStateForPlacement(context).setValue(TEXTURE_VARIANT, rnd); // fluid state + if (face == Direction.UP) return placement_state.setValue(TYPE, SlabType.BOTTOM); + if (face == Direction.DOWN) return placement_state.setValue(TYPE, SlabType.TOP); + if (!face.getAxis().isHorizontal()) return placement_state; + final boolean isupper = ((context.getClickLocation().y() - context.getClickedPos().getY()) > 0.5); + return placement_state.setValue(TYPE, isupper ? SlabType.TOP : SlabType.BOTTOM); + } + + @Override + @SuppressWarnings("deprecation") + public boolean canBeReplaced(BlockState state, BlockPlaceContext context) { + if (context.getItemInHand().getItem() != this.asItem()) return false; + if (!context.replacingClickedOnBlock()) return true; + final Direction face = context.getClickedFace(); + final SlabType type = state.getValue(TYPE); + if ((face == Direction.UP) && (type == SlabType.BOTTOM)) return true; + if ((face == Direction.DOWN) && (type == SlabType.TOP)) return true; + if (!face.getAxis().isHorizontal()) return false; + final boolean isupper = ((context.getClickLocation().y() - context.getClickedPos().getY()) > 0.5); + return isupper ? (type == SlabType.BOTTOM) : (type == SlabType.TOP); + } + + @Override + @SuppressWarnings("deprecation") + public BlockState rotate(BlockState state, Rotation rot) { + return state; + } + + @Override + @SuppressWarnings("deprecation") + public BlockState mirror(BlockState state, Mirror mirrorIn) { + return state; + } + + @Override + public boolean hasDynamicDropList() { + return true; + } + + @Override + public List dropList(BlockState state, Level world, BlockEntity te, boolean explosion) { + return new ArrayList<>(Collections.singletonList(new ItemStack(this.asItem(), num_slabs_contained_in_parts_[state.getValue(TYPE).ordinal() & 0x3]))); + } + + @Override + @SuppressWarnings("deprecation") + public void attack(BlockState state, Level world, BlockPos pos, Player player) { + if ((world.isClientSide) || (!with_pickup)) return; + final ItemStack stack = player.getMainHandItem(); + if (stack.isEmpty() || (Block.byItem(stack.getItem()) != this)) return; + if (stack.getCount() >= stack.getMaxStackSize()) return; + Vec3 lv = player.getLookAngle(); + Direction facing = Direction.getNearest((float) lv.x, (float) lv.y, (float) lv.z); + if ((facing != Direction.UP) && (facing != Direction.DOWN)) return; + if (state.getBlock() != this) return; + SlabType type = state.getValue(TYPE); + if (facing == Direction.DOWN) { + if (type == SlabType.DOUBLE) { + world.setBlock(pos, state.setValue(TYPE, SlabType.BOTTOM), 3); + } else { + world.removeBlock(pos, false); + } + } else if (facing == Direction.UP) { + if (type == SlabType.DOUBLE) { + world.setBlock(pos, state.setValue(TYPE, SlabType.TOP), 3); + } else { + world.removeBlock(pos, false); + } + } + if (!player.isCreative()) { + stack.grow(1); + if (player.getInventory() != null) player.getInventory().setChanged(); + } + SoundType st = this.getSoundType(state, world, pos, null); + world.playSound(player, pos, st.getPlaceSound(), SoundSource.BLOCKS, (st.getVolume() + 1f) / 2.5f, 0.9f * st.getPitch()); + } + + @Override + public boolean placeLiquid(LevelAccessor world, BlockPos pos, BlockState state, FluidState fluidState) { + return (state.getValue(TYPE) != SlabType.DOUBLE) && super.placeLiquid(world, pos, state, fluidState); + } + + @Override + public boolean canPlaceLiquid(BlockGetter world, BlockPos pos, BlockState state, Fluid fluid) { + return (state.getValue(TYPE) != SlabType.DOUBLE) && super.canPlaceLiquid(world, pos, state, fluid); + } + +} diff --git a/src/main/java/dev/zontreck/libzontreck/edlibmc/VariantWallBlock.java b/src/main/java/dev/zontreck/libzontreck/edlibmc/VariantWallBlock.java new file mode 100644 index 0000000..92af666 --- /dev/null +++ b/src/main/java/dev/zontreck/libzontreck/edlibmc/VariantWallBlock.java @@ -0,0 +1,200 @@ +/* + * @file VariantWallBlock.java + * @author Stefan Wilhelm (wile) + * @copyright (C) 2020 Stefan Wilhelm + * @license MIT (see https://opensource.org/licenses/MIT) + * + * Wall blocks. + */ +package dev.zontreck.libzontreck.edlibmc; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMap.Builder; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.network.chat.Component; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.SpawnPlacements; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.FenceGateBlock; +import net.minecraft.world.level.block.WallBlock; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.*; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.material.Fluids; +import net.minecraft.world.level.material.PushReaction; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.Map; + + +public class VariantWallBlock extends WallBlock implements StandardBlocks.IStandardBlock { + public static final BooleanProperty UP = BlockStateProperties.UP; + public static final EnumProperty WALL_EAST = BlockStateProperties.EAST_WALL; + public static final EnumProperty WALL_NORTH = BlockStateProperties.NORTH_WALL; + public static final EnumProperty WALL_SOUTH = BlockStateProperties.SOUTH_WALL; + public static final EnumProperty WALL_WEST = BlockStateProperties.WEST_WALL; + public static final BooleanProperty WATERLOGGED = BlockStateProperties.WATERLOGGED; + public static final IntegerProperty TEXTURE_VARIANT = IntegerProperty.create("tvariant", 0, 7); + private final Map shape_voxels; + private final Map collision_shape_voxels; + private final long config; + + public VariantWallBlock(long config, BlockBehaviour.Properties builder) { + super(builder); + shape_voxels = buildWallShapes(4, 16, 4, 0, 16, 16); + collision_shape_voxels = buildWallShapes(6, 16, 5, 0, 24, 24); + this.config = config; + } + + @Override + public long config() { + return config; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void appendHoverText(ItemStack stack, @Nullable BlockGetter world, List tooltip, TooltipFlag flag) { + Auxiliaries.Tooltip.addInformation(stack, world, tooltip, flag, true); + } + + private static VoxelShape combinedShape(VoxelShape pole, WallSide height, VoxelShape low, VoxelShape high) { + if (height == WallSide.TALL) return Shapes.or(pole, high); + if (height == WallSide.LOW) return Shapes.or(pole, low); + return pole; + } + + protected Map buildWallShapes(double pole_width, double pole_height, double side_width, double side_min_y, double side_max_low_y, double side_max_tall_y) { + final double px0 = 8.0 - pole_width, px1 = 8.0 + pole_width, sx0 = 8.0 - side_width, sx1 = 8.0 + side_width; + VoxelShape vp = Block.box(px0, 0, px0, px1, pole_height, px1); + VoxelShape vs1 = Block.box(sx0, side_min_y, 0, sx1, side_max_low_y, sx1); + VoxelShape vs2 = Block.box(sx0, side_min_y, sx0, sx1, side_max_low_y, 16); + VoxelShape vs3 = Block.box(0, side_min_y, sx0, sx1, side_max_low_y, sx1); + VoxelShape vs4 = Block.box(sx0, side_min_y, sx0, 16, side_max_low_y, sx1); + VoxelShape vs5 = Block.box(sx0, side_min_y, 0, sx1, side_max_tall_y, sx1); + VoxelShape vs6 = Block.box(sx0, side_min_y, sx0, sx1, side_max_tall_y, 16); + VoxelShape vs7 = Block.box(0, side_min_y, sx0, sx1, side_max_tall_y, sx1); + VoxelShape vs8 = Block.box(sx0, side_min_y, sx0, 16, side_max_tall_y, sx1); + Builder builder = ImmutableMap.builder(); + for (Boolean up : UP.getPossibleValues()) { + for (WallSide wh_east : WALL_EAST.getPossibleValues()) { + for (WallSide wh_north : WALL_NORTH.getPossibleValues()) { + for (WallSide wh_west : WALL_WEST.getPossibleValues()) { + for (WallSide wh_south : WALL_SOUTH.getPossibleValues()) { + VoxelShape shape = Shapes.empty(); + shape = combinedShape(shape, wh_east, vs4, vs8); + shape = combinedShape(shape, wh_west, vs3, vs7); + shape = combinedShape(shape, wh_north, vs1, vs5); + shape = combinedShape(shape, wh_south, vs2, vs6); + if (up) shape = Shapes.or(shape, vp); + BlockState bs = defaultBlockState().setValue(UP, up) + .setValue(WALL_EAST, wh_east) + .setValue(WALL_NORTH, wh_north) + .setValue(WALL_WEST, wh_west) + .setValue(WALL_SOUTH, wh_south); + final VoxelShape tvs = shape; + TEXTURE_VARIANT.getPossibleValues().forEach((tv) -> { + builder.put(bs.setValue(TEXTURE_VARIANT, tv).setValue(WATERLOGGED, false), tvs); + builder.put(bs.setValue(TEXTURE_VARIANT, tv).setValue(WATERLOGGED, true), tvs); + }); + } + } + } + } + } + return builder.build(); + } + + @Override + public VoxelShape getShape(BlockState state, BlockGetter world, BlockPos pos, CollisionContext selectionContext) { + return shape_voxels.getOrDefault(state, Shapes.block()); + } + + @Override + public VoxelShape getCollisionShape(BlockState state, BlockGetter world, BlockPos pos, CollisionContext selectionContext) { + return collision_shape_voxels.getOrDefault(state, Shapes.block()); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + super.createBlockStateDefinition(builder); + builder.add(TEXTURE_VARIANT); + } + + protected boolean attachesTo(BlockState facingState, LevelReader world, BlockPos facingPos, Direction side) { + final Block block = facingState.getBlock(); + if ((block instanceof FenceGateBlock) || (block instanceof WallBlock)) return true; + final BlockState oppositeState = world.getBlockState(facingPos.relative(side, 2)); + if (!(oppositeState.getBlock() instanceof VariantWallBlock)) return false; + return facingState.isRedstoneConductor(world, facingPos) && Block.canSupportCenter(world, facingPos, side); + } + + protected WallSide selectWallHeight(LevelReader world, BlockPos pos, Direction direction) { + return WallSide.LOW; + } + + public BlockState getStateForPlacement(BlockPlaceContext context) { + LevelReader world = context.getLevel(); + BlockPos pos = context.getClickedPos(); + FluidState fs = context.getLevel().getFluidState(context.getClickedPos()); + boolean n = attachesTo(world.getBlockState(pos.north()), world, pos.north(), Direction.SOUTH); + boolean e = attachesTo(world.getBlockState(pos.east()), world, pos.east(), Direction.WEST); + boolean s = attachesTo(world.getBlockState(pos.south()), world, pos.south(), Direction.NORTH); + boolean w = attachesTo(world.getBlockState(pos.west()), world, pos.west(), Direction.EAST); + boolean not_straight = (!n || !s || e || w) && (n || s || !e || !w); + return defaultBlockState().setValue(UP, not_straight) + .setValue(WALL_NORTH, n ? selectWallHeight(world, pos, Direction.NORTH) : WallSide.NONE) + .setValue(WALL_EAST, e ? selectWallHeight(world, pos, Direction.EAST) : WallSide.NONE) + .setValue(WALL_SOUTH, s ? selectWallHeight(world, pos, Direction.SOUTH) : WallSide.NONE) + .setValue(WALL_WEST, w ? selectWallHeight(world, pos, Direction.WEST) : WallSide.NONE) + .setValue(WATERLOGGED, fs.getType() == Fluids.WATER); + } + + @Override + public BlockState updateShape(BlockState state, Direction side, BlockState facingState, LevelAccessor world, BlockPos pos, BlockPos facingPos) { + if (state.getValue(WATERLOGGED)) world.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(world)); + if (side == Direction.DOWN) return super.updateShape(state, side, facingState, world, pos, facingPos); + boolean n = (side == Direction.NORTH) ? this.attachesTo(facingState, world, facingPos, side) : state.getValue(WALL_NORTH) != WallSide.NONE; + boolean e = (side == Direction.EAST) ? this.attachesTo(facingState, world, facingPos, side) : state.getValue(WALL_EAST) != WallSide.NONE; + boolean s = (side == Direction.SOUTH) ? this.attachesTo(facingState, world, facingPos, side) : state.getValue(WALL_SOUTH) != WallSide.NONE; + boolean w = (side == Direction.WEST) ? this.attachesTo(facingState, world, facingPos, side) : state.getValue(WALL_WEST) != WallSide.NONE; + boolean not_straight = (!n || !s || e || w) && (n || s || !e || !w); + return state.setValue(UP, not_straight) + .setValue(WALL_NORTH, n ? selectWallHeight(world, pos, Direction.NORTH) : WallSide.NONE) + .setValue(WALL_EAST, e ? selectWallHeight(world, pos, Direction.EAST) : WallSide.NONE) + .setValue(WALL_SOUTH, s ? selectWallHeight(world, pos, Direction.SOUTH) : WallSide.NONE) + .setValue(WALL_WEST, w ? selectWallHeight(world, pos, Direction.WEST) : WallSide.NONE) + .setValue(TEXTURE_VARIANT, ((int) Mth.getSeed(pos)) & 0x7); + } + + @Override + public boolean isValidSpawn(BlockState state, BlockGetter world, BlockPos pos, SpawnPlacements.Type type, @Nullable EntityType entityType) { + return false; + } + + @Override + public boolean isPossibleToRespawnInThis(BlockState state) { + return false; + } + + @Override + @SuppressWarnings("deprecation") + public PushReaction getPistonPushReaction(BlockState state) { + return PushReaction.NORMAL; + } +}