-
-
Notifications
You must be signed in to change notification settings - Fork 2
Flags and saves
English | 日本語
This is the Flags and saves library, DragNWash.ModFramework.Saves.dll, in the namespace DragNWash.ModFramework.Saves. It reads the game's save slots and can change their level and flags. It also keeps a history of every version of each save, so you can undo any change.
[BepInDependency(GameSaves.Guid, BepInDependency.DependencyFlags.HardDependency)]The game keeps one save per slot at <persistentDataPath>/<steamid>_slot<N>/savegame.dgn and overwrites it every time it saves. The library copies the file into BepInEx/SaveHistory/<slot>/ whenever it changes (players can turn that off in its settings). It always makes a copy before an edit made through the library, though.
Each copy is named by the second it was taken, like 20260923-142501.dgn. From 1.5.0, a second copy in the same second (an edit, then the game saving right after) is named 20260923-142501-2.dgn, then -3 and so on, so both are kept. Before, the second one was copied over the first, and the save from before the edit was lost. A slot keeps [History] Keep copies (30 unless the player changes it), and once it has that many, each new copy deletes the oldest one. A copy is only taken when the save differs from the newest copy.
An edit only rewrites the file. The game reads it when a slot is loaded, so to see an edit, the player has to go back to the title screen and load the slot. If they save in game after that, the file gets overwritten again.
foreach (string slot in GameSaves.Slots()) // most recently written first
{
int level = GameSaves.ReadLevel(slot); // -1 when unreadable
List<SaveFlag> flags = GameSaves.ReadFlags(slot); // in file order
Logger.LogInfo($"{GameSaves.ShortName(slot)}: level {level}, {flags.Count} flags");
}
string message = GameSaves.SetFlag(MyMod.Guid, slot, "level_1_complete", true);
ToolWindow.ShowNotice(message); // every edit returns a message for the playerFrom 1.5.0, an edit that changes nothing (a level the save already has, flags already set that way) takes no copy first and returns "Nothing changed (...)". With the history full, that copy used to push the oldest one out for nothing.
| Member | What it does |
|---|---|
List<string> Slots() |
Slot folder names that hold a save |
string ShortName(string slot) |
slot 1 for 76561198000000000_slot1
|
string SavePath(string slot) |
Full path of the slot's savegame.dgn
|
string SaveFolder |
Where the game keeps its slots |
int ReadLevel(string slot) |
The level index |
List<SaveFlag> ReadFlags(string slot) |
SaveFlag has Id and Value
|
string SetLevel(string owner, string slot, int level) |
Sets the level |
string SetFlag(string owner, string slot, string id, bool value) |
Sets one flag, adding it if the game never set it |
string SetFlags(string owner, string slot, IEnumerable<KeyValuePair<string, bool>> values, string description) |
Several flags in one edit (one snapshot, one write) |
event Action<string> SaveWritten |
The game wrote a slot with new content (main thread) |
List<SaveSnapshot> history = GameSaves.Snapshots(slot); // newest first
string message = GameSaves.Restore(slot, history[1]); // the replaced save is snapshotted first
// Since 1.5.0: say when the next change will push the oldest copy out.
string kept = $"{history.Count} of {GameSaves.Keep} kept";
bool full = history.Count >= GameSaves.Keep;Keep is how many copies a slot keeps, the library's [History] Keep setting, so a tab can show "30 of 30 kept" and warn before a change pushes the oldest copy out. It's right even while [History] Enabled is off. An edit or a restore while the newest copy already matches the save takes no copy, so it deletes nothing.
SnapshotMatchesSave is how a tab marks which copy is the save right now. From 1.5.0 it also says true for a copy from before the game update of 2026-09-14 once it has been restored. Restore adds the {"version":1} entry the game needs now, so the save and that old copy were never byte for byte the same, and a tab couldn't mark it as the current save.
Also from 1.5.0, restoring the oldest copy works when the history is full. Before a restore, the save being replaced is kept as a copy, and with the history full that pushed the oldest one out. When that was the very copy being restored, the restore failed with "Restore failed" and the copy was gone. Now the copy is read first. The current save was never at risk.
| Member | What it does |
|---|---|
string HistoryFolder |
BepInEx/SaveHistory |
List<SaveSnapshot> Snapshots(string slot) |
SaveSnapshot has Path, Taken, Level and Label
|
int Keep |
How many copies a slot keeps, from [History] Keep (1.5.0) |
bool SnapshotMatchesSave(string slot, SaveSnapshot snapshot) |
True when the snapshot holds what the save holds now. A pre-2026-09-14 copy matches the save restored from it (1.5.0) |
string Restore(string slot, SaveSnapshot snapshot) |
Puts a snapshot back; can itself be undone |
int ImportHistory(string folder) |
Moves an older history folder of your own into HistoryFolder
|
The game only lists flags that have been set at least once. So the library keeps a catalog of known flags, which lets it show the unset ones too.
GameFlags.AddCatalog(Path.Combine(dir, "MyFlags.csv")); // columns: id,group,set_by,description
FlagInfo info = GameFlags.Find("level_1_complete");| Member | What it does |
|---|---|
IReadOnlyList<FlagInfo> Catalog |
Every catalogued flag. FlagInfo has Id, Group, SetBy, Description
|
FlagInfo Find(string id) |
The entry, or null |
void AddCatalog(string csvPath) |
Adds a catalog file; the first row for an id wins |
void Reload() |
Reads the catalog files again |
CsvReader is the reader the flag catalog uses, and now any mod can use it. ReadRows(path) gives you each data row as a dictionary keyed by the header row's column names (case doesn't matter). It handles quoted fields, "" inside quotes, and commas and line breaks inside quotes. It opens the file as a shared read, so a translator can keep it open in Excel or an editor while the game runs. A line that starts with # is a comment. Escape(value) makes one value safe to write back: it quotes a value with a comma, a quote or a line break, and one that starts with #, so it isn't read back as a comment. It comes from the localization mod.
foreach (Dictionary<string, string> row in CsvReader.ReadRows(path))
{
string id = row["id"];
}
writer.WriteLine(CsvReader.Escape(id) + "," + CsvReader.Escape(description));| Member | What it does |
|---|---|
IEnumerable<Dictionary<string, string>> ReadRows(string path) |
Each data row, keyed by the header's column names (1.5.0) |
string Escape(string value) |
One value as a CSV field, quoted when it needs to be (1.5.0) |
- Write
savegame.dgnyourself. Go through the library, so there's always a copy to go back to.
Players
Mod authors
- Getting started
- Playing well with others (the guide)
- Going online
- Installer
- Mod reload
- Overrides (no code)
- Graphs (no code, makes things happen)
Tools (F1, developer tools)
- Inspector
- Console
- Code graph
- Bridge (AI clients, MCP)
API
日本語
- ホーム
- プレイヤー向け · ランチャー · FAQ · クラッシュレポート
- はじめての Mod · ほかの Mod と一緒に動かす · 外と通信する Mod · インストーラー · Mod の再読み込み · Overrides · Graphs
- Inspector · Console · コードのグラフ · Bridge
- 中核 API · 操作の登録簿 · Text · Dialogue · Tool window · Assets · Flags and saves · GameEvents · SettingMeta
Links
- Repository
- Releases
- Changelog
- Design records: DESIGN · ROADMAP · CONTENT_POLICY