feat: import of custom platforms & file extensions

This commit is contained in:
DecDuck
2025-09-06 18:29:04 +10:00
parent 7266d0485b
commit fcfc30e5df
36 changed files with 13182 additions and 271 deletions

View File

@ -1,11 +1,14 @@
<!-- eslint-disable vue/no-v-html -->
<template>
<div class="flex flex-row gap-x-4">
<div class="relative size-24 bg-zinc-800 rounded-md overflow-hidden">
<label
for="icon-upload"
class="relative size-24 bg-zinc-800 rounded-md overflow-hidden has-[:focus]:ring-2 has-[:focus]:ring-blue-600"
>
<input
id="icon-upload"
type="file"
class="hidden"
class="sr-only"
accept="image/*"
@change="addFile"
/>
@ -14,14 +17,13 @@
:src="currentFileObjectUrl"
class="absolute inset-0 object-cover w-full h-full"
/>
<label
for="icon-upload"
<div
class="absolute inset-0 cursor-pointer flex flex-col gap-y-1 items-center justify-center text-zinc-300 bg-zinc-900/50"
>
<ArrowUpTrayIcon class="size-6" />
<span class="text-xs font-bold font-display uppercase">Upload</span>
</label>
</div>
</div>
</label>
<div class="grow flex flex-col gap-y-4">
<div>
<label for="name" class="block text-sm font-medium text-zinc-100"
@ -83,12 +85,16 @@
</SwitchGroup>
<div class="relative">
<div class="flex flex-row gap-x-4">
<div class="relative size-24 bg-zinc-800 rounded-md overflow-hidden">
<label
for="platform-icon-upload"
class="relative size-24 bg-zinc-800 rounded-md overflow-hidden has-[:focus]:ring-2 has-[:focus]:ring-blue-600"
>
<input
id="platform-icon-upload"
type="file"
class="hidden"
class="sr-only"
accept="image/svg+xml"
:disabled="!isPlatform"
@change="addSvg"
/>
<div
@ -96,16 +102,15 @@
class="absolute inset-0 object-cover w-full h-full text-blue-600"
v-html="platform.icon"
/>
<label
for="platform-icon-upload"
class="absolute inset-0 cursor-pointer flex flex-col gap-y-1 items-center justify-center text-zinc-300 bg-zinc-900/50"
<div
class="absolute inset-0 cursor-pointer flex flex-col gap-y-1 items-center justify-center text-zinc-300 bg-zinc-900/50 focus:text-zinc-100"
>
<ArrowUpTrayIcon class="size-6" />
<span class="text-xs font-bold font-display uppercase"
>Upload SVG</span
>
</label>
</div>
</div>
</label>
<div class="grow flex flex-col gap-y-4">
<div>
<label
@ -117,12 +122,74 @@
id="platform-name"
v-model="platform.name"
type="text"
:disabled="!isPlatform"
class="mt-1 block w-full rounded-md border-0 bg-zinc-950 py-1.5 text-white shadow-sm ring-1 ring-inset ring-zinc-700 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
/>
</div>
</div>
</div>
<div class="mt-2 w-full">
<label for="platform-name" class="block text-sm font-medium text-zinc-100"
>File Extensions {{ currentExtDotted }}
</label
>
<Combobox
as="div"
:model-value="currentExtDotted"
nullable
class="mt-1 w-full"
:disabled="!isPlatform"
@update:model-value="(v) => addExt(v)"
>
<div class="relative">
<ComboboxInput
class="w-full block flex-1 rounded-lg border-1 border-zinc-800 py-2 px-2 bg-zinc-950 text-zinc-100 placeholder:text-zinc-400 focus:ring-0 sm:text-sm sm:leading-6"
placeholder=".exe"
@change="currentExt = $event.target.value"
@blur="currentExt = ''"
/>
<ComboboxOptions
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-white/5 focus:outline-none sm:text-sm"
>
<ComboboxOption
v-if="currentExt"
v-slot="{ active }"
:value="currentExtDotted"
>
<li
:class="[
'relative cursor-default select-none py-2 pl-3 pr-9',
active
? 'bg-blue-600 text-white outline-none'
: 'text-zinc-100',
]"
>
<span class="block">
<span class="text-blue-300">filename</span
><span class="font-semibold">{{ currentExtDotted }}</span>
</span>
</li>
</ComboboxOption>
</ComboboxOptions>
</div>
</Combobox>
<div class="mt-2 flex gap-1 flex-wrap">
<div
v-for="ext in platform.fileExts"
:key="ext"
class="bg-blue-600/10 border-1 border-blue-700 rounded-full px-2 py-1 text-xs text-blue-400"
>
{{ ext }}
</div>
<span
v-if="platform.fileExts.length == 0"
class="uppercase font-display text-zinc-700 font-bold text-xs"
>No suggested file extensions.</span
>
</div>
</div>
<div v-if="!isPlatform" class="absolute inset-0 bg-zinc-950/20" />
</div>
<div>
@ -152,6 +219,10 @@
<script setup lang="ts">
import {
Combobox,
ComboboxInput,
ComboboxOption,
ComboboxOptions,
Switch,
SwitchDescription,
SwitchGroup,
@ -172,13 +243,27 @@ const emit = defineEmits<{
const name = ref("");
const description = ref("");
const isPlatform = ref(false);
const currentExt = ref("");
const currentExtDotted = computed(() => {
if(!currentExt.value) return "";
const cleaned = currentExt.value.replace(/\W/g, "").toLowerCase();
return `.${cleaned}`;
});
const platform = ref<{ name: string; icon: string; fileExts: string[] }>({
name: "",
icon: "",
fileExts: [],
});
const buttonDisabled = computed<boolean>(() => !(name.value && description.value && currentFileObjectUrl.value && (!isPlatform.value || (platform.value.name && platform.value.icon))))
const buttonDisabled = computed<boolean>(
() =>
!(
name.value &&
description.value &&
currentFileObjectUrl.value &&
(!isPlatform.value || (platform.value.name && platform.value.icon))
),
);
function addFile(event: Event) {
const file = (event.target as HTMLInputElement)?.files?.[0];
@ -218,6 +303,13 @@ async function addSvg(event: Event) {
}
}
function addExt(ext: string | null) {
if (!ext) return;
if (platform.value.fileExts.includes(ext)) return;
platform.value.fileExts.push(ext);
currentExt.value = "";
}
const props = defineProps<{
gameName: string;
loading: boolean;
@ -233,7 +325,12 @@ function importRedist() {
description: description.value,
icon: currentFile.value,
},
isPlatform.value ? platform.value : undefined,
isPlatform.value
? {
...platform.value,
fileExts: platform.value.fileExts.map((e) => e.slice(1)),
}
: undefined,
);
}
</script>

View File

@ -1,5 +1,5 @@
<template>
<Listbox v-model="typedModel" as="div">
<Listbox v-model="model" as="div">
<ListboxLabel class="block text-sm font-medium leading-6 text-zinc-100"
><slot
/></ListboxLabel>
@ -7,13 +7,13 @@
<ListboxButton
class="relative w-full cursor-default rounded-md bg-zinc-950 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-800 focus:outline-none focus:ring-2 focus:ring-blue-500 sm:text-sm sm:leading-6"
>
<span v-if="model" class="flex items-center">
<component
:is="PLATFORM_ICONS[model]"
alt=""
<span v-if="currentEntry" class="flex items-center">
<IconsPlatform
:platform="currentEntry.platformIcon.key"
:fallback="currentEntry.platformIcon.fallback"
class="h-5 w-5 flex-shrink-0 text-blue-600"
/>
<span class="ml-3 block truncate">{{ model }}</span>
<span class="ml-3 block truncate">{{ currentEntry.name }}</span>
</span>
<span v-else>{{ $t("library.admin.import.selectPlatform") }}</span>
<span
@ -32,11 +32,11 @@
class="absolute z-10 mt-1 max-h-56 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-zinc-950 ring-opacity-5 focus:outline-none sm:text-sm"
>
<ListboxOption
v-for="[name, value] in values"
:key="value"
v-for="entry in values"
:key="entry.param"
v-slot="{ active, selected }"
as="template"
:value="value"
:value="entry.param"
>
<li
:class="[
@ -45,15 +45,13 @@
]"
>
<div class="flex items-center">
<component
:is="PLATFORM_ICONS[value]"
alt=""
:class="[
active ? 'text-zinc-100' : 'text-blue-600',
'h-5 w-5 flex-shrink-0',
]"
<IconsPlatform
v-if="entry.platformIcon"
:platform="entry.platformIcon.key"
:fallback="entry.platformIcon.fallback"
class="size-5 text-blue-500"
/>
<span class="ml-3 block truncate">{{ name }}</span>
<span class="ml-3 block truncate">{{ entry.name }}</span>
</div>
<span
@ -82,19 +80,15 @@ import {
ListboxOptions,
} from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import { Platform } from "~/prisma/client/enums";
const model = defineModel<Platform | undefined>();
const model = defineModel<string | undefined>();
const typedModel = computed<Platform | null>({
get() {
return model.value || null;
},
set(v) {
if (v === null) return (model.value = undefined);
model.value = v;
},
});
const props = defineProps<{ platforms: PlatformRenderable[] }>();
const currentEntry = computed(() =>
model.value
? props.platforms.find((v) => v.param === model.value)
: undefined,
);
const values = Object.entries(Platform);
const values = props.platforms;
</script>

View File

@ -367,7 +367,6 @@ import {
import type { SerializeObject } from "nitropack";
import type { GameModel, GameTagModel } from "~/prisma/client/models";
import MultiItemSelector from "./MultiItemSelector.vue";
import { Platform } from "~/prisma/client/enums";
const { showGamePanelTextDecoration } = await $dropFetch(`/api/v1/settings`);
const mobileFiltersOpen = ref(false);
@ -416,18 +415,7 @@ const options: Array<StoreFilterOption> = [
name: "Platform",
param: "platform",
multiple: true,
options: [
...Object.values(Platform).map((e) => ({
name: e,
param: e,
platformIcon: { key: e },
})),
...userPlatforms.map((e) => ({
name: e.platformName,
param: e.id,
platformIcon: { key: e.id, fallback: e.iconSvg },
})),
],
options: renderPlatforms(userPlatforms),
},
...(props.extraOptions ?? []),
];

24
composables/platform.ts Normal file
View File

@ -0,0 +1,24 @@
import { Platform } from "~/prisma/client/enums";
export type PlatformRenderable = {
name: string;
param: string;
platformIcon: { key: string; fallback?: string };
};
export function renderPlatforms(
userPlatforms: { platformName: string; id: string; iconSvg: string }[],
): PlatformRenderable[] {
return [
...Object.values(Platform).map((e) => ({
name: e,
param: e,
platformIcon: { key: e },
})),
...userPlatforms.map((e) => ({
name: e.platformName,
param: e.id,
platformIcon: { key: e.id, fallback: e.iconSvg },
})),
];
}

View File

@ -4,7 +4,7 @@ import type {
NitroFetchRequest,
TypedInternalResponse,
} from "nitropack/types";
import type { FetchError } from "ofetch";
import { FetchError } from "ofetch";
interface DropFetch<
DefaultT = unknown,
@ -66,6 +66,9 @@ export const $dropFetch: DropFetch = async (rawRequest, opts) => {
(_, c) => c(),
);
}
if(e instanceof FetchError) {
e.message = e.data.message ?? e.message;
}
throw e;
}
}

View File

@ -69,7 +69,7 @@
"chars": {
"arrow": "→",
"arrowBack": "←",
"quoted": "\"\"",
"quoted": "\"{text}\"",
"srComma": ", {0}"
},
"common": {

View File

@ -71,7 +71,7 @@
"chars": {
"arrow": "→",
"arrowBack": "←",
"quoted": "\"\"",
"quoted": "\"{text}\"",
"srComma": ", {0}"
},
"common": {

View File

@ -93,7 +93,7 @@
"chars": {
"arrow": "→",
"arrowBack": "←",
"quoted": "\"\"",
"quoted": "\"{text}\"",
"srComma": ", {0}"
},
"common": {

View File

@ -70,7 +70,7 @@
"chars": {
"arrow": "→",
"arrowBack": "←",
"quoted": "\"\"",
"quoted": "\"{text}\"",
"srComma": ", {0}"
},
"common": {

View File

@ -241,6 +241,9 @@ export default defineNuxtConfig({
file: "zh_tw.json",
},
],
bundle: {
optimizeTranslationDirective: false,
},
},
security: {

View File

@ -66,7 +66,6 @@
"@nuxt/eslint": "^1.3.0",
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"@types/bcryptjs": "^3.0.0",
"@types/jsdom": "^21.1.7",
"@types/luxon": "^3.6.2",
"@types/node": "^22.13.16",

View File

@ -438,7 +438,11 @@
/>
</div>
<PlatformSelector v-model="versionSettings.platform" class="max-w-lg">
<PlatformSelector
v-model="versionSettings.platform"
class="max-w-lg"
:platforms="allPlatforms"
>
{{ $t("library.admin.import.version.platform") }}
</PlatformSelector>
<SwitchGroup as="div" class="flex items-center justify-between max-w-lg">
@ -636,6 +640,10 @@ const gameId = route.params.id.toString();
const versions = await $dropFetch(
`/api/v1/admin/import/version?id=${encodeURIComponent(gameId)}`,
);
const userPlatforms = await $dropFetch(
"/api/v1/admin/import/version/platforms",
);
const allPlatforms = renderPlatforms(userPlatforms);
const currentlySelectedVersion = ref(-1);
const versionSettings = ref<Partial<typeof ImportVersion.infer>>({
id: gameId,
@ -643,7 +651,8 @@ const versionSettings = ref<Partial<typeof ImportVersion.infer>>({
});
const versionGuesses =
ref<Array<SerializeObject<{ platform: Platform; filename: string }>>>();
ref<Array<SerializeObject<{ platform: string; filename: string }>>>();
const launchProcessQuery = ref("");
const setupProcessQuery = ref("");
@ -698,15 +707,12 @@ async function updateCurrentlySelectedVersion(value: number) {
if (currentlySelectedVersion.value == value) return;
currentlySelectedVersion.value = value;
const version = versions[currentlySelectedVersion.value];
const results = await $dropFetch(
const options = await $dropFetch(
`/api/v1/admin/import/version/preload?id=${encodeURIComponent(
gameId,
)}&version=${encodeURIComponent(version)}`,
);
versionGuesses.value = results.map((e) => ({
...e,
platform: e.platform as Platform,
}));
versionGuesses.value = options;
versionSettings.value.name = version;
}

View File

@ -198,8 +198,8 @@
>{{ metadata.title }}
<span class="ml-2 font-mono text-zinc-500 text-xs">{{
source
}}</span></RadioGroupLabel
>
}}</span>
</RadioGroupLabel>
<RadioGroupDescription
as="span"
class="text-zinc-400 text-xs"
@ -405,18 +405,21 @@ function performActionSource_wrapper() {
modalError.value = undefined;
modalLoading.value = true;
performActionSource()
.then(() => {
actionSourceOpen.value = false;
sourceConfig.value = {};
sourceName.value = "";
})
.catch((e) => {
if (e instanceof FetchError) {
modalError.value = e.message ?? e.message;
} else {
modalError.value = e as string;
}
})
.then(
() => {
actionSourceOpen.value = false;
sourceConfig.value = {};
sourceName.value = "";
},
(e) => {
if (e instanceof FetchError) {
console.log(e.data.message);
modalError.value = e.message;
} else {
modalError.value = e as string;
}
},
)
.finally(() => {
modalLoading.value = false;
});

View File

@ -84,6 +84,7 @@
? undefined
: platform.iconSvg
"
class="size-8 text-blue-600"
/>
<span
v-if="platforms.length == 0"
@ -257,7 +258,7 @@ const gameId = route.params.id.toString();
const user = useUser();
const { game, rating } = await $dropFetch(`/api/v1/games/${gameId}`);
const { game, rating, platforms } = await $dropFetch(`/api/v1/games/${gameId}`);
// Preview description (first 30 lines)
const showPreview = ref(true);
@ -283,11 +284,6 @@ const previewHTML = micromark(previewDescription);
const descriptionHTML = micromark(game.mDescription);
const showReadMore = previewHTML != descriptionHTML;
const platforms = game.versions
.map((e) => e.platform ?? e.userPlatform)
.flat()
.filter((e) => e !== null)
.filter((e, i, u) => u.indexOf(e) === i);
// const rating = Math.round(game.mReviewRating * 5);
const averageRating = Math.round((rating._avg.mReviewRating ?? 0) * 5);

12557
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +0,0 @@
-- DropIndex
DROP INDEX "public"."GameTag_name_idx";
-- AlterTable
ALTER TABLE "public"."UserPlatform" ADD COLUMN "fileExtensions" TEXT[] DEFAULT ARRAY[]::TEXT[];
-- CreateIndex
CREATE INDEX "GameTag_name_idx" ON "public"."GameTag" USING GIST ("name" gist_trgm_ops(siglen=32));

View File

@ -1,14 +0,0 @@
/*
Warnings:
- Made the column `libraryId` on table `Game` required. This step will fail if there are existing NULL values in that column.
*/
-- DropIndex
DROP INDEX "public"."GameTag_name_idx";
-- AlterTable
ALTER TABLE "public"."Game" ALTER COLUMN "libraryId" SET NOT NULL;
-- CreateIndex
CREATE INDEX "GameTag_name_idx" ON "public"."GameTag" USING GIST ("name" gist_trgm_ops(siglen=32));

View File

@ -1,32 +0,0 @@
/*
Warnings:
- Added the required column `mReleased` to the `Mod` table without a default value. This is not possible if the table is not empty.
*/
-- DropIndex
DROP INDEX "public"."GameTag_name_idx";
-- AlterTable
ALTER TABLE "public"."Mod" ADD COLUMN "mReleased" TIMESTAMP(3) NOT NULL;
-- CreateTable
CREATE TABLE "public"."GameDLCMetadata" (
"id" TEXT NOT NULL,
"mName" TEXT NOT NULL,
"mShortDescription" TEXT NOT NULL,
"mDescription" TEXT NOT NULL,
"mIconObjectId" TEXT NOT NULL,
"mBannerObjectId" TEXT NOT NULL,
"mCoverObjectId" TEXT NOT NULL,
"mImageCarouselObjectIds" TEXT[],
"mImageLibraryObjectIds" TEXT[],
CONSTRAINT "GameDLCMetadata_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "GameTag_name_idx" ON "public"."GameTag" USING GIST ("name" gist_trgm_ops(siglen=32));
-- AddForeignKey
ALTER TABLE "public"."GameDLCMetadata" ADD CONSTRAINT "GameDLCMetadata_id_fkey" FOREIGN KEY ("id") REFERENCES "public"."DLC"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -1,3 +1,4 @@
-- enable pg_trgm
CREATE EXTENSION pg_trgm;
@ -19,6 +20,9 @@ CREATE TYPE "public"."APITokenMode" AS ENUM ('User', 'System', 'Client');
-- CreateEnum
CREATE TYPE "public"."ClientCapabilities" AS ENUM ('peerAPI', 'userStatus', 'cloudSaves', 'trackPlaytime');
-- CreateEnum
CREATE TYPE "public"."HardwarePlatform" AS ENUM ('windows', 'linux', 'macos');
-- CreateEnum
CREATE TYPE "public"."MetadataSource" AS ENUM ('Manual', 'GiantBomb', 'PCGamingWiki', 'IGDB', 'Metacritic', 'OpenCritic');
@ -107,7 +111,7 @@ CREATE TABLE "public"."Client" (
"userId" TEXT NOT NULL,
"capabilities" "public"."ClientCapabilities"[],
"name" TEXT NOT NULL,
"platform" "public"."Platform" NOT NULL,
"platform" "public"."HardwarePlatform" NOT NULL,
"lastConnected" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Client_pkey" PRIMARY KEY ("id")
@ -134,13 +138,38 @@ CREATE TABLE "public"."CollectionEntry" (
-- CreateTable
CREATE TABLE "public"."UserPlatform" (
"id" TEXT NOT NULL,
"redistId" TEXT,
"platformName" TEXT NOT NULL,
"iconSvg" TEXT NOT NULL,
"fileExtensions" TEXT[] DEFAULT ARRAY[]::TEXT[],
"redistId" TEXT,
CONSTRAINT "UserPlatform_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "public"."PlatformLink" (
"id" TEXT NOT NULL,
"platform" "public"."HardwarePlatform",
"userPlatformId" TEXT,
CONSTRAINT "PlatformLink_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "public"."LaunchOption" (
"launchId" TEXT NOT NULL,
"versionId" TEXT NOT NULL,
"launchGId" TEXT,
"installGId" TEXT,
"uninstallGId" TEXT,
"name" TEXT NOT NULL,
"description" TEXT NOT NULL,
"command" TEXT NOT NULL,
"args" TEXT NOT NULL DEFAULT '',
CONSTRAINT "LaunchOption_pkey" PRIMARY KEY ("launchId")
);
-- CreateTable
CREATE TABLE "public"."Version" (
"versionId" TEXT NOT NULL,
@ -151,8 +180,6 @@ CREATE TABLE "public"."Version" (
"redistId" TEXT,
"dlcId" TEXT,
"modId" TEXT,
"platform" "public"."Platform",
"userPlatformRedistId" TEXT,
"dropletManifest" JSONB NOT NULL,
CONSTRAINT "Version_pkey" PRIMARY KEY ("versionId")
@ -161,32 +188,22 @@ CREATE TABLE "public"."Version" (
-- CreateTable
CREATE TABLE "public"."GameVersion" (
"versionId" TEXT NOT NULL,
"setupCommand" TEXT NOT NULL DEFAULT '',
"setupArgs" TEXT NOT NULL DEFAULT '',
"installId" TEXT,
"uninstallId" TEXT,
"onlySetup" BOOLEAN NOT NULL DEFAULT false,
"umuIdOverride" TEXT,
"versionIndex" INTEGER NOT NULL,
"delta" BOOLEAN NOT NULL DEFAULT false,
"hidden" BOOLEAN NOT NULL DEFAULT false,
"platformId" TEXT NOT NULL,
CONSTRAINT "GameVersion_pkey" PRIMARY KEY ("versionId")
);
-- CreateTable
CREATE TABLE "public"."LaunchOption" (
"launchId" TEXT NOT NULL,
"versionId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT NOT NULL,
"launchCommand" TEXT NOT NULL,
"launchArgs" TEXT NOT NULL DEFAULT '',
CONSTRAINT "LaunchOption_pkey" PRIMARY KEY ("launchId")
);
-- CreateTable
CREATE TABLE "public"."DLCVersion" (
"versionId" TEXT NOT NULL,
"platformId" TEXT NOT NULL,
CONSTRAINT "DLCVersion_pkey" PRIMARY KEY ("versionId")
);
@ -194,6 +211,7 @@ CREATE TABLE "public"."DLCVersion" (
-- CreateTable
CREATE TABLE "public"."RedistVersion" (
"versionId" TEXT NOT NULL,
"platformId" TEXT NOT NULL,
CONSTRAINT "RedistVersion_pkey" PRIMARY KEY ("versionId")
);
@ -202,6 +220,7 @@ CREATE TABLE "public"."RedistVersion" (
CREATE TABLE "public"."ModVersion" (
"versionId" TEXT NOT NULL,
"dependencies" TEXT[],
"platformId" TEXT NOT NULL,
CONSTRAINT "ModVersion_pkey" PRIMARY KEY ("versionId")
);
@ -267,7 +286,7 @@ CREATE TABLE "public"."Game" (
"mCoverObjectId" TEXT NOT NULL,
"mImageCarouselObjectIds" TEXT[],
"mImageLibraryObjectIds" TEXT[],
"libraryId" TEXT,
"libraryId" TEXT NOT NULL,
"libraryPath" TEXT NOT NULL,
CONSTRAINT "Game_pkey" PRIMARY KEY ("id")
@ -285,6 +304,21 @@ CREATE TABLE "public"."DLC" (
CONSTRAINT "DLC_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "public"."GameDLCMetadata" (
"id" TEXT NOT NULL,
"mName" TEXT NOT NULL,
"mShortDescription" TEXT NOT NULL,
"mDescription" TEXT NOT NULL,
"mIconObjectId" TEXT NOT NULL,
"mBannerObjectId" TEXT NOT NULL,
"mCoverObjectId" TEXT NOT NULL,
"mImageCarouselObjectIds" TEXT[],
"mImageLibraryObjectIds" TEXT[],
CONSTRAINT "GameDLCMetadata_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "public"."Redist" (
"id" TEXT NOT NULL,
@ -307,6 +341,7 @@ CREATE TABLE "public"."Mod" (
"mName" TEXT NOT NULL,
"mShortDescription" TEXT NOT NULL,
"mDescription" TEXT NOT NULL,
"mReleased" TIMESTAMP(3) NOT NULL,
"mIconObjectId" TEXT NOT NULL,
"mBannerObjectId" TEXT NOT NULL,
"mCoverObjectId" TEXT NOT NULL,
@ -486,9 +521,21 @@ CREATE INDEX "APIToken_token_idx" ON "public"."APIToken"("token");
-- CreateIndex
CREATE UNIQUE INDEX "UserPlatform_redistId_key" ON "public"."UserPlatform"("redistId");
-- CreateIndex
CREATE UNIQUE INDEX "LaunchOption_installGId_key" ON "public"."LaunchOption"("installGId");
-- CreateIndex
CREATE UNIQUE INDEX "LaunchOption_uninstallGId_key" ON "public"."LaunchOption"("uninstallGId");
-- CreateIndex
CREATE UNIQUE INDEX "Version_versionId_key" ON "public"."Version"("versionId");
-- CreateIndex
CREATE UNIQUE INDEX "GameVersion_installId_key" ON "public"."GameVersion"("installId");
-- CreateIndex
CREATE UNIQUE INDEX "GameVersion_uninstallId_key" ON "public"."GameVersion"("uninstallId");
-- CreateIndex
CREATE INDEX "Screenshot_gameId_userId_idx" ON "public"."Screenshot"("gameId", "userId");
@ -574,7 +621,16 @@ ALTER TABLE "public"."CollectionEntry" ADD CONSTRAINT "CollectionEntry_collectio
ALTER TABLE "public"."CollectionEntry" ADD CONSTRAINT "CollectionEntry_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "public"."Game"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."UserPlatform" ADD CONSTRAINT "UserPlatform_redistId_fkey" FOREIGN KEY ("redistId") REFERENCES "public"."Redist"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "public"."UserPlatform" ADD CONSTRAINT "UserPlatform_redistId_fkey" FOREIGN KEY ("redistId") REFERENCES "public"."Redist"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."PlatformLink" ADD CONSTRAINT "PlatformLink_userPlatformId_fkey" FOREIGN KEY ("userPlatformId") REFERENCES "public"."UserPlatform"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."LaunchOption" ADD CONSTRAINT "redistVersion_fkey" FOREIGN KEY ("versionId") REFERENCES "public"."RedistVersion"("versionId") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."LaunchOption" ADD CONSTRAINT "LaunchOption_launchGId_fkey" FOREIGN KEY ("launchGId") REFERENCES "public"."GameVersion"("versionId") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."Version" ADD CONSTRAINT "game_link" FOREIGN KEY ("gameId") REFERENCES "public"."Game"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@ -588,27 +644,36 @@ ALTER TABLE "public"."Version" ADD CONSTRAINT "dlc_link" FOREIGN KEY ("dlcId") R
-- AddForeignKey
ALTER TABLE "public"."Version" ADD CONSTRAINT "mod_link" FOREIGN KEY ("modId") REFERENCES "public"."Mod"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."Version" ADD CONSTRAINT "Version_userPlatformRedistId_fkey" FOREIGN KEY ("userPlatformRedistId") REFERENCES "public"."UserPlatform"("redistId") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."GameVersion" ADD CONSTRAINT "GameVersion_versionId_fkey" FOREIGN KEY ("versionId") REFERENCES "public"."Version"("versionId") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."LaunchOption" ADD CONSTRAINT "gameVersion_fkey" FOREIGN KEY ("versionId") REFERENCES "public"."GameVersion"("versionId") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "public"."GameVersion" ADD CONSTRAINT "GameVersion_installId_fkey" FOREIGN KEY ("installId") REFERENCES "public"."LaunchOption"("launchId") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."LaunchOption" ADD CONSTRAINT "redistVersion_fkey" FOREIGN KEY ("versionId") REFERENCES "public"."RedistVersion"("versionId") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "public"."GameVersion" ADD CONSTRAINT "GameVersion_uninstallId_fkey" FOREIGN KEY ("uninstallId") REFERENCES "public"."LaunchOption"("launchId") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."GameVersion" ADD CONSTRAINT "GameVersion_platformId_fkey" FOREIGN KEY ("platformId") REFERENCES "public"."PlatformLink"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."DLCVersion" ADD CONSTRAINT "DLCVersion_versionId_fkey" FOREIGN KEY ("versionId") REFERENCES "public"."Version"("versionId") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."DLCVersion" ADD CONSTRAINT "DLCVersion_platformId_fkey" FOREIGN KEY ("platformId") REFERENCES "public"."PlatformLink"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."RedistVersion" ADD CONSTRAINT "RedistVersion_versionId_fkey" FOREIGN KEY ("versionId") REFERENCES "public"."Version"("versionId") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."RedistVersion" ADD CONSTRAINT "RedistVersion_platformId_fkey" FOREIGN KEY ("platformId") REFERENCES "public"."PlatformLink"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."ModVersion" ADD CONSTRAINT "ModVersion_versionId_fkey" FOREIGN KEY ("versionId") REFERENCES "public"."Version"("versionId") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."ModVersion" ADD CONSTRAINT "ModVersion_platformId_fkey" FOREIGN KEY ("platformId") REFERENCES "public"."PlatformLink"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."SaveSlot" ADD CONSTRAINT "SaveSlot_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "public"."Game"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@ -636,6 +701,9 @@ ALTER TABLE "public"."Game" ADD CONSTRAINT "Game_libraryId_fkey" FOREIGN KEY ("l
-- AddForeignKey
ALTER TABLE "public"."DLC" ADD CONSTRAINT "DLC_libraryId_fkey" FOREIGN KEY ("libraryId") REFERENCES "public"."Library"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."GameDLCMetadata" ADD CONSTRAINT "GameDLCMetadata_id_fkey" FOREIGN KEY ("id") REFERENCES "public"."DLC"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."Redist" ADD CONSTRAINT "Redist_libraryId_fkey" FOREIGN KEY ("libraryId") REFERENCES "public"."Library"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,15 @@
/*
Warnings:
- You are about to drop the column `platform` on the `PlatformLink` table. All the data in the column will be lost.
*/
-- DropIndex
DROP INDEX "public"."GameTag_name_idx";
-- AlterTable
ALTER TABLE "public"."PlatformLink" DROP COLUMN "platform",
ADD COLUMN "hardwarePlatform" "public"."HardwarePlatform";
-- CreateIndex
CREATE INDEX "GameTag_name_idx" ON "public"."GameTag" USING GIST ("name" gist_trgm_ops(siglen=32));

View File

@ -0,0 +1,21 @@
/*
Warnings:
- You are about to drop the column `versionId` on the `LaunchOption` table. All the data in the column will be lost.
*/
-- DropForeignKey
ALTER TABLE "public"."LaunchOption" DROP CONSTRAINT "redistVersion_fkey";
-- DropIndex
DROP INDEX "public"."GameTag_name_idx";
-- AlterTable
ALTER TABLE "public"."LaunchOption" DROP COLUMN "versionId",
ADD COLUMN "redistVersionId" TEXT;
-- CreateIndex
CREATE INDEX "GameTag_name_idx" ON "public"."GameTag" USING GIST ("name" gist_trgm_ops(siglen=32));
-- AddForeignKey
ALTER TABLE "public"."LaunchOption" ADD CONSTRAINT "redistVersion_fkey" FOREIGN KEY ("redistVersionId") REFERENCES "public"."RedistVersion"("versionId") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,20 @@
/*
Warnings:
- You are about to drop the column `userPlatformId` on the `PlatformLink` table. All the data in the column will be lost.
*/
-- DropForeignKey
ALTER TABLE "public"."PlatformLink" DROP CONSTRAINT "PlatformLink_userPlatformId_fkey";
-- DropIndex
DROP INDEX "public"."GameTag_name_idx";
-- AlterTable
ALTER TABLE "public"."PlatformLink" DROP COLUMN "userPlatformId";
-- CreateIndex
CREATE INDEX "GameTag_name_idx" ON "public"."GameTag" USING GIST ("name" gist_trgm_ops(siglen=32));
-- AddForeignKey
ALTER TABLE "public"."PlatformLink" ADD CONSTRAINT "PlatformLink_id_fkey" FOREIGN KEY ("id") REFERENCES "public"."UserPlatform"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -1,3 +1,6 @@
-- DropForeignKey
ALTER TABLE "public"."PlatformLink" DROP CONSTRAINT "PlatformLink_id_fkey";
-- DropIndex
DROP INDEX "public"."GameTag_name_idx";

View File

@ -0,0 +1,20 @@
/*
Warnings:
- Made the column `redistId` on table `UserPlatform` required. This step will fail if there are existing NULL values in that column.
*/
-- DropForeignKey
ALTER TABLE "public"."UserPlatform" DROP CONSTRAINT "UserPlatform_redistId_fkey";
-- DropIndex
DROP INDEX "public"."GameTag_name_idx";
-- AlterTable
ALTER TABLE "public"."UserPlatform" ALTER COLUMN "redistId" SET NOT NULL;
-- CreateIndex
CREATE INDEX "GameTag_name_idx" ON "public"."GameTag" USING GIST ("name" gist_trgm_ops(siglen=32));
-- AddForeignKey
ALTER TABLE "public"."UserPlatform" ADD CONSTRAINT "UserPlatform_redistId_fkey" FOREIGN KEY ("redistId") REFERENCES "public"."Redist"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -14,7 +14,7 @@ model Client {
capabilities ClientCapabilities[]
name String
platform Platform
platform HardwarePlatform
lastConnected DateTime
lastAccessedSaves SaveSlot[]

View File

@ -1,16 +1,62 @@
enum HardwarePlatform {
Windows @map("windows")
Linux @map("linux")
macOS @map("macos")
// Switch @map("switch")
// etc
// @@map("Platform")
}
model UserPlatform {
id String @id @default(uuid())
redistId String? @unique
redist Redist? @relation(fields: [redistId], references: [id], onDelete: Cascade, onUpdate: Cascade)
platformName String
iconSvg String
fileExtensions String[] @default([])
versions Version[]
redistId String @unique
redist Redist @relation(fields: [redistId], references: [id], onDelete: Cascade, onUpdate: Cascade)
//platform PlatformLink[]
}
model PlatformLink {
id String @id // This is either the ID of the user platform, or a repeat of the HardwarePlatform enum. It's cursed.
hardwarePlatform HardwarePlatform?
// Waiting on weak reference
// userPlatform UserPlatform? @relation(fields: [id], references: [id])
gameVersions GameVersion[]
dlcVersions DLCVersion[]
redistVerisons RedistVersion[]
modVersions ModVersion[]
}
/**
*/
model LaunchOption {
launchId String @id @default(uuid())
redistVersionId String?
redistVersion RedistVersion? @relation(fields: [redistVersionId], references: [versionId], onDelete: Cascade, onUpdate: Cascade, map: "redistVersion_fkey")
launchGId String?
launchGVersion GameVersion? @relation(name: "launches", fields: [launchGId], references: [versionId])
installGId String? @unique
installGVersion GameVersion? @relation(name: "install")
uninstallGId String? @unique
uninstallGVersion GameVersion? @relation(name: "uninstall")
name String
description String
command String
args String @default("")
}
// Platform agnostic object
model Version {
versionId String @id @unique @default(uuid())
@ -19,72 +65,62 @@ model Version {
versionName String
created DateTime @default(now())
gameId String?
game Game? @relation(fields: [gameId], references: [id], map: "game_link", onDelete: Cascade, onUpdate: Cascade)
gameVersion GameVersion?
gameId String?
game Game? @relation(fields: [gameId], references: [id], map: "game_link", onDelete: Cascade, onUpdate: Cascade)
gameVersions GameVersion[]
redistId String?
redist Redist? @relation(fields: [redistId], references: [id], map: "redist_link", onDelete: Cascade, onUpdate: Cascade)
redistVersion RedistVersion?
redistId String?
redist Redist? @relation(fields: [redistId], references: [id], map: "redist_link", onDelete: Cascade, onUpdate: Cascade)
redistVersions RedistVersion[]
dlcId String?
dlc DLC? @relation(fields: [dlcId], references: [id], map: "dlc_link", onDelete: Cascade, onUpdate: Cascade)
dlcVersion DLCVersion?
dlcId String?
dlc DLC? @relation(fields: [dlcId], references: [id], map: "dlc_link", onDelete: Cascade, onUpdate: Cascade)
dlcVersions DLCVersion[]
modId String?
mod Mod? @relation(fields: [modId], references: [id], map: "mod_link", onDelete: Cascade, onUpdate: Cascade)
modVersion ModVersion?
platform Platform?
userPlatformRedistId String?
userPlatform UserPlatform? @relation(fields: [userPlatformRedistId], references: [redistId])
modId String?
mod Mod? @relation(fields: [modId], references: [id], map: "mod_link", onDelete: Cascade, onUpdate: Cascade)
modVersions ModVersion[]
dropletManifest Json // Results from droplet
}
// Platform specific object
model GameVersion {
versionId String @id
version Version @relation(fields: [versionId], references: [versionId], onDelete: Cascade, onUpdate: Cascade)
redistDeps RedistVersion[]
launches LaunchOption[]
launches LaunchOption[] @relation(name: "launches")
setupCommand String @default("")
setupArgs String @default("")
onlySetup Boolean @default(false)
installId String? @unique
install LaunchOption? @relation(name: "install", fields: [installId], references: [launchId])
uninstallId String? @unique
uninstall LaunchOption? @relation(name: "uninstall", fields: [uninstallId], references: [launchId])
onlySetup Boolean @default(false)
umuIdOverride String?
versionIndex Int
delta Boolean @default(false)
hidden Boolean @default(false)
platformId String
platform PlatformLink @relation(fields: [platformId], references: [id])
}
model LaunchOption {
launchId String @id @default(uuid())
versionId String
gameVersion GameVersion? @relation(fields: [versionId], references: [versionId], onDelete: Cascade, onUpdate: Cascade, map: "gameVersion_fkey")
redistVersion RedistVersion? @relation(fields: [versionId], references: [versionId], onDelete: Cascade, onUpdate: Cascade, map: "redistVersion_fkey")
name String
description String
launchCommand String
launchArgs String @default("")
}
// Platform specific object
model DLCVersion {
versionId String @id
version Version @relation(fields: [versionId], references: [versionId], onDelete: Cascade, onUpdate: Cascade)
redistDeps RedistVersion[]
platformId String
platform PlatformLink @relation(fields: [platformId], references: [id])
}
// Platform specific object
model RedistVersion {
versionId String @id
version Version @relation(fields: [versionId], references: [versionId], onDelete: Cascade, onUpdate: Cascade)
@ -93,13 +129,20 @@ model RedistVersion {
gameDependees GameVersion[]
dlcDependees DLCVersion[]
platformId String
platform PlatformLink @relation(fields: [platformId], references: [id])
}
// Platform specific object
model ModVersion {
versionId String @id
version Version @relation(fields: [versionId], references: [versionId], onDelete: Cascade, onUpdate: Cascade)
dependencies String[]
platformId String
platform PlatformLink @relation(fields: [platformId], references: [id])
}
// A save slot for a game

View File

@ -14,16 +14,6 @@ export default defineEventHandler(async (h3) => {
},
include: {
versions: {
where: {
gameVersion: {
isNot: null,
},
},
orderBy: {
gameVersion: {
versionIndex: "asc",
},
},
omit: {
dropletManifest: true,
},

View File

@ -16,7 +16,7 @@ export const ImportRedist = type({
"platform?": type({
name: "string",
icon: "string",
fileExts: type("string").pipe.try((s) => JSON.parse(s), type("string[]")),
fileExts: type("string").pipe.try((s) => JSON.parse(s), type("string.alphanumeric").array()),
}),
});
@ -78,7 +78,7 @@ export default defineEventHandler(async (h3) => {
create: {
platformName: options.platform.name,
iconSvg: svgContent,
fileExtensions: options.platform.fileExts,
fileExtensions: options.platform.fileExts.map((v) => `.${v}`),
},
}
: undefined),

View File

@ -1,23 +1,23 @@
import { type } from "arktype";
import { Platform } from "~/prisma/client/enums";
import { readDropValidatedBody, throwingArktype } from "~/server/arktype";
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
import libraryManager from "~/server/internal/library";
import { convertIDToLink } from "~/server/internal/platform/link";
export const LaunchCommands = type({
name: "string > 0",
description: "string = ''",
launchCommand: "string > 0",
launchArgs: "string = ''",
}).array();
name: "string > 0",
description: "string = ''",
launchCommand: "string > 0",
launchArgs: "string = ''",
}).array();
export const ImportVersion = type({
id: "string",
version: "string",
name: "string?",
platform: type.valueOf(Platform),
platform: "string",
setup: "string = ''",
setupArgs: "string = ''",
onlySetup: "boolean = false",
@ -33,11 +33,18 @@ export default defineEventHandler(async (h3) => {
const body = await readDropValidatedBody(h3, ImportVersion);
const platform = await convertIDToLink(body.platform);
if (!platform)
throw createError({ statusCode: 400, message: "Invalid platform." });
if (body.delta) {
const validOverlayVersions = await prisma.gameVersion.count({
where: {
version: { gameId: body.id, platform: body.platform },
version: {
gameId: body.id,
},
delta: false,
platform,
},
});
if (validOverlayVersions == 0)

View File

@ -0,0 +1,11 @@
import aclManager from "~/server/internal/acls"
import prisma from "~/server/internal/db/database";
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["import:version:read"]);
if(!allowed) throw createError({statusCode: 403});
const userPlatforms = await prisma.userPlatform.findMany({});
return userPlatforms;
})

View File

@ -1,5 +1,6 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
import { convertIDsToPlatforms } from "~/server/internal/platform/link";
export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, ["store:read"]);
@ -17,7 +18,11 @@ export default defineEventHandler(async (h3) => {
include: {
versions: {
include: {
userPlatform: true,
gameVersions: {
include: {
platform: true,
},
},
},
},
publishers: {
@ -40,8 +45,7 @@ export default defineEventHandler(async (h3) => {
},
});
if (!game)
throw createError({ statusCode: 404, message: "Game not found" });
if (!game) throw createError({ statusCode: 404, message: "Game not found" });
const rating = await prisma.gameRating.aggregate({
where: {
@ -55,5 +59,18 @@ export default defineEventHandler(async (h3) => {
},
});
return { game, rating };
const platformIDs = game.versions
.map((e) => e.gameVersions)
.flat()
.map((e) => e.platform)
.flat()
.map((e) => e.id)
.filter((e) => e !== null)
.filter((v, index, arr) => arr.findIndex((k) => k == v) == index);
const platforms = await convertIDsToPlatforms(platformIDs);
const noVersionsGame = { ...game, versions: undefined };
return { game: noVersionsGame, rating, platforms };
});

View File

@ -2,7 +2,6 @@ import { ArkErrors, type } from "arktype";
import type { Prisma } from "~/prisma/client/client";
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
import { parsePlatform } from "~/server/internal/utils/parseplatform";
const StoreRead = type({
skip: type("string")
@ -45,18 +44,19 @@ export default defineEventHandler(async (h3) => {
}
: undefined;
const platformFilter = options.platform
? {
? ({
versions: {
some: {
platform: {
in: options.platform
.split(",")
.map(parsePlatform)
.filter((e) => e !== undefined),
},
gameVersions: {
some: {
platform: {
id: options.platform
}
}
}
},
},
}
} satisfies Prisma.GameWhereInput)
: undefined;
/**

View File

@ -20,7 +20,7 @@ class DownloadContextManager {
where: {
gameId: game,
versionPath,
gameVersion: {
game: {
isNot: null,
},
},

View File

@ -55,47 +55,64 @@ class ManifestGenerator {
async generateManifest(versionId: string) {
const versions = [];
const baseVersion = await prisma.version.findUnique({
const baseVersion = await prisma.gameVersion.findUnique({
where: {
versionId,
version: {
gameId: {
not: null,
},
},
},
include: {
gameVersion: true,
platform: true,
version: {
select: {
gameId: true,
dropletManifest: true,
},
},
},
});
if (!baseVersion) return undefined;
versions.push(baseVersion);
// Collect other versions if this is a delta
if (baseVersion.gameVersion?.delta) {
if (baseVersion.delta) {
// Start at the same index minus one, and keep grabbing them
// until we run out or we hit something that isn't a delta
// eslint-disable-next-line no-constant-condition
for (let i = baseVersion.gameVersion.versionIndex - 1; true; i--) {
const currentVersion = await prisma.version.findFirst({
for (let i = baseVersion.versionIndex - 1; true; i--) {
const currentVersion = await prisma.gameVersion.findFirst({
where: {
gameId: baseVersion.gameId,
platform: baseVersion.platform,
gameVersion: {
versionIndex: i,
version: {
gameId: baseVersion.version.gameId!,
},
platform: {
id: baseVersion.platform.id,
},
versionIndex: i,
},
include: {
gameVersion: true,
version: {
select: {
dropletManifest: true,
},
},
},
});
if (!currentVersion) return undefined;
versions.push(currentVersion);
if (!currentVersion.gameVersion?.delta) break;
if (!currentVersion?.delta) break;
}
}
versions.reverse();
const metadata: DropManifestMetadata[] = versions.map((version) => {
const metadata: DropManifestMetadata[] = versions.map((gameVersion) => {
return {
manifest: JSON.parse(
version.dropletManifest?.toString() ?? "{}",
gameVersion.version.dropletManifest?.toString() ?? "{}",
) as DropManifest,
versionId: version.versionId,
versionId: gameVersion.versionId,
};
});

View File

@ -9,13 +9,12 @@ import path from "path";
import prisma from "../db/database";
import { fuzzy } from "fast-fuzzy";
import taskHandler from "../tasks";
import { parsePlatform } from "../utils/parseplatform";
import notificationSystem from "../notifications";
import { GameNotFoundError, type LibraryProvider } from "./provider";
import { logger } from "../logging";
import { createHash } from "node:crypto";
import type { ImportVersion } from "~/server/api/v1/admin/import/version/index.post";
import type { LaunchOptionCreateManyGameVersionInput } from "~/prisma/client/models";
import type { LaunchOptionCreateManyInput } from "~/prisma/client/models";
export function createGameImportTaskId(libraryId: string, libraryPath: string) {
return createHash("md5")
@ -221,6 +220,8 @@ class LibraryManager {
const library = this.libraries.get(game.libraryId);
if (!library) return undefined;
const userPlatforms = await prisma.userPlatform.findMany({});
const fileExts: { [key: string]: string[] } = {
Linux: [
// Ext for Unity games
@ -239,6 +240,12 @@ class LibraryManager {
],
};
for (const platform of userPlatforms) {
fileExts[platform.id] = platform.fileExtensions;
}
console.log(fileExts);
const options: Array<{
filename: string;
platform: string;
@ -299,9 +306,6 @@ class LibraryManager {
) {
const taskId = createVersionImportTaskId(gameId, versionPath);
const platform = parsePlatform(metadata.platform);
if (!platform) return undefined;
const game = await prisma.game.findUnique({
where: { id: gameId },
select: { mName: true, libraryId: true, libraryPath: true },
@ -345,17 +349,14 @@ class LibraryManager {
versionPath: versionPath,
versionName: metadata.name ?? versionPath,
dropletManifest: manifest,
platform: platform,
gameVersion: {
gameVersions: {
create: {
versionIndex: currentIndex,
delta: metadata.delta,
umuIdOverride: metadata.umuId,
onlySetup: metadata.onlySetup,
setupCommand: metadata.setup,
setupArgs: metadata.setupArgs,
launches: {
createMany: {
@ -364,12 +365,27 @@ class LibraryManager {
({
name: v.name,
description: v.description,
launchCommand: v.launchCommand,
launchArgs: v.launchArgs,
}) satisfies LaunchOptionCreateManyGameVersionInput,
command: v.launchCommand,
args: v.launchArgs,
}) satisfies LaunchOptionCreateManyInput,
),
},
},
install: {
create: {
name: "",
description: "",
command: metadata.setup,
args: metadata.setupArgs,
},
},
platform: {
connect: {
id: metadata.platform,
},
},
},
},
},

View File

@ -0,0 +1,47 @@
import { Platform, type HardwarePlatform } from "~/prisma/client/enums";
import prisma from "../db/database";
import type { PlatformLink } from "~/prisma/client/client";
export async function convertIDsToPlatforms(platformIDs: string[]) {
const userPlatforms = await prisma.userPlatform.findMany({
where: {
id: {
in: platformIDs,
},
},
});
const platforms = platformIDs.map(
(e) => userPlatforms.find((v) => v.id === e) ?? (e as HardwarePlatform),
);
return platforms;
}
export async function convertIDToLink(
id: string,
): Promise<PlatformLink | undefined> {
const link = await prisma.platformLink.findUnique({
where: { id },
});
if (link) return link;
if (Platform[id as Platform]) {
return await prisma.platformLink.create({
data: {
id,
},
});
}
const userPlatform = await prisma.userPlatform.findUnique({
where: { id },
});
if (!userPlatform) return undefined;
return await prisma.platformLink.create({
data: {
id,
},
});
}