cs2-cdn
Version:
Retrieves the Steam CDN Image URLs for CS2/CS:GO Items
857 lines (856 loc) • 33.5 kB
JavaScript
import EventEmitter from 'events';
import fs from 'fs';
import { VPK } from 'vpk2';
import * as vdf from 'simple-vdf3';
import hasha from 'hasha';
import winston from 'winston';
import Store from './store.js';
const CSGO_APP_ID = 730;
const CSGO_DEPOT_ID = 731;
const defaultConfig = {
directory: 'data',
updateInterval: 32400000,
enableHashCache: true,
stickers: true,
patches: true,
graffiti: true,
characters: true,
musicKits: true,
cases: true,
tools: true,
statusIcons: true,
logLevel: 'info'
};
const wears = ['Factory New', 'Minimal Wear', 'Field-Tested', 'Well-Worn', 'Battle-Scarred'];
const neededDirectories = {
stickers: 'resource/flash/econ/stickers',
patches: 'resource/flash/econ/patches',
graffiti: 'resource/flash/econ/stickers/default',
characters: 'resource/flash/econ/characters',
musicKits: 'resource/flash/econ/music_kits',
cases: 'resource/flash/econ/weapon_cases',
tools: 'resource/flash/econ/tools',
statusIcons: 'resource/flash/econ/status_icons',
};
function bytesToMB(bytes) {
return (bytes / 1000000).toFixed(2);
}
class CSCdn extends EventEmitter {
#ready = false;
#config;
user;
log;
vpkDir;
itemsGame;
csEnglish;
itemsGameCDN;
weaponNameMap = [];
csEnglishKeys = [];
#vpkStickerFiles = [];
#vpkPatchFiles = [];
#hashes = undefined;
get ready() {
return this.#ready;
}
get steamReady() {
return !!this.user.steamID;
}
static phase = {
ruby: 'am_ruby_marbleized',
sapphire: 'am_sapphire_marbleized',
blackpearl: 'am_blackpearl_marbleized',
emerald: 'am_emerald_marbleized',
phase1: 'phase1',
phase2: 'phase2',
phase3: 'phase3',
phase4: 'phase4'
};
set ready(r) {
const old = this.ready;
this.#ready = r;
if (r !== old && r) {
this.log.debug('Ready');
this.emit('ready');
}
}
constructor(steamUser, config = {}) {
super();
this.#config = Object.assign(defaultConfig, config);
this.user = steamUser;
this.#createDataDirectory();
if (this.#config.enableHashCache) {
this.#hashes = new Store(`${this.#config.directory}/hash_cache.json`);
}
this.log = winston.createLogger({
level: config.logLevel,
transports: [
new winston.transports.Console({
format: winston.format.printf((info) => {
return `[cs2-cdn] ${info.level}: ${info.message}`;
})
})
]
});
if (!this.steamReady) {
this.log.debug('Steam not ready, waiting for logon');
this.user.once('loggedOn', () => {
this.#updateLoop();
});
}
else {
this.#updateLoop();
}
}
/**
* Creates the data directory specified in the config if it doesn't exist
*/
#createDataDirectory() {
const dir = `./${this.#config.directory}`;
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
}
/**
* Runs the update loop at the specified config interval
* @return {Promise<undefined>|void}
*/
#updateLoop() {
if (this.#config.updateInterval && this.#config.updateInterval > 0) {
return this.update().then(() => setInterval(() => {
this.update();
}, this.#config.updateInterval));
}
else {
this.log.info('Auto-updates disabled, checking if required files exist');
// Try to load the resources locally
try {
this.#loadResources();
this.#loadVPK();
this.ready = true;
}
catch (e) {
this.log.warn('Needed CS:GO files not installed');
this.update();
}
}
}
/**
* Returns the product info for CSGO, with its depots and packages
*/
#getProductInfo() {
this.log.debug('Obtaining CS:GO product info');
return new Promise((resolve, reject) => {
this.user.getProductInfo([CSGO_APP_ID], [], true, (err, apps, packages, unknownApps, unknownPackages) => {
resolve([apps, packages, unknownApps, unknownPackages]);
});
});
}
/**
* Returns the latest CSGO manifest ID for the public CSGO_DEPOT_ID depot
* @return {*|PromiseLike<*[]>|Promise<*[]>} CSGO_DEPOT_ID Depot Manifest ID
*/
#getLatestManifestId() {
this.log.debug('Obtaining latest manifest ID');
return this.#getProductInfo().then(([apps, packages, unknownApps, unknownPackages]) => {
const csgo = apps[(CSGO_APP_ID).toString()].appinfo;
const commonDepot = csgo.depots[(CSGO_DEPOT_ID).toString()];
return commonDepot.manifests.public;
});
}
/**
* Retrieves and updates the sticker file directory from Valve
*
* Ensures that only the required VPK files are downloaded and that files with the same SHA1 aren't
* redownloaded
*
* @return {Promise<void>}
*/
async update() {
this.log.info('Checking for CS:GO file updates');
if (!this.steamReady) {
this.log.warn(`Steam not ready, can't check for updates`);
return;
}
const manifestId = await this.#getLatestManifestId();
this.log.debug(`Obtained latest manifest ID: ${manifestId.gid}`);
//@ts-ignore
const { manifest } = await this.user.getManifest(CSGO_APP_ID, CSGO_DEPOT_ID, manifestId.gid, 'public');
const manifestFiles = manifest.files;
const dirFile = manifest.files.find((file) => file.filename.endsWith("csgo\\pak01_dir.vpk"));
const itemsGameFile = manifest.files.find((file) => file.filename.endsWith("items_game.txt"));
const itemsGameCDNFile = manifest.files.find((file) => file.filename.endsWith("items_game_cdn.txt"));
const csEnglishFile = manifest.files.find((file) => file.filename.endsWith("csgo_english.txt"));
if (!dirFile || !itemsGameFile || !itemsGameCDNFile || !csEnglishFile) {
this.log.error("Failed to get a required manifestfile");
let manifest = "";
if (!dirFile) {
manifest = "dirFile";
}
if (!itemsGameFile) {
manifest = "itemsGameFile";
}
if (!itemsGameCDNFile) {
manifest = "itemsGameCDNFile";
}
if (!csEnglishFile) {
manifest = "csEnglishFile";
}
this.log.debug(`This manifestFile and potentially more could not be retrieved: ${manifest}`);
return;
}
this.log.debug(`Downloading required static files`);
await this.#downloadFiles([dirFile, itemsGameFile, itemsGameCDNFile, csEnglishFile]);
this.log.debug('Loading static file resources');
this.#loadResources();
this.#loadVPK();
if (this.vpkDir) {
await this.#downloadVPKFiles(this.vpkDir, manifestFiles);
}
else {
this.log.error("No VPK class available.");
}
this.ready = true;
}
#loadResources() {
this.itemsGame = vdf.parse(fs.readFileSync(`${this.#config.directory}/items_game.txt`, 'utf8'))['items_game'];
this.csEnglish = vdf.parse(fs.readFileSync(`${this.#config.directory}/csgo_english.txt`, 'ucs2'))['lang']['Tokens'];
this.itemsGameCDN = this.#parseItemsCDN(fs.readFileSync(`${this.#config.directory}/items_game_cdn.txt`, 'utf8'));
this.weaponNameMap = Object.keys(this.csEnglish).filter(n => n.startsWith("SFUI_WPNHUD"));
this.csEnglishKeys = Object.keys(this.csEnglish);
// Ensure paint kit descriptions are available in lowercase to resolve inconsistencies in the language and items_game file
Object.keys(this.itemsGame.paint_kits).forEach((n) => {
const kit = this.itemsGame.paint_kits[n];
if ('description_tag' in kit) {
// Check if english file and paintkit don't match, if they don't, make them the same.
if (!(kit.description_tag.replace("#", "") in this.csEnglish)) {
const endKey = kit.description_tag.replace("#", "").toLowerCase();
const engKey = getKeyCaseInsensitive(this.csEnglish, kit.description_tag.replace("#", ""));
if (engKey) {
this.csEnglish[endKey] = this.csEnglish[engKey];
this.csEnglishKeys.push(endKey);
}
kit.description_tag = "#" + endKey;
}
}
});
this.#invertDictionary(this.csEnglish);
}
/**
* Inverts the key mapping of a dictionary recursively while preserving the original keys
*
* Duplicate values with be an array
*
* @param dict Dictionary to invert
*/
#invertDictionary(dict) {
dict['inverted'] = {};
for (const prop in dict) {
if (prop === 'inverted' || !dict.hasOwnProperty(prop))
continue;
const val = dict[prop];
if (typeof val === 'object' && !(val instanceof Array)) {
this.#invertDictionary(val);
}
else {
if (dict['inverted'][val] === undefined) {
dict['inverted'][val] = [prop];
}
else {
dict['inverted'][val].push(prop);
}
}
}
}
#parseItemsCDN(data) {
let lines = data.split('\n');
const items_game_cdn = {};
for (let line of lines) {
let kv = line.split('=');
if (kv[1]) {
items_game_cdn[kv[0]] = kv[1];
}
}
return items_game_cdn;
}
/**
* Downloads the given VPK files from the Steam CDN
* @param files Steam Manifest File Array
* @return {Promise<any>} Fulfilled when completed downloading
*/
async #downloadFiles(files) {
const promises = [];
for (const file of files) {
const nameArr = file.filename.split('\\');
let name = nameArr[nameArr.length - 1];
const path = `${this.#config.directory}/${name}`;
const isDownloaded = await this.#isFileDownloaded(path, file.sha_content);
if (isDownloaded) {
continue;
}
//@ts-ignore
const promise = this.user.downloadFile(CSGO_APP_ID, CSGO_DEPOT_ID, file, `${this.#config.directory}/${name}`);
promises.push(promise);
}
return Promise.all(promises);
}
/**
* Loads the CSGO dir VPK specified in the config
*/
#loadVPK() {
this.vpkDir = new VPK(this.#config.directory + '/pak01_dir.vpk');
this.vpkDir.load();
this.#vpkStickerFiles = this.vpkDir.files.filter((f) => f.startsWith('resource/flash/econ/stickers'));
this.#vpkPatchFiles = this.vpkDir.files.filter((f) => f.startsWith('resource/flash/econ/patches'));
}
/**
* Given the CSGO VPK Directory, returns the necessary indices for the chosen options
* @param vpkDir CSGO VPK Directory
* @return {Array} Necessary Sticker VPK Indices
*/
#getRequiredVPKFiles(vpkDir) {
const requiredIndices = [];
const neededDirs = Object.keys(neededDirectories).filter((f) => !!(this.#config[f])).map((f) => neededDirectories[f]);
for (const fileName of vpkDir.files) {
for (const dir of neededDirs) {
if (fileName.startsWith(dir)) {
const archiveIndex = vpkDir.tree[fileName].archiveIndex;
if (!requiredIndices.includes(archiveIndex)) {
requiredIndices.push(archiveIndex);
}
break;
}
}
}
return requiredIndices.sort();
}
/**
* Downloads the required VPK files
* @param vpkDir CSGO VPK Directory
* @param manifestFiles Manifest files
* @return {Promise<void>}
*/
async #downloadVPKFiles(vpkDir, manifestFiles) {
this.log.debug('Computing required VPK files for selected packages');
const requiredIndices = this.#getRequiredVPKFiles(vpkDir);
this.log.debug(`Required VPK files ${requiredIndices}`);
for (let indexStr in requiredIndices) {
const index = parseInt(indexStr);
// pad to 3 zeroes
const archiveIndex = requiredIndices[index];
const paddedIndex = '0'.repeat(3 - archiveIndex.toString().length) + archiveIndex;
const fileName = `pak01_${paddedIndex}.vpk`;
const file = manifestFiles.find((file) => file.filename.endsWith(fileName));
if (!file) {
this.log.error(`Couldn't find ${fileName} in manifest.`);
continue;
}
const filePath = `${this.#config.directory}/${fileName}`;
const isDownloaded = await this.#isFileDownloaded(filePath, file.sha_content);
if (isDownloaded) {
this.log.info(`Already downloaded ${filePath}`);
continue;
}
const status = `[${index + 1}/${requiredIndices.length}]`;
this.log.info(`${status} Downloading ${fileName} - ${bytesToMB(file.size)} MB`);
//@ts-ignore
await this.user.downloadFile(CSGO_APP_ID, CSGO_DEPOT_ID, file, filePath, (_none, details) => {
const { type, bytesDownloaded, totalSizeBytes } = details;
if (type === 'progress') {
this.log.info(`${status} ${(bytesDownloaded * 100 / totalSizeBytes).toFixed(2)}% - ${bytesToMB(bytesDownloaded)}/${bytesToMB(totalSizeBytes)} MB`);
}
});
this.log.info(`${status} Downloaded ${fileName}`);
}
}
/**
* Returns whether a file at the given path has the given sha1
* @param path File path
* @param sha1 File SHA1 hash
* @return {Promise<boolean>} Whether the file has the hash
*/
async #isFileDownloaded(path, sha1) {
try {
if (this.#hashes?.getValue(path)) {
return this.#hashes.getValue(path);
}
const hash = await hasha.fromFile(path, { algorithm: 'sha1' });
if (this.#hashes) {
this.#hashes.setValue(path, hash);
}
return hash === sha1;
}
catch (e) {
return false;
}
}
/**
* Given a VPK path, returns the CDN URL
* @param path VPK path
* @return {string|void} CDN URL
*/
getPathURL(path) {
if (!this.vpkDir) {
this.log.debug("VPK is not initialized.");
return;
}
const file = this.vpkDir.getFile(path);
if (!file) {
this.log.error(`Failed to retrieve ${path} in VPK, do you have the package category enabled in options?`);
return;
}
const sha1 = hasha(file, {
'algorithm': 'sha1'
});
path = path.replace('resource/flash', 'icons');
path = path.replace('.png', `.${sha1}.png`);
return `https://steamcdn-a.akamaihd.net/apps/${CSGO_APP_ID}/${path}`;
}
/**
* Returns the item Steam CDN URL for the specified name
*
* Example Sticker Names: cologne2016/nv, cologne2016/fntc_holo, cologne2016/fntc_foil, cluj2015/sig_olofmeister_gold
*
* You can find the sticker names from their relevant "sticker_material" fields in items_game.txt
* items_game.txt can be found in the core game files of CS:GO or as itemsGame here
*
* @param name The item name (the sticker_material field in items_game.txt, or the cdn file format)
* @param large Whether to obtain the "large" CDN version of the item
* @return {string|void} If successful, the HTTPS CDN URL for the item
*/
getStickerURL(name, large = true) {
if (!this.ready) {
return;
}
const fileName = large ? `${name}_large.png` : `${name}.png`;
const path = this.#vpkStickerFiles.find((t) => t.endsWith(fileName));
if (path) {
return this.getPathURL(path);
}
}
/**
* Returns the item Steam CDN URL for the specified name
*
* Example Patch Names: case01/patch_phoenix, case01/patch_dangerzone, case01/patch_easypeasy, case_skillgroups/patch_goldnova1
*
* You can find the patch names from their relevant "patch_material" fields in items_game.txt
* items_game.txt can be found in the core game files of CS:GO or as itemsGame here
*
* @param name The item name (the patch_material field in items_game.txt, or the cdn file format)
* @param large Whether to obtain the "large" CDN version of the item
* @return {string|void} If successful, the HTTPS CDN URL for the item
*/
getPatchURL(name, large = true) {
if (!this.ready) {
return;
}
const fileName = large ? `${name}_large.png` : `${name}.png`;
const path = this.#vpkPatchFiles.find((t) => t.endsWith(fileName));
if (path)
return this.getPathURL(path);
}
/**
* Given the specified defindex and paintindex, returns the CDN URL
*
* The item properties can be found in items_game.txt
*
* @param defindex Item Definition Index (weapon type)
* @param paintindex Item Paint Index (skin type)
* @return {string|void} Weapon CDN URL
*/
getWeaponURL(defindex, paintindex) {
if (!this.ready) {
return;
}
const paintKits = this.itemsGame.paint_kits;
// Get the skin name
let skinName = '';
if (paintindex in paintKits) {
skinName = paintKits[paintindex].name;
if (skinName === 'default') {
skinName = '';
}
}
// Get the weapon name
let weaponName;
const items = this.itemsGame.items;
if (defindex in items) {
weaponName = items[defindex].name;
}
// Get the image url
const cdnName = `${weaponName}_${skinName}`;
return this.itemsGameCDN[cdnName];
}
/**
* Returns whether the given name is a weapon by checking
* the prefab and whether it is used by one of the sides
* @param marketHashName Item name
* @return {boolean} Whether a weapon
*/
isWeapon(marketHashName) {
const prefabs = this.itemsGame.prefabs;
const items = this.itemsGame.items;
const weaponName = marketHashName.split('|')[0].trim();
const weaponTags = this.csEnglish['inverted'][weaponName];
if (!weaponTags)
return false;
// For every matching weapon tag...
for (const t of weaponTags) {
const weaponTag = `#${t}`;
const prefab = Object.keys(prefabs).find((n) => {
const fab = prefabs[n];
return fab.item_name === weaponTag;
});
let fab;
if (!prefab) {
// special knives aren't in the prefab (karambits, etc...)
const item = Object.keys(items).find((n) => {
const i = items[n];
return i.item_name === weaponTag;
});
if (!item) {
return false;
}
fab = items[item];
}
else {
fab = prefabs[prefab];
}
if (fab && fab.used_by_classes) {
const used = fab.used_by_classes;
// Ensure that the item is used by one of the sides
if (used['terrorists'] || used['counter-terrorists']) {
return true;
}
}
}
return false;
}
/**
* Returns the sticker URL given the market hash name
* @param marketHashName Sticker name
* @return {string|void} Sticker image URL
*/
getStickerNameURL(marketHashName) {
const reg = /Sticker \| (.*)/;
const match = marketHashName.match(reg);
if (!match)
return;
const stickerName = match[1];
for (const tag of this.csEnglish['inverted'][stickerName] || []) {
const stickerTag = `#${tag}`;
const stickerKits = this.itemsGame.sticker_kits;
const kitIndex = Object.keys(stickerKits).find((n) => {
const k = stickerKits[n];
return k.item_name === stickerTag;
});
if (!kitIndex) {
continue;
}
const kit = stickerKits[kitIndex];
if (!kit || !kit.sticker_material)
continue;
const url = this.getStickerURL(stickerKits[kitIndex].sticker_material, true);
if (url) {
return url;
}
}
}
/**
* Returns the patch URL given the market hash name
* @param marketHashName Patch name
* @return {string|void} Patch image URL
*/
getPatchNameURL(marketHashName) {
const reg = /Patch \| (.*)/;
const match = marketHashName.match(reg);
if (!match)
return;
const stickerName = match[1];
for (const tag of this.csEnglish['inverted'][stickerName] || []) {
const stickerTag = `#${tag}`;
const stickerKits = this.itemsGame.sticker_kits; // Patches are in the sticker_kits as well
const kitIndex = Object.keys(stickerKits).find((n) => {
const k = stickerKits[n];
return k.item_name === stickerTag;
});
if (!kitIndex) {
continue;
}
const kit = stickerKits[kitIndex];
if (!kit || !kit.patch_material)
continue;
const url = this.getPatchURL(stickerKits[kitIndex].patch_material, true);
if (url)
return url;
}
}
/**
* Returns the graffiti URL given the market hash name
* @param marketHashName Graffiti name (optional tint)
* @param large Whether to obtain the "large" CDN version of the item
* @return {string|void} CDN Image URL
*/
getGraffitiNameURL(marketHashName, large = true) {
const reg = /Sealed Graffiti \| ([^(]*)/;
const match = marketHashName.match(reg);
if (!match) {
return;
}
const graffitiName = match[1].trim();
for (const tag of this.csEnglish['inverted'][graffitiName] || []) {
const stickerTag = `#${tag}`;
const stickerKits = this.itemsGame.sticker_kits;
const kitIndices = Object.keys(stickerKits).filter((n) => {
const k = stickerKits[n];
return k.item_name === stickerTag;
});
// prefer kit indices with "graffiti" in the name
kitIndices.sort((a, b) => {
const index1 = !!stickerKits[a].name && stickerKits[a].name.indexOf('graffiti');
const index2 = !!stickerKits[b].name && stickerKits[b].name.indexOf('graffiti');
if (index1 === index2) {
return 0;
}
else if (index1 > -1) {
return -1;
}
else {
return 1;
}
});
for (const kitIndex of kitIndices) {
const kit = stickerKits[kitIndex];
if (!kit || !kit.sticker_material)
continue;
const url = this.getStickerURL(kit.sticker_material, large);
if (url) {
return url;
}
}
}
}
/**
* Returns the weapon URL given the market hash name
* @param marketHashName Weapon name
* @param {Phase?} phase Optional Doppler Phase from the phase enum
* @return {string|void} Weapon image URL
*/
getWeaponNameURL(marketHashName, phase) {
const hasWear = wears.findIndex((n) => marketHashName.includes(n)) > -1;
if (hasWear) {
// remove it
marketHashName = marketHashName.replace(/\([^)]*\)$/, '');
}
const match = marketHashName.split('|').map((m) => m.trim());
const weaponName = match[0];
const skinName = match[1];
if (!weaponName)
return;
const weaponTags = this.csEnglish['inverted'][weaponName] || [];
const prefabs = this.itemsGame.prefabs;
const items = this.itemsGame.items;
// For every matching weapon tag...
for (const t of weaponTags) {
const weaponTag = `#${t}`;
const prefab = Object.keys(prefabs).find((n) => {
const fab = prefabs[n];
return fab.item_name === weaponTag;
});
let weaponClass;
if (!prefab) {
// special knives aren't in the prefab (karambits, etc...)
const item = Object.keys(items).find((n) => {
const i = items[n];
return i.item_name === weaponTag;
});
if (!item) {
continue;
}
if (items[item]) {
weaponClass = items[item].name;
}
}
else {
const item = Object.keys(items).find((n) => {
const i = items[n];
return i.prefab === prefab;
});
if (!item) {
continue;
}
if (items[item]) {
weaponClass = items[item].name;
}
}
if (!weaponClass) {
continue;
}
// Check if this is a vanilla weapon
if (!skinName) {
if (weaponClass && this.itemsGameCDN[weaponClass]) {
return this.itemsGameCDN[weaponClass];
}
else {
continue;
}
}
// Check if is widow knife and if so if the specified phase is special (the gems have the same index as regular dopplers, but the widow has it's own indexes for phase 1-4).
const isSpecialWidow = weaponClass === "weapon_knife_widowmaker"
&& skinName === "Doppler"
&& phase !== "am_ruby_marbleized"
&& phase !== "am_sapphire_marbleized"
&& phase !== "am_blackpearl_marbleized";
// Roughly same as isSpecialWidow, but for butterfly and push knives.
const isSpecialB = (weaponClass === "weapon_knife_push" || weaponClass === "weapon_knife_butterfly")
&& skinName === "Doppler"
&& (phase === "phase2"
|| phase === "am_sapphire_marbleized"
|| phase === "am_blackpearl_marbleized");
// For every matching skin name...
for (const key of this.csEnglish['inverted'][skinName] || []) {
const skinTag = `#${key}`;
const paintKits = this.itemsGame.paint_kits;
const paintindexes = Object.keys(paintKits).filter((n) => {
const kit = paintKits[n];
let isPhase;
if (weaponClass === "weapon_glock") {
isPhase = !phase || kit.name.endsWith(phase + "_glock");
}
else if (isSpecialWidow) {
isPhase = !phase || kit.name.endsWith(phase + "_widow");
}
else if (isSpecialB) {
isPhase = !phase || kit.name.endsWith(phase + "_b");
}
else {
isPhase = !phase || kit.name.endsWith(phase);
}
return isPhase && kit.description_tag === skinTag;
});
// For every matching paint index...
for (const paintindex of paintindexes) {
const paintKit = paintKits[paintindex].name;
const path = (paintKit ? `${weaponClass}_${paintKit}` : weaponClass).toLowerCase();
if (this.itemsGameCDN[path]) {
return this.itemsGameCDN[path];
}
}
}
}
}
/**
* Returns the music kit URL given the market hash name
* @param marketHashName Music kit name
* @return {string|void} Music kit image URL
*/
getMusicKitNameURL(marketHashName) {
const reg = /Music Kit \| (.*)/;
const match = marketHashName.match(reg);
if (!match) {
return;
}
const kitName = match[1];
for (const t of this.csEnglish['inverted'][kitName] || []) {
const tag = `#${t}`;
const musicDefs = this.itemsGame.music_definitions;
const kitIndex = Object.keys(musicDefs).find((n) => {
const k = musicDefs[n];
return k.loc_name === tag;
});
if (!kitIndex) {
continue;
}
const kit = musicDefs[kitIndex];
if (!kit || !kit.image_inventory)
continue;
const path = `resource/flash/${kit.image_inventory}.png`;
const url = this.getPathURL(path);
if (url) {
return url;
}
}
}
/**
* Retrieves the given item CDN URL given its market_hash_name
*
* Examples: M4A4 | 龍王 (Dragon King) (Field-Tested), Sticker | Robo, AWP | Redline (Field-Tested)
*
* Note: For a weapon, the name MUST have the associated wear
*
* @param marketHashName Item name
* @param {Phase?} phase Optional Doppler Phase from the phase enum
*/
getItemNameURL(marketHashName, phase) {
marketHashName = marketHashName.trim();
let strippedMarketHashName = marketHashName;
// Weapons and Music Kits can have extra tags we need to ignore
const extraTags = ['★ ', 'StatTrak™ ', 'Souvenir '];
for (const tag of extraTags) {
if (strippedMarketHashName.startsWith(tag)) {
strippedMarketHashName = strippedMarketHashName.replace(tag, '');
}
}
if (this.isWeapon(strippedMarketHashName)) {
return this.getWeaponNameURL(strippedMarketHashName, phase);
}
else if (strippedMarketHashName.startsWith('Music Kit |')) {
return this.getMusicKitNameURL(strippedMarketHashName);
}
else if (marketHashName.startsWith('Sticker |')) {
return this.getStickerNameURL(marketHashName);
}
else if (marketHashName.startsWith('Sealed Graffiti |')) {
return this.getGraffitiNameURL(marketHashName);
}
else if (marketHashName.startsWith('Patch |')) {
return this.getPatchNameURL(marketHashName);
}
else {
// Other in items
for (const t of this.csEnglish['inverted'][marketHashName] || []) {
const tag = `#${t.toLowerCase()}`;
const items = this.itemsGame.items;
const prefabs = this.itemsGame.prefabs;
let item = Object.keys(items).find((n) => {
const i = items[n];
return i.item_name && i.item_name.toLowerCase() === tag;
});
if (!item) {
continue;
}
let path;
if (!items[item] || !items[item].image_inventory) {
// search the prefabs (ex. CS:GO Case Key)
item = Object.keys(prefabs).find((n) => {
const i = prefabs[n];
return i.item_name && i.item_name.toLowerCase() === tag;
});
if (!item) {
continue;
}
if (!prefabs[item] || !prefabs[item].image_inventory)
continue;
path = `resource/flash/${prefabs[item].image_inventory}.png`;
}
else {
path = `resource/flash/${items[item].image_inventory}.png`;
}
const url = this.getPathURL(path);
if (url) {
return url;
}
}
}
}
}
/**
* @param {Object} object
* @param {string} key
* @return {string | undefined} key
*/
function getKeyCaseInsensitive(object, key) {
const asLowercase = key.toLowerCase();
return Object.keys(object)
.find((k) => k.toLowerCase() === asLowercase);
}
export default CSCdn;