Jump to content

New Shop API


liverare

Recommended Posts

Shop V2 Port

Shop Item V2 Port

 

 

 

 

 

I'll update more in the future, and feel free to contribute! smile.png

 

Shop.scr

import java.awt.Graphics2D;
import java.util.HashSet;
import java.util.Set;

import org.osbot.script.MethodProvider;
import org.osbot.script.Script;
import org.osbot.script.rs2.model.Item;
import org.osbot.script.rs2.model.NPC;
import org.osbot.script.rs2.ui.RS2Interface;
import org.osbot.script.rs2.ui.RS2InterfaceChild;

/**
 * 
 * @author LiveRare
 * 
 *         Shop class that provides simple access mechanisms to purchase items
 *         from a shop interface.
 * 
 */
public class Shop {

	/*
	 * CONSTANT VARIABLES
	 */

	/**
	 * Common parent ID of most (if not all) shops.
	 */
	public static final int COMMON_PARENT_ID = 300;

	/**
	 * Common child ID for the component that contains the data for the items.
	 */
	public static final int COMMON_CONTENT_CHILD_ID = 75;

	/**
	 * Common child ID for the shop's title.
	 */
	public static final int COMMON_TITLE_CHILD_ID = 76;

	/**
	 * Common child ID for the shop's close button.
	 */
	public static final int COMMON_CLOSE_CHILD_ID = 92;

	/**
	 * Maximum amount of slots available in a shop interface
	 */
	public static final int MAXIMUM_SLOT_AMOUNT = 40;

	/*
	 * DYNAMIC VARIABLES
	 */

	/**
	 * Script instance that will be accessing the interfaces from.
	 */
	private final Script script;

	/**
	 * A cache of 40 available items.
	 * 
	 * Note: 40 Is the maximum amount of item slots a shop can hold.
	 */
	private final ShopItem[] items;

	/**
	 * ID values for the parent interface and child components.
	 */
	private final int parentID, childContentID, childTitleID, childCloseID;

	/**
	 * Parent instance that will be accessed elsewhere in this class.
	 */
	private RS2Interface parent;

	/**
	 * Name of the clerk to interact with.
	 */
	private String npcName;

	/*
	 * Constructors
	 */

	public Shop(Script script, String npcName, int parentID,
			int childContentID, int childTitleID, int childCloseID) {
		this.script = script;
		this.npcName = npcName;
		this.parentID = parentID;
		this.childContentID = childContentID;
		this.childTitleID = childTitleID;
		this.childCloseID = childCloseID;
		this.items = new ShopItem[MAXIMUM_SLOT_AMOUNT];

		init();
	}

	public Shop(Script script, String npcName) {
		this(script, npcName, COMMON_PARENT_ID, COMMON_CONTENT_CHILD_ID,
				COMMON_TITLE_CHILD_ID, COMMON_CLOSE_CHILD_ID);
	}

	/*
	 * Analytics methods
	 */

	/**
	 * This class provides a property for an NPC name. This will allow the
	 * {@link Shop#open(String)} function to execute with a viable NPC name.
	 * 
	 * @param npcName
	 *            Name of the NPC to find
	 */
	public void setNPCName(String npcName) {
		this.npcName = npcName;
	}

	/**
	 * This method will initialise the ShopItem 2D array that can contain up to
	 * 40 ({@link Shop#MAXIMUM_SLOT_AMOUNT}).
	 */
	public void init() {
		for (int i = 0; i < MAXIMUM_SLOT_AMOUNT; i++)
			items[i] = new ShopItem(script, i);
	}

	/**
	 * @Deprecated this method is really inefficient usage of the item cache
	 *             acquired from the shop interface. Since only one index is
	 *             required, the rest are disregarded. This method should only
	 *             be regarded if it's absolutely necessary!
	 * 
	 * @param slot
	 *            Slot to validate
	 * @return <tt>Validation was successful</tt>
	 */
	@Deprecated
	public boolean validate(int slot) {
		try {
			Item[] items = getItems();
			this.items[slot].setDate(slot < items.length ? items[slot] : null);
			return true;
		} catch (Exception e) {
			this.items[slot].setDate(null);
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * This method will analyse the shop's contents and stores the data in the
	 * 2D ShopItem array.
	 * 
	 * @return <tt>Successfully validated the shop's content</tt>
	 */
	public boolean validate() {
		try {

			Item[] items = getItems();

			for (int i = 0; i < MAXIMUM_SLOT_AMOUNT; i++)
				this.items[i].setDate(i < items.length ? items[i] : null);
			
			return true;

		} catch (Exception e) {

			// If error occurs reset all ShopItem cache
			init();

			e.printStackTrace();

			return false;
		}
	}

	/**
	 * 
	 * @return <tt>Shop interface is valid, thus; shop is open</tt>
	 */
	public boolean isOpen() {
		return (parent = script.client.getInterface(parentID)) != null
				&& parent.isValid() && parent.isVisible();
	}

	/**
	 * 
	 * @return Shop's title
	 */
	public String getTitle() {
		return parent.getChild(childTitleID).getMessage();
	}

	/**
	 * 
	 * @return Items in the shop
	 */
	public Item[] getItems() {
		return parent.getItems(childContentID);
	}
	
	public int getAmount(int... itemIDs) {
		
		if (itemIDs == null || itemIDs.length == 0)
			return -1;
		
		int amount = 0;
		
		i: for (ShopItem item : items) if (item != null) for (int id : itemIDs) {
			
			if (item.getID() == id) {
				amount += item.getAmount();
				continue i;
			}
		}
		
		return amount;
	}
	
	public int getAmount(String... itemNames) {
		
		if (itemNames == null || itemNames.length == 0)
			return -1;
		
		int amount = 0;
		
		i: for (ShopItem item : items) if (item != null && isStringValid(item.getName())) for (String name : itemNames) if (isStringValid(name)) {
			
			if (item.getName().equalsIgnoreCase(name)) {
				amount += item.getAmount();
				continue i;
			}
		}
		
		return amount;
	}

	/**
	 * Acquire an item based on its slot in the shop interface.
	 * 
	 * ONLY USE THIS METHOD IF YOU <u>KNOW</u> FOR CERTAIN THE SLOT'S ITEM WILL
	 * BE CONSISTENT, OTHERWISE USE ANOTHER GETTER.
	 * 
	 * @param slot
	 *            Slot position within the shop
	 * @return ShopItem by index
	 */
	public ShopItem getItemBySlot(int slot) {
		return slot >= 0 && slot < MAXIMUM_SLOT_AMOUNT ? this.items[slot]
				: null;
	}

	/**
	 * Acquire an item based on item ID.
	 * 
	 * @param ids
	 *            IDs of the item to search for
	 * @return First shop item with a corresponding ID
	 */
	public ShopItem getItemByID(int... ids) {
		if (ids != null && ids.length > 0)
			for (ShopItem nextItem : items)
				for (int nextID : ids)
					if (nextItem.getID() == nextID)
						return nextItem;
		return null;
	}

	public ShopItem[] getItemsByID(int... ids) {
		Set<ShopItem> cache = new HashSet<>();
		if (ids != null && ids.length > 0)
			for (ShopItem nextItem : items)
				for (int nextID : ids)
					if (nextItem.getID() == nextID)
						cache.add(nextItem);
		return cache.isEmpty() ? null : cache
				.toArray(new ShopItem[cache.size()]);
	}

	/**
	 * Acquire an item based on item name.
	 * 
	 * @param names
	 *            Names of the item to search for
	 * @return First shop item with a corresponding name
	 */
	public ShopItem getItemByName(String... names) {
		if (names != null && names.length > 0)
			for (ShopItem nextItem : items) {
				String name = nextItem.getName();
				if (!isStringValid(name))
					continue;
				else
					for (String nextName : names)
						if (isStringValid(nextName)
								&& name.equalsIgnoreCase(nextName))
							return nextItem;
			}
		return null;
	}

	public ShopItem[] getItemsByName(String... names) {
		Set<ShopItem> cache = new HashSet<>();
		if (names != null && names.length > 0)
			for (ShopItem nextItem : items) {
				String name = nextItem.getName();
				if (!isStringValid(name))
					continue;
				else
					for (String nextName : names)
						if (isStringValid(nextName)
								&& name.equalsIgnoreCase(nextName))
							cache.add(nextItem);
			}
		return cache.isEmpty() ? null : cache
				.toArray(new ShopItem[cache.size()]);
	}

	public void paint(Graphics2D g) {
		for (ShopItem next : items)
			next.draw(g);
	}

	/*
	 * Interact methods
	 */

	public int tryPurchaseInvAmount(ShopItem item)
			throws InterruptedException {
		final int empty = script.client.getInventory()
				.getEmptySlots();
		return tryPurchase(item, item.getAmount() > empty ? empty : item.getAmount());
	}

	public int tryPurchase(ShopItem item, int amount)
			throws InterruptedException {

		if (item != null && amount > 0 && this.isOpen()) {

			this.validate();

			int total = amount, onesPurchase = 0, fivesPurchase = 0, tensPurchase = 0;

			if (total >= 10) {
				tensPurchase = total / 10;
				total -= tensPurchase * 10;
			}
			if (total >= 5) {
				fivesPurchase = 1;
				total -= 5;
			}

			onesPurchase = total;

			int count = 0;

			try {

				for (int i = 0; i < tensPurchase; i++)
					if (purchase(item, 10))
						count += 10;
				for (int i = 0; i < fivesPurchase; i++)
					if (purchase(item, 5))
						count += 5;
				for (int i = 0; i < onesPurchase; i++)
					if (purchase(item, 1))
						count += 1;

			} catch (RuntimeException e) {

				script.log(e.getMessage());

			}

			return amount - count;
		}
		return -1;
	}

	private boolean purchase(ShopItem item, int amount)
			throws InterruptedException {
		final int oldID = item.getID();
		validate(item.getSlot());
		if (oldID != item.getID())
			throw new RuntimeException("Inconsistent item! Required: " + oldID
					+ ", but found: " + item.getID());
		else if (item.getAmount() <= 0)
			return false;
		switch (amount) {
		case 1:
			return item.purchaseOne();
		case 5:
			return item.purchaseFive();
		case 10:
			return item.purchaseTen();
		}
		return false;
	}
		
	public int trySell(Item item, int amount) throws InterruptedException {

		if (item != null && amount > 0 && this.isOpen()) {

			this.validate();

			int total = amount, onesPurchase = 0, fivesPurchase = 0, tensPurchase = 0;

			if (total >= 10) {
				tensPurchase = total / 10;
				total -= tensPurchase * 10;
			}
			if (total >= 5) {
				fivesPurchase = 1;
				total -= 5;
			}

			onesPurchase = total;

			int count = 0;

			try {

				for (int i = 0; i < tensPurchase; i++)
					if (sell(item, 10))
						count += 10;
				for (int i = 0; i < fivesPurchase; i++)
					if (sell(item, 5))
						count += 5;
				for (int i = 0; i < onesPurchase; i++)
					if (sell(item, 1))
						count += 1;

			} catch (RuntimeException e) {

				script.log(e.getMessage());

			}

			return amount - count;
		}
		return -1;
	}
	
	private boolean sell(Item item, int amount) throws InterruptedException {
		
		return script.client.getInventory().interactWithId(item.getId(), "Sell " + amount);
	}

	/**
	 * This method requires a pre-initialised <i>valid</i> NPC name or one needs
	 * to be provided on the constructor.
	 * 
	 * @param altNPCName
	 *            Alternative NPC to search for
	 * @return <tt>Shop is open</tt>
	 */
	public boolean tryOpen(String altNPCName) throws InterruptedException {

		boolean open = isOpen();

		try {
			if (open)
				return true;

			NPC npc = script
					.closestNPCForName(isStringValid(altNPCName) ? altNPCName
							: npcName);
			if (npc != null && npc.exists() && npc.interact("Trade"))
				script.sleep(MethodProvider.random(350, 600));

			return open = isOpen();
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		} finally {
			if (open)
				validate();
		}
	}

	public boolean tryOpen() throws InterruptedException {
		return tryOpen(null);
	}

	public boolean close() throws InterruptedException {
		if (!isOpen()) // Prevent unnecessary re-closing
			return false;
		RS2InterfaceChild child = parent.getChild(childCloseID);
		return child != null && child.interact("Close");
	}

	/*
	 * Static methods
	 */
	
	private static boolean isStringValid(String arg0) {
		return arg0 != null && !arg0.isEmpty();
	}

} 

ShopItem.scr

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Stroke;

import org.osbot.script.Script;
import org.osbot.script.mouse.RectangleDestination;
import org.osbot.script.rs2.model.Item;

/**
 * 
 * @author LiveRare
 * 
 * This class is designed to store the details for every item
 * within a shop interface. There should be only 40 instances
 * of this object stored in an array because there's a maximum
 * of 40 available item slots in a shop interface.
 * 
 */
public class ShopItem {

	public static final Stroke STROKE = new BasicStroke(0.655f);
	public static final Color FOREGROUND_COLOR = new Color(255, 255, 255, 150);

	public static final Point ITEM_STARTING_POSITION = new Point(80, 70);
	public static final Dimension ITEM_BOUNDS = new Dimension(30, 25);
	public static final Dimension SPACE_MARGIN = new Dimension(17, 23);

	private final Script script;
	private final int slot;
	private final int slotColumn;
	private final int slotRow;
	private final Rectangle slotBounds;
	private final RectangleDestination slotDestination;

	private int id;
	private String name;
	private int amount;

	public ShopItem(Script script, int slot) {
		this.script = script;
		this.slot = slot;
		this.slotColumn = (slot % 8);
		this.slotRow = (int) (slot / (double) 8);
		this.slotBounds = new Rectangle(
				ITEM_STARTING_POSITION.x + (ITEM_BOUNDS.width + SPACE_MARGIN.width) * (slotColumn),
				ITEM_STARTING_POSITION.y + (ITEM_BOUNDS.height + SPACE_MARGIN.height) * (slotRow),
				ITEM_BOUNDS.width,
				ITEM_BOUNDS.height);
		this.slotDestination = new RectangleDestination(slotBounds);
	}

	@Override
	public boolean equals(Object obj) {
		return obj != null
				&& obj instanceof ShopItem
				&& ((ShopItem) obj).getSlot() == getSlot();
	}

	@Override
	public String toString() {
		return "[Slot: " + slot + " | Name: " + name + " | Item ID: " + id + " | Amount: " + amount + "]";
	}
	
	/*
	 * Item profile values
	 */

	public int getSlot() {
		return slot;
	}

	public int getID() {
		return id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public void setID(int id) {
		this.id = id;
	}

	public int getAmount() {
		return amount;
	}

	public void setAmount(int amount) {
		this.amount = amount;
	}

	public int getSlotColumn() {
		return slotColumn;
	}
	
	public void setDate(Item item) {
		if (item != null) {
			this.id = item.getId();
			this.name = item.getName();
			this.amount = item.getAmount();
		} else {
			this.id = -1;
			this.name = "null";
			this.amount = -1;
		}
	}
	
	/*
	 * Other methods
	 */

	public int getSlotRow() {
		return slotRow;
	}

	public Rectangle getSlotBounds() {
		return slotBounds;
	}

	public RectangleDestination getSlotDestination() {
		return slotDestination;
	}
	
	public boolean interact(String interact) throws InterruptedException {
		script.client.moveMouse(getSlotDestination(), false);
		return script.selectOption(null, getSlotDestination(), interact);	
	}
	
	public boolean purchaseOne() throws InterruptedException {
		return interact("Buy 1");
	}
	
	public boolean purchaseFive() throws InterruptedException {
		return interact("Buy 5");
	}
	
	public boolean purchaseTen() throws InterruptedException {
		return interact("Buy 10");
	}
	
	public void draw(Graphics2D g) {
		
		Rectangle r = new Rectangle(slotBounds.x - 4, slotBounds.y - 6, slotBounds.width + 4, slotBounds.height + 10);
		
		{ // Draw bounding box
			g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
			g.setStroke(STROKE);
			g.setColor(FOREGROUND_COLOR);
			g.draw(r);
			g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_DEFAULT);
		}
		
		{ // Draw text
			g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
			FontMetrics fm = g.getFontMetrics();
			String id = String.valueOf(this.id);
			int width = (int) fm.getStringBounds(id, g).getWidth();
			Point p = new Point(r.x + ((r.width - width) / 2), r.y + 32);
			g.setColor(Color.BLACK);
			g.drawString(id, p.x - 1, p.y - 1);
			g.drawString(id, p.x - 1, p.y + 1);
			g.drawString(id, p.x + 1, p.y - 1);
			g.drawString(id, p.x + 1, p.y + 1);
			g.setColor(Color.WHITE);
			g.drawString(id, p.x, p.y);
			g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
		}
	}

}

Example of usage:


	Shop shop;
		
	@Override
	public void onStart() {
		
		shop = new Shop(this, "Shop keeper"); // Shop keeper in the Varrock general store
		
	}
	
	@Override
	public int onLoop() throws InterruptedException {
		
		if (shop.isOpen() && shop.validate()) { //Validate method is required to launch at least once before item profiling
			
			ShopItem item = shop.getItemByName("Willow shortbow (u)"); // Common item dumped in that store
			
			if (item != null) { // ENSURE TO NULL-CHECK
				
				int failedToPurchaseCount = shop.tryPurchase(item, 26); // Item amount that wasn't purchased!
				
				log("Need to purchase: " + failedToPurchaseCount + " more..."); // Debug string
			}
		} else {
			shop.tryOpen(); // shop.tryOpen(String) can be used instead if you didn't already assign ab NPC name on constructor
		}
			
		return 300;
	}
	
	@Override
	public void onPaint(Graphics arg0) {
		
		Graphics2D g = (Graphics2D) arg0;
		
		if (shop != null && shop.isOpen()) {
			
			shop.paint(g);
			
		}
	}

shop_zpsfbfa36f2.png

 

Note: There may be a problem with items with a quantity of 0. I will look into this tomorrow.

Edited by liverare
  • Like 2
Link to comment
Share on other sites

There's still a small bug if the stock of the shop is zero, you can get a NullPointerException.

 

shop.getItemByName("zzz").getAmount() == 1 gets an error if the stock of zzz is 0.

 

I managed to "fix" this by assigning each item I'm buying as a variable and running a null check before the shop.getItemByName("zzz").getAmount() == 1 portion of the code. However, it would be nice and it would save a lot of space when doing multiple items if this could be programmed into the API.

 

Also the shop.close() method doesn't close the store 100% of the time. I use a do until !shop.isOpen around my shop.close(). Maybe this could be programmed into the API as well. 

 

Other than that, this has been working perfectly for me. Very nice release. Should be something that's added to OSBot's API IMO. :)

Link to comment
Share on other sites

I think there was a runescape update or something, the shop API doesn't work anymore.

It won't find the items I'm looking for anymore.

I'm guessing it's just a child ID issue, but I'm not sure.

 

EDIT:

..nevermind it's working fine now. Not a single idea what happened.

 

EDIT2:

Figured it out.
Here's what happened:
Created shop object.

Sold pure essence.

Since I created the shop object when there was no pure essence, it didn't register that there was now pure essence, so it was returning null.

I was trying to recreate the shop object, and that just messed all kinds of things up.

 

Could you add a "refresh shop" method maybe?

Or I'm wrong and it worked perfectly fine just...not for a bit there.

Edited by Nezz
Link to comment
Share on other sites

		if (shop.isOpen() && shop.validate()) { //Validate method is required to launch at least once before item profiling

Perhaps my choice of words were bad. Would you prefer Shop#validate() to be renamed to Shop#update() ? It actual does read rather nicely compared to the current. :)

 

And also I may take into consideration the ability to declare a ShopItem as a field variable, but to then validate it later on. Hmm...I think it can already be done (to an extent), but for more dev-friendly I may remove the abstract modifier and implement a new interface that links through to Shop. AKA: I think I've got a nice idea - inspired by you. :)

Link to comment
Share on other sites

  • 2 weeks later...
Guest
This topic is now closed to further replies.
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...