const fs = require("fs"); const path = require("path"); const chokidar = require("chokidar"); class DatapackLoader { constructor() { this.registry = { items: new Map(), recipes: new Map(), skills: new Map(), dungeons: new Map(), enemies: new Map(), rooms: new Map(), languages: new Map(), manifest: {}, }; this.rootPath = null; } init(datapacksRoot, io = null) { this.rootPath = datapacksRoot || path.join(__dirname, "../../datapacks"); if (!fs.existsSync(this.rootPath)) return; this.loadAll(); if (io) this.watch(io); } deepMerge(target, source) { for (const key in source) { if (source[key] instanceof Object && key in target) { Object.assign(source[key], this.deepMerge(target[key], source[key])); } } Object.assign(target || {}, source); return target; } loadAll() { Object.values(this.registry).forEach((val) => { if (val instanceof Map) val.clear(); }); this.registry.manifest = {}; let manifestCount = 0; const packs = fs .readdirSync(this.rootPath, { withFileTypes: true }) .filter((d) => d.isDirectory()) .map((d) => d.name) .sort((a, b) => (a === "original" ? -1 : 1)); packs.forEach((packName) => { const packPath = path.join(this.rootPath, packName); const dataPath = path.join(packPath, "data"); const langPath = path.join(packPath, "assets/languages"); const manifestPath = path.join(packPath, "manifest.json"); if (fs.existsSync(manifestPath)) { try { const manifestContent = JSON.parse( fs.readFileSync(manifestPath, "utf8"), ); this.deepMerge(this.registry.manifest, manifestContent); manifestCount++; } catch (err) { console.error(`❌ Manifest Error [${packName}]:`, err.message); } } if (fs.existsSync(dataPath)) this.loadRecursive(dataPath, packName); if (fs.existsSync(langPath)) this.loadLanguages(langPath); }); console.log( `🚀 Registry Ready: ${this.registry.items.size} Items, ${this.registry.dungeons.size} Dungeons, ${this.registry.languages.size} Langs, ${manifestCount} Manifests`, ); } getRecipe(id) { return this.registry.recipes.get(id); } getRecipesByCategory(category) { const allRecipes = Array.from(this.registry.recipes.values()); return allRecipes.filter((r) => r.category === category); } getRecipeCategories() { const allRecipes = Array.from(this.registry.recipes.values()); const categories = new Set( allRecipes.map((r) => r.category).filter(Boolean), ); return Array.from(categories); } loadLanguages(langPath) { try { const files = fs.readdirSync(langPath).filter((f) => f.endsWith(".json")); files.forEach((file) => { const langCode = path.basename(file, ".json"); const fullPath = path.join(langPath, file); const newContent = JSON.parse(fs.readFileSync(fullPath, "utf8")); if (!this.registry.languages.has(langCode)) { this.registry.languages.set(langCode, {}); } const currentDict = this.registry.languages.get(langCode); this.deepMerge(currentDict, newContent); }); } catch (err) { console.error(`❌ Language Load Error:`, err.message); } } watch(io) { const watcher = chokidar.watch(this.rootPath, { persistent: true, ignoreInitial: true, }); watcher.on("all", (event, filePath) => { if (filePath.endsWith(".json")) { console.log(`🔄 Datapack change detected: ${path.basename(filePath)}`); this.loadAll(); io.emit( "admin:log", `System: Datapacks reloaded (${path.basename(filePath)})`, ); io.emit("system:data_updated", this.getStaticData()); } }); } loadRecursive(dirPath, packName) { const entries = fs.readdirSync(dirPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dirPath, entry.name); if (entry.isDirectory()) { this.loadRecursive(fullPath, packName); } else if (entry.isFile() && entry.name.endsWith(".json")) { this.parseFile(fullPath, packName); } } } parseFile(filePath, packName) { try { const fileContent = fs.readFileSync(filePath, "utf8"); const json = JSON.parse(fileContent); const typeKey = Object.keys(json)[0]; const data = json[typeKey]; if (!data) return; const fullId = `${packName}:${data.id}`; switch (typeKey) { case "armour": case "plating": case "materials": case "weapons": data.type = typeKey; this.registry.items.set(fullId, data); this.registry.items.set(data.id, data); break; case "recipe": const recipeId = json.craft?.id || data.id; this.registry.recipes.set(recipeId, { ...json.craft, ...data }); break; case "skills": this.registry.skills.set(fullId, data); this.registry.skills.set(data.id, data); break; case "dungeon": this.registry.dungeons.set(fullId, data); this.registry.dungeons.set(data.id, data); break; case "hostile": this.registry.enemies.set(fullId, data); this.registry.enemies.set(data.id, data); break; case "rooms": this.registry.rooms.set(fullId, data); this.registry.rooms.set(data.id, data); break; } } catch (err) { console.error(`❌ Parse Error [${filePath}]:`, err.message); } } getItem(id) { return this.registry.items.get(id); } getEnemy(id) { return this.registry.enemies.get(id); } getDungeon(id) { return this.registry.dungeons.get(id); } getRoom(id) { return this.registry.rooms.get(id); } getRecipes() { return Array.from(this.registry.recipes.values()); } getStaticData() { return { items: Array.from(this.registry.items.values()), recipes: Array.from(this.registry.recipes.values()), skills: Array.from(this.registry.skills.values()), dungeons: Array.from(this.registry.dungeons.values()), enemies: Array.from(this.registry.enemies.values()), rooms: Array.from(this.registry.rooms.values()), languages: Object.fromEntries(this.registry.languages), manifest: this.registry.manifest, }; } } module.exports = new DatapackLoader();