import { jsonReplacer } from './utils'; 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 GuildConfig { studentRole?: string; visitorRole?: string; colorRoles: string[]; welcomeChannelId?: string; welcomeMessage?: string; feedbackChannelId?: string; terminal?: { channelId: string; messageId: string; }; moderation: { cases: { dmOnWarn: boolean; logChannelId?: string; autoTimeoutThreshold?: number; }; }; } const guildConfigCache = new Map(); const CACHE_TTL_MS = 60000; export async function getGuildConfig(guildId: string): Promise { const cached = guildConfigCache.get(guildId); if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) { return cached.config; } try { const { guildSettingsService } = await import('@shared/modules/guild-settings/guild-settings.service'); const dbSettings = await guildSettingsService.getSettings(guildId); if (dbSettings) { const config: GuildConfig = { studentRole: dbSettings.studentRoleId, visitorRole: dbSettings.visitorRoleId, colorRoles: dbSettings.colorRoleIds ?? [], welcomeChannelId: dbSettings.welcomeChannelId, welcomeMessage: dbSettings.welcomeMessage, feedbackChannelId: dbSettings.feedbackChannelId, terminal: dbSettings.terminalChannelId ? { channelId: dbSettings.terminalChannelId, messageId: dbSettings.terminalMessageId ?? "", } : undefined, moderation: { cases: { dmOnWarn: dbSettings.moderationDmOnWarn ?? true, logChannelId: dbSettings.moderationLogChannelId, autoTimeoutThreshold: dbSettings.moderationAutoTimeoutThreshold, }, }, }; guildConfigCache.set(guildId, { config, timestamp: Date.now() }); return config; } } catch (error) { console.error("Failed to load guild config from database:", error); } return { studentRole: undefined, visitorRole: undefined, colorRoles: [], welcomeChannelId: undefined, welcomeMessage: undefined, feedbackChannelId: undefined, terminal: undefined, moderation: { cases: { dmOnWarn: true } }, }; } export function invalidateGuildConfigCache(guildId: string) { guildConfigCache.delete(guildId); } export interface LevelingConfig { base: number; exponent: number; chat: { cooldownMs: number; minXp: number; maxXp: number; }; } export interface EconomyConfig { daily: { amount: bigint; streakBonus: bigint; weeklyBonus: bigint; cooldownMs: number; }; transfers: { allowSelfTransfer: boolean; minAmount: bigint; }; exam: { multMin: number; multMax: number; }; } export interface InventoryConfig { maxStackSize: bigint; maxSlots: number; } export interface LootdropConfig { activityWindowMs: number; minMessages: number; spawnChance: number; cooldownMs: number; reward: { min: number; max: number; currency: string; }; } export interface TriviaConfig { entryFee: bigint; rewardMultiplier: number; timeoutSeconds: number; cooldownMs: number; categories: number[]; difficulty: 'easy' | 'medium' | 'hard' | 'random'; } export interface ModerationConfig { prune: { maxAmount: number; confirmThreshold: number; batchSize: number; batchDelayMs: number; }; cases: { dmOnWarn: boolean; logChannelId?: string; autoTimeoutThreshold?: number; }; } export interface GameConfigType { leveling: LevelingConfig; economy: EconomyConfig; inventory: InventoryConfig; commands: Record; lootdrop: LootdropConfig; trivia: TriviaConfig; moderation: ModerationConfig; system: Record; studentRole: string; visitorRole: string; colorRoles: string[]; welcomeChannelId?: string; welcomeMessage?: string; feedbackChannelId?: string; terminal?: { channelId: string; messageId: string; }; } 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 fileConfigSchema = 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(), feedbackChannelId: z.string().optional(), terminal: z.object({ channelId: z.string(), messageId: z.string() }).optional(), moderation: z.object({ prune: z.object({ maxAmount: z.number().default(100), confirmThreshold: z.number().default(50), batchSize: z.number().default(100), batchDelayMs: z.number().default(1000) }), cases: z.object({ dmOnWarn: z.boolean().default(true), logChannelId: z.string().optional(), autoTimeoutThreshold: z.number().optional() }) }).default({ prune: { maxAmount: 100, confirmThreshold: 50, batchSize: 100, batchDelayMs: 1000 }, cases: { dmOnWarn: true } }), trivia: z.object({ entryFee: bigIntSchema, rewardMultiplier: z.number().min(0).max(10), timeoutSeconds: z.number().min(5).max(300), cooldownMs: z.number().min(0), categories: z.array(z.number()).default([]), difficulty: z.enum(['easy', 'medium', 'hard', 'random']).default('random'), }).default({ entryFee: 50n, rewardMultiplier: 1.8, timeoutSeconds: 30, cooldownMs: 60000, categories: [], difficulty: 'random' }), system: z.record(z.string(), z.any()).default({}), }); type FileConfig = z.infer; function loadFromFile(): FileConfig | null { if (!existsSync(configPath)) { return null; } try { const raw = readFileSync(configPath, 'utf-8'); const rawConfig = JSON.parse(raw); return fileConfigSchema.parse(rawConfig); } catch (error) { console.error("Failed to load config from file:", error); return null; } } function applyFileConfig(fileConfig: FileConfig) { Object.assign(config, { leveling: fileConfig.leveling, economy: fileConfig.economy, inventory: fileConfig.inventory, commands: fileConfig.commands, lootdrop: fileConfig.lootdrop, trivia: fileConfig.trivia, moderation: fileConfig.moderation, system: fileConfig.system, studentRole: fileConfig.studentRole, visitorRole: fileConfig.visitorRole, colorRoles: fileConfig.colorRoles, welcomeChannelId: fileConfig.welcomeChannelId, welcomeMessage: fileConfig.welcomeMessage, feedbackChannelId: fileConfig.feedbackChannelId, terminal: fileConfig.terminal, }); } async function loadFromDatabase(): Promise { try { const { gameSettingsService } = await import('@shared/modules/game-settings/game-settings.service'); const dbSettings = await gameSettingsService.getSettings(); if (dbSettings) { Object.assign(config, { leveling: { ...dbSettings.leveling, }, economy: { daily: { ...dbSettings.economy.daily, amount: BigInt(dbSettings.economy.daily.amount), streakBonus: BigInt(dbSettings.economy.daily.streakBonus), weeklyBonus: BigInt(dbSettings.economy.daily.weeklyBonus), }, transfers: { ...dbSettings.economy.transfers, minAmount: BigInt(dbSettings.economy.transfers.minAmount), }, exam: dbSettings.economy.exam, }, inventory: { ...dbSettings.inventory, maxStackSize: BigInt(dbSettings.inventory.maxStackSize), }, commands: dbSettings.commands, lootdrop: dbSettings.lootdrop, trivia: { ...dbSettings.trivia, entryFee: BigInt(dbSettings.trivia.entryFee), }, moderation: dbSettings.moderation, system: dbSettings.system, }); console.log("🎮 Game config loaded from database."); return true; } } catch (error) { console.error("Failed to load game config from database:", error); } return false; } export async function reloadConfig(): Promise { const dbLoaded = await loadFromDatabase(); if (!dbLoaded) { const fileConfig = loadFromFile(); if (fileConfig) { applyFileConfig(fileConfig); console.log("📄 Game config loaded from file (database not available)."); } else { console.warn("⚠️ No game config found in database or file. Using defaults."); const { gameSettingsService } = await import('@shared/modules/game-settings/game-settings.service'); const defaults = gameSettingsService.getDefaults(); Object.assign(config, { leveling: defaults.leveling, economy: { ...defaults.economy, daily: { ...defaults.economy.daily, amount: BigInt(defaults.economy.daily.amount), streakBonus: BigInt(defaults.economy.daily.streakBonus), weeklyBonus: BigInt(defaults.economy.daily.weeklyBonus), }, transfers: { ...defaults.economy.transfers, minAmount: BigInt(defaults.economy.transfers.minAmount), }, }, inventory: { ...defaults.inventory, maxStackSize: BigInt(defaults.inventory.maxStackSize), }, commands: defaults.commands, lootdrop: defaults.lootdrop, trivia: { ...defaults.trivia, entryFee: BigInt(defaults.trivia.entryFee), }, moderation: defaults.moderation, system: defaults.system, }); } } } export function loadFileSync(): void { const fileConfig = loadFromFile(); if (fileConfig) { applyFileConfig(fileConfig); console.log("📄 Game config loaded from file (sync)."); } } export const GameConfig = config; export function saveConfig(newConfig: unknown) { const validatedConfig = fileConfigSchema.parse(newConfig); const jsonString = JSON.stringify(validatedConfig, jsonReplacer, 4); writeFileSync(configPath, jsonString, 'utf-8'); applyFileConfig(validatedConfig); console.log("🔄 Config saved to file."); } export function toggleCommand(commandName: string, enabled: boolean) { const fileConfig = loadFromFile(); if (!fileConfig) { console.error("Cannot toggle command: no file config available"); return; } const newConfig = { ...fileConfig, commands: { ...fileConfig.commands, [commandName]: enabled } }; saveConfig(newConfig); } export async function initializeConfig(): Promise { loadFileSync(); await reloadConfig(); } loadFileSync();