/
preferences_savefile.dm
411 lines (324 loc) · 13.9 KB
/
preferences_savefile.dm
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
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
//This is the lowest supported version, anything below this is completely obsolete and the entire savefile will be wiped.
#define SAVEFILE_VERSION_MIN 32
//This is the current version, anything below this will attempt to update (if it's not obsolete)
// You do not need to raise this if you are adding new values that have sane defaults.
// Only raise this value when changing the meaning/format/name/layout of an existing value
// where you would want the updater procs below to run
#define SAVEFILE_VERSION_MAX 42
/*
SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Carn
This proc checks if the current directory of the savefile S needs updating
It is to be used by the load_character and load_preferences procs.
(S.cd=="/" is preferences, S.cd=="/character[integer]" is a character slot, etc)
if the current directory's version is below SAVEFILE_VERSION_MIN it will simply wipe everything in that directory
(if we're at root "/" then it'll just wipe the entire savefile, for instance.)
if its version is below SAVEFILE_VERSION_MAX but above the minimum, it will load data but later call the
respective update_preferences() or update_character() proc.
Those procs allow coders to specify format changes so users do not lose their setups and have to redo them again.
Failing all that, the standard sanity checks are performed. They simply check the data is suitable, reverting to
initial() values if necessary.
*/
/datum/preferences/proc/savefile_needs_update(savefile/S)
var/savefile_version
READ_FILE(S["version"], savefile_version)
if(savefile_version < SAVEFILE_VERSION_MIN)
S.dir.Cut()
return -2
if(savefile_version < SAVEFILE_VERSION_MAX)
return savefile_version
return -1
//should these procs get fairly long
//just increase SAVEFILE_VERSION_MIN so it's not as far behind
//SAVEFILE_VERSION_MAX and then delete any obsolete if clauses
//from these procs.
//This only really meant to avoid annoying frequent players
//if your savefile is 3 months out of date, then 'tough shit'.
/datum/preferences/proc/update_preferences(current_version, savefile/S)
if(current_version < 33)
toggles |= SOUND_ENDOFROUND
if(current_version < 34)
write_preference(/datum/preference/toggle/auto_fit_viewport, TRUE)
if(current_version < 35) //makes old keybinds compatible with #52040, sets the new default
var/newkey = FALSE
for(var/list/key in key_bindings)
for(var/bind in key)
if(bind == "quick_equipbelt")
key -= "quick_equipbelt"
key |= "quick_equip_belt"
if(bind == "bag_equip")
key -= "bag_equip"
key |= "quick_equip_bag"
if(bind == "quick_equip_suit_storage")
newkey = TRUE
if(!newkey && !key_bindings["ShiftQ"])
key_bindings["ShiftQ"] = list("quick_equip_suit_storage")
if(current_version < 36)
if(key_bindings["ShiftQ"] == "quick_equip_suit_storage")
key_bindings["ShiftQ"] = list("quick_equip_suit_storage")
if(current_version < 37)
if(read_preference(/datum/preference/numeric/fps) == 0)
write_preference(GLOB.preference_entries[/datum/preference/numeric/fps], -1)
if (current_version < 38)
var/found_block_movement = FALSE
for (var/list/key in key_bindings)
for (var/bind in key)
if (bind == "block_movement")
found_block_movement = TRUE
break
if (found_block_movement)
break
if (!found_block_movement)
LAZYADD(key_bindings["Ctrl"], "block_movement")
if (current_version < 39)
LAZYADD(key_bindings["F"], "toggle_combat_mode")
LAZYADD(key_bindings["4"], "toggle_combat_mode")
if (current_version < 40)
LAZYADD(key_bindings["Space"], "hold_throw_mode")
if (current_version < 41)
migrate_preferences_to_tgui_prefs_menu()
/datum/preferences/proc/update_character(current_version, savefile/savefile)
if (current_version < 41)
migrate_character_to_tgui_prefs_menu()
if (current_version < 42)
migrate_body_types(savefile)
/// checks through keybindings for outdated unbound keys and updates them
/datum/preferences/proc/check_keybindings()
if(!parent)
return
var/list/binds_by_key = get_key_bindings_by_key(key_bindings)
var/list/notadded = list()
for (var/name in GLOB.keybindings_by_name)
var/datum/keybinding/kb = GLOB.keybindings_by_name[name]
if(kb.name in key_bindings)
continue // key is unbound and or bound to something
var/addedbind = FALSE
key_bindings[kb.name] = list()
if(parent.hotkeys)
for(var/hotkeytobind in kb.hotkey_keys)
if(!length(binds_by_key[hotkeytobind]) && hotkeytobind != "Unbound") //Only bind to the key if nothing else is bound expect for Unbound
key_bindings[kb.name] |= hotkeytobind
addedbind = TRUE
else
for(var/classickeytobind in kb.classic_keys)
if(!length(binds_by_key[classickeytobind]) && classickeytobind != "Unbound") //Only bind to the key if nothing else is bound expect for Unbound
key_bindings[kb.name] |= classickeytobind
addedbind = TRUE
if(!addedbind)
notadded += kb
save_preferences() //Save the players pref so that new keys that were set to Unbound as default are permanently stored
if(length(notadded))
addtimer(CALLBACK(src, .proc/announce_conflict, notadded), 5 SECONDS)
/datum/preferences/proc/announce_conflict(list/notadded)
to_chat(parent, "<span class='warningplain'><b><u>Keybinding Conflict</u></b></span>\n\
<span class='warningplain'><b>There are new <a href='?src=[REF(src)];open_keybindings=1'>keybindings</a> that default to keys you've already bound. The new ones will be unbound.</b></span>")
for(var/item in notadded)
var/datum/keybinding/conflicted = item
to_chat(parent, span_danger("[conflicted.category]: [conflicted.full_name] needs updating"))
/datum/preferences/proc/load_path(ckey,filename="preferences.sav")
if(!ckey)
return
path = "data/player_saves/[ckey[1]]/[ckey]/[filename]"
/datum/preferences/proc/load_preferences()
if(!path)
return FALSE
if(!fexists(path))
return FALSE
var/savefile/S = new /savefile(path)
if(!S)
return FALSE
S.cd = "/"
var/needs_update = savefile_needs_update(S)
if(needs_update == -2) //fatal, can't load any data
var/bacpath = "[path].updatebac" //todo: if the savefile version is higher then the server, check the backup, and give the player a prompt to load the backup
if (fexists(bacpath))
fdel(bacpath) //only keep 1 version of backup
fcopy(S, bacpath) //byond helpfully lets you use a savefile for the first arg.
return FALSE
apply_all_client_preferences()
//general preferences
READ_FILE(S["lastchangelog"], lastchangelog)
READ_FILE(S["be_special"] , be_special)
READ_FILE(S["default_slot"], default_slot)
READ_FILE(S["chat_toggles"], chat_toggles)
READ_FILE(S["toggles"], toggles)
READ_FILE(S["ignoring"], ignoring)
// OOC commendations
READ_FILE(S["hearted_until"], hearted_until)
if(hearted_until > world.realtime)
hearted = TRUE
//favorite outfits
READ_FILE(S["favorite_outfits"], favorite_outfits)
var/list/parsed_favs = list()
for(var/typetext in favorite_outfits)
var/datum/outfit/path = text2path(typetext)
if(ispath(path)) //whatever typepath fails this check probably doesn't exist anymore
parsed_favs += path
favorite_outfits = unique_list(parsed_favs)
// Custom hotkeys
READ_FILE(S["key_bindings"], key_bindings)
//try to fix any outdated data if necessary
if(needs_update >= 0)
var/bacpath = "[path].updatebac" //todo: if the savefile version is higher then the server, check the backup, and give the player a prompt to load the backup
if (fexists(bacpath))
fdel(bacpath) //only keep 1 version of backup
fcopy(S, bacpath) //byond helpfully lets you use a savefile for the first arg.
update_preferences(needs_update, S) //needs_update = savefile_version if we need an update (positive integer)
check_keybindings() // this apparently fails every time and overwrites any unloaded prefs with the default values, so don't load anything after this line or it won't actually save
key_bindings_by_key = get_key_bindings_by_key(key_bindings)
//Sanitize
lastchangelog = sanitize_text(lastchangelog, initial(lastchangelog))
default_slot = sanitize_integer(default_slot, 1, max_save_slots, initial(default_slot))
toggles = sanitize_integer(toggles, 0, (2**24)-1, initial(toggles))
be_special = sanitize_be_special(SANITIZE_LIST(be_special))
key_bindings = sanitize_keybindings(key_bindings)
favorite_outfits = SANITIZE_LIST(favorite_outfits)
if(needs_update >= 0) //save the updated version
var/old_default_slot = default_slot
var/old_max_save_slots = max_save_slots
for (var/slot in S.dir) //but first, update all current character slots.
if (copytext(slot, 1, 10) != "character")
continue
var/slotnum = text2num(copytext(slot, 10))
if (!slotnum)
continue
max_save_slots = max(max_save_slots, slotnum) //so we can still update byond member slots after they lose memeber status
default_slot = slotnum
if (load_character())
save_character()
default_slot = old_default_slot
max_save_slots = old_max_save_slots
save_preferences()
return TRUE
/datum/preferences/proc/save_preferences()
if(!path)
return FALSE
var/savefile/S = new /savefile(path)
if(!S)
return FALSE
S.cd = "/"
WRITE_FILE(S["version"] , SAVEFILE_VERSION_MAX) //updates (or failing that the sanity checks) will ensure data is not invalid at load. Assume up-to-date
for (var/preference_type in GLOB.preference_entries)
var/datum/preference/preference = GLOB.preference_entries[preference_type]
if (preference.savefile_identifier != PREFERENCE_PLAYER)
continue
if (!(preference.type in recently_updated_keys))
continue
recently_updated_keys -= preference.type
if (preference_type in value_cache)
write_preference(preference, preference.serialize(value_cache[preference_type]))
//general preferences
WRITE_FILE(S["lastchangelog"], lastchangelog)
WRITE_FILE(S["be_special"], be_special)
WRITE_FILE(S["default_slot"], default_slot)
WRITE_FILE(S["toggles"], toggles)
WRITE_FILE(S["chat_toggles"], chat_toggles)
WRITE_FILE(S["ignoring"], ignoring)
WRITE_FILE(S["key_bindings"], key_bindings)
WRITE_FILE(S["hearted_until"], (hearted_until > world.realtime ? hearted_until : null))
WRITE_FILE(S["favorite_outfits"], favorite_outfits)
return TRUE
/datum/preferences/proc/load_character(slot)
SHOULD_NOT_SLEEP(TRUE)
if(!path)
return FALSE
if(!fexists(path))
return FALSE
character_savefile = null
var/savefile/S = new /savefile(path)
if(!S)
return FALSE
S.cd = "/"
if(!slot)
slot = default_slot
slot = sanitize_integer(slot, 1, max_save_slots, initial(default_slot))
if(slot != default_slot)
default_slot = slot
WRITE_FILE(S["default_slot"] , slot)
S.cd = "/character[slot]"
var/needs_update = savefile_needs_update(S)
if(needs_update == -2) //fatal, can't load any data
return FALSE
// Read everything into cache
for (var/preference_type in GLOB.preference_entries)
var/datum/preference/preference = GLOB.preference_entries[preference_type]
if (preference.savefile_identifier != PREFERENCE_CHARACTER)
continue
value_cache -= preference_type
read_preference(preference_type)
//Character
READ_FILE(S["randomise"], randomise)
READ_FILE(S["persistent_scars"] , persistent_scars)
//Load prefs
READ_FILE(S["job_preferences"], job_preferences)
//Quirks
READ_FILE(S["all_quirks"], all_quirks)
//try to fix any outdated data if necessary
//preference updating will handle saving the updated data for us.
if(needs_update >= 0)
update_character(needs_update, S) //needs_update == savefile_version if we need an update (positive integer)
//Sanitize
randomise = SANITIZE_LIST(randomise)
persistent_scars = sanitize_integer(persistent_scars)
//Validate job prefs
for(var/j in job_preferences)
if(job_preferences[j] != JP_LOW && job_preferences[j] != JP_MEDIUM && job_preferences[j] != JP_HIGH)
job_preferences -= j
all_quirks = SSquirks.filter_invalid_quirks(SANITIZE_LIST(all_quirks))
validate_quirks()
return TRUE
/datum/preferences/proc/save_character()
SHOULD_NOT_SLEEP(TRUE)
if(!path)
return FALSE
var/savefile/S = new /savefile(path)
if(!S)
return FALSE
S.cd = "/character[default_slot]"
for (var/datum/preference/preference as anything in get_preferences_in_priority_order())
if (preference.savefile_identifier != PREFERENCE_CHARACTER)
continue
if (!(preference.type in recently_updated_keys))
continue
recently_updated_keys -= preference.type
if (preference.type in value_cache)
write_preference(preference, preference.serialize(value_cache[preference.type]))
WRITE_FILE(S["version"] , SAVEFILE_VERSION_MAX) //load_character will sanitize any bad data, so assume up-to-date.)
// This is the version when the random security department was removed.
// When the minimum is higher than that version, it's impossible for someone to have the "Random" department.
#if SAVEFILE_VERSION_MIN > 40
#warn The prefered_security_department check in code/modules/client/preferences/security_department.dm is no longer necessary.
#endif
//Character
WRITE_FILE(S["randomise"] , randomise)
WRITE_FILE(S["persistent_scars"] , persistent_scars)
//Write prefs
WRITE_FILE(S["job_preferences"] , job_preferences)
//Quirks
WRITE_FILE(S["all_quirks"] , all_quirks)
return TRUE
/datum/preferences/proc/sanitize_be_special(list/input_be_special)
var/list/output = list()
for (var/role in input_be_special)
if (role in GLOB.special_roles)
output += role
return output.len == input_be_special.len ? input_be_special : output
/proc/sanitize_keybindings(value)
var/list/base_bindings = sanitize_islist(value,list())
for(var/keybind_name in base_bindings)
if (!(keybind_name in GLOB.keybindings_by_name))
base_bindings -= keybind_name
return base_bindings
#undef SAVEFILE_VERSION_MAX
#undef SAVEFILE_VERSION_MIN
#ifdef TESTING
//DEBUG
//Some crude tools for testing savefiles
//path is the savefile path
/client/verb/savefile_export(path as text)
var/savefile/S = new /savefile(path)
S.ExportText("/",file("[path].txt"))
//path is the savefile path
/client/verb/savefile_import(path as text)
var/savefile/S = new /savefile(path)
S.ImportText("/",file("[path].txt"))
#endif