Restock o'Clock

- The stock ticker block can now order packages to refill the inventory below it on a redstone pulse
This commit is contained in:
simibubi 2024-09-13 16:07:33 +02:00
parent a51706633d
commit e0852c53f9
9 changed files with 421 additions and 9 deletions

View file

@ -62,6 +62,7 @@ import com.simibubi.create.content.logistics.packagePort.PackagePortPlacementPac
import com.simibubi.create.content.logistics.stockTicker.LogisticalStockRequestPacket;
import com.simibubi.create.content.logistics.stockTicker.LogisticalStockResponsePacket;
import com.simibubi.create.content.logistics.stockTicker.PackageOrderRequestPacket;
import com.simibubi.create.content.logistics.stockTicker.StockTickerConfigurationPacket;
import com.simibubi.create.content.logistics.tunnel.TunnelFlapPacket;
import com.simibubi.create.content.redstone.displayLink.DisplayLinkConfigurationPacket;
import com.simibubi.create.content.redstone.link.controller.LinkedControllerBindPacket;
@ -177,6 +178,7 @@ public enum AllPackets {
CHAIN_CONVEYOR_CONNECT(ChainConveyorConnectionPacket.class, ChainConveyorConnectionPacket::new, PLAY_TO_SERVER),
CHAIN_CONVEYOR_RIDING(ChainConveyorRidingPacket.class, ChainConveyorRidingPacket::new, PLAY_TO_SERVER),
CHAIN_PACKAGE_INTERACTION(ChainPackageInteractionPacket.class, ChainPackageInteractionPacket::new, PLAY_TO_SERVER),
STOCK_TICKER_CONFIGURATION(StockTickerConfigurationPacket.class, StockTickerConfigurationPacket::new, PLAY_TO_SERVER),
// Server to Client
SYMMETRY_EFFECT(SymmetryEffectPacket.class, SymmetryEffectPacket::new, PLAY_TO_CLIENT),

View file

@ -520,4 +520,27 @@ public class PackagerBlockEntity extends SmartBlockEntity {
return animationTicks >= CYCLE / 2 ? ItemStack.EMPTY : heldBox;
}
public boolean isTargetingSameInventory(IItemHandler inventory) {
IItemHandler myInventory = targetInventory.getInventory();
if (myInventory == null || inventory == null)
return false;
if (myInventory == inventory)
return true;
// If a contained ItemStack instance is the same, we can be pretty sure these
// inventories are the same (works for compound inventories)
for (int i = 0; i < inventory.getSlots(); i++) {
ItemStack stackInSlot = inventory.getStackInSlot(i);
if (stackInSlot.isEmpty())
continue;
for (int j = 0; j < myInventory.getSlots(); j++)
if (stackInSlot == myInventory.getStackInSlot(j))
return true;
break;
}
return false;
}
}

View file

@ -30,6 +30,7 @@ import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraftforge.items.IItemHandler;
public class LogisticallyLinkedBehaviour extends BlockEntityBehaviour {
@ -169,9 +170,10 @@ public class LogisticallyLinkedBehaviour extends BlockEntityBehaviour {
}
public int processRequest(ItemStack stack, int amount, String address, int linkIndex, MutableBoolean finalLink,
int orderId, @Nullable PackageOrder orderContext) {
int orderId, @Nullable PackageOrder orderContext, @Nullable IItemHandler ignoredHandler) {
if (blockEntity instanceof PackagerLinkBlockEntity plbe)
return plbe.processRequest(stack, amount, address, linkIndex, finalLink, orderId, orderContext);
return plbe.processRequest(stack, amount, address, linkIndex, finalLink, orderId, orderContext,
ignoredHandler);
return 0;
}

View file

@ -18,6 +18,7 @@ import net.minecraft.core.Direction;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraftforge.items.IItemHandler;
public class PackagerLinkBlockEntity extends LinkWithBulbBlockEntity {
@ -38,10 +39,12 @@ public class PackagerLinkBlockEntity extends LinkWithBulbBlockEntity {
}
public int processRequest(ItemStack stack, int amount, String address, int linkIndex, MutableBoolean finalLink,
int orderId, @Nullable PackageOrder orderContext) {
int orderId, @Nullable PackageOrder orderContext, @Nullable IItemHandler ignoredHandler) {
PackagerBlockEntity packager = getPackager();
if (packager == null || packager.defragmenterActive)
return 0;
if (packager.isTargetingSameInventory(ignoredHandler))
return 0;
InventorySummary summary = packager.getAvailableItems();
int availableCount = summary.getCountOf(stack);

View file

@ -38,7 +38,7 @@ public class PackageOrderRequestPacket extends BlockEntityConfigurationPacket<St
@Override
protected void applySettings(ServerPlayer player, StockTickerBlockEntity be) {
be.receivePackageRequest(order, player, address);
be.broadcastPackageRequest(order, null, address);
}
}

View file

@ -0,0 +1,197 @@
package com.simibubi.create.content.logistics.stockTicker;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import com.mojang.blaze3d.vertex.PoseStack;
import com.simibubi.create.AllPackets;
import com.simibubi.create.foundation.utility.CreateLang;
import net.createmod.catnip.gui.AbstractSimiScreen;
import net.createmod.catnip.gui.element.GuiGameElement;
import net.createmod.catnip.utility.IntAttached;
import net.createmod.catnip.utility.Iterate;
import net.createmod.catnip.utility.lang.Components;
import net.createmod.catnip.utility.theme.Color;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.EditBox;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.util.Mth;
import net.minecraft.world.item.ItemStack;
public class StockTickerAutoRequestScreen extends AbstractSimiScreen {
StockTickerBlockEntity blockEntity;
EditBox addressBox;
Button takeSnapshotButton;
List<Integer> modifiedAmounts;
public StockTickerAutoRequestScreen(StockTickerBlockEntity be) {
super(be.getBlockState()
.getBlock()
.getName());
blockEntity = be;
blockEntity.lastClientsideStockSnapshot = null;
modifiedAmounts = null;
}
private void resetAmounts() {
modifiedAmounts = new ArrayList<>();
for (IntAttached<ItemStack> intAttached : blockEntity.restockAmounts)
modifiedAmounts.add(intAttached.getFirst());
}
@Override
protected void init() {
setWindowSize(256, 128);
super.init();
int x = guiLeft;
int y = guiTop;
takeSnapshotButton = Button.builder(Components.literal("Take Snapshot"), this::onPress)
.bounds(x, y + 44, 100, 20)
.build();
MutableComponent addressLabel = CreateLang.translateDirect("gui.stock_ticker.package_adress");
boolean initial = addressBox == null;
addressBox = new EditBox(this.font, x, y + 80, 120, 9, addressLabel);
addressBox.setMaxLength(50);
addressBox.setBordered(false);
addressBox.setTextColor(0xffffff);
if (initial)
addressBox.setValue(blockEntity.restockAddress);
addRenderableWidget(addressBox);
addRenderableWidget(takeSnapshotButton);
}
private void onPress(Button button) {
if (button == takeSnapshotButton) {
AllPackets.getChannel()
.sendToServer(
new StockTickerConfigurationPacket(blockEntity.getBlockPos(), true, "", Collections.emptyList()));
modifiedAmounts = null;
}
}
@Override
public void removed() {
if (modifiedAmounts != null)
for (int i = 0; i < blockEntity.restockAmounts.size(); i++)
blockEntity.restockAmounts.get(i)
.setFirst(modifiedAmounts.get(i));
AllPackets.getChannel()
.sendToServer(new StockTickerConfigurationPacket(blockEntity.getBlockPos(), false, addressBox.getValue(),
blockEntity.restockAmounts));
super.removed();
}
final int rows = 8;
final int cols = 8;
final int rowHeight = 18;
final int colWidth = 26;
@Override
protected void renderWindow(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) {
if (modifiedAmounts != null && blockEntity.restockAmounts.size() != modifiedAmounts.size())
resetAmounts();
Color color = new Color(255, 255, 255, 50);
int hoveredSlot = getHoveredSlot(mouseX, mouseY);
// Render some boxes
graphics.renderOutline(addressBox.getX() - 4, addressBox.getY() - 4, addressBox.getWidth() + 12,
addressBox.getHeight() + 7, color.getRGB());
graphics.renderOutline(guiLeft - 3, guiTop - 4 + 20, cols * colWidth + 6, rowHeight + 8, color.getRGB());
// Render text input hints
if (addressBox.getValue()
.isBlank())
graphics.drawString(font, addressBox.getMessage(), addressBox.getX(), addressBox.getY(), 0x88dddddd);
graphics.drawString(font, Components.literal("Target Amounts:"), guiLeft, guiTop, 0x88dddddd);
PoseStack ms = graphics.pose();
for (int i = 0; i < blockEntity.restockAmounts.size(); i++) {
IntAttached<ItemStack> entry = blockEntity.restockAmounts.get(i);
ms.pushPose();
ms.translate(guiLeft + i % cols * colWidth, guiTop + 20 + i / cols * rowHeight, 200);
int customCount = modifiedAmounts == null ? blockEntity.restockAmounts.get(i)
.getFirst() : modifiedAmounts.get(i);
drawItemCount(graphics, customCount, customCount);
ms.translate(0, 0, -200);
float scaleFromHover = hoveredSlot == i ? 1.075f : 1;
ms.translate((colWidth - 18) / 2.0, (rowHeight - 18) / 2.0, 0);
ms.translate(18 / 2.0, 18 / 2.0, 0);
ms.scale(scaleFromHover, scaleFromHover, scaleFromHover);
ms.translate(-18 / 2.0, -18 / 2.0, 0);
GuiGameElement.of(entry.getSecond())
.render(graphics);
ms.popPose();
}
if (hoveredSlot != -1)
graphics.renderTooltip(font, blockEntity.restockAmounts.get(hoveredSlot)
.getValue(), mouseX, mouseY);
}
private int getHoveredSlot(int mouseX, int mouseY) {
if (mouseY < guiTop + 20 || mouseY > guiTop + 20 + rowHeight)
return -1;
int slot = (mouseX - guiLeft) / colWidth;
if (slot >= blockEntity.restockAmounts.size())
return -1;
return Math.max(-1, slot);
}
@Override
public boolean mouseScrolled(double pMouseX, double pMouseY, double pDelta) {
if (modifiedAmounts == null)
resetAmounts();
int hoveredSlot = getHoveredSlot(Mth.floor(pMouseX), Mth.floor(pMouseY));
if (hoveredSlot != -1) {
int amount = modifiedAmounts.get(hoveredSlot);
amount += (hasShiftDown() ? 64 : 1) * Math.signum(pDelta);
if (hasShiftDown())
amount = 64 * (amount / 64);
amount = Math.max(amount, 1);
modifiedAmounts.set(hoveredSlot, amount);
return true;
}
return super.mouseScrolled(pMouseX, pMouseY, pDelta);
}
private void drawItemCount(GuiGraphics graphics, int count, int customCount) {
boolean special = customCount != count;
if (!special && count == 1)
return;
count = customCount;
String text = count >= 1000000 ? (count / 1000000) + "m"
: count >= 10000 ? (count / 1000) + "k"
: count >= 1000 ? ((count * 10) / 1000) / 10f + "k"
: count >= 100 ? count + "" : count > 0 ? " " + count : " \u2714";
int lightOutline = 0x444444;
int darkOutline = 0x222222;
int middleColor = special ? 0xaaffaa : 0xdddddd;
for (int xi : Iterate.positiveAndNegative)
graphics.drawString(font, CreateLang.text(text)
.component(), 11 + xi, 10, xi < 0 ? lightOutline : darkOutline, false);
for (int yi : Iterate.positiveAndNegative)
graphics.drawString(font, CreateLang.text(text)
.component(), 11, 10 + yi, yi < 0 ? lightOutline : darkOutline, false);
graphics.drawString(font, CreateLang.text(text)
.component(), 11, 10, middleColor, false);
}
}

View file

@ -6,21 +6,29 @@ import com.simibubi.create.AllPartialModels;
import com.simibubi.create.AllShapes;
import com.simibubi.create.foundation.block.IBE;
import net.createmod.catnip.gui.ScreenOpener;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
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.HorizontalDirectionalBlock;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition.Builder;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.VoxelShape;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.fml.DistExecutor;
public class StockTickerBlock extends HorizontalDirectionalBlock implements IBE<StockTickerBlockEntity> {
@ -42,6 +50,31 @@ public class StockTickerBlock extends HorizontalDirectionalBlock implements IBE<
super.createBlockStateDefinition(pBuilder.add(FACING));
}
@Override
public InteractionResult use(BlockState pState, Level pLevel, BlockPos pPos, Player pPlayer, InteractionHand pHand,
BlockHitResult pHit) {
return onBlockEntityUse(pLevel, pPos, stbe -> {
if (!stbe.observedInventory.hasInventory())
return InteractionResult.PASS;
DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> displayScreen(stbe, pPlayer));
return InteractionResult.SUCCESS;
});
}
@Override
public void neighborChanged(BlockState pState, Level pLevel, BlockPos pPos, Block pNeighborBlock,
BlockPos pNeighborPos, boolean pMovedByPiston) {
if (pLevel.isClientSide())
return;
withBlockEntityDo(pLevel, pPos, StockTickerBlockEntity::onRedstonePowerChanged);
}
@OnlyIn(value = Dist.CLIENT)
protected void displayScreen(StockTickerBlockEntity be, Player player) {
if (player instanceof LocalPlayer)
ScreenOpener.open(new StockTickerAutoRequestScreen(be));
}
@Override
public VoxelShape getShape(BlockState pState, BlockGetter pLevel, BlockPos pPos, CollisionContext pContext) {
return AllShapes.STOCK_TICKER;

View file

@ -6,27 +6,49 @@ import java.util.List;
import org.apache.commons.lang3.mutable.MutableBoolean;
import com.simibubi.create.AllPackets;
import com.simibubi.create.content.logistics.packager.InventorySummary;
import com.simibubi.create.content.logistics.packagerLink.LogisticallyLinkedBehaviour;
import com.simibubi.create.foundation.blockEntity.behaviour.BlockEntityBehaviour;
import com.simibubi.create.foundation.blockEntity.behaviour.inventory.InvManipulationBehaviour;
import net.createmod.catnip.utility.BlockFace;
import net.createmod.catnip.utility.IntAttached;
import net.createmod.catnip.utility.NBTHelper;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.entity.player.Player;
import net.minecraft.nbt.Tag;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraftforge.items.IItemHandler;
public class StockTickerBlockEntity extends StockCheckingBlockEntity {
// Player-interface Feature
protected List<IntAttached<ItemStack>> lastClientsideStockSnapshot;
protected List<IntAttached<ItemStack>> newlyReceivedStockSnapshot;
protected String previouslyUsedAddress;
protected int activeLinks;
// Auto-restock Feature
protected InvManipulationBehaviour observedInventory;
protected List<IntAttached<ItemStack>> restockAmounts;
protected String restockAddress;
protected boolean powered;
public StockTickerBlockEntity(BlockEntityType<?> type, BlockPos pos, BlockState state) {
super(type, pos, state);
previouslyUsedAddress = "";
restockAddress = "";
restockAmounts = new ArrayList<>();
}
@Override
public void addBehaviours(List<BlockEntityBehaviour> behaviours) {
super.addBehaviours(behaviours);
behaviours
.add(observedInventory = new InvManipulationBehaviour(this, (w, p, s) -> new BlockFace(p, Direction.DOWN)));
}
public void refreshClientStockSnapshot() {
@ -53,6 +75,11 @@ public class StockTickerBlockEntity extends StockCheckingBlockEntity {
protected void write(CompoundTag tag, boolean clientPacket) {
super.write(tag, clientPacket);
tag.putString("PreviousAddress", previouslyUsedAddress);
tag.put("RestockAmounts",
NBTHelper.writeCompoundList(restockAmounts, ia -> ia.serializeNBT(ItemStack::serializeNBT)));
tag.putString("RestockAddress", restockAddress);
tag.putBoolean("Powered", powered);
if (clientPacket)
tag.putInt("ActiveLinks", activeLinks);
}
@ -61,10 +88,73 @@ public class StockTickerBlockEntity extends StockCheckingBlockEntity {
protected void read(CompoundTag tag, boolean clientPacket) {
super.read(tag, clientPacket);
previouslyUsedAddress = tag.getString("PreviousAddress");
restockAmounts = NBTHelper.readCompoundList(tag.getList("RestockAmounts", Tag.TAG_COMPOUND),
c -> IntAttached.read(c, ItemStack::of));
restockAddress = tag.getString("RestockAddress");
powered = tag.getBoolean("Powered");
if (clientPacket)
activeLinks = tag.getInt("ActiveLinks");
}
protected void takeInventoryStockSnapshot() {
restockAmounts = new ArrayList<>();
IItemHandler inventory = observedInventory.getInventory();
if (inventory == null)
return;
restockAmounts = summariseObservedInventory().getStacksByCount();
if (restockAmounts.size() > 8)
restockAmounts.subList(8, restockAmounts.size())
.clear();
notifyUpdate();
}
private InventorySummary summariseObservedInventory() {
IItemHandler inventory = observedInventory.getInventory();
if (inventory == null)
return InventorySummary.EMPTY;
InventorySummary inventorySummary = new InventorySummary();
for (int i = 0; i < inventory.getSlots(); i++)
inventorySummary.add(inventory.getStackInSlot(i));
return inventorySummary;
}
protected void onRedstonePowerChanged() {
boolean hasNeighborSignal = level.hasNeighborSignal(worldPosition);
if (powered == hasNeighborSignal)
return;
if (hasNeighborSignal)
triggerRestock();
powered = hasNeighborSignal;
setChanged();
}
protected void triggerRestock() {
if (!observedInventory.hasInventory() || restockAmounts.isEmpty())
return;
InventorySummary presentStock = summariseObservedInventory();
List<IntAttached<ItemStack>> missingItems = new ArrayList<>();
for (IntAttached<ItemStack> required : restockAmounts) {
int diff = required.getFirst() - presentStock.getCountOf(required.getValue());
if (diff > 0)
missingItems.add(IntAttached.with(diff, required.getValue()));
}
if (missingItems.isEmpty())
return;
broadcastPackageRequest(new PackageOrder(missingItems), observedInventory.getInventory(), restockAddress);
}
protected void updateAutoRestockSettings(String address, List<IntAttached<ItemStack>> amounts) {
restockAmounts = amounts;
restockAddress = address;
notifyUpdate();
}
public void receiveStockPacket(List<IntAttached<ItemStack>> stacks, boolean endOfTransmission) {
if (newlyReceivedStockSnapshot == null)
newlyReceivedStockSnapshot = new ArrayList<>();
@ -75,7 +165,7 @@ public class StockTickerBlockEntity extends StockCheckingBlockEntity {
newlyReceivedStockSnapshot = null;
}
public void receivePackageRequest(PackageOrder order, Player player, String address) {
public void broadcastPackageRequest(PackageOrder order, IItemHandler ignoredHandler, String address) {
List<IntAttached<ItemStack>> stacks = order.stacks();
// Packages need to track their index and successors for successful defrag
@ -87,7 +177,7 @@ public class StockTickerBlockEntity extends StockCheckingBlockEntity {
PackageOrder contextToSend = order;
// Packages from future orders should not be merged in the packager queue
int orderId = player.level().random.nextInt();
int orderId = level.random.nextInt();
for (int i = 0; i < stacks.size(); i++) {
IntAttached<ItemStack> entry = stacks.get(i);
@ -103,7 +193,7 @@ public class StockTickerBlockEntity extends StockCheckingBlockEntity {
isFinalLink = finalLinkTracker;
int processedCount = link.processRequest(requestedItem, remainingCount, address, linkIndex, isFinalLink,
orderId, contextToSend);
orderId, contextToSend, ignoredHandler);
if (processedCount > 0 && usedIndex == -1) {
contextToSend = null;
usedLinks.add(link);

View file

@ -0,0 +1,62 @@
package com.simibubi.create.content.logistics.stockTicker;
import java.util.ArrayList;
import java.util.List;
import com.simibubi.create.foundation.networking.BlockEntityConfigurationPacket;
import net.createmod.catnip.utility.IntAttached;
import net.minecraft.core.BlockPos;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.item.ItemStack;
public class StockTickerConfigurationPacket extends BlockEntityConfigurationPacket<StockTickerBlockEntity> {
private boolean takeSnapshot;
private String address;
private List<IntAttached<ItemStack>> amounts;
public StockTickerConfigurationPacket(BlockPos pos, boolean takeSnapshot, String address,
List<IntAttached<ItemStack>> amounts) {
super(pos);
this.takeSnapshot = takeSnapshot;
this.address = address;
this.amounts = amounts;
}
public StockTickerConfigurationPacket(FriendlyByteBuf buffer) {
super(buffer);
}
@Override
protected void writeSettings(FriendlyByteBuf buffer) {
buffer.writeBoolean(takeSnapshot);
buffer.writeUtf(address);
buffer.writeVarInt(amounts.size());
for (IntAttached<ItemStack> intAttached : amounts) {
buffer.writeVarInt(intAttached.getFirst());
buffer.writeItem(intAttached.getValue());
}
}
@Override
protected void readSettings(FriendlyByteBuf buffer) {
takeSnapshot = buffer.readBoolean();
address = buffer.readUtf();
int items = buffer.readVarInt();
amounts = new ArrayList<>();
for (int i = 0; i < items; i++)
amounts.add(IntAttached.with(buffer.readVarInt(), buffer.readItem()));
}
@Override
protected void applySettings(StockTickerBlockEntity be) {
if (takeSnapshot) {
be.takeInventoryStockSnapshot();
return;
}
be.updateAutoRestockSettings(address, amounts);
}
}