328 lines
14 KiB
Lua
328 lines
14 KiB
Lua
|
--- Operations for game data / save slots.
|
||
|
-- @module Noble.GameData
|
||
|
--
|
||
|
Noble.GameData = {}
|
||
|
|
||
|
local gameDatas = {} -- This is the actual "GameDatas" object, which holds multiple GameData slots. We keep it local to avoid direct tampering.
|
||
|
local gameDataDefault = nil
|
||
|
local numberOfGameDataSlotsAtSetup = 1
|
||
|
local numberOfSlots = 1
|
||
|
local currentSlot = 1 -- This is a helper value, so you don't have to specify a save slot with every GameData operation.
|
||
|
|
||
|
local function keyChange(__dataDefault, __data)
|
||
|
local defaultKeys = {}
|
||
|
local keys = {}
|
||
|
for key, value in pairs(__dataDefault.data) do
|
||
|
table.insert(defaultKeys, key)
|
||
|
end
|
||
|
for key, value in pairs(__data.data) do
|
||
|
table.insert(keys, key)
|
||
|
end
|
||
|
for i = 1, #keys, 1 do
|
||
|
if (defaultKeys[i] ~= keys[i]) then return true end
|
||
|
end
|
||
|
return false
|
||
|
end
|
||
|
|
||
|
local function exists(__gameDataSlot, __key)
|
||
|
-- Check for valid gameSlot.
|
||
|
if (__gameDataSlot > #gameDatas or __gameDataSlot <= 0 ) then
|
||
|
error("BONK: Game Slot number " .. __gameDataSlot .. " does not exist. Use Noble.GameData.addSlot().", 3)
|
||
|
return false
|
||
|
end
|
||
|
|
||
|
if (__key ~= nil) then
|
||
|
-- Check for valid data item.
|
||
|
for key, value in pairs(gameDatas[__gameDataSlot].data) do
|
||
|
if __key == key then
|
||
|
return true
|
||
|
end
|
||
|
end
|
||
|
else
|
||
|
return true
|
||
|
end
|
||
|
|
||
|
error("BONK: Game Datum \"" .. __key .. "\" does not exist. Maybe you spellet it wronlgly.", 3)
|
||
|
return false
|
||
|
end
|
||
|
|
||
|
local function updateTimestamp(__gameData)
|
||
|
__gameData.timestamp = playdate.getGMTTime()
|
||
|
end
|
||
|
|
||
|
local gameDataHasBeenSetup = false
|
||
|
|
||
|
--- Sets up the GameDatas (save slots) for your game, and/or loads any existing GameDatas from disk.
|
||
|
-- You can only run this once, ideally in your main.lua before you load your first scene.
|
||
|
-- @tparam table __keyValuePairs All the data items for a saved game, and their default values, as key/value pairs. <strong>NOTE:</strong> Do not use "nil" as a value.
|
||
|
-- @int[opt=1] __numberOfSlots If you want multiple save slots, enter an integer here. You can add additional slots later.
|
||
|
-- @bool[opt=true] __saveToDisk Saves your default values immediately to disk.
|
||
|
-- @bool[opt=true] __modifyExistingOnKeyChange Updates the existing gameData objects on disk if you make changes to your keys (not values) during development or when updating your game.
|
||
|
-- @usage
|
||
|
-- Noble.GameData.setup(
|
||
|
-- {
|
||
|
-- name = "",
|
||
|
-- checkpointReached = 0,
|
||
|
-- score = 0
|
||
|
-- },
|
||
|
-- 3,
|
||
|
-- true,
|
||
|
-- true
|
||
|
-- )
|
||
|
-- Noble.GameData.set("name", "Game A", 1)
|
||
|
-- Noble.GameData.set("name", "Game B", 2)
|
||
|
-- Noble.GameData.set("name", "Game C", 3)
|
||
|
-- @see addSlot
|
||
|
-- @see deleteSlot
|
||
|
function Noble.GameData.setup(__keyValuePairs, __numberOfSlots, __saveToDisk, __modifyExistingOnKeyChange)
|
||
|
if (gameDataHasBeenSetup) then
|
||
|
error("BONK: You can only run Noble.GameData.setup() once.")
|
||
|
return
|
||
|
else
|
||
|
gameDataHasBeenSetup = true
|
||
|
end
|
||
|
|
||
|
numberOfSlots = __numberOfSlots or numberOfSlots
|
||
|
numberOfGameDataSlotsAtSetup = numberOfSlots
|
||
|
|
||
|
local saveToDisk = Utilities.handleOptionalBoolean(__saveToDisk, true)
|
||
|
local modifyExistingOnKeyChange = Utilities.handleOptionalBoolean(__modifyExistingOnKeyChange, true)
|
||
|
|
||
|
gameDataDefault = {
|
||
|
data = __keyValuePairs,
|
||
|
timestamp = playdate.getGMTTime()
|
||
|
}
|
||
|
|
||
|
local createdNewData = false
|
||
|
|
||
|
-- Noble Engine checks on disk for GameDatas, including ones that were
|
||
|
-- added with addSlot, but it assumes your game will have no greater than 1000 of them.
|
||
|
for i = 1, 1000, 1 do
|
||
|
-- We use a local here to avoid adding a nil item to the gameDatas table.
|
||
|
local gameData = Datastore.read("Game" .. i)
|
||
|
|
||
|
if (gameData == nil) then
|
||
|
if (i <= numberOfSlots) then
|
||
|
-- No gameData on disk, so we create a new ones using default values
|
||
|
-- up to the numberOfGameDataSlots.
|
||
|
gameDatas[i] = table.deepcopy(gameDataDefault)
|
||
|
createdNewData = true
|
||
|
else
|
||
|
-- We can't find any more GameDatas on disk, so we update the
|
||
|
-- value of numberOfGameDataSlots if necessary and get outta town!
|
||
|
numberOfSlots = i - 1
|
||
|
print("Total number of game slots: " .. numberOfSlots)
|
||
|
|
||
|
if (saveToDisk and createdNewData) then
|
||
|
Noble.GameData.saveAll()
|
||
|
end
|
||
|
|
||
|
return -- This is our only way out!
|
||
|
end
|
||
|
else
|
||
|
-- We found a gameData on disk, so we use it (either as-is or modified by a key change).
|
||
|
if (modifyExistingOnKeyChange and keyChange(gameDataDefault, gameData)) then
|
||
|
-- Found gameData on disk, but key changes have been made...
|
||
|
-- ...so we start with a new one, with default values...
|
||
|
local existingGameData = table.deepcopy(gameData)
|
||
|
gameData = table.deepcopy(gameDataDefault)
|
||
|
for key, _ in pairs(gameData.data) do
|
||
|
-- ...then copy data with unchanged keys to the new object,
|
||
|
-- naturally discarding keys that don't exist anymore.
|
||
|
if (existingGameData.data[key] ~= nil) then gameData.data[key] = existingGameData.data[key] end
|
||
|
end
|
||
|
gameDatas.timestamp = existingGameData.timestamp
|
||
|
createdNewData = true
|
||
|
end
|
||
|
gameDatas[i] = gameData
|
||
|
end
|
||
|
|
||
|
end
|
||
|
|
||
|
end
|
||
|
|
||
|
--- Returns the value of the requested data item.
|
||
|
-- @string __dataItemName The name of the data item.
|
||
|
-- @int[opt] __gameDataSlot If set, uses a specific GameData slot. If not, uses the most recently touched GameData slot.
|
||
|
-- @treturn any
|
||
|
-- @usage Noble.GameData.get("equippedItem")
|
||
|
-- @usage Noble.GameData.get("equippedItem", 2)
|
||
|
function Noble.GameData.get(__dataItemName, __gameDataSlot)
|
||
|
currentSlot = __gameDataSlot or currentSlot
|
||
|
if (exists(currentSlot, __dataItemName)) then
|
||
|
return gameDatas[currentSlot].data[__dataItemName]
|
||
|
end
|
||
|
end
|
||
|
|
||
|
--- Set the value of a GameData item.
|
||
|
-- @string __dataItemName The name of the data item.
|
||
|
-- @tparam any __value The data item's new value
|
||
|
-- @int[opt] __gameDataSlot If set, uses a specific GameData slot. If not, uses the most recently touched GameData slot.
|
||
|
-- @bool[opt=true] __saveToDisk Saves to disk immediately. Set to false if you prefer to manually save (via a checkpoint or menu).
|
||
|
-- @bool[opt=true] __updateTimestamp Sets the timestamp of this GameData to the current time. Leave false to retain existing timestamp.
|
||
|
-- @usage Noble.GameData.set("score", 74205)
|
||
|
-- @usage Noble.GameData.set("score", Noble.GameData.get("score") + 100)
|
||
|
-- @see save
|
||
|
function Noble.GameData.set(__dataItemName, __value, __gameDataSlot, __saveToDisk, __updateTimestamp)
|
||
|
currentSlot = __gameDataSlot or currentSlot
|
||
|
if (exists(currentSlot, __dataItemName)) then
|
||
|
gameDatas[currentSlot].data[__dataItemName] = __value
|
||
|
local setTimestamp = Utilities.handleOptionalBoolean(__updateTimestamp, true)
|
||
|
if (setTimestamp) then updateTimestamp(gameDatas[currentSlot]) end
|
||
|
local saveToDisk = Utilities.handleOptionalBoolean(__saveToDisk, true)
|
||
|
if (saveToDisk) then Noble.GameData.save() end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
--- Reset a GameData item to its default value, defined in @{setup|setup}.
|
||
|
-- @string __dataItemName The name of the data item.
|
||
|
-- @int[opt] __gameDataSlot If set, uses a specific GameData slot. If not, uses the most recently touched GameData slot.
|
||
|
-- @bool[opt=true] __saveToDisk Saves to disk immediately. Set to false if you prefer to manually save (via a checkpoint or menu).
|
||
|
-- @bool[opt=true] __updateTimestamp Resets the timestamp of this GameData to the current time. Leave false to retain existing timestamp.
|
||
|
-- @see save
|
||
|
function Noble.GameData.reset(__dataItemName, __gameDataSlot, __saveToDisk, __updateTimestamp)
|
||
|
currentSlot = __gameDataSlot or currentSlot
|
||
|
if (exists(currentSlot, __dataItemName)) then
|
||
|
gameDatas[currentSlot].data[__dataItemName] = gameDataDefault.data[__dataItemName]
|
||
|
local setTimestamp = Utilities.handleOptionalBoolean(__updateTimestamp, true)
|
||
|
if (setTimestamp) then updateTimestamp(gameDatas[currentSlot]) end
|
||
|
local saveToDisk = Utilities.handleOptionalBoolean(__saveToDisk, true)
|
||
|
if (saveToDisk) then Noble.GameData.save(currentSlot) end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
--- Reset all values in a GameData slot to the default values, defined in @{setup|setup}.
|
||
|
-- @int[opt] __gameDataSlot If set, uses a specific GameData slot. If not, uses the most recently touched GameData slot.
|
||
|
-- @bool[opt=true] __saveToDisk Saves to disk immediately. Set to false if you prefer to manually save (via a checkpoint or menu).
|
||
|
-- @bool[opt=true] __updateTimestamp Resets the timestamp of this GameData to the current time. Leave false to retain existing timestamp.
|
||
|
-- @see save
|
||
|
function Noble.GameData.resetAll(__gameDataSlot, __saveToDisk, __updateTimestamp)
|
||
|
currentSlot = __gameDataSlot or currentSlot
|
||
|
for key, _ in pairs(gameDatas[currentSlot].data) do
|
||
|
gameDatas[currentSlot].data[key] = gameDataDefault.data[key]
|
||
|
end
|
||
|
local setTimestamp = Utilities.handleOptionalBoolean(__updateTimestamp, true)
|
||
|
if (setTimestamp) then updateTimestamp(gameDatas[currentSlot]) end
|
||
|
local saveToDisk = Utilities.handleOptionalBoolean(__saveToDisk, true)
|
||
|
if (saveToDisk) then Noble.GameData.save(currentSlot) end
|
||
|
end
|
||
|
|
||
|
--- Add a save slot to your game. This is useful for games which have arbitrary save slots, or encourage save scumming.
|
||
|
-- @int[opt=1] __numberToAdd What it says on the tin.
|
||
|
-- @bool[opt=true] __saveToDisk Saves to disk immediately. Set to false if you prefer to manually save (via a checkpoint or menu).
|
||
|
-- @usage Noble.GameData.addSlot()
|
||
|
-- @usage Noble.GameData.addSlot(10)
|
||
|
function Noble.GameData.addSlot(__numberToAdd, __saveToDisk)
|
||
|
local numberToAdd = __numberToAdd or 1
|
||
|
local saveToDisk = Utilities.handleOptionalBoolean(__saveToDisk, true)
|
||
|
if (__numberToAdd < 1) then error ("BONK: Don't use a number smaller than 1, silly.", 2) return end
|
||
|
for i = 1, numberToAdd, 1 do
|
||
|
local newGameData = table.deepcopy(gameDataDefault)
|
||
|
updateTimestamp(newGameData)
|
||
|
table.insert(gameDatas, newGameData)
|
||
|
if (saveToDisk) then Noble.GameData.save() end
|
||
|
end
|
||
|
numberOfSlots = numberOfSlots + numberToAdd
|
||
|
print("Added " .. numberToAdd .. " GameData slots. Total GameData slots: " .. numberOfSlots)
|
||
|
end
|
||
|
|
||
|
--- Deletes a GameData from disk if its save slot is greater than the default number established in `setup`.
|
||
|
-- Otherwise, resets all data items to default values using @{resetAll|resetAll}.
|
||
|
--
|
||
|
-- Generally, you won't need this unless you've added save slots using @{addSlot|addSlot}. In other cases, use @{resetAll|resetAll}.
|
||
|
-- @int __gameDataSlot The slot holding the GameData to delete. Unlike other methods that take this argument, this is not optional.
|
||
|
-- @bool[opt=true] __collapseGameDatas Re-sorts the gameDatas table (and renames existing JSON files on disk) to fill the gap left by the deleted GameData.
|
||
|
-- @see deleteAllSlots
|
||
|
-- @see addSlot
|
||
|
-- @see resetAll
|
||
|
-- @usage Noble.GameData.deleteSlot(6)
|
||
|
-- @usage Noble.GameData.deleteSlot(15, false)
|
||
|
function Noble.GameData.deleteSlot(__gameDataSlot, __collapseGameDatas)
|
||
|
if (__gameDataSlot == nil) then
|
||
|
error("BONK: You must specify a GameData slot to delete.")
|
||
|
end
|
||
|
local collapseGameDatas = Utilities.handleOptionalBoolean(__collapseGameDatas, true)
|
||
|
if (exists(__gameDataSlot)) then
|
||
|
if (__gameDataSlot > numberOfGameDataSlotsAtSetup) then
|
||
|
-- If this GameData is not one of the default ones from setup(), it is removed from disk.
|
||
|
if (playdate.file.exists("Game" .. __gameDataSlot)) then
|
||
|
gameDatas[__gameDataSlot] = nil -- Clear from memory.
|
||
|
Datastore.delete("Game" .. __gameDataSlot) -- Clear from disk.
|
||
|
if (currentSlot == __gameDataSlot) then currentSlot = 1 end -- Safety!
|
||
|
if (collapseGameDatas) then
|
||
|
-- Collapse GameDatas
|
||
|
local newGameDatas = {}
|
||
|
for i = 1, numberOfSlots do
|
||
|
if(gameDatas[i] ~= nil) then
|
||
|
table.insert(newGameDatas, gameDatas[i])
|
||
|
if (i >= __gameDataSlot) then
|
||
|
playdate.file.rename("Game" .. __gameDataSlot, "Game" .. __gameDataSlot-1)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
gameDatas = newGameDatas
|
||
|
numberOfSlots = numberOfSlots - 1
|
||
|
end
|
||
|
else
|
||
|
error("BONK: This GameData is in memory, but its file doesn't exist on disk, so it can't be deleted.", 2)
|
||
|
end
|
||
|
else
|
||
|
-- If this GameData is one of the default ones from setup(), it is reset to default values.
|
||
|
Noble.GameData.resetAll(__gameDataSlot)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
--- Deletes all GameDatas from disk, except for the number specified in setup, which are reset to default values.
|
||
|
-- Use this to clear all data as if you were running setup again.
|
||
|
-- Generally, you don't need this unless you've added save slots using @{addSlot|addSlot}. In other cases, use @{resetAll|resetAll} on each slot.
|
||
|
-- @see deleteSlot
|
||
|
-- @see addSlot
|
||
|
function Noble.GameData.deleteAllSlots()
|
||
|
local numberToDelete = numberOfSlots
|
||
|
for i = numberToDelete, 1, -1 do
|
||
|
Noble.GameData.deleteSlot(i, false) -- Don't need to collapse the table because we're deleting them in reverse order.
|
||
|
end
|
||
|
end
|
||
|
|
||
|
--- Returns the timestamp of the requested GameData, as a tuple (local time, GMT time). The timestamp is updated
|
||
|
-- See Playdate SDK for details on how a time object is formatted. <strong>NOTE: Timestamps are stored internally in GMT.</strong>
|
||
|
-- @int __gameDataSlot The GameData slot to get the timestamp of. Unlike other methods that take this argument, this is not optional.
|
||
|
-- @treturn table Local time
|
||
|
-- @treturn table GMT time
|
||
|
-- @usage Noble.GameData.getTimestamp(1)
|
||
|
function Noble.GameData.getTimestamp(__gameDataSlot)
|
||
|
if (exists(__gameDataSlot)) then
|
||
|
return playdate.timeFromEpoch(playdate.epochFromGMTTime(gameDatas[__gameDataSlot].timestamp)), gameDatas[__gameDataSlot].timestamp
|
||
|
end
|
||
|
end
|
||
|
|
||
|
--- Returns the current number of GameData slots.
|
||
|
-- @treturn int
|
||
|
function Noble.GameData.getNumberOfSlots() return numberOfSlots end
|
||
|
|
||
|
--- Returns the number of the current GameData slot.
|
||
|
-- @treturn int
|
||
|
function Noble.GameData.getCurrentSlot() return currentSlot end
|
||
|
|
||
|
--- Saves a single GameData to disk. If you want to save all GameDatas, use @{saveAll|saveAll} instead.
|
||
|
-- @int[opt] __gameDataSlot If set, uses a specific GameData slot. If not, uses the most recently touched GameData slot.
|
||
|
-- @see saveAll
|
||
|
-- @usage Noble.GameData.save()
|
||
|
-- @usage Noble.GameData.save(3)
|
||
|
function Noble.GameData.save(__gameDataSlot)
|
||
|
local gameDataSlot = __gameDataSlot or currentSlot
|
||
|
if (exists(gameDataSlot)) then
|
||
|
currentSlot = gameDataSlot
|
||
|
Datastore.write(gameDatas[currentSlot], "Game" .. currentSlot)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
--- Save all GameDatas to disk. If you only have one, or want to save a specific one, use @{save|save} instead.
|
||
|
-- @see save
|
||
|
function Noble.GameData.saveAll()
|
||
|
for i = 1, numberOfSlots, 1 do
|
||
|
Datastore.write(gameDatas[i], "Game" .. currentSlot)
|
||
|
end
|
||
|
end
|