Files

127 lines
6.6 KiB
JavaScript
Raw Permalink Normal View History

"use strict";
const fs = require("fs");
const path = require("path");
const projectRoot = path.resolve(process.env.Ananta_ROOT || path.resolve(__dirname, "..", ".."));
const configPath = path.resolve(process.env.Ananta_CONFIG || path.join(projectRoot, "config", "private-server.json"));
function fail(message) {
throw new Error(`[CONFIG] ${message} (${configPath})`);
}
function requireObject(value, name) {
if (!value || typeof value !== "object" || Array.isArray(value)) fail(`missing or invalid section '${name}'`);
return value;
}
function requireText(value, name) {
if (typeof value !== "string" || value.trim() === "") fail(`${name} must not be empty`);
}
function requirePositiveInteger(value, name) {
if (!Number.isSafeInteger(value) || value <= 0) fail(`${name} must be a positive safe integer`);
}
function requirePort(value, name) {
if (!Number.isInteger(value) || value < 1 || value > 65535) fail(`${name} must be 1..65535, got '${value}'`);
}
function readConfig() {
if (!fs.existsSync(configPath)) fail("private-server.json not found");
let value;
try {
value = JSON.parse(fs.readFileSync(configPath, "utf8").replace(/^\uFEFF/, ""));
} catch (error) {
fail(`invalid JSON: ${error.message}`);
}
const client = requireObject(value.client, "client");
const network = requireObject(value.network, "network");
const proxy = requireObject(network.proxy, "network.proxy");
const player = requireObject(value.player, "player");
const world = requireObject(value.world, "world");
const content = requireObject(value.content, "content");
const roster = requireObject(content.roster, "content.roster");
const gameplay = requireObject(value.gameplay, "gameplay");
const combat = requireObject(gameplay.combat, "gameplay.combat");
const skills = requireObject(combat.skills, "gameplay.combat.skills");
const web = requireObject(gameplay.webTraversal, "gameplay.webTraversal");
const ui = requireObject(value.ui, "ui");
const paths = requireObject(value.paths, "paths");
requirePositiveInteger(client.version, "client.version");
const supportedClientVersions = [4229938];
if (!supportedClientVersions.includes(client.version))
fail(`client.version=${client.version} is not supported by this source tree; expected one of ${supportedClientVersions.join(", ")}`);
requirePositiveInteger(client.serverId, "client.serverId");
requireText(client.rpcMd5, "client.rpcMd5");
requireText(network.bindHost, "network.bindHost");
requireText(network.advertisedHost, "network.advertisedHost");
if (!Array.isArray(network.loginPorts) || network.loginPorts.length !== 2)
fail("network.loginPorts must contain exactly two ports");
network.loginPorts.forEach((port, i) => requirePort(port, `network.loginPorts[${i}]`));
requirePort(network.gamePort, "network.gamePort");
for (const name of ["httpsPort", "httpPort", "loginListPort", "loginTcpPort", "gameTcpPort", "sceneSubPort"])
requirePort(proxy[name], `network.proxy.${name}`);
requireText(proxy.updateHost, "network.proxy.updateHost");
requireText(proxy.certificatePassphrase, "network.proxy.certificatePassphrase");
requirePositiveInteger(player.pid, "player.pid");
requirePositiveInteger(player.initialUnitId, "player.initialUnitId");
requirePositiveInteger(player.initialSpiritTemplateId, "player.initialSpiritTemplateId");
for (const name of ["accountId", "userName", "displayName", "loginToken", "gameToken", "shareToken", "fpPassToken", "skey"])
requireText(player[name], `player.${name}`);
requirePositiveInteger(world.raidId, "world.raidId");
requirePositiveInteger(world.sceneInstanceId, "world.sceneInstanceId");
requirePositiveInteger(world.universeId, "world.universeId");
const spawn = requireObject(world.spawn, "world.spawn");
for (const axis of ["x", "y", "z"]) if (!Number.isFinite(spawn[axis])) fail(`world.spawn.${axis} must be a finite number`);
requirePositiveInteger(roster.templateIdMin, "content.roster.templateIdMin");
requirePositiveInteger(roster.templateIdMaxExclusive, "content.roster.templateIdMaxExclusive");
if (roster.templateIdMin >= roster.templateIdMaxExclusive)
fail("content.roster.templateIdMin must be less than templateIdMaxExclusive");
requirePositiveInteger(roster.unitIdBase, "content.roster.unitIdBase");
if (!Number.isFinite(combat.maxHp) || combat.maxHp <= 0) fail("gameplay.combat.maxHp must be positive");
const skillNames = ["common", "pressCommon", "heavyCommon", "dodge", "dodgeAttack", "grappleAttack", "active", "unique"];
for (const name of skillNames) requirePositiveInteger(skills[name], `gameplay.combat.skills.${name}`);
if (!Array.isArray(combat.charges) || combat.charges.length === 0) fail("gameplay.combat.charges must not be empty");
const chargeNames = new Set(skillNames.map(x => x.toLowerCase()));
for (const [i, charge] of combat.charges.entries()) {
if (!charge || typeof charge !== "object") fail(`gameplay.combat.charges[${i}] must be an object`);
if (typeof charge.skill !== "string" || !chargeNames.has(charge.skill.toLowerCase()))
fail(`gameplay.combat.charges[${i}].skill is unknown: '${charge.skill}'`);
if (!Number.isInteger(charge.max) || charge.max <= 0 || !Number.isInteger(charge.current) || charge.current < 0 || charge.current > charge.max)
fail(`gameplay.combat.charges[${i}] has invalid current/max`);
if (!Number.isFinite(charge.period) || charge.period < 0) fail(`gameplay.combat.charges[${i}].period must be >= 0`);
}
if (!Array.isArray(web.sharedBuffIds) || web.sharedBuffIds.length === 0) fail("gameplay.webTraversal.sharedBuffIds must not be empty");
requirePositiveInteger(web.persistentGrappleBuffId, "gameplay.webTraversal.persistentGrappleBuffId");
if (!web.sharedBuffIds.includes(web.persistentGrappleBuffId))
fail("gameplay.webTraversal.persistentGrappleBuffId must also be in sharedBuffIds");
const uidLabel = requireObject(ui.uidLabel, "ui.uidLabel");
if (typeof uidLabel.enabled !== "boolean") fail("ui.uidLabel.enabled must be boolean");
if (uidLabel.enabled) requireText(uidLabel.text, "ui.uidLabel.text");
if (typeof ui.removeStockConfidentialLabel !== "boolean") fail("ui.removeStockConfidentialLabel must be boolean");
requireText(paths.clientConfigs, "paths.clientConfigs");
requireText(paths.runtimeFastpatch, "paths.runtimeFastpatch");
return value;
}
const config = readConfig();
function resolveProjectPath(configuredPath) {
if (!configuredPath) return projectRoot;
return path.isAbsolute(configuredPath)
? path.normalize(configuredPath)
: path.resolve(projectRoot, configuredPath);
}
module.exports = { config, configPath, projectRoot, resolveProjectPath };