Compare commits

...

10 commits

19 changed files with 164 additions and 930 deletions

View file

@ -6,8 +6,7 @@ A dead-simple energy mod for Fabric.
- Totally server-side
- Polymer-compatible
- Does not rely on Minecraft APIs (portable to other Vector3I-based games, in theory)
- As an extension of the above, supports entities and networks of any size
- Supports entities and networks of any size
## Usage
@ -31,7 +30,7 @@ repositories {
}
dependencies {
modImplementation "dev.ailur.polyenergy:polyenergy:{version}"
modImplementation "arzumify.polyenergy:polyenergy:{version}"
}
```
@ -44,10 +43,10 @@ energy.
To create a provider, implement the `EnergyProvider` interface. This interface has two methods:
- `void exists(EnergyReceiver receiver, ArrayList<Vec3i> pointsOfPresence);`
- `void exists(EnergyReceiver receiver);`
This method is called when the matchmaking service finds a receiver that can receive energy from this provider. The
provider should check if this receiver meets all of it's requirements, then call the Ready() method on the receiver to
provider should check if this receiver meets all of its requirements, then call the Ready() method on the receiver to
make a connection.
- `long extract(long amount, EnergyReceiver receiver);`

View file

@ -11,11 +11,10 @@ base {
}
repositories {
// Add repositories to retrieve artifacts from in here.
// You should only use this when depending on other mods because
// Loom adds the essential maven repositories to download Minecraft and libraries from automatically.
// See https://docs.gradle.org/current/userguide/declaring_repositories.html
// for more information about repositories.
maven {
name = "Arzumify's Maven"
url = "https://maven.ailur.dev"
}
}
loom {
@ -42,6 +41,7 @@ dependencies {
testImplementation(platform('org.junit:junit-bom:5.12.0'))
testImplementation('org.junit.jupiter:junit-jupiter')
testRuntimeOnly('org.junit.platform:junit-platform-launcher')
testImplementation "net.fabricmc:fabric-loader-junit:${project.loader_version}"
}
tasks.named("test", Test) {
@ -79,21 +79,3 @@ jar {
rename { "${it}_${inputs.properties.archivesName}" }
}
}
// configure the maven publication
publishing {
publications {
create("mavenJava", MavenPublication) {
artifactId = project.archives_base_name
from components.java
}
}
// See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing.
repositories {
// Add repositories to publish to here.
// Notice: This block does NOT have the same function as the block in the top level.
// The repositories here will be used for publishing your artifact, not for
// retrieving dependencies.
}
}

View file

@ -3,12 +3,12 @@ org.gradle.jvmargs=-Xmx1G
org.gradle.parallel=true
# Fabric Properties
# check these on https://fabricmc.net/develop
minecraft_version=1.21.4
yarn_mappings=1.21.4+build.8
minecraft_version=1.20
yarn_mappings=1.20+build.1
loader_version=0.16.10
# Mod Properties
mod_version=1.1.0
maven_group=arzumify.polyenergy
mod_version=1.0.0
maven_group=arzumify.polyenergy-cursed-edition
archives_base_name=polyenergy
# Dependencies
fabric_version=0.118.0+1.21.4
fabric_version=0.118.0+1.21.4

0
logs/latest.log Normal file
View file

View file

@ -1,9 +0,0 @@
package arzumify.polyenergy;
import net.fabricmc.api.ModInitializer;
public class Polyenergy implements ModInitializer {
@Override
public void onInitialize() {
}
}

View file

@ -0,0 +1,4 @@
package arzumify.polyenergy.api;
public interface EnergyBlock {
}

View file

@ -1,27 +1,14 @@
package arzumify.polyenergy.api;
import net.minecraft.util.math.Vec3i;
import java.util.ArrayList;
// Provides energy to EnergyReceivers. Can be implemented by anything able to tick and provide energy!
public interface EnergyProvider {
/**
* 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.
*/
long extract(long amount, EnergyReceiver receiver);
public interface EnergyProvider extends EnergyBlock {
/**
* 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 or a large entity).
*/
void exists(EnergyReceiver receiver, ArrayList<Vec3i> pointsOfPresence);
void ready(EnergyReceiver receiver);
void unready(EnergyReceiver receiver);
}

View file

@ -1,7 +1,7 @@
package arzumify.polyenergy.api;
// Receives energy from EnergyProviders. Can be implemented by anything able to tick and receive energy!
public interface EnergyReceiver {
public interface EnergyReceiver extends EnergyBlock {
/**
* When called by a provider, indicates that the provider exists and is ready to have energy extracted from.
* Do not attempt to call {@link EnergyProvider#extract(long, EnergyReceiver)} on any provider until this method is called.
@ -19,4 +19,6 @@ public interface EnergyReceiver {
* @param provider The provider that is no longer able to provide energy to this receiver.
*/
void unready(EnergyProvider provider);
long receive(long amount, EnergyProvider provider);
}

View file

@ -0,0 +1,21 @@
package arzumify.polyenergy.api;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.world.World;
import java.util.HashSet;
import java.util.Set;
public class SearchForNeighbours {
public static Set<EnergyBlock> searchForNeighbours(World world, BlockPos pos) {
var neighbours = new HashSet<EnergyBlock>();
Direction.stream().forEach(direction -> {
var entity = world.getBlockEntity(pos.offset(direction));
if (entity instanceof EnergyBlock) {
neighbours.add((EnergyBlock) entity);
}
});
return neighbours;
}
}

View file

@ -1,64 +0,0 @@
package arzumify.polyenergy.impl;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
// 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 CopyOnWriteArrayList<ProviderDetails> providers = new CopyOnWriteArrayList<>();
private static final CopyOnWriteArrayList<ReceiverDetails> receivers = new CopyOnWriteArrayList<>();
private static final ConcurrentHashMap<ProviderDetails, Set<ReceiverDetails>> matches = new ConcurrentHashMap<>();
public static void reset() {
providers.clear();
receivers.clear();
matches.clear();
}
public static void addProvider(ProviderDetails provider) {
providers.add(provider);
matches.put(provider, ConcurrentHashMap.newKeySet());
// 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);
// 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);
if (matches.get(provider) != null) {
for (ReceiverDetails receiver : matches.get(provider)) {
receiver.receiver().unready(provider.provider());
}
}
matches.remove(provider);
}
public static void removeReceiver(ReceiverDetails receiver) {
receivers.remove(receiver);
}
}

View file

@ -1,28 +0,0 @@
package arzumify.polyenergy.impl;
import arzumify.polyenergy.api.EnergyProvider;
import net.minecraft.util.math.Vec3i;
import java.util.ArrayList;
public record ProviderDetails(ArrayList<Vec3i> pointsOfPresence, EnergyProvider provider, int range) {
public static ProviderDetails NewSimple(Vec3i position, EnergyProvider provider) {
var pointsOfPresence = new ArrayList<Vec3i>();
pointsOfPresence.add(position);
return new ProviderDetails(pointsOfPresence, provider, 1);
}
public boolean isInRange(ArrayList<Vec3i> pointsOfPresence) {
for (Vec3i point : pointsOfPresence) {
for (Vec3i myPoint : this.pointsOfPresence) {
var manhattanDistance = point.getManhattanDistance(myPoint);
if (manhattanDistance == 0) {
return false;
} else if (manhattanDistance <= range) {
return true;
}
}
}
return false;
}
}

View file

@ -1,14 +0,0 @@
package arzumify.polyenergy.impl;
import arzumify.polyenergy.api.EnergyReceiver;
import net.minecraft.util.math.Vec3i;
import java.util.ArrayList;
public record ReceiverDetails(ArrayList<Vec3i> pointsOfPresence, EnergyReceiver receiver) {
public static ReceiverDetails NewSimple(Vec3i position, EnergyReceiver receiver) {
var pointsOfPresence = new ArrayList<Vec3i>();
pointsOfPresence.add(position);
return new ReceiverDetails(pointsOfPresence, receiver);
}
}

View file

@ -1,105 +0,0 @@
package arzumify.polyenergy.util;
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.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.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* A simple battery that can provide and receive energy. Has a range of 1 block in all directions (directly adjacent blocks).
*/
@SuppressWarnings("unused")
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 ProviderDetails providerDetails = ProviderDetails.NewSimple(pos, this);
private final ReceiverDetails receiverDetails = ReceiverDetails.NewSimple(pos, this);
private final Set<EnergyProvider> providers = ConcurrentHashMap.newKeySet();
private long energy = 0;
private boolean providing = false;
public SimpleBattery(BlockEntityType<?> type, BlockPos pos, BlockState state, long capacity, long inputRate, long outputRate) {
super(type, pos, state);
this.capacity = capacity;
this.inputRate = inputRate;
this.outputRate = outputRate;
CoordinateMatchMaker.addProvider(providerDetails);
CoordinateMatchMaker.addReceiver(receiverDetails);
}
public static void tick(World ignoredWorld, BlockPos ignoredPos, BlockState ignoredState, 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, battery);
battery.energy += extracted;
leftToFill -= extracted;
}
if (!battery.providing && battery.energy > 0) {
CoordinateMatchMaker.addProvider(battery.providerDetails);
battery.providing = true;
}
}
@Override
public long extract(long amount, EnergyReceiver receiver) {
if (providing) {
long extracted = Math.min(Math.min(outputRate, amount), energy);
energy -= extracted;
if (energy == 0) {
CoordinateMatchMaker.removeProvider(providerDetails);
}
return extracted;
} else {
return 0;
}
}
@Override
public void exists(EnergyReceiver receiver, ArrayList<Vec3i> pointsOfPresence) {
receiver.ready(this);
}
@Override
public void ready(EnergyProvider provider) {
providers.add(provider);
}
@Override
public void unready(EnergyProvider provider) {
providers.remove(provider);
}
@Override
public void onLoad(BlockEntity blockEntity, ServerWorld serverWorld) {
if (energy > 0) {
CoordinateMatchMaker.addProvider(providerDetails);
providing = true;
}
CoordinateMatchMaker.addReceiver(receiverDetails);
}
@Override
public void onUnload(BlockEntity blockEntity, ServerWorld serverWorld) {
CoordinateMatchMaker.removeProvider(providerDetails);
CoordinateMatchMaker.removeReceiver(receiverDetails);
providers.clear();
}
}

View file

@ -1,88 +0,0 @@
package arzumify.polyenergy.util;
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.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;
// Cables can have ONE provider and MANY receivers.
@SuppressWarnings("unused")
public class SimpleCable extends BlockEntity implements EnergyProvider, EnergyReceiver, ServerBlockEntityEvents.Unload, ServerBlockEntityEvents.Load {
private final long capacity;
private final long inputRate;
private final long outputRate;
private final ProviderDetails providerDetails = ProviderDetails.NewSimple(pos, this);
private final ReceiverDetails receiverDetails = ReceiverDetails.NewSimple(pos, this);
private EnergyProvider currentProvider;
private long energy = 0;
public SimpleCable(BlockEntityType<?> type, BlockPos pos, BlockState state, long capacity, long inputRate, long outputRate) {
super(type, pos, state);
this.capacity = capacity;
this.inputRate = inputRate;
this.outputRate = outputRate;
CoordinateMatchMaker.addReceiver(receiverDetails);
}
public static void tick(World ignoredWorld, BlockPos ignoredPos, BlockState ignoredState, SimpleCable cable) {
if (cable.currentProvider != null && cable.energy < cable.capacity) {
cable.energy += cable.currentProvider.extract(Math.min(cable.capacity - cable.energy, cable.inputRate), cable);
}
}
@Override
public long extract(long amount, EnergyReceiver receiver) {
long extracted = Math.min(Math.min(outputRate, amount), energy);
energy -= extracted;
if (energy == 0) {
CoordinateMatchMaker.removeProvider(providerDetails);
}
return extracted;
}
@Override
public void exists(EnergyReceiver receiver, ArrayList<Vec3i> pointsOfPresence) {
if (currentProvider != null) {
receiver.ready(currentProvider);
}
}
@Override
public void ready(EnergyProvider provider) {
if (currentProvider == null) {
currentProvider = provider;
CoordinateMatchMaker.addProvider(providerDetails);
}
}
@Override
public void unready(EnergyProvider provider) {
if (currentProvider == provider) {
currentProvider = null;
CoordinateMatchMaker.removeProvider(providerDetails);
}
}
@Override
public void onLoad(BlockEntity blockEntity, ServerWorld serverWorld) {
CoordinateMatchMaker.addReceiver(receiverDetails);
}
@Override
public void onUnload(BlockEntity blockEntity, ServerWorld serverWorld) {
CoordinateMatchMaker.removeProvider(providerDetails);
CoordinateMatchMaker.removeReceiver(receiverDetails);
currentProvider = null;
}
}

View file

@ -1,27 +1,22 @@
{
"schemaVersion": 1,
"id": "polyenergy",
"id": "polyenergy-cursed-edition",
"version": "${version}",
"name": "Polyenergy",
"description": "A dead-simple serverside energy api",
"description": "A very cursed and broken serverside energy api",
"authors": [
"Arzumify"
],
"contact": {
"homepage": "https://git.ailur.dev/arzumify/polyenergy/",
"sources": "https://git.ailur.dev/arzumify/polyenergy"
"homepage": "https://git.ailur.dev/arzumify/polyenergy-cursed-edition/",
"sources": "https://git.ailur.dev/arzumify/polyenergy-cursed-edition/"
},
"license": "GPL-3.0",
"icon": "assets/polyenergy/icon-scaled.png",
"environment": "*",
"entrypoints": {
"main": [
"arzumify.polyenergy.Polyenergy"
]
},
"depends": {
"fabricloader": ">=0.16.10",
"minecraft": "~1.21.4",
"minecraft": ">1.20",
"java": ">=21",
"fabric-api": "*"
}

View file

@ -0,0 +1,40 @@
{
"schema_version": 1,
"quilt_loader": {
"group": "${group}",
"id": "polyenergy-cursed-edition",
"version": "${version}",
"metadata": {
"name": "Polyenergy",
"description": "A cursed serverside energy api",
"contributors": {
"Arzumify": "Owner"
},
"contact": {
"homepage": "https://git.ailur.dev/arzumify/polyenergy-cursed-edition/",
"sources": "https://git.ailur.dev/arzumify/polyenergy-cursed-edition/",
"issues": "https://git.ailur.dev/arzumify/polyenergy-cursed-edition/issues"
},
"icon": "assets/polyenergy/icon-scaled.png",
"license": "GPL-3.0"
},
"intermediate_mappings": "net.fabricmc:intermediary",
"repositories": [
"https://maven.ailur.dev/"
],
"depends": [
{
"id": "quilt_loader",
"versions": ">=0.19.1"
},
{
"id": "quilted_fabric_api",
"versions": ">=7.0.2"
},
{
"id": "minecraft",
"versions": ">=1.20"
}
]
}
}

View file

@ -1,412 +1,4 @@
import arzumify.polyenergy.impl.CoordinateMatchMaker;
import net.minecraft.util.math.Vec3i;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import static org.junit.jupiter.api.Assertions.assertTrue;
@Timeout(1)
public class EnergyTests {
public static void assertWithError(double expected, double actual, double error) {
assertTrue(expected - error <= actual && actual <= expected + error);
}
public static void assertWithError(double expected, double actual) {
assertWithError(expected, actual, 1);
}
@BeforeEach
public void setup() {
// Delete all batteries
CoordinateMatchMaker.reset();
System.out.println("Reset CoordinateMatchMaker");
}
@Test
public void testEnergyTransfer() {
System.out.println("Energy transfer test");
// 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
assertWithError(6, battery1.energy);
assertWithError(10, battery2.energy);
}
@Test
public void testUnloadBattery1() {
System.out.println("Unloading battery1 test");
// 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(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 6 energy and battery2 should have gained 6 energy (±1)
assertWithError(10, battery1.energy);
assertWithError(6, battery2.energy);
}
@Test
public void testUnloadBattery2() {
System.out.println("Unloading battery2 test");
// 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
assertWithError(11, battery1.energy);
assertWithError(5, battery2.energy);
}
@Test
public void testUnloadThenReloadBattery1() {
System.out.println("Unloading then reloading battery1 test");
// 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(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
assertWithError(6, battery1.energy);
assertWithError(10, battery2.energy);
}
@Test
public void testUnloadThenReloadBattery2() {
System.out.println("Unloading then reloading battery2 test");
// 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
assertWithError(6, battery1.energy);
assertWithError(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 11 times
for (int i = 0; i < 11; 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
assert (battery1.energy >= 7 && battery1.energy <= 9);
assert (battery2.energy >= 7 && battery2.energy <= 9);
}
@Test
public void middleOneCharges() {
System.out.println("Middle one charges");
// 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
assertWithError(8, battery1.energy);
assertWithError(0, battery2.energy);
assertWithError(8, battery3.energy);
}
@Test
public void middleOneReceives() {
System.out.println("Middle one receives");
// 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 17 times
for (int i = 0; i < 17; 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
assertWithError(0, battery1.energy);
assertWithError(16, battery2.energy);
assertWithError(0, battery3.energy);
}
@Test
public void testEnergyTransferWithRange() {
System.out.println("Energy transfer with range test");
// 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
assertWithError(16, battery1.energy);
assertWithError(0, battery2.energy);
}
@Test
public void testCables() {
System.out.println("Cables test");
// Create two batteries with coordinates (0, 0, 0) and (2, 0, 0) and a cable at (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(2, 0, 0), 16, 1, 0, "Battery2");
var cable = new SimpleCodeOnlyCable(new Vec3i(1, 0, 0), 1, 1, 1, "Cable");
// Give battery1 some energy
battery1.energy = 16;
// Tick the batteries and cable 16 times
for (int i = 0; i < 16; i++) {
SimpleCodeOnlyBattery.tick(battery1);
SimpleCodeOnlyCable.tick(cable);
SimpleCodeOnlyBattery.tick(battery2);
}
// Output the energy of the batteries
System.out.println("Battery1 energy: " + battery1.energy);
System.out.println("Battery2 energy: " + battery2.energy);
// Output the energy of the cable
System.out.println("Cable energy: " + cable.energy);
// Battery1 should have lost 16 energy, battery2 should have gained 15 energy, and the cable should have 1 energy
assertWithError(1, cable.energy);
assertWithError(0, battery1.energy);
assertWithError(15, battery2.energy);
}
@Test
public void longCableChain() {
System.out.println("Long cable chain test");
// Create 2 batteries with coordinates (0, 0, 0) and (1001, 0, 0) and 1000 cables in between
// Battery1 should be able to provide energy to battery2
var battery1 = new SimpleCodeOnlyBattery(new Vec3i(0, 0, 0), 32, 0, 1, "Battery1");
var battery2 = new SimpleCodeOnlyBattery(new Vec3i(11, 0, 0), 16, 1, 0, "Battery2");
var cables = new SimpleCodeOnlyCable[10];
for (int i = 0; i < 10; i++) {
cables[i] = new SimpleCodeOnlyCable(new Vec3i(i + 1, 0, 0), 16, 1, 1, "Cable" + (i + 1));
}
// Give battery1 some energy
battery1.energy = 32;
// Tick the batteries and cables
for (int i = 0; i < 10; i++) {
SimpleCodeOnlyBattery.tick(battery1);
SimpleCodeOnlyBattery.tick(battery2);
for (SimpleCodeOnlyCable cable : cables) {
SimpleCodeOnlyCable.tick(cable);
}
}
// Output the energy of the batteries
System.out.println("Battery1 energy: " + battery1.energy);
System.out.println("Battery2 energy: " + battery2.energy);
// Output the energy of the cables
for (SimpleCodeOnlyCable cable : cables) {
System.out.println(cable.name + " energy: " + cable.energy);
}
}
@Test
public void stressTest() {
System.out.println("Stress test");
// 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);
}
}
}
/*
TESTS ARE BROKEN IN CURSED EDITION
DO NOT TRY TO TEST
*/

View file

@ -1,108 +0,0 @@
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.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* 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 Set<EnergyProvider> providers = ConcurrentHashMap.newKeySet();
public long energy = 0;
public boolean providing = false;
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: list is " + battery.providers);
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);
battery.energy += extracted;
leftToFill -= extracted;
}
if (!battery.providing && battery.energy > 0) {
CoordinateMatchMaker.addProvider(battery.providerDetails);
battery.providing = true;
}
}
@Override
public long extract(long amount, EnergyReceiver receiver) {
if (providing) {
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;
} else {
return 0;
}
}
@Override
public void exists(EnergyReceiver receiver, ArrayList<Vec3i> 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) {
System.out.println(name + " lost other provider");
providers.remove(provider);
}
@Override
public void onLoad(BlockEntity blockEntity, ServerWorld serverWorld) {
if (energy > 0) {
CoordinateMatchMaker.addProvider(providerDetails);
providing = true;
}
CoordinateMatchMaker.addReceiver(receiverDetails);
}
@Override
public void onUnload(BlockEntity blockEntity, ServerWorld serverWorld) {
CoordinateMatchMaker.removeProvider(providerDetails);
CoordinateMatchMaker.removeReceiver(receiverDetails);
providers.clear();
}
}

View file

@ -1,90 +1,118 @@
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.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.minecraft.block.entity.BlockEntity;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.math.Vec3i;
import java.util.ArrayList;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
// Cables can have ONE provider and MANY receivers.
public class SimpleCodeOnlyCable implements EnergyProvider, EnergyReceiver, ServerBlockEntityEvents.Unload, ServerBlockEntityEvents.Load {
import static arzumify.polyenergy.api.SearchForNeighbours.searchForNeighbours;
/**
* SimpleCable, but implemented using only the API and not the actual block entity for testing purposes.
* All fields are public for testing purposes.
*/
public class SimpleCodeOnlyCable implements EnergyProvider, EnergyReceiver, ServerBlockEntityEvents.Unload, ServerBlockEntityEvents.Load, ServerTickEvents.EndTick {
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 Set<EnergyProvider> providers = ConcurrentHashMap.newKeySet();
public final Set<EnergyProvider> blackListed = ConcurrentHashMap.newKeySet();
public final Set<EnergyReceiver> receivers = ConcurrentHashMap.newKeySet();
public long energy = 0;
public EnergyProvider currentProvider;
public SimpleCodeOnlyCable(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.addReceiver(receiverDetails);
}
public static void tick(SimpleCodeOnlyCable cable) {
System.out.println(cable.name + " ticking");
if (cable.currentProvider != null && cable.energy < cable.capacity) {
cable.energy += cable.currentProvider.extract(Math.min(cable.capacity - cable.energy, cable.inputRate), cable);
public static void tick(SimpleCodeOnlyCable Cable) {
System.out.println(Cable.name + " ticking");
// Split the energy between all receivers
var energyToGive = Cable.energy / Cable.receivers.size();
for (EnergyReceiver receiver : Cable.receivers) {
var taken = receiver.receive(energyToGive, Cable);
Cable.energy -= taken;
if (receiver instanceof EnergyProvider) {
Cable.blackListed.add((EnergyProvider) receiver);
}
}
}
@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);
public long receive(long amount, EnergyProvider provider) {
if (!blackListed.contains(provider)) {
System.out.println(name + " receiving " + amount + " energy");
long received = Math.min(Math.min(inputRate, amount), capacity - energy);
energy += received;
System.out.println(name + " received " + received + " energy");
blackListed.add(provider);
return received;
} else {
return 0;
}
return extracted;
}
@Override
public void exists(EnergyReceiver receiver, ArrayList<Vec3i> pointsOfPresence) {
public void ready(EnergyReceiver receiver) {
System.out.println(name + " found other receiver");
if (currentProvider != null) {
receiver.ready(currentProvider);
}
receivers.add(receiver);
receiver.ready(this);
}
@Override
public void ready(EnergyProvider provider) {
System.out.println(name + " found other provider");
if (currentProvider == null) {
currentProvider = provider;
CoordinateMatchMaker.addProvider(providerDetails);
}
providers.add(provider);
}
@Override
public void unready(EnergyReceiver receiver) {
System.out.println(name + " lost other receiver");
receivers.remove(receiver);
}
@Override
public void unready(EnergyProvider provider) {
if (currentProvider == provider) {
System.out.println(name + " lost other provider");
currentProvider = null;
CoordinateMatchMaker.removeProvider(providerDetails);
}
System.out.println(name + " lost other provider");
providers.remove(provider);
}
@Override
public void onLoad(BlockEntity blockEntity, ServerWorld serverWorld) {
CoordinateMatchMaker.addReceiver(receiverDetails);
var energyBlocks = searchForNeighbours(serverWorld, blockEntity.getPos());
for (var block : energyBlocks) {
if (block instanceof EnergyProvider) {
((EnergyProvider) block).ready(this);
}
if (block instanceof EnergyReceiver) {
ready((EnergyReceiver) block);
}
}
}
@Override
public void onUnload(BlockEntity blockEntity, ServerWorld serverWorld) {
CoordinateMatchMaker.removeProvider(providerDetails);
CoordinateMatchMaker.removeReceiver(receiverDetails);
currentProvider = null;
for (var provider : providers) {
provider.unready(this);
}
providers.clear();
for (var receiver : receivers) {
receiver.unready(this);
}
receivers.clear();
}
@Override
public void onEndTick(MinecraftServer server) {
blackListed.clear();
}
}