import { readFileSync, existsSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { z } from 'zod'; const configPath = join(import.meta.dir, '..', '..', 'config', 'config.json'); export interface GameConfigType { leveling: { base: number; exponent: number; chat: { cooldownMs: number; minXp: number; maxXp: number; } }, economy: { daily: { amount: bigint; streakBonus: bigint; weeklyBonus: bigint; cooldownMs: number; }, transfers: { allowSelfTransfer: boolean; minAmount: bigint; }, exam: { multMin: number; multMax: number; } }, inventory: { maxStackSize: bigint; maxSlots: number; }, commands: Record; lootdrop: { activityWindowMs: number; minMessages: number; spawnChance: number; cooldownMs: number; reward: { min: number; max: number; currency: string; } }; studentRole: string; visitorRole: string; colorRoles: string[]; welcomeChannelId?: string; welcomeMessage?: string; } // Initial default config state export const config: GameConfigType = {} as GameConfigType; const bigIntSchema = z.union([z.string(), z.number(), z.bigint()]) .refine((val) => { try { BigInt(val); return true; } catch { return false; } }, { message: "Must be a valid integer" }) .transform((val) => BigInt(val)); const configSchema = z.object({ leveling: z.object({ base: z.number(), exponent: z.number(), chat: z.object({ cooldownMs: z.number(), minXp: z.number(), maxXp: z.number(), }) }), economy: z.object({ daily: z.object({ amount: bigIntSchema, streakBonus: bigIntSchema, weeklyBonus: bigIntSchema.default(50n), cooldownMs: z.number(), }), transfers: z.object({ allowSelfTransfer: z.boolean(), minAmount: bigIntSchema, }), exam: z.object({ multMin: z.number(), multMax: z.number(), }) }), inventory: z.object({ maxStackSize: bigIntSchema, maxSlots: z.number(), }), commands: z.record(z.string(), z.boolean()), lootdrop: z.object({ activityWindowMs: z.number(), minMessages: z.number(), spawnChance: z.number(), cooldownMs: z.number(), reward: z.object({ min: z.number(), max: z.number(), currency: z.string(), }) }), studentRole: z.string(), visitorRole: z.string(), colorRoles: z.array(z.string()).default([]), welcomeChannelId: z.string().optional(), welcomeMessage: z.string().optional() }); export function reloadConfig() { if (!existsSync(configPath)) { throw new Error(`Config file not found at ${configPath}`); } const raw = readFileSync(configPath, 'utf-8'); const rawConfig = JSON.parse(raw); // Update config object in place // We use Object.assign to keep the reference to the exported 'config' object same const validatedConfig = configSchema.parse(rawConfig); Object.assign(config, validatedConfig); console.log("🔄 Config reloaded from disk."); } // Initial load reloadConfig(); // Backwards compatibility alias export const GameConfig = config; export function saveConfig(newConfig: unknown) { // Validate and transform input const validatedConfig = configSchema.parse(newConfig); const replacer = (key: string, value: any) => { if (typeof value === 'bigint') { return value.toString(); } return value; }; const jsonString = JSON.stringify(validatedConfig, replacer, 4); writeFileSync(configPath, jsonString, 'utf-8'); reloadConfig(); }