From 0331c451f18384fe356a0ad047e8add242ca78aa Mon Sep 17 00:00:00 2001 From: gc <30398469+gc@users.noreply.github.com> Date: Thu, 1 Aug 2024 01:49:38 +1000 Subject: [PATCH 01/27] Add cl_array column --- prisma/schema.prisma | 1 + 1 file changed, 1 insertion(+) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e25b832224..c16671d51a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -303,6 +303,7 @@ model User { QP Int @default(0) bank Json @default("{}") @db.Json collectionLogBank Json @default("{}") @db.JsonB + cl_array Int[] @default([]) blowpipe Json @default("{\"scales\":0,\"dartID\":null,\"dartQuantity\":0}") @db.Json slayer_unlocks Int[] @default([]) @map("slayer.unlocks") slayer_blocked_ids Int[] @default([]) @map("slayer.blocked_ids") From 5761c8f316cf72e574cf1cebc372b74a79ca5998 Mon Sep 17 00:00:00 2001 From: gc <30398469+gc@users.noreply.github.com> Date: Thu, 1 Aug 2024 02:02:03 +1000 Subject: [PATCH 02/27] Test fixes --- src/lib/util/clLeaderboard.ts | 6 +-- tests/integration/clArrayUpdate.test.ts | 61 +++++++++++-------------- 2 files changed, 30 insertions(+), 37 deletions(-) diff --git a/src/lib/util/clLeaderboard.ts b/src/lib/util/clLeaderboard.ts index 43def04ef5..da506b3fd7 100644 --- a/src/lib/util/clLeaderboard.ts +++ b/src/lib/util/clLeaderboard.ts @@ -22,7 +22,7 @@ export async function fetchCLLeaderboard({ const specificUsers = userIds.length > 0 ? await prisma.$queryRawUnsafe<{ id: string; qty: number }[]>(` - SELECT user_id::text AS id, CARDINALITY(cl_array) - CARDINALITY(cl_array - array[${items + SELECT user_id::text AS id, CARDINALITY(s.cl_array) - CARDINALITY(s.cl_array - array[${items .map(i => `${i}`) .join(', ')}]) AS qty FROM user_stats s INNER JOIN users u ON s.user_id::text = u.id @@ -30,12 +30,12 @@ export async function fetchCLLeaderboard({ `) : []; const generalUsers = await prisma.$queryRawUnsafe<{ id: string; qty: number }[]>(` - SELECT user_id::text AS id, CARDINALITY(cl_array) - CARDINALITY(cl_array - array[${items + SELECT user_id::text AS id, CARDINALITY(user_stats.cl_array) - CARDINALITY(user_stats.cl_array - array[${items .map(i => `${i}`) .join(', ')}]) AS qty FROM user_stats ${ironmenOnly ? 'INNER JOIN "users" on "users"."id" = "user_stats"."user_id"::text' : ''} - WHERE (cl_array && array[${items.map(i => `${i}`).join(', ')}] + WHERE (user_stats.cl_array && array[${items.map(i => `${i}`).join(', ')}] ${ironmenOnly ? 'AND "users"."minion.ironman" = true' : ''}) ${userIds.length > 0 ? `AND user_id::text NOT IN (${userIdsList})` : ''} ORDER BY qty DESC diff --git a/tests/integration/clArrayUpdate.test.ts b/tests/integration/clArrayUpdate.test.ts index 9c190aca5a..15f0917f16 100644 --- a/tests/integration/clArrayUpdate.test.ts +++ b/tests/integration/clArrayUpdate.test.ts @@ -1,4 +1,3 @@ -import { Time } from 'e'; import { expect, test } from 'vitest'; import { Bank } from 'oldschooljs'; @@ -6,39 +5,33 @@ import { itemID } from 'oldschooljs/dist/util'; import { roboChimpSyncData } from '../../src/lib/roboChimp'; import { createTestUser } from './util'; -test( - 'All Commands Base Test', - async () => { - const user = await createTestUser(); - await user.addItemsToBank({ items: new Bank().add('Coal', 100) }); - await roboChimpSyncData(user); - expect(user.fetchStats({ cl_array: true, cl_array_length: true })).resolves.toMatchObject({ - cl_array: [], - cl_array_length: 0 - }); +test('CL Updates', async () => { + const user = await createTestUser(); + await user.addItemsToBank({ items: new Bank().add('Coal', 100) }); + await roboChimpSyncData(user); + expect(await user.fetchStats({ cl_array: true, cl_array_length: true })).toMatchObject({ + cl_array: [], + cl_array_length: 0 + }); - await user.addItemsToBank({ items: new Bank().add('Egg', 100), collectionLog: true }); - await roboChimpSyncData(user); - expect(user.fetchStats({ cl_array: true, cl_array_length: true })).resolves.toMatchObject({ - cl_array: [itemID('Egg')], - cl_array_length: 1 - }); + await user.addItemsToBank({ items: new Bank().add('Egg', 100), collectionLog: true }); + await roboChimpSyncData(user); + expect(await user.fetchStats({ cl_array: true, cl_array_length: true })).toMatchObject({ + cl_array: [itemID('Egg')], + cl_array_length: 1 + }); - await user.addItemsToBank({ items: new Bank().add('Egg', 100), collectionLog: true }); - await roboChimpSyncData(user); - expect(user.fetchStats({ cl_array: true, cl_array_length: true })).resolves.toMatchObject({ - cl_array: [itemID('Egg')], - cl_array_length: 1 - }); + await user.addItemsToBank({ items: new Bank().add('Egg', 100), collectionLog: true }); + await roboChimpSyncData(user); + expect(await user.fetchStats({ cl_array: true, cl_array_length: true })).toMatchObject({ + cl_array: [itemID('Egg')], + cl_array_length: 1 + }); - await user.addItemsToBank({ items: new Bank().add('Trout', 100), collectionLog: true }); - await roboChimpSyncData(user); - expect(user.fetchStats({ cl_array: true, cl_array_length: true })).resolves.toMatchObject({ - cl_array: [itemID('Trout'), itemID('Egg')], - cl_array_length: 2 - }); - }, - { - timeout: Time.Minute * 10 - } -); + await user.addItemsToBank({ items: new Bank().add('Trout', 100), collectionLog: true }); + await roboChimpSyncData(user); + expect(await user.fetchStats({ cl_array: true, cl_array_length: true })).toMatchObject({ + cl_array: [itemID('Trout'), itemID('Egg')], + cl_array_length: 2 + }); +}); From ad6d6d41f0d1f152a14783210e31c80997ccbd9e Mon Sep 17 00:00:00 2001 From: GC <30398469+gc@users.noreply.github.com> Date: Fri, 2 Aug 2024 01:50:28 +1000 Subject: [PATCH 03/27] Roles task rewrite (#5982) --- prisma/schema.prisma | 6 +- src/lib/constants.ts | 8 +- src/lib/handleNewCLItems.ts | 22 +- src/lib/preStartup.ts | 5 +- src/lib/rawSql.ts | 16 + src/lib/rolesTask.ts | 767 ++++++++++------------- src/lib/util/addSubTaskToActivityTask.ts | 28 +- src/lib/util/clLeaderboard.ts | 140 +++-- src/mahoji/commands/leaderboard.ts | 14 +- src/mahoji/commands/rp.ts | 148 ++--- tests/integration/misc.test.ts | 31 +- tests/integration/rolesTask.test.ts | 2 +- 12 files changed, 518 insertions(+), 669 deletions(-) create mode 100644 src/lib/rawSql.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c16671d51a..0c894e7942 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -303,7 +303,6 @@ model User { QP Int @default(0) bank Json @default("{}") @db.Json collectionLogBank Json @default("{}") @db.JsonB - cl_array Int[] @default([]) blowpipe Json @default("{\"scales\":0,\"dartID\":null,\"dartQuantity\":0}") @db.Json slayer_unlocks Int[] @default([]) @map("slayer.unlocks") slayer_blocked_ids Int[] @default([]) @map("slayer.blocked_ids") @@ -405,7 +404,6 @@ model User { store_bitfield Int[] @default([]) - // Migrate farmingPatches_herb Json? @map("farmingPatches.herb") @db.Json farmingPatches_fruit_tree Json? @map("farmingPatches.fruit tree") @db.Json farmingPatches_tree Json? @map("farmingPatches.tree") @db.Json @@ -437,6 +435,8 @@ model User { gift_boxes_owned GiftBox[] @relation("gift_boxes_owned") gift_boxes_created GiftBox[] @relation("gift_boxes_created") + cl_array Int[] @default([]) + @@index([id, last_command_date]) @@map("users") } @@ -745,7 +745,7 @@ model UserStats { creature_scores Json @default("{}") monster_scores Json @default("{}") laps_scores Json @default("{}") - sacrificed_bank Json @default("{}") + sacrificed_bank Json @default("{}") @db.JsonB openable_scores Json @default("{}") gp_luckypick BigInt @default(0) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index c3732622d0..cb7b989f77 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -344,7 +344,9 @@ export const BadgesEnum = { TopSkiller: 9, TopCollector: 10, TopMinigame: 11, - SotWTrophy: 12 + SotWTrophy: 12, + Slayer: 13, + TopGiveawayer: 14 } as const; export const badges: { [key: number]: string } = { @@ -360,7 +362,9 @@ export const badges: { [key: number]: string } = { [BadgesEnum.TopSkiller]: Emoji.Skiller, [BadgesEnum.TopCollector]: Emoji.CollectionLog, [BadgesEnum.TopMinigame]: Emoji.MinigameIcon, - [BadgesEnum.SotWTrophy]: Emoji.SOTWTrophy + [BadgesEnum.SotWTrophy]: Emoji.SOTWTrophy, + [BadgesEnum.Slayer]: Emoji.Slayer, + [BadgesEnum.TopGiveawayer]: Emoji.SantaHat }; export const MAX_XP = 200_000_000; diff --git a/src/lib/handleNewCLItems.ts b/src/lib/handleNewCLItems.ts index c57afd9041..d5e84942fa 100644 --- a/src/lib/handleNewCLItems.ts +++ b/src/lib/handleNewCLItems.ts @@ -9,6 +9,7 @@ import { allCLItems, allCollectionLogsFlat, calcCLDetails } from './data/Collect import { calculateMastery } from './mastery'; import { calculateOwnCLRanking, roboChimpSyncData } from './roboChimp'; +import { RawSQL } from './rawSql'; import { MUserStats } from './structures/MUserStats'; import { fetchCLLeaderboard } from './util/clLeaderboard'; import { insertUserEvent } from './util/userEvents'; @@ -49,6 +50,10 @@ export async function handleNewCLItems({ await prisma.historicalData.create({ data: await createHistoricalData(user) }); } + if (didGetNewCLItem) { + await prisma.$queryRawUnsafe(RawSQL.updateCLArray(user.id)); + } + if (!didGetNewCLItem) return; const previousCLDetails = calcCLDetails(previousCL); @@ -101,15 +106,14 @@ export async function handleNewCLItems({ })}!` : ''; - const nthUser = ( - await fetchCLLeaderboard({ - ironmenOnly: false, - items: finishedCL.items, - resultLimit: 100_000, - method: 'raw_cl', - userEvents: null - }) - ).filter(u => u.qty === finishedCL.items.length).length; + const leaderboardUsers = await fetchCLLeaderboard({ + ironmenOnly: false, + items: finishedCL.items, + resultLimit: 100_000, + clName: finishedCL.name + }); + + const nthUser = leaderboardUsers.users.filter(u => u.qty === finishedCL.items.length).length; const placeStr = nthUser > 100 ? '' : ` They are the ${formatOrdinal(nthUser)} user to finish this CL.`; diff --git a/src/lib/preStartup.ts b/src/lib/preStartup.ts index 0b98474cd1..b47aefc1f0 100644 --- a/src/lib/preStartup.ts +++ b/src/lib/preStartup.ts @@ -1,3 +1,4 @@ +import { noOp } from 'e'; import { syncCustomPrices } from '../mahoji/lib/events'; import { syncActivityCache } from './Task'; import { cacheBadges } from './badges'; @@ -5,6 +6,7 @@ import { syncBlacklists } from './blacklists'; import { GrandExchange } from './grandExchange'; import { cacheGEPrices } from './marketPrices'; import { populateRoboChimpCache } from './perkTier'; +import { RawSQL } from './rawSql'; import { runStartupScripts } from './startupScripts'; import { logWrapFn } from './util'; import { syncActiveUserIDs } from './util/cachedUserIDs'; @@ -21,6 +23,7 @@ export const preStartup = logWrapFn('PreStartup', async () => { cacheBadges(), GrandExchange.init(), populateRoboChimpCache(), - cacheGEPrices() + cacheGEPrices(), + prisma.$queryRawUnsafe(RawSQL.updateAllUsersCLArrays()).then(noOp) ]); }); diff --git a/src/lib/rawSql.ts b/src/lib/rawSql.ts new file mode 100644 index 0000000000..54b68fc695 --- /dev/null +++ b/src/lib/rawSql.ts @@ -0,0 +1,16 @@ +import { Prisma } from '@prisma/client'; + +const u = Prisma.UserScalarFieldEnum; + +export const RawSQL = { + updateAllUsersCLArrays: () => `UPDATE users +SET ${u.cl_array} = ( + SELECT (ARRAY(SELECT jsonb_object_keys("${u.collectionLogBank}")::int)) +) +WHERE last_command_date > now() - INTERVAL '1 week';`, + updateCLArray: (userID: string) => `UPDATE users +SET ${u.cl_array} = ( + SELECT (ARRAY(SELECT jsonb_object_keys("${u.collectionLogBank}")::int)) +) +WHERE ${u.id} = '${userID}';` +}; diff --git a/src/lib/rolesTask.ts b/src/lib/rolesTask.ts index dc694bb83c..91482bfae7 100644 --- a/src/lib/rolesTask.ts +++ b/src/lib/rolesTask.ts @@ -1,535 +1,418 @@ -import { Prisma } from '@prisma/client'; -import { noOp, notEmpty } from 'e'; +import { noOp, uniqueArr } from 'e'; -import { SupportServer, production } from '../config'; -import { ClueTiers } from '../lib/clues/clueTiers'; -import { Roles } from '../lib/constants'; +import { SupportServer } from '../config'; +import { BadgesEnum, Roles } from '../lib/constants'; import { getCollectionItems } from '../lib/data/Collections'; import { Minigames } from '../lib/settings/minigames'; +import { Prisma } from '@prisma/client'; import Skills from '../lib/skilling/skills'; -import { convertXPtoLVL, getUsername } from '../lib/util'; -import { logError } from '../lib/util/logError'; +import { Stopwatch, convertXPtoLVL, getUsernameSync } from '../lib/util'; +import { ClueTiers } from './clues/clueTiers'; import { TeamLoot } from './simulation/TeamLoot'; import type { ItemBank } from './types'; +import { fetchMultipleCLLeaderboards } from './util/clLeaderboard'; -function addToUserMap(userMap: Record, id: string, reason: string) { - if (!userMap[id]) userMap[id] = []; - userMap[id].push(reason); +interface RoleResult { + roleID: string; + userID: string; + reason: string; + badge?: (typeof BadgesEnum)[keyof typeof BadgesEnum]; } const minigames = Minigames.map(game => game.column).filter(i => i !== 'tithe_farm'); -const collections = ['pets', 'skilling', 'clues', 'bosses', 'minigames', 'raids', 'slayer', 'other', 'custom']; - -for (const cl of collections) { +const CLS_THAT_GET_ROLE = [ + 'pets', + 'skilling', + 'clues', + 'bosses', + 'minigames', + 'raids', + 'slayer', + 'other', + 'custom', + 'overall' +]; + +for (const cl of CLS_THAT_GET_ROLE) { const items = getCollectionItems(cl); if (!items || items.length === 0) { throw new Error(`${cl} isn't a valid CL.`); } } -const mostSlayerPointsQuery = `SELECT id, 'Most Points' as desc -FROM users -WHERE "slayer.points" > 50 -ORDER BY "slayer.points" DESC -LIMIT 1;`; - -const longerSlayerTaskStreakQuery = `SELECT user_id::text as id, 'Longest Task Streak' as desc -FROM user_stats -WHERE "slayer_task_streak" > 20 -ORDER BY "slayer_task_streak" DESC -LIMIT 1;`; - -const mostSlayerTasksDoneQuery = `SELECT user_id::text as id, 'Most Tasks' as desc -FROM slayer_tasks -GROUP BY user_id -ORDER BY count(user_id)::int DESC -LIMIT 1;`; - -async function addRoles({ - users, - role, - badge, - userMap -}: { - users: string[]; - role: string; - badge: number | null; - userMap?: Record; -}): Promise { - if (process.env.TEST) return ''; - const g = globalClient.guilds.cache.get(SupportServer); - if (!g) throw new Error('No support guild'); - const added: string[] = []; - const removed: string[] = []; - const _role = await g.roles.fetch(role).catch(noOp); - if (!_role) return `\nCould not check ${role} role`; - for (const u of users.filter(notEmpty)) { - await g.members.fetch(u).catch(noOp); - } - const roleName = _role.name!; - const noChangeUserDescriptions: string[] = []; - for (const mem of g.members.cache.values()) { - const mUser = await mUserFetch(mem.user.id); - if (mem.roles.cache.has(role) && !users.includes(mem.user.id)) { - if (production) { - await mem.roles.remove(role).catch(noOp); - } - - if (badge && mUser.user.badges.includes(badge)) { - await mUser.update({ - badges: mUser.user.badges.filter(i => i !== badge) - }); - } - removed.push(mem.user.username); - } - - if (users.includes(mem.user.id)) { - noChangeUserDescriptions.push(`${mem.user.username}`); - if (production && !mem.roles.cache.has(role)) { - added.push(mem.user.username); - await mem.roles.add(role).catch(noOp); - } - if (badge && !mUser.user.badges.includes(badge)) { - await mUser.update({ - badges: { - push: badge - } - }); - } - } - } - let str = `\n**${roleName}**`; - if (added.length > 0) { - str += `\nAdded to: ${added.join(', ')}.`; - } - if (removed.length > 0) { - str += `\nRemoved from: ${removed.join(', ')}.`; - } - if (userMap) { - const userArr = []; - for (const [id, arr] of Object.entries(userMap)) { - const username = await getUsername(id); - userArr.push(`${username}(${arr.join(', ')})`); - } - str += `\n${userArr.join(',')}`; - } - if (added.length > 0 || removed.length > 0) { - str += '\n'; - } else { - return `**No Changes:** ${str}`; - } - return str; -} - -export async function runRolesTask() { +async function topSkillers() { + const results: RoleResult[] = []; const skillVals = Object.values(Skills); - const results: string[] = []; - const q = async (str: string) => { - const result = await prisma.$queryRawUnsafe(str).catch(err => { - logError(`This query failed: ${str}`, err); - return []; - }); - return result; - }; - - // Top Skillers - async function topSkillers() { - const topSkillers = ( - await Promise.all([ - ...skillVals.map(s => - q< - { - id: string; - xp: string; - }[] - >(`SELECT id, "skills.${s.id}" as xp FROM users ORDER BY xp DESC LIMIT 1;`) - ), - q< - { - id: string; - }[] - >( - `SELECT id, ${skillVals.map(s => `"skills.${s.id}"`)}, ${skillVals - .map(s => `"skills.${s.id}"::bigint`) - .join(' + ')} as totalxp FROM users ORDER BY totalxp DESC LIMIT 1;` - ) - ]) - ).map(i => i[0]?.id); - - // Rank 1 Total Level - const rankOneTotal = ( - await q( - `SELECT id, ${skillVals.map(s => `"skills.${s.id}"`)}, ${skillVals - .map(s => `"skills.${s.id}"::bigint`) - .join(' + ')} as totalxp FROM users ORDER BY totalxp DESC LIMIT 200;` - ) + const [top200TotalXPUsers, ...top200ms] = await prisma.$transaction([ + prisma.$queryRawUnsafe( + `SELECT id, ${skillVals.map(s => `"skills.${s.id}"`)}, ${skillVals + .map(s => `"skills.${s.id}"::bigint`) + .join(' + ')} as totalxp FROM users ORDER BY totalxp DESC LIMIT 200;` + ), + ...skillVals.map(s => + prisma.$queryRawUnsafe<{ + id: string; + xp: string; + skill: string; + }>(`SELECT id, "skills.${s.id}" AS xp, '${s.id}' AS skill FROM users ORDER BY xp DESC LIMIT 1;`) ) - .map((u: any) => { - let totalLevel = 0; - for (const skill of skillVals) { - totalLevel += convertXPtoLVL(Number(u[`skills.${skill.id}` as keyof any]) as any); - } - return { - id: u.id, - totalLevel - }; - }) - .sort((a: any, b: any) => b.totalLevel - a.totalLevel)[0]; - topSkillers.push(rankOneTotal.id); - - results.push(await addRoles({ users: topSkillers, role: Roles.TopSkiller, badge: 9 })); + ]); + + for (const { id, skill } of top200ms) { + results.push({ + userID: id, + roleID: Roles.TopSkiller, + reason: `Rank 1 ${skill} XP`, + badge: BadgesEnum.TopSkiller + }); } - // Top Collectors - async function topCollector() { - const userMap = {}; - - function generateQuery(items: number[], ironmenOnly: boolean, limit: number) { - const t = ` -SELECT id, (cardinality(u.cl_keys) - u.inverse_length) as qty - FROM ( - SELECT ARRAY(SELECT * FROM JSONB_OBJECT_KEYS("collectionLogBank")) "cl_keys", - id, "collectionLogBank", - cardinality(ARRAY(SELECT * FROM JSONB_OBJECT_KEYS("collectionLogBank" - array[${items - .map(i => `'${i}'`) - .join(', ')}]))) "inverse_length" - FROM users - WHERE "collectionLogBank" ?| array[${items.map(i => `'${i}'`).join(', ')}] - ${ironmenOnly ? 'AND "minion.ironman" = true' : ''} - ) u - ORDER BY qty DESC - LIMIT ${limit}; -`; - - return t; - } + const rankOneTotal = top200TotalXPUsers + .map((u: any) => { + let totalLevel = 0; + for (const skill of skillVals) { + totalLevel += convertXPtoLVL(Number(u[`skills.${skill.id}` as keyof any]) as any); + } + return { + id: u.id, + totalLevel + }; + }) + .sort((a: any, b: any) => b.totalLevel - a.totalLevel)[0]; - const topCollectors = ( - await Promise.all( - collections.map(async clName => { - const items = getCollectionItems(clName); - if (!items || items.length === 0) { - logError(`${clName} collection log doesnt exist`); - return []; - } - - function handleErr(): any[] { - logError(`Failed to select top collectors for ${clName}`); - return []; - } - - const [users, ironUsers] = await Promise.all([ - q(generateQuery(items, false, 1)) - .then(i => i.filter((i: any) => i.qty > 0) as any[]) - .catch(handleErr), - q(generateQuery(items, true, 1)) - .then(i => i.filter((i: any) => i.qty > 0) as any[]) - .catch(handleErr) - ]); - - const result = []; - const userID = users[0]?.id; - const ironmanID = ironUsers[0]?.id; - - if (userID) { - addToUserMap(userMap, userID, `Rank 1 ${clName} CL`); - result.push(userID); - } - if (ironmanID) { - addToUserMap(userMap, ironmanID, `Rank 1 Ironman ${clName} CL`); - result.push(ironmanID); - } - - return result; - }) - ) - ).flat(2); - - const topIronUsers = (await q(generateQuery(getCollectionItems('overall'), true, 3))).filter( - (i: any) => i.qty > 0 - ) as any[]; - for (let i = 0; i < topIronUsers.length; i++) { - const id = topIronUsers[i]?.id; - addToUserMap(userMap, id, `Rank ${i + 1} Ironman Collector`); - topCollectors.push(id); - } - const topNormieUsers = (await q(generateQuery(getCollectionItems('overall'), false, 3))).filter( - (i: any) => i.qty > 0 - ) as any[]; - for (let i = 0; i < topNormieUsers.length; i++) { - const id = topNormieUsers[i]?.id; - addToUserMap(userMap, id, `Rank ${i + 1} Collector`); - topCollectors.push(id); - } + results.push({ + userID: rankOneTotal.id, + roleID: Roles.TopSkiller, + reason: 'Rank 1 Total Level', + badge: BadgesEnum.TopSkiller + }); - results.push(await addRoles({ users: topCollectors, role: Roles.TopCollector, badge: 10, userMap })); - } + return results; +} - // Top sacrificers - async function topSacrificers() { - const userMap = {}; - const topSacrificers: string[] = []; - const mostValue = await q('SELECT id FROM users ORDER BY "sacrificedValue" DESC LIMIT 3;'); - for (let i = 0; i < 3; i++) { - if (mostValue[i] !== undefined) { - topSacrificers.push(mostValue[i].id); - addToUserMap(userMap, mostValue[i].id, `Rank ${i + 1} Sacrifice Value`); - } +async function topCollector() { + const results: RoleResult[] = []; + const rankOneInSpecifiedCLs = await fetchMultipleCLLeaderboards( + CLS_THAT_GET_ROLE.map(cl => { + const items = getCollectionItems(cl); + const base = { + items, + clName: cl, + resultLimit: cl === 'overall' ? 3 : 1 + } as const; + return [ + { ...base, ironmenOnly: true }, + { ...base, ironmenOnly: false } + ]; + }).flat(2) + ); + for (const { users, clName, ironmenOnly } of rankOneInSpecifiedCLs) { + for (const user of users) { + results.push({ + userID: user.id, + roleID: Roles.TopCollector, + reason: `Rank 1 ${ironmenOnly ? 'Iron' : 'Main'} ${clName}`, + badge: BadgesEnum.TopCollector + }); } - const mostValueIronman = await q( - 'SELECT id FROM users WHERE "minion.ironman" = true ORDER BY "sacrificedValue" DESC LIMIT 1;' - ); - topSacrificers.push(mostValueIronman[0].id); - addToUserMap(userMap, mostValueIronman[0].id, 'Rank 1 Ironman Sacrificed Value'); + } + return results; +} - const mostUniques = await q(`SELECT u.id, u.sacbanklength FROM ( +async function topSacrificers() { + const results: RoleResult[] = []; + const users = await prisma.$transaction([ + prisma.$queryRawUnsafe<{ id: string; reason: string }[]>( + `SELECT id, 'Top 3' AS reason FROM users ORDER BY "sacrificedValue" DESC LIMIT 3;` + ), + prisma.$queryRawUnsafe<{ id: string; reason: string }[]>( + `SELECT id, 'Top Ironman' AS reason FROM users WHERE "minion.ironman" = true ORDER BY "sacrificedValue" DESC LIMIT 1;` + ), + prisma.$queryRawUnsafe<{ id: string; reason: string }[]>(`SELECT u.id, 'Top Uniques' AS reason FROM ( SELECT (SELECT COUNT(*)::int FROM JSONB_OBJECT_KEYS("sacrificed_bank")) sacbanklength, user_id::text as id FROM user_stats ) u -ORDER BY u.sacbanklength DESC LIMIT 1;`); - - const mostUniquesIron = await q(`SELECT u.id, u.sacbanklength FROM ( +ORDER BY u.sacbanklength DESC LIMIT 1;`), + prisma.$queryRawUnsafe<{ id: string; reason: string }[]>(`SELECT u.id, 'Top Ironman Uniques' AS reason FROM ( SELECT (SELECT COUNT(*)::int FROM JSONB_OBJECT_KEYS("sacrificed_bank")) sacbanklength, user_id::text as id FROM user_stats INNER JOIN users ON "user_stats"."user_id"::text = "users"."id" WHERE "users"."minion.ironman" = true ) u -ORDER BY u.sacbanklength DESC LIMIT 1;`); - topSacrificers.push(mostUniques[0].id); - addToUserMap(userMap, mostUniques[0].id, 'Most Uniques Sacrificed'); - topSacrificers.push(mostUniquesIron[0].id); - addToUserMap(userMap, mostUniquesIron[0].id, 'Most Ironman Uniques Sacrificed'); - - results.push(await addRoles({ users: topSacrificers, role: Roles.TopSacrificer, badge: 8, userMap })); +ORDER BY u.sacbanklength DESC LIMIT 1;`) + ]); + + for (const res of users.flat()) { + results.push({ + userID: res.id, + reason: res.reason, + roleID: Roles.TopSacrificer, + badge: BadgesEnum.TopSacrifice + }); } - // Top minigamers - async function topMinigamers() { - const topMinigamers = ( - await Promise.all( - minigames.map(m => - q( - `SELECT user_id, '${m}' as m + return results; +} + +async function topMinigamers() { + const results: RoleResult[] = []; + const topMinigamers = await prisma.$transaction( + minigames.map(m => + prisma.$queryRawUnsafe<{ id: string; minigame_name: string; qty: number }[]>( + `SELECT user_id::text AS id, '${m}' AS minigame_name FROM minigames ORDER BY ${m} DESC LIMIT 1;` - ) - ) ) - ).map((i: any) => [i[0].user_id, Minigames.find(m => m.column === i[0].m)?.name]); - - const userMap = {}; - for (const [id, m] of topMinigamers) { - addToUserMap(userMap, id, `Rank 1 ${m}`); - } + ) + ); - results.push( - await addRoles({ - users: topMinigamers.map(i => i[0]), - role: Roles.TopMinigamer, - badge: 11, - userMap - }) - ); + for (const { id, minigame_name } of topMinigamers.flat()) { + results.push({ + userID: id, + roleID: Roles.TopMinigamer, + reason: `Rank 1 ${minigame_name}`, + badge: BadgesEnum.TopMinigame + }); } - // Top clue hunters - async function topClueHunters() { - const topClueHunters = ( - await Promise.all( - ClueTiers.map(t => - q( - `SELECT id, '${t.name}' as n, (openable_scores->>'${t.id}')::int as qty -FROM users -INNER JOIN "user_stats" ON "user_stats"."user_id"::text = "users"."id" + return results; +} + +async function topClueHunters() { + const results: RoleResult[] = []; + const topClueHunters = await prisma.$transaction( + ClueTiers.map(t => + prisma.$queryRawUnsafe<{ user_id: string; tier_name: string; qty: string }>( + ` +SELECT user_id::text, '${t.name}' AS tier_name, (openable_scores->>'${t.id}')::int AS qty +FROM user_stats WHERE "openable_scores"->>'${t.id}' IS NOT NULL ORDER BY qty DESC LIMIT 1;` - ) - ) ) ) - .filter((i: any) => Boolean(i[0]?.id)) - .map((i: any) => [i[0]?.id, i[0]?.n]); - - const userMap = {}; - - for (const [id, n] of topClueHunters) { - addToUserMap(userMap, id, `Rank 1 ${n} Clues`); - } + ); - results.push( - await addRoles({ - users: topClueHunters.map(i => i[0]), - role: Roles.TopClueHunter, - badge: null, - userMap - }) - ); + for (const res of topClueHunters) { + results.push({ + userID: res.user_id, + roleID: Roles.TopClueHunter, + reason: `Rank 1 ${res.tier_name} Clues`, + badge: BadgesEnum.TopMinigame + }); } + return results; +} - // Top farmers - async function farmers() { - const queries = [ - `SELECT id, 'Top 2 Farming Contracts' as desc +async function topFarmers() { + const results: RoleResult[] = []; + const queries = [ + `SELECT id, 'Top 2 Farming Contracts' as desc FROM users WHERE "minion.farmingContract" IS NOT NULL AND "minion.ironman" = true ORDER BY ("minion.farmingContract"->>'contractsCompleted')::int DESC LIMIT 2;`, - `SELECT id, 'Top 2 Ironman Farming Contracts' as desc + `SELECT id, 'Top 2 Ironman Farming Contracts' as desc FROM users WHERE "minion.farmingContract" IS NOT NULL ORDER BY ("minion.farmingContract"->>'contractsCompleted')::int DESC LIMIT 2;`, - `SELECT user_id::text as id, 'Top 2 Most Farming Trips' as desc + `SELECT user_id::text as id, 'Top 2 Most Farming Trips' as desc FROM activity WHERE type = 'Farming' GROUP BY user_id ORDER BY count(user_id)::int DESC LIMIT 2;`, - `SELECT user_id::text as id, 'Top 2 Tithe Farm' as desc + `SELECT user_id::text as id, 'Top 2 Tithe Farm' as desc FROM user_stats ORDER BY "tithe_farms_completed" DESC LIMIT 2;` - ]; - const res = (await Promise.all(queries.map(q))).map((i: any) => [i[0]?.id, i[0]?.desc]); - const userMap = {}; - for (const [id, desc] of res) { - addToUserMap(userMap, id, desc); - } - - results.push( - await addRoles({ - users: res.map(i => i[0]), - role: '894194027363205150', - badge: null, - userMap - }) - ); + ]; + const res = await prisma.$transaction(queries.map(q => prisma.$queryRawUnsafe<{ id: string; desc: string }[]>(q))); + for (const { id, desc } of res.flat()) { + results.push({ + userID: id, + roleID: '894194027363205150', + reason: desc, + badge: BadgesEnum.Slayer + }); } + return results; +} - // Top slayers - async function slayer() { - const topSlayers = ( - await Promise.all( - [mostSlayerPointsQuery, longerSlayerTaskStreakQuery, mostSlayerTasksDoneQuery].map(query => q(query)) - ) - ) - .filter((i: any) => Boolean(i[0]?.id)) - .map((i: any) => [i[0]?.id, i[0]?.desc]); - - const userMap = {}; - for (const [id, desc] of topSlayers) { - addToUserMap(userMap, id, desc); - } - - results.push( - await addRoles({ - users: topSlayers.map(i => i[0]), - role: Roles.TopSlayer, - badge: null, - userMap - }) - ); +async function fetchSlayerResults() { + const results: RoleResult[] = []; + const topSlayers = await prisma.$transaction([ + prisma.$queryRawUnsafe<{ id: string; desc: string }[]>(`SELECT id, 'Most Points' as desc +FROM users +WHERE "slayer.points" > 50 +ORDER BY "slayer.points" DESC +LIMIT 1;`), + prisma.$queryRawUnsafe<{ id: string; desc: string }[]>(`SELECT user_id::text as id, 'Longest Task Streak' as desc +FROM user_stats +WHERE "slayer_task_streak" > 20 +ORDER BY "slayer_task_streak" DESC +LIMIT 1;`), + prisma.$queryRawUnsafe<{ id: string; desc: string }[]>(`SELECT user_id::text as id, 'Most Tasks' as desc +FROM slayer_tasks +GROUP BY user_id +ORDER BY count(user_id)::int DESC +LIMIT 1;`) + ]); + + for (const { id, desc } of topSlayers.flat()) { + results.push({ + userID: id, + roleID: Roles.TopSlayer, + reason: desc, + badge: BadgesEnum.Slayer + }); } + return results; +} - // Top giveawayers - async function giveaways() { - const GIVEAWAY_CHANNELS = [ - '792691343284764693', - '732207379818479756', - '342983479501389826', - '982989775399174184', - '346304390858145792' - ]; - const res = await prisma.$queryRaw<{ user_id: string; qty: number }[]>`SELECT user_id, COUNT(user_id)::int AS qty -FROM giveaway -WHERE channel_id IN (${Prisma.join(GIVEAWAY_CHANNELS)}) -AND user_id NOT IN ('157797566833098752') -GROUP BY user_id -ORDER BY qty DESC -LIMIT 50;`; - const userIDsToCheck = res.map(i => i.user_id); - - const giveawayBank = new TeamLoot(); - - const giveaways = await prisma.giveaway.findMany({ - where: { - channel_id: { - in: GIVEAWAY_CHANNELS - }, - user_id: { - in: userIDsToCheck - } +async function giveaways() { + const results: RoleResult[] = []; + const GIVEAWAY_CHANNELS = [ + '792691343284764693', + '732207379818479756', + '342983479501389826', + '982989775399174184', + '346304390858145792' + ]; + const res = await prisma.$queryRaw<{ user_id: string; qty: number }[]>`SELECT user_id, COUNT(user_id)::int AS qty + FROM giveaway + WHERE channel_id IN (${Prisma.join(GIVEAWAY_CHANNELS)}) + AND user_id NOT IN ('157797566833098752') + GROUP BY user_id + ORDER BY qty DESC + LIMIT 50;`; + const userIDsToCheck = res.map(i => i.user_id); + + const giveawayBank = new TeamLoot(); + + const giveaways = await prisma.giveaway.findMany({ + where: { + channel_id: { + in: GIVEAWAY_CHANNELS + }, + user_id: { + in: userIDsToCheck } - }); - for (const giveaway of giveaways) { - giveawayBank.add(giveaway.user_id, giveaway.loot as ItemBank); } + }); + + if (giveaways.length === 0) return results; - const userMap = {}; - const [[highestID, loot]] = giveawayBank.entries().sort((a, b) => b[1].value() - a[1].value()); - addToUserMap(userMap, highestID, `Most Value Given Away (${loot.value()})`); - - results.push( - await addRoles({ - users: [highestID], - role: '1052481561603346442', - badge: null, - userMap - }) - ); + for (const giveaway of giveaways) { + giveawayBank.add(giveaway.user_id, giveaway.loot as ItemBank); } - // Global CL % - async function globalCL() { - const result = await roboChimpClient.$queryRaw<{ id: string; total_cl_percent: number }[]>`SELECT ((osb_cl_percent + bso_cl_percent) / 2) AS total_cl_percent, id::text AS id -FROM public.user -WHERE osb_cl_percent IS NOT NULL AND bso_cl_percent IS NOT NULL -ORDER BY total_cl_percent DESC -LIMIT 10;`; - - results.push( - await addRoles({ - users: result.slice(0, 10).map(i => i.id), - role: Roles.TopGlobalCL, - badge: null - }) - ); + const [[highestID, loot]] = giveawayBank.entries().sort((a, b) => b[1].value() - a[1].value()); + + results.push({ + userID: highestID, + roleID: '1052481561603346442', + reason: `Most Value Given Away (${loot.value()})`, + badge: BadgesEnum.TopGiveawayer + }); + return results; +} + +async function globalCL() { + const results: RoleResult[] = []; + const result = await roboChimpClient.$queryRaw<{ id: string; total_cl_percent: number }[]>`SELECT ((osb_cl_percent + bso_cl_percent) / 2) AS total_cl_percent, id::text AS id + FROM public.user + WHERE osb_cl_percent IS NOT NULL AND bso_cl_percent IS NOT NULL + ORDER BY total_cl_percent DESC + LIMIT 10;`; + + for (const user of result) { + results.push({ + userID: user.id, + roleID: Roles.TopGlobalCL, + reason: `Top Global CL ${user.total_cl_percent}%`, + badge: BadgesEnum.TopCollector + }); } + return results; +} + +export async function runRolesTask(dryRun: boolean) { + const results: RoleResult[] = []; const tup = [ - ['Top Slayer', slayer], + ['Top Slayer', fetchSlayerResults], ['Top Clue Hunters', topClueHunters], ['Top Minigamers', topMinigamers], ['Top Sacrificers', topSacrificers], ['Top Collectors', topCollector], ['Top Skillers', topSkillers], - ['Top Farmers', farmers], + ['Top Farmers', topFarmers], ['Top Giveawayers', giveaways], ['Global CL', globalCL] ] as const; - const failed: string[] = []; - await Promise.all( - tup.map(async ([name, fn]) => { - try { - await fn(); - } catch (err: any) { - if (process.env.TEST) { - throw err; - } - failed.push(`${name} (${err.message})`); - logError(err); - } + await Promise.all([ + ...tup.map(async ([name, fn]) => { + const stopwatch = new Stopwatch(); + const res = await fn(); + console.log(`Ran ${name} in ${stopwatch.stop()}`); + results.push(...res); }) - ); + ]); + + const allBadgeIDs = uniqueArr(results.map(i => i.badge)); + const allRoleIDs = uniqueArr(results.map(i => i.roleID)); + + if (!dryRun) { + const roleNames = new Map(); + const supportServerGuild = globalClient.guilds.cache.get(SupportServer)!; + if (!supportServerGuild) throw new Error('No support guild'); + + // Remove all top badges from all users (and add back later) + const badgeIDs = `ARRAY[${allBadgeIDs.join(',')}]`; + await prisma.$queryRawUnsafe(` +UPDATE users +SET badges = badges - ${badgeIDs} +WHERE badges && ${badgeIDs} +`); + + // Remove roles from ineligible users + for (const member of supportServerGuild.members.cache.values()) { + const rolesToRemove = member.roles.cache.filter(r => allRoleIDs.includes(r.id)); + if (rolesToRemove.size > 0) { + await member.roles.remove(rolesToRemove.map(r => r.id)).catch(noOp); + } + } + + // Add roles to users + for (const { userID, roleID, badge } of results) { + const role = await supportServerGuild.roles.fetch(roleID).catch(noOp); + const member = await supportServerGuild.members.fetch(userID).catch(noOp); + if (!member || !role) continue; + roleNames.set(roleID, role.name); - const res = `**Roles** -${results.join('\n')} -${failed.length > 0 ? `Failed: ${failed.join(', ')}` : ''}`; + if (!member.roles.cache.has(roleID)) { + await member.roles.add(roleID).catch(noOp); + } + + if (badge) { + const user = await mUserFetch(userID); + if (!user.user.badges.includes(badge)) { + await user.update({ + badges: { + push: badge + } + }); + } + } + } + return `**Roles**\n${results.map(r => `${getUsernameSync(r.userID)} got ${roleNames.get(r.roleID)} because ${r.reason}`).join('\n')}`; + } - return res; + return 'Dry run'; } diff --git a/src/lib/util/addSubTaskToActivityTask.ts b/src/lib/util/addSubTaskToActivityTask.ts index e717d57794..8e0fe2f06c 100644 --- a/src/lib/util/addSubTaskToActivityTask.ts +++ b/src/lib/util/addSubTaskToActivityTask.ts @@ -39,24 +39,28 @@ export default async function addSubTaskToActivityTask 0 ? userIds.map(i => `'${i}'`).join(', ') : 'NULL'; - const specificUsers = - userIds.length > 0 - ? await prisma.$queryRawUnsafe<{ id: string; qty: number }[]>(` - SELECT user_id::text AS id, CARDINALITY(s.cl_array) - CARDINALITY(s.cl_array - array[${items - .map(i => `${i}`) - .join(', ')}]) AS qty - FROM user_stats s INNER JOIN users u ON s.user_id::text = u.id - WHERE ${ironmenOnly ? 'u."minion.ironman" = true AND' : ''} user_id::text IN (${userIdsList}) -`) - : []; - const generalUsers = await prisma.$queryRawUnsafe<{ id: string; qty: number }[]>(` - SELECT user_id::text AS id, CARDINALITY(user_stats.cl_array) - CARDINALITY(user_stats.cl_array - array[${items - .map(i => `${i}`) - .join(', ')}]) AS qty - FROM user_stats - ${ironmenOnly ? 'INNER JOIN "users" on "users"."id" = "user_stats"."user_id"::text' : ''} - WHERE (user_stats.cl_array && array[${items.map(i => `${i}`).join(', ')}] - ${ironmenOnly ? 'AND "users"."minion.ironman" = true' : ''}) - ${userIds.length > 0 ? `AND user_id::text NOT IN (${userIdsList})` : ''} - ORDER BY qty DESC - LIMIT ${resultLimit} +export async function fetchMultipleCLLeaderboards( + leaderboards: { + ironmenOnly: boolean; + items: number[]; + resultLimit: number; + clName: string; + }[] +) { + const userEvents = await prisma.userEvent.findMany({ + where: { + type: 'CLCompletion' + }, + orderBy: { + date: 'asc' + } + }); + const parsedLeaderboards = leaderboards.map(l => { + const userEventMap = userEventsToMap( + userEvents.filter(e => e.collection_log_name && stringMatches(e.collection_log_name, l.clName)) + ); + return { + ...l, + userEventMap + }; + }); + + const results = await prisma.$transaction([ + ...parsedLeaderboards.map(({ items, userEventMap, ironmenOnly, resultLimit }) => { + const SQL_ITEMS = `ARRAY[${items.map(i => `${i}`).join(', ')}]`; + const userIds = Array.from(userEventMap.keys()); + const userIdsList = userIds.length > 0 ? userIds.map(i => `'${i}'`).join(', ') : 'NULL'; + + return prisma.$queryRawUnsafe<{ id: string; qty: number }[]>(` +SELECT id, qty +FROM ( + SELECT id, CARDINALITY(cl_array & ${SQL_ITEMS}) AS qty + FROM users + WHERE (cl_array && ${SQL_ITEMS} + ${ironmenOnly ? 'AND "users"."minion.ironman" = true' : ''}) ${userIdsList.length > 0 ? `OR id IN (${userIdsList})` : ''} + LIMIT ${resultLimit} + ) AS subquery +ORDER BY qty DESC; `); + }) + ]); - const users = [...specificUsers, ...generalUsers] + return results.map((res, index) => { + const src = parsedLeaderboards[index]; + + const users = res .sort((a, b) => { const valueDifference = b.qty - a.qty; if (valueDifference !== 0) { return valueDifference; } - const dateA = userEventMap.get(a.id); - const dateB = userEventMap.get(b.id); + const dateA = src.userEventMap.get(a.id); + const dateB = src.userEventMap.get(b.id); if (dateA && dateB) { return dateA - dateB; } @@ -62,30 +70,26 @@ export async function fetchCLLeaderboard({ return 0; }) .filter(i => i.qty > 0); - return users; - } - const users = ( - await prisma.$queryRawUnsafe<{ id: string; qty: number }[]>(` -SELECT id, (cardinality(u.cl_keys) - u.inverse_length) as qty - FROM ( - SELECT array(SELECT * FROM jsonb_object_keys("collectionLogBank")) "cl_keys", - id, "collectionLogBank", - cardinality(array(SELECT * FROM jsonb_object_keys("collectionLogBank" - array[${items - .map(i => `'${i}'`) - .join(', ')}]))) "inverse_length" - FROM users - WHERE ("collectionLogBank" ?| array[${items.map(i => `'${i}'`).join(', ')}] - ${ironmenOnly ? 'AND "minion.ironman" = true' : ''}) - ${ - userIds.length > 0 - ? `OR (id in (${userIds.map(i => `'${i}'`).join(', ')})${ironmenOnly ? 'AND "minion.ironman" = true' : ''})` - : '' - } -) u -ORDER BY qty DESC -LIMIT ${resultLimit}; -`) - ).filter(i => i.qty > 0); - return users; + return { + ...src, + users + }; + }); +} + +export async function fetchCLLeaderboard({ + ironmenOnly, + items, + resultLimit, + clName +}: { + ironmenOnly: boolean; + items: number[]; + resultLimit: number; + method?: 'cl_array'; + clName: string; +}) { + const result = await fetchMultipleCLLeaderboards([{ ironmenOnly, items, resultLimit, clName }]); + return result[0]; } diff --git a/src/mahoji/commands/leaderboard.ts b/src/mahoji/commands/leaderboard.ts index f1c2e39cd4..f98a813caf 100644 --- a/src/mahoji/commands/leaderboard.ts +++ b/src/mahoji/commands/leaderboard.ts @@ -324,20 +324,10 @@ async function clLb( ) { const { resolvedCl, items } = getCollectionItems(inputType, false, false, true); if (!items || items.length === 0) { - return "That's not a valid collection log category. Check +cl for all possible logs."; + return "That's not a valid collection log category. Check /cl for all possible logs."; } - const userEventOrders = await prisma.userEvent.findMany({ - where: { - type: 'CLCompletion', - collection_log_name: resolvedCl.toLowerCase() - }, - orderBy: { - date: 'asc' - } - }); - - const users = await fetchCLLeaderboard({ ironmenOnly, items, resultLimit: 200, userEvents: userEventOrders }); + const { users } = await fetchCLLeaderboard({ ironmenOnly, items, resultLimit: 200, clName: resolvedCl }); inputType = toTitleCase(inputType.toLowerCase()); return doMenuWrapper({ diff --git a/src/mahoji/commands/rp.ts b/src/mahoji/commands/rp.ts index 99f8533f3f..0ca844d664 100644 --- a/src/mahoji/commands/rp.ts +++ b/src/mahoji/commands/rp.ts @@ -11,8 +11,7 @@ import { Bank } from 'oldschooljs'; import type { Item } from 'oldschooljs/dist/meta/types'; import { ADMIN_IDS, OWNER_IDS, SupportServer, production } from '../../config'; -import { analyticsTick } from '../../lib/analytics'; -import { BitField, Channel } from '../../lib/constants'; +import { BitField, Channel, globalConfig } from '../../lib/constants'; import { allCollectionLogsFlat } from '../../lib/data/Collections'; import type { GearSetupType } from '../../lib/gear/types'; import { GrandExchange } from '../../lib/grandExchange'; @@ -21,10 +20,11 @@ import { unEquipAllCommand } from '../../lib/minions/functions/unequipAllCommand import { unequipPet } from '../../lib/minions/functions/unequipPet'; import { premiumPatronTime } from '../../lib/premiumPatronTime'; +import { runRolesTask } from '../../lib/rolesTask'; import { TeamLoot } from '../../lib/simulation/TeamLoot'; import { SkillsEnum } from '../../lib/skilling/types'; import type { ItemBank } from '../../lib/types'; -import { dateFm, isValidDiscordSnowflake, returnStringOrFile } from '../../lib/util'; +import { dateFm, isValidDiscordSnowflake } from '../../lib/util'; import getOSItem from '../../lib/util/getOSItem'; import { handleMahojiConfirmation } from '../../lib/util/handleMahojiConfirmation'; import { deferInteraction } from '../../lib/util/interactionReply'; @@ -44,7 +44,14 @@ import { sellPriceOfItem } from './sell'; const itemFilters = [ { name: 'Tradeable', - filter: (item: Item) => itemIsTradeable(item.id, true) + filter: (item: Item) => itemIsTradeable(item.id, true), + run: async () => { + const isValid = await GrandExchange.extensiveVerification(); + if (isValid) { + return 'No issues found.'; + } + return 'Something was invalid. Check logs!'; + } } ]; @@ -142,6 +149,35 @@ function isProtectedAccount(user: MUser) { return false; } +const actions = [ + { + name: 'validate_ge', + allowed: (user: MUser) => ADMIN_IDS.includes(user.id) || OWNER_IDS.includes(user.id), + run: async () => { + const isValid = await GrandExchange.extensiveVerification(); + if (isValid) { + return 'No issues found.'; + } + return 'Something was invalid. Check logs!'; + } + }, + { + name: 'sync_roles', + allowed: (user: MUser) => + ADMIN_IDS.includes(user.id) || OWNER_IDS.includes(user.id) || user.bitfield.includes(BitField.isModerator), + run: async () => { + return runRolesTask(!globalConfig.isProduction); + } + }, + { + name: 'sync_usernames', + allowed: (user: MUser) => ADMIN_IDS.includes(user.id) || OWNER_IDS.includes(user.id), + run: async () => { + return usernameSync(); + } + } +]; + export const rpCommand: OSBMahojiCommand = { name: 'rp', description: 'Admin tools second set', @@ -150,45 +186,13 @@ export const rpCommand: OSBMahojiCommand = { { type: ApplicationCommandOptionType.SubcommandGroup, name: 'action', - description: 'Action tools', - options: [ - { - type: ApplicationCommandOptionType.Subcommand, - name: 'validate_ge', - description: 'Validate the g.e.', - options: [] - }, - { - type: ApplicationCommandOptionType.Subcommand, - name: 'patreon_reset', - description: 'Reset all patreon data.', - options: [] - }, - { - type: ApplicationCommandOptionType.Subcommand, - name: 'view_all_items', - description: 'View all item IDs present in banks/cls.', - options: [] - }, - { - type: ApplicationCommandOptionType.Subcommand, - name: 'analytics_tick', - description: 'analyticsTick.', - options: [] - }, - { - type: ApplicationCommandOptionType.Subcommand, - name: 'networth_sync', - description: 'networth_sync.', - options: [] - }, - { - type: ApplicationCommandOptionType.Subcommand, - name: 'redis_sync', - description: 'redis sync.', - options: [] - } - ] + description: 'Actions', + options: actions.map(a => ({ + type: ApplicationCommandOptionType.Subcommand, + name: a.name, + description: a.name, + options: [] + })) }, { type: ApplicationCommandOptionType.SubcommandGroup, @@ -520,14 +524,7 @@ export const rpCommand: OSBMahojiCommand = { max_total?: { user: MahojiUserOption; type: UserEventType; message_id: string }; max?: { user: MahojiUserOption; type: UserEventType; skill: xp_gains_skill_enum; message_id: string }; }; - action?: { - validate_ge?: {}; - patreon_reset?: {}; - view_all_items?: {}; - analytics_tick?: {}; - networth_sync?: {}; - redis_sync?: {}; - }; + action?: any; player?: { viewbank?: { user: MahojiUserOption; json?: boolean }; add_patron_time?: { user: MahojiUserOption; tier: number; time: string }; @@ -620,52 +617,13 @@ Date: ${dateFm(date)}`; if (!isMod) return randArrItem(gifs); - if (options.action?.validate_ge) { - const isValid = await GrandExchange.extensiveVerification(); - if (isValid) { - return 'No issues found.'; - } - return 'Something was invalid. Check logs!'; - } - if (options.action?.analytics_tick) { - await analyticsTick(); - return 'Finished.'; - } - if (options.action?.redis_sync) { - const result = await usernameSync(); - return result; - } - if (options.action?.networth_sync) { - const users = await prisma.user.findMany({ - where: { - GP: { - gt: 10_000_000_000 - } - }, - take: 20, - orderBy: { - GP: 'desc' - }, - select: { - id: true + if (options.action) { + for (const action of actions) { + if (options.action[action.name]) { + if (!action.allowed(adminUser)) return randArrItem(gifs); + return action.run(); } - }); - for (const { id } of users) { - const user = await mUserFetch(id); - await user.update({ - cached_networth_value: (await user.calculateNetWorth()).value - }); } - return 'Done.'; - } - if (options.action?.view_all_items) { - const result = await prisma.$queryRawUnsafe<{ item_id: number }[]>(`SELECT DISTINCT json_object_keys(bank)::int AS item_id -FROM users -UNION -SELECT DISTINCT jsonb_object_keys("collectionLogBank")::int AS item_id -FROM users -ORDER BY item_id ASC;`); - return returnStringOrFile(`[${result.map(i => i.item_id).join(',')}]`); } if (options.player?.set_buy_date) { diff --git a/tests/integration/misc.test.ts b/tests/integration/misc.test.ts index 2d113c1b15..eae4db6a9a 100644 --- a/tests/integration/misc.test.ts +++ b/tests/integration/misc.test.ts @@ -1,4 +1,3 @@ -import type { UserEvent } from '@prisma/client'; import { randArrItem } from 'e'; import { describe, expect, test } from 'vitest'; @@ -18,30 +17,14 @@ describe('Integration Misc', () => { expect(await global.prisma!.analytic.count()).toBeGreaterThanOrEqual(1); }); test('fetchCLLeaderboard', async () => { + const cl = randArrItem(allCollectionLogsFlat); for (const ironManOnly of [true, false]) { - for (const method of ['cl_array', 'raw_cl'] as const) { - for (const userEvents of [ - [ - { - id: 'asdf', - date: new Date(), - user_id: '123', - type: 'CLCompletion', - skill: null, - collection_log_name: 'giant mole' - } as UserEvent - ], - null - ]) { - await fetchCLLeaderboard({ - ironmenOnly: ironManOnly, - method, - userEvents, - resultLimit: 100, - items: randArrItem(allCollectionLogsFlat).items - }); - } - } + await fetchCLLeaderboard({ + ironmenOnly: ironManOnly, + resultLimit: 100, + items: cl.items, + clName: cl.name + }); } await Promise.all([fetchCLLeaderboard]); }); diff --git a/tests/integration/rolesTask.test.ts b/tests/integration/rolesTask.test.ts index 252bee8faf..dcd69d0dcc 100644 --- a/tests/integration/rolesTask.test.ts +++ b/tests/integration/rolesTask.test.ts @@ -43,7 +43,7 @@ describe.skip('Roles Task', async () => { duration: 10_000 } }); - const result = await runRolesTask(); + const result = await runRolesTask(true); expect(result).toBeTruthy(); expect(result).includes('Roles'); }); From 98904fab6163e409c06b8ca10c60f212198afce0 Mon Sep 17 00:00:00 2001 From: gc <30398469+gc@users.noreply.github.com> Date: Fri, 2 Aug 2024 17:15:00 +1000 Subject: [PATCH 04/27] Remove old sync_roles command --- src/mahoji/commands/admin.ts | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/mahoji/commands/admin.ts b/src/mahoji/commands/admin.ts index 64af8ef97a..631d507ccf 100644 --- a/src/mahoji/commands/admin.ts +++ b/src/mahoji/commands/admin.ts @@ -482,11 +482,6 @@ export const adminCommand: OSBMahojiCommand = { } ] }, - { - type: ApplicationCommandOptionType.Subcommand, - name: 'sync_roles', - description: 'Sync roles' - }, { type: ApplicationCommandOptionType.Subcommand, name: 'badges', @@ -665,7 +660,6 @@ export const adminCommand: OSBMahojiCommand = { sync_blacklist?: {}; loot_track?: { name: string }; cancel_task?: { user: MahojiUserOption }; - sync_roles?: {}; badges?: { user: MahojiUserOption; add?: string; remove?: string }; bypass_age?: { user: MahojiUserOption }; command?: { enable?: string; disable?: string }; @@ -697,20 +691,6 @@ export const adminCommand: OSBMahojiCommand = { minionActivityCacheDelete(user.id); return 'Done.'; } - if (options.sync_roles) { - // try { - // const result = await runRolesTask(); - // if (result.length < 2000) return result; - // return { - // content: 'The result was too big! Check the file.', - // files: [new AttachmentBuilder(Buffer.from(result), { name: 'roles.txt' })] - // }; - // } catch (err: any) { - // logError(err); - // return `Failed to run roles task. ${err.message}`; - // } - return 'The roles task is disabled for now.'; - } if (options.badges) { if ((!options.badges.remove && !options.badges.add) || (options.badges.add && options.badges.remove)) { From f24a0a46901a7f3c066a68c17e0f7bc492b2609f Mon Sep 17 00:00:00 2001 From: gc <30398469+gc@users.noreply.github.com> Date: Fri, 2 Aug 2024 17:15:17 +1000 Subject: [PATCH 05/27] Use a promise-queue for roles task and return file --- src/lib/rolesTask.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/lib/rolesTask.ts b/src/lib/rolesTask.ts index 91482bfae7..9a1ccbfaf0 100644 --- a/src/lib/rolesTask.ts +++ b/src/lib/rolesTask.ts @@ -6,8 +6,9 @@ import { getCollectionItems } from '../lib/data/Collections'; import { Minigames } from '../lib/settings/minigames'; import { Prisma } from '@prisma/client'; +import PQueue from 'p-queue'; import Skills from '../lib/skilling/skills'; -import { Stopwatch, convertXPtoLVL, getUsernameSync } from '../lib/util'; +import { Stopwatch, convertXPtoLVL, getUsernameSync, returnStringOrFile } from '../lib/util'; import { ClueTiers } from './clues/clueTiers'; import { TeamLoot } from './simulation/TeamLoot'; import type { ItemBank } from './types'; @@ -344,6 +345,8 @@ async function globalCL() { export async function runRolesTask(dryRun: boolean) { const results: RoleResult[] = []; + const promiseQueue = new PQueue({ concurrency: 2 }); + const tup = [ ['Top Slayer', fetchSlayerResults], ['Top Clue Hunters', topClueHunters], @@ -356,14 +359,14 @@ export async function runRolesTask(dryRun: boolean) { ['Global CL', globalCL] ] as const; - await Promise.all([ - ...tup.map(async ([name, fn]) => { + for (const [name, fn] of tup) { + promiseQueue.add(async () => { const stopwatch = new Stopwatch(); const res = await fn(); - console.log(`Ran ${name} in ${stopwatch.stop()}`); + console.log(`[RolesTask] Ran ${name} in ${stopwatch.stop()}`); results.push(...res); - }) - ]); + }); + } const allBadgeIDs = uniqueArr(results.map(i => i.badge)); const allRoleIDs = uniqueArr(results.map(i => i.roleID)); @@ -411,7 +414,10 @@ WHERE badges && ${badgeIDs} } } } - return `**Roles**\n${results.map(r => `${getUsernameSync(r.userID)} got ${roleNames.get(r.roleID)} because ${r.reason}`).join('\n')}`; + + return returnStringOrFile( + `**Roles**\n${results.map(r => `${getUsernameSync(r.userID)} got ${roleNames.get(r.roleID)} because ${r.reason}`).join('\n')}` + ); } return 'Dry run'; From dcc3a22ac65086531d3e30d64e84872b04208872 Mon Sep 17 00:00:00 2001 From: gc <30398469+gc@users.noreply.github.com> Date: Fri, 2 Aug 2024 17:30:17 +1000 Subject: [PATCH 06/27] Roles task improvements/fixes --- src/lib/rolesTask.ts | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/lib/rolesTask.ts b/src/lib/rolesTask.ts index 9a1ccbfaf0..48350015f5 100644 --- a/src/lib/rolesTask.ts +++ b/src/lib/rolesTask.ts @@ -368,6 +368,10 @@ export async function runRolesTask(dryRun: boolean) { }); } + await promiseQueue.onIdle(); + + debugLog(`Finished role functions, ${results.length} results`); + const allBadgeIDs = uniqueArr(results.map(i => i.badge)); const allRoleIDs = uniqueArr(results.map(i => i.roleID)); @@ -377,6 +381,8 @@ export async function runRolesTask(dryRun: boolean) { if (!supportServerGuild) throw new Error('No support guild'); // Remove all top badges from all users (and add back later) + + debugLog('Removing badges...'); const badgeIDs = `ARRAY[${allBadgeIDs.join(',')}]`; await prisma.$queryRawUnsafe(` UPDATE users @@ -385,22 +391,31 @@ WHERE badges && ${badgeIDs} `); // Remove roles from ineligible users + debugLog('Remove roles from ineligible users...'); for (const member of supportServerGuild.members.cache.values()) { const rolesToRemove = member.roles.cache.filter(r => allRoleIDs.includes(r.id)); if (rolesToRemove.size > 0) { - await member.roles.remove(rolesToRemove.map(r => r.id)).catch(noOp); + await member.roles.remove(rolesToRemove.map(r => r.id)).catch(console.error); } } // Add roles to users + debugLog('Add roles to users...'); for (const { userID, roleID, badge } of results) { - const role = await supportServerGuild.roles.fetch(roleID).catch(noOp); + const role = await supportServerGuild.roles.fetch(roleID).catch(console.error); const member = await supportServerGuild.members.fetch(userID).catch(noOp); - if (!member || !role) continue; + if (!member) { + debugLog(`Failed to find member ${userID}`); + continue; + } + if (!role) { + debugLog(`Failed to find role ${roleID}`); + continue; + } roleNames.set(roleID, role.name); if (!member.roles.cache.has(roleID)) { - await member.roles.add(roleID).catch(noOp); + await member.roles.add(roleID).catch(console.error); } if (badge) { From cd9f3bec240e000cf636b10be7e8b0fcb61a0fdd Mon Sep 17 00:00:00 2001 From: gc <30398469+gc@users.noreply.github.com> Date: Fri, 2 Aug 2024 18:14:15 +1000 Subject: [PATCH 07/27] Fix some issues --- src/lib/rolesTask.ts | 1 + src/mahoji/commands/admin.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib/rolesTask.ts b/src/lib/rolesTask.ts index 48350015f5..d25934040c 100644 --- a/src/lib/rolesTask.ts +++ b/src/lib/rolesTask.ts @@ -402,6 +402,7 @@ WHERE badges && ${badgeIDs} // Add roles to users debugLog('Add roles to users...'); for (const { userID, roleID, badge } of results) { + if (!userID) continue; const role = await supportServerGuild.roles.fetch(roleID).catch(console.error); const member = await supportServerGuild.members.fetch(userID).catch(noOp); if (!member) { diff --git a/src/mahoji/commands/admin.ts b/src/mahoji/commands/admin.ts index 631d507ccf..595f318521 100644 --- a/src/mahoji/commands/admin.ts +++ b/src/mahoji/commands/admin.ts @@ -864,6 +864,7 @@ export const adminCommand: OSBMahojiCommand = { ${META_CONSTANTS.RENDERED_STR}` }).catch(noOp); import('exit-hook').then(({ gracefulExit }) => gracefulExit(1)); + return; } if (options.shut_down) { debugLog('SHUTTING DOWN'); @@ -873,13 +874,14 @@ ${META_CONSTANTS.RENDERED_STR}` content: `Shutting down in ${dateFm(new Date(Date.now() + timer))}.` }); await economyLog('Flushing economy log due to shutdown', true); - await Promise.all([sleep(timer), GrandExchange.queue.onEmpty()]); + await Promise.all([sleep(timer), GrandExchange.queue.onIdle()]); await sendToChannelID(Channel.GeneralChannel, { content: `I am shutting down! Goodbye :( ${META_CONSTANTS.RENDERED_STR}` }).catch(noOp); import('exit-hook').then(({ gracefulExit }) => gracefulExit(0)); + return; } if (options.sync_blacklist) { From 21a377823052a1543d379932fb3f86bf2f906f32 Mon Sep 17 00:00:00 2001 From: gc <30398469+gc@users.noreply.github.com> Date: Fri, 2 Aug 2024 18:50:33 +1000 Subject: [PATCH 08/27] Fixes --- src/lib/rolesTask.ts | 4 ++-- src/lib/util/clLeaderboard.ts | 11 ++++++----- src/mahoji/commands/admin.ts | 4 ++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/lib/rolesTask.ts b/src/lib/rolesTask.ts index d25934040c..cf2952421f 100644 --- a/src/lib/rolesTask.ts +++ b/src/lib/rolesTask.ts @@ -8,7 +8,7 @@ import { Minigames } from '../lib/settings/minigames'; import { Prisma } from '@prisma/client'; import PQueue from 'p-queue'; import Skills from '../lib/skilling/skills'; -import { Stopwatch, convertXPtoLVL, getUsernameSync, returnStringOrFile } from '../lib/util'; +import { type CommandResponse, Stopwatch, convertXPtoLVL, getUsernameSync, returnStringOrFile } from '../lib/util'; import { ClueTiers } from './clues/clueTiers'; import { TeamLoot } from './simulation/TeamLoot'; import type { ItemBank } from './types'; @@ -342,7 +342,7 @@ async function globalCL() { return results; } -export async function runRolesTask(dryRun: boolean) { +export async function runRolesTask(dryRun: boolean): Promise { const results: RoleResult[] = []; const promiseQueue = new PQueue({ concurrency: 2 }); diff --git a/src/lib/util/clLeaderboard.ts b/src/lib/util/clLeaderboard.ts index fe7faab3bb..175a9c5596 100644 --- a/src/lib/util/clLeaderboard.ts +++ b/src/lib/util/clLeaderboard.ts @@ -33,17 +33,18 @@ export async function fetchMultipleCLLeaderboards( const userIds = Array.from(userEventMap.keys()); const userIdsList = userIds.length > 0 ? userIds.map(i => `'${i}'`).join(', ') : 'NULL'; - return prisma.$queryRawUnsafe<{ id: string; qty: number }[]>(` + const query = ` SELECT id, qty FROM ( SELECT id, CARDINALITY(cl_array & ${SQL_ITEMS}) AS qty FROM users WHERE (cl_array && ${SQL_ITEMS} - ${ironmenOnly ? 'AND "users"."minion.ironman" = true' : ''}) ${userIdsList.length > 0 ? `OR id IN (${userIdsList})` : ''} - LIMIT ${resultLimit} + ${ironmenOnly ? 'AND "users"."minion.ironman" = true' : ''}) ${userIds.length > 0 ? `OR id IN (${userIdsList})` : ''} ) AS subquery -ORDER BY qty DESC; -`); +ORDER BY qty DESC +LIMIT ${resultLimit}; +`; + return prisma.$queryRawUnsafe<{ id: string; qty: number }[]>(query); }) ]); diff --git a/src/mahoji/commands/admin.ts b/src/mahoji/commands/admin.ts index 595f318521..92701fbe01 100644 --- a/src/mahoji/commands/admin.ts +++ b/src/mahoji/commands/admin.ts @@ -864,7 +864,7 @@ export const adminCommand: OSBMahojiCommand = { ${META_CONSTANTS.RENDERED_STR}` }).catch(noOp); import('exit-hook').then(({ gracefulExit }) => gracefulExit(1)); - return; + return 'Turning off...'; } if (options.shut_down) { debugLog('SHUTTING DOWN'); @@ -881,7 +881,7 @@ ${META_CONSTANTS.RENDERED_STR}` ${META_CONSTANTS.RENDERED_STR}` }).catch(noOp); import('exit-hook').then(({ gracefulExit }) => gracefulExit(0)); - return; + return 'Turning off...'; } if (options.sync_blacklist) { From 13836a0ddad23614d5796e895bd3988be2908533 Mon Sep 17 00:00:00 2001 From: gc <30398469+gc@users.noreply.github.com> Date: Fri, 2 Aug 2024 20:18:01 +1000 Subject: [PATCH 09/27] Roles task fixes --- package.json | 1 + src/lib/rolesTask.ts | 48 +++++++++++++++++++++++++------------------- yarn.lock | 17 ++++++++++++++++ 3 files changed, 45 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index fbd80a360a..69c6554519 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "p-queue": "^6.6.2", "piscina": "^4.6.1", "random-js": "^2.1.0", + "remeda": "^2.7.0", "simple-statistics": "^7.8.3", "sonic-boom": "^4.0.1", "zlib-sync": "^0.1.9", diff --git a/src/lib/rolesTask.ts b/src/lib/rolesTask.ts index cf2952421f..b9c7d0e375 100644 --- a/src/lib/rolesTask.ts +++ b/src/lib/rolesTask.ts @@ -7,19 +7,22 @@ import { Minigames } from '../lib/settings/minigames'; import { Prisma } from '@prisma/client'; import PQueue from 'p-queue'; -import Skills from '../lib/skilling/skills'; +import { partition } from 'remeda'; +import z from 'zod'; import { type CommandResponse, Stopwatch, convertXPtoLVL, getUsernameSync, returnStringOrFile } from '../lib/util'; import { ClueTiers } from './clues/clueTiers'; import { TeamLoot } from './simulation/TeamLoot'; +import { SkillsArray } from './skilling/types'; import type { ItemBank } from './types'; import { fetchMultipleCLLeaderboards } from './util/clLeaderboard'; -interface RoleResult { - roleID: string; - userID: string; - reason: string; - badge?: (typeof BadgesEnum)[keyof typeof BadgesEnum]; -} +const RoleResultSchema = z.object({ + roleID: z.string().min(17).max(19), + userID: z.string().min(17).max(19), + reason: z.string(), + badge: z.number().int().optional() +}); +type RoleResult = z.infer; const minigames = Minigames.map(game => game.column).filter(i => i !== 'tithe_farm'); @@ -45,24 +48,24 @@ for (const cl of CLS_THAT_GET_ROLE) { async function topSkillers() { const results: RoleResult[] = []; - const skillVals = Object.values(Skills); const [top200TotalXPUsers, ...top200ms] = await prisma.$transaction([ prisma.$queryRawUnsafe( - `SELECT id, ${skillVals.map(s => `"skills.${s.id}"`)}, ${skillVals - .map(s => `"skills.${s.id}"::bigint`) - .join(' + ')} as totalxp FROM users ORDER BY totalxp DESC LIMIT 200;` + `SELECT id, ${SkillsArray.map(s => `"skills.${s}"`)}, ${SkillsArray.map(s => `"skills.${s}"::bigint`).join( + ' + ' + )} as totalxp FROM users ORDER BY totalxp DESC LIMIT 200;` ), - ...skillVals.map(s => - prisma.$queryRawUnsafe<{ + ...SkillsArray.map(s => { + const query = `SELECT id, "skills.${s}" AS xp, '${s}' AS skill FROM users ORDER BY xp DESC LIMIT 1;`; + return prisma.$queryRawUnsafe<{ id: string; xp: string; skill: string; - }>(`SELECT id, "skills.${s.id}" AS xp, '${s.id}' AS skill FROM users ORDER BY xp DESC LIMIT 1;`) - ) + }>(query); + }) ]); - for (const { id, skill } of top200ms) { + for (const { id, skill } of top200ms.flat()) { results.push({ userID: id, roleID: Roles.TopSkiller, @@ -74,8 +77,8 @@ async function topSkillers() { const rankOneTotal = top200TotalXPUsers .map((u: any) => { let totalLevel = 0; - for (const skill of skillVals) { - totalLevel += convertXPtoLVL(Number(u[`skills.${skill.id}` as keyof any]) as any); + for (const skill of SkillsArray) { + totalLevel += convertXPtoLVL(Number(u[`skills.${skill}` as keyof any]) as any); } return { id: u.id, @@ -196,7 +199,7 @@ LIMIT 1;` ) ); - for (const res of topClueHunters) { + for (const res of topClueHunters.flat()) { results.push({ userID: res.user_id, roleID: Roles.TopClueHunter, @@ -364,7 +367,11 @@ export async function runRolesTask(dryRun: boolean): Promise { const stopwatch = new Stopwatch(); const res = await fn(); console.log(`[RolesTask] Ran ${name} in ${stopwatch.stop()}`); - results.push(...res); + const [validResults, invalidResults] = partition(res, i => RoleResultSchema.safeParse(i).success); + results.push(...validResults); + if (invalidResults.length > 0) { + console.error(`[RolesTask] Invalid results for ${name}: ${JSON.stringify(invalidResults)}`); + } }); } @@ -381,7 +388,6 @@ export async function runRolesTask(dryRun: boolean): Promise { if (!supportServerGuild) throw new Error('No support guild'); // Remove all top badges from all users (and add back later) - debugLog('Removing badges...'); const badgeIDs = `ARRAY[${allBadgeIDs.join(',')}]`; await prisma.$queryRawUnsafe(` diff --git a/yarn.lock b/yarn.lock index be7b9c09c5..5a427b1ee5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3972,6 +3972,15 @@ __metadata: languageName: node linkType: hard +"remeda@npm:^2.7.0": + version: 2.7.0 + resolution: "remeda@npm:2.7.0" + dependencies: + type-fest: "npm:^4.21.0" + checksum: 10c0/4e7d0dc616f00961653244ea9df3f297720fc9346ac8ec7502abf4c434741af4a4750d5bd83ea9938ee406089b37e3a2270b8f022d48b345ba83218e47dd8918 + languageName: node + linkType: hard + "require-directory@npm:^2.1.1": version: 2.1.1 resolution: "require-directory@npm:2.1.1" @@ -4148,6 +4157,7 @@ __metadata: prettier: "npm:^3.3.2" prisma: "npm:^5.17.0" random-js: "npm:^2.1.0" + remeda: "npm:^2.7.0" simple-statistics: "npm:^7.8.3" sonic-boom: "npm:^4.0.1" tsx: "npm:^4.16.2" @@ -4613,6 +4623,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^4.21.0": + version: 4.23.0 + resolution: "type-fest@npm:4.23.0" + checksum: 10c0/c42bb14e99329ab37983d1f188e307bf0cc705a23807d9b2268d8fb2ae781d610ac6e2058dde8f9ea2b1b8ddc77ceb578d157fa81f69f8f70aef1d42fb002996 + languageName: node + linkType: hard + "typescript@npm:^5.2.2, typescript@npm:^5.5.3": version: 5.5.3 resolution: "typescript@npm:5.5.3" From f3d124d0163017257e45cc54ebb7fbe0ea35e775 Mon Sep 17 00:00:00 2001 From: nwjgit <69014816+nwjgit@users.noreply.github.com> Date: Tue, 6 Aug 2024 05:15:22 -0500 Subject: [PATCH 10/27] Fix CA & few adjustments (#5981) --- src/lib/combat_achievements/combatAchievements.ts | 8 ++++---- src/lib/combat_achievements/hard.ts | 2 +- src/lib/combat_achievements/medium.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lib/combat_achievements/combatAchievements.ts b/src/lib/combat_achievements/combatAchievements.ts index 5545b07c0d..5aef785fb0 100644 --- a/src/lib/combat_achievements/combatAchievements.ts +++ b/src/lib/combat_achievements/combatAchievements.ts @@ -162,13 +162,13 @@ const indexesWithRng = entries.flatMap(i => i[1].tasks.filter(t => 'rng' in t)); export const combatAchievementTripEffect = async ({ data, messages, user }: Parameters[0]) => { const dataCopy = deepClone(data); if (dataCopy.type === 'Inferno' && !dataCopy.diedPreZuk && !dataCopy.diedZuk) { - (dataCopy as any).quantity = 1; + (dataCopy as any).q = 1; } if (dataCopy.type === 'Colosseum') { - (dataCopy as any).quantity = 1; + (dataCopy as any).q = 1; } - if (!('quantity' in dataCopy)) return; - let quantity = Number(dataCopy.quantity); + if (!('q' in dataCopy)) return; + let quantity = Number(dataCopy.q); if (Number.isNaN(quantity)) return; if (data.type === 'TombsOfAmascut') { diff --git a/src/lib/combat_achievements/hard.ts b/src/lib/combat_achievements/hard.ts index 82f33c3077..1522239efb 100644 --- a/src/lib/combat_achievements/hard.ts +++ b/src/lib/combat_achievements/hard.ts @@ -597,7 +597,7 @@ export const hardCombatAchievements: CombatAchievement[] = [ monster: 'Tempoross', desc: 'Subdue Tempoross, getting rewarded with 10 reward permits from a single Tempoross fight.', rng: { - chancePerKill: 30, + chancePerKill: 5, hasChance: data => data.type === 'Tempoross' } }, diff --git a/src/lib/combat_achievements/medium.ts b/src/lib/combat_achievements/medium.ts index 420f3c1b43..30d8255701 100644 --- a/src/lib/combat_achievements/medium.ts +++ b/src/lib/combat_achievements/medium.ts @@ -431,7 +431,7 @@ export const mediumCombatAchievements: CombatAchievement[] = [ monster: 'Skotizo', desc: 'Kill Skotizo with no altars active.', rng: { - chancePerKill: 15, + chancePerKill: 5, hasChance: isCertainMonsterTrip(Monsters.Skotizo.id) } }, From d0193ffa25f84f5be1bd10918eb19969151bd38f Mon Sep 17 00:00:00 2001 From: GC <30398469+gc@users.noreply.github.com> Date: Tue, 6 Aug 2024 20:52:47 +1000 Subject: [PATCH 11/27] Fix docker (#5987) --- .github/workflows/integration_tests.yml | 2 +- package.json | 2 +- tests/integration/redis.test.ts | 8 ++++---- yarn.lock | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index 165927f7c5..5442864076 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -20,4 +20,4 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Run Integration Tests - run: docker-compose up --build --abort-on-container-exit + run: docker compose up --build --abort-on-container-exit --remove-orphans && docker compose down --volumes --remove-orphans diff --git a/package.json b/package.json index 69c6554519..0e160aa765 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ }, "dependencies": { "@napi-rs/canvas": "^0.1.53", - "@oldschoolgg/toolkit": "git+https://github.com/oldschoolgg/toolkit.git#2813f25327093fcf2cb12bee7d4c85ce629069a0", + "@oldschoolgg/toolkit": "git+https://github.com/oldschoolgg/toolkit.git#cd7c6865229ca7dc4a66b3816586f2d3f4a4fbed", "@prisma/client": "^5.17.0", "@sapphire/ratelimits": "^2.4.9", "@sapphire/snowflake": "^3.5.3", diff --git a/tests/integration/redis.test.ts b/tests/integration/redis.test.ts index 7584d1bd67..1407d57583 100644 --- a/tests/integration/redis.test.ts +++ b/tests/integration/redis.test.ts @@ -15,7 +15,7 @@ test('Should add patron badge', async () => { const user = await createTestUser(); expect(user.user.badges).not.includes(BadgesEnum.Patron); const _redis = makeSender(); - _redis.publish({ + await _redis.publish({ type: 'patron_tier_change', discord_ids: [user.id], new_tier: 1, @@ -31,7 +31,7 @@ test('Should remove patron badge', async () => { const user = await createTestUser(undefined, { badges: [BadgesEnum.Patron] }); expect(user.user.badges).includes(BadgesEnum.Patron); const _redis = makeSender(); - _redis.publish({ + await _redis.publish({ type: 'patron_tier_change', discord_ids: [user.id], new_tier: 0, @@ -52,7 +52,7 @@ test('Should add to cache', async () => { })) }); const _redis = makeSender(); - _redis.publish({ + await _redis.publish({ type: 'patron_tier_change', discord_ids: users.map(u => u.id), new_tier: 5, @@ -76,7 +76,7 @@ test('Should remove from cache', async () => { })) }); const _redis = makeSender(); - _redis.publish({ + await _redis.publish({ type: 'patron_tier_change', discord_ids: users.map(u => u.id), new_tier: 0, diff --git a/yarn.lock b/yarn.lock index 5a427b1ee5..5dfb8015f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -646,9 +646,9 @@ __metadata: languageName: node linkType: hard -"@oldschoolgg/toolkit@git+https://github.com/oldschoolgg/toolkit.git#2813f25327093fcf2cb12bee7d4c85ce629069a0": +"@oldschoolgg/toolkit@git+https://github.com/oldschoolgg/toolkit.git#cd7c6865229ca7dc4a66b3816586f2d3f4a4fbed": version: 0.0.24 - resolution: "@oldschoolgg/toolkit@https://github.com/oldschoolgg/toolkit.git#commit=2813f25327093fcf2cb12bee7d4c85ce629069a0" + resolution: "@oldschoolgg/toolkit@https://github.com/oldschoolgg/toolkit.git#commit=cd7c6865229ca7dc4a66b3816586f2d3f4a4fbed" dependencies: decimal.js: "npm:^10.4.3" deep-object-diff: "npm:^1.1.9" @@ -664,7 +664,7 @@ __metadata: peerDependencies: discord.js: ^14.15.3 oldschooljs: ^2.5.9 - checksum: 10c0/c83f2188e18ac1e7d79edd9ab06b7ab0f96f8774a404a26fd766d22606bd65a8def0c0a74d0f681c5bde0d7b5b5bbb16f829e751b4e75f6821a1baf5c62b2580 + checksum: 10c0/42eaec1c99c671adab7b56ca7e11d37bf5a0e07d0f0da0a892cdf477a78c061ea131a43b1c578d09f1c6b02e05d1ce47db9586ad9a8de62679cc492c847c3fca languageName: node linkType: hard @@ -4122,7 +4122,7 @@ __metadata: dependencies: "@biomejs/biome": "npm:^1.8.3" "@napi-rs/canvas": "npm:^0.1.53" - "@oldschoolgg/toolkit": "git+https://github.com/oldschoolgg/toolkit.git#2813f25327093fcf2cb12bee7d4c85ce629069a0" + "@oldschoolgg/toolkit": "git+https://github.com/oldschoolgg/toolkit.git#cd7c6865229ca7dc4a66b3816586f2d3f4a4fbed" "@prisma/client": "npm:^5.17.0" "@sapphire/ratelimits": "npm:^2.4.9" "@sapphire/snowflake": "npm:^3.5.3" From cc17c1cb45661fdd1719f105ea56083a2200d655 Mon Sep 17 00:00:00 2001 From: GC <30398469+gc@users.noreply.github.com> Date: Tue, 6 Aug 2024 21:20:58 +1000 Subject: [PATCH 12/27] Add rebase cmd action (#5988) --- .github/workflows/rebase_command.yml | 20 ++++++++++++++++++++ .github/workflows/slash_command_dispatch.yml | 15 +++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 .github/workflows/rebase_command.yml create mode 100644 .github/workflows/slash_command_dispatch.yml diff --git a/.github/workflows/rebase_command.yml b/.github/workflows/rebase_command.yml new file mode 100644 index 0000000000..5ec001e848 --- /dev/null +++ b/.github/workflows/rebase_command.yml @@ -0,0 +1,20 @@ +name: rebase-command +on: + repository_dispatch: + types: [rebase-command] +jobs: + rebase: + runs-on: ubuntu-latest + steps: + - uses: peter-evans/rebase@v3 + id: rebase + with: + head: ${{ github.event.client_payload.pull_request.head.label }} + - name: Add reaction + if: steps.rebase.outputs.rebased-count == 1 + uses: peter-evans/create-or-update-comment@v1 + with: + token: ${{ secrets.REBASE_TOKEN }} + repository: ${{ github.event.client_payload.github.payload.repository.full_name }} + comment-id: ${{ github.event.client_payload.github.payload.comment.id }} + reaction-type: hooray diff --git a/.github/workflows/slash_command_dispatch.yml b/.github/workflows/slash_command_dispatch.yml new file mode 100644 index 0000000000..fe6aa33450 --- /dev/null +++ b/.github/workflows/slash_command_dispatch.yml @@ -0,0 +1,15 @@ +name: Slash Command Dispatch +on: + issue_comment: + types: [created] +jobs: + slashCommandDispatch: + runs-on: ubuntu-latest + steps: + - name: Slash Command Dispatch + uses: peter-evans/slash-command-dispatch@v3 + with: + token: ${{ secrets.REBASE_TOKEN }} + commands: rebase + permission: write + issue-type: pull-request From ee2be2d73443c174995cf5c862d65e860d3dfd67 Mon Sep 17 00:00:00 2001 From: GC <30398469+gc@users.noreply.github.com> Date: Tue, 6 Aug 2024 22:03:49 +1000 Subject: [PATCH 13/27] Remove rebase command stuff (#5989) --- .github/workflows/rebase_command.yml | 20 -------------------- .github/workflows/slash_command_dispatch.yml | 15 --------------- 2 files changed, 35 deletions(-) delete mode 100644 .github/workflows/rebase_command.yml delete mode 100644 .github/workflows/slash_command_dispatch.yml diff --git a/.github/workflows/rebase_command.yml b/.github/workflows/rebase_command.yml deleted file mode 100644 index 5ec001e848..0000000000 --- a/.github/workflows/rebase_command.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: rebase-command -on: - repository_dispatch: - types: [rebase-command] -jobs: - rebase: - runs-on: ubuntu-latest - steps: - - uses: peter-evans/rebase@v3 - id: rebase - with: - head: ${{ github.event.client_payload.pull_request.head.label }} - - name: Add reaction - if: steps.rebase.outputs.rebased-count == 1 - uses: peter-evans/create-or-update-comment@v1 - with: - token: ${{ secrets.REBASE_TOKEN }} - repository: ${{ github.event.client_payload.github.payload.repository.full_name }} - comment-id: ${{ github.event.client_payload.github.payload.comment.id }} - reaction-type: hooray diff --git a/.github/workflows/slash_command_dispatch.yml b/.github/workflows/slash_command_dispatch.yml deleted file mode 100644 index fe6aa33450..0000000000 --- a/.github/workflows/slash_command_dispatch.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Slash Command Dispatch -on: - issue_comment: - types: [created] -jobs: - slashCommandDispatch: - runs-on: ubuntu-latest - steps: - - name: Slash Command Dispatch - uses: peter-evans/slash-command-dispatch@v3 - with: - token: ${{ secrets.REBASE_TOKEN }} - commands: rebase - permission: write - issue-type: pull-request From 23a85fed09c2db87faa2b36b8b72f8e4d19029fd Mon Sep 17 00:00:00 2001 From: Keres <88959451+DaughtersOfNyx@users.noreply.github.com> Date: Tue, 6 Aug 2024 22:04:17 +1000 Subject: [PATCH 14/27] Fix incorrect command name in Hunt (#5986) --- src/mahoji/lib/abstracted_commands/mageTrainingArenaCommand.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mahoji/lib/abstracted_commands/mageTrainingArenaCommand.ts b/src/mahoji/lib/abstracted_commands/mageTrainingArenaCommand.ts index 1ee8cb5719..3c71531837 100644 --- a/src/mahoji/lib/abstracted_commands/mageTrainingArenaCommand.ts +++ b/src/mahoji/lib/abstracted_commands/mageTrainingArenaCommand.ts @@ -113,7 +113,7 @@ ${mageTrainingArenaBuyables .map(i => `${i.item.name} - ${i.cost} pts - ${formatDuration((i.cost / pizazzPointsPerHour) * (Time.Minute * 60))}`) .join('\n')} -Hint: Magic Training Arena is combined into 1 room, and 1 set of points - rewards take approximately the same amount of time to get. To get started use **/minigames mage_training_arena train**. You can buy rewards using **/minigames mage_training_arena buy**.`; +Hint: Magic Training Arena is combined into 1 room, and 1 set of points - rewards take approximately the same amount of time to get. To get started use **/minigames mage_training_arena start**. You can buy rewards using **/minigames mage_training_arena buy**.`; } export async function mageTrainingArenaStartCommand(user: MUser, channelID: string): CommandResponse { From cf085e45c6b1357d0956326ee3a0b09950313c22 Mon Sep 17 00:00:00 2001 From: Keres <88959451+DaughtersOfNyx@users.noreply.github.com> Date: Tue, 6 Aug 2024 22:13:25 +1000 Subject: [PATCH 15/27] Fix typo in the finished hunting message (#5978) --- src/tasks/minions/HunterActivity/hunterActivity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tasks/minions/HunterActivity/hunterActivity.ts b/src/tasks/minions/HunterActivity/hunterActivity.ts index 914d29dfdd..7a333ffdc2 100644 --- a/src/tasks/minions/HunterActivity/hunterActivity.ts +++ b/src/tasks/minions/HunterActivity/hunterActivity.ts @@ -179,7 +179,7 @@ export const hunterTask: MinionTask = { let str = `${user}, ${user.minionName} finished hunting ${creature.name}${ crystalImpling ? '.' - : `${quantity}x times, due to clever creatures you missed out on ${ + : ` ${quantity}x times, due to clever creatures you missed out on ${ quantity - successfulQuantity }x catches. ` }${xpStr}\n\nYou received: ${loot}.${magicSecStr.length > 1 ? magicSecStr : ''}`; From eb0dc0ad30a25e7eed7246a646f57cea36ab8f08 Mon Sep 17 00:00:00 2001 From: nwjgit <69014816+nwjgit@users.noreply.github.com> Date: Tue, 6 Aug 2024 07:15:39 -0500 Subject: [PATCH 16/27] Allow multiple combat options for PVM trips (#5962) --- .../data/killableMonsters/vannakaMonsters.ts | 1 - src/lib/minions/functions/index.ts | 4 +- src/lib/minions/types.ts | 2 +- src/lib/slayer/slayerUtil.ts | 67 +++++++++++-------- .../abstracted_commands/autoSlayCommand.ts | 18 ++--- .../lib/abstracted_commands/minionKill.ts | 49 ++++++++------ 6 files changed, 80 insertions(+), 61 deletions(-) diff --git a/src/lib/minions/data/killableMonsters/vannakaMonsters.ts b/src/lib/minions/data/killableMonsters/vannakaMonsters.ts index d6c0feb8b6..23e6c5ff52 100644 --- a/src/lib/minions/data/killableMonsters/vannakaMonsters.ts +++ b/src/lib/minions/data/killableMonsters/vannakaMonsters.ts @@ -243,7 +243,6 @@ export const vannakaMonsters: KillableMonster[] = [ healAmountNeeded: 12, attackStyleToUse: GearStat.AttackRanged, attackStylesUsed: [GearStat.AttackMagic], - canCannon: true, pkActivityRating: 4, pkBaseDeathChance: 6, revsWeaponBoost: true diff --git a/src/lib/minions/functions/index.ts b/src/lib/minions/functions/index.ts index f3bd7b49eb..2464f0acec 100644 --- a/src/lib/minions/functions/index.ts +++ b/src/lib/minions/functions/index.ts @@ -50,7 +50,7 @@ export function resolveAttackStyles( // Automatically use magic if barrage/burst is chosen if ( params.boostMethod && - (params.boostMethod === 'barrage' || params.boostMethod === 'burst') && + (params.boostMethod.includes('barrage') || params.boostMethod.includes('burst')) && !attackStyles.includes(SkillsEnum.Magic) ) { if (attackStyles.includes(SkillsEnum.Defence)) { @@ -63,7 +63,7 @@ export function resolveAttackStyles( } export async function addMonsterXP(user: MUser, params: AddMonsterXpParams) { - const boostMethod = params.burstOrBarrage ? 'barrage' : 'none'; + const boostMethod = params.burstOrBarrage ? ['barrage'] : ['none']; const [, osjsMon, attackStyles] = resolveAttackStyles(user, { monsterID: params.monsterID, diff --git a/src/lib/minions/types.ts b/src/lib/minions/types.ts index e5efadbb39..cc81695e13 100644 --- a/src/lib/minions/types.ts +++ b/src/lib/minions/types.ts @@ -177,7 +177,7 @@ export interface AddMonsterXpParams { export interface ResolveAttackStylesParams { monsterID: number | undefined; - boostMethod?: string; + boostMethod?: string[]; } export interface BlowpipeData { diff --git a/src/lib/slayer/slayerUtil.ts b/src/lib/slayer/slayerUtil.ts index 8ed9e4cdbb..c1d9d5cc4e 100644 --- a/src/lib/slayer/slayerUtil.ts +++ b/src/lib/slayer/slayerUtil.ts @@ -34,42 +34,53 @@ interface DetermineBoostParams { cbOpts: CombatOptionsEnum[]; user: MUser; monster: KillableMonster; - method?: PvMMethod | null; + methods?: PvMMethod[] | null; isOnTask?: boolean; wildyBurst?: boolean; } -export function determineBoostChoice(params: DetermineBoostParams) { - let boostChoice = 'none'; +export function determineCombatBoosts(params: DetermineBoostParams) { + // if EHP slayer (PvMMethod) the methods are initialized with boostMethods variable + const boostMethods = (params.methods ?? ['none']).flat().filter(method => method); - if (params.method && params.method === 'none') { - return boostChoice; + // check if user has cannon combat option turned on + if (params.cbOpts.includes(CombatOptionsEnum.AlwaysCannon)) { + boostMethods.includes('cannon') ? null : boostMethods.push('cannon'); } - if (params.method && params.method === 'chinning') { - boostChoice = 'chinning'; - } else if (params.method && params.method === 'barrage') { - boostChoice = 'barrage'; - } else if (params.method && params.method === 'burst') { - boostChoice = 'burst'; - } else if (params.method && params.method === 'cannon') { - boostChoice = 'cannon'; - } else if ( - params.cbOpts.includes(CombatOptionsEnum.AlwaysIceBarrage) && - (params.monster?.canBarrage || params.wildyBurst) - ) { - boostChoice = 'barrage'; - } else if ( - params.cbOpts.includes(CombatOptionsEnum.AlwaysIceBurst) && - (params.monster?.canBarrage || params.wildyBurst) - ) { - boostChoice = 'burst'; - } else if (params.cbOpts.includes(CombatOptionsEnum.AlwaysCannon)) { - boostChoice = 'cannon'; + + // check for special burst case under wildyBurst variable + if (params.wildyBurst) { + if (params.cbOpts.includes(CombatOptionsEnum.AlwaysIceBarrage)) { + boostMethods.includes('barrage') ? null : boostMethods.push('barrage'); + } + if (params.cbOpts.includes(CombatOptionsEnum.AlwaysIceBurst)) { + boostMethods.includes('burst') ? null : boostMethods.push('burst'); + } } - if (boostChoice === 'barrage' && params.user.skillLevel(SkillsEnum.Magic) < 94) { - boostChoice = 'burst'; + // check if the monster can be barraged + if (params.monster.canBarrage) { + // check if the monster exists in catacombs + if (params.monster.existsInCatacombs) { + if (params.cbOpts.includes(CombatOptionsEnum.AlwaysIceBarrage)) { + boostMethods.includes('barrage') ? null : boostMethods.push('barrage'); + } + if (params.cbOpts.includes(CombatOptionsEnum.AlwaysIceBurst)) { + boostMethods.includes('burst') ? null : boostMethods.push('burst'); + } + } else if (!params.monster.cannonMulti) { + // prevents cases such as: cannoning in singles but receiving multi combat bursting boost + return boostMethods; + } else { + if (params.cbOpts.includes(CombatOptionsEnum.AlwaysIceBarrage)) { + boostMethods.includes('barrage') ? null : boostMethods.push('barrage'); + } + if (params.cbOpts.includes(CombatOptionsEnum.AlwaysIceBurst)) { + boostMethods.includes('burst') ? null : boostMethods.push('burst'); + } + } } - return boostChoice; + + return boostMethods; } export async function calculateSlayerPoints(currentStreak: number, master: SlayerMaster, user: MUser) { diff --git a/src/mahoji/lib/abstracted_commands/autoSlayCommand.ts b/src/mahoji/lib/abstracted_commands/autoSlayCommand.ts index 6d2d7183dc..f6e99e8ad3 100644 --- a/src/mahoji/lib/abstracted_commands/autoSlayCommand.ts +++ b/src/mahoji/lib/abstracted_commands/autoSlayCommand.ts @@ -17,7 +17,7 @@ interface AutoslayLink { // Name and Monster must be specified if either is. efficientName?: string; efficientMonster?: number; - efficientMethod?: PvMMethod; + efficientMethod?: PvMMethod | PvMMethod[]; slayerMasters?: SlayerMasterEnum[]; } @@ -147,7 +147,7 @@ const AutoSlayMaxEfficiencyTable: AutoslayLink[] = [ monsterID: Monsters.SmokeDevil.id, efficientName: Monsters.SmokeDevil.name, efficientMonster: Monsters.SmokeDevil.id, - efficientMethod: 'barrage' + efficientMethod: ['barrage', 'cannon'] }, { monsterID: Monsters.DarkBeast.id, @@ -216,13 +216,13 @@ const WildyAutoSlayMaxEfficiencyTable: AutoslayLink[] = [ monsterID: Monsters.AbyssalDemon.id, efficientName: Monsters.AbyssalDemon.name, efficientMonster: Monsters.AbyssalDemon.id, - efficientMethod: 'barrage' + efficientMethod: ['barrage', 'cannon'] }, { monsterID: Monsters.Ankou.id, efficientName: Monsters.Ankou.name, efficientMonster: Monsters.Ankou.id, - efficientMethod: 'barrage' + efficientMethod: ['barrage', 'cannon'] }, { monsterID: Monsters.BlackDemon.id, @@ -240,7 +240,7 @@ const WildyAutoSlayMaxEfficiencyTable: AutoslayLink[] = [ monsterID: Monsters.Bloodveld.id, efficientName: Monsters.Bloodveld.name, efficientMonster: Monsters.Bloodveld.id, - efficientMethod: 'barrage' + efficientMethod: 'none' }, { monsterID: Monsters.ChaosDruid.id, @@ -264,7 +264,7 @@ const WildyAutoSlayMaxEfficiencyTable: AutoslayLink[] = [ monsterID: Monsters.DustDevil.id, efficientName: Monsters.DustDevil.name, efficientMonster: Monsters.DustDevil.id, - efficientMethod: 'barrage' + efficientMethod: ['barrage', 'cannon'] }, { monsterID: Monsters.ElderChaosDruid.id, @@ -318,7 +318,7 @@ const WildyAutoSlayMaxEfficiencyTable: AutoslayLink[] = [ monsterID: Monsters.Jelly.id, efficientName: Monsters.Jelly.name, efficientMonster: Monsters.Jelly.id, - efficientMethod: 'barrage' + efficientMethod: ['barrage', 'cannon'] }, { monsterID: Monsters.LesserDemon.id, @@ -348,7 +348,7 @@ const WildyAutoSlayMaxEfficiencyTable: AutoslayLink[] = [ monsterID: Monsters.GreaterNechryael.id, efficientName: Monsters.GreaterNechryael.name, efficientMonster: Monsters.GreaterNechryael.id, - efficientMethod: 'barrage' + efficientMethod: ['barrage', 'cannon'] }, { monsterID: Monsters.RevenantImp.id, @@ -485,7 +485,7 @@ export async function autoSlayCommand({ name: ehpMonster.efficientName }; if (ehpMonster.efficientMethod) { - args.method = ehpMonster.efficientMethod; + args.method = ehpMonster.efficientMethod as unknown as CommandOptions; } runCommand({ commandName: 'k', diff --git a/src/mahoji/lib/abstracted_commands/minionKill.ts b/src/mahoji/lib/abstracted_commands/minionKill.ts index 94bd7ad621..f729fee9a7 100644 --- a/src/mahoji/lib/abstracted_commands/minionKill.ts +++ b/src/mahoji/lib/abstracted_commands/minionKill.ts @@ -48,7 +48,7 @@ import type { Consumable } from '../../../lib/minions/types'; import { calcPOHBoosts } from '../../../lib/poh'; import { SkillsEnum } from '../../../lib/skilling/types'; import { SlayerTaskUnlocksEnum } from '../../../lib/slayer/slayerUnlocks'; -import { determineBoostChoice, getUsersCurrentSlayerInfo } from '../../../lib/slayer/slayerUtil'; +import { determineCombatBoosts, getUsersCurrentSlayerInfo } from '../../../lib/slayer/slayerUtil'; import { maxOffenceStats } from '../../../lib/structures/Gear'; import type { Peak } from '../../../lib/tickers'; import type { MonsterActivityTaskOptions } from '../../../lib/types/minions'; @@ -134,7 +134,7 @@ export async function minionKillCommand( channelID: string, name: string, quantity: number | undefined, - method: PvMMethod | undefined, + method: PvMMethod | PvMMethod[] | undefined, wilderness: boolean | undefined, solo: boolean | undefined ): Promise { @@ -206,19 +206,18 @@ export async function minionKillCommand( } } - // Add jelly check as can barrage in wilderness + // Add check for burstable monsters in wilderness const jelly = monster.id === Monsters.Jelly.id; - const bloodveld = monster.id === Monsters.Bloodveld.id; + const wildyBurst = jelly && isInWilderness; - const wildyBurst = (jelly || bloodveld) && isInWilderness; - - // Set chosen boost based on priority: + // determines what pvm methods the user can use const myCBOpts = user.combatOptions; - const boostChoice = determineBoostChoice({ + const methods = [method] as PvMMethod[]; + const combatMethods = determineCombatBoosts({ cbOpts: myCBOpts as CombatOptionsEnum[], user, monster, - method, + methods, isOnTask, wildyBurst }); @@ -239,7 +238,7 @@ export async function minionKillCommand( const [, osjsMon, attackStyles] = resolveAttackStyles(user, { monsterID: monster.id, - boostMethod: boostChoice + boostMethod: combatMethods }); const [newTime, skillBoostMsg] = applySkillBoost(user, timeToFinish, attackStyles); @@ -457,21 +456,25 @@ export async function minionKillCommand( } // Check for stats - if (boostChoice === 'barrage' && user.skillLevel(SkillsEnum.Magic) < 94) { + if (combatMethods.includes('barrage') && user.skillLevel(SkillsEnum.Magic) < 94) { return `You need 94 Magic to use Ice Barrage. You have ${user.skillLevel(SkillsEnum.Magic)}`; } - if (boostChoice === 'burst' && user.skillLevel(SkillsEnum.Magic) < 70) { + if (combatMethods.includes('burst') && user.skillLevel(SkillsEnum.Magic) < 70) { return `You need 70 Magic to use Ice Burst. You have ${user.skillLevel(SkillsEnum.Magic)}`; } - if (boostChoice === 'chinning' && user.skillLevel(SkillsEnum.Ranged) < 65) { + if (combatMethods.includes('chinning') && user.skillLevel(SkillsEnum.Ranged) < 65) { return `You need 65 Ranged to use Chinning method. You have ${user.skillLevel(SkillsEnum.Ranged)}`; } - // Wildy Monster checks - if (isInWilderness === true && boostChoice === 'cannon') { + // Wildy monster cannon checks + if (isInWilderness === true && combatMethods.includes('cannon')) { if (monster.id === Monsters.HillGiant.id || monster.id === Monsters.MossGiant.id) { usingCannon = isInWilderness; } + if (monster.id === Monsters.Spider.id || Monsters.Scorpion.id) { + usingCannon = isInWilderness; + cannonMulti = isInWilderness; + } if (monster.wildySlayerCave) { usingCannon = isInWilderness; cannonMulti = isInWilderness; @@ -482,8 +485,9 @@ export async function minionKillCommand( } } + // Burst/barrage check with wilderness conditions if ((method === 'burst' || method === 'barrage') && !monster?.canBarrage) { - if (jelly || bloodveld) { + if (jelly) { if (!isInWilderness) { return `${monster.name} can only be barraged or burst in the wilderness.`; } @@ -496,14 +500,18 @@ export async function minionKillCommand( } } - if (boostChoice === 'barrage' && attackStyles.includes(SkillsEnum.Magic) && (monster?.canBarrage || wildyBurst)) { + if ( + combatMethods.includes('barrage') && + attackStyles.includes(SkillsEnum.Magic) && + (monster?.canBarrage || wildyBurst) + ) { consumableCosts.push(iceBarrageConsumables); calculateVirtusBoost(); timeToFinish = reduceNumByPercent(timeToFinish, boostIceBarrage + virtusBoost); boosts.push(`${boostIceBarrage + virtusBoost}% for Ice Barrage${virtusBoostMsg}`); burstOrBarrage = SlayerActivityConstants.IceBarrage; } else if ( - boostChoice === 'burst' && + combatMethods.includes('burst') && attackStyles.includes(SkillsEnum.Magic) && (monster?.canBarrage || wildyBurst) ) { @@ -512,13 +520,14 @@ export async function minionKillCommand( timeToFinish = reduceNumByPercent(timeToFinish, boostIceBurst + virtusBoost); boosts.push(`${boostIceBurst + virtusBoost}% for Ice Burst${virtusBoostMsg}`); burstOrBarrage = SlayerActivityConstants.IceBurst; - } else if ((boostChoice === 'cannon' && hasCannon && monster?.cannonMulti) || cannonMulti) { + } + if ((combatMethods.includes('cannon') && hasCannon && monster?.cannonMulti) || cannonMulti) { usingCannon = true; cannonMulti = true; consumableCosts.push(cannonMultiConsumables); timeToFinish = reduceNumByPercent(timeToFinish, boostCannonMulti); boosts.push(`${boostCannonMulti}% for Cannon in multi`); - } else if ((boostChoice === 'cannon' && hasCannon && monster?.canCannon) || usingCannon) { + } else if ((combatMethods.includes('cannon') && hasCannon && monster?.canCannon) || usingCannon) { usingCannon = true; consumableCosts.push(cannonSingleConsumables); timeToFinish = reduceNumByPercent(timeToFinish, boostCannon); From 99adf7682c7213a33c1fb33158ed455795ee5d7e Mon Sep 17 00:00:00 2001 From: Keres <88959451+DaughtersOfNyx@users.noreply.github.com> Date: Tue, 6 Aug 2024 22:16:05 +1000 Subject: [PATCH 17/27] Remove prospector items from Falador Hard Diary req (#5977) --- src/lib/diaries.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/lib/diaries.ts b/src/lib/diaries.ts index d74961b72a..861361f43d 100644 --- a/src/lib/diaries.ts +++ b/src/lib/diaries.ts @@ -468,13 +468,7 @@ export const FaladorDiary: Diary = { woodcutting: 71 }, qp: 32, - collectionLogReqs: resolveItems([ - 'Mind rune', - 'Prospector jacket', - 'Prospector helmet', - 'Prospector legs', - 'Prospector boots' - ]), + collectionLogReqs: resolveItems(['Mind rune', 'Prospector helmet']), monsterScores: { 'Skeletal Wyvern': 1, 'Blue Dragon': 1 From 7816fe44e994a3fb7bcdb425d99851e1814bfb00 Mon Sep 17 00:00:00 2001 From: TastyPumPum <79149170+TastyPumPum@users.noreply.github.com> Date: Tue, 6 Aug 2024 13:19:10 +0100 Subject: [PATCH 18/27] Add Valamore Farming Patches (#5966) --- src/lib/skilling/functions/calcsFarming.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/lib/skilling/functions/calcsFarming.ts b/src/lib/skilling/functions/calcsFarming.ts index 170e12dd98..a234f023fe 100644 --- a/src/lib/skilling/functions/calcsFarming.ts +++ b/src/lib/skilling/functions/calcsFarming.ts @@ -1,5 +1,6 @@ import { randInt } from 'e'; +import { QuestID } from '../../minions/data/quests'; import type { Plant } from '../types'; import { SkillsEnum } from '../types'; @@ -28,6 +29,19 @@ export function calcNumOfPatches(plant: Plant, user: MUser, qp: number): [number break; } } + + if (user.user.finished_quest_ids.includes(QuestID.ChildrenOfTheSun)) { + switch (plant.seedType) { + case 'allotment': + numOfPatches += 2; + break; + case 'herb': + case 'flower': + numOfPatches += 1; + break; + } + } + return [numOfPatches]; } From bdc6f788205c59a273686cfbf954662f872bbdca Mon Sep 17 00:00:00 2001 From: nwjgit <69014816+nwjgit@users.noreply.github.com> Date: Tue, 6 Aug 2024 07:22:46 -0500 Subject: [PATCH 19/27] quest/lamp fixes (#5949) --- src/lib/data/itemAliases.ts | 5 ++++- src/lib/minions/data/quests.ts | 5 ++--- src/mahoji/lib/abstracted_commands/lampCommand.ts | 10 ++++++++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/lib/data/itemAliases.ts b/src/lib/data/itemAliases.ts index ec44fda255..557bd0b85f 100644 --- a/src/lib/data/itemAliases.ts +++ b/src/lib/data/itemAliases.ts @@ -178,12 +178,15 @@ setItemAlias(2993, 'Chompy bird hat (dragon archer)'); setItemAlias(2994, 'Chompy bird hat (expert ogre dragon archer)'); setItemAlias(2995, 'Chompy bird hat (expert dragon archer)'); -// Item aliases +// Achievement diary lamps setItemAlias(11_137, 'Antique lamp 1'); setItemAlias(11_139, 'Antique lamp 2'); setItemAlias(11_141, 'Antique lamp 3'); setItemAlias(11_185, 'Antique lamp 4'); +// Defender of varrock quest lamp +setItemAlias(28_820, 'Antique lamp (defender of varrock)'); + // Dragonfire shields setItemAlias(11_284, 'Uncharged dragonfire shield'); setItemAlias(11_283, 'Dragonfire shield'); diff --git a/src/lib/minions/data/quests.ts b/src/lib/minions/data/quests.ts index 70451b6ac9..91a92ec0cb 100644 --- a/src/lib/minions/data/quests.ts +++ b/src/lib/minions/data/quests.ts @@ -84,7 +84,7 @@ export const quests: Quest[] = [ }, combatLevelReq: 50, qpReq: 10, - rewards: new Bank().add(28_587).add(28_587).add(28_588).add(28_589).add(28_590).freeze(), + rewards: new Bank().add(28_587).add(28_588).add(28_589).add(28_590).freeze(), calcTime: (user: MUser) => { let duration = Time.Minute * 10; if (user.combatLevel < 90) { @@ -112,8 +112,7 @@ export const quests: Quest[] = [ }, combatLevelReq: 65, qpReq: 20, - // Awaiting item update for the lamp to be added - // rewards: new Bank().add(28_820).freeze(), + rewards: new Bank().add(28_820).freeze(), skillsRewards: { smithing: 15_000, hunter: 15_000 diff --git a/src/mahoji/lib/abstracted_commands/lampCommand.ts b/src/mahoji/lib/abstracted_commands/lampCommand.ts index 2a5d9bb0b3..db678e76fb 100644 --- a/src/mahoji/lib/abstracted_commands/lampCommand.ts +++ b/src/mahoji/lib/abstracted_commands/lampCommand.ts @@ -84,13 +84,12 @@ export const XPLamps: IXPLamp[] = [ minimumLevel: 1, allowedSkills: [SkillsEnum.Magic] }, - /* Needs OSJS Update { itemID: 28_820, amount: 5000, name: 'Antique lamp (defender of varrock)', minimumLevel: 1 - },*/ + }, { itemID: itemID('Antique lamp (easy ca)'), amount: 5000, @@ -149,6 +148,13 @@ export const Lampables: IXPObject[] = [ skills[skill] = data.user.skillLevel(skill) * ([ + SkillsEnum.Attack, + SkillsEnum.Strength, + SkillsEnum.Defence, + SkillsEnum.Magic, + SkillsEnum.Ranged, + SkillsEnum.Hitpoints, + SkillsEnum.Prayer, SkillsEnum.Mining, SkillsEnum.Woodcutting, SkillsEnum.Herblore, From 83d9fa7372fc9b4ccfbcb86dcd89334c329348a4 Mon Sep 17 00:00:00 2001 From: Me <8233310+minimicronano@users.noreply.github.com> Date: Tue, 6 Aug 2024 12:26:33 +0000 Subject: [PATCH 20/27] Update setup steps to mention db schema update (#5947) --- SETUP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SETUP.md b/SETUP.md index 87c7f5d7a9..ee08f7bdf0 100644 --- a/SETUP.md +++ b/SETUP.md @@ -15,7 +15,7 @@ This assumes you are using VSCode as your IDE. If you have errors or issues, you 2. Install [Postgres 16](https://www.postgresql.org/download/) and PGAdmin4 for interacting with postgres (optional, but helpful) 3. Install Yarn using: `npm i -g yarn` 4. Clone the repo: `git clone https://github.com/oldschoolgg/oldschoolbot.git` -5. Run `corepack enable` and `yarn` in the root of the repo. +5. Run the following commands in the root of the repo: `corepack enable`, `yarn`, `npx prisma db push` and `npx prisma db push --schema ./prisma/robochimp.prisma` ### Configuration From fed4468c9d9461030a3a941dbc6601f0022f7a89 Mon Sep 17 00:00:00 2001 From: gc <30398469+gc@users.noreply.github.com> Date: Tue, 6 Aug 2024 22:38:08 +1000 Subject: [PATCH 21/27] Fix docker commands --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0e160aa765..a11ef3d90e 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "test": "concurrently --raw --kill-others-on-fail \"tsc -p src && yarn test:circular\" \"yarn test:lint\" \"yarn test:unit\" \"tsc -p tests/integration --noEmit\" \"tsc -p tests/unit --noEmit\"", "test:lint": "biome check --diagnostic-level=error", "test:unit": "vitest run --coverage --config vitest.unit.config.mts", - "test:docker": "docker-compose up --build --abort-on-container-exit --remove-orphans && docker-compose down --volumes --remove-orphans", + "test:docker": "docker compose up --build --abort-on-container-exit --remove-orphans && docker compose down --volumes --remove-orphans", "test:watch": "vitest --config vitest.unit.config.mts --coverage", "buildandrun": "yarn build:esbuild && node --enable-source-maps dist", "build:esbuild": "concurrently --raw \"yarn build:main\" \"yarn build:workers\"", From b91e7ff34c30ebb2a8663aa8c3dbd9b092a74caa Mon Sep 17 00:00:00 2001 From: Me <8233310+minimicronano@users.noreply.github.com> Date: Sat, 10 Aug 2024 23:27:45 +0000 Subject: [PATCH 22/27] Fix grammar (#5992) --- src/lib/MUser.ts | 2 +- src/lib/colosseum.ts | 2 +- src/lib/data/buyables/skillCapeBuyables.ts | 2 +- src/lib/data/cox.ts | 4 +- src/lib/degradeableItems.ts | 6 +- src/lib/minions/functions/lmsSimCommand.ts | 2 +- src/lib/musicCape.ts | 2 +- src/lib/simulation/nex.ts | 2 +- src/lib/simulation/toa.ts | 18 +++--- src/lib/skilling/types.ts | 2 +- src/lib/util/chatHeadImage.ts | 2 +- src/mahoji/commands/admin.ts | 2 +- src/mahoji/commands/bingo.ts | 8 +-- src/mahoji/commands/drycalc.ts | 2 +- src/mahoji/commands/hunt.ts | 4 +- src/mahoji/commands/laps.ts | 2 +- src/mahoji/commands/mine.ts | 2 +- src/mahoji/commands/mix.ts | 2 +- src/mahoji/commands/offer.ts | 4 +- src/mahoji/commands/smelt.ts | 2 +- src/mahoji/commands/steal.ts | 2 +- .../aerialFishingCommand.ts | 2 +- .../lib/abstracted_commands/bankBgCommand.ts | 2 +- .../abstracted_commands/chompyHuntCommand.ts | 2 +- .../lib/abstracted_commands/coxCommand.ts | 2 +- .../lib/abstracted_commands/diceCommand.ts | 2 +- .../abstracted_commands/driftNetCommand.ts | 2 +- .../abstracted_commands/fightCavesCommand.ts | 4 +- .../lib/abstracted_commands/fishingTrawler.ts | 2 +- .../abstracted_commands/gauntletCommand.ts | 2 +- .../lib/abstracted_commands/gearCommands.ts | 2 +- .../giantsFoundryCommand.ts | 2 +- .../gnomeRestaurantCommand.ts | 2 +- .../lib/abstracted_commands/infernoCommand.ts | 4 +- .../lib/abstracted_commands/lmsCommand.ts | 2 +- .../abstracted_commands/nightmareCommand.ts | 6 +- .../abstracted_commands/puroPuroCommand.ts | 2 +- .../pyramidPlunderCommand.ts | 2 +- .../abstracted_commands/sepulchreCommand.ts | 4 +- .../lib/abstracted_commands/tobCommand.ts | 12 ++-- .../lib/abstracted_commands/trekCommand.ts | 4 +- .../volcanicMineCommand.ts | 2 +- .../warriorsGuildCommand.ts | 6 +- src/mahoji/mahojiSettings.ts | 2 +- .../minions/HunterActivity/hunterActivity.ts | 2 +- src/tasks/minions/mageArena2Activity.ts | 12 ++-- .../minions/minigames/fightCavesActivity.ts | 18 ++++-- .../minions/minigames/infernoActivity.ts | 60 +++++++++++-------- 48 files changed, 126 insertions(+), 112 deletions(-) diff --git a/src/lib/MUser.ts b/src/lib/MUser.ts index 3c4bd10f0e..c9b47bac4f 100644 --- a/src/lib/MUser.ts +++ b/src/lib/MUser.ts @@ -137,7 +137,7 @@ export class MUserClass { this.bitfield = this.user.bitfield as readonly BitField[]; } - countSkillsAtleast99() { + countSkillsAtLeast99() { return Object.values(this.skillsAsLevels).filter(lvl => lvl >= 99).length; } diff --git a/src/lib/colosseum.ts b/src/lib/colosseum.ts index 08f53f2ee8..2a37ae359e 100644 --- a/src/lib/colosseum.ts +++ b/src/lib/colosseum.ts @@ -600,7 +600,7 @@ export async function colosseumCommand(user: MUser, channelID: string) { cost.add('Dragon arrow', 50); } else { messages.push( - 'Missed 7% Venator bow boost. If you have one, charge it and keep it in your bank. You also need atleast 50 dragon arrows equipped.' + 'Missed 7% Venator bow boost. If you have one, charge it and keep it in your bank. You also need at least 50 dragon arrows equipped.' ); } diff --git a/src/lib/data/buyables/skillCapeBuyables.ts b/src/lib/data/buyables/skillCapeBuyables.ts index d6061f7a9f..fddc4fd69c 100644 --- a/src/lib/data/buyables/skillCapeBuyables.ts +++ b/src/lib/data/buyables/skillCapeBuyables.ts @@ -12,7 +12,7 @@ for (const skillcape of Skillcapes) { outputItems: (user: MUser) => { const output = new Bank().add(skillcape.hood); - if (user.countSkillsAtleast99() > 1) { + if (user.countSkillsAtLeast99() > 1) { output.add(skillcape.trimmed); } else { output.add(skillcape.untrimmed); diff --git a/src/lib/data/cox.ts b/src/lib/data/cox.ts index 05a7df1fc7..912f2ecf42 100644 --- a/src/lib/data/cox.ts +++ b/src/lib/data/cox.ts @@ -240,11 +240,11 @@ export const minimumCoxSuppliesNeeded = new Bank({ export async function checkCoxTeam(users: MUser[], cm: boolean, quantity = 1): Promise { const hasHerbalist = users.some(u => u.skillLevel(SkillsEnum.Herblore) >= 78); if (!hasHerbalist) { - return 'nobody with atleast level 78 Herblore'; + return 'nobody with at least level 78 Herblore'; } const hasFarmer = users.some(u => u.skillLevel(SkillsEnum.Farming) >= 55); if (!hasFarmer) { - return 'nobody with atleast level 55 Farming'; + return 'nobody with at least level 55 Farming'; } const suppliesNeeded = minimumCoxSuppliesNeeded.clone().multiply(quantity); const userWithoutSupplies = users.find(u => !u.bank.has(suppliesNeeded)); diff --git a/src/lib/degradeableItems.ts b/src/lib/degradeableItems.ts index dc3010152a..42a0d37496 100644 --- a/src/lib/degradeableItems.ts +++ b/src/lib/degradeableItems.ts @@ -396,10 +396,8 @@ export async function degradeItem({ const chargesAfter = user.user[degItem.settingsKey]; assert(typeof chargesAfter === 'number' && chargesAfter > 0); return { - userMessage: `Your ${ - item.name - } degraded by ${chargesToDegrade} charges, and now has ${chargesAfter} remaining.${ - pennyReduction > 0 ? ` Your Ghommal's lucky penny saved ${pennyReduction} charges` : '' + userMessage: `Your ${item.name} degraded by ${chargesToDegrade} charges, and now has ${chargesAfter} remaining${ + pennyReduction > 0 ? `. Your Ghommal's lucky penny saved ${pennyReduction} charges` : '' }` }; } diff --git a/src/lib/minions/functions/lmsSimCommand.ts b/src/lib/minions/functions/lmsSimCommand.ts index ef34e9c1bc..ae715ec690 100644 --- a/src/lib/minions/functions/lmsSimCommand.ts +++ b/src/lib/minions/functions/lmsSimCommand.ts @@ -125,7 +125,7 @@ export async function lmsSimCommand(channel: Channel | undefined, names?: string if (filtered.size < 4) { return channel.send( - 'Please specify atleast 4 players for Last Man Standing, like so: `+lms Alex, Kyra, Magna, Rick`, or type `+lms auto` to automatically pick people from the chat.' + 'Please specify at least 4 players for Last Man Standing, like so: `+lms Alex, Kyra, Magna, Rick`, or type `+lms auto` to automatically pick people from the chat.' ); } diff --git a/src/lib/musicCape.ts b/src/lib/musicCape.ts index ee32e947d1..84bba310e1 100644 --- a/src/lib/musicCape.ts +++ b/src/lib/musicCape.ts @@ -100,7 +100,7 @@ export const musicCapeRequirements = new Requirements() } }) .add({ - name: 'Runecraft all runes atleast once', + name: 'Runecraft all runes at least once', has: ({ uniqueRunesCrafted }) => { const runesToCheck = resolveItems([ 'Mind rune', diff --git a/src/lib/simulation/nex.ts b/src/lib/simulation/nex.ts index a5f928a3d3..34f649c2a0 100644 --- a/src/lib/simulation/nex.ts +++ b/src/lib/simulation/nex.ts @@ -59,7 +59,7 @@ export function checkNexUser(user: MUser): [false] | [true, string] { if (!user.hasSkillReqs(minStats)) { return [true, `${tag} doesn't have the skill requirements: ${formatSkillRequirements(minStats)}.`]; } - if (user.GP < 1_000_000) return [true, `${tag} needs atleast 1m GP to cover potential deaths.`]; + if (user.GP < 1_000_000) return [true, `${tag} needs at least 1m GP to cover potential deaths.`]; const { offence, defence, rangeGear } = nexGearStats(user); if (offence < 50) { return [ diff --git a/src/lib/simulation/toa.ts b/src/lib/simulation/toa.ts index 8e0d27684f..810b071f63 100644 --- a/src/lib/simulation/toa.ts +++ b/src/lib/simulation/toa.ts @@ -247,7 +247,7 @@ const toaRequirements: { return true; }, desc: () => - `atleast ${BP_DARTS_NEEDED}x darts per raid, and using one of: ${ALLOWED_DARTS.map(i => i.name).join( + `at least ${BP_DARTS_NEEDED}x darts per raid, and using one of: ${ALLOWED_DARTS.map(i => i.name).join( ', ' )}, loaded in Blowpipe` }, @@ -278,7 +278,7 @@ const toaRequirements: { return true; }, desc: () => - `decent range gear (BiS is ${maxRangeGear.toString()}), atleast ${BOW_ARROWS_NEEDED}x arrows equipped, and one of these bows: ${REQUIRED_RANGE_WEAPONS.map( + `decent range gear (BiS is ${maxRangeGear.toString()}), at least ${BOW_ARROWS_NEEDED}x arrows equipped, and one of these bows: ${REQUIRED_RANGE_WEAPONS.map( itemNameFromID ).join(', ')}` }, @@ -335,11 +335,11 @@ const toaRequirements: { minimumSuppliesNeeded = minSuppliesWithAtkStr; } if (!user.owns(minimumSuppliesNeeded.clone().multiply(quantity))) { - return `You need atleast this much supplies: ${minimumSuppliesNeeded}.`; + return `You need at least this much supplies: ${minimumSuppliesNeeded}.`; } const bfCharges = BLOOD_FURY_CHARGES_PER_RAID * quantity; if (user.gear.melee.hasEquipped('Amulet of blood fury') && user.user.blood_fury_charges < bfCharges) { - return `You need atleast ${bfCharges} Blood fury charges to use it, otherwise it has to be unequipped: ${mentionCommand( + return `You need at least ${bfCharges} Blood fury charges to use it, otherwise it has to be unequipped: ${mentionCommand( globalClient, 'minion', 'charge' @@ -348,7 +348,7 @@ const toaRequirements: { const tumCharges = TUMEKEN_SHADOW_PER_RAID * quantity; if (user.gear.mage.hasEquipped("Tumeken's shadow") && user.user.tum_shadow_charges < tumCharges) { - return `You need atleast ${tumCharges} Tumeken's shadow charges to use it, otherwise it has to be unequipped: ${mentionCommand( + return `You need at least ${tumCharges} Tumeken's shadow charges to use it, otherwise it has to be unequipped: ${mentionCommand( globalClient, 'minion', 'charge' @@ -357,7 +357,7 @@ const toaRequirements: { return true; }, - desc: () => `Need atleast ${minimumSuppliesNeeded}` + desc: () => `Need at least ${minimumSuppliesNeeded}` }, { name: 'Rune Pouch', @@ -368,7 +368,7 @@ const toaRequirements: { } return true; }, - desc: () => `Need atleast ${minimumSuppliesNeeded}` + desc: () => `Need at least ${minimumSuppliesNeeded}` }, { name: 'Poison Protection', @@ -1044,7 +1044,7 @@ async function checkTOAUser( true, `${ user.usernameOrMention - } doesn't have enough Serpentine helm charges. You need atleast ${serpHelmCharges} charges to do a ${formatDuration( + } doesn't have enough Serpentine helm charges. You need at least ${serpHelmCharges} charges to do a ${formatDuration( duration )} TOA raid.` ]; @@ -1057,7 +1057,7 @@ async function checkTOAUser( if (kc < dividedRaidLevel) { return [ true, - `${user.usernameOrMention}, you need atleast ${dividedRaidLevel} TOA KC to ${ + `${user.usernameOrMention}, you need at least ${dividedRaidLevel} TOA KC to ${ teamSize === 2 ? 'duo' : 'solo' } a level ${raidLevel} TOA raid.` ]; diff --git a/src/lib/skilling/types.ts b/src/lib/skilling/types.ts index 5d08411b7b..364f407eff 100644 --- a/src/lib/skilling/types.ts +++ b/src/lib/skilling/types.ts @@ -313,7 +313,7 @@ export enum HunterTechniqueEnum { BoxTrapping = 'box trapping', ButterflyNetting = 'butterfly netting', DeadfallTrapping = 'deadfall trapping', - Falconry = 'falconry', + Falconry = 'hawking', MagicBoxTrapping = 'magic box trapping', NetTrapping = 'net trapping', PitfallTrapping = 'pitfall trapping', diff --git a/src/lib/util/chatHeadImage.ts b/src/lib/util/chatHeadImage.ts index 36c117262c..0aee1092b1 100644 --- a/src/lib/util/chatHeadImage.ts +++ b/src/lib/util/chatHeadImage.ts @@ -34,7 +34,7 @@ const names: Record = { santa: 'Santa', izzy: "Cap'n Izzy No-Beard", alry: 'Alry the Angler', - ketKeh: 'Tzhaar-Ket-Keh', + ketKeh: 'TzHaar-Ket-Keh', gertrude: 'Gertrude', antiSanta: 'Anti-Santa', bunny: 'Easter Bunny', diff --git a/src/mahoji/commands/admin.ts b/src/mahoji/commands/admin.ts index 92701fbe01..27bb5bdfef 100644 --- a/src/mahoji/commands/admin.ts +++ b/src/mahoji/commands/admin.ts @@ -986,7 +986,7 @@ ${guildCommands.length} Guild commands`; FROM users WHERE bank->>'${item.id}' IS NOT NULL;`); return `There are ${ownedResult[0].qty.toLocaleString()} ${item.name} owned by everyone. -There are ${await countUsersWithItemInCl(item.id, isIron)} ${isIron ? 'ironmen' : 'people'} with atleast 1 ${ +There are ${await countUsersWithItemInCl(item.id, isIron)} ${isIron ? 'ironmen' : 'people'} with at least 1 ${ item.name } in their collection log.`; } diff --git a/src/mahoji/commands/bingo.ts b/src/mahoji/commands/bingo.ts index 6f56fdc656..0f73adc593 100644 --- a/src/mahoji/commands/bingo.ts +++ b/src/mahoji/commands/bingo.ts @@ -675,7 +675,7 @@ export const bingoCommand: OSBMahojiCommand = { const fee = BOT_TYPE === 'OSB' ? 20_000_000 : 50_000_000; const creationCost = new Bank().add('Coins', fee); if (user.GP < creationCost.amount('Coins')) { - return `You need atleast ${creationCost} to create a bingo.`; + return `You need at least ${creationCost} to create a bingo.`; } const channel = globalClient.channels.cache.get(options.create_bingo.notifications_channel_id); @@ -713,9 +713,9 @@ export const bingoCommand: OSBMahojiCommand = { return 'Team size must be between 1 and 5.'; } - // Start date must be atleast 3 hours into the future + // Start date must be at least 3 hours into the future if (createOptions.start_date.getTime() < Date.now() + Time.Minute * 3) { - return 'Start date must be atleast 3 minutes into the future.'; + return 'Start date must be at least 3 minutes into the future.'; } // Start date cannot be more than 31 days into the future @@ -914,7 +914,7 @@ Example: \`add_tile:Coal|Trout|Egg\` is a tile where you have to receive a coal const cost = new Bank().add('Coins', amount); if (user.GP < cost.amount('Coins')) { - return `You need atleast ${cost} to add that much GP to the prize pool.`; + return `You need at least ${cost} to add that much GP to the prize pool.`; } await handleMahojiConfirmation( diff --git a/src/mahoji/commands/drycalc.ts b/src/mahoji/commands/drycalc.ts index 1975749cf5..dbc12697d6 100644 --- a/src/mahoji/commands/drycalc.ts +++ b/src/mahoji/commands/drycalc.ts @@ -39,7 +39,7 @@ export const dryCalcCommand: OSBMahojiCommand = { )}%** chance of not receiving any drop, and a **${round( dropChance, 2 - )}%** chance of receiving atleast one drop.`; + )}%** chance of receiving at least one drop.`; return output; } diff --git a/src/mahoji/commands/hunt.ts b/src/mahoji/commands/hunt.ts index 64d6d7e1ff..883a735cd4 100644 --- a/src/mahoji/commands/hunt.ts +++ b/src/mahoji/commands/hunt.ts @@ -280,8 +280,8 @@ export const huntCommand: OSBMahojiCommand = { type: 'Hunter' }); - let response = `${user.minionName} is now ${crystalImpling ? 'hunting' : `${creature.huntTechnique}`} ${ - crystalImpling ? '' : ` ${quantity}x ` + let response = `${user.minionName} is now ${crystalImpling ? 'hunting' : `${creature.huntTechnique}`}${ + crystalImpling ? ' ' : ` ${quantity}x ` }${creature.name}, it'll take around ${formatDuration(duration)} to finish.`; if (boosts.length > 0) { diff --git a/src/mahoji/commands/laps.ts b/src/mahoji/commands/laps.ts index 2e7961b462..45585d18b8 100644 --- a/src/mahoji/commands/laps.ts +++ b/src/mahoji/commands/laps.ts @@ -127,7 +127,7 @@ export const lapsCommand: OSBMahojiCommand = { } if (course.qpRequired && user.QP < course.qpRequired) { - return `You need atleast ${course.qpRequired} Quest Points to do this course.`; + return `You need at least ${course.qpRequired} Quest Points to do this course.`; } const maxTripLength = calcMaxTripLength(user, 'Agility'); diff --git a/src/mahoji/commands/mine.ts b/src/mahoji/commands/mine.ts index 8de09264f4..7f8ab8a624 100644 --- a/src/mahoji/commands/mine.ts +++ b/src/mahoji/commands/mine.ts @@ -91,7 +91,7 @@ export const mineCommand: OSBMahojiCommand = { return `To mine ${ore.name}, you need ${formatSkillRequirements(sinsOfTheFatherSkillRequirements)}.`; } if (user.QP < 125) { - return `To mine ${ore.name}, you need atleast 125 Quest Points.`; + return `To mine ${ore.name}, you need at least 125 Quest Points.`; } } diff --git a/src/mahoji/commands/mix.ts b/src/mahoji/commands/mix.ts index b16a74d103..85db808a86 100644 --- a/src/mahoji/commands/mix.ts +++ b/src/mahoji/commands/mix.ts @@ -72,7 +72,7 @@ export const mixCommand: OSBMahojiCommand = { } if (mixableItem.qpRequired && user.QP < mixableItem.qpRequired) { - return `You need atleast **${mixableItem.qpRequired}** QP to make ${mixableItem.item.name}.`; + return `You need at least **${mixableItem.qpRequired}** QP to make ${mixableItem.item.name}.`; } const requiredItems = new Bank(mixableItem.inputItems); diff --git a/src/mahoji/commands/offer.ts b/src/mahoji/commands/offer.ts index dd52db93ae..08401b2474 100644 --- a/src/mahoji/commands/offer.ts +++ b/src/mahoji/commands/offer.ts @@ -208,10 +208,10 @@ export const offerCommand: OSBMahojiCommand = { const specialBone = specialBones.find(bone => stringMatches(bone.item.name, options.name)); if (specialBone) { if (user.QP < 8) { - return 'You need atleast 8 QP to offer long/curved bones for XP.'; + return 'You need at least 8 QP to offer long/curved bones for XP.'; } if (user.skillLevel(SkillsEnum.Construction) < 30) { - return 'You need atleast level 30 Construction to offer long/curved bones for XP.'; + return 'You need at least level 30 Construction to offer long/curved bones for XP.'; } const amountHas = userBank.amount(specialBone.item.id); if (!quantity) quantity = Math.max(amountHas, 1); diff --git a/src/mahoji/commands/smelt.ts b/src/mahoji/commands/smelt.ts index ca8bded912..d099c2d281 100644 --- a/src/mahoji/commands/smelt.ts +++ b/src/mahoji/commands/smelt.ts @@ -145,7 +145,7 @@ export const smeltingCommand: OSBMahojiCommand = { coinsToRemove = Math.floor(gpPerHour * (duration / Time.Hour)); const gp = user.GP; if (gp < coinsToRemove) { - return `You need atleast ${coinsToRemove} GP to work at the Blast Furnace.`; + return `You need at least ${coinsToRemove} GP to work at the Blast Furnace.`; } cost.add('Coins', coinsToRemove); diff --git a/src/mahoji/commands/steal.ts b/src/mahoji/commands/steal.ts index 5ba6d227d1..c1248ed3df 100644 --- a/src/mahoji/commands/steal.ts +++ b/src/mahoji/commands/steal.ts @@ -70,7 +70,7 @@ export const stealCommand: OSBMahojiCommand = { } if (stealable.qpRequired && user.QP < stealable.qpRequired) { - return `You need atleast **${stealable.qpRequired}** QP to ${ + return `You need at least **${stealable.qpRequired}** QP to ${ stealable.type === 'pickpockable' ? 'pickpocket' : 'steal from' } a ${stealable.name}.`; } diff --git a/src/mahoji/lib/abstracted_commands/aerialFishingCommand.ts b/src/mahoji/lib/abstracted_commands/aerialFishingCommand.ts index d8e69518b2..b21649e697 100644 --- a/src/mahoji/lib/abstracted_commands/aerialFishingCommand.ts +++ b/src/mahoji/lib/abstracted_commands/aerialFishingCommand.ts @@ -8,7 +8,7 @@ import { calcMaxTripLength } from '../../../lib/util/calcMaxTripLength'; export async function aerialFishingCommand(user: MUser, channelID: string) { if (user.skillLevel(SkillsEnum.Fishing) < 43 || user.skillLevel(SkillsEnum.Hunter) < 35) { - return 'You need atleast level 35 Hunter and 43 Fishing to do Aerial fishing.'; + return 'You need at least level 35 Hunter and 43 Fishing to do Aerial fishing.'; } const timePerFish = randomVariation(2, 7.5) * Time.Second; diff --git a/src/mahoji/lib/abstracted_commands/bankBgCommand.ts b/src/mahoji/lib/abstracted_commands/bankBgCommand.ts index 4cae340826..9af5fa5346 100644 --- a/src/mahoji/lib/abstracted_commands/bankBgCommand.ts +++ b/src/mahoji/lib/abstracted_commands/bankBgCommand.ts @@ -36,7 +36,7 @@ export async function bankBgCommand(interaction: ChatInputCommandInteraction, us if (selectedImage.sacValueRequired) { const sac = Number(user.user.sacrificedValue); if (sac < selectedImage.sacValueRequired) { - return `You have to have sacrificed atleast ${toKMB( + return `You have to have sacrificed at least ${toKMB( selectedImage.sacValueRequired )} GP worth of items to use this background.`; } diff --git a/src/mahoji/lib/abstracted_commands/chompyHuntCommand.ts b/src/mahoji/lib/abstracted_commands/chompyHuntCommand.ts index 0e2a1853a1..49341819e6 100644 --- a/src/mahoji/lib/abstracted_commands/chompyHuntCommand.ts +++ b/src/mahoji/lib/abstracted_commands/chompyHuntCommand.ts @@ -40,7 +40,7 @@ export async function chompyHuntClaimCommand(user: MUser) { export async function chompyHuntCommand(user: MUser, channelID: string) { if (user.QP < 10) { - return 'You need atleast 10 QP to hunt Chompy birds.'; + return 'You need at least 10 QP to hunt Chompy birds.'; } const rangeGear = user.gear.range; diff --git a/src/mahoji/lib/abstracted_commands/coxCommand.ts b/src/mahoji/lib/abstracted_commands/coxCommand.ts index 366ed284e0..6eeaca3135 100644 --- a/src/mahoji/lib/abstracted_commands/coxCommand.ts +++ b/src/mahoji/lib/abstracted_commands/coxCommand.ts @@ -95,7 +95,7 @@ export async function coxCommand( if (isChallengeMode) { const normalKC = await getMinigameScore(user.id, 'raids'); if (normalKC < 200) { - return 'You need atleast 200 completions of the Chambers of Xeric before you can attempt Challenge Mode.'; + return 'You need at least 200 completions of the Chambers of Xeric before you can attempt Challenge Mode.'; } } if (user.minionIsBusy) { diff --git a/src/mahoji/lib/abstracted_commands/diceCommand.ts b/src/mahoji/lib/abstracted_commands/diceCommand.ts index b53ebbbe98..f8a4117615 100644 --- a/src/mahoji/lib/abstracted_commands/diceCommand.ts +++ b/src/mahoji/lib/abstracted_commands/diceCommand.ts @@ -29,7 +29,7 @@ export async function diceCommand(user: MUser, interaction: ChatInputCommandInte } if (amount < 1_000_000) { - return 'You have to dice atleast 1,000,000.'; + return 'You have to dice at least 1,000,000.'; } const gp = user.GP; diff --git a/src/mahoji/lib/abstracted_commands/driftNetCommand.ts b/src/mahoji/lib/abstracted_commands/driftNetCommand.ts index 9c24f5cde9..de2b715d50 100644 --- a/src/mahoji/lib/abstracted_commands/driftNetCommand.ts +++ b/src/mahoji/lib/abstracted_commands/driftNetCommand.ts @@ -21,7 +21,7 @@ export async function driftNetCommand( } if (user.skillLevel(SkillsEnum.Fishing) < 47 || user.skillLevel(SkillsEnum.Hunter) < 44) { - return 'You need atleast level 44 Hunter and 47 Fishing to do Drift net fishing.'; + return 'You need at least level 44 Hunter and 47 Fishing to do Drift net fishing.'; } if (!user.hasEquipped(['Graceful gloves', 'Graceful top', 'Graceful legs'])) { diff --git a/src/mahoji/lib/abstracted_commands/fightCavesCommand.ts b/src/mahoji/lib/abstracted_commands/fightCavesCommand.ts index b30248334e..543a275fa5 100644 --- a/src/mahoji/lib/abstracted_commands/fightCavesCommand.ts +++ b/src/mahoji/lib/abstracted_commands/fightCavesCommand.ts @@ -86,11 +86,11 @@ function checkGear(user: MUser): string | undefined { } if (!user.owns(fightCavesCost)) { - return `JalYt, you need supplies to have a chance in the caves...come back with ${fightCavesCost}.`; + return `JalYt, you need supplies to have a chance in the caves... Come back with ${fightCavesCost}.`; } if (user.skillLevel('prayer') < 43) { - return 'JalYt, come back when you have atleast 43 Prayer, TzTok-Jad annihilate you without protection from gods.'; + return 'JalYt, come back when you have at least 43 Prayer, TzTok-Jad annihilate you without protection from gods.'; } } diff --git a/src/mahoji/lib/abstracted_commands/fishingTrawler.ts b/src/mahoji/lib/abstracted_commands/fishingTrawler.ts index 36e4ccb8fd..21657215d8 100644 --- a/src/mahoji/lib/abstracted_commands/fishingTrawler.ts +++ b/src/mahoji/lib/abstracted_commands/fishingTrawler.ts @@ -8,7 +8,7 @@ import { calcMaxTripLength } from '../../../lib/util/calcMaxTripLength'; export async function fishingTrawlerCommand(user: MUser, channelID: string) { if (user.skillLevel('fishing') < 15) { - return 'You need atleast level 15 Fishing to do the Fishing Trawler.'; + return 'You need at least level 15 Fishing to do the Fishing Trawler.'; } const tripsDone = await getMinigameScore(user.id, 'fishing_trawler'); diff --git a/src/mahoji/lib/abstracted_commands/gauntletCommand.ts b/src/mahoji/lib/abstracted_commands/gauntletCommand.ts index 086ba8d13f..362c9a1d18 100644 --- a/src/mahoji/lib/abstracted_commands/gauntletCommand.ts +++ b/src/mahoji/lib/abstracted_commands/gauntletCommand.ts @@ -44,7 +44,7 @@ const corruptedRequirements = { export async function gauntletCommand(user: MUser, channelID: string, type: 'corrupted' | 'normal' = 'normal') { if (user.minionIsBusy) return `${user.minionName} is busy.`; if (user.QP < 200) { - return 'You need atleast 200 QP to do the Gauntlet.'; + return 'You need at least 200 QP to do the Gauntlet.'; } const readableName = `${toTitleCase(type)} Gauntlet`; const requiredSkills = type === 'corrupted' ? corruptedRequirements : standardRequirements; diff --git a/src/mahoji/lib/abstracted_commands/gearCommands.ts b/src/mahoji/lib/abstracted_commands/gearCommands.ts index 3cb1fb6c54..b3a98117ce 100644 --- a/src/mahoji/lib/abstracted_commands/gearCommands.ts +++ b/src/mahoji/lib/abstracted_commands/gearCommands.ts @@ -406,7 +406,7 @@ export async function gearViewCommand(user: MUser, input: string, text: boolean) }) .join('\n\n'); - const updatedContent = `${content}\n\nThese assume you have atleast 25 prayer for the protect item prayer.`; + const updatedContent = `${content}\n\nThese assume you have at least 25 prayer for the protect item prayer.`; return { content: updatedContent }; } diff --git a/src/mahoji/lib/abstracted_commands/giantsFoundryCommand.ts b/src/mahoji/lib/abstracted_commands/giantsFoundryCommand.ts index 63bb5cfa19..2576fa30ff 100644 --- a/src/mahoji/lib/abstracted_commands/giantsFoundryCommand.ts +++ b/src/mahoji/lib/abstracted_commands/giantsFoundryCommand.ts @@ -172,7 +172,7 @@ export async function giantsFoundryStartCommand( } if (userSmithingLevel < alloy.level) { - return `${user.minionName} needs atleast level ${alloy.level} Smithing to user ${alloy.name} alloy in the Giants' Foundry.`; + return `${user.minionName} needs at least level ${alloy.level} Smithing to user ${alloy.name} alloy in the Giants' Foundry.`; } // If they have the entire Smiths' Uniform, give an extra 15% speed bonus diff --git a/src/mahoji/lib/abstracted_commands/gnomeRestaurantCommand.ts b/src/mahoji/lib/abstracted_commands/gnomeRestaurantCommand.ts index 369f9f0a56..f78cc30f8e 100644 --- a/src/mahoji/lib/abstracted_commands/gnomeRestaurantCommand.ts +++ b/src/mahoji/lib/abstracted_commands/gnomeRestaurantCommand.ts @@ -18,7 +18,7 @@ export async function gnomeRestaurantCommand(user: MUser, channelID: string) { const itemsToRemove = new Bank(); const gp = user.GP; if (gp < 5000) { - return 'You need atleast 5k GP to work at the Gnome Restaurant.'; + return 'You need at least 5k GP to work at the Gnome Restaurant.'; } itemsToRemove.add('Coins', 5000); diff --git a/src/mahoji/lib/abstracted_commands/infernoCommand.ts b/src/mahoji/lib/abstracted_commands/infernoCommand.ts index 56f2e1f54f..080b9ff36d 100644 --- a/src/mahoji/lib/abstracted_commands/infernoCommand.ts +++ b/src/mahoji/lib/abstracted_commands/infernoCommand.ts @@ -249,7 +249,7 @@ async function infernoRun({ const dartIndex = blowpipeDarts.indexOf(dartItem); const percent = dartIndex >= 3 ? dartIndex * 0.9 : -(4 * (4 - dartIndex)); if (dartIndex < 5) { - return 'Your darts are simply too weak, to work in the Inferno!'; + return 'Your darts are simply too weak to work in the Inferno!'; } duration.add(true, -percent, `${dartItem.name} in blowpipe`); @@ -516,7 +516,7 @@ export async function infernoStartCommand(user: MUser, channelID: string): Comma { name: 'image.jpg', attachment: await newChatHeadImage({ - content: "You're on your own now JalYt, you face certain death... prepare to fight for your life.", + content: "You're on your own now JalYt, you face certain death... Prepare to fight for your life.", head: 'ketKeh' }) } diff --git a/src/mahoji/lib/abstracted_commands/lmsCommand.ts b/src/mahoji/lib/abstracted_commands/lmsCommand.ts index e7460c0fb4..056bea5212 100644 --- a/src/mahoji/lib/abstracted_commands/lmsCommand.ts +++ b/src/mahoji/lib/abstracted_commands/lmsCommand.ts @@ -53,7 +53,7 @@ export async function lmsCommand( return `You don't have enough points. ${quantity}x ${itemToBuy.item.name} costs ${cost}, but you have ${stats.points}.`; } if (itemToBuy.wins && stats.gamesWon < itemToBuy.wins) { - return `You are not worthy! You need to have won atleast ${itemToBuy.wins} games to buy the ${itemToBuy.item.name}.`; + return `You are not worthy! You need to have won at least ${itemToBuy.wins} games to buy the ${itemToBuy.item.name}.`; } const loot = new Bank().add(itemToBuy.item.id, quantity * (itemToBuy.quantity ?? 1)); await handleMahojiConfirmation(interaction, `Are you sure you want to spend ${cost} points on buying ${loot}?`); diff --git a/src/mahoji/lib/abstracted_commands/nightmareCommand.ts b/src/mahoji/lib/abstracted_commands/nightmareCommand.ts index 759dd7800c..170602cfdb 100644 --- a/src/mahoji/lib/abstracted_commands/nightmareCommand.ts +++ b/src/mahoji/lib/abstracted_commands/nightmareCommand.ts @@ -89,7 +89,7 @@ async function checkReqs(user: MUser, monster: KillableMonster, isPhosani: boole return `${user.usernameOrMention} doesn't meet the requirements: ${requirements[1]}.`; } if ((await user.getKC(NightmareMonster.id)) < 50) { - return "You need to have killed The Nightmare atleast 50 times before you can face the Phosani's Nightmare."; + return "You need to have killed The Nightmare at least 50 times before you can face the Phosani's Nightmare."; } } } @@ -100,13 +100,13 @@ function perUserCost(user: MUser, quantity: number, isPhosani: boolean, hasShado const sangCharges = sangChargesPerKc * quantity; if (isPhosani) { if (hasShadow && user.user.tum_shadow_charges < tumCharges) { - return `You need atleast ${tumCharges} Tumeken's shadow charges to use it, otherwise it has to be unequipped: ${mentionCommand( + return `You need at least ${tumCharges} Tumeken's shadow charges to use it, otherwise it has to be unequipped: ${mentionCommand( globalClient, 'minion', 'charge' )}`; } else if (hasSang && user.user.sang_charges < sangCharges) { - return `You need atleast ${sangCharges} Sanguinesti staff charges to use it, otherwise it has to be unequipped: ${mentionCommand( + return `You need at least ${sangCharges} Sanguinesti staff charges to use it, otherwise it has to be unequipped: ${mentionCommand( globalClient, 'minion', 'charge' diff --git a/src/mahoji/lib/abstracted_commands/puroPuroCommand.ts b/src/mahoji/lib/abstracted_commands/puroPuroCommand.ts index 41a816b15d..c4142eb55d 100644 --- a/src/mahoji/lib/abstracted_commands/puroPuroCommand.ts +++ b/src/mahoji/lib/abstracted_commands/puroPuroCommand.ts @@ -68,7 +68,7 @@ export async function puroPuroStartCommand( } if (!impToHunt) return 'Error selecting impling, please try again.'; if (hunterLevel < impToHunt.hunterLevel) - return `${user.minionName} needs atleast level ${impToHunt.hunterLevel} hunter to hunt ${impToHunt.name} in Puro-Puro.`; + return `${user.minionName} needs at least level ${impToHunt.hunterLevel} hunter to hunt ${impToHunt.name} in Puro-Puro.`; if (!darkLure || (darkLure && !impToHunt.spell)) darkLure = false; if (darkLure) { if (user.QP < 9) return 'To use Dark Lure, you need 9 QP.'; diff --git a/src/mahoji/lib/abstracted_commands/pyramidPlunderCommand.ts b/src/mahoji/lib/abstracted_commands/pyramidPlunderCommand.ts index 8825d580a5..9c51f0e8ab 100644 --- a/src/mahoji/lib/abstracted_commands/pyramidPlunderCommand.ts +++ b/src/mahoji/lib/abstracted_commands/pyramidPlunderCommand.ts @@ -15,7 +15,7 @@ export async function pyramidPlunderCommand(user: MUser, channelID: string) { const thievingLevel = skills.thieving; const minLevel = plunderRooms[0].thievingLevel; if (thievingLevel < minLevel) { - return `You need atleast level ${minLevel} Thieving to do the Pyramid Plunder.`; + return `You need at least level ${minLevel} Thieving to do the Pyramid Plunder.`; } const completableRooms = plunderRooms.filter(room => thievingLevel >= room.thievingLevel); diff --git a/src/mahoji/lib/abstracted_commands/sepulchreCommand.ts b/src/mahoji/lib/abstracted_commands/sepulchreCommand.ts index e1bc9defe6..46b2c4fc29 100644 --- a/src/mahoji/lib/abstracted_commands/sepulchreCommand.ts +++ b/src/mahoji/lib/abstracted_commands/sepulchreCommand.ts @@ -14,11 +14,11 @@ export async function sepulchreCommand(user: MUser, channelID: string) { const thievingLevel = skills.thieving; const minLevel = sepulchreFloors[0].agilityLevel; if (agilityLevel < minLevel) { - return `You need atleast level ${minLevel} Agility to do the Hallowed Sepulchre.`; + return `You need at least level ${minLevel} Agility to do the Hallowed Sepulchre.`; } if (thievingLevel < 66) { - return 'You need atleast level 66 Thieving to do the Hallowed Sepulchre.'; + return 'You need at least level 66 Thieving to do the Hallowed Sepulchre.'; } if (!userHasGracefulEquipped(user)) { diff --git a/src/mahoji/lib/abstracted_commands/tobCommand.ts b/src/mahoji/lib/abstracted_commands/tobCommand.ts index c8f9d1930b..5b6f1b9e06 100644 --- a/src/mahoji/lib/abstracted_commands/tobCommand.ts +++ b/src/mahoji/lib/abstracted_commands/tobCommand.ts @@ -180,11 +180,11 @@ async function checkTOBUser( } const dartsNeeded = 150 * quantity; if (blowpipeData.dartQuantity < dartsNeeded) { - return [true, `${user.usernameOrMention}, you need atleast ${dartsNeeded} darts in your blowpipe.`]; + return [true, `${user.usernameOrMention}, you need at least ${dartsNeeded} darts in your blowpipe.`]; } const scalesNeeded = 1000 * quantity; if (blowpipeData.scales < scalesNeeded) { - return [true, `${user.usernameOrMention}, you need atleast ${scalesNeeded} scales in your blowpipe.`]; + return [true, `${user.usernameOrMention}, you need at least ${scalesNeeded} scales in your blowpipe.`]; } const dartIndex = blowpipeDarts.indexOf(getOSItem(blowpipeData.dartID)); if (dartIndex < 5) { @@ -209,7 +209,7 @@ async function checkTOBUser( if (rangeGear.ammo!.quantity < arrowsRequired) { return [ true, - `${user.usernameOrMention}, you need atleast ${arrowsRequired} arrows equipped in your range setup.` + `${user.usernameOrMention}, you need at least ${arrowsRequired} arrows equipped in your range setup.` ]; } @@ -217,7 +217,7 @@ async function checkTOBUser( const kc = await getMinigameScore(user.id, 'tob'); if (kc < 250) { - return [true, `${user.usernameOrMention} needs atleast 250 Theatre of Blood KC before doing Hard mode.`]; + return [true, `${user.usernameOrMention} needs at least 250 Theatre of Blood KC before doing Hard mode.`]; } if (!meleeGear.hasEquipped('Infernal cape')) { return [true, `${user.usernameOrMention} needs an Infernal cape to do Hard mode.`]; @@ -227,7 +227,7 @@ async function checkTOBUser( if (teamSize === 2) { const kc = await getMinigameScore(user.id, isHardMode ? 'tob_hard' : 'tob'); if (kc < 150) { - return [true, `${user.usernameOrMention} needs atleast 150 KC before doing duo's.`]; + return [true, `${user.usernameOrMention} needs at least 150 KC before doing duo's.`]; } } @@ -306,7 +306,7 @@ export async function tobStartCommand( if (isHardMode) { const normalKC = await getMinigameScore(user.id, 'tob'); if (normalKC < 250) { - return 'You need atleast 250 completions of the Theatre of Blood before you can attempt Hard Mode.'; + return 'You need at least 250 completions of the Theatre of Blood before you can attempt Hard Mode.'; } } if (user.minionIsBusy) { diff --git a/src/mahoji/lib/abstracted_commands/trekCommand.ts b/src/mahoji/lib/abstracted_commands/trekCommand.ts index 12f05f54dd..25b62cfcdc 100644 --- a/src/mahoji/lib/abstracted_commands/trekCommand.ts +++ b/src/mahoji/lib/abstracted_commands/trekCommand.ts @@ -53,7 +53,7 @@ export async function trekCommand(user: MUser, channelID: string, difficulty: st if (!meetsRequirements) { return `You don't have the requirements to do ${tier.difficulty} treks! Your ${readableStatName( unmetKey! - )} stat in your ${setup} setup is ${has}, but you need atleast ${ + )} stat in your ${setup} setup is ${has}, but you need at least ${ tier.minimumGearRequirements[setup]?.[unmetKey!] }.`; } @@ -62,7 +62,7 @@ export async function trekCommand(user: MUser, channelID: string, difficulty: st } if (qp < 30) { - return 'You need atleast level 30 QP to do Temple Trekking.'; + return 'You need at least level 30 QP to do Temple Trekking.'; } if (minLevel !== undefined && user.combatLevel < minLevel) { diff --git a/src/mahoji/lib/abstracted_commands/volcanicMineCommand.ts b/src/mahoji/lib/abstracted_commands/volcanicMineCommand.ts index 8ddb48a360..6abdcf379d 100644 --- a/src/mahoji/lib/abstracted_commands/volcanicMineCommand.ts +++ b/src/mahoji/lib/abstracted_commands/volcanicMineCommand.ts @@ -234,6 +234,6 @@ export async function volcanicMineStatsCommand(user: MUser) { const currentUserPoints = user.user.volcanic_mine_points; const kc = await getMinigameScore(user.id, 'volcanic_mine'); - return `You have ${currentUserPoints.toLocaleString()} Volanic Mine points points. + return `You have ${currentUserPoints.toLocaleString()} Volcanic Mine points. You have completed ${kc} games of Volcanic Mine.`; } diff --git a/src/mahoji/lib/abstracted_commands/warriorsGuildCommand.ts b/src/mahoji/lib/abstracted_commands/warriorsGuildCommand.ts index 46a0472f69..fe12eddfff 100644 --- a/src/mahoji/lib/abstracted_commands/warriorsGuildCommand.ts +++ b/src/mahoji/lib/abstracted_commands/warriorsGuildCommand.ts @@ -82,7 +82,7 @@ async function cyclopsCommand(user: MUser, channelID: string, quantity: number | // Check if either 100 warrior guild tokens or attack cape (similar items in future) const amountTokens = userBank.amount('Warrior guild token'); if (!hasAttackCape && amountTokens < 100) { - return 'You need atleast 100 Warriors guild tokens to kill Cyclops.'; + return 'You need at least 100 Warriors guild tokens to kill Cyclops.'; } // If no quantity provided, set it to the max. if (!quantity) { @@ -107,7 +107,7 @@ async function cyclopsCommand(user: MUser, channelID: string, quantity: number | if (!hasAttackCape && amountTokens < tokensToSpend) { return `You don't have enough Warrior guild tokens to kill cyclopes for ${formatDuration( duration - )}, try a lower quantity. You need atleast ${Math.floor( + )}, try a lower quantity. You need at least ${Math.floor( (duration / Time.Minute) * 10 + 10 )}x Warrior guild tokens to kill ${quantity}x cyclopes.`; } @@ -144,7 +144,7 @@ export async function warriorsGuildCommand( const atkLvl = user.skillLevel('attack'); const strLvl = user.skillLevel('strength'); if (atkLvl + strLvl < 130 && atkLvl !== 99 && strLvl !== 99) { - return "To enter the Warrior's Guild, your Attack and Strength levels must add up to atleast 130, or you must have level 99 in either."; + return "To enter the Warrior's Guild, your Attack and Strength levels must add up to at least 130, or you must have level 99 in either."; } if (choice === 'cyclops') { diff --git a/src/mahoji/mahojiSettings.ts b/src/mahoji/mahojiSettings.ts index 6eb0c25293..e4df529d54 100644 --- a/src/mahoji/mahojiSettings.ts +++ b/src/mahoji/mahojiSettings.ts @@ -257,7 +257,7 @@ export function hasMonsterRequirements(user: MUser, monster: KillableMonster) { false, `You don't have the requirements to kill ${monster.name}! Your ${readableStatName( unmetKey! - )} stat in your ${setup} setup is ${has}, but you need atleast ${ + )} stat in your ${setup} setup is ${has}, but you need at least ${ monster.minimumGearRequirements[setup]?.[unmetKey!] }.` ]; diff --git a/src/tasks/minions/HunterActivity/hunterActivity.ts b/src/tasks/minions/HunterActivity/hunterActivity.ts index 7a333ffdc2..24994a87f5 100644 --- a/src/tasks/minions/HunterActivity/hunterActivity.ts +++ b/src/tasks/minions/HunterActivity/hunterActivity.ts @@ -178,7 +178,7 @@ export const hunterTask: MinionTask = { let str = `${user}, ${user.minionName} finished hunting ${creature.name}${ crystalImpling - ? '.' + ? '. ' : ` ${quantity}x times, due to clever creatures you missed out on ${ quantity - successfulQuantity }x catches. ` diff --git a/src/tasks/minions/mageArena2Activity.ts b/src/tasks/minions/mageArena2Activity.ts index 4a348b5e13..7cc092ecf6 100644 --- a/src/tasks/minions/mageArena2Activity.ts +++ b/src/tasks/minions/mageArena2Activity.ts @@ -14,12 +14,12 @@ export const mageArenaTwoTask: MinionTask = { let loot: Bank | undefined = undefined; if (percentChance(70)) { const deathReason = randArrItem([ - 'Died to Porazdir.', - 'Killed by Derwen.', - 'Killed by Justiciar Zachariah.', - "PK'd by a clan.", - 'Killed by Chaos Elemental.', - 'Killed by a PKer.' + 'Died to Porazdir', + 'Killed by Derwen', + 'Killed by Justiciar Zachariah', + "PK'd by a clan", + 'Killed by Chaos Elemental', + 'Killed by a PKer' ]); str = `${user}, ${user.minionName} failed to complete the Mage Arena II: ${deathReason}. Try again.`; } else { diff --git a/src/tasks/minions/minigames/fightCavesActivity.ts b/src/tasks/minions/minigames/fightCavesActivity.ts index f4ced983d9..a5cb2e71c2 100644 --- a/src/tasks/minions/minigames/fightCavesActivity.ts +++ b/src/tasks/minions/minigames/fightCavesActivity.ts @@ -35,7 +35,7 @@ export const fightCavesTask: MinionTask = { { fight_caves_attempts: true } ); - const attemptsStr = `You have tried Fight caves ${newFightCavesAttempts}x times.`; + const attemptsStr = `You have tried Fight caves ${newFightCavesAttempts}x times`; // Add slayer const usersTask = await getUsersCurrentSlayerInfo(user.id); @@ -81,7 +81,7 @@ export const fightCavesTask: MinionTask = { preJadDeathTime )} into your attempt.${slayerMsg} The following supplies were refunded back into your bank: ${itemLootBank}.`, await chatHeadImage({ - content: `You die before you even reach TzTok-Jad...atleast you tried, I give you ${tokkulReward}x Tokkul. ${attemptsStr}`, + content: `You die before you even reach TzTok-Jad... At least you tried, I give you ${tokkulReward}x Tokkul. ${attemptsStr}.`, head: 'mejJal' }), data, @@ -117,7 +117,7 @@ export const fightCavesTask: MinionTask = { channelID, `${user} ${msg}`, await chatHeadImage({ - content: `TzTok-Jad stomp you to death...nice try though JalYt, for your effort I give you ${tokkulReward}x Tokkul. ${attemptsStr}.`, + content: `TzTok-Jad stomp you to death... Nice try though JalYt, for your effort I give you ${tokkulReward}x Tokkul. ${attemptsStr}.`, head: 'mejJal' }), data, @@ -152,8 +152,8 @@ export const fightCavesTask: MinionTask = { itemsToAdd: loot }); - const rangeXP = await user.addXP({ skillName: SkillsEnum.Ranged, amount: 47_580, duration }); - const hpXP = await user.addXP({ skillName: SkillsEnum.Hitpoints, amount: 15_860, duration }); + const rangeXP = await user.addXP({ skillName: SkillsEnum.Ranged, amount: 47_580, duration, minimal: true }); + const hpXP = await user.addXP({ skillName: SkillsEnum.Hitpoints, amount: 15_860, duration, minimal: true }); let msg = `${rangeXP}. ${hpXP}.`; if (isOnTask) { @@ -186,7 +186,13 @@ export const fightCavesTask: MinionTask = { } }); - const slayXP = await user.addXP({ skillName: SkillsEnum.Slayer, amount: slayerXP, duration }); + const slayXP = await user.addXP({ + skillName: SkillsEnum.Slayer, + amount: slayerXP, + duration, + minimal: true + }); + const xpMessage = `${msg} ${slayXP}`; msg = `Jad task completed. ${xpMessage}. \n**You've completed ${currentStreak} tasks and received ${points} points; giving you a total of ${secondNewUser.newUser.slayer_points}; return to a Slayer master.**`; diff --git a/src/tasks/minions/minigames/infernoActivity.ts b/src/tasks/minions/minigames/infernoActivity.ts index c5dc66f388..02693b14b7 100644 --- a/src/tasks/minions/minigames/infernoActivity.ts +++ b/src/tasks/minions/minigames/infernoActivity.ts @@ -51,33 +51,43 @@ export const infernoTask: MinionTask = { const [hasDiary] = await userhasDiaryTier(user, diariesObject.KaramjaDiary.elite); if (hasDiary) tokkul *= 2; const baseBank = new Bank().add('Tokkul', tokkul); - - let xpStr = await user.addXP({ - skillName: SkillsEnum.Ranged, - amount: calcPercentOfNum(percentMadeItThrough, 80_000), - duration, - minimal: true - }); - xpStr += await user.addXP({ - skillName: SkillsEnum.Hitpoints, - amount: calcPercentOfNum(percentMadeItThrough, 35_000), - duration, - minimal: true - }); - xpStr += await user.addXP({ - skillName: SkillsEnum.Magic, - amount: calcPercentOfNum(percentMadeItThrough, 25_000), - duration, - minimal: true - }); + const xpBonuses = []; + + xpBonuses.push( + await user.addXP({ + skillName: SkillsEnum.Ranged, + amount: calcPercentOfNum(percentMadeItThrough, 80_000), + duration, + minimal: true + }) + ); + xpBonuses.push( + await user.addXP({ + skillName: SkillsEnum.Hitpoints, + amount: calcPercentOfNum(percentMadeItThrough, 35_000), + duration, + minimal: true + }) + ); + xpBonuses.push( + await user.addXP({ + skillName: SkillsEnum.Magic, + amount: calcPercentOfNum(percentMadeItThrough, 25_000), + duration, + minimal: true + }) + ); if (isOnTask) { - xpStr += await user.addXP({ - skillName: SkillsEnum.Slayer, - amount: deathTime === null ? 125_000 : calcPercentOfNum(percentMadeItThrough, 25_000), - duration - }); + xpBonuses.push( + await user.addXP({ + skillName: SkillsEnum.Slayer, + amount: deathTime === null ? 125_000 : calcPercentOfNum(percentMadeItThrough, 25_000), + duration + }) + ); } + const xpStr = xpBonuses.join(', '); if (!deathTime) { await incrementMinigameScore(userID, 'inferno', 1); } @@ -156,7 +166,7 @@ export const infernoTask: MinionTask = { if (diedPreZuk) { text += `You died ${formatDuration(deathTime!)} into your attempt, before you reached Zuk.`; - chatText = `You die before you even reach TzKal-Zuk...atleast you tried, I give you ${baseBank.amount( + chatText = `You die before you even reach TzKal-Zuk... At least you tried, I give you ${baseBank.amount( 'Tokkul' )}x Tokkul.`; } else if (diedZuk) { From 6af89b175fc5e4c66724a3b822c5d8fb38186e15 Mon Sep 17 00:00:00 2001 From: GC <30398469+gc@users.noreply.github.com> Date: Thu, 15 Aug 2024 07:26:57 +1000 Subject: [PATCH 23/27] Catch role task errors better (#6002) --- src/lib/rolesTask.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/lib/rolesTask.ts b/src/lib/rolesTask.ts index b9c7d0e375..2145740d7d 100644 --- a/src/lib/rolesTask.ts +++ b/src/lib/rolesTask.ts @@ -15,6 +15,7 @@ import { TeamLoot } from './simulation/TeamLoot'; import { SkillsArray } from './skilling/types'; import type { ItemBank } from './types'; import { fetchMultipleCLLeaderboards } from './util/clLeaderboard'; +import { logError } from './util/logError'; const RoleResultSchema = z.object({ roleID: z.string().min(17).max(19), @@ -365,12 +366,17 @@ export async function runRolesTask(dryRun: boolean): Promise { for (const [name, fn] of tup) { promiseQueue.add(async () => { const stopwatch = new Stopwatch(); - const res = await fn(); - console.log(`[RolesTask] Ran ${name} in ${stopwatch.stop()}`); - const [validResults, invalidResults] = partition(res, i => RoleResultSchema.safeParse(i).success); - results.push(...validResults); - if (invalidResults.length > 0) { - console.error(`[RolesTask] Invalid results for ${name}: ${JSON.stringify(invalidResults)}`); + try { + const res = await fn(); + const [validResults, invalidResults] = partition(res, i => RoleResultSchema.safeParse(i).success); + results.push(...validResults); + if (invalidResults.length > 0) { + logError(`[RolesTask] Invalid results for ${name}: ${JSON.stringify(invalidResults)}`); + } + } catch (err) { + logError(`[RolesTask] Error in ${name}: ${err}`); + } finally { + debugLog(`[RolesTask] Ran ${name} in ${stopwatch.stop()}`); } }); } From 8f328de2de3c150b0c8d1113a1f6d554e131d775 Mon Sep 17 00:00:00 2001 From: gc <30398469+gc@users.noreply.github.com> Date: Thu, 15 Aug 2024 07:36:50 +1000 Subject: [PATCH 24/27] Catch top-level errors from roles task --- src/mahoji/commands/rp.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/mahoji/commands/rp.ts b/src/mahoji/commands/rp.ts index 0ca844d664..ee0f40f6ce 100644 --- a/src/mahoji/commands/rp.ts +++ b/src/mahoji/commands/rp.ts @@ -29,6 +29,7 @@ import getOSItem from '../../lib/util/getOSItem'; import { handleMahojiConfirmation } from '../../lib/util/handleMahojiConfirmation'; import { deferInteraction } from '../../lib/util/interactionReply'; import itemIsTradeable from '../../lib/util/itemIsTradeable'; +import { logError } from '../../lib/util/logError'; import { makeBankImage } from '../../lib/util/makeBankImage'; import { migrateUser } from '../../lib/util/migrateUser'; import { parseBank } from '../../lib/util/parseStringBank'; @@ -621,7 +622,13 @@ Date: ${dateFm(date)}`; for (const action of actions) { if (options.action[action.name]) { if (!action.allowed(adminUser)) return randArrItem(gifs); - return action.run(); + try { + const result = await action.run(); + return result; + } catch (err) { + logError(err); + return 'An error occurred.'; + } } } } From 84d04b00a540f930e3617c92a55c85a7f5319207 Mon Sep 17 00:00:00 2001 From: gc <30398469+gc@users.noreply.github.com> Date: Thu, 15 Aug 2024 08:09:46 +1000 Subject: [PATCH 25/27] Improve role task queries --- src/lib/rawSql.ts | 12 ++++++++++++ src/lib/rolesTask.ts | 9 +++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/lib/rawSql.ts b/src/lib/rawSql.ts index 54b68fc695..45def967e9 100644 --- a/src/lib/rawSql.ts +++ b/src/lib/rawSql.ts @@ -1,4 +1,5 @@ import { Prisma } from '@prisma/client'; +import { logError } from './util/logError'; const u = Prisma.UserScalarFieldEnum; @@ -14,3 +15,14 @@ SET ${u.cl_array} = ( ) WHERE ${u.id} = '${userID}';` }; + +export async function loggedRawPrismaQuery(query: string): Promise { + try { + const result = await prisma.$queryRawUnsafe(query); + return result; + } catch (err) { + logError(err, { query: query.slice(0, 100) }); + } + + return null; +} diff --git a/src/lib/rolesTask.ts b/src/lib/rolesTask.ts index 2145740d7d..eb76b1d692 100644 --- a/src/lib/rolesTask.ts +++ b/src/lib/rolesTask.ts @@ -1,4 +1,4 @@ -import { noOp, uniqueArr } from 'e'; +import { noOp, notEmpty, uniqueArr } from 'e'; import { SupportServer } from '../config'; import { BadgesEnum, Roles } from '../lib/constants'; @@ -11,6 +11,7 @@ import { partition } from 'remeda'; import z from 'zod'; import { type CommandResponse, Stopwatch, convertXPtoLVL, getUsernameSync, returnStringOrFile } from '../lib/util'; import { ClueTiers } from './clues/clueTiers'; +import { loggedRawPrismaQuery } from './rawSql'; import { TeamLoot } from './simulation/TeamLoot'; import { SkillsArray } from './skilling/types'; import type { ItemBank } from './types'; @@ -385,8 +386,8 @@ export async function runRolesTask(dryRun: boolean): Promise { debugLog(`Finished role functions, ${results.length} results`); - const allBadgeIDs = uniqueArr(results.map(i => i.badge)); - const allRoleIDs = uniqueArr(results.map(i => i.roleID)); + const allBadgeIDs = uniqueArr(results.map(i => i.badge)).filter(notEmpty); + const allRoleIDs = uniqueArr(results.map(i => i.roleID)).filter(notEmpty); if (!dryRun) { const roleNames = new Map(); @@ -396,7 +397,7 @@ export async function runRolesTask(dryRun: boolean): Promise { // Remove all top badges from all users (and add back later) debugLog('Removing badges...'); const badgeIDs = `ARRAY[${allBadgeIDs.join(',')}]`; - await prisma.$queryRawUnsafe(` + await loggedRawPrismaQuery(` UPDATE users SET badges = badges - ${badgeIDs} WHERE badges && ${badgeIDs} From 190dff203b0288417a8ba3aec802c3e3c9aa269c Mon Sep 17 00:00:00 2001 From: gc <30398469+gc@users.noreply.github.com> Date: Thu, 15 Aug 2024 08:16:03 +1000 Subject: [PATCH 26/27] Fix leagues tasks lb --- src/mahoji/commands/leaderboard.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mahoji/commands/leaderboard.ts b/src/mahoji/commands/leaderboard.ts index 2541d30272..dc57ab4c4f 100644 --- a/src/mahoji/commands/leaderboard.ts +++ b/src/mahoji/commands/leaderboard.ts @@ -883,7 +883,7 @@ async function leaguesPointsLeaderboard(interaction: ChatInputCommandInteraction } async function leastCompletedLeagueTasksLb() { - const taskCounts = await roboChimpClient.$queryRaw<{ task_id: number; qty: number }[]>`SELECT task_id, count(*) AS qty + const taskCounts = await roboChimpClient.$queryRaw<{ task_id: number; qty: number }[]>`SELECT task_id, count(*)::int AS qty FROM ( SELECT unnest(leagues_completed_tasks_ids) AS task_id FROM public.user From 08c27021bfa6951bfa5957f1d2043f946ab7cd6e Mon Sep 17 00:00:00 2001 From: nwjgit <69014816+nwjgit@users.noreply.github.com> Date: Tue, 20 Aug 2024 12:45:35 -0500 Subject: [PATCH 27/27] Adjust mining portent charges (#6001) --- src/lib/bso/divination.ts | 4 ++-- src/tasks/minions/miningActivity.ts | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/lib/bso/divination.ts b/src/lib/bso/divination.ts index 1e0d3b4622..2535457efe 100644 --- a/src/lib/bso/divination.ts +++ b/src/lib/bso/divination.ts @@ -351,9 +351,9 @@ export const portents: SourcePortent[] = [ description: 'Consumes stone spirits to grant extra mining XP, instead of extra ore.', divinationLevelToCreate: 90, cost: new Bank().add('Incandescent energy', 1200), - chargesPerPortent: 1000, + chargesPerPortent: 60 * 10, addChargeMessage: portent => - `You used a Spiritual mining portent, your next ${portent.charges_remaining}x stone spirits will grant XP instead of ore.` + `You used a Spiritual mining portent, it will turn stone spirits into extra mining XP, instead of ore, in your next ${portent.charges_remaining} minutes of mining.` }, { id: PortentID.PacifistPortent, diff --git a/src/tasks/minions/miningActivity.ts b/src/tasks/minions/miningActivity.ts index 2f93938b93..4b5c42d16e 100644 --- a/src/tasks/minions/miningActivity.ts +++ b/src/tasks/minions/miningActivity.ts @@ -251,6 +251,7 @@ export const miningTask: MinionTask = { async run(data: MiningActivityTaskOptions) { const { oreID, userID, channelID, duration, powermine } = data; const { quantity } = data; + const minutes = Math.round(duration / Time.Minute); const user = await mUserFetch(userID); const ore = Mining.Ores.find(ore => ore.id === oreID)!; @@ -264,7 +265,7 @@ export const miningTask: MinionTask = { ? await chargePortentIfHasCharges({ user, portentID: PortentID.MiningPortent, - charges: amountOfSpiritsToUse + charges: minutes }) : null; const {