Scripting
Use a Custom Progress Store
Implement IProgressStore, install it before initialization, preserve its validation contract, and handle operation results.
Implement four exact-key operations
IProgressStore has exactly four operations: TryRead(string, out string), Write(string, string), Delete(string), and Flush(). The Manager supplies complete opaque keys. Do not parse, rename, enumerate, prefix again, or combine them with project slot semantics inside the adapter.
A missing key is a normal false read and must set the out value to an empty string. An empty stored string remains distinguishable because TryRead returns true. Null keys throw ArgumentNullException; empty or whitespace keys throw ArgumentException; Write also rejects a null value.
Install the provider before progression locks
Call ChapterManager.TrySetProgressStore(store) before the Manager first reads, writes, hydrates, clears, flushes, or starts progression. Success replaces the backend without copying, deleting, reading, writing, or flushing data. A null provider returns StoreUnavailable; a late call returns ManagerAlreadyInitialized.
An Inspector provider must be a MonoBehaviour implementing IProgressStore. The default when none is assigned is PlayerPrefsProgressStore. Store calls are made on Unity's main thread in V1, so the interface does not require thread safety.
Map backend failures without hiding them
- Write creates or overwrites one exact string but does not flush.
- Delete reports whether the exact key existed and does not flush.
- Flush commits pending backend operations but captures no additional progress.
- Backend exceptions become
ProgressOperationResult.StoreErrorat the Manager boundary. - Unsupported stored schema returns UnsupportedSchema; invalid stable IDs return InvalidIdentifier.
If the backend is asynchronous, buffer exact-key operations synchronously and make Flush define the project-approved durability boundary, or keep persistence outside this V1 adapter. Do not block the main thread on an unbounded network request.
Backend acceptance tests
- 1Verify missing and empty-string values remain distinguishable.
- 2Write twice to the same key and confirm the second exact value replaces the first.
- 3Delete an existing key and a missing key; confirm true then false.
- 4Confirm Write/Delete do not implicitly flush and one Flush commits the batch.
- 5Inject read, write, delete, and flush failures and require StoreError from public Manager operations.
- 6Run the persistence Play Mode scenario and verify no historical events replay after hydration.
Flush progress at a save boundary
- Where to place it
- Add this method to a project save coordinator or checkpoint controller.
- When to call it
- Call FlushAtSavePoint after accepted progression writes or before a controlled quit.
- Behavior
- FlushProgress flushes only the configured progression store; it does not serialize scenes, inventory or arbitrary GameObjects.
using System;
using System.Collections;
using System.Collections.Generic;
using OverFuture.ChapterObjectiveSystem.Chapters;
using OverFuture.ChapterObjectiveSystem.Events;
using OverFuture.ChapterObjectiveSystem.Objectives;
using OverFuture.ChapterObjectiveSystem.Persistence;
using OverFuture.ChapterObjectiveSystem.Progression;
using UnityEngine;
public sealed class FlushProgressExample : MonoBehaviour
{
public ProgressOperationResult FlushAtSavePoint()
{
ChapterManager manager = ChapterManager.Instance;
return manager != null
? manager.FlushProgress()
: ProgressOperationResult.StoreUnavailable;
}
}
Implement a custom IProgressStore
- Where to place it
- Place the store in the project's save assembly and pass an instance to TrySetProgressStore before Manager initialization locks configuration.
- When to call it
- The Manager calls exact-key operations from Unity's main thread; Flush is called only at explicit boundaries.
- Behavior
- The in-memory example demonstrates the contract but is not durable. Replace Flush and storage with the project backend without enumerating or rewriting opaque keys.
using System;
using System.Collections;
using System.Collections.Generic;
using OverFuture.ChapterObjectiveSystem.Chapters;
using OverFuture.ChapterObjectiveSystem.Events;
using OverFuture.ChapterObjectiveSystem.Objectives;
using OverFuture.ChapterObjectiveSystem.Persistence;
using OverFuture.ChapterObjectiveSystem.Progression;
using UnityEngine;
public sealed class MemoryProgressStore : IProgressStore
{
private readonly Dictionary<string, string> values =
new Dictionary<string, string>(StringComparer.Ordinal);
public bool TryRead(string key, out string value)
{
ValidateKey(key);
if (values.TryGetValue(key, out value))
{
return true;
}
value = string.Empty;
return false;
}
public void Write(string key, string value)
{
ValidateKey(key);
values[key] = value ?? throw new ArgumentNullException(nameof(value));
}
public bool Delete(string key)
{
ValidateKey(key);
return values.Remove(key);
}
public void Flush()
{
// Persist pending data in the project-specific backend here.
}
private static void ValidateKey(string key)
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
if (string.IsNullOrWhiteSpace(key))
{
throw new ArgumentException("A progress key cannot be empty.", nameof(key));
}
}
}