From b01c9cd51901c894ade643db3d68fa144180d0a0 Mon Sep 17 00:00:00 2001 From: Nickolaj Madsen Date: Thu, 20 Feb 2020 10:51:14 +0100 Subject: [PATCH 1/2] Implemented the proper logic for the 'buy x' packets. --- data/config/npc-spawns.yaml | 8 ++ data/config/shops.yaml | 70 ++++++++++++++++ .../npc/lumbridge/bobs-axes/bob-plugin.ts | 9 +++ .../general-store/shopkeeper-plugin.ts | 9 +++ src/world/config/shops.ts | 80 +++++++++++++++++++ src/world/items/item-container.ts | 6 +- .../mob/player/action/buy-item-action.ts | 15 ++++ src/world/mob/player/game-interface.ts | 3 +- .../mob/player/packet/impl/buy-item-packet.ts | 49 ++++++++++++ .../packet/incoming-packet-directory.ts | 7 +- src/world/world.ts | 3 + 11 files changed, 256 insertions(+), 3 deletions(-) create mode 100644 data/config/shops.yaml create mode 100644 src/plugins/npc/lumbridge/bobs-axes/bob-plugin.ts create mode 100644 src/plugins/npc/lumbridge/general-store/shopkeeper-plugin.ts create mode 100644 src/world/config/shops.ts create mode 100644 src/world/mob/player/action/buy-item-action.ts create mode 100644 src/world/mob/player/packet/impl/buy-item-packet.ts diff --git a/data/config/npc-spawns.yaml b/data/config/npc-spawns.yaml index f055caac..90aa3f1a 100644 --- a/data/config/npc-spawns.yaml +++ b/data/config/npc-spawns.yaml @@ -14,3 +14,11 @@ x: 3222 y: 3220 radius: 4 +- npcId: 520 + x: 3211 + y: 3247 + radius: 1 +- npcId: 519 + x: 3230 + y: 3203 + radius: 1 diff --git a/data/config/shops.yaml b/data/config/shops.yaml new file mode 100644 index 00000000..bab4e412 --- /dev/null +++ b/data/config/shops.yaml @@ -0,0 +1,70 @@ +- identification: LUMBRIDGE_GENERAL_STORE + name: Lumbridge General Store + items: + - id: 1931 + text: Pot + price: 1 + amountInStock: 5 + - id: 1935 + text: Jug + price: 1 + amountInStock: 2 + - id: 5603 + text: Shears + price: 1 + amountInStock: 2 + - id: 1925 + text: Bucket + price: 2 + amountInStock: 3 + - id: 1887 + text: Cake tin + price: 13 + amountInStock: 2 + - id: 590 + text: Tinderbox + price: 1 + amountInStock: 2 + - id: 1755 + text: Chisel + price: 1 + amountInStock: 2 + - id: 952 + text: Spade + price: 3 + amountInStock: 5 + - id: 2347 + text: Hammer + price: 1 + amountInStock: 5 +- identification: BOBS_AXES + name: Bob's Brilliant Axes + items: + - id: 1265 + text: Bronze pickaxe + price: 1 + amountInStock: 5 + - id: 1351 + text: Bronze axe + price: 16 + amountInStock: 10 + - id: 1349 + text: Iron axe + price: 56 + amountInStock: 5 + - id: 1353 + text: Steel axe + price: 200 + amountInStock: 3 + - id: 1363 + text: Iron battleaxe + price: 182 + amountInStock: 5 + - id: 1365 + text: Steel battleaxe + price: 650 + amountInStock: 2 + - id: 1369 + text: Mithril battleaxe + price: 1690 + amountInStock: 1 diff --git a/src/plugins/npc/lumbridge/bobs-axes/bob-plugin.ts b/src/plugins/npc/lumbridge/bobs-axes/bob-plugin.ts new file mode 100644 index 00000000..6518f87b --- /dev/null +++ b/src/plugins/npc/lumbridge/bobs-axes/bob-plugin.ts @@ -0,0 +1,9 @@ +import { npcAction, NpcActionPlugin } from '@server/world/mob/player/action/npc-action'; +import { openShop } from '@server/world/config/shops'; + +const action: npcAction = (details) => { + const { player, npc } = details; + openShop(details.player, 'BOBS_AXES'); +}; + +export default { npcIds: 519, options: 'trade', walkTo: true, action } as NpcActionPlugin; diff --git a/src/plugins/npc/lumbridge/general-store/shopkeeper-plugin.ts b/src/plugins/npc/lumbridge/general-store/shopkeeper-plugin.ts new file mode 100644 index 00000000..33a04ee7 --- /dev/null +++ b/src/plugins/npc/lumbridge/general-store/shopkeeper-plugin.ts @@ -0,0 +1,9 @@ +import { npcAction, NpcActionPlugin } from '@server/world/mob/player/action/npc-action'; +import { openShop } from '@server/world/config/shops'; + +const action: npcAction = (details) => { + const { player, npc } = details; + openShop(details.player, 'LUMBRIDGE_GENERAL_STORE'); +}; + +export default { npcIds: 520, options: 'trade', walkTo: true, action } as NpcActionPlugin; diff --git a/src/world/config/shops.ts b/src/world/config/shops.ts new file mode 100644 index 00000000..2a4b82b6 --- /dev/null +++ b/src/world/config/shops.ts @@ -0,0 +1,80 @@ +import { logger } from '@runejs/logger/dist/logger'; +import { JSON_SCHEMA, safeLoad } from 'js-yaml'; +import { readFileSync } from 'fs'; +import { world } from '@server/game-server'; +import { Player } from '@server/world/mob/player/player'; + +export enum ShopName { + +} +export interface Shop { + identification: string; + name: string; + interfaceId: number; + items: ShopItems[]; +} + +interface ShopItems { + id: number; + name: string; + amountInStock: number; + price: number; +} + +export function parseShops(): Shop[] { + try { + logger.info('Parsing shops...'); + + const shops = safeLoad(readFileSync('data/config/shops.yaml', 'utf8'), { schema: JSON_SCHEMA }) as Shop[]; + + if(!shops || shops.length === 0) { + throw 'Unable to read shops.'; + } + + logger.info(`${shops.length} shops found.`); + + return shops; + } catch(error) { + logger.error('Error parsing shops: ' + error); + return null; + } +} + +function findShop(identification: string): Shop { + for(let i = 0; i <= world.shops.length; i++) { + if(world.shops[i].identification === identification) return world.shops[i]; + } + return undefined; +} + +export function openShop(player: Player, identification: string, closeOnWalk: boolean = true): void { + try { + const openedShop = findShop(identification); + if(openedShop === undefined) { + throw `Unable to find the shop with identification of: ${identification}`; + } + player.packetSender.updateInterfaceString(3901, openedShop.name); + for(let i = 0; i < 30; i++) { + if(openedShop.items.length <= i) { + player.packetSender.sendUpdateSingleInterfaceItem(3900, i, null); + } else { + player.packetSender.sendUpdateSingleInterfaceItem(3900, i, { + itemId: openedShop.items[i].id, amount: openedShop.items[i].amountInStock + }); + } + } + for(let i = 0; i < openedShop.items.length; i++) { + player.packetSender.sendUpdateSingleInterfaceItem(3900, i, { + itemId: openedShop.items[i].id, amount: openedShop.items[i].amountInStock + }); + } + player.activeInterface = { + interfaceId: 3824, + type: 'SCREEN', + closeOnWalk: closeOnWalk + }; + } catch (error) { + logger.error(`Error opening shop ${identification}: ` + error); + } + +} \ No newline at end of file diff --git a/src/world/items/item-container.ts b/src/world/items/item-container.ts index c05ba76c..6f7920c1 100644 --- a/src/world/items/item-container.ts +++ b/src/world/items/item-container.ts @@ -59,7 +59,7 @@ export class ItemContainer { } } - return -1; + return undefined; } public add(item: number | Item, fireEvent: boolean = true): { item: Item, slot: number} { @@ -102,6 +102,10 @@ export class ItemContainer { } } + public amountInStack(index: number): number { + return this._items[index].amount; + } + public removeFirst(item: number | Item, fireEvent: boolean = true): number { const slot = this.findIndex(item); if(slot === -1) { diff --git a/src/world/mob/player/action/buy-item-action.ts b/src/world/mob/player/action/buy-item-action.ts new file mode 100644 index 00000000..00c474fb --- /dev/null +++ b/src/world/mob/player/action/buy-item-action.ts @@ -0,0 +1,15 @@ +import { Player } from '@server/world/mob/player/player'; +import { gameCache } from '@server/game-server'; + +export const buyItemAction = (player: Player, itemId: number, amount: number, slot: number, interfaceId: number) => { + + const purchasedItem = gameCache.itemDefinitions.get(itemId); + const coinsInInventoryIndex = player.inventory.findIndex(995); + const amountInStack = player.inventory.amountInStack(coinsInInventoryIndex); + const amountLeftAfterPurchase = amountInStack - (purchasedItem.value * amount); + + // player.inventory.removeFirst(995); + player.inventory.add({itemId: 995, amount: amountLeftAfterPurchase}); + player.inventory.add({itemId: itemId, amount: amount}); + +}; \ No newline at end of file diff --git a/src/world/mob/player/game-interface.ts b/src/world/mob/player/game-interface.ts index 7e4d32d4..844162c1 100644 --- a/src/world/mob/player/game-interface.ts +++ b/src/world/mob/player/game-interface.ts @@ -1,7 +1,8 @@ export const interfaceIds = { characterDesign: 3559, inventory: 3214, - equipment: 1688 + equipment: 1688, + shop: 3900 }; export const interfaceSettings = { diff --git a/src/world/mob/player/packet/impl/buy-item-packet.ts b/src/world/mob/player/packet/impl/buy-item-packet.ts new file mode 100644 index 00000000..bed2fde5 --- /dev/null +++ b/src/world/mob/player/packet/impl/buy-item-packet.ts @@ -0,0 +1,49 @@ +import { incomingPacket } from '@server/world/mob/player/packet/incoming-packet'; +import { Player } from '@server/world/mob/player/player'; +import { RsBuffer } from '@server/net/rs-buffer'; +import { gameCache } from '@server/game-server'; +import { buyItemAction } from '@server/world/mob/player/action/buy-item-action'; + +export const buyItemPacket: incomingPacket = (player: Player, packetId: number, packetSize: number, packet: RsBuffer): void => { + + if(packetId === 177) { + const slot = packet.readNegativeOffsetShortBE(); + const itemId = packet.readShortLE(); + const interfaceId = packet.readShortLE(); + + if(player.inventory.findItemIndex({itemId: 995, amount: gameCache.itemDefinitions.get(itemId).value}) === undefined) { + player.packetSender.chatboxMessage(`You don't have enough coins.`); + return; + } + + buyItemAction(player, itemId, 1, slot, interfaceId); + } + + if(packetId === 91) { + const itemId = packet.readShortLE(); + const slot = packet.readNegativeOffsetShortLE(); + const interfaceId = packet.readShortBE(); + + if(player.inventory.findItemIndex({itemId: 995, amount: gameCache.itemDefinitions.get(itemId).value * 5}) === undefined) { + player.packetSender.chatboxMessage(`You don't have enough coins.`); + return; + } + + buyItemAction(player, itemId, 5, slot, interfaceId); + } + + if(packetId === 231) { + const interfaceId = packet.readNegativeOffsetShortLE(); + const slot = packet.readShortLE(); + const itemId = packet.readShortBE(); + + if(player.inventory.findItemIndex({itemId: 995, amount: gameCache.itemDefinitions.get(itemId).value * 10}) === undefined) { + player.packetSender.chatboxMessage(`You don't have enough coins.`); + return; + } + + buyItemAction(player, itemId, 10, slot, interfaceId); + } + + return; +}; \ No newline at end of file diff --git a/src/world/mob/player/packet/incoming-packet-directory.ts b/src/world/mob/player/packet/incoming-packet-directory.ts index 33ef9181..8c70a727 100644 --- a/src/world/mob/player/packet/incoming-packet-directory.ts +++ b/src/world/mob/player/packet/incoming-packet-directory.ts @@ -18,6 +18,7 @@ import { objectInteractionPacket } from '@server/world/mob/player/packet/impl/ob import { chatPacket } from '@server/world/mob/player/packet/impl/chat-packet'; import { dropItemPacket } from '@server/world/mob/player/packet/impl/drop-item-packet'; import { itemOnItemPacket } from '@server/world/mob/player/packet/impl/item-on-item-packet'; +import { buyItemPacket } from '@server/world/mob/player/packet/impl/buy-item-packet'; const packets: { [key: number]: incomingPacket } = { 19: interfaceClickPacket, @@ -51,7 +52,11 @@ const packets: { [key: number]: incomingPacket } = { 1: itemOnItemPacket, 49: chatPacket, - 56: commandPacket + 56: commandPacket, + + 177: buyItemPacket, + 91: buyItemPacket, + 231: buyItemPacket }; export function handlePacket(player: Player, packetId: number, packetSize: number, buffer: Buffer): void { diff --git a/src/world/world.ts b/src/world/world.ts index 02b329b5..0449b35a 100644 --- a/src/world/world.ts +++ b/src/world/world.ts @@ -7,6 +7,7 @@ import { Position } from './position'; import yargs from 'yargs'; import { NpcSpawn, parseNpcSpawns } from './config/npc-spawn'; import { Npc } from './mob/npc/npc'; +import { parseShops, Shop } from '@server/world/config/shops'; /** * Controls the game world and all entities within it. @@ -22,10 +23,12 @@ export class World { public readonly chunkManager: ChunkManager = new ChunkManager(); public readonly itemData: Map; public readonly npcSpawns: NpcSpawn[]; + public readonly shops: Shop[]; public constructor() { this.itemData = parseItemData(gameCache.itemDefinitions); this.npcSpawns = parseNpcSpawns(); + this.shops = parseShops(); this.setupWorldTick(); } From 29458a4f7dc581117af4f0ac1a4d268b9b544b5f Mon Sep 17 00:00:00 2001 From: Nickolaj Madsen Date: Thu, 20 Feb 2020 12:06:35 +0100 Subject: [PATCH 2/2] Added a basic buy action. --- src/net/rs-buffer.ts | 4 ++++ .../mob/player/action/buy-item-action.ts | 21 ++++++++++++++++--- src/world/mob/player/packet/packet-sender.ts | 20 +++++++++--------- 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/net/rs-buffer.ts b/src/net/rs-buffer.ts index 307fa626..0044411e 100644 --- a/src/net/rs-buffer.ts +++ b/src/net/rs-buffer.ts @@ -182,6 +182,10 @@ export class RsBuffer { return value; } + public writeUnsignedByteInverted(value: number): void { + this.writeUnsignedByte(~value & 0xff); + } + public readSmart(): number { const peek = this.buffer.readUInt8(this.readerIndex); if(peek < 128) { diff --git a/src/world/mob/player/action/buy-item-action.ts b/src/world/mob/player/action/buy-item-action.ts index 00c474fb..58da9593 100644 --- a/src/world/mob/player/action/buy-item-action.ts +++ b/src/world/mob/player/action/buy-item-action.ts @@ -1,5 +1,6 @@ import { Player } from '@server/world/mob/player/player'; import { gameCache } from '@server/game-server'; +import { widgetIds } from '@server/world/mob/player/widget'; export const buyItemAction = (player: Player, itemId: number, amount: number, slot: number, interfaceId: number) => { @@ -7,9 +8,23 @@ export const buyItemAction = (player: Player, itemId: number, amount: number, sl const coinsInInventoryIndex = player.inventory.findIndex(995); const amountInStack = player.inventory.amountInStack(coinsInInventoryIndex); const amountLeftAfterPurchase = amountInStack - (purchasedItem.value * amount); + + // Take the money. + player.inventory.set(player.inventory.findIndex(itemId), { itemId, amount: amount}); + player.inventory.set(coinsInInventoryIndex, {itemId: 995, amount: amountLeftAfterPurchase}); - // player.inventory.removeFirst(995); - player.inventory.add({itemId: 995, amount: amountLeftAfterPurchase}); - player.inventory.add({itemId: itemId, amount: amount}); + // Add the purchased item(s) to the inventory. + if (amount > 1) { + for (let i = 0; i < amount; i++) { + player.inventory.add(itemId); + } + } + + if(amount === 1) { + player.inventory.add(itemId); + } + + // Update the inventory items. + player.packetSender.sendUpdateAllWidgetItems(widgetIds.inventory, player.inventory); }; \ No newline at end of file diff --git a/src/world/mob/player/packet/packet-sender.ts b/src/world/mob/player/packet/packet-sender.ts index 63a0efaf..a17e7407 100644 --- a/src/world/mob/player/packet/packet-sender.ts +++ b/src/world/mob/player/packet/packet-sender.ts @@ -314,20 +314,20 @@ export class PacketSender { public sendUpdateSingleWidgetItem(widgetId: number, slot: number, item: Item): void { const packet = new Packet(134, PacketType.DYNAMIC_LARGE); - packet.writeShortBE(widgetId); + packet.writeUnsignedShortBE(widgetId); packet.writeSmart(slot); if(!item) { - packet.writeShortBE(0); - packet.writeByte(0); + packet.writeUnsignedShortBE(0); + packet.writeUnsignedByte(0); } else { - packet.writeShortBE(item.itemId + 1); // +1 because 0 means an empty slot + packet.writeUnsignedShortBE(item.itemId + 1); // +1 because 0 means an empty slot if(item.amount >= 255) { - packet.writeByte(255); + packet.writeUnsignedByte(255); packet.writeIntBE(item.amount); } else { - packet.writeByte(item.amount); + packet.writeUnsignedByte(item.amount); } } @@ -344,15 +344,15 @@ export class PacketSender { if(!item) { // Empty slot packet.writeOffsetShortLE(0); - packet.writeByteInverted(0); + packet.writeUnsignedByteInverted(0 - 1); } else { packet.writeOffsetShortLE(item.itemId + 1); // +1 because 0 means an empty slot if(item.amount >= 255) { - packet.writeByteInverted(255); - packet.writeIntBE(item.amount); + packet.writeUnsignedByteInverted(254); + packet.writeIntLE(item.amount); } else { - packet.writeByteInverted(item.amount); + packet.writeUnsignedByteInverted(item.amount - 1); } } });