/* Copyright (c) 2008-2010, developers of the Ascension Log Visualizer * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom * the Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ package com.googlecode.logVisualizer.parser; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.Reader; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; /** * This class can read mafia session logs and return them to the caller in nice * and easier to handle chunks. */ public final class MafiaSessionLogReader { public static final Set<String> BROKEN_AREAS_ENCOUNTER_SET = Collections .unmodifiableSet(new HashSet<>(Arrays.asList( "Encounter: Big Wisniewski", "Encounter: The Big Wisniewski", "Encounter: The Man", "Encounter: Lord Spookyraven", "Encounter: Ed the Undying", "Encounter: The Infiltrationist", "Encounter: giant sandworm"))); private static final String ENCOUNTER_START_STRING = "Encounter: "; private static final String FAMILIAR_POUND_GAIN_END_STRING = "gains a pound!"; private static final String USE_STRING = "use"; private static final String EAT_STRING = "eat"; private static final String DRINK_STRING = "drink"; private static final String BUY_STRING = "Buy"; private static final String SNAPSHOT_START_END = "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-="; private static final String LEVEL_12_QUEST_BOSSFIGHT_BEGINNING_STRING = "bigisland.php?"; private final BufferedReader log; private boolean hasNext = true; /** * @param log * The condensed mafia session log that is supposed to be parsed. * @throws IOException * if there were issues with accessing the log */ MafiaSessionLogReader(final File log) throws IOException { if (!log.exists()) { throw new IllegalArgumentException("Log file must exist."); } if (log.isDirectory()) { throw new IllegalArgumentException( "Log file has to be a file, not a directory."); } this.log = new BufferedReader(new FileReader(log)); } /** * This method reads and returns the next block of text in the session log. * <p> * Currently, there are four possible versions of text blocks that can * recognised: * <li>Encounter blocks</li> * <li>Consumable blocks</li> * <li>Player snapshot blocks</li> * <li>Other blocks (for everything else that wouldn't fit in the above * categories)</li> * * @return The parsed out text block from the session log. * @throws IOException * if there were issues with reading the log; in certain * circumstances, if the was a line with more than 500 * characters * @throws IllegalStateException * if there is no more block to parse in the session log */ LogBlock next() throws IOException { final LogBlock block; this.log.mark(500); String line = this.log.readLine(); String line2 = this.log.readLine(); this.log.reset(); if (line == null) { throw new IllegalStateException( "There are no more blocks to be read."); } if (line2 == null) { line2 = UsefulPatterns.EMPTY_STRING; } if ((line.startsWith(UsefulPatterns.SQUARE_BRACKET_OPEN) && UsefulPatterns.TURNS_USED .matcher(line).matches()) || (line2 .startsWith(MafiaSessionLogReader.ENCOUNTER_START_STRING) && MafiaSessionLogReader.BROKEN_AREAS_ENCOUNTER_SET .contains(line2))) { block = new EncounterLogBlock(this.parseEncounterBlock()); } else if ((line.startsWith(MafiaSessionLogReader.USE_STRING) || line.startsWith(MafiaSessionLogReader.EAT_STRING) || line.startsWith(MafiaSessionLogReader.DRINK_STRING) || line .startsWith(MafiaSessionLogReader.BUY_STRING)) && UsefulPatterns.CONSUMABLE_USED.matcher(line).matches()) { block = new ConsumableLogBlock(this.parseNormalBlock()); } else if (line.equals(MafiaSessionLogReader.SNAPSHOT_START_END)) { block = new PlayerSnapshotLogBlock(this.parsePlayerSnapshotBlock()); } else { block = new OtherLogBlock(this.parseNormalBlock()); } // Skip empty lines and decide at the end whether the log is finished. do { this.log.mark(500); } while (((line = this.log.readLine()) != null) && (line.length() <= 0)); if (line == null) { this.hasNext = false; } else { this.log.reset(); } return block; } private List<String> parseEncounterBlock() throws IOException { final List<String> result = new ArrayList<>(); String line; while ((line = this.log.readLine()) != null) { /** * Mafia saves a familiar pound gain this way in older versions: * * <pre> * Round _NUMBER_: _FAMNAME_ gains a pound! * * familiar _FAMTYPE_ (_POUNDS_ lbs) * * </pre> * * This is problematic because empty lines will end the while loop * even though the combat rundown isn't over. Thus we attempt to * skip the above mentioned lines. */ if (line.endsWith(MafiaSessionLogReader.FAMILIAR_POUND_GAIN_END_STRING)) { // Remember current position. this.log.mark(500); // Check next line, if it is empty, the problematic logging is // occurring, otherwise reset back to the original position. final String tmpLine = this.log.readLine(); if (tmpLine.length() <= 0) { this.log.readLine(); this.log.readLine(); line = this.log.readLine(); if (line == null) { break; } } else { this.log.reset(); } } // If there is an empty line, it means the encounter is over. There // are cases were this is not true for combats however, because // sometimes mafia puts empty lines in which aren't actually // supposed to be there. Such "false" empty lines should be // attempted to be recognised and skipped. if (line.length() <= 0) { // Remember current position. this.log.mark(600); // Look-ahead of three lines to try and see whether the combat // is actually continued. boolean isFightContinued = false; for (int i = 0; i < 3; i++) { final String tmpLine = this.log.readLine(); // A square bracket means that a new turn was started. Extra // check for the level 12 quest bossfight. if ((tmpLine == null) || tmpLine .startsWith(UsefulPatterns.SQUARE_BRACKET_OPEN) || tmpLine .startsWith(MafiaSessionLogReader.LEVEL_12_QUEST_BOSSFIGHT_BEGINNING_STRING)) { break; } else if (tmpLine .startsWith(UsefulPatterns.COMBAT_ROUND_LINE_BEGINNING_STRING)) { isFightContinued = true; line = tmpLine; break; } } // If the fight has ended, set the reader back to the original // position and stop the while loop. if (!isFightContinued) { this.log.reset(); break; } } result.add(line); } if (line == null) { this.hasNext = false; } return result; } private List<String> parsePlayerSnapshotBlock() throws IOException { final List<String> result = new ArrayList<>(); String line; // Add first three lines of the snapshot without check, so that the end // of the snapshot is not prematurely recognised. result.add(this.log.readLine()); result.add(this.log.readLine()); result.add(this.log.readLine()); while (((line = this.log.readLine()) != null) && !line.equals(MafiaSessionLogReader.SNAPSHOT_START_END)) { result.add(line); } if (line == null) { this.hasNext = false; } return result; } private List<String> parseNormalBlock() throws IOException { final List<String> result = new ArrayList<>(); String line; while (((line = this.log.readLine()) != null) && (line.length() > 0)) { result.add(line); } if (line == null) { this.hasNext = false; } return result; } /** * Use this method to check whether {@link #next()} is still able to return * another {@link LogBlock}. * * @return True if there are still blocks left to parse in the session log. */ boolean hasNext() { return this.hasNext; } /** * Closes the {@link Reader} used to read the session log. */ void close() { // Calling close() on a reader should not actually throw an exception, // so we'll just catch it in here. try { this.log.close(); } catch (final IOException e) { e.printStackTrace(); } } /** * An enumeration of all the possible types that a {@link LogBlock} can * have. */ static enum LogBlockType { ENCOUNTER_BLOCK, CONSUMABLE_BLOCK, PLAYER_SNAPSHOT_BLOCK, OTHER_BLOCK; } /** * Implementations of this interface are container classes to hold the block * of text that was parsed by a {@link MafiaSessionLogReader} and link it * with a certain version of {@link LogBlockType}. */ static interface LogBlock { List<String> getBlockLines(); LogBlockType getBlockType(); } private static abstract class AbstractLogBlock implements LogBlock { private final List<String> blockLines; AbstractLogBlock(final List<String> blockLines) { this.blockLines = blockLines; } @Override public List<String> getBlockLines() { return Collections.unmodifiableList(this.blockLines); } } private static final class EncounterLogBlock extends AbstractLogBlock { EncounterLogBlock(final List<String> blockLines) { super(blockLines); } @Override public LogBlockType getBlockType() { return LogBlockType.ENCOUNTER_BLOCK; } } private static final class ConsumableLogBlock extends AbstractLogBlock { ConsumableLogBlock(final List<String> blockLines) { super(blockLines); } @Override public LogBlockType getBlockType() { return LogBlockType.CONSUMABLE_BLOCK; } } private static final class PlayerSnapshotLogBlock extends AbstractLogBlock { PlayerSnapshotLogBlock(final List<String> blockLines) { super(blockLines); } @Override public LogBlockType getBlockType() { return LogBlockType.PLAYER_SNAPSHOT_BLOCK; } } private static final class OtherLogBlock extends AbstractLogBlock { OtherLogBlock(final List<String> blockLines) { super(blockLines); } @Override public LogBlockType getBlockType() { return LogBlockType.OTHER_BLOCK; } } }