API/game-server/src/game/DatapackLoader.js
2026-04-21 08:48:52 +03:00

234 lines
6.6 KiB
JavaScript

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(),
quests: 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.quests.size} Quests, ${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 = `${data.id}`;
switch (typeKey) {
case "armour":
case "plating":
case "materials":
case "weapons":
data.type = typeKey;
this.registry.items.set(fullId, 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);
break;
case "dungeon":
this.registry.dungeons.set(fullId, data);
break;
case "hostile":
this.registry.enemies.set(fullId, data);
break;
case "rooms":
this.registry.rooms.set(fullId, data);
break;
case "quest":
this.registry.quests.set(fullId, 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);
}
getQuest(id) {
return this.registry.quests.get(id);
}
getAutoStartQuests() {
return Array.from(this.registry.quests.values()).filter(
(q) => q.meta?.autoAccept,
);
}
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()),
quests: Array.from(this.registry.quests.values()),
languages: Object.fromEntries(this.registry.languages),
manifest: this.registry.manifest,
};
}
}
module.exports = new DatapackLoader();