diff --git a/data/config/items/equipment/standard-metals/adamant-armour.json b/data/config/items/equipment/standard-metals/adamant-armour.json index e4c5abe63..e14fc38a8 100644 --- a/data/config/items/equipment/standard-metals/adamant-armour.json +++ b/data/config/items/equipment/standard-metals/adamant-armour.json @@ -9,7 +9,8 @@ "defence": 30 } } - } + }, + "groups": ["adamant_metal", "equipment"] } }, @@ -31,7 +32,8 @@ "magic": -4, "ranged": 31 } - } + }, + "groups": ["legs"] }, "rs:adamant_plateskirt": { @@ -52,6 +54,7 @@ "magic": -4, "ranged": 31 } - } + }, + "groups": ["legs"] } } diff --git a/data/config/items/equipment/standard-metals/black-armour.json b/data/config/items/equipment/standard-metals/black-armour.json index f4488aedc..1c8a919a3 100644 --- a/data/config/items/equipment/standard-metals/black-armour.json +++ b/data/config/items/equipment/standard-metals/black-armour.json @@ -9,7 +9,8 @@ "defence": 10 } } - } + }, + "groups": ["black_metal", "equipment"] } }, @@ -31,7 +32,8 @@ "magic": -4, "ranged": 20 } - } + }, + "groups": ["legs"] }, "rs:black_plateskirt": { @@ -52,6 +54,7 @@ "magic": -4, "ranged": 20 } - } + }, + "groups": ["legs"] } } diff --git a/data/config/items/equipment/standard-metals/bronze-armour.json b/data/config/items/equipment/standard-metals/bronze-armour.json index 71d91a115..1d2dc8d0c 100644 --- a/data/config/items/equipment/standard-metals/bronze-armour.json +++ b/data/config/items/equipment/standard-metals/bronze-armour.json @@ -1,5 +1,20 @@ { + "presets": { + "rs:bronze_armour_base": { + "tradable": true, + "equippable": true, + "equipment_data": { + "requirements": { + "skills": { + "defence": 1 + } + } + }, + "groups": ["bronze_metal", "equipment"] + } + }, "rs:bronze_platelegs": { + "extends": "rs:bronze_armour_base", "game_id": 1075, "examine": "These look pretty heavy.", "weight": 9.071, @@ -18,11 +33,13 @@ "magic": -4, "ranged": 7 } - } + }, + "groups": ["legs"] }, "rs:bronze_plateskirt": { "game_id": 1087, + "extends": "rs:bronze_armour_base", "examine": "Designer leg protection.", "weight": 8.164, "tradable": true, @@ -40,6 +57,7 @@ "magic": -4, "ranged": 7 } - } + }, + "groups": ["legs"] } } diff --git a/data/config/items/equipment/standard-metals/bronze-weapons.json b/data/config/items/equipment/standard-metals/bronze-weapons.json index 7ab93f147..edbd8c96c 100644 --- a/data/config/items/equipment/standard-metals/bronze-weapons.json +++ b/data/config/items/equipment/standard-metals/bronze-weapons.json @@ -1,6 +1,21 @@ { + "presets": { + "rs:bronze_weapon_base": { + "tradable": true, + "equippable": true, + "equipment_data": { + "requirements": { + "skills": { + "defence": 1 + } + } + }, + "groups": ["bronze_metal", "equipment", "weapon"] + } + }, "rs:bronze_dagger": { "game_id": 1205, + "extends": "rs:bronze_weapon_base", "tradable": true, "weight": 0.453, "equippable": true, @@ -28,6 +43,7 @@ "weapon_info": { "style": "dagger" } - } + }, + "groups": ["main_hand", "dagger"] } } diff --git a/src/engine/config/config-handler.ts b/src/engine/config/config-handler.ts index 7638a7335..0a2d609bd 100644 --- a/src/engine/config/config-handler.ts +++ b/src/engine/config/config-handler.ts @@ -5,7 +5,6 @@ import { ItemDetails, ItemPresetConfiguration, loadItemConfigurations, - translateItemConfig } from '@engine/config/item-config'; import { filestore } from '@server/game/game-server'; import { @@ -24,6 +23,7 @@ import { questMap } from '@engine/plugins'; export let itemMap: { [key: string]: ItemDetails }; +export let itemGroupMap: Record>; export let itemIdMap: { [key: number]: string }; export let objectMap: { [key: number]: ObjectConfig }; export let itemPresetMap: ItemPresetConfiguration; @@ -46,8 +46,9 @@ export async function loadCoreConfigurations(): Promise { export async function loadGameConfigurations(): Promise { logger.info(`Loading server configurations...`); - const { items, itemIds, itemPresets } = await loadItemConfigurations('data/config/items/'); + const { items, itemIds, itemPresets, itemGroups } = await loadItemConfigurations('data/config/items/'); itemMap = items; + itemGroupMap = itemGroups; itemIdMap = itemIds; itemPresetMap = itemPresets; @@ -70,6 +71,57 @@ export async function loadGameConfigurations(): Promise { } +/** + * find all items in all select groups + * @param groupKey string or array of string of which to find items connected with + * @return itemsKeys array of itemkeys in all select groups + */ +export const findItemTagsInGroup = (groupKey: string | string[]): string[] => { + if(!groupKey) { + return []; + } + + if(Array.isArray(groupKey)) { + const collection: Record = {} + groupKey.forEach((currentGroup) => { + const items = findItemTagsInGroup(currentGroup); + items.forEach((item) => collection[item] = true) + }) + return Object.keys(collection) + } + + return Object.keys(itemGroupMap[groupKey] || {}) +} + + +/** + * find all items which are shared by all the groups, and discard items not in all groups + * @param groupKeys groups keys which to find items shared by + * @return itemKeys of items shared by all groups + */ +export const findItemTagsInGroupFilter = (groupKeys: string[]): string[] => { + if(!groupKeys || groupKeys.length === 0) { + return []; + } + let collection: Record | undefined = undefined + groupKeys.forEach((groupKey) => { + if(!collection) { + collection = { ...(itemGroupMap[groupKey] || {}) }; + return; + } + const current = itemGroupMap[groupKey] || {}; + + Object.keys(collection).forEach((existingItemKey) => { + if(!(existingItemKey in current)) { + delete collection[existingItemKey]; + } + }) + }) + + return Object.keys(collection) +} + + export const findItem = (itemKey: number | string): ItemDetails | null => { if(!itemKey) { return null; @@ -96,20 +148,6 @@ export const findItem = (itemKey: number | string): ItemDetails | null => { if(item?.gameId) { gameId = item.gameId; } - - if(item?.extends) { - let extensions = item.extends; - if(typeof extensions === 'string') { - extensions = [ extensions ]; - } - - extensions.forEach(extKey => { - const extensionItem = itemPresetMap[extKey]; - if(extensionItem) { - item = _.merge(item, translateItemConfig(undefined, extensionItem)); - } - }); - } } if(gameId) { diff --git a/src/engine/config/item-config.ts b/src/engine/config/item-config.ts index ce76a0beb..2701fba9e 100644 --- a/src/engine/config/item-config.ts +++ b/src/engine/config/item-config.ts @@ -1,7 +1,7 @@ import { loadConfigurationFiles } from '@runejs/core/fs'; import { SkillName } from '@engine/world/actor/skills'; -import _ from 'lodash'; import { logger } from '@runejs/core'; +import { deepMerge } from '@engine/util/objects'; export type WeaponStyle = 'axe' | 'hammer' | 'bow' | 'claws' | 'crossbow' | 'longsword' @@ -125,6 +125,7 @@ export interface ItemConfiguration { equippable?: boolean; consumable?: boolean; destroy?: string | boolean; + groups?: string[]; equipment_data?: { equipment_slot: EquipmentSlot; equipment_type?: EquipmentType; @@ -155,6 +156,7 @@ export class ItemDetails { consumable?: boolean; stackable: boolean = false; value: number = 0; + groups: string[] = []; members: boolean = false; groundOptions: string[] = []; inventoryOptions: string[] = []; @@ -194,6 +196,7 @@ export function translateItemConfig(key: string, config: ItemConfiguration): any equippable: config.equippable, weight: config.weight, destroy: config.destroy || undefined, + groups: config.groups || [], consumable: config.consumable, equipmentData: config.equipment_data ? { equipmentType: config.equipment_data?.equipment_type || undefined, @@ -209,12 +212,14 @@ export function translateItemConfig(key: string, config: ItemConfiguration): any } export async function loadItemConfigurations(path: string): Promise<{ items: { [key: string]: ItemDetails }; - itemIds: { [key: number]: string }; itemPresets: ItemPresetConfiguration; }> { + itemIds: { [key: number]: string }; itemPresets: ItemPresetConfiguration; itemGroups: Record>; }> { const itemIds: { [key: number]: string } = {}; const items: { [key: string]: ItemDetails } = {}; + const itemGroups : Record> = {} // Record where key is group id, and value is an array of all itemstags in group let itemPresets: ItemPresetConfiguration = {}; const files = await loadConfigurationFiles(path); + const itemConfigurations: Record = {}; files.forEach(itemConfigs => { const itemKeys = Object.keys(itemConfigs); @@ -222,30 +227,73 @@ export async function loadItemConfigurations(path: string): Promise<{ items: { [ if(key === 'presets') { itemPresets = { ...itemPresets, ...itemConfigs[key] }; } else { + itemConfigurations[key] = itemConfigs[key] as ItemConfiguration; + } + }); + }); + Object.entries(itemConfigurations).forEach(([key, itemConfig]) => { + if(!isNaN(itemConfig.game_id)) { + itemIds[itemConfig.game_id] = key; + let item = { ...translateItemConfig(key, itemConfig) } + if(item?.extends) { + let extensions = item.extends; + if(typeof extensions === 'string') { + extensions = [ extensions ]; + } - const itemConfig: ItemConfiguration = itemConfigs[key] as ItemConfiguration; - if(!isNaN(itemConfig.game_id)) { - itemIds[itemConfig.game_id] = key; - items[key] = { ...translateItemConfig(key, itemConfig) }; + extensions.forEach(extKey => { + const extensionItem = itemPresets[extKey]; + if(extensionItem) { + const preset = translateItemConfig(undefined, extensionItem); + item = deepMerge(item, preset); + } + }); + } + items[key] = item; + item.groups.forEach((group) => { + if(!itemGroups[group]) { + itemGroups[group] = {}; } + itemGroups[group][key] = true; + }) + } - if(itemConfig.variations) { - for(const subItem of itemConfig.variations) { - const subKey = subItem.suffix ? key + ':' + subItem.suffix : key; - const baseItem = JSON.parse(JSON.stringify({ ...translateItemConfig(key, itemConfig) })); - const subBaseItem = JSON.parse(JSON.stringify({ ...translateItemConfig(subKey, subItem) })); - itemIds[subItem.game_id] = subKey; - - if(!items[subKey]) { - items[subKey] = _.merge(baseItem, subBaseItem); - } else { - logger.warn(`Duplicate item key ${subKey} found - the item was not loaded.`); + if(itemConfig.variations) { + for(const subItem of itemConfig.variations) { + const subKey = subItem.suffix ? key + ':' + subItem.suffix : key; + const baseItem = JSON.parse(JSON.stringify({ ...translateItemConfig(key, itemConfig) })); + const subBaseItem = JSON.parse(JSON.stringify({ ...translateItemConfig(subKey, subItem) })); + itemIds[subItem.game_id] = subKey; + + if(!items[subKey]) { + let item = deepMerge(baseItem, subBaseItem); + if(item?.extends) { + let extensions = item.extends; + if(typeof extensions === 'string') { + extensions = [ extensions ]; } + + extensions.forEach(extKey => { + const extensionItem = itemPresets[extKey]; + if(extensionItem) { + const preset = translateItemConfig(undefined, extensionItem); + item = deepMerge(item, preset); + } + }); } + items[subKey] = item + items[subKey].groups.forEach((group) => { + if(!itemGroups[group]) { + itemGroups[group] = {}; + } + itemGroups[group][subKey] = true; + }) + } else { + logger.warn(`Duplicate item key ${subKey} found - the item was not loaded.`); } } - }); - }); + } + }) - return { items, itemIds, itemPresets }; + return { items, itemIds, itemPresets, itemGroups }; } diff --git a/src/engine/util/objects.ts b/src/engine/util/objects.ts new file mode 100644 index 000000000..400b63389 --- /dev/null +++ b/src/engine/util/objects.ts @@ -0,0 +1,52 @@ +/** + * Merge two objects or arrays, first object takes priority in case where the values cannot be merged + * @param objectA + * @param objectB + * @return objectC combination of objectA and objectB + */ +export function deepMerge(objectA: T, objectB: T): T { + if(!objectA) { + return objectB; + } + if(!objectB) { + return objectA; + } + if(Array.isArray(objectA)) { + return [...new Set([...objectA as any, ...objectB as any])] as any; + } + if(typeof objectA === 'object') { + const newObject: T = { ...objectA }; + const keys = [...new Set([...Object.keys(objectA), ...Object.keys(objectB)])]; + keys.forEach((key) => { + if(!objectA[key]) { + newObject[key] = objectB[key]; + return; + } + if(!objectB[key]){ + newObject[key] = objectA[key]; + } + if(Array.isArray(objectA[key])) { + if(!Array.isArray(objectB[key])) { + newObject[key] = [...objectA[key], objectB[key]] + return; + } + newObject[key] = deepMerge(objectA[key], objectB[key]); + return; + } + if(Array.isArray(objectB[key])) { + if(!Array.isArray(objectA[key])) { + newObject[key] = [...objectB[key], objectA[key]] + return; + } + console.error('Something is wrong with deepmerger', key, objectA, objectB); + } + if(typeof objectA[key] === 'object' || typeof objectB === 'object') { + newObject[key] = deepMerge(objectA[key], objectB[key]); + return; + } + newObject[key] = objectA[key]; + }) + return newObject + } + return objectA; +} diff --git a/src/plugins/commands/groups-debug.plugin.ts b/src/plugins/commands/groups-debug.plugin.ts new file mode 100644 index 000000000..4e188086e --- /dev/null +++ b/src/plugins/commands/groups-debug.plugin.ts @@ -0,0 +1,57 @@ +import { commandActionHandler } from '@engine/action'; +import { findItemTagsInGroup, findItemTagsInGroupFilter } from '@engine/config/config-handler'; + +const selectGroups: commandActionHandler = ({ player, args, isConsole }) => { + const groups: string | number = args.groupkeys; + if(!groups || typeof groups !== 'string') { + player.sendLogMessage('invalid input', isConsole) + return + } + player.sendLogMessage('results:', isConsole) + findItemTagsInGroup(groups.split(',')).forEach((itemName) => { + player.sendLogMessage(itemName, isConsole); + }) + return; +}; + +const filterGroups: commandActionHandler = ({ player, args, isConsole }) => { + const groups: string | number = args.groupkeys; + if(!groups || typeof groups !== 'string') { + player.sendLogMessage('invalid input', isConsole) + return + } + + player.sendLogMessage('results:', isConsole) + findItemTagsInGroupFilter(groups.split(',')).forEach((itemName) => { + player.sendLogMessage(itemName, isConsole); + }) + return; +}; + +export default { + pluginId: 'promises:groups-debug', + hooks: [ + { + type: 'player_command', + commands: [ 'selectgroups' ], + args: [ + { + name: 'groupkeys', + type: 'string' + } + ], + handler: selectGroups + }, + { + type: 'player_command', + commands: [ 'filtergroups' ], + args: [ + { + name: 'groupkeys', + type: 'string' + } + ], + handler: filterGroups + } + ] +};