From 442300415326c13d2401b76e3d64857f2281ad6a Mon Sep 17 00:00:00 2001 From: DuckySoLucky Date: Sat, 6 Aug 2022 19:02:15 +0200 Subject: [PATCH] 1.0 --- AuctionHandler.js | 101 +++++++++++++++++++++++++++ config.json | 106 +++++++++++++++++++++++++++++ index.js | 139 +++++++++++++++++++++++++++++++++++++ package.json | 23 +++++++ src/Item.js | 28 ++++++++ src/helperFunctions.js | 151 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 548 insertions(+) create mode 100644 AuctionHandler.js create mode 100644 config.json create mode 100644 index.js create mode 100644 package.json create mode 100644 src/Item.js create mode 100644 src/helperFunctions.js diff --git a/AuctionHandler.js b/AuctionHandler.js new file mode 100644 index 0000000..3dc82a7 --- /dev/null +++ b/AuctionHandler.js @@ -0,0 +1,101 @@ +const {default: axios} = require("axios"); +const {getParsed, getProfit, splitNumber, getRawCraft} = require("./src/helperFunctions"); +const {parentPort, workerData} = require("worker_threads"); +const config = require("./config.json") +let minProfit = config.data.minSnipeProfit +let minPercentProfit = config.data.minSnipePP +let ignoredAuctions = [] +const {Item} = require("./src/Item") +const threadsToUse = require("./config.json").data["threadsToUse/speed"] +const promises = [] + +parentPort.on("message", async (message) => { + if (message.type === "pageCount") { + await doTask(message.data) + } else if (message.type === "moulberry") { + workerData.itemDatas = message.data + } +}) + +async function parsePage(i) { + const auctionPage = await axios.get(`https://api.hypixel.net/skyblock/auctions?page=${i}`) + for (const auction of auctionPage.data.auctions) { + if (!auction.bin) continue + const uuid = auction.uuid + if (ignoredAuctions.includes(uuid) || config.data.ignoreCategories[auction.category]) continue + const item = await getParsed(auction.item_bytes) + const extraAtt = item["i"][0].tag.ExtraAttributes + const itemID = extraAtt.id + let startingBid = auction.starting_bid + const itemData = workerData.itemDatas[itemID] + if (!itemData) continue + const lbin = itemData.lbin + const sales = itemData.sales + const prettyItem = new Item(item.i[0].tag.display.Name, uuid, startingBid, auction.tier, extraAtt.enchantments, + extraAtt.hot_potato_count > 10 ? 10 : extraAtt.hot_potato_count, extraAtt.hot_potato_count > 10 ? + extraAtt.hot_potato_count - 10 : 0, extraAtt.rarity_upgrades === 1, + extraAtt.art_of_war_count === 1, extraAtt.dungeon_item_level, + extraAtt.gems, itemID, auction.category, 0, 0, lbin, sales, auction.item_lore) + const unstableOrMarketManipulated = Math.abs((lbin - itemData.cleanPrice) / lbin) > config.data.maxAvgLbinDiff + ignoredAuctions.push(uuid) + const rcCost = config.data.includeCraftCost ? getRawCraft(prettyItem, workerData.bazaarData, workerData.itemDatas) : 0 + const carriedByRC = rcCost >= config.data.rawCraftMaxWeightPP * lbin + + if (carriedByRC || unstableOrMarketManipulated || sales <= config.data.minSales || !sales) continue + + if (config.filters.nameFilter.find((name) => itemID.includes(name)) === undefined) { + if ((lbin + rcCost) - startingBid > minProfit) { + const profitData = getProfit(startingBid, rcCost, lbin) + let auctionType = null + if (rcCost > (lbin - startingBid) && profitData.snipeProfit < minProfit) { + auctionType = "VALUE" + } else if (profitData.snipeProfit >= minProfit && rcCost < (lbin - startingBid)) { + auctionType = "SNIPE" + } else if (profitData.snipeProfit >= minProfit && rcCost > 0) { + auctionType = "BOTH" + } + + prettyItem.auctionData.ahType = auctionType + + if (auctionType === "VALUE" || auctionType === "BOTH") { + if (profitData.RCProfit > config.data.minCraftProfit && profitData.RCPP > config.data.minCraftPP) { + prettyItem.auctionData.profit = profitData.RCProfit + prettyItem.auctionData.percentProfit = profitData.RCPP + parentPort.postMessage(prettyItem) + } + } else { + if (profitData.snipeProfit > minProfit && profitData.snipePP > minPercentProfit) { + prettyItem.auctionData.profit = profitData.snipeProfit + prettyItem.auctionData.percentProfit = profitData.snipePP + parentPort.postMessage(prettyItem) + } + } + } + } + } +} + +async function doTask(totalPages) { + let startingPage = 0 + const pagePerThread = splitNumber(totalPages, threadsToUse) + + if (workerData.workerNumber !== 0 && startingPage === 0) { + const clonedStarting = pagePerThread.slice() + clonedStarting.splice(workerData.workerNumber, 9999); + clonedStarting.forEach((pagePer) => { + startingPage += pagePer + }) + } + + let pageToStop = parseInt(startingPage) + parseInt(pagePerThread[workerData.workerNumber]) + + if (pageToStop !== totalPages) { + pageToStop -= 1 + } + + for (let i = startingPage; i < pageToStop; i++) { + promises.push(parsePage(i)) + } + await Promise.all(promises) + //parentPort.postMessage("finished") +} diff --git a/config.json b/config.json new file mode 100644 index 0000000..7550460 --- /dev/null +++ b/config.json @@ -0,0 +1,106 @@ +{ + "data": { + "threadsToUse/speed": 64, + + "minSnipeProfit": 100000, + "minCraftProfit": 100000, + + "maxAvgLbinDiff": 0.35, + "rawCraftMaxWeightPP": 0.4, + "minSnipePP": 8, + "minCraftPP": 8, + + "ignoreCategories": { + "weapon": false, + "accessories": true, + "armor": false, + "misc": false, + "blocks": false, + "consumables": true + }, + "minSales": 4, + "includeCraftCost": true, + "minPriceForRawcraft": 5000000 + }, + "webhook": { + "discordWebhookUrl": "WEBHOOK_URL", + "webhookName": "Flipper", + "webhookPFP": "https://cdn.discordapp.com/avatars/486155512568741900/164084b936b4461fe9505398f7383a0e.png?size=4096" + }, + "filters": { + "rawCraftIgnoreEnchants": { + "WITHER_": { + "growth": 6, + "protection": 6 + }, + "SHADOW_ASSASSIN": { + "growth": 6, + "protection": 6 + }, + "NECROMANCER": { + "growth": 6, + "protection": 6 + }, + "SHREDDED": { + "ultimate_soul_eater": 5 + }, + "GOLD_": { + "growth": 6, + "protection": 6 + } + }, + "badEnchants": { + "giant_killer": 6, + "growth": 6, + "power": 6, + "protection": 6, + "sharpness": 6, + "ender_slayer": 6, + "smite": 6, + "critical": 6, + "bane_of arthropods": 6, + "vampirism": 6, + "luck": 6, + "syphon": 4, + "ultimate_soul_eater": 3, + "ultimate_wise": 4, + "ultimate_wisdom": 4, + "ultimate_legion": 3 + }, + "nameFilter": [ + "SALMON", + "PERFECT", + "BEASTMASTER", + "MASTER_SKULL", + "BLAZE", + "TITANIUM", + "SUPER_HEAVY", + "WAND_OF", + "FARM_ARMOR", + "PURE_MITHRIL", + "STEEL_CHESTPLATE", + "MIDAS", + "TRIBAL_SPEAR", + "POWER_SCROLL", + "_TRAVEL_SCROLL", + "ARTISINAL", + "ZOMBIE", + "CRYPT", + "SOULSTEALER", + "GUN", + "SPIRIT_DECOY", + "EXP", + "PET_SKIN", + "SEEKER", + "MOSQUITO", + "REVELATION", + "FRAGMENT", + "RECALL", + "HEAT_CORE", + "DIVER", + "SPONGE", + "CREEPER", + "CAKE_SOUL" + ] + } +} diff --git a/index.js b/index.js new file mode 100644 index 0000000..17c7bcf --- /dev/null +++ b/index.js @@ -0,0 +1,139 @@ +const {default: axios} = require("axios") +const config = require("./config.json") +const discord = require('discord.js') +const {Worker} = require("worker_threads") +const { asyncInterval, addNotation } = require("./src/helperFunctions") +let threadsToUse = config.data["threadsToUse/speed"] ?? 1 +let lastUpdated = 0 +let doneWorkers = 0 +let startingTime +let maxPrice = 0 +let itemDatas = {} +const workers = [] +const webhookRegex = /https:\/\/discord.com\/api\/webhooks\/(.+)\/(.+)/ + +const bazaarPrice = { + "RECOMBOBULATOR_3000": 0, + "HOT_POTATO_BOOK": 0, + "FUMING_POTATO_BOOK": 0 +} + +async function initialize() { + matches = config.webhook.discordWebhookUrl.match(webhookRegex) + if (!matches) return console.log(`[Main thread] Couldn't parse Webhook URL`) + const webhook = new discord.WebhookClient(matches[1], matches[2]); + + await getBzData() + await getMoulberry() + await getLBINs() + + for (let j = 0; j < threadsToUse; j++) { + workers[j] = new Worker('./AuctionHandler.js', { + workerData: { + itemDatas: itemDatas, + bazaarData: bazaarPrice, + workerNumber: j, + maxPrice: maxPrice + } + }) + + workers[j].on("message", async (result) => { + if (result.itemData !== undefined) { + if (result.auctionData.lbin >= result.auctionData.price) { + await webhook.send({ + username: config.webhook.webhookName, + avatarURL: config.webhook.webhookPFP, + embeds: [new discord.MessageEmbed() + .setTitle(`**${(result.itemData.name).replaceAll(/ยง./g, '')}**`) + .setColor("#2e3137") + .setThumbnail(`https://sky.shiiyu.moe/item/${result.itemData.id}`) + .setDescription(`Auction: \`/viewauction ${result.auctionData.auctionID}\`\nProfit: \`${addNotation("oneLetters", (result.auctionData.profit))} (${result.auctionData.percentProfit}%)\`\nCost: \`${addNotation("oneLetters", (result.auctionData.price))}\`\nLBIN: \`${addNotation("oneLetters", (result.auctionData.lbin))}\`\nSales/Day: \`${addNotation("oneLetters", result.auctionData.sales)}\`\nType: \`${result.auctionData.ahType}\``) + ] + }) + + } + } else if (result === "finished") { + doneWorkers++ + if (doneWorkers === threadsToUse) { + doneWorkers = 0 + console.log(`Completed in ${(Date.now() - startingTime) / 1000} seconds`) + startingTime = 0 + workers[0].emit("done") + } + } + }); + } + + asyncInterval(async () => { + await getLBINs() + workers.forEach((worker) => { + worker.postMessage({type: "moulberry", data: itemDatas}) + }) + }, "lbin", 60000) + + asyncInterval(async () => { + await getMoulberry() + workers.forEach((worker) => { + worker.postMessage({type: "moulberry", data: itemDatas}) + }) + }, "avg", 60e5) + + + asyncInterval(async () => { + return new Promise(async (resolve) => { + const ahFirstPage = await axios.get("https://api.hypixel.net/skyblock/auctions?page=0") + const totalPages = ahFirstPage.data.totalPages + if (ahFirstPage.data.lastUpdated === lastUpdated) { + resolve() + } else { + lastUpdated = ahFirstPage.data.lastUpdated + startingTime = Date.now() + console.log("Getting auctions..") + workers.forEach((worker) => { + worker.postMessage({type: "pageCount", data: totalPages}) + }) + workers[0].once("done", () => { + resolve() + }) + } + }) + }, "check", 0) +} + +async function getLBINs() { + const lbins = await axios.get("https://moulberry.codes/lowestbin.json") + const lbinData = lbins.data + for (const item of Object.keys(lbinData)) { + if (!itemDatas[item]) itemDatas[item] = {} + itemDatas[item].lbin = lbinData[item] + } +} + +async function getMoulberry() { + const moulberryAvgs = await axios.get("https://moulberry.codes/auction_averages/3day.json") + const avgData = moulberryAvgs.data + for (const item of Object.keys(avgData)) { + itemDatas[item] = {} + const itemInfo = avgData[item] + if (itemInfo.sales !== undefined) { + itemDatas[item].sales = itemInfo.sales + } else { + itemDatas[item].sales = 0 + } + if (itemInfo.clean_price) { + itemDatas[item].cleanPrice = itemInfo.clean_price + } else { + itemDatas[item].cleanPrice = itemInfo.price + } + } +} + +async function getBzData() { + const bzData = await axios.get("https://api.hypixel.net/skyblock/bazaar") + bazaarPrice["RECOMBOBULATOR_3000"] = bzData.data.products.RECOMBOBULATOR_3000.quick_status.buyPrice + bazaarPrice["HOT_POTATO_BOOK"] = bzData.data.products.HOT_POTATO_BOOK.quick_status.buyPrice + bazaarPrice["FUMING_POTATO_BOOK"] = bzData.data.products.FUMING_POTATO_BOOK.quick_status.buyPrice +} + + +initialize() diff --git a/package.json b/package.json new file mode 100644 index 0000000..60c288f --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "hypixel-auction-flipper", + "version": "0.6.9", + "dependencies": { + "axios": "^0.24.0", + "copy-paste": "^1.3.0", + "discord.js": "^12.5.3", + "express": "^4.17.1", + "node-notifier": "^10.0.0", + "open": "^8.4.0", + "prismarine-nbt": "^2.0.0", + "socket.io": "^4.4.0", + "toastify-js": "^1.11.2" + }, + "description": "Hypixel Skyblock Auction House Flip Notifier", + "main": "index.js", + "devDependencies": {}, + "scripts": { + "start": "node ." + }, + "keywords": [], + "author": "DuckySoLucky" +} diff --git a/src/Item.js b/src/Item.js new file mode 100644 index 0000000..27b0717 --- /dev/null +++ b/src/Item.js @@ -0,0 +1,28 @@ +function Item(name, auctionID, price, rarity, enchants, hpbs, fpbs, recomb, artofwar, stars, gemstones, id, category, profit, percentProfit, lbin, sales, lore) { + this.itemData = { + "name": name, + "id": id, + "stars": stars, + "rarity": rarity, + "recomb": recomb, + "enchants": enchants, + "hpbs": hpbs, + "fpbs": fpbs, + "gemstones": gemstones, + "aow": artofwar, + "lore": lore + } + this.auctionData = { + "auctionID": auctionID, + "category": category, + "sales": sales, + "price": price, + "profit": profit, + "percentProfit": percentProfit, + "lbin": lbin + } +} + +module.exports = { + Item +} \ No newline at end of file diff --git a/src/helperFunctions.js b/src/helperFunctions.js new file mode 100644 index 0000000..890eb29 --- /dev/null +++ b/src/helperFunctions.js @@ -0,0 +1,151 @@ +const config = require('../config.json') +const nbt = require('prismarine-nbt') +let currentAsyncIntervals = {} + +function addNotation(type, value) { + let returnVal = value; + let notList = []; + if (type === "shortScale") { + notList = [ + " Thousand", + " Million", + " Billion", + " Trillion", + " Quadrillion", + " Quintillion" + ]; + } + + if (type === "oneLetters") { + notList = ["K", "M", "B", "T"]; + } + + let checkNum = 1000; + if (type !== "none" && type !== "commas") { + let notValue = notList[notList.length - 1]; + for (let u = notList.length; u >= 1; u--) { + notValue = notList.shift(); + for (let o = 3; o >= 1; o--) { + if (value >= checkNum) { + returnVal = value / (checkNum / 100); + returnVal = Math.floor(returnVal); + returnVal = (returnVal / Math.pow(10, o)) * 10; + returnVal = +returnVal.toFixed(o - 1) + notValue; + } + checkNum *= 10; + } + } + } else { + returnVal = numberWithCommas(value.toFixed(0)); + } + + return returnVal; +} + +async function getParsed(encoded) { + return new Promise((resolve) => { + let buf = Buffer.from(encoded, 'base64'); + nbt.parse(buf, (err, dat) => { + if (err) throw err; + resolve(nbt.simplify(dat)) + }); + }) +} + +function getProfit(price, rcCost, lbin) { + const profitItem = {} + if (price >= 1000000) { + profitItem.RCProfit = ((lbin + rcCost) - price) + - ((lbin + rcCost) * 0.02); + profitItem.RCPP = parseFloat(((profitItem.RCProfit * 100) / lbin).toFixed(1)) + profitItem.snipeProfit = (lbin - price) - (lbin * 0.02) + profitItem.snipePP = parseFloat(((profitItem.snipeProfit * 100) / lbin).toFixed(1)) + } else { + profitItem.RCProfit = ((lbin + rcCost) - price) + - ((lbin + rcCost) * 0.01); + profitItem.RCPP = parseFloat(((profitItem.RCProfit * 100) / lbin).toFixed(1)) + profitItem.snipeProfit = (lbin - price) - (lbin * 0.01) + profitItem.snipePP = parseFloat(((profitItem.snipeProfit * 100) / lbin).toFixed(1)) + } + + return profitItem +} + +function splitNumber (num = 1, parts = 1) { + let n = Math.floor(num / parts); + const arr = []; + for (let i = 0; i < parts; i++){ + arr.push(n) + } + if(arr.reduce((a, b)=> a + b,0) === num){ + return arr; + } + for(let i = 0; i < parts; i++){ + arr[i]++; + if(arr.reduce((a, b) => a + b, 0) === num){ + return arr; + } + } +} + +function getRawCraft(item, bazaarPrice, lbins) { + let price = 0 + const ignoreMatch = Object.keys(config.filters.rawCraftIgnoreEnchants).find((key) => { + if (item.itemData.id.includes(key)) return true + }) + if (item.auctionData.lbin < config.data.minPriceForRawcraft) return 0 + let isInIgnore = ignoreMatch ? ignoreMatch : false + if (item.itemData.enchants && !item.itemData.id.includes(';')) { + for (const enchant of Object.keys(item.itemData.enchants)) { + const degree = item.itemData.enchants[enchant] + const badEnchant = typeof config.filters.badEnchants[enchant] === 'number' ? degree >= config.filters.badEnchants[enchant] : false + if (isInIgnore) { + const enchantMinValue = config.filters.rawCraftIgnoreEnchants[ignoreMatch][enchant] + if (enchantMinValue >= degree) continue + } + if (badEnchant) { + price += lbins[`${enchant.toUpperCase()};${degree.toString()}`] ? lbins[`${enchant.toUpperCase()};${degree.toString()}`].lbin * 0.5 : 0 + } + } + } + if (item.itemData.aow) { + price += lbins['THE_ART_OF_WAR'] * 0.3 + } + if (item.itemData.recomb && (item.auctionData.category === 'weapon' || item.auctionData.category === 'armor' || item.auctionData.category === 'accessories')) { + price += bazaarPrice['RECOMBOBULATOR_3000'] * 0.5 + } + price += (item.itemData.hpbs ? item.itemData.hpbs : 0) * bazaarPrice['HOT_POTATO_BOOK'] * 0.05 + price += (item.itemData.fpbs ? item.itemData.fpbs : 0) * bazaarPrice['FUMING_POTATO_BOOK'] * 0.1 + + return price +} + +async function asyncInterval(asyncTask, intervalname, timeout) { + currentAsyncIntervals[intervalname] = true + setTimeout(async function () { + if (!currentAsyncIntervals[intervalname]) return + asyncTask().then(async function () { + await asyncInterval(asyncTask, intervalname, timeout) + }) + }, timeout) +} + +function stopAsyncInterval(intervalname) { + currentAsyncIntervals[intervalname] = false +} + +function currentIntervals() { + return currentAsyncIntervals +} + + +module.exports = { + addNotation, + getParsed, + getProfit, + splitNumber, + getRawCraft, + asyncInterval, + stopAsyncInterval, + currentIntervals +}