diff --git a/src/equicordplugins/animalese/index.ts b/src/equicordplugins/animalese/index.ts new file mode 100644 index 000000000..265c2e0d6 --- /dev/null +++ b/src/equicordplugins/animalese/index.ts @@ -0,0 +1,243 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { definePluginSettings } from "@api/Settings"; +import { EquicordDevs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { SelectedChannelStore, UserStore } from "@webpack/common"; + +const settings = definePluginSettings({ + volume: { + type: OptionType.SLIDER, + description: "Volume of the animalese sound", + default: 0.5, + markers: [0, 0.1, 0.25, 0.5, 0.6, 0.75, 1], + }, + speed: { + type: OptionType.SLIDER, + description: "Speed of the animalese sound", + default: 1, + markers: [0.5, 0.75, 1, 1.25, 1.5], + }, + pitch: { + type: OptionType.SLIDER, + description: "Pitch multiplier", + default: 1, + markers: [0.75, 0.8, 0.85, 1, 1.15, 1.25, 1.35, 1.5], + }, + messageLengthLimit: { + type: OptionType.NUMBER, + description: "Maximum length of message to process", + default: 50, + }, + processOwnMessages: { + type: OptionType.BOOLEAN, + description: "Enable to yap your own messages too", + default: true, + }, + soundQuality: { + type: OptionType.SELECT, + description: "Quality of sound to use", + options: [ + { + label: "High", + value: "high", + default: true + }, + { + label: "Medium", + value: "med" + }, + { + label: "Low", + value: "low" + }, + { + label: "Lowest", + value: "low" + } + ] + } +}); + +let audioContext: AudioContext | null = null; + +// better than my old hardcoded garbage +const highSounds = Array.from( + { length: 30 }, + (_, i) => `sound${String(i + 1).padStart(2, "0")}.wav` +); +const soundBuffers: Record = {}; + +const BASE_URL_HIGH = "https://raw.githubusercontent.com/Equicord/Equibored/main/sounds/animalese"; + +async function initSoundBuffers() { + if (!audioContext) audioContext = new AudioContext(); + const quality = settings.store.soundQuality; + for (const file of highSounds) { + const nameWithoutExt = file.replace(".wav", ""); + soundBuffers[nameWithoutExt] = await loadSound( + `${BASE_URL_HIGH}/${quality}/${file}` + ); + } +} + +async function loadSound(url: string): Promise { + if (!audioContext) audioContext = new AudioContext(); + const response = await fetch(url); + if (!response.ok) throw new Error("Network response was not OK"); + const arrayBuffer = await response.arrayBuffer(); + return audioContext.decodeAudioData(arrayBuffer); +} + +async function generateAnimalese(text: string): Promise { + if (!audioContext) audioContext = new AudioContext(); + + const soundIndices: string[] = []; + const text_lower = text.toLowerCase(); + + for (let i = 0; i < text_lower.length; i++) { + const char = text_lower[i]; + if (char === "s" && text_lower[i + 1] === "h") { + soundIndices.push("sound27"); + i++; + } else if (char === "t" && text_lower[i + 1] === "h") { + soundIndices.push("sound28"); + i++; + } else if ( + char === "h" && + (text_lower[i - 1] === "s" || text_lower[i - 1] === "t") + ) { + continue; + } else if (char === "," || char === "?") { + soundIndices.push("sound30"); + } else if (char === text_lower[i - 1]) { + continue; + } else if (char.match(/[a-z]/)) { + const index = char.charCodeAt(0) - 96; + soundIndices.push(`sound${String(index).padStart(2, "0")}`); + } + } + + // No valid characters? Just return null + if (soundIndices.length === 0) { + return null; + } + + const totalDuration = soundIndices.length * 0.1; + const frameCount = Math.max(1, Math.floor(audioContext.sampleRate * totalDuration)); + + const outputBuffer = audioContext.createBuffer( + 1, + frameCount, + audioContext.sampleRate + ); + const outputData = outputBuffer.getChannelData(0); + + let offset = 0; + const baseLetterDuration = audioContext.sampleRate * (0.09 / settings.store.speed); + // ~90ms per letter at 1x speed (tweakable!) + + for (let i = 0; i < soundIndices.length; i++) { + const buffer = soundBuffers[soundIndices[i]]; + if (!buffer) continue; + + const variation = 0.15; + let pitchShift = (2.8 * settings.store.pitch) + (Math.random() * variation); + + const isQuestion = text_lower.endsWith("?"); + if (isQuestion && i >= soundIndices.length * 0.8) { + const progress = + (i - soundIndices.length * 0.8) / (soundIndices.length * 0.2); + pitchShift += progress * 0.1 + 0.1; + } + + const inputData = buffer.getChannelData(0); + const inputLength = inputData.length; + const outputLength = Math.floor(inputLength / pitchShift); + + // copy sound into the slot + for (let j = 0; j < outputLength; j++) { + const inputIndex = Math.floor(j * pitchShift); + const targetIndex = offset + j; + if (inputIndex < inputLength && targetIndex < outputData.length) { + outputData[targetIndex] = inputData[inputIndex]; + } + } + + // instead of stacking lengths, jump forward a *fixed slot* per character + offset += Math.floor(baseLetterDuration); + } + + return outputBuffer; +} + +async function playSound(buffer: AudioBuffer, volume: number) { + if (!audioContext) audioContext = new AudioContext(); + const source = audioContext.createBufferSource(); + const gainNode = audioContext.createGain(); + + source.buffer = buffer; + source.playbackRate.value = settings.store.pitch; + gainNode.gain.value = volume; + + source.connect(gainNode); + gainNode.connect(audioContext.destination); + + source.start(); +} + +export default definePlugin({ + name: "Animalese", + description: "Plays animal crossing animalese for every message sent (they yap a lot)", + authors: [EquicordDevs.ryanamay, EquicordDevs.Mocha], + settings, + + flux: { + async MESSAGE_CREATE({ optimistic, type, message, channelId }) { + if (optimistic || type !== "MESSAGE_CREATE") return; + if (message.state === "SENDING") return; + if (!message.content || message.author?.bot || channelId !== SelectedChannelStore.getChannelId()) return; + + const urlPattern = /https?:\/\/[^\s]+/; + const maxLength = settings.store.messageLengthLimit || 100; + const processOwnMessages = settings.store.processOwnMessages ?? true; + + if ( + urlPattern.test(message.content) + || message.content.length > maxLength + || !processOwnMessages + && String(message.author.id) === String(UserStore.getCurrentUser().id) + ) return; + + try { + const buffer = await generateAnimalese(message.content); + if (buffer) await playSound(buffer, settings.store.volume); + } catch (err) { + console.error("[Animalese]", err); + } + } + }, + + start() { + // Only subscribe once! + const init = async () => { + if (!audioContext) { + audioContext = new AudioContext(); + await initSoundBuffers(); + } + init(); + }; + init(); + }, + + stop() { + if (audioContext) { + audioContext.close(); + audioContext = null; + } + }, +}); diff --git a/src/equicordplugins/equissant/README.md b/src/equicordplugins/equissant/README.md new file mode 100644 index 000000000..c83a66e11 --- /dev/null +++ b/src/equicordplugins/equissant/README.md @@ -0,0 +1,3 @@ +# Equissant + +Plays crossant every specified amount of clicks diff --git a/src/equicordplugins/moyai/index.ts b/src/equicordplugins/moyai/index.ts new file mode 100644 index 000000000..2a5ad52d1 --- /dev/null +++ b/src/equicordplugins/moyai/index.ts @@ -0,0 +1,174 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { definePluginSettings } from "@api/Settings"; +import { Devs } from "@utils/constants"; +import { sleep } from "@utils/misc"; +import definePlugin, { makeRange, OptionType } from "@utils/types"; +import { Message, ReactionEmoji } from "@vencord/discord-types"; +import { RelationshipStore, SelectedChannelStore, UserStore } from "@webpack/common"; + +interface IMessageCreate { + type: "MESSAGE_CREATE"; + optimistic: boolean; + isPushNotification: boolean; + channelId: string; + message: Message; +} + +interface IReactionAdd { + type: "MESSAGE_REACTION_ADD"; + optimistic: boolean; + channelId: string; + messageId: string; + messageAuthorId: string; + userId: "195136840355807232"; + emoji: ReactionEmoji; +} + +interface IVoiceChannelEffectSendEvent { + type: string; + emoji?: ReactionEmoji; // Just in case... + channelId: string; + userId: string; + animationType: number; + animationId: number; +} + +const MOYAI = "🗿"; +const MOYAI_URL = "https://github.com/Equicord/Equibored/raw/main/sounds/moyai/moyai.mp3"; +const MOYAI_URL_HD = "https://github.com/Equicord/Equibored/raw/main/sounds/moyai/moyai.wav"; + +const settings = definePluginSettings({ + volume: { + description: "Volume of the 🗿🗿🗿", + type: OptionType.SLIDER, + markers: makeRange(0, 1, 0.1), + default: 0.5, + stickToMarkers: false + }, + quality: { + description: "Quality of the 🗿🗿🗿", + type: OptionType.SELECT, + options: [ + { label: "Normal", value: "Normal", default: true }, + { label: "HD", value: "HD" } + ], + }, + triggerWhenUnfocused: { + description: "Trigger the 🗿 even when the window is unfocused", + type: OptionType.BOOLEAN, + default: true + }, + ignoreBots: { + description: "Ignore bots", + type: OptionType.BOOLEAN, + default: true + }, + ignoreBlocked: { + description: "Ignore blocked users", + type: OptionType.BOOLEAN, + default: true + } +}); + +export default definePlugin({ + name: "Moyai", + authors: [Devs.Megu, Devs.Nuckyz], + description: "🗿🗿🗿🗿🗿🗿🗿🗿", + settings, + + flux: { + async MESSAGE_CREATE({ optimistic, type, message, channelId }: IMessageCreate) { + if (optimistic || type !== "MESSAGE_CREATE") return; + if (message.state === "SENDING") return; + if (settings.store.ignoreBots && message.author?.bot) return; + if (settings.store.ignoreBlocked && RelationshipStore.isBlocked(message.author?.id)) return; + if (!message.content) return; + if (channelId !== SelectedChannelStore.getChannelId()) return; + + const moyaiCount = getMoyaiCount(message.content); + + for (let i = 0; i < moyaiCount; i++) { + boom(); + await sleep(300); + } + }, + + MESSAGE_REACTION_ADD({ optimistic, type, channelId, userId, messageAuthorId, emoji }: IReactionAdd) { + if (optimistic || type !== "MESSAGE_REACTION_ADD") return; + if (settings.store.ignoreBots && UserStore.getUser(userId)?.bot) return; + if (settings.store.ignoreBlocked && RelationshipStore.isBlocked(messageAuthorId)) return; + if (channelId !== SelectedChannelStore.getChannelId()) return; + + const name = emoji.name.toLowerCase(); + if (name !== MOYAI && !name.includes("moyai") && !name.includes("moai")) return; + + boom(); + }, + + VOICE_CHANNEL_EFFECT_SEND({ emoji }: IVoiceChannelEffectSendEvent) { + if (!emoji?.name) return; + const name = emoji.name.toLowerCase(); + if (name !== MOYAI && !name.includes("moyai") && !name.includes("moai")) return; + + boom(); + } + } +}); + +function countOccurrences(sourceString: string, subString: string) { + let i = 0; + let lastIdx = 0; + while ((lastIdx = sourceString.indexOf(subString, lastIdx) + 1) !== 0) + i++; + + return i; +} + +function countMatches(sourceString: string, pattern: RegExp) { + if (!pattern.global) + throw new Error("pattern must be global"); + + let i = 0; + while (pattern.test(sourceString)) + i++; + + return i; +} + +const customMoyaiRe = //gi; + +function getMoyaiCount(message: string) { + const count = countOccurrences(message, MOYAI) + + countMatches(message, customMoyaiRe); + + return Math.min(count, 10); +} + +function boom() { + if (!settings.store.triggerWhenUnfocused && !document.hasFocus()) return; + const audioElement = document.createElement("audio"); + + audioElement.src = settings.store.quality === "HD" + ? MOYAI_URL_HD + : MOYAI_URL; + + audioElement.volume = settings.store.volume; + audioElement.play(); +} diff --git a/src/equicordplugins/remix/index.tsx b/src/equicordplugins/remix/index.tsx index c45a34b2c..cec2b8f2d 100644 --- a/src/equicordplugins/remix/index.tsx +++ b/src/equicordplugins/remix/index.tsx @@ -6,7 +6,7 @@ import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; import { addMessagePreSendListener, MessageSendListener, removeMessagePreSendListener } from "@api/MessageEvents"; -import { definePluginSettings, Settings } from "@api/Settings"; +import { definePluginSettings } from "@api/Settings"; import { disableStyle, enableStyle } from "@api/Styles"; import { PaintbrushIcon } from "@components/Icons"; import { EquicordDevs } from "@utils/constants"; @@ -133,10 +133,6 @@ export default definePlugin({ await requireSettingsMenu(); enableStyle(css); - if (Settings.plugins?.RemixMe?.enabled) { - Settings.plugins.RemixMe.enabled = false; - settings.store.remixMe = true; - } if (settings.store.remixMe) { addMessagePreSendListener(handleMessage); } diff --git a/src/equicordplugins/uwuifier/index.ts b/src/equicordplugins/uwuifier/index.ts deleted file mode 100644 index 8e632f904..000000000 --- a/src/equicordplugins/uwuifier/index.ts +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 exhq - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -import { findOption, RequiredMessageOption } from "@api/Commands"; -import { addMessagePreEditListener, addMessagePreSendListener, MessageObject, removeMessagePreEditListener, removeMessagePreSendListener } from "@api/MessageEvents"; -import { definePluginSettings } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; - -const endings = [ - "rawr x3", - "OwO", - "UwU", - "o.O", - "-.-", - ">w<", - "(⑅˘꒳˘)", - "(ꈍᴗꈍ)", - "(˘ω˘)", - "(U ᵕ U❁)", - "σωσ", - "òωó", - "(///ˬ///✿)", - "(U ﹏ U)", - "( ͡o ω ͡o )", - "ʘwʘ", - ":3", - ":3", // important enough to have twice - ":3", // important enough to have thrice - "XD", - "nyaa~~", - "mya", - ">_<", - "😳", - "🥺", - "😳😳😳", - "rawr", - "^^", - "^^;;", - "(ˆ ﻌ ˆ)♡", - "^•ﻌ•^", - "/(^•ω•^)", - "(✿oωo)" -]; - -const replacements = [ - ["small", "smol"], - ["cute", "kawaii"], - ["fluff", "floof"], - ["love", "luv"], - ["stupid", "baka"], - ["what", "nani"], - ["meow", "nya"], - ["hello", "hewwo"], -]; - -const settings = definePluginSettings({ - uwuEveryMessage: { - description: "Make every single message uwuified", - type: OptionType.BOOLEAN, - default: false, - restartNeeded: false - }, - uwuEverything: { - description: "Makes *all* text uwuified - really bad idea", - type: OptionType.BOOLEAN, - default: false, - restartNeeded: true - } -}); - -function selectRandomElement(arr) { - // generate a random index based on the length of the array - const randomIndex = Math.floor(Math.random() * arr.length); - - // return the element at the randomly generated index - return arr[randomIndex]; -} -const isOneCharacterString = (str: string): boolean => { - return str.split("").every((char: string) => char === str[0]); -}; - -function replaceString(inputString) { - let replaced = false; - for (const replacement of replacements) { - const regex = new RegExp(`\\b${replacement[0]}\\b`, "gi"); - if (regex.test(inputString)) { - inputString = inputString.replace(regex, replacement[1]); - replaced = true; - } - } - return replaced ? inputString : false; -} - -function uwuify(message: string): string { - const rule = /\S+|\s+/g; - const words: string[] | null = message.match(rule); - let answer = ""; - - if (words === null) return ""; - - for (let i = 0; i < words.length; i++) { - if (isOneCharacterString(words[i]) || words[i].startsWith("https://")) { - answer += words[i]; - continue; - } - - if (!replaceString(words[i])) { - answer += words[i] - .replace(/n(?=[aeo])/g, "ny") - .replace(/l|r/g, "w"); - } else answer += replaceString(words[i]); - - } - - answer += " " + selectRandomElement(endings); - return answer; -} - -function uwuifyArray(arr) { - const newArr = [...arr]; - - newArr.forEach((item, index) => { - if (Array.isArray(item)) { - newArr[index] = uwuifyArray(item); - } else if (typeof item === "string") { - newArr[index] = uwuify(item); - } - }); - - return newArr; -} - - -// actual command declaration -export default definePlugin({ - name: "UwUifier", - description: "Make everything uwu", - authors: [Devs.amy], - dependencies: ["MessageEventsAPI"], - settings, - - commands: [ - { - name: "uwuify", - description: "uwuifies your messages", - options: [RequiredMessageOption], - - execute: opts => ({ - content: uwuify(findOption(opts, "message", "")), - }), - }, - ], - - patches: [ - { - find: ".isPureReactComponent=!0;", - predicate: () => settings.store.uwuEverything, - replacement: { - match: /(?<=.defaultProps\)void 0.{0,60})(\i)\)/, - replace: "$self.uwuifyProps($1))" - } - } - ], - - uwuifyProps(props: any) { - if (!props.children) return props; - if (typeof props.children === "string") props.children = uwuify(props.children); - else if (Array.isArray(props.children)) props.children = uwuifyArray(props.children); - return props; - }, - - onSend(msg: MessageObject) { - // Only run when it's enabled - if (settings.store.uwuEveryMessage) { - msg.content = uwuify(msg.content); - } - }, - - start() { - this.preSend = addMessagePreSendListener((_, msg) => this.onSend(msg)); - this.preEdit = addMessagePreEditListener((_cid, _mid, msg) => - this.onSend(msg) - ); - }, - - stop() { - removeMessagePreSendListener(this.preSend); - removeMessagePreEditListener(this.preEdit); - }, -});