diff --git a/.gitignore b/.gitignore
index 6642a3af..539e2e4b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,10 @@
build/
out/
classes/
+/common/bin/
+/fabric/bin/
+/neoforge/bin/
+/neoforge/runs/
# Eclipse
*.launch
diff --git a/neoforge/src/main/java/com/logistics/neoforge/energy/NeoForgeEnergyCapabilityLookup.java b/neoforge/src/main/java/com/logistics/neoforge/energy/NeoForgeEnergyCapabilityLookup.java
index 0527a46c..c2f538ae 100644
--- a/neoforge/src/main/java/com/logistics/neoforge/energy/NeoForgeEnergyCapabilityLookup.java
+++ b/neoforge/src/main/java/com/logistics/neoforge/energy/NeoForgeEnergyCapabilityLookup.java
@@ -2,22 +2,19 @@
import com.logistics.core.lib.energy.EnergyCapabilityLookup;
import com.logistics.core.lib.energy.IEnergyStorage;
+import net.neoforged.neoforge.capabilities.Capabilities;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable;
/**
- * NeoForge stub implementation of {@link EnergyCapabilityLookup}.
- *
- *
TODO: implement via NeoForge's {@code Capabilities.EnergyStorage} capability lookup,
- * wrapping the found {@code IEnergyStorage} (NeoForge) in an {@link IEnergyStorage} adapter.
+ * NeoForge implementation of {@link EnergyCapabilityLookup}.
*/
public final class NeoForgeEnergyCapabilityLookup implements EnergyCapabilityLookup {
@Override
public @Nullable IEnergyStorage find(Level level, BlockPos pos, Direction side) {
- // TODO(neoforge): implement via Capabilities.EnergyStorage.getBlockCapability(level, pos, state, blockEntity, side)
- return null;
+ return NeoForgeEnergyStorage.wrap(level.getCapability(Capabilities.Energy.BLOCK, pos, side));
}
}
diff --git a/neoforge/src/main/java/com/logistics/neoforge/energy/NeoForgeEnergyStorage.java b/neoforge/src/main/java/com/logistics/neoforge/energy/NeoForgeEnergyStorage.java
new file mode 100644
index 00000000..df1f3892
--- /dev/null
+++ b/neoforge/src/main/java/com/logistics/neoforge/energy/NeoForgeEnergyStorage.java
@@ -0,0 +1,156 @@
+package com.logistics.neoforge.energy;
+
+import com.logistics.core.lib.energy.IEnergyStorage;
+import net.neoforged.neoforge.transfer.energy.EnergyHandler;
+import net.neoforged.neoforge.transfer.transaction.Transaction;
+import net.neoforged.neoforge.transfer.transaction.TransactionContext;
+import net.neoforged.neoforge.transfer.transaction.SnapshotJournal;
+import org.jetbrains.annotations.Nullable;
+
+public final class NeoForgeEnergyStorage implements IEnergyStorage {
+ private final EnergyHandler handler;
+
+ private NeoForgeEnergyStorage(EnergyHandler handler) {
+ this.handler = handler;
+ }
+
+ @Nullable
+ public static IEnergyStorage wrap(@Nullable EnergyHandler handler) {
+ return handler == null ? null : new NeoForgeEnergyStorage(handler);
+ }
+
+ @Nullable
+ public static EnergyHandler asNeoForge(@Nullable IEnergyStorage storage) {
+ return storage == null ? null : new CommonEnergyHandler(storage);
+ }
+
+ @Override
+ public long insert(long maxAmount, boolean simulate) {
+ try (Transaction tx = openTransaction()) {
+ int inserted = handler.insert(clampToInt(maxAmount), tx);
+ if (!simulate) {
+ tx.commit();
+ }
+ return inserted;
+ }
+ }
+
+ @Override
+ public long extract(long maxAmount, boolean simulate) {
+ try (Transaction tx = openTransaction()) {
+ int extracted = handler.extract(clampToInt(maxAmount), tx);
+ if (!simulate) {
+ tx.commit();
+ }
+ return extracted;
+ }
+ }
+
+ @Override
+ public long getAmount() {
+ return handler.getAmountAsLong();
+ }
+
+ @Override
+ public long getCapacity() {
+ return handler.getCapacityAsLong();
+ }
+
+ @Override
+ public boolean canInsert() {
+ if (handler instanceof CommonEnergyHandler common) {
+ return common.canInsert();
+ }
+ try (Transaction tx = openTransaction()) {
+ return handler.insert(1, tx) > 0;
+ }
+ }
+
+ @Override
+ public boolean canExtract() {
+ if (handler instanceof CommonEnergyHandler common) {
+ return common.canExtract();
+ }
+ try (Transaction tx = openTransaction()) {
+ return handler.extract(1, tx) > 0;
+ }
+ }
+
+ private static Transaction openTransaction() {
+ TransactionContext current = Transaction.getCurrentOpenedTransaction();
+ return current == null ? Transaction.openRoot() : Transaction.open(current);
+ }
+
+ private static int clampToInt(long amount) {
+ return (int) Math.max(0, Math.min(amount, Integer.MAX_VALUE));
+ }
+
+ private static final class CommonEnergyHandler extends SnapshotJournal implements EnergyHandler {
+ private final IEnergyStorage storage;
+ private long pendingDelta;
+
+ private CommonEnergyHandler(IEnergyStorage storage) {
+ this.storage = storage;
+ }
+
+ @Override
+ public long getAmountAsLong() {
+ return Math.max(0, storage.getAmount() + pendingDelta);
+ }
+
+ @Override
+ public long getCapacityAsLong() {
+ return storage.getCapacity();
+ }
+
+ private boolean canInsert() {
+ return storage.canInsert();
+ }
+
+ private boolean canExtract() {
+ return storage.canExtract();
+ }
+
+ @Override
+ public int insert(int amount, net.neoforged.neoforge.transfer.transaction.TransactionContext transaction) {
+ updateSnapshots(transaction);
+ long effectiveFree = Math.max(0, getCapacityAsLong() - getAmountAsLong());
+ long inserted = Math.min(amount, effectiveFree);
+ if (inserted > 0) {
+ pendingDelta += inserted;
+ }
+ return clampToInt(inserted);
+ }
+
+ @Override
+ public int extract(int amount, net.neoforged.neoforge.transfer.transaction.TransactionContext transaction) {
+ updateSnapshots(transaction);
+ long extracted = Math.min(getAmountAsLong(), storage.extract(amount, true));
+ if (extracted > 0) {
+ pendingDelta -= extracted;
+ }
+ return clampToInt(extracted);
+ }
+
+ @Override
+ protected Long createSnapshot() {
+ return pendingDelta;
+ }
+
+ @Override
+ protected void revertToSnapshot(Long snapshot) {
+ pendingDelta = snapshot;
+ }
+
+ @Override
+ protected void onRootCommit(Long originalState) {
+ long delta = pendingDelta - originalState;
+ if (delta > 0) {
+ storage.insert(delta, false);
+ } else if (delta < 0) {
+ storage.extract(-delta, false);
+ }
+ pendingDelta = 0;
+ }
+ }
+}
diff --git a/neoforge/src/main/java/com/logistics/neoforge/fluids/NeoForgeFluidKey.java b/neoforge/src/main/java/com/logistics/neoforge/fluids/NeoForgeFluidKey.java
new file mode 100644
index 00000000..501b51d7
--- /dev/null
+++ b/neoforge/src/main/java/com/logistics/neoforge/fluids/NeoForgeFluidKey.java
@@ -0,0 +1,60 @@
+package com.logistics.neoforge.fluids;
+
+import com.logistics.core.lib.fluids.IFluidKey;
+import net.minecraft.core.component.DataComponentPatch;
+import net.minecraft.world.level.material.Fluid;
+import net.neoforged.neoforge.fluids.FluidStack;
+import net.neoforged.neoforge.transfer.fluid.FluidResource;
+
+public final class NeoForgeFluidKey implements IFluidKey {
+ private final FluidResource resource;
+
+ public NeoForgeFluidKey(FluidResource resource) {
+ if (resource == null) {
+ throw new NullPointerException("resource must not be null");
+ }
+ this.resource = resource;
+ }
+
+ public static NeoForgeFluidKey of(FluidStack stack) {
+ return new NeoForgeFluidKey(FluidResource.of(stack));
+ }
+
+ public static NeoForgeFluidKey of(FluidResource resource) {
+ return new NeoForgeFluidKey(resource);
+ }
+
+ public FluidResource resource() {
+ return resource;
+ }
+
+ @Override
+ public Fluid getFluid() {
+ return resource.getFluid();
+ }
+
+ @Override
+ public DataComponentPatch getComponents() {
+ return resource.getComponentsPatch();
+ }
+
+ @Override
+ public boolean isBlank() {
+ return resource.isEmpty();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o instanceof NeoForgeFluidKey other) return resource.equals(other.resource);
+ if (o instanceof IFluidKey other) {
+ return getFluid() == other.getFluid() && getComponents().equals(other.getComponents());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * getFluid().hashCode() + getComponents().hashCode();
+ }
+}
diff --git a/neoforge/src/main/java/com/logistics/neoforge/fluids/NeoForgeFluidStorage.java b/neoforge/src/main/java/com/logistics/neoforge/fluids/NeoForgeFluidStorage.java
new file mode 100644
index 00000000..521ec434
--- /dev/null
+++ b/neoforge/src/main/java/com/logistics/neoforge/fluids/NeoForgeFluidStorage.java
@@ -0,0 +1,207 @@
+package com.logistics.neoforge.fluids;
+
+import com.logistics.core.lib.fluids.IFluidKey;
+import com.logistics.core.lib.fluids.IFluidStorage;
+import com.logistics.core.lib.fluids.IFluidView;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import net.neoforged.neoforge.transfer.ResourceHandler;
+import net.neoforged.neoforge.transfer.fluid.FluidResource;
+import net.neoforged.neoforge.transfer.transaction.SnapshotJournal;
+import net.neoforged.neoforge.transfer.transaction.Transaction;
+import net.neoforged.neoforge.transfer.transaction.TransactionContext;
+import org.jetbrains.annotations.Nullable;
+
+public final class NeoForgeFluidStorage implements IFluidStorage {
+ private final ResourceHandler handler;
+
+ private NeoForgeFluidStorage(ResourceHandler handler) {
+ this.handler = handler;
+ }
+
+ @Nullable
+ public static IFluidStorage wrap(@Nullable ResourceHandler handler) {
+ return handler == null ? null : new NeoForgeFluidStorage(handler);
+ }
+
+ @Nullable
+ public static ResourceHandler asNeoForge(@Nullable IFluidStorage storage) {
+ return storage == null ? null : new CommonFluidHandler(storage);
+ }
+
+ @Override
+ public long insert(IFluidKey fluid, long maxAmount, boolean simulate) {
+ FluidResource resource = toResource(fluid);
+ if (resource.isEmpty() || maxAmount <= 0) {
+ return 0;
+ }
+ try (Transaction tx = Transaction.openRoot()) {
+ int inserted = handler.insert(resource, clampToInt(maxAmount), tx);
+ if (!simulate) {
+ tx.commit();
+ }
+ return inserted;
+ }
+ }
+
+ @Override
+ public long extract(IFluidKey fluid, long maxAmount, boolean simulate) {
+ FluidResource resource = toResource(fluid);
+ if (resource.isEmpty() || maxAmount <= 0) {
+ return 0;
+ }
+ try (Transaction tx = Transaction.openRoot()) {
+ int extracted = handler.extract(resource, clampToInt(maxAmount), tx);
+ if (!simulate) {
+ tx.commit();
+ }
+ return extracted;
+ }
+ }
+
+ @Override
+ public Iterable contents() {
+ List views = new ArrayList<>();
+ for (int i = 0; i < handler.size(); i++) {
+ FluidResource resource = handler.getResource(i);
+ long amount = handler.getAmountAsLong(i);
+ if (resource.isEmpty() || amount <= 0) {
+ continue;
+ }
+ IFluidKey key = NeoForgeFluidKey.of(resource);
+ views.add(new IFluidView() {
+ @Override public IFluidKey resource() { return key; }
+ @Override public long amount() { return amount; }
+ });
+ }
+ return views;
+ }
+
+ private static FluidResource toResource(IFluidKey key) {
+ return key instanceof NeoForgeFluidKey nfKey
+ ? nfKey.resource()
+ : FluidResource.of(key.getFluid(), key.getComponents());
+ }
+
+ private static int clampToInt(long amount) {
+ return (int) Math.max(0, Math.min(amount, Integer.MAX_VALUE));
+ }
+
+ private static final class CommonFluidHandler extends SnapshotJournal