diff --git a/gradle.properties b/gradle.properties index a57240398..c5ada32b7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -47,7 +47,7 @@ mod_name=Super Factory Manager # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. mod_license=Mozilla Public License Version 2.0 # The mod version. See https://semver.org/ -mod_version=4.16.1 +mod_version=4.17.0 # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # This should match the base package used for the mod sources. # See https://maven.apache.org/guides/mini/guide-naming-conventions.html diff --git a/src/gametest/java/ca/teamdman/sfm/SFMCorrectnessGameTests.java b/src/gametest/java/ca/teamdman/sfm/SFMCorrectnessGameTests.java index c70cbd4f3..3efe92ddc 100644 --- a/src/gametest/java/ca/teamdman/sfm/SFMCorrectnessGameTests.java +++ b/src/gametest/java/ca/teamdman/sfm/SFMCorrectnessGameTests.java @@ -1,14 +1,20 @@ package ca.teamdman.sfm; +import ca.teamdman.sfm.common.Constants; import ca.teamdman.sfm.common.blockentity.ManagerBlockEntity; import ca.teamdman.sfm.common.blockentity.PrintingPressBlockEntity; import ca.teamdman.sfm.common.cablenetwork.CableNetwork; import ca.teamdman.sfm.common.cablenetwork.CableNetworkManager; import ca.teamdman.sfm.common.item.DiskItem; import ca.teamdman.sfm.common.item.FormItem; +import ca.teamdman.sfm.common.net.ServerboundOutputInspectionRequestPacket; +import ca.teamdman.sfm.common.program.GatherWarningsProgramBehaviour; import ca.teamdman.sfm.common.program.LabelPositionHolder; +import ca.teamdman.sfm.common.program.ProgramContext; import ca.teamdman.sfm.common.registry.SFMBlocks; import ca.teamdman.sfm.common.registry.SFMItems; +import ca.teamdman.sfml.ast.OutputStatement; +import ca.teamdman.sfml.ast.Program; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.gametest.framework.GameTest; @@ -71,9 +77,6 @@ public static void manager_state_update(GameTestHelper helper) { helper.succeed(); } - /** - * Ensure moving everything a single stack of - */ @GameTest(template = "3x2x1") public static void move_1_stack(GameTestHelper helper) { helper.setBlock(new BlockPos(1, 2, 0), SFMBlocks.MANAGER_BLOCK.get()); @@ -2465,6 +2468,44 @@ public static void forget_slot(GameTestHelper helper) { } + @GameTest(template = "3x2x1") + public static void forget_input_count_state(GameTestHelper helper) { + helper.setBlock(new BlockPos(1, 2, 0), SFMBlocks.MANAGER_BLOCK.get()); + BlockPos rightPos = new BlockPos(0, 2, 0); + helper.setBlock(rightPos, SFMBlocks.TEST_BARREL_BLOCK.get()); + BlockPos leftPos = new BlockPos(2, 2, 0); + helper.setBlock(leftPos, SFMBlocks.TEST_BARREL_BLOCK.get()); + + var rightChest = getItemHandler(helper, rightPos); + var leftChest = getItemHandler(helper, leftPos); + + leftChest.insertItem(0, new ItemStack(Blocks.DIRT, 64), false); + + ManagerBlockEntity manager = (ManagerBlockEntity) helper.getBlockEntity(new BlockPos(1, 2, 0)); + manager.setItem(0, new ItemStack(SFMItems.DISK_ITEM.get())); + manager.setProgram(""" + EVERY 20 TICKS DO + INPUT 10 FROM a,b + OUTPUT 1 to z + FORGET b + OUTPUT to z + END + """.stripTrailing().stripIndent()); + + // set the labels + LabelPositionHolder.empty() + .add("a", helper.absolutePos(leftPos)) + .add("b", helper.absolutePos(leftPos)) + .add("z", helper.absolutePos(rightPos)) + .save(manager.getDisk().get()); + + succeedIfManagerDidThingWithoutLagging(helper, manager, () -> { + assertTrue(leftChest.getStackInSlot(0).getCount() == 64-10, "did not remain"); + assertTrue(rightChest.getStackInSlot(0).getCount() == 10, "did not arrive"); + helper.succeed(); + }); + } + @GameTest(template = "3x2x1") public static void reorder_1(GameTestHelper helper) { helper.setBlock(new BlockPos(1, 2, 0), SFMBlocks.MANAGER_BLOCK.get()); @@ -2868,7 +2909,7 @@ public static void multi_io_limits(GameTestHelper helper) { }); } - @GameTest(template = "3x4x3", batch = "laggy") + @GameTest(template = "3x4x3") public static void move_on_pulse(GameTestHelper helper) { var managerPos = new BlockPos(1, 2, 1); var buttonPos = managerPos.offset(Direction.NORTH.getNormal()); @@ -2918,4 +2959,481 @@ public static void move_on_pulse(GameTestHelper helper) { // push the button helper.pressButton(buttonPos); } + + @GameTest(template = "3x2x1", batch="linting") + public static void count_execution_paths_1(GameTestHelper helper) { + // place inventories + helper.setBlock(new BlockPos(1, 2, 0), SFMBlocks.MANAGER_BLOCK.get()); + BlockPos rightPos = new BlockPos(0, 2, 0); + helper.setBlock(rightPos, SFMBlocks.TEST_BARREL_BLOCK.get()); + BlockPos leftPos = new BlockPos(2, 2, 0); + helper.setBlock(leftPos, SFMBlocks.TEST_BARREL_BLOCK.get()); + + // place manager + ManagerBlockEntity manager = (ManagerBlockEntity) helper.getBlockEntity(new BlockPos(1, 2, 0)); + manager.setItem(0, new ItemStack(SFMItems.DISK_ITEM.get())); + + // set the labels + LabelPositionHolder labelPositionHolder = LabelPositionHolder.empty() + .add("left", helper.absolutePos(leftPos)) + .add("right", helper.absolutePos(rightPos)) + .save(manager.getDisk().get()); + + // load the program + manager.setProgram(""" + EVERY 20 TICKS DO + INPUT FROM left + OUTPUT TO right + END + """.stripTrailing().stripIndent()); + assertManagerRunning(manager); + var program = manager.getProgram().get(); + + // ensure no warnings + var warnings = DiskItem.getWarnings(manager.getDisk().get()); + assertTrue(warnings.isEmpty(), "expected 0 warning, got " + warnings.size()); + + // count the execution paths + GatherWarningsProgramBehaviour simulation = new GatherWarningsProgramBehaviour(warnings::addAll); + program.tick(ProgramContext.createSimulationContext( + program, + labelPositionHolder, + 0, + simulation + )); + assertTrue(simulation.getSeenPaths().size() == 1, "expected single execution path"); + assertTrue(simulation.getSeenPaths().get(0).history().size() == 2, "expected two elements in execution path"); + helper.succeed(); + } + + @GameTest(template = "3x2x1", batch="linting") + public static void count_execution_paths_2(GameTestHelper helper) { + // place inventories + helper.setBlock(new BlockPos(1, 2, 0), SFMBlocks.MANAGER_BLOCK.get()); + BlockPos rightPos = new BlockPos(0, 2, 0); + helper.setBlock(rightPos, SFMBlocks.TEST_BARREL_BLOCK.get()); + BlockPos leftPos = new BlockPos(2, 2, 0); + helper.setBlock(leftPos, SFMBlocks.TEST_BARREL_BLOCK.get()); + + // place manager + ManagerBlockEntity manager = (ManagerBlockEntity) helper.getBlockEntity(new BlockPos(1, 2, 0)); + manager.setItem(0, new ItemStack(SFMItems.DISK_ITEM.get())); + + // set the labels + LabelPositionHolder labelPositionHolder = LabelPositionHolder.empty() + .add("left", helper.absolutePos(leftPos)) + .add("right", helper.absolutePos(rightPos)) + .save(manager.getDisk().get()); + + // load the program + manager.setProgram(""" + EVERY 20 TICKS DO + INPUT FROM left + OUTPUT TO right + END + EVERY 20 TICKS DO + INPUT FROM left + OUTPUT TO right + OUTPUT TO right + END + """.stripTrailing().stripIndent()); + assertManagerRunning(manager); + var program = manager.getProgram().get(); + + // ensure no warnings + var warnings = DiskItem.getWarnings(manager.getDisk().get()); + assertTrue(warnings.isEmpty(), "expected 0 warning, got " + warnings.size()); + + // count the execution paths + GatherWarningsProgramBehaviour simulation = new GatherWarningsProgramBehaviour(warnings::addAll); + program.tick(ProgramContext.createSimulationContext( + program, + labelPositionHolder, + 0, + simulation + )); + assertTrue(simulation.getSeenPaths().size() == 2, "expected single execution path"); + assertTrue(simulation.getSeenPaths().get(0).history().size() == 2, "expected two elements in execution path"); + assertTrue(simulation.getSeenPaths().get(1).history().size() == 3, "expected two elements in execution path"); + helper.succeed(); + } + + @GameTest(template = "3x2x1", batch="linting") + public static void count_execution_paths_3(GameTestHelper helper) { + // place inventories + helper.setBlock(new BlockPos(1, 2, 0), SFMBlocks.MANAGER_BLOCK.get()); + BlockPos rightPos = new BlockPos(0, 2, 0); + helper.setBlock(rightPos, SFMBlocks.TEST_BARREL_BLOCK.get()); + BlockPos leftPos = new BlockPos(2, 2, 0); + helper.setBlock(leftPos, SFMBlocks.TEST_BARREL_BLOCK.get()); + + // place manager + ManagerBlockEntity manager = (ManagerBlockEntity) helper.getBlockEntity(new BlockPos(1, 2, 0)); + manager.setItem(0, new ItemStack(SFMItems.DISK_ITEM.get())); + + // set the labels + LabelPositionHolder labelPositionHolder = LabelPositionHolder.empty() + .add("left", helper.absolutePos(leftPos)) + .add("right", helper.absolutePos(rightPos)) + .save(manager.getDisk().get()); + + // load the program + manager.setProgram(""" + EVERY 20 TICKS DO + INPUT FROM left + OUTPUT TO right + END + EVERY 20 TICKS DO + INPUT FROM left + INPUT FROM left + OUTPUT TO right + OUTPUT TO right + END + EVERY 20 TICKS DO + INPUT FROM left + INPUT FROM left + OUTPUT TO right + END + """.stripTrailing().stripIndent()); + assertManagerRunning(manager); + var program = manager.getProgram().get(); + + // ensure no warnings + var warnings = DiskItem.getWarnings(manager.getDisk().get()); + assertTrue(warnings.isEmpty(), "expected 0 warning, got " + warnings.size()); + + // count the execution paths + GatherWarningsProgramBehaviour simulation = new GatherWarningsProgramBehaviour(warnings::addAll); + program.tick(ProgramContext.createSimulationContext( + program, + labelPositionHolder, + 0, + simulation + )); + assertTrue(simulation.getSeenPaths().size() == 3, "expected single execution path"); + assertTrue(simulation.getSeenPaths().get(0).history().size() == 2, "expected two elements in execution path"); + assertTrue(simulation.getSeenPaths().get(1).history().size() == 4, "expected two elements in execution path"); + assertTrue(simulation.getSeenPaths().get(2).history().size() == 3, "expected two elements in execution path"); + helper.succeed(); + } + + + @GameTest(template = "3x2x1", batch="linting") + public static void count_execution_paths_conditional_1(GameTestHelper helper) { + // place inventories + helper.setBlock(new BlockPos(1, 2, 0), SFMBlocks.MANAGER_BLOCK.get()); + BlockPos rightPos = new BlockPos(0, 2, 0); + helper.setBlock(rightPos, SFMBlocks.TEST_BARREL_BLOCK.get()); + BlockPos leftPos = new BlockPos(2, 2, 0); + helper.setBlock(leftPos, SFMBlocks.TEST_BARREL_BLOCK.get()); + + // place manager + ManagerBlockEntity manager = (ManagerBlockEntity) helper.getBlockEntity(new BlockPos(1, 2, 0)); + manager.setItem(0, new ItemStack(SFMItems.DISK_ITEM.get())); + + // set the labels + LabelPositionHolder labelPositionHolder = LabelPositionHolder.empty() + .add("left", helper.absolutePos(leftPos)) + .add("right", helper.absolutePos(rightPos)) + .save(manager.getDisk().get()); + + // load the program + manager.setProgram(""" + EVERY 20 TICKS DO + IF left HAS gt 0 stone THEN + INPUT FROM left + END + OUTPUT TO right + END + """.stripTrailing().stripIndent()); + assertManagerRunning(manager); + var program = manager.getProgram().get(); + + // ensure no warnings + var warnings = DiskItem.getWarnings(manager.getDisk().get()); + assertTrue(warnings.isEmpty(), "expected 0 warning, got " + warnings.size()); + + // count the execution paths + GatherWarningsProgramBehaviour simulation = new GatherWarningsProgramBehaviour(warnings::addAll); + program.tick(ProgramContext.createSimulationContext( + program, + labelPositionHolder, + 0, + simulation + )); + + List expectedPathSizes = new ArrayList<>(List.of(1,2)); + assertTrue(simulation.getSeenPaths().size() == expectedPathSizes.size(), "expected " + expectedPathSizes.size() + " execution paths, got " + simulation.getSeenPaths().size()); + int[] actualPathIOSizes = simulation.getSeenIOStatementCountForEachPath(); + // don't assume the order, just that each path size has occurred the specified number of times + for (int i = 0; i < actualPathIOSizes.length; i++) { + int pathSize = actualPathIOSizes[i]; + if (!expectedPathSizes.remove((Integer) pathSize)) { + helper.fail("unexpected path size " + pathSize + " at index " + i + " of " + simulation.getSeenPaths().size() + " paths"); + } + } + helper.succeed(); + } + @GameTest(template = "3x2x1", batch="linting") + public static void count_execution_paths_conditional_1b(GameTestHelper helper) { + // place inventories + helper.setBlock(new BlockPos(1, 2, 0), SFMBlocks.MANAGER_BLOCK.get()); + BlockPos rightPos = new BlockPos(0, 2, 0); + helper.setBlock(rightPos, SFMBlocks.TEST_BARREL_BLOCK.get()); + BlockPos leftPos = new BlockPos(2, 2, 0); + helper.setBlock(leftPos, SFMBlocks.TEST_BARREL_BLOCK.get()); + + // place manager + ManagerBlockEntity manager = (ManagerBlockEntity) helper.getBlockEntity(new BlockPos(1, 2, 0)); + manager.setItem(0, new ItemStack(SFMItems.DISK_ITEM.get())); + + // set the labels + LabelPositionHolder.empty() + .add("left", helper.absolutePos(leftPos)) + .save(manager.getDisk().get()); + + // load the program + manager.setProgram(""" + EVERY 20 TICKS DO + IF left HAS gt 0 stone THEN + INPUT FROM left + END + END + """.stripTrailing().stripIndent()); + assertManagerRunning(manager); + + // assert expected warnings + var warnings = DiskItem.getWarnings(manager.getDisk().get()); + assertTrue(warnings.size() == 1, "expected 1 warning, got " + warnings.size()); + assertTrue(warnings + .get(0) + .getKey() + .equals(Constants.LocalizationKeys.PROGRAM_WARNING_UNUSED_INPUT_LABEL // should be unused input + .key() + .get()), "expected output without matching input warning"); + helper.succeed(); + } + @GameTest(template = "3x2x1", batch="linting") + public static void count_execution_paths_conditional_2(GameTestHelper helper) { + // place inventories + helper.setBlock(new BlockPos(1, 2, 0), SFMBlocks.MANAGER_BLOCK.get()); + BlockPos rightPos = new BlockPos(0, 2, 0); + helper.setBlock(rightPos, SFMBlocks.TEST_BARREL_BLOCK.get()); + BlockPos leftPos = new BlockPos(2, 2, 0); + helper.setBlock(leftPos, SFMBlocks.TEST_BARREL_BLOCK.get()); + + // place manager + ManagerBlockEntity manager = (ManagerBlockEntity) helper.getBlockEntity(new BlockPos(1, 2, 0)); + manager.setItem(0, new ItemStack(SFMItems.DISK_ITEM.get())); + + // set the labels + LabelPositionHolder labelPositionHolder = LabelPositionHolder.empty() + .add("left1", helper.absolutePos(leftPos)) + .add("left2", helper.absolutePos(leftPos)) + .add("right", helper.absolutePos(rightPos)) + .save(manager.getDisk().get()); + + // load the program + manager.setProgram(""" + EVERY 20 TICKS DO + IF left2 HAS gt 0 stone THEN + INPUT FROM left1 + END + IF left1 HAS gt 0 stone THEN + INPUT FROM left2 + END + OUTPUT TO right + END + """.stripTrailing().stripIndent()); + assertManagerRunning(manager); + var program = manager.getProgram().get(); + + // ensure no warnings + var warnings = DiskItem.getWarnings(manager.getDisk().get()); + assertTrue(warnings.isEmpty(), "expected 0 warning, got " + warnings.size()); + + // count the execution paths + GatherWarningsProgramBehaviour simulation = new GatherWarningsProgramBehaviour(warnings::addAll); + program.tick(ProgramContext.createSimulationContext( + program, + labelPositionHolder, + 0, + simulation + )); + List expectedPathSizes = new ArrayList<>(List.of(1,2,2,3)); + assertTrue(simulation.getSeenPaths().size() == expectedPathSizes.size(), "expected " + expectedPathSizes.size() + " execution paths, got " + simulation.getSeenPaths().size()); + int[] actualPathIOSizes = simulation.getSeenIOStatementCountForEachPath(); + // don't assume the order, just that each path size has occurred the specified number of times + for (int i = 0; i < actualPathIOSizes.length; i++) { + int pathSize = actualPathIOSizes[i]; + if (!expectedPathSizes.remove((Integer) pathSize)) { + helper.fail("unexpected path size " + pathSize + " at index " + i + " of " + simulation.getSeenPaths().size() + " paths"); + } + } + helper.succeed(); + } + + @GameTest(template = "3x2x1", batch="linting") + public static void unused_io_warning_output_label_not_presnet_in_input(GameTestHelper helper) { + // place inventories + helper.setBlock(new BlockPos(1, 2, 0), SFMBlocks.MANAGER_BLOCK.get()); + BlockPos rightPos = new BlockPos(0, 2, 0); + helper.setBlock(rightPos, SFMBlocks.TEST_BARREL_BLOCK.get()); + BlockPos leftPos = new BlockPos(2, 2, 0); + helper.setBlock(leftPos, SFMBlocks.TEST_BARREL_BLOCK.get()); + + // place manager + ManagerBlockEntity manager = (ManagerBlockEntity) helper.getBlockEntity(new BlockPos(1, 2, 0)); + manager.setItem(0, new ItemStack(SFMItems.DISK_ITEM.get())); + LabelPositionHolder.empty() + .add("bruh", helper.absolutePos(leftPos)) + .save(manager.getDisk().get()); + manager.setProgram(""" + EVERY 20 TICKS DO + OUTPUT TO bruh + END + """.stripTrailing().stripIndent()); + assertManagerRunning(manager); + + // assert expected warnings + var warnings = DiskItem.getWarnings(manager.getDisk().get()); + assertTrue(warnings.size() == 1, "expected 1 warning, got " + warnings.size()); + assertTrue(warnings + .get(0) + .getKey() + .equals(Constants.LocalizationKeys.PROGRAM_WARNING_OUTPUT_RESOURCE_TYPE_NOT_FOUND_IN_INPUTS + .key() + .get()), "expected output without matching input warning"); + helper.succeed(); + } + + + @GameTest(template = "3x2x1", batch="linting") + public static void unused_io_warning_input_label_not_present_in_output(GameTestHelper helper) { + // place inventories + helper.setBlock(new BlockPos(1, 2, 0), SFMBlocks.MANAGER_BLOCK.get()); + BlockPos rightPos = new BlockPos(0, 2, 0); + helper.setBlock(rightPos, SFMBlocks.TEST_BARREL_BLOCK.get()); + BlockPos leftPos = new BlockPos(2, 2, 0); + helper.setBlock(leftPos, SFMBlocks.TEST_BARREL_BLOCK.get()); + + // place manager + ManagerBlockEntity manager = (ManagerBlockEntity) helper.getBlockEntity(new BlockPos(1, 2, 0)); + manager.setItem(0, new ItemStack(SFMItems.DISK_ITEM.get())); + LabelPositionHolder.empty() + .add("left", helper.absolutePos(leftPos)) + .save(manager.getDisk().get()); + manager.setProgram(""" + EVERY 20 TICKS DO + INPUT FROM left + END + """.stripTrailing().stripIndent()); + assertManagerRunning(manager); + + // assert expected warnings + var warnings = DiskItem.getWarnings(manager.getDisk().get()); + assertTrue(warnings.size() == 1, "expected 1 warning, got " + warnings.size()); + assertTrue(warnings + .get(0) + .getKey() + .equals(Constants.LocalizationKeys.PROGRAM_WARNING_UNUSED_INPUT_LABEL // should be unused input + .key() + .get()), "expected output without matching input warning"); + helper.succeed(); + } + + + @GameTest(template = "3x2x1", batch="linting") + public static void conditional_output_inspection(GameTestHelper helper) { + helper.setBlock(new BlockPos(1, 2, 0), SFMBlocks.MANAGER_BLOCK.get()); + BlockPos rightPos = new BlockPos(0, 2, 0); + helper.setBlock(rightPos, SFMBlocks.TEST_BARREL_BLOCK.get()); + BlockPos leftPos = new BlockPos(2, 2, 0); + helper.setBlock(leftPos, SFMBlocks.TEST_BARREL_BLOCK.get()); + + var rightChest = getItemHandler(helper, rightPos); + var leftChest = getItemHandler(helper, leftPos); + + leftChest.insertItem(0, new ItemStack(Blocks.DIRT, 64), false); + + ManagerBlockEntity manager = (ManagerBlockEntity) helper.getBlockEntity(new BlockPos(1, 2, 0)); + manager.setItem(0, new ItemStack(SFMItems.DISK_ITEM.get())); + + + // set the labels + LabelPositionHolder.empty() + .add("a", helper.absolutePos(leftPos)) + .add("b", helper.absolutePos(rightPos)) + .save(manager.getDisk().get()); + + // set the program + String code = """ + EVERY 20 TICKS DO + IF a HAS = 64 dirt THEN + INPUT RETAIN 32 FROM a + END + OUTPUT TO b + END + """.stripTrailing().stripIndent(); + manager.setProgram(code); + assertManagerRunning(manager); + + // compile new program for inspection + Program program = compile(code); + + + OutputStatement outputStatement = (OutputStatement) program + .triggers() + .get(0) + .getBlock() + .getStatements() + .get(1); + + String inspectionResults = ServerboundOutputInspectionRequestPacket.getOutputStatementInspectionResultsString( + manager, + program, + outputStatement + ); + + //noinspection TrailingWhitespacesInTextBlock + String expected = """ + OUTPUT TO b + -- predictions may differ from actual execution results + -- POSSIBILITY 0 -- all false + OVERALL a HAS = 64 dirt -- false + + -- predicted inputs: + none + -- predicted outputs: + none + -- POSSIBILITY 1 -- all true + OVERALL a HAS = 64 dirt -- true + + -- predicted inputs: + INPUT 32 minecraft:dirt FROM a SLOTS 0 + -- predicted outputs: + OUTPUT 32 minecraft:dirt TO b + """.stripLeading().stripIndent().stripTrailing(); + if (!inspectionResults.equals(expected)) { + System.out.println("Received results:"); + System.out.println(inspectionResults); + System.out.println("Expected:"); + System.out.println(expected); + + // get the position of the difference and show it + for (int i = 0; i < inspectionResults.length(); i++) { + if (inspectionResults.charAt(i) != expected.charAt(i)) { + System.out.println("Difference at position " + i + ":" + inspectionResults.charAt(i) + " vs " + expected.charAt(i)); + break; + } + } + + helper.fail("inspection didn't match results"); + } + + succeedIfManagerDidThingWithoutLagging(helper, manager, () -> { + assertTrue(leftChest.getStackInSlot(0).getCount() == 32, "Dirt did not depart"); + assertTrue(rightChest.getStackInSlot(0).getCount() == 32, "Dirt did not arrive"); + }); + } } diff --git a/src/gametest/java/ca/teamdman/sfm/SFMGameTestBase.java b/src/gametest/java/ca/teamdman/sfm/SFMGameTestBase.java index e4e151e2c..d5aa600c8 100644 --- a/src/gametest/java/ca/teamdman/sfm/SFMGameTestBase.java +++ b/src/gametest/java/ca/teamdman/sfm/SFMGameTestBase.java @@ -4,6 +4,7 @@ import ca.teamdman.sfm.common.item.DiskItem; import ca.teamdman.sfm.common.program.ProgramContext; import ca.teamdman.sfml.ast.Block; +import ca.teamdman.sfml.ast.Program; import ca.teamdman.sfml.ast.Trigger; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; @@ -23,6 +24,7 @@ import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.IntStream; import java.util.stream.LongStream; @@ -33,6 +35,21 @@ protected static void assertTrue(boolean condition, String message) { } } + protected static Program compile(String code) { + AtomicReference rtn = new AtomicReference<>(); + Program.compile( + code, + rtn::set, + errors -> { + throw new GameTestAssertException("Failed to compile program: " + errors + .stream() + .map(Object::toString) + .reduce("", (a, b) -> a + "\n" + b)); + } + ); + return rtn.get(); + } + protected static void succeedIfManagerDidThingWithoutLagging( GameTestHelper helper, ManagerBlockEntity manager, diff --git a/src/generated/resources/assets/sfm/lang/en_us.json b/src/generated/resources/assets/sfm/lang/en_us.json index f40a0ffdd..3e149f392 100644 --- a/src/generated/resources/assets/sfm/lang/en_us.json +++ b/src/generated/resources/assets/sfm/lang/en_us.json @@ -50,6 +50,10 @@ "gui.sfm.manager.edit_button.tooltip": "Press Ctrl+E to edit.", "gui.sfm.manager.hovered_tick_time": "Hovered tick time: %s ms", "gui.sfm.manager.peak_tick_time": "Peak tick time: %s ms", + "gui.sfm.manager.reset_confirm_screen.message": "Are you sure you want to reset this disk?", + "gui.sfm.manager.reset_confirm_screen.no_button": "Never mind, make no changes", + "gui.sfm.manager.reset_confirm_screen.title": "Reset disk?", + "gui.sfm.manager.reset_confirm_screen.yes_button": "Wipe program and labels", "gui.sfm.manager.state": "State: %s", "gui.sfm.manager.state.invalid_program": "invalid program", "gui.sfm.manager.state.no_disk": "missing disk", @@ -70,6 +74,7 @@ "gui.sfm.save_changes_confirm.yes_button": "Overwrite disk", "gui.sfm.text_editor.done_button.tooltip": "Shift+Enter to submit", "gui.sfm.text_editor.title": "Text Editor", + "gui.sfm.text_editor.toggle_line_numbers_button.tooltip": "Toggle line numbers", "gui.sfm.title.labelgun": "Label Gun", "gui.sfm.title.program_template_picker": "Program Template Picker", "item.sfm.disk": "Factory Manager Program Disk", @@ -109,7 +114,7 @@ "log.sfm.program.tick": "PROGRAM TICK BEGIN", "log.sfm.program.tick.redstone_count": "Program ticking with %d unprocessed redstone pulses.", "log.sfm.program.voided_resources": "!!!RESOURCE LOSS HAS OCCURRED!!! Failed to move all promised items, found %s %s:%s, took %d but had %d left over after insertion", - "log.sfm.resource_type.get_capabilities.begin": "Gathering capabilities of type %s against labels %s", + "log.sfm.resource_type.get_capabilities.begin": "Gathering capabilities of type %s (%s) against labels %s", "log.sfm.resource_type.get_capabilities.not_present": "Capability %s %s direction=%s not present", "log.sfm.resource_type.get_capabilities.present": "Capability %s %s direction=%s present", "log.sfm.statement.tick.forget": "FORGET %s", @@ -124,7 +129,7 @@ "log.sfm.statement.tick.io.gather_slots.not_each": "EACH keyword not used - trackers will be shared between blocks", "log.sfm.statement.tick.io.gather_slots.not_in_range": "Slot %d - not in range", "log.sfm.statement.tick.io.gather_slots.range": "Gathering slots in range set: %s", - "log.sfm.statement.tick.io.gather_slots.resource_types": "Gathering for: %s", + "log.sfm.statement.tick.io.gather_slots.resource_types": "Gathering for: %s (%s)", "log.sfm.statement.tick.io.gather_slots.should_not_create": "Slot %d - skipping - %s", "log.sfm.statement.tick.io.move_to.begin": "Begin moving %s into %s", "log.sfm.statement.tick.io.move_to.destination_tracker_reject": "Destination tracker rejected the transfer, skipping", @@ -156,9 +161,11 @@ "program.sfm.warnings.adjacent_but_disconnected_label": "Label \"%s\" is assigned in the world at %s and is connected by cables but is not detected as a valid inventory.", "program.sfm.warnings.disconnected_label": "Label \"%s\" is assigned in the world at %s but not connected by cables.", "program.sfm.warnings.each_without_pattern": "EACH used without a pattern, statement %s", + "program.sfm.warnings.output_label_not_found_in_inputs": "Statement \"%s\" at %s uses resource type \"%s\" which has no matching input statement.", "program.sfm.warnings.round_robin_smelly_count": "Round robin by label should be used with more than one label, statement %s", "program.sfm.warnings.round_robin_smelly_each": "Round robin by block shouldn't be used with EACH, statement %s", "program.sfm.warnings.undefined_label": "Label \"%s\" is assigned in the world but not defined in code.", "program.sfm.warnings.unknown_resource_id": "Resource \"%s\" was not found.", + "program.sfm.warnings.unused_input_label": "Statement \"%s\" at %s inputs \"%s\" from \"%s\" but no future output statement consume \"%s\".", "program.sfm.warnings.unused_label": "Label \"%s\" is used in code but not assigned in the world." } \ No newline at end of file diff --git a/src/main/java/ca/teamdman/sfm/client/ProgramTokenContextActions.java b/src/main/java/ca/teamdman/sfm/client/ProgramTokenContextActions.java index bbde471a0..dc5ca5de2 100644 --- a/src/main/java/ca/teamdman/sfm/client/ProgramTokenContextActions.java +++ b/src/main/java/ca/teamdman/sfm/client/ProgramTokenContextActions.java @@ -33,7 +33,7 @@ public static Optional getContextAction(String programString, int curs .getNodesUnderCursor(cursorPosition - 1) .stream() ) - .map(pair -> getContextAction(programString, builder, pair.a, pair.b, cursorPosition)) + .map(pair -> getContextAction(programString, builder, pair.getFirst(), pair.getSecond(), cursorPosition)) .filter(Optional::isPresent) .map(Optional::get) .findFirst(); diff --git a/src/main/java/ca/teamdman/sfm/client/gui/screen/ExampleEditScreen.java b/src/main/java/ca/teamdman/sfm/client/gui/screen/ExampleEditScreen.java index 897821959..6858c5ebb 100644 --- a/src/main/java/ca/teamdman/sfm/client/gui/screen/ExampleEditScreen.java +++ b/src/main/java/ca/teamdman/sfm/client/gui/screen/ExampleEditScreen.java @@ -27,7 +27,7 @@ public boolean equalsAnyTemplate(String content) { @Override public void saveAndClose() { // The user is attempting to apply a code change to the disk - if (equalsAnyTemplate(program)) { + if (program.isBlank() || equalsAnyTemplate(program)) { // The disk contains template code, safe to overwrite super.saveAndClose(); } else { diff --git a/src/main/java/ca/teamdman/sfm/client/gui/screen/ExamplesScreen.java b/src/main/java/ca/teamdman/sfm/client/gui/screen/ExamplesScreen.java index 68046ad49..13e9f9351 100644 --- a/src/main/java/ca/teamdman/sfm/client/gui/screen/ExamplesScreen.java +++ b/src/main/java/ca/teamdman/sfm/client/gui/screen/ExamplesScreen.java @@ -53,7 +53,7 @@ protected void init() { String finalProgram = program; Program.compile( program, - (successProgram, builder) -> templatePrograms.put( + successProgram -> templatePrograms.put( successProgram.name().isBlank() ? entry.getKey().toString() : successProgram.name(), finalProgram ), diff --git a/src/main/java/ca/teamdman/sfm/client/gui/screen/ManagerScreen.java b/src/main/java/ca/teamdman/sfm/client/gui/screen/ManagerScreen.java index 8fd5bd392..89a531419 100644 --- a/src/main/java/ca/teamdman/sfm/client/gui/screen/ManagerScreen.java +++ b/src/main/java/ca/teamdman/sfm/client/gui/screen/ManagerScreen.java @@ -3,6 +3,7 @@ import ca.teamdman.sfm.SFM; import ca.teamdman.sfm.client.ClientDiagnosticInfo; import ca.teamdman.sfm.client.ClientStuff; +import ca.teamdman.sfm.common.Constants; import ca.teamdman.sfm.common.containermenu.ManagerContainerMenu; import ca.teamdman.sfm.common.item.DiskItem; import ca.teamdman.sfm.common.net.*; @@ -13,6 +14,7 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.ConfirmScreen; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; import net.minecraft.client.player.LocalPlayer; @@ -102,7 +104,7 @@ protected void init() { buttonWidth, 16, MANAGER_GUI_PASTE_FROM_CLIPBOARD_BUTTON.getComponent(), - button -> this.onLoadClipboard(), + button -> this.onClipboardPasteButtonClicked(), buildTooltip(MANAGER_GUI_PASTE_FROM_CLIPBOARD_BUTTON_TOOLTIP) )); editButton = this.addRenderableWidget(new ExtendedButtonWithTooltip( @@ -111,7 +113,7 @@ protected void init() { buttonWidth, 16, MANAGER_GUI_EDIT_BUTTON.getComponent(), - button -> onEdit(), + button -> onEditButtonClicked(), buildTooltip(MANAGER_GUI_EDIT_BUTTON_TOOLTIP) )); examplesButton = this.addRenderableWidget(new ExtendedButtonWithTooltip( @@ -120,7 +122,7 @@ protected void init() { buttonWidth, 16, MANAGER_GUI_VIEW_EXAMPLES_BUTTON.getComponent(), - button -> onShowExamples(), + button -> onExamplesButtonClicked(), buildTooltip(MANAGER_GUI_VIEW_EXAMPLES_BUTTON_TOOLTIP) )); clipboardCopyButton = this.addRenderableWidget(new ExtendedButton( @@ -129,7 +131,7 @@ protected void init() { buttonWidth, 16, MANAGER_GUI_COPY_TO_CLIPBOARD_BUTTON.getComponent(), - button -> this.onSaveClipboard() + button -> this.onClipboardCopyButtonClicked() )); logsButton = this.addRenderableWidget(new ExtendedButton( (this.width - this.imageWidth) / 2 - buttonWidth, @@ -137,7 +139,7 @@ protected void init() { buttonWidth, 16, MANAGER_GUI_VIEW_LOGS_BUTTON.getComponent(), - button -> onShowLogs() + button -> onLogsButtonClicked() )); rebuildButton = this.addRenderableWidget(new ExtendedButton( (this.width - this.imageWidth) / 2 - buttonWidth, @@ -145,7 +147,7 @@ protected void init() { buttonWidth, 16, MANAGER_GUI_REBUILD_BUTTON.getComponent(), - button -> this.onSendRebuild() + button -> this.onRebuildButtonClicked() )); resetButton = this.addRenderableWidget(new ExtendedButtonWithTooltip( (this.width - this.imageWidth) / 2 + 120, @@ -153,7 +155,7 @@ protected void init() { 50, 12, MANAGER_GUI_RESET_BUTTON.getComponent(), - button -> sendReset(), + button -> onResetButtonClicked(), buildTooltip(MANAGER_GUI_RESET_BUTTON_TOOLTIP) )); diagButton = this.addRenderableWidget(new ExtendedButtonWithTooltip( @@ -162,13 +164,7 @@ protected void init() { 12, 14, Component.literal("!"), - button -> { - if (Screen.hasShiftDown() && !isReadOnly()) { - sendAttemptFix(); - } else { - this.onSaveDiagClipboard(); - } - }, + button -> onDiagButtonClicked(), buildTooltip(isReadOnly() ? MANAGER_GUI_WARNING_BUTTON_TOOLTIP_READ_ONLY : MANAGER_GUI_WARNING_BUTTON_TOOLTIP) @@ -176,28 +172,52 @@ protected void init() { updateVisibilities(); } - private void onEdit() { + private void onDiagButtonClicked() { + if (Screen.hasShiftDown() && !isReadOnly()) { + sendAttemptFix(); + } else { + this.onSaveDiagClipboard(); + } + } + + private void onEditButtonClicked() { ClientStuff.showProgramEditScreen(DiskItem.getProgram(menu.getDisk()), this::sendProgram); } - private void onShowExamples() { + private void onExamplesButtonClicked() { ClientStuff.showExampleListScreen(DiskItem.getProgram(menu.getDisk()), this::sendProgram); } - private void onShowLogs() { + private void onLogsButtonClicked() { ClientStuff.showLogsScreen(menu); } - private void sendReset() { - SFMPackets.MANAGER_CHANNEL.sendToServer(new ServerboundManagerResetPacket( - menu.containerId, - menu.MANAGER_POSITION - )); - status = MANAGER_GUI_STATUS_RESET.getComponent(); - statusCountdown = STATUS_DURATION; + private void onResetButtonClicked() { + ConfirmScreen confirmScreen = new ConfirmScreen( + proceed -> { + assert this.minecraft != null; + this.minecraft.popGuiLayer(); // Close confirm screen + + if (proceed) { + SFMPackets.MANAGER_CHANNEL.sendToServer(new ServerboundManagerResetPacket( + menu.containerId, + menu.MANAGER_POSITION + )); + status = MANAGER_GUI_STATUS_RESET.getComponent(); + statusCountdown = STATUS_DURATION; + } + }, + Constants.LocalizationKeys.MANAGER_RESET_CONFIRM_SCREEN_TITLE.getComponent(), + Constants.LocalizationKeys.MANAGER_RESET_CONFIRM_SCREEN_MESSAGE.getComponent(), + Constants.LocalizationKeys.MANAGER_RESET_CONFIRM_SCREEN_YES_BUTTON.getComponent(), + Constants.LocalizationKeys.MANAGER_RESET_CONFIRM_SCREEN_NO_BUTTON.getComponent() + ); + assert this.minecraft != null; + this.minecraft.pushGuiLayer(confirmScreen); + confirmScreen.setDelay(20); } - private void onSendRebuild() { + private void onRebuildButtonClicked() { SFMPackets.MANAGER_CHANNEL.sendToServer(new ServerboundManagerRebuildPacket( menu.containerId, menu.MANAGER_POSITION @@ -226,7 +246,7 @@ private void sendProgram(String program) { statusCountdown = STATUS_DURATION; } - private void onSaveClipboard() { + private void onClipboardCopyButtonClicked() { try { Minecraft.getInstance().keyboardHandler.setClipboard(menu.program); status = MANAGER_GUI_STATUS_SAVED_CLIPBOARD.getComponent(); @@ -259,7 +279,7 @@ private void onSaveDiagClipboard() { } } - private void onLoadClipboard() { + private void onClipboardPasteButtonClicked() { try { String contents = Minecraft.getInstance().keyboardHandler.getClipboard(); sendProgram(contents); @@ -271,19 +291,19 @@ private void onLoadClipboard() { @Override public boolean keyPressed(int pKeyCode, int pScanCode, int pModifiers) { if (Screen.isPaste(pKeyCode) && clipboardPasteButton.visible) { - onLoadClipboard(); + onClipboardPasteButtonClicked(); return true; } else if (Screen.isCopy(pKeyCode) && clipboardCopyButton.visible) { - onSaveClipboard(); + onClipboardCopyButtonClicked(); return true; } else if (pKeyCode == GLFW.GLFW_KEY_E && Screen.hasControlDown() && Screen.hasShiftDown() && examplesButton.visible) { - onShowExamples(); + onExamplesButtonClicked(); return true; } else if (pKeyCode == GLFW.GLFW_KEY_E && Screen.hasControlDown() && editButton.visible) { - onEdit(); + onEditButtonClicked(); return true; } return super.keyPressed(pKeyCode, pScanCode, pModifiers); diff --git a/src/main/java/ca/teamdman/sfm/client/gui/screen/ProgramEditScreen.java b/src/main/java/ca/teamdman/sfm/client/gui/screen/ProgramEditScreen.java index 2c406d69c..b4f506f10 100644 --- a/src/main/java/ca/teamdman/sfm/client/gui/screen/ProgramEditScreen.java +++ b/src/main/java/ca/teamdman/sfm/client/gui/screen/ProgramEditScreen.java @@ -5,12 +5,14 @@ import ca.teamdman.sfm.client.ProgramTokenContextActions; import ca.teamdman.sfm.client.gui.EditorUtils; import ca.teamdman.sfm.common.Constants; +import ca.teamdman.sfm.common.SFMConfig; import com.mojang.blaze3d.vertex.PoseStack; import ca.teamdman.sfm.common.item.DiskItem; import com.mojang.blaze3d.vertex.Tesselator; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.Font; import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.MultiLineEditBox; import net.minecraft.client.gui.components.MultilineTextField; import net.minecraft.client.gui.components.Tooltip; @@ -34,6 +36,8 @@ import java.util.function.Consumer; import static ca.teamdman.sfm.common.Constants.LocalizationKeys.PROGRAM_EDIT_SCREEN_DONE_BUTTON_TOOLTIP; +import static ca.teamdman.sfm.common.Constants.LocalizationKeys.PROGRAM_EDIT_SCREEN_TOGGLE_LINE_NUMBERS_BUTTON_TOOLTIP; +import static ca.teamdman.sfm.common.Constants.LocalizationKeys.PROGRAM_EDIT_SCREEN_TOGGLE_LINE_NUMBERS_BUTTON_TOOLTIP; public class ProgramEditScreen extends Screen { protected final String INITIAL_CONTENT; @@ -82,6 +86,15 @@ protected void init() { textarea.setValue(INITIAL_CONTENT); this.setInitialFocus(textarea); + this.addRenderableWidget(new ExtendedButtonWithTooltip( + this.width / 2 - 200, + this.height / 2 - 100 + 195, + 16, + 20, + Component.literal("#"), + (button) -> this.onToggleLineNumbersButtonClicked(), + Tooltip.create(PROGRAM_EDIT_SCREEN_TOGGLE_LINE_NUMBERS_BUTTON_TOOLTIP.getComponent()) + )); this.addRenderableWidget(new ExtendedButtonWithTooltip( this.width / 2 - 2 - 150, this.height / 2 - 100 + 195, @@ -101,6 +114,13 @@ protected void init() { )); } + private static boolean shouldShowLineNumbers() { + return SFMConfig.getOrDefault(SFMConfig.CLIENT.showLineNumbers); + } + private void onToggleLineNumbersButtonClicked() { + SFMConfig.CLIENT.showLineNumbers.set(!shouldShowLineNumbers()); + } + public void saveAndClose() { SAVE_CALLBACK.accept(textarea.getValue()); @@ -281,12 +301,18 @@ public void setCursorPosition(int cursor) { this.textField.cursor = cursor; } + public int getLineNumberWidth() { + if (shouldShowLineNumbers()) { + return this.font.width("000"); + } else { + return 0; + } + } - @Override public boolean mouseClicked(double pMouseX, double pMouseY, int pButton) { // Accommodate line numbers if (pMouseX >= this.getX() + 1 && pMouseX <= this.getX() + this.width - 1) { - pMouseX -= 1 + this.font.width("000"); + pMouseX -= getLineNumberWidth(); } // we need to override the default behaviour because Mojang broke it @@ -335,7 +361,7 @@ public boolean mouseDragged( ) { // if mouse in bounds, translate to accommodate line numbers if (mx >= this.getX() + 1 && mx <= this.getX() + this.width - 1) { - mx -= 1 + this.font.width("000"); + mx -= getLineNumberWidth(); } return super.mouseDragged(mx, my, button, dx, dy); } @@ -366,7 +392,7 @@ protected void renderContents(GuiGraphics graphics, int mx, int my, float partia boolean isCursorVisible = this.isFocused() && this.frame++ / 60 % 2 == 0; boolean isCursorAtEndOfLine = false; int cursorIndex = textField.cursor(); - int lineX = this.getX() + this.innerPadding() + this.font.width("000"); + int lineX = this.getX() + this.innerPadding() + getLineNumberWidth(); int lineY = this.getY() + this.innerPadding(); int charCount = 0; int cursorX = 0; @@ -384,20 +410,23 @@ protected void renderContents(GuiGraphics graphics, int mx, int my, float partia && cursorIndex <= charCount + lineLength; var buffer = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder()); - // Draw line number - String lineNumber = String.valueOf(line + 1); - this.font.drawInBatch( - lineNumber, - lineX - 2 - this.font.width(lineNumber), - lineY, - -1, - true, - matrix4f, - buffer, - Font.DisplayMode.NORMAL, - 0, - LightTexture.FULL_BRIGHT - ); + + if (shouldShowLineNumbers()) { + // Draw line number + String lineNumber = String.valueOf(line + 1); + this.font.drawInBatch( + lineNumber, + lineX - 2 - this.font.width(lineNumber), + lineY, + -1, + true, + matrix4f, + buffer, + Font.DisplayMode.NORMAL, + 0, + LightTexture.FULL_BRIGHT + ); + } if (cursorOnThisLine) { isCursorAtEndOfLine = cursorIndex == charCount + lineLength; diff --git a/src/main/java/ca/teamdman/sfm/common/Constants.java b/src/main/java/ca/teamdman/sfm/common/Constants.java index e2ed51a72..7b3c989d3 100644 --- a/src/main/java/ca/teamdman/sfm/common/Constants.java +++ b/src/main/java/ca/teamdman/sfm/common/Constants.java @@ -28,6 +28,10 @@ public static final class LocalizationKeys { "gui.sfm.text_editor.done_button.tooltip", "Shift+Enter to submit" ); + public static final LocalizationEntry PROGRAM_EDIT_SCREEN_TOGGLE_LINE_NUMBERS_BUTTON_TOOLTIP = new LocalizationEntry( + "gui.sfm.text_editor.toggle_line_numbers_button.tooltip", + "Toggle line numbers" + ); public static final LocalizationEntry SAVE_CHANGES_CONFIRM_SCREEN_TITLE = new LocalizationEntry( "gui.sfm.save_changes_confirm.title", "Save changes" @@ -44,6 +48,22 @@ public static final class LocalizationKeys { "gui.sfm.save_changes_confirm.no_button", "Continue editing" ); + public static final LocalizationEntry MANAGER_RESET_CONFIRM_SCREEN_TITLE = new LocalizationEntry( + "gui.sfm.manager.reset_confirm_screen.title", + "Reset disk?" + ); + public static final LocalizationEntry MANAGER_RESET_CONFIRM_SCREEN_MESSAGE = new LocalizationEntry( + "gui.sfm.manager.reset_confirm_screen.message", + "Are you sure you want to reset this disk?" + ); + public static final LocalizationEntry MANAGER_RESET_CONFIRM_SCREEN_YES_BUTTON = new LocalizationEntry( + "gui.sfm.manager.reset_confirm_screen.yes_button", + "Wipe program and labels" + ); + public static final LocalizationEntry MANAGER_RESET_CONFIRM_SCREEN_NO_BUTTON = new LocalizationEntry( + "gui.sfm.manager.reset_confirm_screen.no_button", + "Never mind, make no changes" + ); public static final LocalizationEntry EXIT_WITHOUT_SAVING_CONFIRM_SCREEN_TITLE = new LocalizationEntry( "gui.sfm.exit_without_saving_confirm.title", "Exit without saving?" @@ -287,6 +307,14 @@ public static final class LocalizationKeys { "program.sfm.warnings.unused_label", "Label \"%s\" is used in code but not assigned in the world." ); + public static final LocalizationEntry PROGRAM_WARNING_OUTPUT_RESOURCE_TYPE_NOT_FOUND_IN_INPUTS = new LocalizationEntry( + "program.sfm.warnings.output_label_not_found_in_inputs", + "Statement \"%s\" at %s uses resource type \"%s\" which has no matching input statement." + ); + public static final LocalizationEntry PROGRAM_WARNING_UNUSED_INPUT_LABEL = new LocalizationEntry( + "program.sfm.warnings.unused_input_label", + "Statement \"%s\" at %s inputs \"%s\" from \"%s\" but no future output statement consume \"%s\"." + ); public static final LocalizationEntry PROGRAM_WARNING_UNKNOWN_RESOURCE_ID = new LocalizationEntry( "program.sfm.warnings.unknown_resource_id", "Resource \"%s\" was not found." @@ -349,7 +377,7 @@ public static final class LocalizationKeys { public static final LocalizationEntry LOG_RESOURCE_TYPE_GET_CAPABILITIES_BEGIN = new LocalizationEntry( "log.sfm.resource_type.get_capabilities.begin", - "Gathering capabilities of type %s against labels %s" + "Gathering capabilities of type %s (%s) against labels %s" ); public static final LocalizationEntry LOG_RESOURCE_TYPE_GET_CAPABILITIES_CAP_NOT_PRESENT = new LocalizationEntry( "log.sfm.resource_type.get_capabilities.not_present", @@ -498,7 +526,7 @@ public static final class LocalizationKeys { ); public static final LocalizationEntry LOG_PROGRAM_TICK_IO_STATEMENT_GATHER_SLOTS_FOR_RESOURCE_TYPE = new LocalizationEntry( "log.sfm.statement.tick.io.gather_slots.resource_types", - "Gathering for: %s" + "Gathering for: %s (%s)" ); public static final LocalizationEntry LOG_PROGRAM_TICK_OUTPUT_STATEMENT = new LocalizationEntry( "log.sfm.statement.tick.output", diff --git a/src/main/java/ca/teamdman/sfm/common/SFMConfig.java b/src/main/java/ca/teamdman/sfm/common/SFMConfig.java index 854c076af..6dff01dc9 100644 --- a/src/main/java/ca/teamdman/sfm/common/SFMConfig.java +++ b/src/main/java/ca/teamdman/sfm/common/SFMConfig.java @@ -8,22 +8,37 @@ public class SFMConfig { public static final ModConfigSpec COMMON_SPEC; + public static final ModConfigSpec CLIENT_SPEC; public static final SFMConfig.Common COMMON; + public static final Client CLIENT; static { final Pair commonSpecPair = new ModConfigSpec.Builder().configure(SFMConfig.Common::new); COMMON_SPEC = commonSpecPair.getRight(); COMMON = commonSpecPair.getLeft(); + final Pair clientSpecPair = new ModConfigSpec.Builder().configure(SFMConfig.Client::new); + CLIENT_SPEC = clientSpecPair.getRight(); + CLIENT = clientSpecPair.getLeft(); } public static class Common { public final ModConfigSpec.IntValue timerTriggerMinimumIntervalInTicks; public final ModConfigSpec.IntValue timerTriggerMinimumIntervalInTicksWhenOnlyForgeEnergyIO; + public final ModConfigSpec.IntValue maxIfStatementsInTriggerBeforeSimulationIsntAllowed; Common(ModConfigSpec.Builder builder) { timerTriggerMinimumIntervalInTicks = builder .defineInRange("timerTriggerMinimumIntervalInTicks", 20, 1, Integer.MAX_VALUE); timerTriggerMinimumIntervalInTicksWhenOnlyForgeEnergyIO = builder - .defineInRange("timerTriggerMinimumIntervalInTicksWhenOnlyForgeEnergyIOStatementsPresent", 1, 1, Integer.MAX_VALUE); + .defineInRange( + "timerTriggerMinimumIntervalInTicksWhenOnlyForgeEnergyIOStatementsPresent", + 1, + 1, + Integer.MAX_VALUE + ); + maxIfStatementsInTriggerBeforeSimulationIsntAllowed = builder + .comment( + "The number of scenarios to check is 2^n where n is the number of if statements in a trigger") + .defineInRange("maxIfStatementsInTriggerBeforeSimulationIsntAllowed", 10, 0, Integer.MAX_VALUE); } } @@ -40,5 +55,15 @@ public static T getOrDefault(ModConfigSpec.ConfigValue configValue) { public static void register(ModLoadingContext context) { context.registerConfig(ModConfig.Type.COMMON, SFMConfig.COMMON_SPEC); + context.registerConfig(ModConfig.Type.CLIENT, SFMConfig.CLIENT_SPEC); + } + + public static class Client { + public final ModConfigSpec.BooleanValue showLineNumbers; + + Client(ModConfigSpec.Builder builder) { + showLineNumbers = builder + .define("showLineNumbers", false); + } } } diff --git a/src/main/java/ca/teamdman/sfm/common/block/ManagerBlock.java b/src/main/java/ca/teamdman/sfm/common/block/ManagerBlock.java index 5986e12ef..727ac9516 100644 --- a/src/main/java/ca/teamdman/sfm/common/block/ManagerBlock.java +++ b/src/main/java/ca/teamdman/sfm/common/block/ManagerBlock.java @@ -5,6 +5,8 @@ import ca.teamdman.sfm.common.cablenetwork.ICableBlock; import ca.teamdman.sfm.common.containermenu.ManagerContainerMenu; import ca.teamdman.sfm.common.item.DiskItem; +import ca.teamdman.sfm.common.program.LabelPositionHolder; +import ca.teamdman.sfm.common.program.ProgramLinter; import ca.teamdman.sfm.common.registry.SFMBlockEntities; import com.mojang.serialization.MapCodec; import net.minecraft.core.BlockPos; @@ -103,7 +105,8 @@ public InteractionResult use( .getDisk() .ifPresent(disk -> manager .getProgram() - .ifPresent(program -> DiskItem.setWarnings(disk, program.gatherWarnings(disk, manager)))); + .ifPresent(program -> DiskItem.setWarnings(disk, ProgramLinter.gatherWarnings(program, + LabelPositionHolder.from(disk), manager)))); NetworkHooks.openScreen(sp, manager, buf -> ManagerContainerMenu.encode(manager, buf)); return InteractionResult.CONSUME; } diff --git a/src/main/java/ca/teamdman/sfm/common/blockentity/ManagerBlockEntity.java b/src/main/java/ca/teamdman/sfm/common/blockentity/ManagerBlockEntity.java index 6359bab05..5b77aaaf7 100644 --- a/src/main/java/ca/teamdman/sfm/common/blockentity/ManagerBlockEntity.java +++ b/src/main/java/ca/teamdman/sfm/common/blockentity/ManagerBlockEntity.java @@ -8,6 +8,7 @@ import ca.teamdman.sfm.common.net.ClientboundManagerGuiUpdatePacket; import ca.teamdman.sfm.common.net.ClientboundManagerLogLevelUpdatedPacket; import ca.teamdman.sfm.common.net.ClientboundManagerLogsPacket; +import ca.teamdman.sfm.common.program.LabelPositionHolder; import ca.teamdman.sfm.common.registry.SFMBlockEntities; import ca.teamdman.sfm.common.registry.SFMPackets; import ca.teamdman.sfm.common.util.OpenContainerTracker; @@ -164,7 +165,7 @@ public Optional getDisk() { public void rebuildProgramAndUpdateDisk() { if (level != null && level.isClientSide()) return; this.program = getDisk() - .flatMap(itemStack -> DiskItem.compileAndUpdateAttributes(itemStack, this)) + .flatMap(itemStack -> DiskItem.compileAndUpdateErrorsAndWarnings(itemStack, this)) .orElse(null); sendUpdatePacket(); } @@ -238,6 +239,7 @@ public void clearContent() { public void reset() { getDisk().ifPresent(disk -> { + LabelPositionHolder.purge(disk); disk.setTag(null); setItem(0, disk); setChanged(); diff --git a/src/main/java/ca/teamdman/sfm/common/item/DiskItem.java b/src/main/java/ca/teamdman/sfm/common/item/DiskItem.java index 35a94b95c..4e6829c47 100644 --- a/src/main/java/ca/teamdman/sfm/common/item/DiskItem.java +++ b/src/main/java/ca/teamdman/sfm/common/item/DiskItem.java @@ -7,6 +7,8 @@ import ca.teamdman.sfm.common.blockentity.ManagerBlockEntity; import ca.teamdman.sfm.common.net.ServerboundDiskItemSetProgramPacket; import ca.teamdman.sfm.common.program.LabelPositionHolder; +import ca.teamdman.sfm.common.program.ProgramLinter; +import ca.teamdman.sfm.common.registry.SFMItems; import ca.teamdman.sfm.common.registry.SFMPackets; import ca.teamdman.sfm.common.util.SFMUtils; import ca.teamdman.sfml.ast.Program; @@ -55,16 +57,15 @@ public static void setProgram(ItemStack stack, String program) { } - public static Optional compileAndUpdateAttributes(ItemStack stack, @Nullable ManagerBlockEntity manager) { + public static Optional compileAndUpdateErrorsAndWarnings(ItemStack stack, @Nullable ManagerBlockEntity manager) { if (manager != null) { manager.logger.info(x -> x.accept(Constants.LocalizationKeys.PROGRAM_COMPILE_FROM_DISK_BEGIN.get())); } AtomicReference rtn = new AtomicReference<>(null); Program.compile( getProgram(stack), - (successProgram, builder) -> { - ArrayList warnings = successProgram.gatherWarnings(stack, manager); - List errors = Collections.emptyList(); + successProgram -> { + ArrayList warnings = ProgramLinter.gatherWarnings(successProgram, LabelPositionHolder.from(stack), manager); // Log to disk if (manager != null) { @@ -78,7 +79,7 @@ public static Optional compileAndUpdateAttributes(ItemStack stack, @Nul // Update disk properties setProgramName(stack, successProgram.name()); setWarnings(stack, warnings); - setErrors(stack, errors); + setErrors(stack, Collections.emptyList()); // Track result rtn.set(successProgram); diff --git a/src/main/java/ca/teamdman/sfm/common/net/ServerboundBoolExprStatementInspectionRequestPacket.java b/src/main/java/ca/teamdman/sfm/common/net/ServerboundBoolExprStatementInspectionRequestPacket.java index bbdb90c32..2c9a9dac6 100644 --- a/src/main/java/ca/teamdman/sfm/common/net/ServerboundBoolExprStatementInspectionRequestPacket.java +++ b/src/main/java/ca/teamdman/sfm/common/net/ServerboundBoolExprStatementInspectionRequestPacket.java @@ -3,6 +3,7 @@ import ca.teamdman.sfm.common.blockentity.ManagerBlockEntity; import ca.teamdman.sfm.common.containermenu.ManagerContainerMenu; import ca.teamdman.sfm.common.program.ProgramContext; +import ca.teamdman.sfm.common.program.SimulateExploreAllPathsProgramBehaviour; import ca.teamdman.sfm.common.registry.SFMPackets; import ca.teamdman.sfm.common.util.SFMUtils; import ca.teamdman.sfml.ast.BoolExpr; @@ -12,11 +13,16 @@ import net.neoforged.neoforge.network.NetworkEvent; import net.neoforged.neoforge.network.PacketDistributor; +import java.util.function.Supplier; + public record ServerboundBoolExprStatementInspectionRequestPacket( String programString, int inputNodeIndex ) { - public static void encode(ServerboundBoolExprStatementInspectionRequestPacket msg, FriendlyByteBuf friendlyByteBuf) { + public static void encode( + ServerboundBoolExprStatementInspectionRequestPacket msg, + FriendlyByteBuf friendlyByteBuf + ) { friendlyByteBuf.writeUtf(msg.programString, Program.MAX_PROGRAM_LENGTH); friendlyByteBuf.writeInt(msg.inputNodeIndex()); } @@ -29,7 +35,8 @@ public static ServerboundBoolExprStatementInspectionRequestPacket decode(Friendl } public static void handle( - ServerboundBoolExprStatementInspectionRequestPacket msg, NetworkEvent.Context context + ServerboundBoolExprStatementInspectionRequestPacket msg, + NetworkEvent.Context context ) { context.enqueueWork(() -> { // todo: duplicate code @@ -54,7 +61,7 @@ public static void handle( } Program.compile( msg.programString, - (successProgram, builder) -> builder + successProgram -> successProgram.builder() .getNodeAtIndex(msg.inputNodeIndex) .filter(BoolExpr.class::isInstance) .map(BoolExpr.class::cast) @@ -66,7 +73,7 @@ public static void handle( ProgramContext programContext = new ProgramContext( successProgram, manager, - ProgramContext.ExecutionPolicy.EXPLORE_BRANCHES + new SimulateExploreAllPathsProgramBehaviour() ); boolean result = expr.test(programContext); payload.append(result ? "TRUE" : "FALSE"); diff --git a/src/main/java/ca/teamdman/sfm/common/net/ServerboundDiskItemSetProgramPacket.java b/src/main/java/ca/teamdman/sfm/common/net/ServerboundDiskItemSetProgramPacket.java index 422db13a2..f8f534d9f 100644 --- a/src/main/java/ca/teamdman/sfm/common/net/ServerboundDiskItemSetProgramPacket.java +++ b/src/main/java/ca/teamdman/sfm/common/net/ServerboundDiskItemSetProgramPacket.java @@ -38,7 +38,7 @@ public static void handle( var stack = sender.getItemInHand(msg.hand); if (stack.getItem() instanceof DiskItem) { DiskItem.setProgram(stack, msg.programString); - DiskItem.compileAndUpdateAttributes(stack, null); + DiskItem.compileAndUpdateErrorsAndWarnings(stack, null); } }); diff --git a/src/main/java/ca/teamdman/sfm/common/net/ServerboundIfStatementInspectionRequestPacket.java b/src/main/java/ca/teamdman/sfm/common/net/ServerboundIfStatementInspectionRequestPacket.java index df673df57..fc7058e4a 100644 --- a/src/main/java/ca/teamdman/sfm/common/net/ServerboundIfStatementInspectionRequestPacket.java +++ b/src/main/java/ca/teamdman/sfm/common/net/ServerboundIfStatementInspectionRequestPacket.java @@ -3,6 +3,7 @@ import ca.teamdman.sfm.common.blockentity.ManagerBlockEntity; import ca.teamdman.sfm.common.containermenu.ManagerContainerMenu; import ca.teamdman.sfm.common.program.ProgramContext; +import ca.teamdman.sfm.common.program.SimulateExploreAllPathsProgramBehaviour; import ca.teamdman.sfm.common.registry.SFMPackets; import ca.teamdman.sfm.common.util.SFMUtils; import ca.teamdman.sfml.ast.IfStatement; @@ -57,7 +58,7 @@ public static void handle( } Program.compile( msg.programString, - (successProgram, builder) -> builder + successProgram -> successProgram.builder() .getNodeAtIndex(msg.inputNodeIndex) .filter(IfStatement.class::isInstance) .map(IfStatement.class::cast) @@ -69,7 +70,7 @@ public static void handle( ProgramContext programContext = new ProgramContext( successProgram, manager, - ProgramContext.ExecutionPolicy.EXPLORE_BRANCHES + new SimulateExploreAllPathsProgramBehaviour() ); boolean result = ifStatement.condition().test(programContext); payload.append(result ? "TRUE" : "FALSE"); diff --git a/src/main/java/ca/teamdman/sfm/common/net/ServerboundInputInspectionRequestPacket.java b/src/main/java/ca/teamdman/sfm/common/net/ServerboundInputInspectionRequestPacket.java index 28abb3f89..20a5a3738 100644 --- a/src/main/java/ca/teamdman/sfm/common/net/ServerboundInputInspectionRequestPacket.java +++ b/src/main/java/ca/teamdman/sfm/common/net/ServerboundInputInspectionRequestPacket.java @@ -3,6 +3,7 @@ import ca.teamdman.sfm.common.blockentity.ManagerBlockEntity; import ca.teamdman.sfm.common.containermenu.ManagerContainerMenu; import ca.teamdman.sfm.common.program.ProgramContext; +import ca.teamdman.sfm.common.program.SimulateExploreAllPathsProgramBehaviour; import ca.teamdman.sfm.common.registry.SFMPackets; import ca.teamdman.sfm.common.util.SFMUtils; import ca.teamdman.sfml.ast.InputStatement; @@ -18,7 +19,10 @@ public record ServerboundInputInspectionRequestPacket( String programString, int inputNodeIndex ) { - public static void encode(ServerboundInputInspectionRequestPacket msg, FriendlyByteBuf friendlyByteBuf) { + public static void encode( + ServerboundInputInspectionRequestPacket msg, + FriendlyByteBuf friendlyByteBuf + ) { friendlyByteBuf.writeUtf(msg.programString, Program.MAX_PROGRAM_LENGTH); friendlyByteBuf.writeInt(msg.inputNodeIndex()); } @@ -56,7 +60,7 @@ public static void handle( } Program.compile( msg.programString, - (successProgram, builder) -> builder + successProgram -> successProgram.builder() .getNodeAtIndex(msg.inputNodeIndex) .filter(InputStatement.class::isInstance) .map(InputStatement.class::cast) @@ -69,7 +73,7 @@ public static void handle( ProgramContext programContext = new ProgramContext( successProgram, manager, - ProgramContext.ExecutionPolicy.EXPLORE_BRANCHES + new SimulateExploreAllPathsProgramBehaviour() ); int preLen = payload.length(); inputStatement.gatherSlots( diff --git a/src/main/java/ca/teamdman/sfm/common/net/ServerboundManagerFixPacket.java b/src/main/java/ca/teamdman/sfm/common/net/ServerboundManagerFixPacket.java index 7d4815d1a..c2ec0b985 100644 --- a/src/main/java/ca/teamdman/sfm/common/net/ServerboundManagerFixPacket.java +++ b/src/main/java/ca/teamdman/sfm/common/net/ServerboundManagerFixPacket.java @@ -2,6 +2,7 @@ import ca.teamdman.sfm.common.blockentity.ManagerBlockEntity; import ca.teamdman.sfm.common.containermenu.ManagerContainerMenu; +import ca.teamdman.sfm.common.program.ProgramLinter; import ca.teamdman.sfm.common.registry.SFMPackets; import net.minecraft.core.BlockPos; import net.minecraft.network.FriendlyByteBuf; @@ -36,7 +37,11 @@ public static void handle(ServerboundManagerFixPacket msg, NetworkEvent.Context .getDisk() .ifPresent(disk -> manager .getProgram() - .ifPresent(program -> program.fixWarnings(disk, manager))) + .ifPresent(program -> ProgramLinter.fixWarningsByRemovingBadLabelsFromDisk( + manager, + disk, + program + ))) ); context.setPacketHandled(true); } diff --git a/src/main/java/ca/teamdman/sfm/common/net/ServerboundOutputInspectionRequestPacket.java b/src/main/java/ca/teamdman/sfm/common/net/ServerboundOutputInspectionRequestPacket.java index 1c3780eee..b1b681949 100644 --- a/src/main/java/ca/teamdman/sfm/common/net/ServerboundOutputInspectionRequestPacket.java +++ b/src/main/java/ca/teamdman/sfm/common/net/ServerboundOutputInspectionRequestPacket.java @@ -5,6 +5,8 @@ import ca.teamdman.sfm.common.containermenu.ManagerContainerMenu; import ca.teamdman.sfm.common.program.LimitedInputSlot; import ca.teamdman.sfm.common.program.ProgramContext; +import ca.teamdman.sfm.common.program.SimulateExploreAllPathsProgramBehaviour; +import ca.teamdman.sfm.common.program.SimulateExploreAllPathsProgramBehaviour.Branch; import ca.teamdman.sfm.common.registry.SFMPackets; import ca.teamdman.sfm.common.registry.SFMResourceTypes; import ca.teamdman.sfm.common.resourcetype.ResourceType; @@ -19,7 +21,11 @@ import net.neoforged.neoforge.network.PacketDistributor; import org.antlr.v4.runtime.misc.Pair; -import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Predicate; import java.util.function.Supplier; @@ -39,41 +45,6 @@ public static ServerboundOutputInspectionRequestPacket decode(FriendlyByteBuf fr ); } - private static ResourceLimit getSlotResource( - LimitedInputSlot limitedInputSlot - ) { - ResourceType resourceType = limitedInputSlot.type; - //noinspection OptionalGetWithoutIsPresent - ResourceKey> resourceTypeResourceKey = SFMResourceTypes.DEFERRED_TYPES - .getResourceKey(limitedInputSlot.type) - .map(x -> { - //noinspection unchecked,rawtypes - return (ResourceKey>) (ResourceKey) x; - }) - .get(); - STACK stack = limitedInputSlot.peekExtractPotential(); - long amount = limitedInputSlot.type.getAmount(stack); - amount = Long.min(amount, limitedInputSlot.tracker.getResourceLimit().limit().quantity().number().value()); - long remainingObligation = limitedInputSlot.tracker.getRemainingRetentionObligation(); - amount -= Long.min(amount, remainingObligation); - Limit amountLimit = new Limit( - new ResourceQuantity(new Number(amount), ResourceQuantity.IdExpansionBehaviour.NO_EXPAND), - ResourceQuantity.MAX_QUANTITY - ); - ResourceLocation stackId = resourceType.getRegistryKey(stack); - ResourceIdentifier resourceIdentifier = new ResourceIdentifier<>( - resourceTypeResourceKey.location().getNamespace(), - resourceTypeResourceKey.location().getPath(), - stackId.getNamespace(), - stackId.getPath() - ); - return new ResourceLimit<>( - resourceIdentifier, - amountLimit - ); - } - - public static void handle( ServerboundOutputInspectionRequestPacket msg, NetworkEvent.Context context @@ -101,212 +72,20 @@ public static void handle( } Program.compile( msg.programString, - (successProgram, builder) -> builder + successProgram -> successProgram.builder() .getNodeAtIndex(msg.outputNodeIndex) .filter(OutputStatement.class::isInstance) .map(OutputStatement.class::cast) .ifPresent(outputStatement -> { - StringBuilder payload = new StringBuilder(); - payload.append(outputStatement.toStringPretty()).append("\n"); - payload.append("-- predictions may differ from actual execution results\n"); - - successProgram.replaceOutputStatement(outputStatement, new OutputStatement( - outputStatement.labelAccess(), - outputStatement.resourceLimits(), - outputStatement.each() - ) { - private final Set> seen = new HashSet<>(); - // TODO: overhaul speculative output execution - // currently performing 2^n speculative executions, where n is the number - // of if statements in the entire program. - // Should find out what different scenarios the output can be in - // instead of brute forcing. - // If-blocks can affect subsequent statements, so fork on forget statements instead - @Override - public void tick(ProgramContext context) { - StringBuilder branchPayload = new StringBuilder(); - - if (!context.getExecutionPath().isEmpty()) { - if (!seen.add(context.getExecutionPath())) { - // not sure this actually works - return; - } - payload - .append("-- POSSIBILITY ") - .append(context.getExplorationBranchIndex()) - .append(" --"); - if (context.getExecutionPath().stream().allMatch(ProgramContext.Branch::wasTrue)) { - payload.append(" all true\n"); - } else if (context.getExecutionPath().stream().allMatch(Predicate.not(ProgramContext.Branch::wasTrue))) { - payload.append(" all false\n"); - } else { - payload.append('\n'); - } - context.getExecutionPath() - .forEach(branch -> { - if (branch.wasTrue()) { - payload - .append(branch.ifStatement().condition().sourceCode()) - .append(" -- true"); - } else { - payload - .append(branch.ifStatement().condition().sourceCode()) - .append(" -- false"); - } - payload.append("\n"); - }); - payload.append("\n"); - } - - branchPayload.append("-- predicted inputs:\n"); - List, LabelAccess>> inputSlots = new ArrayList<>(); - context - .getInputs() - .forEach(inputStatement -> inputStatement.gatherSlots( - context, - slot -> inputSlots.add(new Pair<>( - slot, - inputStatement.labelAccess() - )) - )); - List inputStatements = inputSlots.stream() - .map(slot -> SFMUtils.getInputStatementForSlot(slot.a, slot.b)) - .filter(Optional::isPresent) - .map(Optional::get) - .toList(); - if (inputStatements.isEmpty()) { - branchPayload.append("none\n-- predicted outputs:\nnone"); - } else { - inputStatements.stream() - .map(InputStatement::toStringPretty) - .map(x -> x + "\n") - .forEach(branchPayload::append); - - branchPayload.append( - "-- predicted outputs:\n"); - ResourceLimits condensedResourceLimits; - { - ResourceLimits resourceLimits = new ResourceLimits( - inputSlots - .stream() - .map(slot -> slot.a) - .map(ServerboundOutputInspectionRequestPacket::getSlotResource) - .toList(), - ResourceIdSet.EMPTY - ); - List> condensedResourceLimitList = new ArrayList<>(); - for (ResourceLimit resourceLimit : resourceLimits.resourceLimits()) { - // check if an existing resource limit has the same resource identifier - condensedResourceLimitList - .stream() - .filter(x -> x - .resourceId() - .equals(resourceLimit.resourceId())) - .findFirst() - .ifPresentOrElse(found -> { - int i = condensedResourceLimitList.indexOf(found); - ResourceLimit newLimit = found.withLimit(new Limit( - found - .limit() - .quantity() - .add(resourceLimit.limit().quantity()), - ResourceQuantity.MAX_QUANTITY - )); - condensedResourceLimitList.set(i, newLimit); - }, () -> condensedResourceLimitList.add(resourceLimit)); - } - { - // prune items not covered by the output resource limits - ListIterator> iter = condensedResourceLimitList.listIterator(); - while (iter.hasNext()) { - ResourceLimit resourceLimit = iter.next(); - // because these resource limits were generated from resource stacks - // they should always be valid resource locations (not patterns) - ResourceLocation resourceLimitLocation = new ResourceLocation( - resourceLimit.resourceId().resourceNamespace, - resourceLimit.resourceId().resourceName - ); - long accept = outputStatement - .resourceLimits() - .resourceLimits() - .stream() - .filter(outputResourceLimit -> outputResourceLimit - .resourceId() - .matchesStack( - resourceLimitLocation) - && outputStatement - .resourceLimits() - .exclusions() - .resourceIds() - .stream() - .noneMatch( - exclusion -> exclusion.matchesStack( - resourceLimitLocation))) - .mapToLong(rl -> rl.limit().quantity().number().value()) - .max() - .orElse(0); - if (accept == 0) { - iter.remove(); - } else { - iter.set(resourceLimit.withLimit(new Limit( - new ResourceQuantity(new Number(Long.min( - accept, - resourceLimit - .limit() - .quantity() - .number() - .value() - )), resourceLimit.limit().quantity() - .idExpansionBehaviour()), - ResourceQuantity.MAX_QUANTITY - ))); - } - } - } - condensedResourceLimits = new ResourceLimits( - condensedResourceLimitList, - ResourceIdSet.EMPTY - ); - } - if (condensedResourceLimits.resourceLimits().isEmpty()) { - branchPayload.append("none\n"); - } else { - branchPayload - .append(new OutputStatement( - outputStatement.labelAccess(), - condensedResourceLimits, - outputStatement.each() - ).toStringPretty()); - } - - } - branchPayload.append("\n"); - if (successProgram.getConditionCount() == 0) { - payload.append(branchPayload); - } else { - payload.append(branchPayload.toString().indent(4)); - } - } - }); - - // run the program down each possible if-branch combination - for ( - int branchIndex = 0; - branchIndex < Math.pow(2, successProgram.getConditionCount()); - branchIndex++ - ) { - successProgram.tick(new ProgramContext( - successProgram, - manager, - ProgramContext.ExecutionPolicy.EXPLORE_BRANCHES, - branchIndex - )); - } - - SFM.LOGGER.debug("Sending packet with length {}", payload.length()); + String payload = getOutputStatementInspectionResultsString( + manager, + successProgram, + outputStatement + ); + SFM.LOGGER.debug("Sending output inspection results packet with length {}", payload.length()); SFMPackets.INSPECTION_CHANNEL.send( PacketDistributor.PLAYER.with(() -> player), - new ClientboundOutputInspectionResultsPacket(payload.toString().strip()) + new ClientboundOutputInspectionResultsPacket(payload) ); }), failure -> { @@ -318,6 +97,227 @@ public void tick(ProgramContext context) { } ); }); - context.setPacketHandled(true); + context.setPacketHandled(true); + } + + public static String getOutputStatementInspectionResultsString( + ManagerBlockEntity manager, Program successProgram, OutputStatement outputStatement + ) { + StringBuilder payload = new StringBuilder(); + payload.append(outputStatement.toStringPretty()).append("\n"); + payload.append("-- predictions may differ from actual execution results\n"); + + AtomicInteger branchCount = new AtomicInteger(0); + successProgram.replaceOutputStatement(outputStatement, new OutputStatement( + outputStatement.labelAccess(), + outputStatement.resourceLimits(), + outputStatement.each() + ) { + @Override + public void tick(ProgramContext context) { + if (!(context.getBehaviour() instanceof SimulateExploreAllPathsProgramBehaviour behaviour)) { + throw new IllegalStateException("Expected behaviour to be SimulateExploreAllPathsProgramBehaviour"); + } + StringBuilder branchPayload = new StringBuilder(); + + payload + .append("-- POSSIBILITY ") + .append(branchCount.getAndIncrement()) + .append(" --"); + if (behaviour.getCurrentPath().streamBranches().allMatch(Branch::wasTrue)) { + payload.append(" all true\n"); + } else if (behaviour + .getCurrentPath() + .streamBranches() + .allMatch(Predicate.not(Branch::wasTrue))) { + payload.append(" all false\n"); + } else { + payload.append('\n'); + } + behaviour.getCurrentPath() + .streamBranches() + .forEach(branch -> { + if (branch.wasTrue()) { + payload + .append(branch.ifStatement().condition().sourceCode()) + .append(" -- true"); + } else { + payload + .append(branch.ifStatement().condition().sourceCode()) + .append(" -- false"); + } + payload.append("\n"); + }); + payload.append("\n"); + + + branchPayload.append("-- predicted inputs:\n"); + List, LabelAccess>> inputSlots = new ArrayList<>(); + context + .getInputs() + .forEach(inputStatement -> inputStatement.gatherSlots( + context, + slot -> inputSlots.add(new Pair<>( + slot, + inputStatement.labelAccess() + )) + )); + List inputStatements = inputSlots.stream() + .map(slot -> SFMUtils.getInputStatementForSlot(slot.a, slot.b)) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + if (inputStatements.isEmpty()) { + branchPayload.append("none\n-- predicted outputs:\nnone"); + } else { + inputStatements.stream() + .map(InputStatement::toStringPretty) + .map(x -> x + "\n") + .forEach(branchPayload::append); + + branchPayload.append( + "-- predicted outputs:\n"); + ResourceLimits condensedResourceLimits; + { + ResourceLimits resourceLimits = new ResourceLimits( + inputSlots + .stream() + .map(slot -> slot.a) + .map(ServerboundOutputInspectionRequestPacket::getSlotResource) + .toList(), + ResourceIdSet.EMPTY + ); + List> condensedResourceLimitList = new ArrayList<>(); + for (ResourceLimit resourceLimit : resourceLimits.resourceLimits()) { + // check if an existing resource limit has the same resource identifier + condensedResourceLimitList + .stream() + .filter(x -> x + .resourceId() + .equals(resourceLimit.resourceId())) + .findFirst() + .ifPresentOrElse(found -> { + int i = condensedResourceLimitList.indexOf(found); + ResourceLimit newLimit = found.withLimit(new Limit( + found + .limit() + .quantity() + .add(resourceLimit.limit().quantity()), + ResourceQuantity.MAX_QUANTITY + )); + condensedResourceLimitList.set(i, newLimit); + }, () -> condensedResourceLimitList.add(resourceLimit)); + } + { + // prune items not covered by the output resource limits + ListIterator> iter = condensedResourceLimitList.listIterator(); + while (iter.hasNext()) { + ResourceLimit resourceLimit = iter.next(); + // because these resource limits were generated from resource stacks + // they should always be valid resource locations (not patterns) + ResourceLocation resourceLimitLocation = new ResourceLocation( + resourceLimit.resourceId().resourceNamespace, + resourceLimit.resourceId().resourceName + ); + long accept = outputStatement + .resourceLimits() + .resourceLimits() + .stream() + .filter(outputResourceLimit -> outputResourceLimit + .resourceId() + .matchesStack( + resourceLimitLocation) + && outputStatement + .resourceLimits() + .exclusions() + .resourceIds() + .stream() + .noneMatch( + exclusion -> exclusion.matchesStack( + resourceLimitLocation))) + .mapToLong(rl -> rl.limit().quantity().number().value()) + .max() + .orElse(0); + if (accept == 0) { + iter.remove(); + } else { + iter.set(resourceLimit.withLimit(new Limit( + new ResourceQuantity(new Number(Long.min( + accept, + resourceLimit + .limit() + .quantity() + .number() + .value() + )), resourceLimit.limit().quantity() + .idExpansionBehaviour()), + ResourceQuantity.MAX_QUANTITY + ))); + } + } + } + condensedResourceLimits = new ResourceLimits( + condensedResourceLimitList, + ResourceIdSet.EMPTY + ); + } + if (condensedResourceLimits.resourceLimits().isEmpty()) { + branchPayload.append("none\n"); + } else { + branchPayload + .append(new OutputStatement( + outputStatement.labelAccess(), + condensedResourceLimits, + outputStatement.each() + ).toStringPretty()); + } + + } + branchPayload.append("\n"); + payload.append(branchPayload.toString().indent(4)); + } + }); + + successProgram.tick(new ProgramContext( + successProgram, + manager, + new SimulateExploreAllPathsProgramBehaviour() + )); + + return payload.toString().strip(); + } + + private static ResourceLimit getSlotResource( + LimitedInputSlot limitedInputSlot + ) { + ResourceType resourceType = limitedInputSlot.type; + //noinspection OptionalGetWithoutIsPresent + ResourceKey> resourceTypeResourceKey = SFMResourceTypes.DEFERRED_TYPES + .getResourceKey(limitedInputSlot.type) + .map(x -> { + //noinspection unchecked,rawtypes + return (ResourceKey>) (ResourceKey) x; + }) + .get(); + STACK stack = limitedInputSlot.peekExtractPotential(); + long amount = limitedInputSlot.type.getAmount(stack); + amount = Long.min(amount, limitedInputSlot.tracker.getResourceLimit().limit().quantity().number().value()); + long remainingObligation = limitedInputSlot.tracker.getRemainingRetentionObligation(); + amount -= Long.min(amount, remainingObligation); + Limit amountLimit = new Limit( + new ResourceQuantity(new Number(amount), ResourceQuantity.IdExpansionBehaviour.NO_EXPAND), + ResourceQuantity.MAX_QUANTITY + ); + ResourceLocation stackId = resourceType.getRegistryKey(stack); + ResourceIdentifier resourceIdentifier = new ResourceIdentifier<>( + resourceTypeResourceKey.location().getNamespace(), + resourceTypeResourceKey.location().getPath(), + stackId.getNamespace(), + stackId.getPath() + ); + return new ResourceLimit<>( + resourceIdentifier, + amountLimit + ); } } diff --git a/src/main/java/ca/teamdman/sfm/common/program/CapabilityConsumer.java b/src/main/java/ca/teamdman/sfm/common/program/CapabilityConsumer.java new file mode 100644 index 000000000..d34272c3c --- /dev/null +++ b/src/main/java/ca/teamdman/sfm/common/program/CapabilityConsumer.java @@ -0,0 +1,15 @@ +package ca.teamdman.sfm.common.program; + +import ca.teamdman.sfml.ast.Label; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; + +@FunctionalInterface +public interface CapabilityConsumer { + void accept( + Label label, + BlockPos pos, + Direction direction, + T cap + ); +} diff --git a/src/main/java/ca/teamdman/sfm/common/program/DefaultProgramBehaviour.java b/src/main/java/ca/teamdman/sfm/common/program/DefaultProgramBehaviour.java new file mode 100644 index 000000000..ba590df0d --- /dev/null +++ b/src/main/java/ca/teamdman/sfm/common/program/DefaultProgramBehaviour.java @@ -0,0 +1,9 @@ +package ca.teamdman.sfm.common.program; + +public class DefaultProgramBehaviour implements ProgramBehaviour { + @Override + public ProgramBehaviour fork() { + return this; // this is stateless so this should be fine + } + +} diff --git a/src/main/java/ca/teamdman/sfm/common/program/GatherWarningsProgramBehaviour.java b/src/main/java/ca/teamdman/sfm/common/program/GatherWarningsProgramBehaviour.java new file mode 100644 index 000000000..fdf6fb5d0 --- /dev/null +++ b/src/main/java/ca/teamdman/sfm/common/program/GatherWarningsProgramBehaviour.java @@ -0,0 +1,316 @@ +package ca.teamdman.sfm.common.program; + +import ca.teamdman.sfm.SFM; +import ca.teamdman.sfm.common.resourcetype.ResourceType; +import ca.teamdman.sfml.ast.*; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import com.mojang.datafixers.util.Pair; +import net.minecraft.network.chat.contents.TranslatableContents; + +import java.math.BigInteger; +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import static ca.teamdman.sfm.common.Constants.LocalizationKeys.PROGRAM_WARNING_OUTPUT_RESOURCE_TYPE_NOT_FOUND_IN_INPUTS; +import static ca.teamdman.sfm.common.Constants.LocalizationKeys.PROGRAM_WARNING_UNUSED_INPUT_LABEL; + +@SuppressWarnings("rawtypes") +public class GatherWarningsProgramBehaviour extends SimulateExploreAllPathsProgramBehaviour { + private final List>>> sharedMultiverseWarningsByPath; + private final Consumer> sharedMultiverseWarningDisplay; + private final List> warnings = new ArrayList<>(); + private final Multimap resourceTypesInputted = HashMultimap.create(); + private final Set resourceTypesOutputted = new HashSet<>(); + + public GatherWarningsProgramBehaviour(Consumer> sharedMultiverseWarningDisplay) { + this.sharedMultiverseWarningDisplay = sharedMultiverseWarningDisplay; + this.sharedMultiverseWarningsByPath = new ArrayList<>(); + } + + public GatherWarningsProgramBehaviour( + List seenPaths, + ExecutionPath currentPath, + AtomicReference triggerPathCount, + Consumer> sharedMultiverseWarningDisplay, + List>>> sharedMultiverseWarningsByPath, + List> warnings + ) { + super(seenPaths, currentPath, triggerPathCount); + this.warnings.addAll(warnings); + this.sharedMultiverseWarningDisplay = sharedMultiverseWarningDisplay; + this.sharedMultiverseWarningsByPath = sharedMultiverseWarningsByPath; + } + + + @Override + public ProgramBehaviour fork() { + return new GatherWarningsProgramBehaviour( + this.seenPaths, + this.currentPath, + this.triggerPathCount, + this.sharedMultiverseWarningDisplay, + this.sharedMultiverseWarningsByPath, + this.warnings + ); + } + + @Override + public void onInputStatementExecution( + ProgramContext context, + InputStatement inputStatement) { + super.onInputStatementExecution(context, inputStatement); + Set> inputtingResourceTypes = inputStatement + .getReferencedIOResourceIds() + .map(ResourceIdentifier::getResourceType) + .collect(Collectors.toSet()); + for (Label label : inputStatement.labelAccess().labels()) { + for (ResourceType resourceType : inputtingResourceTypes) { + resourceTypesInputted.put(resourceType, label); + } + } + } + + @Override + public void onOutputStatementExecution( + ProgramContext context, + OutputStatement outputStatement + ) { + super.onOutputStatementExecution(context, outputStatement); + + + // identify resource types being outputted + Set seekingResourceTypes = outputStatement + .getReferencedIOResourceIds() + .map(ResourceIdentifier::getResourceType) + .collect(Collectors.toSet()); + + for (ResourceType resourceType : seekingResourceTypes) { + if (!resourceTypesInputted.containsKey(resourceType)) { + // if the resource type was never inputted, warn + warnings.add(Pair.of( + getLatestPathElement(), + PROGRAM_WARNING_OUTPUT_RESOURCE_TYPE_NOT_FOUND_IN_INPUTS.get( + outputStatement, + context.getProgram().builder().getLineColumnForNode(outputStatement), + resourceType.displayAsCode() + ) + )); + } + } + + + // track what we have outputted, so we can find what we input and never use + resourceTypesOutputted.addAll(seekingResourceTypes); + } + + @Override + public void onInputStatementForgetTransform( + ProgramContext context, + InputStatement old, + InputStatement next + ) { + super.onInputStatementForgetTransform(context, old, next); + + /* + INPUT stick FROM a,b + FORGET a - (item::,a) going out of scope, warn a is never used + OUTPUT TO chest + */ + + + // Identify labels that are no longer active + Set