/** * 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.persistence; import java.io.BufferedReader; import java.util.ArrayList; import java.util.Collections; import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.java.dev.spellcast.utilities.LockableListModel; import net.sourceforge.kolmafia.KoLConstants; import net.sourceforge.kolmafia.KoLConstants.MafiaState; import net.sourceforge.kolmafia.KoLmafia; import net.sourceforge.kolmafia.RequestThread; import net.sourceforge.kolmafia.StaticEntity; import net.sourceforge.kolmafia.objectpool.EffectPool; import net.sourceforge.kolmafia.objectpool.SkillPool; import net.sourceforge.kolmafia.session.BuffBotManager.Offering; import net.sourceforge.kolmafia.session.ContactManager; import net.sourceforge.kolmafia.utilities.FileUtilities; import net.sourceforge.kolmafia.utilities.StringUtilities; public class BuffBotDatabase { public static final String OPTOUT_URL = "http://forums.kingdomofloathing.com/"; private static final Pattern BUFFDATA_PATTERN = Pattern.compile( "<buffdata>(.*?)</buffdata>", Pattern.DOTALL ); private static final Pattern NAME_PATTERN = Pattern.compile( "<name>(.*?)</name>", Pattern.DOTALL ); private static final Pattern PRICE_PATTERN = Pattern.compile( "<price>(.*?)</price>", Pattern.DOTALL ); private static final Pattern TURN_PATTERN = Pattern.compile( "<turns>(.*?)</turns>", Pattern.DOTALL ); private static final Pattern FREE_PATTERN = Pattern.compile( "<philanthropic>(.*?)</philanthropic>", Pattern.DOTALL ); private static boolean hasNameList = false; private static boolean isInitialized = false; private static final ArrayList<String> nameList = new ArrayList<String>(); private static final TreeMap<String, String[]> buffDataMap = new TreeMap<String, String[]>(); private static final TreeMap<String, LockableListModel<Offering>> normalOfferings = new TreeMap<String, LockableListModel<Offering>>(); private static final TreeMap<String, LockableListModel<Offering>> freeOfferings = new TreeMap<String, LockableListModel<Offering>>(); public static final int getOffering( String name, final int amount ) { // If you have no idea what the names present in // the database are, go ahead and refresh it. if ( !BuffBotDatabase.hasNameList ) { String[] data; BufferedReader reader = FileUtilities.getVersionedReader( "buffbots.txt", KoLConstants.BUFFBOTS_VERSION ); while ( ( data = FileUtilities.readData( reader ) ) != null ) { if ( data.length >= 2 ) { ContactManager.registerPlayerId( data[ 0 ], data[ 1 ] ); BuffBotDatabase.nameList.add( data[ 0 ].toLowerCase() ); BuffBotDatabase.buffDataMap.put( data[ 0 ].toLowerCase(), data ); } } BuffBotDatabase.hasNameList = true; try { reader.close(); } catch ( Exception e ) { StaticEntity.printStackTrace( e ); } } // If the player is not a known buffbot, go ahead // and allow the amount. name = ContactManager.getPlayerName( name ).toLowerCase(); if ( !BuffBotDatabase.nameList.contains( name ) ) { return amount; } // Otherwise, retrieve the information for the buffbot // to see if there are non-philanthropic offerings. String[] data = (String[]) BuffBotDatabase.buffDataMap.get( name ); if ( data[ 2 ].equals( BuffBotDatabase.OPTOUT_URL ) ) { KoLmafia.updateDisplay( MafiaState.ABORT, data[ 0 ] + " has requested to be excluded from scripted requests." ); return 0; } RequestThread.runInParallel( new DynamicBotFetcher( data ), false ); // If this is clearly not a philanthropic buff, then // no alternative amount needs to be sent. LockableListModel<Offering> possibles = BuffBotDatabase.getPhilanthropicOfferings( data[ 0 ] ); if ( possibles == null || possibles.isEmpty() ) { return amount; } Offering current = null; boolean foundMatch = false; for ( int i = 0; i < possibles.size() && !foundMatch; ++i ) { current = possibles.get( i ); if ( current.getPrice() == amount ) { foundMatch = true; } } if ( !foundMatch ) { return amount; } // If this offers more than 300 turns, chances are it's not // a philanthropic buff. Buff packs are also not protected // because the logic is complicated. if ( current.buffs == null || current.buffs.length > 1 ) { return amount; } // If no alternative exists, go ahead and return the // original amount. LockableListModel<Offering> alternatives = BuffBotDatabase.getStandardOfferings( data[ 0 ] ); if ( alternatives == null || alternatives.isEmpty() ) { return amount; } String matchBuff = current.buffs[ 0 ]; int matchTurns = current.turns[ 0 ]; String testBuff = null; int testTurns = 0; Offering bestMatch = null; int bestTurns = 0; // Search for the best match, which is defined as the // buff which provides the closest number of turns. for ( int i = 0; i < alternatives.size(); ++i ) { current = alternatives.get( i ); if ( current.buffs.length > 1 ) { continue; } testBuff = current.buffs[ 0 ]; testTurns = current.turns[ 0 ]; if ( !matchBuff.equals( testBuff ) ) { continue; } if ( bestMatch == null || testTurns >= matchTurns && testTurns < bestTurns ) { bestMatch = current; bestTurns = testTurns; } } if ( bestMatch == null ) { return amount; } int effectId = EffectDatabase.getEffectId( bestMatch.buffs[ 0 ] ); if ( KoLConstants.activeEffects.contains( EffectPool.get( effectId ) ) ) { return 0; } KoLmafia.updateDisplay( "Converted to non-philanthropic request: " + bestMatch.turns[ 0 ] + " turns of " + bestMatch.buffs[ 0 ] + " for " + bestMatch.getPrice() + " Meat." ); return bestMatch.getPrice(); } public static final boolean hasOfferings() { if ( !BuffBotDatabase.isInitialized ) { BuffBotDatabase.configureBuffBots(); } return !BuffBotDatabase.normalOfferings.isEmpty() || !BuffBotDatabase.freeOfferings.isEmpty(); } public static final String[] getCompleteBotList() { ArrayList<String> completeList = new ArrayList<String>(); completeList.addAll( BuffBotDatabase.normalOfferings.keySet() ); for ( String bot : BuffBotDatabase.freeOfferings.keySet() ) { if ( !completeList.contains( bot ) ) { completeList.add( bot ); } } Collections.sort( completeList, String.CASE_INSENSITIVE_ORDER ); completeList.add( 0, "" ); return completeList.toArray( new String[0] ); } public static final LockableListModel<Offering> getStandardOfferings( final String botName ) { return botName != null && BuffBotDatabase.normalOfferings.containsKey( botName ) ? BuffBotDatabase.normalOfferings.get( botName ) : new LockableListModel<Offering>(); } public static final LockableListModel<Offering> getPhilanthropicOfferings( final String botName ) { return botName != null && BuffBotDatabase.freeOfferings.containsKey( botName ) ? BuffBotDatabase.freeOfferings.get( botName ) : new LockableListModel<Offering>(); } private static final void configureBuffBots() { if ( BuffBotDatabase.isInitialized ) { return; } KoLmafia.updateDisplay( "Configuring dynamic buff prices..." ); String[] data = null; BufferedReader reader = FileUtilities.getVersionedReader( "buffbots.txt", KoLConstants.BUFFBOTS_VERSION ); while ( ( data = FileUtilities.readData( reader ) ) != null ) { if ( data.length == 3 ) { RequestThread.postRequest( new DynamicBotFetcher( data ) ); } } try { reader.close(); } catch ( Exception e ) { StaticEntity.printStackTrace( e ); } KoLmafia.updateDisplay( "Buff prices fetched." ); BuffBotDatabase.isInitialized = true; } private static class DynamicBotFetcher implements Runnable { private final String botName, location; public DynamicBotFetcher( final String[] data ) { this.botName = data[ 0 ]; this.location = data[ 2 ]; ContactManager.registerPlayerId( data[ 0 ], data[ 1 ] ); } public void run() { if ( BuffBotDatabase.freeOfferings.containsKey( this.botName ) || BuffBotDatabase.normalOfferings.containsKey( this.botName ) ) { return; } if ( this.location.equals( BuffBotDatabase.OPTOUT_URL ) ) { BuffBotDatabase.freeOfferings.put( this.botName, new LockableListModel<Offering>() ); BuffBotDatabase.normalOfferings.put( this.botName, new LockableListModel<Offering>() ); return; } StringBuilder responseText = new StringBuilder(); BufferedReader reader = FileUtilities.getReader( this.location ); if ( reader == null ) { return; } try { String line; while ( ( line = reader.readLine() ) != null ) { responseText.append( line ); } } catch ( Exception e ) { return; } // Now, for the infamous XML parse tree. Rather than building // a tree (which would probably be smarter), simply do regular // expression matching and assume we have a properly-structured // XML file -- which is assumed because of the XSLT. Matcher nodeMatcher = BuffBotDatabase.BUFFDATA_PATTERN.matcher( responseText.toString() ); LockableListModel<Offering> freeBuffs = new LockableListModel<Offering>(); LockableListModel<Offering> normalBuffs = new LockableListModel<Offering>(); Matcher nameMatcher, priceMatcher, turnMatcher, freeMatcher; while ( nodeMatcher.find() ) { String buffMatch = nodeMatcher.group( 1 ); nameMatcher = BuffBotDatabase.NAME_PATTERN.matcher( buffMatch ); priceMatcher = BuffBotDatabase.PRICE_PATTERN.matcher( buffMatch ); turnMatcher = BuffBotDatabase.TURN_PATTERN.matcher( buffMatch ); freeMatcher = BuffBotDatabase.FREE_PATTERN.matcher( buffMatch ); if ( nameMatcher.find() && priceMatcher.find() && turnMatcher.find() ) { String name = nameMatcher.group( 1 ).trim(); if ( name.startsWith( "Jala" ) ) { name = SkillDatabase.getSkillName( SkillPool.JALAPENO_SAUCESPHERE ); } int price = StringUtilities.parseInt( priceMatcher.group( 1 ).trim() ); int turns = StringUtilities.parseInt( turnMatcher.group( 1 ).trim() ); boolean philanthropic = freeMatcher.find() ? freeMatcher.group( 1 ).trim().equals( "true" ) : false; LockableListModel<Offering> tester = philanthropic ? freeBuffs : normalBuffs; Offering priceMatch = null; Offering currentTest = null; for ( int i = 0; i < tester.size(); ++i ) { currentTest = (Offering) tester.get( i ); if ( currentTest.getPrice() == price ) { priceMatch = currentTest; } } if ( priceMatch == null ) { tester.add( new Offering( name, this.botName, price, turns, philanthropic ) ); } else { priceMatch.addBuff( name, turns ); } } } // If the bot offers some philanthropic buffs, then // add them to the philanthropic bot list. if ( !freeBuffs.isEmpty() ) { freeBuffs.sort(); BuffBotDatabase.freeOfferings.put( this.botName, freeBuffs ); } if ( !normalBuffs.isEmpty() ) { normalBuffs.sort(); BuffBotDatabase.normalOfferings.put( this.botName, normalBuffs ); } } } }