- Game:
+
+
+ Search for an executor
+
updateGame(value)"
/>
@@ -45,28 +45,27 @@
>
No versions imported.
- Version:
+
+
+ Select a version
+
@@ -74,11 +73,10 @@
-
- Launch:
+
+
+ Select a launch command
+
(launchId = v)"
>
@@ -169,7 +167,7 @@ const versions = ref<
platform: Platform;
}[];
versionId: string;
- versionPath: string;
+ versionPath: string | null;
};
}
| undefined
@@ -180,7 +178,9 @@ const emit = defineEmits<{
}>();
async function search(query: string) {
- return await $dropFetch("/api/v1/admin/search/game", { query: { q: query } });
+ return await $dropFetch("/api/v1/admin/search/game", {
+ query: { q: query, type: "Executor" },
+ });
}
function updateGame(value: GameMetadataSearchResult | undefined) {
diff --git a/components/Selector/FileExtension.vue b/components/Selector/FileExtension.vue
new file mode 100644
index 00000000..5a18ef91
--- /dev/null
+++ b/components/Selector/FileExtension.vue
@@ -0,0 +1,118 @@
+
+
+
+
+ {{ extension }}
+
+
+
No extensions selected.
+
+
+
+
+
+
+
+
+
+
+
+ Add "{{ normalize(query) }}"
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/Selector/Game.vue b/components/Selector/Game.vue
index 85058899..887c7e14 100644
--- a/components/Selector/Game.vue
+++ b/components/Selector/Game.vue
@@ -51,9 +51,9 @@
- No results.
+ No results
-
+
{
if (timeout) clearTimeout(timeout);
resultsLoading.value = true;
timeout = setTimeout(async () => {
- const newResults = await props.search(v);
- results.value = newResults.map((v) => ({ ...v, icon: useObject(v.icon) }));
+ results.value = await props.search(v);
resultsLoading.value = false;
timeout = undefined;
}, 600);
diff --git a/error.vue b/error.vue
index ce00b95f..d9b6cd23 100644
--- a/error.vue
+++ b/error.vue
@@ -21,11 +21,6 @@ async function signIn() {
redirect: `/auth/signin?redirect=${encodeURIComponent(route.fullPath)}`,
});
}
-switch (statusCode) {
- case 401:
- case 403:
- await signIn();
-}
useHead({
title: t("errors.pageTitle", [statusCode ?? message]),
diff --git a/nuxt.config.ts b/nuxt.config.ts
index 5308fc16..f5c2b7ef 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -65,6 +65,7 @@ export default defineNuxtConfig({
experimental: {
buildCache: true,
viewTransition: false,
+ appManifest: false,
componentIslands: true,
},
diff --git a/package.json b/package.json
index 7c5165b3..eef05ecf 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
"@lobomfz/prismark": "0.0.3",
"@nuxt/fonts": "^0.11.0",
"@nuxt/image": "^1.10.0",
+ "@nuxt/kit": "3.20.1",
"@nuxtjs/i18n": "^9.5.5",
"@prisma/client": "^6.11.1",
"@simplewebauthn/browser": "^13.2.2",
diff --git a/pages/admin/library/[id]/import.vue b/pages/admin/library/[id]/import.vue
index 84b89e4e..2c5befc9 100644
--- a/pages/admin/library/[id]/import.vue
+++ b/pages/admin/library/[id]/import.vue
@@ -1,5 +1,5 @@
-
+
{{
- versions[currentlySelectedVersion]
+ versions[currentlySelectedVersion].name
}}
{{
$t("library.admin.import.selectDir")
@@ -38,7 +38,7 @@
>
{{ version }}{{ version.name }}
-
-
- {{ $t("library.admin.import.version.setupMode") }}
- {{
- $t("library.admin.import.version.setupModeDesc")
- }}
-
-
+
-
+ {{ $t("library.admin.import.version.setupMode") }}
+ {{
+ $t("library.admin.import.version.setupModeDesc")
+ }}
+
+
-
-
+ >
+
+
+
+
+
@@ -172,19 +178,48 @@
:key="launchIdx"
class="py-2 inline-flex items-start gap-x-1 w-full"
>
-
-
+
+
+ {{
+ launch.name
+ }}
+ No name provided.
+
+
+
+
+
+
+
+
+
+
+
({
- id: gameId,
- version: "",
- delta: false,
- onlySetup: false,
- launches: [],
- setups: [],
-});
+const versionSettings = ref>(
+ {
+ delta: false,
+ onlySetup: type === GameType.Redist,
+ launches: [],
+ setups: [],
+ requiredContent: [],
+ },
+);
-const versionGuesses = ref>();
+const versionGuesses = ref>();
const importLoading = ref(false);
const importError = ref();
@@ -336,14 +378,14 @@ async function updateCurrentlySelectedVersion(value: number) {
currentlySelectedVersion.value = value;
const version = versions[currentlySelectedVersion.value];
try {
- const results = await $dropFetch(
- `/api/v1/admin/import/version/preload?id=${encodeURIComponent(
- gameId,
- )}&version=${encodeURIComponent(version)}`,
- {
- failTitle: "Failed to fetch version information",
+ const results = await $dropFetch(`/api/v1/admin/import/version/preload`, {
+ failTitle: "Failed to fetch version information",
+ query: {
+ id: gameId,
+ type: version.type,
+ version: version.identifier,
},
- );
+ });
versionGuesses.value = results as typeof versionGuesses.value;
} catch {
currentlySelectedVersion.value = -1;
diff --git a/pages/admin/library/import.vue b/pages/admin/library/import.vue
index d6eede49..ee528ee2 100644
--- a/pages/admin/library/import.vue
+++ b/pages/admin/library/import.vue
@@ -1,5 +1,5 @@
-
+
+
+
();
+const importMode = ref("Game");
async function importGame(useMetadata: boolean) {
if (!metadataResults.value && useMetadata) return;
@@ -389,6 +444,7 @@ async function importGame(useMetadata: boolean) {
path: option.game,
library: option.library.id,
metadata,
+ type: importMode.value,
},
});
diff --git a/pages/admin/library/index.vue b/pages/admin/library/index.vue
index cae78173..d7fd2e15 100644
--- a/pages/admin/library/index.vue
+++ b/pages/admin/library/index.vue
@@ -88,7 +88,7 @@
/>
{{ game.mName }}
- {{ $t("library.admin.shortDesc") }}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e5a40995..a62e5355 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -29,6 +29,9 @@ importers:
'@nuxt/image':
specifier: ^1.10.0
version: 1.10.0(@netlify/blobs@9.1.2)(db0@0.3.4)(ioredis@5.8.2)(magicast@0.5.1)
+ '@nuxt/kit':
+ specifier: 3.20.1
+ version: 3.20.1(magicast@0.5.1)
'@nuxtjs/i18n':
specifier: ^9.5.5
version: 9.5.6(@vue/compiler-dom@3.5.27)(eslint@9.31.0(jiti@2.6.1))(magicast@0.5.1)(rollup@4.53.3)(vue@3.5.27(typescript@5.8.3))
@@ -1370,18 +1373,10 @@ packages:
resolution: {integrity: sha512-/B58GeEmme7bkmQUrXzEw8P9sJb9BkMaYZqLDtq8ZdDLEddE3P4nVya8RQPB+p4b7EdqWajpPqdy1A2ZPLev/A==}
engines: {node: '>=18.20.6'}
- '@nuxt/kit@3.18.0':
- resolution: {integrity: sha512-svS1CBEx7gMgEIaNYrQt26J/t5bDSUdIf7GQWr5M6yszOzLw+IVzyfH7TBmuxZEbjovhLaJEG379mgKp82H/lA==}
- engines: {node: '>=18.12.0'}
-
'@nuxt/kit@3.20.1':
resolution: {integrity: sha512-TIslaylfI5kd3AxX5qts0qyrIQ9Uq3HAA1bgIIJ+c+zpDfK338YS+YrCWxBBzDMECRCbAS58mqAd2MtJfG1ENA==}
engines: {node: '>=18.12.0'}
- '@nuxt/kit@4.0.2':
- resolution: {integrity: sha512-OtLkVYHpfrm1FzGSGxl0H3QXLgO41yxOgni5S6zzLG4gblG71Fy82B2QTdqJLzTLKWObiILKDhrysBtmDkp3LA==}
- engines: {node: '>=18.12.0'}
-
'@nuxt/kit@4.2.1':
resolution: {integrity: sha512-lLt8KLHyl7IClc3RqRpRikz15eCfTRlAWL9leVzPyg5N87FfKE/7EWgWvpiL/z4Tf3dQCIqQb88TmHE0JTIDvA==}
engines: {node: '>=18.12.0'}
@@ -7537,10 +7532,10 @@ snapshots:
find-up: 7.0.0
get-port-please: 3.2.0
h3: 1.15.5
- mlly: 1.7.4
+ mlly: 1.8.0
mrmime: 2.0.1
open: 10.2.0
- tinyglobby: 0.2.14
+ tinyglobby: 0.2.15
ws: 8.18.3
transitivePeerDependencies:
- bufferutil
@@ -7613,7 +7608,7 @@ snapshots:
escodegen: 2.1.0
estree-walker: 2.0.2
jsonc-eslint-parser: 2.4.0
- mlly: 1.7.4
+ mlly: 1.8.0
source-map-js: 1.2.1
yaml-eslint-parser: 1.3.0
optionalDependencies:
@@ -7960,7 +7955,7 @@ snapshots:
'@nuxt/devtools-kit@2.6.2(magicast@0.5.1)(vite@7.2.2(@types/node@22.16.5)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.1))':
dependencies:
- '@nuxt/kit': 3.18.0(magicast@0.5.1)
+ '@nuxt/kit': 3.20.1(magicast@0.5.1)
execa: 8.0.1
vite: 7.2.2(@types/node@22.16.5)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.1)
transitivePeerDependencies:
@@ -8072,7 +8067,7 @@ snapshots:
'@nuxt/devtools-kit': 2.6.2(magicast@0.5.1)(vite@7.2.2(@types/node@22.16.5)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.1))
'@nuxt/eslint-config': 1.7.1(@typescript-eslint/utils@8.50.0(eslint@9.31.0(jiti@2.6.1))(typescript@5.8.3))(@vue/compiler-sfc@3.5.27)(eslint@9.31.0(jiti@2.6.1))(typescript@5.8.3)
'@nuxt/eslint-plugin': 1.7.1(eslint@9.31.0(jiti@2.6.1))(typescript@5.8.3)
- '@nuxt/kit': 4.0.2(magicast@0.5.1)
+ '@nuxt/kit': 4.2.1(magicast@0.5.1)
chokidar: 4.0.3
eslint: 9.31.0(jiti@2.6.1)
eslint-flat-config-utils: 2.1.1
@@ -8097,7 +8092,7 @@ snapshots:
'@nuxt/fonts@0.11.4(@netlify/blobs@9.1.2)(db0@0.3.4)(ioredis@5.8.2)(magicast@0.5.1)(vite@7.2.2(@types/node@22.16.5)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.1))':
dependencies:
'@nuxt/devtools-kit': 2.6.2(magicast@0.5.1)(vite@7.2.2(@types/node@22.16.5)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.1))
- '@nuxt/kit': 3.18.0(magicast@0.5.1)
+ '@nuxt/kit': 3.20.1(magicast@0.5.1)
consola: 3.4.2
css-tree: 3.1.0
defu: 6.1.4
@@ -8141,7 +8136,7 @@ snapshots:
'@nuxt/image@1.10.0(@netlify/blobs@9.1.2)(db0@0.3.4)(ioredis@5.8.2)(magicast@0.5.1)':
dependencies:
- '@nuxt/kit': 3.18.0(magicast@0.5.1)
+ '@nuxt/kit': 3.20.1(magicast@0.5.1)
consola: 3.4.2
defu: 6.1.4
h3: 1.15.5
@@ -8175,33 +8170,6 @@ snapshots:
- magicast
- uploadthing
- '@nuxt/kit@3.18.0(magicast@0.5.1)':
- dependencies:
- c12: 3.2.0(magicast@0.5.1)
- consola: 3.4.2
- defu: 6.1.4
- destr: 2.0.5
- errx: 0.1.0
- exsolve: 1.0.7
- ignore: 7.0.5
- jiti: 2.5.1
- klona: 2.0.6
- knitwork: 1.2.0
- mlly: 1.7.4
- ohash: 2.0.11
- pathe: 2.0.3
- pkg-types: 2.2.0
- scule: 1.3.0
- semver: 7.7.2
- std-env: 3.9.0
- tinyglobby: 0.2.14
- ufo: 1.6.3
- unctx: 2.4.1
- unimport: 5.2.0
- untyped: 2.0.0
- transitivePeerDependencies:
- - magicast
-
'@nuxt/kit@3.20.1(magicast@0.5.1)':
dependencies:
c12: 3.3.2(magicast@0.5.1)
@@ -8228,32 +8196,6 @@ snapshots:
transitivePeerDependencies:
- magicast
- '@nuxt/kit@4.0.2(magicast@0.5.1)':
- dependencies:
- c12: 3.2.0(magicast@0.5.1)
- consola: 3.4.2
- defu: 6.1.4
- destr: 2.0.5
- errx: 0.1.0
- exsolve: 1.0.7
- ignore: 7.0.5
- jiti: 2.5.1
- klona: 2.0.6
- mlly: 1.7.4
- ohash: 2.0.11
- pathe: 2.0.3
- pkg-types: 2.2.0
- scule: 1.3.0
- semver: 7.7.2
- std-env: 3.9.0
- tinyglobby: 0.2.14
- ufo: 1.6.3
- unctx: 2.4.1
- unimport: 5.2.0
- untyped: 2.0.0
- transitivePeerDependencies:
- - magicast
-
'@nuxt/kit@4.2.1(magicast@0.5.1)':
dependencies:
c12: 3.3.2(magicast@0.5.1)
@@ -8436,7 +8378,7 @@ snapshots:
'@intlify/unplugin-vue-i18n': 6.0.8(@vue/compiler-dom@3.5.27)(eslint@9.31.0(jiti@2.6.1))(rollup@4.53.3)(typescript@5.8.3)(vue-i18n@10.0.8(vue@3.5.27(typescript@5.8.3)))(vue@3.5.27(typescript@5.8.3))
'@intlify/utils': 0.13.0
'@miyaneee/rollup-plugin-json5': 1.2.0(rollup@4.53.3)
- '@nuxt/kit': 3.18.0(magicast@0.5.1)
+ '@nuxt/kit': 3.20.1(magicast@0.5.1)
'@oxc-parser/wasm': 0.60.0
'@rollup/plugin-yaml': 4.1.2(rollup@4.53.3)
'@vue/compiler-sfc': 3.5.18
@@ -9427,7 +9369,7 @@ snapshots:
fast-glob: 3.3.3
is-glob: 4.0.3
minimatch: 9.0.5
- semver: 7.7.2
+ semver: 7.7.3
ts-api-utils: 2.1.0(typescript@5.8.3)
typescript: 5.8.3
transitivePeerDependencies:
@@ -9826,7 +9768,7 @@ snapshots:
'@vueuse/nuxt@13.6.0(magicast@0.5.1)(nuxt@3.20.1(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.16.5)(@vue/compiler-sfc@3.5.27)(db0@0.3.4)(eslint@9.31.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.1)(magicast@0.5.1)(ms@2.1.3)(optionator@0.9.4)(rollup@4.53.3)(sass@1.89.2)(terser@5.43.1)(typescript@5.8.3)(vite@7.2.2(@types/node@22.16.5)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.1))(vue-tsc@3.0.4(typescript@5.8.3))(yaml@2.8.1))(vue@3.5.27(typescript@5.8.3))':
dependencies:
- '@nuxt/kit': 4.0.2(magicast@0.5.1)
+ '@nuxt/kit': 4.2.1(magicast@0.5.1)
'@vueuse/core': 13.6.0(vue@3.5.27(typescript@5.8.3))
'@vueuse/metadata': 13.6.0
local-pkg: 1.1.1
@@ -10129,23 +10071,6 @@ snapshots:
optionalDependencies:
magicast: 0.3.5
- c12@3.2.0(magicast@0.5.1):
- dependencies:
- chokidar: 4.0.3
- confbox: 0.2.2
- defu: 6.1.4
- dotenv: 17.2.3
- exsolve: 1.0.7
- giget: 2.0.0
- jiti: 2.5.1
- ohash: 2.0.11
- pathe: 2.0.3
- perfect-debounce: 1.0.0
- pkg-types: 2.2.0
- rc9: 2.1.2
- optionalDependencies:
- magicast: 0.5.1
-
c12@3.3.2(magicast@0.5.1):
dependencies:
chokidar: 4.0.3
@@ -10844,7 +10769,7 @@ snapshots:
eslint-import-context: 0.1.9(unrs-resolver@1.11.1)
is-glob: 4.0.3
minimatch: 10.0.3
- semver: 7.7.2
+ semver: 7.7.3
stable-hash-x: 0.2.0
unrs-resolver: 1.11.1
optionalDependencies:
@@ -10863,7 +10788,7 @@ snapshots:
espree: 10.4.0
esquery: 1.6.0
parse-imports-exports: 0.2.4
- semver: 7.7.2
+ semver: 7.7.3
spdx-expression-parse: 4.0.0
transitivePeerDependencies:
- supports-color
@@ -10898,7 +10823,7 @@ snapshots:
pluralize: 8.0.0
regexp-tree: 0.1.27
regjsparser: 0.12.0
- semver: 7.7.2
+ semver: 7.7.3
strip-indent: 4.0.0
eslint-plugin-vue@10.4.0(@typescript-eslint/parser@8.38.0(eslint@9.31.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.31.0(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.31.0(jiti@2.6.1))):
@@ -10908,7 +10833,7 @@ snapshots:
natural-compare: 1.4.0
nth-check: 2.1.1
postcss-selector-parser: 6.1.2
- semver: 7.7.2
+ semver: 7.7.3
vue-eslint-parser: 10.2.0(eslint@9.31.0(jiti@2.6.1))
xml-name-validator: 4.0.0
optionalDependencies:
@@ -11636,7 +11561,7 @@ snapshots:
acorn: 8.15.0
eslint-visitor-keys: 3.4.3
espree: 9.6.1
- semver: 7.7.2
+ semver: 7.7.3
jsonfile@4.0.0:
optionalDependencies:
@@ -11760,8 +11685,8 @@ snapshots:
local-pkg@1.1.1:
dependencies:
- mlly: 1.7.4
- pkg-types: 2.2.0
+ mlly: 1.8.0
+ pkg-types: 2.3.0
quansync: 0.2.10
local-pkg@1.1.2:
@@ -11819,7 +11744,7 @@ snapshots:
dependencies:
estree-walker: 3.0.3
magic-string: 0.30.17
- mlly: 1.7.4
+ mlly: 1.8.0
regexp-tree: 0.1.27
type-level-regexp: 0.1.17
ufo: 1.6.3
@@ -12312,7 +12237,7 @@ snapshots:
node-abi@3.75.0:
dependencies:
- semver: 7.7.2
+ semver: 7.7.3
optional: true
node-addon-api@6.1.0:
@@ -12363,7 +12288,7 @@ snapshots:
normalize-package-data@6.0.2:
dependencies:
hosted-git-info: 7.0.2
- semver: 7.7.2
+ semver: 7.7.3
validate-npm-package-license: 3.0.4
normalize-path@2.1.1:
@@ -12391,7 +12316,7 @@ snapshots:
nuxt-csurf@1.6.5(magicast@0.5.1):
dependencies:
- '@nuxt/kit': 3.18.0(magicast@0.5.1)
+ '@nuxt/kit': 3.20.1(magicast@0.5.1)
defu: 6.1.4
uncsrf: 1.2.0
transitivePeerDependencies:
@@ -12399,7 +12324,7 @@ snapshots:
nuxt-security@2.2.0(magicast@0.5.1)(rollup@4.53.3):
dependencies:
- '@nuxt/kit': 3.18.0(magicast@0.5.1)
+ '@nuxt/kit': 3.20.1(magicast@0.5.1)
basic-auth: 2.0.1
defu: 6.1.4
nuxt-csurf: 1.6.5(magicast@0.5.1)
@@ -13424,7 +13349,7 @@ snapshots:
detect-libc: 2.0.4
node-addon-api: 6.1.0
prebuild-install: 7.1.3
- semver: 7.7.2
+ semver: 7.7.3
simple-get: 4.0.1
tar-fs: 3.1.1
tunnel-agent: 0.6.0
@@ -13902,13 +13827,13 @@ snapshots:
estree-walker: 3.0.3
local-pkg: 1.1.1
magic-string: 0.30.17
- mlly: 1.7.4
+ mlly: 1.8.0
pathe: 2.0.3
picomatch: 4.0.3
- pkg-types: 2.2.0
+ pkg-types: 2.3.0
scule: 1.3.0
strip-literal: 3.0.0
- tinyglobby: 0.2.14
+ tinyglobby: 0.2.15
unplugin: 2.3.5
unplugin-utils: 0.2.4
@@ -13969,7 +13894,7 @@ snapshots:
local-pkg: 1.1.1
magic-string: 0.30.17
micromatch: 4.0.8
- mlly: 1.7.4
+ mlly: 1.8.0
pathe: 2.0.3
scule: 1.3.0
unplugin: 2.3.5
@@ -14266,7 +14191,7 @@ snapshots:
eslint-visitor-keys: 4.2.1
espree: 10.4.0
esquery: 1.6.0
- semver: 7.7.2
+ semver: 7.7.3
transitivePeerDependencies:
- supports-color
@@ -14290,7 +14215,7 @@ snapshots:
vue3-carousel-nuxt@1.1.6(magicast@0.5.1)(vue@3.5.27(typescript@5.8.3)):
dependencies:
- '@nuxt/kit': 3.18.0(magicast@0.5.1)
+ '@nuxt/kit': 3.20.1(magicast@0.5.1)
vue3-carousel: 0.15.1(vue@3.5.27(typescript@5.8.3))
transitivePeerDependencies:
- magicast
diff --git a/prisma/migrations/20260119070859_add_game_type/migration.sql b/prisma/migrations/20260119070859_add_game_type/migration.sql
new file mode 100644
index 00000000..48d0518a
--- /dev/null
+++ b/prisma/migrations/20260119070859_add_game_type/migration.sql
@@ -0,0 +1,17 @@
+-- CreateEnum
+CREATE TYPE "GameType" AS ENUM ('Game', 'Executor', 'Redist');
+
+-- DropIndex
+DROP INDEX "Game_mName_idx";
+
+-- DropIndex
+DROP INDEX "GameTag_name_idx";
+
+-- AlterTable
+ALTER TABLE "Game" ADD COLUMN "type" "GameType" NOT NULL DEFAULT 'Game';
+
+-- 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));
diff --git a/prisma/migrations/20260119123507_update_game_version/migration.sql b/prisma/migrations/20260119123507_update_game_version/migration.sql
new file mode 100644
index 00000000..153ab230
--- /dev/null
+++ b/prisma/migrations/20260119123507_update_game_version/migration.sql
@@ -0,0 +1,64 @@
+/*
+ Warnings:
+
+ - The primary key for the `GameVersion` table will be changed. If it partially fails, the table could be left without primary key constraint.
+ - You are about to drop the column `gameId` on the `LaunchConfiguration` table. All the data in the column will be lost.
+ - You are about to drop the column `gameId` on the `SetupConfiguration` table. All the data in the column will be lost.
+
+*/
+-- DropForeignKey
+ALTER TABLE "LaunchConfiguration" DROP CONSTRAINT "LaunchConfiguration_executorId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "LaunchConfiguration" DROP CONSTRAINT "LaunchConfiguration_gameId_versionId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "SetupConfiguration" DROP CONSTRAINT "SetupConfiguration_gameId_versionId_fkey";
+
+-- DropIndex
+DROP INDEX "Game_mName_idx";
+
+-- DropIndex
+DROP INDEX "GameTag_name_idx";
+
+-- AlterTable
+ALTER TABLE "GameVersion" DROP CONSTRAINT "GameVersion_pkey",
+ADD CONSTRAINT "GameVersion_pkey" PRIMARY KEY ("versionId");
+
+-- AlterTable
+ALTER TABLE "LaunchConfiguration" DROP COLUMN "gameId";
+
+-- AlterTable
+ALTER TABLE "SetupConfiguration" DROP COLUMN "gameId";
+
+-- CreateTable
+CREATE TABLE "_requiredContent" (
+ "A" TEXT NOT NULL,
+ "B" TEXT NOT NULL,
+
+ CONSTRAINT "_requiredContent_AB_pkey" PRIMARY KEY ("A","B")
+);
+
+-- CreateIndex
+CREATE INDEX "_requiredContent_B_index" ON "_requiredContent"("B");
+
+-- 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 "SetupConfiguration" ADD CONSTRAINT "SetupConfiguration_versionId_fkey" FOREIGN KEY ("versionId") REFERENCES "GameVersion"("versionId") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "LaunchConfiguration" ADD CONSTRAINT "LaunchConfiguration_executorId_fkey" FOREIGN KEY ("executorId") REFERENCES "LaunchConfiguration"("launchId") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "LaunchConfiguration" ADD CONSTRAINT "LaunchConfiguration_versionId_fkey" FOREIGN KEY ("versionId") REFERENCES "GameVersion"("versionId") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_requiredContent" ADD CONSTRAINT "_requiredContent_A_fkey" FOREIGN KEY ("A") REFERENCES "GameVersion"("versionId") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_requiredContent" ADD CONSTRAINT "_requiredContent_B_fkey" FOREIGN KEY ("B") REFERENCES "GameVersion"("versionId") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/prisma/migrations/20260119132823_add_executor_suggestions/migration.sql b/prisma/migrations/20260119132823_add_executor_suggestions/migration.sql
new file mode 100644
index 00000000..21aaa132
--- /dev/null
+++ b/prisma/migrations/20260119132823_add_executor_suggestions/migration.sql
@@ -0,0 +1,14 @@
+-- DropIndex
+DROP INDEX "Game_mName_idx";
+
+-- DropIndex
+DROP INDEX "GameTag_name_idx";
+
+-- AlterTable
+ALTER TABLE "LaunchConfiguration" ADD COLUMN "executorSuggestions" 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));
diff --git a/prisma/migrations/20260122002153_add_unimported_game_version/migration.sql b/prisma/migrations/20260122002153_add_unimported_game_version/migration.sql
new file mode 100644
index 00000000..e251919e
--- /dev/null
+++ b/prisma/migrations/20260122002153_add_unimported_game_version/migration.sql
@@ -0,0 +1,27 @@
+-- DropIndex
+DROP INDEX "Game_mName_idx";
+
+-- DropIndex
+DROP INDEX "GameTag_name_idx";
+
+-- AlterTable
+ALTER TABLE "GameVersion" ALTER COLUMN "versionPath" DROP NOT NULL;
+
+-- CreateTable
+CREATE TABLE "UnimportedGameVersion" (
+ "id" TEXT NOT NULL,
+ "gameId" TEXT NOT NULL,
+ "versionName" TEXT NOT NULL,
+ "manifest" JSONB NOT NULL,
+
+ CONSTRAINT "UnimportedGameVersion_pkey" PRIMARY KEY ("id")
+);
+
+-- 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 "UnimportedGameVersion" ADD CONSTRAINT "UnimportedGameVersion_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/prisma/migrations/20260122014854_add_filelist_to_unimported/migration.sql b/prisma/migrations/20260122014854_add_filelist_to_unimported/migration.sql
new file mode 100644
index 00000000..a77d7aee
--- /dev/null
+++ b/prisma/migrations/20260122014854_add_filelist_to_unimported/migration.sql
@@ -0,0 +1,14 @@
+-- DropIndex
+DROP INDEX "Game_mName_idx";
+
+-- DropIndex
+DROP INDEX "GameTag_name_idx";
+
+-- AlterTable
+ALTER TABLE "UnimportedGameVersion" ADD COLUMN "fileList" 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));
diff --git a/prisma/migrations/20260123021108_add_file_list/migration.sql b/prisma/migrations/20260123021108_add_file_list/migration.sql
new file mode 100644
index 00000000..59f40f05
--- /dev/null
+++ b/prisma/migrations/20260123021108_add_file_list/migration.sql
@@ -0,0 +1,15 @@
+-- DropIndex
+DROP INDEX "Game_mName_idx";
+
+-- DropIndex
+DROP INDEX "GameTag_name_idx";
+
+-- AlterTable
+ALTER TABLE "GameVersion" ADD COLUMN "fileList" TEXT[],
+ADD COLUMN "negativeFileList" 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));
diff --git a/prisma/models/content.prisma b/prisma/models/content.prisma
index fbd4244b..f4e543e8 100644
--- a/prisma/models/content.prisma
+++ b/prisma/models/content.prisma
@@ -8,6 +8,12 @@ enum MetadataSource {
OpenCritic
}
+enum GameType {
+ Game
+ Executor
+ Redist
+}
+
model Game {
id String @id @default(uuid())
@@ -15,6 +21,8 @@ model Game {
metadataId String
created DateTime @default(now())
+ type GameType @default(Game)
+
// Any field prefixed with m is filled in from metadata
// Acts as a cache so we can search and filter it
mName String // Name of game
@@ -47,8 +55,9 @@ model Game {
tags GameTag[]
playtime Playtime[]
- developers Company[] @relation(name: "developers")
- publishers Company[] @relation(name: "publishers")
+ developers Company[] @relation(name: "developers")
+ publishers Company[] @relation(name: "publishers")
+ unimportedGameVersions UnimportedGameVersion[]
@@unique([metadataSource, metadataId], name: "metadataKey")
@@unique([libraryId, libraryPath], name: "libraryKey")
@@ -82,14 +91,24 @@ model GameRating {
@@unique([metadataSource, metadataId], name: "metadataKey")
}
+model UnimportedGameVersion {
+ id String @id @default(uuid())
+ gameId String
+ game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)
+ versionName String
+
+ manifest Json
+ fileList String[]
+}
+
// A particular set of files that relate to the version
model GameVersion {
gameId String
game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)
- versionId String @default(uuid())
+ versionId String @id @default(uuid())
displayName String?
- versionPath String
+ versionPath String?
created DateTime @default(now())
@@ -98,12 +117,15 @@ model GameVersion {
onlySetup Boolean @default(false)
- dropletManifest Json // Results from droplet
+ dropletManifest Json // Results from droplet
+ fileList String[] // List of all files, for delta updates
+ negativeFileList String[] // List of files to remove, for delta updates
versionIndex Int
delta Boolean @default(false)
- @@id([gameId, versionId])
+ requiredContent GameVersion[] @relation(name: "requiredContent")
+ requiringContent GameVersion[] @relation(name: "requiredContent")
}
model SetupConfiguration {
@@ -113,9 +135,8 @@ model SetupConfiguration {
platform Platform
- gameId String
versionId String
- gameVersion GameVersion @relation(fields: [gameId, versionId], references: [gameId, versionId], onDelete: Cascade, onUpdate: Cascade)
+ gameVersion GameVersion @relation(fields: [versionId], references: [versionId], onDelete: Cascade, onUpdate: Cascade)
}
model LaunchConfiguration {
@@ -128,14 +149,14 @@ model LaunchConfiguration {
platform Platform
// For emulation targets
- executorId String?
- executor LaunchConfiguration? @relation(fields: [executorId], references: [launchId], name: "executor", onDelete: Cascade, onUpdate: Cascade)
+ executorId String?
+ executor LaunchConfiguration? @relation(fields: [executorId], references: [launchId], name: "executor")
+ executorSuggestions String[]
umuIdOverride String?
- gameId String
versionId String
- gameVersion GameVersion @relation(fields: [gameId, versionId], references: [gameId, versionId], onDelete: Cascade, onUpdate: Cascade)
+ gameVersion GameVersion @relation(fields: [versionId], references: [versionId], onDelete: Cascade, onUpdate: Cascade)
executions LaunchConfiguration[] @relation("executor")
}
diff --git a/server/api/v1/admin/depot/index.get.ts b/server/api/v1/admin/depot/index.get.ts
new file mode 100644
index 00000000..e355d53b
--- /dev/null
+++ b/server/api/v1/admin/depot/index.get.ts
@@ -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, ["depot:read"]);
+ if (!allowed) throw createError({ statusCode: 403 });
+
+ const depots = await prisma.depot.findMany({});
+
+ return depots;
+});
diff --git a/server/api/v1/admin/depot/manifest.get.ts b/server/api/v1/admin/depot/torrential/manifest.get.ts
similarity index 93%
rename from server/api/v1/admin/depot/manifest.get.ts
rename to server/api/v1/admin/depot/torrential/manifest.get.ts
index 1f0b7b7f..c7edee51 100644
--- a/server/api/v1/admin/depot/manifest.get.ts
+++ b/server/api/v1/admin/depot/torrential/manifest.get.ts
@@ -6,7 +6,6 @@ import { castManifest } from "~/server/internal/library/manifest";
const AUTHORIZATION_HEADER_PREFIX = "Bearer ";
const Query = type({
- game: "string",
version: "string",
});
@@ -31,10 +30,7 @@ export default defineEventHandler(async (h3) => {
const version = await prisma.gameVersion.findUnique({
where: {
- gameId_versionId: {
- gameId: query.game,
- versionId: query.version,
- },
+ versionId: query.version,
},
select: {
dropletManifest: true,
diff --git a/server/api/v1/admin/depot/versions.get.ts b/server/api/v1/admin/depot/torrential/versions.get.ts
similarity index 81%
rename from server/api/v1/admin/depot/versions.get.ts
rename to server/api/v1/admin/depot/torrential/versions.get.ts
index 0e0aafaf..00553d3a 100644
--- a/server/api/v1/admin/depot/versions.get.ts
+++ b/server/api/v1/admin/depot/torrential/versions.get.ts
@@ -11,6 +11,11 @@ export default defineEventHandler(async (h3) => {
select: {
versionId: true,
},
+ where: {
+ versionPath: {
+ not: null
+ }
+ }
},
},
});
diff --git a/server/api/v1/admin/depot/upload.post.ts b/server/api/v1/admin/depot/upload.post.ts
new file mode 100644
index 00000000..69586298
--- /dev/null
+++ b/server/api/v1/admin/depot/upload.post.ts
@@ -0,0 +1,51 @@
+import { type } from "arktype";
+import { readDropValidatedBody, throwingArktype } from "~/server/arktype";
+import aclManager from "~/server/internal/acls";
+import prisma from "~/server/internal/db/database";
+
+const UploadManifest = type({
+ gameId: "string",
+ versionName: "string",
+
+ manifest: type({
+ version: "'2'",
+ size: "number",
+ key: "16 <= number[] <= 16",
+ chunks: type({
+ ["string"]: {
+ checksum: "string",
+ iv: "16 <= number[] <= 16",
+ files: type({
+ filename: "string",
+ start: "number",
+ length: "number",
+ permissions: "number",
+ }).array(),
+ },
+ }),
+ }),
+ fileList: "string[]",
+}).configure(throwingArktype);
+
+export default defineEventHandler(async (h3) => {
+ const allowed = await aclManager.allowSystemACL(h3, ["depot:upload:new"]);
+ if (!allowed) throw createError({ statusCode: 403 });
+
+ const { gameId, versionName, manifest, fileList } =
+ await readDropValidatedBody(h3, UploadManifest);
+
+ const version = await prisma.unimportedGameVersion.create({
+ data: {
+ game: {
+ connect: {
+ id: gameId,
+ },
+ },
+ versionName,
+ manifest,
+ fileList,
+ },
+ });
+
+ return { id: version.id };
+});
diff --git a/server/api/v1/admin/game/[id]/index.get.ts b/server/api/v1/admin/game/[id]/index.get.ts
index 90f84990..a94496b6 100644
--- a/server/api/v1/admin/game/[id]/index.get.ts
+++ b/server/api/v1/admin/game/[id]/index.get.ts
@@ -1,6 +1,7 @@
import type { GameVersion, Prisma } from "~/prisma/client/client";
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
+import type { UnimportedVersionInformation } from "~/server/internal/library";
import libraryManager from "~/server/internal/library";
async function getGameVersionSize<
@@ -59,7 +60,7 @@ export default defineEventHandler<
{ body: never },
Promise<{
game: AdminFetchGameType;
- unimportedVersions: string[] | undefined;
+ unimportedVersions: UnimportedVersionInformation[] | undefined;
}>
>(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["game:read"]);
diff --git a/server/api/v1/admin/import/game/index.post.ts b/server/api/v1/admin/import/game/index.post.ts
index e3e32478..cd582167 100644
--- a/server/api/v1/admin/import/game/index.post.ts
+++ b/server/api/v1/admin/import/game/index.post.ts
@@ -1,4 +1,5 @@
import { type } from "arktype";
+import { GameType } from "~/prisma/client/enums";
import { readDropValidatedBody, throwingArktype } from "~/server/arktype";
import aclManager from "~/server/internal/acls";
import libraryManager from "~/server/internal/library";
@@ -7,6 +8,7 @@ import metadataHandler from "~/server/internal/metadata";
const ImportGameBody = type({
library: "string",
path: "string",
+ type: type.valueOf(GameType),
["metadata?"]: {
id: "string",
sourceId: "string",
@@ -19,7 +21,7 @@ export default defineEventHandler<{ body: typeof ImportGameBody.infer }>(
const allowed = await aclManager.allowSystemACL(h3, ["import:game:new"]);
if (!allowed) throw createError({ statusCode: 403 });
- const { library, path, metadata } = await readDropValidatedBody(
+ const { library, path, metadata, type } = await readDropValidatedBody(
h3,
ImportGameBody,
);
@@ -38,8 +40,8 @@ export default defineEventHandler<{ body: typeof ImportGameBody.infer }>(
});
const taskId = metadata
- ? await metadataHandler.createGame(metadata, library, path)
- : await metadataHandler.createGameWithoutMetadata(library, path);
+ ? await metadataHandler.createGame(metadata, library, path, type)
+ : await metadataHandler.createGameWithoutMetadata(library, path, type);
if (!taskId)
throw createError({
diff --git a/server/api/v1/admin/import/version/index.get.ts b/server/api/v1/admin/import/version/index.get.ts
index 893d295c..4371d9d9 100644
--- a/server/api/v1/admin/import/version/index.get.ts
+++ b/server/api/v1/admin/import/version/index.get.ts
@@ -16,7 +16,7 @@ export default defineEventHandler(async (h3) => {
const game = await prisma.game.findUnique({
where: { id: gameId },
- select: { libraryId: true, libraryPath: true },
+ select: { libraryId: true, libraryPath: true, type: true },
});
if (!game || !game.libraryId)
throw createError({ statusCode: 404, statusMessage: "Game not found" });
@@ -28,5 +28,5 @@ export default defineEventHandler(async (h3) => {
if (!unimportedVersions)
throw createError({ statusCode: 400, statusMessage: "Invalid game ID" });
- return unimportedVersions;
+ return { versions: unimportedVersions, type: game.type };
});
diff --git a/server/api/v1/admin/import/version/index.post.ts b/server/api/v1/admin/import/version/index.post.ts
index cac47661..a42531cf 100644
--- a/server/api/v1/admin/import/version/index.post.ts
+++ b/server/api/v1/admin/import/version/index.post.ts
@@ -7,7 +7,11 @@ import libraryManager from "~/server/internal/library";
export const ImportVersion = type({
id: "string",
- version: "string",
+ version: type({
+ type: "'depot' | 'local'",
+ identifier: "string",
+ name: "string",
+ }),
displayName: "string?",
launches: type({
@@ -16,6 +20,7 @@ export const ImportVersion = type({
launch: "string",
umuId: "string?",
executorId: "string?",
+ suggestions: "string[]?",
}).array(),
setups: type({
@@ -25,6 +30,10 @@ export const ImportVersion = type({
onlySetup: "boolean = false",
delta: "boolean = false",
+
+ requiredContent: type("string")
+ .array()
+ .default(() => []),
}).configure(throwingArktype);
export default defineEventHandler(async (h3) => {
@@ -47,7 +56,7 @@ export default defineEventHandler(async (h3) => {
if (validOverlayVersions == 0)
throw createError({
statusCode: 400,
- statusMessage: "Update mode requires a pre-existing version.",
+ statusMessage: `Update mode requires a pre-existing version for platform: ${platformObject.platform}`,
});
}
}
diff --git a/server/api/v1/admin/import/version/preload.get.ts b/server/api/v1/admin/import/version/preload.get.ts
index 5e0476ce..3ad2b661 100644
--- a/server/api/v1/admin/import/version/preload.get.ts
+++ b/server/api/v1/admin/import/version/preload.get.ts
@@ -1,35 +1,43 @@
+import { ArkErrors, type } from "arktype";
import aclManager from "~/server/internal/acls";
import libraryManager from "~/server/internal/library";
+const Query = type({
+ id: "string",
+ type: "'depot' | 'local'",
+ version: "string",
+});
+
export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["import:version:read"]);
if (!allowed) throw createError({ statusCode: 403 });
- const query = await getQuery(h3);
- const gameId = query.id?.toString();
- const versionName = query.version?.toString();
- if (!gameId || !versionName)
+ const query = Query(getQuery(h3));
+ if (query instanceof ArkErrors)
throw createError({
statusCode: 400,
- statusMessage: "Missing id or version in request params",
+ message: query.summary,
});
try {
const preload = await libraryManager.fetchUnimportedVersionInformation(
- gameId,
- versionName,
+ query.id,
+ {
+ type: query.type,
+ identifier: query.version,
+ },
);
if (!preload)
throw createError({
statusCode: 400,
- statusMessage: "Invalid game or version id/name",
+ message: "Invalid game or version id/name",
});
return preload;
} catch (e) {
throw createError({
statusCode: 500,
- message: `Failed to fetch preload information for ${gameId}: ${e}`,
+ message: `Failed to fetch preload information for ${query.id}: ${e}`,
});
}
});
diff --git a/server/api/v1/admin/search/game.get.ts b/server/api/v1/admin/search/game.get.ts
index 2d10a9f3..6fa57b1e 100644
--- a/server/api/v1/admin/search/game.get.ts
+++ b/server/api/v1/admin/search/game.get.ts
@@ -1,14 +1,19 @@
import { ArkErrors, type } from "arktype";
+import { GameType } from "~/prisma/client/enums";
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
import type { GameMetadataSearchResult } from "~/server/internal/metadata/types";
const Query = type({
q: "string",
+ type: type.valueOf(GameType).optional(),
});
export default defineEventHandler(async (h3) => {
- const allowed = await aclManager.allowSystemACL(h3, ["game:read"]);
+ const allowed = await aclManager.allowSystemACL(h3, [
+ "game:read",
+ "depot:read",
+ ]);
if (!allowed) throw createError({ statusCode: 403 });
const query = Query(getQuery(h3));
@@ -22,7 +27,7 @@ export default defineEventHandler(async (h3) => {
mShortDescription: string;
mReleased: string;
}[] =
- await prisma.$queryRaw`SELECT id, "mName", "mIconObjectId", "mShortDescription", "mReleased" FROM "Game" WHERE SIMILARITY("mName", ${query.q}) > 0.2 ORDER BY SIMILARITY("mName", ${query.q}) DESC;`;
+ await prisma.$queryRaw`SELECT id, "mName", "mIconObjectId", "mShortDescription", "mReleased" FROM "Game" WHERE SIMILARITY("mName", ${query.q}) > 0.2 AND (${query.type || "undefined"} = 'undefined' OR type::text = ${query.type}) ORDER BY SIMILARITY("mName", ${query.q}) DESC;`;
const resultsMapped = results.map(
(v) =>
diff --git a/server/api/v1/client/game/[id]/version/[versionid]/index.get.ts b/server/api/v1/client/game/[id]/version/[versionid]/index.get.ts
index fd9a62cc..1f3e7a22 100644
--- a/server/api/v1/client/game/[id]/version/[versionid]/index.get.ts
+++ b/server/api/v1/client/game/[id]/version/[versionid]/index.get.ts
@@ -13,15 +13,24 @@ export default defineClientEventHandler(async (h3) => {
const gameVersion = await prisma.gameVersion.findUnique({
where: {
- gameId_versionId: {
- gameId: id,
- versionId: version,
- },
+ versionId: version,
},
include: {
launches: {
include: {
- executor: true,
+ executor: {
+ include: {
+ gameVersion: {
+ select: {
+ game: {
+ select: {
+ id: true,
+ },
+ },
+ },
+ },
+ },
+ },
},
},
setups: true,
@@ -34,8 +43,22 @@ export default defineClientEventHandler(async (h3) => {
statusMessage: "Game version not found",
});
- return {
+ const gameVersionMapped = {
...gameVersion,
+ launches: gameVersion.launches.map((launch) => ({
+ ...launch,
+ executor: launch.executor
+ ? {
+ ...launch.executor,
+ gameVersion: undefined,
+ gameId: launch.executor.gameVersion.game.id,
+ }
+ : undefined,
+ })),
+ };
+
+ return {
+ ...gameVersionMapped,
size: libraryManager.getGameVersionSize(id, version),
};
});
diff --git a/server/api/v1/client/game/manifest.get.ts b/server/api/v1/client/game/manifest.get.ts
index 77a278ed..44208a40 100644
--- a/server/api/v1/client/game/manifest.get.ts
+++ b/server/api/v1/client/game/manifest.get.ts
@@ -1,24 +1,15 @@
import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
-import prisma from "~/server/internal/db/database";
+import { createDownloadManifestDetails } from "~/server/internal/library/manifest/index";
export default defineClientEventHandler(async (h3) => {
const query = getQuery(h3);
- const id = query.id?.toString();
const version = query.version?.toString();
- if (!id || !version)
+ if (!version)
throw createError({
statusCode: 400,
- statusMessage: "Missing id or version in query",
+ statusMessage: "Missing version ID in query",
});
- const manifest = await prisma.gameVersion.findUnique({
- where: { gameId_versionId: { gameId: id, versionId: version } },
- select: { dropletManifest: true },
- });
- if (!manifest)
- throw createError({
- statusCode: 400,
- statusMessage: "Invalid game or version, or no versions added.",
- });
- return manifest.dropletManifest;
+ const result = await createDownloadManifestDetails(version);
+ return result;
});
diff --git a/server/api/v1/client/game/versions.get.ts b/server/api/v1/client/game/versions.get.ts
index 8dd7e9c2..efa5c4cb 100644
--- a/server/api/v1/client/game/versions.get.ts
+++ b/server/api/v1/client/game/versions.get.ts
@@ -5,8 +5,8 @@ import gameSizeManager from "~/server/internal/gamesize";
type VersionDownloadOption = {
versionId: string;
- displayName?: string;
- versionPath: string;
+ displayName?: string | undefined;
+ versionPath?: string | undefined;
platform: Platform;
size: number;
requiredContent: Array<{
@@ -106,7 +106,8 @@ export default defineClientEventHandler(async (h3) => {
([platform, requiredContent]) =>
({
versionId: v.versionId,
- versionPath: v.versionPath,
+ displayName: v.displayName || undefined,
+ versionPath: v.versionPath || undefined,
platform,
requiredContent,
size: size!,
diff --git a/server/api/v1/store/index.get.ts b/server/api/v1/store/index.get.ts
index 5d98321e..be1b5ccb 100644
--- a/server/api/v1/store/index.get.ts
+++ b/server/api/v1/store/index.get.ts
@@ -1,5 +1,6 @@
import { ArkErrors, type } from "arktype";
import type { Prisma } from "~/prisma/client/client";
+import { GameType } from "~/prisma/client/enums";
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
import { parsePlatform } from "~/server/internal/utils/parseplatform";
@@ -100,6 +101,7 @@ export default defineEventHandler(async (h3) => {
...tagFilter,
...platformFilter,
...companyFilter,
+ type: GameType.Game,
};
const sort: Prisma.GameOrderByWithRelationInput = {};
diff --git a/server/internal/acls/descriptions.ts b/server/internal/acls/descriptions.ts
index c46e5fcc..83cf959f 100644
--- a/server/internal/acls/descriptions.ts
+++ b/server/internal/acls/descriptions.ts
@@ -108,8 +108,11 @@ export const systemACLDescriptions: ObjectFromList = {
"settings:update": "Update system settings.",
+ "depot:read": "Read depot information, and search for games",
"depot:new": "Create a new download depot",
"depot:delete": "Remove a download depot",
+ "depot:upload:new": "Upload a new version to a depot",
+ "depot:upload:delete": "Remove a depot version",
"system-data:listen":
"Connect to a websocket to receive system data updates.",
diff --git a/server/internal/acls/index.ts b/server/internal/acls/index.ts
index 80d2b189..f0bf391c 100644
--- a/server/internal/acls/index.ts
+++ b/server/internal/acls/index.ts
@@ -47,8 +47,11 @@ export type UserACL = Array<(typeof userACLs)[number]>;
export const systemACLs = [
"setup",
+ "depot:read",
"depot:new",
"depot:delete",
+ "depot:upload:new",
+ "depot:upload:delete",
"auth:read",
"auth:simple:invitation:read",
diff --git a/server/internal/gamesize/index.ts b/server/internal/gamesize/index.ts
index dac70b96..a919ad17 100644
--- a/server/internal/gamesize/index.ts
+++ b/server/internal/gamesize/index.ts
@@ -69,7 +69,7 @@ class GameSizeManager {
}
const { dropletManifest } = (await prisma.gameVersion.findUnique({
- where: { gameId_versionId: { versionId, gameId } },
+ where: { versionId },
}))!;
return castManifest(dropletManifest).size;
diff --git a/server/internal/library/index.ts b/server/internal/library/index.ts
index 48c7f196..84513a5f 100644
--- a/server/internal/library/index.ts
+++ b/server/internal/library/index.ts
@@ -18,6 +18,8 @@ import type { WorkingLibrarySource } from "~/server/api/v1/admin/library/sources
import gameSizeManager from "~/server/internal/gamesize";
import { TORRENTIAL_SERVICE } from "../services/services/torrential";
import type { ImportVersion } from "~/server/api/v1/admin/import/version/index.post";
+import { GameType, type Platform } from "~/prisma/client/enums";
+import { castManifest } from "./manifest";
export function createGameImportTaskId(libraryId: string, libraryPath: string) {
return createHash("md5")
@@ -34,6 +36,30 @@ export function createVersionImportTaskKey(
.digest("hex");
}
+export interface ExecutorVersionGuess {
+ type: "executor";
+ executorId: string;
+ icon: string;
+ gameName: string;
+ versionName: string;
+ launchName: string;
+ platform: Platform;
+}
+export interface PlatformVersionGuess {
+ platform: Platform;
+ type: "platform";
+}
+export type VersionGuess = {
+ filename: string;
+ match: number;
+} & (PlatformVersionGuess | ExecutorVersionGuess);
+
+export interface UnimportedVersionInformation {
+ type: "local" | "depot";
+ name: string;
+ identifier: string;
+}
+
class LibraryManager {
private libraries: Map> = new Map();
@@ -95,7 +121,10 @@ class LibraryManager {
return unimportedGames;
}
- async fetchUnimportedGameVersions(libraryId: string, libraryPath: string) {
+ async fetchUnimportedGameVersions(
+ libraryId: string,
+ libraryPath: string,
+ ): Promise {
const provider = this.libraries.get(libraryId);
if (!provider) return undefined;
const game = await prisma.game.findUnique({
@@ -115,14 +144,40 @@ class LibraryManager {
try {
const versions = await provider.listVersions(
libraryPath,
- game.versions.map((v) => v.versionPath),
+ game.versions.map((v) => v.versionPath).filter((v) => v !== null),
);
- const unimportedVersions = versions.filter(
- (e) =>
- game.versions.findIndex((v) => v.versionPath == e) == -1 &&
- !taskHandler.hasTaskKey(createVersionImportTaskKey(game.id, e)),
+ const unimportedVersions = versions
+ .filter(
+ (e) =>
+ game.versions.findIndex((v) => v.versionPath == e) == -1 &&
+ !taskHandler.hasTaskKey(createVersionImportTaskKey(game.id, e)),
+ )
+ .map(
+ (v) =>
+ ({
+ type: "local",
+ name: v,
+ identifier: v,
+ }) satisfies UnimportedVersionInformation,
+ );
+ const depotVersions = await prisma.unimportedGameVersion.findMany({
+ where: {
+ gameId: game.id,
+ },
+ select: {
+ versionName: true,
+ id: true,
+ },
+ });
+ const mappedDepotVersions = depotVersions.map(
+ (v) =>
+ ({
+ type: "depot",
+ name: v.versionName,
+ identifier: v.id,
+ }) satisfies UnimportedVersionInformation,
);
- return unimportedVersions;
+ return [...unimportedVersions, ...mappedDepotVersions];
} catch (e) {
if (e instanceof GameNotFoundError) {
logger.warn(e);
@@ -165,10 +220,13 @@ class LibraryManager {
/**
* Fetches recommendations and extra data about the version. Doesn't actually check if it's been imported.
* @param gameId
- * @param versionName
+ * @param versionIdentifier
* @returns
*/
- async fetchUnimportedVersionInformation(gameId: string, versionName: string) {
+ async fetchUnimportedVersionInformation(
+ gameId: string,
+ versionIdentifier: Omit,
+ ) {
const game = await prisma.game.findUnique({
where: { id: gameId },
select: { libraryPath: true, libraryId: true, mName: true },
@@ -178,7 +236,7 @@ class LibraryManager {
const library = this.libraries.get(game.libraryId);
if (!library) return undefined;
- const fileExts: { [key: string]: string[] } = {
+ const fileExts: { [key in Platform]: string[] } = {
Linux: [
// Ext for Unity games
".x86_64",
@@ -196,13 +254,60 @@ class LibraryManager {
],
};
- const options: Array<{
- filename: string;
- platform: string;
- match: number;
- }> = [];
+ const executorSuggestions = await prisma.launchConfiguration.findMany({
+ where: {
+ executorSuggestions: {
+ isEmpty: false,
+ },
+ gameVersion: {
+ game: {
+ type: GameType.Executor,
+ },
+ },
+ },
+ select: {
+ executorSuggestions: true,
+ gameVersion: {
+ select: {
+ game: {
+ select: {
+ mIconObjectId: true,
+ mName: true,
+ },
+ },
+ displayName: true,
+ versionPath: true,
+ },
+ },
+ name: true,
+ launchId: true,
+ platform: true,
+ },
+ });
+
+ const options: Array = [];
+
+ let files;
+ if (versionIdentifier.type === "local") {
+ files = await library.versionReaddir(
+ game.libraryPath,
+ versionIdentifier.identifier,
+ );
+ } else if (versionIdentifier.type === "depot") {
+ const unimported = await prisma.unimportedGameVersion.findUnique({
+ where: {
+ id: versionIdentifier.identifier,
+ },
+ select: {
+ fileList: true,
+ },
+ });
+ if (!unimported) return undefined;
+ files = unimported.fileList;
+ } else {
+ return undefined;
+ }
- const files = await library.versionReaddir(game.libraryPath, versionName);
for (const filename of files) {
const basename = path.basename(filename);
const dotLocation = filename.lastIndexOf(".");
@@ -213,9 +318,29 @@ class LibraryManager {
if (checkExt != ext) continue;
const fuzzyValue = fuzzy(basename, game.mName);
options.push({
+ type: "platform",
+ filename: filename.replaceAll(" ", "\\ "),
+ platform: platform as Platform,
+ match: fuzzyValue,
+ });
+ }
+ }
+ for (const executorSuggestion of executorSuggestions) {
+ for (const suggestion of executorSuggestion.executorSuggestions) {
+ if (suggestion != ext) continue;
+ const fuzzyValue = fuzzy(basename, game.mName);
+ options.push({
+ type: "executor",
filename: filename.replaceAll(" ", "\\ "),
- platform,
match: fuzzyValue,
+ executorId: executorSuggestion.launchId,
+
+ icon: executorSuggestion.gameVersion.game.mIconObjectId,
+ gameName: executorSuggestion.gameVersion.game.mName,
+ versionName: (executorSuggestion.gameVersion.displayName ??
+ executorSuggestion.gameVersion.versionPath)!,
+ launchName: executorSuggestion.name,
+ platform: executorSuggestion.platform,
});
}
}
@@ -247,49 +372,79 @@ class LibraryManager {
async importVersion(
gameId: string,
- versionPath: string,
+ version: UnimportedVersionInformation,
metadata: typeof ImportVersion.infer,
) {
- const taskKey = createVersionImportTaskKey(gameId, versionPath);
+ const taskKey = createVersionImportTaskKey(gameId, version.identifier);
const game = await prisma.game.findUnique({
where: { id: gameId },
- select: { mName: true, libraryId: true, libraryPath: true },
+ select: { mName: true, libraryId: true, libraryPath: true, type: true },
});
if (!game || !game.libraryId) return undefined;
+ if (game.type === GameType.Redist && !metadata.onlySetup)
+ throw createError({
+ statusCode: 400,
+ message: "Redistributables can only be in setup-only mode.",
+ });
+
const library = this.libraries.get(game.libraryId);
if (!library) return undefined;
+ const unimportedVersion =
+ version.type === "depot"
+ ? await prisma.unimportedGameVersion.findUnique({
+ where: { id: version.identifier },
+ })
+ : undefined;
+
return await taskHandler.create({
key: taskKey,
taskGroup: "import:game",
- name: `Importing version ${versionPath} for ${game.mName}`,
+ name: `Importing version ${version.name} for ${game.mName}`,
acls: ["system:import:version:read"],
async run({ progress, logger }) {
- // First, create the manifest via droplet.
- // This takes up 90% of our progress, so we wrap it in a *0.9
- const manifest = await library.generateDropletManifest(
- game.libraryPath,
- versionPath,
- (err, value) => {
- if (err) throw err;
- progress(value * 0.9);
- },
- (err, value) => {
- if (err) throw err;
- logger.info(value);
- },
- );
+ let versionPath: string | null = null;
+ let manifest;
+ let fileList;
+
+ if (version.type === "local") {
+ versionPath = version.identifier;
+ // First, create the manifest via droplet.
+ // This takes up 90% of our progress, so we wrap it in a *0.9
- logger.info("Created manifest successfully!");
+ manifest = await library.generateDropletManifest(
+ game.libraryPath,
+ versionPath,
+ (err, value) => {
+ if (err) throw err;
+ progress(value * 0.9);
+ },
+ (err, value) => {
+ if (err) throw err;
+ logger.info(value);
+ },
+ );
+ fileList = await library.versionReaddir(
+ game.libraryPath,
+ versionPath,
+ );
+ logger.info("Created manifest successfully!");
+ } else if (version.type === "depot" && unimportedVersion) {
+ manifest = castManifest(unimportedVersion.manifest);
+ fileList = unimportedVersion.fileList;
+ progress(90);
+ } else {
+ throw "Could not find or create manifest for this version.";
+ }
const currentIndex = await prisma.gameVersion.count({
where: { gameId: gameId },
});
// Then, create the database object
- await prisma.gameVersion.create({
+ const newVersion = await prisma.gameVersion.create({
data: {
game: {
connect: {
@@ -301,6 +456,7 @@ class LibraryManager {
versionPath,
dropletManifest: manifest,
+ fileList,
versionIndex: currentIndex,
delta: metadata.delta,
@@ -321,9 +477,13 @@ class LibraryManager {
name: v.name,
command: v.launch,
platform: v.platform,
- ...(v.executorId
- ? { executorId: v.executorId }
+ ...(v.executorId && game.type === "Game"
+ ? {
+ executorId: v.executorId,
+ }
: undefined),
+ executorSuggestions:
+ game.type === "Executor" ? (v.suggestions ?? []) : [],
})),
}
: { data: [] },
@@ -333,17 +493,30 @@ class LibraryManager {
logger.info("Successfully created version!");
notificationSystem.systemPush({
- nonce: `version-create-${gameId}-${versionPath}`,
- title: `'${game.mName}' ('${versionPath}') finished importing.`,
- description: `Drop finished importing version ${versionPath} for ${game.mName}.`,
+ nonce: `version-create-${gameId}-${version}`,
+ title: `'${game.mName}' ('${version}') finished importing.`,
+ description: `Drop finished importing version ${version} for ${game.mName}.`,
actions: [`View|/admin/library/${gameId}`],
acls: ["system:import:version:read"],
});
await libraryManager.cacheCombinedGameSize(gameId);
- await libraryManager.cacheGameVersionSize(gameId, versionPath);
+ await libraryManager.cacheGameVersionSize(gameId, newVersion.versionId);
+
+ await TORRENTIAL_SERVICE.utils().invalidate(
+ gameId,
+ newVersion.versionId,
+ );
- await TORRENTIAL_SERVICE.utils().invalidate(gameId, versionPath);
+ if (version.type === "depot") {
+ // SAFETY: we can only reach this if the type is depot and identifier is valid
+ // eslint-disable-next-line drop/no-prisma-delete
+ await prisma.unimportedGameVersion.delete({
+ where: {
+ id: version.identifier,
+ },
+ });
+ }
progress(100);
},
});
@@ -390,6 +563,20 @@ class LibraryManager {
},
});
await gameSizeManager.deleteGame(gameId);
+ // Delete all game versions that depended on this game
+ await prisma.gameVersion.deleteMany({
+ where: {
+ launches: {
+ some: {
+ executor: {
+ gameVersion: {
+ gameId,
+ },
+ },
+ },
+ },
+ },
+ });
}
async getGameVersionSize(
@@ -421,7 +608,7 @@ class LibraryManager {
await gameSizeManager.cacheCombinedGame(game);
}
- async cacheGameVersionSize(gameId: string, versionName: string) {
+ async cacheGameVersionSize(gameId: string, versionId: string) {
const game = await prisma.game.findFirst({
where: { id: gameId },
include: { versions: true },
@@ -429,7 +616,7 @@ class LibraryManager {
if (!game) {
return;
}
- await gameSizeManager.cacheGameVersion(game, versionName);
+ await gameSizeManager.cacheGameVersion(game, versionId);
}
}
diff --git a/server/internal/library/manifest.ts b/server/internal/library/manifest.ts
index c6f5d06a..63a2c16c 100644
--- a/server/internal/library/manifest.ts
+++ b/server/internal/library/manifest.ts
@@ -1,12 +1,12 @@
import type { JsonValue } from "@prisma/client/runtime/library";
-export type Manifest = V2Manifest;
+export type DropletManifest = V2Manifest;
export type V2Manifest = {
version: "2";
size: number;
key: number[];
- chunks: { [key: string]: V2ChunkData[] };
+ chunks: { [key: string]: V2ChunkData };
};
export type V2ChunkData = {
@@ -22,6 +22,6 @@ export type V2FileEntry = {
permissions: number;
};
-export function castManifest(manifest: JsonValue): Manifest {
- return JSON.parse(manifest as string) as Manifest;
+export function castManifest(manifest: JsonValue): DropletManifest {
+ return JSON.parse(manifest as string) as DropletManifest;
}
diff --git a/server/internal/library/manifest/index.ts b/server/internal/library/manifest/index.ts
new file mode 100644
index 00000000..ed9c1ed7
--- /dev/null
+++ b/server/internal/library/manifest/index.ts
@@ -0,0 +1,100 @@
+import prisma from "../../db/database";
+import { castManifest, type DropletManifest } from "../manifest";
+
+export type DownloadManifestDetails = {
+ manifests: { [key: string]: DropletManifest };
+ fileList: { [key: string]: string };
+};
+
+function convertMap(map: Map): { [key: string]: T } {
+ return Object.fromEntries(map.entries().toArray());
+}
+
+/**
+ *
+ * @param gameId Game ID
+ * @param versionId Version ID
+ */
+export async function createDownloadManifestDetails(
+ versionId: string,
+): Promise {
+ const mainVersion = await prisma.gameVersion.findUnique({
+ where: { versionId },
+ select: {
+ versionId: true,
+ delta: true,
+ versionIndex: true,
+ fileList: true,
+ negativeFileList: true,
+ gameId: true,
+ dropletManifest: true,
+ },
+ });
+ if (!mainVersion)
+ throw createError({ statusCode: 404, message: "Version not found" });
+
+ const collectedVersions = [];
+ let versionIndex = mainVersion.versionIndex;
+ while (true) {
+ const nextVersion = await prisma.gameVersion.findFirst({
+ where: { gameId: mainVersion.gameId, versionIndex: { lt: versionIndex } },
+ orderBy: {
+ versionIndex: "desc",
+ },
+ select: {
+ versionId: true,
+ versionIndex: true,
+ delta: true,
+ fileList: true,
+ negativeFileList: true,
+ dropletManifest: true,
+ },
+ });
+ if (!nextVersion)
+ throw createError({
+ statusCode: 500,
+ message: "Delta version without version underneath it.",
+ });
+
+ versionIndex = nextVersion.versionIndex;
+ collectedVersions.push(nextVersion);
+ if (!nextVersion.delta) break;
+ }
+
+ collectedVersions.reverse();
+ // Apply fileList in lowest priority to newest priority
+ const versionOrder = [...collectedVersions, mainVersion];
+
+ const fileList = new Map();
+ for (const version of versionOrder) {
+ for (const file of version.fileList) {
+ fileList.set(file, version.versionId);
+ }
+ for (const negFile of version.negativeFileList) {
+ fileList.delete(negFile);
+ }
+ }
+
+ // Now that we have our file list, filter the manifests
+ const manifests = new Map();
+ for (const version of versionOrder) {
+ const files = fileList
+ .entries()
+ .filter(([, versionId]) => version.versionId === versionId)
+ .toArray();
+ if (files.length == 0) continue;
+ const fileNames = Object.fromEntries(files);
+ const manifest = castManifest(version.dropletManifest);
+ const filteredChunks = Object.fromEntries(
+ Object.entries(manifest.chunks).filter(([, chunkData]) =>
+ chunkData.files.some((fileEntry) => !!fileNames[fileEntry.filename]),
+ ),
+ );
+ manifests.set(version.versionId, {
+ ...manifest,
+ chunks: filteredChunks,
+ });
+ }
+
+ return { fileList: convertMap(fileList), manifests: convertMap(manifests) };
+}
diff --git a/server/internal/metadata/index.ts b/server/internal/metadata/index.ts
index 34412d04..07bd8fe3 100644
--- a/server/internal/metadata/index.ts
+++ b/server/internal/metadata/index.ts
@@ -1,4 +1,5 @@
import type { Prisma } from "~/prisma/client/client";
+import type { GameType } from "~/prisma/client/enums";
import { MetadataSource } from "~/prisma/client/enums";
import prisma from "../db/database";
import type {
@@ -118,7 +119,11 @@ export class MetadataHandler {
return successfulResults;
}
- async createGameWithoutMetadata(libraryId: string, libraryPath: string) {
+ async createGameWithoutMetadata(
+ libraryId: string,
+ libraryPath: string,
+ type: GameType,
+ ) {
return await this.createGame(
{
id: "",
@@ -127,6 +132,7 @@ export class MetadataHandler {
},
libraryId,
libraryPath,
+ type,
);
}
@@ -174,6 +180,7 @@ export class MetadataHandler {
result: { sourceId: string; id: string; name: string },
libraryId: string,
libraryPath: string,
+ type: GameType,
) {
const provider = this.providers.get(result.sourceId);
if (!provider)
@@ -286,6 +293,8 @@ export class MetadataHandler {
libraryId,
libraryPath,
+
+ type,
},
});
diff --git a/server/internal/system-data/index.ts b/server/internal/system-data/index.ts
index 3347fead..9f61ffd9 100644
--- a/server/internal/system-data/index.ts
+++ b/server/internal/system-data/index.ts
@@ -7,6 +7,33 @@ export type SystemData = {
cpuCores: number;
};
+// See https://github.com/oscmejia/os-utils/blob/master/lib/osutils.js
+function getCPUInfo() {
+ const cpus = os.cpus();
+
+ let user = 0;
+ let nice = 0;
+ let sys = 0;
+ let idle = 0;
+ let irq = 0;
+
+ for (const cpu in cpus) {
+ if (!Object.prototype.hasOwnProperty.call(cpus, cpu)) continue;
+ user += cpus[cpu].times.user;
+ nice += cpus[cpu].times.nice;
+ sys += cpus[cpu].times.sys;
+ irq += cpus[cpu].times.irq;
+ idle += cpus[cpu].times.idle;
+ }
+
+ const total = user + nice + sys + idle + irq;
+
+ return {
+ idle: idle,
+ total: total,
+ };
+}
+
class SystemManager {
// userId to acl to listenerId
private listeners = new Map<
@@ -14,6 +41,20 @@ class SystemManager {
Map void }>
>();
+ private lastCPUUpdate: { idle: number; total: number } | undefined;
+
+ constructor() {
+ setInterval(() => {
+ const systemData = this.getSystemData();
+ if (!systemData) return;
+ for (const [, map] of this.listeners.entries()) {
+ for (const [, { callback }] of map.entries()) {
+ callback(systemData);
+ }
+ }
+ }, 3000);
+ }
+
listen(
userId: string,
id: string,
@@ -22,25 +63,17 @@ class SystemManager {
if (!this.listeners.has(userId)) this.listeners.set(userId, new Map());
// eslint-disable-next-line @typescript-eslint/no-extra-non-null-assertion
this.listeners.get(userId)!!.set(id, { callback });
- this.pushUpdate(userId, id);
- setInterval(() => this.pushUpdate(userId, id), 3000);
}
unlisten(userId: string, id: string) {
this.listeners.get(userId)?.delete(id);
}
- private async pushUpdate(userId: string, id: string) {
- const listener = this.listeners.get(userId)?.get(id);
- if (!listener) {
- throw new Error("Failed to catch-up listener: callback does not exist");
- }
- listener.callback(this.getSystemData());
- }
-
- getSystemData(): SystemData {
+ getSystemData(): SystemData | undefined {
+ const cpu = this.cpuLoad();
+ if (!cpu) return undefined;
return {
- cpuLoad: this.cpuLoad(),
+ cpuLoad: cpu * 100,
totalRam: os.totalmem(),
freeRam: os.freemem(),
cpuCores: os.cpus().length,
@@ -48,9 +81,15 @@ class SystemManager {
}
private cpuLoad() {
- const [oneMinLoad, _fiveMinLoad, _fiftenMinLoad] = os.loadavg();
- const numberCpus = os.cpus().length;
- return 100 - ((numberCpus - oneMinLoad) / numberCpus) * 100;
+ const last = this.lastCPUUpdate;
+ this.lastCPUUpdate = getCPUInfo();
+ if (!last) return undefined;
+
+ const idle = this.lastCPUUpdate.idle - last.idle;
+ const total = this.lastCPUUpdate.total - last.total;
+
+ const perc = idle / total;
+ return 1 - perc;
}
}
diff --git a/torrential b/torrential
index 57f0b9b5..0098bee3 160000
--- a/torrential
+++ b/torrential
@@ -1 +1 @@
-Subproject commit 57f0b9b548b8fe8750e9250c5ee7bfb936764075
+Subproject commit 0098bee3e0d71cf52d58bbc7e7eb633be3d69c9d