package rabbitescape.engine.textworld; import static rabbitescape.engine.Block.Material.EARTH; import static rabbitescape.engine.Block.Material.METAL; import static rabbitescape.engine.Block.Shape.BRIDGE_UP_LEFT; import static rabbitescape.engine.Block.Shape.BRIDGE_UP_RIGHT; import static rabbitescape.engine.Block.Shape.FLAT; import static rabbitescape.engine.Block.Shape.UP_LEFT; import static rabbitescape.engine.Block.Shape.UP_RIGHT; import static rabbitescape.engine.Direction.LEFT; import static rabbitescape.engine.Direction.RIGHT; import static rabbitescape.engine.util.Util.asChars; import static rabbitescape.engine.util.Util.split; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import rabbitescape.engine.Block; import rabbitescape.engine.Entrance; import rabbitescape.engine.Exit; import rabbitescape.engine.Fire; import rabbitescape.engine.Pipe; import rabbitescape.engine.Rabbit; import rabbitescape.engine.Thing; import rabbitescape.engine.Token; import rabbitescape.engine.VoidMarkerStyle; import rabbitescape.engine.util.Dimension; import rabbitescape.engine.util.MegaCoder; import rabbitescape.engine.util.Position; import rabbitescape.engine.util.VariantGenerator; import rabbitescape.engine.util.WaterUtil; public class LineProcessor { public static final String CODE_SUFFIX = ".code"; public static class KeyListKey { public final String prefix; public final int number; public KeyListKey( String prefix, int number ) { this.prefix = prefix; this.number = number; } @Override public String toString() { return "KeyListKey( " + prefix + ", " + number + " )"; } @Override public int hashCode() { return number + 31 * ( number + prefix.hashCode() ); } @Override public boolean equals( Object otherObj ) { if ( ! ( otherObj instanceof KeyListKey ) ) { return false; } KeyListKey other = (KeyListKey)otherObj; return ( number == other.number && prefix.equals( other.prefix ) ); } } static final Pattern keyListKeyRegex = Pattern.compile( "(.*)\\.(\\d{1,3})" ); private final List<Block> blocks; private final List<Rabbit> rabbits; private final List<Thing> things; private final Map<Position, Integer> waterAmounts; private final Map<Token.Type, Integer> abilities; public final String[] lines; private final Map<String, String> m_metaStrings; private final Map<String, Map<Integer, String>> m_metaStringArraysByKey; private final Map<String, Integer> m_metaInts; private final Map<String, Boolean> m_metaBools; private final Map<String, ArrayList<Integer>> m_metaIntArrays; private final List<Position> starPoints; private final List<Comment> comments; private int width; private int height; public int lineNum; private int currentStarPoint; public LineProcessor( List<Block> blocks, List<Rabbit> rabbits, List<Thing> things, Map<Position, Integer> waterAmounts, Map<Token.Type, Integer> abilities, String[] lines, VariantGenerator variantGen ) { this.blocks = blocks; this.rabbits = rabbits; this.things = things; this.waterAmounts = waterAmounts; this.abilities = abilities; this.lines = lines; this.m_metaStrings = new HashMap<>(); this.m_metaStringArraysByKey = new HashMap<>(); this.m_metaInts = new HashMap<>(); this.m_metaBools = new HashMap<>(); this.m_metaIntArrays = new HashMap<>(); starPoints = new ArrayList<Position>(); this.comments = new ArrayList<Comment>(); width = -1; height = 0; lineNum = 0; currentStarPoint = 0; process( variantGen ); } public Comment[] getComments() { return comments.toArray( new Comment[comments.size()] ); } public String metaString( String key, String def ) { String ret = m_metaStrings.get( key ); if ( ret == null ) { return def; } else { return ret; } } public String[] metaStringArrayByKey( String key, String[] def ) { Map<Integer, String> temp = m_metaStringArraysByKey.get( key ); if ( temp == null ) { return def; } else { String[] ret = new String[temp.size()]; for ( int i = 1 ; i <= temp.size() ; i++ ) { String v = temp.get( i ); if ( null == v ) { throw new RuntimeException( "temp should have 1, 2, ..., temp.size() members." ); } ret[i - 1] = v; } return ret; } } public int metaInt( String key, int def ) { Integer ret = m_metaInts.get( key ); if ( ret == null ) { return def; } else { return ret; } } public int[] metaIntArray( String key, int[] def ) { ArrayList<Integer> temp = m_metaIntArrays.get( key ); if ( temp == null ) { return def; } int[] ret = new int[temp.size()]; for ( int i = 0; i < temp.size(); i++ ) { ret[i] = temp.get( i ); } return ret; } public boolean metaBool( String key, boolean def ) { Boolean ret = m_metaBools.get( key ); if ( ret == null ) { return def; } else { return ret; } } public Dimension size() { return new Dimension( width, height ); } private void process( VariantGenerator variantGen ) { for ( String line : lines ) { if ( line.startsWith( ":" ) ) { processMetaLine( line, variantGen ); } else if ( line.startsWith( "%" ) ) { processCommentLine( line ); } else { processItemsLine( line, variantGen ); } ++lineNum; } if ( starPoints.size() > currentStarPoint ) { throw new TooManyStars( lines ); } } private void duplicateMetaCheck( Set<String> set, String key ) { if ( set.contains( key ) ) { throw new DuplicateMetaKey( lines, lineNum ); } } private void processCommentLine( String line ) { // Create temporary comment, until we know the line following, // to create the association. Comment c = Comment.createUnlinkedComment( line ); comments.add( c ); } private void maybeLinkToLastComment( String key ) { if ( comments.size() == 0 ) { return; // No comments to link. } // Iterate backwards linking all comments in a block // until we hit one that it already linked with the // previous non-comment line for ( int i = comments.size() - 1; i >= 0 ; i--) { Comment c = comments.get( i ); if( c.isUnlinked() ) { comments.set( i, c.link( key) ); } else { return; } } } /** * Strips the code suffix, if it is present, or returns the key unchanged. */ public static String stripCodeSuffix( String key ) { if ( key.endsWith( CODE_SUFFIX ) ) { key = key.substring( 0, key.length() - CODE_SUFFIX.length() ); } return key; } private void processMetaLine( String line, VariantGenerator variantGen ) { String[] splitLine = split( line.substring( 1 ), "=", 1 ); if ( splitLine.length != 2 ) { throw new InvalidMetaLine( lines, lineNum ); } String key = splitLine[0]; String value = splitLine[1]; if ( !key.equals( key = stripCodeSuffix( key ) ) ) { value = MegaCoder.decode( value ); } maybeLinkToLastComment( key ); if ( TextWorldManip.META_INTS.contains( key ) ) { duplicateMetaCheck( m_metaInts.keySet(), key ); m_metaInts.put( key, toInt( value ) ); } else if ( TextWorldManip.META_STRINGS.contains( key ) ) { duplicateMetaCheck( m_metaStrings.keySet(), key ); m_metaStrings.put( key, value ); } else if ( TextWorldManip.META_BOOLS.contains( key ) ) { duplicateMetaCheck( m_metaBools.keySet(), key ); m_metaBools.put( key, toBool( value ) ); } else if ( TextWorldManip.META_INT_ARRAYS.contains( key ) ) { duplicateMetaCheck( m_metaIntArrays.keySet(), key ); m_metaIntArrays.put( key, toIntArray( value ) ); } else if ( matchesKeyList( TextWorldManip.META_STRING_ARRAYS_BY_KEY, key ) ) { KeyListKey listKey = parseKeyListKey( key ); Map<Integer, String> list = m_metaStringArraysByKey.get( listKey.prefix ); if ( list == null ) { list = new HashMap<Integer, String>(); m_metaStringArraysByKey.put( listKey.prefix, list ); } else if ( list.containsKey( listKey.number ) ) { throw new DuplicateMetaKey( lines, lineNum ); } if ( list.size() != listKey.number - 1 ) { throw new ArrayByKeyElementMissing( lines, lineNum ); } list.put( listKey.number, value ); } else if ( TextWorldManip.ABILITIES.contains( key ) ) { if ( abilities.keySet().contains( Token.Type.valueOf( key ) ) ) { throw new DuplicateMetaKey( lines, lineNum ); } abilities.put( Token.Type.valueOf( key ), toInt( value ) ); } else if ( key.equals( TextWorldManip.water_definition ) ) { String[] valueParts = split( value, "," ); if ( valueParts.length != 3 ) { throw new InvalidWaterDescription( lines, lineNum ); } int x = toInt( valueParts[0] ); int y = toInt( valueParts[1] ); int contents = toInt( valueParts[2] ); waterAmounts.put( new Position( x, y ), contents ); } else if ( key.equals( "*" ) ) { if ( currentStarPoint >= starPoints.size() ) { throw new NotEnoughStars( lines, lineNum ); } Position p = starPoints.get( currentStarPoint ); new ItemsLineProcessor( this, p.x, p.y, value ) .process( variantGen ); ++currentStarPoint; } else { throw new UnknownMetaKey( lines, lineNum ); } } private boolean matchesKeyList( List<String> keyList, String key ) { return keyList.contains( parseKeyListKey( key ).prefix ); } public static KeyListKey parseKeyListKey( String key ) { Matcher m = keyListKeyRegex.matcher( key ); if ( m.matches() ) { return new KeyListKey( m.group( 1 ), Integer.parseInt( m.group( 2 ) ) ); } else { return new KeyListKey( "NO KEY LIST MATCH", -1 ); } } private int toInt( String value ) { try { return Integer.valueOf( value ); } catch( NumberFormatException e ) { throw new NonIntegerMetaValue( lines, lineNum ); } } private ArrayList<Integer> toIntArray( String value ) { try { String[] items = value.split(","); ArrayList<Integer> ret = new ArrayList<Integer> ( items.length ); for ( int i=0; i<items.length; i++ ) { ret.add( i, new Integer( items[i] ) ); } return ret; } catch( NumberFormatException e ) { throw new NonIntegerMetaValue ( lines, lineNum ); } } private boolean toBool( String value ) { if ( value == null ) { throw new NullBooleanMetaValue( lines, lineNum ); } else if ( value.equals( "true" ) ) { return true; } else if ( value.equals( "false" ) ) { return false; } else { throw new NonBooleanMetaValue( lines, lineNum ); } } private void processItemsLine( String line, VariantGenerator variantGen ) { maybeLinkToLastComment( Comment.WORLD_ASCII_ART ); // Treat empty lines as blank lines (Github converts blank lines to empty lines, so it seems sensible to reverse the process). if ( line.length() != 0 ) { if ( width == -1 ) { width = line.length(); } else if ( line.length() != width ) { throw new WrongLineLength( lines, lineNum ); } int i = 0; for ( char ch : asChars( line ) ) { processChar( ch, i, height, variantGen ); ++i; } } ++height; } public Thing processChar( char c, int x, int y, VariantGenerator variantGen ) { Thing ret = null; switch( c ) { case ' ': { break; } case '#': { blocks.add( new Block( x, y, EARTH, FLAT, variantGen.next( 4 ) ) ); break; } case 'M': { blocks.add( new Block( x, y, METAL, FLAT, variantGen.next( 4 ) ) ); break; } case '/': { blocks.add( new Block( x, y, EARTH, UP_RIGHT, variantGen.next( 4 ) ) ); break; } case '\\': { blocks.add( new Block( x, y, EARTH, UP_LEFT, variantGen.next( 4 ) ) ); break; } case '(': { blocks.add( new Block( x, y, EARTH, BRIDGE_UP_RIGHT, 0 ) ); break; } case ')': { blocks.add( new Block( x, y, EARTH, BRIDGE_UP_LEFT, 0 ) ); break; } case 'r': { Rabbit r = new Rabbit( x, y, RIGHT ); ret = r; rabbits.add( r ); break; } case 'j': { Rabbit r = new Rabbit( x, y, LEFT );; ret = r; rabbits.add( r ); break; } case 'Q': { ret = new Entrance( x, y ); things.add( ret ); break; } case 'O': { ret = new Exit( x, y ); things.add( ret ); break; } case 'A': { ret = new Fire( x, y ); things.add( ret ); break; } case 'P': { ret = new Pipe( x, y ); things.add( ret ); break; } case 'b': { ret = new Token( x, y, Token.Type.bash ); things.add( ret ); break; } case 'd': { ret = new Token( x, y, Token.Type.dig ); things.add( ret ); break; } case 'i': { ret = new Token( x, y, Token.Type.bridge ); things.add( ret ); break; } case 'k': { ret = new Token( x, y, Token.Type.block ); things.add( ret ); break; } case 'c': { ret = new Token( x, y, Token.Type.climb ); things.add( ret ); break; } case 'p': { ret = new Token( x, y, Token.Type.explode ); things.add( ret ); break; } case 'l': { ret = new Token( x, y, Token.Type.brolly ); things.add( ret ); break; } case 'N': { // Default amount for a full water region, but may be overwritten by // an explicit water definition line. waterAmounts.put( new Position( x, y ), WaterUtil.MAX_CAPACITY ); break; } case 'n': { // Default amount for a half water region, but may be overwritten by // an explicit water definition line. waterAmounts.put( new Position( x, y ), WaterUtil.HALF_CAPACITY ); break; } case '*': { starPoints.add( new Position( x, y ) ); break; } default: { throw new UnknownCharacter( lines, lineNum, x ); } } return ret; } public VoidMarkerStyle.Style generateVoidMarkerStyle() { String marker = m_metaStrings.get( TextWorldManip.void_marker_style ); if ( marker == null ) { String name = m_metaStrings.get( TextWorldManip.name ); if ( name == null ) { // It is not a proper level, so this does not matter return VoidMarkerStyle.Style.HIGHLIGHTER; } // Generate a reproducible style from the level name int i = stringHash( name ) % VoidMarkerStyle.Style.values().length; return VoidMarkerStyle.Style.values()[i]; } try { VoidMarkerStyle.Style s = VoidMarkerStyle.Style.valueOf( marker.toUpperCase() ); return s; } catch ( IllegalArgumentException e ) { throw new UnknownVoidMarkerStyle( marker ); } } /** * Not a fancy hash, but the same string will always yield the * same number. Note that Object.hashCode results may vary each * time the JVM is started. */ public static int stringHash( String s ) { int hash = 0; for ( char c: s.toCharArray() ) { hash += (int)c; } return hash; } }