mtkennerly / ludusavi

Backup tool for PC game saves
MIT License
2.57k stars 58 forks source link

Automatic backup after playing a game #211

Closed mtkennerly closed 4 months ago

mtkennerly commented 1 year ago

What's your idea?

This would require some research to see if we can do it accurately enough. The manifest launch section has executable paths/names, which may be useful for this. Some potential options:

My first choice would probably be by process path, as long as it ends up being accurate enough, since it would be one implementation that could work generically.

lucasfabre commented 1 year ago

Hello, I am not currently a user of ludusavi (but I plan to use it very soon). I was working on something similar and came up with an alterative option, that wasn't listed here.

It is to modify the launch options of the launcher entry, and make ludusavi manage the game process. For example you can run a game using "C:\Windows\System32\cmd.exe" /C %command% as a launchoption in steam. In this case cmd.exe is responsible of the game process. For ludusavi it can be something like ludusavi startgame -- %command%

It has several advantages and disadvantages:

The main disadvantage is that it require to edit the steam entry. And some of them are generated and managed by other tools (ex: emudeck)

This can be used in combination with a process watcher, for the best of both worlds.

Of course this is just an idea, I did not do any proof of concept and I don't know if it is possible implement in ludusavi.

PPORCH3bis commented 1 year ago

I just finished configure ludusavi on my Windows but used it with playnite and loved this hability to backup the save after I close the game.

It will be nice if you find a way to make possible "just" with ludusavi cause with my steam deck it will be nice to have to install playnite just for that.

Sry for my English it's not my first language.

sluedecke commented 1 year ago

It is to modify the launch options of the launcher entry, and make ludusavi manage the game process.

I use this approach to "hook" ludusavi into the game launching process with Heroic. It allows to set a wrapper command which receives the actual command to launch the game, including all parameters. This way, my wrapper can do some checks and restore backups before the game is launched and do a backup once it has finished.

The actual command contains enough information to determine the game name used in ludusavi, for example:

/usr/bin/mangohud --dlsym ./gogdl --auth-config-path /home/MYUSER/.config/heroic/gog_store/auth.json launch /home/MYUSER/Games/Avernum 1207663333 --no-wine --wrapper '/home/MYUSER/.config/heroic/tools/proton/Proton-GE-Proton8-3/proton' run --platform windows --prefer-task 0

Unfortunately this heavily depends on how Heroic launches a game and which parameters are used. Using mangohud for example results in parameters being shifted.

sluedecke commented 1 year ago

This is the script (not working if mangohud is used!), also found here: https://github.com/sluedecke/ludusavi-launcher

#!/bin/sh

# -- ABOUT --
#
# Wrapper script to use ludusavi for savegame backup / restoration.
#
# Assumes that $5 is the directory with the game install which contains the file
# `gameinfo`.
#
# This file comes without warranty, use at your own risk!
#
# License: MIT License
# Author: Sascha Lüdecke <sascha@meta-x.de>

# -- BUGS --
#
# - does not work if mangohud is used

# -- HISTORY --
#
# Version: 0.X - WIP
#
#
# Version: 0.2 - 2022-09-29
#
# . use zenity to tell user that we back up / restore
#
# Version: 0.1 - 2022-09-27

SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
LOGFILE=$SCRIPT_DIR/ludusavi-cloud.log

# Standard paths
## LUDUSAVI=/usr/bin/ludusavi
LUDUSAVI=$HOME/Projekte/contributing/ludusavi/target/debug/ludusavi
ZENITY=/usr/bin/zenity
TEE=/usr/bin/tee
JQ=/usr/bin/jq
HEAD=/usr/bin/head
DATE=/usr/bin/date
ECHO=/usr/bin/echo

GAMENAME=""
if [ -r "$5/gameinfo" ]
then
    GAMENAME=`$HEAD -1 "$5/gameinfo"`
else
    GAMENAME=`$JQ -r .name "$5/goggame-$6.info"`
fi

# Path on steam deck if installed via flatpack
if [ $USER == deck ]
then
    LUDUSAVI=/home/.steamos/offload/var/lib/flatpak/exports/bin/com.github.mtkennerly.ludusavi
fi

{
    $ECHO ==================================================
    $ECHO
    $ECHO Gamename is: $GAMENAME
    $ECHO Start time: `$DATE`
    $ECHO Parameters: $@

    # restore savegame
    $ECHO $LUDUSAVI restore --force "$GAMENAME"
    (
        $ECHO "# Restoring savegame for $GAMENAME"
        # bypass STDOUT 
        $LUDUSAVI restore --force "$GAMENAME" 1>&2
    ) | $ZENITY --progress \
               --title="Savegame restore" \
               --no-cancel \
               --auto-close \
               --pulsate

    # run game
    $ECHO Game run command:
    $ECHO $@
    "$@"

    # backup savegame
    $ECHO $LUDUSAVI backup --merge --force "$GAMENAME"
    (
        $ECHO "# Backing up savegames for $GAMENAME"
        # bypass STDOUT
        $LUDUSAVI backup --merge --force "$GAMENAME" 1>&2
    ) | $ZENITY --progress \
               --title="Savegame backup" \
               --no-cancel \
               --auto-close \
               --pulsate

    $ECHO End time: `$DATE`
    $ECHO
    $ECHO ==================================================
}  2>&1 | $TEE -a $LOGFILE
sluedecke commented 1 year ago

I have added a first shot at implementing a launcher command. It is based on the shell script above and currently needs to be invoked as (note the double dash after launcher, they are needed ATM):

ludusavi launcher -- /opt/Heroic/resources/app.asar.unpacked/build/bin/linux/gogdl --auth-config-path /home/MYUSER/.config/heroic/gog_store/auth.json launch /home/MYUSER/Games/Factorio 1238653230 --platform linux

Currently only gogdl based launches (if installed from heroic) are implemented.

See here: https://github.com/sluedecke/ludusavi/tree/feature/launcher

mtkennerly commented 1 year ago

Thanks for looking into this \:D

The only reservation I have is about how hard it may be to maintain this approach when Heroic/etc make changes to how they launch games. That said, if we can make the implementation safe and robust, then I think it's worth incorporating as an option.

Some initial feedback:

We could support these two styles:

# Predetermined name - don't need to do any special lookups.
ludusavi wrap Factorio -- ...

# Infer info from CLI explicitly for Heroic.
ludusavi wrap --infer heroic -- ...

I think we should make it required to pass either a name or an --infer option. That way, it's very explicit about what it supports inferring from, which could help to avoid false positives and to constrain how many possibilities we have to consider at any give time.

sluedecke commented 1 year ago

Thanks for the feedback!

  1. Depending gogdl positional parameters is error prone, since it is an heroic internal project and can change without notice :( Still, according to https://github.com/Heroic-Games-Launcher/heroic-gogdl/blob/d7f29dfef5818e8b323d04761e18a9abb750f93e/gogdl/args.py#L90 the order is "fixed"
  2. moving away from info/goggame: definitely worth looking into it
  3. done
  4. I fully agree!

Recent commit is about:

sluedecke commented 1 year ago

Could look like this:

cargo run -- wrap --help
   Compiling ludusavi v0.19.0 (/home/saschal/Projekte/contributing/ludusavi)
    Finished dev [optimized + debuginfo] target(s) in 13.10s
     Running `target/debug/ludusavi wrap --help`
ludusavi-wrap 
Wrap restore/backup around game execution

USAGE:
    ludusavi wrap [OPTIONS] [COMMANDS]...

ARGS:
    <COMMANDS>...    Commands to launch the game

OPTIONS:
    -h, --help                Print help information
        --infer <LAUNCHER>    Infer game name from commands based on launcher
                              type [possible values: heroic]
        --name <NAME>         Directly set game name as known to ludusavi
mtkennerly commented 1 year ago

Ah, I guess Clap can't mark the name as "positional, but only if it comes before --". That looks fine then :+1:

Feel free to open a draft PR, and we can move the implementation discussion there.

seniorm0ment commented 1 year ago

This would be beneficial alongside #90

nioncode commented 1 year ago

An alternative solution would be to run ludusavi as a daemon and set up a file system event watcher (e.g. inotifywait) for all the known paths. Then the savegames could be copied whenever a new savegame is made even while playing the game and not just after the game has been stopped.

sluedecke commented 10 months ago

After merging https://github.com/mtkennerly/ludusavi/pull/235, is this still an open issue?

mtkennerly commented 10 months ago

I think there's still room to explore the monitoring options in the first message. There could be different pros/cons between monitoring and the wrap CLI, so I think both have value.

kekonn commented 6 months ago

I have a proposition to add to wrap's functionality: using a parameter, let me specify if I want to restore or backup?

You'd get something like ludusavi wrap --backup --gui. I can probably add that myself, but before I put in the effort, I'd like to run the idea by you @mtkennerly .

Second idea: add more options to --infer. I would think that if we have the complete name of the exe, we can try at least matching Lutris etc as well. Only caveat I can think of is that it could not be used to make the first backup.

mtkennerly commented 6 months ago

I have a proposition to add to wrap's functionality: using a parameter, let me specify if I want to restore or backup?

That sounds fine to me :) I'd be open to --no-backup and --no-restore.

Second idea: add more options to --infer. I would think that if we have the complete name of the exe, we can try at least matching Lutris etc as well.

This one's a bit tricky. How do we know which argument is the game exe? We might have cases with wrapper scripts like mangohud ./Celeste (where mangohud and Celeste are both executables), or cases where just the game name is passed like legendary launch Celeste (where only legendary is an executable).

That said, maybe Lutris sets some environment variables that we could check, or they might be open to adding some (like I did for Heroic).

kekonn commented 6 months ago

This one's a bit tricky. How do we know which argument is the game exe? We might have cases with wrapper scripts like mangohud ./Celeste (where mangohud and Celeste are both executables), or cases where just the game name is passed like legendary launch Celeste (where only legendary is an executable).

That said, maybe Lutris sets some environment variables that we could check, or they might be open to adding some (like I did for Heroic).

It is indeed tricky, but I was just thining of Lutris in this case. Lutris has a setting where you can set a command prefix, that is where you'd set ludusavi wrap --infer Lutris. So in this case infer would be "Assume you are wrapped around a game being launched with Lutris".

But investigating the env vars it sets is also a good idea.

EDIT: I could ofcourse also have a look at what it would take to make an official integration between lutris and ludusavi, but I'm not a Python dev :) I think if they could just set some env vars that'd be specific to ludusavi, that could be enough?

mtkennerly commented 6 months ago

@kekonn I've inquired about adding some environment variables here: https://github.com/lutris/lutris/issues/5407

mtkennerly commented 6 months ago

@kekonn I've pushed a branch called feature/wrap-infer-lutris that uses the environment variables discussed in that ticket. Could you test it out? The game_name and WINEPREFIX variables should already be set in the current version of Lutris. The code will check for the proposed GAME_DIRECTORY variable as well, but it should still work even if that one's not present (just won't check <base> paths).

kekonn commented 6 months ago

I might have some time tonight or tomorrow evening (CEST).

kekonn commented 6 months ago

@mtkennerly good news and bad news: it is able to pick up the game, but I can't set the backup location this way and so it is looking in the wrong place 😅 This shouldn't be a problem when not running from a target folder in a branch though, no?

[2024-04-09T20:21:34.780Z] DEBUG [ludusavi::cli] Title finder result: Some("Horizon Forbidden West")
[2024-04-09T20:21:34.780Z] ERROR [ludusavi::scan::layout] Unable to load mapping: StrictPath { raw: "/home/kekkon/ludusavi-backup/Horizon Forbidden West/mapping.yaml", basis: None } | "File does not exist"
[2024-04-09T20:21:34.781Z] DEBUG [ludusavi::cli::ui] Showing confirmation to user (GUI=true, force=None): This game does not have a backup to restore.

Incidentally the line breaks added in the error dialog are not translated: image

mtkennerly commented 6 months ago

it is able to pick up the game

Nice 🎉

I can't set the backup location this way and so it is looking in the wrong place 😅 This shouldn't be a problem when not running from a target folder in a branch though, no?

If you normally run Ludusavi via Flatpak, then there might be an issue with $XDG_DATA_HOME not pointing to your normal config when you run it standalone. If so, you could try passing --config ~/.var/app/com.github.mtkennerly.ludusavi/config/ludusavi. It could also be that Lutris is setting a different value for $XDG_DATA_HOME.

Incidentally the line breaks added in the error dialog are not translated:

Oh, that's weird. It looks like it's coming from the native-dialog crate when it invokes kdialog. Thanks for letting me know.

What version of kdialog do you have?

kekonn commented 6 months ago

it is able to pick up the game

Nice 🎉

I can't set the backup location this way and so it is looking in the wrong place 😅 This shouldn't be a problem when not running from a target folder in a branch though, no?

If you normally run Ludusavi via Flatpak, then there might be an issue with $XDG_DATA_HOME not pointing to your normal config when you run it standalone. If so, you could try passing --config ~/.var/app/com.github.mtkennerly.ludusavi/config/ludusavi. It could also be that Lutris is setting a different value for $XDG_DATA_HOME.

I think this is because I run ludusavi from the flatpak, but I run lutris from the system. Guess I should start thinking about packaging ludusavi for OpenSUSE :sweat_smile: . Adding --config did the trick though, so barring the little formatting bug with KDialog, this is just fine for me.

Incidentally the line breaks added in the error dialog are not translated:

Oh, that's weird. It looks like it's coming from the native-dialog crate when it invokes kdialog. Thanks for letting me know.

What version of kdialog do you have?

kdialog --version returns 24.02.1. I am currently on OpenSUSE Tumbleweed.

mtkennerly commented 6 months ago

Found a ticket for the formatting issue (https://github.com/native-dialog-rs/native-dialog-rs/issues/41) and added a comment with some more info. It looks like downgrading the native-dialog version might help, although I'm not sure what else changed between the versions.

kekonn commented 5 months ago

Just to confirm, it worked when I set the $XDG_DATA_HOME. I know you're busy, but any idea when this would hit main? Using infer I could simply add ludusavi to my standard Lutris Wine settings and not have to do game per game settings.

mtkennerly commented 5 months ago

@kekonn I've merged the --infer lutris changes to master already. I'd like to make a new release within a week or two as well, although I'm still figuring out what else to include.

mtkennerly commented 4 months ago

So there are a few solutions here that work today and get pretty close to this functionality:

Given the technical challenges with detecting game events without hooking into a launcher, and given that the above options are probably "good enough" for most cases, I think I'll refrain from implementing a process monitor in Ludusavi.