Cloudward
Sync your Outward saves across devices via a mounted/shared folder: boot-time reconcile, offline-first play, fork detection/resolution, crash-safe session lock. Verified across real devices; some offline/crash-recovery edge cases still being staged.
| Last updated | 3 weeks ago |
| Total downloads | 178 |
| Total rating | 0 |
| Categories | Utility Misc |
| Dependency string | CeruleanCutlass-Cloudward-0.2.0 |
| Dependants | 0 other packages depend on this package |
This mod requires the following mods to function
CeruleanCutlass-ForgeKit
Dependency-free dev-tooling for Outward BepInEx mods: file-driven dev command loop, self-test harness, on-screen toasts, player-ready lifecycle wait, embedded/override table loaders, and a shared dev-verb pack (movement/combat/skill/status probes).
Preferred version: 0.4.10README
Cloudward — sync your saves across devices
📖 Full documentation: Cloudward wiki page
Actively in use across real devices. The compute layer (mount guard, fork detection, lock/lease,
reconciliation) is fully unit-tested, and the full sync loop — boot-time reconcile, offline play,
clean two-device handoff, and both paused and auto fork resolution — has been live-verified across
real physical machines over a real NAS share, including three bugs found, fixed, and re-verified in
that process. Still being staged: some offline/reconnect edge cases and crash-recovery scenarios
that need a human at the keyboard to exercise. 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 when it can positively confirm the
folder is a real mount — the share already holds save data, or the platform probe sees it in the
mount table (native Linux /proc/mounts, or a Windows network drive / UNC path). It remembers doing
so per mount path, so a later launch with the share disconnected is correctly read as "offline",
not re-marked onto the placeholder. Under Proton (Linux / Steam Deck) an empty first-run share can
never be confirmed — the game-side process only sees Wine drive paths, so the probe can't reach the
mount table. That's expected: Cloudward stays offline, logs the instruction, and you run the
syncmarker command once (or touch the marker file) to bless the share; after that everything is
automatic. Set AutoCreateMarker=false for strict manual setup, where syncmarker is always
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. On native Windows (network drive / UNC) or where the share already holds data, Cloudward auto-creates the mount marker on first run and you're done. On Proton (Linux / Steam Deck), an empty first-run share can't be auto-confirmed — the log will say so and stay offline; run the
syncmarkercommand once (writesyncmarkerintoBepInEx/config/Cloudward_cmd.txtwhile the game runs), ortouch <mount>/.outward-sync-rootby hand, and every later launch is automatic.
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 when the share is positively confirmable: it already holds real data, or the platform probe sees a real mount (native-Linux /proc/mounts, Windows network drive / UNC). An ambiguous empty dir stays offline with a one-time log hint instead of being stamped — under Proton the probe can't see the mount table, so an empty first-run share always takes the one-time syncmarker path. Created once per mount path and remembered, so an absent marker later reads as "disconnected → offline", never re-stamped onto a placeholder. false = strict manual mode (syncmarker always 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. |
Syncing your mods and settings too ([Payload], optional, off by default)
Cloudward can also carry your mod folders and mod configuration between the same devices, so
a character doesn't arrive on a machine running last month's build. It is off by default: this
rewrites BepInEx/plugins, so it should be a decision, not something that happens because you
turned save sync on. The first boot after you enable it only reports what it would do.
⚠ This tier is NOT live-verified. The "actively in use across real devices" line at the top of this README describes the save-sync tier only.
[Payload]shipped in 0.2.0 and every item ondocs/cloudward-payload-testplan.md(P1–P16) is still open — including the full round trip (P3), rollback recovery (P7), the corrupted-file refusal (P9), the privacy gate (P10) and.cloudward-disable(P12). Everything below describes what it is built to do, not what has been observed to work. Have a backup ofBepInEx/pluginsbefore you turn it on.
How it behaves, in short:
- It only does work when something actually changed. Each launch it walks the tree and compares a size+mtime signature; unchanged means zero files are hashed and nothing is copied.
- Incoming updates apply at the NEXT launch, not this one. BepInEx loads mods at startup, so a
running game cannot swap them. Cloudward downloads the update, verifies every file's checksum, and
a small preloader component (
BepInEx/patchers/Cloudward.Preload.dll) installs it just before mods load next time you start the game. If anything fails verification, the update is discarded and your install is left exactly as it was. - Config sync is an allowlist, not a file copy. Only the keys named in
BepInEx/plugins/Cloudward/overlays/*.cfg.overlaytravel. Everything else in your.cfgfiles — includingMountPath,DeviceName, your keybinds, resolution and controller bindings — is host-local and its bytes are never rewritten. - Your saves, logs, and per-character mod data never leave via this tier, whatever else is in those folders.
- If both devices changed, it stops and waits for you (
payloadresolve) rather than guessing. The replaced version is always backed up first;payloadrollbackputs it back. - Emergency stop that doesn't need the game: create an empty file called
.cloudward-disablein your Outward game folder and all payload activity halts, including the part that runs at startup.
| Setting | Default | What it does |
|---|---|---|
[Payload] Enable |
false |
Master switch for mod + config sync. Needs [Sync] Enable and a MountPath as well. |
SyncPlugins |
true |
Sync the BepInEx/plugins tree (whole folders, including the data files mods ship beside their DLL). |
SyncConfig |
true |
Sync the config keys named in plugins/Cloudward/overlays/*.cfg.overlay, and only those. |
Role |
Both |
Both = publish and adopt. Publisher = never adopt. Subscriber = never write to the share (use this on someone else's machine). |
ApplyMode |
Auto |
Auto = a downloaded update installs itself at the next launch. StageOnly = download it and wait for payloadapply. |
OnConflict |
Skip |
Both sides changed. Skip = touch nothing and wait for payloadresolve; PreferLocal / PreferShare decide automatically at boot. |
PushAtQuit |
false |
Also publish on quit. Off by default: quitting has a 15-second budget and a plugins folder is large. Publishing normally happens at boot. |
ExtraExcludes |
"" |
Extra ;-separated patterns to keep off the share. Additive only — the built-in exclusions can't be switched off. |
MaxPayloadMegabytes |
2048 |
Refuse to publish a tier bigger than this (a sudden jump almost always means a mis-set folder). |
KeepBackups |
3 |
How many pre-replace backups to keep per tier. |
[Payload]
## Master switch for mod + config sync. Requires [Sync] Enable and MountPath.
Enable = false
SyncPlugins = true
SyncConfig = true
## Both | Publisher | Subscriber
Role = Both
## Auto | StageOnly
ApplyMode = Auto
## Skip | PreferLocal | PreferShare
OnConflict = Skip
PushAtQuit = false
ExtraExcludes =
MaxPayloadMegabytes = 2048
KeepBackups = 3
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. |
payloadstatus |
Dump mod/config sync state: tiers, generations, what's staged, conflicts, backups. |
payloadscan [plugins|config] |
Re-check from cold, ignoring the size+mtime shortcut. |
payloadpush [plugins|config] |
Publish this device's mods/config now. |
payloadstage [plugins|config] |
Download the share's version now, ready for the next launch. |
payloadresolve <local|share> [tier] |
Resolve a mod/config divergence. |
payloadrollback <plugins|config> [n] |
Restore the share from a backup (0 = newest). |
payloadapply |
Release an update held by ApplyMode = StageOnly. |
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.