Contesto
Roblox fornisce un insieme di API per interfacciarsi con i data store tramite DataStoreService. Il caso d'uso più comune per queste API è il salvataggio, il caricamento e la replicazione dei dati del giocatore. Vale a dire, dati associati ai progressi del giocatore, agli acquisti e ad altre caratteristiche della sessione che persistono tra le singole sessioni di gioco.
La maggior parte dei giochi su Roblox utilizza queste API per implementare qualche forma di sistema di dati per i giocatori. Queste implementazioni differiscono nel loro approccio, ma generalmente cercano di risolvere gli stessi problemi.
Problemi comuni
Di seguito sono riportati alcuni dei maggiori problemi che i sistemi di dati dei giocatori tentano di risolvere:
Accesso in memoria: Le richieste di DataStoreService effettuano richieste web che operano in modo asincrono e sono soggette a limiti di frequenza. Questo è appropriato per un caricamento iniziale all'inizio della sessione, ma non per operazioni di lettura e scrittura ad alta frequenza durante il normale corso del gioco. La maggior parte dei sistemi di dati dei giocatori degli sviluppatori memorizza questi dati in memoria sul server Roblox, limitando le richieste di DataStoreService ai seguenti scenari:
- Lettura iniziale all'inizio di una sessione
- Scrittura finale alla fine della sessione
- Scritture periodiche a intervalli per mitigare lo scenario in cui la scrittura finale fallisce
- Scritture per garantire che i dati siano salvati durante l'elaborazione di un acquisto
Archiviazione efficiente: Memorizzare tutti i dati della sessione di un giocatore in una singola tabella consente di aggiornare più valori atomicamente e gestire la stessa quantità di dati in meno richieste. Rimuove anche il rischio di desincronizzazione tra i valori e rende più facile gestire i rollback.
Alcuni sviluppatori implementano anche una serializzazione personalizzata per comprimere grandi strutture di dati (tipicamente per salvare contenuti generati dagli utenti in-game).
Replicazione: Il client necessita di un accesso regolare ai dati di un giocatore (ad esempio, per aggiornare l'interfaccia utente). Un approccio generico alla replicazione dei dati dei giocatori al client consente di trasmettere queste informazioni senza dover creare sistemi di replicazione su misura per ogni componente di dato. Gli sviluppatori spesso vogliono la possibilità di essere selettivi riguardo a ciò che viene replicato e ciò che non lo è.
Gestione degli errori: Quando i DataStore non possono essere accessi, la maggior parte delle soluzioni implementerà un meccanismo di ripetizione e una soluzione ai dati 'default'. È necessaria una cura speciale per garantire che i dati di fallback non sovrascrivano successivamente i dati 'reali' e che questo venga comunicato al giocatore in modo appropriato.
Ripetizioni: Quando i data store sono inaccessibili, la maggior parte delle soluzioni implementa un meccanismo di ripetizione e una soluzione a dati di default. Prestare particolare attenzione a garantire che i dati di fallback non sovrascrivano successivamente i dati "reali" e comunicare la situazione al giocatore in modo appropriato.
Blocco della sessione: Se i dati di un singolo giocatore vengono caricati e memorizzati in memoria su più server, possono verificarsi problemi in cui un server salva informazioni obsolete. Questo può portare a perdite di dati e comuni sistemi di duplicazione di oggetti.
Gestione atomica degli acquisti: Verifica, assegna e registra gli acquisti in modo atomico per prevenire la perdita di oggetti o l'assegnazione multipla.
Codice di esempio
Roblox ha codice di riferimento per assisterti nella progettazione e nella costruzione di sistemi di dati per i giocatori. Il resto di questa pagina esamina il contesto, i dettagli di implementazione e le avvertenze generali.
Dopo aver importato il modello in Studio, dovresti vedere la seguente struttura di cartelle:

Architettura
Questo diagramma ad alto livello illustra i principali sistemi nel campione e come si interfacciano con il codice nel resto del gioco.

Ripetizioni
Class: DataStoreWrapper
Contesto
Poiché DataStoreService effettua richieste web "sotto il cofano", le sue richieste non sono garantite di avere successo. Quando ciò accade, i metodi DataStore generano errori, consentendoti di gestirli.
Una comune "insidia" può verificarsi se tenti di gestire i fallimenti del data store in questo modo:
local function retrySetAsync(dataStore, key, value)
for _ = 1, MAX_ATTEMPTS do
local success, result = pcall(dataStore.SetAsync, dataStore, key, value)
if success then
break
end
task.wait(TIME_BETWEEN_ATTEMPTS)
end
end
Sebbene questo sia un meccanismo di ripetizione perfettamente valido per una funzione generica, non è adatto per le richieste di DataStoreService perché non garantisce l'ordine in cui vengono effettuate le richieste. Preservare l'ordine delle richieste è importante per le richieste di DataStoreService poiché interagiscono con uno stato. Considera il seguente scenario:
- La richiesta A viene effettuata per impostare il valore della chiave K a 1.
- La richiesta fallisce, quindi viene programmata una ripetizione per essere eseguita tra 2 secondi.
- Prima che si verifichi la ripetizione, la richiesta B imposta il valore di K a 2, ma la ripetizione della richiesta A sovrascrive immediatamente questo valore e imposta K a 1.
Anche se UpdateAsync opera sull'ultima versione del valore della chiave, le richieste UpdateAsync devono comunque essere elaborate in ordine per evitare stati transitori non validi (ad esempio, un acquisto sottrae monete prima che un'aggiunta di monete venga elaborata, risultando in monete negative).
Il nostro sistema di dati dei giocatori utilizza una nuova classe, DataStoreWrapper, che fornisce ripetizioni in attesa che sono garantite di essere elaborate in ordine per chiave.
Approccio

DataStoreWrapper fornisce metodi corrispondenti ai metodi di DataStore: DataStore:GetAsync(), DataStore:SetAsync(), DataStore:UpdateAsync() e DataStore:RemoveAsync().
Questi metodi, quando vengono chiamati:
Aggiungono la richiesta a una coda. Ogni chiave ha la propria coda, dove le richieste vengono elaborate in ordine e in serie. Il thread richiedente si ferma fino al completamento della richiesta.
Questa funzionalità si basa sulla classe ThreadQueue, che è un pianificatore di attività e limitatore di frequenza basato su coroutine. Invece di restituire una promessa, ThreadQueue sospende il thread attuale fino al completamento dell'operazione e genera un errore se fallisce. Questo è più in linea con i modelli asincroni idiomatici di Luau.
Se una richiesta fallisce, ripete con una configurazione di backoff esponenziale. Queste ripetizioni fanno parte del callback inviato al ThreadQueue, quindi sono garantite per completarsi prima che inizi la prossima richiesta nella coda per questa chiave.
Quando una richiesta è completata, il metodo di richiesta restituisce il pattern success, result.
DataStoreWrapper espone anche metodi per ottenere la lunghezza della coda per una determinata chiave e per eliminare richieste obsolete. Quest'ultima opzione è particolarmente utile in scenari in cui il server sta per essere arrestato e non c'è tempo per elaborare altre richieste se non le più recenti.
Avvertenze
DataStoreWrapper segue il principio secondo cui, al di fuori di scenari estremi, ogni richiesta del data store dovrebbe essere consentita a completarsi (con successo o meno), anche se una richiesta più recente la rende ridondante. Quando si verifica una nuova richiesta, le richieste obsolete non vengono rimosse dalla coda, ma consentite a completarsi prima che venga avviata la nuova richiesta. La razionalità di questo è radicata nell'applicabilità di questo modulo come utility generica per i data store piuttosto che come strumento specifico per i dati dei giocatori e si basa sui seguenti punti:
È difficile decidere un insieme intuitivo di regole per quando una richiesta sia sicura da rimuovere dalla coda. Considera la seguente coda:
Value=0, SetAsync(1), GetAsync(), SetAsync(2)
Ci si aspetterebbe che GetAsync() restituisse 1, ma se rimuovessimo la richiesta SetAsync() dalla coda a causa della sua ridondanza rispetto alla più recente, restituirebbe 0.
La progressione logica è che quando viene aggiunta una nuova richiesta di scrittura, si possano ripulire le richieste obsolete solo fino alla più recente richiesta di lettura. UpdateAsync, di gran lunga l'operazione più comune (e l'unica utilizzata da questo sistema), può sia leggere che scrivere, quindi sarebbe difficile riconciliare questo design senza aggiungere complessità extra.
DataStoreWrapper potrebbe richiederti di specificare se una richiesta UpdateAsync() fosse consentita a leggere e/o scrivere, ma ciò non sarebbe applicabile al nostro sistema di dati dei giocatori, dove questo non può essere determinato in anticipo a causa del meccanismo di blocco della sessione (trattato in dettaglio successivamente).
Una volta rimosso dalla coda, è difficile decidere una regola intuitiva per come questo dovrebbe essere gestito. Quando viene effettuata una richiesta DataStoreWrapper, il thread attuale viene sospeso fino al completamento. Se rimuovessimo le richieste obsolete dalla coda, dovremmo decidere se restituire false, "Rimosso dalla coda" o non restituire mai e scartare il thread attivo. Entrambi gli approcci comportano svantaggi e caricano ulteriore complessità sul consumatore.
In definitiva, la nostra opinione è che l'approccio semplice (elaborare ogni richiesta) sia preferibile qui e crei un ambiente più chiaro da navigare quando si affrontano questioni complesse come il blocco della sessione. L'unica eccezione a questo è durante DataModel:BindToClose(), dove la pulizia della coda diventa necessaria per salvare i dati di tutti gli utenti in tempo e il valore restituito dalle singole chiamate a funzione non è più una preoccupazione corrente. Per ulteriori dettagli, vedere Dati del giocatore.
Blocco della sessione
Class: SessionLockedDataStoreWrapper
Contesto
I dati dei giocatori sono memorizzati in memoria sul server e vengono letti e scritti solo negli data store sottostanti quando necessario. Puoi leggere e aggiornare i dati in memoria del giocatore istantaneamente senza dover effettuare richieste web e evitare di superare i limiti di DataStoreService.
Affinché questo modello funzioni come previsto, è fondamentale che non più di un server possa caricare i dati di un giocatore nella memoria dal DataStore allo stesso tempo.
Ad esempio, se il server A carica i dati di un giocatore, il server B non può caricare quei dati fino a quando il server A non rilascia il suo blocco su di essi durante un salvataggio finale. Senza un meccanismo di blocco, il server B potrebbe caricare dati del giocatore obsoleti dal data store prima che il server A abbia avuto la possibilità di salvare la versione più recente che ha in memoria. Poi, se il server A salva i suoi dati più recenti dopo che il server B ha caricato i dati obsoleti, il server B sovrascriverebbe quei dati più recenti durante il suo successivo salvataggio.
Anche se Roblox consente solo a un client di essere connesso a un server alla volta, non puoi assumere che i dati di una sessione vengano sempre salvati prima che inizi la sessione successiva. Considera i seguenti scenari che possono verificarsi quando un giocatore lascia il server A:
- Il server A effettua una richiesta DataStore per salvare i propri dati, ma la richiesta fallisce e richiede diverse ripetizioni per completarsi con successo. Durante il periodo di ripetizione, il giocatore si unisce al server B.
- Il server A effettua troppe chiamate UpdateAsync() alla stessa chiave e viene soggetto a throttling. L'ultima richiesta di salvataggio viene posta in una coda. Mentre la richiesta è in coda, il giocatore si unisce al server B.
- Sul server A, del codice connesso all'evento PlayerRemoving sospende prima che i dati del giocatore siano stati salvati. Prima che questa operazione sia completata, il giocatore si unisce al server B.
- Le prestazioni del server A sono degradate al punto che il salvataggio finale è ritardato fino dopo che il giocatore si è unito al server B.
Questi scenari dovrebbero essere rari, ma si verificano, in particolare in situazioni in cui un giocatore si disconnette da un server e si connette a un altro in rapida successione (ad esempio, mentre sta teletrasportando). Alcuni utenti malevoli potrebbero anche tentare di abusare di questo comportamento per completare azioni senza che esse persistano. Questo può avere un impatto significativo in giochi che consentono ai giocatori di scambiare oggetti ed è una comune fonte di exploit di duplicazione di oggetti.
Il blocco della sessione affronta questa vulnerabilità garantendo che, quando la chiave DataStore di un giocatore viene letta per la prima volta dal server, il server scriva atomica un blocco nei metadati della chiave all'interno della stessa chiamata UpdateAsync(). Se questo valore di blocco è presente quando qualunque altro server tenta di leggere o scrivere la chiave, il server non procede.
Approccio

SessionLockedDataStoreWrapper è un meta-wrapper attorno alla classe DataStoreWrapper. DataStoreWrapper fornisce funzionalità di coda e ripetizione, che SessionLockedDataStoreWrapper integra con il blocco della sessione.
SessionLockedDataStoreWrapper passa ogni richiesta di DataStore—indipendentemente dal fatto che sia GetAsync, SetAsync o UpdateAsync—tramite UpdateAsync. Questo è perché UpdateAsync consente a una chiave di essere sia letta che scritta in modo atomico. È anche possibile abbandonare la scrittura in base al valore letto restituendo nil nel callback di trasformazione.
La funzione di trasformazione passata a UpdateAsync per ogni richiesta esegue le seguenti operazioni:
Verifica che la chiave sia sicura da accedere, abbandonando l'operazione se non lo è. "Sicura da accedere" significa:
L'oggetto metadata della chiave non include un valore LockId non riconosciuto che è stato aggiornato meno di tanto tempo quanto il tempo di scadenza del blocco. Questo tiene conto del rispetto di un blocco imposto da un altro server e ignora quel blocco se è scaduto.
Se questo server ha precedentemente impostato un proprio valore LockId nei metadati della chiave, allora questo valore è ancora nei metadati della chiave. Questo tiene conto della situazione in cui un altro server ha preso in carico il blocco di questo server (per scadenza o per forza) e successivamente lo ha rilasciato. In altre parole, anche se LockId è nil, un altro server potrebbe ancora aver sostituito e rimosso un blocco nel tempo intercorso da quando hai bloccato la chiave.
UpdateAsync esegue l'operazione DataStore richiesta dal consumatore di SessionLockedDataStoreWrapper. Ad esempio, GetAsync() si traduce nella funzione function(value) return value end.
A seconda dei parametri passati nella richiesta, UpdateAsync blocca o sblocca la chiave:
Se la chiave deve essere bloccata, UpdateAsync imposta il LockId nei metadati della chiave a un GUID. Questo GUID è memorizzato in memoria sul server in modo che possa essere verificato la prossima volta che accede alla chiave. Se il server ha già un blocco su questa chiave, non apporta modifiche. Pianifica anche un'attività per avvisarti se non accedi nuovamente alla chiave per mantenere il blocco entro il tempo di scadenza del blocco.
Se la chiave deve essere sbloccata, UpdateAsync rimuove il LockId nei metadati della chiave.
Un gestore di ripetizione personalizzato è passato nel sottostante DataStoreWrapper in modo che l'operazione venga ripetuta se è stata abortita al passo 1 a causa del blocco della sessione.
Un messaggio di errore personalizzato viene restituito anche al consumatore, consentendo al sistema di dati del giocatore di riportare un errore alternativo in caso di blocco della sessione al client.
Avvertenze
Il regime di blocco della sessione si basa su un server che rilascia sempre il suo blocco su una chiave quando ha finito di usarla. Questo dovrebbe sempre accadere tramite un'istruzione per sbloccare la chiave come parte della scrittura finale in PlayerRemoving o BindToClose().
Tuttavia, lo sblocco può fallire in determinate situazioni. Ad esempio:
- Il server è andato in crash o DataStoreService non era operabile per tutti i tentativi di accesso alla chiave.
- A causa di un errore nella logica o di un bug simile, l'istruzione per sbloccare la chiave non è stata eseguita.
Per mantenere il blocco su una chiave, è necessario accedervi regolarmente per tutto il tempo in cui è caricata in memoria. Questo di solito viene fatto come parte del ciclo di salvataggio automatico che si svolge in background nella maggior parte dei sistemi di dati dei giocatori, ma questo sistema espone anche un metodo refreshLockAsync se hai bisogno di farlo manualmente.
Se il tempo di scadenza del blocco è stato superato senza che il blocco sia stato aggiornato, qualsiasi server è libero di prendere il blocco. Se un server diverso prende il blocco, i tentativi da parte del server attuale di leggere o scrivere la chiave falliscono a meno che non stabilisca un nuovo blocco.
Elaborazione dei prodotti per sviluppatori
Singleton: ReceiptHandler
Contesto
Il callback ProcessReceipt svolge il compito fondamentale di determinare quando finalizzare un'acquisto. ProcessReceipt viene chiamato in scenari molto specifici. Per il suo insieme di garanzie, vedi MarketplaceService.ProcessReceipt.
Sebbene la definizione di "gestire" un acquisto possa differire tra i giochi, utilizziamo i seguenti criteri
L'acquisto non è stato precedentemente gestito.
L'acquisto è riflesso nella sessione corrente.
Questo richiede di condurre le seguenti operazioni prima di restituire PurchaseGranted:
- Verifica che il PurchaseId non sia già stato registrato come gestito.
- Assegna l'acquisto ai dati in memoria del giocatore.
- Registra il PurchaseId come gestito nei dati in memoria del giocatore.
- Scrivi i dati in memoria del giocatore nel DataStore.
Il blocco della sessione semplifica questo flusso, poiché non è più necessario preoccuparsi dei seguenti scenari:
- I dati in memoria del giocatore nel server corrente potrebbero essere obsoleti, rendendo necessario recuperare il valore più recente dal DataStore prima di verificare la storia del PurchaseId.
- Il callback per lo stesso acquisto in esecuzione in un altro server, consentendo la lettura e la scrittura della storia del PurchaseId e il salvataggio dei dati aggiornati del giocatore con l'acquisto riflesso in modo atomico per prevenire condizioni di gara.
Il blocco della sessione garantisce che, se un tentativo di scrivere nel DataStore del giocatore ha successo, nessun altro server ha con successo letto o scritto nel DataStore del giocatore tra il caricamento e il salvataggio dei dati in questo server. In breve, i dati in memoria del giocatore in questo server sono la versione più aggiornata disponibile. Ci sono alcune avvertenze, ma non influenzano questo comportamento.
Approccio
I commenti in ReceiptProcessor delineano l'approccio:
Verifica che i dati del giocatore siano attualmente caricati su questo server e che siano stati caricati senza errori.
Poiché questo sistema utilizza il blocco della sessione, questo controllo verifica anche che i dati in memoria siano la versione più aggiornata.
Se i dati del giocatore non si sono ancora caricati (cosa prevista quando un giocatore entra in un gioco), attendere che i dati del giocatore vengano caricati. Il sistema ascolta anche per il giocatore che esce dal gioco prima che i suoi dati vengano caricati, poiché non dovrebbe sospendersi indefinitamente e bloccare questo callback dall'essere invocato nuovamente su questo server per questo acquisto se il giocatore si riunisce.
Verifica che il PurchaseId non sia già registrato come elaborato nei dati del giocatore.
Grazie al blocco di sessione, l'array di PurchaseIds che il sistema ha in memoria è la versione più aggiornata. Se il PurchaseId è registrato come elaborato e riflesso in un valore che è stato caricato o salvato nel DataStore, restituisci PurchaseGranted. Se è registrato come elaborato, ma non riflesso nel DataStore, restituisci NotProcessedYet.
Aggiorna i dati del giocatore localmente in questo server per "assegnare" l'acquisto.
ReceiptProcessor adotta un approccio a callback generico e assegna un callback diverso per ogni DeveloperProductId.
Aggiorna i dati del giocatore localmente in questo server per memorizzare il PurchaseId.
Invia una richiesta per salvare i dati in memoria nel DataStore, restituendo PurchaseGranted se la richiesta ha successo. In caso contrario, restituisci NotProcessedYet.
Se questa richiesta di salvataggio non ha successo, una richiesta successiva per salvare i dati della sessione in memoria del giocatore potrebbe ancora avere successo. Durante la prossima chiamata a ProcessReceipt, il passaggio 2 gestisce questa situazione e restituisce PurchaseGranted.
Dati del giocatore
Singletons: PlayerData.Server, PlayerData.Client
Contesto
I moduli che forniscono un'interfaccia per il codice per leggere e scrivere in modo sincrono i dati delle sessioni di giocatore sono comuni nei giochi Roblox. Questa sezione copre PlayerData.Server e PlayerData.Client.
Approccio
PlayerData.Server e PlayerData.Client gestiscono quanto segue:
- Caricamento dei dati del giocatore in memoria, inclusa la gestione dei casi in cui il caricamento fallisce.
- Fornire un'interfaccia per il codice del server per interrogare e modificare i dati del giocatore.
- Replicazione delle modifiche nei dati del giocatore al client affinché il codice client possa accedervi.
- Replicazione di errori di caricamento e/o salvataggio al client in modo che possa visualizzare dialoghi di errore.
- Salvataggio dei dati del giocatore periodicamente, quando il giocatore esce e quando il server si arresta.
Carica i dati del giocatore

SessionLockedDataStoreWrapper effettua una richiesta getAsync al data store.
Se questa richiesta fallisce, vengono utilizzati i dati di default e il profilo è contrassegnato come "errore" per garantire che non venga scritto nel data store in seguito.
Un'opzione alternativa è espellere il giocatore, ma raccomandiamo di lasciare che il giocatore giochi con dati di default e una comunicazione chiara su ciò che è accaduto piuttosto che rimuoverli dal gioco.
Un payload iniziale viene inviato a PlayerDataClient contenente i dati caricati e lo stato di errore (se presente).
Qualsiasi thread sospeso utilizzando waitForDataLoadAsync per il giocatore viene ripreso.
Fornire un'interfaccia per il codice del server
- PlayerDataServer è un singleton che può essere richiesto e accessibile da qualsiasi codice del server in esecuzione nello stesso ambiente.
- I dati dei giocatori sono organizzati in un dizionario di chiavi e valori. Puoi manipolare questi valori sul server utilizzando i metodi setValue, getValue, updateValue e removeValue. Questi metodi operano tutti in modo sincrono senza sospendere.
- I metodi hasLoaded e waitForDataLoadAsync sono disponibili per garantire che i dati siano stati caricati prima di accedervi. Raccomandiamo di farlo una volta durante uno schermo di caricamento prima che vengano avviati altri sistemi per evitare di dover controllare gli errori di caricamento prima di ogni interazione con i dati sul client.
- Un metodo hasErrored può verificare se il caricamento iniziale del giocatore è fallito, costringendolo a utilizzare dati di default. Controlla questo metodo prima di consentire al giocatore di effettuare acquisti, poiché gli acquisti non possono essere salvati nei dati senza un caricamento riuscito.
- Un segnale playerDataUpdated viene attivato con il player, key e value ogni volta che i dati di un giocatore vengono modificati. I singoli sistemi possono iscriversi a questo.
Replicare le modifiche al client
- Qualsiasi modifica ai dati del giocatore in PlayerDataServer viene replicata in PlayerDataClient, a meno che quella chiave non sia stata contrassegnata come privata utilizzando setValueAsPrivate
- setValueAsPrivate viene utilizzato per denotare le chiavi che non dovrebbero essere inviate al client
- PlayerDataClient include un metodo per ottenere il valore di una chiave (get) e un segnale che viene attivato quando viene aggiornato (updated). Un metodo hasLoaded e un segnale loaded sono inclusi, affinché il client possa attendere il caricamento e replicare i dati prima di avviare i propri sistemi.
- PlayerDataClient è un singleton che può essere richiesto e accessibile da qualsiasi codice client in esecuzione nello stesso ambiente.
Replicare errori al client
- Gli stati di errore riscontrati quando si salvano o si caricano i dati dei giocatori vengono replicati in PlayerDataClient.
- Accedi a queste informazioni con i metodi getLoadError e getSaveError, insieme ai segnali loaded e saved.
- Ci sono due tipi di errori: DataStoreError (la richiesta a DataStoreService è fallita) e SessionLocked (vedere Blocco della sessione).
- Usa questi eventi per disabilitare i prompt per gli acquisti dei clienti e implementare dialoghi di avviso. Questa immagine mostra un esempio di dialogo:

Salva i dati del giocatore

Quando il giocatore lascia il gioco, il sistema esegue i seguenti passaggi:
- Controlla se è sicuro scrivere i dati del giocatore nel data store. Gli scenari in cui non sarebbe sicuro include il fallimento del caricamento dei dati del giocatore o se sono ancora in fase di caricamento.
- Effettua una richiesta attraverso SessionLockedDataStoreWrapper per scrivere il valore attuale dei dati in memoria nel data store e rimuovere il blocco della sessione una volta completato.
- Cancella i dati del giocatore (e altre variabili come metadati e stati di errore) dalla memoria del server.
In un ciclo periodico, il server scrive i dati di ogni giocatore nel data store (purché sia sicuro effettuare il salvataggio). Questa benvenuta ridondanza mitiga le perdite in caso di crash del server ed è anche necessaria per mantenere il blocco della sessione.
Quando viene ricevuta una richiesta di arresto del server, si verifica quanto segue in un callback di BindToClose:
- Viene effettuata una richiesta per salvare i dati di ciascun giocatore nel server, seguendo il processo normalmente seguito quando un giocatore lascia il server. Queste richieste vengono effettuate in parallelo, poiché i callback di BindToClose hanno solo 30 secondi per completarsi.
- Per accelerare i salvataggi, tutte le altre richieste nella coda di ciascuna chiave vengono eliminate dal sottostante DataStoreWrapper (vedere Ripetizioni).
- Il callback non restituisce fino al completamento di tutte le richieste.