diff --git a/data/plugins/shops/currency.rb b/data/plugins/shops/currency.rb
new file mode 100644
index 00000000..b0c9a5fc
--- /dev/null
+++ b/data/plugins/shops/currency.rb
@@ -0,0 +1,35 @@
+require 'java'
+
+java_import 'org.apollo.cache.def.ItemDefinition'
+
+# A currency that can be used to purchase items in a Shop.
+class Currency
+ attr_reader :name
+
+ # Creates the Currency.
+ def initialize(id, name = ItemDefinition.lookup(id).name)
+ fail 'Currency must have a name.' if name.nil?
+ @id = id
+ @name = name.to_s
+ end
+
+ # Adds the specified amount of this `Currency` to the specified `Player`'s inventory.
+ def add(player, amount)
+ player.inventory.add(@id, amount)
+ end
+
+ # Removes the specified amount of this `Currency` from the specified `Player`'s inventory.
+ def remove(player, amount)
+ player.inventory.remove(@id, amount)
+ end
+
+ # Gets the amount of this Currency in the specified player's inventory.
+ def total(player)
+ player.inventory.get_amount(@id)
+ end
+
+ def sell_value(id)
+ (ItemDefinition.lookup(id).value * 0.60).floor
+ end
+
+end
diff --git a/data/plugins/shops/plugin.xml b/data/plugins/shops/plugin.xml
new file mode 100644
index 00000000..e8d68231
--- /dev/null
+++ b/data/plugins/shops/plugin.xml
@@ -0,0 +1,20 @@
+
+
+ shops
+ 0.1
+ Shops
+ Adds shop support.
+
+ Stuart
+ Major
+
+
+
+
+
+
+
+
+ util
+
+
diff --git a/data/plugins/shops/shop.rb b/data/plugins/shops/shop.rb
new file mode 100644
index 00000000..3cb73a99
--- /dev/null
+++ b/data/plugins/shops/shop.rb
@@ -0,0 +1,28 @@
+require 'java'
+
+java_import 'org.apollo.game.model.inv.Inventory'
+
+# A shop containing items that can be sold.
+class Shop
+ attr_reader :buys, :currency, :items, :inventory, :name, :npc_options
+
+ def initialize(name, items, currency, options, buys)
+ @name = name
+ @items = items
+ @currency = currency
+ @buys = buys
+ @npc_options = options
+ @inventory = Inventory.new(DEFAULT_CAPACITY, Inventory::StackMode::STACK_ALWAYS)
+
+ items.each { |item| @inventory.add(item.id, item.amount) }
+ end
+
+end
+
+private
+
+# The `Currency` used by default.
+DEFAULT_CURRENCY = Currency.new(995, 'money')
+
+# The default capacity of a shop.
+DEFAULT_CAPACITY = 30
diff --git a/data/plugins/shops/shop_item.rb b/data/plugins/shops/shop_item.rb
new file mode 100644
index 00000000..a761702c
--- /dev/null
+++ b/data/plugins/shops/shop_item.rb
@@ -0,0 +1,20 @@
+require 'java'
+
+java_import 'org.apollo.cache.def.ItemDefinition'
+
+java_import 'org.apollo.game.model.Item'
+
+# An Item in a Shop.
+class ShopItem
+ attr_reader :amount, :cost, :id, :name
+
+ # Creates the ShopItem.
+ def initialize(id, amount, cost = nil)
+ definition = ItemDefinition.lookup(id)
+ @id = id
+ @amount = amount
+ @cost = cost.nil? ? definition.value : cost
+ @name = definition.name
+ end
+
+end
diff --git a/data/plugins/shops/shops.rb b/data/plugins/shops/shops.rb
new file mode 100644
index 00000000..9f5f9cb8
--- /dev/null
+++ b/data/plugins/shops/shops.rb
@@ -0,0 +1,297 @@
+require 'java'
+
+java_import 'org.apollo.cache.def.ItemDefinition'
+
+java_import 'org.apollo.game.action.DistancedAction'
+java_import 'org.apollo.game.message.impl.SetWidgetTextMessage'
+java_import 'org.apollo.game.message.handler.ItemVerificationHandler'
+java_import 'org.apollo.game.model.inv.SynchronizationInventoryListener'
+java_import 'org.apollo.game.model.inter.InterfaceListener'
+
+# The hash of npc ids to Shops.
+SHOPS = {}
+
+# Creates the Shop from the specified Hash.
+def create_shop(hash)
+ unless hash.has_keys?(:items, :name, :npc_id)
+ fail 'Shop name, npc, and items must be specified to create a shop.'
+ end
+
+ npc_id, name = hash[:npc_id], hash[:name]
+ currency = hash[:currency] || DEFAULT_CURRENCY
+
+ options = hash[:npc_options] || [1]
+ buys = hash[:buys] || :own
+
+ items = hash.delete(:items).collect { |data| ShopItem.new(*data) }
+ shop = Shop.new(name, items, currency, options, buys)
+
+ SHOPS[npc_id] = shop
+end
+
+private
+
+# The sidebar id for the inventory, when a Shop window is open.
+INVENTORY_SIDEBAR = 3822
+
+# The container id for the above inventory, when a Shop window is open.
+INVENTORY_CONTAINER = 3823
+
+# The Shop interface id.
+SHOP_INTERFACE = 3824
+
+# The container id for the Shop interface.
+SHOP_CONTAINER = 3900
+
+# The widget that displays the shop name.
+SHOP_NAME_WIDGET = 3901
+
+# The delay before a Shop is opened when the Player is in range of the Npc, in ticks.
+SHOP_OPEN_DELAY = 0
+
+# The distance, in tiles, the Player must reach before a Shop can be opened.
+SHOP_DISTANCE = 1
+
+# An `InventorySupplier` for a `Shop`.
+class ShopInventorySupplier
+ java_implements ItemVerificationHandler::InventorySupplier
+
+ def getInventory(player)
+ shop = player.open_shop
+ shop == -1 ? nil : SHOPS[shop].inventory
+ end
+
+end
+
+# An `InventorySupplier` for a `Player` with the shop window open.
+class PlayerInventorySupplier
+ java_implements ItemVerificationHandler::InventorySupplier
+
+ def getInventory(player)
+ player.open_shop == -1 ? nil : player.inventory
+ end
+
+end
+
+ItemVerificationHandler.add_inventory(SHOP_CONTAINER, ShopInventorySupplier.new)
+ItemVerificationHandler.add_inventory(INVENTORY_CONTAINER, PlayerInventorySupplier.new)
+
+# A DistancedAction causing a Player to open a shop.
+class OpenShopAction < DistancedAction
+ attr_reader :player, :npc, :shop
+
+ # Creates the OpenShopAction.
+ def initialize(player, npc, shop)
+ super(SHOP_OPEN_DELAY, true, player, npc.position, 1)
+ @npc = npc
+ @shop = shop
+ end
+
+ # Executes this DistancedAction, opening the shop.
+ def executeAction
+ mob.interacting_mob = @npc
+ open_shop(mob, @npc.id)
+ stop
+ end
+
+ # Returns whether or not this DistancedAction is equal to the specified Object.
+ def equals(other)
+ get_class == other.get_class && @npc == other.npc && @shop == other.shop
+ end
+
+end
+
+# An InterfaceListener for when a Shop is closed.
+class ShopCloseInterfaceListener
+ java_implements InterfaceListener
+
+ # Creates the ShopCloseInterfaceListener.
+ def initialize(player, inventory_listener, shop_listener)
+ @player = player
+ @inventory_listener = inventory_listener
+ @shop_listener = shop_listener
+ end
+
+ # Executed when the Shop interface is closed.
+ def interface_closed
+ @player.inventory.remove_listener(@inventory_listener)
+ SHOPS[@player.open_shop].inventory.remove_listener(@shop_listener)
+
+ @player.open_shop = -1
+ @player.reset_interacting_mob
+ end
+
+end
+
+# Intercept the npc action message.
+on :message, :first_npc_action do |player, message|
+ npc = $world.npc_repository.get(message.index)
+
+ if SHOPS.key?(npc.id)
+ shop = SHOPS[npc.id]
+
+ valid = shop.npc_options.empty? || shop.npc_options.include?(message.option)
+ player.start_action(OpenShopAction.new(player, npc, SHOPS[npc.id])) if valid
+ end
+end
+
+# Opens the Shop registered to the specified npc.
+def open_shop(player, npc)
+ shop = SHOPS[npc]
+ fail "No shop registered to npc #{npc} exists." if shop.nil?
+
+ player.open_shop = npc
+
+ inventory_listener = SynchronizationInventoryListener.new(player, INVENTORY_CONTAINER)
+ shop_listener = SynchronizationInventoryListener.new(player, SHOP_CONTAINER)
+
+ player_inventory, shop_inventory = player.inventory, shop.inventory
+
+ player_inventory.add_listener(inventory_listener)
+ player_inventory.force_refresh
+
+ shop_inventory.add_listener(shop_listener)
+ shop_inventory.force_refresh
+
+ player.send(SetWidgetTextMessage.new(SHOP_NAME_WIDGET, shop.name))
+
+ listener = ShopCloseInterfaceListener.new(player, inventory_listener, shop_listener)
+ player.interface_set.open_window_with_sidebar(listener, SHOP_INTERFACE, INVENTORY_SIDEBAR)
+end
+
+# Intercept the Item action.
+on :message, :item_action do |player, message|
+ interface = message.interface_id
+
+ if interface == INVENTORY_CONTAINER || interface == SHOP_CONTAINER
+ if player.open_shop == -1 || !SHOPS.key?(player.open_shop)
+ message.terminate
+ next
+ end
+ end
+
+ shop = SHOPS[player.open_shop]
+ inventory = shop.inventory
+ currency = shop.currency
+ slot = message.slot
+
+ player_inventory = player.inventory
+
+ if interface == INVENTORY_CONTAINER
+ id = message.id
+ contains = inventory.contains(id)
+
+ if !shop.buys == :none || shop.buys == :own && !contains
+ player.send_message('You can\'t sell this item to this shop.')
+ message.terminate
+ next
+ end
+
+ if !contains && inventory.free_space == 0
+ player.send_message('The shop is currently full at the moment.')
+ message.terminate
+ next
+ end
+
+ item = player_inventory.get(slot)
+ value = currency.sell_value(id)
+
+ option = message.option
+ if option == 1
+ player.send_message("#{item.definition.name}: shop will buy for #{value} #{currency.name}.")
+ next
+ end
+
+ sell_amount = case option
+ when 2 then 1
+ when 3 then 5
+ when 4 then 10
+ else next
+ end
+
+ available = player_inventory.get_amount(id)
+ sell_amount = available if sell_amount > available
+
+ total_value = (value * sell_amount).floor
+
+ player_inventory.remove(id, sell_amount)
+ inventory.add(id, sell_amount)
+ currency.add(player, total_value) if total_value > 0
+
+ message.terminate
+ elsif interface == SHOP_CONTAINER
+ buy(shop, player, message, currency)
+ end
+end
+
+# Buys the item from the `Shop`.
+def buy(shop, player, message, currency)
+ inventory, slot = shop.inventory, message.slot
+ shop_item, invent_item = shop.items[slot], inventory.get(slot)
+
+ id = shop_item.id
+
+ option = message.option
+ if option == 1
+ player.send_message("#{shop_item.name}: currently costs #{shop_item.cost} #{currency.name}.")
+ next
+ end
+
+ buy_amount = case option
+ when 2 then 1
+ when 3 then 5
+ when 4 then 10
+ else next
+ end
+
+ no_stock = false
+ if buy_amount > invent_item.amount
+ buy_amount = invent_item.amount
+ no_stock = true
+ end
+
+ player_inventory = player.inventory
+ has_item = player_inventory.get_amount(id) == 0
+
+ definition = invent_item.definition
+ space_required = if definition.stackable && has_item then 0
+ elsif !definition.stackable then buy_amount
+ else 1
+ end
+
+ free_slots = player_inventory.free_slots
+ not_enough_space = false
+
+ if space_required > free_slots
+ not_enough_space = true
+ buy_amount = free_slots
+ end
+
+ total_currency = shop.currency.total(player)
+ too_poor = false
+ total_cost = buy_amount * shop_item.cost
+
+ if total_cost > total_currency
+ buy_amount = (total_currency / shop_item.cost).floor
+ too_poor = true
+ end
+
+ if buy_amount > 0
+ currency.remove(player, buy_amount * shop_item.cost)
+ player_inventory.add(id, buy_amount)
+
+ keep = invent_item.amount == buy_amount && shop.buys == :own
+ keep ? inventory.set(slot, Item.new(id, 0)) : inventory.remove(id, buy_amount)
+ end
+
+ warning = if too_poor then "You don't have enough #{currency.name}."
+ elsif no_stock then 'The shop has run out of stock.'
+ elsif not_enough_space then 'You don\'t have enough inventory space.'
+ end
+
+ player.send_message(warning) unless warning.nil?
+ message.terminate
+end
+
+# Declares the open_shop attribute, which contains the id of the currently open shop.
+declare_attribute(:open_shop, -1)
diff --git a/game/src/main/org/apollo/game/message/handler/ItemVerificationHandler.java b/game/src/main/org/apollo/game/message/handler/ItemVerificationHandler.java
index 178417e6..5f61d804 100644
--- a/game/src/main/org/apollo/game/message/handler/ItemVerificationHandler.java
+++ b/game/src/main/org/apollo/game/message/handler/ItemVerificationHandler.java
@@ -25,7 +25,7 @@ public final class ItemVerificationHandler extends MessageHandler= inventory.capacity()) {
+ if (inventory == null || slot < 0 || slot >= inventory.capacity()) {
message.terminate();
return;
}