import { AudioPlayer, AudioPlayerState, AudioPlayerStatus, AudioResource, createAudioPlayer, createAudioResource, entersState, joinVoiceChannel, StreamType, VoiceConnection, VoiceConnectionStatus } from "@discordjs/voice"; import { Guild } from "discord.js"; import { log } from "./utils"; import { Readable } from "node:stream"; export type Hratelny = string | Readable | { src: string | Readable; volume: number; type?: StreamType; }; type Vojska = { conn: VoiceConnection; players: [AudioPlayer, AudioPlayer, AudioPlayer]; queues: [AudioResource[], AudioResource[], AudioResource[]]; }; export const enum Priority { Time, Etc, Music } const vojsy = new Map(); let lastId = 0; const getId = () => `Id:${lastId++}`; /** * @returns Předchozí stav bota, před tímto příkazem * `true` znamená, že byl bot do tohoto kanelu již připojen * `false` znamená, že nebyl připojen nikam * `channelId` znamená, že v tutý gildě byl předtím připojenej do kanelu s tímto ID */ export const novejJoin = (guild: Guild, channelId: string) => new Promise(async (resolve, reject) => { const guildId = guild.id; let prev: false | string = false; const gildina = vojsy.get(guildId); if (gildina) { const con = gildina.conn; if (con.joinConfig.channelId == channelId) { if (con.state.status != VoiceConnectionStatus.Ready) { log(`pri pripojovani do vojsu tady uz existovalo stejny pripojeni, ale status byl ${gildina.conn.state.status}, tak jsem to zahodil`); novejLeave(guildId); } else return resolve(true); } else { prev = con.joinConfig.channelId!; if (!prev) { log(new Error("fakt tam nic neni, jak je to mozny?")); prev = false; } /* Tuto prostě nefunguje, řekl Discord. Sráč jeden. Ale já se jen tak nevzdám. Jen počkej, ty čuráku. // Přesunout se const state = guild.voiceStates.cache.get(guild.client.user!.id); if (state) { await state.setChannel(channelId); resolve(prev); return; } */ // Z nějakýho neznámího důvodu se nepodařilo přepojit, prostě vytvoříme nový připojení // log(new Error("jo tak tuto nechapu")); novejLeave(guildId); } } const conn = joinVoiceChannel({ channelId: channelId, guildId, adapterCreator: guild.voiceAdapterCreator }); await entersState(conn, VoiceConnectionStatus.Ready, 1e4) .catch(e => { novejLeave(guildId); reject(e); }); vypocitatCas(guildId); conn.on(VoiceConnectionStatus.Disconnected, () => { // Je možný, že bota někdo "přetáhnul" do jinýho vojsu, v tom případě připojení recyklovat nechem entersState(conn, VoiceConnectionStatus.Ready, 5e3).catch(() => { // Taky je možný, že ho někdo fakt odpojil, ale hned znova připojil // V tom případě už by bylo o recyklaci postaráno if (conn.state.status != VoiceConnectionStatus.Disconnected) return; log("recykluju"); novejLeave(guildId); }); }); const player = createAudioPlayer(); conn.subscribe(player); vojsy.set(guildId, { conn, players: [createAudioPlayer(), createAudioPlayer(), createAudioPlayer()], queues: [[], [], []] }); resolve(prev); }); export function novejLeave(guildId: string) { const vojs = vojsy.get(guildId); if (!vojs) return false; clearTimeout(timeouty.get(guildId)); timeouty.delete(guildId); vojs.conn.destroy(); vojsy.delete(guildId); return true; } /** * Tahle funkce by se měla zavolat pokaždý, když dojde k nějaký změně v queue nebo něco dohraje. * Řeší hovna typu, přeregistrace jinýho přehrávače, pouštění dalšího resource, a to je asi všechno * @param vojska Vojska, o kterou se má postarat * @param priority Priorita, na kterou se má brát ičkon ohled, je to buďto priorita nově zařazenýho zvuku, nebo prostě toho, s nejvyšší prioritou */ function handelieren(vojska: Vojska, priority: Priority) { for (let i = 0; i < priority; i++) { if (vojska.players[i].state.status != AudioPlayerStatus.Idle) return; } const hrac = vojska.players[priority]; vojska.conn.subscribe(hrac); const resource = vojska.queues[priority].shift()!; hrac.play(resource); function nasafunkca(pred: AudioPlayerState, po: AudioPlayerState) { if (po.status != AudioPlayerStatus.Idle || pred.status != AudioPlayerStatus.Playing || pred.resource.metadata != resource.metadata) return; vojska?.players[priority].removeListener("stateChange", nasafunkca); let nejprioritnejsi = -1; for (let i = 0; i < 3; i++) { const hrac = vojska.players[i]; if (hrac.state.status == AudioPlayerStatus.AutoPaused) { vojska.conn.subscribe(hrac); return; } if (!vojska.queues[i].length) continue; nejprioritnejsi = i; break; } if (nejprioritnejsi != -1) handelieren(vojska, nejprioritnejsi); } vojska.players[priority].on("stateChange", nasafunkca); } function makeResource(co: Hratelny, id: string) { if (typeof co == "string" || co instanceof Readable) return createAudioResource(co, { metadata: id }); const res = createAudioResource(co.src, { inlineVolume: true, inputType: co.type || StreamType.Arbitrary, metadata: id }); res.volume?.setVolume(co.volume); return res; } function convert(resources: { id: string; res: AudioResource; }[]) { const odpoved: AudioResource[] = []; resources.forEach(res => { odpoved.push(res.res); }); return odpoved; } export const novejPlay = (guildId: string, co: Hratelny | Hratelny[], priority: Priority) => new Promise(async (resolve, reject) => { const vojska = vojsy.get(guildId); if (!vojska) return reject("nejsi pripojenej do vojsu debile"); const resources: { id: string; res: AudioResource; }[] = []; if (!Array.isArray(co)) { const id = getId(); resources.push({ id, res: makeResource(co, id) }); } else { co.forEach(c => { const id = getId(); resources.push({ id, res: makeResource(c, id) }); }); } // Tuto se možná může někdy hodit, třeba nikdy, kdovi // if (neprepisovat) { // vojska.queues[priority].push(resource); // } else { // vojska.queues[priority] = [resource]; // } vojska.queues[priority] = convert(resources); const posledniId = resources.at(-1)?.id; function nasafunkca(pred: AudioPlayerState, po: AudioPlayerState) { if (!(("resource" in pred && pred.resource.metadata == posledniId) && (("resource" in po && po.resource.metadata != posledniId) || !("resource" in po)))) return; vojska?.players[priority].removeListener("stateChange", nasafunkca); resolve(po); } vojska.players[priority].on("stateChange", nasafunkca); handelieren(vojska, priority); }); export const handlePrevVoice = (prev: boolean | string, guild: Guild) => { if (prev === true) return; // Byl jsem v tomdle vojsu if (prev === false) return novejLeave(guild.id); // Nebyl jsem ve vojsu vůbec novejJoin(guild, prev); // Byl jsem jinde }; export function stopPlayer(guildId: string, priorita: Priority) { const vojs = vojsy.get(guildId); if (!vojs) return false; vojs.players[priorita].stop(); return true; } export const getConn = (guildId: string) => vojsy.get(guildId)?.conn; // Ohlašování času const timeouty = new Map(); const nula = (a: number) => a < 10 ? `0${a}` : a; const vypocitatCas = (guildId: string) => { const d = new Date(); d.setMinutes((d.getMinutes() >= 30 ? 60 : 30)); d.setSeconds(0); timeouty.set(guildId, setTimeout(() => rekniCas(guildId, `${nula(d.getHours())}${nula(d.getMinutes())}`), Number(d) - Number(new Date()))); }; const rekniCas = (guildId: string, cas: string) => { novejPlay(guildId, [{ src: "zvuky/intro.mp3", volume: 0.8 }, { src: `zvuky/${cas}.mp3`, volume: 2 }, { src: "zvuky/grg.mp3", volume: 0.5 }], Priority.Time) .then(() => { vypocitatCas(guildId); }) .catch(err => { log("cas error:", err); }); };