Use Git and PowerShell to Implement Cloud Saves for Any Game
21st September 2024 • 6 min read
As someone who grew up in the '90s, I now tend to go back to the games I used to play as a kid or to well-made remakes like Diablo II: Resurrected, which is an excellent one. It has everything the original had, including all the glitches, and… it's basically the original game that just looks better, especially on today's big flat screens, where the original graphics lose the magic it once had on CRT monitors.
The remake also comes with an excellent gamepad support, but why am I talking about Diablo II: Resurrected? In this game, you can choose to play as either an online or offline character. When you play online, your saves are stored on a server, but what if a blizzard comes and destroys the server? Pun not intended, and there’s probably a backup. The point is, you don’t own the files, and you can only access the latest snapshot of your progress through the game.
On the other hand, with an offline character, your saves are stored locally on your drive, and you can't play on multiple computers without manually transferring these files.
But you can have your cake and eat it too. An automated solution using a Git repository, implemented in a single and fairly simple PowerShell script that I'll describe in this post, essentially gives any game that stores saves in a local directory three new features:
- Cloud Saves — You'll be able to deploy the script on multiple devices and sync your progress automatically. All you'd need to do is run the game via this script and turn directory with game save files to a Git repository.
- Progress History — At the end of every game session, your progress will be saved as a single commit. If you're familiar with Git version control, you know it's easy to revert files to any previous commit.
- Play Time — This is a little bonus. Since the script runs the game and then waits until it ends, it's simple to store a timestamp before the game starts, then store another one after it ends, and add the difference to the overall time stored in a versioned text file. You can later refer to this file to see how many hours of your life were "wasted" :-).
Saves Folder as Git Repository
The first thing you need to do is turn your game's save folder into a Git repository. I'm going to assume you have Git installed, along with a basic understanding of this VCS, and that you're familiar with GitHub, as this is primarily a programming blog.
If this this is not the case, learn the basics from Git Doc, then create an account on GitHub and explore a bit.
I'm going to continue with the illustration using Diablo II: Resurrected as an example, but as I already mentioned, the solution should generally work with any game. First, locate the path to the game save files and initialize a Git repository in this directory using a PowerShell command line like this:
cd "$env:USERPROFILE\Saved Games\Diablo II Resurrected"
git init
Now create a new GitHub repository. I'm going to name mine d2r-savegames
. Then follow the instructions you find at the bottom of the page under …or push an existing repository from the command line
header. For me, the command looks like this:
git remote add origin "git@github.com:marianpekar/d2r-savegames"
Our repositories are almost ready. The last thing we need to do is to set an upstream between local and remote branches — that is between origin
and master
. Let's do that while pushing a file so we can immediately confirm everything is set up correctly.
Create a new file named PlayTime.txt
with zero hours, minutes, and seconds encoded simply as 00:00:00
. We'll use this file to store the accumulated playtime.
"00:00:00" > PlayTime.txt
Add this file to local Git repository:
git add PlayTime.txt
Create a commit with the message first commit
. It could be anything; I often use phrases like initial commit
or just init
for short.
git commit -m "first commit"
And finally, push this commit to the remote repository:
git push --set-upstream origin master
Next time, just git push
should be sufficient, but that's something our PowerShell script will take care of.
PowerShell Script
Now that our GitHub and local repositories are ready, it's time to implement the script we'll use to run the game. First, declare variables for the game saves directory and the executable. For Diablo II: Resurrected, the values are as follows:
$SaveGamesPath = "$env:USERPROFILE\Saved Games\Diablo II Resurrected" # git repository
$GameExePath = "C:\Program Files (x86)\Diablo II Resurrected\D2R.exe"
In the next part, the script enters the directory with the game saves, resets any local changes, and pulls everything from the remote repository.
Set-Location $SaveGamesPath
git reset --hard
git pull -X theirs
Pulling with -X theirs
to resolve conflicts by automatically accepting the version from the remote repository is redundant after git reset --hard
, but let's keep it… just in case.
Now assign the current time to a new variable and run the executable.
$SessionStartDate = Get-Date
. $GameExePath | Out-Null
If we run this script now, it will pull everything from the remote repository, run the game, and then halt execution until we exit the game. That's why storing the current time again in another variable after this line, and then subtracting $SessionStartDate
from it, gives us the duration of this play session.
$SessionEndDate = Get-Date
$SessionDuration = $SessionEndDate - $SessionStartDate
It's time for the script to go back to our local repository and update our total playtime. But first, it checks if the PlayTime.txt
file exists; if it doesn't, it creates a default one with 00:00:00
.
Set-Location $SaveGamesPath
$PlayTimeFile = "PlayTime.txt"
if (-Not (Test-Path $PlayTimeFile)) {
Write-Output "00:00:00" | Out-File $PlayTimeFile
}
Now that we're sure PlayTime.txt
exists and contains time in the expected format hh:mm:ss
, let the script add the length of the current game session to the total playtime:
$PlayTimeFileContent = Get-Content $PlayTimeFile -Raw
$LastPlayTime = [TimeSpan]::Parse($PlayTimeFileContent.Trim())
$CurrentPlayTime = $LastPlayTime + $SessionDuration
$CurrentPlayTime.ToString('hh\:mm\:ss') | Out-File $PlayTimeFile
And finally, after this nice-to-have feature, let the script actually push the saves to the remote repository. Here, we can use our time variables to construct a nice commit message.
git add .
git commit -m "$SessionStartDate - $SessionEndDate ($($SessionDuration.ToString('hh\:mm\:ss')))"
git push
And that's it! The very last line in our script simply takes us back to where the script lives, so we can run it again.
Set-Location $PSScriptRoot
If your script doesn't work as expected, compare it with the final version at Gist.
After some play sessions, you can browse your commits and even create some interesting statistics from the commit messages, which contain when and how long you've played, as you can see in the following image.
Every commit incrementally stores progress after each play session, and you can revert your progress to any previous "checkpoint", even if just temporarily.