mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-12 17:28:31 +03:00
fix: do scoreID hashing better
This commit is contained in:
@@ -23,6 +23,8 @@ const BANNED_METRIC_NAMES = [
|
||||
"enumIndexes", // haha
|
||||
"optional", // used for optional metrics
|
||||
"playcount", // used by showcase stats
|
||||
"scoreID",
|
||||
"userID",
|
||||
];
|
||||
|
||||
t.test("#GetGamePTConfig", (t) => {
|
||||
|
||||
@@ -167,7 +167,7 @@ t.test("#ScoreID Canary", (t) => {
|
||||
|
||||
t.equal(
|
||||
scoreID,
|
||||
"Tc4f10c2ca8e99478de5ae318a40ee66051f7297ca648415502b6f393a23e6f10",
|
||||
"T5d669c4d5d6ca80761e87698acd77c51d2bed95b64ab76e65952dbca7c26bc81",
|
||||
"ScoreID **SHOULD BE DETERMINISTIC**. A change to the ScoreID algorithm is **a major breaking change.**"
|
||||
);
|
||||
|
||||
|
||||
@@ -1,22 +1,9 @@
|
||||
import db from "external/mongo/db";
|
||||
import fjsh from "fast-json-stable-hash";
|
||||
import { GetGPTConfig } from "tachi-common";
|
||||
import crypto from "crypto";
|
||||
import type { DryScore } from "../common/types";
|
||||
import type { integer, GPTString, ProvidedMetrics, OptionalMetrics } from "tachi-common";
|
||||
|
||||
/**
|
||||
* Performs sha256 hashing on the input data.
|
||||
* @param scoreIDString - The string to sha256 hash.
|
||||
* @returns A sha256 checksum in lowercase hex.
|
||||
*/
|
||||
function HashScoreIDString(scoreIDString: string) {
|
||||
return crypto.createHash("sha256").update(scoreIDString).digest("hex");
|
||||
}
|
||||
|
||||
function SortKeysAlphabetically(a: string, b: string) {
|
||||
return a.localeCompare(b, "en-GB");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an identifier for this score.
|
||||
* This is used to deduplicate repeated scores.
|
||||
@@ -28,34 +15,30 @@ export function CreateScoreID(
|
||||
dryScore: DryScore,
|
||||
chartID: string
|
||||
) {
|
||||
const elements = [userID, chartID];
|
||||
const elements: Record<string, number | string> = { userID, chartID };
|
||||
|
||||
const gptConfig = GetGPTConfig(gptString);
|
||||
|
||||
// @warn
|
||||
// we need to sort these metric keys deterministically instead
|
||||
// of relying on any sort of object-insertion order
|
||||
// as that would throw the checksum out of sync.
|
||||
for (const m of Object.keys(gptConfig.providedMetrics).sort(SortKeysAlphabetically)) {
|
||||
for (const m of Object.keys(gptConfig.providedMetrics)) {
|
||||
const metric = m as keyof ProvidedMetrics[GPTString];
|
||||
|
||||
elements.push(dryScore.scoreData[metric]);
|
||||
elements[metric] = dryScore.scoreData[metric];
|
||||
}
|
||||
|
||||
// Also include optional metrics in the checksum if they should be
|
||||
// part of the scoreID.
|
||||
for (const [m, conf] of Object.entries(gptConfig.optionalMetrics).sort((a, b) =>
|
||||
SortKeysAlphabetically(a[0], b[0])
|
||||
)) {
|
||||
for (const [m, conf] of Object.entries(gptConfig.optionalMetrics)) {
|
||||
const metric = m as keyof OptionalMetrics[GPTString];
|
||||
|
||||
if (conf.partOfScoreID) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
elements.push(dryScore.scoreData.optional[metric] ?? null);
|
||||
elements[`optional.${metric}`] = dryScore.scoreData.optional[metric] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
const hash = HashScoreIDString(elements.join("\0"));
|
||||
// use a stable object hashing method instead of string joining
|
||||
// as it's immune to key order or anything screwy like that.
|
||||
const hash = fjsh.hash(elements, "sha256");
|
||||
|
||||
return `T${hash}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user