diff --git a/.github/scripts/parsers/1_21_5/attributes.mjs b/.github/scripts/parsers/1_21_5/attributes.mjs index ffa6c5d..6837eca 100644 --- a/.github/scripts/parsers/1_21_5/attributes.mjs +++ b/.github/scripts/parsers/1_21_5/attributes.mjs @@ -71,7 +71,7 @@ export const Attributes = { attributeOverlaysFile.push({ type: "attribute", id: item.nbt.ExtraAttributes.id, - ...getOverlay(item), + ...overlayProps, }); } }, diff --git a/.github/scripts/parsers/1_21_5/id_overlays.mjs b/.github/scripts/parsers/1_21_5/id_overlays.mjs index 259e3f4..896ef58 100644 --- a/.github/scripts/parsers/1_21_5/id_overlays.mjs +++ b/.github/scripts/parsers/1_21_5/id_overlays.mjs @@ -1,4 +1,6 @@ import {cleanObject} from "./items.mjs"; +import {romanToInt} from "../../utils/roman_numeral.mjs"; +import {isCollectionItem,getCollectionId} from "../../utils/collection.mjs"; const getWiki = (item) => { if (item.infoType !== "WIKI_URL" || !item.info || item.info.length === 0) return undefined; @@ -9,9 +11,91 @@ const getWiki = (item) => { }); }; +const hotm = ["hotm", "heart of the mountain", "heart of the mountain tier"]; +const hotf = ["hotf", "heart of the forest", "heart of the forest tier"]; +const bossCollection = ["bonzo", "scarf", "the professor", "thorn", "livid", "sadan", "necron"]; +const skills = ["combat", "farming", "fishing", "mining", "foraging", "enchanting", "alchemy", "carpentry", "taming", "hunting", "duneoneering"]; +const slayers = ["zombie", "spider", "wolf", "enderman", "blaze", "vampire"]; + +const parseLevel = (levelStr) => !isNaN(levelStr) ? parseInt(levelStr, 10) : romanToInt(levelStr); + +const getRequirements = (item) => { + const out = []; + + if (item.crafttext) { + if (item.crafttext.startsWith("Requires")) { + const stringReqs = item.crafttext.replace(/^(Requires:?)/, "").trim().split(" & "); + + for (const req of stringReqs) { + const match = req.match(/^(.*?)\s+([0-9]+|[IVXLCDM]+)$/); + + if (match) { + const [, name, levelStr] = match; + const lowerName = name.toLowerCase(); + + const reqData = { level: parseLevel(levelStr) }; + + if (lowerName.endsWith("slayer")) { + reqData.type = "slayer"; + reqData.name = name.substring(0, name.length - 6).trim(); + } else if (hotm.includes(lowerName)) { + reqData.type = "hotm"; + } else if (hotf.includes(lowerName)) { + reqData.type = "hotf"; + } else if (bossCollection.includes(lowerName) || skills.includes(lowerName)) { + reqData.type = bossCollection.includes(lowerName) ? "bossCollection" : "skill"; + reqData.name = name; + } else if (isCollectionItem(name)) { + reqData.type = "collection"; + reqData.name = name; + reqData.id = getCollectionId(name); + } else { + reqData.type = "unknown"; + reqData.name = name; + } + + out.push(reqData); + } else { + out.push({ type: "unknown", name: req, level: null }); + } + } + } else { + console.warn("Non Requirement crafttext found: " + item.crafttext); + } + } + + if (item.slayer_req) { + const [nameStr, levelStr] = item.slayer_req.split("_"); + const lowerName = nameStr?.toLowerCase(); + + if (slayers.includes(lowerName)) { + out.push({ + type: "slayer", + name: lowerName.charAt(0).toUpperCase() + lowerName.slice(1), + level: parseLevel(levelStr) + }); + } + } + + if (out.length === 0) return undefined; + + const uniqueReqs = new Map(); + for (const req of out) { + const key = `${req.type}_${req.name || ''}`; + const existing = uniqueReqs.get(key); + + if (!existing || (req.level !== null && existing.level !== null && req.level > existing.level)) { + uniqueReqs.set(key, req); + } + } + + return Array.from(uniqueReqs.values()); +} + export const getOverlay = (item) => { const overlay = cleanObject({ vanilla: item.vanilla ? true : undefined, + requirements: getRequirements(item), wiki: getWiki(item), }); diff --git a/.github/scripts/parsers/1_21_5/items.mjs b/.github/scripts/parsers/1_21_5/items.mjs index d6cf958..ce73fdb 100644 --- a/.github/scripts/parsers/1_21_5/items.mjs +++ b/.github/scripts/parsers/1_21_5/items.mjs @@ -150,7 +150,7 @@ export const Items = { itemOverlaysFile.push({ type: "item", id: itemStack.components["minecraft:custom_data"].id, - ...getOverlay(item), + ...overlayProps, }); } }, diff --git a/.github/scripts/parsers/parse.mjs b/.github/scripts/parsers/parse.mjs index 8e5300b..5386f93 100644 --- a/.github/scripts/parsers/parse.mjs +++ b/.github/scripts/parsers/parse.mjs @@ -10,6 +10,7 @@ import {Runes} from "./1_21_5/runes.mjs"; import {Enchantments} from "./1_21_5/enchantments.mjs"; import {Potions} from "./1_21_5/potions.mjs"; import {Attributes} from "./1_21_5/attributes.mjs"; +import {fetchCollections} from "../utils/collection.mjs"; const specialItems = JSON.parse(fs.readFileSync(".github/scripts/data/special_items.json", "utf-8")); const itemOverlayIndex = buildItemOverlayIndex(); @@ -26,57 +27,69 @@ const isEntity = (file) => { return false; } -const post = [] -for (let file of fs.readdirSync("neu/items")) { - if (!file.endsWith(".json")) { - console.error("[WARN] (Parse) Skipping non-json file found in items directory: " + file); - continue; - } - const itemId = file.slice(0, -".json".length); - const data = JSON.parse(fs.readFileSync(`./neu/items/${file}`, "utf-8")); - data.nbt = decodeLegacy(data.nbttag); - const itemOverlay = itemOverlayIndex.get(itemId); - if (itemOverlay) { - try { - data.itemOverlay = decodeModern(fs.readFileSync(itemOverlay.path, "utf-8")); - } catch (error) { - throw new Error(`Failed to parse item overlay ${itemOverlay.path}: ${error.message}`, {cause: error}); - } - } else { - console.warn(`[WARN] (Parse) Missing item SNBT overlay: ${itemId}`); - } +async function run() { + await fetchCollections() + + const post = [] - const attributes = data.nbt.ExtraAttributes; + for (let file of fs.readdirSync("neu/items")) { + if (!file.endsWith(".json")) { + console.error("[WARN] (Parse) Skipping non-json file found in items directory: " + file); + continue; + } + const itemId = file.slice(0, -".json".length); + const data = JSON.parse(fs.readFileSync(`./neu/items/${file}`, "utf-8")); + data.nbt = decodeLegacy(data.nbttag); + const itemOverlay = itemOverlayIndex.get(itemId); + if (itemOverlay) { + try { + data.itemOverlay = decodeModern(fs.readFileSync(itemOverlay.path, "utf-8")); + } catch (error) { + throw new Error(`Failed to parse item overlay ${itemOverlay.path}: ${error.message}`, {cause: error}); + } + } else { + console.warn(`[WARN] (Parse) Missing item SNBT overlay: ${itemId}`); + } - if (specialItems.items.includes(data.internalname)) continue; - if (specialItems.items.includes(attributes?.id)) continue; + const attributes = data.nbt.ExtraAttributes; - post.push(() => { - Recipes.parse(data) - }) + if (specialItems.items.includes(data.internalname)) continue; + if (specialItems.items.includes(attributes?.id)) continue; - if (isEntity(file)) { post.push(() => { - Mobs.parseMob(data); + Recipes.parse(data) }) - } else { - if (attributes.hasOwnProperty("attributes") && data.internalname.startsWith("ATTRIBUTE_SHARD_")) { - Attributes.parseAttribute(data) - } else if (attributes.hasOwnProperty("runes")) { - Runes.parseRune(data); - } else if (attributes.hasOwnProperty("petInfo")) { - data.pet = JSON.parse(attributes.petInfo); - Pets.parsePet(data); - } else if (data.displayname.match(/§.Enchanted Book/) && data.itemid === "minecraft:enchanted_book" && attributes.enchantments) { - Enchantments.parseEnchantments(data); - } else if (isPotion(data)) { - Potions.parsePotions(data); - } else if (data.internalname.includes(";")) { - //console.log(file + " is a variant"); + + if (isEntity(file)) { + post.push(async () => { + Mobs.parseMob(data); + }) } else { - Mc1215.items.parseItem(data); + if (attributes.hasOwnProperty("attributes") && data.internalname.startsWith("ATTRIBUTE_SHARD_")) { + Attributes.parseAttribute(data) + } else if (attributes.hasOwnProperty("runes")) { + Runes.parseRune(data); + } else if (attributes.hasOwnProperty("petInfo")) { + data.pet = JSON.parse(attributes.petInfo); + Pets.parsePet(data); + } else if (data.displayname.match(/§.Enchanted Book/) && data.itemid === "minecraft:enchanted_book" && attributes.enchantments) { + Enchantments.parseEnchantments(data); + } else if (isPotion(data)) { + Potions.parsePotions(data); + } else if (data.internalname.includes(";")) { + //console.log(file + " is a variant"); + } else { + Mc1215.items.parseItem(data); + } } } + + post.forEach((recipe) => recipe()) + + fs.writeFileSync("cloudflare/shas.json", JSON.stringify({ + "1_21_5": Mc1215.shas(), + ...clone(), + }, null, 4)); } function isPotion(data) { @@ -108,13 +121,6 @@ function isPotion(data) { return false } -post.forEach((recipe) => recipe()) - -fs.writeFileSync("cloudflare/shas.json", JSON.stringify({ - "1_21_5": Mc1215.shas(), - ...clone(), -}, null, 4)); - function buildItemOverlayIndex() { const result = new Map(); const overlayRoot = "neu/itemsOverlay"; @@ -146,3 +152,5 @@ function buildItemOverlayIndex() { return result; } + +await run(); diff --git a/.github/scripts/utils/collection.mjs b/.github/scripts/utils/collection.mjs new file mode 100644 index 0000000..951b67c --- /dev/null +++ b/.github/scripts/utils/collection.mjs @@ -0,0 +1,42 @@ +const url = "https://api.hypixel.net/v2/resources/skyblock/collections"; + +let collectionCache = null; + +export async function fetchCollections() { + if (collectionCache) return; + + await fetch(url) + .then((res) => { + if (!res.ok) throw new Error(`Error: ${res.status}`); + return res.json(); + }) + .then((data) => { + const cache = new Map(); + if (data.success && data.collections) { + for (const category of Object.values(data.collections)) { + if (!category.items) continue; + for (const [id, item] of Object.entries(category.items)) { + cache.set(id, id); + if (item.name) { + cache.set(item.name.toLowerCase(), id); + } + } + } + } + console.info(`Loaded ${cache.size} collection items.`); + collectionCache = cache; + }) + .catch((err) => { + console.error("Failed to fetch Hypixel collections:", err); + }); +} + +export const isCollectionItem = (name) => { + if (!collectionCache) return false; + return collectionCache.has(name) || collectionCache.has(name.toLowerCase()); +}; + +export const getCollectionId = (name) => { + if (!collectionCache) return null; + return collectionCache.get(name) || collectionCache.get(name.toLowerCase()); +}; \ No newline at end of file diff --git a/.github/scripts/utils/roman_numeral.mjs b/.github/scripts/utils/roman_numeral.mjs new file mode 100644 index 0000000..d4cb239 --- /dev/null +++ b/.github/scripts/utils/roman_numeral.mjs @@ -0,0 +1,16 @@ +export const romanToInt = (roman) => { + const romanMap = { I: 1, V: 5, X: 10, L: 50, C: 100, D: 500, M: 1000 }; + let total = 0; + let prevValue = 0; + + for (let i = roman.length - 1; i >= 0; i--) { + const currentValue = romanMap[roman[i]]; + if (currentValue < prevValue) { + total -= currentValue; + } else { + total += currentValue; + } + prevValue = currentValue; + } + return total; +}; \ No newline at end of file