/**
* 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;
import java.util.ArrayList;
import java.util.TreeMap;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.sourceforge.kolmafia.KoLConstants.MafiaState;
import net.sourceforge.kolmafia.objectpool.IntegerPool;
import net.sourceforge.kolmafia.objectpool.ItemPool;
import net.sourceforge.kolmafia.persistence.ItemDatabase;
import net.sourceforge.kolmafia.request.EquipmentRequest;
import net.sourceforge.kolmafia.session.EquipmentManager;
import net.sourceforge.kolmafia.session.InventoryManager;
import net.sourceforge.kolmafia.utilities.StringUtilities;
public class SpecialOutfit
implements Comparable<SpecialOutfit>
{
private static final Pattern OPTION_PATTERN = Pattern.compile( "<option value=[\'\"]?(.*?)[\'\"]?>(.*?)</option>" );
private static final Stack<AdventureResult[]> implicitPoints = new Stack<AdventureResult[]>();
private static final Stack<AdventureResult[]> explicitPoints = new Stack<AdventureResult[]>();
private int outfitId;
private String outfitName;
private String outfitImage;
// This is TreeMap so that the pieces will be ordered by slot
private final TreeMap<Integer, AdventureResult> pieces;
private int hash;
public static final SpecialOutfit NO_CHANGE = new SpecialOutfit( Integer.MAX_VALUE, " - No Change - " );
public static final SpecialOutfit BIRTHDAY_SUIT = new SpecialOutfit( Integer.MAX_VALUE, "Birthday Suit" );
public static final SpecialOutfit PREVIOUS_OUTFIT = new SpecialOutfit( Integer.MAX_VALUE, "Your Previous Outfit" );
private static SpecialOutfit implicitOutfit = null;
private static int markedCheckpoint = -1;
public SpecialOutfit( final int outfitId, final String outfitName )
{
this.outfitId = outfitId;
// The name is normally a substring of the equipment page,
// and would keep that entire page in memory if not copied.
this.outfitName = new String( outfitName );
this.outfitImage = null;
this.pieces = new TreeMap<Integer, AdventureResult>();
this.hash = 0;
}
public int pieceCount( AdventureResult piece )
{
int type = EquipmentManager.itemIdToEquipmentType( piece.getItemId() );
// Everything aside from weapons and accessories can only be equipped once.
if ( type != EquipmentManager.WEAPON && type != EquipmentManager.ACCESSORY1 )
{
return this.pieces.values().contains( piece ) ? 1 : 0;
}
int count = 0;
for ( int slot = 0; slot < EquipmentManager.FAMILIAR; slot++ )
{
AdventureResult outfitPiece = this.pieces.get( slot );
if ( null == outfitPiece )
{
continue;
}
if ( piece.getItemId() == outfitPiece.getItemId() )
{
count++;
}
}
return count;
}
public boolean hasAllPieces()
{
for ( int slot = 0; slot < EquipmentManager.FAMILIAR; slot++ )
{
AdventureResult piece = this.pieces.get( slot );
if ( null == piece )
{
continue;
}
if ( !EquipmentManager.canEquip( piece.getName() ) )
{
return false;
}
if ( InventoryManager.getAccessibleCount( piece ) < this.pieceCount( piece ) )
{
return false;
}
}
return true;
}
public boolean isWearing()
{
return this.isWearing( -1 );
}
public boolean isWearing( AdventureResult piece, int type )
{
if ( type == EquipmentManager.ACCESSORY1 || type == EquipmentManager.ACCESSORY2 || type == EquipmentManager.ACCESSORY3 )
{
int accessoryCount = ( KoLCharacter.hasEquipped( piece, EquipmentManager.ACCESSORY1 ) ? 1 : 0 )
+ ( KoLCharacter.hasEquipped( piece, EquipmentManager.ACCESSORY2 ) ? 1 : 0 )
+ ( KoLCharacter.hasEquipped( piece, EquipmentManager.ACCESSORY3 ) ? 1 : 0 );
if ( accessoryCount < this.pieceCount( piece ) )
{
return false;
}
}
else if ( type == EquipmentManager.WEAPON
|| ( type == EquipmentManager.OFFHAND && ItemDatabase.getConsumptionType( piece.getItemId() ) == KoLConstants.EQUIP_WEAPON ))
{
int weaponCount = ( KoLCharacter.hasEquipped( piece, EquipmentManager.WEAPON ) ? 1 : 0 )
+ ( KoLCharacter.hasEquipped( piece, EquipmentManager.OFFHAND ) ? 1 : 0 );
if ( weaponCount < this.pieceCount( piece ) )
{
return false;
}
}
else if ( !KoLCharacter.hasEquipped( piece, type ) )
{
return false;
}
return true;
}
public boolean isWearing( int hash )
{
if ( ( hash & this.hash ) != this.hash ) return false;
for ( int slot = 0; slot < EquipmentManager.FAMILIAR; slot++ )
{
AdventureResult piece = this.pieces.get( slot );
if ( null == piece )
{
continue;
}
if ( !this.isWearing( piece, slot ) )
{
return false;
}
}
return true;
}
public boolean isWearing( AdventureResult[] equipment )
{
return this.isWearing( equipment, -1 );
}
public boolean isWearing( AdventureResult[] equipment, int hash )
{
if ( ( hash & this.hash ) != this.hash ) return false;
for ( int slot = 0; slot < EquipmentManager.FAMILIAR; slot++ )
{
AdventureResult piece = this.pieces.get( slot );
if ( null == piece )
{
continue;
}
if ( !KoLCharacter.hasEquipped( equipment, piece ) )
{
return false;
}
}
return true;
}
public boolean retrieve()
{
for ( int slot = 0; slot < EquipmentManager.FAMILIAR; slot++ )
{
AdventureResult piece = this.pieces.get( slot );
if ( null == piece )
{
continue;
}
if ( this.isWearing( piece, slot ) )
{
continue;
}
int pieceCount = this.pieceCount( piece );
if ( InventoryManager.getAccessibleCount( piece ) >= pieceCount )
{
InventoryManager.retrieveItem( ItemPool.get( piece.getItemId(), pieceCount ) );
continue;
}
this.updateDisplayMissing();
return false;
}
return true;
}
public AdventureResult[] getPieces()
{
return this.pieces.values().toArray( new AdventureResult[ this.pieces.values().size() ] );
}
public static int pieceHash( final AdventureResult piece )
{
if ( piece == null || piece == EquipmentRequest.UNEQUIP )
{
return 0;
}
return 1 << ( piece.getItemId() & 0x1F );
}
public static int equipmentHash( AdventureResult[] equipment )
{
int hash = 0;
// Must consider every slot that can contain an outfit piece
for ( int i = 0; i < EquipmentManager.FAMILIAR; ++i )
{
hash |= SpecialOutfit.pieceHash( equipment[ i ] );
}
return hash;
}
public void addPiece( final AdventureResult piece )
{
if ( piece != EquipmentRequest.UNEQUIP )
{
int type = EquipmentManager.itemIdToEquipmentType( piece.getItemId() );
if ( null != this.pieces.get( type ) )
{
// If a weapon is already equipped, set this piece to the offhand slot.
// If it is an accessory, find the next empty accessory slot.
if ( type == EquipmentManager.WEAPON )
{
type = EquipmentManager.OFFHAND;
}
else if ( type == EquipmentManager.ACCESSORY1 )
{
type = EquipmentManager.ACCESSORY2;
if ( null != this.pieces.get( type ) )
{
type = EquipmentManager.ACCESSORY3;
}
}
}
this.pieces.put( IntegerPool.get( type ), piece );
this.hash |= SpecialOutfit.pieceHash( piece );
}
}
private void updateDisplayMissing()
{
ArrayList<AdventureResult> missing = new ArrayList<AdventureResult>();
for ( int slot = 0; slot < EquipmentManager.FAMILIAR; slot++ )
{
AdventureResult piece = this.pieces.get( slot );
if ( null == piece )
{
continue;
}
boolean skip = false;
for ( int i = 0; i < missing.size(); i++ )
{
if ( missing.get( i ).getItemId() == piece.getItemId() )
{
skip = true;
break;
}
}
if ( skip )
{
continue;
}
int pieceCount = this.pieceCount( piece );
int accessibleCount = InventoryManager.getAccessibleCount( piece );
if ( accessibleCount < pieceCount )
{
missing.add( ItemPool.get( piece.getItemId(), pieceCount - accessibleCount ) );
continue;
}
}
for ( int i = 0; i < missing.size(); i++ )
{
AdventureResult item = missing.get( i );
RequestLogger.printLine( MafiaState.ERROR, "You need " + item.getCount() + " more " + item.getName() + " to continue." );
}
KoLmafia.updateDisplay(
MafiaState.ERROR, "Unable to wear outfit " + this.getName() + "." );
}
@Override
public String toString()
{
return this.outfitName;
}
public int getOutfitId()
{
return this.outfitId;
}
public String getName()
{
return this.outfitName;
}
public void setImage( final String image )
{
this.outfitImage = new String( image );
}
public String getImage()
{
return this.outfitImage;
}
@Override
public boolean equals( final Object o )
{
if ( o == null || !( o instanceof SpecialOutfit ) )
{
return false;
}
if ( this.outfitId != ( (SpecialOutfit) o ).outfitId )
{
return false;
}
return this.outfitName.equalsIgnoreCase( ( (SpecialOutfit) o ).outfitName );
}
@Override
public int hashCode()
{
return this.outfitId;
}
public int compareTo( final SpecialOutfit o )
{
if ( o == null || !( o instanceof SpecialOutfit ) )
{
return -1;
}
return this.outfitName.compareToIgnoreCase( ( (SpecialOutfit) o ).outfitName );
}
/**
* Restores a checkpoint. This should be called whenever the player needs to revert to their checkpointed outfit.
*/
private static final void restoreCheckpoint( final AdventureResult[] checkpoint )
{
AdventureResult equippedItem;
for ( int i = 0; i < checkpoint.length && !KoLmafia.refusesContinue(); ++i )
{
if ( checkpoint[ i ] == null )
{
continue;
}
equippedItem = EquipmentManager.getEquipment( i );
if ( equippedItem.equals( checkpoint[ i ] ) )
{
continue;
}
int itemId = checkpoint[ i ].getItemId();
if ( EquipmentManager.itemIdToEquipmentType( itemId ) == EquipmentManager.FAMILIAR )
{
FamiliarData familiar = KoLCharacter.getFamiliar();
if ( familiar == FamiliarData.NO_FAMILIAR )
{
KoLmafia.updateDisplay( MafiaState.ERROR, "You have no familiar with you." );
continue;
}
if ( !familiar.canEquip( checkpoint[ i ] ) )
{
KoLmafia.updateDisplay( MafiaState.ERROR, "Your " + familiar.getRace() + " can't wear a " + checkpoint[ i ].getName() );
continue;
}
}
RequestThread.postRequest( new EquipmentRequest( checkpoint[ i ], i ) );
}
}
/**
* Creates a checkpoint. This should be called whenever the player needs an outfit marked to revert to.
*/
public static final void createExplicitCheckpoint()
{
AdventureResult[] explicit = new AdventureResult[ EquipmentManager.SLOTS ];
for ( int i = 0; i < explicit.length; ++i )
{
explicit[ i ] = EquipmentManager.getEquipment( i );
}
SpecialOutfit.explicitPoints.push( explicit );
}
/**
* Restores a checkpoint. This should be called whenever the player needs to revert to their checkpointed outfit.
*/
public static final void restoreExplicitCheckpoint()
{
if ( SpecialOutfit.explicitPoints.isEmpty() )
{
return;
}
SpecialOutfit.restoreCheckpoint( (AdventureResult[]) SpecialOutfit.explicitPoints.pop() );
}
/**
* Creates a checkpoint. This should be called whenever the player needs an outfit marked to revert to.
*/
public static final void createImplicitCheckpoint()
{
synchronized ( SpecialOutfit.class )
{
AdventureResult[] implicit = new AdventureResult[ EquipmentManager.SLOTS ];
for ( int i = 0; i < implicit.length; ++i )
{
implicit[ i ] = EquipmentManager.getEquipment( i );
}
SpecialOutfit.implicitPoints.push( implicit );
EquipmentRequest.savePreviousOutfit();
}
}
/**
* Restores a checkpoint. This should be called whenever the player needs to revert to their checkpointed outfit.
*/
public static final void restoreImplicitCheckpoint()
{
if ( SpecialOutfit.implicitPoints.isEmpty() )
{
return;
}
AdventureResult[] implicit = (AdventureResult[]) SpecialOutfit.implicitPoints.pop();
if ( SpecialOutfit.implicitPoints.size() < SpecialOutfit.markedCheckpoint )
{
RequestThread.postRequest( new EquipmentRequest( SpecialOutfit.implicitOutfit ) );
SpecialOutfit.markedCheckpoint = -1;
}
else if ( SpecialOutfit.markedCheckpoint == -1 )
{
SpecialOutfit.restoreCheckpoint( implicit );
}
}
public static final void discardImplicitCheckpoint()
{
if ( SpecialOutfit.implicitPoints.isEmpty() )
{
return;
}
SpecialOutfit.implicitPoints.pop();
if ( SpecialOutfit.implicitPoints.size() < SpecialOutfit.markedCheckpoint )
{
SpecialOutfit.markedCheckpoint = -1;
}
}
public static final boolean markImplicitCheckpoint()
{
if ( SpecialOutfit.markedCheckpoint != -1 || SpecialOutfit.implicitPoints.isEmpty() )
{
return false;
}
SpecialOutfit.markedCheckpoint = SpecialOutfit.implicitPoints.size();
return true;
}
/**
* static final method used to determine all of the custom outfits,
* based on the given HTML enclosed in <code><select></code> tags.
*/
public static final void checkOutfits( final String selectHTML )
{
// Punt immediately if no outfits
if ( selectHTML == null )
{
return;
}
Matcher singleOutfitMatcher = SpecialOutfit.OPTION_PATTERN.matcher( selectHTML );
SpecialOutfit.implicitOutfit = null;
while ( singleOutfitMatcher.find() )
{
int outfitId = StringUtilities.parseInt( singleOutfitMatcher.group( 1 ) );
if ( outfitId >= 0 )
{
continue;
}
String outfitName = singleOutfitMatcher.group( 2 );
SpecialOutfit outfit = EquipmentManager.getCustomOutfit( outfitName );
if ( outfit == null )
{
outfit = new SpecialOutfit( outfitId, outfitName );
EquipmentManager.addCustomOutfit( outfit );
}
if ( outfitId != outfit.outfitId )
{
// Id has changed
outfit.outfitId = outfitId;
}
checkImplicitOutfit( outfit );
}
}
public static final void clearImplicitOutfit()
{
SpecialOutfit.implicitOutfit = null;
}
public static final void checkImplicitOutfit( final SpecialOutfit outfit )
{
if ( outfit.getName().equals( "Backup" ) )
{
SpecialOutfit.implicitOutfit = outfit;
}
}
/**
* Method to remove a particular piece of equipment from all active checkpoints,
* after it has been transformed or consumed.
*/
public static final void forgetEquipment( AdventureResult item )
{
SpecialOutfit.replaceEquipment( item, EquipmentRequest.UNEQUIP );
}
/**
* Method to replace a particular piece of equipment in all active checkpoints,
* after it has been transformed or consumed.
*/
public static void replaceEquipment( AdventureResult item , AdventureResult replaceWith )
{
for ( AdventureResult[] checkpoint : SpecialOutfit.implicitPoints )
{
for ( int j = 0; j < checkpoint.length; ++j )
{
if ( item.equals( checkpoint[ j ] ) )
{
checkpoint[ j ] = replaceWith;
}
}
}
for ( AdventureResult[] checkpoint : SpecialOutfit.explicitPoints )
{
for ( int j = 0; j < checkpoint.length; ++j )
{
if ( item.equals( checkpoint[ j ] ) )
{
checkpoint[ j ] = replaceWith;
}
}
}
}
public static void replaceEquipmentInSlot( AdventureResult item, int slot )
{
for ( AdventureResult[] checkpoint : SpecialOutfit.implicitPoints )
{
if ( slot < checkpoint.length )
{
checkpoint[ slot ] = item;
}
}
for ( AdventureResult[] checkpoint : SpecialOutfit.explicitPoints )
{
if ( slot < checkpoint.length )
{
checkpoint[ slot ] = item;
}
}
}
/**
* Method to remove all active checkpoints, in cases where restoring the original
* outfit is undesirable (currently, Fernswarthy's Basement).
*/
public static final void forgetCheckpoints()
{
SpecialOutfit.implicitPoints.clear();
SpecialOutfit.explicitPoints.clear();
SpecialOutfit.markedCheckpoint = -1;
}
}