diff --git a/core/src/io/anuke/mindustry/content/blocks/PowerBlocks.java b/core/src/io/anuke/mindustry/content/blocks/PowerBlocks.java index 6b47d31a67..cf59a636d1 100644 --- a/core/src/io/anuke/mindustry/content/blocks/PowerBlocks.java +++ b/core/src/io/anuke/mindustry/content/blocks/PowerBlocks.java @@ -4,6 +4,7 @@ import io.anuke.mindustry.content.Liquids; import io.anuke.mindustry.content.fx.BlockFx; import io.anuke.mindustry.game.ContentList; import io.anuke.mindustry.world.Block; +import io.anuke.mindustry.world.Tile; import io.anuke.mindustry.world.blocks.power.*; public class PowerBlocks extends BlockList implements ContentList{ @@ -19,17 +20,17 @@ public class PowerBlocks extends BlockList implements ContentList{ thermalGenerator = new LiquidHeatGenerator("thermal-generator"){{ maxLiquidGenerate = 4f; - // TODO: Adapt to new power system - powerProduction = -1; - powerPerLiquid = 0.1f; + // TODO: Balance + powerProduction = 0.17f; + liquidPowerMultiplier = 0.1f; generateEffect = BlockFx.redgeneratespark; size = 2; }}; turbineGenerator = new TurbineGenerator("turbine-generator"){{ - // TODO: Adapt to new power system + // TODO: Balance powerProduction = 0.28f; - powerPerLiquid = 0.1f; + liquidPowerMultiplier = 0.3f; itemDuration = 30f; consumes.liquid(Liquids.water, 0.05f); size = 2; @@ -41,14 +42,26 @@ public class PowerBlocks extends BlockList implements ContentList{ itemDuration = 220f; }}; - solarPanel = new PowerGenerator("solar-panel"){{ - powerProduction = 0.0045f; - }}; + // TODO: Maybe reintroduce a class for the initial production efficiency + solarPanel = new PowerGenerator("solar-panel"){ + { + powerProduction = 0.0045f; + } + @Override + public void update(Tile tile){ + tile.entity().productionEfficiency = 1.0f; + } + }; - largeSolarPanel = new PowerGenerator("solar-panel-large"){{ - powerProduction = 0.055f; - size = 3; - }}; + largeSolarPanel = new PowerGenerator("solar-panel-large"){ + { + powerProduction = 0.055f; + } + @Override + public void update(Tile tile){ + tile.entity().productionEfficiency = 1.0f; + } + }; thoriumReactor = new NuclearReactor("thorium-reactor"){{ size = 3; diff --git a/core/src/io/anuke/mindustry/world/Block.java b/core/src/io/anuke/mindustry/world/Block.java index 21ee395822..4e6caa78d2 100644 --- a/core/src/io/anuke/mindustry/world/Block.java +++ b/core/src/io/anuke/mindustry/world/Block.java @@ -337,9 +337,8 @@ public class Block extends BaseBlock { } public void setBars(){ - if(consumes.has(ConsumePower.class)){ + if(consumes.has(ConsumePower.class)) bars.add(new BlockBar(BarType.power, true, tile -> tile.entity.power.satisfaction)); - } if(hasLiquids) bars.add(new BlockBar(BarType.liquid, true, tile -> tile.entity.liquids.total() / liquidCapacity)); if(hasItems) diff --git a/core/src/io/anuke/mindustry/world/blocks/power/ItemLiquidGenerator.java b/core/src/io/anuke/mindustry/world/blocks/power/ItemLiquidGenerator.java index 07f16b8767..c41f8d1c39 100644 --- a/core/src/io/anuke/mindustry/world/blocks/power/ItemLiquidGenerator.java +++ b/core/src/io/anuke/mindustry/world/blocks/power/ItemLiquidGenerator.java @@ -14,7 +14,7 @@ import static io.anuke.mindustry.Vars.tilesize; public abstract class ItemLiquidGenerator extends ItemGenerator{ protected float minLiquidEfficiency = 0.2f; - protected float powerPerLiquid = 0.13f; + protected float liquidPowerMultiplier = 1.3f; // A liquid with 100% flammability will be 30% more efficient than an item with 100% flammability. /**Maximum liquid used per frame.*/ protected float maxLiquidGenerate = 0.4f; @@ -45,44 +45,48 @@ public abstract class ItemLiquidGenerator extends ItemGenerator{ } } + // Note: Do not use this delta when calculating the amount of power or the power efficiency, but use it for resource consumption if necessary. + // Power amount is delta'd by PowerGraph class already. + float calculationDelta = entity.delta(); + + if(!entity.cons.valid()){ + entity.productionEfficiency = 0.0f; + return; + } //liquid takes priority over solids - if(liquid != null && entity.liquids.get(liquid) >= 0.001f && entity.cons.valid()){ - float powerPerLiquid = getLiquidEfficiency(liquid) * this.powerPerLiquid; - float used = Math.min(entity.liquids.get(liquid), maxLiquidGenerate * entity.delta()); - // TODO: Adapt to new power system - //used = Math.min(used, (powerCapacity - entity.power.amount) / powerPerLiquid); + if(liquid != null && entity.liquids.get(liquid) >= 0.001f){ + float baseLiquidEfficiency = getLiquidEfficiency(liquid) * this.liquidPowerMultiplier; + float maximumPossible = maxLiquidGenerate * calculationDelta; + float used = Math.min(entity.liquids.get(liquid) * calculationDelta, maximumPossible); entity.liquids.remove(liquid, used); - // TODO: Adapt to new power system - //entity.power.amount += used * powerPerLiquid; + + // Note: 1 Item with 100% Flammability = 100% efficiency. This means 100% is not max but rather a reference point for this generator. + entity.productionEfficiency = baseLiquidEfficiency * used / maximumPossible; if(used > 0.001f && Mathf.chance(0.05 * entity.delta())){ Effects.effect(generateEffect, tile.drawx() + Mathf.range(3f), tile.drawy() + Mathf.range(3f)); } - }else if(entity.cons.valid()){ - - // TODO: Adapt to new power system - //float maxPower = Math.min(powerCapacity - entity.power.amount, powerOutput * entity.delta()) * entity.efficiency; + }else{ if(entity.generateTime <= 0f && entity.items.total() > 0){ Effects.effect(generateEffect, tile.worldx() + Mathf.range(3f), tile.worldy() + Mathf.range(3f)); Item item = entity.items.take(); - // TODO: Adapt to new power system - //entity.efficiency = getItemEfficiency(item); + entity.productionEfficiency = getItemEfficiency(item); entity.explosiveness = item.explosiveness; entity.generateTime = 1f; } if(entity.generateTime > 0f){ entity.generateTime -= 1f / itemDuration * entity.delta(); - // TODO: Adapt to new power system - //entity.power.amount += maxPower; entity.generateTime = Mathf.clamp(entity.generateTime); if(Mathf.chance(entity.delta() * 0.06 * Mathf.clamp(entity.explosiveness - 0.25f))){ entity.damage(Mathf.random(8f)); Effects.effect(explodeEffect, tile.worldx() + Mathf.range(size * tilesize / 2f), tile.worldy() + Mathf.range(size * tilesize / 2f)); } + }else{ + entity.productionEfficiency = 0.0f; } } } diff --git a/core/src/io/anuke/mindustry/world/blocks/power/LiquidGenerator.java b/core/src/io/anuke/mindustry/world/blocks/power/LiquidGenerator.java index 0b19f94fc0..7314c56a0d 100644 --- a/core/src/io/anuke/mindustry/world/blocks/power/LiquidGenerator.java +++ b/core/src/io/anuke/mindustry/world/blocks/power/LiquidGenerator.java @@ -13,7 +13,7 @@ import io.anuke.ucore.util.Mathf; public abstract class LiquidGenerator extends PowerGenerator{ protected float minEfficiency = 0.2f; - protected float powerPerLiquid; + protected float liquidPowerMultiplier; /**Maximum liquid used per frame.*/ protected float maxLiquidGenerate; protected Effect generateEffect = BlockFx.generatespark; @@ -50,8 +50,9 @@ public abstract class LiquidGenerator extends PowerGenerator{ public void update(Tile tile){ TileEntity entity = tile.entity(); + // TODO Code duplication with ItemLiquidGenerator if(entity.liquids.get(entity.liquids.current()) >= 0.001f){ - float powerPerLiquid = getEfficiency(entity.liquids.current()) * this.powerPerLiquid; + //float powerPerLiquid = getEfficiency(entity.liquids.current()) * this.powerPerLiquid; float used = Math.min(entity.liquids.currentAmount(), maxLiquidGenerate * entity.delta()); // TODO Adapt to new power system //used = Math.min(used, (powerCapacity - entity.power.amount) / powerPerLiquid); diff --git a/core/src/io/anuke/mindustry/world/blocks/power/LiquidHeatGenerator.java b/core/src/io/anuke/mindustry/world/blocks/power/LiquidHeatGenerator.java index 8b5665b4c5..9129e884e9 100644 --- a/core/src/io/anuke/mindustry/world/blocks/power/LiquidHeatGenerator.java +++ b/core/src/io/anuke/mindustry/world/blocks/power/LiquidHeatGenerator.java @@ -14,9 +14,9 @@ public class LiquidHeatGenerator extends LiquidGenerator{ public void setStats(){ super.setStats(); - // TODO Verify for new power system stats.remove(BlockStat.basePowerGeneration); - stats.add(BlockStat.basePowerGeneration, maxLiquidGenerate * powerPerLiquid * 60f, StatUnit.powerSecond); + // TODO Adapt to new new power system. Maybe this override can be removed. + //stats.add(BlockStat.basePowerGeneration, * 60f, StatUnit.powerSecond); } @Override diff --git a/core/src/io/anuke/mindustry/world/blocks/power/PowerGenerator.java b/core/src/io/anuke/mindustry/world/blocks/power/PowerGenerator.java index 915ef48021..8a3e59dd9d 100644 --- a/core/src/io/anuke/mindustry/world/blocks/power/PowerGenerator.java +++ b/core/src/io/anuke/mindustry/world/blocks/power/PowerGenerator.java @@ -1,5 +1,7 @@ package io.anuke.mindustry.world.blocks.power; +import io.anuke.mindustry.world.BarType; +import io.anuke.mindustry.world.meta.BlockBar; import io.anuke.mindustry.world.meta.StatUnit; import io.anuke.ucore.util.EnumSet; @@ -10,7 +12,12 @@ import io.anuke.mindustry.world.meta.BlockStat; public class PowerGenerator extends PowerDistributor{ /** The amount of power produced per tick. */ - public float powerProduction; + protected float powerProduction; + /** The maximum possible efficiency for this generator. Supply values larger than 1.0f if more than 100% is possible. + * This could be the case when e.g. an item with 100% flammability is the reference point, but a more effective liquid + * can be supplied as an alternative. + */ + protected float maxEfficiency = 1.0f; public BlockStat generationType = BlockStat.basePowerGeneration; public PowerGenerator(String name){ @@ -40,8 +47,16 @@ public class PowerGenerator extends PowerDistributor{ return new GeneratorEntity(); } + @Override + public void setBars(){ + super.setBars(); + if(hasPower){ + bars.add(new BlockBar(BarType.power, true, tile -> tile.entity().productionEfficiency / maxEfficiency)); + } + } + public static class GeneratorEntity extends TileEntity{ public float generateTime; - public float productionEfficiency = 1; + public float productionEfficiency = 0.0f; } } diff --git a/core/src/io/anuke/mindustry/world/blocks/power/PowerGraph.java b/core/src/io/anuke/mindustry/world/blocks/power/PowerGraph.java index 8439d79c11..e9e693755a 100644 --- a/core/src/io/anuke/mindustry/world/blocks/power/PowerGraph.java +++ b/core/src/io/anuke/mindustry/world/blocks/power/PowerGraph.java @@ -121,7 +121,7 @@ public class PowerGraph{ if(consumePower.isBuffered){ // Add a percentage of the requested amount, but limit it to the mission amount. // TODO This can maybe be calculated without converting to absolute values first - float maximumRate = consumePower.requestedPower(consumer.block(), consumer.entity()) * coverage; + float maximumRate = consumePower.requestedPower(consumer.block(), consumer.entity()) * coverage * consumer.entity.delta(); float missingAmount = consumePower.powerCapacity * (1 - consumer.entity.power.satisfaction); consumer.entity.power.satisfaction += Math.min(missingAmount, maximumRate) / consumePower.powerCapacity; }else{ diff --git a/core/src/io/anuke/mindustry/world/consumers/ConsumePower.java b/core/src/io/anuke/mindustry/world/consumers/ConsumePower.java index 2fd27ace54..d4d9a3dffb 100644 --- a/core/src/io/anuke/mindustry/world/consumers/ConsumePower.java +++ b/core/src/io/anuke/mindustry/world/consumers/ConsumePower.java @@ -86,7 +86,9 @@ public class ConsumePower extends Consume{ * @return The amount of power which is requested per tick. */ public float requestedPower(Block block, TileEntity entity){ - // TODO Is it possible to make the block not consume power while items/liquids are missing? + // TODO Make the block not consume power on the following conditions, either here or in PowerGraph: + // - Other consumers are not valid, e.g. additional input items/liquids are missing + // - Buffer is full return powerPerTick; } diff --git a/tests/src/test/java/PowerTestFixture.java b/tests/src/test/java/PowerTestFixture.java deleted file mode 100644 index 2ecd5a7dc7..0000000000 --- a/tests/src/test/java/PowerTestFixture.java +++ /dev/null @@ -1,65 +0,0 @@ -import io.anuke.mindustry.content.blocks.Blocks; -import io.anuke.mindustry.world.Block; -import io.anuke.mindustry.world.Tile; -import io.anuke.mindustry.world.blocks.Floor; -import io.anuke.mindustry.world.blocks.power.Battery; -import io.anuke.mindustry.world.blocks.power.PowerGenerator; -import io.anuke.mindustry.world.modules.PowerModule; - -import java.lang.reflect.Field; - -/** This class provides objects commonly used by power related unit tests. - * For now, this is a helper with static methods, but this might change. - * */ -public class PowerTestFixture{ - - protected static PowerGenerator createFakeProducerBlock(float producedPower){ - return new PowerGenerator("fakegen"){{ - powerProduction = producedPower; - }}; - } - - protected static Battery createFakeBattery(float capacity, float ticksToFill){ - return new Battery("fakebattery"){{ - consumes.powerBuffered(capacity, ticksToFill); - }}; - } - - protected static Block createFakeDirectConsumer(float powerPerTick, float minimumSatisfaction){ - return new Block("fakedirectconsumer"){{ - consumes.powerDirect(powerPerTick, minimumSatisfaction); - }}; - } - - protected static Block createFakeBufferedConsumer(float capacity, float ticksToFill){ - return new Block("fakebufferedconsumer"){{ - consumes.powerBuffered(capacity, ticksToFill); - }}; - } - /** - * Creates a fake tile on the given location using the given block. - * @param x The X coordinate. - * @param y The y coordinate. - * @param block The block on the tile. - * @return The created tile or null in case of exceptions. - */ - protected static Tile createFakeTile(int x, int y, Block block){ - try{ - Tile tile = new Tile(x, y); - - Field field = Tile.class.getDeclaredField("wall"); - field.setAccessible(true); - field.set(tile, block); - - field = Tile.class.getDeclaredField("floor"); - field.setAccessible(true); - field.set(tile, Blocks.sand); - - tile.entity = block.newEntity(); - tile.entity.power = new PowerModule(); - return tile; - }catch(Exception ex){ - return null; - } - } -} diff --git a/tests/src/test/java/FakeThreadHandler.java b/tests/src/test/java/power/FakeThreadHandler.java similarity index 78% rename from tests/src/test/java/FakeThreadHandler.java rename to tests/src/test/java/power/FakeThreadHandler.java index 3588205e98..9767268df3 100644 --- a/tests/src/test/java/FakeThreadHandler.java +++ b/tests/src/test/java/power/FakeThreadHandler.java @@ -1,15 +1,17 @@ -import com.badlogic.gdx.Gdx; +package power; + import io.anuke.mindustry.core.ThreadHandler; import io.anuke.ucore.core.Timers; /** Fake thread handler which produces a new frame each time getFrameID is called and always provides a delta of 1. */ public class FakeThreadHandler extends ThreadHandler{ private int fakeFrameId = 0; + public static final float fakeDelta = 0.5f; FakeThreadHandler(){ super(); - Timers.setDeltaProvider(() -> 1.0f); + Timers.setDeltaProvider(() -> fakeDelta); } @Override public long getFrameID(){ diff --git a/tests/src/test/java/power/ItemLiquidGeneratorTests.java b/tests/src/test/java/power/ItemLiquidGeneratorTests.java new file mode 100644 index 0000000000..c1b1ff6bf8 --- /dev/null +++ b/tests/src/test/java/power/ItemLiquidGeneratorTests.java @@ -0,0 +1,142 @@ +package power; + +import io.anuke.mindustry.Vars; +import io.anuke.mindustry.content.Items; +import io.anuke.mindustry.content.Liquids; +import io.anuke.mindustry.type.Item; +import io.anuke.mindustry.type.Liquid; +import io.anuke.mindustry.world.Tile; +import io.anuke.mindustry.world.blocks.power.BurnerGenerator; +import io.anuke.mindustry.world.blocks.power.ItemGenerator; +import io.anuke.mindustry.world.blocks.power.ItemLiquidGenerator; +import org.junit.jupiter.api.*; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; + +/** + * This class tests ItemLiquidGenerators. Currently, testing is only performed on the BurnerGenerator subclass, + * which means only power calculations based on flammability are tested. + * All tests are run with a fixed delta of 0.5 so delta considerations can be tested as well. + * Additionally, each PowerGraph::update() call will have its own thread frame, i.e. the method will never be called twice within the same frame. + * Both of these constraints are handled by FakeThreadHandler within PowerTestFixture. + * Any power amount (produced, consumed, buffered) should be affected by FakeThreadHandler.fakeDelta but satisfaction should not! + */ +public class ItemLiquidGeneratorTests extends PowerTestFixture{ + + private ItemLiquidGenerator generator; + private Tile tile; + private ItemGenerator.ItemGeneratorEntity entity; + private final float fakeLiquidPowerMultiplier = 2.0f; + private final float fakeItemDuration = 60f; // 60 ticks + private final float maximumLiquidUsage = 0.5f; + + @BeforeEach + public void createBurnerGenerator(){ + // Use a burner generator instead of a custom ItemLiquidGenerator subclass since we would implement abstract methods the same way. + generator = new BurnerGenerator("fakegen"){{ + powerProduction = 0.1f; + itemDuration = 60f; + liquidPowerMultiplier = fakeLiquidPowerMultiplier; + itemDuration = fakeItemDuration; + maxLiquidGenerate = maximumLiquidUsage; + }}; + + tile = createFakeTile(0, 0, generator); + entity = tile.entity(); + } + + /** Tests the consumption and efficiency when being supplied with liquids. */ + @TestFactory + DynamicTest[] testLiquidConsumption(){ + return new DynamicTest[]{ + dynamicTest("01", () -> test_liquidConsumption(Liquids.oil, 0.0f, "No liquids provided")), + dynamicTest("02", () -> test_liquidConsumption(Liquids.oil, maximumLiquidUsage / 4.0f, "Low oil provided")), + dynamicTest("03", () -> test_liquidConsumption(Liquids.oil, maximumLiquidUsage * 1.0f, "Sufficient oil provided")), + dynamicTest("04", () -> test_liquidConsumption(Liquids.oil, maximumLiquidUsage * 2.0f, "Excess oil provided")) + // Note: The generator will decline any other liquid since it's not flammable + }; + } + + void test_liquidConsumption(Liquid liquid, float availableLiquidAmount, String parameterDescription){ + final float baseEfficiency = fakeLiquidPowerMultiplier * liquid.flammability; + final float expectedEfficiency = Math.min(1.0f, availableLiquidAmount / maximumLiquidUsage) * baseEfficiency; + final float expectedConsumptionPerTick = Math.min(maximumLiquidUsage, availableLiquidAmount); + final float expectedRemainingLiquidAmount = Math.max(0.0f, availableLiquidAmount - expectedConsumptionPerTick * FakeThreadHandler.fakeDelta); + + assertTrue(generator.acceptLiquid(tile, null, liquid, availableLiquidAmount), parameterDescription + ": Liquids which will be declined by the generator don't need to be tested - The code won't be called for those cases."); + + // Reset liquids since BeforeEach will not be called between dynamic tests + for(Liquid tmpLiquid : Vars.content.liquids()){ + entity.liquids.reset(tmpLiquid, 0.0f); + } + entity.liquids.add(liquid, availableLiquidAmount); + entity.cons.update(tile.entity); + assertTrue(entity.cons.valid()); + + // Perform an update on the generator once - This should use up any resource up to the maximum liquid usage + generator.update(tile); + + assertEquals(expectedRemainingLiquidAmount, entity.liquids.get(liquid), parameterDescription + ": Remaining liquid amount mismatch."); + assertEquals(expectedEfficiency, entity.productionEfficiency, parameterDescription + ": Efficiency mismatch."); + } + + /** Tests the consumption and efficiency when being supplied with items. */ + @TestFactory + DynamicTest[] testItemConsumption(){ + return new DynamicTest[]{ + dynamicTest("01", () -> test_itemConsumption(Items.coal, 0, "No items provided")), + dynamicTest("02", () -> test_itemConsumption(Items.coal, 1, "Sufficient coal provided")), + dynamicTest("03", () -> test_itemConsumption(Items.coal, 10, "Excess coal provided")), + dynamicTest("04", () -> test_itemConsumption(Items.blastCompound, 1, "Blast compound provided")), + //dynamicTest("03", () -> test_itemConsumption(Items.plastanium, 1, "Plastanium provided")), // Not accepted by generator due to low flammability + dynamicTest("05", () -> test_itemConsumption(Items.biomatter, 1, "Biomatter provided")), + dynamicTest("06", () -> test_itemConsumption(Items.pyratite, 1, "Pyratite provided")) + }; + } + + void test_itemConsumption(Item item, int amount, String parameterDescription){ + final float expectedEfficiency = Math.min(1.0f, amount > 0 ? item.flammability : 0f); + final float expectedRemainingItemAmount = Math.max(0, amount - 1); + assertTrue(generator.acceptItem(item, tile, null), parameterDescription + ": Items which will be declined by the generator don't need to be tested - The code won't be called for those cases."); + + + // Clean up manually since BeforeEach will not be called between dynamic tests + entity.items.clear(); + entity.generateTime = 0.0f; + entity.productionEfficiency = 0.0f; + + if(amount > 0){ + entity.items.add(item, amount); + } + entity.cons.update(tile.entity); + assertTrue(entity.cons.valid()); + + // Perform an update on the generator once - This should use up one or zero items - dependent on if the item is accepted and available or not. + generator.update(tile); + + assertEquals(expectedRemainingItemAmount, entity.items.get(item), parameterDescription + ": Remaining item amount mismatch."); + assertEquals(expectedEfficiency, entity.productionEfficiency, parameterDescription + ": Efficiency mismatch."); + } + + /** Makes sure the efficiency stays equal during the item duration. */ + @Test + void test_efficiencyConstantDuringItemDuration(){ + + // Burn a single coal and test for the duration + entity.items.add(Items.coal, 1); + entity.cons.update(tile.entity); + generator.update(tile); + + float expectedEfficiency = entity.productionEfficiency; + + float currentDuration = 0.0f; + while((currentDuration += FakeThreadHandler.fakeDelta) <= fakeItemDuration){ + generator.update(tile); + assertEquals(expectedEfficiency, entity.productionEfficiency, "Duration: " + String.valueOf(currentDuration)); + } + generator.update(tile); + assertEquals(0.0f, entity.productionEfficiency, "Duration: " + String.valueOf(currentDuration)); + } +} diff --git a/tests/src/test/java/power/PowerTestFixture.java b/tests/src/test/java/power/PowerTestFixture.java new file mode 100644 index 0000000000..1e46d1e287 --- /dev/null +++ b/tests/src/test/java/power/PowerTestFixture.java @@ -0,0 +1,104 @@ +package power; + +import com.badlogic.gdx.math.MathUtils; +import io.anuke.mindustry.Vars; +import io.anuke.mindustry.content.blocks.Blocks; +import io.anuke.mindustry.core.ContentLoader; +import io.anuke.mindustry.core.World; +import io.anuke.mindustry.entities.TileEntity; +import io.anuke.mindustry.world.Block; +import io.anuke.mindustry.world.Tile; +import io.anuke.mindustry.world.blocks.PowerBlock; +import io.anuke.mindustry.world.blocks.power.Battery; +import io.anuke.mindustry.world.blocks.power.PowerGenerator; +import io.anuke.mindustry.world.modules.ConsumeModule; +import io.anuke.mindustry.world.modules.ItemModule; +import io.anuke.mindustry.world.modules.LiquidModule; +import io.anuke.mindustry.world.modules.PowerModule; +import io.anuke.ucore.entities.Entities; +import org.junit.jupiter.api.BeforeAll; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import static io.anuke.mindustry.Vars.world; + +/** This class provides objects commonly used by power related unit tests. + * For now, this is a helper with static methods, but this might change. + * + * Note: All tests which subclass this will run with a fixed delta of 0.5! + * */ +public class PowerTestFixture{ + + public static final float smallRoundingTolerance = MathUtils.FLOAT_ROUNDING_ERROR; + public static final float mediumRoundingTolerance = MathUtils.FLOAT_ROUNDING_ERROR * 10; + public static final float highRoundingTolerance = MathUtils.FLOAT_ROUNDING_ERROR * 100; + + @BeforeAll + static void initializeDependencies(){ + Vars.content = new ContentLoader(); + Vars.content.load(); + Vars.threads = new FakeThreadHandler(); + } + + protected static PowerGenerator createFakeProducerBlock(float producedPower){ + return new PowerGenerator("fakegen"){{ + powerProduction = producedPower; + }}; + } + + protected static Battery createFakeBattery(float capacity, float ticksToFill){ + return new Battery("fakebattery"){{ + consumes.powerBuffered(capacity, ticksToFill); + }}; + } + + protected static Block createFakeDirectConsumer(float powerPerTick, float minimumSatisfaction){ + return new PowerBlock("fakedirectconsumer"){{ + consumes.powerDirect(powerPerTick, minimumSatisfaction); + }}; + } + + protected static Block createFakeBufferedConsumer(float capacity, float ticksToFill){ + return new PowerBlock("fakebufferedconsumer"){{ + consumes.powerBuffered(capacity, ticksToFill); + }}; + } + /** + * Creates a fake tile on the given location using the given block. + * @param x The X coordinate. + * @param y The y coordinate. + * @param block The block on the tile. + * @return The created tile or null in case of exceptions. + */ + protected static Tile createFakeTile(int x, int y, Block block){ + try{ + Tile tile = new Tile(x, y); + + // Using the Tile(int, int, byte, byte) constructor would require us to register any fake block or tile we create + // Since this part shall not be part of the test and would require more work anyway, we manually set the block and floor + // through reflections and then simulate part of what the changed() method does. + + Field field = Tile.class.getDeclaredField("wall"); + field.setAccessible(true); + field.set(tile, block); + + field = Tile.class.getDeclaredField("floor"); + field.setAccessible(true); + field.set(tile, Blocks.sand); + + // Simulate the "changed" method. Calling it through reflections would require half the game to be initialized. + tile.entity = block.newEntity().init(tile, false); + tile.entity.cons = new ConsumeModule(); + if(block.hasItems) tile.entity.items = new ItemModule(); + if(block.hasLiquids) tile.entity.liquids = new LiquidModule(); + if(block.hasPower){ + tile.entity.power = new PowerModule(); + tile.entity.power.graph.add(tile); + } + return tile; + }catch(Exception ex){ + return null; + } + } +} diff --git a/tests/src/test/java/PowerTests.java b/tests/src/test/java/power/PowerTests.java similarity index 62% rename from tests/src/test/java/PowerTests.java rename to tests/src/test/java/power/PowerTests.java index 3b93652c0e..25a85c58d2 100644 --- a/tests/src/test/java/PowerTests.java +++ b/tests/src/test/java/power/PowerTests.java @@ -1,28 +1,26 @@ +package power; + import com.badlogic.gdx.math.MathUtils; -import io.anuke.mindustry.Vars; -import io.anuke.mindustry.content.blocks.PowerBlocks; -import io.anuke.mindustry.content.blocks.ProductionBlocks; -import io.anuke.mindustry.core.ContentLoader; import io.anuke.mindustry.world.Tile; +import io.anuke.mindustry.world.blocks.power.PowerGenerator; import io.anuke.mindustry.world.blocks.power.PowerGraph; +import io.anuke.mindustry.world.consumers.ConsumePower; import org.junit.jupiter.api.*; -import org.junit.jupiter.params.provider.ValueSource; -import org.junit.jupiter.params.ParameterizedTest; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.junit.jupiter.api.DynamicTest.dynamicTest; +/** + * Tests code related to the power system in general, but not specific blocks. + * All tests are run with a fixed delta of 0.5 so delta considerations can be tested as well. + * Additionally, each PowerGraph::update() call will have its own thread frame, i.e. the method will never be called twice within the same frame. + * Both of these constraints are handled by FakeThreadHandler within PowerTestFixture. + * Any power amount (produced, consumed, buffered) should be affected by FakeThreadHandler.fakeDelta but satisfaction should not! + */ public class PowerTests extends PowerTestFixture{ - @BeforeAll - static void initializeDependencies(){ - Vars.content = new ContentLoader(); - Vars.content.load(); - Vars.threads = new FakeThreadHandler(); - } - @BeforeEach void initTest(){ } @@ -39,6 +37,7 @@ public class PowerTests extends PowerTestFixture{ return new DynamicTest[]{ // Note: Unfortunately, the display names are not yet output through gradle. See https://github.com/gradle/gradle/issues/5975 // That's why we inject the description into the test method for now. + // Additional Note: If you don't see any labels in front of the values supplied as function parameters, use a better IDE like IntelliJ IDEA. dynamicTest("01", () -> test_directConsumptionCalculation(0.0f, 1.0f, 0.0f, "0.0 produced, 1.0 consumed (no power available)")), dynamicTest("02", () -> test_directConsumptionCalculation(0.0f, 0.0f, 0.0f, "0.0 produced, 0.0 consumed (no power anywhere)")), dynamicTest("03", () -> test_directConsumptionCalculation(1.0f, 0.0f, 0.0f, "1.0 produced, 0.0 consumed (no power requested)")), @@ -50,14 +49,15 @@ public class PowerTests extends PowerTestFixture{ } void test_directConsumptionCalculation(float producedPower, float requiredPower, float expectedSatisfaction, String parameterDescription){ Tile producerTile = createFakeTile(0, 0, createFakeProducerBlock(producedPower)); + producerTile.entity().productionEfficiency = 1.0f; Tile directConsumerTile = createFakeTile(0, 1, createFakeDirectConsumer(requiredPower, 0.6f)); PowerGraph powerGraph = new PowerGraph(); powerGraph.add(producerTile); powerGraph.add(directConsumerTile); - assumeTrue(MathUtils.isEqual(producedPower, powerGraph.getPowerProduced())); - assumeTrue(MathUtils.isEqual(requiredPower, powerGraph.getPowerNeeded())); + assertEquals(producedPower * FakeThreadHandler.fakeDelta, powerGraph.getPowerProduced(), MathUtils.FLOAT_ROUNDING_ERROR); + assertEquals(requiredPower * FakeThreadHandler.fakeDelta, powerGraph.getPowerNeeded(), MathUtils.FLOAT_ROUNDING_ERROR); // Update and check for the expected power satisfaction of the consumer powerGraph.update(); @@ -69,33 +69,36 @@ public class PowerTests extends PowerTestFixture{ DynamicTest[] testBufferedConsumption(){ return new DynamicTest[]{ // Note: powerPerTick may not be 0 in any of the test cases. This would equal a "ticksToFill" of infinite. + // Note: Due to a fixed delta of 0.5, only half of what is defined here will in fact be produced/consumed. Keep this in mind when defining expectedSatisfaction! dynamicTest("01", () -> test_bufferedConsumptionCalculation(0.0f, 0.0f, 0.1f, 0.0f, 0.0f, "Empty Buffer, No power anywhere")), dynamicTest("02", () -> test_bufferedConsumptionCalculation(0.0f, 1.0f, 0.1f, 0.0f, 0.0f, "Empty Buffer, No power provided")), dynamicTest("03", () -> test_bufferedConsumptionCalculation(1.0f, 0.0f, 0.1f, 0.0f, 0.0f, "Empty Buffer, No power requested")), - dynamicTest("04", () -> test_bufferedConsumptionCalculation(1.0f, 1.0f, 1.0f, 0.0f, 1.0f, "Empty Buffer, Stable Power, One tick to fill")), - dynamicTest("05", () -> test_bufferedConsumptionCalculation(1.0f, 1.0f, 0.1f, 0.0f, 0.1f, "Empty Buffer, Stable Power, multiple ticks to fill")), - dynamicTest("06", () -> test_bufferedConsumptionCalculation(1.0f, 0.5f, 0.5f, 0.0f, 1.0f, "Empty Buffer, Power excess, one tick to fill")), - dynamicTest("07", () -> test_bufferedConsumptionCalculation(1.0f, 0.5f, 0.1f, 0.0f, 0.2f, "Empty Buffer, Power excess, multiple ticks to fill")), - dynamicTest("08", () -> test_bufferedConsumptionCalculation(0.5f, 1.0f, 1.0f, 0.0f, 0.5f, "Empty Buffer, Power shortage, one tick to fill")), - dynamicTest("09", () -> test_bufferedConsumptionCalculation(0.5f, 1.0f, 0.1f, 0.0f, 0.1f, "Empty Buffer, Power shortage, multiple ticks to fill")), - dynamicTest("10", () -> test_bufferedConsumptionCalculation(0.0f, 1.0f, 0.1f, 0.5f, 0.5f, "Unchanged buffer with no power produced")), - dynamicTest("11", () -> test_bufferedConsumptionCalculation(1.0f, 1.0f, 0.1f, 1.0f, 1.0f, "Unchanged buffer when already full")), - dynamicTest("12", () -> test_bufferedConsumptionCalculation(0.2f, 1.0f, 0.5f, 0.5f, 0.7f, "Half buffer, power shortage")), - dynamicTest("13", () -> test_bufferedConsumptionCalculation(1.0f, 1.0f, 0.5f, 0.7f, 1.0f, "Buffer does not get exceeded")), - dynamicTest("14", () -> test_bufferedConsumptionCalculation(1.0f, 1.0f, 0.5f, 0.5f, 1.0f, "Half buffer, filled with excess")) + dynamicTest("04", () -> test_bufferedConsumptionCalculation(1.0f, 1.0f, 1.0f, 0.0f, 0.5f, "Empty Buffer, Stable Power, One tick to fill")), + dynamicTest("05", () -> test_bufferedConsumptionCalculation(2.0f, 1.0f, 2.0f, 0.0f, 1.0f, "Empty Buffer, Stable Power, One delta to fill")), + dynamicTest("06", () -> test_bufferedConsumptionCalculation(1.0f, 1.0f, 0.1f, 0.0f, 0.05f, "Empty Buffer, Stable Power, multiple ticks to fill")), + dynamicTest("07", () -> test_bufferedConsumptionCalculation(1.2f, 0.5f, 1.0f, 0.0f, 1.0f, "Empty Buffer, Power excess, one delta to fill")), + dynamicTest("08", () -> test_bufferedConsumptionCalculation(1.0f, 0.5f, 0.1f, 0.0f, 0.1f, "Empty Buffer, Power excess, multiple ticks to fill")), + dynamicTest("09", () -> test_bufferedConsumptionCalculation(1.0f, 1.0f, 2.0f, 0.0f, 0.5f, "Empty Buffer, Power shortage, one delta to fill")), + dynamicTest("10", () -> test_bufferedConsumptionCalculation(0.5f, 1.0f, 0.1f, 0.0f, 0.05f, "Empty Buffer, Power shortage, multiple ticks to fill")), + dynamicTest("11", () -> test_bufferedConsumptionCalculation(0.0f, 1.0f, 0.1f, 0.5f, 0.5f, "Unchanged buffer with no power produced")), + dynamicTest("12", () -> test_bufferedConsumptionCalculation(1.0f, 1.0f, 0.1f, 1.0f, 1.0f, "Unchanged buffer when already full")), + dynamicTest("13", () -> test_bufferedConsumptionCalculation(0.2f, 1.0f, 0.5f, 0.5f, 0.6f, "Half buffer, power shortage")), + dynamicTest("14", () -> test_bufferedConsumptionCalculation(1.0f, 1.0f, 0.5f, 0.9f, 1.0f, "Buffer does not get exceeded")), + dynamicTest("15", () -> test_bufferedConsumptionCalculation(2.0f, 1.0f, 1.0f, 0.5f, 1.0f, "Half buffer, filled with excess")) }; } - void test_bufferedConsumptionCalculation(float producedPower, float maxBuffer, float powerPerTick, float initialSatisfaction, float expectedSatisfaction, String parameterDescription){ + void test_bufferedConsumptionCalculation(float producedPower, float maxBuffer, float powerConsumedPerTick, float initialSatisfaction, float expectedSatisfaction, String parameterDescription){ Tile producerTile = createFakeTile(0, 0, createFakeProducerBlock(producedPower)); - Tile bufferedConsumerTile = createFakeTile(0, 1, createFakeBufferedConsumer(maxBuffer, maxBuffer > 0.0f ? maxBuffer/powerPerTick : 1.0f)); + producerTile.entity().productionEfficiency = 1.0f; + Tile bufferedConsumerTile = createFakeTile(0, 1, createFakeBufferedConsumer(maxBuffer, maxBuffer > 0.0f ? maxBuffer/powerConsumedPerTick : 1.0f)); bufferedConsumerTile.entity.power.satisfaction = initialSatisfaction; PowerGraph powerGraph = new PowerGraph(); powerGraph.add(producerTile); powerGraph.add(bufferedConsumerTile); - assumeTrue(MathUtils.isEqual(producedPower, powerGraph.getPowerProduced())); - //assumeTrue(MathUtils.isEqual(Math.min(maxBuffer, powerPerTick), powerGraph.getPowerNeeded())); + assertEquals(producedPower * FakeThreadHandler.fakeDelta, powerGraph.getPowerProduced(), MathUtils.FLOAT_ROUNDING_ERROR, parameterDescription + ": Produced power did not match"); + assertEquals(Math.min(maxBuffer, powerConsumedPerTick * FakeThreadHandler.fakeDelta), powerGraph.getPowerNeeded(), MathUtils.FLOAT_ROUNDING_ERROR, parameterDescription + ": ConsumedPower did not match"); // Update and check for the expected power satisfaction of the consumer powerGraph.update(); @@ -108,14 +111,15 @@ public class PowerTests extends PowerTestFixture{ @TestFactory DynamicTest[] testDirectConsumptionWithBattery(){ return new DynamicTest[]{ - dynamicTest("01", () -> test_directConsumptionWithBattery(10.0f, 0.0f, 0.0f, 10.0f, 0.0f, "Empty battery, no consumer")), - dynamicTest("02", () -> test_directConsumptionWithBattery(10.0f, 0.0f, 90.0f, 100.0f, 0.0f, "Battery full after update, no consumer")), + // Note: expectedBatteryCapacity is currently adjusted to a delta of 0.5! (FakeThreadHandler sets it to that) + dynamicTest("01", () -> test_directConsumptionWithBattery(10.0f, 0.0f, 0.0f, 5.0f, 0.0f, "Empty battery, no consumer")), + dynamicTest("02", () -> test_directConsumptionWithBattery(10.0f, 0.0f, 94.999f, 99.999f, 0.0f, "Battery almost full after update, no consumer")), dynamicTest("03", () -> test_directConsumptionWithBattery(10.0f, 0.0f, 100.0f, 100.0f, 0.0f, "Full battery, no consumer")), dynamicTest("04", () -> test_directConsumptionWithBattery(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, "No producer, no consumer, empty battery")), dynamicTest("05", () -> test_directConsumptionWithBattery(0.0f, 0.0f, 100.0f, 100.0f, 0.0f, "No producer, no consumer, full battery")), dynamicTest("06", () -> test_directConsumptionWithBattery(0.0f, 10.0f, 0.0f, 0.0f, 0.0f, "No producer, empty battery")), - dynamicTest("07", () -> test_directConsumptionWithBattery(0.0f, 10.0f, 100.0f, 90.0f, 1.0f, "No producer, full battery")), - dynamicTest("08", () -> test_directConsumptionWithBattery(0.0f, 10.0f, 5.0f, 0.0f, 0.5f, "No producer, low battery")), + dynamicTest("07", () -> test_directConsumptionWithBattery(0.0f, 10.0f, 100.0f, 95.0f, 1.0f, "No producer, full battery")), + dynamicTest("08", () -> test_directConsumptionWithBattery(0.0f, 10.0f, 2.5f, 0.0f, 0.5f, "No producer, low battery")), dynamicTest("09", () -> test_directConsumptionWithBattery(5.0f, 10.0f, 5.0f, 0.0f, 1.0f, "Producer + Battery = Consumed")), }; } @@ -124,6 +128,7 @@ public class PowerTests extends PowerTestFixture{ if(producedPower > 0.0f){ Tile producerTile = createFakeTile(0, 0, createFakeProducerBlock(producedPower)); + producerTile.entity().productionEfficiency = 1.0f; powerGraph.add(producerTile); } Tile directConsumerTile = null; @@ -138,10 +143,35 @@ public class PowerTests extends PowerTestFixture{ powerGraph.add(batteryTile); powerGraph.update(); - assertEquals(expectedBatteryCapacity, batteryTile.entity.power.satisfaction * maxCapacity, MathUtils.FLOAT_ROUNDING_ERROR, parameterDescription + ": Expected battery capacity did not match"); + assertEquals(expectedBatteryCapacity / maxCapacity, batteryTile.entity.power.satisfaction, MathUtils.FLOAT_ROUNDING_ERROR, parameterDescription + ": Expected battery satisfaction did not match"); if(directConsumerTile != null){ assertEquals(expectedSatisfaction, directConsumerTile.entity.power.satisfaction, MathUtils.FLOAT_ROUNDING_ERROR, parameterDescription + ": Satisfaction of direct consumer did not match"); } } + + /** Makes sure a direct consumer stops working after power production is set to zero. */ + @Test + void testDirectConsumptionStopsWithNoPower(){ + Tile producerTile = createFakeTile(0, 0, createFakeProducerBlock(10.0f)); + producerTile.entity().productionEfficiency = 1.0f; + Tile consumerTile = createFakeTile(0, 1, createFakeDirectConsumer(5.0f, 0.6f)); + + PowerGraph powerGraph = new PowerGraph(); + powerGraph.add(producerTile); + powerGraph.add(consumerTile); + powerGraph.update(); + + assertEquals(1.0f, consumerTile.entity.power.satisfaction, MathUtils.FLOAT_ROUNDING_ERROR); + + powerGraph.remove(producerTile); + powerGraph.add(consumerTile); + powerGraph.update(); + + assertEquals(0.0f, consumerTile.entity.power.satisfaction, MathUtils.FLOAT_ROUNDING_ERROR); + if(consumerTile.block().consumes.has(ConsumePower.class)){ + ConsumePower consumePower = consumerTile.block().consumes.get(ConsumePower.class); + assertFalse(consumePower.valid(consumerTile.block(), consumerTile.entity())); + } + } } }