diff --git a/.gitignore b/.gitignore
index dc0470a..e043c9f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -203,6 +203,11 @@ PublishScripts/
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
+# and except a Unity project's Packages/, which is source: Unity's package manifest and its
+# resolved lock file are both meant to be committed, and a NuGet restore folder never contains
+# a file by either name.
+!**/[Pp]ackages/manifest.json
+!**/[Pp]ackages/packages-lock.json
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
@@ -651,3 +656,16 @@ Temporary Items
# ImGui.ini files
imgui.ini
+
+# Game engine projects
+#
+# Godot: the import cache, and the mono/temp bin+obj a C# build writes.
+.godot/
+
+# Unity: .meta files are source, not the Visual Studio C++ build artifact that the `*.meta` rule
+# further up targets. Unity generates one per asset and it carries the GUID that scenes, prefabs
+# and serialized references point at, so ignoring them gives every clone fresh GUIDs and silently
+# breaks those references - including for a plug-in whose .dll is itself a build output. This
+# negation has to come after that rule to win, and is scoped to the asset tree so the Visual
+# Studio artifact stays ignored everywhere else.
+!**/[Aa]ssets/**/*.meta
diff --git a/AppDataStorage.Test/AppDataTests.cs b/AppDataStorage.Test/AppDataTests.cs
index 4ee2a36..ff096f1 100644
--- a/AppDataStorage.Test/AppDataTests.cs
+++ b/AppDataStorage.Test/AppDataTests.cs
@@ -988,6 +988,48 @@ public void TestQueueSaveStaticMethod()
Assert.IsGreaterThanOrEqualTo(beforeQueueTime, appData.SaveQueuedTime, "SaveQueuedTime was not set correctly.");
}
+ // ktsu-dev/AppDataStorage#330: the docs describe LoadOrCreate() and Get() as separate instances, and
+ // the static QueueSave()/SaveIfRequired() as acting on Get() only. These pin that contract, and that
+ // each documented save path reaches disk.
+ [TestMethod]
+ [DoNotParallelize]
+ public void TestLoadOrCreateReturnsAnInstanceSeparateFromGet()
+ {
+ TestAppData singleton = TestAppData.Get();
+ TestAppData copy = TestAppData.LoadOrCreate();
+
+ Assert.AreNotSame(singleton, copy, "LoadOrCreate() is documented as returning a new instance, not the Get() singleton.");
+ Assert.AreEqual(singleton.FilePath, copy.FilePath, "Both instances are documented as using the same file.");
+ }
+
+ [TestMethod]
+ [DoNotParallelize]
+ public async Task TestEditOnGetSavedThroughStaticQueueSaveReachesDisk()
+ {
+ TestAppData settings = TestAppData.Get();
+ settings.Data = "edited through Get";
+
+ TestAppData.QueueSave();
+ await Task.Delay(settings.SaveDebounceTime + TimeSpan.FromMilliseconds(100)).ConfigureAwait(false);
+ TestAppData.SaveIfRequired();
+
+ Assert.Contains("edited through Get", await AppData.FileSystem.File.ReadAllTextAsync(settings.FilePath).ConfigureAwait(false));
+ }
+
+ [TestMethod]
+ [DoNotParallelize]
+ public async Task TestEditOnLoadOrCreateSavedThroughItsOwnQueueSaveReachesDisk()
+ {
+ using TestAppData settings = TestAppData.LoadOrCreate();
+ settings.Data = "edited through LoadOrCreate";
+
+ settings.QueueSave();
+ await Task.Delay(settings.SaveDebounceTime + TimeSpan.FromMilliseconds(100)).ConfigureAwait(false);
+ settings.SaveIfRequired();
+
+ Assert.Contains("edited through LoadOrCreate", await AppData.FileSystem.File.ReadAllTextAsync(settings.FilePath).ConfigureAwait(false));
+ }
+
[TestMethod]
[DoNotParallelize]
public async Task TestSaveIfRequiredStaticMethod()
diff --git a/AppDataStorage/AppData.cs b/AppDataStorage/AppData.cs
index 0abca13..067097d 100644
--- a/AppDataStorage/AppData.cs
+++ b/AppDataStorage/AppData.cs
@@ -686,6 +686,15 @@ private static T LoadOrCreate(AbsoluteDirectoryPath? root, RelativeDirectoryPath
///
/// Loads the app data from the file system or creates a new instance if the file does not exist.
///
+ ///
+ /// Every call reads the file again and returns a new instance. It is not the instance
+ /// returns, even though both use the same file, so an edit made on one is not seen by the other and
+ /// whichever saves last overwrites the file. The static and
+ /// act on , so they do not save this instance: call
+ /// ,
+ /// or on it instead. Use throughout when the application wants
+ /// one shared copy.
+ ///
/// The loaded or newly created app data instance.
public static T LoadOrCreate() => LoadOrCreate(subdirectory: null, fileName: null);
@@ -704,12 +713,17 @@ private static T LoadOrCreate(AbsoluteDirectoryPath? root, RelativeDirectoryPath
public static T LoadOrCreate(FileName? fileName) => LoadOrCreate(subdirectory: null, fileName);
///
- /// Queues a save operation for the current app data instance.
+ /// Queues a save operation for the singleton.
///
+ ///
+ /// This does not save an instance returned by or its overloads, which are
+ /// separate copies. Call the instance's own QueueSave() for those.
+ ///
public static void QueueSave() => Get().QueueSave();
///
- /// Saves the app data if required based on the debounce time and the last save time.
+ /// Saves the singleton if a save is queued and the debounce time has passed.
///
+ ///
public static void SaveIfRequired() => Get().SaveIfRequired();
}
diff --git a/CLAUDE.md b/CLAUDE.md
index d591995..06bc7c0 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -51,10 +51,14 @@ public class MyAppData : AppData
public string Setting { get; set; } = "default";
}
-// Load or create, then use static accessor
-var data = MyAppData.LoadOrCreate();
-var same = MyAppData.Get(); // Singleton-like access
-data.Save();
+// The shared instance: static QueueSave()/SaveIfRequired() act on this one
+var data = MyAppData.Get();
+data.Setting = "changed";
+MyAppData.QueueSave();
+
+// A separate copy, read from the same file: not the Get() instance, so save it through itself
+var copy = MyAppData.LoadOrCreate();
+copy.Save();
```
### Key Implementation Details
@@ -66,6 +70,7 @@ data.Save();
- Automatic backup files (`.bk` suffix) with timestamped collision handling
- Safe write pattern: write to `.tmp`, backup existing, move `.tmp` to final
- Lazy singleton access via `Get()` backed by `Lazy`
+- `LoadOrCreate()` reads the file into a new instance on every call and never returns the `Get()` singleton, so the two do not see each other's edits. The static `QueueSave()` / `SaveIfRequired()` act on `Get()` only
- `LoadOrCreate()` overloads support custom subdirectories and file names; `LoadOrCreateIn()` takes an absolute root directory
- Corrupt file recovery: falls back to `.bk` backup, then creates a fresh instance
- Process exit handler ensures queued saves are flushed via `IDisposable`
diff --git a/README.md b/README.md
index f099826..8a8f919 100644
--- a/README.md
+++ b/README.md
@@ -86,6 +86,12 @@ var sameSettings = MySettings.Get();
Console.WriteLine(sameSettings.Theme); // "dark"
```
+`Get()` and `LoadOrCreate()` are not the same object. `LoadOrCreate()` reads the file into a new instance
+every time it is called, so an edit made on that instance is not seen by `Get()`, and whichever of the two
+saves last overwrites the file. The static `MySettings.QueueSave()` and `MySettings.SaveIfRequired()` act on
+`Get()`. To save an instance from `LoadOrCreate()`, call `Save()`, `QueueSave()` or `SaveIfRequired()` on that
+instance. Use `Get()` throughout when the application wants one shared copy.
+
### Saving Data
Modify properties and call `Save()` to persist changes immediately.