feat: rearrange game benchmark wizard and improve overall

This commit is contained in:
2026-05-01 01:18:13 +08:00
parent 293aadbff0
commit 824fbbd13f
22 changed files with 722 additions and 143 deletions
+2 -1
View File
@@ -8,6 +8,7 @@ import {
user,
} from "@/lib/db/schema"
import { eq, desc, sql, and, ilike, isNull, isNotNull, or } from "drizzle-orm"
import { fuzzySearchTerm } from "@/lib/db/search"
import {
requireContributorOrAdmin,
requireAdmin,
@@ -47,7 +48,7 @@ export const adminPerformanceRoutes = new Elysia({ prefix: "/admin" })
conditions.push(
or(
ilike(user.name, `%${searchTerm}%`),
ilike(games.title, `%${searchTerm}%`),
ilike(games.title, fuzzySearchTerm(searchTerm)),
),
)
}
+7 -1
View File
@@ -17,6 +17,7 @@ import {
or,
} from "drizzle-orm"
import { requireRole } from "@/lib/auth/guard"
import { fuzzySearchTerm } from "@/lib/db/search"
/** Which columns are text-searchable via ilike */
export type CrudSearchConfig = {
@@ -94,10 +95,15 @@ export function createCrudRoutes<T extends AnyPgTable>(
// Search
if (query.search && search) {
const searchStr = query.search
const searchConditions = search.fields
.map((field) => {
const col = columns[field]
return col ? ilike(col, `%${query.search}%`) : null
if (!col) return null
const pattern = field === "title"
? fuzzySearchTerm(searchStr)
: `%${searchStr}%`
return ilike(col, pattern)
})
.filter(Boolean) as SQL[]
if (searchConditions.length > 0) {
+3 -1
View File
@@ -8,6 +8,7 @@ import {
hardware,
} from "@/lib/db/schema"
import { ilike, or, sql, eq, and, desc, asc, inArray, gte, lte } from "drizzle-orm"
import { fuzzySearchTerm } from "@/lib/db/search"
const MAX_OFFSET = 10000
const PAGE_SIZE = 24
@@ -39,10 +40,11 @@ export const gamesListingRoutes = new Elysia({ prefix: "/games/listing" }).get(
// Search filter (title, developer, publisher)
if (search) {
const titleTerm = fuzzySearchTerm(search)
const term = `%${search}%`
conditions.push(
or(
ilike(games.title, term),
ilike(games.title, titleTerm),
ilike(games.developer, term),
ilike(games.publisher, term),
)!,
+52 -11
View File
@@ -4,8 +4,9 @@ import {
performanceEntries,
gameVersions,
hardware,
gamePlatformSupport,
} from "@/lib/db/schema"
import { eq, sql } from "drizzle-orm"
import { eq, and, sql } from "drizzle-orm"
import { requireRole } from "@/lib/auth/guard"
import { recalculatePlayability } from "./playability"
@@ -72,7 +73,7 @@ export const performanceSubmitRoutes = new Elysia({ prefix: "/performance" })
// Verify the game version exists
const [version] = await db
.select({ id: gameVersions.id })
.select({ id: gameVersions.id, gameId: gameVersions.gameId })
.from(gameVersions)
.where(eq(gameVersions.id, body.versionId))
.limit(1)
@@ -120,18 +121,48 @@ export const performanceSubmitRoutes = new Elysia({ prefix: "/performance" })
})
.returning()
// Recalculate playability for this game (fire and forget)
const [gameVersion] = await db
.select({ gameId: gameVersions.gameId })
.from(gameVersions)
.where(eq(gameVersions.id, body.versionId))
.limit(1)
if (gameVersion) {
recalculatePlayability(gameVersion.gameId).catch((err) =>
console.error("Failed to recalculate playability:", err),
// Update or create gamePlatformSupport with anti-cheat info
const [existingSupport] = await db
.select()
.from(gamePlatformSupport)
.where(
and(
eq(gamePlatformSupport.gameId, version.gameId),
eq(gamePlatformSupport.hardwareSlug, body.hardwareSlug),
),
)
.limit(1)
if (existingSupport) {
await db
.update(gamePlatformSupport)
.set({
antiCheatRelevant: body.antiCheatRelevant ?? existingSupport.antiCheatRelevant,
antiCheatName: body.antiCheatRelevant
? (body.antiCheatName ?? existingSupport.antiCheatName)
: null,
antiCheatStatus: body.antiCheatStatus ?? existingSupport.antiCheatStatus,
updatedAt: new Date(),
})
.where(eq(gamePlatformSupport.id, existingSupport.id))
} else {
await db.insert(gamePlatformSupport).values({
gameId: version.gameId,
hardwareSlug: body.hardwareSlug,
isSupported: true,
protonStatus: "unknown",
antiCheatRelevant: body.antiCheatRelevant ?? false,
antiCheatName: body.antiCheatRelevant ? body.antiCheatName ?? null : null,
antiCheatStatus: body.antiCheatStatus ?? "unknown",
playabilityStatus: "unknown",
})
}
// Recalculate playability for this game (fire and forget)
recalculatePlayability(version.gameId).catch((err) =>
console.error("Failed to recalculate playability:", err),
)
set.status = 201
return {
id: entry.id,
@@ -190,6 +221,16 @@ export const performanceSubmitRoutes = new Elysia({ prefix: "/performance" })
]),
),
userNotes: t.Optional(t.Union([t.String(), t.Null()])),
antiCheatRelevant: t.Optional(t.Boolean()),
antiCheatName: t.Optional(t.Union([t.String(), t.Null()])),
antiCheatStatus: t.Optional(
t.Union([
t.Literal("none"),
t.Literal("supported"),
t.Literal("unsupported"),
t.Literal("unknown"),
]),
),
}),
},
)
+87 -9
View File
@@ -1,6 +1,6 @@
import { Elysia, t } from "elysia"
import { createCrudRoutes } from "./crud-builder"
import { performanceEntries, games, gameVersions, hardware, user } from "@/lib/db/schema"
import { performanceEntries, games, gameVersions, hardware, user, gamePlatformSupport } from "@/lib/db/schema"
import { db } from "@/lib/db/index"
import { eq, and, desc, sql } from "drizzle-orm"
import { requireRole } from "@/lib/auth/guard"
@@ -268,10 +268,10 @@ export const performanceVerifyRoutes = new Elysia({
updatedAt: new Date(),
}
if (body.fpsAvg !== undefined) updateData.fpsAvg = body.fpsAvg
if (body.fpsOnePercentLow !== undefined) updateData.fpsOnePercentLow = body.fpsOnePercentLow
if (body.fpsLow !== undefined) updateData.fpsLow = body.fpsLow
if (body.fpsHigh !== undefined) updateData.fpsHigh = body.fpsHigh
if (body.fpsAvg !== undefined) updateData.fpsAvg = body.fpsAvg ?? undefined
if (body.fpsOnePercentLow !== undefined) updateData.fpsOnePercentLow = body.fpsOnePercentLow ?? undefined
if (body.fpsLow !== undefined) updateData.fpsLow = body.fpsLow ?? undefined
if (body.fpsHigh !== undefined) updateData.fpsHigh = body.fpsHigh ?? undefined
if (body.protonVersion !== undefined)
updateData.protonVersion = body.protonVersion
if (body.osVersion !== undefined)
@@ -295,15 +295,82 @@ export const performanceVerifyRoutes = new Elysia({
.where(eq(performanceEntries.id, params.id))
.returning()
// Update gamePlatformSupport anti-cheat info if provided
if (
body.antiCheatRelevant !== undefined ||
body.antiCheatName !== undefined ||
body.antiCheatStatus !== undefined
) {
// Need versionId to resolve gameId
const [entryVersion] = await db
.select({ versionId: performanceEntries.versionId })
.from(performanceEntries)
.where(eq(performanceEntries.id, params.id))
.limit(1)
if (entryVersion) {
const [gv] = await db
.select({ gameId: gameVersions.gameId })
.from(gameVersions)
.where(eq(gameVersions.id, entryVersion.versionId))
.limit(1)
if (gv) {
const [existingSupport] = await db
.select()
.from(gamePlatformSupport)
.where(
and(
eq(gamePlatformSupport.gameId, gv.gameId),
eq(gamePlatformSupport.hardwareSlug, updated.hardwareSlug),
),
)
.limit(1)
if (existingSupport) {
await db
.update(gamePlatformSupport)
.set({
antiCheatRelevant:
body.antiCheatRelevant !== undefined
? body.antiCheatRelevant
: existingSupport.antiCheatRelevant,
antiCheatName:
body.antiCheatName !== undefined
? body.antiCheatName
: existingSupport.antiCheatName,
antiCheatStatus:
body.antiCheatStatus !== undefined
? (body.antiCheatStatus ?? "unknown")
: existingSupport.antiCheatStatus,
updatedAt: new Date(),
})
.where(eq(gamePlatformSupport.id, existingSupport.id))
} else {
await db.insert(gamePlatformSupport).values({
gameId: gv.gameId,
hardwareSlug: updated.hardwareSlug,
isSupported: true,
protonStatus: "unknown",
antiCheatRelevant: body.antiCheatRelevant ?? false,
antiCheatName: body.antiCheatName ?? null,
antiCheatStatus: (body.antiCheatStatus ?? "unknown"),
playabilityStatus: "unknown",
})
}
}
}
}
return updated
},
{
params: t.Object({ id: t.String() }),
body: t.Object({
fpsAvg: t.Optional(t.Number()),
fpsOnePercentLow: t.Optional(t.Number()),
fpsLow: t.Optional(t.Number()),
fpsHigh: t.Optional(t.Number()),
fpsAvg: t.Optional(t.Union([t.Number(), t.Null()])),
fpsOnePercentLow: t.Optional(t.Union([t.Number(), t.Null()])),
fpsLow: t.Optional(t.Union([t.Number(), t.Null()])),
fpsHigh: t.Optional(t.Union([t.Number(), t.Null()])),
protonVersion: t.Optional(t.Union([t.String(), t.Null()])),
osVersion: t.Optional(t.Union([t.String(), t.Null()])),
upscalerType: t.Optional(
@@ -331,6 +398,17 @@ export const performanceVerifyRoutes = new Elysia({
launchOptions: t.Optional(t.Union([t.String(), t.Null()])),
settingsJson: t.Optional(t.Union([t.Array(t.Any()), t.Null()])),
userNotes: t.Optional(t.Union([t.String(), t.Null()])),
antiCheatRelevant: t.Optional(t.Boolean()),
antiCheatName: t.Optional(t.Union([t.String(), t.Null()])),
antiCheatStatus: t.Optional(
t.Union([
t.Literal("none"),
t.Literal("supported"),
t.Literal("unsupported"),
t.Literal("unknown"),
t.Null(),
]),
),
}),
},
)
+17 -2
View File
@@ -7,6 +7,7 @@ import {
gameComments,
} from "@/lib/db/schema"
import { ilike, or, sql, eq, inArray, and } from "drizzle-orm"
import { fuzzySearchTerm } from "@/lib/db/search"
interface SteamSearchItem {
id: number
@@ -31,6 +32,7 @@ export const searchUnifiedRoutes = new Elysia({ prefix: "/search" }).get(
return { error: "Query must be at least 2 characters" }
}
const titleTerm = fuzzySearchTerm(query.q)
const term = `%${query.q}%`
// ── 1. Search local database ────────────────────────────────────
@@ -39,7 +41,7 @@ export const searchUnifiedRoutes = new Elysia({ prefix: "/search" }).get(
.from(games)
.where(
or(
ilike(games.title, term),
ilike(games.title, titleTerm),
ilike(games.developer, term),
ilike(games.publisher, term),
),
@@ -281,6 +283,7 @@ export const searchUnifiedRoutes = new Elysia({ prefix: "/search" }).get(
genres: g.genres,
source: g.source,
counts,
platforms: g.platforms,
platformSupport: platform
? {
isSupported: platform.isSupported,
@@ -289,7 +292,19 @@ export const searchUnifiedRoutes = new Elysia({ prefix: "/search" }).get(
antiCheatName: platform.antiCheatName,
antiCheatStatus: platform.antiCheatStatus,
}
: null,
: g.platforms
? {
isSupported: g.platforms.linux || g.platforms.windows || false,
protonStatus: g.platforms.linux
? "native"
: g.platforms.windows
? "proton"
: "unsupported",
antiCheatRelevant: false,
antiCheatName: null,
antiCheatStatus: "unknown",
}
: null,
isRawPerformer: rawPerformerMap.get(g.id) ?? false,
isPoorPerformance: poorPerformerMap.get(g.id) ?? false,
bestFps: bestFpsMap.get(g.id) ?? null,
+17
View File
@@ -0,0 +1,17 @@
/**
* Normalizes a raw search query so that common word separators
* (spaces, hyphens, underscores, colons, dots) are treated equivalently.
*
* Replaces any sequence of separator chars with a single `%` ILIKE wildcard,
* then wraps the whole pattern in `%…%`.
*
* Example:
* fuzzySearchTerm("counter strike") → "%counter%strike%"
* fuzzySearchTerm("counter-strike") → "%counter%strike%"
*
* Both will match "Counter-Strike", "Counter Strike", "Counter_Strike", etc.
*/
export function fuzzySearchTerm(rawQuery: string): string {
const normalized = rawQuery.replace(/[-_\s:.]+/g, "%")
return `%${normalized}%`
}