aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/bot.ts10
-rw-r--r--src/commands/info/inviteInfo.ts43
-rw-r--r--src/commands/moulberry-bush/neuRepo.ts47
-rw-r--r--src/commands/tickets/ticket-!.ts0
-rw-r--r--src/lib/common/util/Minecraft.ts331
-rw-r--r--src/tasks/cache/updateNeuItemCache.ts12
6 files changed, 438 insertions, 5 deletions
diff --git a/src/bot.ts b/src/bot.ts
index d7b5ad9..038fbbb 100644
--- a/src/bot.ts
+++ b/src/bot.ts
@@ -3,11 +3,11 @@ import { init } from './lib/utils/BushLogger.js';
// also starts a REPL session
init();
-const { dirname } = await import('path');
-const { fileURLToPath } = await import('url');
-const { default: config } = await import('../config/options.js');
-const { Sentry } = await import('./lib/common/Sentry.js');
-const { BushClient } = await import('./lib/index.js');
+import { dirname } from 'path';
+import { fileURLToPath } from 'url';
+import { default as config } from '../config/options.js';
+import { Sentry } from './lib/common/Sentry.js';
+import { BushClient } from './lib/index.js';
const isDry = process.argv.includes('dry');
if (!isDry && config.credentials.sentryDsn !== null) new Sentry(dirname(fileURLToPath(import.meta.url)) || process.cwd(), config);
diff --git a/src/commands/info/inviteInfo.ts b/src/commands/info/inviteInfo.ts
new file mode 100644
index 0000000..bfe7eae
--- /dev/null
+++ b/src/commands/info/inviteInfo.ts
@@ -0,0 +1,43 @@
+import { Arg, ArgType, BushCommand, clientSendAndPermCheck, colors, type CommandMessage, type SlashMessage } from '#lib';
+import { ApplicationCommandOptionType, EmbedBuilder, Invite, PermissionFlagsBits } from 'discord.js';
+
+export default class InviteInfoCommand extends BushCommand {
+ public constructor() {
+ super('inviteInfo', {
+ aliases: ['invite-info', 'ii'],
+ category: 'info',
+ description: 'Get info about an invite.',
+ usage: ['invite-info [invite]'],
+ examples: ['invite-info discord.gg/moulberry'],
+ args: [
+ {
+ id: 'invite',
+ description: 'The guild to find information about.',
+ type: 'invite',
+ prompt: 'What invite would you like to find information about?',
+ retry: '{error} Choose a valid invite to find information about.',
+ slashType: ApplicationCommandOptionType.String
+ }
+ ],
+ slash: true,
+ clientPermissions: (m) => clientSendAndPermCheck(m, [PermissionFlagsBits.EmbedLinks], true),
+ userPermissions: []
+ });
+ }
+
+ public override async exec(message: CommandMessage | SlashMessage, args: { invite: ArgType<'invite' | 'string'> }) {
+ const invite = message.util.isSlashMessage(message)
+ ? ((await Arg.cast('invite', message, args.invite as string)) as ArgType<'invite'>)
+ : args.invite;
+
+ const inviteInfoEmbed = new EmbedBuilder().setTitle(`Invite to ${invite.guild.name}`).setColor(colors.default);
+
+ this.generateAboutField(inviteInfoEmbed, invite);
+ }
+
+ private generateAboutField(embed: EmbedBuilder, invite: Invite) {
+ const about = [`**code:** ${invite.code}`, `**channel:** ${}`];
+
+ embed.addFields({ name: '» About', value: about.join('\n') });
+ }
+}
diff --git a/src/commands/moulberry-bush/neuRepo.ts b/src/commands/moulberry-bush/neuRepo.ts
new file mode 100644
index 0000000..24f83ad
--- /dev/null
+++ b/src/commands/moulberry-bush/neuRepo.ts
@@ -0,0 +1,47 @@
+import { BushCommand, clientSendAndPermCheck, type ArgType, type CommandMessage, type SlashMessage } from '#lib';
+import { ApplicationCommandOptionType, AutocompleteInteraction, CacheType, PermissionFlagsBits } from 'discord.js';
+
+export default class NeuRepoCommand extends BushCommand {
+ public static items: { name: string; id: string }[] = [];
+
+ public constructor() {
+ super('neuRepo', {
+ aliases: ['neu-repo', 'repo-item', 'neu-item', 'item-repo'],
+ category: "Moulberry's Bush",
+ description: 'Get information about an item from the NEU item repo.',
+ usage: ['neu-repo <item>'],
+ examples: ['neu-repo BARRIER'],
+ args: [
+ {
+ id: 'item',
+ description: 'The item id that you would like to find neu item information about.',
+ type: 'string',
+ prompt: 'What SkyBlock item would you like to get information about?',
+ retry: '{error} Pick a valid SkyBlock item ID. Try using the slash command for a better experience.',
+ slashType: ApplicationCommandOptionType.String,
+ autocomplete: true
+ }
+ /* {
+ id: 'dangerous',
+ description: 'Whether or not to use the dangerous branch.',
+ prompt: 'Would you like to use the dangerous branch instead of the master branch?',
+ match: 'flag',
+ flag: ['--dangerous', '-d'],
+ default: false,
+ optional: true,
+ slashType: ApplicationCommandOptionType.Boolean
+ } */
+ ],
+ slash: true,
+ clientPermissions: (m) => clientSendAndPermCheck(m, [PermissionFlagsBits.EmbedLinks], true),
+ userPermissions: []
+ });
+ }
+
+ public override async exec(
+ message: CommandMessage | SlashMessage,
+ args: { item: ArgType<'string'> /* dangerous: ArgType<'flag'> */ }
+ ) {}
+
+ public override async autocomplete(interaction: AutocompleteInteraction<CacheType>) {}
+}
diff --git a/src/commands/tickets/ticket-!.ts b/src/commands/tickets/ticket-!.ts
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/commands/tickets/ticket-!.ts
diff --git a/src/lib/common/util/Minecraft.ts b/src/lib/common/util/Minecraft.ts
new file mode 100644
index 0000000..b4b013f
--- /dev/null
+++ b/src/lib/common/util/Minecraft.ts
@@ -0,0 +1,331 @@
+import { Byte, Int, parse } from '@ironm00n/nbt-ts';
+import { BitField } from 'discord.js';
+import fs from 'fs/promises';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+export enum FormattingCodes {
+ Black = '§0',
+ DarkBlue = '§1',
+ DarkGreen = '§2',
+ DarkAqua = '§3',
+ DarkRed = '§4',
+ DarkPurple = '§5',
+ Gold = '§6',
+ Gray = '§7',
+ DarkGray = '§8',
+ Blue = '§9',
+ Green = '§a',
+ Aqua = '§b',
+ Red = '§c',
+ LightPurple = '§d',
+ Yellow = '§e',
+ White = '§f',
+
+ Obfuscated = '§k',
+ Bold = '§l',
+ Strikethrough = '§m',
+ Underline = '§n',
+ Italic = '§o',
+ Reset = '§r'
+}
+
+// https://minecraft.fandom.com/wiki/Formatting_codes
+export const formattingInfo = {
+ [FormattingCodes.Black]: { foreground: '#000000', background: '#000000', ansi: '\u001b[0;30m' },
+ [FormattingCodes.DarkBlue]: { foreground: '#0000AA', background: '#00002A', ansi: '\u001b[0;34m' },
+ [FormattingCodes.DarkGreen]: { foreground: '#00AA00', background: '#002A00', ansi: '\u001b[0;32m' },
+ [FormattingCodes.DarkAqua]: { foreground: '#00AAAA', background: '#002A2A', ansi: '\u001b[0;36m' },
+ [FormattingCodes.DarkRed]: { foreground: '#AA0000', background: '#2A0000', ansi: '\u001b[0;31m' },
+ [FormattingCodes.DarkPurple]: { foreground: '#AA00AA', background: '#2A002A', ansi: '\u001b[0;35m' },
+ [FormattingCodes.Gold]: { foreground: '#FFAA00', background: '#2A2A00', ansi: '\u001b[0;33m' },
+ [FormattingCodes.Gray]: { foreground: '#AAAAAA', background: '#2A2A2A', ansi: '\u001b[0;37m' },
+ [FormattingCodes.DarkGray]: { foreground: '#555555', background: '#151515', ansi: '\u001b[0;90m' },
+ [FormattingCodes.Blue]: { foreground: '#5555FF', background: '#15153F', ansi: '\u001b[0;94m' },
+ [FormattingCodes.Green]: { foreground: '#55FF55', background: '#153F15', ansi: '\u001b[0;92m' },
+ [FormattingCodes.Aqua]: { foreground: '#55FFFF', background: '#153F3F', ansi: '\u001b[0;96m' },
+ [FormattingCodes.Red]: { foreground: '#FF5555', background: '#3F1515', ansi: '\u001b[0;91m' },
+ [FormattingCodes.LightPurple]: { foreground: '#FF55FF', background: '#3F153F', ansi: '\u001b[0;95m' },
+ [FormattingCodes.Yellow]: { foreground: '#FFFF55', background: '#3F3F15', ansi: '\u001b[0;93m' },
+ [FormattingCodes.White]: { foreground: '#FFFFFF', background: '#3F3F3F', ansi: '\u001b[0;97m' },
+
+ [FormattingCodes.Obfuscated]: { ansi: '\u001b[8m' },
+ [FormattingCodes.Bold]: { ansi: '\u001b[1m' },
+ [FormattingCodes.Strikethrough]: { ansi: '\u001b[9m' },
+ [FormattingCodes.Underline]: { ansi: '\u001b[4m' },
+ [FormattingCodes.Italic]: { ansi: '\u001b[3m' },
+ [FormattingCodes.Reset]: { ansi: '\u001b[0m' }
+} as const;
+
+export type McItemId = Lowercase<string>;
+export type SbItemId = Uppercase<string>;
+export type MojangJson = string;
+export type SbRecipeItem = `${SbItemId}:${number}` | '';
+export type SbRecipe = {
+ [Location in `${'A' | 'B' | 'C'}${1 | 2 | 3}`]: SbRecipeItem;
+};
+export type InfoType = 'WIKI_URL' | '';
+
+type Slayer = `${'WOLF' | 'BLAZE' | 'EMAN'}_${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}`;
+
+export interface RawNeuItem {
+ itemid: McItemId;
+ displayname: string;
+ nbttag: MojangJson;
+ damage: number;
+ lore: string[];
+ recipe?: SbRecipe;
+ internalname: SbItemId;
+ modver: string;
+ infoType: InfoType;
+ info?: string[];
+ crafttext: string;
+ vanilla?: boolean;
+ useneucraft?: boolean;
+ slayer_req?: Slayer;
+ clickcommand?: string;
+ x?: number;
+ y?: number;
+ z?: number;
+ island?: string;
+ recipes?: { type: string; cost: any[]; result: SbItemId }[];
+ /** @deprecated */
+ parent?: SbItemId;
+ noseal?: boolean;
+}
+
+export enum HideFlagsBits {
+ Enchantments = 1,
+ AttributeModifiers = 2,
+ Unbreakable = 4,
+ CanDestroy = 8,
+ CanPlaceOn = 16,
+ /**
+ * potion effects, shield pattern info, "StoredEnchantments", written book
+ * "generation" and "author", "Explosion", "Fireworks", and map tooltips
+ */
+ OtherInformation = 32,
+ Dyed = 64
+}
+
+export type HideFlagsString = keyof typeof HideFlagsBits;
+
+export class HideFlags extends BitField<HideFlagsString> {
+ public static override Flags = HideFlagsBits;
+}
+
+export const formattingCode = new RegExp(
+ `§[${Object.values(FormattingCodes)
+ .filter((v) => v.startsWith('§'))
+ .map((v) => v.substring(1))
+ .join('')}]`
+);
+
+export function removeMCFormatting(str: string) {
+ return str.replaceAll(formattingCode, '');
+}
+
+const repo = path.join(__dirname, '..', '..', '..', '..', '..', 'neu-item-repo-dangerous');
+const itemPath = path.join(repo, 'items');
+const items = await fs.readdir(itemPath);
+
+// for (let i = 0; i < 5; i++) {
+for (const path_ of items) {
+ // const randomItem = items[Math.floor(Math.random() * items.length)];
+ // console.log(randomItem);
+ const item = (await import(path.join(itemPath, /* randomItem */ path_), { assert: { type: 'json' } })).default as RawNeuItem;
+ if (/.*?((_MONSTER)|(_NPC)|(_ANIMAL)|(_MINIBOSS)|(_BOSS)|(_SC))$/.test(item.internalname)) continue;
+ if (!/.*?;[0-5]$/.test(item.internalname)) continue;
+ /* console.log(path_);
+ console.dir(item, { depth: Infinity }); */
+
+ /* console.log('==========='); */
+ const nbt = parse(item.nbttag) as NbtTag;
+
+ if (nbt?.SkullOwner?.Properties?.textures?.[0]?.Value) {
+ nbt.SkullOwner.Properties.textures[0].Value = parse(
+ Buffer.from(nbt.SkullOwner.Properties.textures[0].Value, 'base64').toString('utf-8')
+ ) as string;
+ }
+
+ if (nbt.ExtraAttributes?.petInfo) {
+ nbt.ExtraAttributes.petInfo = JSON.parse(nbt.ExtraAttributes.petInfo as any as string);
+ }
+
+ // delete nbt.display?.Lore;
+
+ console.dir(nbt, { depth: Infinity });
+ console.log('===========');
+
+ /* if (nbt?.display && nbt.display.Name !== item.displayname)
+ console.log(`${path_} display name mismatch: ${mcToAnsi(nbt.display.Name)} != ${mcToAnsi(item.displayname)}`);
+
+ if (nbt?.ExtraAttributes && nbt?.ExtraAttributes.id !== item.internalname)
+ console.log(`${path_} internal name mismatch: ${mcToAnsi(nbt?.ExtraAttributes.id)} != ${mcToAnsi(item.internalname)}`); */
+
+ /* console.log('===========');
+
+ console.log(mcToAnsi(item.displayname));
+ console.log(item.lore.map((l) => mcToAnsi(l)).join('\n')); */
+
+ /* const keys = [
+ 'itemid',
+ 'displayname',
+ 'nbttag',
+ 'damage',
+ 'lore',
+ 'recipe',
+ 'internalname',
+ 'modver',
+ 'infoType',
+ 'info',
+ 'crafttext',
+ 'vanilla',
+ 'useneucraft',
+ 'slayer_req',
+ 'clickcommand',
+ 'x',
+ 'y',
+ 'z',
+ 'island',
+ 'recipes',
+ 'parent',
+ 'noseal'
+ ];
+
+ Object.keys(item).forEach((k) => {
+ if (!keys.includes(k)) throw new Error(`Unknown key: ${k}`);
+ });
+
+ if (
+ 'slayer_req' in item &&
+ !new Array(10).flatMap((_, i) => ['WOLF', 'BLAZE', 'EMAN'].map((e) => e + (i + 1)).includes(item.slayer_req!))
+ )
+ throw new Error(`Unknown slayer req: ${item.slayer_req!}`); */
+
+ /* console.log('=-=-=-=-=-=-=-=-=-=-=-=-=-=-\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-'); */
+}
+
+interface NbtTag {
+ overrideMeta?: Byte;
+ Unbreakable?: Int;
+ ench?: string[];
+ HideFlags?: HideFlags;
+ SkullOwner?: SkullOwner;
+ display?: NbtTagDisplay;
+ ExtraAttributes?: ExtraAttributes;
+}
+
+interface SkullOwner {
+ Id?: string;
+ Properties?: {
+ textures?: { Value?: string }[];
+ };
+}
+
+interface NbtTagDisplay {
+ Lore?: string[];
+ color?: Int;
+ Name?: string;
+}
+
+type RuneId = string;
+
+interface ExtraAttributes {
+ originTag?: Origin;
+ id?: string;
+ generator_tier?: Int;
+ boss_tier?: Int;
+ enchantments?: { hardened_mana?: Int };
+ dungeon_item_level?: Int;
+ runes?: { [key: RuneId]: Int };
+ petInfo?: PetInfo;
+}
+
+interface PetInfo {
+ type: 'ZOMBIE';
+ active: boolean;
+ exp: number;
+ tier: 'COMMON' | 'UNCOMMON' | 'RARE' | 'EPIC' | 'LEGENDARY';
+ hideInfo: boolean;
+ candyUsed: number;
+}
+
+type Origin = 'SHOP_PURCHASE';
+
+const neuConstantsPath = path.join(repo, 'constants');
+const neuPetsPath = path.join(neuConstantsPath, 'pets.json');
+const neuPets = (await import(neuPetsPath, { assert: { type: 'json' } })) as PetsConstants;
+const neuPetNumsPath = path.join(neuConstantsPath, 'petnums.json');
+const neuPetNums = (await import(neuPetNumsPath, { assert: { type: 'json' } })) as PetNums;
+
+interface PetsConstants {
+ pet_rarity_offset: Record<string, number>;
+ pet_levels: number[];
+ custom_pet_leveling: Record<string, { type: number; pet_levels: number[]; max_level: number }>;
+ pet_types: Record<string, string>;
+}
+
+interface PetNums {
+ [key: string]: {
+ [key: string]: {
+ '1': {
+ otherNums: number[];
+ statNums: Record<string, number>;
+ };
+ '100': {
+ otherNums: number[];
+ statNums: Record<string, number>;
+ };
+ 'stats_levelling_curve'?: `${number};${number};${number}`;
+ };
+ };
+}
+
+class NeuItem {
+ public itemId: McItemId;
+ public displayName: string;
+ public nbtTag: NbtTag;
+ public internalName: SbItemId;
+ public lore: string[];
+
+ public constructor(raw: RawNeuItem) {
+ this.itemId = raw.itemid;
+ this.nbtTag = <NbtTag>parse(raw.nbttag);
+ this.displayName = raw.displayname;
+ this.internalName = raw.internalname;
+ this.lore = raw.lore;
+
+ this.petLoreReplacements();
+ }
+
+ private petLoreReplacements(level = -1) {
+ if (/.*?;[0-5]$/.test(this.internalName) && this.displayName.includes('LVL')) {
+ const maxLevel = neuPets?.custom_pet_leveling?.[this.internalName]?.max_level ?? 100;
+ this.displayName = this.displayName.replace('LVL', `1➡${maxLevel}`);
+
+ const nums = neuPetNums[this.internalName];
+ if (!nums) throw new Error(`Pet (${this.internalName}) has no pet nums.`);
+
+ const teir = ['COMMON', 'UNCOMMON', 'RARE', 'EPIC', 'LEGENDARY', 'MYTHIC'][+this.internalName.at(-1)!];
+ const petInfoTier = nums[teir];
+ if (!petInfoTier) throw new Error(`Pet (${this.internalName}) has no pet nums for ${teir} rarity.`);
+
+ const curve = petInfoTier?.stats_levelling_curve?.split(';');
+
+ const minStatsLevel = parseInt(curve?.[0] ?? '0');
+ const maxStatsLevel = parseInt(curve?.[0] ?? '100');
+
+ const lore = '';
+ }
+ }
+}
+
+function mcToAnsi(str: string) {
+ for (const format in formattingInfo) {
+ str = str.replaceAll(format, formattingInfo[format as keyof typeof formattingInfo].ansi);
+ }
+ return `${str}\u001b[0m`;
+}
diff --git a/src/tasks/cache/updateNeuItemCache.ts b/src/tasks/cache/updateNeuItemCache.ts
new file mode 100644
index 0000000..fff9e08
--- /dev/null
+++ b/src/tasks/cache/updateNeuItemCache.ts
@@ -0,0 +1,12 @@
+import { BushTask, Time } from '#lib';
+
+export default class UpdatePriceItemCache extends BushTask {
+ public constructor() {
+ super('updatePriceItemCache', {
+ delay: 1 * Time.Hour,
+ runOnStart: true
+ });
+ }
+
+ public async exec() {}
+}