Compare commits

...

51 Commits

Author SHA1 Message Date
97083db78d fix: allow overwriting and add caching 2025-08-03 15:59:35 +10:00
ef2153611e fix: use full shared object library (ldd) 2025-08-03 15:49:12 +10:00
48ccf638ce fix: final issues with github pipeline 2025-08-03 15:33:29 +10:00
4361798661 fix: final fixes 2025-08-03 15:21:24 +10:00
732df1ddb9 fix: various ci fixes 2025-08-03 15:03:20 +10:00
4389d8beb8 fix: add fix for aarch64 build and start working on github upload 2025-08-03 14:47:29 +10:00
38dd6a2c64 fix: if statement 2025-08-03 14:32:53 +10:00
6038509a0c fix: add fuse dependency 2025-08-03 14:23:08 +10:00
919148c6c2 fix: only use first library result 2025-08-03 14:13:02 +10:00
b3f73d6d1e fix: add permission fixes 2025-08-03 14:02:37 +10:00
41d23730cc fix: use ldconfig to find libraries 2025-08-03 13:50:55 +10:00
d19c5b8e97 fix: fixes to pipeline, script, and .gitignorey 2025-08-03 13:20:26 +10:00
c78079e4be bump version to 0.3.1-appimage 2025-08-03 12:58:04 +10:00
92e42262c1 Merge branch 'develop' of https://github.com/Beethoven-n/drop-app into Beethoven-n-develop 2025-08-03 12:55:27 +10:00
106c229c8f (build/appimage/drop-app.d/drop-oss-app.desktop) add drop link handling 2025-08-02 21:39:10 -05:00
527476e7c1 (build_appimage.sh) don't use sudo for this u don't need it 2025-08-02 21:39:10 -05:00
81d324a026 (build_appimage.sh) wrong variable. whoops! 2025-08-02 21:39:09 -05:00
3d5de9bec8 (build/appimage/appimagetool) these don't belong here! woops! 2025-08-02 21:39:09 -05:00
380084bb95 (build/appimage/drop-oss-app.d/) 2025-08-02 21:39:09 -05:00
50d76bb76f (build/appimage/appimagetool) 2025-08-02 21:39:09 -05:00
de3be25914 (build_appimage.sh) 2025-08-02 21:39:09 -05:00
95fc7d9605 (.github/workflows/release.yml) add --nobuild flag 2025-08-02 21:39:08 -05:00
f9c45053cc (build/appimage/drop-app.d/AppRun) change program name 2025-08-02 21:39:08 -05:00
683e3ac7f2 (build/appimage/drop-app.d/) rename AppDir
lol woops this caused a typo that cascaded into an appimage that failed
to find the executable
2025-08-02 21:39:08 -05:00
fe586be658 (build_appimage.sh) install libs by UID and GID
installing by $USER caused issues when i did it so i've fixed it
also added $PWD because ./ was lazy
2025-08-02 21:39:08 -05:00
aa034c21a9 (build_appimage.sh) add basic arch detection 2025-08-02 21:39:08 -05:00
50f7955734 (.github/workflows/release.yml) add build_appimage.sh to workflow 2025-08-02 21:39:07 -05:00
73608ab85b (build_appimage.sh) add build script
this should build the appimage without tauri's involvement
2025-08-02 21:39:07 -05:00
1ed29aaa12 (src-tauri/tauri.conf.json) remove appimage target from tauri
tauri was overcomplicating this. let's make the appimage independent of
that
2025-08-02 21:39:07 -05:00
849dc26677 (build/) add build directory for non-tauri builds 2025-08-02 21:39:07 -05:00
7db2e42006 (appimage/build_appimage.sh) 2025-08-02 21:39:07 -05:00
61fa43dd86 (appimage/) add manual appimage
the existing one is broken, and i think it might be tauri's fault.
2025-08-02 21:39:06 -05:00
301152a69f (build_appimage.sh) don't use sudo for this u don't need it 2025-08-02 20:54:08 -05:00
5e945734c7 (build_appimage.sh) wrong variable. whoops! 2025-08-02 20:53:49 -05:00
53211e94fe (build/appimage/appimagetool) these don't belong here! woops! 2025-08-02 20:52:51 -05:00
af49a6751f (build/appimage/drop-oss-app.d/) 2025-08-02 20:27:46 -05:00
9e93ec688c (build/appimage/appimagetool) 2025-08-02 20:27:46 -05:00
a70c2c64e4 (build_appimage.sh) 2025-08-02 20:27:46 -05:00
d63d6efa3e (.github/workflows/release.yml) add --nobuild flag 2025-08-02 20:18:02 -05:00
51555e43ee (build/appimage/drop-app.d/AppRun) change program name 2025-08-02 20:17:13 -05:00
cb7c297443 (build/appimage/drop-app.d/) rename AppDir
lol woops this caused a typo that cascaded into an appimage that failed
to find the executable
2025-08-02 20:16:37 -05:00
4484de0507 (build_appimage.sh) install libs by UID and GID
installing by $USER caused issues when i did it so i've fixed it
also added $PWD because ./ was lazy
2025-08-02 20:15:03 -05:00
4599e5713c (build_appimage.sh) add basic arch detection 2025-08-02 18:05:53 -05:00
263cceb17f (.github/workflows/release.yml) add build_appimage.sh to workflow 2025-08-02 18:05:25 -05:00
0249a3248f (build_appimage.sh) add build script
this should build the appimage without tauri's involvement
2025-08-02 16:45:55 -05:00
bf4b2e0ad6 (src-tauri/tauri.conf.json) remove appimage target from tauri
tauri was overcomplicating this. let's make the appimage independent of
that
2025-08-02 16:45:24 -05:00
7555cc3cdf (build/) add build directory for non-tauri builds 2025-08-02 16:45:24 -05:00
6f30d248eb (appimage/build_appimage.sh) 2025-08-02 14:42:36 -05:00
cc562662f0 (appimage/) add manual appimage
the existing one is broken, and i think it might be tauri's fault.
2025-08-02 14:42:36 -05:00
dbe8c8df4d Process manager templating & game importing (#96)
* feat: add new template options, asahi support, and refactoring

* feat: install dir scanning, validation fixes, progress fixes, download manager refactor

This kind of ballooned out of scope, but I implemented some much
needed fixes for the download manager.

First off, I cleanup the Downloadable trait, there was some
duplication of function.

Second, I refactored the "validate" into the GameDownloadAgent,
which calls a 'validate_chunk_logic' yada, same structure as
downloading.

Third, I fixed the progress and validation issues.

Fourth, I added game scanning

* feat: out of box support for Asahi Linux

* fix: clippy

* fix: don't break database
2025-08-02 20:17:27 +10:00
35f49b8811 macOS app signing (#95)
* feat: add macos signing args

* fix: update all versions to -mac specific

* fix: fetch signing identity

* feat: add signing pre-steps like the docs say

* fix: remove apple requirement from signing

* fix: add drop cert to keychain when signing

* fix: add drop.pem to add-trusted-cert

* fix: re-order and specify import operation

* fix: let's try the user store

* fix: password required to update trust

* fix: try another non-interactive fix

* fix: try sudo

* fix: revert attempt fix

* fix: add cert id debug

* fix: attempt to use id rather than name

* fix: revert code id to name
2025-08-02 15:01:53 +10:00
45 changed files with 894 additions and 484 deletions

View File

@ -25,7 +25,7 @@ jobs:
- platform: 'ubuntu-22.04' # for Tauri v1 you could replace this with ubuntu-20.04.
args: ''
- platform: 'ubuntu-22.04-arm'
args: '--target aarch64-unknown-linux-gnu'
args: ''
- platform: 'windows-latest'
args: ''
@ -40,6 +40,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'yarn'
- name: install Rust nightly
uses: dtolnay/rust-toolchain@nightly
@ -51,19 +52,74 @@ jobs:
if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm' # This must match the platform value defined above.
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf binutils fuse
# webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2.
- name: Import Apple Developer Certificate
if: matrix.platform == 'macos-latest'
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security set-keychain-settings -t 3600 -u build.keychain
curl https://droposs.org/drop.crt --output drop.pem
sudo security authorizationdb write com.apple.trust-settings.admin allow
sudo security add-trusted-cert -d -r trustRoot -k build.keychain -p codeSign -u -1 drop.pem
sudo security authorizationdb remove com.apple.trust-settings.admin
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
security find-identity -v -p codesigning build.keychain
- name: Verify Certificate
if: matrix.platform == 'macos-latest'
run: |
CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Drop OSS")
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
echo "Certificate imported. Using identity: $CERT_ID"
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: './src-tauri -> target'
- name: install frontend dependencies
run: yarn install # change this to npm, pnpm or bun depending on which one you use.
- uses: tauri-apps/tauri-action@v0
- id: build
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
with:
tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version.
tagName: v__VERSION__ # update the below "upload artifacts to GitHub" if this gets updated
releaseName: 'Auto-release v__VERSION__'
releaseBody: 'See the assets to download this version and install. This release was created automatically.'
releaseDraft: false
prerelease: true
args: ${{ matrix.args }}
- name: Bundle AppImage
if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm'
run: |
./build_appimage.sh --nobuild
- name: Upload binaries to release
if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm'
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: build/appimage/*.AppImage
file_glob: true
release_id: ${{ steps.build.outputs.releaseId }}
overwrite: true

11
.gitignore vendored
View File

@ -26,4 +26,13 @@ dist-ssr
.output
src-tauri/flamegraph.svg
src-tauri/perf*
src-tauri/perf*
build/appimage/*
!build/appimage/drop-app.d
build/appimage/drop-app.d/usr/bin/*
!build/appimage/drop-app.d/usr/bin/.gitkeep
build/appimage/drop-app.d/usr/lib/*
!build/appimage/drop-app.d/usr/lib/.gitkeep

View File

@ -27,6 +27,7 @@ try {
console.error("failed to parse state", e);
}
// This is inefficient but apparently we do it lol
router.beforeEach(async () => {
try {
state.value = JSON.parse(await invoke("fetch_state"));

View File

@ -0,0 +1 @@
drop-oss-app.png

View File

@ -0,0 +1,8 @@
#!/bin/sh
program="drop-app"
# point to libraries and run
LD_LIBRARY_PATH=$APPDIR/usr/lib:/usr/local/lib:/usr/lib $APPDIR/usr/bin/$program $@
# vim:ft=sh

View File

@ -0,0 +1,8 @@
[Desktop Entry]
Name=Drop Desktop App
Comment=The client application for the open-source, self-hosted game distribution platform Drop.
Exec=AppRun
Icon=drop-oss-app
Type=Application
Categories=Game;Network
MimeType=x-scheme-handler/drop

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

42
build_appimage.sh Executable file
View File

@ -0,0 +1,42 @@
#!/bin/sh
set -e
# run this from the root of the git repo to make this work
arch="$(uname -m)"
git_dir="$PWD"
target_dir="$git_dir/src-tauri/target"
appimage_dir="$git_dir/build/appimage"
appdir="$appimage_dir/drop-app.d"
build() {
# set up the repo
git submodule init
git submodule update --recursive
# set up yarn and build
yarn
yarn tauri build
}
rm -f $appdir/usr/bin/* $appdir/usr/lib/*
if [[ ! "$1" == "--nobuild" ]]; then
build
fi
# install binaries in the appdir, then the libraries
cp $target_dir/release/drop-app $appdir/usr/bin
for lib_name in $(ldd "$target_dir/release/drop-app" | grep '=>' | awk '{ print $(NF-1) }');
do
echo $lib_name
sudo install -g 1000 -o 1000 -Dm755 "$(ls -L1 $lib_name)" $appdir/usr/lib
done
wget -O $appimage_dir/appimagetool https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-$arch.AppImage
cd $appimage_dir
chmod u+x appimagetool
./appimagetool $appdir
ls "$appimage_dir/"

View File

@ -1,52 +1,78 @@
<template>
<!-- Do not add scale animations to this: https://stackoverflow.com/a/35683068 -->
<div class="inline-flex divide-x divide-zinc-900">
<button type="button" @click="() => buttonActions[props.status.type]()" :class="[
styles[props.status.type],
showDropdown ? 'rounded-l-md' : 'rounded-md',
'inline-flex uppercase font-display items-center gap-x-2 px-4 py-3 text-md font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
]">
<component :is="buttonIcons[props.status.type]" class="-mr-0.5 size-5" aria-hidden="true" />
<button
type="button"
@click="() => buttonActions[props.status.type]()"
:class="[
styles[props.status.type],
showDropdown ? 'rounded-l-md' : 'rounded-md',
'inline-flex uppercase font-display items-center gap-x-2 px-4 py-3 text-md font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
]"
>
<component
:is="buttonIcons[props.status.type]"
class="-mr-0.5 size-5"
aria-hidden="true"
/>
{{ buttonNames[props.status.type] }}
</button>
<Menu v-if="showDropdown" as="div" class="relative inline-block text-left grow">
<Menu
v-if="showDropdown"
as="div"
class="relative inline-block text-left grow"
>
<div class="h-full">
<MenuButton :class="[
styles[props.status.type],
'inline-flex w-full h-full justify-center items-center rounded-r-md px-1 py-2 text-sm font-semibold shadow-sm group',
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
]">
<MenuButton
:class="[
styles[props.status.type],
'inline-flex w-full h-full justify-center items-center rounded-r-md px-1 py-2 text-sm font-semibold shadow-sm group',
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
]"
>
<ChevronDownIcon class="size-5" aria-hidden="true" />
</MenuButton>
</div>
<transition enter-active-class="transition ease-out duration-100" enter-from-class="transform opacity-0 scale-95"
enter-to-class="transform opacity-100 scale-100" leave-active-class="transition ease-in duration-75"
leave-from-class="transform opacity-100 scale-100" leave-to-class="transform opacity-0 scale-95">
<transition
enter-active-class="transition ease-out duration-100"
enter-from-class="transform opacity-0 scale-95"
enter-to-class="transform opacity-100 scale-100"
leave-active-class="transition ease-in duration-75"
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95"
>
<MenuItems
class="absolute right-0 z-[500] mt-2 w-32 origin-top-right rounded-md bg-zinc-900 shadow-lg ring-1 ring-zinc-100/5 focus:outline-none">
class="absolute right-0 z-[500] mt-2 w-32 origin-top-right rounded-md bg-zinc-900 shadow-lg ring-1 ring-zinc-100/5 focus:outline-none"
>
<div class="py-1">
<MenuItem v-slot="{ active }">
<button @click="() => emit('options')" :class="[
active
? 'bg-zinc-800 text-zinc-100 outline-none'
: 'text-zinc-400',
'w-full block px-4 py-2 text-sm inline-flex justify-between',
]">
Options
<Cog6ToothIcon class="size-5" />
</button>
<MenuItem v-if="showOptions" v-slot="{ active }">
<button
@click="() => emit('options')"
:class="[
active
? 'bg-zinc-800 text-zinc-100 outline-none'
: 'text-zinc-400',
'w-full block px-4 py-2 text-sm inline-flex justify-between',
]"
>
Options
<Cog6ToothIcon class="size-5" />
</button>
</MenuItem>
<MenuItem v-slot="{ active }">
<button @click="() => emit('uninstall')" :class="[
active
? 'bg-zinc-800 text-zinc-100 outline-none'
: 'text-zinc-400',
'w-full block px-4 py-2 text-sm inline-flex justify-between',
]">
Uninstall
<TrashIcon class="size-5" />
</button>
<button
@click="() => emit('uninstall')"
:class="[
active
? 'bg-zinc-800 text-zinc-100 outline-none'
: 'text-zinc-400',
'w-full block px-4 py-2 text-sm inline-flex justify-between',
]"
>
Uninstall
<TrashIcon class="size-5" />
</button>
</MenuItem>
</div>
</MenuItems>
@ -61,6 +87,7 @@ import {
ChevronDownIcon,
PlayIcon,
QueueListIcon,
ServerIcon,
StopIcon,
WrenchIcon,
} from "@heroicons/vue/20/solid";
@ -78,7 +105,7 @@ const emit = defineEmits<{
(e: "uninstall"): void;
(e: "kill"): void;
(e: "options"): void;
(e: "resume"): void
(e: "resume"): void;
}>();
const showDropdown = computed(
@ -88,6 +115,10 @@ const showDropdown = computed(
props.status.type === GameStatusEnum.PartiallyInstalled
);
const showOptions = computed(
() => props.status.type === GameStatusEnum.Installed
);
const styles: { [key in GameStatusEnum]: string } = {
[GameStatusEnum.Remote]:
"bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600 hover:bg-blue-500",
@ -95,6 +126,8 @@ const styles: { [key in GameStatusEnum]: string } = {
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
[GameStatusEnum.Downloading]:
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
[GameStatusEnum.Validating]:
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
[GameStatusEnum.SetupRequired]:
"bg-yellow-600 text-white hover:bg-yellow-500 focus-visible:outline-yellow-600 hover:bg-yellow-500",
[GameStatusEnum.Installed]:
@ -106,42 +139,45 @@ const styles: { [key in GameStatusEnum]: string } = {
[GameStatusEnum.Running]:
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
[GameStatusEnum.PartiallyInstalled]:
"bg-gray-600 text-white hover:bg-gray-500 focus-visible:outline-gray-600 hover:bg-gray-500"
"bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600 hover:bg-blue-500",
};
const buttonNames: { [key in GameStatusEnum]: string } = {
[GameStatusEnum.Remote]: "Install",
[GameStatusEnum.Queued]: "Queued",
[GameStatusEnum.Downloading]: "Downloading",
[GameStatusEnum.Validating]: "Validating",
[GameStatusEnum.SetupRequired]: "Setup",
[GameStatusEnum.Installed]: "Play",
[GameStatusEnum.Updating]: "Updating",
[GameStatusEnum.Uninstalling]: "Uninstalling",
[GameStatusEnum.Running]: "Stop",
[GameStatusEnum.PartiallyInstalled]: "Resume"
[GameStatusEnum.PartiallyInstalled]: "Resume",
};
const buttonIcons: { [key in GameStatusEnum]: Component } = {
[GameStatusEnum.Remote]: ArrowDownTrayIcon,
[GameStatusEnum.Queued]: QueueListIcon,
[GameStatusEnum.Downloading]: ArrowDownTrayIcon,
[GameStatusEnum.Validating]: ServerIcon,
[GameStatusEnum.SetupRequired]: WrenchIcon,
[GameStatusEnum.Installed]: PlayIcon,
[GameStatusEnum.Updating]: ArrowDownTrayIcon,
[GameStatusEnum.Uninstalling]: TrashIcon,
[GameStatusEnum.Running]: StopIcon,
[GameStatusEnum.PartiallyInstalled]: ArrowDownTrayIcon
[GameStatusEnum.PartiallyInstalled]: ArrowDownTrayIcon,
};
const buttonActions: { [key in GameStatusEnum]: () => void } = {
[GameStatusEnum.Remote]: () => emit("install"),
[GameStatusEnum.Queued]: () => emit("queue"),
[GameStatusEnum.Downloading]: () => emit("queue"),
[GameStatusEnum.Validating]: () => emit("queue"),
[GameStatusEnum.SetupRequired]: () => emit("launch"),
[GameStatusEnum.Installed]: () => emit("launch"),
[GameStatusEnum.Updating]: () => emit("queue"),
[GameStatusEnum.Uninstalling]: () => { },
[GameStatusEnum.Uninstalling]: () => {},
[GameStatusEnum.Running]: () => emit("kill"),
[GameStatusEnum.PartiallyInstalled]: () => emit("resume")
[GameStatusEnum.PartiallyInstalled]: () => emit("resume"),
};
</script>

View File

@ -81,18 +81,20 @@ import { listen } from "@tauri-apps/api/event";
const gameStatusTextStyle: { [key in GameStatusEnum]: string } = {
[GameStatusEnum.Installed]: "text-green-500",
[GameStatusEnum.Downloading]: "text-blue-500",
[GameStatusEnum.Validating]: "text-blue-300",
[GameStatusEnum.Running]: "text-green-500",
[GameStatusEnum.Remote]: "text-zinc-500",
[GameStatusEnum.Queued]: "text-blue-500",
[GameStatusEnum.Updating]: "text-blue-500",
[GameStatusEnum.Uninstalling]: "text-zinc-100",
[GameStatusEnum.SetupRequired]: "text-yellow-500",
[GameStatusEnum.PartiallyInstalled]: "text-gray-600",
[GameStatusEnum.PartiallyInstalled]: "text-gray-400",
};
const gameStatusText: { [key in GameStatusEnum]: string } = {
[GameStatusEnum.Remote]: "Not installed",
[GameStatusEnum.Queued]: "Queued",
[GameStatusEnum.Downloading]: "Downloading...",
[GameStatusEnum.Validating]: "Validating...",
[GameStatusEnum.Installed]: "Installed",
[GameStatusEnum.Updating]: "Updating...",
[GameStatusEnum.Uninstalling]: "Uninstalling...",

View File

@ -1,7 +1,7 @@
{
"name": "drop-app",
"private": true,
"version": "0.3.1",
"version": "0.3.1-appimage",
"type": "module",
"scripts": {
"build": "nuxt build",

View File

@ -368,6 +368,13 @@
</template>
</ModalTemplate>
<!--
Dear future DecDuck,
This v-if is necessary for Vue rendering reasons
(it tries to access the game version for not installed games)
You have already tried to remove it
Don't.
-->
<GameOptionsModal
v-if="status.type === GameStatusEnum.Installed"
v-model="configureModalOpen"

13
src-tauri/Cargo.lock generated
View File

@ -1284,7 +1284,7 @@ dependencies = [
[[package]]
name = "drop-app"
version = "0.3.1"
version = "0.3.1-appimage"
dependencies = [
"atomic-instant-full",
"bitcode",
@ -1306,6 +1306,7 @@ dependencies = [
"log4rs",
"md5",
"native_model",
"page_size",
"parking_lot 0.12.3",
"rand 0.9.1",
"rayon",
@ -3523,6 +3524,16 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "page_size"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "pango"
version = "0.18.3"

View File

@ -1,6 +1,6 @@
[package]
name = "drop-app"
version = "0.3.1"
version = "0.3.1-appimage"
description = "The client application for the open-source, self-hosted game distribution platform Drop"
authors = ["Drop OSS"]
edition = "2024"
@ -70,6 +70,7 @@ tauri-plugin-opener = "2.4.0"
bitcode = "0.6.6"
reqwest-websocket = "0.5.0"
futures-lite = "2.6.0"
page_size = "0.6.0"
# tailscale = { path = "./tailscale" }
[dependencies.dynfmt]

View File

@ -7,7 +7,7 @@ use std::{
use serde_json::Value;
use crate::{
database::db::borrow_db_mut_checked, error::download_manager_error::DownloadManagerError,
database::{db::borrow_db_mut_checked, scan::scan_install_dirs}, error::download_manager_error::DownloadManagerError,
};
use super::{
@ -59,6 +59,8 @@ pub fn add_download_dir(new_dir: PathBuf) -> Result<(), DownloadManagerError<()>
lock.applications.install_dirs.push(new_dir);
drop(lock);
scan_install_dirs();
Ok(())
}

View File

@ -10,7 +10,7 @@ use chrono::Utc;
use log::{debug, error, info, warn};
use native_model::{Decode, Encode};
use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
use serde::{de::DeserializeOwned, Serialize};
use serde::{Serialize, de::DeserializeOwned};
use url::Url;
use crate::DB;
@ -67,17 +67,18 @@ impl DatabaseImpls for DatabaseInterface {
let exists = fs::exists(db_path.clone()).unwrap();
if exists { match PathDatabase::load_from_path(db_path.clone()) {
Ok(db) => db,
Err(e) => handle_invalid_database(e, db_path, games_base_dir, cache_dir),
} } else {
if exists {
match PathDatabase::load_from_path(db_path.clone()) {
Ok(db) => db,
Err(e) => handle_invalid_database(e, db_path, games_base_dir, cache_dir),
}
} else {
let default = Database::new(games_base_dir, None, cache_dir);
debug!(
"Creating database at path {}",
db_path.as_os_str().to_str().unwrap()
);
PathDatabase::create_at_path(db_path, default)
.expect("Database could not be created")
PathDatabase::create_at_path(db_path, default).expect("Database could not be created")
}
}
@ -121,24 +122,24 @@ fn handle_invalid_database(
pub struct DBRead<'a>(RwLockReadGuard<'a, Database>);
pub struct DBWrite<'a>(ManuallyDrop<RwLockWriteGuard<'a, Database>>);
impl<'a> Deref for DBWrite<'a> {
type Target = RwLockWriteGuard<'a, Database>;
type Target = Database;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> DerefMut for DBWrite<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<'a> Deref for DBRead<'a> {
type Target = RwLockReadGuard<'a, Database>;
type Target = Database;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for DBWrite<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Drop for DBWrite<'_> {
fn drop(&mut self) {
unsafe {
@ -154,6 +155,7 @@ impl Drop for DBWrite<'_> {
}
}
}
pub fn borrow_db_checked<'a>() -> DBRead<'a> {
match DB.borrow_data() {
Ok(data) => DBRead(data),

View File

@ -2,3 +2,4 @@ pub mod commands;
pub mod db;
pub mod debug;
pub mod models;
pub mod scan;

View File

@ -1,8 +1,14 @@
use crate::database::models::data::Database;
/**
* NEXT BREAKING CHANGE
*
* UPDATE DATABASE TO USE RPMSERDENAMED
*
* WE CAN'T DELETE ANY FIELDS
*/
pub mod data {
use std::path::PathBuf;
use native_model::native_model;
use serde::{Deserialize, Serialize};
@ -18,16 +24,14 @@ pub mod data {
pub type DatabaseApplications = v2::DatabaseApplications;
pub type DatabaseCompatInfo = v2::DatabaseCompatInfo;
use std::{collections::HashMap, process::Command};
use crate::process::process_manager::UMU_LAUNCHER_EXECUTABLE;
use std::collections::HashMap;
pub mod v1 {
use crate::process::process_manager::Platform;
use serde_with::serde_as;
use std::{collections::HashMap, path::PathBuf};
use super::{Serialize, Deserialize, native_model};
use super::{Deserialize, Serialize, native_model};
fn default_template() -> String {
"{}".to_owned()
@ -115,6 +119,7 @@ pub mod data {
Downloading { version_name: String },
Uninstalling {},
Updating { version_name: String },
Validating { version_name: String },
Running {},
}
@ -174,7 +179,10 @@ pub mod data {
use serde_with::serde_as;
use super::{Serialize, Deserialize, native_model, Settings, DatabaseAuth, v1, GameVersion, DownloadableMetadata, ApplicationTransientStatus};
use super::{
ApplicationTransientStatus, DatabaseAuth, Deserialize, DownloadableMetadata,
GameVersion, Serialize, Settings, native_model, v1,
};
#[native_model(id = 1, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Serialize, Deserialize, Clone, Default)]
@ -206,7 +214,7 @@ pub mod data {
applications: value.applications,
prev_database: value.prev_database,
cache_dir: value.cache_dir,
compat_info: crate::database::models::Database::create_new_compat_info(),
compat_info: None,
}
}
}
@ -283,7 +291,10 @@ pub mod data {
mod v3 {
use std::path::PathBuf;
use super::{Serialize, Deserialize, native_model, Settings, DatabaseAuth, DatabaseApplications, DatabaseCompatInfo, v2};
use super::{
DatabaseApplications, DatabaseAuth, DatabaseCompatInfo, Deserialize, Serialize,
Settings, native_model, v2,
};
#[native_model(id = 1, version = 3, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Database {
@ -297,6 +308,7 @@ pub mod data {
pub cache_dir: PathBuf,
pub compat_info: Option<DatabaseCompatInfo>,
}
impl From<v2::Database> for Database {
fn from(value: v2::Database) -> Self {
Self {
@ -306,22 +318,13 @@ pub mod data {
applications: value.applications.into(),
prev_database: value.prev_database,
cache_dir: value.cache_dir,
compat_info: Database::create_new_compat_info(),
compat_info: None,
}
}
}
}
impl Database {
fn create_new_compat_info() -> Option<DatabaseCompatInfo> {
#[cfg(target_os = "windows")]
return None;
let has_umu_installed = Command::new(UMU_LAUNCHER_EXECUTABLE).spawn().is_ok();
Some(DatabaseCompatInfo {
umu_installed: has_umu_installed,
})
}
pub fn new<T: Into<PathBuf>>(
games_base_dir: T,
prev_database: Option<PathBuf>,
@ -340,7 +343,7 @@ pub mod data {
auth: None,
settings: Settings::default(),
cache_dir,
compat_info: Database::create_new_compat_info(),
compat_info: None,
}
}
}

View File

@ -0,0 +1,52 @@
use std::fs;
use log::warn;
use crate::{
database::{
db::borrow_db_mut_checked,
models::data::v1::{DownloadType, DownloadableMetadata},
},
games::{
downloads::drop_data::{v1::DropData, DROP_DATA_PATH},
library::set_partially_installed_db,
},
};
pub fn scan_install_dirs() {
let mut db_lock = borrow_db_mut_checked();
for install_dir in db_lock.applications.install_dirs.clone() {
let Ok(files) = fs::read_dir(install_dir) else {
continue;
};
for game in files.into_iter().flatten() {
let drop_data_file = game.path().join(DROP_DATA_PATH);
if !drop_data_file.exists() {
continue;
}
let game_id = game.file_name().into_string().unwrap();
let Ok(drop_data) = DropData::read(&game.path()) else {
warn!(
".dropdata exists for {}, but couldn't read it. is it corrupted?",
game.file_name().into_string().unwrap()
);
continue;
};
if db_lock.applications.game_statuses.contains_key(&game_id) {
continue;
}
let metadata = DownloadableMetadata::new(
drop_data.game_id,
Some(drop_data.game_version),
DownloadType::Game,
);
set_partially_installed_db(
&mut db_lock,
&metadata,
drop_data.base_path.to_str().unwrap().to_string(),
None,
);
}
}
}

View File

@ -254,13 +254,13 @@ impl DownloadManagerBuilder {
}
};
// If the download gets cancel
// If the download gets cancelled
// immediately return, on_cancelled gets called for us earlier
if !download_result {
download_agent.on_incomplete(&app_handle);
return;
}
let validate_result = match download_agent.validate() {
let validate_result = match download_agent.validate(&app_handle) {
Ok(v) => v,
Err(e) => {
error!(

View File

@ -14,14 +14,14 @@ use super::{
pub trait Downloadable: Send + Sync {
fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>;
fn validate(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>;
fn progress(&self) -> Arc<ProgressObject>;
fn control_flag(&self) -> DownloadThreadControl;
fn validate(&self) -> Result<bool, ApplicationDownloadError>;
fn status(&self) -> DownloadStatus;
fn metadata(&self) -> DownloadableMetadata;
fn on_initialised(&self, app_handle: &AppHandle);
fn on_error(&self, app_handle: &AppHandle, error: &ApplicationDownloadError);
fn on_complete(&self, app_handle: &AppHandle);
fn on_incomplete(&self, app_handle: &AppHandle);
fn on_cancelled(&self, app_handle: &AppHandle);
}

View File

@ -38,9 +38,9 @@ impl DownloadThreadControl {
}
}
pub fn get(&self) -> DownloadThreadControlFlag {
self.inner.load(Ordering::Relaxed).into()
self.inner.load(Ordering::Acquire).into()
}
pub fn set(&self, flag: DownloadThreadControlFlag) {
self.inner.store(flag.into(), Ordering::Relaxed);
self.inner.store(flag.into(), Ordering::Release);
}
}

View File

@ -39,20 +39,20 @@ impl ProgressHandle {
}
}
pub fn set(&self, amount: usize) {
self.progress.store(amount, Ordering::Relaxed);
self.progress.store(amount, Ordering::Release);
}
pub fn add(&self, amount: usize) {
self.progress
.fetch_add(amount, std::sync::atomic::Ordering::Relaxed);
.fetch_add(amount, std::sync::atomic::Ordering::AcqRel);
calculate_update(&self.progress_object);
}
pub fn skip(&self, amount: usize) {
self.progress
.fetch_add(amount, std::sync::atomic::Ordering::Relaxed);
.fetch_add(amount, std::sync::atomic::Ordering::Acquire);
// Offset the bytes at last offset by this amount
self.progress_object
.bytes_last_update
.fetch_add(amount, Ordering::Relaxed);
.fetch_add(amount, Ordering::Acquire);
// Dont' fire update
}
}
@ -60,7 +60,6 @@ impl ProgressHandle {
impl ProgressObject {
pub fn new(max: usize, length: usize, sender: Sender<DownloadManagerSignal>) -> Self {
let arr = Mutex::new((0..length).map(|_| Arc::new(AtomicUsize::new(0))).collect());
// TODO: consolidate this calculation with the set_max function below
Self {
max: Arc::new(Mutex::new(max)),
progress_instances: Arc::new(arr),
@ -81,19 +80,18 @@ impl ProgressObject {
.lock()
.unwrap()
.iter()
.map(|instance| instance.load(Ordering::Relaxed))
.map(|instance| instance.load(Ordering::Acquire))
.sum()
}
pub fn reset(&self, size: usize) {
pub fn reset(&self) {
self.set_time_now();
self.set_size(size);
self.bytes_last_update.store(0, Ordering::Release);
self.rolling.reset();
self.progress_instances
.lock()
.unwrap()
.iter()
.for_each(|x| x.store(0, Ordering::Release));
.for_each(|x| x.store(0, Ordering::SeqCst));
}
pub fn get_max(&self) -> usize {
*self.max.lock().unwrap()
@ -127,7 +125,7 @@ pub fn calculate_update(progress: &ProgressObject) {
let max = progress.get_max();
let bytes_at_last_update = progress
.bytes_last_update
.swap(current_bytes_downloaded, Ordering::Relaxed);
.swap(current_bytes_downloaded, Ordering::Acquire);
let bytes_since_last_update = current_bytes_downloaded - bytes_at_last_update;

View File

@ -26,7 +26,7 @@ impl<const S: usize> RollingProgressWindow<S> {
.iter()
.enumerate()
.filter(|(i, _)| i < &current)
.map(|(_, x)| x.load(Ordering::Relaxed))
.map(|(_, x)| x.load(Ordering::Acquire))
.sum::<usize>()
/ S
}

View File

@ -6,7 +6,6 @@ use serde_with::SerializeDisplay;
pub enum ProcessError {
NotInstalled,
AlreadyRunning,
NotDownloaded,
InvalidID,
InvalidVersion,
IOError(Error),
@ -20,7 +19,6 @@ impl Display for ProcessError {
let s = match self {
ProcessError::NotInstalled => "Game not installed",
ProcessError::AlreadyRunning => "Game already running",
ProcessError::NotDownloaded => "Game not downloaded",
ProcessError::InvalidID => "Invalid game ID",
ProcessError::InvalidVersion => "Invalid game version",
ProcessError::IOError(error) => &error.to_string(),

View File

@ -3,19 +3,23 @@ use std::sync::Mutex;
use tauri::AppHandle;
use crate::{
database::models::data::GameVersion,
AppState,
database::{
db::borrow_db_checked,
models::data::GameVersion,
},
error::{library_error::LibraryError, remote_access_error::RemoteAccessError},
games::library::{
fetch_game_logic_offline, fetch_library_logic_offline, get_current_meta,
uninstall_game_logic,
},
offline, AppState,
offline,
};
use super::{
library::{
fetch_game_logic, fetch_game_verion_options_logic, fetch_library_logic, FetchGameStruct,
Game,
FetchGameStruct, Game, fetch_game_logic, fetch_game_verion_options_logic,
fetch_library_logic,
},
state::{GameStatusManager, GameStatusWithTransient},
};
@ -48,7 +52,8 @@ pub fn fetch_game(
#[tauri::command]
pub fn fetch_game_status(id: String) -> GameStatusWithTransient {
GameStatusManager::fetch_state(&id)
let db_handle = borrow_db_checked();
GameStatusManager::fetch_state(&id, &db_handle)
}
#[tauri::command]

View File

@ -12,8 +12,11 @@ use crate::download_manager::util::progress_object::{ProgressHandle, ProgressObj
use crate::error::application_download_error::ApplicationDownloadError;
use crate::error::remote_access_error::RemoteAccessError;
use crate::games::downloads::manifest::{DropDownloadContext, DropManifest};
use crate::games::downloads::validate::game_validate_logic;
use crate::games::library::{on_game_complete, on_game_incomplete, push_game_update};
use crate::games::downloads::validate::validate_game_chunk;
use crate::games::library::{
on_game_complete, push_game_update, set_partially_installed,
};
use crate::games::state::GameStatusManager;
use crate::remote::requests::make_request;
use crate::remote::utils::DROP_CLIENT_SYNC;
use log::{debug, error, info};
@ -41,7 +44,7 @@ pub struct GameDownloadAgent {
pub manifest: Mutex<Option<DropManifest>>,
pub progress: Arc<ProgressObject>,
sender: Sender<DownloadManagerSignal>,
pub stored_manifest: DropData,
pub dropdata: DropData,
status: Mutex<DownloadStatus>,
}
@ -82,38 +85,43 @@ impl GameDownloadAgent {
context_map: Mutex::new(HashMap::new()),
progress: Arc::new(ProgressObject::new(0, 0, sender.clone())),
sender,
stored_manifest,
dropdata: stored_manifest,
status: Mutex::new(DownloadStatus::Queued),
}
}
// Blocking
pub fn setup_download(&self) -> Result<(), ApplicationDownloadError> {
pub fn setup_download(&self, app_handle: &AppHandle) -> Result<(), ApplicationDownloadError> {
self.ensure_manifest_exists()?;
self.ensure_contexts()?;
self.control_flag.set(DownloadThreadControlFlag::Go);
let mut db_lock = borrow_db_mut_checked();
db_lock.applications.transient_statuses.insert(
self.metadata(),
ApplicationTransientStatus::Downloading {
version_name: self.version.clone(),
},
);
push_game_update(
app_handle,
&self.metadata().id,
None,
GameStatusManager::fetch_state(&self.metadata().id, &db_lock),
);
Ok(())
}
// Blocking
pub fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
self.setup_download()?;
self.set_progress_object_params();
self.setup_download(app_handle)?;
let timer = Instant::now();
push_game_update(
app_handle,
&self.metadata().id,
None,
(
None,
Some(ApplicationTransientStatus::Downloading {
version_name: self.version.clone(),
}),
),
);
info!("beginning download for {}...", self.metadata().id);
let res = self
.run()
.map_err(|()| ApplicationDownloadError::DownloadError);
@ -166,12 +174,8 @@ impl GameDownloadAgent {
Err(ApplicationDownloadError::Lock)
}
fn set_progress_object_params(&self) {
// Avoid re-setting it
if self.progress.get_max() != 0 {
return;
}
// Sets it up for both download and validate
fn setup_progress(&self) {
let contexts = self.contexts.lock().unwrap();
let length = contexts.len();
@ -180,7 +184,7 @@ impl GameDownloadAgent {
self.progress.set_max(chunk_count);
self.progress.set_size(length);
self.progress.set_time_now();
self.progress.reset();
}
pub fn ensure_contexts(&self) -> Result<(), ApplicationDownloadError> {
@ -188,7 +192,7 @@ impl GameDownloadAgent {
self.generate_contexts()?;
}
*self.context_map.lock().unwrap() = self.stored_manifest.get_contexts();
*self.context_map.lock().unwrap() = self.dropdata.get_contexts();
Ok(())
}
@ -198,7 +202,7 @@ impl GameDownloadAgent {
let game_id = self.id.clone();
let mut contexts = Vec::new();
let base_path = Path::new(&self.stored_manifest.base_path);
let base_path = Path::new(&self.dropdata.base_path);
create_dir_all(base_path).unwrap();
for (raw_path, chunk) in manifest {
@ -207,11 +211,12 @@ impl GameDownloadAgent {
let container = path.parent().unwrap();
create_dir_all(container).unwrap();
let already_exists = path.exists();
let file = OpenOptions::new()
.read(true)
.write(true)
.truncate(true)
.create(true)
.truncate(false)
.open(path.clone())
.unwrap();
let mut running_offset = 0;
@ -232,12 +237,12 @@ impl GameDownloadAgent {
}
#[cfg(target_os = "linux")]
if running_offset > 0 {
if running_offset > 0 && !already_exists {
let _ = fallocate(file, FallocateFlags::empty(), 0, running_offset);
}
}
let existing_contexts = self.stored_manifest.get_completed_contexts();
self.stored_manifest.set_contexts(
let existing_contexts = self.dropdata.get_completed_contexts();
self.dropdata.set_contexts(
&contexts
.iter()
.map(|x| (x.checksum.clone(), existing_contexts.contains(&x.checksum)))
@ -249,8 +254,8 @@ impl GameDownloadAgent {
Ok(())
}
// TODO: Change return value on Err
pub fn run(&self) -> Result<bool, ()> {
fn run(&self) -> Result<bool, ()> {
self.setup_progress();
let max_download_threads = borrow_db_checked().settings.max_download_threads;
debug!(
@ -266,7 +271,6 @@ impl GameDownloadAgent {
let completed_indexes_loop_arc = completed_contexts.clone();
let contexts = self.contexts.lock().unwrap();
debug!("{contexts:#?}");
pool.scope(|scope| {
let client = &DROP_CLIENT_SYNC.clone();
let context_map = self.context_map.lock().unwrap();
@ -278,7 +282,10 @@ impl GameDownloadAgent {
let progress_handle = ProgressHandle::new(progress, self.progress.clone());
// If we've done this one already, skip it
if Some(&true) == context_map.get(&context.checksum) {
// Note to future DecDuck, DropData gets loaded into context_map
if let Some(v) = context_map.get(&context.checksum)
&& *v
{
progress_handle.skip(context.length);
continue;
}
@ -345,8 +352,8 @@ impl GameDownloadAgent {
.collect::<Vec<(String, bool)>>();
drop(context_map_lock);
self.stored_manifest.set_contexts(&contexts);
self.stored_manifest.write();
self.dropdata.set_contexts(&contexts);
self.dropdata.write();
// If there are any contexts left which are false
if !contexts.iter().all(|x| x.1) {
@ -361,6 +368,93 @@ impl GameDownloadAgent {
Ok(true)
}
fn setup_validate(&self, app_handle: &AppHandle) {
self.setup_progress();
self.control_flag.set(DownloadThreadControlFlag::Go);
let mut db_lock = borrow_db_mut_checked();
db_lock.applications.transient_statuses.insert(
self.metadata(),
ApplicationTransientStatus::Validating {
version_name: self.version.clone(),
},
);
push_game_update(
app_handle,
&self.metadata().id,
None,
GameStatusManager::fetch_state(&self.metadata().id, &db_lock),
);
}
pub fn validate(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
self.setup_validate(app_handle);
let contexts = self.contexts.lock().unwrap();
let max_download_threads = borrow_db_checked().settings.max_download_threads;
debug!(
"validating game: {} with {} threads",
self.dropdata.game_id, max_download_threads
);
let pool = ThreadPoolBuilder::new()
.num_threads(max_download_threads)
.build()
.unwrap();
let invalid_chunks = Arc::new(boxcar::Vec::new());
pool.scope(|scope| {
for (index, context) in contexts.iter().enumerate() {
let current_progress = self.progress.get(index);
let progress_handle = ProgressHandle::new(current_progress, self.progress.clone());
let invalid_chunks_scoped = invalid_chunks.clone();
let sender = self.sender.clone();
scope.spawn(move |_| {
match validate_game_chunk(context, &self.control_flag, progress_handle) {
Ok(true) => {}
Ok(false) => {
invalid_chunks_scoped.push(context.checksum.clone());
}
Err(e) => {
error!("{e}");
sender.send(DownloadManagerSignal::Error(e)).unwrap();
}
}
});
}
});
// If there are any contexts left which are false
if !invalid_chunks.is_empty() {
info!("validation of game id {} failed", self.id);
for context in invalid_chunks.iter() {
self.dropdata.set_context(context.1.clone(), false);
}
self.dropdata.write();
return Ok(false);
}
Ok(true)
}
pub fn cancel(&self, app_handle: &AppHandle) {
// See docs on usage
set_partially_installed(
&self.metadata(),
self.dropdata.base_path.to_str().unwrap().to_string(),
Some(app_handle),
);
self.dropdata.write();
}
}
impl Downloadable for GameDownloadAgent {
@ -369,6 +463,11 @@ impl Downloadable for GameDownloadAgent {
self.download(app_handle)
}
fn validate(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
*self.status.lock().unwrap() = DownloadStatus::Validating;
self.validate(app_handle)
}
fn progress(&self) -> Arc<ProgressObject> {
self.progress.clone()
}
@ -407,37 +506,25 @@ impl Downloadable for GameDownloadAgent {
fn on_complete(&self, app_handle: &tauri::AppHandle) {
on_game_complete(
&self.metadata(),
self.stored_manifest.base_path.to_string_lossy().to_string(),
self.dropdata.base_path.to_string_lossy().to_string(),
app_handle,
)
.unwrap();
}
// TODO: fix this function. It doesn't restart the download properly, nor does it reset the state properly
fn on_incomplete(&self, app_handle: &tauri::AppHandle) {
on_game_incomplete(
&self.metadata(),
self.stored_manifest.base_path.to_string_lossy().to_string(),
app_handle,
)
.unwrap();
println!("Attempting to redownload");
fn on_cancelled(&self, app_handle: &tauri::AppHandle) {
self.cancel(app_handle);
/*
on_game_incomplete(
&self.metadata(),
self.dropdata.base_path.to_string_lossy().to_string(),
app_handle,
)
.unwrap();
*/
}
fn on_cancelled(&self, _app_handle: &tauri::AppHandle) {}
fn status(&self) -> DownloadStatus {
self.status.lock().unwrap().clone()
}
fn validate(&self) -> Result<bool, ApplicationDownloadError> {
*self.status.lock().unwrap() = DownloadStatus::Validating;
game_validate_logic(
&self.stored_manifest,
self.contexts.lock().unwrap().clone(),
self.progress.clone(),
self.sender.clone(),
&self.control_flag,
)
}
}

View File

@ -1,13 +1,13 @@
use std::{
collections::HashMap, fs::File, io::{Read, Write}, path::PathBuf
collections::HashMap, fs::File, io::{self, Read, Write}, path::{Path, PathBuf}
};
use log::{debug, error, info, warn};
use log::error;
use native_model::{Decode, Encode};
pub type DropData = v1::DropData;
static DROP_DATA_PATH: &str = ".dropdata";
pub static DROP_DATA_PATH: &str = ".dropdata";
pub mod v1 {
use std::{collections::HashMap, path::PathBuf, sync::Mutex};
@ -38,27 +38,18 @@ pub mod v1 {
impl DropData {
pub fn generate(game_id: String, game_version: String, base_path: PathBuf) -> Self {
let mut file = if let Ok(file) = File::open(base_path.join(DROP_DATA_PATH)) { file } else {
debug!("Generating new dropdata for game {game_id}");
return DropData::new(game_id, game_version, base_path);
};
match DropData::read(&base_path) {
Ok(v) => v,
Err(_) => DropData::new(game_id, game_version, base_path),
}
}
pub fn read(base_path: &Path) -> Result<Self, io::Error> {
let mut file = File::open(base_path.join(DROP_DATA_PATH))?;
let mut s = Vec::new();
match file.read_to_end(&mut s) {
Ok(_) => {}
Err(e) => {
error!("{e}");
return DropData::new(game_id, game_version, base_path);
}
}
file.read_to_end(&mut s)?;
match native_model::rmp_serde_1_3::RmpSerde::decode(s) {
Ok(manifest) => manifest,
Err(e) => {
warn!("{e}");
DropData::new(game_id, game_version, base_path)
}
}
Ok(native_model::rmp_serde_1_3::RmpSerde::decode(s).unwrap())
}
pub fn write(&self) {
let manifest_raw = match native_model::rmp_serde_1_3::RmpSerde::encode(&self) {
@ -94,10 +85,6 @@ impl DropData {
.collect()
}
pub fn get_contexts(&self) -> HashMap<String, bool> {
info!(
"Any contexts which are complete? {}",
self.contexts.lock().unwrap().iter().any(|x| *x.1)
);
self.contexts.lock().unwrap().clone()
}
}

View File

@ -1,6 +1,6 @@
pub mod commands;
pub mod download_agent;
mod download_logic;
mod drop_data;
pub mod drop_data;
mod manifest;
pub mod validate;

View File

@ -1,99 +1,22 @@
use std::{
fs::File,
io::{self, BufWriter, Read, Seek, SeekFrom, Write},
sync::{Arc, mpsc::Sender},
};
use log::{debug, error, info};
use log::debug;
use md5::Context;
use rayon::ThreadPoolBuilder;
use crate::{
database::db::borrow_db_checked,
download_manager::{
download_manager_frontend::DownloadManagerSignal,
download_manager::
util::{
download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
progress_object::{ProgressHandle, ProgressObject},
},
},
progress_object::ProgressHandle,
}
,
error::application_download_error::ApplicationDownloadError,
games::downloads::{drop_data::DropData, manifest::DropDownloadContext},
games::downloads::manifest::DropDownloadContext,
};
pub fn game_validate_logic(
dropdata: &DropData,
contexts: Vec<DropDownloadContext>,
progress: Arc<ProgressObject>,
sender: Sender<DownloadManagerSignal>,
control_flag: &DownloadThreadControl,
) -> Result<bool, ApplicationDownloadError> {
progress.reset(contexts.len());
let max_download_threads = borrow_db_checked().settings.max_download_threads;
debug!(
"validating game: {} with {} threads",
dropdata.game_id, max_download_threads
);
let pool = ThreadPoolBuilder::new()
.num_threads(max_download_threads)
.build()
.unwrap();
debug!("{contexts:#?}");
let invalid_chunks = Arc::new(boxcar::Vec::new());
pool.scope(|scope| {
for (index, context) in contexts.iter().enumerate() {
let current_progress = progress.get(index);
let progress_handle = ProgressHandle::new(current_progress, progress.clone());
let invalid_chunks_scoped = invalid_chunks.clone();
let sender = sender.clone();
scope.spawn(move |_| {
match validate_game_chunk(context, control_flag, progress_handle) {
Ok(true) => {
debug!(
"Finished context #{} with checksum {}",
index, context.checksum
);
}
Ok(false) => {
debug!(
"Didn't finish context #{} with checksum {}",
index, &context.checksum
);
invalid_chunks_scoped.push(context.checksum.clone());
}
Err(e) => {
error!("{e}");
sender.send(DownloadManagerSignal::Error(e)).unwrap();
}
}
});
}
});
// If there are any contexts left which are false
if !invalid_chunks.is_empty() {
info!(
"validation of game id {} failed for chunks {:?}",
dropdata.game_id.clone(),
invalid_chunks
);
for context in invalid_chunks.iter() {
dropdata.set_context(context.1.clone(), false);
}
dropdata.write();
return Ok(false);
}
Ok(true)
}
pub fn validate_game_chunk(
ctx: &DropDownloadContext,
control_flag: &DownloadThreadControl,
@ -129,10 +52,6 @@ pub fn validate_game_chunk(
let res = hex::encode(hasher.compute().0);
if res != ctx.checksum {
println!(
"Checksum failed. Correct: {}, actual: {}",
&ctx.checksum, &res
);
return Ok(false);
}

View File

@ -7,7 +7,9 @@ use serde::{Deserialize, Serialize};
use tauri::AppHandle;
use tauri::Emitter;
use crate::AppState;
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
use crate::database::models::data::Database;
use crate::database::models::data::{
ApplicationTransientStatus, DownloadableMetadata, GameDownloadStatus, GameVersion,
};
@ -16,11 +18,11 @@ use crate::error::library_error::LibraryError;
use crate::error::remote_access_error::RemoteAccessError;
use crate::games::state::{GameStatusManager, GameStatusWithTransient};
use crate::remote::auth::generate_authorization_header;
use crate::remote::cache::cache_object_db;
use crate::remote::cache::{cache_object, get_cached_object, get_cached_object_db};
use crate::remote::requests::make_request;
use crate::remote::utils::DROP_CLIENT_SYNC;
use crate::AppState;
use bitcode::{Encode, Decode};
use bitcode::{Decode, Encode};
#[derive(Serialize, Deserialize, Debug)]
pub struct FetchGameStruct {
@ -146,27 +148,22 @@ pub fn fetch_game_logic(
) -> Result<FetchGameStruct, RemoteAccessError> {
let mut state_handle = state.lock().unwrap();
let handle = borrow_db_checked();
let db_lock = borrow_db_checked();
let metadata_option = handle.applications.installed_game_version.get(&id);
let metadata_option = db_lock.applications.installed_game_version.get(&id);
let version = match metadata_option {
None => None,
Some(metadata) => Some(
handle
.applications
.game_versions
.get(&metadata.id)
.unwrap()
.get(metadata.version.as_ref().unwrap())
.unwrap()
.clone(),
),
Some(metadata) => db_lock
.applications
.game_versions
.get(&metadata.id)
.map(|v| v.get(metadata.version.as_ref().unwrap()).unwrap())
.cloned(),
};
drop(handle);
let game = state_handle.games.get(&id);
if let Some(game) = game {
let status = GameStatusManager::fetch_state(&id);
let status = GameStatusManager::fetch_state(&id, &db_lock);
let data = FetchGameStruct {
game: game.clone(),
@ -174,10 +171,12 @@ pub fn fetch_game_logic(
version,
};
cache_object(&id, game)?;
cache_object_db(&id, game, &db_lock)?;
return Ok(data);
}
drop(db_lock);
let client = DROP_CLIENT_SYNC.clone();
let response = make_request(&client, &["/api/v1/client/game/", &id], &[], |r| {
r.header("Authorization", generate_authorization_header())
@ -203,9 +202,10 @@ pub fn fetch_game_logic(
.game_statuses
.entry(id.clone())
.or_insert(GameDownloadStatus::Remote {});
drop(db_handle);
let status = GameStatusManager::fetch_state(&id);
let status = GameStatusManager::fetch_state(&id, &db_handle);
drop(db_handle);
let data = FetchGameStruct {
game: game.clone(),
@ -222,12 +222,12 @@ pub fn fetch_game_logic_offline(
id: String,
_state: tauri::State<'_, Mutex<AppState>>,
) -> Result<FetchGameStruct, RemoteAccessError> {
let handle = borrow_db_checked();
let metadata_option = handle.applications.installed_game_version.get(&id);
let db_handle = borrow_db_checked();
let metadata_option = db_handle.applications.installed_game_version.get(&id);
let version = match metadata_option {
None => None,
Some(metadata) => Some(
handle
db_handle
.applications
.game_versions
.get(&metadata.id)
@ -237,11 +237,12 @@ pub fn fetch_game_logic_offline(
.clone(),
),
};
drop(handle);
let status = GameStatusManager::fetch_state(&id);
let status = GameStatusManager::fetch_state(&id, &db_handle);
let game = get_cached_object::<Game>(&id)?;
drop(db_handle);
Ok(FetchGameStruct {
game,
status,
@ -275,7 +276,11 @@ pub fn fetch_game_verion_options_logic(
let process_manager_lock = state_lock.process_manager.lock().unwrap();
let data: Vec<GameVersion> = data
.into_iter()
.filter(|v| process_manager_lock.valid_platform(&v.platform).unwrap())
.filter(|v| {
process_manager_lock
.valid_platform(&v.platform, &state_lock)
.unwrap()
})
.collect();
drop(process_manager_lock);
drop(state_lock);
@ -283,6 +288,49 @@ pub fn fetch_game_verion_options_logic(
Ok(data)
}
/**
* Called by:
* - on_cancel, when cancelled, for obvious reasons
* - when downloading, so if drop unexpectedly quits, we can resume the download. hidden by the "Downloading..." transient state, though
* - when scanning, to import the game
*/
pub fn set_partially_installed(
meta: &DownloadableMetadata,
install_dir: String,
app_handle: Option<&AppHandle>,
) {
set_partially_installed_db(&mut borrow_db_mut_checked(), meta, install_dir, app_handle);
}
pub fn set_partially_installed_db(
db_lock: &mut Database,
meta: &DownloadableMetadata,
install_dir: String,
app_handle: Option<&AppHandle>,
) {
db_lock.applications.transient_statuses.remove(meta);
db_lock.applications.game_statuses.insert(
meta.id.clone(),
GameDownloadStatus::PartiallyInstalled {
version_name: meta.version.as_ref().unwrap().clone(),
install_dir,
},
);
db_lock
.applications
.installed_game_version
.insert(meta.id.clone(), meta.clone());
if let Some(app_handle) = app_handle {
push_game_update(
app_handle,
&meta.id,
None,
GameStatusManager::fetch_state(&meta.id, db_lock),
);
}
}
pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) {
debug!("triggered uninstall for agent");
let mut db_handle = borrow_db_mut_checked();
@ -296,7 +344,7 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
app_handle,
&meta.id,
None,
(None, Some(ApplicationTransientStatus::Uninstalling {})),
GameStatusManager::fetch_state(&meta.id, &db_handle),
);
let previous_state = db_handle.applications.game_statuses.get(&meta.id).cloned();
@ -330,31 +378,35 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
drop(db_handle);
let app_handle = app_handle.clone();
spawn(move || if let Err(e) = remove_dir_all(install_dir) {
error!("{e}");
} else {
let mut db_handle = borrow_db_mut_checked();
db_handle.applications.transient_statuses.remove(&meta);
db_handle
.applications
.installed_game_version
.remove(&meta.id);
db_handle
.applications
.game_statuses
.entry(meta.id.clone())
.and_modify(|e| *e = GameDownloadStatus::Remote {});
drop(db_handle);
spawn(move || {
if let Err(e) = remove_dir_all(install_dir) {
error!("{e}");
} else {
let mut db_handle = borrow_db_mut_checked();
db_handle.applications.transient_statuses.remove(&meta);
db_handle
.applications
.installed_game_version
.remove(&meta.id);
db_handle
.applications
.game_statuses
.entry(meta.id.clone())
.and_modify(|e| *e = GameDownloadStatus::Remote {});
let _ = db_handle.applications.transient_statuses.remove(&meta);
debug!("uninstalled game id {}", &meta.id);
app_handle.emit("update_library", ()).unwrap();
push_game_update(
&app_handle,
&meta.id,
None,
GameStatusManager::fetch_state(&meta.id, &db_handle),
);
push_game_update(
&app_handle,
&meta.id,
None,
(Some(GameDownloadStatus::Remote {}), None),
);
debug!("uninstalled game id {}", &meta.id);
app_handle.emit("update_library", ()).unwrap();
drop(db_handle);
}
});
} else {
warn!("invalid previous state for uninstall, failing silently.");
@ -369,66 +421,6 @@ pub fn get_current_meta(game_id: &String) -> Option<DownloadableMetadata> {
.cloned()
}
pub fn on_game_incomplete(
meta: &DownloadableMetadata,
install_dir: String,
app_handle: &AppHandle,
) -> Result<(), RemoteAccessError> {
// Fetch game version information from remote
if meta.version.is_none() {
return Err(RemoteAccessError::GameNotFound(meta.id.clone()));
}
let client = DROP_CLIENT_SYNC.clone();
let response = make_request(
&client,
&["/api/v1/client/game/version"],
&[
("id", &meta.id),
("version", meta.version.as_ref().unwrap()),
],
|f| f.header("Authorization", generate_authorization_header()),
)?
.send()?;
let game_version: GameVersion = response.json()?;
let mut handle = borrow_db_mut_checked();
handle
.applications
.game_versions
.entry(meta.id.clone())
.or_default()
.insert(meta.version.clone().unwrap(), game_version.clone());
handle
.applications
.installed_game_version
.insert(meta.id.clone(), meta.clone());
let status = GameDownloadStatus::PartiallyInstalled {
version_name: meta.version.clone().unwrap(),
install_dir,
};
handle
.applications
.game_statuses
.insert(meta.id.clone(), status.clone());
drop(handle);
app_handle
.emit(
&format!("update_game/{}", meta.id),
GameUpdateEvent {
game_id: meta.id.clone(),
status: (Some(status), None),
version: Some(game_version),
},
)
.unwrap();
Ok(())
}
pub fn on_game_complete(
meta: &DownloadableMetadata,
install_dir: String,

View File

@ -1,7 +1,4 @@
use crate::database::{
db::borrow_db_checked,
models::data::{ApplicationTransientStatus, GameDownloadStatus},
};
use crate::database::models::data::{ApplicationTransientStatus, Database, GameDownloadStatus};
pub type GameStatusWithTransient = (
Option<GameDownloadStatus>,
@ -10,14 +7,12 @@ pub type GameStatusWithTransient = (
pub struct GameStatusManager {}
impl GameStatusManager {
pub fn fetch_state(game_id: &String) -> GameStatusWithTransient {
let db_lock = borrow_db_checked();
let online_state = match db_lock.applications.installed_game_version.get(game_id) {
Some(meta) => db_lock.applications.transient_statuses.get(meta).cloned(),
pub fn fetch_state(game_id: &String, database: &Database) -> GameStatusWithTransient {
let online_state = match database.applications.installed_game_version.get(game_id) {
Some(meta) => database.applications.transient_statuses.get(meta).cloned(),
None => None,
};
let offline_state = db_lock.applications.game_statuses.get(game_id).cloned();
drop(db_lock);
let offline_state = database.applications.game_statuses.get(game_id).cloned();
if online_state.is_some() {
return (None, online_state);

View File

@ -11,7 +11,9 @@ mod error;
mod process;
mod remote;
use crate::database::scan::scan_install_dirs;
use crate::process::commands::open_process_logs;
use crate::process::process_handlers::UMU_LAUNCHER_EXECUTABLE;
use crate::remote::commands::auth_initiate_code;
use crate::{database::db::DatabaseImpls, games::downloads::commands::resume_download};
use bitcode::{Decode, Encode};
@ -60,6 +62,7 @@ use std::fs::File;
use std::io::Write;
use std::panic::PanicHookInfo;
use std::path::Path;
use std::process::{Command, Stdio};
use std::str::FromStr;
use std::sync::Arc;
use std::time::SystemTime;
@ -95,6 +98,27 @@ pub struct User {
profile_picture_object_id: String,
}
#[derive(Clone)]
pub struct CompatInfo {
umu_installed: bool,
}
fn create_new_compat_info() -> Option<CompatInfo> {
#[cfg(target_os = "windows")]
return None;
let has_umu_installed = Command::new(UMU_LAUNCHER_EXECUTABLE)
.stdout(Stdio::null())
.spawn();
if let Err(umu_error) = &has_umu_installed {
warn!("disabling windows support with error: {umu_error}");
}
let has_umu_installed = has_umu_installed.is_ok();
Some(CompatInfo {
umu_installed: has_umu_installed,
})
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AppState<'a> {
@ -106,6 +130,8 @@ pub struct AppState<'a> {
download_manager: Arc<DownloadManager>,
#[serde(skip_serializing)]
process_manager: Arc<Mutex<ProcessManager<'a>>>,
#[serde(skip_serializing)]
compat_info: Option<CompatInfo>,
}
fn setup(handle: AppHandle) -> AppState<'static> {
@ -142,9 +168,13 @@ fn setup(handle: AppHandle) -> AppState<'static> {
let games = HashMap::new();
let download_manager = Arc::new(DownloadManagerBuilder::build(handle.clone()));
let process_manager = Arc::new(Mutex::new(ProcessManager::new(handle.clone())));
let compat_info = create_new_compat_info();
debug!("checking if database is set up");
let is_set_up = DB.database_is_set_up();
scan_install_dirs();
if !is_set_up {
return AppState {
status: AppStatus::NotConfigured,
@ -152,6 +182,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
games,
download_manager,
process_manager,
compat_info,
};
}
@ -164,6 +195,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
let mut missing_games = Vec::new();
let statuses = db_handle.applications.game_statuses.clone();
drop(db_handle);
for (game_id, status) in statuses {
match status {
GameDownloadStatus::Remote {} => {}
@ -215,6 +247,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
games,
download_manager,
process_manager,
compat_info,
}
}

View File

@ -16,7 +16,7 @@ pub fn launch_game(
// download_type: DownloadType::Game,
//};
match process_manager_lock.launch_process(id) {
match process_manager_lock.launch_process(id, &state_lock) {
Ok(()) => {}
Err(e) => return Err(e),
}

View File

@ -0,0 +1,33 @@
use std::collections::HashMap;
use dynfmt::{Argument, FormatArgs};
pub struct DropFormatArgs {
positional: Vec<String>,
map: HashMap<&'static str, String>,
}
impl DropFormatArgs {
pub fn new(launch_string: String, working_dir: &String, executable_name: &String, absolute_executable_name: String) -> Self {
let mut positional = Vec::new();
let mut map: HashMap<&'static str, String> = HashMap::new();
positional.push(launch_string);
map.insert("dir", working_dir.to_string());
map.insert("exe", executable_name.to_string());
map.insert("abs_exe", absolute_executable_name);
Self { positional, map }
}
}
impl FormatArgs for DropFormatArgs {
fn get_index(&self, index: usize) -> Result<Option<dynfmt::Argument<'_>>, ()> {
Ok(self.positional.get(index).map(|arg| arg as Argument<'_>))
}
fn get_key(&self, key: &str) -> Result<Option<dynfmt::Argument<'_>>, ()> {
Ok(self.map.get(key).map(|arg| arg as Argument<'_>))
}
}

View File

@ -2,3 +2,5 @@ pub mod commands;
#[cfg(target_os = "linux")]
pub mod compat;
pub mod process_manager;
pub mod process_handlers;
pub mod format;

View File

@ -0,0 +1,109 @@
use log::debug;
use crate::{
AppState,
database::models::data::{Database, DownloadableMetadata, GameVersion},
process::process_manager::{Platform, ProcessHandler},
};
pub struct NativeGameLauncher;
impl ProcessHandler for NativeGameLauncher {
fn create_launch_process(
&self,
_meta: &DownloadableMetadata,
launch_command: String,
args: Vec<String>,
_game_version: &GameVersion,
_current_dir: &str,
) -> String {
format!("\"{}\" {}", launch_command, args.join(" "))
}
fn valid_for_platform(&self, _db: &Database, _state: &AppState, _target: &Platform) -> bool {
true
}
}
pub const UMU_LAUNCHER_EXECUTABLE: &str = "umu-run";
pub struct UMULauncher;
impl ProcessHandler for UMULauncher {
fn create_launch_process(
&self,
_meta: &DownloadableMetadata,
launch_command: String,
args: Vec<String>,
game_version: &GameVersion,
_current_dir: &str,
) -> String {
debug!("Game override: \"{:?}\"", &game_version.umu_id_override);
let game_id = match &game_version.umu_id_override {
Some(game_override) => {
if game_override.is_empty() {
game_version.game_id.clone()
} else {
game_override.clone()
}
}
None => game_version.game_id.clone(),
};
format!(
"GAMEID={game_id} {umu} \"{launch}\" {args}",
umu = UMU_LAUNCHER_EXECUTABLE,
launch = launch_command,
args = args.join(" ")
)
}
fn valid_for_platform(&self, _db: &Database, state: &AppState, _target: &Platform) -> bool {
let Some(ref compat_info) = state.compat_info else {
return false;
};
compat_info.umu_installed
}
}
pub struct AsahiMuvmLauncher;
impl ProcessHandler for AsahiMuvmLauncher {
fn create_launch_process(
&self,
meta: &DownloadableMetadata,
launch_command: String,
args: Vec<String>,
game_version: &GameVersion,
current_dir: &str,
) -> String {
let umu_launcher = UMULauncher {};
let umu_string = umu_launcher.create_launch_process(
meta,
launch_command,
args,
game_version,
current_dir,
);
let mut args_cmd = umu_string.split("umu-run").collect::<Vec<&str>>().into_iter();
let args = args_cmd.next().unwrap().trim();
let cmd = format!("umu-run{}", args_cmd.next().unwrap());
format!("{args} muvm -- {cmd}")
}
#[allow(unreachable_code)]
fn valid_for_platform(&self, _db: &Database, state: &AppState, _target: &Platform) -> bool {
#[cfg(not(target_os = "linux"))]
return false;
#[cfg(not(target_arch = "aarch64"))]
return false;
let page_size = page_size::get();
if page_size != 16384 {
return false;
}
let Some(ref compat_info) = state.compat_info else {
return false;
};
compat_info.umu_installed
}
}

View File

@ -21,14 +21,18 @@ use tauri_plugin_opener::OpenerExt;
use crate::{
AppState, DB,
database::{
db::{DATA_ROOT_DIR, borrow_db_mut_checked},
db::{DATA_ROOT_DIR, borrow_db_checked, borrow_db_mut_checked},
models::data::{
ApplicationTransientStatus, DownloadType, DownloadableMetadata, GameDownloadStatus,
GameVersion,
ApplicationTransientStatus, Database, DownloadType, DownloadableMetadata,
GameDownloadStatus, GameVersion,
},
},
error::process_error::ProcessError,
games::{library::push_game_update, state::GameStatusManager},
process::{
format::DropFormatArgs,
process_handlers::{AsahiMuvmLauncher, NativeGameLauncher, UMULauncher},
},
};
pub struct RunningProcess {
@ -42,7 +46,10 @@ pub struct ProcessManager<'a> {
log_output_dir: PathBuf,
processes: HashMap<String, RunningProcess>,
app_handle: AppHandle,
game_launchers: HashMap<(Platform, Platform), &'a (dyn ProcessHandler + Sync + Send + 'static)>,
game_launchers: Vec<(
(Platform, Platform),
&'a (dyn ProcessHandler + Sync + Send + 'static),
)>,
}
impl ProcessManager<'_> {
@ -62,7 +69,7 @@ impl ProcessManager<'_> {
app_handle,
processes: HashMap::new(),
log_output_dir,
game_launchers: HashMap::from([
game_launchers: vec![
// Current platform to target platform
(
(Platform::Windows, Platform::Windows),
@ -76,11 +83,15 @@ impl ProcessManager<'_> {
(Platform::MacOs, Platform::MacOs),
&NativeGameLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
),
(
(Platform::Linux, Platform::Windows),
&AsahiMuvmLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
),
(
(Platform::Linux, Platform::Windows),
&UMULauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
),
]),
],
}
}
@ -99,8 +110,12 @@ impl ProcessManager<'_> {
}
}
fn get_log_dir(&self, game_id: String) -> PathBuf {
self.log_output_dir.join(game_id)
}
pub fn open_process_logs(&mut self, game_id: String) -> Result<(), ProcessError> {
let dir = self.log_output_dir.join(game_id);
let dir = self.get_log_dir(game_id);
self.app_handle
.opener()
.open_path(dir.to_str().unwrap(), None::<&str>)
@ -145,7 +160,6 @@ impl ProcessManager<'_> {
},
);
}
drop(db_handle);
let elapsed = process.start.elapsed().unwrap_or(Duration::ZERO);
// If we started and ended really quickly, something might've gone wrong
@ -158,16 +172,42 @@ impl ProcessManager<'_> {
let _ = self.app_handle.emit("launch_external_error", &game_id);
}
let status = GameStatusManager::fetch_state(&game_id);
let status = GameStatusManager::fetch_state(&game_id, &db_handle);
drop(db_handle);
push_game_update(&self.app_handle, &game_id, None, status);
}
pub fn valid_platform(&self, platform: &Platform) -> Result<bool, String> {
let current = &self.current_platform;
Ok(self.game_launchers.contains_key(&(*current, *platform)))
fn fetch_process_handler(
&self,
db_lock: &Database,
state: &AppState,
target_platform: &Platform,
) -> Result<&(dyn ProcessHandler + Send + Sync), ProcessError> {
Ok(self
.game_launchers
.iter()
.find(|e| {
let (e_current, e_target) = e.0;
e_current == self.current_platform
&& e_target == *target_platform
&& e.1.valid_for_platform(db_lock, state, target_platform)
})
.ok_or(ProcessError::InvalidPlatform)?
.1)
}
pub fn launch_process(&mut self, game_id: String) -> Result<(), ProcessError> {
pub fn valid_platform(&self, platform: &Platform, state: &AppState) -> Result<bool, String> {
let db_lock = borrow_db_checked();
let process_handler = self.fetch_process_handler(&db_lock, state, platform);
Ok(process_handler.is_ok())
}
pub fn launch_process(
&mut self,
game_id: String,
state: &AppState,
) -> Result<(), ProcessError> {
if self.processes.contains_key(&game_id) {
return Err(ProcessError::AlreadyRunning);
}
@ -191,10 +231,6 @@ impl ProcessManager<'_> {
};
let mut db_lock = borrow_db_mut_checked();
debug!(
"Launching process {:?} with games {:?}",
&game_id, db_lock.applications.game_versions
);
let game_status = db_lock
.applications
@ -211,13 +247,15 @@ impl ProcessManager<'_> {
version_name,
install_dir,
} => (version_name, install_dir),
GameDownloadStatus::PartiallyInstalled {
version_name,
install_dir,
} => (version_name, install_dir),
_ => return Err(ProcessError::NotDownloaded),
_ => return Err(ProcessError::NotInstalled),
};
debug!(
"Launching process {:?} with version {:?}",
&game_id,
db_lock.applications.game_versions.get(&game_id).unwrap()
);
let game_version = db_lock
.applications
.game_versions
@ -227,7 +265,7 @@ impl ProcessManager<'_> {
.ok_or(ProcessError::InvalidVersion)?;
// TODO: refactor this path with open_process_logs
let game_log_folder = &self.log_output_dir.join(game_id);
let game_log_folder = &self.get_log_dir(game_id);
create_dir_all(game_log_folder).map_err(ProcessError::IOError)?;
let current_time = chrono::offset::Local::now();
@ -251,13 +289,9 @@ impl ProcessManager<'_> {
)))
.map_err(ProcessError::IOError)?;
let current_platform = self.current_platform;
let target_platform = game_version.platform;
let game_launcher = self
.game_launchers
.get(&(current_platform, target_platform))
.ok_or(ProcessError::InvalidPlatform)?;
let process_handler = self.fetch_process_handler(&db_lock, state, &target_platform)?;
let (launch, args) = match game_status {
GameDownloadStatus::Installed {
@ -278,7 +312,7 @@ impl ProcessManager<'_> {
let launch = PathBuf::from_str(install_dir).unwrap().join(launch);
let launch = launch.to_str().unwrap();
let launch_string = game_launcher.create_launch_process(
let launch_string = process_handler.create_launch_process(
&meta,
launch.to_string(),
args.clone(),
@ -286,8 +320,15 @@ impl ProcessManager<'_> {
install_dir,
);
let format_args = DropFormatArgs::new(
launch_string,
install_dir,
&game_version.launch_command,
launch.to_string(),
);
let launch_string = SimpleCurlyFormat
.format(&game_version.launch_command_template, &[launch_string])
.format(&game_version.launch_command_template, format_args)
.map_err(|e| ProcessError::FormatError(e.to_string()))?
.to_string();
@ -306,9 +347,12 @@ impl ProcessManager<'_> {
#[cfg(unix)]
command.args(vec!["-c", &launch_string]);
debug!("final launch string:\n\n{launch_string}\n");
command
.stderr(error_file)
.stdout(log_file)
.env_remove("RUST_LOG")
.current_dir(install_dir);
let child = command.spawn().map_err(ProcessError::IOError)?;
@ -413,49 +457,6 @@ pub trait ProcessHandler: Send + 'static {
game_version: &GameVersion,
current_dir: &str,
) -> String;
}
struct NativeGameLauncher;
impl ProcessHandler for NativeGameLauncher {
fn create_launch_process(
&self,
_meta: &DownloadableMetadata,
launch_command: String,
args: Vec<String>,
_game_version: &GameVersion,
_current_dir: &str,
) -> String {
format!("\"{}\" {}", launch_command, args.join(" "))
}
}
pub const UMU_LAUNCHER_EXECUTABLE: &str = "umu-run";
struct UMULauncher;
impl ProcessHandler for UMULauncher {
fn create_launch_process(
&self,
_meta: &DownloadableMetadata,
launch_command: String,
args: Vec<String>,
game_version: &GameVersion,
_current_dir: &str,
) -> String {
debug!("Game override: \"{:?}\"", &game_version.umu_id_override);
let game_id = match &game_version.umu_id_override {
Some(game_override) => {
if game_override.is_empty() {
game_version.game_id.clone()
} else {
game_override.clone()
}
}
None => game_version.game_id.clone(),
};
format!(
"GAMEID={game_id} {umu} \"{launch}\" {args}",
umu = UMU_LAUNCHER_EXECUTABLE,
launch = launch_command,
args = args.join(" ")
)
}
fn valid_for_platform(&self, db: &Database, state: &AppState, target: &Platform) -> bool;
}

View File

@ -51,8 +51,15 @@ fn read_sync(base: &Path, key: &str) -> io::Result<Vec<u8>> {
}
pub fn cache_object<D: Encode>(key: &str, data: &D) -> Result<(), RemoteAccessError> {
cache_object_db(key, data, &borrow_db_checked())
}
pub fn cache_object_db<D: Encode>(
key: &str,
data: &D,
database: &Database,
) -> Result<(), RemoteAccessError> {
let bytes = bitcode::encode(data);
write_sync(&borrow_db_checked().cache_dir, key, bytes).map_err(RemoteAccessError::Cache)
write_sync(&database.cache_dir, key, bytes).map_err(RemoteAccessError::Cache)
}
pub fn get_cached_object<D: Encode + DecodeOwned>(key: &str) -> Result<D, RemoteAccessError> {
get_cached_object_db::<D>(key, &borrow_db_checked())

View File

@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2.0.0",
"productName": "Drop Desktop Client",
"version": "0.3.1",
"version": "0.3.1-appimage",
"identifier": "dev.drop.app",
"build": {
"beforeDevCommand": "yarn dev --port 1432",
@ -27,7 +27,7 @@
},
"bundle": {
"active": true,
"targets": ["nsis", "deb", "rpm", "dmg", "appimage"],
"targets": ["nsis", "deb", "rpm", "dmg"],
"windows": {
"nsis": {
"installMode": "both"

View File

@ -1,4 +1,5 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
"extends": "./.nuxt/tsconfig.json",
"exclude": ["src-tauri/**/*"]
}

View File

@ -54,12 +54,13 @@ export enum GameStatusEnum {
Remote = "Remote",
Queued = "Queued",
Downloading = "Downloading",
Validating = "Validating",
Installed = "Installed",
Updating = "Updating",
Uninstalling = "Uninstalling",
SetupRequired = "SetupRequired",
Running = "Running",
PartiallyInstalled = "PartiallyInstalled"
PartiallyInstalled = "PartiallyInstalled",
}
export type GameStatus = {