diff --git a/package-lock.json b/package-lock.json index 7a6c0c672..c1ecca460 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,7 @@ "steam-totp": "^2.1.2", "steam-user": "^4.28.6", "steamid": "^2.0.0", + "tf2-backpack": "^1.1.5", "url": "^0.11.1", "valid-url": "^1.0.9", "winston": "^3.10.0", diff --git a/package.json b/package.json index 6cf4d9e2c..0f31343be 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "steam-user": "^4.28.6", "steamid": "^2.0.0", "url": "^0.11.1", + "tf2-backpack": "^1.1.5", "valid-url": "^1.0.9", "winston": "^3.10.0", "winston-daily-rotate-file": "^4.7.1", diff --git a/src/classes/Bot.ts b/src/classes/Bot.ts index c9723be47..d2cb88acd 100644 --- a/src/classes/Bot.ts +++ b/src/classes/Bot.ts @@ -16,6 +16,7 @@ import pluralize from 'pluralize'; import * as timersPromises from 'timers/promises'; import fs from 'fs'; import path from 'path'; +import { BackpackParser } from 'tf2-backpack'; import * as files from '../lib/files'; // Reference: https://github.com/tf2-automatic/tf2-automatic/commit/cf7b807cae11eb172a78ef184bbafdb4ebe86501#diff-58f39591209025b16105c9f25a34c119332983a0d8cea7819b534d9d408324c4L329 @@ -87,6 +88,10 @@ export default class Bot { readonly inventoryGetter: InventoryGetter; + backpackParser: BackpackParser; + + needSave = false; + readonly boundInventoryGetter: ( steamID: SteamID | string, appid: number, @@ -123,8 +128,6 @@ export default class Bot { spy: string[]; }; - public updateSchemaPropertiesInterval: NodeJS.Timeout; - // Settings private readonly maxLoginAttemptsWithinPeriod: number = 3; @@ -995,6 +998,17 @@ export default class Bot { log.info('Getting TF2 schema...'); void this.initializeSchema().asCallback(callback); }, + (callback): void => { + log.info('Initializing Backpack parser...'); + this.setProperties(); + this.backpackParser = new BackpackParser(this.schema.raw.items_game); + this.schemaManager.on('schema', () => { + this.backpackParser = new BackpackParser(this.schema.raw.items_game); + this.setProperties(); + }); + /* eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call */ + return callback(null); + }, (callback: (err?) => void): void => { log.info('Initializing pricelist...'); @@ -1230,15 +1244,6 @@ export default class Bot { sniper: this.schema.getWeaponsForCraftingByClass('Sniper'), spy: this.schema.getWeaponsForCraftingByClass('Spy') }; - - clearInterval(this.updateSchemaPropertiesInterval); - this.refreshSchemaProperties(); - } - - private refreshSchemaProperties(): void { - this.updateSchemaPropertiesInterval = setInterval(() => { - this.setProperties(); - }, 24 * 60 * 60 * 1000); } setCookies(cookies: string[]): Promise { diff --git a/src/classes/BotManager.ts b/src/classes/BotManager.ts index 7ebf38fb1..ed9bb1553 100644 --- a/src/classes/BotManager.ts +++ b/src/classes/BotManager.ts @@ -209,7 +209,6 @@ export default class BotManager { // Stop updating schema clearTimeout(this.schemaManager?._updateTimeout); clearInterval(this.schemaManager?._updateInterval); - clearInterval(this.bot.updateSchemaPropertiesInterval); // Stop heartbeat and inventory timers clearInterval(this.bot.listingManager?._heartbeatInterval); diff --git a/src/classes/Inventory.ts b/src/classes/Inventory.ts index 166bdfe03..cdb8ba514 100644 --- a/src/classes/Inventory.ts +++ b/src/classes/Inventory.ts @@ -1,11 +1,12 @@ import SteamID from 'steamid'; import { EconItem, ItemAttributes, PartialSKUWithMention } from '@tf2autobot/tradeoffer-manager'; -import { Effect, Paints, StrangeParts } from '@tf2autobot/tf2-schema'; +import SchemaManager, { Item, Effect, Paints, StrangeParts } from '@tf2autobot/tf2-schema'; import SKU from '@tf2autobot/tf2-sku'; import { HighValue } from './Options'; import Bot from './Bot'; import { noiseMakers, spellsData, killstreakersData, sheensData } from '../lib/data'; import Pricelist from './Pricelist'; +import * as bp from 'tf2-backpack'; export default class Inventory { private readonly steamID: SteamID; @@ -70,7 +71,7 @@ export default class Inventory { ) => void ): Inventory { const inventory = new Inventory(steamID, bot, which, boundInventoryGetter); - inventory.setItems = items; + inventory.setItemsEcon({ items }); return inventory; } @@ -134,13 +135,13 @@ export default class Inventory { return reject(err); } - this.setItems = items; + this.setItemsEcon({ items }); resolve(); }); }); } - private set setItems(items: EconItem[]) { + private setItemsEcon({ items }: { items: EconItem[] }) { this.tradable = Inventory.createDictionary( items.filter(item => item.tradable), this.bot, @@ -155,6 +156,8 @@ export default class Inventory { ); } + // TODO: Process internal inventory + findByAssetid(assetid: string): string | null { for (const sku in this.tradable) { if (!Object.prototype.hasOwnProperty.call(this.tradable, sku)) { @@ -595,6 +598,73 @@ export default class Inventory { return attributes; } + // For tf2-backpack + private static getSKU({ + item, + schema, + normalizeFestivizedItems, + normalizeStrangeAsSecondQuality, + normalizePainted, + normalizeCraftNumber, + paintsInOptions + }: { + item: bp.Item; + schema: SchemaManager.Schema; + normalizeFestivizedItems: boolean; + normalizeStrangeAsSecondQuality: boolean; + normalizePainted: boolean; + normalizeCraftNumber: boolean; + paintsInOptions: string[]; + }): { sku: string; isPainted: boolean } { + const paint = this.getPaint(schema, item, normalizePainted, paintsInOptions); + + const itemData: Item = { + defindex: item.defindex, + quality: item.quality, + craftable: item.craftable, + killstreak: item.killstreakTier || null, + australium: item.australium || null, + festive: !normalizeFestivizedItems ? item.festivized : false, + effect: item.effect || null, + wear: item.wear || null, + paintkit: item.paintkit || null, + quality2: !normalizeStrangeAsSecondQuality ? (item.elevated ? 11 : null) : null, + crateseries: item.crateNo || null, + target: item.target || null, + output: item.outputItem?.defindex, + outputQuality: item.outputItem?.quality, + paint: paint.decimalValue, + craftnumber: !normalizeCraftNumber ? item.craft : null + }; + + return { sku: SKU.fromObject(itemData), isPainted: paint.isPainted }; + } + + // For tf2-backpack + private static getPaint( + schema: SchemaManager.Schema, + item: bp.Item, + normalize: boolean, + inOptions: string[] + ): { decimalValue: number; isPainted: boolean } { + if (!item.paint) { + return null; + } + + if (!normalize) { + if (item.paint === 'B8383B' && item.paint_other !== '5885A2' && inOptions.includes('legacy paint')) { + // legacy paint for Team Spirit + return { decimalValue: 5801378, isPainted: true }; + } + + if (inOptions.includes(item.paint.toLowerCase())) { + return { decimalValue: schema.paints[bp.paints[item.paint]], isPainted: true }; + } + } + + return { decimalValue: null, isPainted: false }; + } + clearFetch(): void { this.tradable = undefined; this.nonTradable = undefined; diff --git a/src/classes/TF2GC.ts b/src/classes/TF2GC.ts index 32e848900..70e646363 100644 --- a/src/classes/TF2GC.ts +++ b/src/classes/TF2GC.ts @@ -36,7 +36,8 @@ type Job = { | 'delete' | 'sort' | 'removeAttributes' - | 'craftToken'; + | 'craftToken' + | 'backpackLoad'; defindex?: number; sku?: string; skus?: string[]; @@ -151,6 +152,12 @@ export default class TF2GC { this.newJob({ type: 'craftToken', assetids, tokenType, subTokenType, callback: callback }); } + waitForBackpackLoaded(callback: (err: Error | null) => void): void { + log.debug(`Enqueueing backpackLoaded trigger job`); + + this.newJob({ type: 'backpackLoad', callback: callback }); + } + private newJob(job: Job): void { this.jobs.push(job); this.handleJobQueue(); @@ -203,6 +210,8 @@ export default class TF2GC { func = this.handleSortJob.bind(this, job); } else if (job.type === 'craftToken') { func = this.handleCraftTokenJob.bind(this, job); + } else if (job.type === 'backpackLoad') { + func = this.handleBackpackLoadedJob.bind(this, job); } if (func) { @@ -527,6 +536,21 @@ export default class TF2GC { ); } + private handleBackpackLoadedJob(): void { + this.bot.client.gamesPlayed([]); + this.bot.client.gamesPlayed(440); + + this.listenForEvent( + 'backpackLoaded', + () => { + this.finishedProcessingJob(); + }, + err => { + this.finishedProcessingJob(err); + } + ); + } + /** * Listens for GC event * diff --git a/src/classes/Trades.ts b/src/classes/Trades.ts index 915cc71ce..e7ba191a3 100644 --- a/src/classes/Trades.ts +++ b/src/classes/Trades.ts @@ -46,9 +46,9 @@ export default class Trades { private resetRetryAcceptOfferTimeout: NodeJS.Timeout; - private retryFetchInventoryTimeout: NodeJS.Timeout; + private retryRefreshInventoryTimeout: NodeJS.Timeout; - private calledRetryFetchFreq = 0; + private calledRetryRefreshFreq = 0; private offerChangedAcc: { offer: TradeOffer; oldState: number; timeTakenToComplete: number }[] = []; @@ -1655,42 +1655,33 @@ export default class Trades { // Just handle changes this.bot.handler.onTradeOfferChanged(offer, oldState, timeTakenToComplete); } else { - // Exit all running apps ("TF2Autobot" or custom, and Team Fortress 2) - // Will play again after craft/smelt/sort inventory job - // https://github.com/TF2Autobot/tf2autobot/issues/527 - this.bot.client.gamesPlayed([]); - this.offerChangedAcc.push({ offer, oldState, timeTakenToComplete }); log.debug('Accumulated offerChanged: ', this.offerChangedAcc.length); if (this.offerChangedAcc.length <= 1) { - // Only call `fetch` if accumulated offerChanged is less than or equal to 1 - // Prevent never ending "The request is a duplicate and the action has already occurred in the past, ignored this time" - - // Accepted, Invalid trade (possible) => new item assetid - log.debug('Fetching our inventory...'); - return void this.bot.inventoryManager.getInventory - .fetch() - .then(() => { - this.onSuccessfulFetch(); - }) - .catch(err => { - log.warn('Error fetching inventory: ', err); - log.debug('Retrying to fetch inventory in 30 seconds...'); - this.calledRetryFetchFreq++; - - if (this.calledRetryFetchFreq === 1) { + log.debug('Refreshing our inventory...'); + return void this.bot.tf2gc.waitForBackpackLoaded(err => { + if (err) { + log.warn('Error running "waitForBackpackLoaded": ', err); + log.debug('Retrying to in 30 seconds...'); + this.calledRetryRefreshFreq++; + + if (this.calledRetryRefreshFreq === 1) { // Only call this once (before reset) - this.retryFetchInventory(); + this.retryRefreshInventory(); } - }); + return; + } + + this.onSuccessfulRefresh(); + }); } - log.debug('Not fetching inventory this time...'); + log.debug('Not refreshing inventory this time...'); } } - private onSuccessfulFetch(): void { + private onSuccessfulRefresh(): void { if (this.offerChangedAcc.length > 0) { this.offerChangedAcc.forEach(el => { this.bot.handler.onTradeOfferChanged(el.offer, el.oldState, el.timeTakenToComplete); @@ -1701,34 +1692,33 @@ export default class Trades { } } - private retryFetchInventory(): void { - clearTimeout(this.retryFetchInventoryTimeout); - this.retryFetchInventoryTimeout = setTimeout(() => { - this.bot.inventoryManager.getInventory - .fetch() - .then(() => { - this.onSuccessfulFetch(); - - // Reset to 0 - this.calledRetryFetchFreq = 0; - }) - .catch(err => { - log.warn('Error fetching inventory: ', err); + private retryRefreshInventory(): void { + clearTimeout(this.retryRefreshInventoryTimeout); + this.retryRefreshInventoryTimeout = setTimeout(() => { + this.bot.tf2gc.waitForBackpackLoaded(err => { + if (err) { + log.warn('Error refreshing inventory: ', err); - if (this.calledRetryFetchFreq > 3) { + if (this.calledRetryRefreshFreq > 3) { // If more than 3 times failed, then just proceed with an outdated inventory - this.onSuccessfulFetch(); + this.onSuccessfulRefresh(); // Reset to 0 - this.calledRetryFetchFreq = 0; + this.calledRetryRefreshFreq = 0; return; } - log.debug('Retrying to fetch inventory in 30 seconds...'); - this.calledRetryFetchFreq++; - this.retryFetchInventory(); - }); + log.debug('Retrying to refresh inventory in 30 seconds...'); + this.calledRetryRefreshFreq++; + return this.retryRefreshInventory(); + } + + this.onSuccessfulRefresh(); + + // Reset to 0 + this.calledRetryRefreshFreq = 0; + }); }, 30 * 1000); }