Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -796,8 +796,9 @@ private SimulationEngine buildEngineFromSetup() {
CoordinationPolicy policy = buildCoordinationPolicy();

Dispatcher dispatcher = new Dispatcher();
// Seed the dispatcher with the initial rack-to-station workload.
if (maxTasks > 0 && map != null) {

// Seed the dispatcher with the initial rack-to-station workload if not in manual mode
if (!isManualMode && maxTasks > 0 && map != null) {
generateFixedTasks(map, seed, maxTasks, dispatcher);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1100,7 +1100,7 @@ private void onViewportMousePressed(MouseEvent e) {
}
} else {
cancelDropoffPicking();
log("Dropoff assignment cancelled.");
log("Dropoff assignment cancelled. Please select a drop off station");
}
return;
}
Expand Down Expand Up @@ -1432,7 +1432,7 @@ private void showPropertiesFor(MapEntity entity) {
xSpinner.setPrefWidth(60);
ySpinner.setPrefWidth(60);
xSpinner.valueProperty().addListener((obs, oldVal, newVal) -> {
if (newVal == null || oldVal == null) return;
if (newVal == null || oldVal == null || newVal == (int) entity.getPosition().getX()) return;
if (guardEditor("move")) { xSpinner.getValueFactory().setValue(oldVal); return; }
int targetX = newVal;
int targetY = (int) entity.getPosition().getY();
Expand All @@ -1449,7 +1449,7 @@ private void showPropertiesFor(MapEntity entity) {
}
});
ySpinner.valueProperty().addListener((obs, oldVal, newVal) -> {
if (newVal == null || oldVal == null) return;
if (newVal == null || oldVal == null || newVal == (int) entity.getPosition().getY()) return;
if (guardEditor("move")) { ySpinner.getValueFactory().setValue(oldVal); return; }
int targetX = (int) entity.getPosition().getX();
int targetY = newVal;
Expand Down Expand Up @@ -1545,20 +1545,18 @@ private void showPropertiesFor(MapEntity entity) {
});

batterySpinner.valueProperty().addListener((obs, oldVal, newVal) -> {
if (newVal != null && oldVal != null && !newVal.equals(oldVal)) {
if (guardEditor("change battery")) {
// Revert spinner if editor is locked
if (newVal == null || oldVal == null || newVal.floatValue() == robot.getBattery()) return;
if (guardEditor("change battery")) {
// Revert spinner if editor is locked
Platform.runLater(() -> batterySpinner.getValueFactory().setValue(oldVal));
return;
}

return;
}
float newBat = newVal.floatValue();
float oldBat = oldVal.floatValue();

robot.setBattery(newBat);
drawViewport();
pushAction(new BatteryChangeAction(robot, oldBat, newBat));
}
drawViewport();
pushAction(new BatteryChangeAction(robot, oldBat, newBat));
});

batteryBox.getChildren().addAll(new Label("Battery:"), batterySpinner);
Expand All @@ -1577,30 +1575,54 @@ private void showPropertiesFor(MapEntity entity) {
if (globalManual) {
HBox boxCountBox = new HBox(8);
boxCountBox.setAlignment(javafx.geometry.Pos.CENTER_LEFT);

Spinner<Integer> boxCountSpinner = new Spinner<>(
new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 99, rack.getBoxCount()));
new SpinnerValueFactory.IntegerSpinnerValueFactory(0, 99, rack.getBoxCount()));
boxCountSpinner.setPrefWidth(80);
boxCountSpinner.setEditable(true);

// Restrict input to positive integers only (no decimals, no negatives)
boxCountSpinner.getEditor().setTextFormatter(new TextFormatter<>(change -> {
if (change.getControlNewText().matches("\\d*")) {
return change;
}
return null; // Reject non-digit characters
}));

// Force spinner to commit text value when the user clicks away
boxCountSpinner.getEditor().focusedProperty().addListener((obs, wasFocused, isFocused) -> {
if (!isFocused) {
boxCountSpinner.increment(0);
}
});

boxCountSpinner.valueProperty().addListener((obs, oldV, newV) -> {
if (newV == null || oldV == null || newV.equals(oldV)) return;
if (newV == null || oldV == null || newV.equals(rack.getBoxCount())) return;

if (guardEditor("change box count")) {
boxCountSpinner.getValueFactory().setValue(oldV);
// Revert spinner if editor is locked
javafx.application.Platform.runLater(() -> boxCountSpinner.getValueFactory().setValue(oldV));
return;
}

rack.setBoxCount(newV);
persistEditorChanges();
});
boxCountBox.getChildren().addAll(new Label("Number of boxes:"), boxCountSpinner);

boxCountBox.getChildren().addAll(new Label("Number of tasks:"), boxCountSpinner);
propertiesPanel.getChildren().add(boxCountBox);

HBox manualBox = new HBox(8);
manualBox.setAlignment(javafx.geometry.Pos.CENTER_LEFT);
CheckBox manualCheck = new CheckBox("Manual dropoff assignment");
manualCheck.setSelected(rack.isManualDropoffAssignment());

VBox dropoffArrayBox = new VBox(4);
dropoffArrayBox.setVisible(rack.isManualDropoffAssignment());
dropoffArrayBox.setManaged(rack.isManualDropoffAssignment());

manualCheck.selectedProperty().addListener((obs, oldV, newV) -> {
if (newV == rack.isManualDropoffAssignment()) return;
if (guardEditor("toggle manual assignment")) {
manualCheck.setSelected(oldV);
return;
Expand All @@ -1611,6 +1633,7 @@ private void showPropertiesFor(MapEntity entity) {
renderRackDropoffArray(rack, dropoffArrayBox);
persistEditorChanges();
});

manualBox.getChildren().add(manualCheck);
propertiesPanel.getChildren().add(manualBox);

Expand Down Expand Up @@ -1945,15 +1968,18 @@ private void onPlay() {
} else {
generated = com.openrobotics.task.TaskGenerator.generateAutomaticTasks(
engine.getMap(), engine.getMaxTasks(), engine.getSeed());

// No task assignments could be generated due to invalid map configuration
if (generated.isEmpty()) {
log("\u26a0 No tasks could be generated.");
engine.setSimulationError(SimulationError.INVALID_MAP_CONFIGURATION);
handleSimulationFailure();
return;
}
}
if (!generated.isEmpty()) {
engine.getDispatcher().addTasks(generated);
log("Auto-generated " + generated.size() + " tasks from map racks and delivery stations.");
} else {
log("\u26a0 No tasks could be generated.");
engine.setSimulationError(SimulationError.INVALID_MAP_CONFIGURATION);
handleSimulationFailure();
return;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public class Rack extends MapEntity {
*/
public Rack(String name, Vector2D position) {
super(name, position);
this.boxCount = 1;
this.boxCount = 0;
this.validDropoffIds = new ArrayList<>();
}

Expand All @@ -34,7 +34,7 @@ public Rack(String name, Vector2D position) {
*/
public Rack(UUID id, String name, Vector2D position) {
super(id, name, position);
this.boxCount = 1;
this.boxCount = 0;
this.validDropoffIds = new ArrayList<>();
}

Expand All @@ -47,7 +47,7 @@ public Rack(UUID id, String name, Vector2D position) {
*/
public void setBoxCount(int boxCount) {
// a rack always has at least 1 box; 0 would mean nothing to pick up
this.boxCount = Math.max(1, boxCount);
this.boxCount = Math.max(0, boxCount);
}

public List<UUID> getValidDropoffIds() { return validDropoffIds; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ public int assignTasks(Robot[] robots) {
return 0; // there are no tasks available, no assignments are made
}

int currentTick = AppState.getEngine().getTickCounter(); // getting the current simulation tick from global app state
int assignmentCount = 0;

for (Robot robot : robots) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -484,14 +484,27 @@ public boolean tick() {
return false;
}

// guard against ticking when no tasks were ever added to the dispatcher
if (dispatcher.getTotalTasksAdded() == 0) {
this.running = false;

// differentiate between no tasks because of manual assignment (user didn't add any) vs. automatic generation (generation failed)
if (manualTaskAssignment) {
simulationError = SimulationError.NO_TASKS_ASSIGNED;
} else {
simulationError = SimulationError.NO_TASKS_GENERATED;
}

return false;
}

if (tickCounter >= maxTicks) {
this.running = false;
return false;
}

// "No configured tasks" is treated as sandbox mode: ticks still run.
// Only short-circuit when a workload was actually configured and is now finished.
if (dispatcher.getTotalTasksAdded() > 0 && workloadComplete()) {
// Check if workload complete before ticking
if (workloadComplete()) {
this.running = false;

SimulationRunRecordBuilder recordBuilder = new SimulationRunRecordBuilder(this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ public enum SimulationError {
NONE("No errors detected. Simulation is running smoothly."),
ALL_ROBOTS_DEAD("All robots have depleted their batteries. Simulation cannot continue.\nConsider adding more charging stations to the map."),
NO_ROBOTS_SPAWNED("No robots were spawned in the simulation. Simulation cannot run.\nPlease add robots to the simulation"),
INVALID_MAP_CONFIGURATION("The map configuration is invalid. Simulation cannot run.\nEnsure the map has at least one rack and one delivery station.");
INVALID_MAP_CONFIGURATION("The map configuration is invalid. Simulation cannot run.\nEnsure the map has at least one rack and one delivery station."),
NO_TASKS_ASSIGNED("No tasks were added to the simulation. Simulation cannot run.\nPlease manually add tasks to the simulation by configuring racks on the map."),
NO_TASKS_GENERATED("No tasks were generated for the simulation. Simulation cannot continue.\nThere was likely an error with automatic task generation.");

private final String message;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,18 +336,18 @@ void sync_task_positions_updates_pickup_and_dropoff_references() {
assertTrue(outliner().getItems().stream().anyMatch(item -> item.contains("(2, 1) → (4, 1)")));
}

/** Verifies single-step frame advances simulation tick/progress/status and logs. */
/** Verifies single-step frame fails to advance simulation tick/progress/status and logs. */
@Test
void next_frame_with_engine_advances_tick_status_progress_and_console() {
void next_frame_with_engine_does_not_advance_tick_status_progress_and_console() {
loadScreenWith(emptyEngine(), null, 30, 30);
Label simStatus = installOptionalStatusLabel();

invokeOnFx("onNextFrame", new Class<?>[0]);

assertEquals("STEPPING", simStatus.getText());
assertEquals("TICK 1", tickLabel().getText());
assertEquals(0.001, progressBar().getProgress(), 0.0001);
assertTrue(consoleText().contains("Step → TICK 1"));
assertEquals("FAILURE", simStatus.getText());
assertEquals("TICK 0", tickLabel().getText());
assertEquals(0.0, progressBar().getProgress(), 0.0001);
assertTrue(consoleText().contains("Step → TICK 0"));
}

/** Verifies completion path updates status/progress without incrementing tick counter. */
Expand Down Expand Up @@ -471,7 +471,7 @@ void sidebar_and_console_toggles_collapse_and_restore_split_panes() {
assertEquals(0.83, consoleSplit.getDividerPositions()[0], 0.08);
}

/** Verifies restart with valid engine resets run state and rotates run identifier. */
/** Verifies restart with invalid engine resets run state and rotates run identifier. */
@Test
void restart_with_engine_resets_tick_progress_status_and_updates_run_id() {
SimulationEngine engine = emptyEngine();
Expand All @@ -480,7 +480,7 @@ void restart_with_engine_resets_tick_progress_status_and_updates_run_id() {
UUID originalRunId = engine.getRunId();

invokeOnFx("onNextFrame", new Class<?>[0]);
assertEquals("TICK 1", tickLabel().getText());
assertEquals("TICK 0", tickLabel().getText());

fireButtonByText("↺");

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.openrobotics.integration.simulation;

import com.openrobotics.AppState;
import com.openrobotics.integration.SimulationIntegrationTestSupport;
import com.openrobotics.logging.Logger;
import com.openrobotics.logging.LoggerMode;
import com.openrobotics.map.Map;
import com.openrobotics.robot.Robot;
import com.openrobotics.robot.RobotState;
Expand Down Expand Up @@ -51,13 +54,20 @@ void reservationKMakesTrailingRobotWaitForLeadRobotsWindow() {
map.addEntity(leadRobot);
map.addEntity(trailingRobot);

Dispatcher dispatcher = new Dispatcher();
dispatcher.addTask(trailingTask); // adding task so ticking doesn't fail

SimulationEngine engine = new SimulationEngine(
map,
new Robot[]{leadRobot, trailingRobot},
new Dispatcher(),
dispatcher,
new ReservationKPolicy(2)
);

// Setting global state for logging
AppState.setEngine(engine);
Logger.setMode(LoggerMode.NO_OP);

engine.tick();
assertEquals(pos(2, 0), leadRobot.getPosition());
assertEquals(pos(0, 0), trailingRobot.getPosition());
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,13 @@ void trafficRulesKeepsIntersectionExclusiveAcrossTicks() {
map.getTile(2, 1)
));

Dispatcher dispatcher = new Dispatcher();
dispatcher.addTask(taskA); // adding task so ticking doesn't fail

SimulationEngine engine = new SimulationEngine(
map,
new Robot[]{robotA, robotB},
new Dispatcher(),
dispatcher,
policy
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,14 @@ void togglingManualOffPreservesValidDropoffIds() {
}

/**
* Verifies box count setter clamps invalid low values to minimum supported count of one.
* Verifies box count setter clamps invalid low values to minimum supported count of zero.
*/
@Test
void boxCountSetterEnforcesMinimumOfOne() {
void boxCountSetterEnforcesMinimumOfZero() {
Rack rack = new Rack("r1", new Vector2D(0, 0));
rack.setBoxCount(0);
assertEquals(1, rack.getBoxCount());
assertEquals(0, rack.getBoxCount());
rack.setBoxCount(-5);
assertEquals(1, rack.getBoxCount());
assertEquals(0, rack.getBoxCount());
}
}
Loading
Loading