import { AutocompleteInteraction } from "discord.js"; import { resolveUser } from "@systems/users"; import { getCharacters } from "@systems/characters"; import { Config } from "@systems/config"; import { Emoji } from "@systems/emojis"; import { CharacterRegistry } from "@registry/character-registry"; import { UserRegistry } from "@registry/user-registry"; import { Paths } from "@helpers/paths"; import { Nation } from "@types"; import { NATION_UNICODE } from "@systems/nations"; import { autocompleteLayout } from "@subcommands/tg-config/set-layout"; import { UpdatesCommands } from "@subcommands/admin/updates"; import { ResultCommands } from "@subcommands/admin/result-post"; import { SetResultLayoutCommands } from "@subcommands/tg-config/set-result-layout"; import { SetLeaderboardLayoutCommands } from "@subcommands/tg-config/set-leaderboard-layout"; import fs from "fs"; // ─── Usermap cache ──────────────────────────────────────────────────────────── let _usermapCache: Record | null = null; function getUsermapCache(): Record { if (!_usermapCache) { try { _usermapCache = JSON.parse(fs.readFileSync(Paths.data("usermap.json"), "utf8")); } catch { _usermapCache = {}; } } return _usermapCache!; } export function invalidateUsermapCache(): void { _usermapCache = null; } // ─── Autocomplete subsets ───────────────────────────────────────────────────── async function autocompleteCharNames( interaction: AutocompleteInteraction, focused: string, nation?: Nation | null // optional — if provided, filter by nation ): Promise { const member = await interaction.guild!.members.fetch(interaction.user.id); const user = await resolveUser(member); // For bringer set — scan all chars by nation, no user filter needed if (nation !== undefined) { const all = CharacterRegistry.all(); const results = all .filter((c) => !nation || c.nation === nation) .filter((c) => c.name.toLowerCase().includes(focused.toLowerCase())) .map((c) => { const nationEmoji = c.nation ? (NATION_UNICODE[c.nation] || c.nation) : ""; return { name: `${c.class} ${c.level} ${c.name} ${nationEmoji}`.trim(), value: c.name }; }) .slice(0, 25); return interaction.respond(results); } if (!user.userKey) return interaction.respond([]); // Own chars const ownChars = getCharacters(user.userKey).map((c) => { const nationEmoji = c.nation ? (Emoji.nation(c.nation) || c.nation) : ""; return { name: `${c.class} ${c.level} ${c.name} ${nationEmoji}`.trim(), value: c.name, }; }); // Shared chars const sharedChars = CharacterRegistry.sharedWith(user.userKey).map(({ char }) => { const nationEmoji = char.nation ? (Emoji.nation(char.nation) || char.nation) : ""; return { name: `${char.class} ${char.level} ${char.name} 🔗 ${nationEmoji}`.trim(), value: char.name, }; }); const all = [...ownChars, ...sharedChars] .filter((c) => c.name.toLowerCase().includes(focused.toLowerCase())) .slice(0, 25); await interaction.respond(all); } async function autocompleteUserKeys( interaction: AutocompleteInteraction, focused: string ): Promise { try { const usermap = getUsermapCache(); const choices = Object.entries(usermap) .map(([, entry]: [string, any]) => { const fileKey = typeof entry === "string" ? entry : entry.file; const alias = typeof entry === "object" ? (entry.aliases?.[0] ?? fileKey) : fileKey; return { name: `${alias} (${fileKey})`, value: fileKey }; }) .filter((c) => c.name.toLowerCase().includes(focused.toLowerCase())) .slice(0, 25); await interaction.respond(choices); } catch { await interaction.respond([]); } } async function autocompleteSlots( interaction: AutocompleteInteraction, focused: string ): Promise { const slots = Config.get({ section: "poll", key: "slots" }) .filter((s) => s.active) .map((s) => ({ name: `${s.tgHour}:00`, value: String(s.tgHour) })) .filter((s) => s.name.includes(focused)); await interaction.respond(slots); } // ─── Router ─────────────────────────────────────────────────────────────────── export async function handleAutocomplete(interaction: AutocompleteInteraction): Promise { try { const focused = interaction.options.getFocused(true); const optionName = focused.name; const focusedValue = focused.value as string; const sub = interaction.options.getSubcommand(false); const subGroup = interaction.options.getSubcommandGroup(false); if (optionName === "char_name") { // Bringer set — filter by selected nation if (sub === "set" && subGroup === "bringer") { const nation = interaction.options.getString("nation") as Nation | null; return await autocompleteCharNames(interaction, focusedValue, nation); } return await autocompleteCharNames(interaction, focusedValue); } if (optionName === "name") return await autocompleteUserKeys(interaction, focusedValue); if (optionName === "slot") return await autocompleteSlots(interaction, focusedValue); if (optionName === "owner") return await autocompleteUserKeys(interaction, focusedValue); if (optionName === "layout") return await autocompleteLayout(interaction); if (optionName === "version") return UpdatesCommands.autocomplete(interaction); if (optionName === "history_key") return await ResultCommands.autocompleteHistory(interaction); if (optionName === "week_key") return await ResultCommands.autocompleteWeekKey(interaction); if (optionName === "layout") { if (sub === "set-result-layout") return await SetResultLayoutCommands.autocomplete(interaction); if (sub === "set-leaderboard-layout") return await SetLeaderboardLayoutCommands.autocomplete(interaction); return await autocompleteLayout(interaction); // poll default } await interaction.respond([]); } catch (err) { console.error("[autocomplete] error:", err); try { await interaction.respond([]); } catch {} } }