/*
* 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.config;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.TimeoutException;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.slf4j.LoggerFactory;
import com.shootoff.Main;
import com.shootoff.camera.CameraFactory;
import com.shootoff.camera.CameraManager;
import com.shootoff.camera.cameratypes.Camera;
import com.shootoff.camera.cameratypes.IpCamera;
import com.shootoff.camera.processors.MalfunctionsProcessor;
import com.shootoff.camera.processors.ShotProcessor;
import com.shootoff.camera.processors.VirtualMagazineProcessor;
import com.shootoff.gui.CalibrationOption;
import com.shootoff.gui.controller.VideoPlayerController;
import com.shootoff.plugins.TrainingExercise;
import com.shootoff.plugins.engine.Plugin;
import com.shootoff.session.SessionRecorder;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.ConsoleAppender;
import javafx.geometry.Point2D;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.paint.Color;
/**
* Used to parse, store, and update configuration data from a file and in memory
* at run time. All of ShootOFF's global settings are managed by this class.
*
* @author phrack
*/
public class Configuration {
private static final org.slf4j.Logger logger = LoggerFactory.getLogger(Configuration.class);
private static final String FIRST_RUN_PROP = "shootoff.firstrun";
private static final String ERROR_REPORTING_PROP = "shootoff.errorreporting";
private static final String IPCAMS_PROP = "shootoff.ipcams";
private static final String WEBCAMS_PROP = "shootoff.webcams";
private static final String RECORDING_WEBCAMS_PROP = WEBCAMS_PROP + ".recording";
private static final String MARKER_RADIUS_PROP = "shootoff.markerradius";
private static final String IGNORE_LASER_COLOR_PROP = "shootoff.ignorelasercolor";
private static final String USE_RED_LASER_SOUND_PROP = "shootoff.redlasersound.use";
private static final String RED_LASER_SOUND_PROP = "shootoff.redlasersound";
private static final String USE_GREEN_LASER_SOUND_PROP = "shootoff.greenlasersound.use";
private static final String GREEN_LASER_SOUND_PROP = "shootoff.greenlasersound";
private static final String USE_VIRTUAL_MAGAZINE_PROP = "shootoff.virtualmagazine.use";
private static final String VIRTUAL_MAGAZINE_CAPACITY_PROP = "shootoff.virtualmagazine.capacity";
private static final String USE_MALFUNCTIONS_PROP = "shootoff.malfunctions.use";
private static final String MALFUNCTIONS_PROBABILITY_PROP = "shootoff.malfunctions.probability";
private static final String ARENA_POSITION_X_PROP = "shootoff.arena.x";
private static final String ARENA_POSITION_Y_PROP = "shootoff.arena.y";
private static final String MUTED_CHIME_MESSAGES = "shootoff.diagnosticmessages.chime.muted";
private static final String PERSPECTIVE_WEBCAM_DISTANCES = WEBCAMS_PROP + ".distances";
private static final String CALIBRATED_FEED_BEHAVIOR_PROP = "shootoff.arena.calibrated.behavior";
private static final String SHOW_ARENA_SHOT_MARKERS = "shootoff.arena.show.markers";
private static final String CALIBRATE_AUTO_ADJUST_EXPOSURE = "shootoff.arena.calibrated.exposure";
private static final String SHOWED_PERSPECTIVE_USAGE_MESSAGE = "shootoff.arena.notified.perspective";
private static final String POI_ADJUSTMENT_X = "shootoff.poiadjust.x";
private static final String POI_ADJUSTMENT_Y = "shootoff.poiadjust.y";
protected static final String MARKER_RADIUS_MESSAGE = "MARKER_RADIUS has an invalid value: %d. Acceptable values are "
+ "between 1 and 20.";
protected static final String LASER_COLOR_MESSAGE = "LASER_COLOR has an invalid value: %s. Acceptable values are "
+ "\"red\" and \"green\".";
protected static final String LASER_SOUND_MESSAGE = "LASER_SOUND has an invalid value: %s. Sound file must exist.";
protected static final String VIRTUAL_MAGAZINE_MESSAGE = "VIRTUAL_MAGAZINE has an invalid value: %d. Acceptable values are "
+ "between 1 and 45.";
protected static final String INJECT_MALFUNCTIONS_MESSAGE = "INJECT_MALFUNCTIONS has an invalid value: %f. Acceptable values are "
+ "between 0.1 and 99.9.";
private static final String DEFAULT_CONFIG_FILE = "shootoff.properties";
private static final int DEFAULT_DISPLAY_WIDTH = 640;
private static final int DEFAULT_DISPLAY_HEIGHT = 480;
private InputStream configInput;
private final String configName;
private boolean isFirstRun = false;
private boolean useErrorReporting = true;
private final Map<String, URL> ipcams = new HashMap<>();
private final Map<String, String> ipcamCredentials = new HashMap<>();
private final Map<String, Camera> webcams = new HashMap<>();
private int markerRadius = 4;
private boolean ignoreLaserColor = false;
private String ignoreLaserColorName = "None";
private boolean useRedLaserSound = false;
private File redLaserSound = new File("sounds/walther_ppq.wav");
private boolean useGreenLaserSound = false;
private File greenLaserSound = new File("sounds/walther_ppq.wav");
private boolean useVirtualMagazine = false;
private int virtualMagazineCapacity = 7;
private boolean useMalfunctions = false;
private float malfunctionsProbability = (float) 10.0;
private boolean debugMode = false;
private boolean headless = false;
private Set<Camera> recordingCameras = new HashSet<>();
private final Set<CameraManager> recordingManagers = new HashSet<>();
private final Set<VideoPlayerController> videoPlayers = new HashSet<>();
private Optional<SessionRecorder> sessionRecorder = Optional.empty();
private TrainingExercise currentExercise = null;
private Plugin currentPlugin = null;
private Optional<Color> shotRowColor = Optional.empty();
private Optional<Point2D> arenaPosition = Optional.empty();
private final Map<String, Integer> cameraDistances = new HashMap<>();
private final Set<String> messagesChimeMuted = new HashSet<>();
private boolean showedPerspectiveMessage = false;
private int displayWidth = DEFAULT_DISPLAY_WIDTH;
private int displayHeight = DEFAULT_DISPLAY_HEIGHT;
private final boolean debugShotsRecordToFiles = false;
private final Set<ShotProcessor> shotProcessors = new HashSet<>();
private VirtualMagazineProcessor magazineProcessor = null;
private MalfunctionsProcessor malfunctionsProcessor = null;
private CalibrationOption calibratedFeedBehavior = CalibrationOption.ONLY_IN_BOUNDS;
private boolean showArenaShotMarkers = false;
private boolean autoAdjustExposure = true;
private Optional<Double> poiAdjustmentX = Optional.empty();
private Optional<Double> poiAdjustmentY = Optional.empty();
private boolean adjustingPOI = false;
private int poiAdjustmentCount = 0;
private static Configuration config = null;
public static Configuration getConfig() {
return config;
}
private static void setConfig(Configuration config) {
Configuration.config = config;
}
protected Configuration(InputStream configInputStream, String name) throws IOException, ConfigurationException {
configInput = configInputStream;
configName = name;
readConfigurationFile();
setConfig(this);
}
protected Configuration(String name) throws IOException, ConfigurationException {
configName = name;
readConfigurationFile();
setConfig(this);
}
protected Configuration(InputStream configInputStream, String name, String[] args)
throws IOException, ConfigurationException {
configInput = configInputStream;
configName = name;
parseCmdLine(args);
readConfigurationFile();
parseCmdLine(args); // Parse twice so that we guarantee debug is set and
// override config file
setConfig(this);
}
/**
* Loads the configuration from a file named <tt>name</tt> and then updates
* the configuration using the programs arguments stored in <tt>args</tt>.
*
* @param name
* the configuration file to load properties from
* @param args
* the command line arguments for this program
* @throws IOException
* <tt>name</tt> doesn't exist on the file system
* @throws ConfigurationException
* a specific property value is out of spec
*/
public Configuration(String name, String[] args) throws IOException, ConfigurationException {
configName = name;
parseCmdLine(args);
readConfigurationFile();
parseCmdLine(args);
setConfig(this);
}
public Configuration(String[] args) throws ConfigurationException {
configName = DEFAULT_CONFIG_FILE;
parseCmdLine(args);
setConfig(this);
}
private void readConfigurationFile() throws ConfigurationException, IOException {
InputStream inputStream;
if (configInput != null) {
inputStream = configInput;
} else {
try {
inputStream = new FileInputStream(configName);
} catch (final FileNotFoundException e) {
throw new FileNotFoundException("Could not read configuration file " + configName);
}
}
final Properties prop = new Properties();
try {
prop.load(inputStream);
} catch (final IOException ioe) {
throw ioe;
} finally {
inputStream.close();
}
if (prop.containsKey(FIRST_RUN_PROP)) {
setFirstRun(Boolean.parseBoolean(prop.getProperty(FIRST_RUN_PROP)));
} else {
setFirstRun(false);
}
if (prop.containsKey(ERROR_REPORTING_PROP)) {
setUseErrorReporting(Boolean.parseBoolean(prop.getProperty(ERROR_REPORTING_PROP)));
}
if (prop.containsKey(IPCAMS_PROP)) {
for (final String nameString : prop.getProperty(IPCAMS_PROP).split(",")) {
final String[] names = nameString.split("\\|");
if (names.length == 2) {
registerIpCam(names[0], names[1], Optional.empty(), Optional.empty());
} else if (names.length > 2) {
registerIpCam(names[0], names[1], Optional.of(names[2]), Optional.of(names[3]));
}
}
}
if (prop.containsKey(WEBCAMS_PROP)) {
final List<String> webcamNames = new ArrayList<>();
final List<String> webcamInternalNames = new ArrayList<>();
for (final String nameString : prop.getProperty(WEBCAMS_PROP).split(",")) {
final String[] names = nameString.split(":");
if (names.length > 1) {
webcamNames.add(names[0].replaceAll("//`", ":"));
webcamInternalNames.add(names[1].replaceAll("//`", ":"));
}
}
for (final Camera webcam : CameraFactory.getWebcams()) {
final int cameraIndex = webcamInternalNames.indexOf(webcam.getName());
if (cameraIndex >= 0) webcams.put(webcamNames.get(cameraIndex), webcam);
}
}
final Set<Camera> recordingCameras = new HashSet<>();
if (prop.containsKey(RECORDING_WEBCAMS_PROP)) {
for (final String nameString : prop.getProperty(RECORDING_WEBCAMS_PROP).split(",")) {
for (final Camera webcam : webcams.values()) {
if (webcam.getName().equals(nameString)) {
recordingCameras.add(webcam);
continue;
}
}
}
}
setRecordingCameras(recordingCameras);
if (prop.containsKey(MARKER_RADIUS_PROP)) {
setMarkerRadius(Integer.parseInt(prop.getProperty(MARKER_RADIUS_PROP)));
}
if (prop.containsKey(IGNORE_LASER_COLOR_PROP)) {
final String colorName = prop.getProperty(IGNORE_LASER_COLOR_PROP);
if (!colorName.equals("None")) {
setIgnoreLaserColor(true);
setIgnoreLaserColorName(colorName);
}
}
if (prop.containsKey(USE_RED_LASER_SOUND_PROP)) {
setUseRedLaserSound(Boolean.parseBoolean(prop.getProperty(USE_RED_LASER_SOUND_PROP)));
}
if (prop.containsKey(RED_LASER_SOUND_PROP)) {
setRedLaserSound(new File(prop.getProperty(RED_LASER_SOUND_PROP)));
}
if (prop.containsKey(USE_GREEN_LASER_SOUND_PROP)) {
setUseGreenLaserSound(Boolean.parseBoolean(prop.getProperty(USE_GREEN_LASER_SOUND_PROP)));
}
if (prop.containsKey(GREEN_LASER_SOUND_PROP)) {
setGreenLaserSound(new File(prop.getProperty(GREEN_LASER_SOUND_PROP)));
}
if (prop.containsKey(USE_VIRTUAL_MAGAZINE_PROP)) {
setUseVirtualMagazine(Boolean.parseBoolean(prop.getProperty(USE_VIRTUAL_MAGAZINE_PROP)));
}
if (prop.containsKey(VIRTUAL_MAGAZINE_CAPACITY_PROP)) {
setVirtualMagazineCapacity(Integer.parseInt(prop.getProperty(VIRTUAL_MAGAZINE_CAPACITY_PROP)));
}
if (prop.containsKey(USE_MALFUNCTIONS_PROP)) {
setMalfunctions(Boolean.parseBoolean(prop.getProperty(USE_MALFUNCTIONS_PROP)));
}
if (prop.containsKey(MALFUNCTIONS_PROBABILITY_PROP)) {
setMalfunctionsProbability(Float.parseFloat(prop.getProperty(MALFUNCTIONS_PROBABILITY_PROP)));
}
if (prop.containsKey(ARENA_POSITION_X_PROP) && prop.containsKey(ARENA_POSITION_Y_PROP)) {
setArenaPosition(Double.parseDouble(prop.getProperty(ARENA_POSITION_X_PROP)),
Double.parseDouble(prop.getProperty(ARENA_POSITION_Y_PROP)));
}
if (prop.containsKey(PERSPECTIVE_WEBCAM_DISTANCES)) {
for (final String distanceString : prop.getProperty(PERSPECTIVE_WEBCAM_DISTANCES).split(",")) {
final String[] distanceComponents = distanceString.split("\\|");
if (distanceComponents.length == 2) {
cameraDistances.put(distanceComponents[0], Integer.parseInt(distanceComponents[1]));
}
}
}
if (prop.containsKey(MUTED_CHIME_MESSAGES)) {
for (final String message : prop.getProperty(MUTED_CHIME_MESSAGES).split("\\|")) {
muteMessageChime(message);
}
}
if (prop.containsKey(CALIBRATED_FEED_BEHAVIOR_PROP)) {
setCalibratedFeedBehavior(CalibrationOption.valueOf(prop.getProperty(CALIBRATED_FEED_BEHAVIOR_PROP)));
}
if (prop.containsKey(SHOW_ARENA_SHOT_MARKERS)) {
setShowArenaShotMarkers(Boolean.parseBoolean(prop.getProperty(SHOW_ARENA_SHOT_MARKERS)));
}
if (prop.containsKey(SHOWED_PERSPECTIVE_USAGE_MESSAGE)) {
setShowedPerspectiveMessage(Boolean.parseBoolean(prop.getProperty(SHOWED_PERSPECTIVE_USAGE_MESSAGE)));
}
if (prop.containsKey(CALIBRATE_AUTO_ADJUST_EXPOSURE)) {
setAutoAdjustExposure(Boolean.parseBoolean(prop.getProperty(CALIBRATE_AUTO_ADJUST_EXPOSURE)));
}
if (prop.containsKey(POI_ADJUSTMENT_X) && prop.containsKey(POI_ADJUSTMENT_Y)) {
poiAdjustmentX = Optional.of(Double.parseDouble(prop.getProperty(POI_ADJUSTMENT_X)));
poiAdjustmentY = Optional.of(Double.parseDouble(prop.getProperty(POI_ADJUSTMENT_Y)));
adjustingPOI = true;
logger.info("POI Adjustment loaded from config, x {} y {}", poiAdjustmentX.get(), poiAdjustmentY.get());
}
validateConfiguration();
}
public boolean writeConfigurationFile() throws ConfigurationException, IOException {
validateConfiguration();
if (!new File(configName).canWrite()) {
final Alert writeAlert = new Alert(AlertType.ERROR);
writeAlert.setTitle("Cannot Persist Preferences");
writeAlert.setHeaderText("Configuration File Unwritable!");
writeAlert.setResizable(true);
writeAlert.setContentText("The file " + configName + " is not writable, thus your preferences"
+ " cannot be saved. This is likely the case because you placed ShootOFF in a location"
+ " that only the administrator can write to, but ShootOFF is not running as an"
+ " administrator. Please either move ShootOFF to a different location or grant write"
+ " privileges to the file.");
writeAlert.showAndWait();
return false;
}
final Properties prop = new Properties();
final StringBuilder ipcamList = new StringBuilder();
for (final Entry<String, URL> entry : ipcams.entrySet()) {
if (ipcamList.length() > 0) ipcamList.append(",");
ipcamList.append(entry.getKey());
ipcamList.append("|");
ipcamList.append(entry.getValue().toString());
if (ipcamCredentials.containsKey(entry.getKey())) {
ipcamList.append("|");
ipcamList.append(ipcamCredentials.get(entry.getKey()));
}
}
final StringBuilder webcamList = new StringBuilder();
for (final Entry<String, Camera> entry : webcams.entrySet()) {
if (webcamList.length() > 0) webcamList.append(",");
webcamList.append(entry.getKey().replaceAll(":", "//`"));
webcamList.append(":");
webcamList.append(entry.getValue().getName().replaceAll(":", "//`"));
}
final StringBuilder recordingWebcamList = new StringBuilder();
for (final Camera c : recordingCameras) {
if (recordingWebcamList.length() > 0) recordingWebcamList.append(",");
recordingWebcamList.append(c.getName());
}
final StringBuilder mutedChimeMessages = new StringBuilder();
for (final String m : messagesChimeMuted) {
if (mutedChimeMessages.length() > 0) mutedChimeMessages.append("|");
mutedChimeMessages.append(m);
}
final StringBuilder cameraDistancesList = new StringBuilder();
for (final Entry<String, Integer> distanceEntry : cameraDistances.entrySet()) {
if (cameraDistancesList.length() > 0) cameraDistancesList.append(",");
cameraDistancesList.append(distanceEntry.getKey());
cameraDistancesList.append("|");
cameraDistancesList.append(distanceEntry.getValue());
}
prop.setProperty(FIRST_RUN_PROP, String.valueOf(isFirstRun));
prop.setProperty(ERROR_REPORTING_PROP, String.valueOf(useErrorReporting));
prop.setProperty(IPCAMS_PROP, ipcamList.toString());
prop.setProperty(WEBCAMS_PROP, webcamList.toString());
prop.setProperty(RECORDING_WEBCAMS_PROP, recordingWebcamList.toString());
prop.setProperty(MARKER_RADIUS_PROP, String.valueOf(markerRadius));
prop.setProperty(IGNORE_LASER_COLOR_PROP, ignoreLaserColorName);
prop.setProperty(USE_RED_LASER_SOUND_PROP, String.valueOf(useRedLaserSound));
prop.setProperty(RED_LASER_SOUND_PROP, redLaserSound.getPath());
prop.setProperty(USE_GREEN_LASER_SOUND_PROP, String.valueOf(useGreenLaserSound));
prop.setProperty(GREEN_LASER_SOUND_PROP, greenLaserSound.getPath());
prop.setProperty(USE_VIRTUAL_MAGAZINE_PROP, String.valueOf(useVirtualMagazine));
prop.setProperty(VIRTUAL_MAGAZINE_CAPACITY_PROP, String.valueOf(virtualMagazineCapacity));
prop.setProperty(USE_MALFUNCTIONS_PROP, String.valueOf(useMalfunctions));
prop.setProperty(MALFUNCTIONS_PROBABILITY_PROP, String.valueOf(malfunctionsProbability));
prop.setProperty(MUTED_CHIME_MESSAGES, mutedChimeMessages.toString());
if (getArenaPosition().isPresent()) {
final Point2D arenaPosition = getArenaPosition().get();
prop.setProperty(ARENA_POSITION_X_PROP, String.valueOf(arenaPosition.getX()));
prop.setProperty(ARENA_POSITION_Y_PROP, String.valueOf(arenaPosition.getY()));
}
prop.setProperty(PERSPECTIVE_WEBCAM_DISTANCES, cameraDistancesList.toString());
prop.setProperty(CALIBRATED_FEED_BEHAVIOR_PROP, calibratedFeedBehavior.name());
prop.setProperty(SHOW_ARENA_SHOT_MARKERS, String.valueOf(showArenaShotMarkers));
prop.setProperty(CALIBRATE_AUTO_ADJUST_EXPOSURE, String.valueOf(autoAdjustExposure));
prop.setProperty(SHOWED_PERSPECTIVE_USAGE_MESSAGE, String.valueOf(showedPerspectiveMessage));
if (isAdjustingPOI() && poiAdjustmentX.isPresent() && poiAdjustmentY.isPresent()) {
prop.setProperty(POI_ADJUSTMENT_X, String.valueOf(poiAdjustmentX.get()));
prop.setProperty(POI_ADJUSTMENT_Y, String.valueOf(poiAdjustmentY.get()));
}
final OutputStream outputStream = new FileOutputStream(configName);
try {
prop.store(outputStream, "ShootOFF Configuration");
outputStream.flush();
} catch (final IOException ioe) {
throw ioe;
} finally {
outputStream.close();
}
return true;
}
private void parseCmdLine(String[] args) throws ConfigurationException {
final Options options = new Options();
options.addOption("d", "debug", false, "turn on debug log messages");
options.addOption("h", "headless", false, "run without the main GUI and immediately open the projector arena");
options.addOption("m", "marker-radius", true, "sets the radius of shot markers in pixels [1,20]");
options.addOption("c", "ignore-laser-color", true,
"sets the color of laser that should be ignored by ShootOFF (green "
+ "or red). No color is ignored by default");
options.addOption("u", "use-virtual-magazine", true,
"turns on the virtual magazine and sets the number rounds it holds [1,45]");
options.addOption("f", "use-malfunctions", true,
"turns on malfunctions and sets the probability of them happening");
try {
final CommandLineParser parser = new DefaultParser();
final CommandLine cmd = parser.parse(options, args);
if (cmd.hasOption("d")) setDebugMode(true);
if (cmd.hasOption("h")) headless = true;
if (cmd.hasOption("m")) setMarkerRadius(Integer.parseInt(cmd.getOptionValue("m")));
if (cmd.hasOption("c")) {
setIgnoreLaserColor(true);
setIgnoreLaserColorName(cmd.getOptionValue("c"));
}
if (cmd.hasOption("u")) {
setUseVirtualMagazine(true);
setVirtualMagazineCapacity(Integer.parseInt(cmd.getOptionValue("u")));
}
if (cmd.hasOption("f")) {
setMalfunctions(true);
setMalfunctionsProbability(Float.parseFloat(cmd.getOptionValue("f")));
}
} catch (final ParseException e) {
System.err.println(e.getMessage());
final HelpFormatter formatter = new HelpFormatter();
formatter.printHelp("com.shootoff.Main", options);
Main.forceClose(-1);
}
validateConfiguration();
}
protected void validateConfiguration() throws ConfigurationException {
if (markerRadius < 1 || markerRadius > 20) {
throw new ConfigurationException(String.format(MARKER_RADIUS_MESSAGE, markerRadius));
}
if (!redLaserSound.isAbsolute())
redLaserSound = new File(System.getProperty("shootoff.home") + File.separator + redLaserSound.getPath());
if (useRedLaserSound && !redLaserSound.exists()) {
throw new ConfigurationException(String.format(LASER_SOUND_MESSAGE, redLaserSound.getPath()));
}
if (!greenLaserSound.isAbsolute()) greenLaserSound = new File(
System.getProperty("shootoff.home") + File.separator + greenLaserSound.getPath());
if (useGreenLaserSound && !greenLaserSound.exists()) {
throw new ConfigurationException(String.format(LASER_SOUND_MESSAGE, greenLaserSound.getPath()));
}
if (ignoreLaserColor && !ignoreLaserColorName.equals("red") && !ignoreLaserColorName.equals("green")) {
throw new ConfigurationException(String.format(LASER_COLOR_MESSAGE, ignoreLaserColorName));
}
if (virtualMagazineCapacity < 1 || virtualMagazineCapacity > 45) {
throw new ConfigurationException(String.format(VIRTUAL_MAGAZINE_MESSAGE, virtualMagazineCapacity));
}
if (malfunctionsProbability < (float) 0.1 || malfunctionsProbability > (float) 99.9) {
throw new ConfigurationException(String.format(INJECT_MALFUNCTIONS_MESSAGE, malfunctionsProbability));
}
}
public int getDisplayWidth() {
return displayWidth;
}
public int getDisplayHeight() {
return displayHeight;
}
public void setDisplayResolution(int displayWidth, int displayHeight) {
this.displayWidth = displayWidth;
this.displayHeight = displayHeight;
}
public boolean isFirstRun() {
return isFirstRun;
}
public void setFirstRun(boolean isFirstRun) {
this.isFirstRun = isFirstRun;
}
public boolean useErrorReporting() {
return useErrorReporting;
}
public void setUseErrorReporting(boolean useErrorReporting) {
this.useErrorReporting = useErrorReporting;
}
public static void disableErrorReporting() {
final Logger rootLogger = (Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
final LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
setLogConsoleAppender(rootLogger, loggerContext);
}
public void registerVideoPlayer(VideoPlayerController videoPlayer) {
videoPlayers.add(videoPlayer);
}
public void unregisterVideoPlayer(VideoPlayerController videoPlayer) {
videoPlayers.remove(videoPlayer);
}
public Set<VideoPlayerController> getVideoPlayers() {
return videoPlayers;
}
public Optional<Camera> registerIpCam(String cameraName, String cameraURL, Optional<String> username,
Optional<String> password) {
try {
final URL url = new URL(cameraURL);
final Camera cam = IpCamera.registerIpCamera(cameraName, url, username, password);
ipcams.put(cameraName, url);
if (username.isPresent() && password.isPresent()) {
ipcamCredentials.put(cameraName, username.get() + "|" + password.get());
}
return Optional.of(cam);
} catch (MalformedURLException | URISyntaxException ue) {
final Alert ipcamURLAlert = new Alert(AlertType.ERROR);
ipcamURLAlert.setTitle("Malformed URL");
ipcamURLAlert.setHeaderText("IPCam URL is Malformed!");
ipcamURLAlert.setResizable(true);
ipcamURLAlert.setContentText("IPCam URL is not valid: \n\n" + ue.getMessage());
ipcamURLAlert.showAndWait();
} catch (final UnknownHostException uhe) {
final Alert ipcamHostAlert = new Alert(AlertType.ERROR);
ipcamHostAlert.setTitle("Unknown Host");
ipcamHostAlert.setHeaderText("IPCam URL Unknown!");
ipcamHostAlert.setResizable(true);
ipcamHostAlert.setContentText("The IPCam at " + cameraURL
+ " cannot be resolved. Ensure the URL is correct "
+ "and that you are either connected to the internet or on the same network as the camera.");
ipcamHostAlert.showAndWait();
} catch (final TimeoutException te) {
final Alert ipcamTimeoutAlert = new Alert(AlertType.ERROR);
ipcamTimeoutAlert.setTitle("IPCam Timeout");
ipcamTimeoutAlert.setHeaderText("Connection to IPCam Reached Timeout!");
ipcamTimeoutAlert.setResizable(true);
ipcamTimeoutAlert.setContentText("Could not communicate with the IP at " + cameraURL
+ ". Please check the following:\n\n" + "-The IPCam URL is correct\n"
+ "-You are connected to the Internet (for external cameras)\n"
+ "-You are connected to the same network as the camera (for local cameras)");
ipcamTimeoutAlert.showAndWait();
}
return Optional.empty();
}
public void unregisterIpCam(String cameraName) {
if (IpCamera.unregisterIpCamera(cameraName)) {
ipcams.remove(cameraName);
ipcamCredentials.remove(cameraName);
}
}
public void setWebcams(List<String> webcamNames, List<Camera> configuredCameras) {
webcams.clear();
for (int i = 0; i < webcamNames.size(); i++) {
webcams.put(webcamNames.get(i), configuredCameras.get(i));
}
}
public void setMarkerRadius(int markRadius) {
markerRadius = markRadius;
}
public void setIgnoreLaserColor(boolean ignoreLaserColor) {
this.ignoreLaserColor = ignoreLaserColor;
}
public void setIgnoreLaserColorName(String ignoreLaserColorName) {
this.ignoreLaserColorName = ignoreLaserColorName;
}
public void setUseRedLaserSound(Boolean useRedLaserSound) {
this.useRedLaserSound = useRedLaserSound;
}
public void setRedLaserSound(File redLaserSound) {
this.redLaserSound = redLaserSound;
}
public void setUseGreenLaserSound(Boolean useGreenLaserSound) {
this.useGreenLaserSound = useGreenLaserSound;
}
public void setGreenLaserSound(File greenLaserSound) {
this.greenLaserSound = greenLaserSound;
}
public void setUseVirtualMagazine(boolean useVirtualMagazine) {
this.useVirtualMagazine = useVirtualMagazine;
if (!useVirtualMagazine && magazineProcessor != null) {
shotProcessors.remove(magazineProcessor);
magazineProcessor = null;
}
}
public void setVirtualMagazineCapacity(int virtualMagazineCapacity) {
this.virtualMagazineCapacity = virtualMagazineCapacity;
if (useVirtualMagazine) {
if (magazineProcessor != null) {
shotProcessors.remove(magazineProcessor);
}
magazineProcessor = new VirtualMagazineProcessor(this);
shotProcessors.add(magazineProcessor);
}
}
public void setMalfunctions(boolean injectMalfunctions) {
useMalfunctions = injectMalfunctions;
if (!useMalfunctions && malfunctionsProcessor != null) {
shotProcessors.remove(malfunctionsProcessor);
malfunctionsProcessor = null;
}
}
public void setMalfunctionsProbability(float injectMalfunctionsProbability) {
malfunctionsProbability = injectMalfunctionsProbability;
if (useMalfunctions) {
if (malfunctionsProcessor != null) {
shotProcessors.remove(malfunctionsProcessor);
}
malfunctionsProcessor = new MalfunctionsProcessor(this);
shotProcessors.add(malfunctionsProcessor);
}
}
public void setDebugMode(boolean debugMode) {
this.debugMode = debugMode;
if (debugMode) {
// Ignore first run operations if we are running in debug mode
setFirstRun(false);
}
final Logger rootLogger = (Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
if (debugMode) {
final LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
setLogConsoleAppender(rootLogger, loggerContext);
if (rootLogger.getLevel().equals(Level.TRACE)) {
return;
}
rootLogger.setLevel(Level.DEBUG);
// Ensure webcam-capture logger stays at info because it is quite
// noisy and doesn't output information we care about.
final Logger webcamCaptureLogger = loggerContext.getLogger("com.github.sarxos");
webcamCaptureLogger.setLevel(Level.INFO);
// Drop WebcamDiscoveryService even lower because it is extremely
// noisy
final Logger webcamDiscoveryLogger = loggerContext
.getLogger("com.github.sarxos.webcam.WebcamDiscoveryService");
webcamDiscoveryLogger.setLevel(Level.WARN);
} else {
rootLogger.setLevel(Level.WARN);
}
}
private static void setLogConsoleAppender(Logger rootLogger, LoggerContext loggerContext) {
final PatternLayoutEncoder ple = new PatternLayoutEncoder();
ple.setPattern("%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n");
ple.setContext(loggerContext);
ple.start();
final ConsoleAppender<ILoggingEvent> consoleAppender = new ConsoleAppender<>();
consoleAppender.setEncoder(ple);
consoleAppender.setContext(loggerContext);
consoleAppender.start();
rootLogger.detachAndStopAllAppenders();
rootLogger.setAdditive(false);
rootLogger.addAppender(consoleAppender);
}
public void setRecordingCameras(Set<Camera> recordingCameras2) {
recordingCameras = recordingCameras2;
}
public void setShotTimerRowColor(Color c) {
shotRowColor = Optional.ofNullable(c);
}
public void muteMessageChime(String message) {
messagesChimeMuted.add(message);
}
public void unmuteMessageChime(String message) {
messagesChimeMuted.remove(message);
}
public void setCalibratedFeedBehavior(CalibrationOption calibrationOption) {
calibratedFeedBehavior = calibrationOption;
}
public void setShowArenaShotMarkers(boolean showMarkers) {
showArenaShotMarkers = showMarkers;
}
public void setAutoAdjustExposure(boolean autoAdjust) {
autoAdjustExposure = autoAdjust;
}
public Set<Camera> getRecordingCameras() {
return recordingCameras;
}
public void registerRecordingCameraManager(CameraManager cm) {
recordingManagers.add(cm);
}
public void unregisterRecordingCameraManager(CameraManager cm) {
recordingManagers.remove(cm);
}
public void unregisterAllRecordingCameraManagers() {
recordingManagers.clear();
}
public void setSessionRecorder(SessionRecorder sessionRecorder) {
this.sessionRecorder = Optional.ofNullable(sessionRecorder);
}
public void setExercise(TrainingExercise exercise) {
if (currentExercise != null) currentExercise.destroy();
currentExercise = exercise;
}
public void setPlugin(Plugin plugin) {
currentPlugin = plugin;
}
public void setArenaPosition(double x, double y) {
arenaPosition = Optional.of(new Point2D(x, y));
}
public void setCameraDistance(String webcamName, int distance) {
cameraDistances.put(webcamName, distance);
}
public void setShowedPerspectiveMessage(boolean showedPerspectiveMessage) {
this.showedPerspectiveMessage = showedPerspectiveMessage;
}
public Map<String, URL> getRegistedIpCams() {
return ipcams;
}
public Map<String, Camera> getWebcams() {
return webcams;
}
public Optional<String> getWebcamsUserName(Camera webcam) {
for (final Entry<String, Camera> entry : webcams.entrySet()) {
if (entry.getValue().equals(webcam)) return Optional.of(entry.getKey());
}
return Optional.empty();
}
public int getMarkerRadius() {
return markerRadius;
}
public boolean ignoreLaserColor() {
return ignoreLaserColor;
}
public Optional<Color> getIgnoreLaserColor() {
if (ignoreLaserColorName.equals("red")) {
return Optional.of(Color.RED);
} else if (ignoreLaserColorName.equals("green")) {
return Optional.of(Color.GREEN);
}
return Optional.empty();
}
public String getIgnoreLaserColorName() {
return ignoreLaserColorName;
}
public boolean useRedLaserSound() {
return useRedLaserSound;
}
public File getRedLaserSound() {
if (!redLaserSound.isAbsolute())
redLaserSound = new File(System.getProperty("shootoff.home") + File.separator + redLaserSound.getPath());
return redLaserSound;
}
public boolean useGreenLaserSound() {
return useGreenLaserSound;
}
public File getGreenLaserSound() {
if (!greenLaserSound.isAbsolute()) greenLaserSound = new File(
System.getProperty("shootoff.home") + File.separator + greenLaserSound.getPath());
return greenLaserSound;
}
public boolean useVirtualMagazine() {
return useVirtualMagazine;
}
public int getVirtualMagazineCapacity() {
return virtualMagazineCapacity;
}
public boolean useMalfunctions() {
return useMalfunctions;
}
public float getMalfunctionsProbability() {
return malfunctionsProbability;
}
public boolean inDebugMode() {
return debugMode;
}
public boolean isHeadless() {
return headless;
}
public Optional<SessionRecorder> getSessionRecorder() {
return sessionRecorder;
}
public Set<CameraManager> getRecordingManagers() {
return recordingManagers;
}
public Set<ShotProcessor> getShotProcessors() {
return shotProcessors;
}
public Optional<TrainingExercise> getExercise() {
return Optional.ofNullable(currentExercise);
}
public Optional<Plugin> getPlugin() {
return Optional.ofNullable(currentPlugin);
}
public Optional<Color> getShotTimerRowColor() {
return shotRowColor;
}
public boolean isDebugShotsRecordToFiles() {
return debugShotsRecordToFiles;
}
public Optional<Point2D> getArenaPosition() {
return arenaPosition;
}
public Optional<Integer> getCameraDistance(String cameraName) {
return Optional.ofNullable(cameraDistances.get(cameraName));
}
public boolean isChimeMuted(String message) {
return messagesChimeMuted.contains(message);
}
public CalibrationOption getCalibratedFeedBehavior() {
return calibratedFeedBehavior;
}
public boolean showArenaShotMarkers() {
return showArenaShotMarkers;
}
public boolean showedPerspectiveMessage() {
return showedPerspectiveMessage;
}
public boolean autoAdjustExposure() {
return autoAdjustExposure;
}
private final static int POI_NUM_TARGETS = 5;
// Returns true IFF the current action is TURNING OFF POI Adjustment
// Logic:
// 1. Does not enable POI adjustment until 5 shots have been reached
// 2. POI is average of those 5 shots
// 3. Sixth shot turns off POI and resets shot count to 0
public boolean updatePOIAdjustment(double offsetX, double offsetY) {
// If it is already enabled, disable it and return. Don't process the
// current value
if (adjustingPOI == true) {
adjustingPOI = false;
poiAdjustmentX = Optional.empty();
poiAdjustmentY = Optional.empty();
poiAdjustmentCount = 0;
try {
writeConfigurationFile();
} catch (ConfigurationException | IOException e) {
logger.error("Failed to disable POI", e);
return false;
}
return true;
}
// First call
if (poiAdjustmentCount == 0) {
poiAdjustmentX = Optional.of(-1.0 * offsetX);
poiAdjustmentY = Optional.of(-1.0 * offsetY);
poiAdjustmentCount = 1;
}
// Second through numTargets calls
else {
double weightedAdjX = poiAdjustmentCount * poiAdjustmentX.get();
double weightedAdjY = poiAdjustmentCount * poiAdjustmentY.get();
poiAdjustmentX = Optional.of((weightedAdjX - offsetX) / (double) (poiAdjustmentCount + 1));
poiAdjustmentY = Optional.of((weightedAdjY - offsetY) / (double) (poiAdjustmentCount + 1));
poiAdjustmentCount++;
}
logger.trace("POI Adjustment: x {} y {}", poiAdjustmentX.get(), poiAdjustmentY.get());
if (poiAdjustmentCount == POI_NUM_TARGETS) {
adjustingPOI = true;
logger.info("Setting POI Adjustment: x {} y {}", poiAdjustmentX.get(), poiAdjustmentY.get());
try {
writeConfigurationFile();
} catch (ConfigurationException | IOException e) {
logger.error("Failed to write new POI", e);
return false;
}
}
return false;
}
public Optional<Double> getPOIAdjustmentX() {
return poiAdjustmentX;
}
public Optional<Double> getPOIAdjustmentY() {
return poiAdjustmentY;
}
public boolean isAdjustingPOI() {
return adjustingPOI;
}
}