diff --git a/README.md b/README.md index 729eec8..a55fc0f 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,30 @@ A dead-simple energy mod for Fabric. ### Installation -This mod is in early development and is not yet available for download. +The mod is available on [Modrinth](https://modrinth.com/project/polyenergy). You can download it from there or build it +yourself. ### API +#### Installation + +To use the API, add the following to your `build.gradle`: + +```groovy +repositories { + maven { + name = "Arzumify's Maven" + url = "https://maven.ailur.dev" + } +} + +dependencies { + modImplementation "dev.ailur.polyenergy:polyenergy:{version}" +} +``` + +To get the latest version, check the [Maven repository](https://maven.ailur.dev/arzumify/polyenergy/). + #### Providers Providers are the source of energy in the game. They can be blocks, entities, or any other object that can provide diff --git a/build.gradle b/build.gradle index 17d7b96..fb77326 100644 --- a/build.gradle +++ b/build.gradle @@ -38,6 +38,16 @@ dependencies { // Fabric API. This is technically optional, but you probably want it anyway. modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" + // Testing + testImplementation(platform('org.junit:junit-bom:5.12.0')) + testImplementation('org.junit.jupiter:junit-jupiter') + testRuntimeOnly('org.junit.platform:junit-platform-launcher') +} + +tasks.named("test", Test) { + useJUnitPlatform() + + } processResources { diff --git a/gradle.properties b/gradle.properties index 0edffc2..9a20da9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,7 +7,7 @@ minecraft_version=1.21.4 yarn_mappings=1.21.4+build.8 loader_version=0.16.10 # Mod Properties -mod_version=1.0.0 +mod_version=1.1.0 maven_group=arzumify.polyenergy archives_base_name=polyenergy # Dependencies diff --git a/src/main/java/arzumify/polyenergy/Polyenergy.java b/src/main/java/arzumify/polyenergy/Polyenergy.java index 7602bb2..c185e4b 100644 --- a/src/main/java/arzumify/polyenergy/Polyenergy.java +++ b/src/main/java/arzumify/polyenergy/Polyenergy.java @@ -3,10 +3,6 @@ package arzumify.polyenergy; import net.fabricmc.api.ModInitializer; public class Polyenergy implements ModInitializer { - // We don't actually use either of these, so it's commented out. - // public static final String MOD_ID = "polyenergy"; - // public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); - @Override public void onInitialize() { } diff --git a/src/main/java/arzumify/polyenergy/api/EnergyReceiver.java b/src/main/java/arzumify/polyenergy/api/EnergyReceiver.java index c52e2dc..34e9500 100644 --- a/src/main/java/arzumify/polyenergy/api/EnergyReceiver.java +++ b/src/main/java/arzumify/polyenergy/api/EnergyReceiver.java @@ -9,4 +9,12 @@ public interface EnergyReceiver { * @param provider The provider that exists and is ready to provide energy to this receiver. */ void ready(EnergyProvider provider); + + /** + * When called by the server matchmaker, indicates that this provider is no longer able to provide energy to this receiver. + * Wait for another call to {@link EnergyReceiver#ready(EnergyProvider)} before attempting to extract energy from this provider. + * + * @param provider The provider that is no longer able to provide energy to this receiver. + */ + void unready(EnergyProvider provider); } diff --git a/src/main/java/arzumify/polyenergy/impl/CoordinateMatchMaker.java b/src/main/java/arzumify/polyenergy/impl/CoordinateMatchMaker.java index 2760d6f..95a0f6a 100644 --- a/src/main/java/arzumify/polyenergy/impl/CoordinateMatchMaker.java +++ b/src/main/java/arzumify/polyenergy/impl/CoordinateMatchMaker.java @@ -1,31 +1,61 @@ package arzumify.polyenergy.impl; import java.util.ArrayList; +import java.util.HashMap; -// TODO: This seems very inefficient. Measure performance and then try improving this? This is O(n * m) which is very bad! +// It's slightly more optimized now, as in it could realistically run in Minecraft. +// Reset: O(1) constant time, we're just deleting everything +// Add Provider: O(m * k^2) where m is the number of receivers and k is the number of points of presence +// Add Receiver: O(n * k^2) where n is the number of providers and k is the number of points of presence +// Remove Provider: O(n + m) where n is the number of providers and m is the number of matches for the provider +// Remove Receiver: O(m) where m is the number of receivers +// I think we stop the optimisations here, the bottleneck is no longer the matchmaker but Minecraft itself. +// If more optimisation is needed we can try to perform Vector math in a more efficient way, but I think this is good enough. public class CoordinateMatchMaker { private static final ArrayList providers = new ArrayList<>(); private static final ArrayList receivers = new ArrayList<>(); + private static final HashMap> matches = new HashMap<>(); - public static void update() { - for (ReceiverDetails receiver : receivers) { - for (ProviderDetails provider : providers) { - if (provider.isInRange(receiver.pointsOfPresence())) { - provider.provider().exists(receiver.receiver(), provider.pointsOfPresence()); - } - } - } + public static void reset() { + providers.clear(); + receivers.clear(); + matches.clear(); } public static void addProvider(ProviderDetails provider) { providers.add(provider); - update(); + matches.put(provider, new ArrayList<>()); + // Search for receivers within range of this provider + for (ReceiverDetails receiver : receivers) { + if (provider.isInRange(receiver.pointsOfPresence())) { + provider.provider().exists(receiver.receiver(), provider.pointsOfPresence()); + matches.get(provider).add(receiver); + } + } } public static void addReceiver(ReceiverDetails receiver) { receivers.add(receiver); - update(); + // Search for providers within range of this receiver + for (ProviderDetails provider : providers) { + if (provider.isInRange(receiver.pointsOfPresence())) { + provider.provider().exists(receiver.receiver(), provider.pointsOfPresence()); + matches.get(provider).add(receiver); + } + } + } + + public static void removeProvider(ProviderDetails provider) { + providers.remove(provider); + for (ReceiverDetails receiver : matches.get(provider)) { + receiver.receiver().unready(provider.provider()); + } + matches.remove(provider); + } + + public static void removeReceiver(ReceiverDetails receiver) { + receivers.remove(receiver); } } diff --git a/src/main/java/arzumify/polyenergy/impl/ProviderDetails.java b/src/main/java/arzumify/polyenergy/impl/ProviderDetails.java index 1a009bd..420937b 100644 --- a/src/main/java/arzumify/polyenergy/impl/ProviderDetails.java +++ b/src/main/java/arzumify/polyenergy/impl/ProviderDetails.java @@ -15,7 +15,10 @@ public record ProviderDetails(ArrayList pointsOfPresence, EnergyProvider public boolean isInRange(ArrayList pointsOfPresence) { for (Vec3i point : pointsOfPresence) { for (Vec3i myPoint : this.pointsOfPresence) { - if (point.isWithinDistance(myPoint, this.range)) { + var manhattanDistance = point.getManhattanDistance(myPoint); + if (manhattanDistance == 0) { + return false; + } else if (manhattanDistance <= range) { return true; } } diff --git a/src/main/java/arzumify/polyenergy/util/SimpleBattery.java b/src/main/java/arzumify/polyenergy/util/SimpleBattery.java index 54e3e7b..1cf6708 100644 --- a/src/main/java/arzumify/polyenergy/util/SimpleBattery.java +++ b/src/main/java/arzumify/polyenergy/util/SimpleBattery.java @@ -5,22 +5,28 @@ import arzumify.polyenergy.api.EnergyReceiver; import arzumify.polyenergy.impl.CoordinateMatchMaker; import arzumify.polyenergy.impl.ProviderDetails; import arzumify.polyenergy.impl.ReceiverDetails; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerBlockEntityEvents; import net.minecraft.block.BlockState; import net.minecraft.block.entity.BlockEntity; import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.server.world.ServerWorld; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.Vec3i; +import net.minecraft.world.World; import java.util.ArrayList; +import java.util.concurrent.CopyOnWriteArrayList; /** * A simple battery that can provide and receive energy. Has a range of 1 block in all directions (directly adjacent blocks). */ -public class SimpleBattery extends BlockEntity implements EnergyProvider, EnergyReceiver { +public class SimpleBattery extends BlockEntity implements EnergyProvider, EnergyReceiver, ServerBlockEntityEvents.Unload, ServerBlockEntityEvents.Load { private final long capacity; private final long inputRate; private final long outputRate; - private final ArrayList providers = new ArrayList<>(); + private final ProviderDetails providerDetails = ProviderDetails.NewSimple(pos, this); + private final ReceiverDetails receiverDetails = ReceiverDetails.NewSimple(pos, this); + private final CopyOnWriteArrayList providers = new CopyOnWriteArrayList<>(); private long energy = 0; public SimpleBattery(BlockEntityType type, BlockPos pos, BlockState state, long capacity, long inputRate, long outputRate) { @@ -28,18 +34,10 @@ public class SimpleBattery extends BlockEntity implements EnergyProvider, Energy this.capacity = capacity; this.inputRate = inputRate; this.outputRate = outputRate; - CoordinateMatchMaker.addProvider(ProviderDetails.NewSimple(pos, this)); - CoordinateMatchMaker.addReceiver(ReceiverDetails.NewSimple(pos, this)); + CoordinateMatchMaker.addProvider(providerDetails); + CoordinateMatchMaker.addReceiver(receiverDetails); } - /** - * When called by a receiver, attempts to remove energy from the provider. - * Do not attempt to extract energy until {@link EnergyReceiver#ready(EnergyProvider)} is called by the receiver. - * - * @param amount The target amount of energy to extract - * @param receiver The receiver that is attempting to extract energy - * @return The amount of energy actually extracted. If zero, the receiver will not attempt to extract energy until {@link EnergyReceiver#ready(EnergyProvider)} is called. - */ @Override public long extract(long amount, EnergyReceiver receiver) { long extracted = Math.min(Math.min(outputRate, amount), energy); @@ -47,41 +45,44 @@ public class SimpleBattery extends BlockEntity implements EnergyProvider, Energy return extracted; } - /** - * When called by the server matchmaker, indicates that this is a possible receiver to call {@link EnergyReceiver#ready(EnergyProvider)} on. - * This particular overload is for coordinate-based matchmaking. - * - * @param receiver The receiver that exists. - * @param pointsOfPresence The points of presence of the receiver (can be multiple, e.g. for a network of wires). - */ @Override public void exists(EnergyReceiver receiver, ArrayList pointsOfPresence) { receiver.ready(this); } - /** - * When called by a provider, indicates that the provider is ready to have energy extracted from it. - * Do not attempt to call {@link EnergyProvider#extract(long, EnergyReceiver)} on any provider until this method is called. - * - * @param provider The provider that is ready to provide energy to this receiver - */ @Override public void ready(EnergyProvider provider) { providers.add(provider); } - /** - * Called every tick to update the battery's energy level. - */ - public void tick() { - var leftToFill = Math.min(inputRate, capacity - energy); - for (EnergyProvider provider : providers) { - if (leftToFill == 0) { + public static void tick(World world, BlockPos pos, BlockState state, SimpleBattery battery) { + var leftToFill = Math.min(battery.inputRate, battery.capacity - battery.energy); + for (EnergyProvider provider : battery.providers) { + // Less than zero shouldn't be possible, but just in case... + if (leftToFill <= 0) { break; } - long extracted = provider.extract(leftToFill, this); - energy += extracted; + long extracted = provider.extract(leftToFill, battery); + battery.energy += extracted; leftToFill -= extracted; } } + + @Override + public void unready(EnergyProvider provider) { + providers.remove(provider); + } + + @Override + public void onLoad(BlockEntity blockEntity, ServerWorld serverWorld) { + CoordinateMatchMaker.addProvider(providerDetails); + CoordinateMatchMaker.addReceiver(receiverDetails); + } + + @Override + public void onUnload(BlockEntity blockEntity, ServerWorld serverWorld) { + CoordinateMatchMaker.removeProvider(providerDetails); + CoordinateMatchMaker.removeReceiver(receiverDetails); + providers.clear(); + } } diff --git a/src/main/resources/assets/polyenergy/icon-scaled.png b/src/main/resources/assets/polyenergy/icon-scaled.png new file mode 100644 index 0000000..067ba6b Binary files /dev/null and b/src/main/resources/assets/polyenergy/icon-scaled.png differ diff --git a/src/main/resources/assets/polyenergy/icon.png b/src/main/resources/assets/polyenergy/icon.png index d312c76..fd1defd 100644 Binary files a/src/main/resources/assets/polyenergy/icon.png and b/src/main/resources/assets/polyenergy/icon.png differ diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 3040eed..8b511c8 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -12,7 +12,7 @@ "sources": "https://git.ailur.dev/arzumify/polyenergy" }, "license": "GPL-3.0", - "icon": "assets/polyenergy/icon.png", + "icon": "assets/polyenergy/icon-scaled.png", "environment": "*", "entrypoints": { "main": [ diff --git a/src/test/java/EnergyTests.java b/src/test/java/EnergyTests.java new file mode 100644 index 0000000..dba9ad8 --- /dev/null +++ b/src/test/java/EnergyTests.java @@ -0,0 +1,329 @@ +import arzumify.polyenergy.impl.CoordinateMatchMaker; +import net.minecraft.util.math.Vec3i; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class EnergyTests { + @BeforeEach + public void setup() { + // Delete all batteries + CoordinateMatchMaker.reset(); + System.out.println("Reset CoordinateMatchMaker"); + } + + @Test + public void testEnergyTransfer() { + // Create two batteries with coordinates (0, 0, 0) and (1, 0, 0) + // Battery1 should be able to provide energy to battery2 + var battery1 = new SimpleCodeOnlyBattery(new Vec3i(0, 0, 0), 16, 0, 1, "Battery1"); + var battery2 = new SimpleCodeOnlyBattery(new Vec3i(1, 0, 0), 16, 1, 0, "Battery2"); + + // Give battery1 some energy + battery1.energy = 16; + + // Tick the batteries 10 times + for (int i = 0; i < 10; i++) { + SimpleCodeOnlyBattery.tick(battery1); + SimpleCodeOnlyBattery.tick(battery2); + } + + // Output the energy of the batteries + System.out.println("Battery1 energy: " + battery1.energy); + System.out.println("Battery2 energy: " + battery2.energy); + + // Battery1 should have lost 10 energy and battery2 should have gained 10 energy + assertEquals(6, battery1.energy); + assertEquals(10, battery2.energy); + } + + @Test + public void testUnloadBattery1() { + // Create two batteries with coordinates (0, 0, 0) and (1, 0, 0) + // Battery1 should be able to provide energy to battery2 + var battery1 = new SimpleCodeOnlyBattery(new Vec3i(0, 0, 0), 16, 0, 1, "Battery1"); + var battery2 = new SimpleCodeOnlyBattery(new Vec3i(1, 0, 0), 16, 1, 0, "Battery2"); + + // Give battery1 some energy + battery1.energy = 16; + + // Tick 5 times + for (int i = 0; i < 5; i++) { + SimpleCodeOnlyBattery.tick(battery1); + SimpleCodeOnlyBattery.tick(battery2); + } + + // Unload battery1 + System.out.println("Unloading battery1"); + battery1.onUnload(null, null); + + // Tick 5 more times + for (int i = 0; i < 5; i++) { + SimpleCodeOnlyBattery.tick(battery1); + SimpleCodeOnlyBattery.tick(battery2); + } + + // Output the energy of the batteries + System.out.println("Battery1 energy: " + battery1.energy); + System.out.println("Battery2 energy: " + battery2.energy); + + // Battery1 should have lost 5 energy and battery2 should have gained 5 energy + assertEquals(11, battery1.energy); + assertEquals(5, battery2.energy); + } + + @Test + public void testUnloadBattery2() { + // Create two batteries with coordinates (0, 0, 0) and (1, 0, 0) + // Battery1 should be able to provide energy to battery2 + var battery1 = new SimpleCodeOnlyBattery(new Vec3i(0, 0, 0), 16, 0, 1, "Battery1"); + var battery2 = new SimpleCodeOnlyBattery(new Vec3i(1, 0, 0), 16, 1, 0, "Battery2"); + + // Give battery1 some energy + battery1.energy = 16; + + // Tick 5 times + for (int i = 0; i < 5; i++) { + SimpleCodeOnlyBattery.tick(battery1); + SimpleCodeOnlyBattery.tick(battery2); + } + + // Unload battery2 + System.out.println("Unloading battery2"); + battery2.onUnload(null, null); + + // Tick 5 more times + for (int i = 0; i < 5; i++) { + SimpleCodeOnlyBattery.tick(battery1); + SimpleCodeOnlyBattery.tick(battery2); + } + + // Output the energy of the batteries + System.out.println("Battery1 energy: " + battery1.energy); + System.out.println("Battery2 energy: " + battery2.energy); + + // Battery1 should have lost 5 energy and battery2 should have gained 5 energy + assertEquals(11, battery1.energy); + assertEquals(5, battery2.energy); + } + + @Test + public void testUnloadThenReloadBattery1() { + // Create two batteries with coordinates (0, 0, 0) and (1, 0, 0) + // Battery1 should be able to provide energy to battery2 + var battery1 = new SimpleCodeOnlyBattery(new Vec3i(0, 0, 0), 16, 0, 1, "Battery1"); + var battery2 = new SimpleCodeOnlyBattery(new Vec3i(1, 0, 0), 16, 1, 0, "Battery2"); + + // Give battery1 some energy + battery1.energy = 16; + + // Tick 5 times + for (int i = 0; i < 5; i++) { + SimpleCodeOnlyBattery.tick(battery1); + SimpleCodeOnlyBattery.tick(battery2); + } + + // Unload battery1 + System.out.println("Unloading battery1"); + battery1.onUnload(null, null); + + // Tick 5 more times + for (int i = 0; i < 5; i++) { + SimpleCodeOnlyBattery.tick(battery1); + SimpleCodeOnlyBattery.tick(battery2); + } + + // Reload battery1 + System.out.println("Reloading battery1"); + battery1.onLoad(null, null); + + // Tick 5 more times + for (int i = 0; i < 5; i++) { + SimpleCodeOnlyBattery.tick(battery1); + SimpleCodeOnlyBattery.tick(battery2); + } + + // Output the energy of the batteries + System.out.println("Battery1 energy: " + battery1.energy); + System.out.println("Battery2 energy: " + battery2.energy); + + // Battery1 should have lost 10 energy and battery2 should have gained 10 energy + assertEquals(6, battery1.energy); + assertEquals(10, battery2.energy); + } + + @Test + public void testUnloadThenReloadBattery2() { + // Create two batteries with coordinates (0, 0, 0) and (1, 0, 0) + // Battery1 should be able to provide energy to battery2 + var battery1 = new SimpleCodeOnlyBattery(new Vec3i(0, 0, 0), 16, 0, 1, "Battery1"); + var battery2 = new SimpleCodeOnlyBattery(new Vec3i(1, 0, 0), 16, 1, 0, "Battery2"); + + // Give battery1 some energy + battery1.energy = 16; + + // Tick 5 times + for (int i = 0; i < 5; i++) { + SimpleCodeOnlyBattery.tick(battery1); + SimpleCodeOnlyBattery.tick(battery2); + } + + // Unload battery2 + System.out.println("Unloading battery2"); + battery2.onUnload(null, null); + + // Tick 5 more times + for (int i = 0; i < 5; i++) { + SimpleCodeOnlyBattery.tick(battery1); + SimpleCodeOnlyBattery.tick(battery2); + } + + // Reload battery2 + System.out.println("Reloading battery2"); + battery2.onLoad(null, null); + + // Tick 5 more times + for (int i = 0; i < 5; i++) { + SimpleCodeOnlyBattery.tick(battery1); + SimpleCodeOnlyBattery.tick(battery2); + } + + // Output the energy of the batteries + System.out.println("Battery1 energy: " + battery1.energy); + System.out.println("Battery2 energy: " + battery2.energy); + + // Battery1 should have lost 10 energy and battery2 should have gained 10 energy + assertEquals(6, battery1.energy); + assertEquals(10, battery2.energy); + } + + @Test + public void twoHalfFullBatteries() { + System.out.println("Two half-full batteries"); + // Create two batteries with coordinates (0, 0, 0) and (1, 0, 0) + // They should both provide energy to each other and stay at equilibrium + var battery1 = new SimpleCodeOnlyBattery(new Vec3i(0, 0, 0), 16, 1, 1, "Battery1"); + var battery2 = new SimpleCodeOnlyBattery(new Vec3i(1, 0, 0), 16, 1, 1, "Battery2"); + + // Give battery1 and battery2 some energy + battery1.energy = 8; + battery2.energy = 8; + + // Tick 10 times + for (int i = 0; i < 10; i++) { + SimpleCodeOnlyBattery.tick(battery1); + SimpleCodeOnlyBattery.tick(battery2); + } + + // Output the energy of the batteries + System.out.println("Battery1 energy: " + battery1.energy); + System.out.println("Battery2 energy: " + battery2.energy); + + // Battery1 and battery2 should have lost no energy + assertEquals(8, battery1.energy); + assertEquals(8, battery2.energy); + } + + @Test + public void middleOneCharges() { + // Create three batteries with coordinates (0, 0, 0), (1, 0, 0), and (2, 0, 0) + // Battery2 should provide energy to battery1 and battery3 + var battery1 = new SimpleCodeOnlyBattery(new Vec3i(0, 0, 0), 16, 1, 0, "Battery1"); + var battery2 = new SimpleCodeOnlyBattery(new Vec3i(1, 0, 0), 16, 0, 1, "Battery2"); + var battery3 = new SimpleCodeOnlyBattery(new Vec3i(2, 0, 0), 16, 1, 0, "Battery3"); + + // Give battery2 some energy + battery2.energy = 16; + + // Tick 10 times + for (int i = 0; i < 10; i++) { + SimpleCodeOnlyBattery.tick(battery1); + SimpleCodeOnlyBattery.tick(battery2); + SimpleCodeOnlyBattery.tick(battery3); + } + + // Output the energy of the batteries + System.out.println("Battery1 energy: " + battery1.energy); + System.out.println("Battery2 energy: " + battery2.energy); + System.out.println("Battery3 energy: " + battery3.energy); + + // Battery1 and battery3 should have gained 8 energy and battery2 should have lost 16 energy + assertEquals(8, battery1.energy); + assertEquals(0, battery2.energy); + assertEquals(8, battery3.energy); + } + + @Test + public void middleOneReceives() { + // Create three batteries with coordinates (0, 0, 0), (1, 0, 0), and (2, 0, 0) + // Battery1 and battery3 should provide energy to battery2 + var battery1 = new SimpleCodeOnlyBattery(new Vec3i(0, 0, 0), 16, 0, 1, "Battery1"); + var battery2 = new SimpleCodeOnlyBattery(new Vec3i(1, 0, 0), 16, 1, 0, "Battery2"); + var battery3 = new SimpleCodeOnlyBattery(new Vec3i(2, 0, 0), 16, 0, 1, "Battery3"); + + // Give battery1 and battery3 some energy + battery1.energy = 8; + battery3.energy = 8; + + // Tick 10 times + for (int i = 0; i < 16; i++) { + SimpleCodeOnlyBattery.tick(battery1); + SimpleCodeOnlyBattery.tick(battery2); + SimpleCodeOnlyBattery.tick(battery3); + } + + // Output the energy of the batteries + System.out.println("Battery1 energy: " + battery1.energy); + System.out.println("Battery2 energy: " + battery2.energy); + System.out.println("Battery3 energy: " + battery3.energy); + + // Battery2 should have 16 energy and battery1 and battery3 should have 0 energy + assertEquals(0, battery1.energy); + assertEquals(16, battery2.energy); + assertEquals(0, battery3.energy); + } + + @Test + public void testEnergyTransferWithRange() { + // Create two batteries with coordinates (0, 0, 0) and (2, 0, 0) + // Battery1 should be able to provide energy to battery2 + var battery1 = new SimpleCodeOnlyBattery(new Vec3i(0, 0, 0), 16, 0, 1, "Battery1"); + var battery2 = new SimpleCodeOnlyBattery(new Vec3i(2, 0, 0), 16, 1, 0, "Battery2"); + + // Give battery1 some energy + battery1.energy = 16; + + // Tick the batteries 10 times + for (int i = 0; i < 10; i++) { + SimpleCodeOnlyBattery.tick(battery1); + SimpleCodeOnlyBattery.tick(battery2); + } + + // Output the energy of the batteries + System.out.println("Battery1 energy: " + battery1.energy); + System.out.println("Battery2 energy: " + battery2.energy); + + // Battery1 should have lost no energy and battery2 should have gained no energy + assertEquals(16, battery1.energy); + assertEquals(0, battery2.energy); + } + + @Test + public void stressTest() { + // Create 1000 batteries with coordinates (0, 0, 0) to (999, 0, 0) + // Each battery should be able to provide energy to the next battery + var batteries = new SimpleCodeOnlyBattery[1000]; + for (int i = 0; i < 1000; i++) { + batteries[i] = new SimpleCodeOnlyBattery(new Vec3i(i, 0, 0), 16, 1, 2, "Battery" + i); + } + + // Give the first battery some energy + batteries[0].energy = 16; + + // Tick once + for (SimpleCodeOnlyBattery battery : batteries) { + SimpleCodeOnlyBattery.tick(battery); + } + } +} diff --git a/src/test/java/SimpleCodeOnlyBattery.java b/src/test/java/SimpleCodeOnlyBattery.java new file mode 100644 index 0000000..f75c877 --- /dev/null +++ b/src/test/java/SimpleCodeOnlyBattery.java @@ -0,0 +1,97 @@ +import arzumify.polyenergy.api.EnergyProvider; +import arzumify.polyenergy.api.EnergyReceiver; +import arzumify.polyenergy.impl.CoordinateMatchMaker; +import arzumify.polyenergy.impl.ProviderDetails; +import arzumify.polyenergy.impl.ReceiverDetails; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerBlockEntityEvents; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.math.Vec3i; + +import java.util.ArrayList; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * SimpleBattery, but implemented using only the API and not the actual block entity for testing purposes. + * All fields are public for testing purposes. + */ +public class SimpleCodeOnlyBattery implements EnergyProvider, EnergyReceiver, ServerBlockEntityEvents.Unload, ServerBlockEntityEvents.Load { + public final long capacity; + public final long inputRate; + public final long outputRate; + public final String name; + public final ProviderDetails providerDetails; + public final ReceiverDetails receiverDetails; + public final CopyOnWriteArrayList providers = new CopyOnWriteArrayList<>(); + public long energy = 0; + + public SimpleCodeOnlyBattery(Vec3i pos, long capacity, long inputRate, long outputRate, String name) { + this.capacity = capacity; + this.inputRate = inputRate; + this.outputRate = outputRate; + this.providerDetails = ProviderDetails.NewSimple(pos, this); + this.receiverDetails = ReceiverDetails.NewSimple(pos, this); + this.name = name; + CoordinateMatchMaker.addProvider(providerDetails); + CoordinateMatchMaker.addReceiver(receiverDetails); + } + + public static void tick(SimpleCodeOnlyBattery battery) { + System.out.println(battery.name + " ticking"); + var leftToFill = Math.min(battery.inputRate, battery.capacity - battery.energy); + for (EnergyProvider provider : battery.providers) { + // Less than zero shouldn't be possible, but just in case... + if (leftToFill <= 0) { + break; + } + long extracted = provider.extract(leftToFill, battery); + if (extracted == 0) { + battery.providers.remove(provider); + } + battery.energy += extracted; + leftToFill -= extracted; + } + } + + @Override + public long extract(long amount, EnergyReceiver receiver) { + System.out.println(name + " extracting " + amount + " energy"); + long extracted = Math.min(Math.min(outputRate, amount), energy); + energy -= extracted; + System.out.println(name + " extracted " + extracted + " energy"); + if (energy == 0) { + CoordinateMatchMaker.removeProvider(providerDetails); + } + return extracted; + } + + @Override + public void exists(EnergyReceiver receiver, ArrayList pointsOfPresence) { + System.out.println(name + " found other receiver"); + receiver.ready(this); + } + + @Override + public void ready(EnergyProvider provider) { + System.out.println(name + " found other provider"); + providers.add(provider); + } + + @Override + public void unready(EnergyProvider provider) { + providers.remove(provider); + } + + @Override + public void onLoad(BlockEntity blockEntity, ServerWorld serverWorld) { + CoordinateMatchMaker.addProvider(providerDetails); + CoordinateMatchMaker.addReceiver(receiverDetails); + } + + @Override + public void onUnload(BlockEntity blockEntity, ServerWorld serverWorld) { + CoordinateMatchMaker.removeProvider(providerDetails); + CoordinateMatchMaker.removeReceiver(receiverDetails); + providers.clear(); + } +}