/**
* 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.io.IOException;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.sourceforge.kolmafia.KoLAdventure;
import net.sourceforge.kolmafia.KoLCharacter;
import net.sourceforge.kolmafia.KoLConstants;
import net.sourceforge.kolmafia.KoLConstants.MafiaState;
import net.sourceforge.kolmafia.KoLmafia;
import net.sourceforge.kolmafia.MonsterData;
import net.sourceforge.kolmafia.RequestLogger;
import net.sourceforge.kolmafia.StaticEntity;
import net.sourceforge.kolmafia.combat.CombatActionManager;
import net.sourceforge.kolmafia.objectpool.AdventurePool;
import net.sourceforge.kolmafia.objectpool.EffectPool;
import net.sourceforge.kolmafia.objectpool.FamiliarPool;
import net.sourceforge.kolmafia.objectpool.ItemPool;
import net.sourceforge.kolmafia.persistence.AdventureDatabase;
import net.sourceforge.kolmafia.persistence.AdventureQueueDatabase;
import net.sourceforge.kolmafia.persistence.AdventureSpentDatabase;
import net.sourceforge.kolmafia.persistence.MonsterDatabase;
import net.sourceforge.kolmafia.persistence.QuestDatabase;
import net.sourceforge.kolmafia.persistence.QuestDatabase.Quest;
import net.sourceforge.kolmafia.preferences.Preferences;
import net.sourceforge.kolmafia.session.BatManager;
import net.sourceforge.kolmafia.session.ChoiceManager;
import net.sourceforge.kolmafia.session.ConsequenceManager;
import net.sourceforge.kolmafia.session.DvorakManager;
import net.sourceforge.kolmafia.session.EncounterManager;
import net.sourceforge.kolmafia.session.EquipmentManager;
import net.sourceforge.kolmafia.session.GoalManager;
import net.sourceforge.kolmafia.session.LouvreManager;
import net.sourceforge.kolmafia.session.ResultProcessor;
import net.sourceforge.kolmafia.session.SorceressLairManager;
import net.sourceforge.kolmafia.session.TavernManager;
import net.sourceforge.kolmafia.session.TurnCounter;
import net.sourceforge.kolmafia.session.WumpusManager;
import net.sourceforge.kolmafia.swingui.RequestSynchFrame;
import net.sourceforge.kolmafia.utilities.HTMLParserUtils;
import net.sourceforge.kolmafia.utilities.StringUtilities;
import net.sourceforge.kolmafia.webui.BarrelDecorator;
import org.htmlcleaner.HtmlCleaner;
import org.htmlcleaner.TagNode;
import org.htmlcleaner.XPatherException;
public class AdventureRequest
extends GenericRequest
{
public static final String NOT_IN_A_FIGHT = "Not in a Fight";
private static final Pattern AREA_PATTERN = Pattern.compile( "(adv|snarfblat)=(\\d*)", Pattern.DOTALL );
// <img id='monpic' src="http://images.kingdomofloathing.com/adventureimages/ssd_sundae.gif" width=100 height=100>
private static final Pattern MONSTER_IMAGE = Pattern.compile( "<img id='monpic' .*?adventureimages/(.*?)\\.gif" );
private static final GenericRequest ZONE_UNLOCK = new GenericRequest( "" );
private final String adventureName;
private final String formSource;
private final String adventureId;
private int override = -1;
/**
* Constructs a new <code>AdventureRequest</code> which executes the adventure designated by the given Id by
* posting to the provided form, notifying the givenof results (or errors).
*
* @param adventureName The name of the adventure location
* @param formSource The form to which the data will be posted
* @param adventureId The identifier for the adventure to be executed
*/
public AdventureRequest( final String adventureName, final String formSource, final String adventureId )
{
super( formSource );
this.adventureName = adventureName;
this.formSource = formSource;
this.adventureId = adventureId;
// The adventure Id is all you need to identify the adventure;
// posting it in the form sent to adventure.php will handle
// everything for you.
// Those that change mid session should be added to run() also.
if ( formSource.equals( "adventure.php" ) )
{
this.addFormField( "snarfblat", adventureId );
}
else if ( formSource.equals( "casino.php" ) )
{
this.addFormField( "action", "slot" );
this.addFormField( "whichslot", adventureId );
}
else if ( formSource.equals( "crimbo10.php" ) )
{
this.addFormField( "place", adventureId );
}
else if ( formSource.equals( "cobbsknob.php" ) )
{
this.addFormField( "action", "throneroom" );
}
else if ( formSource.equals( "friars.php" ) )
{
this.addFormField( "action", "ritual" );
}
else if ( formSource.equals( "invasion.php" ) )
{
this.addFormField( "action", adventureId );
}
else if ( formSource.equals( "mining.php" ) )
{
this.addFormField( "mine", adventureId );
}
else if ( formSource.equals( "place.php" ) )
{
if ( adventureId.equals( "cloudypeak2" ) )
{
this.addFormField( "whichplace", "mclargehuge" );
this.addFormField( "action", adventureId );
}
else if ( this.adventureId.equals( "pyramid_state" ) )
{
this.addFormField( "whichplace", "pyramid" );
StringBuilder action = new StringBuilder();
action.append( adventureId );
action.append( Preferences.getString( "pyramidPosition" ) );
if ( Preferences.getBoolean( "pyramidBombUsed" ) )
{
action.append( "a" );
}
this.addFormField( "action", action.toString() );
}
else if ( this.adventureId.equals( "manor4_chamberboss" ) )
{
this.addFormField( "whichplace", "manor4" );
this.addFormField( "action", adventureId );
}
else if ( this.adventureId.startsWith( "ns_" ) )
{
this.addFormField( "whichplace", "nstower" );
this.addFormField( "action", adventureId );
}
else if ( this.adventureId.equals( "town_eincursion" ) ||
this.adventureId.equals( "town_eicfight2" ))
{
this.addFormField( "whichplace", "town" );
this.addFormField( "action", adventureId );
}
else if ( this.adventureId.equals( "ioty2014_wolf" ) )
{
this.addFormField( "whichplace", "manor4" );
this.addFormField( "action", "wolf_houserun" );
}
}
else if ( !formSource.equals( "basement.php" ) &&
!formSource.equals( "cellar.php" ) &&
!formSource.equals( "barrel.php" ) )
{
this.addFormField( "action", adventureId );
}
}
@Override
protected boolean retryOnTimeout()
{
return true;
}
@Override
public void run()
{
// Prevent the request from happening if they attempted
// to cancel in the delay period.
if ( !KoLmafia.permitsContinue() )
{
return;
}
else if ( this.formSource.equals( "adventure.php" ) )
{
if ( this.adventureId.equals( AdventurePool.THE_SHORE_ID ) )
{
// The Shore
int adv = KoLCharacter.inFistcore() ? 5 : 3;
if ( KoLCharacter.getAdventuresLeft() < adv )
{
KoLmafia.updateDisplay( MafiaState.ERROR, "Ran out of adventures." );
return;
}
}
}
else if ( this.formSource.equals( "barrel.php" ) )
{
int square = BarrelDecorator.recommendSquare();
if ( square == 0 )
{
KoLmafia.updateDisplay( MafiaState.ERROR,
"All booze in the specified rows has been collected." );
return;
}
this.addFormField( "smash", String.valueOf( square ) );
}
else if ( this.formSource.equals( "cellar.php" ) )
{
if ( TavernManager.shouldAutoFaucet() )
{
this.removeFormField( "whichspot" );
this.addFormField( "action", "autofaucet" );
}
else
{
int square = TavernManager.recommendSquare();
if ( square == 0 )
{
KoLmafia.updateDisplay( MafiaState.ERROR, "Don't know which square to visit in the Typical Tavern Cellar." );
return;
}
this.addFormField( "whichspot", String.valueOf( square ) );
this.addFormField( "action", "explore" );
}
}
else if ( this.formSource.equals( "mining.php" ) )
{
KoLmafia.updateDisplay( MafiaState.ERROR, "Automated mining is not currently implemented." );
return;
}
else if ( this.formSource.equals( "place.php" ) && this.adventureId.equals( "pyramid_state" ) )
{
this.addFormField( "whichplace", "pyramid" );
StringBuilder action = new StringBuilder();
action.append( adventureId );
action.append( Preferences.getString( "pyramidPosition" ) );
if ( Preferences.getBoolean( "pyramidBombUsed" ) )
{
action.append( "a" );
}
this.addFormField( "action", action.toString() );
}
else if ( this.formSource.equals( "place.php" ) && this.adventureId.equals( "manor4_chamber" ) )
{
this.addFormField( "whichplace", "manor4" );
if ( !QuestDatabase.isQuestFinished( Quest.MANOR ) )
{
this.addFormField( "action", "manor4_chamberboss" );
}
else
{
this.addFormField( "action", "manor4_chamber" );
}
}
super.run();
}
@Override
public void processResults()
{
// Sometimes, there's no response from the server.
// In this case, skip and continue onto the next one.
if ( this.responseText == null ||
this.responseText.trim().length() == 0 ||
this.responseText.contains( "No, that isn't a place yet." ) )
{
KoLmafia.updateDisplay( MafiaState.ERROR, "You can't get to that area yet." );
return;
}
if ( this.formSource.equals( "place.php" ) )
{
if ( this.getURLString().contains( "whichplace=nstower" ) )
{
// nstower locations redirect to a fight or choice. If
// it didn't do that, you can't adventure there.
KoLmafia.updateDisplay( MafiaState.PENDING, "You can't adventure there." );
SorceressLairManager.parseTowerResponse( "", this.responseText );
}
return;
}
// We're missing an item, haven't been given a quest yet, or
// otherwise trying to go somewhere not allowed.
int index = KoLAdventure.findAdventureFailure( this.responseText );
if ( index >= 0 )
{
String failure = KoLAdventure.adventureFailureMessage( index );
MafiaState severity = KoLAdventure.adventureFailureSeverity( index );
KoLmafia.updateDisplay( severity, failure );
this.override = 0;
return;
}
// This is a server error. Hope for the best and repeat the
// request.
if ( this.responseText.contains( "No adventure data exists for this location" ) )
{
KoLmafia.updateDisplay( MafiaState.ERROR, "Server error. Please wait and try again." );
return;
}
// Nothing more to do in this area
if ( this.formSource.equals( "adventure.php" ) )
{
if ( this.adventureId.equals( AdventurePool.MERKIN_COLOSSEUM_ID ) )
{
SeaMerkinRequest.parseColosseumResponse( this.getURLString(), this.responseText );
}
if ( !this.responseText.contains( "adventure.php" ) &&
!this.responseText.contains( "You acquire" ) )
{
if ( !EncounterManager.isAutoStop( this.encounter ) )
{
KoLmafia.updateDisplay( MafiaState.PENDING, "Nothing more to do here." );
}
return;
}
}
// If you're at the casino, each of the different slot
// machines deducts meat from your tally
if ( this.formSource.equals( "casino.php" ) )
{
if ( this.adventureId.equals( "1" ) )
{
ResultProcessor.processMeat( -5 );
}
else if ( this.adventureId.equals( "2" ) )
{
ResultProcessor.processMeat( -10 );
}
else if ( this.adventureId.equals( "11" ) )
{
ResultProcessor.processMeat( -10 );
}
}
if ( this.adventureId.equals( AdventurePool.ROULETTE_TABLES_ID ) )
{
ResultProcessor.processMeat( -10 );
}
else if ( this.adventureId.equals( String.valueOf( AdventurePool.POKER_ROOM ) ) )
{
ResultProcessor.processMeat( -30 );
}
// Trick-or-treating requires a costume;
// notify the user of this error.
if ( this.formSource.equals( "trickortreat.php" ) && this.responseText.contains( "without a costume" ) )
{
KoLmafia.updateDisplay( MafiaState.ERROR, "You must wear a costume." );
return;
}
}
public static final String registerEncounter( final GenericRequest request )
{
// No encounters in chat!
if ( request.isChatRequest )
{
return "";
}
String urlString = request.getURLString();
String responseText = request.responseText;
boolean isFight = urlString.startsWith( "fight.php" );
boolean isChoice = urlString.startsWith( "choice.php" );
// If we were redirected into a fight or a choice through using
// an item, there will be an encounter in the responseText.
// Otherwise, if KoLAdventure didn't log the location, there
// can't be an encounter for us to log.
if ( GenericRequest.itemMonster == null && !KoLAdventure.recordToSession( urlString, responseText ) )
{
return "";
}
if ( !( request instanceof AdventureRequest ) && !AdventureRequest.containsEncounter( urlString, responseText ) )
{
return "";
}
String encounter = null;
String type = null;
if ( isFight )
{
type = "Combat";
encounter = AdventureRequest.parseCombatEncounter( responseText );
}
else if ( isChoice )
{
int choice = ChoiceManager.extractChoice( responseText );
type = choiceType( choice );
encounter = AdventureRequest.parseChoiceEncounter( urlString, choice, responseText );
ChoiceManager.registerDeferredChoice( choice, encounter );
}
else
{
type = "Noncombat";
encounter = parseNoncombatEncounter( urlString, responseText );
if ( responseText.contains( "charpane.php" ) )
{
// Since a charpane refresh was requested, this might have taken a turn
AdventureSpentDatabase.setNoncombatEncountered( true );
}
}
if ( encounter == null )
{
return "";
}
// Silly check for silly situation
if ( encounter == AdventureRequest.NOT_IN_A_FIGHT )
{
return encounter;
}
if ( isFight )
{
FightRequest.setCurrentEncounter( encounter );
}
Preferences.setString( "lastEncounter", encounter );
RequestLogger.printLine( "Encounter: " + encounter );
RequestLogger.updateSessionLog( "Encounter: " + encounter );
AdventureRequest.registerDemonName( encounter, responseText );
// We are done registering the item's encounter.
if ( type != null )
{
if ( type.equals( "Combat" ) )
{
encounter = AdventureRequest.translateGenericType( encounter, responseText );
// Only queue normal monster encounters
if ( !EncounterManager.ignoreSpecialMonsters &&
!EncounterManager.isWanderingMonster( encounter ) &&
!EncounterManager.isUltrarareMonster( encounter ) &&
!EncounterManager.isSemiRareMonster( encounter ) &&
!EncounterManager.isSuperlikelyMonster( encounter ) &&
!EncounterManager.isFreeCombatMonster( encounter ) &&
!EncounterManager.isNoWanderMonster( encounter ) &&
!EncounterManager.isEnamorangEncounter( responseText, false ) &&
!EncounterManager.isDigitizedEncounter( responseText, false ) &&
!EncounterManager.isRomanticEncounter( responseText, false ) &&
!FightRequest.edFightInProgress() )
{
AdventureQueueDatabase.enqueue( KoLAdventure.lastVisitedLocation(), encounter );
}
}
else if ( type.equals( "Noncombat" ) )
{
// only log the FIRST choice that we see in a choiceadventure chain.
if ( ( !urlString.startsWith( "choice.php" ) || ChoiceManager.getLastChoice() == 0 ) &&
!FightRequest.edFightInProgress() )
{
AdventureQueueDatabase.enqueueNoncombat( KoLAdventure.lastVisitedLocation(), encounter );
}
}
EncounterManager.registerEncounter( encounter, type, responseText );
}
TurnCounter.handleTemporaryCounters( type, encounter );
return encounter;
}
private static String fromName = null;
private static String toName = null;
public static final void setNameOverride( final String from, final String to )
{
fromName = from;
toName = to;
}
public static final String parseMonsterEncounter( final String responseText )
{
String encounter = AdventureRequest.parseCombatEncounter( responseText );
return AdventureRequest.translateGenericType( encounter, responseText );
}
private static final Pattern [] MONSTER_NAME_PATTERNS =
{
Pattern.compile( "You're fighting <span id='monname'> *(.*?)</span>", Pattern.DOTALL ),
// papier weapons can change "fighting" to some other verb
Pattern.compile( "You're (?:<u>.*?</u>) <span id='monname'>(.*?)</span>", Pattern.DOTALL ),
// KoL sure generates a lot of bogus HTML
Pattern.compile( "<b>.*?(<b>.*?<(/b|/td)>.*?)<(br|/td|/tr)>", Pattern.DOTALL ),
};
public static final String parseCombatEncounter( final String responseText )
{
// Silly check for silly situation
if ( responseText.contains( "Not in a Fight" ) )
{
return AdventureRequest.NOT_IN_A_FIGHT;
}
String name = null;
for ( Pattern pattern : MONSTER_NAME_PATTERNS )
{
Matcher matcher = pattern.matcher( responseText );
if ( matcher.find() )
{
name = matcher.group(1);
break;
}
}
if ( name == null )
{
return "";
}
// If the name has bold markup, strip formatting
name = StringUtilities.globalStringReplace( name, "<b>", "" );
name = StringUtilities.globalStringReplace( name, "</b>", "" );
// Brute force fix for haiku dungeon monsters, which have
// punctuation at the end because of bad HTML
name = name.startsWith( "amateur ninja" ) ? "amateur ninja" :
name.startsWith( "ancient insane monk" ) ? "ancient insane monk" :
name.startsWith( "Ferocious bugbear" ) ? "ferocious bugbear" :
name.startsWith( "gelatinous cube" ) ? "gelatinous cube" :
name.startsWith( "Knob Goblin poseur" ) ? "Knob Goblin poseur" :
name;
// Canonicalize
name = CombatActionManager.encounterKey( name, false );
// Coerce name if needed
if ( name.equalsIgnoreCase( fromName ) )
{
name = CombatActionManager.encounterKey( toName, false );
}
fromName = null;
EquipmentManager.decrementTurns();
return name;
}
public static final String translateGenericType( final String encounterToCheck, final String responseText )
{
if ( KoLAdventure.lastLocationName != null &&
KoLAdventure.lastLocationName.startsWith( "Fernswarthy's Basement" ) )
{
return BasementRequest.basementMonster;
}
// If the monster has random modifiers, remove and save them
String encounter = AdventureRequest.handleRandomModifiers( encounterToCheck.trim(), responseText );
encounter = AdventureRequest.handleIntergnat( encounter );
encounter = AdventureRequest.handleNuclearAutumn( encounter );
// Adventuring in the Wumpus cave while temporarily blind is
// stupid, but since we won't clear the cave after defeating it
// if we can't recognize it, allow for it
if ( WumpusManager.isWumpus() )
{
return "wumpus";
}
// Disambiguate via responseText, if possible
encounter = ConsequenceManager.disambiguateMonster( encounter, responseText );
if ( MonsterDatabase.findMonster( encounter, false ) != null )
{
return encounter;
}
// For monsters that have a randomly-generated name, identify them by the image they use instead
String override = null;
String image = null;
Matcher monster = AdventureRequest.MONSTER_IMAGE.matcher( responseText );
if ( monster.find() )
{
image = monster.group( 1 );
}
// You'd think that the following could/should be:
// - in MonsterDatabase
// - a Map lookup
if ( image != null )
{
// Always-available monsters are listed above obsolete monsters
// to get a quicker match on average. Obsolete monsters can
// still be fought due to the Fax Machine. Due to monster copying,
// any of these monsters can show up in any zone, or in no zone.
override =
// The Copperhead Club
image.startsWith( "coppertender" ) ? "Copperhead Club bartender" :
// Spookyraven
image.startsWith( "srpainting" ) ? "ancestral Spookyraven portrait" :
// Spring Break Beach
image.startsWith( "ssd_burger" ) ? "Sloppy Seconds Burger" :
image.startsWith( "ssd_cocktail" ) ? "Sloppy Seconds Cocktail" :
image.startsWith( "ssd_sundae" ) ? "Sloppy Seconds Sundae" :
image.startsWith( "fun-gal" ) ? "Fun-Guy Playmate" :
// VYKEA
image.startsWith( "vykfemale" ) ? "VYKEA viking (female)" :
image.startsWith( "vykmale" ) ? "VYKEA viking (male)" :
// The Old Landfill
image.startsWith( "js_bender" ) ? "junksprite bender" :
image.startsWith( "js_melter" ) ? "junksprite melter" :
image.startsWith( "js_sharpener" ) ? "junksprite sharpener" :
// Dreadsylvania
image.startsWith( "dvcoldbear" ) ? "cold bugbear" :
image.startsWith( "dvcoldghost" ) ? "cold ghost" :
image.startsWith( "dvcoldskel" ) ? "cold skeleton" :
image.startsWith( "dvcoldvamp" ) ? "cold vampire" :
image.startsWith( "dvcoldwolf" ) ? "cold werewolf" :
image.startsWith( "dvcoldzom" ) ? "cold zombie" :
image.startsWith( "dvhotbear" ) ? "hot bugbear" :
image.startsWith( "dvhotghost" ) ? "hot ghost" :
image.startsWith( "dvhotskel" ) ? "hot skeleton" :
image.startsWith( "dvhotvamp" ) ? "hot vampire" :
image.startsWith( "dvhotwolf" ) ? "hot werewolf" :
image.startsWith( "dvhotzom" ) ? "hot zombie" :
image.startsWith( "dvsleazebear" ) ? "sleaze bugbear" :
image.startsWith( "dvsleazeghost" ) ? "sleaze ghost" :
image.startsWith( "dvsleazeskel" ) ? "sleaze skeleton" :
image.startsWith( "dvsleazevamp" ) ? "sleaze vampire" :
image.startsWith( "dvsleazewolf" ) ? "sleaze werewolf" :
image.startsWith( "dvsleazezom" ) ? "sleaze zombie" :
image.startsWith( "dvspookybear" ) ? "spooky bugbear" :
image.startsWith( "dvspookyghost" ) ? "spooky ghost" :
image.startsWith( "dvspookyskel" ) ? "spooky skeleton" :
image.startsWith( "dvspookyvamp" ) ? "spooky vampire (Dreadsylvanian)" :
image.startsWith( "dvspookywolf" ) ? "spooky werewolf" :
image.startsWith( "dvspookyzom" ) ? "spooky zombie" :
image.startsWith( "dvstenchbear" ) ? "stench bugbear" :
image.startsWith( "dvstenchghost" ) ? "stench ghost" :
image.startsWith( "dvstenchskel" ) ? "stench skeleton" :
image.startsWith( "dvstenchvamp" ) ? "stench vampire" :
image.startsWith( "dvstenchwolf" ) ? "stench werewolf" :
image.startsWith( "dvstenchzom" ) ? "stench zombie" :
// Hobopolis
image.startsWith( "nhobo" ) ? "Normal Hobo" :
image.startsWith( "hothobo" ) ? "Hot Hobo" :
image.startsWith( "coldhobo" ) ? "Cold Hobo" :
image.startsWith( "stenchhobo" ) ? "Stench Hobo" :
image.startsWith( "spookyhobo" ) ? "Spooky Hobo" :
image.startsWith( "slhobo" ) ? "Sleaze Hobo" :
// Slime Tube
image.startsWith( "slime1" ) ? "Slime" :
image.startsWith( "slime2" ) ? "Slime Hand" :
image.startsWith( "slime3" ) ? "Slime Mouth" :
image.startsWith( "slime4" ) ? "Slime Construct" :
image.startsWith( "slime5" ) ? "Slime Colossus" :
// GamePro Bosses
image.startsWith( "faq_boss" ) ? "Video Game Boss" :
image.startsWith( "faq_miniboss" ) ? "Video Game Miniboss" :
// KOLHS
image.startsWith( "shopteacher" ) ? "X-fingered Shop Teacher" :
// Actually Ed the Undying
image.startsWith( "../otherimages/classav" ) ? "You the Adventurer" :
image.startsWith( "wingedyeti" ) && KoLCharacter.isEd() ? "Your winged yeti" :
// Trick or Treat
image.startsWith( "vandalkid" ) ? "vandal kid" :
image.startsWith( "paulblart" ) ? "suburban security civilian" :
image.startsWith( "tooold" ) ? "kid who is too old to be Trick-or-Treating" :
// Bugbear Invasion
image.startsWith( "bb_caveman" ) ? "angry cavebugbear" :
// Crimbo 2012 wandering elves
image.startsWith( "tacoelf_sign" ) ? "sign-twirling Crimbo elf" :
image.startsWith( "tacoelf_taco" ) ? "taco-clad Crimbo elf" :
image.startsWith( "tacoelf_cart" ) ? "tacobuilding elf" :
// Crimbobokutown Toy Factory
image.startsWith( "animelf1" ) ? "tiny-screwing animelf" :
image.startsWith( "animelf2" ) ? "plastic-extruding animelf" :
image.startsWith( "animelf3" ) ? "circuit-soldering animelf" :
image.startsWith( "animelf4" ) ? "quality control animelf" :
image.startsWith( "animelf5" ) ? "toy assembling animelf" :
// Elf Alley
image.startsWith( "elfhobo" ) ? "Hobelf" :
// Haunted Sorority House
image.startsWith( "sororeton" ) ? "sexy sorority skeleton" :
image.startsWith( "sororpire" ) ? "sexy sorority vampire" :
image.startsWith( "sororwolf" ) ? "sexy sorority werewolf" :
image.startsWith( "sororeton" ) ? "sexy sorority skeleton" :
image.startsWith( "sororbie" ) ? "sexy sorority zombie" :
image.startsWith( "sororghost" ) ? "sexy sorority ghost" :
// Lord Flameface's Castle Entryway
image.startsWith( "fireservant" ) ? "Servant Of Lord Flameface" :
// Abyssal Portals
image.startsWith( "generalseal" ) ? "general seal" :
// Crimbo 13
image.startsWith( "warbear1" ) ? "Warbear Foot Soldier" :
image.startsWith( "warbear2" ) ? "Warbear Officer" :
image.startsWith( "warbear3" ) ? "High-Ranking Warbear Officer" :
// Crashed Space Beast
image.startsWith( "spacebeast" ) ? "space beast" :
// Roman Forum
image.startsWith( "gladiator" ) ? "Gladiator" :
image.startsWith( "madiator" ) ? "Madiator" :
image.startsWith( "radiator" ) ? "Radiator" :
image.startsWith( "sadiator" ) ? "Sadiator" :
// Spelunky
image.startsWith( "spelunkbeeq" ) ? "queen bee (spelunky)" :
image.startsWith( "spelunkghost" ) ? "ghost (Spelunky)" :
// Globe Theatre Main Stage
image.startsWith( "richardx" ) ? "Richard X" :
// BatFellow
image.startsWith( "henchman" ) ? "very [adjective] henchman" :
image.startsWith( "henchwoman" ) ? "very [adjective] henchwoman" :
// Tres de Mayo
image.startsWith( "reveler" ) ? "Cinco de Mayo reveler" :
// The Source
image.startsWith( "sourceagents" ) ? "One Thousand Source Agents" :
image.startsWith( "sourceagent" ) ? "Source Agent" :
// KoL Con 13
// female needs checking before male
image.startsWith( "congoerf" ) ? "stumbling-drunk congoer (female)" :
image.startsWith( "congoer" ) ? "stumbling-drunk congoer (male)" :
// Eldritch Incursion
image.startsWith( "eldtentacle" ) ? "Eldritch Tentacle" :
null;
}
// These monsters cannot be identified by their image
if ( override == null )
{
switch ( KoLAdventure.lastAdventureId() )
{
// Video Game Minions need to be checked for after checking for Video Game bosses
case 319:
override = "Video Game Minion (weak)";
break;
case 320:
override = "Video Game Minion (moderate)";
break;
case 321:
override = "Video Game Minion (strong)";
break;
case 458:
if ( responseText.contains( "dmtmonster_part1.png" ) )
{
override = "Performer of Actions";
}
else if ( responseText.contains( "dmtmonster_part4.png" ) )
{
override = "Thinker of Thoughts";
}
else if ( responseText.contains( "dmtmonster_part6.png" ) )
{
override = "Perceiver of Sensations";
}
break;
}
}
if ( override == null )
{
// LT&T monsters can have different names
override = encounter.equals( "professional gunman" ) || encounter.contains( "trained mercenary" ) ? "hired gun" :
encounter.equals( "vengeful ghost" ) || encounter.contains( "shrieking ghost" ) ? "restless ghost" :
// This is twitch content, but shares image with hired gun, so detection moved here
image != null && image.startsWith( "outlawboss" ) ? "outlaw leader" :
null;
}
if ( override == null )
{
if ( responseText.contains( "A figure steps out from behind this morning and says" ) )
{
// SBIP will ruin that text, but so will the staph, and probably other stuff too.
// Not going to worry about those.
override = "time-spinner prank";
}
}
if ( override != null )
{
return override;
}
return encounter;
}
private static final String parseChoiceEncounter( final String urlString, final int choice, final String responseText )
{
if ( LouvreManager.louvreChoice( choice ) )
{
return LouvreManager.encounterName( choice );
}
switch ( choice )
{
case 443: // Chess Puzzle
// No "encounter" when moving on the chessboard
if ( urlString.contains( "xy" ) )
{
return null;
}
break;
case 1085: // Deck of Every Card
return DeckOfEveryCardRequest.parseCardEncounter( responseText );
case 535: // Deep Inside Ronald, Baby
case 536: // Deep Inside Grimace, Bow Chick-a Bow Bow
case 585: // Screwing Around!
case 595: // Fire! I... have made... fire!
case 807: // Breaker Breaker!
case 1003: // Test Your Might And Also Test Other Things
case 1086: // Pick a Card
return null;
case 1135: // The Bat-Sedan
return BatManager.parseBatSedan( responseText );
}
// No "encounter" for certain arcade games
if ( ArcadeRequest.arcadeChoice( choice ) )
{
return null;
}
if ( ChoiceManager.canWalkFromChoice( choice ) )
{
return null;
}
return AdventureRequest.parseEncounter( responseText );
}
private static final String choiceType( final int choice )
{
if ( LouvreManager.louvreChoice( choice ) )
{
return null;
}
return "Noncombat";
}
private static final String[][] LIMERICKS =
{
{ "Nantucket Snapper", "ancient old turtle" },
{ "The Apathetic Lizardman", "lizardman quite apathetic" },
{ "The Bleary-Eyed Cyclops", "bleary-eyed cyclops" },
{ "The Crass Goblin", "goblin is crass" },
{ "The Crotchety Wizard", "crotchety wizard" },
{ "The Dumb Minotaur", "dumb minotaur" },
{ "The Fierce-Looking Giant", "fierce-looking giant" },
{ "The Gelatinous Cube", "gelatinous cube" },
{ "The Gnome with a Truncheon", "gnome with a truncheon" },
{ "The Goblin King's Vassal", "Goblin King's vassal" },
{ "The Insatiable Maiden", "insatiable maiden" },
{ "The Jewelry Gnoll", "bejeweled it" },
{ "The Martini Booth", "martini booth" },
{ "The One-Legged Trouser", "one-pantlegged schnauzer" },
{ "The Orc With a Spork", "waving a spork" },
{ "The Slime Puddle", "slime puddle" },
{ "The Sozzled Old Dragon", "sozzled old dragon" },
{ "The Superior Ogre", "I am superior" },
{ "The Unguarded Chest", "chest full of meat" },
{ "The Unpleasant Satyr", "unpleasant satyr" },
{ "The Vampire Buffer", "vampire buffer" },
{ "The Weathered Old Chap", "weathered old chap" },
{ "The Witch", "A witch" },
{ "Thud", "hobo glyphs" },
};
private static final String parseNoncombatEncounter( final String urlString, final String responseText )
{
// Fernswarthy's Basement
if ( urlString.startsWith( "basement.php" ) )
{
return null;
}
if ( urlString.startsWith( "adventure.php" ) )
{
int area = parseArea( urlString );
switch ( area )
{
case 17:
// Hidden Temple
// Dvorak's revenge
// You jump to the last letter, and put your pom-poms down with a sign of relief --
// thank goodness that's over. Worst. Spelling bee. Ever.
if ( responseText.contains( "put your pom-poms down" ) )
{
QuestDatabase.setQuestProgress( Quest.WORSHIP, "step2" );
}
break;
case 19:
// Limerick Dungeon
for ( int i = 0; i < LIMERICKS.length; ++i )
{
if ( responseText.contains( LIMERICKS[i][1] ) )
{
return LIMERICKS[i][0];
}
}
return "Unrecognized Limerick";
case 114: // Outskirts of The Knob
// Unstubbed
// You go back to the tree where the wounded Knob Goblin guard was resting,
// and find him just where you left him, continuing to whine about his stubbed toe.
//
// "Here you go, tough guy" you say, and hand him the unguent.
if ( responseText.contains( "you say, and hand him the unguent" ) )
{
ResultProcessor.processItem( ItemPool.PUNGENT_UNGUENT, -1 );
}
break;
}
}
else if ( urlString.startsWith( "barrel.php" ) )
{
// Without this special case, encounter names in the Barrels would
// be things like "bottle of rum"
return "Barrel Smash";
}
String encounter = parseEncounter( responseText );
if ( encounter != null )
{
return encounter;
}
return null;
}
private static final String parseEncounter( final String responseText )
{
// Look only in HTML body; the header can have scripts with
// bold text.
int index = responseText.indexOf( "<body>" );
// Skip past the Adventure Results
int brIndex = responseText.indexOf( "Results:</b>", index );
if ( brIndex != -1 )
{
int resultsIndex = responseText.indexOf( "<div id=\"results\">", index );
if ( resultsIndex != -1 )
{
// KoL was nice enough to put results into a div for us
index = responseText.indexOf( "</div>", resultsIndex );
}
else
{
// There is no results div, but it doesn't mean that
// there aren't results. Nothing like consistency. Not.
index = brIndex;
}
}
int boldIndex = responseText.indexOf( "<b>", index );
if ( boldIndex == -1 )
{
return "";
}
int endBoldIndex = responseText.indexOf( "</b>", boldIndex );
if ( endBoldIndex == -1 )
{
return "";
}
return responseText.substring( boldIndex + 3, endBoldIndex );
}
private static final int parseArea( final String urlString )
{
Matcher matcher = AREA_PATTERN.matcher( urlString );
if ( matcher.find() )
{
return StringUtilities.parseInt( matcher.group(2) );
}
return -1;
}
private static final Object[][] demons =
{
{
"Summoning Chamber",
Pattern.compile( "Did you say your name was (.*?)\\?" ),
"delicious-looking pies",
"demonName1",
},
{
"Hoom Hah",
Pattern.compile( "(.*?)! \\1, cooooome to meeeee!" ),
"fifty meat",
"demonName2",
},
{
"Every Seashell Has a Story to Tell If You're Listening",
Pattern.compile( "Hello\\? Is (.*?) there\\?" ),
"fish-guy",
"demonName3",
},
{
"Leavesdropping",
Pattern.compile( "(.*?), we call you! \\1, come to us!" ),
"bullwhip",
"demonName4",
},
{
"These Pipes... Aren't Clean!",
Pattern.compile( "Blurgle. (.*?). Gurgle. By the way," ),
"coprodaemon",
"demonName5",
},
{
"Flying In Circles",
// SC: Then his claws slip, and he falls
// backwards.<p>"<Demon Name>!" he screams as he
// tumbles backwards. "LORD OF REVENGE! GIVE ME
// STRENGTH!"
//
// TT: With a scrape, her sickle slips from the
// rock.<p>"<Demon Name>" she shrieks as she plummets
// toward the lava. "Lord of Revenge! I accept your
// contract! Give me your power!"
//
// PA: Its noodles lose their grip, and the evil
// pastaspawn falls toward the churning
// lava.<p><i>"<Demon Name>!"</i> it howls. "<i>Lord of
// Revenge! Come to my aid!</i>"
//
// SA: As it falls, a mouth opens on its surface and
// howls: "<Demon Name>! Revenge!"
//
// DB: His grip slips, and he falls.<p>"<Demon Name>!
// Lord of Revenge! I call to you! I pray to you! Help
// m--"
//
// AT: His grip slips, and he tumbles
// backward.<p>"<Demon Name>!" he screams. "Emperador
// de la Venganza! Come to my aid! I beg of you!"
Pattern.compile( "(?:he falls backwards|her sickle slips from the rock|falls toward the churning lava|a mouth opens on its surface and howls|His grip slips, and he falls|he tumbles backward).*?(?:<i>)?"(.*?)!?"(?:</i>)?(?: he screams| she shrieks| it howls| Revenge| Lord of Revenge)" ),
"Lord of Revenge",
"demonName8",
},
{
"Sinister Ancient Tablet",
Pattern.compile( "<font.*?color=#cccccc>(.*?)</font>" ),
"flame-wreathed mouth",
"demonName9",
},
{
"Strange Cube",
Pattern.compile( "Come to me! Come to (.*?)!" ),
"writhing and twisting snake",
"demonName10",
},
{
"Where Have All The Drunkards Gone?",
Pattern.compile( "Is (.*?) a word?"),
"Gary's friend",
"demonName11",
},
};
private static final Pattern NAME_PATTERN = Pattern.compile( "<b>"(.*?)"</b>" );
public static final boolean registerDemonName( final String encounter, final String responseText )
{
String place = null;
String demon = null;
String setting = null;
for ( int i = 0; i < AdventureRequest.demons.length; ++i )
{
Object [] demons = AdventureRequest.demons[ i ];
place = (String) demons[ 0 ];
if ( place == null || !place.equals( encounter ) )
{
continue;
}
Pattern pattern = (Pattern) demons[ 1 ];
Matcher matcher = pattern.matcher( responseText );
if ( matcher.find() )
{
// We found the name
demon = matcher.group( 1 );
setting = (String) demons[ 3 ];
}
break;
}
// If we didn't recognize the demon and he used a valid name in
// the Summoning Chamber, we can deduce which one it is from
// the result text
if ( setting == null && encounter.equals( "Summoning Chamber" ) )
{
place = encounter;
Matcher matcher = AdventureRequest.NAME_PATTERN.matcher( responseText );
if ( !matcher.find() )
{
return false;
}
// Save the name he used.
demon = matcher.group( 1 );
// Look for tell-tale string
for ( int i = 0; i < AdventureRequest.demons.length; ++i )
{
Object [] demons = AdventureRequest.demons[ i ];
String text = (String) demons[ 2 ];
if ( responseText.contains( text ) )
{
setting = (String) demons[ 3 ];
break;
}
}
}
// Couldn't figure out which demon he called.
if ( setting == null )
{
return false;
}
String previousName = Preferences.getString( setting );
if ( previousName.equals( demon ) )
{
// Valid demon name
return true;
}
RequestLogger.printLine( "Demon name: " + demon );
RequestLogger.updateSessionLog( "Demon name: " + demon );
Preferences.setString( setting, demon );
GoalManager.checkAutoStop( place );//
// Valid demon name
return true;
}
private static final boolean containsEncounter( final String formSource, final String responseText )
{
if ( formSource.startsWith( "adventure.php" ) )
{
return true;
}
else if ( formSource.startsWith( "fight.php" ) )
{
return FightRequest.getCurrentRound() == 0;
}
else if ( formSource.startsWith( "choice.php" ) )
{
return responseText.contains( "choice.php" );
}
else if ( formSource.startsWith( "cave.php" ) )
{
return formSource.contains( "sanctum" );
}
else if ( formSource.startsWith( "cobbsknob.php" ) )
{
return formSource.contains( "throneroom" );
}
else if ( formSource.startsWith( "crypt.php" ) )
{
return formSource.contains( "action" );
}
else if ( formSource.startsWith( "cellar.php" ) )
{
// Simply visiting the map is not an encounter.
return !formSource.equals( "cellar.php" );
}
else if ( formSource.startsWith( "suburbandis.php" ) )
{
return formSource.contains( "action=dothis" );
}
else if ( formSource.startsWith( "tiles.php" ) )
{
// Only register initial encounter of Dvorak's Revenge
DvorakManager.saveResponse( responseText );
return responseText.contains( "I before E, except after C" );
}
else if ( formSource.startsWith( "barrel.php?smash" ) )
{
return true;
}
else if ( formSource.startsWith( "mining.php" ) )
{
if ( formSource.contains( "which=" ) )
{
AdventureSpentDatabase.setNoncombatEncountered( true );
}
return false;
}
// It is not a known adventure. Therefore,
// do not log the encounter yet.
return false;
}
@Override
public int getAdventuresUsed()
{
if ( this.override >= 0 )
{
return this.override;
}
if ( this.adventureId.equals( AdventurePool.THE_SHORE_ID ) )
{
return KoLCharacter.inFistcore() ? 5 : 3;
}
String zone = AdventureDatabase.getZone( this.adventureName );
if ( zone != null && ( zone.equals( "The Sea" ) || this.adventureId.equals( AdventurePool.YACHT_ID ) ) )
{
return KoLConstants.activeEffects.contains( EffectPool.get( EffectPool.FISHY ) ) ? 1 : 2;
}
return 1;
}
public void overrideAdventuresUsed( int used )
{
this.override = used;
}
@Override
public String toString()
{
return this.adventureName;
}
public static final void handleServerRedirect( final String redirectLocation )
{
if ( redirectLocation.contains( "main.php" ) )
{
return;
}
AdventureRequest.ZONE_UNLOCK.constructURLString( redirectLocation );
if ( redirectLocation.contains( "palinshelves.php" ) )
{
AdventureRequest.ZONE_UNLOCK.run();
AdventureRequest.ZONE_UNLOCK.constructURLString(
"palinshelves.php?action=placeitems&whichitem1=2259&whichitem2=2260&whichitem3=493&whichitem4=2261" ).run();
return;
}
if ( redirectLocation.contains( "tiles.php" ) )
{
AdventureRequest.handleDvoraksRevenge( AdventureRequest.ZONE_UNLOCK );
return;
}
if ( redirectLocation.contains( "place.php" ) )
{
AdventureRequest.ZONE_UNLOCK.run();
// Don't error out if it's just a redirect to a container zone
// eg. Using grimstone mask, with choice adventure autoselected
return;
}
if ( redirectLocation.startsWith( "witchess.php" ) )
{
// Grabbing the buff from Witchess
AdventureRequest.ZONE_UNLOCK.run();
return;
}
RequestSynchFrame.showRequest( AdventureRequest.ZONE_UNLOCK );
KoLmafia.updateDisplay( MafiaState.ABORT, "Unknown adventure type encountered." );
}
public static final void handleDvoraksRevenge( final GenericRequest request )
{
EncounterManager.registerEncounter( "Dvorak's Revenge", "Noncombat", null );
RequestLogger.printLine( "Encounter: Dvorak's Revenge" );
RequestLogger.updateSessionLog( "Encounter: Dvorak's Revenge" );
request.run();
request.constructURLString( "tiles.php?action=jump&whichtile=4" ).run();
request.constructURLString( "tiles.php?action=jump&whichtile=6" ).run();
request.constructURLString( "tiles.php?action=jump&whichtile=3" ).run();
request.constructURLString( "tiles.php?action=jump&whichtile=5" ).run();
request.constructURLString( "tiles.php?action=jump&whichtile=7" ).run();
request.constructURLString( "tiles.php?action=jump&whichtile=6" ).run();
request.constructURLString( "tiles.php?action=jump&whichtile=3" ).run();
}
private static String handleRandomModifiers( String monsterName, final String responseText )
{
MonsterData.lastRandomModifiers.clear();
if ( !responseText.contains( "var ocrs" ) )
{
return monsterName;
}
HtmlCleaner cleaner = HTMLParserUtils.configureDefaultParser();
String xpath = "//script/text()";
TagNode doc;
try
{
doc = cleaner.clean( responseText );
}
catch( IOException e )
{
StaticEntity.printStackTrace( e );
return monsterName;
}
Object[] result;
try
{
result = doc.evaluateXPath( xpath );
}
catch ( XPatherException ex )
{
return monsterName;
}
String text = "";
for ( Object result1 : result )
{
text = result1.toString();
if ( text.startsWith( "var ocrs" ) )
{
break;
}
}
ArrayList<String> internal = new ArrayList<String>();
String[] temp = text.split( "\"" );
for ( int i = 1; i < temp.length - 1; i++ ) // The first and last elements are never useful
{
if ( !temp[i].contains( ":" ) && !temp[i].equals( "," ) )
{
internal.add( temp[i] );
}
}
// Before we remove the random modifiers, if the monster has
// "The " in front of the adjectives, remove it. This allows us
// to recognize that "The 1337 N4ugh7y 50rc3r355" is actually
// "Naughty Sorceress", which is Manuel's name for her.
String trimmed = "";
if ( monsterName.startsWith( "The " ) || monsterName.startsWith( "the " ) )
{
trimmed = monsterName.substring( 0, 4 );
monsterName = monsterName.substring( 4 );
}
// Ditto for "a " to recognize the aliases for Your Shadow
else if ( monsterName.startsWith( "a " ) )
{
trimmed = monsterName.substring( 0, 2 );
monsterName = monsterName.substring( 2 );
}
int count = internal.size() - 1;
boolean leet = false;
for ( int j = 0; j <= count; ++j )
{
String modifier = internal.get( j );
String remove = MonsterData.crazySummerModifiers.get( modifier );
if ( remove == null )
{
RequestLogger.printLine( "Unrecognized monster modifier: " + modifier );
MonsterData.lastRandomModifiers.add( modifier );
continue;
}
if ( remove.equals( "1337" ) )
{
leet = true;
}
MonsterData.lastRandomModifiers.add( remove );
remove += ( j == count ) ? " " : ", ";
monsterName = StringUtilities.singleStringDelete( monsterName, remove );
}
if ( leet )
{
// We have an "encounterKey" which has been munged by
// StringUtilities.getEntityEncode(). 1337 monsters
// don't have character entities, so, decode it again.
String decoded = StringUtilities.getEntityDecode( monsterName );
monsterName = MonsterDatabase.translateLeetMonsterName( decoded );
}
return trimmed + monsterName;
}
private static final String handleIntergnat( String monsterName )
{
if ( KoLCharacter.getFamiliar().getId() != FamiliarPool.INTERGNAT )
{
return monsterName;
}
if ( monsterName.contains( " WITH BACON!!!" ) )
{
MonsterData.lastRandomModifiers.add( "bacon" );
return StringUtilities.globalStringDelete( monsterName, " WITH BACON!!!" );
}
if ( monsterName.contains( "ELDRITCH HORROR " ) )
{
MonsterData.lastRandomModifiers.add( "eldritch" );
return StringUtilities.globalStringDelete( monsterName, "ELDRITCH HORROR " );
}
if ( monsterName.contains( " NAMED NEIL" ) )
{
MonsterData.lastRandomModifiers.add( "neil" );
return StringUtilities.globalStringDelete( monsterName, " NAMED NEIL" );
}
if ( monsterName.contains( " WITH SCIENCE!" ) )
{
MonsterData.lastRandomModifiers.add( "science" );
return StringUtilities.globalStringDelete( monsterName, " WITH SCIENCE!" );
}
if ( monsterName.contains( " AND TESLA!" ) )
{
MonsterData.lastRandomModifiers.add( "tesla" );
return StringUtilities.globalStringDelete( monsterName, " AND TESLA!" );
}
return monsterName;
}
private static final String handleNuclearAutumn( String monsterName )
{
if ( !KoLCharacter.inNuclearAutumn() )
{
return monsterName;
}
if ( monsterName.contains( "mutant " ) )
{
// This check isn't perfect, since a monster with "mutant " in its name
// could show up in Nuclear Autumn eventually. Currently, the only
// monsters fitting that criteria are from Crimbo 2008, so this should be
// a safe way to check.
monsterName = StringUtilities.singleStringDelete( monsterName, "mutant " );
MonsterData.lastRandomModifiers.add( "mutant" );
}
return monsterName;
}
}