API optimisations (#343)

* feat: api optimisation

* feat: emulator rename
This commit is contained in:
DecDuck
2026-02-06 23:12:03 +11:00
committed by GitHub
parent d6920700cb
commit bc5623cc78
20 changed files with 246 additions and 134 deletions
@@ -1,11 +1,11 @@
<template>
<div
v-if="executor"
v-if="emulator"
class="flex space-x-4 rounded-md bg-zinc-900/50 px-6 outline -outline-offset-1 outline-white/10 w-fit text-xs font-bold text-zinc-100"
>
<div class="inline-flex gap-x-2 items-center">
<img :src="useObject(executor.gameIcon)" class="size-6" />
<span>{{ executor.gameName }}</span>
<img :src="useObject(emulator.gameIcon)" class="size-6" />
<span>{{ emulator.gameName }}</span>
</div>
<div class="flex items-center">
<svg
@@ -17,7 +17,7 @@
>
<path d="M.293 0l22 22-22 22h1.414l22-22-22-22H.293z" />
</svg>
<span class="ml-4">{{ executor.versionName }}</span>
<span class="ml-4">{{ emulator.versionName }}</span>
</div>
<div class="flex items-center">
<svg
@@ -29,13 +29,13 @@
>
<path d="M.293 0l22 22-22 22h1.414l22-22-22-22H.293z" />
</svg>
<span class="ml-4 truncate">{{ executor.launchName }}</span>
<span class="ml-4 truncate">{{ emulator.launchName }}</span>
</div>
</div>
</template>
<script setup lang="ts">
import type { ExecutorLaunchObject } from "~/composables/frontend";
import type { EmulatorLaunchObject } from "~/composables/frontend";
defineProps<{ executor: ExecutorLaunchObject }>();
defineProps<{ emulator: EmulatorLaunchObject }>();
</script>
@@ -23,16 +23,16 @@
>
<p>{{ props.config.command }}</p>
</div>
<ExecutorWidget
v-if="!isSetup(props.config) && props.config.executor"
:executor="{
<EmulatorWidget
v-if="!isSetup(props.config) && props.config.emulator"
:emulator="{
launchId: props.config.launchId,
gameName: props.config.executor.gameVersion.game.mName,
gameIcon: props.config.executor.gameVersion.game.mIconObjectId,
versionName: (props.config.executor.gameVersion.displayName ??
props.config.executor.gameVersion.versionPath)!,
launchName: props.config.executor.name,
platform: props.config.executor.platform,
gameName: props.config.emulator.gameVersion.game.mName,
gameIcon: props.config.emulator.gameVersion.game.mIconObjectId,
versionName: (props.config.emulator.gameVersion.displayName ??
props.config.emulator.gameVersion.versionPath)!,
launchName: props.config.emulator.name,
platform: props.config.emulator.platform,
}"
/>
</li>
+30 -30
View File
@@ -87,7 +87,7 @@
class="size-5"
/>
<img
v-if="guess.type === 'executor'"
v-if="guess.type === 'emulator'"
:src="useObject(guess.icon)"
class="size-5"
/>
@@ -142,7 +142,7 @@
</Combobox>
</div>
<div
v-if="props.type && props.type === 'Executor'"
v-if="props.type && props.type === 'Emulator'"
class="ml-1 mt-2 rounded-lg bg-blue-900/10 p-1 outline outline-blue-900"
>
<div class="flex items-center">
@@ -155,16 +155,16 @@
<div class="ml-2 inline-flex items-center">
<p class="text-sm text-blue-200">
<i18n-t
keypath="library.admin.launchRow.executorHint"
keypath="library.admin.launchRow.emulatorHint"
tag="span"
scope="global"
>
<template #executor>
<template #rom>
<span
class="font-mono bg-zinc-950 text-zinc-100 py-1 px-0.5 rounded-xl"
>{{
// eslint-disable-next-line @intlify/vue-i18n/no-raw-text
"{executor}"
"{rom}"
}}</span
>
</template>
@@ -181,32 +181,32 @@
>
{{ $t("library.admin.import.version.platform") }}
</SelectorPlatform>
<div v-if="props.type && props.type === 'Game' && props.allowExecutor">
<div v-if="props.type && props.type === 'Game' && props.allowEmulator">
<h1 class="block text-sm font-medium leading-6 text-zinc-100">
{{ $t("library.admin.launchRow.executorTitle") }}
{{ $t("library.admin.launchRow.emulatorTitle") }}
</h1>
<div class="relative mt-2 space-x-1 inline-flex items-center w-full">
<ExecutorWidget v-if="executor" :executor="executor" />
<EmulatorWidget v-if="emulator" :emulator="emulator" />
<div
v-else
class="font-bold uppercase font-display text-zinc-500 text-sm"
>
{{ $t("library.admin.launchRow.noExecutorSelected") }}
{{ $t("library.admin.launchRow.noEmulatorSelected") }}
</div>
<div class="grow" />
<LoadingButton :loading="false" @click="selectLaunchOpen = true">{{
$t("library.admin.launchRow.executorSelect")
$t("library.admin.launchRow.emulatorSelect")
}}</LoadingButton>
<button
:disabled="!executor"
:disabled="!emulator"
class="transition rounded p-2 bg-zinc-900/30 group hover:enabled:bg-red-600/10 text-zinc-400 hover:enabled:text-red-600 disabled:bg-zinc-900/80 disabled:text-zinc-700"
@click="() => (executor = undefined)"
@click="() => (emulator = undefined)"
>
<TrashIcon class="transition size-5" />
</button>
</div>
</div>
<div v-if="props.type && props.type === 'Executor'">
<div v-if="props.type && props.type === 'Emulator'">
<p class="block text-sm font-medium leading-6 text-zinc-100">
{{ $t("library.admin.launchRow.autosuggestHint") }}
</p>
@@ -219,7 +219,7 @@
v-model="selectLaunchOpen"
class="-mt-2"
:filter-platform="launchConfiguration.platform"
@select="(v) => (executor = v)"
@select="(v) => (emulator = v)"
/>
</div>
</template>
@@ -234,7 +234,7 @@ import {
} from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import { InformationCircleIcon, TrashIcon } from "@heroicons/vue/24/outline";
import type { ExecutorLaunchObject } from "~/composables/frontend";
import type { EmulatorLaunchObject } from "~/composables/frontend";
import type { GameType, Platform } from "~/prisma/client/enums";
import type { ImportVersion } from "~/server/api/v1/admin/import/version/index.post";
@@ -247,17 +247,17 @@ const launchConfiguration = defineModel<
name?: string;
}
>({ required: true });
const _executorMetadata = ref<ExecutorLaunchObject | undefined>(undefined);
const executor = computed({
const _emulatorMetadata = ref<EmulatorLaunchObject | undefined>(undefined);
const emulator = computed({
get() {
return _executorMetadata.value;
return _emulatorMetadata.value;
},
set(v) {
_executorMetadata.value = v;
_emulatorMetadata.value = v;
if (v) {
launchConfiguration.value.executorId = v.launchId;
launchConfiguration.value.emulatorId = v.launchId;
} else {
launchConfiguration.value.executorId = undefined;
launchConfiguration.value.emulatorId = undefined;
}
},
});
@@ -265,9 +265,9 @@ const executor = computed({
function updatePlatform(v: Platform | undefined) {
if (!v) return;
launchConfiguration.value.platform = v;
if (executor.value) {
if (executor.value.platform !== v) {
executor.value = undefined;
if (emulator.value) {
if (emulator.value.platform !== v) {
emulator.value = undefined;
}
}
}
@@ -275,11 +275,11 @@ function updatePlatform(v: Platform | undefined) {
const props = defineProps<{
versionGuesses: Array<VersionGuess> | undefined;
needsName: boolean;
allowExecutor?: boolean;
allowEmulator?: boolean;
type?: GameType;
}>();
if (props.type && props.type === "Executor")
if (props.type && props.type === "Emulator")
launchConfiguration.value.suggestions ??= [];
const selectLaunchOpen = ref(false);
@@ -299,15 +299,15 @@ function updateLaunchCommand(command: string) {
if (autosetGuess) {
if (autosetGuess.type === "platform") {
launchConfiguration.value.platform = autosetGuess.platform;
} else if (autosetGuess.type === "executor") {
executor.value = {
launchId: autosetGuess.executorId,
} else if (autosetGuess.type === "emulator") {
emulator.value = {
launchId: autosetGuess.emulatorId,
gameName: autosetGuess.gameName,
gameIcon: autosetGuess.icon,
versionName: autosetGuess.launchName,
launchName: autosetGuess.launchName,
platform: autosetGuess.platform,
} satisfies ExecutorLaunchObject;
} satisfies EmulatorLaunchObject;
launchConfiguration.value.platform = autosetGuess.platform;
}
}
+3 -3
View File
@@ -141,7 +141,7 @@
<script setup lang="ts">
import { XCircleIcon } from "@heroicons/vue/24/outline";
import type { ExecutorLaunchObject } from "~/composables/frontend";
import type { EmulatorLaunchObject } from "~/composables/frontend";
import type { Platform } from "~/prisma/client/enums";
import type { GameMetadataSearchResult } from "~/server/internal/metadata/types";
@@ -174,12 +174,12 @@ const versions = ref<
>(undefined);
const emit = defineEmits<{
select: [data: ExecutorLaunchObject];
select: [data: EmulatorLaunchObject];
}>();
async function search(query: string) {
return await $dropFetch("/api/v1/admin/search/game", {
query: { q: query, type: "Executor" },
query: { q: query, type: "Emulator" },
});
}
+1 -1
View File
@@ -12,7 +12,7 @@ declare module "@vue/runtime-core" {
interface ComponentCustomOptions extends _ComponentCustomOptions {}
}
export interface ExecutorLaunchObject {
export interface EmulatorLaunchObject {
launchId: string;
gameName: string;
gameIcon: string;
+6 -6
View File
@@ -436,16 +436,16 @@
"launchRow": {
"autosuggestHint": "Auto-suggest extensions",
"currentDirHint": "The installation directory is set as the current directory when launching. It is not prepended to your command.",
"executorHint": "{executor} is replaced with the game's launch command for emulators.",
"executorSelect": "Select new executor",
"executorTitle": "Executor",
"noExecutorSelected": "No executor selected"
"emulatorHint": "{rom} is replaced with the game's launch command for emulators.",
"emulatorSelect": "Select new emulator",
"emulatorTitle": "Emulator",
"noEmulatorSelected": "No emulator selected"
},
"launchSelector": {
"description": "Select a launch option as an executor for your new launch option.",
"description": "Select a launch option as an emulator for your new launch option.",
"noVersions": "No versions imported.",
"platformFilterHint": "Only showing launches for:",
"search": "Search for an executor",
"search": "Search for an emulator",
"selectCommand": "Select a launch command",
"selectVersions": "Select a version",
"title": "Select a launch option"
+1
View File
@@ -59,6 +59,7 @@
"prisma": "6.11.1",
"sanitize-filename": "^1.6.3",
"semver": "^7.7.1",
"shescape": "^2.1.8",
"stream-mime-type": "^2.0.0",
"turndown": "^7.2.0",
"unstorage": "^1.15.0",
+3 -4
View File
@@ -75,7 +75,6 @@
<div v-if="versionGuesses" class="flex flex-col gap-4">
<!-- setup executable -->
<div class="bg-zinc-800 p-4 rounded-xl relative flex flex-col gap-y-2">
<div>
<label class="block text-sm font-medium leading-6 text-zinc-100">{{
@@ -155,7 +154,7 @@
</Switch>
</SwitchGroup>
<div
v-if="type === GameType.Redist"
v-if="type === GameType.Dependency"
class="absolute inset-0 bg-zinc-900/50"
/>
</div>
@@ -215,7 +214,7 @@
v-model="versionSettings.launches[launchIdx]"
:version-guesses="versionGuesses"
:needs-name="true"
:allow-executor="true"
:allow-emulator="true"
:type="type"
/>
</DisclosurePanel>
@@ -361,7 +360,7 @@ const currentlySelectedVersion = ref(-1);
const versionSettings = ref<Omit<typeof ImportVersion.infer, "version" | "id">>(
{
delta: false,
onlySetup: type === GameType.Redist,
onlySetup: type === GameType.Dependency,
launches: [],
setups: [],
requiredContent: [],
+6 -6
View File
@@ -369,15 +369,15 @@ const importModes: {
title: "Game",
description: "Games are shown in store, and are discoverable.",
},
Executor: {
title: "Executor",
Emulator: {
title: "Emulator",
description:
"Executors are used to launch games. Mainly emulators or wrappers.",
"Emulators are used to launch other games, wrapping them in another executable.",
},
Redist: {
title: "Redistributable",
Dependency: {
title: "Dependency",
description:
"Additional content that must be downloaded and installed before running the game.",
"Dependencies are setup-only files that get installed before the game is launched.",
},
};
+5 -12
View File
@@ -93,12 +93,12 @@
{{ $t("store.size") }}
</td>
<td
v-if="size.versions.length > 0"
v-if="sizes.length > 0"
class="whitespace-nowrap inline-flex gap-x-4 px-3 py-4 text-sm text-zinc-400"
>
<ul class="flex flex-col">
<ol
v-for="version in size.versions"
v-for="version in sizes"
:key="version.versionId"
class="inline-flex items-center gap-x-1"
>
@@ -268,21 +268,14 @@ const gameId = route.params.id.toString();
const user = useUser();
const { game, rating, size } = await $dropFetch(`/api/v1/games/${gameId}`);
const { game, rating, sizes, platforms } = await $dropFetch(
`/api/v1/games/${gameId}`,
);
const isClient = isClientRequest();
const descriptionHTML = micromark(game.mDescription);
const platforms = game.versions
.map((e) => [
...e.launches.map((v) => v.platform),
...e.setups.map((v) => v.platform),
])
.flat()
.flat()
.filter((e, i, u) => u.indexOf(e) === i);
// const rating = Math.round(game.mReviewRating * 5);
const averageRating = Math.round((rating._avg.mReviewRating ?? 0) * 5);
const ratingArray = Array(5)
+26
View File
@@ -125,6 +125,9 @@ importers:
semver:
specifier: ^7.7.1
version: 7.7.2
shescape:
specifier: ^2.1.8
version: 2.1.8
stream-mime-type:
specifier: ^2.0.0
version: 2.0.0
@@ -532,6 +535,10 @@ packages:
'@emnapi/wasi-threads@1.1.0':
resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==}
'@ericcornelissen/lregexp@1.0.7':
resolution: {integrity: sha512-1FFCOKq49eWRCinu3VuTfV1jv0kgeSET89OHDAUDaASzvIpwmh2/nFUayoEXB65mPT3atgECjlu3KVTIQU7jhw==}
engines: {node: ^12.20.0 || ^14.13.0 || ^15 || ^16 || ^17 || ^18 || ^19 || ^20 || ^21 || ^22 || ^23 || ^24 || ^25}
'@es-joy/jsdoccomment@0.52.0':
resolution: {integrity: sha512-BXuN7BII+8AyNtn57euU2Yxo9yA/KUDNzrpXyi3pfqKmBhhysR6ZWOebFh3vyPoqA3/j1SOvGgucElMGwlXing==}
engines: {node: '>=20.11.0'}
@@ -4435,6 +4442,10 @@ packages:
resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==}
engines: {node: '>=18'}
is-supported-regexp-flag@2.0.0:
resolution: {integrity: sha512-8i4+OYUjdUaJ88KAs1WojIThDFjIpeYNrSlYy1g/At2p9YjQ7HEmB1yn60un0jRFjM3TQbKPMAluTPEPncZfqA==}
engines: {node: '>=12.20'}
is-url-superb@4.0.0:
resolution: {integrity: sha512-GI+WjezhPPcbM+tqE9LnmsY5qqjwHzTvjJ36wxYX5ujNXefSUJ/T17r5bqDV8yLhcgB59KTPNOc9O9cmHTPWsA==}
engines: {node: '>=10'}
@@ -5941,6 +5952,10 @@ packages:
resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==}
engines: {node: '>= 0.4'}
shescape@2.1.8:
resolution: {integrity: sha512-owfw+5BB1A51KyNiCuUOyDSh1JOw+DrxgX0Uac7eORpF6YBEoLjbDlHn6tHQudnyi3pt3HeHl9jJ30YPmtEMHQ==}
engines: {node: ^14.18.0 || ^16.13.0 || ^18 || ^19 || ^20 || ^22 || ^24 || ^25}
side-channel-list@1.0.0:
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
engines: {node: '>= 0.4'}
@@ -7324,6 +7339,10 @@ snapshots:
tslib: 2.8.1
optional: true
'@ericcornelissen/lregexp@1.0.7':
dependencies:
is-supported-regexp-flag: 2.0.0
'@es-joy/jsdoccomment@0.52.0':
dependencies:
'@types/estree': 1.0.8
@@ -11564,6 +11583,8 @@ snapshots:
is-stream@4.0.1: {}
is-supported-regexp-flag@2.0.0: {}
is-url-superb@4.0.0: {}
is-url@1.2.4: {}
@@ -13484,6 +13505,11 @@ snapshots:
shell-quote@1.8.3: {}
shescape@2.1.8:
dependencies:
'@ericcornelissen/lregexp': 1.0.7
which: 5.0.0
side-channel-list@1.0.0:
dependencies:
es-errors: 1.3.0
@@ -0,0 +1,31 @@
/*
Warnings:
- The values [Executor,Redist] on the enum `GameType` will be removed. If these variants are still used in the database, this will fail.
*/
-- AlterEnum
BEGIN;
CREATE TYPE "GameType_new" AS ENUM ('Game', 'Emulator', 'Dependency');
ALTER TABLE "Game" ALTER COLUMN "type" DROP DEFAULT;
ALTER TABLE "Game" ALTER COLUMN "type" TYPE "GameType_new" USING ("type"::text::"GameType_new");
ALTER TYPE "GameType" RENAME TO "GameType_old";
ALTER TYPE "GameType_new" RENAME TO "GameType";
DROP TYPE "GameType_old";
ALTER TABLE "Game" ALTER COLUMN "type" SET DEFAULT 'Game';
COMMIT;
-- DropIndex
DROP INDEX "Game_mName_idx";
-- DropIndex
DROP INDEX "GameTag_name_idx";
-- AlterTable
ALTER TABLE "LaunchConfiguration" ADD COLUMN "umuStoreOverride" TEXT;
-- CreateIndex
CREATE INDEX "Game_mName_idx" ON "Game" USING GIST ("mName" gist_trgm_ops(siglen=32));
-- CreateIndex
CREATE INDEX "GameTag_name_idx" ON "GameTag" USING GIST ("name" gist_trgm_ops(siglen=32));
@@ -0,0 +1,30 @@
/*
Warnings:
- You are about to drop the column `executorId` on the `LaunchConfiguration` table. All the data in the column will be lost.
- You are about to drop the column `executorSuggestions` on the `LaunchConfiguration` table. All the data in the column will be lost.
*/
-- DropForeignKey
ALTER TABLE "LaunchConfiguration" DROP CONSTRAINT "LaunchConfiguration_executorId_fkey";
-- DropIndex
DROP INDEX "Game_mName_idx";
-- DropIndex
DROP INDEX "GameTag_name_idx";
-- AlterTable
ALTER TABLE "LaunchConfiguration" DROP COLUMN "executorId",
DROP COLUMN "executorSuggestions",
ADD COLUMN "emulatorId" TEXT,
ADD COLUMN "emulatorSuggestions" TEXT[];
-- CreateIndex
CREATE INDEX "Game_mName_idx" ON "Game" USING GIST ("mName" gist_trgm_ops(siglen=32));
-- CreateIndex
CREATE INDEX "GameTag_name_idx" ON "GameTag" USING GIST ("name" gist_trgm_ops(siglen=32));
-- AddForeignKey
ALTER TABLE "LaunchConfiguration" ADD CONSTRAINT "LaunchConfiguration_emulatorId_fkey" FOREIGN KEY ("emulatorId") REFERENCES "LaunchConfiguration"("launchId") ON DELETE SET NULL ON UPDATE CASCADE;
+8 -7
View File
@@ -10,8 +10,8 @@ enum MetadataSource {
enum GameType {
Game
Executor
Redist
Emulator
Dependency
}
model Game {
@@ -149,16 +149,17 @@ model LaunchConfiguration {
platform Platform
// For emulation targets
executorId String?
executor LaunchConfiguration? @relation(fields: [executorId], references: [launchId], name: "executor")
executorSuggestions String[]
emulatorId String?
emulator LaunchConfiguration? @relation(fields: [emulatorId], references: [launchId], name: "emulator")
emulatorSuggestions String[]
umuIdOverride String?
umuIdOverride String?
umuStoreOverride String?
versionId String
gameVersion GameVersion @relation(fields: [versionId], references: [versionId], onDelete: Cascade, onUpdate: Cascade)
executions LaunchConfiguration[] @relation("executor")
emulations LaunchConfiguration[] @relation("emulator")
}
// A save slot for a game
@@ -20,7 +20,7 @@ export type AdminFetchGameType = Prisma.GameGetPayload<{
setups: true;
launches: {
include: {
executor: {
emulator: {
include: {
gameVersion: {
select: {
@@ -38,7 +38,7 @@ export type AdminFetchGameType = Prisma.GameGetPayload<{
};
};
};
executions: {
emulations: {
select: {
launchId: true;
};
@@ -77,7 +77,7 @@ export default defineEventHandler<
setups: true,
launches: {
include: {
executor: {
emulator: {
include: {
gameVersion: {
select: {
@@ -95,7 +95,7 @@ export default defineEventHandler<
},
},
},
executions: {
emulations: {
select: {
launchId: true,
},
@@ -19,7 +19,7 @@ export const ImportVersion = type({
name: "string",
launch: "string",
umuId: "string?",
executorId: "string?",
emulatorId: "string?",
suggestions: "string[]?",
}).array(),
@@ -17,7 +17,7 @@ export default defineClientEventHandler(async (h3) => {
include: {
launches: {
include: {
executor: {
emulator: {
include: {
gameVersion: {
select: {
@@ -46,11 +46,11 @@ export default defineClientEventHandler(async (h3) => {
...gameVersion,
launches: gameVersion.launches.map((launch) => ({
...launch,
executor: launch.executor
emulator: launch.emulator
? {
...launch.executor,
...launch.emulator,
gameVersion: undefined,
gameId: launch.executor.gameVersion.game.id,
gameId: launch.emulator.gameVersion.game.id,
}
: undefined,
})),
@@ -5,6 +5,7 @@ import type { GameVersionSize } from "~/server/internal/gamesize";
import gameSizeManager from "~/server/internal/gamesize";
type VersionDownloadOption = {
gameId: string;
versionId: string;
displayName?: string | undefined;
versionPath?: string | undefined;
@@ -43,7 +44,7 @@ export default defineClientEventHandler(async (h3) => {
launches: {
select: {
platform: true,
executor: {
emulator: {
select: {
gameVersion: {
select: {
@@ -78,18 +79,16 @@ export default defineClientEventHandler(async (h3) => {
if (!platformOptions.has(launch.platform))
platformOptions.set(launch.platform, []);
if ("executor" in launch && launch.executor) {
if ("emulator" in launch && launch.emulator) {
const old = platformOptions.get(launch.platform)!;
const gv = launch.emulator.gameVersion;
old.push({
gameId: launch.executor.gameVersion.game.id,
versionId: launch.executor.gameVersion.versionId,
name: launch.executor.gameVersion.game.mName,
iconObjectId: launch.executor.gameVersion.game.mIconObjectId,
shortDescription:
launch.executor.gameVersion.game.mShortDescription,
size: (await gameSizeManager.getVersionSize(
launch.executor.gameVersion.versionId,
))!,
gameId: gv.game.id,
versionId: gv.versionId,
name: gv.game.mName,
iconObjectId: gv.game.mIconObjectId,
shortDescription: gv.game.mShortDescription,
size: (await gameSizeManager.getVersionSize(gv.versionId))!,
});
}
}
@@ -101,6 +100,7 @@ export default defineClientEventHandler(async (h3) => {
.map(
([platform, requiredContent]) =>
({
gameId: v.gameId,
versionId: v.versionId,
displayName: v.displayName || undefined,
versionPath: v.versionPath || undefined,
+31 -2
View File
@@ -21,6 +21,12 @@ export default defineEventHandler(async (h3) => {
launches: true,
setups: true,
},
omit: {
dropletManifest: true,
},
orderBy: {
versionIndex: "desc",
},
},
publishers: {
select: {
@@ -57,7 +63,30 @@ export default defineEventHandler(async (h3) => {
},
});
const size = (await gameSizeManager.getGameBreakdown(gameId))!;
const sizes = await Promise.all(
game.versions!.map(
async (v) => (await gameSizeManager.getVersionSize(v.versionId))!,
),
);
return { game, rating, size };
const platforms = new Set(
game
.versions!.map((v) => [
...v.setups.map((v) => v.platform),
...v.launches.map((v) => v.platform),
])
.flat(),
);
const gameV: Omit<typeof game, "versions"> = game;
// @ts-expect-error value exists at runtime
delete gameV.versions;
return {
game: gameV,
rating,
sizes,
platforms: platforms.values().toArray(),
};
});
+29 -27
View File
@@ -19,6 +19,7 @@ import gameSizeManager from "~/server/internal/gamesize";
import type { ImportVersion } from "~/server/api/v1/admin/import/version/index.post";
import { GameType, type Platform } from "~/prisma/client/enums";
import { castManifest } from "./manifest/utils";
import { Shescape } from "shescape";
export function createGameImportTaskId(libraryId: string, libraryPath: string) {
return createHash("md5")
@@ -35,9 +36,9 @@ export function createVersionImportTaskKey(
.digest("hex");
}
export interface ExecutorVersionGuess {
type: "executor";
executorId: string;
export interface EmulatorVersionGuess {
type: "emulator";
emulatorId: string;
icon: string;
gameName: string;
versionName: string;
@@ -51,7 +52,7 @@ export interface PlatformVersionGuess {
export type VersionGuess = {
filename: string;
match: number;
} & (PlatformVersionGuess | ExecutorVersionGuess);
} & (PlatformVersionGuess | EmulatorVersionGuess);
export interface UnimportedVersionInformation {
type: "local" | "depot";
@@ -61,6 +62,7 @@ export interface UnimportedVersionInformation {
class LibraryManager {
private libraries: Map<string, LibraryProvider<unknown>> = new Map();
private shescape = new Shescape({});
addLibrary(library: LibraryProvider<unknown>) {
this.libraries.set(library.id(), library);
@@ -253,19 +255,19 @@ class LibraryManager {
],
};
const executorSuggestions = await prisma.launchConfiguration.findMany({
const emulators = await prisma.launchConfiguration.findMany({
where: {
executorSuggestions: {
emulatorSuggestions: {
isEmpty: false,
},
gameVersion: {
game: {
type: GameType.Executor,
type: GameType.Emulator,
},
},
},
select: {
executorSuggestions: true,
emulatorSuggestions: true,
gameVersion: {
select: {
game: {
@@ -318,28 +320,28 @@ class LibraryManager {
const fuzzyValue = fuzzy(basename, game.mName);
options.push({
type: "platform",
filename: filename.replaceAll(" ", "\\ "),
filename: this.shescape.escape(filename),
platform: platform as Platform,
match: fuzzyValue,
});
}
}
for (const executorSuggestion of executorSuggestions) {
for (const suggestion of executorSuggestion.executorSuggestions) {
for (const emulator of emulators) {
for (const suggestion of emulator.emulatorSuggestions) {
if (suggestion != ext) continue;
const fuzzyValue = fuzzy(basename, game.mName);
options.push({
type: "executor",
filename: filename.replaceAll(" ", "\\ "),
type: "emulator",
filename: this.shescape.escape(filename),
match: fuzzyValue,
executorId: executorSuggestion.launchId,
emulatorId: emulator.launchId,
icon: executorSuggestion.gameVersion.game.mIconObjectId,
gameName: executorSuggestion.gameVersion.game.mName,
versionName: (executorSuggestion.gameVersion.displayName ??
executorSuggestion.gameVersion.versionPath)!,
launchName: executorSuggestion.name,
platform: executorSuggestion.platform,
icon: emulator.gameVersion.game.mIconObjectId,
gameName: emulator.gameVersion.game.mName,
versionName: (emulator.gameVersion.displayName ??
emulator.gameVersion.versionPath)!,
launchName: emulator.name,
platform: emulator.platform,
});
}
}
@@ -382,10 +384,10 @@ class LibraryManager {
});
if (!game || !game.libraryId) return undefined;
if (game.type === GameType.Redist && !metadata.onlySetup)
if (game.type === GameType.Dependency && !metadata.onlySetup)
throw createError({
statusCode: 400,
message: "Redistributables can only be in setup-only mode.",
message: "Dependencies can only be in setup-only mode.",
});
const library = this.libraries.get(game.libraryId);
@@ -474,13 +476,13 @@ class LibraryManager {
name: v.name,
command: v.launch,
platform: v.platform,
...(v.executorId && game.type === "Game"
...(v.emulatorId && game.type === "Game"
? {
executorId: v.executorId,
emulatorId: v.emulatorId,
}
: undefined),
executorSuggestions:
game.type === "Executor" ? (v.suggestions ?? []) : [],
emulatorSuggestions:
game.type === "Emulator" ? (v.suggestions ?? []) : [],
})),
}
: { data: [] },
@@ -549,7 +551,7 @@ class LibraryManager {
where: {
launches: {
some: {
executor: {
emulator: {
gameVersion: {
gameId,
},