// // MUData.java // Thud // // Created by asp on Tue Nov 20 2001. // Copyright (c) 2001-2006 Anthony Parker & the THUD team. // All rights reserved. See LICENSE.TXT for more information. // package net.sourceforge.btthud.data; import java.io.*; import java.util.*; import java.util.regex.*; import java.awt.event.ActionListener; /** * Stores all the information from contacts and tactical. * * Some notes: * Since a lot of other classes (mainly ones for displaying info) use this, we should keep things * thread safe if possible. We want to store most (if not all) of the info in this one class so that * we can keep things simple when passing data around. * * @author Anthony Parker */ public class MUData implements Runnable { // Making these public sorta defeats the purpose of hiding them in the class in the first place, but // I just want to make it easier on myself at this point. Maybe I'll fix it later. public static final int MAX_X = 1000; public static final int MAX_Y = 1000; public boolean hudRunning = false, hudStarted = false; public boolean mainWindowMuted = false; public MUMyInfo myUnit = null; // The map MUHex map[][] = null; boolean terrainChanged = true; public String mapName, mapId, mapVersion; public boolean mapLOSOnly = false; public String mapFileName; // One MUHex for each elevation and terrain // By storing references to MUHexes we can save memory // 19 = -9 thru 0 and 1 thru 9 MUHex hexCache[][] = new MUHex[MUHex.TOTAL_TERRAIN][19]; // We store the contact data in a ArrayList, because we need to iterate over it efficiently public ArrayList<MUUnitInfo> contacts = null; ArrayList<MUUnitInfo> buildings = null; // LOS info is in a Hashtable since we don't iterate, we query it public Hashtable LOSinfo = new Hashtable(); public int lastLOSX = 0, lastLOSY = 0, lastLOSZ = 0; // Weather info public MUWeather weather; // This is the time that we received our last hudinfo data public long lastDataTime; // What version of hudinfo are we working with? int hudInfoMajorVersion = 0; int hudInfoMinorVersion = 0; /** * Constructor */ public MUData() { hudRunning = false; clearData(); createHexCache(); LOSinfo = new Hashtable(); map = new MUHex[MAX_X][MAX_Y]; // individual hexes will be allocated if they are needed.. this is not very memory efficient still start(); } public void createHexCache() { for (int i = 0; i < MUHex.TOTAL_TERRAIN; i++) { for (int j = -9; j < 10; j++) { hexCache[i][j + 9] = new MUHex(i, j); } } } /** * Adds a new contact to our list of contacts, or updates an existing one. * * @param con Contact to be added */ public void newContact(MUUnitInfo con) { int index = indexForId(con.id); if (index != -1) contacts.set(index, con); else contacts.add(con); //System.out.println ("map:newContact unit :" + con.id + ":" + new Boolean (con.isTarget ()).toString ()); } /** * Iterates through the contact list and marks contacts as expired or old or whatever. */ public void expireAllContacts() { try { // We need a ListIterator because it allows us to modify the list while we iterate ListIterator it = contacts.listIterator(); while (it.hasNext()) { MUUnitInfo unit = (MUUnitInfo) it.next(); if (unit.isExpired()) it.remove(); // Remove this contact else unit.expireMore(); // Increase the age of this contact } } catch (Exception e) { System.out.println("Error: expireAllContacts: " + e); } } /** * Returns the index in the contacts ArayList of a specific map id. * Since we keep track of everything in our LinkedList in our hashtable, just check the hashtable to see if we have it * * @param id Map ID of contact to find */ protected int indexForId(String id) { ListIterator it = contacts.listIterator(); int index; while (it.hasNext()) { index = it.nextIndex(); // See if the next unit's upper-case id matches the id sent in if (((MUUnitInfo) it.next()).id.toUpperCase().equals(id.toUpperCase())) return index; } // Must not have found it return -1; } /** * Returns a MUUnitInfo of a specific map id. * @param id Map ID of unit to return */ public MUUnitInfo getContact(String id) { return (MUUnitInfo) contacts.get(indexForId(id)); } /** * Returns an Iterator for the contact list. Used for looping on contacts when drawing the map, for example * @param sorted True if we want a sorted list */ public Iterator getContactsIterator(boolean sorted) { if (!sorted) return contacts.iterator(); else return ((new TreeSet<MUUnitInfo>(contacts)).iterator()); } // ---------------------------------- /** * Get the terrain of a specific hex (return the id, not the char). * @param x X coordinate * @param y Y coordinate */ public int getHexTerrain(int x, int y) { if (x >= 0 && x < MAX_X && y >= 0 && y < MAX_Y) { if (map[x][y] != null) { if(map[x][y].hasDS) { return MUHex.WALL; } else { return map[x][y].terrain(); } } else return MUHex.UNKNOWN; } return MUHex.UNKNOWN; } /** * Get the elevation of a specific hex. * @param x X coordinate * @param y Y coordinate */ public int getHexElevation(int x, int y) { if (x >= 0 && x < MAX_X && y >= 0 && y < MAX_Y) { if (map[x][y] != null) return map[x][y].elevation(); else return 0; } return 0; } /** * Get the "cliff" elevation of a specific hex (is negative for water, and 0 for ice). * @param x X coordinate * @param y Y coordinate */ public int getHexCliffElevation(int x, int y) { int e = getHexElevation(x, y); // Since we use this function in determining cliff edges, a few corrections... if (getHexTerrain(x, y) == MUHex.ICE) // ice e = 0; // You can cross it, even tho it may be dangerous return e; } /** * Set the details of a specific hex. * @param x X coordinate * @param y Y coordinate * @param ter The terrain character representation * @param elevation The elevation of the hex */ public void setHex(int x, int y, char ter, int elevation) { if (x >= 0 && x < MAX_X && y >= 0 && y < MAX_Y) { map[x][y] = hexCache[MUHex.idForTerrain(ter)][elevation + 9]; } } /** * Change a normal terrain hex into a hex with a dropship marker on it. Used * by map drawing stuff to draw a '=' instead of normal terrain. */ public void setHexDS(int x, int y) { if (x >= 0 && x < MAX_X && y >= 0 && y < MAX_Y) { map[x][y] = new MUHex(getHexTerrain(x, y), getHexElevation(x, y)); map[x][y].hasDS = true; } } /** * Clear dropship marker from a hex. */ public void setHexNoDS(int x, int y) { if (x >= 0 && x < MAX_X && y >= 0 && y < MAX_Y) { map[x][y] = hexCache[map[x][y].terrain()][getHexElevation(x,y) + 9]; } } /** * Clear data that is 'Mech specific, so that when we start the HUD again we have a clean slate. */ public void clearData() { // Clear contacts and our unit, but leave the map alone contacts = new ArrayList<MUUnitInfo>(20); // data for our contact list myUnit = new MUMyInfo(); // data that represents our own unit clearMap(); this.mapName = ""; weather = new MUWeather(); lastDataTime = 0; // clear our last recieved data clearLOS(); } /** * Re-initializes map. */ public void clearMap() { map = new MUHex[MAX_X][MAX_Y]; } /** * Clear LOS data. */ public void clearLOS() { LOSinfo = new Hashtable(); } /** * Sets the map changed flag. */ public void setTerrainChanged(boolean b) { terrainChanged = b; } /** * Has the terrain changed? */ public boolean terrainChanged() { return terrainChanged; } public void setHudInfoMajorVersion(int v) { hudInfoMajorVersion = v; } public void setHudInfoMinorVersion(int v) { hudInfoMinorVersion = v; } // --------------------------------- // These functions are to determine if certain features exist (they were added in particular versions) // I thought it'd be easier to keep track of these here instead of spreading them across multiple files // that have 'magic numbers' to compare to // 'hi' = hudinfo public boolean hiSupportsOwnJumpInfo() { if (hudInfoMinorVersion > 6) return true; else return false; } public boolean hiSupportsWLHeatInfo() { if (hudInfoMinorVersion > 6) return true; else return false; } public boolean hiSupportsBuildingContacts() { if (hudInfoMinorVersion > 6) return true; else return false; } public boolean hiSupportsExtendedMapInfo() { if (hudInfoMinorVersion > 6) return true; else return false; } public boolean hiSupportsAllArgumentHudinfo() { if (hudInfoMinorVersion > 6) return true; else return false; } /** Attempts to load map information from a .tmap file on disk. * Uses the name of the current map . tmap (ie: 'DC.city3.tmap') * @return true if succesful, false if error/no file found/etc */ public boolean loadMapFromDisk() { if(mapFileName.length() <= 1) // do we have a real mapname? return false; try { File mapFile = new File(mapFileName); BufferedReader brin = new BufferedReader(new FileReader(mapFile)); String s = brin.readLine(); /* Check and see if this is a mux-format mapfile */ String patternStr = "^([0-9]+) ([0-9]+$)"; Pattern pattern = Pattern.compile(patternStr); Matcher matcher = pattern.matcher(s); boolean matchFound = matcher.find(); if(matchFound) { /* Looks like a btech format, let's parse it. */ int mapMaxX = Integer.parseInt(matcher.group(1)); int mapMaxY = Integer.parseInt(matcher.group(2)); System.out.println("Reading btmap file: " + String.valueOf(mapMaxX) + " by " + String.valueOf(mapMaxY)); map = new MUHex[MAX_X][MAX_Y]; int x = 0; int y = 0; while ((s = brin.readLine()) != null) { if(!s.matches("(\\D\\d)+")) { // only process line if it looks like good terrain System.out.println("Rejecting line: " + s); } else { // good line while(x < mapMaxX) { CharSequence hexSeq = s.subSequence(x * 2, (x * 2) + 2); char terrain = hexSeq.charAt(0); char elev = hexSeq.charAt(1); //System.out.println("Hex " + x + " " + y + "= " + terrain + " level " + elev); map[x][y] = hexCache[MUHex.idForTerrain(terrain)][Integer.parseInt(String.valueOf(elev)) + 9]; x++; } x=0; y++; } } return true; } else { /* Not a btech format mapfile, try using our 'Thud Format' */ FileInputStream in = new FileInputStream(mapFile); ObjectInputStream ois = new ObjectInputStream(in); map = new MUHex[MAX_X][MAX_Y]; map = (MUHex[][]) ois.readObject(); ois.close(); in.close(); return true; } } catch(Exception e) { System.out.println("Error loading map " + mapFileName + ": " + e); return false; } } /** Attempts to write map information to a .tmap file on disk. * Uses the name of the current map . tmap (ie: 'DC.city3.tmap') * @return true if succesful, false if error/file io error/etc */ public boolean saveMapToDisk() { if(mapFileName == null || mapFileName.length() <= 1) // do we have a real mapname? return false; try { File mapFile = new File(mapFileName); mapFile.delete(); mapFile.createNewFile(); FileOutputStream out = new FileOutputStream(mapFile); ObjectOutputStream oos = new ObjectOutputStream(out); oos.writeObject(this.map); oos.flush(); oos.close(); out.close(); return true; } catch(Exception e) { System.out.println("Error saving map " + mapName + ".tmap: " + e); return false; } } /** * Validate state. Users should validate before using certain kinds of * data. In particular, this handles dropships, which modify the terrain * they land on. */ public void validate () { // Clear terrain around landed dropships. Iterator contacts = getContactsIterator(false); while (contacts.hasNext()) { final MUUnitInfo unit = (MUUnitInfo)contacts.next(); if (unit.type.equals("D") || unit.type.equals("A")) { final int unitX = unit.getX(); final int unitY = unit.getY(); if (unit.getZ() == getHexElevation(unitX, unitY)) { // Landed dropship. setHexDS(unitX, unitY); setHexDS(unitX - 1, unitY - 1); setHexDS(unitX, unitY - 1); setHexDS(unitX + 1, unitY - 1); setHexDS(unitX - 1, unitY); setHexDS(unitX + 1, unitY); setHexDS(unitX, unitY + 1); setTerrainChanged(true); } } } } // // Data update thread. // private final List<ActionListener> listenerList = new ArrayList<ActionListener> (); public void addActionListener (final ActionListener listener) { listenerList.add(listener); } private void runActionListeners () { for (final ActionListener listener: listenerList) { listener.actionPerformed(null); } } private boolean go = true; private void start () { new Thread (this, "MUData").start(); } public void run () { synchronized (this) { while (go) { runActionListeners(); try { this.wait(1000); } catch (final InterruptedException e) { // No big deal. } } } } public void pleaseStop () { synchronized (this) { go = false; this.notify(); } } }