001/************************* PROJECT RON *************************/ 002/* Copyright (c) 2026 StuyPulse Robotics. All rights reserved. */ 003/* Use of this source code is governed by an MIT-style license */ 004/* that can be found in the repository LICENSE file. */ 005/***************************************************************/ 006package com.stuypulse.robot.util.simulation; 007 008import static edu.wpi.first.units.Units.*; 009 010import com.stuypulse.robot.constants.Settings; 011import com.stuypulse.robot.subsystems.handoff.Handoff; 012import com.stuypulse.robot.subsystems.handoff.Handoff.HandoffState; 013import com.stuypulse.robot.subsystems.intake.Intake; 014import com.stuypulse.robot.subsystems.intake.Intake.IntakeState; 015import com.stuypulse.robot.subsystems.shooter.Shooter; 016import com.stuypulse.robot.subsystems.shooter.Shooter.ShooterState; 017import com.stuypulse.robot.subsystems.swerve.CommandSwerveDrivetrain; 018import dev.doglog.DogLog; 019import edu.wpi.first.math.geometry.Pose2d; 020import edu.wpi.first.math.geometry.Pose3d; 021import edu.wpi.first.math.geometry.Rotation2d; 022import edu.wpi.first.math.geometry.Rotation3d; 023import edu.wpi.first.math.geometry.Translation2d; 024import edu.wpi.first.networktables.NetworkTableInstance; 025import edu.wpi.first.networktables.StructArrayPublisher; 026import edu.wpi.first.networktables.StructPublisher; 027import edu.wpi.first.units.measure.Angle; 028import edu.wpi.first.units.measure.Distance; 029import edu.wpi.first.units.measure.LinearVelocity; 030import edu.wpi.first.wpilibj.Notifier; 031import org.ironmaple.simulation.IntakeSimulation; 032import org.ironmaple.simulation.SimulatedArena; 033import org.ironmaple.simulation.drivesims.SwerveDriveSimulation; 034import org.ironmaple.simulation.seasonspecific.rebuilt2026.Arena2026Rebuilt; 035import org.ironmaple.simulation.seasonspecific.rebuilt2026.RebuiltFuelOnFly; 036 037public class Simulation { 038 039 private static final Simulation instance; 040 041 public final Arena2026Rebuilt arenaInstance; 042 043 private final Notifier shotLoop; 044 045 private final SwerveDriveSimulation swerveMSim; 046 047 private final IntakeSimulation intakeMSim; 048 049 private final Intake intakeSim; 050 051 private final Shooter shooterSim; 052 053 private final Handoff handoffSim; 054 055 private final StructArrayPublisher<Pose3d> fuel; 056 057 private final StructPublisher<Pose3d> intakePivot; 058 059 private final StructPublisher<Pose3d> hopper; 060 061 private final StructArrayPublisher<Pose3d> fuelLayers; 062 063 private final StructPublisher<Pose3d> shooter; 064 065 static { 066 if (CommandSwerveDrivetrain.getInstance().getMapleSimDrive() != null) 067 instance = new Simulation(); 068 else 069 // extra safeguarding to ensure NO overlap between sim code and actual code 070 instance = null; 071 } 072 073 public static Simulation getInstance() { 074 return instance; 075 } 076 077 private Simulation() { 078 intakeSim = Intake.getInstance(); 079 shooterSim = Shooter.getInstance(); 080 handoffSim = Handoff.getInstance(); 081 swerveMSim = CommandSwerveDrivetrain.getInstance().getMapleSimDrive(); 082 arenaInstance = new Arena2026Rebuilt(false); 083 configureArena(arenaInstance, swerveMSim); 084 085 intakeMSim = createIntakeSimulation(); 086 intakeMSim.addGamePiecesToIntake(SimulationConstants.Hopper.FUEL_CAPACITY); 087 088 shotLoop = new Notifier(this::updateSubsystemsBPSLoop); 089 shotLoop.startPeriodic(1.0 / SimulationConstants.Shooter.BPS); 090 091 NetworkTableInstance table = NetworkTableInstance.getDefault(); 092 fuel = table.getStructArrayTopic("AdvScope/FuelPoses", Pose3d.struct).publish(); 093 intakePivot = table.getStructTopic("AdvScope/IntakePose", Pose3d.struct).publish(); 094 hopper = table.getStructTopic("AdvScope/HopperPose", Pose3d.struct).publish(); 095 fuelLayers = table.getStructArrayTopic("AdvScope/FuelLayers", Pose3d.struct).publish(); 096 shooter = table.getStructTopic("AdvScope/ShooterPose", Pose3d.struct).publish(); 097 } 098 099 private void configureArena(Arena2026Rebuilt arena, SwerveDriveSimulation drivetrain) { 100 arena.setEfficiencyMode(SimulationConstants.SPAWN_GAMEPIECES_SPARSELY); 101 arena.resetFieldForAuto(); 102 arena.addDriveTrainSimulation(drivetrain); 103 SimulatedArena.overrideInstance(arena); 104 } 105 106 private IntakeSimulation createIntakeSimulation() { 107 return IntakeSimulation.OverTheBumperIntake( 108 "Fuel", 109 swerveMSim, 110 Meters.of(SimulationConstants.Intake.INTAKE_WIDTH), 111 Meters.of(SimulationConstants.Intake.INTAKE_LENGTH), 112 IntakeSimulation.IntakeSide.FRONT, 113 SimulationConstants.Hopper.FUEL_CAPACITY); 114 } 115 116 private Pose3d getIntakePivotPose() { 117 return SimulationConstants.Intake.PIVOT_OFFSETS.withRotation( 118 new Rotation3d( 119 0, // inverts the angle 120 intakeSim.getRelativePosition().in(Radians), 121 0)); 122 } 123 124 private double getIntakeArmEndX() { 125 return SimulationConstants.Intake.PIVOT_END_X 126 + // sin works because we're zeroed at horizontal 127 Settings.Intake.Pivot.PIVOT_ARM_LENGTH.in(Meters) 128 * Math.sin( 129 intakeSim.getRelativePosition().in(Radians) 130 + SimulationConstants.Intake.PIVOT_OFFSETS.toRotation3d().getX()); 131 } 132 133 private void updateIntakeEnabled(boolean enabled) { 134 if (enabled) { 135 intakeMSim.startIntake(); 136 } else { 137 intakeMSim.stopIntake(); 138 } 139 } 140 141 private void updateIntake() { 142 boolean intakeEnabled = intakeSim.atTargetAngle() 143 && (intakeSim.getState() == IntakeState.DOWN) 144 && Settings.EnabledSubsystems.INTAKE.get(); 145 updateIntakeEnabled(intakeEnabled); 146 } 147 148 private void updateHopperFuel() { 149 final double hopperPercentage = (double) intakeMSim.getGamePiecesAmount() 150 / (double) SimulationConstants.Hopper.FUEL_CAPACITY; 151 final int layers = SimulationConstants.Hopper.FUEL_LAYERS; 152 final Pose3d visiblePose = SimulationConstants.Hopper.VISIBLE_POSE; 153 final Pose3d hiddenPose = SimulationConstants.Hopper.HIDDEN_POSE; 154 final Pose3d[] poses = new Pose3d[layers]; 155 for (int i = 0; i < layers; i++) { 156 poses[i] = hopperPercentage >= (1 / (double) layers) * (i + 1) ? visiblePose : hiddenPose; 157 } 158 fuelLayers.set(poses); 159 DogLog.log("Intake/hopperpercentage", hopperPercentage); 160 } 161 162 /** 163 * <h4>Extension of {@link Arena2026Rebuilt#addPieceWithVariance} that uses 164 * chassis speeds</h4> 165 * 166 * <p> 167 * Adds a gamepiece too the arena with a certain random variance. 168 * 169 * @param piecePose the field relative position at which to spawn the gamepiece 170 * @param yaw the initial yaw of the gamepiece 171 * @param height the initial height of the gamepiece above the field 172 * @param speed the initial speed of the gamepiece 173 * @param pitch the initial pitch of the gamepiece 174 * @param xVariance the maximum random offset applied to the x coordinate 175 * @param yVariance the maximum random offset applied to the y coordinate 176 * @param yawVariance the maximum random offset applied to the yaw, in degrees 177 * @param speedVariance the maximum random offset applied to the speed, in m/s 178 * @param pitchVariance the maximum random offset applied to the pitch, in degrees 179 */ 180 private void robotRelativeAddPieceWithVariance( 181 Translation2d piecePose, 182 Rotation2d yaw, 183 Distance height, 184 LinearVelocity speed, 185 Angle pitch, 186 double xVariance, 187 double yVariance, 188 double yawVariance, 189 double speedVariance, 190 double pitchVariance) { 191 arenaInstance.addGamePieceProjectile( 192 new RebuiltFuelOnFly( 193 piecePose.plus( 194 new Translation2d( 195 Arena2026Rebuilt.randomInRange(xVariance), 196 Arena2026Rebuilt.randomInRange(yVariance))), 197 new Translation2d(), 198 swerveMSim.getDriveTrainSimulatedChassisSpeedsFieldRelative(), 199 yaw.plus(Rotation2d.fromDegrees(Arena2026Rebuilt.randomInRange(yawVariance))), 200 height, 201 speed.plus(MetersPerSecond.of(Arena2026Rebuilt.randomInRange(speedVariance))), 202 Degrees.of(pitch.in(Degrees) + Arena2026Rebuilt.randomInRange(pitchVariance)))); 203 } 204 205 private void summonFuelAtIntake() { 206 robotRelativeAddPieceWithVariance( 207 swerveMSim 208 .getSimulatedDriveTrainPose() 209 .getTranslation() 210 .plus( 211 SimulationConstants.Intake.OUTTAKE_OFFSETS 212 .applyToPose3dRobotRelative( 213 new Pose3d( 214 getIntakeArmEndX(), 215 0, 216 0, 217 new Rotation3d( 218 swerveMSim.getSimulatedDriveTrainPose().getRotation()))) 219 .getTranslation() 220 .toTranslation2d()), 221 swerveMSim.getSimulatedDriveTrainPose().getRotation(), 222 Meters.of(0), 223 MetersPerSecond.of(2), 224 Radians.of(0), // x 225 SimulationConstants.Intake.INTAKE_WIDTH, 226 0.0, 227 0.0, // speed 228 1.0, 229 0.0); 230 } 231 232 private boolean canShoot() { 233 // final ShooterState shooterState = shooterSim.getState(); 234 // final boolean shooterEnabled = (shooterState == ShooterState.SHOOT || shooterState == ShooterState.MANUAL_HUB || shooterState == ShooterState.FERRY) && Settings.EnabledSubsystems.SHOOTER.get(); 235 return handoffSim.getState() == HandoffState.FORWARD && intakeMSim.obtainGamePieceFromIntake(); 236 } 237 238 /** 239 * <h4>Custom interval periodic function</h4> 240 * <p>Runs at the speed of 1 over the balls per second constant {@link SimulationConstants.Shooter#BPS}</p> 241 */ 242 private void updateSubsystemsBPSLoop() { 243 if (intakeSim.getState() == IntakeState.OUTTAKE 244 && Settings.EnabledSubsystems.INTAKE.get() 245 && intakeMSim.obtainGamePieceFromIntake()) { 246 summonFuelAtIntake(); 247 } 248 if (this.canShoot()) { 249 final Pose2d shooterPose = SimulationConstants.Shooter.OFFSETS.applyToPose2d( 250 swerveMSim.getSimulatedDriveTrainPose()); 251 final double launchAngle = 67.67; // random hood exit angle? 252 this.robotRelativeAddPieceWithVariance( 253 shooterPose.getTranslation(), 254 swerveMSim.getSimulatedDriveTrainPose().getRotation(), 255 Meters.of(SimulationConstants.Shooter.OFFSETS.toPose3d().getZ()), 256 MetersPerSecond.of( 257 SimulationConstants.Shooter.angularVelocityToMps( 258 shooterSim.getCurrentAngularVelocity())), 259 Degrees.of(launchAngle), 260 SimulationConstants.Intake.INTAKE_WIDTH, 261 0, 262 0, 263 0.5, 264 0); 265 } 266 } 267 268 public synchronized void update() { 269 if (swerveMSim == null) 270 return; 271 fuel.set(arenaInstance.getGamePiecesArrayByType("Fuel")); 272 updateIntake(); 273 updateHopperFuel(); 274 double armEndX = getIntakeArmEndX(); 275 intakePivot.set(getIntakePivotPose()); 276 hopper.set( 277 SimulationConstants.Hopper.OFFSETS.applyToPose3d( 278 new Pose3d(armEndX, 0, 0, new Rotation3d()))); 279 // Translation2d outtakeTranslationRobotRelative = 280 // swerveMSim.getSimulatedDriveTrainPose().getTranslation().plus( 281 // SimulationConstants.Intake.OUTTAKE_OFFSETS.applyToPose3dRobotRelative( 282 // new Pose3d(getIntakeArmEndX(), 0, 0, new 283 // Rotation3d(swerveMSim.getSimulatedDriveTrainPose().getRotation()))).getTranslation().toTranslation2d()); 284 // shooter.set(new Pose3d(tra.getX(), tra.getY(), 0, new Rotation3d())); 285 // shooter.set(SimulationConstants.Shooter.OFFSETS.applyToPose3dRobotRelative(new 286 // Pose3d(swerveMSim.getSimulatedDriveTrainPose()))); 287 } 288}