forked from PlaydateSquad/pd-achievements
-
Notifications
You must be signed in to change notification settings - Fork 0
/
achievements.lua
228 lines (193 loc) · 8.11 KB
/
achievements.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
--[[
==prototype achievements library==
This is an initial prototype implementation in order to help effect a standard.
This prototype will have no strong error checks and be small in scope. Any
wider-scope implementation of the standard will be separate.
== planned usage ==
Import the library using `import "achievements"
The library has now created a global variable named "achievements".
I hate this approach, but it's a prototype and this is how the playdate
does things because y'all are crazy.
The user now needs to configure the library. Make a config table as so:
local achievementData = {
-- Technically, any string. We need to spell it out explicitly
-- instead of using metadata.bundleID so that it won't get
-- mangled by online sideloading. Plus, this way multi-pdx
-- games or demos can share achievements.
gameID = "com.yourcompany.yourgame",
-- These are optional, and will be auto-filled with metadata
-- values if not specified here. This is also for multi-pdx
-- games.
name = "My Awesome Game",
author = "You, Inc",
description = "The next evolution in cranking technology.",
-- And finally, a table of achievements.
achievements = {
{
id = "test_achievement",
name = "Achievement Name",
description = "Achievement Description",
is_secret = false,
icon = "filepath" -- to be iterated on
[more to be determined]
},
}
}
This table makes up the top-level data structure being saved to the shared
json file. The gameID field determines the name of the folder it will
be written to, rather than bundleID, to keep things consistent.
The only thing that is truly required is the gameID field, because this is
necessary for identification. Everything else can be left blank, and it
will be auto-filled or simply absent in the case of achievement data.
The user passes the config table to the library like so:
achievements.initialize(achievementData, preventdebug)
This function finishes populating the configuration table with metadata
if necessary, merges the achievement data with the saved list of granted
achievements, creates the shared folder and .json file with the new data,
and iterates over the achievement data in order to copy images given to
the shared folder.
If `preventdebug` evaluates true, initialization debugging messages will not
be printed to the console.
In order to grant an achievement to the player, run `achievements.grant(id)`
If this is a valid achievement id, it will key the id to the current epoch
second in the achievement save data.
In order to revoke an achievement, run `achievements.revoke(id)`
If this is a valid achievement id, it will remove the id from the save
data keys.
To save achievement data, run `achievements.save()`. This will save granted
achievements to disk and save the game data to the shared json file. Run this
function alongside other game-save functions when the game exits. Of course,
unfortunately, achievements don't respect save slots.
==details==
The achievements file in the game's save directory is the prime authority on active achievements.
It contains nothing more than a map of achievement IDs which have been earned by the player to when they were earned.
This should make it extremely easy to manage, and prevents other games from directly messing with achievement data.
The achievement files in the /Shared/Achievements/bundleID folder are regenerated at game load and when saving.
They are to be generated by serializing `module.achievements` along with `module.localData` and copying any images (when we get to those).
--]]
local local_achievement_file = "Achievements.json"
-- Right, we're gonna make this easier to change in the future.
-- Another note: changing the data directory to `/Shared/gameID`
-- rather than the previously penciled in `/Shared/Achievements/gameID`
local function get_achievement_folder_root_path(gameID)
if type(gameID) ~= "string" then
error("bad argument #1: expected string, got " .. type(gameID), 2)
end
local root = string.format("/Shared/%s/", gameID)
return root
end
local function get_achievement_data_file_path(gameID)
if type(gameID) ~= "string" then
error("bad argument #1: expected string, got " .. type(gameID), 2)
end
local root = get_achievement_folder_root_path(gameID)
return root .. "Achievements.json"
end
-- local achievement_folder = root_folder .. playdate.metadata.bundleID .. "/"
local metadata <const> = playdate.metadata
---@diagnostic disable-next-line: lowercase-global
achievements = {
version = "prototype 0.1"
}
local function load_granted_data()
local data = json.decodeFile(local_achievement_file)
if not data then
data = {}
end
achievements.granted = data
end
local function export_data()
local data = achievements.gameData
json.encodeToFile(get_achievement_data_file_path(data.gameID), true, data)
end
function achievements.save()
export_data()
json.encodeToFile(local_achievement_file, false, achievements.granted)
end
local function copy_images_to_shared()
end
local function donothing(...) end
function achievements.initialize(gamedata, prevent_debug)
local print = (prevent_debug and donothing) or print
print("------")
print("Initializing achievements...")
if gamedata.achievements == nil then
print("WARNING: no achievements configured")
gamedata.achievements = {}
elseif type(gamedata.achievements) ~= "table" then
error("achievements must be a table", 2)
end
if gamedata.gameID == nil then
gamedata.gameID = string.gsub(metadata.bundleID, "^user%.%d+%.", "")
print("gameID not configured: defaulting to \"" .. gamedata.gameID .. "\"")
elseif type(gamedata.gameID) ~= "string" then
error("gameID must be a string", 2)
end
for _, field in ipairs{"name", "author", "description"} do
if gamedata[field] == nil then
gamedata[field] = playdate.metadata[field]
print(field .. " not configured: defaulting to \"" .. gamedata[field] .. "\"")
elseif type(gamedata[field]) ~= "string" then
error(field .. " must be a string", 2)
end
end
gamedata.version = metadata.version
gamedata.libversion = achievements.version
print("game version saved as \"" .. gamedata.version .. "\"")
print("library version saved as \"" .. gamedata.libversion .. "\"")
achievements.gameData = gamedata
load_granted_data()
achievements.keyedAchievements = {}
for _, ach in ipairs(gamedata.achievements) do
if achievements.keyedAchievements[ach.id] then
error("achievement id '" .. ach.id .. "' defined multiple times", 2)
end
achievements.keyedAchievements[ach.id] = ach
ach.granted_at = achievements.granted[ach.id] or false
end
playdate.file.mkdir(get_achievement_folder_root_path(gamedata.gameID))
export_data()
copy_images_to_shared()
print("files exported to /Shared")
print("Achievements have been initialized!")
print("------")
end
--[[ Achievement Management Functions ]]--
achievements.getInfo = function(achievement_id)
return achievements.keyedAchievements[achievement_id] or false
end
achievements.grant = function(achievement_id, display_style)
local ach = achievements.keyedAchievements[achievement_id]
if not ach then
error("attempt to grant unconfigured achevement '" .. achievement_id .. "'", 2)
end
local time = playdate.getSecondsSinceEpoch()
achievements.granted[achievement_id] = ( time )
ach.granted_at = time
-- Drawing to come later...
end
achievements.revoke = function(achievement_id)
local ach = achievements.keyedAchievements[achievement_id]
if not ach then
error("attempt to revoke unconfigured achevement '" .. achievement_id .. "'", 2)
end
ach.granted_at = false
achievements.granted[achievement_id] = nil
end
--[[ External Game Functions ]]--
achievements.gamePlayed = function(game_id)
return playdate.file.isdir(get_achievement_folder_root_path(game_id))
end
achievements.gameData = function(game_id)
if not achievements.gamePlayed(game_id) then
error("No game with ID '" .. game_id .. "' was found", 2)
end
local data = json.decodeFile(get_achievement_data_file_path(game_id))
local keys = {}
for _, ach in ipairs(data.achievements) do
keys[ach.id] = ach
end
data.keyedAchievements = keys
return data
end
return achievements