Compare commits
13 commits
125c0e9f90
...
8031a71bc5
Author | SHA1 | Date | |
---|---|---|---|
8031a71bc5 | |||
3d24fea0b2 | |||
7ce3de2229 | |||
6aa5a5a9e8 | |||
325b86f4a7 | |||
14759592b8 | |||
97ac5bf9f1 | |||
e9d2c9a6cf | |||
b756637cad | |||
e4495938ff | |||
470f9e0a25 | |||
95c39eca7c | |||
02337bdb2d |
23 changed files with 978 additions and 461 deletions
8
.dockerignore
Normal file
8
.dockerignore
Normal file
|
@ -0,0 +1,8 @@
|
|||
config*
|
||||
docker*
|
||||
Docker*
|
||||
README*
|
||||
node_modules/
|
||||
.*
|
||||
pnpm-lock*
|
||||
dist
|
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
node_modules
|
||||
.env
|
||||
pnpm-lock*
|
||||
dist
|
||||
*.db
|
||||
*.sqlite*
|
9
.prettierrc
Normal file
9
.prettierrc
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"parser": "babel-ts",
|
||||
"bracketSameLine": true,
|
||||
"printWidth": 120
|
||||
}
|
|
@ -1,114 +0,0 @@
|
|||
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 = [];
|
||||
|
||||
console.log(`[Worker ${workerData.workerNumber}] Worker started`);
|
||||
|
||||
parentPort.on("message", async (message) => {
|
||||
console.log(`[Worker ${workerData.workerNumber}] Received message: ${message.type}`);
|
||||
if (message.type === "pageCount") {
|
||||
await doTask(message.data);
|
||||
} else if (message.type === "moulberry") {
|
||||
workerData.itemDatas = message.data;
|
||||
console.log(`[Worker ${workerData.workerNumber}] Updated item data`);
|
||||
}
|
||||
});
|
||||
|
||||
async function parsePage(i) {
|
||||
console.log(`[Worker ${workerData.workerNumber}] Parsing page ${i}`);
|
||||
try {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[Worker ${workerData.workerNumber}] Error parsing page ${i}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
async function doTask(totalPages) {
|
||||
console.log(`[Worker ${workerData.workerNumber}] Starting task for ${totalPages} pages`);
|
||||
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;
|
||||
}
|
||||
|
||||
console.log(`[Worker ${workerData.workerNumber}] Processing pages from ${startingPage} to ${pageToStop}`);
|
||||
|
||||
for (let i = startingPage; i < pageToStop; i++) {
|
||||
promises.push(parsePage(i));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
console.log(`[Worker ${workerData.workerNumber}] Finished task`);
|
||||
parentPort.postMessage("finished");
|
||||
}
|
178
AuctionHandler.worker.ts
Normal file
178
AuctionHandler.worker.ts
Normal file
|
@ -0,0 +1,178 @@
|
|||
import axios from 'axios';
|
||||
import { getParsed, getProfit, splitNumber, getRawCraft } from './src/helperFunctions';
|
||||
import { parentPort, workerData, isMainThread } from 'worker_threads';
|
||||
import { Item } from './src/Item';
|
||||
import { AuctionResponse, Auction, Bid } from './src/auctionType';
|
||||
|
||||
import { loadConfig } from './src/configLoader';
|
||||
const config = loadConfig();
|
||||
|
||||
if (isMainThread || !parentPort) {
|
||||
throw new Error('This module can only be run in a Worker thread.');
|
||||
}
|
||||
|
||||
const worker_count: number = config.data.worker_count;
|
||||
let minProfit = config.data.minSnipeProfit;
|
||||
let minPercentProfit = config.data.minSnipePP;
|
||||
let ignoredAuctions: any[] = [];
|
||||
const promises: Promise<any>[] = [];
|
||||
|
||||
console.log(`[Worker ${workerData.workerNumber}] Worker started`);
|
||||
|
||||
parentPort.on('message', async (message) => {
|
||||
console.log(
|
||||
`[Worker ${workerData.workerNumber}] Received message: ${message.type}`
|
||||
);
|
||||
if (message.type === 'pageCount') {
|
||||
await doTask(message.data);
|
||||
} else if (message.type === 'moulberry') {
|
||||
workerData.itemDatas = message.data;
|
||||
console.log(`[Worker ${workerData.workerNumber}] Updated item data`);
|
||||
}
|
||||
});
|
||||
|
||||
async function parsePage(i: number) {
|
||||
console.log(`[Worker ${workerData.workerNumber}] Parsing page ${i}`);
|
||||
try {
|
||||
const auctionPage = await axios.get<AuctionResponse>(
|
||||
`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.itemIDExclusions.find((name) =>
|
||||
itemID.includes(name)
|
||||
) === undefined
|
||||
) {
|
||||
if (lbin + rcCost - startingBid > minProfit) {
|
||||
const profitData = getProfit(startingBid, rcCost, lbin);
|
||||
let auctionType: string | null = 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[Worker ${workerData.workerNumber}] Error parsing page ${i}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function doTask(totalPages: number) {
|
||||
console.log(
|
||||
`[Worker ${workerData.workerNumber}] Starting task for ${totalPages} pages`
|
||||
);
|
||||
let startingPage = 0;
|
||||
const pagePerThread = splitNumber(totalPages, worker_count);
|
||||
|
||||
if (workerData.workerNumber !== 0 && startingPage === 0) {
|
||||
const clonedStarting = pagePerThread.slice();
|
||||
clonedStarting.splice(workerData.workerNumber, 9999);
|
||||
clonedStarting.forEach((pagePer) => {
|
||||
startingPage += pagePer;
|
||||
});
|
||||
}
|
||||
|
||||
let pageToStop = startingPage + pagePerThread[workerData.workerNumber];
|
||||
|
||||
if (pageToStop !== totalPages) {
|
||||
pageToStop -= 1;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[Worker ${workerData.workerNumber}] Processing pages from ${startingPage} to ${pageToStop}`
|
||||
);
|
||||
|
||||
for (let i = startingPage; i < pageToStop; i++) {
|
||||
promises.push(parsePage(i));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
console.log(`[Worker ${workerData.workerNumber}] Finished task`);
|
||||
parentPort!.postMessage('finished');
|
||||
}
|
11
Dockerfile
Normal file
11
Dockerfile
Normal file
|
@ -0,0 +1,11 @@
|
|||
FROM node:23-alpine3.20
|
||||
|
||||
COPY . /app
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN npm install
|
||||
|
||||
RUN npm run build
|
||||
|
||||
CMD npm start
|
23
README.md
23
README.md
|
@ -19,3 +19,26 @@
|
|||
- Ability to make some high tier enchantments worthless (like Looking 4, Luck 6 etc..)
|
||||
- Bad Enchantment filter
|
||||
- And more..
|
||||
|
||||
# Setup
|
||||
|
||||
## .env
|
||||
`.env` file is needed due to docker compose. Even if you are not using docker and docker compose, **.ENV IS NEEDED**
|
||||
|
||||
__the example .env is__
|
||||
```
|
||||
WEBHOOK_URL=<webhook_url>
|
||||
<<<<<<< HEAD
|
||||
WEBHOOK_NAME=Auction_Notifier
|
||||
WEBHOOK_PROFILE_PICTURE=<picture_url>
|
||||
=======
|
||||
WEBHOOK_NAME=Flipper
|
||||
WEBHOOK_PROFILE_PICTURE=https://cdn.discordapp.com/avatars/486155512568741900/164084b936b4461fe9505398f7383a0e.png?size=4096
|
||||
>>>>>>> aad843d (uppercased environment variables)
|
||||
```
|
||||
You can also add these as environment variables into your system when running Node or run the app via `node --env-file=.env index.js`
|
||||
|
||||
## config.json
|
||||
### filters
|
||||
- itemIDExclusions: exclusion based on itemID (contains, doesnt have to be the full itemID name)
|
||||
- EnchantThreshold and EnchantThresholdConditionalBypass are used for **Raw Crafting**. I suggest leaving them as they are.
|
||||
|
|
2
add_remotes.bat
Normal file
2
add_remotes.bat
Normal file
|
@ -0,0 +1,2 @@
|
|||
git remote add upstream https://github.com/MashClashXD/Hypixel-Auction-Flipper.git
|
||||
git remote add upstream2 https://github.com/DuckySoLucky/Hypixel-Auction-Flipper.git
|
2
add_remotes.sh
Normal file
2
add_remotes.sh
Normal file
|
@ -0,0 +1,2 @@
|
|||
git remote add upstream https://github.com/MashClashXD/Hypixel-Auction-Flipper.git
|
||||
git remote add upstream2 https://github.com/DuckySoLucky/Hypixel-Auction-Flipper.git
|
21
config.json
21
config.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"data": {
|
||||
"threadsToUse/speed": 48,
|
||||
"worker_count": 48,
|
||||
|
||||
"minSnipeProfit": 900000,
|
||||
"minAvgProfit": 500000,
|
||||
|
@ -23,13 +23,8 @@
|
|||
"includeCraftCost": true,
|
||||
"minPriceForRawcraft": 5000000
|
||||
},
|
||||
"webhook": {
|
||||
"discordWebhookUrl": "WEBHOOK_URL",
|
||||
"webhookName": "Flipper",
|
||||
"webhookPFP": "https://cdn.discordapp.com/avatars/486155512568741900/164084b936b4461fe9505398f7383a0e.png?size=4096"
|
||||
},
|
||||
"filters": {
|
||||
"rawCraftIgnoreEnchants": {
|
||||
"EnchantThresholdConditionalBypass": {
|
||||
"WITHER_": {
|
||||
"growth": 6,
|
||||
"protection": 6
|
||||
|
@ -39,7 +34,7 @@
|
|||
"protection": 6
|
||||
},
|
||||
"NECROMANCER": {
|
||||
"growth": 6,
|
||||
"growth": 4,
|
||||
"protection": 6
|
||||
},
|
||||
"SHREDDED": {
|
||||
|
@ -50,7 +45,7 @@
|
|||
"protection": 6
|
||||
}
|
||||
},
|
||||
"badEnchants": {
|
||||
"EnchantThreshold": {
|
||||
"giant_killer": 6,
|
||||
"growth": 6,
|
||||
"power": 6,
|
||||
|
@ -63,12 +58,12 @@
|
|||
"vampirism": 6,
|
||||
"luck": 6,
|
||||
"syphon": 4,
|
||||
"ultimate_soul_eater": 3,
|
||||
"ultimate_wise": 4,
|
||||
"ultimate_wisdom": 4,
|
||||
"ultimate_soul_eater": 6,
|
||||
"ultimate_wise": 5,
|
||||
"ultimate_wisdom": 5,
|
||||
"ultimate_legion": 3
|
||||
},
|
||||
"nameFilter": [
|
||||
"itemIDExclusions": [
|
||||
"SALMON",
|
||||
"PERFECT",
|
||||
"BEASTMASTER",
|
||||
|
|
14
docker-compose.yml
Normal file
14
docker-compose.yml
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
services:
|
||||
bot:
|
||||
container_name: hypixel-auc-notifier
|
||||
build: .
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
NODE_ENV: "production"
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- ./config.json:/app/dist/src/config.json
|
10
env.d.ts
vendored
Normal file
10
env.d.ts
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
declare global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
WEBHOOK_URL: string;
|
||||
WEBHOOK_NAME: string;
|
||||
WEBHOOK_PROFILE_PICTURE: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
export { };
|
137
index.js
137
index.js
|
@ -1,137 +0,0 @@
|
|||
const { default: axios } = require("axios");
|
||||
const config = require("./config.json");
|
||||
const { WebhookClient, MessageEmbed } = 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() {
|
||||
const matches = config.webhook.discordWebhookUrl.match(webhookRegex);
|
||||
if (!matches) return console.log(`[Main thread] Couldn't parse Webhook URL`);
|
||||
const webhook = new WebhookClient({ id: matches[1], token: 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) {
|
||||
let averagePrice = itemDatas[result.itemData.id]?.cleanPrice || "N/A";
|
||||
if (result.auctionData.lbin - result.auctionData.price >= config.data.minSnipeProfit && averagePrice - result.auctionData.price >= config.data.minAvgProfit) {
|
||||
let mustBuyMessage = '';
|
||||
const embed = new MessageEmbed()
|
||||
.setTitle(`**${(result.itemData.name).replace(/§./g, '')}**`)
|
||||
.setColor("#2e3137")
|
||||
.setThumbnail(`https://sky.shiiyu.moe/item/${result.itemData.id}`)
|
||||
.setDescription(`${mustBuyMessage}\nAuction: \`\`\`/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}\`\nAverage Price: \`${addNotation("oneLetters", averagePrice)}\``);
|
||||
|
||||
await webhook.send({
|
||||
username: config.webhook.webhookName,
|
||||
avatarURL: config.webhook.webhookPFP,
|
||||
embeds: [embed]
|
||||
});
|
||||
}
|
||||
} 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;
|
||||
|
||||
const cleanPriceAvgs = await axios.get("https://moulberry.codes/auction_averages_lbin/1day.json");
|
||||
const cleanPriceData = cleanPriceAvgs.data;
|
||||
|
||||
for (const item of Object.keys(avgData)) {
|
||||
if (!itemDatas[item]) itemDatas[item] = {};
|
||||
const itemInfo = avgData[item];
|
||||
|
||||
itemDatas[item].sales = itemInfo.sales !== undefined ? itemInfo.sales : 0;
|
||||
itemDatas[item].cleanPrice = cleanPriceData[item] !== undefined ? Math.round(cleanPriceData[item]) : (itemInfo.clean_price !== undefined ? itemInfo.clean_price : 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();
|
203
index.ts
Normal file
203
index.ts
Normal file
|
@ -0,0 +1,203 @@
|
|||
import axios from 'axios';
|
||||
import { WebhookClient, EmbedBuilder, Embed } from 'discord.js';
|
||||
import { Worker } from 'worker_threads';
|
||||
import { asyncInterval, addNotation } from './src/helperFunctions';
|
||||
import { string } from 'prismarine-nbt';
|
||||
import { InitTable } from './src/sqlFunctions';
|
||||
import { loadConfig } from './src/configLoader';
|
||||
const config = loadConfig();
|
||||
|
||||
class ItemData {
|
||||
public sales: number;
|
||||
public lbin: number;
|
||||
public cleanPrice: number;
|
||||
public price: number;
|
||||
|
||||
constructor(sales: number = 0, lbin: number = 0, cleanPrice: number = 0, price: number = 0) {
|
||||
this.sales = sales;
|
||||
this.lbin = lbin;
|
||||
this.cleanPrice = cleanPrice;
|
||||
this.price = price;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let worker_count = config.data.worker_count ?? 1;
|
||||
let lastUpdated = 0;
|
||||
let doneWorkers = 0;
|
||||
let startingTime: number;
|
||||
let maxPrice = 0;
|
||||
let itemDatas: Record<string, ItemData> = {};
|
||||
const workers: Worker[] = [];
|
||||
const webhookRegex = /https:\/\/discord.com\/api\/webhooks\/(.+)\/(.+)/;
|
||||
|
||||
const bazaarPrice = {
|
||||
RECOMBOBULATOR_3000: 0,
|
||||
HOT_POTATO_BOOK: 0,
|
||||
FUMING_POTATO_BOOK: 0,
|
||||
};
|
||||
|
||||
async function initialize() {
|
||||
await InitTable();
|
||||
const matches = process.env.WEBHOOK_URL.match(webhookRegex);
|
||||
if (!matches) return console.log(`[Main thread] Couldn't parse Webhook URL`);
|
||||
const webhook = new WebhookClient({ id: matches[1], token: matches[2] });
|
||||
|
||||
await getBzData();
|
||||
await getMoulberry();
|
||||
await getLBINs();
|
||||
|
||||
for (let j = 0; j < worker_count; j++) {
|
||||
workers[j] = new Worker('/app/dist/AuctionHandler.worker.js', {
|
||||
workerData: {
|
||||
itemDatas: itemDatas,
|
||||
bazaarData: bazaarPrice,
|
||||
workerNumber: j,
|
||||
maxPrice: maxPrice,
|
||||
},
|
||||
});
|
||||
|
||||
workers[j].on('message', async (result) => {
|
||||
if (result.itemData !== undefined) {
|
||||
let averagePrice: number | null = itemDatas[result.itemData.id]?.cleanPrice || null;
|
||||
if (
|
||||
result.auctionData.lbin - result.auctionData.price >=
|
||||
config.data.minSnipeProfit &&
|
||||
(averagePrice || averagePrice! - result.auctionData.price >= config.data.minAvgProfit)
|
||||
) {
|
||||
let mustBuyMessage = '';
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(`**${result.itemData.name.replace(/§./g, '')}**`)
|
||||
.setColor('#2e3137')
|
||||
.setThumbnail(`https://sky.shiiyu.moe/item/${result.itemData.id}`)
|
||||
.setDescription(
|
||||
`${mustBuyMessage}\nAuction:
|
||||
\`\`\`/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}\`
|
||||
\nAverage Price: \`${averagePrice ? addNotation('oneLetters', averagePrice) : 'N/A'}\``
|
||||
);
|
||||
|
||||
await webhook.send({
|
||||
username: process.env.WEBHOOK_NAME,
|
||||
avatarURL: process.env.WEBHOOK_PROFILE_PICTURE,
|
||||
embeds: [embed],
|
||||
});
|
||||
}
|
||||
} else if (result === 'finished') {
|
||||
doneWorkers++;
|
||||
if (doneWorkers === worker_count) {
|
||||
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(): Promise<void> {
|
||||
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] = new ItemData();
|
||||
itemDatas[item].lbin = lbinData[item];
|
||||
}
|
||||
}
|
||||
|
||||
async function getMoulberry(): Promise<void> {
|
||||
const moulberryAvgs = await axios.get(
|
||||
'https://moulberry.codes/auction_averages/3day.json'
|
||||
);
|
||||
const avgData = moulberryAvgs.data;
|
||||
|
||||
const cleanPriceAvgs = await axios.get(
|
||||
'https://moulberry.codes/auction_averages_lbin/1day.json'
|
||||
);
|
||||
const cleanPriceData = cleanPriceAvgs.data;
|
||||
|
||||
for (const item of Object.keys(avgData)) {
|
||||
if (!itemDatas[item]) itemDatas[item] = new ItemData();
|
||||
const itemInfo = avgData[item];
|
||||
|
||||
itemDatas[item].sales = itemInfo.sales !== undefined ? itemInfo.sales : 0;
|
||||
itemDatas[item].cleanPrice =
|
||||
cleanPriceData[item] !== undefined
|
||||
? Math.round(cleanPriceData[item])
|
||||
: itemInfo.clean_price !== undefined
|
||||
? itemInfo.clean_price
|
||||
: itemInfo.price;
|
||||
}
|
||||
}
|
||||
|
||||
async function getBzData(): Promise<void> {
|
||||
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();
|
42
package.json
42
package.json
|
@ -1,23 +1,29 @@
|
|||
{
|
||||
"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"
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"description": "Hypixel Skyblock Auction House Flip Notifier",
|
||||
"main": "index.js",
|
||||
"devDependencies": {},
|
||||
"scripts": {
|
||||
"start": "node ."
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"author": "DuckySoLucky + MashClashXD + Ulysia + Sol",
|
||||
"keywords": [],
|
||||
"author": "DuckySoLucky + MashClashXD"
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "ts-node index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/toastify-js": "^1.12.3",
|
||||
"axios": "^0.24.0",
|
||||
"copy-paste": "^1.5.3",
|
||||
"discord.js": "^14.16.3",
|
||||
"express": "^4.21.2",
|
||||
"node-notifier": "^10.0.1",
|
||||
"open": "^8.4.2",
|
||||
"prismarine-nbt": "^2.7.0",
|
||||
"socket.io": "^4.8.1",
|
||||
"sqlite3": "^5.1.7",
|
||||
"toastify-js": "^1.12.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
|
|
28
src/Item.js
28
src/Item.js
|
@ -1,28 +0,0 @@
|
|||
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
|
||||
}
|
78
src/Item.ts
Normal file
78
src/Item.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
// item.ts
|
||||
|
||||
export interface ItemData {
|
||||
name: string;
|
||||
id: string;
|
||||
stars: number;
|
||||
rarity: string;
|
||||
recomb: boolean;
|
||||
enchants: Record<string, number>;
|
||||
hpbs: number;
|
||||
fpbs: number;
|
||||
gemstones: string[];
|
||||
aow: boolean;
|
||||
lore: string;
|
||||
}
|
||||
|
||||
export interface AuctionData {
|
||||
auctionID: string;
|
||||
category: string;
|
||||
sales: number;
|
||||
price: number;
|
||||
profit: number;
|
||||
percentProfit: number;
|
||||
lbin: number;
|
||||
ahType: string | null;
|
||||
}
|
||||
|
||||
export class Item {
|
||||
public itemData: ItemData;
|
||||
public auctionData: AuctionData;
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
auctionID: string,
|
||||
price: number,
|
||||
rarity: string,
|
||||
enchants: Record<string, number>,
|
||||
hpbs: number,
|
||||
fpbs: number,
|
||||
recomb: boolean,
|
||||
artofwar: boolean,
|
||||
stars: number,
|
||||
gemstones: string[],
|
||||
id: string,
|
||||
category: string,
|
||||
profit: number,
|
||||
percentProfit: number,
|
||||
lbin: number,
|
||||
sales: number,
|
||||
lore: string,
|
||||
ahType: string | null = null
|
||||
) {
|
||||
this.itemData = {
|
||||
name,
|
||||
id,
|
||||
stars,
|
||||
rarity,
|
||||
recomb,
|
||||
enchants,
|
||||
hpbs,
|
||||
fpbs,
|
||||
gemstones,
|
||||
aow: artofwar,
|
||||
lore,
|
||||
};
|
||||
|
||||
this.auctionData = {
|
||||
auctionID,
|
||||
category,
|
||||
sales,
|
||||
price,
|
||||
profit,
|
||||
percentProfit,
|
||||
lbin,
|
||||
ahType,
|
||||
};
|
||||
}
|
||||
}
|
39
src/auctionType.ts
Normal file
39
src/auctionType.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
export type AuctionResponse = {
|
||||
success: boolean
|
||||
page: number
|
||||
totalPages: number
|
||||
totalAuctions: number
|
||||
lastUpdated: number
|
||||
auctions: Auction[]
|
||||
}
|
||||
|
||||
export type Auction = {
|
||||
uuid: string
|
||||
auctioneer: string
|
||||
profile_id: string
|
||||
coop: string[]
|
||||
start: number
|
||||
end: number
|
||||
item_name: string
|
||||
item_lore: string
|
||||
extra: string
|
||||
category: string
|
||||
tier: string
|
||||
starting_bid: number
|
||||
item_bytes: string
|
||||
claimed: boolean
|
||||
claimed_bidders: any[]
|
||||
highest_bid_amount: number
|
||||
last_updated: number
|
||||
bin: boolean
|
||||
bids: Bid[]
|
||||
item_uuid?: string
|
||||
}
|
||||
|
||||
export type Bid = {
|
||||
auction_id: string
|
||||
bidder: string
|
||||
profile_id: string
|
||||
amount: number
|
||||
timestamp: number
|
||||
}
|
34
src/configLoader.ts
Normal file
34
src/configLoader.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export interface ConfigType {
|
||||
data: {
|
||||
worker_count: number;
|
||||
minSnipeProfit: number;
|
||||
minAvgProfit: number;
|
||||
minCraftProfit: number;
|
||||
maxAvgLbinDiff: number;
|
||||
rawCraftMaxWeightPP: number;
|
||||
minSnipePP: number;
|
||||
minCraftPP: number;
|
||||
ignoreCategories: Record<string, boolean>;
|
||||
minSales: number;
|
||||
includeCraftCost: boolean;
|
||||
minPriceForRawcraft: number;
|
||||
};
|
||||
filters: {
|
||||
EnchantThresholdConditionalBypass: Record<string, Record<string, number>>;
|
||||
EnchantThreshold: Record<string, number>;
|
||||
itemIDExclusions: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export function loadConfig(): ConfigType {
|
||||
const configPath = path.join(__dirname, 'config.json');
|
||||
try {
|
||||
const fileContents = fs.readFileSync(configPath, 'utf8');
|
||||
return JSON.parse(fileContents) as ConfigType;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load config.json at ${configPath}: ${error}`);
|
||||
}
|
||||
}
|
|
@ -1,151 +0,0 @@
|
|||
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
|
||||
}
|
241
src/helperFunctions.ts
Normal file
241
src/helperFunctions.ts
Normal file
|
@ -0,0 +1,241 @@
|
|||
// utils.ts
|
||||
import nbt from 'prismarine-nbt';
|
||||
import { Item } from './Item';
|
||||
|
||||
import { loadConfig } from './configLoader';
|
||||
const config = loadConfig();
|
||||
|
||||
// If you have a defined structure for your config file, feel free to refine this type.
|
||||
interface ConfigType {
|
||||
data: {
|
||||
minPriceForRawcraft: number;
|
||||
[key: string]: any;
|
||||
};
|
||||
filters: {
|
||||
EnchantThreshold: Record<string, number>;
|
||||
EnchantThresholdConditionalBypass: Record<string, Record<string, number>>;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
interface NbtData {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// Keep track of current intervals by name
|
||||
let currentAsyncIntervals: Record<string, boolean> = {};
|
||||
|
||||
type ProfitItem = {
|
||||
RCProfit: number;
|
||||
RCPP: number;
|
||||
snipeProfit: number;
|
||||
snipePP: number;
|
||||
};
|
||||
|
||||
function numberWithCommas(x: number | string): string {
|
||||
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
}
|
||||
|
||||
function addNotation(type: string, value: number): string {
|
||||
let returnVal: string = value.toString();
|
||||
let notList: string[] = [];
|
||||
|
||||
if (type === 'shortScale') {
|
||||
notList = [' Thousand', ' Million', ' Billion', ' Trillion', ' Quadrillion', ' Quintillion'];
|
||||
} else 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) {
|
||||
// Use a fixed decimal approach
|
||||
returnVal = (
|
||||
+(Math.floor(value / (checkNum / 100)) / Math.pow(10, o)) * 10
|
||||
).toFixed(o - 1) + notValue;
|
||||
}
|
||||
checkNum *= 10;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
returnVal = numberWithCommas(value.toFixed(0));
|
||||
}
|
||||
|
||||
return returnVal;
|
||||
}
|
||||
|
||||
async function getParsed(encoded: string): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const buf = Buffer.from(encoded, 'base64');
|
||||
// The callback signature must match the prismarine-nbt types:
|
||||
nbt.parse(buf, (err: Error | null, data) => {
|
||||
if (err) return reject(err);
|
||||
resolve(nbt.simplify(data));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getProfit(price: number, rcCost: number, lbin: number): ProfitItem {
|
||||
const profitItem: ProfitItem = {
|
||||
RCProfit: 0,
|
||||
RCPP: 0,
|
||||
snipeProfit: 0,
|
||||
snipePP: 0,
|
||||
};
|
||||
|
||||
if (price >= 1_000_000) {
|
||||
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: number = 1, parts: number = 1): number[] {
|
||||
const n: number = Math.floor(num / parts);
|
||||
const arr: number[] = [];
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
// If we still haven’t balanced it out, it’ll just return this array
|
||||
return arr;
|
||||
}
|
||||
|
||||
interface ItemType {
|
||||
itemData: {
|
||||
id: string;
|
||||
enchants?: Record<string, number>;
|
||||
aow?: boolean;
|
||||
recomb?: boolean;
|
||||
hpbs?: number;
|
||||
fpbs?: number;
|
||||
};
|
||||
auctionData: {
|
||||
lbin: number;
|
||||
category?: string;
|
||||
};
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface LbinsType {
|
||||
[key: string]: {
|
||||
lbin: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface BazaarPriceType {
|
||||
[key: string]: number;
|
||||
}
|
||||
|
||||
function getRawCraft(
|
||||
item: Item,
|
||||
bazaarPrice: BazaarPriceType,
|
||||
lbins: LbinsType
|
||||
): number {
|
||||
let price = 0;
|
||||
|
||||
const typedConfig = config as ConfigType;
|
||||
|
||||
const ignoreMatch: string =
|
||||
Object.keys(typedConfig.filters.EnchantThresholdConditionalBypass ?? {}).find((key) => {
|
||||
return item.itemData.id.includes(key);
|
||||
}) ?? '';
|
||||
|
||||
if (item.auctionData.lbin < typedConfig.data.minPriceForRawcraft) return 0;
|
||||
|
||||
const isInIgnore = !!ignoreMatch;
|
||||
|
||||
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 typedConfig.filters.EnchantThreshold[enchant] === 'number'
|
||||
? degree >= typedConfig.filters.EnchantThreshold[enchant]
|
||||
: false;
|
||||
|
||||
if (isInIgnore) {
|
||||
const enchantMinValue =
|
||||
typedConfig.filters.EnchantThresholdConditionalBypass[ignoreMatch]?.[enchant];
|
||||
if (enchantMinValue && degree <= enchantMinValue) 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'].lbin * 0.3;
|
||||
}
|
||||
|
||||
if (
|
||||
item.itemData.recomb &&
|
||||
['weapon', 'armor', 'accessories'].includes(item.auctionData.category ?? '')
|
||||
) {
|
||||
price += bazaarPrice['RECOMBOBULATOR_3000'] * 0.5;
|
||||
}
|
||||
|
||||
price += (item.itemData.hpbs ?? 0) * bazaarPrice['HOT_POTATO_BOOK'] * 0.05;
|
||||
price += (item.itemData.fpbs ?? 0) * bazaarPrice['FUMING_POTATO_BOOK'] * 0.1;
|
||||
|
||||
return price;
|
||||
}
|
||||
|
||||
// Schedules a recurring async task.
|
||||
async function asyncInterval(
|
||||
asyncTask: () => Promise<void>,
|
||||
intervalName: string,
|
||||
timeout: number
|
||||
): Promise<void> {
|
||||
currentAsyncIntervals[intervalName] = true;
|
||||
setTimeout(async function () {
|
||||
if (!currentAsyncIntervals[intervalName]) return;
|
||||
await asyncTask();
|
||||
await asyncInterval(asyncTask, intervalName, timeout);
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
function stopAsyncInterval(intervalName: string): void {
|
||||
currentAsyncIntervals[intervalName] = false;
|
||||
}
|
||||
|
||||
function currentIntervals(): Record<string, boolean> {
|
||||
return currentAsyncIntervals;
|
||||
}
|
||||
|
||||
// Exports
|
||||
export {
|
||||
addNotation,
|
||||
getParsed,
|
||||
getProfit,
|
||||
splitNumber,
|
||||
getRawCraft,
|
||||
asyncInterval,
|
||||
stopAsyncInterval,
|
||||
currentIntervals,
|
||||
};
|
68
src/sqlFunctions.ts
Normal file
68
src/sqlFunctions.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
import sqlite3 from 'sqlite3';
|
||||
|
||||
//TODO
|
||||
// MUTEX functions for adding/removing/upsert
|
||||
// basic read function by id
|
||||
// complex read function by value range
|
||||
|
||||
async function InitTable() {
|
||||
const db = new sqlite3.Database('bot_data');
|
||||
try{
|
||||
await runQuery(db,'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)')
|
||||
console.log('Table created successfully.');
|
||||
|
||||
// Insert data
|
||||
await runQuery(db, 'INSERT INTO users (name) VALUES (?)', ['Alice']);
|
||||
console.log('Data inserted successfully.');
|
||||
|
||||
// Retrieve a single row
|
||||
const row = await getQuery(db, 'SELECT * FROM users WHERE name = ?', ['Alice']);
|
||||
console.log('Retrieved row:', row);
|
||||
|
||||
// Retrieve all rows
|
||||
const rows = await allQuery(db, 'SELECT * FROM users');
|
||||
console.log('All rows:', rows);
|
||||
} catch (err: any) {
|
||||
console.error('Database error:', err.message);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
function runQuery(db: sqlite3.Database, query: string, params: string[] = []) {
|
||||
return new Promise((resolve,reject) => {
|
||||
db.run(query, params, function (err) {
|
||||
if(err){
|
||||
reject(err);
|
||||
}
|
||||
else{
|
||||
resolve(this);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function getQuery(db: sqlite3.Database, query: string, params: string[] = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.get(query, params, (err, row) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(row);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function allQuery(db: sqlite3.Database, query: string, params: string[] = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.all(query, params, (err, rows) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(rows);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
InitTable
|
||||
}
|
20
tsconfig.json
Normal file
20
tsconfig.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "nodenext",
|
||||
"target": "ES2024",
|
||||
"jsx": "preserve",
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "./dist",
|
||||
"strict": true,
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"**/node_modules/*"
|
||||
]
|
||||
}
|
Loading…
Add table
Reference in a new issue