beasty-save-system / advanced / custom-backends.md
Backends de almacenamiento personalizados
Un backend de almacenamiento mueve texto de sobre dentro y fuera de los slots — nada más. La integridad es trabajo del pipeline: para cuando tu backend ve un guardado, ya es un sobre terminado (checksum, versiones, cifrado opcional), y todo lo que se lee de vuelta pasa por la misma validación que recibe un archivo. Esta página es el contrato para escribir uno: la interfaz, el registro, y el punto de extensión de identidad de usuario.
La interfaz
namespace Beasty_SaveSystemCore
{
public interface IBeastySaveStorage
{
bool SupportsSynchronous { get; }
// Síncronos — solo se llaman cuando SupportsSynchronous es true
void Write(SlotRef slot, string text, bool rotateBackup);
string Read(SlotRef slot); // null = el slot no existe
bool Exists(SlotRef slot);
bool Delete(SlotRef slot); // borra slot Y backup; true si el slot existía
string[] ListSlots(SlotRef scope); // scope lleva usuario/ubicación, Slot es null
string ReadBackup(SlotRef slot); // null = sin backup
bool BackupExists(SlotRef slot);
// Asíncronos — todo backend los implementa
Task WriteAsync(SlotRef slot, string text, bool rotateBackup);
Task<string> ReadAsync(SlotRef slot);
Task<bool> ExistsAsync(SlotRef slot);
Task<bool> DeleteAsync(SlotRef slot);
Task<string[]> ListSlotsAsync(SlotRef scope);
Task<string> ReadBackupAsync(SlotRef slot);
Task<bool> BackupExistsAsync(SlotRef slot);
}
}
Las reglas por las que vive un backend:
SupportsSynchronouses el flag de honestidad. Devuelvefalsey la API síncrona deBeastySaverespondeBackendRequiresAsyncen lugar de llamar a tus miembros síncronos — que entonces pueden simplemente lanzarNotSupportedException, porque nada los llamará. El enrutado del Save Mode del manager lee este flag para decidir cuándo un Save ModeSynchronousdebe forzarse a la vía asíncrona.- Un slot ausente es
null, no un error.ReadyReadBackupdevuelven null cuando no hay nada; el pipeline lo convierte enFileNotFound. rotateBackupes tu señal para conservar la versión anterior. Cuando es true, el texto que el slot guarda ahora mismo se convierte en el backup antes de que aterrice el texto nuevo.Deleteelimina el slot y su backup.- Los fallos tipados se lanzan, una vez. Lanza
BeastySaveStorageException(code, message)para aflorar un error tipado —AuthRequired,NetworkError, el que encaje. Cualquier otra excepción el pipeline la trata comoIoError. En ambos casos quien llama recibe un resultado, nunca una excepción.
SlotRef
Cada miembro recibe un SlotRef — dónde vive un guardado, sin asumir backend:
public readonly struct SlotRef
{
public string Slot { get; } // null en operaciones de ámbito (ListSlots)
public string UserId { get; } // null cuando no hay separación por usuario
public string Folder { get; }
public string Extension { get; }
public string DataPath { get; }
}
Tu backend decide cómo materializarlo: el backend local construye una ruta de archivo con DataPath,
Folder y Extension (un nivel más abajo cuando UserId está presente); los backends de Firebase
construyen una ruta de base de datos tipo users/{uid}/saves/{slot} e ignoran los campos de archivo. Usa
lo que encaje con tu almacenamiento e ignora el resto.
El registro
BeastySaveStorageRegistry.Register("my-backend", "My Backend",
() => new MyBackendStorage());
- El id es lo que almacena
BeastySaveSettings.StorageId; el nombre para mostrar es lo que enseña el desplegable Storage. El registro es idempotente — la última llamada para un id gana — y"local"(BeastySaveStorageRegistry.LocalId) siempre está presente. - Registra desde la inicialización de un módulo, para que el backend exista antes de que nada guarde y sobreviva a las recargas de dominio:
internal static class MyBackendModule
{
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
#if UNITY_EDITOR
[UnityEditor.InitializeOnLoadMethod]
#endif
static void Init() =>
BeastySaveStorageRegistry.Register("my-backend", "My Backend",
() => new MyBackendStorage());
}
Una vez registrado, el backend aparece en el desplegable Storage automáticamente — el desplegable lo
alimenta el registro (DescribeAll(), «local» primero, el resto por id). Un StorageId sin registrar
hace fallar cada llamada con BackendUnavailable, y el editor avisa en el desplegable y en la tarjeta de
estado.
Para tests, sáltate el registro: asigna una instancia directamente a BeastySaveSettings.Storage. Gana
sobre StorageId y no se serializa.
De quién son los guardados: el punto de extensión de usuario
El almacenamiento por usuario necesita una fuente de identidad. Eso es IBeastyUserProvider:
public interface IBeastyUserProvider
{
// Garantiza que existe una sesión (p. ej. inicio de sesión anónimo) y devuelve el id de usuario.
// Lanza BeastySaveStorageException(AuthRequired, ...) cuando no puede establecerse una sesión.
Task<string> GetUserIdAsync();
// El id de usuario actual, o null cuando aún no hay sesión. Nunca inicia sesión.
string CurrentUserId { get; }
}
BeastySaveUsers mantiene dos capas, desarrollador sobre módulo:
BeastySaveUsers.Provider = new MyAccountProvider(); // override del desarrollador — gana, se reinicia entre sesiones de Play
BeastySaveUsers.SetDefault(provider); // default de módulo — persiste, idempotente
Cómo lo usa el pipeline:
- La vía asíncrona en un backend remoto llama a
GetUserIdAsync()— ahí es donde ocurre el inicio de sesión anónimo. Sin ningún proveedor, la llamada falla conAuthRequired. - La vía síncrona (archivos locales con
ScopeByUser) lee soloCurrentUserId— nunca inicia la sesión de nadie, y un id null significa que el guardado va sin separar.
El módulo de Firebase Auth es exactamente esto: un proveedor registrado con SetDefault que reutiliza una
sesión de Firebase existente o inicia sesión de forma anónima. Tu proveedor lo sustituye asignando
BeastySaveUsers.Provider.
Lo que NO implementas
El pipeline sigue siendo dueño de todo lo que está por encima del texto: la validación de nombres de
slot, el sobre, el checksum SHA-256, el cifrado, las migraciones de DataVersion, el mapeo estricto o
tolerante, el registro, y los resultados tipados que devuelve cada llamada. Un backend no puede corromper
un guardado hasta hacerlo cargar — una lectura mala falla la misma puerta de checksum que fallaría un
archivo manipulado. Escribe los ocho miembros asíncronos, sé honesto en SupportsSynchronous, y el resto
del sistema — incluida la UI del editor — funciona con tu almacenamiento sin cambios.
Ver también
- Backends de almacenamiento — la vista de backends para el usuario
- Firebase — los dos backends que vienen con el asset
- Resultados y errores — los errores tipados que un backend aflora
- Formato del archivo de guardado — el sobre que tu backend almacena
- API de BeastySave — la fachada delante de todo