/** * Copyright (c) 2005-2017, KoLmafia development team * http://kolmafia.sourceforge.net/ * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * [1] Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * [2] Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * [3] Neither the name "KoLmafia" nor the names of its contributors may * be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package net.sourceforge.kolmafia.session; import java.util.ArrayList; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.sourceforge.kolmafia.KoLConstants; import net.sourceforge.kolmafia.KoLmafia; import net.sourceforge.kolmafia.RequestLogger; import net.sourceforge.kolmafia.RequestThread; import net.sourceforge.kolmafia.MonsterData; import net.sourceforge.kolmafia.persistence.MonsterDatabase; import net.sourceforge.kolmafia.persistence.MonsterDatabase.Element; import net.sourceforge.kolmafia.persistence.MonsterDatabase.Phylum; import net.sourceforge.kolmafia.request.MonsterManuelRequest; import net.sourceforge.kolmafia.utilities.StringUtilities; public class MonsterManuelManager { private static final Map<Integer, String> manuelEntries = new TreeMap<Integer, String>(); private static final Map<Integer, Integer> manuelFactoidCounts = new TreeMap<Integer, Integer>(); public static void flushCache() { MonsterManuelManager.manuelEntries.clear(); MonsterManuelManager.manuelFactoidCounts.clear(); } public static void reset() { // Reset Your winged yeti and You the Adventurer MonsterManuelManager.reset( MonsterDatabase.findMonsterById( 1667 ) ); MonsterManuelManager.reset( MonsterDatabase.findMonsterById( 1669 ) ); Iterator<Entry<Integer,Integer>> entryIterator = MonsterManuelManager.manuelFactoidCounts.entrySet().iterator(); // Remove all entries that have less than 3 factoids, since the // current player may know more. while ( entryIterator.hasNext() ) { Entry<Integer,Integer> entry = entryIterator.next(); Integer key = entry.getKey(); Integer value = entry.getValue(); if ( value.intValue() < 3 ) { MonsterManuelManager.manuelEntries.remove( key ); entryIterator.remove(); } } } public static void reset( MonsterData monster ) { // We just learned a factoid, so flush data for this monster if ( monster != null ) { int id = monster.getId(); if ( id > 0 ) { MonsterManuelManager.manuelEntries.remove( id ); MonsterManuelManager.manuelFactoidCounts.remove( id ); } } } public static void registerMonster( final int id, final String text ) { // See if this is a new entry String old = MonsterManuelManager.manuelEntries.get( id ); if ( old != null && old.equals( text ) ) { // We have seen this exact Manuel entry before. return; } // Either the entry is new or is different from what we have // saved; perhaps there are additional factoids. // Detach the entry from the page text and store in entry map String entry = new String( text ); MonsterManuelManager.manuelEntries.put( id, entry ); // Count the factoids and remember that, too. int factoids = MonsterManuelManager.countFactoids( entry ); MonsterManuelManager.manuelFactoidCounts.put( id, factoids ); // Extract some fields from the entry // If we are looking at this entry for the first time, do some checks. if ( old == null ) { MonsterData monster = MonsterDatabase.findMonsterById( id ); String name = MonsterManuelManager.extractMonsterName( entry ); String image = MonsterManuelManager.extractMonsterImage( entry ); String attackString = MonsterManuelManager.extractMonsterAttack( entry ); String defenseString = MonsterManuelManager.extractMonsterDefense( entry ); String hpString = MonsterManuelManager.extractMonsterHP( entry ); String phylumString = MonsterManuelManager.extractMonsterPhylum( entry ); Element element = MonsterManuelManager.extractMonsterElement( entry ); String initiativeString = MonsterManuelManager.extractMonsterInitiative( entry ); if ( monster == null ) { // We don't know a monster with this ID. Add to monster ID map. String attributes = MonsterManuelManager.buildMonsterAttributes( attackString, defenseString, hpString, phylumString, element, initiativeString ); RequestLogger.printLine( "New monster #" + id + " found in Manuel with name '" + name + "' image '" + image + "' attributes ='" + attributes + "'" ); monster = MonsterDatabase.registerMonster( name, id, image, attributes ); return; } // Check our data with what Manuel says // Don't bother checking name for Your winged yeti && You the Adventurer if ( id != 1667 && id != 1669 && !monster.getManuelName().equals( name ) ) { // We know this monster, but do not have the correct Manuel name RequestLogger.printLine( "Monster #" + id + " has name '" + monster.getManuelName() + "' but Manuel calls it '" + name + "'" ); monster.setManuelName( name ); } // Don't bother checking image for (shadow opponent) and You the Adventurer // noart.gif is used for monsters with variable images if ( id != 210 && id != 1669 && !image.equals( "noart.gif" ) && !monster.hasImage( image ) ) { RequestLogger.printLine( "Manuel says that '" + name + "' (" + id + ") has unrecognized image '" + image + "'" ); } if ( attackString.equals( "?" ) ) { // Scaling monster: either standard scaling or a formula if ( !( monster.scales() || monster.getBaseAttack() == -1 ) ) { RequestLogger.printLine( "Manuel says that '" + name + "' (" + id + ") scales, but KoLmafia doesn't"); } } else { // Non-scaling monster int baseAttack = monster.getBaseAttack(); int attack = StringUtilities.parseInt( attackString ); if ( baseAttack != -1 && attack != 0 && baseAttack != attack ) { RequestLogger.printLine( "Manuel says that '" + name + "' (" + id + ") has attack " + attack + ", but KoLmafia says it is " + baseAttack ); } int baseDefense = monster.getBaseDefense(); int defense = StringUtilities.parseInt( defenseString ); if ( baseDefense != -1 && defense != 0 && baseDefense != defense ) { RequestLogger.printLine( "Manuel says that '" + name + "' (" + id + ") has defense " + defense + ", but KoLmafia says it is " + baseDefense ); } int baseHP = monster.getBaseHP(); int hp = StringUtilities.parseInt( hpString ); if ( baseHP != -1 && hp != 0 && baseHP != hp ) { RequestLogger.printLine( "Manuel says that '" + name + "' (" + id + ") has HP " + hp + ", but KoLmafia says it is " + baseHP ); } } int baseInitiative = monster.getBaseInitiative(); int initiative = MonsterManuelManager.parseInitiative( initiativeString ); if ( baseInitiative != -1 && baseInitiative != initiative ) { RequestLogger.printLine( "Manuel says that '" + name + "' (" + id + ") '" + initiativeString + "', but KoLmafia says it is " + baseInitiative ); } Element attackElement = monster.getAttackElement(); Element defenseElement = monster.getDefenseElement(); if ( element != defenseElement ) { RequestLogger.printLine( "Manuel says that '" + name + "' (" + id + ") has element " + element.toString() + ", but KoLmafia says it has attack element " + attackElement.toString() + " and defense element " + defenseElement ); } Phylum phylum = MonsterManuelManager.parsePhylum( phylumString ); if ( phylum != monster.getPhylum() ) { RequestLogger.printLine( "Manuel says that '" + name + "' (" + id + ") has phylum " + phylum + ", but KoLmafia says it is " + monster.getPhylum() ); } } } public static int parseInitiative( final String initiativeString ) { return // Never wins initiative initiativeString.startsWith( "Never" ) ? -10000 : // Always wins initiative initiativeString.startsWith( "Always" ) ? 10000 : // Initiative +100% StringUtilities.parseInt( initiativeString.substring( 12, initiativeString.length() - 1 ) ); } public static Phylum parsePhylum( final String phylum ) { return MonsterDatabase.parsePhylum( phylum.toLowerCase() ); } public static String buildMonsterAttributes( final String attack, final String defense, final String hp, final String phylum, final Element element, final String initiative ) { StringBuilder buffer = new StringBuilder(); if ( attack.equals( "?" ) ) { // Attack/Defense/HP = ? means this is a scaling monster buffer.append( "Scale: ? Cap: ? Floor: ?" ); } else { buffer.append( "Atk: " ); buffer.append( StringUtilities.parseInt( attack ) ); buffer.append( " Def: " ); buffer.append( StringUtilities.parseInt( defense ) ); buffer.append( " HP: " ); buffer.append( StringUtilities.parseInt( hp ) ); } buffer.append( " Init: " ); buffer.append( String.valueOf( MonsterManuelManager.parseInitiative( initiative ) ) ); if ( element != Element.NONE ) { buffer.append( " E: " ); buffer.append( element.toString() ); } buffer.append( " P: " ); buffer.append( MonsterManuelManager.parsePhylum( phylum ) ); return buffer.toString(); } // <td rowspan=4 valign=top class=small><b><font size=+2>A.M.C. gremlin</font></b> private static final Pattern NAME_PATTERN = Pattern.compile( "<td rowspan=4 valign=top class=small><b><font size=\\+2>(.*?)</font></b>" ); public static String extractMonsterName( final String text ) { Matcher matcher = MonsterManuelManager.NAME_PATTERN.matcher( text ); return matcher.find() ? matcher.group( 1 ).trim() : ""; } // <td rowspan=4 valign=top width=100><img src=http://images.kingdomofloathing.com/adventureimages/gremlinamc.gif width=100></td> // <td rowspan=4 valign=top width=100><img src=/images/otherimages/barrelbeast.gif style="max-width:350;"></td> // <td rowspan=4 valign=top width=100><img src=https://s3.amazonaws.com/images.kingdomofloathing.com/adventureimages/stone_serpent.gif width=100></td> private static final Pattern IMAGE_PATTERN = Pattern.compile( "<td rowspan=4 valign=top width=100><img src=[^>]*?(?:images.kingdomofloathing.com|/images)/(?:(adventureimages|otherimages)/(?:\\.\\./)?)?(.*?\\.gif).*?</td>" ); public static String extractMonsterImage( final String text ) { Matcher matcher = MonsterManuelManager.IMAGE_PATTERN.matcher( text ); if ( !matcher.find() ) { return ""; } String directory = matcher.group( 1 ); String file = matcher.group( 2 ); return directory == null || directory.equals( "adventureimages" ) ? file : ( directory + "/" + file ); } // <td width=30><img src=http://images.kingdomofloathing.com/itemimages/nicesword.gif width=30 height=30 alt="Attack Power (approximate)" title="Attack Power (approximate)"></td><td width=50 valign=center align=left><b><font size=+2>150</font></b></td> // <td width=30><img src=http://images.kingdomofloathing.com/itemimages/nicesword.gif width=30 height=30 alt="Attack Power (approximate)" title="Attack Power (approximate)"></td><td width=50 valign=center align=left><b><font size=+2>?</font></b></td> private static final Pattern ATTACK_PATTERN = Pattern.compile( "Attack Power \\(approximate\\).*?<font size=\\+2>(.*?)</font>" ); public static String extractMonsterAttack( final String text ) { Matcher matcher = MonsterManuelManager.ATTACK_PATTERN.matcher( text ); return matcher.find() ? matcher.group( 1 ) : ""; } // <td width=30><img src=http://images.kingdomofloathing.com/itemimages/whiteshield.gif width=30 height=30 alt="Defense (approximate)" title="Defense (approximate)"></td><td width=50 valign=center align=left><b><font size=+2>150</font></b></td> // <td width=30><img src=http://images.kingdomofloathing.com/itemimages/whiteshield.gif width=30 height=30 alt="Defense (approximate)" title="Defense (approximate)"></td><td width=50 valign=center align=left><b><font size=+2>?</font></b></td> private static final Pattern DEFENSE_PATTERN = Pattern.compile( "Defense \\(approximate\\).*?<font size=\\+2>(.*?)</font>" ); public static String extractMonsterDefense( final String text ) { Matcher matcher = MonsterManuelManager.DEFENSE_PATTERN.matcher( text ); return matcher.find() ? matcher.group( 1 ) : ""; } // <td width=30><img src=http://images.kingdomofloathing.com/itemimages/hp.gif width=30 height=30 alt="Hit Points (approximate)" title="Hit Points (approximate)"></td><td width=50 valign=center align=left><b><font size=+2>1000</font></b></td> // <td width=30><img src=http://images.kingdomofloathing.com/itemimages/hp.gif width=30 height=30 alt="Hit Points (approximate)" title="Hit Points (approximate)"></td><td width=50 valign=center align=left><b><font size=+2>?</font></b></td> private static final Pattern HP_PATTERN = Pattern.compile( "Hit Points \\(approximate\\).*?<font size=\\+2>(.*?)</font>" ); public static String extractMonsterHP( final String text ) { Matcher matcher = MonsterManuelManager.HP_PATTERN.matcher( text ); return matcher.find() ? matcher.group( 1 ) : ""; } // <td width=30><img src=http://images.kingdomofloathing.com/itemimages/beastflavor.gif alt="This monster is a Beast" title="This monster is a Beast" width=30 height=30></td> private static final Pattern PHYLUM_PATTERN = Pattern.compile( "This monster is (?:an? )?(.*?)\"" ); public static String extractMonsterPhylum( final String text ) { Matcher matcher = MonsterManuelManager.PHYLUM_PATTERN.matcher( text ); return matcher.find() ? matcher.group( 1 ) : ""; } // <td width=30><img src=http://images.kingdomofloathing.com/itemimages/circle.gif width=30 height=30 alt="This monster has no particular elemental alignment." title="This monster has no particular elemental alignment."></td> // <td width=30><img src=http://images.kingdomofloathing.com/itemimages/stench.gif width=30 height=30 alt="This monster is Stinky. Stench is weak against Cold and Sleaze." title="This monster is Stinky. Stench is weak against Cold and Sleaze."></td> private static final Pattern ELEMENT_PATTERN = Pattern.compile( "This monster is (Hot|Cold|Spooky|Stinky|Sleazy)" ); public static Element extractMonsterElement( final String text ) { Matcher matcher = MonsterManuelManager.ELEMENT_PATTERN.matcher( text ); if ( !matcher.find() ) { return Element.NONE; } String element = matcher.group( 1 ); return element.equals( "Hot" ) ? Element.HOT : element.equals( "Cold" ) ? Element.COLD : element.equals( "Spooky" ) ? Element.SPOOKY : element.equals( "Stinky" ) ? Element.STENCH : element.equals( "Sleazy" ) ? Element.SLEAZE : Element.NONE; } // <td width=30><img src=http://images.kingdomofloathing.com/itemimages/snail.gif alt="Never wins initiative" title="Never wins initiative" width=30 height=30></td> // <td width=30><img src=http://images.kingdomofloathing.com/itemimages/lightningbolt.gif alt="Always wins initiative" title="Always wins initiative" width=30 height=30></td> // <td width=30><img src=http://images.kingdomofloathing.com/itemimages/watch.gif alt="Initiative +100%" title="Initiative +100%" width=30 height=30></td> private static final Pattern INITIATIVE_PATTERN = Pattern.compile( "\"(Never wins initiative|Always wins initiative|Initiative \\+.*?%)\"" ); public static String extractMonsterInitiative( final String text ) { Matcher matcher = MonsterManuelManager.INITIATIVE_PATTERN.matcher( text ); return matcher.find() ? matcher.group( 1 ) : ""; } private static final Pattern FACTOIDS_PATTERN = Pattern.compile( "<ul>(.*?)</ul>", Pattern.DOTALL ); private static final Pattern FACTOID_PATTERN = Pattern.compile( "<li>(.*?)(?=<li>|$)", Pattern.DOTALL ); public static int countFactoids( final String text ) { int count = 0; Matcher matcher = MonsterManuelManager.FACTOIDS_PATTERN.matcher( text ); if ( matcher.find() ) { Matcher factoids = MonsterManuelManager.FACTOID_PATTERN.matcher( matcher.group( 1 ) ); while ( factoids.find() ) { count += 1; } } return count; } public static final String NO_FACTOIDS = ""; private static int translateManuelId( int id ) { MonsterData monster = MonsterDatabase.findMonsterById( id ); if ( monster == null ) { return id; } String manuelName = monster.getManuelName(); if ( manuelName == null ) { return id; } monster = MonsterDatabase.findMonster( manuelName, false ); return monster != null ? monster.getId() : id; } public static String getManuelText( int id ) { // If we don't know the ID, nothing to be done. if ( id == 0 ) { return MonsterManuelManager.NO_FACTOIDS; } if ( id < 0 ) { id = MonsterManuelManager.translateManuelId( id ); } // See if we have it cached String text = MonsterManuelManager.manuelEntries.get( id ); if ( text == null && id > 0) { // No. Attempt to look up the monster in your quest log MonsterManuelRequest request = new MonsterManuelRequest( id ); RequestThread.postRequest( request ); text = MonsterManuelManager.manuelEntries.get( id ); } return text == null ? MonsterManuelManager.NO_FACTOIDS : text; } public static ArrayList<String> getFactoids( final int id ) { ArrayList<String> list = new ArrayList<String>(); String text = MonsterManuelManager.getManuelText( id ); if ( text == MonsterManuelManager.NO_FACTOIDS ) { return list; } Matcher matcher = MonsterManuelManager.FACTOIDS_PATTERN.matcher( text ); if ( matcher.find() ) { Matcher factoids = MonsterManuelManager.FACTOID_PATTERN.matcher( matcher.group( 1 ) ); while ( factoids.find() ) { list.add( factoids.group( 1 ) ); } } return list; } public static int getFactoidsAvailable( int id, final boolean cachedOnly ) { // If we don't know the ID, nothing to be done. if ( id == 0 ) { return 0; } if ( id < 0 ) { id = MonsterManuelManager.translateManuelId( id ); } // See if we have it cached Integer factoids = MonsterManuelManager.manuelFactoidCounts.get( id ); if ( factoids == null && id > 0 && !cachedOnly ) { // No. Attempt to look up the monster in your quest log MonsterManuelRequest request = new MonsterManuelRequest( id ); RequestThread.postRequest( request ); factoids = MonsterManuelManager.manuelFactoidCounts.get( id ); } return factoids == null ? 0 : factoids.intValue(); } }