/* 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.logData; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; import com.googlecode.logVisualizer.logData.consumables.Consumable; import com.googlecode.logVisualizer.logData.logSummary.LevelData; import com.googlecode.logVisualizer.logData.logSummary.LogSummaryData; import com.googlecode.logVisualizer.logData.turn.SingleTurn; import com.googlecode.logVisualizer.logData.turn.TurnInterval; import com.googlecode.logVisualizer.logData.turn.turnAction.DayChange; import com.googlecode.logVisualizer.logData.turn.turnAction.EquipmentChange; import com.googlecode.logVisualizer.logData.turn.turnAction.FamiliarChange; import com.googlecode.logVisualizer.logData.turn.turnAction.PlayerSnapshot; import com.googlecode.logVisualizer.logData.turn.turnAction.Pull; import com.googlecode.logVisualizer.util.DataNumberPair; /** * This class is basically the representation of an ascension log. It can hold * all the important data accumulated during an ascension. * <p> * All methods in this class throw a {@link NullPointerException} if a null * object reference is passed in any parameter. */ public final class LogDataHolder { private String logName; // A little ugly, but since the TreeSet only checks based on compareTo() and // not equals() and the TurnInterval compareTo() method shouldn't be changed // just to make it easier to use in a TreeSet, this Comparator is used // instead. It should be good enough for this purpose. private final SortedSet<TurnInterval> turnsSpent = new TreeSet<>( new Comparator<TurnInterval>() { @Override public int compare(final TurnInterval o1, final TurnInterval o2) { // Compare start turns final int startTurnComparison = o1.getStartTurn() - o2.getStartTurn(); // Return value, or if that fails, compare end turns. return startTurnComparison != 0 ? startTurnComparison : o1 .getEndTurn() - o2.getEndTurn(); } }); private final SortedMap<Integer, FamiliarChange> familiarChanges = new TreeMap<>(); private final SortedMap<Integer, DayChange> dayChanges = new TreeMap<>(); private final SortedMap<Integer, LevelData> levels = new TreeMap<>(); private final SortedMap<Integer, PlayerSnapshot> playerSnapshots = new TreeMap<>(); private final SortedMap<Integer, EquipmentChange> equipmentChanges = new TreeMap<>(); private final List<Pull> pulls = new ArrayList<>(100); private final List<DataNumberPair<String>> lostCombats = new ArrayList<>(); private CharacterClass characterClass = CharacterClass.NOT_DEFINED; private ParsedLogClass parsedLogCreator = ParsedLogClass.NOT_DEFINED; private LogSummaryData logSummary; public LogDataHolder() { // The start of an ascension is always on day 1. this.dayChanges.put(1, new DayChange(1, 0)); // A new ascension starts at level 1. this.levels.put(1, new LevelData(1, 0)); // You don't have anything equipped at the start of an ascension. this.equipmentChanges.put(0, new EquipmentChange(0)); // The familiar is not known at the very start of an ascension. this.familiarChanges.put(0, new FamiliarChange("none", 0)); // A dummy turn for the start of an ascension. this.addTurnSpent(new SingleTurn("Ascension Start", "Ascension Start", 0, this.getLastEquipmentChange(), this.getLastFamiliarChange())); } /** * Creates the log summary from the data of this log. * <p> * Calling this method should be done after all data additions are finished * (through parsing a log or otherwise). Otherwise, the summary will be * incomplete. */ public void createLogSummary() { this.logSummary = new LogSummaryData(this); } /** * @return A summary of various parts of this ascension log. * @throws IllegalStateException * if this method is called before a log summary is created by * calling {@link #createLogSummary()} */ public LogSummaryData getLogSummary() { if (this.logSummary == null) { throw new IllegalStateException( "Log summary has to be created first."); } return this.logSummary; } /** * Set the name of this ascension log. The format should be * CharacterName-StartdateOfAscension. * * @param logName * The name of this log. */ public void setLogName(final String logName) { if (logName == null) { throw new NullPointerException("Log name must not be null."); } this.logName = logName; } /** * The name of this ascension log. The Format <b>usually</b> is: * CharacterName-StartdateOfAscension * * @return The name of this ascension log. */ public String getLogName() { return this.logName; } /** * @param turnInterval * The turn interval to add. */ public void addTurnsSpent(final TurnInterval turnInterval) { if (turnInterval == null) { throw new NullPointerException("Turn interval must not be null."); } // Remove any existing turn interval in the set that is equal to // turnInterval as defined by the comparator of turnsSpent. if (this.turnsSpent.contains(turnInterval)) { // Add all the data from the interval which should be removed to the // interval that comes before it, or if there is none, to the new // interval so the data is not lost. final SortedSet<TurnInterval> tmp = this.turnsSpent .headSet(turnInterval); final TurnInterval equalInterval = this.turnsSpent.tailSet( turnInterval).first(); if (!tmp.isEmpty()) { tmp.last().addTurnIntervalData(equalInterval); } else { turnInterval.addTurnIntervalData(equalInterval); } this.turnsSpent.remove(equalInterval); } else if (!this.turnsSpent.isEmpty()) { final TurnInterval lastInterval = this.turnsSpent.last(); if (lastInterval.getAreaName().equals(turnInterval.getAreaName())) { for (final SingleTurn st : turnInterval.getTurns()) { lastInterval.addTurn(st); } lastInterval.addNotes(turnInterval.getNotes()); lastInterval.incrementSuccessfulFreeRunaways(turnInterval .getFreeRunaways().getNumberOfSuccessfulRunaways()); return; } } this.turnsSpent.add(turnInterval); } /** * Add the given turn to the log data. * <p> * Note that the integrity of the log data cannot be guaranteed if the given * single turn would be entered right in the middle of already existing turn * intervals. ({@code turn.getTurnNumber() > * getTurnsSpent().first().getStartTurn() && turn.getTurnNumber() < * getTurnsSpent().last().getEndTurn()}) * * @param turn * The single turn to add. */ public void addTurnSpent(final SingleTurn turn) { if (turn == null) { throw new NullPointerException("Turn must not be null."); } // 1. If the turn rundown collection isn't empty and the last turn // interval's area name is equal to that of the given single turn, add // the turn to the last interval. // 2. If the last turn interval ends on the same turn number as the // given turn, remove the last turn from the last turn interval, make // sure the integrity of the turn interval collection is not compromised // and then proceed as stated in 3.. // 3. Otherwise create a new interval and add it to the turn rundown // collection. if (!this.turnsSpent.isEmpty()) { TurnInterval lastInterval = this.turnsSpent.last(); // Check for turn count errors as they sometimes occur in mafia // logs. If one is found, fully remove the intervals that seem to be // out of place in the turn interval collection. while ((lastInterval != null) && ((turn.getTurnNumber() - lastInterval.getEndTurn()) <= -100)) { this.turnsSpent.remove(lastInterval); lastInterval = this.turnsSpent.last(); } if (lastInterval != null) { if (lastInterval.getAreaName().equals(turn.getAreaName())) { lastInterval.addTurn(turn); return; } else if (lastInterval.getEndTurn() == turn.getTurnNumber()) { final SingleTurn equalTurn = lastInterval.removeLastTurn(); // If the skipped turn was a runaway and the Navel Ring was // equipped, it means that it was a successful usage of the // Navel Ring. if (equalTurn.isRanAwayOnThisTurn() && equalTurn.isNavelRingEquipped()) { lastInterval.incrementSuccessfulFreeRunaways(1); } if (lastInterval.getTurns().isEmpty()) { turn.addSingleTurnData(equalTurn); this.turnsSpent.remove(lastInterval); // The removed interval may contain free runaways // usages. if (!this.turnsSpent.isEmpty()) { this.turnsSpent .last() .incrementSuccessfulFreeRunaways( lastInterval .getFreeRunaways() .getNumberOfSuccessfulRunaways()); } } } } } this.addTurnsSpent(new TurnInterval(turn)); } /** * Returns a sorted set of all turn intervals of this ascension log. * <p> * Note that the given set and its contents is directly backed by the * internal collections of this class. This means that changing elements * will in the same way effect the internal collections. In general, it is * best not to change any elements, but if it is done, it must not change * the ordering of this set, otherwise the change will corrupt it. * <p> * Also, note that the returned collection is read-only. * * @return The turns spent. */ public SortedSet<TurnInterval> getTurnsSpent() { return Collections.unmodifiableSortedSet(this.turnsSpent); } /** * Returns a sub interval of this LogDataHolder that includes all turn * intervals and other data that is inside the given interval (both * {@code startTurn} and {@code endTurn} are inclusive). * <p> * Note that turn intervals that start before the interval, but end inside * it will be included in the returned LogDataHolder. The same is true for * turn intervals that start inside the interval and end outside it. * * @param startTurn * The start of the interval. * @param endTurn * The end of the interval. * @return A LogDataHolder including all the data inside the given start and * end points. * @throws IllegalArgumentException * if {@code endTurn} is not greater than {@code startTurn}; if * {@code endTurn} is not greater than 0 */ public LogDataHolder getSubIntervalLogData(final int startTurn, final int endTurn) { if (endTurn <= startTurn) { throw new IllegalArgumentException( "The end turn must be greater than the start turn."); } if (endTurn <= 0) { throw new IllegalArgumentException( "The end turn must be greater than zero."); } final LogDataHolder subLog = new LogDataHolder(); subLog.logName = this.logName; subLog.parsedLogCreator = this.parsedLogCreator; subLog.characterClass = this.characterClass; // Remove "Ascension Start" turn interval that is always included in a // newly created LogDataHolder. subLog.turnsSpent.clear(); // Add turn intervals. for (final TurnInterval ti : this.turnsSpent) { // Stop the iteration once we are outside the interval if (ti.getStartTurn() > endTurn) { break; } // Both start and end are inside the interval if ((ti.getStartTurn() >= startTurn) && (ti.getEndTurn() <= endTurn)) { subLog.turnsSpent.add(ti); } // The start is not, but the end is inside the interval if ((ti.getEndTurn() <= endTurn) && (ti.getEndTurn() > startTurn)) { subLog.turnsSpent.add(ti); } // The start is inside the interval, but the end is not if ((ti.getStartTurn() >= startTurn) && (ti.getStartTurn() < endTurn)) { subLog.turnsSpent.add(ti); } } // Add familiar changes final FamiliarChange famChange = this .getLastFamiliarChangeBeforeTurn(startTurn); if (famChange != null) { subLog.addFamiliarChange(famChange); } for (final FamiliarChange fc : this.getFamiliarChanges()) { // Stop the iteration once we are outside the interval if (fc.getTurnNumber() > endTurn) { break; } // Add if inside the interval if ((fc.getTurnNumber() >= startTurn) && (fc.getTurnNumber() <= endTurn)) { subLog.addFamiliarChange(fc); } } // Add day changes for (final DayChange dc : this.getDayChanges()) { // Stop the iteration once we are outside the interval if (dc.getTurnNumber() > endTurn) { break; } // Add if inside the interval if ((dc.getTurnNumber() >= startTurn) && (dc.getTurnNumber() < endTurn)) { subLog.addDayChange(dc); } } // Add level changes LevelData lastLevelBeforeInterval = null; for (final LevelData ld : this.getLevels()) { // Stop the iteration once we are outside the interval if (ld.getLevelReachedOnTurn() > endTurn) { break; } if (ld.getLevelReachedOnTurn() < startTurn) { lastLevelBeforeInterval = ld; } // Add if inside the interval if ((ld.getLevelReachedOnTurn() >= startTurn) && (ld.getLevelReachedOnTurn() <= endTurn)) { subLog.addLevel(ld); } } if (lastLevelBeforeInterval != null) { subLog.addLevel(lastLevelBeforeInterval); } // Add player snapshots final PlayerSnapshot playSnap = this .getLastPlayerSnapshotBeforeTurn(startTurn); if (playSnap != null) { subLog.addPlayerSnapshot(playSnap); } for (final PlayerSnapshot ps : this.getPlayerSnapshots()) { // Stop the iteration once we are outside the interval if (ps.getTurnNumber() > endTurn) { break; } // Add if inside the interval if ((ps.getTurnNumber() >= startTurn) && (ps.getTurnNumber() < endTurn)) { subLog.addPlayerSnapshot(ps); } } // Add equipment changes final EquipmentChange equipChange = this .getLastEquipmentChangeBeforeTurn(startTurn); if (equipChange != null) { subLog.addEquipmentChange(equipChange); } for (final EquipmentChange ec : this.getEquipmentChanges()) { // Stop the iteration once we are outside the interval if (ec.getTurnNumber() > endTurn) { break; } // Add if inside the interval if ((ec.getTurnNumber() > startTurn) && (ec.getTurnNumber() < endTurn)) { subLog.addEquipmentChange(ec); } } // Add pulls final Set<Integer> includedDays = new HashSet<>(); for (final DayChange dc : subLog.getDayChanges()) { includedDays.add(dc.getDayNumber()); } for (final Pull p : this.getPulls()) { // Stop the iteration once we are outside the interval if (p.getTurnNumber() > endTurn) { break; } // Add if inside the interval if ((p.getTurnNumber() >= startTurn) && (p.getTurnNumber() <= endTurn) && includedDays.contains(p.getDayNumber())) { subLog.addPull(p); } } // Add lost combats for (final DataNumberPair<String> dnp : this.getLostCombats()) { final int turnNumber = dnp.getNumber(); // Stop the iteration once we are outside the interval if (turnNumber > endTurn) { break; } // Add if inside the interval if ((turnNumber >= startTurn) && (turnNumber <= endTurn)) { subLog.addLostCombat(dnp); } } // Create log summary based on the sub interval subLog.createLogSummary(); return subLog; } /** * @param familiarChange * The familiar change to add. */ public void addFamiliarChange(final FamiliarChange familiarChange) { if (familiarChange == null) { throw new NullPointerException("Familiar change must not be null."); } final Integer turnNumber = Integer.valueOf(familiarChange .getTurnNumber()); // Only the last familiar change of a turn should be saved. this.familiarChanges.remove(turnNumber); // If the new familiar change is to a familiar that was already used // before the change, do not add the familiar change, because it would // be redundant. if (this.familiarChanges.isEmpty() || !this.getLastFamiliarChange().getFamiliarName() .equals(familiarChange.getFamiliarName())) { this.familiarChanges.put(turnNumber, familiarChange); } } /** * Returns a sorted collection of all familiar changes of this ascension * log. * <p> * Note that the given collection and its contents is directly backed by the * internal collections of this class. This means that changing elements * will in the same way effect the internal collections. To ensure that the * underlying collection is not corrupted, the returned collection is * read-only. * * @return The familiar changes. */ public Collection<FamiliarChange> getFamiliarChanges() { return Collections .unmodifiableCollection(this.familiarChanges.values()); } /** * @return The last familiar change of this ascension. Returns {@code null} * if there are no familiar changes. */ public FamiliarChange getLastFamiliarChange() { return this.familiarChanges.isEmpty() ? null : this.familiarChanges .get(this.familiarChanges.lastKey()); } /** * @param turn * The turn number before which the last familiar change should * be returned of. * @return The last familiar change before the given turn of this ascension. * Returns {@code null} if there are no such familiar changes. * @throws IllegalArgumentException * if turn is negative. */ public FamiliarChange getLastFamiliarChangeBeforeTurn(final int turn) { if (turn < 0) { throw new IllegalArgumentException( "Turn number cannot be negative."); } return LogDataHolder.getLastElementBeforeInteger(this.familiarChanges, Integer.valueOf(turn)); } /** * @param turn * The turn number after which the first familiar change should * be returned of. * @return The first familiar change on or after the given turn of this * ascension. Returns {@code null} if there are no such familiar * changes. * @throws IllegalArgumentException * if turn is negative. */ public FamiliarChange getFirstFamiliarChangeAfterTurn(final int turn) { if (turn < 0) { throw new IllegalArgumentException( "Turn number cannot be negative."); } return LogDataHolder.getFirstElementAfterInteger(this.familiarChanges, Integer.valueOf(turn)); } /** * @param dayChange * The day change to add. */ public void addDayChange(final DayChange dayChange) { if (dayChange == null) { throw new NullPointerException("Day change must not be null."); } this.dayChanges.put(dayChange.getDayNumber(), dayChange); } /** * Returns a sorted collection of all day changes of this ascension log. * <p> * Note that the given collection and its contents is directly backed by the * internal collections of this class. This means that changing elements * will in the same way effect the internal collections. To ensure that the * underlying collection is not corrupted, the returned collection is * read-only. * * @return The day changes. */ public Collection<DayChange> getDayChanges() { return Collections.unmodifiableCollection(this.dayChanges.values()); } /** * @return The last day change of this ascension. Returns {@code null} if * there are no day changes. */ public DayChange getLastDayChange() { return this.dayChanges.isEmpty() ? null : this.dayChanges .get(this.dayChanges.lastKey()); } /** * @param turnNumber * The turn number specifying the point of which the day is * wanted. * @return The day of the given turn number. * @throws IllegalArgumentException * if turn is negative. */ public DayChange getCurrentDay(final int turnNumber) { if (turnNumber < 0) { throw new IllegalArgumentException( "Turn number cannot be negative."); } // Initialise with day 1, because it is always present. DayChange currentDay = this.dayChanges.get(Integer.valueOf(1)); for (final DayChange day : this.getDayChanges()) { // If the turn number of the day change is higher than the specified // turn number, stop the loop. if (day.getTurnNumber() > turnNumber) { break; } // As long as loop isn't stopped, the checked day change happened // before the given turn number. currentDay = day; } return currentDay; } /** * @param level * The level data to add. */ public void addLevel(final LevelData level) { if (level == null) { throw new NullPointerException("Level must not be null."); } this.levels.put(level.getLevelNumber(), level); } /** * Returns a sorted collection of all level data of this ascension log. * <p> * Note that the given collection and its contents is directly backed by the * internal collections of this class. This means that changing elements * will in the same way effect the internal collections. To ensure that the * underlying collection is not corrupted, the returned collection is * read-only. * * @return The level data. */ public Collection<LevelData> getLevels() { return Collections.unmodifiableCollection(this.levels.values()); } /** * @return The last level reached of this ascension. Returns {@code null} if * there are no levels reached. */ public LevelData getLastLevel() { return this.levels.isEmpty() ? null : this.levels.get(this.levels .lastKey()); } /** * @param turnNumber * The turn number specifying the point of which the level is * wanted. * @return The level reached at the given turn number. * @throws IllegalArgumentException * if turn is negative. */ public LevelData getCurrentLevel(final int turnNumber) { if (turnNumber < 0) { throw new IllegalArgumentException( "Turn number cannot be negative."); } // Initialise with level 1, because it is always present. LevelData currentLevel = this.levels.get(Integer.valueOf(1)); for (final LevelData level : this.getLevels()) { // If the turn number of the level change is higher than the // specified turn number, stop the loop. if (level.getLevelReachedOnTurn() > turnNumber) { break; } // As long as loop isn't stopped, the checked level change happened // before the given turn number. currentLevel = level; } return currentLevel; } /** * @param playerSnapshot * The player snapshot to add. */ public void addPlayerSnapshot(final PlayerSnapshot playerSnapshot) { if (playerSnapshot == null) { throw new NullPointerException("Player snapshot must not be null."); } // Add the player snapshot. this.playerSnapshots .put(Integer.valueOf(playerSnapshot.getTurnNumber()), playerSnapshot); } /** * Returns a sorted collection of all player snapshots of this ascension * log. * <p> * Note that the given collection and its contents is directly backed by the * internal collections of this class. This means that changing elements * will in the same way effect the internal collections. To ensure that the * underlying collection is not corrupted, the returned collection is * read-only. * * @return The player snapshots. */ public Collection<PlayerSnapshot> getPlayerSnapshots() { return Collections .unmodifiableCollection(this.playerSnapshots.values()); } /** * @return The last player snapshot of this ascension. Returns {@code null} * if there are no player snapshots. */ public PlayerSnapshot getLastPlayerSnapshot() { return this.playerSnapshots.isEmpty() ? null : this.playerSnapshots .get(this.playerSnapshots.lastKey()); } /** * @param turn * The turn number before which the last player snapshot should * be returned of. * @return The last player snapshot before the given turn of this ascension. * Returns {@code null} if there are no such player snapshots. * @throws IllegalArgumentException * if turn is negative. */ public PlayerSnapshot getLastPlayerSnapshotBeforeTurn(final int turn) { if (turn < 0) { throw new IllegalArgumentException( "Turn number cannot be negative."); } return LogDataHolder.getLastElementBeforeInteger(this.playerSnapshots, Integer.valueOf(turn)); } /** * @param turn * The turn number after which the first player snapshot should * be returned of. * @return The first player snapshot on or after the given turn of this * ascension. Returns {@code null} if there are no such player * snapshots. * @throws IllegalArgumentException * if turn is negative. */ public PlayerSnapshot getFirstPlayerSnapshotAfterTurn(final int turn) { if (turn < 0) { throw new IllegalArgumentException( "Turn number cannot be negative."); } return LogDataHolder.getFirstElementAfterInteger(this.playerSnapshots, Integer.valueOf(turn)); } /** * @param equipmentChange * The equipment change to add. */ public void addEquipmentChange(final EquipmentChange equipmentChange) { if (equipmentChange == null) { throw new NullPointerException("Equipment change must not be null."); } // Add the equipment change. this.equipmentChanges.put( Integer.valueOf(equipmentChange.getTurnNumber()), equipmentChange); } /** * Returns a sorted collection of all equipment changes of this ascension * log. * <p> * Note that the given collection and its contents is directly backed by the * internal collections of this class. This means that changing elements * will in the same way effect the internal collections. To ensure that the * underlying collection is not corrupted, the returned collection is * read-only. * * @return The equipment changes. */ public Collection<EquipmentChange> getEquipmentChanges() { return Collections.unmodifiableCollection(this.equipmentChanges .values()); } /** * @return The last equipment change of this ascension. Returns {@code null} * if there are no equipment changes. */ public EquipmentChange getLastEquipmentChange() { return this.equipmentChanges.isEmpty() ? null : this.equipmentChanges .get(this.equipmentChanges.lastKey()); } /** * @param turn * The turn number before which the last equipment change should * be returned of. * @return The last equipment change before the given turn of this * ascension. Returns {@code null} if there are no such equipment * changes. * @throws IllegalArgumentException * if turn is negative. */ public EquipmentChange getLastEquipmentChangeBeforeTurn(final int turn) { if (turn < 0) { throw new IllegalArgumentException( "Turn number cannot be negative."); } return LogDataHolder.getLastElementBeforeInteger(this.equipmentChanges, Integer.valueOf(turn)); } /** * @param turn * The turn number after which the first equipment change should * be returned of. * @return The first equipment change on or after the given turn of this * ascension. Returns {@code null} if there are no such equipment * changes. * @throws IllegalArgumentException * if turn is negative. */ public EquipmentChange getFirstEquipmentChangeAfterTurn(final int turn) { if (turn < 0) { throw new IllegalArgumentException( "Turn number cannot be negative."); } return LogDataHolder.getFirstElementAfterInteger(this.equipmentChanges, Integer.valueOf(turn)); } /** * @param map * The sorted map in which the element should be looked for. * @param number * The number before which the last element in the map should be * returned of. * @return The last element before the given number. Returns {@code null} if * there is no such element. */ private static <V> V getLastElementBeforeInteger( final SortedMap<Integer, V> map, final Integer number) { final SortedMap<Integer, V> headMap = map.headMap(number); return headMap.isEmpty() ? null : headMap.get(headMap.lastKey()); } /** * @param map * The sorted map in which the element should be looked for. * @param number * The number on or after which the last element in the map * should be returned of. * @return The last element before the given number. Returns {@code null} if * there is no such element. */ private static <V> V getFirstElementAfterInteger( final SortedMap<Integer, V> map, final Integer number) { final SortedMap<Integer, V> tailMap = map.tailMap(number); return tailMap.isEmpty() ? null : tailMap.get(tailMap.firstKey()); } /** * @param pull * The pull to add. */ public void addPull(final Pull pull) { if (pull == null) { throw new NullPointerException("Pull must not be null."); } this.pulls.add(pull); } /** * Returns the pull list. This list is empty if no pulls were made. * <p> * Note that while this list usually is sorted after the turn numbers of the * pulls, this cannot be guaranteed. * <p> * Also, please note that the given list and its contents is directly backed * by the internal collections of this class. This means that changing * elements will in the same way effect the internal collections. Therefore, * great care should be taken when working with this collection. To ensure * that the underlying collection is not corrupted through remove or add * operations, the returned collection is read-only. * * @return The pulls. */ public List<Pull> getPulls() { return Collections.unmodifiableList(this.pulls); } /** * @param lostCombat * The lost combat to add. */ public void addLostCombat(final DataNumberPair<String> lostCombat) { if (lostCombat == null) { throw new NullPointerException("Lost combat must not be null."); } this.lostCombats.add(lostCombat); } /** * Returns a list of all lost combats. This list is empty if no combats were * lost. * <p> * Note that while this list usually is sorted after the turn numbers of the * combats, this cannot be guaranteed. * <p> * Also, please note that the given list is read-only. * * @return The lost combats. */ public List<DataNumberPair<String>> getLostCombats() { return Collections.unmodifiableList(this.lostCombats); } /** * Returns a list of all dropped items during this ascension. The list is * not sorted. * <p> * Note that this a convenience method. It is equal to * {@code logData.getLogSummary().getDroppedItems()}. * * @return A list of all dropped items during this ascension. */ public List<Item> getDroppedItems() { return this.logSummary.getDroppedItems(); } /** * Returns a list of all skills cast during this ascension. * <p> * Note that this a convenience method. It is equal to * {@code logData.getLogSummary().getSkillsCast()}. * * @return A list of all skills cast during this ascension. */ public List<Skill> getAllSkillsCast() { return this.logSummary.getSkillsCast(); } /** * Returns a list of all consumables used during this ascension. * <p> * Note that this a convenience method. It is equal to * {@code logData.getLogSummary().getAllConsumablesUsed()}. * * @return A list of all consumables used during this ascension. */ public List<Consumable> getAllConsumablesUsed() { return this.logSummary.getAllConsumablesUsed(); } /** * This method will set the character class of this ascension log based on * the given string. If the string doesn't match any of the character class * names, the character class of this ascension log will be set to * {@code NOT_DEFINED}. * * @param characterClassName * The name of the character class to set. */ public void setCharacterClass(final String characterClassName) { this.characterClass = CharacterClass.fromString(characterClassName); } /** * @return The character class of this ascension log. If no character class * has been specified this method will return {@code NOT_DEFINED}. */ public CharacterClass getCharacterClass() { return this.characterClass; } /** * @param parsedLogCreator * The program which created the parsed ascension log behind this * LogDataHolder to set. */ public void setParsedLogCreator(final ParsedLogClass parsedLogCreator) { if (parsedLogCreator == null) { throw new NullPointerException( "The parsed log creator must not be null."); } this.parsedLogCreator = parsedLogCreator; } /** * If this LogDataHolder hasn't been created by a parsed ascension log or * the log creator hasn't been set (most probably because it couldn't be * determined), this method should return {@code NOT_DEFINED}. * * @return The program which created the parsed ascension log behind this * LogDataHolder. */ public ParsedLogClass getParsedLogCreator() { return this.parsedLogCreator; } /** * This enumeration represents all six character classes. */ public static enum CharacterClass { SEAL_CLUBBER("Seal Clubber", StatClass.MUSCLE), TURTLE_TAMER( "Turtle Tamer", StatClass.MUSCLE), PASTAMANCER("Pastamancer", StatClass.MYSTICALITY), SAUCEROR("Sauceror", StatClass.MYSTICALITY), DISCO_BANDIT("Disco Bandit", StatClass.MOXIE), ACCORDION_THIEF("Accordion Thief", StatClass.MOXIE), NOT_DEFINED("not defined", StatClass.MUSCLE); private static final Map<String, CharacterClass> stringToEnum = new HashMap<>(); static { for (final CharacterClass op : CharacterClass.values()) { CharacterClass.stringToEnum.put(op.toString(), op); } } private final String className; private final StatClass statClass; CharacterClass(final String className, final StatClass statClass) { this.className = className; this.statClass = statClass; } /** * @return The mainstat of this character class. */ public StatClass getStatClass() { return this.statClass; } @Override public String toString() { return this.className; } /** * @return The enum whose toString method returns a string which is * equal to the given string. If no match is found this method * will return {@code NOT_DEFINED}. */ public static CharacterClass fromString(final String className) { if (className == null) { throw new NullPointerException("Class name must not be null."); } final CharacterClass characterClass = CharacterClass.stringToEnum .get(className); return characterClass != null ? characterClass : NOT_DEFINED; } } /** * This enumeration represents the three stat classes. */ public static enum StatClass { MUSCLE, MYSTICALITY, MOXIE; } /** * This enumeration represents the different parsers which could have * created a parsed ascension log. */ public static enum ParsedLogClass { LOG_VISUALIZER, AFH_PARSER, NOT_DEFINED; } }