diff --git a/typescript/common/src/config/config.ts b/typescript/common/src/config/config.ts index 62df99f8e..e03fca4d1 100644 --- a/typescript/common/src/config/config.ts +++ b/typescript/common/src/config/config.ts @@ -355,7 +355,7 @@ export function ValidateMetric(gameConfig: GameConfig, metricName: string, metri return "Cannot validate a graph or nullable graph metric."; } - if (conf.chartDependentMax) { + if (conf.chartDependentMax === true) { return `This metric is chart dependent and not appropriate to check in this context.`; } diff --git a/typescript/common/src/config/game-support/arcaea.ts b/typescript/common/src/config/game-support/arcaea.ts index f3fc003f3..7779478f3 100644 --- a/typescript/common/src/config/game-support/arcaea.ts +++ b/typescript/common/src/config/game-support/arcaea.ts @@ -1,3 +1,4 @@ +import { p } from "prudence"; import { z } from "zod"; import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_GROUP_CONFIG } from "../../types/internals"; @@ -48,6 +49,8 @@ export const GAME_ARCAEA_CONF = { score: { type: "INTEGER", chartDependentMax: true, + validate: p.isPositiveInteger, + allowFolderGoalsIf: (v: number) => v < 10_000_000, formatter: FmtNum, description: "The score value. This is between 0 and 10 million, plus bonus points dependent on how many shiny PUREs you get.", diff --git a/typescript/common/src/config/game-support/maimai.ts b/typescript/common/src/config/game-support/maimai.ts index 737355bbd..a651d0175 100644 --- a/typescript/common/src/config/game-support/maimai.ts +++ b/typescript/common/src/config/game-support/maimai.ts @@ -1,3 +1,4 @@ +import { p } from "prudence"; import { z } from "zod"; import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_GROUP_CONFIG } from "../../types/internals"; @@ -62,6 +63,8 @@ export const GAME_MAIMAI_CONF = { percent: { type: "DECIMAL", chartDependentMax: true, + allowFolderGoalsIf: (v) => v >= 0 && v < 100.0, + validate: p.isPositive, formatter: FmtPercent, description: "The percent this score was worth. Sometimes called 'rate' in game. This is upper-bounded by how many BREAK notes the chart has.", diff --git a/typescript/common/src/types/metrics.ts b/typescript/common/src/types/metrics.ts index b28865844..a7b319660 100644 --- a/typescript/common/src/types/metrics.ts +++ b/typescript/common/src/types/metrics.ts @@ -45,6 +45,7 @@ interface ConfDecimalScoreMetricChartDependent { * @example: IIDX's EX Score is upperbounded at 2x the chart's notecount. */ chartDependentMax: true; + allowFolderGoalsIf?: never; } interface ConfIntegerScoreMetricChartDependent { @@ -57,14 +58,51 @@ interface ConfIntegerScoreMetricChartDependent { * @example: IIDX's EX Score is upperbounded at 2x the chart's notecount. */ chartDependentMax: true; + allowFolderGoalsIf?: never; +} + +interface ConfDecimalScoreMetricChartDependentWithExemption { + type: "DECIMAL"; + formatter: (v: number) => string; + validate: (v: number) => string | true; + + /** + * When the value is chart dependent, + * should folder-wide goals be allowed conditionally? + * + * @example: + * Arcaea's score is upperbounded at 10M+notecount, but scores below 10M + * behave as if the max was 10M. + */ + allowFolderGoalsIf: (v: number) => boolean; + chartDependentMax: true; +} + +interface ConfIntegerScoreMetricChartDependentWithExemption { + type: "INTEGER"; + formatter: (v: number) => string; + validate: (v: number) => string | true; + + /** + * When the value is chart dependent, + * should folder-wide goals be allowed conditionally? + * + * @example: + * Arcaea's score is upperbounded at 10M+notecount, but scores below 10M + * behave as if the max was 10M. + */ + allowFolderGoalsIf: (v: number) => boolean; + chartDependentMax: true; } export type ConfDecimalScoreMetric = | ConfDecimalScoreMetricChartDependent + | ConfDecimalScoreMetricChartDependentWithExemption | ConfDecimalScoreMetricNormal; export type ConfIntegerScoreMetric = | ConfIntegerScoreMetricChartDependent + | ConfIntegerScoreMetricChartDependentWithExemption | ConfIntegerScoreMetricNormal; /** diff --git a/typescript/server/src/lib/targets/goal-utils.ts b/typescript/server/src/lib/targets/goal-utils.ts index 162539a14..f401ba4ed 100644 --- a/typescript/server/src/lib/targets/goal-utils.ts +++ b/typescript/server/src/lib/targets/goal-utils.ts @@ -268,7 +268,10 @@ export async function ValidateGoalChartsAndCriteria( switch (config.type) { case "DECIMAL": case "INTEGER": { - if (config.chartDependentMax && charts.type !== "single") { + const allowFolderGoals = + config.chartDependentMax !== true || config.allowFolderGoalsIf?.(criteria.value); + + if (!allowFolderGoals && charts.type !== "single") { throw new Error( `Creating ${criteria.key} goals on multiple charts where the maximum value is relative to the chart is a terrible idea, and has been disabled.`, ); @@ -276,7 +279,7 @@ export async function ValidateGoalChartsAndCriteria( let err; - if (config.chartDependentMax) { + if (!allowFolderGoals) { const chart = await GetChartByIdForGame(game, charts.data as string); if (!chart) { @@ -288,6 +291,7 @@ export async function ValidateGoalChartsAndCriteria( // @ts-expect-error this is fine leave me alone err = gptImpl.chartSpecificValidators[criteria.key](criteria.value, chart); } else { + // @ts-expect-error if allowFolderGoals is true, validate has to exist, and tsc's opinion has no weight here. err = config.validate(criteria.value); }