package teamcomm.data; import data.GameControlData; import data.Rules; import data.SPLStandardMessage; import data.TeamInfo; import data.Teams; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.TreeSet; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import javax.swing.event.EventListenerList; import teamcomm.PluginLoader; import teamcomm.data.event.TeamEvent; import teamcomm.data.event.TeamEventListener; import teamcomm.net.logging.LogReplayer; import teamcomm.net.logging.Logger; /** * Singleton class managing the known information about communicating robots. * * @author Felix Thielke */ public class GameState { /** * Index of the team playing on the left side of the field. */ public static final int TEAM_LEFT = 0; /** * Index of the team playing on the right side of the field. */ public static final int TEAM_RIGHT = 1; /** * Index of the virtual team containing illegally communicating robots. */ public static final int TEAM_OTHER = 2; private static final int CHANGED_LEFT = 1; private static final int CHANGED_RIGHT = 2; private static final int CHANGED_OTHER = 4; private static final GameState instance = new GameState(); private GameControlData lastGameControlData; private final int[] teamNumbers = new int[]{0, 0}; private final Map<Integer, Integer> teamColors = new HashMap<>(); private boolean mirrored = false; private final Map<Integer, Collection<RobotState>> robots = new HashMap<>(); private static final Comparator<RobotState> playerNumberComparator = new Comparator<RobotState>() { @Override public int compare(RobotState o1, RobotState o2) { if (o1.getPlayerNumber() == null) { if (o2.getPlayerNumber() == null) { return o1.hashCode() - o2.hashCode(); } return -1; } else if (o2.getPlayerNumber() == null) { return 1; } return o1.getPlayerNumber() - o2.getPlayerNumber(); } }; private final HashMap<String, RobotState> robotsByAddress = new HashMap<>(); private final EventListenerList listeners = new EventListenerList(); private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); private final ScheduledFuture<?> taskHandle; /** * Returns the only instance of the RobotData class. * * @return instance */ public static GameState getInstance() { return instance; } private GameState() { taskHandle = scheduler.scheduleAtFixedRate(new Runnable() { @Override public void run() { if (!(LogReplayer.getInstance().isReplaying() && LogReplayer.getInstance().isPaused())) { int changed = 0; synchronized (robotsByAddress) { final Iterator<RobotState> iter = robotsByAddress.values().iterator(); while (iter.hasNext()) { final RobotState r = iter.next(); if (r.updateConnectionStatus() == RobotState.ConnectionStatus.INACTIVE) { iter.remove(); final Collection<RobotState> team = robots.get(r.getTeamNumber()); team.remove(r); synchronized (teamNumbers) { if (r.getTeamNumber() == teamNumbers[TEAM_LEFT]) { changed |= CHANGED_LEFT; if (team.isEmpty() && lastGameControlData == null) { teamNumbers[TEAM_LEFT] = 0; } } else if (r.getTeamNumber() == teamNumbers[TEAM_RIGHT]) { changed |= CHANGED_RIGHT; if (team.isEmpty() && lastGameControlData == null) { teamNumbers[TEAM_RIGHT] = 0; } } else { changed |= CHANGED_OTHER; } } } } } sendEvents(changed); } } }, RobotState.ConnectionStatus.HIGH_LATENCY.threshold * 2, RobotState.ConnectionStatus.HIGH_LATENCY.threshold / 2, TimeUnit.MILLISECONDS); } /** * Shuts down the thread which removes inactive robots. To be called before * the program exits. */ public void shutdown() { taskHandle.cancel(false); } /** * Resets all information about robots and teams. */ public void reset() { lastGameControlData = null; synchronized (teamNumbers) { teamNumbers[0] = 0; teamNumbers[1] = 0; } synchronized (robotsByAddress) { robots.clear(); robotsByAddress.clear(); } sendEvents(CHANGED_LEFT | CHANGED_RIGHT | CHANGED_OTHER); } /** * Updates info about the game with a message from the GameController. * * @param data data sent by the GameController */ public void updateGameData(final GameControlData data) { int changed = 0; if (data == null) { if (lastGameControlData != null) { synchronized (teamNumbers) { teamNumbers[TEAM_LEFT] = 0; teamNumbers[TEAM_RIGHT] = 0; int s = 0; for (final Entry<Integer, Collection<RobotState>> entry : robots.entrySet()) { if (!entry.getValue().isEmpty()) { teamNumbers[s++] = entry.getKey(); if (s == 2) { break; } } } } changed = CHANGED_LEFT | CHANGED_RIGHT | CHANGED_OTHER; Logger.getInstance().createLogfile(); } } else { if (lastGameControlData == null) { synchronized (teamNumbers) { teamNumbers[TEAM_LEFT] = data.team[0].teamNumber; teamNumbers[TEAM_RIGHT] = data.team[1].teamNumber; } changed = CHANGED_LEFT | CHANGED_RIGHT | CHANGED_OTHER; } else { synchronized (teamNumbers) { if (data.team[0].teamNumber != teamNumbers[TEAM_LEFT]) { teamNumbers[TEAM_LEFT] = data.team[0].teamNumber; changed = CHANGED_LEFT | CHANGED_OTHER; } if (data.team[1].teamNumber != teamNumbers[TEAM_RIGHT]) { teamNumbers[TEAM_RIGHT] = data.team[1].teamNumber; changed = CHANGED_RIGHT | CHANGED_OTHER; } } } teamColors.put((int) data.team[0].teamNumber, (int) data.team[0].teamColor); teamColors.put((int) data.team[1].teamNumber, (int) data.team[1].teamColor); // Update penalties for (final TeamInfo team : data.team) { final Collection<RobotState> teamRobots = robots.get((int) team.teamNumber); if (teamRobots != null) { for (final RobotState r : teamRobots) { if (r.getPlayerNumber() != null && r.getPlayerNumber() <= team.player.length) { r.setPenalty(team.player[r.getPlayerNumber() - 1].penalty); } } } } // Open a new logfile for the current GameController state if the // state changed from or to initial/finished final StringBuilder logfileName; if ((data.team[0].teamNumber == 98 || data.team[0].teamNumber == 99) && (data.team[1].teamNumber == 98 || data.team[1].teamNumber == 99)) { logfileName = new StringBuilder("Drop-in_"); if (data.firstHalf == GameControlData.C_TRUE) { logfileName.append("1st"); } else { logfileName.append("2nd"); } logfileName.append("Half"); } else { if (data.firstHalf == GameControlData.C_TRUE) { logfileName = new StringBuilder(getTeamName((int) data.team[0].teamNumber, false, false)).append("_").append(getTeamName((int) data.team[1].teamNumber, false, false)).append("_1st"); } else { logfileName = new StringBuilder(getTeamName((int) data.team[1].teamNumber, false, false)).append("_").append(getTeamName((int) data.team[0].teamNumber, false, false)).append("_2nd"); } logfileName.append("Half"); } if (data.gameState == GameControlData.STATE_READY && (lastGameControlData == null || lastGameControlData.gameState == GameControlData.STATE_INITIAL)) { Logger.getInstance().createLogfile(logfileName.toString()); } else if (data.gameState == GameControlData.STATE_INITIAL && (lastGameControlData == null || lastGameControlData.gameState != GameControlData.STATE_INITIAL)) { Logger.getInstance().createLogfile(logfileName.append("_initial").toString()); } else if (data.gameState == GameControlData.STATE_FINISHED && (lastGameControlData == null || lastGameControlData.gameState != GameControlData.STATE_FINISHED)) { Logger.getInstance().createLogfile(logfileName.append("_finished").toString()); } if (changed != 0) { // (re)load plugins PluginLoader.getInstance().update((int) data.team[0].teamNumber, (int) data.team[1].teamNumber); } } lastGameControlData = data; // Log the GameController data if (data != null || changed != 0) { Logger.getInstance().log(data); } // send events sendEvents(changed); } /** * Handles a message that was received from a robot. * * @param address IP address of the sender * @param teamNumber team number belonging to the port on which the message * was received * @param message received message */ public void receiveMessage(final String address, final int teamNumber, final SPLStandardMessage message) { int changed = 0; // update the team info if no GameController info is available if (lastGameControlData == null) { synchronized (teamNumbers) { boolean exists = false; for (int i = 0; i < 2; i++) { if (teamNumbers[i] == teamNumber) { exists = true; break; } } if (!exists) { for (int i = 0; i < 2; i++) { if (teamNumbers[i] == 0) { teamNumbers[i] = teamNumber; // (re)load plugins PluginLoader.getInstance().update(teamNumber); changed = i + 1 | CHANGED_OTHER; break; } } } } } // create the robot state if it does not yet exist RobotState r; synchronized (robotsByAddress) { r = robotsByAddress.get(address); if (r == null) { r = new RobotState(address, teamNumber); robotsByAddress.put(address, r); } Collection<RobotState> set = robots.get(teamNumber); if (set == null) { set = new HashSet<>(); robots.put(teamNumber, set); } if (set.add(r)) { if (teamNumbers[TEAM_LEFT] == teamNumber) { changed |= CHANGED_LEFT; } else if (teamNumbers[TEAM_RIGHT] == teamNumber) { changed |= CHANGED_RIGHT; } } } // let the robot state handle the message r.registerMessage(message); // send events sendEvents(changed); } private void sendEvents(final int changed) { boolean leftSent = false; boolean rightSent = false; if ((changed & CHANGED_OTHER) != 0) { final Collection<RobotState> rs = new TreeSet<>(playerNumberComparator); synchronized (robotsByAddress) { for (final Entry<Integer, Collection<RobotState>> entry : robots.entrySet()) { if (entry.getKey() == teamNumbers[TEAM_LEFT]) { if ((changed & CHANGED_LEFT) != 0) { final Collection<RobotState> list = new TreeSet<>(playerNumberComparator); for (final RobotState r : entry.getValue()) { list.add(r); } fireEvent(new TeamEvent(this, outputSide(TEAM_LEFT), teamNumbers[TEAM_LEFT], list)); leftSent = true; } } else if (entry.getKey() == teamNumbers[TEAM_RIGHT]) { if ((changed & CHANGED_RIGHT) != 0) { final Collection<RobotState> list = new TreeSet<>(playerNumberComparator); for (final RobotState r : entry.getValue()) { list.add(r); } fireEvent(new TeamEvent(this, outputSide(TEAM_RIGHT), teamNumbers[TEAM_RIGHT], list)); rightSent = true; } } else { for (final RobotState r : entry.getValue()) { rs.add(r); } } } } fireEvent(new TeamEvent(this, TEAM_OTHER, 0, rs)); } if (!leftSent && (changed & CHANGED_LEFT) != 0) { final Collection<RobotState> rs; synchronized (robotsByAddress) { rs = robots.get(teamNumbers[TEAM_LEFT]); } final Collection<RobotState> list = new TreeSet<>(playerNumberComparator); if (rs != null) { for (final RobotState r : rs) { list.add(r); } } fireEvent(new TeamEvent(this, outputSide(TEAM_LEFT), teamNumbers[TEAM_LEFT], list)); } if (!rightSent && (changed & CHANGED_RIGHT) != 0) { final Collection<RobotState> rs; synchronized (robotsByAddress) { rs = robots.get(teamNumbers[TEAM_RIGHT]); } final Collection<RobotState> list = new TreeSet<>(playerNumberComparator); if (rs != null) { for (final RobotState r : rs) { list.add(r); } } fireEvent(new TeamEvent(this, outputSide(TEAM_RIGHT), teamNumbers[TEAM_RIGHT], list)); } } private void fireEvent(final TeamEvent e) { for (final TeamEventListener listener : listeners.getListeners(TeamEventListener.class)) { listener.teamChanged(e); } } /** * Returns the team color of the given team. The team color is either sent * by the game controller or given by the GameController configuration. * * @param teamNumber number of the team * @return the team color * @see TeamInfo#teamColor */ public int getTeamColor(final int teamNumber) { Integer color = teamColors.get(teamNumber); if (color == null) { String[] colorStrings = null; try { if (teamNumber == 98 || teamNumber == 99) { Rules.league = Rules.LEAGUES[1]; } else { Rules.league = Rules.LEAGUES[0]; } colorStrings = Teams.getColors(teamNumber); } catch (final NullPointerException | ArrayIndexOutOfBoundsException e) { } if (colorStrings == null || colorStrings.length < 1) { if (teamNumber == teamNumbers[TEAM_RIGHT]) { return GameControlData.TEAM_RED; } else { return GameControlData.TEAM_BLUE; } } else if (colorStrings[0].equals("blue")) { return GameControlData.TEAM_BLUE; } else if (colorStrings[0].equals("red")) { return GameControlData.TEAM_RED; } else if (colorStrings[0].equals("yellow")) { return GameControlData.TEAM_YELLOW; } else { return GameControlData.TEAM_BLACK; } } return color; } /** * Returns the most recently received GameControlData. * * @return GameControlData of null if none was received recently */ public GameControlData getLastGameControlData() { return lastGameControlData; } /** * Returns the team name of the given team. * * @param teamNumber number of the team * @param withNumber whether the team number should be in the returned * string * @param withPrefix whether the pre- or suffix "Team" should be included * @return the team name */ public String getTeamName(final Integer teamNumber, final boolean withNumber, final boolean withPrefix) { if (teamNumber == 98 || teamNumber == 99) { Rules.league = Rules.LEAGUES[1]; } else { Rules.league = Rules.LEAGUES[0]; } final String[] teamNames = Teams.getNames(withNumber); if (teamNumber != null) { if (teamNumber < teamNames.length && teamNames[teamNumber] != null) { return ((withPrefix ? "Team " : "") + teamNames[teamNumber]); } else { return ("Unknown" + (withPrefix ? " Team" : "") + (withNumber ? " (" + teamNumber + ")" : "")); } } else { return "Unknown" + (withPrefix ? " Team" : ""); } } /** * Returns whether the team sides are mirrored. * * @return boolean */ public boolean isMirrored() { return mirrored; } /** * Sets whether the team sides are mirrored. * * @param mirrored boolean */ public void setMirrored(final boolean mirrored) { if (mirrored != this.mirrored) { this.mirrored = mirrored; sendEvents(CHANGED_LEFT | CHANGED_RIGHT); } } private int outputSide(final int side) { return mirrored ? (side == 0 ? 1 : (side == 1 ? 0 : side)) : side; } /** * Registeres a GUI component as a listener receiving events about team * changes. * * @param listener component */ public void addListener(final TeamEventListener listener) { listeners.add(TeamEventListener.class, listener); sendEvents(CHANGED_LEFT | CHANGED_RIGHT | CHANGED_OTHER); } }