Baldur's Gate 3 — Honour Mode Restore
An experiment in recovering an Honour Mode save, documenting the restore process, the save data involved, and the tooling used to bring a run back to a valid state.
Context
Where this started
What I wanted to test
This started from a very simple question: if I’m playing Baldur’s Gate 3 through Steam + Proton on Arch, which save directory is actually the live one? Honour Mode makes that question a bit more interesting than usual. If I was going to keep a manual backup around, I wanted to know exactly what BG3 was updating and what would need to be restored after something worse than a normal misplay (elevator bug, grrrr). So the goal was not to build some giant save manager. I just wanted a small, boring, predictable backup/restore workflow that I could understand and test myself.
Setup
The test environment was:
- Arch Linux
- Steam
- Proton
- Baldur’s Gate 3
- Steam Cloud disabled while testing
- a dedicated local backup directory
The backup directory I used was:
~/Documents/misc/bg3-backups
The important part was keeping the game fully closed while taking or restoring a snapshot. I really did not want Steam, Proton, or BG3 touching the same files halfway through a copy operation.
Save locations
Where the data actually lives
Native Linux location
The first obvious place to look was the native Linux-style directory:
~/.local/share/Larian Studios/Baldur's Gate 3/PlayerProfiles/Public/
It contains the usual:
Savegames/Story/
profile8.lsf
The problem: while running the game through Proton, this was not the directory being updated by the active session. So, cool, one copy found. Just not the one I cared about.
Proton save location
The live Proton profile was under Steam’s compatdata prefix:
~/.local/share/Steam/steamapps/compatdata/1086940/pfx/drive_c/users/steamuser/AppData/Local/Larian Studios/Baldur's Gate 3/PlayerProfiles/Public/
The important files are again:
Savegames/Story/
profile8.lsf
Honour Mode runs live inside a directory matching:
*__HonourMode
with the actual save stored as:
HonourMode.lsv
During the test, saving in-game immediately changed the HonourMode.lsv under this Proton path. That made this the primary source for the current session.
Steam userdata / remote copy
There was also another copy under Steam userdata:
~/.local/share/Steam/userdata/<STEAM_USER_ID>/1086940/remote/_SAVE_Public/Savegames/Story/
and the corresponding profile:
~/.local/share/Steam/userdata/<STEAM_USER_ID>/1086940/remote/_PROFILE_Public/profile8.lsf
That copy was changing too. So by the end of the test I had three distinct places worth keeping mentally separate:
native Linux profile
Proton profile
Steam userdata / remote copy
The main lesson here was mostly: do not assume the first plausible BG3 directory you find is the one your current session is actually using. :)
Verification
What the test confirmed
What actually changed
After launching BG3 through Proton and saving the Honour run, I compared the relevant locations. What I observed:
- the Proton
HonourMode.lsvchanged; - the Steam userdata copy changed as well;
- the old native Linux directory did not;
profile8.lsfexisted separately in the native, Proton, and Steam userdata locations.
That was enough to identify the Proton copy as the live source I wanted my scripts to treat as authoritative.
Why I also back up profile8.lsf
Backing up only the __HonourMode directory sounds reasonable at first, and for ordinary save-state recovery it may be enough. Honour Mode is slightly more annoying (like the actual game difficulty).
After a total party kill, some relevant run state can also be reflected in the profile, so my backup set includes both:
HonourMode.lsv
profile8.lsf
and, when they exist, the Steam userdata copies too. The script refuses to create a backup if the Proton save or Proton profile is missing:
[[ -f "$TEMP_DIR/proton/$run_name/HonourMode.lsv" ]] || {
echo "ERROR: Backup verification failed: proton save copy is missing HonourMode.lsv" >&2
exit 1
}
[[ -f "$TEMP_DIR/proton/profile8.lsf" ]] || {
echo "ERROR: Backup verification failed: proton profile copy is missing profile8.lsf" >&2
exit 1
}
Nothing fancy, but I wanted the backup command to fail loudly rather than happily produce a directory that only looks useful.
Tooling
How backup and restore work
I ended up with two Bash scripts:
bg3-honour-backup.sh
bg3-honour-restore.sh
They are intentionally boring. The interesting part is mostly the amount of defensive checking around what would otherwise just be a couple of cp commands.
bg3-honour-backup.sh
The backup script first refuses to run while BG3 / Wine / Proton-related processes appear to be active:
is_game_running() {
pgrep -af 'bg3|bg3_dx11|bg3\.exe|baldur.?s gate 3|wine|wineserver|wine-preloader' >/dev/null 2>&1
}
Then it looks through the Proton save directory for Honour Mode runs and selects the one whose HonourMode.lsv was modified most recently:
latest_honour_dir() {
local best_dir=""
local best_mtime=""
local dir lsv mtime
while IFS= read -r -d '' dir; do
lsv="$dir/HonourMode.lsv"
[[ -f "$lsv" ]] || continue
mtime=$(stat -c '%Y' "$lsv")
if [[ -z "$best_mtime" || "$mtime" -gt "$best_mtime" ]]; then
best_mtime="$mtime"
best_dir="$dir"
fi
done < <(
find "$PROTON_STORY" -mindepth 1 -maxdepth 1 -type d -name '*__HonourMode' -print0
)
[[ -n "$best_dir" ]] || return 1
printf '%s\n' "$best_dir"
}
The backup is built in a temporary directory first:
TEMP_DIR=$(mktemp -d "$BACKUP_ROOT/.${backup_id}.tmp.XXXXXX")
mkdir -p "$TEMP_DIR/proton" "$TEMP_DIR/steam_remote"
cp -a "$run_dir" "$TEMP_DIR/proton/"
cp -a "$PROTON_PROFILE" "$TEMP_DIR/proton/"
Only after the copies are verified does the script move the temporary directory into its final location:
mv -- "$TEMP_DIR" "$final_dir"
TEMP_DIR=""
That also means a failed copy does not leave behind a half-finished backup that looks complete.
The backup includes a small metadata.txt file recording things such as creation time, source paths, modification times, and whether Steam remote copies were present.
bg3-honour-restore.sh
The restore side is the one I cared about being extra cautious with. Before touching the live files, it creates a snapshot of the current state:
SNAPSHOT_DIR="$SNAPSHOT_ROOT/${now}__${run_name}"
mkdir -p "$SNAPSHOT_DIR/proton" "$SNAPSHOT_DIR/steam_remote"
if [[ -e "$PROTON_TARGET" ]]; then
mv -- "$PROTON_TARGET" "$SNAPSHOT_DIR/proton/"
fi
if [[ -f "$PROTON_PROFILE" ]]; then
mv -- "$PROTON_PROFILE" "$SNAPSHOT_DIR/proton/"
fi
The replacement files are copied into temporary paths and verified before being moved into place:
proton_tmp_dir="$PROTON_STORY/.${run_name}.restore_tmp.$$"
proton_profile_tmp="$PROTON_PUBLIC/.profile8.lsf.restore_tmp.$$"
cp -a -- "$proton_run_dir" "$proton_tmp_dir"
cp -a -- "$proton_backup_profile" "$proton_profile_tmp"
[[ -f "$proton_tmp_dir/HonourMode.lsv" ]] || {
echo "ERROR: Proton temp restore missing HonourMode.lsv." >&2
exit 1
}
[[ -f "$proton_profile_tmp" ]] || {
echo "ERROR: Proton temp restore missing profile8.lsf." >&2
exit 1
}
mv -- "$proton_tmp_dir" "$PROTON_TARGET"
mv -- "$proton_profile_tmp" "$PROTON_PROFILE"
And if something goes wrong after the old live state has been moved away, the error trap attempts to put it back:
rollback() {
local rc=$?
[[ "$ROLLBACK_READY" -eq 1 ]] || exit "$rc"
rm -rf -- "$PROTON_TARGET"
if [[ -d "$SNAPSHOT_DIR/proton/$(basename "$PROTON_TARGET")" ]]; then
mv -- "$SNAPSHOT_DIR/proton/$(basename "$PROTON_TARGET")" "$PROTON_STORY/"
fi
if [[ -f "$SNAPSHOT_DIR/proton/profile8.lsf" ]]; then
mv -- "$SNAPSHOT_DIR/proton/profile8.lsf" "$PROTON_PROFILE"
fi
echo "ERROR: Restore failed. Previous live files were rolled back from $SNAPSHOT_DIR" >&2
exit "$rc"
}
That was the part I cared about most. A restore utility that destroys the current state while failing to restore the old one would be a pretty funny outcome for a “safety” script.
Intended workflow
With BG3 fully closed:
./bg3-honour-backup.sh
To inspect available backups:
./bg3-honour-restore.sh --list
And to restore the latest one:
./bg3-honour-restore.sh --latest
The restore script also asks for confirmation unless --yes is explicitly provided. The versions published here use local configuration values that should be reviewed before running them on another machine. In particular, the Steam user ID and backup location are machine-specific.
Takeaways
What I learned
What I ended up trusting
For the setup I tested, the Proton profile under:
steamapps/compatdata/1086940/pfx/...
was the live source for the current game session. The native Linux directory under:
~/.local/share/Larian Studios/...
was a separate copy and should stay separate rather than being casually mixed into the Proton workflow. The Steam userdata copy was useful as another state worth capturing, but the Proton files were the main source I used to identify the active run.
The actual useful part
The interesting result was not really “here are two Bash scripts”.
It was getting to a workflow where I knew:
- which save BG3 was actually updating;
- which profile file needed to travel with it;
- that the game was closed before touching anything;
- that a backup was verified before being accepted;
- that a restore preserved the previous live state first;
- that a failed restore had something to roll back to.
Pretty small experiment, but exactly the kind of annoying thing I would rather test once than discover during a dead Honour run. And yes, technically the best Honour Mode backup strategy is still “don’t die”. Working on it.