
Cloudward
[ALPHA] Sync your Outward saves across devices via a mounted/shared folder: boot-time reconcile, offline-first play, fork detection with a pause-and-resolve flow, and a crash-safe session lock. Not yet live-verified — see README.Cloudward — sync your saves across devices
📖 Full documentation: Cloudward wiki page
⚠️ Alpha, not yet live-verified. The design is complete and the compute layer (mount guard, fork
detection, lock/lease, reconciliation) is fully unit-tested, but this mod hasn't been run through a
real multi-device session yet. It is inert by default (Enable=false) — installing it changes
nothing until you turn it on. Back up your SaveGames/ folder before your first real sync.
Outward has no built-in way to carry a save between two computers — Steam Cloud is deliberately unsupported for it. Cloudward fills that gap: point it at a folder mounted on every device you play on (a NAS share, a Syncthing folder, a mapped drive) and it keeps your character in sync.
Requires: BepInEx 5 (Outward's Mono branch — see Compatibility) and ForgeKit. No SideLoader.
Installing
Install from Thunderstore and the dependency comes with it. Installing by hand: drop ForgeKit/ and
Cloudward/ side by side into BepInEx/plugins/. If BepInEx logs a dependency error and refuses to
load Cloudward, ForgeKit is missing.
How it works
The game always writes to its own local save folder — Cloudward never redirects that. The mount you point it at is a synced replica, not the source of truth:
- At boot, Cloudward reconciles: it pulls in anything newer on the share before the character list loads, so a character you played on another device shows up here.
- While you play, completed save snapshots push out to the share once you're online and holding the sync lock.
- Offline just works. No mount, no marker, or another device holding the lock — Cloudward plays entirely from local and catches up next time it can reach the share.
A marker file in the mount root is how Cloudward tells "actually mounted" from "empty directory
because the share isn't connected right now" — an unmounted mountpoint looks exactly like an empty
folder to your OS, so without the marker a sync could silently start writing to local disk instead of
the share. Cloudward creates the marker itself on first run (config-only setup), remembering it did so
per mount path — so a later launch with the share disconnected is correctly read as "offline", not
re-marked onto the placeholder. A share that already holds real save data always has its marker
restored. Set AutoCreateMarker=false for strict manual setup, where syncmarker is required.
A session lock (a small heartbeat file on the share) makes sure only one device pushes at a time. It's crash-safe: a lock whose heartbeat goes stale (the device crashed, lost power, whatever) is reclaimed automatically — nothing can wedge your saves.
If two devices genuinely diverge
If the same character was played offline on two devices before either could sync, Cloudward resolves
it automatically per the [Sync] ForkResolution setting — and always backs up the version it
doesn't keep to .cloudward-forks/, so nothing is ever lost:
- NewestWins (default) — keep whichever device has the most recent in-game save.
- ThisDevice / OtherDevice — always keep this / the other device's version.
- KeepBoth — keep this device's version live and preserve the other (v1: a restorable backup; a true second save slot is planned for v2).
- AskMe — don't guess; wait for you to run
syncresolve local|share|both.
syncresolve is always available as a manual override.
Setup
-
Mount the same shared folder on every device you play Outward on (NFS/SMB/autofs on Linux or Deck; a mapped drive or UNC path on Windows; a local Syncthing folder also works). Low-latency is best — a LAN share or local sync folder, not something with multi-second round trips.
-
Launch Outward once so Cloudward generates its config file, then quit. The file is:
BepInEx/config/cobalt.cloudward.cfg(Outward and BepInEx rewrite this file when the game exits, so edit it only while the game is closed — changes made while it's running are overwritten on quit.)
-
Open that file and set two keys in the
[Sync]section —EnableandMountPath. Example:[Sync] Enable = true MountPath = Z:/mnt/nas/outwardMountPath format: Outward runs under Proton on Linux and the Steam Deck, so the path is a Wine drive path —
Z:maps to the filesystem root/. For a share mounted at/mnt/nas/outwardyou'd writeMountPath = Z:/mnt/nas/outward. On native Windows, use a normal path or UNC, e.g.MountPath = Z:\OutwardSyncorMountPath = \\NAS\OutwardSync. Use the same shared folder (and the same Steam account) on every device → they all resolve to oneSaveGames/<SteamID>folder on the share. -
Relaunch. That's it — Cloudward auto-creates the mount marker on first run. (Prefer to bless the share by hand? Set
AutoCreateMarker = falseand run thesyncmarkercommand, ortouch <mount>/.outward-sync-root, once.)
To choose how conflicts resolve, set [Sync] ForkResolution (see the settings table below) — e.g.
ForkResolution = ThisDevice. The default, NewestWins, is usually what you want.
Settings
BepInEx/config/cobalt.cloudward.cfg, created on first launch:
| Setting | Default | What it does |
|---|---|---|
[Sync] Enable |
false |
Master switch. Off = vanilla local-only saves. |
MountPath |
"" |
Root of the shared/mounted folder to sync through. Empty = inert. |
MarkerFileName |
.outward-sync-root |
Sentinel file that marks the share as actually mounted. |
AutoCreateMarker |
true |
Auto-create the marker so config-only setup works (just set MountPath). Created once per mount path and remembered, so an absent marker on a later launch reads as "disconnected → offline" rather than being re-stamped onto a placeholder; a share that already holds real data always gets its marker restored. false = strict manual mode (syncmarker required). |
OnMountDown |
FallbackLocal |
What to do if the mount isn't live at launch. FallbackLocal = play local + warn; RefuseAndLog = same, but log louder (useful for headless/dedicated sessions). |
OnLockHeld |
FallbackLocal |
What to do about pushing when another device holds a live lock (pulls always happen regardless). FallbackLocal = defer pushes and accumulate locally until it frees (safe default); ForceTake = push anyway — only if you're certain the other device is dead. |
ForkResolution |
NewestWins |
How a diverged character (played offline on two devices) is auto-resolved: NewestWins = keep the most recent in-game save; ThisDevice / OtherDevice = always keep this / the other device's version; KeepBoth = keep this device's live and preserve the other (v1: a restorable backup); AskMe = wait for a manual syncresolve. The version not kept is always backed up to .cloudward-forks/. |
[Lock] HeartbeatSeconds |
30 |
How often the held lock's heartbeat refreshes (and the mount gets re-checked). |
StaleSeconds |
180 |
A lock heartbeat older than this is dead and reclaimable by any device — crash recovery. |
DeviceName |
your hostname | This device's name in the lock file. Must be unique per device — own-device locks reclaim instantly, so two devices sharing a name would never block each other. |
Example configuration
BepInEx/config/cobalt.cloudward.cfg — created on first launch. Excerpt (defaults; set at least
Enable and MountPath to turn it on):
[Sync]
## Master switch. Off = vanilla local-only saves.
Enable = false
## Root of the shared/mounted folder to sync through. Empty = inert.
MountPath =
## Sentinel file that marks the share as actually mounted.
MarkerFileName = .outward-sync-root
AutoCreateMarker = true
## FallbackLocal | RefuseAndLog
OnMountDown = FallbackLocal
## FallbackLocal | ForceTake
OnLockHeld = FallbackLocal
## NewestWins | ThisDevice | OtherDevice | KeepBoth | AskMe
ForkResolution = NewestWins
[Lock]
HeartbeatSeconds = 30
StaleSeconds = 180
## This device's name in the lock file; defaults to your hostname. Must be unique per device.
DeviceName = <your hostname>
The dev command channel is BepInEx/config/Cloudward_cmd.txt (see the verb table below). Cloudward
ships no config-override data tables.
Without any settings changes
Everything is also driven by writing a line into BepInEx/config/Cloudward_cmd.txt — it runs on the
next poll, even while the game is paused.
| Verb | What it does |
|---|---|
syncstatus |
Dump sync state: mode, local/share paths, lock holder, pending forks. |
syncnow |
Force a reconcile + push right now (pushes only — a pull needs the next launch). |
syncresolve <local|share|both> [uid] |
Resolve a paused fork. |
synclock / synclock release |
Show the current lock holder / push-and-release your own. |
syncmarker |
Manually stamp the mount-liveness marker into MountPath (optional — auto-created unless AutoCreateMarker=false). |
selftest |
Sanity-check the install; look for [SELFTEST] … DONE in the log. |
Compatibility
Outward must be on its Mono Steam beta branch, not the default IL2CPP build (Properties → Betas →
select mono). If the game runs but no BepInEx mods load and there's no crash log, this is almost
always why.
License
Apache License 2.0 — see LICENSE. You may use, modify, and redistribute this mod (including in
commercial mods) provided you keep the copyright/license notice; see the license text for the full
terms.