diff --git a/code/__defines/serde.dm b/code/__defines/serde.dm index a1e84e3fe30c..3eb089ac4365 100644 --- a/code/__defines/serde.dm +++ b/code/__defines/serde.dm @@ -62,3 +62,5 @@ #define FINALIZE_REAGENTS_SERDE(V) if(islist(V)) { FINALIZE_REAGENTS_SERDE_BODY(V); } #define FINALIZE_REAGENTS_SERDE_AND_RETURN(V) if(islist(V)) { FINALIZE_REAGENTS_SERDE_BODY(V); return; } + +#define BACKUP_TIMESTAMP "[time2text(REALTIMEOFDAY, "YY-MM-DD_hh-mm")].backup" diff --git a/code/__defines/subsystems.dm b/code/__defines/subsystems.dm index 87f4b099d0f5..49406d1adb45 100644 --- a/code/__defines/subsystems.dm +++ b/code/__defines/subsystems.dm @@ -19,7 +19,8 @@ // Subsystems shutdown in the reverse of the order they initialize in // The numbers just define the ordering, they are meaningless otherwise. -#define SS_INIT_INPUT 23 +#define SS_INIT_INPUT 24 +#define SS_INIT_VERY_EARLY 23 #define SS_INIT_EARLY 22 #define SS_INIT_WEBHOOKS 21 #define SS_INIT_MODPACKS 20 diff --git a/code/_helpers/guid.dm b/code/_helpers/guid.dm new file mode 100644 index 000000000000..3b6d76a3a08f --- /dev/null +++ b/code/_helpers/guid.dm @@ -0,0 +1,31 @@ +// Marks a guid as unused for get_guid() to return later. +var/global/alist/_unused_guids_by_distinguisher = alist() +/proc/free_guid(_distinguisher, _guid) + var/list/unused_guids = global._unused_guids_by_distinguisher[_distinguisher] + if(islist(unused_guids)) + unused_guids += _guid + else + global._unused_guids_by_distinguisher[_distinguisher] = list(_guid) + +// Returns an unused guid. +var/global/alist/_guids_by_distinguisher = alist() +/proc/get_guid(_distinguisher) + + // First time this has been used - set up our lists, return 1. + if(!global._guids_by_distinguisher[_distinguisher]) + global._guids_by_distinguisher[_distinguisher] = 1 + global._unused_guids_by_distinguisher[_distinguisher] = list() + return 1 + + // If we have unused guids, use one of those first. Otherwise just increment our GUID counter. + var/list/unused_guids = global._unused_guids_by_distinguisher[_distinguisher] + if(!length(unused_guids)) + global._guids_by_distinguisher[_distinguisher] = global._guids_by_distinguisher[_distinguisher] + 1 + return global._guids_by_distinguisher[_distinguisher] + + . = unused_guids[1] + unused_guids.Cut(1, 2) + +// Sets our last max guid, only really used after initial subsystem load. +/proc/set_guid(_distinguisher, _guid) + global._guids_by_distinguisher[_distinguisher] = _guid diff --git a/code/controllers/subsystems/approvals.dm b/code/controllers/subsystems/approvals.dm new file mode 100644 index 000000000000..72d2d65b05ac --- /dev/null +++ b/code/controllers/subsystems/approvals.dm @@ -0,0 +1,138 @@ +SUBSYSTEM_DEF(approvals) + name = "Approvals" + flags = SS_NO_FIRE + init_order = SS_INIT_EARLY + + var/const/approvals_path = "data/approvals.json" // This should definitely use a DB rather than json but heigh no + var/list/pending_approvals = list() + var/list/all_approvals = list() + var/alist/guid_to_approval = alist() + +/datum/controller/subsystem/approvals/stat_entry() + ..("P:[pending_approvals.len] A:[all_approvals.len]") + +/datum/controller/subsystem/approvals/Initialize(start_timeofday) + ..() + if(!fexists(approvals_path)) + return + + // Notes for future consideration + // - load approved + pending on first run, apply approvals to update whitelist or such + // - only keep pending approvals, discard everything else? or load everything so people can check their approvals? + + // Should also consider integrating this more tightly with instantiate_serialized_data() but given datum handling + // this should be fine. It's not like approvals will be associated with DM refs or turfs or such I would assume. + + try + var/max_guid = 0 + for(var/list/approval_data in json_decode(file2text(approvals_path))) + var/create_type = approval_data[/datum::type] + var/datum/approval/approval = new create_type(approval_data) + all_approvals += approval + guid_to_approval[approval.guid] = approval + max_guid = max(max_guid, approval.guid) + if(approval.status <= /datum/approval::APPROVAL_SUBMITTED) + pending_approvals += approval + set_guid(type, max_guid) + for(var/i = 1 to max_guid) + if(!guid_to_approval[i]) + free_guid(type, i) + + catch(var/exception/E) + error("Exception when loading approvals file: [EXCEPTION_TEXT(E)]") + +/datum/controller/subsystem/approvals/proc/store_approval(mob/_submitter, datum/approval/_approval) + _approval.submitter = _submitter.client?.ckey || "system" + _approval.guid = get_guid(type) + all_approvals += _approval + guid_to_approval[_approval.guid] = _approval.guid + if(_approval.status <= /datum/approval::APPROVAL_SUBMITTED) + pending_approvals |= _approval + _approval.on_creation(_submitter) + +/datum/controller/subsystem/approvals/proc/save_approvals() + + // Take a timestamped backup just in case. + if(fexists(approvals_path)) + var/backup_path = "[approvals_path].[BACKUP_TIMESTAMP]" + if(!fcopy(approvals_path, backup_path)) + log_error("Failed to back up approvals file [approvals_path]") + return + + var/list/all_approval_data = list() + for(var/datum/approval/approval as anything in all_approvals) + all_approval_data += list(approval.Serialize()) + + try + var/write_data = json_encode(all_approval_data) + var/write_file = file(approvals_path) + to_file(write_file, write_data) + + catch(var/exception/E) + log_error("Exception when saving approvals file: [EXCEPTION_TEXT(E)]") + return + +/datum/approval + var/guid + var/name + var/submitter + var/approver + var/body + var/status = APPROVAL_CREATED + + var/const/APPROVAL_CREATED = 0 + var/const/APPROVAL_SUBMITTED = 1 + var/const/APPROVAL_APPROVED = 2 + var/const/APPROVAL_DENIED = 2 + +/datum/approval/proc/on_creation(mob/_submitter) + return + +/datum/approval/New(list/_data) + if(islist(_data)) + guid = _data[nameof(/datum/approval::guid)] + name = _data[nameof(/datum/approval::name)] + body = _data[nameof(/datum/approval::body)] + submitter = _data[nameof(/datum/approval::submitter)] + approver = _data[nameof(/datum/approval::approver)] + status = _data[nameof(/datum/approval::status)] + . = ..() + +/datum/approval/Serialize() + . = ..() + .[nameof(/datum/approval::guid)] = guid + .[nameof(/datum/approval::name)] = name + .[nameof(/datum/approval::body)] = body + .[nameof(/datum/approval::submitter)] = submitter + .[nameof(/datum/approval::approver)] = approver + .[nameof(/datum/approval::status)] = status + +/datum/approval/proc/on_approver_response(_approver, _approved = TRUE, _reason = "Unsupplied.") + approver = _approver + status = _approved ? APPROVAL_APPROVED : APPROVAL_DENIED + if(submitter) + for(var/client/client) + if(client.ckey == submitter) + to_chat(client, "Your submission #[guid] has been [status == APPROVAL_APPROVED ? "approved" : "denied"] by [approver] for reason: [_reason].") + +// Do we need to serialize icons for these separately to uploaded icons, or should we +// immediately record it in the uploaded icon repo even if denied and delete it later? +// Approvals need to persist across restarts... +/datum/approval/player_icon + var/icon_guid + var/icon/icon + +/datum/approval/player_icon/on_creation(mob/_submitter) + . = ..() + icon_guid = SSuploaded_icons.store_icon(_submitter?.ckey || "system", body, 0, icon) + +/datum/approval/player_icon/Serialize() + . = ..() + .[nameof(/datum/approval/player_icon::icon_guid)] = icon_guid + +/datum/approval/player_icon/on_approver_response(_approver, _approved = TRUE, _reason = "Unsupplied.") + . = ..() + if(status == APPROVAL_DENIED && icon_guid) + SSuploaded_icons.remove_icon(icon_guid) + icon = null + icon_guid = null diff --git a/code/controllers/subsystems/uploaded_icons.dm b/code/controllers/subsystems/uploaded_icons.dm new file mode 100644 index 000000000000..579e712a6d40 --- /dev/null +++ b/code/controllers/subsystems/uploaded_icons.dm @@ -0,0 +1,116 @@ +SUBSYSTEM_DEF(uploaded_icons) + name = "Uploaded Icons" + wait = 1 MINUTE + init_order = SS_INIT_VERY_EARLY + + var/need_save = FALSE + var/alist/guid_to_icon = alist() + var/const/icons_path = "data/icons/" + var/const/icons_manifest = "icons.json" + +/datum/controller/subsystem/uploaded_icons/stat_entry() + ..("I:[guid_to_icon.len]") + +/datum/controller/subsystem/uploaded_icons/fire(resumed) + save_icons() + +/datum/controller/subsystem/uploaded_icons/Initialize(start_timeofday) + ..() + try + var/max_guid = 0 + var/manifest_path = "[icons_path][icons_manifest]" + if(fexists(manifest_path)) + for(var/list/manifest_entry in json_decode(file2text(manifest_path))) + var/create_type = manifest_entry[/datum::type] + var/datum/uploaded_icon/uploaded_icon = new create_type(manifest_entry) + guid_to_icon[uploaded_icon.guid] = uploaded_icon + max_guid = max(max_guid, uploaded_icon.guid) + uploaded_icon.icon = icon(file("[icons_path][uploaded_icon.guid].dmi")) + set_guid(type, max_guid) + for(var/i = 1 to max_guid) + if(!guid_to_icon[i]) + free_guid(type, i) + report_progress("Loaded [length(guid_to_icon)] uploaded icon\s.") + + catch(var/exception/E) + error("Exception when loading player icons manifest: [EXCEPTION_TEXT(E)]") + +/datum/controller/subsystem/uploaded_icons/proc/store_icon(_uploader, _description, _icon_flags, icon/_icon) + var/datum/uploaded_icon/new_icon = new(get_guid(type)) + new_icon.uploader = _uploader + new_icon.description = _description + new_icon.icon_flags = _icon_flags + new_icon.icon = _icon + need_save = TRUE + +/datum/controller/subsystem/uploaded_icons/proc/remove_icon(_guid) + var/datum/uploaded_icon/uploaded_icon = guid_to_icon[_guid] + if(!uploaded_icon) + return FALSE + guid_to_icon -= _guid + qdel(uploaded_icon) + free_guid(type, _guid) + need_save = TRUE + +/datum/controller/subsystem/uploaded_icons/Shutdown() + save_icons(force = TRUE) + . = ..() + +/datum/controller/subsystem/uploaded_icons/proc/save_icons(force = FALSE) + + if(!need_save && !force) + return + + try + // Write out our .DMI files and populate our manifest. + var/list/manifest_entries = list() + for(var/icon_guid,icon_data in guid_to_icon) + var/datum/uploaded_icon/uploaded_icon = icon_data + manifest_entries += list(uploaded_icon.Serialize()) + // Should we back up the .DMI folder first? Seems much more + // likely to cause memory issues than backing up .json files. + fcopy(uploaded_icon.icon, "[icons_path][uploaded_icon.guid].dmi") + // Take a timestamped backup. + var/manifest_path = "[icons_path][icons_manifest]" + if(fexists(manifest_path)) + var/backup_contents = file2text(manifest_path) + var/backup_file = file("[manifest_path].[BACKUP_TIMESTAMP]") + to_file(backup_file, backup_contents) + // Write out the manifest. + to_file(manifest_path, json_encode(manifest_entries)) + + catch(var/exception/E) + error("Exception when saving uploaded icons: [EXCEPTION_TEXT(E)]") + + need_save = FALSE + +/datum/uploaded_icon + var/guid + var/uploader + var/description + var/icon_flags = 0 + // License? Artist? + var/icon/icon + + var/const/ICON_FLAG_PUBLIC = BITFLAG(0) + +/datum/uploaded_icon/New(list/_data) + if(islist(_data)) + guid = _data[/datum/uploaded_icon::guid] + uploader = _data[/datum/uploaded_icon::uploader] + icon_flags = _data[/datum/uploaded_icon::icon_flags] + description = _data[/datum/uploaded_icon::description] + else if(isnum(_data)) + guid = _data + if(isnum(guid)) + // Check for collisions before doing this. + SSuploaded_icons.guid_to_icon[guid] = src + // else report an error? + +/datum/uploaded_icon/Serialize() + . = ..() + // The actual icon is saved to disk and referenced by guid, not handled here. + .[/datum/uploaded_icon::guid] = guid + .[/datum/uploaded_icon::uploader] = uploader + .[/datum/uploaded_icon::icon_flags] = icon_flags + .[/datum/uploaded_icon::description] = description diff --git a/code/modules/multiz/level_persistence_handler_json.dm b/code/modules/multiz/level_persistence_handler_json.dm index bebf101a2b78..99329b215496 100644 --- a/code/modules/multiz/level_persistence_handler_json.dm +++ b/code/modules/multiz/level_persistence_handler_json.dm @@ -15,7 +15,7 @@ // Do a backup (at the end to avoid overwriting then throwing an exception) if(fexists(filepath)) var/backup_contents = file2text(filepath) - var/backup_file = file("[filepath].[time2text(REALTIMEOFDAY, "YY-MM-DD_hh-mm")].backup") + var/backup_file = file("[filepath].[BACKUP_TIMESTAMP]") to_file(backup_file, backup_contents) // Clear old file to avoid appending data. // TODO: remove old backups? Leave as an exercise for the admin? diff --git a/code/modules/persistence/persistence_datum.dm b/code/modules/persistence/persistence_datum.dm index db43dff3f8a2..df01fd60af96 100644 --- a/code/modules/persistence/persistence_datum.dm +++ b/code/modules/persistence/persistence_datum.dm @@ -58,7 +58,7 @@ if(length(entries) && !istext(entries[1])) try // Save a backup of the old file just in case we cook it. - fcopy(filename, "[filename]-legacy.[time2text(REALTIMEOFDAY, "YY-MM-DD_hh-mm")].backup") + fcopy(filename, "[filename]-legacy.[BACKUP_TIMESTAMP]") catch(var/exception/e) log_error("Exception during saving backup of legacy file [filename]: [EXCEPTION_TEXT(e)]") diff --git a/nebula.dme b/nebula.dme index e664d65973b8..ba2d392ebd1c 100644 --- a/nebula.dme +++ b/nebula.dme @@ -141,6 +141,7 @@ #include "code\_helpers\game.dm" #include "code\_helpers\gauss.dm" #include "code\_helpers\global_lists.dm" +#include "code\_helpers\guid.dm" #include "code\_helpers\icons.dm" #include "code\_helpers\lists.dm" #include "code\_helpers\logging.dm" @@ -272,6 +273,7 @@ #include "code\controllers\subsystems\alarm.dm" #include "code\controllers\subsystems\ambience.dm" #include "code\controllers\subsystems\ao.dm" +#include "code\controllers\subsystems\approvals.dm" #include "code\controllers\subsystems\atoms.dm" #include "code\controllers\subsystems\configuration.dm" #include "code\controllers\subsystems\daycycle.dm" @@ -310,6 +312,7 @@ #include "code\controllers\subsystems\timer.dm" #include "code\controllers\subsystems\trade.dm" #include "code\controllers\subsystems\typing.dm" +#include "code\controllers\subsystems\uploaded_icons.dm" #include "code\controllers\subsystems\vis_contents.dm" #include "code\controllers\subsystems\vote.dm" #include "code\controllers\subsystems\weather.dm"