package vassal_shim ; import java.lang.reflect.Field ; import java.io.File ; import java.io.FileInputStream ; import java.io.InputStream ; import java.io.InputStreamReader ; import java.io.FileOutputStream ; import java.io.BufferedReader ; import java.io.IOException ; import java.io.FileNotFoundException ; import java.net.URISyntaxException ; import java.util.Collections ; import java.util.Arrays ; import java.util.List ; import java.util.ArrayList ; import java.util.Map ; import java.util.HashMap ; import java.util.Set ; import java.util.HashSet ; import java.util.Iterator ; import java.util.Comparator ; import java.util.Properties ; import java.util.regex.Pattern ; import java.util.regex.Matcher ; import java.awt.Point ; import java.awt.Dimension ; import javax.xml.parsers.DocumentBuilderFactory ; import javax.xml.parsers.DocumentBuilder ; import javax.xml.parsers.ParserConfigurationException ; import javax.xml.transform.TransformerException ; import javax.xml.transform.TransformerConfigurationException ; import javax.xml.xpath.XPathExpressionException ; import javax.swing.Action ; import org.w3c.dom.Document ; import org.w3c.dom.NodeList ; import org.w3c.dom.Node ; import org.w3c.dom.Element ; import org.xml.sax.SAXException ; import org.slf4j.Logger ; import org.slf4j.LoggerFactory ; import VASSAL.build.GameModule ; import VASSAL.build.GpIdChecker ; import VASSAL.build.module.GameState ; import VASSAL.build.module.BasicLogger.LogCommand ; import VASSAL.build.module.ModuleExtension ; import VASSAL.build.module.ObscurableOptions ; import VASSAL.build.module.Chatter.DisplayText ; import VASSAL.build.widget.PieceSlot ; import VASSAL.launch.BasicModule ; import VASSAL.command.Command ; import VASSAL.command.AddPiece ; import VASSAL.command.RemovePiece ; import VASSAL.build.module.map.boardPicker.Board ; import VASSAL.counters.GamePiece ; import VASSAL.counters.BasicPiece ; import VASSAL.counters.Decorator ; import VASSAL.counters.DynamicProperty ; import VASSAL.counters.PieceCloner ; import VASSAL.preferences.Prefs ; import VASSAL.tools.DataArchive ; import VASSAL.tools.DialogUtils ; import vassal_shim.lfa.* ; // -------------------------------------------------------------------- public class VassalShim { private static final Logger logger = LoggerFactory.getLogger( VassalShim.class ) ; private String baseDir ; private Properties config ; private String labelGpid ; private String vmodFilename ; private String boardsDir ; public VassalShim( String vmodFilename, String boardsDir ) throws IOException { // initialize this.vmodFilename = vmodFilename ; this.boardsDir = boardsDir ; // figure out where we live baseDir = null ; try { String jarFilename = this.getClass().getProtectionDomain().getCodeSource().getLocation().toURI().getPath() ; logger.debug( "Loaded from JAR: {}", jarFilename ) ; baseDir = new File( jarFilename ).getParent() ; logger.debug( "Base directory: {}", baseDir ) ; } catch( URISyntaxException ex ) { logger.error( "Can't locate JAR file:", ex ) ; } // load any config settings config = new Properties() ; if ( baseDir != null ) { File configFile = new File( baseDir + File.separator + "vassal-shim.properties" ) ; if ( configFile.isFile() ) { logger.info( "Loading properties: {}", configFile.getAbsolutePath() ) ; config.load( new FileInputStream( configFile ) ) ; for ( String key: config.stringPropertyNames() ) logger.debug( "- {} = {}", key, config.getProperty(key) ) ; } } labelGpid = config.getProperty( "LABEL_GPID", "6295" ) ; // FUDGE! Need this to be able to load the VASL module :-/ logger.debug( "Creating the menu manager." ) ; new ModuleManagerMenuManager() ; // initialize VASL logger.info( "Loading VASL module: {}", vmodFilename ) ; if ( ! ((new File(vmodFilename)).isFile() ) ) throw new IllegalArgumentException( "Can't find VASL module: " + vmodFilename ) ; DataArchive dataArchive = new DataArchive( vmodFilename ) ; logger.debug( "- Initializing module." ) ; BasicModule basicModule = new BasicModule( dataArchive ) ; logger.debug( "- Installing module." ) ; GameModule.init( basicModule ) ; logger.debug( "- Loaded OK." ) ; } public void dumpScenario( String scenarioFilename ) throws IOException { // load the scenario and dump its commands Command cmd = loadScenario( scenarioFilename ) ; dumpCommand( cmd, "" ) ; } public void analyzeLogs( ArrayList logFilenames, String reportFilename ) throws IOException, TransformerException, TransformerConfigurationException, ParserConfigurationException { // analyze each log file ArrayList results = new ArrayList() ; for ( int i=0 ; i < logFilenames.size() ; ++i ) { String fname = logFilenames.get( i ) ; logger.info( "Analyzing log files (" + (1+i) + "/" + logFilenames.size() + "): " + fname ) ; results.add( this._doAnalyzeLogs( fname ) ) ; } // generate the report Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument() ; Element rootElem = doc.createElement( "logFileAnalysis" ) ; doc.appendChild( rootElem ) ; for ( LogFileAnalysis logFileAnalysis: results ) { // add a node for the next log file Element logFileElem = doc.createElement( "logFile" ) ; logFileElem.setAttribute( "filename", logFileAnalysis.logFilename ) ; // check if we found a scenario name if ( logFileAnalysis.scenarioName.length() > 0 ) { Element scenarioElem = doc.createElement( "scenario" ) ; if ( logFileAnalysis.scenarioId.length() > 0 ) scenarioElem.setAttribute( "id", logFileAnalysis.scenarioId ) ; scenarioElem.setTextContent( logFileAnalysis.scenarioName ) ; logFileElem.appendChild( scenarioElem ) ; } // add the extracted events Element eventsElems = doc.createElement( "events" ) ; logFileElem.appendChild( eventsElems ) ; for ( Event evt: logFileAnalysis.events ) eventsElems.appendChild( evt.makeXmlElement( doc ) ) ; rootElem.appendChild( logFileElem ) ; } Utils.saveXml( doc, reportFilename ) ; logger.info( "All done." ) ; } public LogFileAnalysis _doAnalyzeLogs( String logFilename ) throws IOException, TransformerException, TransformerConfigurationException, ParserConfigurationException { // load the log file Command cmd = loadScenario( logFilename ) ; // extract events ArrayList events = new ArrayList() ; findLogFileEvents( cmd, events ) ; // extract the scenario details String scenarioName="", scenarioId="" ; String[] players = new String[2] ; Map ourLabels = new HashMap() ; ArrayList otherLabels = new ArrayList() ; AppBoolean hasPlayerOwnedLabels = new AppBoolean( false ) ; extractLabels( cmd, players, hasPlayerOwnedLabels, ourLabels, otherLabels, false ) ; GamePieceLabelFields labelFields = ourLabels.get( "scenario" ) ; if ( labelFields != null ) { Matcher matcher = Pattern.compile( "(.*?)" ).matcher( labelFields.getLabelContent() ) ; if ( matcher.find() ) scenarioName = matcher.group( 1 ).trim() ; matcher = Pattern.compile( "(.*?)" ).matcher( labelFields.getLabelContent() ) ; if ( matcher.find() ) { scenarioId = matcher.group( 1 ).trim() ; if ( scenarioId.length() > 0 && scenarioId.charAt(0) == '(' && scenarioId.charAt(scenarioId.length()-1) == ')' ) scenarioId = scenarioId.substring( 1, scenarioId.length()-1 ) ; } } return new LogFileAnalysis( logFilename, scenarioName, scenarioId, events ) ; } private void findLogFileEvents( Command cmd, ArrayList events ) { // NOTE: VASSAL doesn't store die/dice rolls and Turn Track rotations as specific events in the log file, // but as plain-text messages in the chat window (which sorta makes sense, since VASSAL won't really understand // these things, since they are specific to the game module). // initialize Pattern diceRollEventPattern = Pattern.compile( config.getProperty( "LFA_PATTERN_DICE_ROLL", "^\\*\\*\\* \\((?.+?) (?DR|dr)\\) (?.+?) \\*\\*\\*\\s+\\<(?.+?)\\>" ) ) ; Pattern diceRoll3EventPattern = Pattern.compile( config.getProperty( "LFA_PATTERN_DICE3_ROLL", "^\\*\\*\\* 3d6 = (?\\d),(?\\d),(?\\d) \\*\\*\\*\\s+\\<(?.+?)\\>" ) ) ; Pattern turnTrackEventPattern = Pattern.compile( config.getProperty( "LFA_PATTERN_TURN_TRACK", "^\\* (?!Phase Wheel)(New: )?(?.+?) Turn (?\\d+) - (?\\S+)" ) ) ; Pattern customLabelEventPattern = Pattern.compile( config.getProperty( "LFA_PATTERN_CUSTOM_LABEL", "!!vt-label (?