Skip to content

Commit 2949bcb

Browse files
authored
Merge branch 'develop' into fix-setting-level
2 parents a3071d3 + 8c48892 commit 2949bcb

File tree

5 files changed

+188
-4
lines changed

5 files changed

+188
-4
lines changed

jest.config.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,10 +153,9 @@ export default {
153153
// testLocationInResults: false,
154154

155155
// The glob patterns Jest uses to detect test files
156-
// testMatch: [
157-
// "**/__tests__/**/*.[jt]s?(x)",
158-
// "**/?(*.)+(spec|test).[tj]s?(x)"
159-
// ],
156+
testMatch: [
157+
"**/*.test.ts"
158+
],
160159

161160
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
162161
// testPathIgnorePatterns: [

src/engine/action/action-pipeline.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export type ActionType =
3131
| 'item_on_npc'
3232
| 'item_on_player'
3333
| 'item_on_item'
34+
| 'item_on_world_item'
3435
| 'item_swap'
3536
| 'move_item'
3637
| 'spawned_item_interaction'

src/engine/action/pipe/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export * from './button.action';
22
export * from './equipment-change.action';
33
export * from './item-interaction.action';
44
export * from './item-on-item.action';
5+
export * from './item-on-world-item.action';
56
export * from './item-on-npc.action';
67
export * from './item-on-object.action';
78
export * from './item-on-player.action';
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { Player } from '@engine/world/actor';
2+
import { Item, WorldItem } from '@engine/world';
3+
import { ActionHook, getActionHooks, questHookFilter, ActionPipe, RunnableHooks } from '@engine/action';
4+
5+
6+
/**
7+
* Defines an item-on-world-item action hook.
8+
*
9+
* @author jameskmonger
10+
*/
11+
export interface ItemOnWorldItemActionHook extends ActionHook<ItemOnWorldItemAction, itemOnWorldItemActionHandler> {
12+
/**
13+
* The item pairs being used. Both items are optional so that you can specify a single item, a pair of items, or neither.
14+
*/
15+
items: { item?: number, worldItem?: number }[];
16+
}
17+
18+
19+
/**
20+
* The item-on-world-item action hook handler function to be called when the hook's conditions are met.
21+
*/
22+
export type itemOnWorldItemActionHandler = (itemOnWorldItemAction: ItemOnWorldItemAction) => void;
23+
24+
25+
/**
26+
* Details about an item-on-world-item action being performed.
27+
*
28+
* @author jameskmonger
29+
*/
30+
export interface ItemOnWorldItemAction {
31+
/**
32+
* The player performing the action.
33+
*/
34+
player: Player;
35+
36+
/**
37+
* The item being used.
38+
*/
39+
usedItem: Item;
40+
41+
/**
42+
* The WorldItem that the first item is being used on.
43+
*/
44+
usedWithItem: WorldItem;
45+
46+
/**
47+
* The ID of the UI widget that the item being used is in.
48+
*/
49+
usedWidgetId: number;
50+
51+
/**
52+
* The ID of the container that the item being used is in.
53+
*/
54+
usedContainerId: number;
55+
56+
/**
57+
* The slot within the container that the item being used is in.
58+
*/
59+
usedSlot: number;
60+
}
61+
62+
/**
63+
* The pipe that the game engine hands item-on-world-item actions off to.
64+
*
65+
* This will call the `item_on_world_item` action hooks, if any are registered and match the action being performed.
66+
*
67+
* Both `item` and `worldItem` are optional, but if they are provided then they must match the items in use.
68+
*
69+
* @author jameskmonger
70+
*/
71+
const itemOnWorldItemActionPipe = (
72+
player: Player,
73+
usedItem: Item, usedWithItem: WorldItem,
74+
usedWidgetId: number, usedContainerId: number, usedSlot: number
75+
): RunnableHooks<ItemOnWorldItemAction> => {
76+
if(player.busy) {
77+
return;
78+
}
79+
80+
// Find all item on item action plugins that match this action
81+
let matchingHooks = getActionHooks<ItemOnWorldItemActionHook>('item_on_world_item', plugin => {
82+
if(questHookFilter(player, plugin)) {
83+
const used = usedItem.itemId;
84+
const usedWith = usedWithItem.itemId;
85+
86+
return (plugin.items.some(({ item, worldItem }) => {
87+
const itemMatch = item === undefined || item === used;
88+
const worldItemMatch = worldItem === undefined || worldItem === usedWith;
89+
90+
return itemMatch && worldItemMatch;
91+
}));
92+
}
93+
94+
return false;
95+
});
96+
97+
const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined);
98+
99+
if(questActions.length !== 0) {
100+
matchingHooks = questActions;
101+
}
102+
103+
if(matchingHooks.length === 0) {
104+
player.outgoingPackets.chatboxMessage(
105+
`Unhandled item on world item interaction: ${usedItem.itemId} on ${usedWithItem.itemId}`);
106+
return null;
107+
}
108+
109+
return {
110+
hooks: matchingHooks,
111+
action: {
112+
player,
113+
usedItem, usedWithItem,
114+
usedWidgetId, usedContainerId, usedSlot
115+
}
116+
}
117+
};
118+
119+
120+
/**
121+
* Item-on-world-item action pipe definition.
122+
*/
123+
export default [ 'item_on_world_item', itemOnWorldItemActionPipe ] as ActionPipe;
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { logger } from '@runejs/common';
2+
import { widgets } from '@engine/config';
3+
import { Player } from '@engine/world/actor';
4+
import { PacketData } from '@engine/net';
5+
import { Position } from '@engine/world';
6+
7+
/**
8+
* Parses the item on world item packet and calls the `item_on_world_item` action pipeline.
9+
*
10+
* This will check that the item being used is in the player's inventory, and that the world item exists in the correct location.
11+
* The action pipeline will not be called if either of these conditions are not met.
12+
*
13+
* @param player The player that sent the packet.
14+
* @param packet The packet to parse.
15+
*
16+
* @author jameskmonger
17+
*/
18+
const itemOnWorldItemPacket = (player: Player, packet: PacketData) => {
19+
const { buffer } = packet;
20+
21+
const usedWithX = buffer.get('short', 'u');
22+
const usedSlot = buffer.get('short', 'u');
23+
const usedWithItemId = buffer.get('short', 'u');
24+
const usedContainerId = buffer.get('short', 's', 'be');
25+
const usedWidgetId = buffer.get('short', 's', 'be');
26+
const usedWithY = buffer.get('short', 'u', 'le');
27+
const usedItemId = buffer.get('short', 'u', 'le');
28+
29+
const position = new Position(usedWithX, usedWithY, player.position.level);
30+
31+
if(usedWidgetId === widgets.inventory.widgetId && usedContainerId === widgets.inventory.containerId) {
32+
// TODO (James) we should use constants for these rather than magic numbers
33+
if(usedSlot < 0 || usedSlot > 27) {
34+
return;
35+
}
36+
37+
const usedItem = player.inventory.items[usedSlot];
38+
const usedWithItem = player.instance.getTileModifications(position).mods.worldItems.find(p => p.itemId === usedWithItemId);
39+
if(!usedItem || !usedWithItem) {
40+
logger.warn(`Unhandled item on world item case (A) for ${usedSlot} (${usedItemId}) on ${usedWithItemId} (${usedWithX}, ${usedWithY}) by ${player.username}`);
41+
return;
42+
}
43+
44+
if(usedItem.itemId !== usedItemId || usedWithItem.itemId !== usedWithItemId) {
45+
logger.warn(`Unhandled item on world item case (B) for ${usedItem.itemId}:${usedItemId} on ${usedWithItem.itemId}:${usedWithItemId} by ${player.username}`);
46+
return;
47+
}
48+
49+
player.actionPipeline.call('item_on_world_item', player, usedItem, usedWithItem, usedWidgetId, usedContainerId, usedSlot);
50+
} else {
51+
logger.warn(`Unhandled item on world item case (C) using widgets ${usedWidgetId}:${usedContainerId} by ${player.username}`);
52+
}
53+
54+
};
55+
56+
export default {
57+
opcode: 172,
58+
size: 14,
59+
handler: itemOnWorldItemPacket
60+
};

0 commit comments

Comments
 (0)