Skip to content
126 changes: 126 additions & 0 deletions src/main/java/com/gregtechceu/gtceu/api/misc/StockingHatchList.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package com.gregtechceu.gtceu.api.misc;

import appeng.api.stacks.AEKey;
import lombok.Getter;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

// List of AEStacks, always sorted largest to smallest.
public class StockingHatchList implements Iterable<StockingHatchList.AEStack> {

private final List<AEStack> list;
private final int capacity;

public StockingHatchList(int capacity) {
this.capacity = capacity;
this.list = new ArrayList<>(capacity);
}

public boolean insert(AEKey key, long amount) {
for (int i = 0; i < list.size(); i++) {
AEStack stack = list.get(i);
if (!key.equals(stack.key)) {
continue;
}
if (amount > stack.amount) {
// stack grew, resort the area before it
stack.amount = amount;
if (i > 0 && list.get(i - 1).amount < amount) {
for (int j = 0; j < i; j++) {
if (amount > list.get(j).amount) {
list.add(j, stack);
list.remove(i + 1); // remove old stack which is now shifted
return true;
}
}
}
// didn't need to move, already updated in place
return true;
} else if (amount < stack.amount) {
// stack shrunk, resort the area after it
stack.amount = amount;
if (i + 1 < list.size() && list.get(i + 1).amount > amount) {
for (int j = i + 1; j < list.size(); j++) {
if (amount > list.get(j).amount) {
list.add(j, stack);
list.remove(i); // remove old stack
return true;
}
}
}
// didn't need to move, already updated in place
return true;
}
// no change
return false;
}
// not enough space in list
if (list.size() >= capacity) {
return false;
}
// unseen item
for (int i = 0; i < list.size(); i++) {
AEStack stack = list.get(i);
if (amount > stack.amount) {
list.add(i, new AEStack(key, amount));
return true;
}
}
list.add(new AEStack(key, amount));
return true;
}

public boolean remove(AEKey key) {
for (int i = 0; i < list.size(); i++) {
AEStack stack = list.get(i);
if (stack.key.equals(key)) {
list.remove(i);
return true;
}
}
return false;
}

public boolean contains(AEKey what) {
for (var stack : list) {
if (stack.getKey().equals(what)) return true;
}
return false;
}

@Override
public @NotNull Iterator<AEStack> iterator() {
return list.iterator();
}

public int size() {
return list.size();
}

public void clear() {
list.clear();
}

public StockingHatchList.AEStack get(int index) {
return list.get(index);
}

public static class AEStack {

@Getter
@Setter
public @NotNull AEKey key;
@Getter
@Setter
public long amount;

public AEStack(@NotNull AEKey key, long amount) {
this.key = key;
this.amount = amount;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.gregtechceu.gtceu.api.machine.feature.multiblock.IMultiController;
import com.gregtechceu.gtceu.api.machine.feature.multiblock.IMultiPart;
import com.gregtechceu.gtceu.api.machine.trait.NotifiableItemStackHandler;
import com.gregtechceu.gtceu.api.misc.StockingHatchList;
import com.gregtechceu.gtceu.common.item.IntCircuitBehaviour;
import com.gregtechceu.gtceu.config.ConfigHolder;
import com.gregtechceu.gtceu.integration.ae2.machine.feature.multiblock.IMEStockingPart;
Expand All @@ -33,6 +34,9 @@

import appeng.api.config.Actionable;
import appeng.api.networking.IGrid;
import appeng.api.networking.IGridNodeListener;
import appeng.api.networking.IStackWatcher;
import appeng.api.networking.storage.IStorageWatcherNode;
import appeng.api.stacks.AEItemKey;
import appeng.api.stacks.AEKey;
import appeng.api.stacks.GenericStack;
Expand All @@ -42,15 +46,13 @@
import lombok.Setter;
import org.jetbrains.annotations.Nullable;

import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.function.Predicate;

import javax.annotation.ParametersAreNonnullByDefault;

@ParametersAreNonnullByDefault
@MethodsReturnNonnullByDefault
public class MEStockingBusPartMachine extends MEInputBusPartMachine implements IMEStockingPart {
public class MEStockingBusPartMachine extends MEInputBusPartMachine implements IMEStockingPart, IStorageWatcherNode {

protected static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder(
MEStockingBusPartMachine.class, MEInputBusPartMachine.MANAGED_FIELD_HOLDER);
Expand All @@ -74,9 +76,15 @@ public class MEStockingBusPartMachine extends MEInputBusPartMachine implements I
@Setter
private Predicate<GenericStack> autoPullTest;

private IStackWatcher watcher;

private StockingHatchList topItems = new StockingHatchList(CONFIG_SIZE);
private boolean pendingWatcherSync;

public MEStockingBusPartMachine(IMachineBlockEntity holder, Object... args) {
super(holder, args);
this.autoPullTest = $ -> false;
this.nodeHolder.getMainNode().addService(IStorageWatcherNode.class, this);
}

/////////////////////////////////
Expand All @@ -95,6 +103,14 @@ public void removedFromController(IMultiController controller) {
super.removedFromController(controller);
}

@Override
public void onMainNodeStateChanged(IGridNodeListener.State reason) {
super.onMainNodeStateChanged(reason);
if (autoPull) {
refreshList();
}
}

@Override
protected NotifiableItemStackHandler createInventory(Object... args) {
this.aeItemHandler = new ExportOnlyAEStockingItemList(this, CONFIG_SIZE);
Expand All @@ -113,36 +129,80 @@ public ManagedFieldHolder getFieldHolder() {
@Override
public void autoIO() {
super.autoIO();
if (ticksPerCycle == 0) ticksPerCycle = ConfigHolder.INSTANCE.compat.ae2.updateIntervals; // Emergency Check to
// Avoid Crash loops.
if (ticksPerCycle == 0) {
// Emergency Check to avoid Crash loops.
ticksPerCycle = ConfigHolder.INSTANCE.compat.ae2.updateIntervals;
}
if (getOffsetTimer() % ticksPerCycle == 0) {
if (autoPull) {
refreshList();
syncWatchedStacks();
}
if (pendingWatcherSync) {
pendingWatcherSync = false;
syncWatchedStacks();
}
}

// Refreshes what things are being watched
private void syncWatchedStacks() {
if (watcher == null) return;
watcher.reset();
if (autoPull) {
watcher.setWatchAll(true);
} else {
watcher.setWatchAll(false);
for (ExportOnlyAEItemSlot slot : this.aeItemHandler.getInventory()) {
if (slot.getConfig() == null) continue;
watcher.add(slot.getConfig().what());
}
syncME();
}
}

@Override
protected void syncME() {
// Update the visual display for the fake items. This also is important for the item handler's
// getStackInSlot() method, as it uses the cached items set here.
MEStorage networkInv = this.getMainNode().getGrid().getStorageService().getInventory();
for (ExportOnlyAEItemSlot slot : this.aeItemHandler.getInventory()) {
var config = slot.getConfig();
if (config != null) {
// Try to fill the slot
var key = config.what();
long extracted = networkInv.extract(key, Long.MAX_VALUE, Actionable.SIMULATE, actionSource);
if (extracted >= minStackSize) {
slot.setStock(new GenericStack(key, extracted));
public void updateWatcher(IStackWatcher watcher) {
this.watcher = watcher;
this.syncWatchedStacks();
}

@Override
public void onStackChange(AEKey what, long amount) {
if (!(what instanceof AEItemKey itemKey)) return;
if (autoPull) {
if (autoPullTest != null && !autoPullTest.test(new GenericStack(itemKey, amount))) {
topItems.remove(what);
} else {
if (!topItems.insert(what, amount)) return;
}

syncListToHandler();
} else {
Comment on lines +176 to +177
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Early return instead of an else block.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for (ExportOnlyAEItemSlot slot : this.aeItemHandler.getInventory()) {
if (slot.getConfig() == null) {
slot.setStock(null);
continue;
}
AEKey key = slot.getConfig().what();
if (key.equals(what)) {
if (amount > 0) {
slot.setStock(new GenericStack(key, amount));
} else {
slot.setStock(null);
}
}
}
slot.setStock(null);
}
}

private void syncListToHandler() {
int index;
for (index = 0; index < topItems.size(); index++) {
var entry = topItems.get(index);
var slot = this.aeItemHandler.getInventory()[index];
slot.setConfig(new GenericStack(entry.getKey(), 1));
slot.setStock(new GenericStack(entry.getKey(), entry.getAmount()));
}
aeItemHandler.clearInventory(index);
}

@Override
public void attachSideTabs(TabsWidget sideTabs) {
sideTabs.setMainTab(this); // removes the cover configurator, it's pointless and clashes with layout.
Expand Down Expand Up @@ -217,54 +277,26 @@ private void refreshList() {
MEStorage networkStorage = grid.getStorageService().getInventory();
var counter = networkStorage.getAvailableStacks();

// Use a PriorityQueue to sort the stacks on size, take the first CONFIG_SIZE
// biggest stacks.
PriorityQueue<Object2LongMap.Entry<AEKey>> topItems = new PriorityQueue<>(
Comparator.comparingLong(Object2LongMap.Entry<AEKey>::getLongValue));
topItems.clear();

for (Object2LongMap.Entry<AEKey> entry : counter) {
long amount = entry.getLongValue();
AEKey what = entry.getKey();

if (amount <= 0) continue;
if (!(what instanceof AEItemKey itemKey)) continue;

long request = networkStorage.extract(what, amount, Actionable.SIMULATE, actionSource);
if (request == 0) continue;

// Ensure that it is valid to configure with this stack
if (autoPullTest != null && !autoPullTest.test(new GenericStack(itemKey, amount))) continue;
if (amount >= minStackSize) {
if (topItems.size() < CONFIG_SIZE) {
topItems.offer(entry);
} else if (amount > topItems.peek().getLongValue()) {
topItems.poll();
topItems.offer(entry);
}
}
}

// Now, topItems is a PQ with CONFIG_SIZE highest amount items in the system.
int index;
int itemAmount = topItems.size();
for (index = 0; index < CONFIG_SIZE; index++) {
if (topItems.isEmpty()) break;
Object2LongMap.Entry<AEKey> entry = topItems.poll();

AEKey what = entry.getKey();
long amount = entry.getLongValue();

// If we get here, the item has already been checked by the PQ.
long request = networkStorage.extract(what, amount, Actionable.SIMULATE, actionSource);
if (autoPullTest != null && !autoPullTest.test(new GenericStack(what, amount))) continue;

// Since we want our items to be displayed from highest to lowest, but poll() returns
// the lowest first, we fill in the slots starting at itemAmount-1
var slot = this.aeItemHandler.getInventory()[itemAmount - index - 1];
slot.setConfig(new GenericStack(what, 1));
slot.setStock(new GenericStack(what, request));
topItems.insert(what, amount);
}

aeItemHandler.clearInventory(index);
syncListToHandler();
}

// Expensive, use sparingly.
private long getAmountInNetwork(AEKey key) {
MEStorage networkInv = this.getMainNode().getGrid().getStorageService().getInventory();
long extracted = networkInv.extract(key, Long.MAX_VALUE, Actionable.SIMULATE, actionSource);
return extracted;
}

///////////////////////////////
Expand Down Expand Up @@ -363,6 +395,21 @@ public ExportOnlyAEStockingItemSlot(@Nullable GenericStack config, @Nullable Gen
super(config, stock);
}

@Override
public void setConfig(@Nullable GenericStack config) {
var oldConfig = this.config;
this.config = config;
if (config == null) {
this.setStock(null);
} else {
if (!config.equals(oldConfig)) {
// Refresh to get stack amount from AE2
this.setStock(new GenericStack(config.what(), getAmountInNetwork(config.what())));
pendingWatcherSync = true;
}
}
}

@Override
public ItemStack extractItem(int slot, int amount, boolean simulate) {
if (slot == 0 && this.stock != null) {
Expand Down
Loading