/**
* 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.util.ArrayList;
import java.util.List;
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.KoLmafia;
import net.sourceforge.kolmafia.moods.HPRestoreItemList;
import net.sourceforge.kolmafia.objectpool.EffectPool;
import net.sourceforge.kolmafia.persistence.AscensionSnapshot;
import net.sourceforge.kolmafia.persistence.SkillDatabase;
import net.sourceforge.kolmafia.preferences.Preferences;
import net.sourceforge.kolmafia.request.UneffectRequest;
import net.sourceforge.kolmafia.session.ResultProcessor;
import net.sourceforge.kolmafia.utilities.StringUtilities;
import org.json.JSONException;
import org.json.JSONObject;
public class CharSheetRequest
extends GenericRequest
{
private static final Pattern BASE_PATTERN = Pattern.compile( " \\(base: ([\\d,]+)\\)" );
private static final Pattern AVATAR_PATTERN =
Pattern.compile( "<img src=[^>]*?(?:images.kingdomofloathing.com|/images)/([^>\'\"\\s]+)" );
/**
* Constructs a new <code>CharSheetRequest</code>. The data in the KoLCharacter entity will be overridden over
* the course of this request.
*/
public CharSheetRequest()
{
// The only thing to do is to retrieve the page from
// the- all variable initialization comes from
// when the request is actually run.
super( "charsheet.php" );
}
/**
* Runs the request. Note that only the character's statistics are retrieved via this retrieval.
*/
@Override
protected boolean retryOnTimeout()
{
return true;
}
@Override
public String getHashField()
{
return null;
}
@Override
public void run()
{
KoLmafia.updateDisplay( "Retrieving character data..." );
super.run();
}
@Override
public void processResults()
{
CharSheetRequest.parseStatus( this.responseText );
}
public static final void parseStatus( final String responseText )
{
// Set the character's avatar.
Matcher avatarMatcher = CharSheetRequest.AVATAR_PATTERN.matcher( responseText );
if ( avatarMatcher.find() )
{
KoLCharacter.setAvatar( avatarMatcher.group( 1 ) );
}
// Strip all of the HTML from the server reply
// and then figure out what to do from there.
String token = "";
StringTokenizer cleanContent =
new StringTokenizer( responseText.replaceAll( "><", "" ).replaceAll( "<.*?>", "\n" ), "\n" );
while ( !token.startsWith( " (#" ) )
{
token = cleanContent.nextToken();
}
KoLCharacter.setUserId( StringUtilities.parseInt( token.substring( 3, token.length() - 1 ) ) );
GenericRequest.skipTokens( cleanContent, 1 );
String className = cleanContent.nextToken().trim();
// Hit point parsing begins with the first index of
// the words indicating that the upcoming token will
// show the HP values (Current, Maximum).
while ( !token.startsWith( "Current" ) )
{
token = cleanContent.nextToken();
}
int currentHP = GenericRequest.intToken( cleanContent );
while ( !token.startsWith( "Maximum" ) )
{
token = cleanContent.nextToken();
}
int maximumHP = GenericRequest.intToken( cleanContent );
token = cleanContent.nextToken();
KoLCharacter.setHP( currentHP, maximumHP, CharSheetRequest.retrieveBase( token, maximumHP ) );
// Mana point parsing is exactly the same as hit point
// parsing - so this is just a copy-paste of the code.
if ( !KoLCharacter.inZombiecore() )
{
// Zombie Masters have no MP
while ( !token.startsWith( "Current" ) )
{
token = cleanContent.nextToken();
}
int currentMP = GenericRequest.intToken( cleanContent );
while ( !token.startsWith( "Maximum" ) )
{
token = cleanContent.nextToken();
}
int maximumMP = GenericRequest.intToken( cleanContent );
token = cleanContent.nextToken();
KoLCharacter.setMP( currentMP, maximumMP, CharSheetRequest.retrieveBase( token, maximumMP ) );
}
else
{
// *** They DO have a Horde.
while ( !token.startsWith( "Zombie Horde" ) )
{
token = cleanContent.nextToken();
}
int horde = GenericRequest.intToken( cleanContent );
KoLCharacter.setMP( horde, horde, horde );
}
// Players with a custom title will have their actual class shown in this area.
while ( !token.startsWith( "Mus" ) )
{
if ( token.equals( "Class:" ) )
{
className = cleanContent.nextToken().trim();
break;
}
token = cleanContent.nextToken();
}
// Next, you begin parsing the different stat points;
// this involves hunting for the stat point's name,
// skipping the appropriate number of tokens, and then
// reading in the numbers.
long[] mus = CharSheetRequest.findStatPoints( cleanContent, token, "Mus" );
long[] mys = CharSheetRequest.findStatPoints( cleanContent, token, "Mys" );
long[] mox = CharSheetRequest.findStatPoints( cleanContent, token, "Mox" );
KoLCharacter.setStatPoints( (int)mus[ 0 ], mus[ 1 ], (int)mys[ 0 ], mys[ 1 ], (int)mox[ 0 ], mox[ 1 ] );
// Drunkenness may or may not exist (in other words,
// if the character is not drunk, nothing will show
// up). Therefore, parse it if it exists; otherwise,
// parse until the "Adventures remaining:" token.
while ( !token.startsWith( "Temul" ) && !token.startsWith( "Inebr" ) && !token.startsWith( "Tipsi" ) && !token.startsWith( "Drunk" ) && !token.startsWith( "Adven" ) )
{
token = cleanContent.nextToken();
}
if ( !token.startsWith( "Adven" ) )
{
KoLCharacter.setInebriety( GenericRequest.intToken( cleanContent ) );
while ( !token.startsWith( "Adven" ) )
{
token = cleanContent.nextToken();
}
}
else
{
KoLCharacter.setInebriety( 0 );
}
// Now parse the number of adventures remaining,
// the monetary value in the character's pocket,
// and the number of turns accumulated.
int oldAdventures = KoLCharacter.getAdventuresLeft();
int newAdventures = GenericRequest.intToken( cleanContent );
ResultProcessor.processAdventuresLeft( newAdventures - oldAdventures );
while ( !token.startsWith( "Meat" ) )
{
token = cleanContent.nextToken();
}
KoLCharacter.setAvailableMeat( GenericRequest.intToken( cleanContent ) );
// Determine the player's ascension count, if any.
// This is seen by whether or not the word "Ascensions"
// appears in their player profile.
if ( responseText.indexOf( "Ascensions:" ) != -1 )
{
while ( !token.startsWith( "Ascensions" ) )
{
token = cleanContent.nextToken();
}
KoLCharacter.setAscensions( GenericRequest.intToken( cleanContent ) );
}
// There may also be a "turns this run" field which
// allows you to have a Ronin countdown.
boolean runStats = responseText.indexOf( "(this run)" ) != -1;
while ( !token.startsWith( "Turns" ) ||
( runStats && token.indexOf( "(this run)" ) == -1 ) )
{
token = cleanContent.nextToken();
}
KoLCharacter.setCurrentRun( GenericRequest.intToken( cleanContent ) );
while ( !token.startsWith( "Days" ) ||
( runStats && token.indexOf( "(this run)" ) == -1 ) )
{
token = cleanContent.nextToken();
}
KoLCharacter.setCurrentDays( GenericRequest.intToken( cleanContent ) );
// Determine the player's zodiac sign, if any. We
// could read the path in next, but it's easier to
// read it from the full response text.
if ( responseText.contains( "Sign:" ) )
{
while ( !cleanContent.nextToken().startsWith( "Sign:" ) )
{
;
}
KoLCharacter.setSign( cleanContent.nextToken() );
}
// This is where Path: optionally appears
KoLCharacter.setRestricted( responseText.contains( "standard.php" ) );
// Consumption restrictions have special messages.
//
// "You may not eat or drink anything."
// "You may not eat any food or drink any non-alcoholic beverages."
// "You may not consume any alcohol."
KoLCharacter.setConsumptionRestriction(
responseText.contains( "You may not eat or drink anything." ) ?
AscensionSnapshot.OXYGENARIAN :
responseText.contains( "You may not eat any food or drink any non-alcoholic beverages." ) ?
AscensionSnapshot.BOOZETAFARIAN :
responseText.contains( "You may not consume any alcohol." ) ?
AscensionSnapshot.TEETOTALER :
AscensionSnapshot.NOPATH );
// You are in Hardcore mode, and may not receive items or buffs
// from other players.
boolean hardcore = responseText.contains( "You are in Hardcore mode" );
KoLCharacter.setHardcore( hardcore );
// You may not receive items from other players until you have
// played # more Adventures.
KoLCharacter.setRonin( responseText.contains( "You may not receive items from other players" ) );
// Deduce interaction from above settings
CharPaneRequest.setInteraction();
// See if the player has a store
KoLCharacter.setStore( responseText.contains( "Mall of Loathing" ) );
// See if the player has a display case
KoLCharacter.setDisplayCase( responseText.contains( "in the Museum" ) );
while ( !token.startsWith( "Skill" ) )
{
token = cleanContent.nextToken();
}
// The first token says "(click the skill name for more
// information)" which is not really a skill.
GenericRequest.skipTokens( cleanContent, 1 );
token = cleanContent.nextToken();
List<UseSkillRequest> newSkillSet = new ArrayList<UseSkillRequest>();
List<UseSkillRequest> permedSkillSet = new ArrayList<UseSkillRequest>();
// Loop until we get to Current Familiar, since everything
// before that contains the player's skills.
while ( !token.startsWith( "Current" ) && !token.startsWith( "[" ) )
{
if ( SkillDatabase.contains( token ) )
{
String skillName = token;
UseSkillRequest skill = UseSkillRequest.getUnmodifiedInstance( skillName );
boolean shouldAddSkill = true;
if ( SkillDatabase.isBookshelfSkill( skillName ) )
{
shouldAddSkill = ( !KoLCharacter.inBadMoon() && !KoLCharacter.inAxecore() ) ||
KoLCharacter.kingLiberated();
}
if ( skillName.equals( "Transcendent Olfaction" ) )
{
shouldAddSkill = ( !KoLCharacter.inBadMoon() && !KoLCharacter.inAxecore() ) ||
KoLCharacter.skillsRecalled();
}
if ( shouldAddSkill )
{
newSkillSet.add( skill );
}
if ( !cleanContent.hasMoreTokens() )
{
break;
}
token = cleanContent.nextToken();
// (<b>HP</b>)
if ( token.equals( "(" ) || token.equals( " (" ) )
{
GenericRequest.skipTokens( cleanContent, 2 );
permedSkillSet.add( skill );
}
// (P)
else if ( token.equals( "(P)" ) || token.equals( " (P)" ) )
{
permedSkillSet.add( skill );
}
else
{
continue;
}
}
// No more tokens if no familiar equipped
if ( !cleanContent.hasMoreTokens() )
{
break;
}
token = cleanContent.nextToken();
}
// The Smile of Mr. A no longer appears on the char sheet
if ( Preferences.getInteger( "goldenMrAccessories" ) > 0 )
{
UseSkillRequest skill = UseSkillRequest.getUnmodifiedInstance( "The Smile of Mr. A." );
newSkillSet.add( skill );
}
// Toggle Optimality does not appear on the char sheet
if ( Preferences.getInteger( "skillLevel7254" ) > 0 )
{
UseSkillRequest skill = UseSkillRequest.getUnmodifiedInstance( "Toggle Optimality" );
newSkillSet.add( skill );
}
// If you have the Cowrruption effect, you can Absorb Cowrruption if a Cow Puncher
if ( KoLConstants.activeEffects.contains( EffectPool.get( EffectPool.COWRRUPTION ) ) && KoLCharacter.getClassType() == KoLCharacter.COWPUNCHER )
{
UseSkillRequest skill = UseSkillRequest.getUnmodifiedInstance( "Absorb Cowrruption" );
newSkillSet.add( skill );
}
// Set the skills that we saw
KoLCharacter.setAvailableSkills( newSkillSet );
KoLCharacter.setPermedSkills( permedSkillSet );
// Finally, set the class name that we figured out.
KoLCharacter.setClassName( className );
// Update uneffect methods and heal amounts for updated skills
UneffectRequest.reset();
HPRestoreItemList.updateHealthRestored();
}
/**
* Helper method used to find the statistic points. This method was created because statistic-point finding is
* exactly the same for every statistic point.
*
* @param tokenizer The <code>StringTokenizer</code> containing the tokens to be parsed
* @param searchString The search string indicating the beginning of the statistic
* @return The 2-element array containing the parsed statistics
*/
private static final long[] findStatPoints( final StringTokenizer tokenizer, String token, final String searchString )
{
long[] stats = new long[ 2 ];
while ( !token.startsWith( searchString ) )
{
token = tokenizer.nextToken();
}
stats[ 0 ] = GenericRequest.intToken( tokenizer );
token = tokenizer.nextToken();
int base = CharSheetRequest.retrieveBase( token, (int) stats[ 0 ] );
while ( !token.startsWith( "(" ) )
{
token = tokenizer.nextToken();
}
stats[ 1 ] = KoLCharacter.calculateSubpoints( base, GenericRequest.intToken( tokenizer ) );
return stats;
}
/**
* Utility method for retrieving the base value for a statistic, given the tokenizer, and assuming that the base
* might be located in the next token. If it isn't, the default value is returned instead. Note that this advances
* the <code>StringTokenizer</code> one token ahead of the base value for the statistic.
*
* @param st The <code>StringTokenizer</code> possibly containing the base value
* @param defaultBase The value to return, if no base value is found
* @return The parsed base value, or the default value if no base value is found
*/
private static final int retrieveBase( final String token, final int defaultBase )
{
Matcher baseMatcher = CharSheetRequest.BASE_PATTERN.matcher( token );
return baseMatcher.find() ? StringUtilities.parseInt( baseMatcher.group( 1 ) ) : defaultBase;
}
public static final void parseStatus( final JSONObject JSON )
throws JSONException
{
int muscle = JSON.getInt( "muscle" );
long rawmuscle = JSON.getLong( "rawmuscle" );
int mysticality = JSON.getInt( "mysticality" );
long rawmysticality = JSON.getLong( "rawmysticality" );
int moxie = JSON.getInt( "moxie" );
long rawmoxie = JSON.getLong( "rawmoxie" );
KoLCharacter.setStatPoints( muscle, rawmuscle,
mysticality, rawmysticality,
moxie, rawmoxie );
}
}