diff --git a/backend/src/api/controllers/result.ts b/backend/src/api/controllers/result.ts index 4cc1d286d..16e1c31d1 100644 --- a/backend/src/api/controllers/result.ts +++ b/backend/src/api/controllers/result.ts @@ -20,7 +20,7 @@ import Logger from "../../utils/logger"; import "dotenv/config"; import { MonkeyResponse } from "../../utils/monkey-response"; import MonkeyError from "../../utils/error"; -import { isTestTooShort } from "../../utils/validation"; +import { areFunboxesCompatible, isTestTooShort } from "../../utils/validation"; import { implemented as anticheatImplemented, validateResult, @@ -36,7 +36,7 @@ import { getDailyLeaderboard } from "../../utils/daily-leaderboards"; import AutoRoleList from "../../constants/auto-roles"; import * as UserDAL from "../../dal/user"; import { buildMonkeyMail } from "../../utils/monkey-mail"; -import FunboxesMetadata from "../../constants/funbox"; +import FunboxList from "../../constants/funbox-list"; import _ from "lodash"; import * as WeeklyXpLeaderboard from "../../services/weekly-xp-leaderboard"; import { UAParser } from "ua-parser-js"; @@ -175,6 +175,17 @@ export async function addResult( } } + if (result.funbox) { + const funboxes = result.funbox.split("#"); + if (funboxes.length !== _.uniq(funboxes).length) { + throw new MonkeyError(400, "Duplicate funboxes"); + } + } + + if (!areFunboxesCompatible(result.funbox)) { + throw new MonkeyError(400, "Impossible funbox combination"); + } + try { result.keySpacingStats = { average: @@ -625,9 +636,8 @@ async function calculateXp( } if (funboxBonusConfiguration > 0) { - const resultFunboxes = _.uniq(funbox.split("#")); - const funboxModifier = _.sumBy(resultFunboxes, (funboxName) => { - const funbox = FunboxesMetadata[funboxName as string]; + const funboxModifier = _.sumBy(funbox.split("#"), (funboxName) => { + const funbox = FunboxList.find((f) => f.name === funboxName); const difficultyLevel = funbox?.difficultyLevel ?? 0; return Math.max(difficultyLevel * funboxBonusConfiguration, 0); }); diff --git a/backend/src/utils/validation.ts b/backend/src/utils/validation.ts index bd4f1af6f..39e6c5cd2 100644 --- a/backend/src/utils/validation.ts +++ b/backend/src/utils/validation.ts @@ -1,7 +1,8 @@ import _ from "lodash"; import { replaceHomoglyphs } from "../constants/homoglyphs"; import { profanities, regexProfanities } from "../constants/profanities"; -import { matchesAPattern, sanitizeString } from "./misc"; +import { intersect, matchesAPattern, sanitizeString } from "./misc"; +import { default as FunboxList } from "../constants/funbox-list"; export function inRange(value: number, min: number, max: number): boolean { return value >= min && value <= max; @@ -105,3 +106,154 @@ export function isTestTooShort(result: MonkeyTypes.CompletedEvent): boolean { return false; } + +export function areFunboxesCompatible(funboxesString: string): boolean { + const funboxes = funboxesString.split("#").filter((f) => f !== "none"); + + const funboxesToCheck = FunboxList.filter((f) => funboxes.includes(f.name)); + + console.log(funboxesToCheck); + console.log(funboxes); + + const allFunboxesAreValid = funboxesToCheck.length === funboxes.length; + const oneWordModifierMax = + funboxesToCheck.filter( + (f) => + f.frontendFunctions?.includes("getWord") || + f.frontendFunctions?.includes("pullSection") || + f.frontendFunctions?.includes("withWords") + ).length <= 1; + const layoutUsability = + funboxesToCheck.filter((f) => + f.properties?.find((fp) => fp === "changesLayout") + ).length === 0 || + funboxesToCheck.filter((f) => + f.properties?.find((fp) => fp === "ignoresLayout" || fp === "usesLayout") + ).length === 0; + const oneNospaceOrToPushMax = + funboxesToCheck.filter((f) => + f.properties?.find((fp) => fp === "nospace" || fp.startsWith("toPush")) + ).length <= 1; + const oneChangesWordsVisibilityMax = + funboxesToCheck.filter((f) => + f.properties?.find((fp) => fp === "changesWordsVisibility") + ).length <= 1; + const oneFrequencyChangesMax = + funboxesToCheck.filter((f) => + f.properties?.find((fp) => fp === "changesWordsFrequency") + ).length <= 1; + const noFrequencyChangesConflicts = + funboxesToCheck.filter((f) => + f.properties?.find((fp) => fp === "changesWordsFrequency") + ).length === 0 || + funboxesToCheck.filter((f) => + f.properties?.find((fp) => fp === "ignoresLanguage") + ).length === 0; + const capitalisationChangePosibility = + funboxesToCheck.filter((f) => + f.properties?.find((fp) => fp === "noLetters") + ).length === 0 || + funboxesToCheck.filter((f) => + f.properties?.find((fp) => fp === "changesCapitalisation") + ).length === 0; + const noConflictsWithSymmetricChars = + funboxesToCheck.filter((f) => + f.properties?.find((fp) => fp === "conflictsWithSymmetricChars") + ).length === 0 || + funboxesToCheck.filter((f) => + f.properties?.find((fp) => fp === "symmetricChars") + ).length === 0; + const canSpeak = + funboxesToCheck.filter((f) => + f.properties?.find((fp) => fp === "speaks" || fp === "unspeakable") + ).length <= 1; + const hasLanguageToSpeak = + funboxesToCheck.filter((f) => f.properties?.find((fp) => fp === "speaks")) + .length === 0 || + funboxesToCheck.filter((f) => + f.properties?.find((fp) => fp === "ignoresLanguage") + ).length === 0; + const oneToPushOrPullSectionMax = + funboxesToCheck.filter( + (f) => + f.properties?.find((fp) => fp.startsWith("toPush:")) || + f.frontendFunctions?.includes("pullSection") + ).length <= 1; + const oneApplyCSSMax = + funboxesToCheck.filter((f) => f.frontendFunctions?.includes("applyCSS")) + .length <= 1; + const onePunctuateWordMax = + funboxesToCheck.filter((f) => + f.frontendFunctions?.includes("punctuateWord") + ).length <= 1; + const oneCharCheckerMax = + funboxesToCheck.filter((f) => + f.frontendFunctions?.includes("isCharCorrect") + ).length <= 1; + const oneCharReplacerMax = + funboxesToCheck.filter((f) => f.frontendFunctions?.includes("getWordHtml")) + .length <= 1; + const allowedConfig = {} as Record; + let noConfigConflicts = true; + for (const f of funboxesToCheck) { + if (!f.frontendForcedConfig) continue; + for (const key in f.frontendForcedConfig) { + if (allowedConfig[key]) { + if ( + intersect( + allowedConfig[key], + f.frontendForcedConfig[key], + true + ).length === 0 + ) { + noConfigConflicts = false; + break; + } + } else { + allowedConfig[key] = f.frontendForcedConfig[key]; + } + } + } + + console.log(allowedConfig); + + console.log( + allFunboxesAreValid, + oneWordModifierMax, + layoutUsability, + oneNospaceOrToPushMax, + oneChangesWordsVisibilityMax, + oneFrequencyChangesMax, + noFrequencyChangesConflicts, + capitalisationChangePosibility, + noConflictsWithSymmetricChars, + canSpeak, + hasLanguageToSpeak, + oneToPushOrPullSectionMax, + oneApplyCSSMax, + onePunctuateWordMax, + oneCharCheckerMax, + oneCharReplacerMax, + noConfigConflicts + ); + + return ( + allFunboxesAreValid && + oneWordModifierMax && + layoutUsability && + oneNospaceOrToPushMax && + oneChangesWordsVisibilityMax && + oneFrequencyChangesMax && + noFrequencyChangesConflicts && + capitalisationChangePosibility && + noConflictsWithSymmetricChars && + canSpeak && + hasLanguageToSpeak && + oneToPushOrPullSectionMax && + oneApplyCSSMax && + onePunctuateWordMax && + oneCharCheckerMax && + oneCharReplacerMax && + noConfigConflicts + ); +}