/* * ShootOFF - Software for Laser Dry Fire Training * Copyright (C) 2016 phrack * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.shootoff.plugins; import java.io.File; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import com.shootoff.camera.Shot; import com.shootoff.courses.Course; import com.shootoff.targets.Hit; import com.shootoff.targets.Target; import com.shootoff.targets.TargetRegion; import com.shootoff.util.NamedThreadFactory; public class SteelChallenge extends ProjectorTrainingExerciseBase implements TrainingExercise { private final static String LENGTH_COL_NAME = "Length"; private final static int LENGTH_COL_WIDTH = 60; private final static String HIT_COL_NAME = "Hit"; private final static int HIT_COL_WIDTH = 60; private final static int START_DELAY = 4; // s private final static int PAUSE_DELAY = 1; // s private static final int CORE_POOL_SIZE = 2; private ScheduledExecutorService executorService = Executors.newScheduledThreadPool(CORE_POOL_SIZE, new NamedThreadFactory("SteelChallengeExercise")); private TrainingExerciseBase thisSuper; private List<Target> targets; private Set<Target> roundTargets; private long startTime = 0; private boolean repeatExercise = true; private boolean testing = false; public SteelChallenge() {} public SteelChallenge(List<Target> targets) { super(targets); thisSuper = super.getInstance(); this.targets = targets; } @Override public void init() { super.pauseShotDetection(true); super.addShotTimerColumn(LENGTH_COL_NAME, LENGTH_COL_WIDTH); super.addShotTimerColumn(HIT_COL_NAME, HIT_COL_WIDTH); repeatExercise = checkTargets(targets); startRound(); } // For testing public void init(final Course course) { testing = true; thisSuper = super.getInstance(); targets = new ArrayList<>(); targets.addAll(course.getTargets()); repeatExercise = checkTargets(targets); startRound(); } @Override public void targetUpdate(Target target, TargetChange change) { switch (change) { case ADDED: targets.add(target); if (!repeatExercise && isStopTarget(target)) { repeatExercise = checkTargets(targets); startRound(); } break; case REMOVED: targets.remove(target); if (repeatExercise && targets.isEmpty()) { repeatExercise = false; } else if (repeatExercise && isStopTarget(target) && !hasStopTarget()) { repeatExercise = false; } break; } } private boolean isStopTarget(Target target) { for (final TargetRegion r : target.getRegions()) { if (r.tagExists("subtarget") && r.getTag("subtarget").equalsIgnoreCase("stop_target")) { return true; } } return false; } private boolean checkTargets(final List<Target> targets) { final boolean hasStopTarget = hasStopTarget(); if (!hasStopTarget) { final List<File> errorMessages = new ArrayList<>(); errorMessages.add(new File("sounds/voice/shootoff-lay-out-own-course.wav")); errorMessages.add(new File("sounds/voice/shootoff-add-stop-target.wav")); TrainingExerciseBase.playSounds(errorMessages); } return hasStopTarget; } private boolean hasStopTarget() { for (final Target t : targets) { if (isStopTarget(t)) return true; } return false; } private void startRound() { if (!repeatExercise) return; reloadVirtualMagazine(); roundTargets = new HashSet<>(targets); if (testing) { new AreYouReady().run(); } else { executorService.schedule(new AreYouReady(), START_DELAY, TimeUnit.SECONDS); } } private class AreYouReady implements Runnable { @Override public void run() { if (!repeatExercise) return; TrainingExerciseBase.playSound("sounds/voice/shootoff-are-you-ready.wav"); if (!testing) { executorService.schedule(new Standby(), PAUSE_DELAY, TimeUnit.SECONDS); } else { new Standby().run(); } } } private class Standby implements Runnable { @Override public void run() { if (!repeatExercise) return; TrainingExerciseBase.playSound("sounds/voice/shootoff-standby.wav"); if (testing) { new BeginTimer().run(); } else { executorService.schedule(new BeginTimer(), START_DELAY, TimeUnit.SECONDS); } } } private class BeginTimer implements Runnable { @Override public void run() { if (!repeatExercise) return; TrainingExerciseBase.playSound("sounds/beep.wav"); thisSuper.pauseShotDetection(false); startTime = System.currentTimeMillis(); } } @Override public ExerciseMetadata getInfo() { return new ExerciseMetadata("Steel Challenge", "1.0", "phrack", "This exercise assumes you will load one of the provided steel challenge courses " + "or lay out your own. When the beep sounds you must shoot every target at least once, " + "ending with the stop target (target with an 's' on it). After you hit the stop target, " + "ShootOFF will tell you your time and how many targets you missed. After you hit the stop " + "target a new round will automatically start."); } @Override public void shotListener(Shot shot, Optional<Hit> hit) { // This is evidence the round hasn't actually started if (roundTargets == null) return; final long elapsedTime = System.currentTimeMillis() - startTime; final String elapsedTimeSeconds; if (testing) { elapsedTimeSeconds = "0.00"; } else { elapsedTimeSeconds = String.format("%.2f", (double) elapsedTime / (double) 1000); } super.setShotTimerColumnText(LENGTH_COL_NAME, elapsedTimeSeconds); if (hit.isPresent()) { final TargetRegion r = hit.get().getHitRegion(); // Ignore tagless regions if (r.getAllTags().size() == 0) { super.setShotTimerColumnText(HIT_COL_NAME, "No"); return; } super.setShotTimerColumnText(HIT_COL_NAME, "Yes"); final Iterator<Target> it = roundTargets.iterator(); while (it.hasNext()) { final Target t = it.next(); if (t.hasRegion(r)) { it.remove(); break; } } if (r.tagExists("subtarget") && r.getTag("subtarget").equalsIgnoreCase("stop_target")) { super.pauseShotDetection(true); final String roundAnnouncement; if (roundTargets.size() == 1) { roundAnnouncement = String.format("Your time was %s seconds. You missed one target!", elapsedTimeSeconds); } else if (roundTargets.size() > 1) { roundAnnouncement = String.format("Your time was %s seconds. You missed %d targets!", elapsedTimeSeconds, roundTargets.size()); } else { roundAnnouncement = String.format("Your time was %s seconds", elapsedTimeSeconds); } TextToSpeech.say(roundAnnouncement); if (testing) { startRound(); } else { executorService.schedule(() -> startRound(), START_DELAY, TimeUnit.SECONDS); } } } else { super.setShotTimerColumnText(HIT_COL_NAME, "No"); } } @Override public void reset(List<Target> targets) { super.pauseShotDetection(true); repeatExercise = false; executorService.shutdownNow(); repeatExercise = true; executorService = Executors.newScheduledThreadPool(CORE_POOL_SIZE, new NamedThreadFactory("SteelChallengeExercise")); this.targets = targets; repeatExercise = checkTargets(targets); startRound(); } @Override public void destroy() { repeatExercise = false; executorService.shutdownNow(); super.destroy(); } }