forked from syntaxbullet/aurorabot
feat: implement database-backed game settings with a new schema, service, and migration script.
This commit is contained in:
@@ -66,66 +66,98 @@ export async function getGuildConfig(guildId: string): Promise<GuildConfig> {
|
||||
console.error("Failed to load guild config from database:", error);
|
||||
}
|
||||
|
||||
const fileConfig: GuildConfig = {
|
||||
studentRole: config.studentRole,
|
||||
visitorRole: config.visitorRole,
|
||||
colorRoles: config.colorRoles,
|
||||
welcomeChannelId: config.welcomeChannelId,
|
||||
welcomeMessage: config.welcomeMessage,
|
||||
feedbackChannelId: config.feedbackChannelId,
|
||||
terminal: config.terminal,
|
||||
moderation: config.moderation,
|
||||
return {
|
||||
studentRole: undefined,
|
||||
visitorRole: undefined,
|
||||
colorRoles: [],
|
||||
welcomeChannelId: undefined,
|
||||
welcomeMessage: undefined,
|
||||
feedbackChannelId: undefined,
|
||||
terminal: undefined,
|
||||
moderation: { cases: { dmOnWarn: true } },
|
||||
};
|
||||
|
||||
return fileConfig;
|
||||
}
|
||||
|
||||
export function invalidateGuildConfigCache(guildId: string) {
|
||||
guildConfigCache.delete(guildId);
|
||||
}
|
||||
|
||||
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<string, boolean>;
|
||||
lootdrop: {
|
||||
activityWindowMs: number;
|
||||
minMessages: number;
|
||||
spawnChance: number;
|
||||
export interface LevelingConfig {
|
||||
base: number;
|
||||
exponent: number;
|
||||
chat: {
|
||||
cooldownMs: number;
|
||||
reward: {
|
||||
min: number;
|
||||
max: number;
|
||||
currency: string;
|
||||
}
|
||||
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<string, boolean>;
|
||||
lootdrop: LootdropConfig;
|
||||
trivia: TriviaConfig;
|
||||
moderation: ModerationConfig;
|
||||
system: Record<string, unknown>;
|
||||
studentRole: string;
|
||||
visitorRole: string;
|
||||
colorRoles: string[];
|
||||
@@ -136,31 +168,8 @@ export interface GameConfigType {
|
||||
channelId: string;
|
||||
messageId: string;
|
||||
};
|
||||
moderation: {
|
||||
prune: {
|
||||
maxAmount: number;
|
||||
confirmThreshold: number;
|
||||
batchSize: number;
|
||||
batchDelayMs: number;
|
||||
};
|
||||
cases: {
|
||||
dmOnWarn: boolean;
|
||||
logChannelId?: string;
|
||||
autoTimeoutThreshold?: number;
|
||||
};
|
||||
};
|
||||
trivia: {
|
||||
entryFee: bigint;
|
||||
rewardMultiplier: number;
|
||||
timeoutSeconds: number;
|
||||
cooldownMs: number;
|
||||
categories: number[];
|
||||
difficulty: 'easy' | 'medium' | 'hard' | 'random';
|
||||
};
|
||||
system: Record<string, any>;
|
||||
}
|
||||
|
||||
// Initial default config state
|
||||
export const config: GameConfigType = {} as GameConfigType;
|
||||
|
||||
const bigIntSchema = z.union([z.string(), z.number(), z.bigint()])
|
||||
@@ -174,7 +183,7 @@ const bigIntSchema = z.union([z.string(), z.number(), z.bigint()])
|
||||
}, { message: "Must be a valid integer" })
|
||||
.transform((val) => BigInt(val));
|
||||
|
||||
const configSchema = z.object({
|
||||
const fileConfigSchema = z.object({
|
||||
leveling: z.object({
|
||||
base: z.number(),
|
||||
exponent: z.number(),
|
||||
@@ -215,7 +224,6 @@ const configSchema = z.object({
|
||||
max: z.number(),
|
||||
currency: z.string(),
|
||||
})
|
||||
|
||||
}),
|
||||
studentRole: z.string(),
|
||||
visitorRole: z.string(),
|
||||
@@ -268,44 +276,170 @@ const configSchema = z.object({
|
||||
system: z.record(z.string(), z.any()).default({}),
|
||||
});
|
||||
|
||||
export function reloadConfig() {
|
||||
type FileConfig = z.infer<typeof fileConfigSchema>;
|
||||
|
||||
function loadFromFile(): FileConfig | null {
|
||||
if (!existsSync(configPath)) {
|
||||
throw new Error(`Config file not found at ${configPath}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
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.");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Initial load
|
||||
reloadConfig();
|
||||
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<boolean> {
|
||||
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<void> {
|
||||
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).");
|
||||
}
|
||||
}
|
||||
|
||||
// Backwards compatibility alias
|
||||
export const GameConfig = config;
|
||||
|
||||
export function saveConfig(newConfig: unknown) {
|
||||
// Validate and transform input
|
||||
const validatedConfig = configSchema.parse(newConfig);
|
||||
|
||||
const validatedConfig = fileConfigSchema.parse(newConfig);
|
||||
const jsonString = JSON.stringify(validatedConfig, jsonReplacer, 4);
|
||||
writeFileSync(configPath, jsonString, 'utf-8');
|
||||
reloadConfig();
|
||||
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 = {
|
||||
...config,
|
||||
...fileConfig,
|
||||
commands: {
|
||||
...config.commands,
|
||||
...fileConfig.commands,
|
||||
[commandName]: enabled
|
||||
}
|
||||
};
|
||||
saveConfig(newConfig);
|
||||
}
|
||||
|
||||
export async function initializeConfig(): Promise<void> {
|
||||
loadFileSync();
|
||||
await reloadConfig();
|
||||
}
|
||||
|
||||
loadFileSync();
|
||||
|
||||
Reference in New Issue
Block a user