Skip to content

Flags and saves ja

Tom_XV edited this page Sep 23, 2026 · 2 revisions

English | 日本語

Flags and saves ライブラリです。ファイルは DragNWash.ModFramework.Saves.dll、名前空間は DragNWash.ModFramework.Saves です。ゲームのセーブスロットを読んで、レベルやフラグを変えられます。セーブごとに全部の版を履歴に残しておくので、どんな変更でも元に戻せます。

[BepInDependency(GameSaves.Guid, BepInDependency.DependencyFlags.HardDependency)]

ゲームのセーブのしくみ

ゲームのセーブはスロットごとに 1 つで、<persistentDataPath>/<steamid>_slot<N>/savegame.dgn に置かれ、セーブするたびに上書きされます。ライブラリはこのファイルが変わるたびに BepInEx/SaveHistory/<スロット>/ へコピーを取っています(プレイヤーが設定でオフにもできます)。ただ、ライブラリを通して編集するときは、その前に 必ず コピーを取ります。

コピーの名前は取った時刻の秒までで、20260923-142501.dgn のようになります。1.5.0 からは、同じ秒に 2 つ目のコピーを取ったとき(編集して、すぐゲームがセーブしたときなど)は 20260923-142501-2.dgn、その次は -3 という名前になって、両方残ります。前は 2 つ目が 1 つ目に上書きされて、編集する前のセーブがなくなっていました。スロットごとに残すコピーは [History] Keep 個(プレイヤーが変えなければ 30)で、その数に達すると、新しいコピーを取るたびにいちばん古いものを消します。コピーを取るのは、セーブがいちばん新しいコピーと違うときだけです。

編集で書き換わるのはファイルだけです。ゲームがこのファイルを読むのはスロットをロードするときなので、編集をゲームに反映させるには、プレイヤーがタイトル画面に戻ってスロットをロードし直す必要があります。そのあとゲーム内でセーブすると、ファイルはまた上書きされます。

読む・書き換える

foreach (string slot in GameSaves.Slots())            // 最後に書き込まれた順
{
    int level = GameSaves.ReadLevel(slot);            // 読めなければ -1
    List<SaveFlag> flags = GameSaves.ReadFlags(slot); // ファイルの順
    Logger.LogInfo($"{GameSaves.ShortName(slot)}: level {level}, {flags.Count} flags");
}

string message = GameSaves.SetFlag(MyMod.Guid, slot, "level_1_complete", true);
ToolWindow.ShowNotice(message);   // 編集はすべてプレイヤー向けのメッセージを返す

1.5.0 からは、何も変わらない編集(セーブがもうそのレベルになっている、フラグがもうその値になっている)では先にコピーを取らず、「Nothing changed (...)」を返します。前は履歴がいっぱいのとき、そのコピーのせいで、いちばん古いコピーが意味もなく押し出されていました。

メンバー 内容
List<string> Slots() セーブが入っているスロットのフォルダ名
string ShortName(string slot) 76561198000000000_slot1 なら slot 1
string SavePath(string slot) スロットの savegame.dgn のフルパス
string SaveFolder ゲームがスロットを置いている場所
int ReadLevel(string slot) レベルの番号
List<SaveFlag> ReadFlags(string slot) SaveFlag は Id と Value を持つ
string SetLevel(string owner, string slot, int level) レベルを設定する
string SetFlag(string owner, string slot, string id, bool value) フラグを 1 つ設定する。ゲームが一度も設定していなければ追加する
string SetFlags(string owner, string slot, IEnumerable<KeyValuePair<string, bool>> values, string description) 複数のフラグを 1 回の編集で(コピー 1 回、書き込み 1 回)
event Action<string> SaveWritten ゲームがスロットに新しい内容を書き込んだ(メインスレッド)

履歴

List<SaveSnapshot> history = GameSaves.Snapshots(slot);   // 新しい順
string message = GameSaves.Restore(slot, history[1]);      // 置き換えられるセーブも先にコピーされる

// 1.5.0 から:次の変更でいちばん古いコピーが押し出されるかを見せる
string kept = $"{history.Count} of {GameSaves.Keep} kept";
bool full = history.Count >= GameSaves.Keep;

Keep はスロットごとに残すコピーの数で、ライブラリの [History] Keep の設定です。タブに「30 of 30 kept」と出して、変更でいちばん古いコピーが押し出される前に知らせる、といった使い方ができます。[History] Enabled がオフの間も正しい数を返します。いちばん新しいコピーがもうセーブと同じときの編集や戻しでは、コピーを取らないので何も消えません。

SnapshotMatchesSave は、どのコピーがいまのセーブかをタブで示すためのものです。1.5.0 からは、2026-09-14 のゲームのアップデートより前のコピーも、それを戻したあとなら true を返します。Restore はいまのゲームに要る {"version":1} の項目を足して戻すので、セーブとその古いコピーはバイト単位では同じにならず、タブがそのコピーをいまのセーブとして示せなかったからです。

もう 1 つ、1.5.0 から、履歴がいっぱいのときでもいちばん古いコピーを戻せます。戻す前に、置き換えられるセーブをコピーとして残しますが、履歴がいっぱいだとそのせいでいちばん古いコピーが押し出されていました。それがちょうど戻そうとしているコピーだと「Restore failed」で失敗して、そのコピーもなくなっていました。いまは先にコピーを読んでおくので、ちゃんと戻せます。いまのセーブが危なくなることは、前からありませんでした。

メンバー 内容
string HistoryFolder BepInEx/SaveHistory
List<SaveSnapshot> Snapshots(string slot) SaveSnapshot は Path、Taken、Level、Label を持つ
int Keep スロットごとに残すコピーの数。[History] Keep の値(1.5.0)
bool SnapshotMatchesSave(string slot, SaveSnapshot snapshot) コピーの中身がいまのセーブと同じなら true。2026-09-14 より前のコピーも、それを戻したセーブとは同じと見なす(1.5.0)
string Restore(string slot, SaveSnapshot snapshot) コピーを戻す。戻したこと自体も元に戻せる
int ImportHistory(string folder) 以前に Mod が独自に持っていた履歴フォルダを HistoryFolder に移す

フラグのカタログ

ゲームが一覧に持っているのは、一度でも設定されたことのあるフラグだけです。そこでライブラリの側で知っているフラグのカタログを持っておき、まだ設定されていないフラグも見せられるようにしています。

GameFlags.AddCatalog(Path.Combine(dir, "MyFlags.csv"));   // 列:id,group,set_by,description
FlagInfo info = GameFlags.Find("level_1_complete");
メンバー 内容
IReadOnlyList<FlagInfo> Catalog カタログのすべてのフラグ。FlagInfo は Id、Group、SetBy、Description を持つ
FlagInfo Find(string id) その id の項目。なければ null
void AddCatalog(string csvPath) カタログのファイルを足す。同じ id は最初の行が優先
void Reload() カタログのファイルを読み直す

CSV ファイルを読む(1.5.0)

CsvReader はフラグのカタログが使っている読み込み部分で、どの Mod からも使えるようになりました。ReadRows(path) は、データの行を 1 行ずつ、見出しの行の列名をキーにした辞書で返します(大文字と小文字は区別しません)。引用符で囲んだフィールド、引用符の中の ""、引用符の中のカンマや改行も読めます。ファイルは共有で開くので、翻訳する人が Excel やエディターで開いたままでもゲームから読めます。# で始まる行はコメントです。Escape(value) は 1 つの値を書き戻せる形にします。カンマや引用符、改行が入っている値と、# で始まる値は引用符で囲むので、読み直したときにコメントと間違われません。もとは Localization の Mod にあったものです。

foreach (Dictionary<string, string> row in CsvReader.ReadRows(path))
{
    string id = row["id"];
}

writer.WriteLine(CsvReader.Escape(id) + "," + CsvReader.Escape(description));
メンバー 内容
IEnumerable<Dictionary<string, string>> ReadRows(string path) データの行を 1 行ずつ、見出しの列名をキーにして返す(1.5.0)
string Escape(string value) 1 つの値を CSV のフィールドにする。要るときだけ引用符で囲む(1.5.0)

やめてほしいこと

  • savegame.dgn を自分で書き換えること。いつでも戻れるコピーが残るように、ライブラリを通してください

Clone this wiki locally