package vassal_shim ; import java.io.File ; import java.io.FileInputStream ; import java.io.InputStream ; import java.io.InputStreamReader ; import java.io.FileOutputStream ; import java.io.OutputStream ; 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 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.GameComponent ; import VASSAL.build.module.ModuleExtension ; import VASSAL.build.module.ObscurableOptions ; import VASSAL.build.module.metadata.SaveMetaData ; import VASSAL.build.widget.PieceSlot ; import VASSAL.launch.BasicModule ; import VASSAL.command.Command ; import VASSAL.command.AddPiece ; import VASSAL.command.RemovePiece ; import VASSAL.command.ConditionalCommand ; import VASSAL.command.AlertCommand ; 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.tools.io.FileArchive ; import VASSAL.tools.io.IOUtils ; import VASSAL.tools.io.FastByteArrayOutputStream ; import VASSAL.tools.io.ObfuscatingOutputStream ; import VASSAL.tools.io.ZipArchive ; import VASSAL.i18n.Resources ; import vassal_shim.Snippet ; import vassal_shim.GamePieceLabelFields ; import vassal_shim.LabelArea ; import vassal_shim.ReportNode ; import vassal_shim.AnalyzeNode ; import vassal_shim.ModuleManagerMenuManager ; import vassal_shim.AppBoolean ; import vassal_shim.Utils ; // -------------------------------------------------------------------- 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 analyzeScenario( String scenarioFilename, String reportFilename ) throws IOException, ParserConfigurationException, TransformerConfigurationException, TransformerException { // load the scenario configureBoards() ; Command cmd = loadScenario( scenarioFilename ) ; cmd.execute() ; // analyze the scenario logger.info( "Analyzing scenario: " + scenarioFilename ) ; HashMap results = new HashMap() ; for ( GamePiece gamePiece: GameModule.getGameModule().getGameState().getAllPieces() ) { if ( gamePiece.getProperty(VASSAL.counters.Properties.OBSCURED_BY) != null || gamePiece.getProperty(VASSAL.counters.Properties.HIDDEN_BY) != null ) { // IMPORTANT: VASSAL blanks out the name of concealed/HIP pieces if they don't belong to the calling user, // but we still get the GPID, which is enough for the main program to figure out which entry to create. // This means that people could use this feature to analyze a scenario in progess, to figure out // what their opponent's concealed/hidden OB is. To avoid this, we exclude these pieces from the report. continue ; } // see if this piece has a GPID GamePiece gp = Decorator.getInnermost( gamePiece ) ; if ( !( gp instanceof BasicPiece ) ) continue ; // yup - check if it's one we're interested in String gpid = ((BasicPiece)gp).getGpId() ; if ( gpid.equals( "" ) || gpid.equals( labelGpid ) ) continue ; // yup - add it to the results if ( ! results.containsKey( gpid ) ) { logger.debug( "Found new GPID " + gpid + ": " + gamePiece.getName() ) ; results.put( gpid, new AnalyzeNode( gamePiece.getName() ) ) ; } else { int newCount = results.get( gpid ).incrementCount() ; logger.debug( "Updating count for GPID " + gpid + ": #=" + newCount ) ; } } // generate the report Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument() ; Element rootElem = doc.createElement( "analyzeReport" ) ; doc.appendChild( rootElem ) ; for ( String gpid: results.keySet() ) { AnalyzeNode node = results.get( gpid ) ; Element elem = doc.createElement( "piece" ) ; elem.setAttribute( "gpid", gpid ) ; elem.setAttribute( "name", node.name ) ; elem.setAttribute( "count", Integer.toString( node.count ) ) ; rootElem.appendChild( elem ) ; } // save the report Utils.saveXml( doc, reportFilename ) ; } public void updateScenario( String scenarioFilename, String snippetsFilename, String saveFilename, String reportFilename ) throws IOException, ParserConfigurationException, SAXException, XPathExpressionException, TransformerException { // load the snippets supplied to us by the web server String[] players = new String[2] ; Map snippets = new HashMap() ; parseSnippets( snippetsFilename, players, snippets ) ; // load the scenario configureBoards() ; Command cmd = loadScenario( scenarioFilename ) ; // NOTE: The call to execute() is what's causing the VASSAL UI to appear on-screen. If we take it out, // label creation still works, but any boards and existing labels are not detected, presumably because // their Command's need to be executed to take effect. cmd.execute() ; // extract the labels from the scenario Map ourLabels = new HashMap() ; ArrayList otherLabels = new ArrayList() ; logger.info( "Searching the VASL scenario for labels (players={};{})...", players[0], players[1] ) ; AppBoolean hasPlayerOwnedLabels = new AppBoolean( false ) ; extractLabels( cmd, players, hasPlayerOwnedLabels, ourLabels, otherLabels ) ; // NOTE: vasl-templates v1.2 started tagging labels with their owning player e.g. "germans/ob_setup_1.1". // This is so that we can ignore labels owned by nationalities not directly involved in the scenario. // For example, if it's Germans vs. Americans, the Americans might have borrowed some British tanks, // and so the save file might contain British labels (for the setup instructions and Chapter H notes). // If we updated such a scenario, the old code would delete the British labels, since it couldn't tell // the difference between a British "ob_setup_1.1" label and an American one. But now labels are tagged // with their nationality, we can process only German and American labels, and ignore the British ones. // However, if don't see any of these new-style labels, we must be updating an older save file, and so // we want to retain the old behavior, which means we need to revert the new-style snippet ID's back // into the old format. if ( ! hasPlayerOwnedLabels.getVal() ) { logger.debug( "Converting snippet ID's to legacy format:" ) ; // locate new-style snippet ID's ArrayList< String[] > snippetIdsToReplace = new ArrayList< String[] >() ; Iterator< Map.Entry > iter2 = snippets.entrySet().iterator() ; while( iter2.hasNext() ) { String snippetId = iter2.next().getKey() ; int pos = snippetId.indexOf( "/" ) ; if ( pos >= 0 ) snippetIdsToReplace.add( new String[]{ snippetId, snippetId.substring(pos+1) } ) ; } // replace the new-style snippet ID's with their old-style version for ( int i=0 ; i < snippetIdsToReplace.size() ; ++i ) { String[] snippetIds = snippetIdsToReplace.get( i ) ; logger.debug( "- {} => {}", snippetIds[0], snippetIds[1] ) ; snippets.put( snippetIds[1], snippets.get(snippetIds[0]) ) ; snippets.remove( snippetIds[0] ) ; } } // update the labels from the snippets Map< String, ArrayList > labelReport = processSnippets( ourLabels, otherLabels, snippets ) ; // save the scenario saveScenario( saveFilename ) ; // generate the report generateLabelReport( labelReport, reportFilename ) ; // NOTE: The test suite always dumps the scenario after updating it, so we could save a lot of time // by dumping it here, thus avoiding the need to run this shim again to do the dump (and spinning up // a JVM, initializing VASSAL/VASL, etc.) but it's probably worth doing things the slow way, to avoid // any possible problems caused by reusing the current session (e.g. there might be some saved state somewhere). } private void parseSnippets( String snippetsFilename, String[] players, Map snippets ) throws IOException, ParserConfigurationException, SAXException, XPathExpressionException { logger.info( "Loading snippets: {}", snippetsFilename ) ; // load the XML DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance() ; DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder() ; Document doc = docBuilder.parse( new File( snippetsFilename ) ) ; doc.getDocumentElement().normalize() ; // get the player details NodeList nodes = doc.getElementsByTagName( "player1" ) ; players[0] = ((Element)nodes.item(0)).getAttribute( "nat" ) ; nodes = doc.getElementsByTagName( "player2" ) ; players[1] = ((Element)nodes.item(0)).getAttribute( "nat" ) ; // load the snippets nodes = doc.getElementsByTagName( "snippet" ) ; for ( int i=0 ; i < nodes.getLength() ; ++i ) { Node node = nodes.item( i ) ; if ( node.getNodeType() != Node.ELEMENT_NODE ) continue ; Snippet snippet = new Snippet( (Element)node, config ) ; logger.debug( "- Added snippet '{}' [{}x{}] (labelArea={}) (autoCreate={}):\n{}", snippet.snippetId, snippet.width, snippet.height, snippet.labelArea, snippet.autoCreate, snippet.content ) ; snippets.put( snippet.snippetId, snippet ) ; } } private void extractLabels( Command cmd, String[] players, AppBoolean hasPlayerOwnedLabels, Map ourLabels, ArrayList otherLabels ) { // check if this command is a label we're interested in // NOTE: We shouldn't really be looking at the object type, see analyzeScenario(). // http://www.gamesquad.com/forums/index.php?threads/new-program-to-help-set-up-vasl-scenarios.148281/post-1983751 if ( cmd instanceof AddPiece ) { AddPiece addPieceCmd = (AddPiece) cmd ; GamePiece target = addPieceCmd.getTarget() ; GamePiece gamePiece = Decorator.getInnermost( target ) ; if ( gamePiece.getName().equals( "User-Labeled" ) ) { // yup - parse the label content ArrayList separators = new ArrayList() ; ArrayList fields = new ArrayList() ; parseGamePieceState( target.getState(), separators, fields ) ; // check if the label is one of ours String snippetId = isVaslTemplatesLabel( fields, GamePieceLabelFields.FIELD_INDEX_LABEL1 ) ; int labelNo=1, fieldIndex=GamePieceLabelFields.FIELD_INDEX_LABEL1 ; if ( snippetId == null ) { snippetId = isVaslTemplatesLabel( fields, GamePieceLabelFields.FIELD_INDEX_LABEL2 ) ; labelNo = 2 ; fieldIndex = GamePieceLabelFields.FIELD_INDEX_LABEL2 ; } if ( snippetId != null ) { boolean addSnippet = true ; // check if the label is associated with a player nationality int pos = snippetId.indexOf( '/' ) ; if ( pos >= 0 ) { // yup - the nationality must be one of the 2 passed in to us // FUDGE! We identify player-owned labels because they have an ID of the form "nationality/snippet-id". // We originally used to just check for the presence of a "/", but this would get tripped up by snippets // generated from an "extras" template, since the snippet ID is the relative path, so something like // "extras/blank-space" would fool us into thinking that the scenario contained player-owned labels. // Adding a simple check for "extras" is not quite right, since template packs allow their template files // to be organized into arbitrary sub-directories, and so their snippets will also be incorrectly identified // as a player-owned label, but it's not really a problem because the reason we're checking is to figure out // if the scenario was generated using an old version of vasl-templates that didn't have player-owned labels. // The only time this check will go wrong is if: // - this scenario was created using an old version of vasl-templates that doesn't support player-owned labels // - the user had used their own template pack that had a sub-directory called something other than "extras". // IOW, not something we really need to worry about. The webapp server could pass in a list of known nationalities, // but that'd be more trouble that it's worth, since this is only an issue for legacy save files. // NOTE: If we've got a scenario that was created using a later version of vasl-templates, and it contains a snippet // generated from a template file in a sub-directory, then yes, that snippet might cause us to "incorrectly" decide // that the scenario contains player-owned labels, but it doesn't matter, because it's still the correct answer :-) String nat = snippetId.substring( 0, pos ) ; if ( ! nat.equals( "extras" ) ) hasPlayerOwnedLabels.setVal( true ) ; if ( ! nat.equals( players[0] ) && ! nat.equals( players[1] ) ) { addSnippet = false ; logger.debug( "- Skipping label: {} (owner={})", snippetId, nat ) ; } } if ( addSnippet ) { logger.debug( "- Found label (" + labelNo + "): {}", snippetId ) ; ourLabels.put( snippetId, new GamePieceLabelFields( target, separators, fields, fieldIndex ) ) ; } } else { otherLabels.add( new GamePieceLabelFields( target, separators, fields, -1 ) ) ; } } } // extract labels in sub-commands for ( Command c: cmd.getSubCommands() ) extractLabels( c, players, hasPlayerOwnedLabels, ourLabels, otherLabels ) ; } private String isVaslTemplatesLabel( ArrayList fields, int fieldIndex ) { // check if a label is one of ours if ( fieldIndex >= fields.size() ) return null ; Matcher matcher = Pattern.compile( "" comment. However, for labels created with older versions of vasl-templates, // this comment won't be present, so we try to match labels based on the raw content the user entered // in the UI of the main program. // NOTE: Since we are dealing with labels that don't have a snippet ID, the GamePieceLabelField's won't have // their fieldIndex set. We set this if and when we match a legacy label, but we don't handle the case // where some phrases are found in label1 and some in label2 :-/ It doesn't really matter which one we use, // since one of the fields will be used to store the snippet, and the other one will be blanked out. int fieldIndex = -1 ; // check each label and record which ones match the snippets's raw content ArrayList matches = new ArrayList() ; for ( GamePieceLabelFields labelFields: otherLabels ) { // check if all the snippet raw content phrases are present in the label if ( snippet.rawContent.size() == 0 ) { // nb: we can get here for snippets that are always passed through, even if they have no content continue ; } boolean allFound = true ; for ( String phrase: snippet.rawContent ) { phrase = phrase.replace( "\n", " " ) ; String labelContent = labelFields.getLabelContent( GamePieceLabelFields.FIELD_INDEX_LABEL1 ) ; if ( labelContent != null && labelContent.indexOf( phrase ) >= 0 ) { fieldIndex = GamePieceLabelFields.FIELD_INDEX_LABEL1 ; continue ; } labelContent = labelFields.getLabelContent( GamePieceLabelFields.FIELD_INDEX_LABEL2 ) ; if ( labelContent != null && labelContent.indexOf( phrase ) >= 0 ) { fieldIndex = GamePieceLabelFields.FIELD_INDEX_LABEL2 ; continue ; } allFound = false ; break ; } // yup - all phrases were found, record the label as a match if ( allFound ) matches.add( labelFields ) ; } // NOTE: Exactly one label must match for us to consider it a match (i.e. if there are // multiple matches, we do nothing and leave it to the user to sort it out). if ( matches.size() == 1 ) { GamePieceLabelFields labelFields = matches.get( 0 ) ; labelFields.setFieldIndex( fieldIndex ) ; return labelFields ; } return null ; } private void generateLabelReport( Map> labelReport, String reportFilename ) throws TransformerException, TransformerConfigurationException, ParserConfigurationException, IOException { // generate the report Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument() ; Element rootElem = doc.createElement( "report" ) ; doc.appendChild( rootElem ) ; boolean wasModified = false ; for ( String key: labelReport.keySet() ) { ArrayList reportNodes = labelReport.get( key ) ; Element elem = doc.createElement( key ) ; for ( ReportNode reportNode: reportNodes ) { Element reportNodeElem = doc.createElement( "label" ) ; reportNodeElem.setAttribute( "id", reportNode.snippetId ) ; if ( reportNode.labelPos != null ) { reportNodeElem.setAttribute( "x", Integer.toString( reportNode.labelPos.x ) ) ; reportNodeElem.setAttribute( "y", Integer.toString( reportNode.labelPos.y ) ) ; } if ( reportNode.caption != null ) { reportNodeElem.setAttribute( "caption", reportNode.caption ) ; if ( reportNode.msg != null ) reportNodeElem.setTextContent( reportNode.msg ) ; } elem.appendChild( reportNodeElem ) ; if ( ! key.equals( "unchanged" ) ) wasModified = true ; } rootElem.appendChild( elem ) ; } rootElem.setAttribute( "wasModified", wasModified?"true":"false" ) ; // save the report Utils.saveXml( doc, reportFilename ) ; } private VASSAL.build.module.Map selectMap() { // NOTE: VASL 6.5.0 introduced a new map ("Casualties") as part of the new Casualties Bin feature, // and also renamed the default map ("Main Map"). List vaslMaps = VASSAL.build.module.Map.getMapList() ; List otherMaps = new ArrayList() ; for ( int i=0 ; i < vaslMaps.size() ; ++i ) { VASSAL.build.module.Map map = vaslMaps.get( i ) ; if ( map.getMapName().equals( "Main Map" ) ) return map ; // nb: we always prefer this map if ( map.getMapName().equals( "Casualties" ) ) continue ; // nb: we ignore this map otherMaps.add( map ) ; } if ( otherMaps.size() == 0 ) { logger.warn( "WARNING: Couldn't find any maps!" ) ; return null ; } logger.warn( "WARNING: Couldn't find the main map, using the first alternate." ) ; return otherMaps.get( 0 ) ; } private String makeVassalCoordString( Point pos, Snippet snippet ) { // FUDGE! VASSAL positions labels by the X/Y co-ords of the label's centre (!) return Integer.toString( pos.x + snippet.width/2 ) + ";" + Integer.toString( pos.y + snippet.height/2 ) ; } private void saveScenario( String saveFilename ) throws IOException { // disable the dialog asking for log file comments Prefs prefs = GameModule.getGameModule().getPrefs() ; String PROMPT_LOG_COMMENT = "promptLogComment"; prefs.setValue( PROMPT_LOG_COMMENT, false ) ; // FUDGE! We would like to just call GameState.saveGame(), but it calls getRestoreCommand(), // which does nothing unless the "save game" menu action has been enabled!?! Due to Java protections, // there doesn't seem to be any way to get at this object and enable it, so we have to re-implement // the whole saveGame() code without this check :-/ // get the save string Command cmd = getRestoreCommand() ; String saveString = GameModule.getGameModule().encode( cmd ) ; // save the scenario logger.info( "Saving scenario: {}", saveFilename ) ; final FastByteArrayOutputStream ba = new FastByteArrayOutputStream() ; OutputStream out = null ; try { out = new ObfuscatingOutputStream( ba ) ; out.write( saveString.getBytes( "UTF-8" ) ) ; out.close() ; } finally { IOUtils.closeQuietly( out ) ; } FileArchive archive = null ; try { archive = new ZipArchive( new File( saveFilename ) ) ; String SAVEFILE_ZIP_ENTRY = "savedGame" ; //$NON-NLS-1$ archive.add( SAVEFILE_ZIP_ENTRY, ba.toInputStream() ) ; (new SaveMetaData()).save( archive ) ; archive.close() ; } finally { IOUtils.closeQuietly( archive ) ; } } private static Command getRestoreCommand() // nb: taken from GameState.getRestoreCommand() { // NOTE: This is the check that's causing the problem :-/ // if (!saveGame.isEnabled()) { // return null; // } GameState gameState = GameModule.getGameModule().getGameState() ; Command c = new GameState.SetupCommand(false); c.append(checkVersionCommand()); c.append( gameState.getRestorePiecesCommand() ); for (GameComponent gc : gameState.getGameComponents()) { c.append(gc.getRestoreCommand()); } c.append(new GameState.SetupCommand(true)); return c; } private static Command checkVersionCommand() { // NOTE: This is the same as GameState.checkVersionCommand(), but we can't call that since it's private :-/ String runningVersion = GameModule.getGameModule().getAttributeValueString(GameModule.VASSAL_VERSION_RUNNING); ConditionalCommand.Condition cond = new ConditionalCommand.Lt(GameModule.VASSAL_VERSION_RUNNING, runningVersion); Command c = new ConditionalCommand(new ConditionalCommand.Condition[]{cond}, new AlertCommand(Resources.getString("GameState.version_mismatch", runningVersion))); //$NON-NLS-1$ String moduleName = GameModule.getGameModule().getAttributeValueString(GameModule.MODULE_NAME); String moduleVersion = GameModule.getGameModule().getAttributeValueString(GameModule.MODULE_VERSION); cond = new ConditionalCommand.Lt(GameModule.MODULE_VERSION, moduleVersion); c.append(new ConditionalCommand(new ConditionalCommand.Condition[]{cond}, new AlertCommand(Resources.getString("GameState.version_mismatch2", moduleName, moduleVersion )))); //$NON-NLS-1$ return c; } private Command loadScenario( String scenarioFilename ) throws IOException { // load the scenario disableBoardWarnings() ; logger.info( "Loading scenario: {}", scenarioFilename ) ; return GameModule.getGameModule().getGameState().decodeSavedGame( new File( scenarioFilename ) ) ; } private static void dumpCommand( Command cmd, String prefix ) { // dump the command StringBuilder buf = new StringBuilder() ; buf.append( prefix + cmd.getClass().getSimpleName() ) ; String details = cmd.getDetails() ; if ( details != null ) buf.append( " [" + details + "]" ) ; if ( cmd instanceof AddPiece ) dumpCommandExtras( (AddPiece)cmd, buf, prefix ) ; else if ( cmd instanceof GameState.SetupCommand ) dumpCommandExtras( (GameState.SetupCommand)cmd, buf, prefix ) ; else if ( cmd instanceof ModuleExtension.RegCmd ) dumpCommandExtras( (ModuleExtension.RegCmd)cmd, buf, prefix ) ; else if ( cmd instanceof ObscurableOptions.SetAllowed ) dumpCommandExtras( (ObscurableOptions.SetAllowed)cmd, buf, prefix ) ; System.out.println( buf.toString() ) ; // dump any sub-commands prefix += " " ; for ( Command c: cmd.getSubCommands() ) dumpCommand( c, prefix ) ; } private static void dumpCommandExtras( AddPiece cmd, StringBuilder buf, String prefix ) { // dump extra command info GamePiece target = cmd.getTarget() ; buf.append( ": " + target.getClass().getSimpleName() ) ; if ( target.getName().length() > 0 ) buf.append( "/" + target.getName() ) ; // check if this is a command we're interested in // NOTE: We used to support VASL 6.3.3, but when we create labels, they're of type Hideable. It would be easy enough // to add that here, but 6.3.3 is pretty old (2.5 years), so it's safer to just drop it from the list of supported versions. if ( !( target instanceof DynamicProperty ) ) return ; if ( ! target.getName().equals( "User-Labeled" ) ) return ; // dump extra command info ArrayList separators = new ArrayList() ; ArrayList fields = new ArrayList() ; parseGamePieceState( cmd.getState(), separators, fields ) ; for ( String field: fields ) { buf.append( "\n" + prefix + "- " ) ; if ( field.length() > 0 ) buf.append( Utils.printableString( field ) ) ; else buf.append( "" ) ; } } private static void dumpCommandExtras( GameState.SetupCommand cmd, StringBuilder buf, String prefix ) { // dump extra command info buf.append( ": starting=" + cmd.isGameStarting() ) ; } private static void dumpCommandExtras( ModuleExtension.RegCmd cmd, StringBuilder buf, String prefix ) { // dump extra command info buf.append( ": " + cmd.getName() + " (" + cmd.getVersion() + ")" ) ; } private static void dumpCommandExtras( ObscurableOptions.SetAllowed cmd, StringBuilder buf, String prefix ) { // dump extra command info buf.append( ": " + cmd.getAllowedIds() ) ; } private static void parseGamePieceState( String state, ArrayList separators, ArrayList fields ) { // parse the GamePiece state Matcher matcher = Pattern.compile( "\\\\+\t" ).matcher( state ) ; int pos = 0 ; while( matcher.find() ) { separators.add( matcher.group() ) ; fields.add( state.substring( pos, matcher.start() ) ) ; pos = matcher.end() ; } fields.add( state.substring( pos ) ) ; } private void configureBoards() { // NOTE: While we can get away with just disabling warnings about missing boards when dumping scenarios, // they need to be present when we update a scenario, otherwise they get removed from the scenario :-/ logger.info( "Configuring boards directory: {}", boardsDir ) ; Prefs prefs = GameModule.getGameModule().getPrefs() ; String BOARD_DIR = "boardURL" ; prefs.setValue( BOARD_DIR, new File(boardsDir) ) ; } private void disableBoardWarnings() { // FUDGE! VASSAL shows a GUI error dialog warning about boards not being found, and while these can be disabled, // the key used to enable/disable them is derived from the board filename :-( ASLBoardPicker catches // the FileNotFoundException thrown by ZipArchive when it can't find a file, and then calls ReadErrorDialog.error(), // which calls WarningDialog.showDisableable(), using the following as the key: // (Object) ( e.getClass().getName() + "@" + filename ) // This means we have to set the "warning disabled" flag for every possible board :-/ // disable warnings for boards 00-99 logger.info( "Disabling board warnings for bd00-99." ) ; for ( int i=0 ; i < 100 ; ++i ) disableBoardWarning( String.format( "bd%02d", i ) ) ; // disable warnings for additional standard boards logger.info( "Disabling board warnings for other standard boards:" ) ; InputStream inputStream = this.getClass().getResourceAsStream( "/data/boardNames.txt" ) ; disableBoardWarnings( inputStream, "" ) ; // disable warnings for user-defined boards if ( baseDir != null ) { String fname = baseDir + File.separator + "boardNames.txt" ; inputStream = null ; try { inputStream = new FileInputStream( fname ) ; } catch( FileNotFoundException ex ) { } if ( inputStream != null ) { logger.info( "Disabling board warnings for user-defined boards: " + fname ) ; disableBoardWarnings( inputStream, fname ) ; } } } private void disableBoardWarnings( InputStream inputStream, String boardFilename ) { // disable warnings for boards listed in a file BufferedReader reader = new BufferedReader( new InputStreamReader( inputStream ) ) ; String lineBuf ; try { while ( (lineBuf = reader.readLine() ) != null ) { lineBuf = lineBuf.trim() ; if ( lineBuf.length() == 0 || lineBuf.charAt(0) == '#' || lineBuf.charAt(0) == ';' || lineBuf.substring(0,2).equals("//") ) continue ; logger.debug( "- {}", lineBuf ) ; disableBoardWarning( lineBuf ) ; } } catch( IOException ex ) { logger.error( "Error reading board file: {}", boardFilename, ex ) ; } } private void disableBoardWarning( String boardName ) { // disable warnings for the specified board String boardsPath = (new File(vmodFilename)).getParent() + File.separator + "boards" ; String key = "java.io.FileNotFoundException@" + boardsPath + File.separator + boardName ; DialogUtils.setDisabled( key, true ) ; } }