/* 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.mafiaLogBlockParsers; import java.io.BufferedReader; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Scanner; import java.util.Set; import java.util.regex.Pattern; import net.java.dev.spellcast.utilities.DataUtilities; import net.java.dev.spellcast.utilities.UtilityConstants; import com.googlecode.logVisualizer.logData.LogDataHolder; import com.googlecode.logVisualizer.logData.turn.SingleTurn; import com.googlecode.logVisualizer.logData.turn.SingleTurn.TurnVersion; import com.googlecode.logVisualizer.logData.turn.turnAction.EquipmentChange; import com.googlecode.logVisualizer.logData.turn.turnAction.FamiliarChange; import com.googlecode.logVisualizer.parser.LineParser; import com.googlecode.logVisualizer.parser.MafiaSessionLogReader; import com.googlecode.logVisualizer.parser.UsefulPatterns; import com.googlecode.logVisualizer.parser.lineParsers.CombatRecognizerLineParser; import com.googlecode.logVisualizer.parser.lineParsers.EquipmentLineParser; import com.googlecode.logVisualizer.parser.lineParsers.ItemAcquisitionLineParser; import com.googlecode.logVisualizer.parser.lineParsers.MPGainLineParser; import com.googlecode.logVisualizer.parser.lineParsers.MPGainLineParser.MPGainType; import com.googlecode.logVisualizer.parser.lineParsers.MafiaDisintegrateLineParser; import com.googlecode.logVisualizer.parser.lineParsers.MafiaFreeRunawaysLineParser; import com.googlecode.logVisualizer.parser.lineParsers.MeatLineParser; import com.googlecode.logVisualizer.parser.lineParsers.MeatLineParser.MeatGainType; import com.googlecode.logVisualizer.parser.lineParsers.MeatSpentLineParser; import com.googlecode.logVisualizer.parser.lineParsers.OdorExtractorUsageLineParser; import com.googlecode.logVisualizer.parser.lineParsers.SkillCastLineParser; import com.googlecode.logVisualizer.parser.lineParsers.StarfishMPGainLineParser; import com.googlecode.logVisualizer.parser.lineParsers.StatLineParser; import com.googlecode.logVisualizer.util.DataNumberPair; /** * A parser for the turn spent notation in mafia logs. * <p> * The format looks like this: * <p> * {@code [_turnNumber_] _areaName_} * <p> * {@code Encounter: _encounterName_} */ public final class EncounterBlockParser implements LogBlockParser { private static final Map<String, String> areaNameStandardizerMap; static { areaNameStandardizerMap = new HashMap<>(50); final Pattern areaNameMappingPattern = Pattern.compile(".+\\|\\s*.+"); final Pattern splitPattern = Pattern.compile("\\s*\\|\\s*"); final String commentStart = "//"; String tmpLine; try (final BufferedReader br = DataUtilities.getReader( UtilityConstants.KOL_DATA_DIRECTORY, "areaNameMappings.txt")) { while ((tmpLine = br.readLine()) != null) { if (!tmpLine.startsWith(commentStart) && areaNameMappingPattern.matcher(tmpLine).matches()) { try (final Scanner s = new Scanner(tmpLine)) { s.useDelimiter(splitPattern); final String areaName = s.next(); final String newAreaName = s.next(); s.close(); EncounterBlockParser.areaNameStandardizerMap.put( areaName, newAreaName); } } } br.close(); } catch (final IOException e) { e.printStackTrace(); } } private static final Set<String> OTHER_ENCOUNTER_AREAS_SET = new HashSet<>( Arrays.asList("Unlucky Sewer", "Sewer With Clovers", "Lemon Party", "Guild Challenge", "Mining (In Disguise)")); private static final String ENCOUNTER_START_STRING = "Encounter: "; private static final String COOKING_START_STRING = "Cook "; private static final String MIXING_START_STRING = "Mix "; private static final String SMITHING_START_STRING = "Smith "; private static final String SHORE_AREAS_END_STRING = " Vacation"; private static final String CLOWNLORD_CHOICE_ENCOUNTER_STRING = "Adventurer, $1.99"; private static final String OUTFIT_STRING = "outfit"; private static final String HP_LOSE_STRING_BEGINNING = "You lose "; private static final Pattern HP_LOSE_PATTERN = Pattern .compile("You lose \\d+ hit points"); private final List<LineParser> lineParsers = new ArrayList<>(); public EncounterBlockParser() { this.lineParsers.add(new ItemAcquisitionLineParser()); this.lineParsers.add(new SkillCastLineParser()); this.lineParsers.add(new MeatLineParser(MeatGainType.ENCOUNTER)); this.lineParsers.add(new MeatSpentLineParser()); this.lineParsers.add(new StatLineParser()); this.lineParsers.add(new MPGainLineParser(MPGainType.ENCOUNTER)); this.lineParsers.add(new CombatRecognizerLineParser()); this.lineParsers.add(new EquipmentLineParser()); this.lineParsers.add(new MafiaFreeRunawaysLineParser()); this.lineParsers.add(new MafiaDisintegrateLineParser()); this.lineParsers.add(new OdorExtractorUsageLineParser()); this.lineParsers.add(new StarfishMPGainLineParser()); } /** * {@inheritDoc} */ @Override public void parseBlock(final List<String> block, final LogDataHolder logData) { final String turnSpentLine = block.get(0).startsWith( UsefulPatterns.SQUARE_BRACKET_OPEN) ? block.get(0) : block .get(1); final SingleTurn turn; // Some areas have broken turn spent strings. If a turn is recognised as // being spent in such an area, the block will start with the encounter // name. We attempt to parse these encounters here. // If it is not a broken area, use the normal parsing. if (turnSpentLine .startsWith(EncounterBlockParser.ENCOUNTER_START_STRING)) { final String encounterName = turnSpentLine .substring(EncounterBlockParser.ENCOUNTER_START_STRING .length()); final int turnNumber = logData.getTurnsSpent().last().getEndTurn() + 1; turn = new SingleTurn(encounterName, encounterName, turnNumber, logData.getLastEquipmentChange(), logData.getLastFamiliarChange()); turn.setTurnVersion(TurnVersion.OTHER); } else { // Area name final int positionTurnEndBrace = turnSpentLine .indexOf(UsefulPatterns.SQUARE_BRACKET_CLOSE); String areaName = turnSpentLine.substring(positionTurnEndBrace + 2); // Check whether there is a mapping for the given area name final String mappedAreaName = EncounterBlockParser.areaNameStandardizerMap .get(areaName); areaName = mappedAreaName != null ? mappedAreaName : areaName; // Special handling for crafting turns is needed, because mafia // screws up the turn number, plus the turn version of these should // be marked as OTHER, so this check has to be done anyway later on. final boolean isCraftingTurn = areaName .startsWith(EncounterBlockParser.COOKING_START_STRING) || areaName .startsWith(EncounterBlockParser.MIXING_START_STRING) || areaName .startsWith(EncounterBlockParser.SMITHING_START_STRING); // Turn number // Special handling for crafting turns, because mafia screws up the // turn numbers of those. int turnNumber; final int positionTurnStartBrace = turnSpentLine .indexOf(UsefulPatterns.SQUARE_BRACKET_OPEN); if (isCraftingTurn) { turnNumber = Integer.parseInt(turnSpentLine.substring( positionTurnStartBrace + 1, positionTurnEndBrace)) - 1; } else { turnNumber = Integer.parseInt(turnSpentLine.substring( positionTurnStartBrace + 1, positionTurnEndBrace)); } // Now parse the encounter name. String encounterName = UsefulPatterns.EMPTY_STRING; boolean isMultipleCombatsHandling = false; for (final String line : block) { if (line.startsWith(EncounterBlockParser.ENCOUNTER_START_STRING)) { if (line.length() == EncounterBlockParser.ENCOUNTER_START_STRING .length()) { // Something strange happened here. Do not count this // turn. (clicking on a already cleansed cyrpt area can // result in this) return; } encounterName = line .substring(EncounterBlockParser.ENCOUNTER_START_STRING .length()); isMultipleCombatsHandling = MafiaSessionLogReader.BROKEN_AREAS_ENCOUNTER_SET .contains(line); break; } } // If a combat may span over multiple turns, it will be handled in // here. if (isMultipleCombatsHandling) { areaName = encounterName; int combatCounter = -1; for (final String line : block) { if (line.startsWith(EncounterBlockParser.ENCOUNTER_START_STRING)) { combatCounter++; } } // Every extra combat counted should be added now. This will // result in stats, meat and so on all being added to the last // combat only, but this problem shouldn't be happening often // enough to be a big deal. (currently only Ed the Undying falls // into this category here) for (int i = 0; i < combatCounter; i++) { final SingleTurn tmp = new SingleTurn(areaName, encounterName, turnNumber, logData.getLastEquipmentChange(), logData.getLastFamiliarChange()); tmp.setTurnVersion(TurnVersion.COMBAT); logData.addTurnSpent(tmp); turnNumber++; } } turn = new SingleTurn(areaName, encounterName, turnNumber, logData.getLastEquipmentChange(), logData.getLastFamiliarChange()); // Set turn version. If the turn is a crafting turn, or the area // name is inside the other-encounters set, set the turn version to // OTHER, otherwise set it to NONCOMBAT. Combats are recognised // separately. if (isCraftingTurn || EncounterBlockParser.OTHER_ENCOUNTER_AREAS_SET .contains(areaName)) { turn.setTurnVersion(TurnVersion.OTHER); } else { turn.setTurnVersion(TurnVersion.NONCOMBAT); } } // Check handling for special encounters. If the encounter is indeed // a special encounter, the specialEncounterHandling() method will // handle adding the turn to the LogDataHolder. if (!EncounterBlockParser .specialEncounterHandling(turn, block, logData)) { // Add the turn to the given LogDataHolder instance. logData.addTurnSpent(turn); } for (final String line : block) { for (final LineParser lp : this.lineParsers) { // If the line parser can parse the line, this method also // returns true. This is used to cut back on the amount of // loops. if (lp.parseLine(line, logData)) { break; } } } // Check whether the turn was a combat and whether it was lost. (If you // lose a combat the last line of the block is you losing HP.) String lastLine = block.get(block.size() - 1); if (lastLine.contains(EncounterBlockParser.OUTFIT_STRING)) { lastLine = block.get(block.size() - 2); } if ((turn.getTurnVersion() == TurnVersion.COMBAT) && lastLine .startsWith(EncounterBlockParser.HP_LOSE_STRING_BEGINNING) && EncounterBlockParser.HP_LOSE_PATTERN.matcher(lastLine) .matches()) { logData.addLostCombat(DataNumberPair.of(turn.getEncounterName(), turn.getTurnNumber())); } } /** * Handling of special encounters which need additional computation, because * most of the time KolMafia doesn't log them correctly, such as multi-turn * encounters (e.g. the shore). * <p> * If an encounter is special and thus was processed here, this method will * return {@code true} and otherwise {@code false}. * <p> * Additionally, this method has to and will handle adding the given turn to * the LogDataHolder if the encounter is special. * * @param turn * The encounter which is tested on whether it is special. * @param block * The current working block of an ascension log. * @param logData * The LogDataHolder of an ascension log in which the data should * be saved in. * @return {@code true} if this method did some computation, because the * given encounter is special, otherwise {@code false}. */ private static boolean specialEncounterHandling(final SingleTurn turn, final List<String> block, final LogDataHolder logData) { if (turn.getAreaName().endsWith( EncounterBlockParser.SHORE_AREAS_END_STRING)) { final EquipmentChange lastEquipment = logData .getLastEquipmentChange(); final FamiliarChange lastFamiliar = logData.getLastFamiliarChange(); final SingleTurn tmpTurn1 = new SingleTurn(turn.getAreaName(), turn.getEncounterName(), turn.getTurnNumber() + 1, lastEquipment, lastFamiliar); final SingleTurn tmpTurn2 = new SingleTurn(turn.getAreaName(), turn.getEncounterName(), turn.getTurnNumber() + 2, lastEquipment, lastFamiliar); turn.setTurnVersion(TurnVersion.OTHER); tmpTurn1.setTurnVersion(TurnVersion.OTHER); tmpTurn2.setTurnVersion(TurnVersion.OTHER); // Shore trip costs 500 meat. logData.getTurnsSpent().last().addMeatSpent(500); logData.addTurnSpent(turn); logData.addTurnSpent(tmpTurn1); logData.addTurnSpent(tmpTurn2); return true; } else if (turn.getEncounterName().equals( EncounterBlockParser.CLOWNLORD_CHOICE_ENCOUNTER_STRING)) { String firstChoice = UsefulPatterns.EMPTY_STRING; String secondChoice = UsefulPatterns.EMPTY_STRING; for (final String line : block) { if (line.contains("choice.php?")) { final String tmp = line.replace("pwd", "").replace("&", ""); if (firstChoice.equals(UsefulPatterns.EMPTY_STRING)) { firstChoice = tmp; } else { secondChoice = tmp; } } } // Check if the correct choices were taken. if (firstChoice.equals("choice.php?whichchoice=151option=1") && secondChoice .equals("choice.php?whichchoice=152option=1")) { final SingleTurn clownlord = new SingleTurn(turn.getAreaName(), "Clownlord Beelzebozo", turn.getTurnNumber() + 1, logData.getLastEquipmentChange(), logData.getLastFamiliarChange()); logData.addTurnSpent(turn); logData.addTurnSpent(clownlord); return true; } } return false; } }