254 lines
7.7 KiB
TypeScript
254 lines
7.7 KiB
TypeScript
import { AudioPlayer, AudioPlayerState, AudioPlayerStatus, AudioResource, createAudioPlayer, createAudioResource, entersState, joinVoiceChannel, StreamType, VoiceConnection, VoiceConnectionStatus } from "@discordjs/voice";
|
|
import { Guild } from "discord.js";
|
|
import { adminLog, 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<string, Vojska>();
|
|
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<boolean | string>(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) {
|
|
adminLog(guild.client, "fakt tam nic neni, jak je to mozny?");
|
|
prev = false;
|
|
}
|
|
|
|
// Přesunout se
|
|
const state = guild.voiceStates.cache.get(guild.client.user!.id);
|
|
if (state) {
|
|
try {
|
|
await state.setChannel(channelId);
|
|
resolve(prev);
|
|
return;
|
|
} catch { }
|
|
}
|
|
|
|
// Z nějakýho neznámýho důvodu se nepodařilo přepojit, prostě vytvoříme nový připojení
|
|
log(new Error("toxic discord back at it, prej"));
|
|
novejLeave(guildId);
|
|
}
|
|
}
|
|
|
|
const conn = joinVoiceChannel({
|
|
channelId: channelId,
|
|
guildId,
|
|
//@ts-ignore PIČOVINA ZASRATA!"!"!"!"!"!"!""""
|
|
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);
|
|
});
|
|
});
|
|
|
|
vojsy.set(guildId, {
|
|
conn,
|
|
players: Array(3).fill(0).map(() => createAudioPlayer().on("error", e => log("chyba v nějakým hráčovi", e))) as [AudioPlayer, AudioPlayer, AudioPlayer],
|
|
queues: [[], [], []]
|
|
});
|
|
|
|
resolve(prev);
|
|
});
|
|
|
|
export function novejLeave(guildId: string) {
|
|
const vojs = vojsy.get(guildId);
|
|
if (!vojs) return false;
|
|
|
|
clearTimeout(timeouty.get(guildId));
|
|
timeouty.delete(guildId);
|
|
|
|
if (vojs.conn.state.status != VoiceConnectionStatus.Destroyed)
|
|
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<AudioPlayerState>(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<string, NodeJS.Timeout>();
|
|
|
|
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); });
|
|
};
|