Data Store (Save Feature)

Overview

The DataStore feature provided by ParaSpace enables you to save persistent data independent of rooms, such as players' items, equipment, and skill points. In other words, this feature allows players to save games. DataStore allows consistent data access and storage. Players can access and modify the same data items when they are playing in any room, even if they are accessing rooms in different areas.

Enable DataStore

By default, you can enable DataStore in all the Worlds created by you. Through unified DataStore APIs, you can get DataStore capabilities and services.

If you are using a simulator and your World has not been uploaded to the server, the DataStore feature is not available.

However, if you are using a simulator and your World has already been uploaded to the server, you can choose to connect to a test environment (Sand Box) or an online production environment (Released Data). If you connect to the online production environment, all your subsequent DataStore operations will affect data in that environment. The following figure shows where to enable DataStore if you are using a simulator:

Access DataStore Objects

All DataStore operations are triggered by the ParaDataStore and ParaOrderedDataStore entity classes.

ParaDataStore

Entity class for DataStore operations. Each ParaDataStore class represents a collection of data of the same type. All data stored in ParaDataStore is composed of key-value pairs and supports metadata and version-related operations.

ParaOrderedDataStore

Entity class for DataStore operations. Each ParaOrderedDataStore class represents a collection of data of the same type that is composed of key-value pairs. All data stored in ParaOrderedDataStore can only be of numerical type and doesn't support metadata and version-related operations. Besides, data stored in ParaOrderedDataStore also supports sorting. By default, data is sorted in ascending or descending numerical order based on the values of the data.

Create and Get a ParaDataStore or ParaOrderedDataStore Object

To store and access data, you must create or get a ParaDataStore or ParaOrderedDataStore object. You can call the GetDataStore or GetOrderedDataStore API provided by ParaDataService to create or get a ParaDataStore or ParaOrderedDataStore object. Note that both creating and getting a ParaDataStore or ParaOrderedDataStore object are done by calling these 2 APIs.

local ok, store1 = ParaDataService.GetDataStore(string name, string scope = “global”, bool all_scope = false)

local ok, store2 = ParaDataService.GetOrderedDataStore(string name, string scope = “global”)

The following describes the parameters used by the 2 APIs:

  • name: name of a ParaDataStore or ParaOrderedDataStore object.
  • scope: specifies the string of the domain name. All the strings are automatically split into tables by domain.
  • allScope: specifies whether to access data within all scopes. If the value is true, the obtained DataStore object can access data within different scopes in the same DataStore object. However, you must specify the scopes in the parameter key of the API for obtaining data, that is, "scope:key". Otherwise, the object can only access data within a specified scope. When allScope is set to true, the scope parameter must be left empty or set to nil. When allScope is set to false and no scope is specified in the scope parameter, the default value "global" will be assigned to the scope parameter.

If you are using a simulator and haven't enabled DataStore or don't meet the enabling prerequisites, these APIs will return a corresponding error code and message.

Operate Data

Get Data

To get data, you must comply with the code format rule. That is, you must place DataStore-related code inside the DATASTORE() code block. In other words, a function is passed in for DATASTORE to call. See the following code example.

You can call the GetData API of DataStore to get corresponding data. If the API operation is successful, GetData returns the following 3 values:

  • code: numerical type. If the value is 0, the API operation is successful. Otherwise, the API operation fails.
  • value: If the API operation is successful, this value equals the value corresponding to the key. If the API operation fails (code is not 0), this value is an error message of string type.
  • info: If the API operation is successful (code is 0), then info includes detailed data information with the ParaDataStoreInfo structure. The detailed information includes specific metadata, version data, and specific key values. You can use the following code example to access data and perform subsequent operations.
DATASTORE(  
  	function()  
        -- Get my_world's bag capacity for local player  
        local player = CS.ParaPlayerService.GetLocalPlayer()  
        if player == nil then  
            print('Local player is nil')
      			return
        end        
				local ok, store = ParaDataService.GetDataStore('my_world', 'bag_' .. player.UID)
        if ok ~= 0 then
            print('Load  Failed:' .. ok .. tostring(store))
            return
        end
        local ok, bag_capacity, info = store:GetData('capacity')
        if ok ~= 0 then
            print('Get data failed!, code:' .. ok .. ' error_msg:' .. bag_capacity)
            return
        end
        print('Bag capacity for player:' .. player.UID .. ' is:' .. bag_capacity)
    end)

The preceding code sends a request to the server to get the latest data for subsequent use. To access other information such as metadata and version information, you can call the API that returns info. For the specific solution, see the description in subsequent sections.

Set Data

Data can be set through the SetData API of DataStore. The input parameters of the SetData API include key, value, and options. The key parameter represents the data's key, the value parameter represents the data's value, and the options parameter represents additional information. The return value of the SetData API is the same as that of the GetData API.

Actually, data is stored in DataStore in the form of key-value pairs. When data with the same key exists in DataStore, the newly set data replaces the original data with the same key and the original data will be saved in historical versions. However, if no such data exists, a new key will be created and the corresponding value will be saved in that key.

To modify the metadata of that value, you can use the options parameter. For how to use the API, see the description in subsequent sections. Note that the options parameter cannot be used to set metadata and historical version information stored in ParaOrderedDataStore.

Update Data Incrementally

Incremental updates can update data without getting the current value of the data. Compared with SetData, incremental updates can reduce the risk of data inconsistency. For example, if data is modified by another operation during the time between getting data and setting data, calling SetData again could lead to data inconsistency. Incremental updates are generally used for modifying globally unique or locally unique values, or for other values that may be modified simultaneously by multiple servers, such as the total number of players in a room or the overall progress of a task.

You can perform incremental updates through the IncrementData API of DataStore:

ok, value, ParaDataStoreInfo info = IncrementData(string key, number delta, ParaDataSetOptions options = nil)

The following code example shows that the total number of players in the game increases by 1 when a new player joins the game:

 DATASTORE(  
        function()  
            -- Get my_world's players info  
            local ok, store = ParaDataService.GetDataStore('my_world', 'players_info')  
            if ok ~= 0 then  
                print('Load  Failed:' .. ok .. tostring(store))  
                return  
            end  
            ok, message = store:IncrementData('total_player_count', 1)  
            if ok ~= 0 then  
                print('Increment data failed!, code:' .. ok .. ' error_msg:' .. message)  
                return  
            end  
            ok, total_players = store:GetData('total_player_count')  
            if ok ~= 0 then  
                print('Get data failed!, code:' .. ok .. ' error_msg:' .. total_players)  
                return  
            end  
            print('Total player count is:' .. total_players)  
        end)

📘

A storage variable is of numerical type. We recommend performing incremental updates on the variable when it is modified simultaneously by multiple servers.

Use a Function to Modify Data

You can modify data using UpdateData.

ok, value, ParaDataStoreInfo info = UpdateData(string key, function updateFunction(currentValue, ParaDataStoreInfo info)
{return newValue, ParaDataSetOptions})

UpdateData can also be used to set data in DataStore. Unlike SetData and IncrementData, UpdateData can pass in a update function. The input of the function is the current value of the key of the data to be updated and the return value is the expected value and additional modification information (such as changes to metadata). This function can simplify the business logic of getting, modifying, and uploading data, reducing the risk of inconsistent data across multiple servers.

The function passed in by UpdateData is:

function updateFunction(currentValue, ParaDataStoreInfo info){return newValue, ParaDataSetOptions options})

The parameter passed in is the current value of the key passed in by UpdateData. The info parameter indicates the additional metadata and version information. The return values of the function are newValue and options.

UpdateData will save the 2 return values into the data corresponding to the key as the new value of the key and additional information. The specific code example is as follows:

DATASTORE(  
        function()  
            -- Get my_world's guild info  
            local ok, store = ParaDataService.GetDataStore('my_world', 'guild_info')  
            if ok ~= 0 then  
                print('Load  Failed:' .. ok .. tostring(store))  
                return  
            end
            local ok,message = store:SetData('notice', 'init_version')
            if ok ~= 0 then
                print('SetData returns:' .. ok .. tostring(message))
                return
            end
            local ok, new_notice_info = store:UpdateData('notice',  
                function(notice)  
                    print('current notice info is:' .. notice)  
                    local update_time = os.date("%c");  
                    local new_notice = notice .. '\\n' .. update_time .. ':new event info'  
                    local opts = ParaDataService.CreateDataSetOptions()  
                    opts:SetMetadata({latest_update_time = update_time})  
                    return new_notice, opts  
                end)  
            if ok == 0 then  
                print('Update guild notice info to:' .. new_notice_info)  
            end  
        end)

IncrementData vs. SetData vs. UpdateData

DataStore provides 3 data update modes. Depending on the usage scenario, you need to select an appropriate upload mode accordingly.

  1. SetData
    SetData is the most basic DataStore API. It is easy to use and suitable for performing basic DataStore operations such as saving player data like levels, points, and login time. However, when you call this API to set data that may be modified simultaneously by multiple servers, data inconsistency may occur. When you use this API to set data like all-server-wide points or game state, you need to prevent data inconsistency caused by simultaneous operations across multiple servers based on the actual business logic of the operation.
  2. IncrementData
    In response to potential data inconsistency caused by SetData operations across multiple servers, IncrementData provides the atomic incremental update mode, which can effectively ensure data consistency. However, IncrementData can only operate on numerical data and cannot perform targeted logic processing based on the current value. Therefore, IncrementData is more suitable for updating global numeric fields. It can only handle simple data update logic. For more complex update logic, IncrementData may not be sufficient.
  3. UpdateData
    UpdateData is a function-based update API suitable for complex data update scenarios with high consistency requirements. The UpdateData API is difficult to use and its operation process is complex, making it less efficient and concise for general update requests.

Clear Data

You can call the RemoveData API to clear corresponding data. As DataStore supports version management by default, deleted data can be obtained through historical version retrieval in version management. The data is truly removed from the database only when historical versions expire or are manually deleted.

Only ParaDataStore supports version management, while ParaOrderedDataStore does not.

Sort Data

ParaOrderedDataStore can automatically sort data based on data values and get data in pagination mode.

ok, DataStorePages pages = GetSortedData(bool ascending, int pagesize, double min_value = -math.huge, double max_value = math.huge)

ParaOrderedDataStore allows you to get a list of data sorted by value through the GetSortedData API. When calling this API, you can set the number of data items to be displayed on each page as well as the minimum and maximum values to be retrieved. Page numbers start from 0 and increment. The default values for the minimum and maximum values are the minimum and maximum floating-point numbers in Lua respectively. When they are set to nil or no value, you will get all data stored in the DataStore object.

The return value of GetSortedData is an error code and a ParaDataPages object containing pagination information. You can call ParaDataPages APIs to traverse data by page. Relevant APIs will be described in subsequent sections. The following code example gets the items contained in a player's inventory:

    DATASTORE(  
        function()  
            local page_size = 10  
            -- Get my_world's arena rank points  
            local ok, store = ParaDataService.GetOrderedDataStore('my_world', 'arena_rank')  
            if ok ~= 0 then  
                print('Load data store failed! code is:' .. ok .. ' message is:' .. store)  
                return  
            end

            local ok, message = store:SetData('rank_1', 100)
            if ok ~= 0 then
                print('Set data failed! code is:' .. ok .. ' message is:' .. message)  
                return
            end
            local ok, message = store:SetData('rank_2', 80)
            if ok ~= 0 then
                print('Set data failed! code is:' .. ok .. ' message is:' .. message)  
                return
            end

            local ok, pages = store:GetSortedData(true, page_size)
            if ok ~= 0 then
                print('GetSortedData failed! code is:' .. ok .. ' message is:' .. pages)
                return
            end
            local page_index = 1
            repeat
                local success, page_values = pages:GetCurrentPage()
                local index = 1
                if success == 0 then
                    for k, v in pairs(page_values) do
                        print('rank:' .. (page_index - 1) * page_size + index .. 
                                ' points:' .. tostring(v.value))
                        index = index + 1
                    end
                end
                page_index = page_index + 1
            until(not pages:AdvanceToNextPage())
        end)

Prefix Search

ParaDataStore supports prefix search based on key and can automatically sort data and get data in pagination mode.

ok, DataStorePages pages = ListKeys(string prefix, bool ascending, int pagesize)

ok, DataStorePages pages = ListValues(string prefix, bool ascending, int pagesize)2

ParaDataStore allows you to get a list of keys or data sorted by key through the ListKeys or ListValue API. When calling either of the APIs, you can set the number of data items to be displayed on each page. If you enable cross-domain all_scope when creating or getting ParaDataStore, you need to add scope as the prefix in the format of scope:prefix. Certainly, any letter can be used as the prefix for prefix search.

The return value of ListKeys and ListValues is an error code and a ParaDataPages object containing pagination information. You can call ParaDataPages APIs to traverse data by page. Relevant APIs will be described in subsequent sections. The following code example gets the points ranking list of a World:

    DATASTORE(
        function()
            print('==============Sorted Section============')
            ok, store = ParaDataService.GetDataStore('PlayerInventory', 'player001')
            if ok ~= 0 then
                print('LoadDataStore Failed:' .. ok .. tostring(store))
                return
            end

            for v = 1, 100 do
                ok, val, info = store:SetData('item'..v, v)
            end

            print('==============ListKeys============')
            success, value = store:ListKeys('item3', false, 5)
            Log.log_page(success, value)
            if success == 0 then
                repeat
                    success, val = value:GetCurrentPage()
                    for k, v in pairs(val) do
                        print('ListKeys:' .. k .. ' : ' .. v)
                    end
                until(not value:AdvanceToNextPage())
            end

            print('==============ListValues============')
            success, value = store:ListValues('item3', false, 5)
            Log.log_page(success, value)
            if success == 0 then
                repeat
                    success, val = value:GetCurrentPage()
                    for k, v in pairs(val) do
                        print('ListValues:', k, v)
                    end
                until(not value:AdvanceToNextPage())
            end
        end)

Metadata

When you are saving data, to ensure data validity and consistency, DataStore will additionally record certain data storage operations. DataStore also allows you to record custom operation information in the form of metadata while saving data. Both types of data are stored in the form of metadata.

Operation information recorded by the system is called system metadata.

Additional information customized by users is called user metadata.

System metadata of DataStore includes:

  • Data creation time
  • Last update time of data
  • Current data version number

All system metadata can be obtained through the third parameter in the return values of GetData, which is the ParaDataInfo instance. The specific property names are described as follows:

  • createTime: Data creation time
  • updateTime: Last update time
  • version: Current data version number

The IncrementData, SetData, and UpdateData APIs all provide the options parameter for setting user metadata. The type of options is ParaDataSetOptions, and DataStore encapsulates metadata setting APIs through ParaDataSetOptions.

All user metadata can be obtained through the third parameter in the return values of GetData, which is the ParaDataInfo instance. The specific API is:
table GetMetadata()

The following code shows how to use GetData to get corresponding user metadata and then use the SetData to modify the obtained metadata:

    DATASTORE(  
        function()  
          -- Get my_world's arena rank info  
          local ok, store = ParaDataService.GetDataStore('my_world', 'arena_rank_point')  
          if ok ~= 0 then  
            print('Load data store failed! code is:' .. ok .. ' message is:' .. store)  
            return  
          end  
          local player = CS.ParaPlayerService.GetLocalPlayer()  
          if player == nil then  
            print('Local player is nil')  
            return  
          end  
          local point_key = 'point_' .. player.UID
          local set_options = ParaDataService.CreateDataSetOptions()
          local meta = {reason = 'init'}
          set_options:SetMetadata(meta)
          local ok, message = store:SetData(point_key, 20, set_options)  
          if ok ~= 0 then  
            print('Load data failed: error code is:' .. ok .. ' error message is:' .. message)  
            return  
          end

          local ok, point, point_info = store:GetData(point_key)  
          if ok ~= 0 then  
            print('Load data failed: error code is:' .. ok .. ' error message is:' .. point)  
            return  
          end  
          local meta_data = point_info:GetMetadata()  
          local new_meta = {}  
          if meta_data ~= nil then  
            for k, v in pairs(meta_data) do  
              if k == 'reason' then  
                print('Arena point change to ' .. point .. ' due to ' .. v)  
                new_meta[k] = 'change to new reason'  
              else  
                new_meta[k] = v  
              end  
            end  
          end  
          local set_options =  ParaDataService.CreateDataSetOptions()
          set_options:SetMetadata(new_meta)  
          local ok, point, point_info = store:SetData(point_key, point, set_options)  
          if ok ~= 0 then  
            print('Set data failed: error code is:' .. ok .. ' error message is:' .. point)  
            return  
          end  
          print('Set data succeed!')  
        end)

Data Version Management

Only ParaDataStore in DataStore supports version management. That is, when you use SetData, UpdateData, and IncrementData to set new data, ParaDataStore automatically stores the original data in historical versions, increments the version number of the current data by 1, and stores the latest data. Data generated 30 days ago will be automatically cleared.

You can access historical versions and their corresponding metadata or delete specified versions through version-related APIs. Deleted versions cannot be recovered. APIs related to data version management mainly include:

ok, value, DataStoreInfo info = GetVersionData(string key, int version)
ok, DataStorePages pages = ListVersionsData(string key, bool isAsce, int page_size, double min_date, double max_date)
ok, value, DataStoreInfo info = RemoveVersionData(string key, int version)

  • You can use GetVersionData to access the data of a specified version. If the version exists, the value and metadata of that version will be returned as the third parameter in a form of a DataStoreInfo object. If the version does not exist, an error code and the corresponding error message will be returned.
  • You can also use ListVersionsData to return all version information within a specified time period. The time period is passed in UTC format to avoid inconsistencies due to time zone differences. ListVersionsData returns sorted version information that meets the conditions in the structure of ParaDataPages. You can use the pagination access API provided by ParaDataPages to access and process version data.
  • To clear the data of a previous version, you can use RemoveVersionData. After deletion, data of that version will be completely removed from the database and cannot be recovered.

The following code example shows how to traverse all version information and keep the data of the latest 5 versions:

    DATASTORE(  
        function()  
            -- Get my_world's arena rank info  
            local ok, store = ParaDataService.GetDataStore('my_world', 'arena_rank_point')  
            if ok ~= 0 then  
                print('Load data store failed! code is:' .. ok .. ' message is:' .. store)  
                return  
            end  
            local player = CS.ParaPlayerService.GetLocalPlayer()  
            if player == nil then  
                print('Local player is nil')  
                return  
            end  
            local page_size = 10  
            local user_key = 'user_' .. player.UID  

            local ok, message = store:SetData(user_key, 100)
            if ok ~= 0 then  
                print('Set data store failed! code is:' .. ok .. ' message is:' .. message)  
                return
            end
            local ok, message = store:SetData(user_key, 90)
            if ok ~= 0 then  
                print('Set data store failed! code is:' .. ok .. ' message is:' .. message)  
                return
            end

            local ok, version_pages = store:ListVersionsData(user_key, false, page_size)  
            if ok ~= 0 then  
                print('Load data store failed! code is:' .. ok .. ' message is:' .. version_pages)  
                return  
            end  
            local kept_version_size = 5
            repeat  
                ok, current_page = version_pages:GetCurrentPage()  
                if ok == 0 then  
                    for k, v in pairs(current_page) do  
                        print('key:' .. k .. ' value:' .. tostring(v.value) .. ' version:' .. tostring(v.version))  
                        if kept_version_size >= 0 then  
                            kept_version_size = kept_version_size - 1  
                        else  
                            store:RemoveVersionData(user_key, v.version)  
                        end  
                    end  
                else  
                    print('GetCurrentPage return error, code is ' .. ok .. ' error message is ' .. current_page)  
                end
            until(not version_pages:AdvanceToNextPage())  
        end)

Data Access Control

As DataStore occupies global server storage resources, there are certain limitations on the length of keys and values that can be stored. The specific limitations are as follows:

Key:

The key must be a string or number, and the maximum length is 256 bytes.

Value:

The value must be a string or number, and the maximum length is 32,767 bytes.

Metadata key:

The metadata key must be a string or number, and the maximum length is 256 bytes.

Metadata value:

The metadata value must be a string or number, and the maximum length is 4,096 bytes.

Size of the entire database:

The server space used for storing all data of a single World cannot exceed 1 GB.

Access traffic throttling:

The maximum number of DataStore API calls in a single room is 60 per minute. Any calls beyond this limit will be queued for processing, with a maximum of 500 requests allowed in the queue. Once the queue reaches the limit of 500 requests, all DataStore requests for that room will fail.

Error Codes

Error CodeError MessageRemarks
-10001DataStore Request successful, but key not found. Ensure the data for the key is set first, then try again.
-10002Value is not number.
-10003DataStore Request successful, but version not found.
-10004Current data cannot be sorted.
-10005DataStore name can't be empty.
-10006Request failed. Please try again.
-10007Request is invalid. Invalid datastore.
-10008Request is invalid.
-10009Request is invalid.
-10010Request is invalid.
-10011Value can't be empty. If you want to save empty data, you can set the value to empty string.
-10012Key name exceeds the 256 character limit.
-10013Value size exceeds 32767 bytes limit.
-10014DataStore request dropped. Request was throttled, but throttled request queue was full.
-10015Cannot write to DataStore from Editor if Datastore is not enabled.
-10016Meta key exceed max size
-10017Metadata attribute size exceeds 4096 bytes limit.
-10018World Id is invalid.
-10019scope must be empty if all_scope is set.
-10020Key name can't be empty.
-10021param name is invalid must be string
-10022Version must be integer.
-10023param delta is invalid must be number
-10024PageSize must be within a predefined range (The maximum page size for an OrderedDataStore is 100).
-10026MaxPage and MinPage must be integers.
-10027Attribute metadata format is invalid.
-10028Cannot store this type in DataStore.
-10029Request failed. Please try again.
-10030Request failed. Please contact us.
-10031Request failed. Please try again.
-10032DataStore request dropped. Request was throttled, but throttled request queue was full.
-10033The data retrieved is empty and the number of pages in DataStorePages is 0.
-10034DataStore related functions should be placed in the DATASTORE.
-10035Unsupported value type, currently support numbers, booleans, strings and tables containing only these types.
-10036Current OrderedDataStore do not support DataStoreSetOptions.
-10037param index can not be empty and must be integer.
-10039Current DataStore do not support this operation.
-10040param Value is invalid must be number in OrderedDataStore.
-10041start_index and stop_index must be integers.
-10043param index can not be out of list range.
-10044param index can not be greater than page count.
-10045param key miss scope when enable all_scope.
-10046param is_asc must be bool.
-10047param prefix must be string.
-10049DataStore type dismatch.
-10050DataStore is not available temporarily
-10051DataStore internal error.
-10052Only world creator is allowed to access release data store.