/** * 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.request; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import java.util.StringTokenizer; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.sourceforge.kolmafia.KoLCharacter; import net.sourceforge.kolmafia.KoLConstants; import net.sourceforge.kolmafia.RequestLogger; import net.sourceforge.kolmafia.RequestThread; import net.sourceforge.kolmafia.StaticEntity; import net.sourceforge.kolmafia.objectpool.IntegerPool; import net.sourceforge.kolmafia.objectpool.ItemPool; import net.sourceforge.kolmafia.persistence.EquipmentDatabase; import net.sourceforge.kolmafia.persistence.ItemDatabase; import net.sourceforge.kolmafia.preferences.Preferences; import net.sourceforge.kolmafia.session.ClanManager; import net.sourceforge.kolmafia.session.ContactManager; import net.sourceforge.kolmafia.session.InventoryManager; import net.sourceforge.kolmafia.session.ResultProcessor; import net.sourceforge.kolmafia.utilities.StringUtilities; public class ProfileRequest extends GenericRequest implements Comparable<ProfileRequest> { private static final Pattern DATA_PATTERN = Pattern.compile( "<td.*?>(.*?)</td>" ); private static final Pattern NUMERIC_PATTERN = Pattern.compile( "\\d+" ); private static final Pattern CLAN_ID_PATTERN = Pattern.compile( "Clan: <b><a class=nounder href=\"showclan\\.php\\?whichclan=(\\d+)\">(.*?)</a>" ); private static final SimpleDateFormat INPUT_FORMAT = new SimpleDateFormat( "MMMM d, yyyy", Locale.US ); public static final SimpleDateFormat OUTPUT_FORMAT = new SimpleDateFormat( "MM/dd/yy", Locale.US ); private final String playerName; private String playerId; private Integer playerLevel; private boolean isHardcore; private String restriction; private Integer currentMeat; private Integer turnsPlayed, currentRun; private String classType; private Date created, lastLogin; private String food, drink; private Integer ascensionCount, pvpRank, karma; private Integer muscle, mysticism, moxie; private String title, rank; private String clanName; private int clanId; private int equipmentPower; public ProfileRequest( final String playerName ) { super( "showplayer.php" ); if ( playerName.startsWith( "#" ) ) { this.playerId = playerName.substring( 1 ); this.playerName = ContactManager.getPlayerName( this.playerId ); } else { this.playerName = playerName; this.playerId = ContactManager.getPlayerId( playerName ); } this.addFormField( "who", this.playerId ); this.muscle = IntegerPool.get( 0 ); this.mysticism = IntegerPool.get( 0 ); this.moxie = IntegerPool.get( 0 ); this.karma = IntegerPool.get( 0 ); } @Override protected boolean retryOnTimeout() { return true; } /** * Internal method used to refresh the fields of the profile request based on the response text. This should be * called after the response text is already retrieved. */ private void refreshFields() { // Nothing to refresh if no text if ( this.responseText == null || this.responseText.length() == 0 ) { return; } this.isHardcore = this.responseText.contains( "<b>(Hardcore)</b></td>" ); // This is a massive replace which makes the profile easier to // parse and re-represent inside of editor panes. String cleanHTML = this.responseText.replaceAll( "><", "" ).replaceAll( "<.*?>", "\n" ); StringTokenizer st = new StringTokenizer( cleanHTML, "\n" ); String token = st.nextToken(); this.playerLevel = IntegerPool.get( 0 ); this.classType = "Recent Ascension"; this.currentMeat = IntegerPool.get( 0 ); this.ascensionCount = IntegerPool.get( 0 ); this.turnsPlayed = IntegerPool.get( 0 ); this.created = new Date(); this.lastLogin = new Date(); this.food = "none"; this.drink = "none"; this.pvpRank = IntegerPool.get( 0 ); if ( cleanHTML.contains( "\nClass:" ) ) { // has custom title while ( !st.nextToken().startsWith( " (#" ) ) { } String title = st.nextToken(); // custom title, may include level // Next token will be one of: // (Level n), if the custom title doesn't include the level // (In Ronin) or possibly similar messages // Class:, if neither of the above applies token = st.nextToken(); if ( token.startsWith( "(Level" ) ) { this.playerLevel = IntegerPool.get( StringUtilities.parseInt( token.substring( 6 ).trim() ) ); } else { // Must attempt to parse the level out of the custom title. // This is inherently inaccurate, since the title can contain other digits, // before, after, or adjacent to the level. Matcher m = ProfileRequest.NUMERIC_PATTERN.matcher( title ); if ( m.find() && m.group().length() < 5 ) { this.playerLevel = IntegerPool.get( StringUtilities.parseInt( m.group() ) ); } } while ( !token.startsWith( "Class" ) ) { token = st.nextToken(); } this.classType = KoLCharacter.getClassType( st.nextToken().trim() ); } else { // no custom title if ( !cleanHTML.contains( "Level" ) ) { return; } while ( !token.contains( "Level" ) ) { token = st.nextToken(); } this.playerLevel = IntegerPool.get( StringUtilities.parseInt( token.substring( 5 ).trim() ) ); this.classType = KoLCharacter.getClassType( st.nextToken().trim() ); } if ( cleanHTML.contains( "\nAscensions" ) && cleanHTML.contains( "\nPath" ) ) { while ( !st.nextToken().startsWith( "Path" ) ) { ; } this.restriction = st.nextToken().trim(); } else { this.restriction = "No-Path"; } if ( cleanHTML.contains( "\nMeat:" ) ) { while ( !st.nextToken().startsWith( "Meat" ) ) { ; } this.currentMeat = IntegerPool.get( StringUtilities.parseInt( st.nextToken().trim() ) ); } if ( cleanHTML.contains( "\nAscensions" ) ) { while ( !st.nextToken().startsWith( "Ascensions" ) ) { ; } st.nextToken(); this.ascensionCount = IntegerPool.get( StringUtilities.parseInt( st.nextToken().trim() ) ); } else { this.ascensionCount = IntegerPool.get( 0 ); } while ( !st.nextToken().startsWith( "Turns" ) ) { ; } this.turnsPlayed = IntegerPool.get( StringUtilities.parseInt( st.nextToken().trim() ) ); if ( cleanHTML.contains( "\nAscensions" ) ) { while ( !st.nextToken().startsWith( "Turns" ) ) { ; } this.currentRun = IntegerPool.get( StringUtilities.parseInt( st.nextToken().trim() ) ); } else { this.currentRun = this.turnsPlayed; } String dateString = null; while ( !st.nextToken().startsWith( "Account" ) ) { ; } try { dateString = st.nextToken().trim(); this.created = ProfileRequest.INPUT_FORMAT.parse( dateString ); } catch ( Exception e ) { StaticEntity.printStackTrace( e, "Could not parse date \"" + dateString + "\"" ); this.created = new Date(); } while ( !st.nextToken().startsWith( "Last" ) ) { ; } try { dateString = st.nextToken().trim(); this.lastLogin = ProfileRequest.INPUT_FORMAT.parse( dateString ); } catch ( Exception e ) { StaticEntity.printStackTrace( e, "Could not parse date \"" + dateString + "\"" ); this.lastLogin = this.created; } if ( cleanHTML.contains( "\nFavorite Food" ) ) { while ( !st.nextToken().startsWith( "Favorite" ) ) { ; } this.food = st.nextToken().trim(); } else { this.food = "none"; } if ( cleanHTML.contains( "\nFavorite Booze" ) ) { while ( !st.nextToken().startsWith( "Favorite" ) ) { ; } this.drink = st.nextToken().trim(); } else { this.drink = "none"; } if ( cleanHTML.contains( "\nFame" ) ) { while ( !st.nextToken().startsWith( "Fame" ) ) { ; } this.pvpRank = IntegerPool.get( StringUtilities.parseInt( st.nextToken().trim() ) ); } else { this.pvpRank = IntegerPool.get( 0 ); } this.equipmentPower = 0; if ( cleanHTML.contains( "\nEquipment" ) ) { while ( !st.nextToken().startsWith( "Equipment" ) ) { ; } int itemId = -1; while ( EquipmentDatabase.contains( itemId = ItemDatabase.getItemId( token = st.nextToken() ) ) ) { switch ( ItemDatabase.getConsumptionType( itemId ) ) { case KoLConstants.EQUIP_HAT: case KoLConstants.EQUIP_PANTS: case KoLConstants.EQUIP_SHIRT: this.equipmentPower += EquipmentDatabase.getPower( itemId ); break; } } } if ( cleanHTML.contains( "\nClan" ) ) { Matcher m = CLAN_ID_PATTERN.matcher( this.responseText ); if ( m.find() ) { this.clanId = StringUtilities.parseInt( m.group( 1 ) ); this.clanName = m.group( 2 ); } } else { this.clanId = -1; this.clanName = ""; } // If we're looking at our own profile, update ClanManager if ( this.playerId.equals( KoLCharacter.getPlayerId() ) ) { ClanManager.setClanId( this.clanId ); ClanManager.setClanName( this.clanName ); } if ( cleanHTML.contains( "\nTitle" ) ) { while ( !token.startsWith( "Title" ) ) { token = st.nextToken(); } this.title = st.nextToken(); } } /** * static final method used by the clan manager in order to get an * instance of a profile request based on the data already known. */ public static final ProfileRequest getInstance( final String playerName, final String playerId, final String playerLevel, final String responseText, final String rosterRow ) { ProfileRequest instance = new ProfileRequest( playerName ); instance.playerId = playerId; // First, initialize the level field for the // current player. if ( playerLevel == null ) { instance.playerLevel = IntegerPool.get( 0 ); } else { instance.playerLevel = Integer.valueOf( playerLevel ); } // Next, refresh the fields for this player. // The response text should be copied over // before this happens. instance.responseText = responseText; instance.refreshFields(); // Next, parse out all the data in the // row of the detail roster table. if ( rosterRow == null ) { instance.muscle = IntegerPool.get( 0 ); instance.mysticism = IntegerPool.get( 0 ); instance.moxie = IntegerPool.get( 0 ); instance.rank = ""; instance.karma = IntegerPool.get( 0 ); } else { Matcher dataMatcher = ProfileRequest.DATA_PATTERN.matcher( rosterRow ); // The name of the player occurs in the first // field of the table. Because you already // know the name of the player, this can be // arbitrarily skipped. dataMatcher.find(); // At some point the player class was added to the table. Skip over it. dataMatcher.find(); // The player's three primary stats appear in // the next three fields of the table. dataMatcher.find(); instance.muscle = IntegerPool.get( StringUtilities.parseInt( dataMatcher.group( 1 ) ) ); dataMatcher.find(); instance.mysticism = IntegerPool.get( StringUtilities.parseInt( dataMatcher.group( 1 ) ) ); dataMatcher.find(); instance.moxie = IntegerPool.get( StringUtilities.parseInt( dataMatcher.group( 1 ) ) ); // The next field contains the total power, // and since this is calculated, it can be // skipped in data retrieval. dataMatcher.find(); // The next three fields contain the ascension // count, number of hardcore runs, and their // pvp ranking. dataMatcher.find(); dataMatcher.find(); dataMatcher.find(); // Next is the player's rank inside of this clan. // Title was removed, so ... not visible here. dataMatcher.find(); instance.rank = dataMatcher.group( 1 ); // The last field contains the total karma // accumulated by this player. dataMatcher.find(); instance.karma = IntegerPool.get( StringUtilities.parseInt( dataMatcher.group( 1 ) ) ); } return instance; } public void initialize() { if ( this.responseText == null ) { RequestThread.postRequest( this ); } } public String getPlayerName() { return this.playerName; } public String getPlayerId() { return this.playerId; } public String getClanName() { this.initialize(); return this.clanName; } public int getClanId() { this.initialize(); return this.clanId; } public boolean isHardcore() { this.initialize(); return this.isHardcore; } public String getRestriction() { this.initialize(); return this.restriction; } public String getClassType() { if ( this.classType == null ) { this.initialize(); } return this.classType; } public Integer getPlayerLevel() { if ( this.playerLevel == null || this.playerLevel.intValue() == 0 ) { this.initialize(); } return this.playerLevel; } public Integer getCurrentMeat() { this.initialize(); return this.currentMeat; } public Integer getTurnsPlayed() { this.initialize(); return this.turnsPlayed; } public Integer getCurrentRun() { this.initialize(); return this.currentRun; } public Date getLastLogin() { this.initialize(); return this.lastLogin; } public Date getCreation() { this.initialize(); return this.created; } public String getCreationAsString() { this.initialize(); return ProfileRequest.OUTPUT_FORMAT.format( this.created ); } public String getLastLoginAsString() { this.initialize(); return ProfileRequest.OUTPUT_FORMAT.format( this.lastLogin ); } public String getFood() { this.initialize(); return this.food; } public String getDrink() { this.initialize(); return this.drink; } public Integer getPvpRank() { if ( this.pvpRank == null || this.pvpRank.intValue() == 0 ) { this.initialize(); } return this.pvpRank; } public Integer getMuscle() { this.initialize(); return this.muscle; } public Integer getMysticism() { this.initialize(); return this.mysticism; } public Integer getMoxie() { this.initialize(); return this.moxie; } public Integer getPower() { this.initialize(); return IntegerPool.get( this.muscle.intValue() + this.mysticism.intValue() + this.moxie.intValue() ); } public Integer getEquipmentPower() { this.initialize(); return IntegerPool.get( this.equipmentPower ); } public String getTitle() { this.initialize(); return this.title != null ? this.title : ClanManager.getTitle( this.playerName ); } public String getRank() { this.initialize(); return this.rank; } public Integer getKarma() { this.initialize(); return this.karma; } public Integer getAscensionCount() { this.initialize(); return this.ascensionCount; } private static final Pattern GOBACK_PATTERN = Pattern.compile( "https?://www\\.kingdomofloathing\\.com/ascensionhistory\\.php?back=self&who=([\\d]+)" ); @Override public void processResults() { Matcher dataMatcher = ProfileRequest.GOBACK_PATTERN.matcher( this.responseText ); if ( dataMatcher.find() ) { this.responseText = dataMatcher.replaceFirst( "../ascensions/" + ClanManager.getURLName( ContactManager.getPlayerName( dataMatcher.group( 1 ) ) ) ); } this.refreshFields(); } public int compareTo( final ProfileRequest o ) { if ( o == null || !( o instanceof ProfileRequest ) ) { return -1; } ProfileRequest pr = (ProfileRequest) o; if ( this.getPvpRank().intValue() != pr.getPvpRank().intValue() ) { return this.getPvpRank().intValue() - pr.getPvpRank().intValue(); } return this.getPlayerLevel().intValue() - pr.getPlayerLevel().intValue(); } private static final Pattern WHO_PATTERN = Pattern.compile( "who=(\\d+)" ); public static int getWho( final String urlString ) { Matcher matcher = ProfileRequest.WHO_PATTERN.matcher( urlString ); return matcher.find() ? StringUtilities.parseInt( matcher.group( 1 ) ) : 0; } private static final Pattern EQUIPMENT_PATTERN = Pattern.compile( "<center>Equipment:</center>(<table>.*?</table>)" ); private static final Pattern FAMILIAR_PATTERN = Pattern.compile( "<p>Familiar:.*?(<table>.*?</table>)" ); public static void parseResponse( String location, String responseText ) { int who = ProfileRequest.getWho( location ); if ( who == 1 ) // if we're looking at Jick's profile { if ( InventoryManager.hasItem( ItemPool.PSYCHOANALYTIC_JAR ) && // and we have an empty jar !Preferences.getBoolean( "_psychoJarFilled" ) ) // and we haven't already filled a jar { Preferences.setString( "_jickJarAvailable", Boolean.toString( responseText.contains( "psychoanalytic jar" ) ) ); } if ( responseText.contains( "jar of psychoses (Jick)" ) ) { ResultProcessor.processItem( false, "You acquire an item:", ItemPool.get( ItemPool.JICK_JAR ), null ); } } if ( location.contains( "action=crossthestreams" ) && ( responseText.contains( "creating an intense but localized nuclear reaction" ) || responseText.contains( "You've already crossed the streams today" ) ) ) { Preferences.setBoolean( "_streamsCrossed", true ); } // Look for new items in equipment Matcher matcher = ProfileRequest.EQUIPMENT_PATTERN.matcher( responseText ); if ( matcher.find() ) { ItemDatabase.parseNewItems( matcher.group( 1 ) ); } // Look for new item on current familiar matcher = ProfileRequest.FAMILIAR_PATTERN.matcher( responseText ); if ( matcher.find() ) { ItemDatabase.parseNewItems( matcher.group( 1 ) ); } } public static boolean registerRequest( final String urlString ) { if ( !urlString.startsWith( "showplayer.php" ) ) { return false; } int who = ProfileRequest.getWho( urlString ); if ( who == 1 ) // if we're looking at Jick's profile { if ( urlString.contains( "action=jung" ) && urlString.contains( "whichperson=jick" ) ) { String message = "Psychoanalyzing Jick"; RequestLogger.updateSessionLog(); RequestLogger.updateSessionLog( message ); return true; } } // No need to log looking at player profiles return true; } }