Files
adrianbonpin cd72b7a948 refactor: convert to bun workspaces monorepo
- Move web app into apps/web/
- Create packages/shared/ with shared types
- Create plugins/decky-vault/ scaffold
- Root package.json manages workspaces only
2026-06-28 05:20:28 +08:00

187 lines
6.7 KiB
TypeScript

import { Elysia, t } from "elysia"
import { db } from "@/lib/db/index"
import { games, gameVersions, performanceEntries, hardware } from "@/lib/db/schema"
import { eq, and, inArray } from "drizzle-orm"
export const compareRoutes = new Elysia({ prefix: "/compare", detail: { tags: ["Games"] } })
.get(
"/games",
async ({ query, set }) => {
const gameIds = query.ids.split(",").filter(Boolean).slice(0, 4)
if (gameIds.length < 2) {
set.status = 400
return { error: "At least 2 games required" }
}
// Fetch games
const gameRows = await db
.select({
id: games.id,
title: games.title,
steamAppId: games.steamAppId,
source: games.source,
capsuleImage: games.capsuleImage,
headerImage: games.headerImage,
})
.from(games)
.where(inArray(games.id, gameIds))
const results = await Promise.all(
gameRows.map(async (game) => {
// Fetch all non-removed entries for this game
const entries = await db
.select({
fpsAvg: performanceEntries.fpsAvg,
fpsLow: performanceEntries.fpsLow,
fpsHigh: performanceEntries.fpsHigh,
fpsOnePercentLow: performanceEntries.fpsOnePercentLow,
upscalerType: performanceEntries.upscalerType,
frameGenMethod: performanceEntries.frameGenMethod,
hardwareSlug: performanceEntries.hardwareSlug,
hardwareName: hardware.name,
})
.from(performanceEntries)
.innerJoin(gameVersions, eq(performanceEntries.versionId, gameVersions.id))
.innerJoin(hardware, eq(performanceEntries.hardwareSlug, hardware.slug))
.where(
and(
eq(gameVersions.gameId, game.id),
eq(performanceEntries.isRemoved, false)
)
)
if (entries.length === 0) {
return {
...game,
stats: {
totalEntries: 0,
avgFps: null,
medianFps: null,
bestFps: null,
avgOnePercentLow: null,
avgStability: null,
bestDevice: null,
tierBreakdown: null,
deviceBreakdown: [],
},
}
}
const fpsValues = entries.map((e) => e.fpsAvg ?? 0).sort((a, b) => a - b)
const avgFps = fpsValues.reduce((a, b) => a + b, 0) / fpsValues.length
const medianFps = fpsValues[Math.floor(fpsValues.length / 2)]
const onePercentLowValues = entries
.filter((e) => e.fpsOnePercentLow != null)
.map((e) => e.fpsOnePercentLow!)
const avgOnePercentLow =
onePercentLowValues.length > 0
? onePercentLowValues.reduce((a, b) => a + b, 0) / onePercentLowValues.length
: null
const stabilityScores = entries
.filter((e) => e.fpsOnePercentLow != null && e.fpsAvg > 0)
.map((e) => Math.min(1, e.fpsOnePercentLow! / (e.fpsAvg ?? 1)))
const avgStability =
stabilityScores.length > 0
? stabilityScores.reduce((a, b) => a + b, 0) / stabilityScores.length
: null
// Best device by avg fps
const deviceFpsMap = new Map<string, number[]>()
for (const e of entries) {
const arr = deviceFpsMap.get(e.hardwareSlug) || []
arr.push(e.fpsAvg ?? 0)
deviceFpsMap.set(e.hardwareSlug, arr)
}
let bestDevice: string | null = null
let bestDeviceAvg = 0
for (const [slug, fpsArr] of deviceFpsMap) {
const mean = fpsArr.reduce((a, b) => a + b, 0) / fpsArr.length
if (mean > bestDeviceAvg) {
bestDeviceAvg = mean
bestDevice = slug
}
}
// Tier breakdown
const tiers = { unplayable: 0, playable: 0, smooth: 0, excellent: 0 }
for (const e of entries) {
const fps = e.fpsAvg ?? 0
if (fps < 30) tiers.unplayable++
else if (fps < 60) tiers.playable++
else if (fps < 120) tiers.smooth++
else tiers.excellent++
}
// Device breakdown
const deviceBreakdown = Array.from(deviceFpsMap.entries()).map(([slug, fpsArr]) => ({
hardwareSlug: slug,
count: fpsArr.length,
avgFps: Math.round((fpsArr.reduce((a, b) => a + b, 0) / fpsArr.length) * 10) / 10,
}))
return {
...game,
stats: {
totalEntries: entries.length,
avgFps: Math.round(avgFps * 10) / 10,
medianFps: Math.round(medianFps * 10) / 10,
bestFps: Math.round(fpsValues[fpsValues.length - 1] * 10) / 10,
avgOnePercentLow: avgOnePercentLow ? Math.round(avgOnePercentLow * 10) / 10 : null,
avgStability: avgStability ? Math.round(avgStability * 100) : null,
bestDevice,
tierBreakdown: tiers,
deviceBreakdown,
},
}
})
)
return { games: results }
},
{
query: t.Object({ ids: t.String() }),
}
)
.get(
"/presets",
async ({ query, set }) => {
const presetIds = query.ids.split(",").filter(Boolean).slice(0, 4)
if (presetIds.length < 2) {
set.status = 400
return { error: "At least 2 presets required" }
}
const presetRows = await db
.select({
id: performanceEntries.id,
fpsAvg: performanceEntries.fpsAvg,
fpsLow: performanceEntries.fpsLow,
fpsHigh: performanceEntries.fpsHigh,
fpsOnePercentLow: performanceEntries.fpsOnePercentLow,
upscalerType: performanceEntries.upscalerType,
upscalerVersion: performanceEntries.upscalerVersion,
frameGenMethod: performanceEntries.frameGenMethod,
protonVersion: performanceEntries.protonVersion,
osVersion: performanceEntries.osVersion,
loadTimeSsd: performanceEntries.loadTimeSsd,
loadTimeSd: performanceEntries.loadTimeSd,
settingsJson: performanceEntries.settingsJson,
launchOptions: performanceEntries.launchOptions,
hardwareSlug: performanceEntries.hardwareSlug,
hardwareName: hardware.name,
createdAt: performanceEntries.createdAt,
})
.from(performanceEntries)
.innerJoin(hardware, eq(performanceEntries.hardwareSlug, hardware.slug))
.where(inArray(performanceEntries.id, presetIds))
return { presets: presetRows }
},
{
query: t.Object({ ids: t.String() }),
}
)