Persistence Panel (#22979)

# Summary

This PR adds a new admin panel for persistence.

## Contents

- The panel features information about the current round and detailed
tracking of objects, records and generics.
- The previously introduced toggle-persistence verb has been removed and
integrated into the new panel.
- The panel can be seen by admins, mods and devs, but the
toggle-persistence is still locked to admin permissions only.

## Preview

The images are WIP and subject to change.

<img width="918" height="229" alt="image"
src="https://github.com/user-attachments/assets/16d3d73d-32e6-4907-bdff-d96699c27d49"
/>

<img width="919" height="253" alt="image"
src="https://github.com/user-attachments/assets/92e7f4d9-ccd4-4e79-949d-b8a35039da01"
/>

<img width="923" height="285" alt="image"
src="https://github.com/user-attachments/assets/e3796504-4cc6-4fce-b5f1-4df3486bfbee"
/>

<img width="923" height="270" alt="image"
src="https://github.com/user-attachments/assets/9fbb185d-181a-40ec-b89b-58eafcd47e4d"
/>
This commit is contained in:
FabianK3
2026-08-08 20:47:55 +00:00
committed by GitHub
parent f6d688f9b6
commit 65a9f06765
17 changed files with 986 additions and 55 deletions
+4 -2
View File
@@ -88,6 +88,7 @@ GLOBAL_LIST_INIT(admin_verbs_admin, list(
/client/proc/allow_stationbound_reset,
/client/proc/end_round,
/client/proc/event_manager_panel,
/client/proc/persistence_panel,
/client/proc/empty_ai_core_toggle_latejoin,
/client/proc/aooc,
/client/proc/change_human_appearance_admin, // Allows an admin to change the basic appearance of human-based mobs ,
@@ -149,8 +150,7 @@ GLOBAL_LIST_INIT(admin_verbs_fun, list(
/client/proc/fab_tip,
/client/proc/apply_sunstate,
/datum/admins/proc/ccannoucment,
/datum/admins/proc/set_odyssey,
/datum/admins/proc/toggle_persistence
/datum/admins/proc/set_odyssey
))
GLOBAL_LIST_INIT(admin_verbs_spawn, list(
@@ -456,6 +456,7 @@ GLOBAL_LIST_INIT(admin_verbs_mod, list(
/client/proc/cmd_admin_check_contents,
/client/proc/print_logout_report,
/client/proc/check_ai_laws, /*shows AI and borg laws*/
/client/proc/persistence_panel,
/client/proc/aooc,
/client/proc/toggle_aooc,
/client/proc/alooc,
@@ -491,6 +492,7 @@ GLOBAL_LIST_INIT(admin_verbs_dev, list( //will need to be altered - Ryan784
/client/proc/debug_controller,
/client/proc/debug_variables,
/client/proc/dsay,
/client/proc/persistence_panel,
/client/proc/hide_most_verbs,
/client/proc/kill_air,
/client/proc/kill_airgroup,
+13
View File
@@ -1157,3 +1157,16 @@ Traitors and the like can also be revived with the previous role mostly intact.
message_admins("[key_name_admin(usr)] used the ADPI Panel")
log_admin("[key_name(usr)] used the ADPI Panel")
feedback_add_details("admin_verb", "ADPI")
/client/proc/persistence_panel()
set category = "Admin"
set name = "Persistence Panel"
set desc = "Open the persistence subsystem admin panel"
if(!check_rights(R_ADMIN|R_MOD|R_DEV, TRUE))
return
var/datum/tgui_module/persistence_panel/panel = new
panel.ui_interact(usr)
feedback_add_details("admin_verb", "PERSIST")
@@ -0,0 +1,236 @@
/datum/tgui_module/persistence_panel/ui_interact(mob/user, var/datum/tgui/ui)
if(!can_use_persistence_panel(user))
return
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "PersistencePanel", "Persistence Panel", 900, 700)
ui.open()
/datum/tgui_module/persistence_panel/ui_data(mob/user)
var/list/data = list()
data["status_initialized"] = SSpersistence.init_success
data["saving_active"] = !SSpersistence.prevent_saving
data["objects_tracked"] = length(SSpersistence.object_track_register)
data["records_cached"] = SSpersistence.history_cache_count
data["generics_cached"] = length(SSpersistence.generic_cache)
data["is_admin"] = check_rights(R_ADMIN, 0, user)
return data
/datum/tgui_module/persistence_panel/ui_static_data(mob/user)
var/list/data = list()
data["objects"] = get_tracked_objects_data()
data["records"] = get_history_records_data()
data["generics"] = get_generic_records_data()
return data
/datum/tgui_module/persistence_panel/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
// Instead of return TRUE for a regular update, return FALSE and call ui.send_update() before.
// Returning TRUE will cause static_ui_data to be sent, which in this panel is costly.
if(!can_use_persistence_panel(ui.user)) // Code behind permissions check
return FALSE
if(action == "toggle_saving") // Toggle persistence saving at round end
if(!check_rights(R_ADMIN))
return FALSE
var/message = ""
var/options = list()
if(SSpersistence.prevent_saving)
message = "The persistence subsystem will NOT save at the end of the round. Do you want to re-enable it?"
options = list("Re-enable saving", "Cancel")
else
message = "The persistence subsystem will save at the end of the round. Do you want to prevent this? This can be un-done before the round ends."
options = list("Prevent saving", "Cancel")
var/confirm = tgui_alert(usr, message, "Toggle Persistence Saving", options)
if(confirm == "Prevent saving")
SSpersistence.prevent_saving = TRUE
to_world(FONT_LARGE(EXAMINE_BLOCK_RED("Persistence saving at the end of the round has been [SPAN_BOLD(SPAN_WARNING("disabled"))] by an administrator.")))
log_and_message_admins("has toggled persistence saving at round end, it is now disabled", usr)
else if (confirm == "Re-enable saving")
SSpersistence.prevent_saving = FALSE
to_world(FONT_LARGE(EXAMINE_BLOCK_RED("Persistence saving at the end of the round has been [SPAN_BOLD(SPAN_GOOD("re-enabled"))] by an administrator.")))
log_and_message_admins("has toggled persistence saving at round end, it is now re-enabled", usr)
ui.send_update()
return FALSE
if(action == "refresh") // Full data pull
return TRUE
if (action == "jump") // Object jump
var/atom/target = locate(params["ref"])
if(!target || !istype(target))
to_chat(ui.user, SPAN_WARNING("Object can no longer be found."))
return FALSE
var/turf/T = get_turf(target)
if(!T)
to_chat(ui.user, SPAN_WARNING("Object can no longer be found."))
return FALSE
var/client/C = ui.user?.client
if(!C)
return FALSE
if(!isghost(usr))
C.admin_ghost()
C.jumptocoord(T.x, T.y, T.z)
return FALSE
if(action == "edit") // Object VV
var/client/C = ui.user?.client
if(!C)
return FALSE
var/atom/target = locate(params["ref"])
if(!target || !istype(target))
to_chat(ui.user, SPAN_WARNING("object can no longer be found."))
return FALSE
C.debug_variables(target)
return FALSE
return FALSE
/datum/tgui_module/persistence_panel/proc/get_tracked_objects_data()
var/list/objects = list()
for(var/obj/track in SSpersistence.object_track_register)
var/turf/T = get_turf(track)
objects += list(list(
"ref" = REF(track), // Needed for VV action
"type" = "[track.type]",
"name" = track.name,
"x" = T?.x || 0,
"y" = T?.y || 0,
"z" = T?.z || 0,
"created_at" = track.persistent_objects_created_at,
"expires_at" = track.persistent_objects_expires_at
))
return objects
/datum/tgui_module/persistence_panel/proc/get_history_records_data()
var/list/history_types = typesof(/singleton/persistent_type/history) - list(/singleton/persistent_type/history, /singleton/persistent_type/history/character)
var/list/records = list()
for(var/type_path in history_types)
var/singleton/persistent_type/history/type_instance = GET_SINGLETON(type_path)
if(!type_instance)
continue
records += list(build_history_type_data(type_instance))
return records
/datum/tgui_module/persistence_panel/proc/build_history_type_data(var/singleton/persistent_type/history/type_instance)
var/list/type_attribute_groups = SSpersistence.historyGetAllRecordsForAllAttributes(type_instance, TRUE)
if(!type_attribute_groups)
type_attribute_groups = list()
var/list/attribute_groups_history = list()
for(var/attribute_group in type_attribute_groups)
attribute_groups_history += list(build_history_attribute_group_data(type_instance, attribute_group))
return list(
"type" = get_persistent_type_display_name(type_instance),
"title" = type_instance.title,
"description" = type_instance.description,
"requires_attribute" = type_instance.requires_attribute,
"attributes" = attribute_groups_history
)
/datum/tgui_module/persistence_panel/proc/build_history_attribute_group_data(var/singleton/persistent_type/history/type_instance, var/list/attribute_group)
var/attribute = attribute_group["attribute"]
var/list/attribute_records = attribute_group["records"] || list()
var/list/record_items = list()
for(var/datum/persistent_record/record in attribute_records)
record_items += list(list(
"created_at" = record.created_at,
"game_id" = record.game_id,
"value" = "[record.value]"
))
var/attribute_value = attribute
if(istype(type_instance, /singleton/persistent_type/history/character))
// Character history is a special case, we want to show the character name instead of the ID
attribute_value = SSpersistence.historyGetCharnameByID(attribute)
return list(
"attribute" = attribute_value,
"records" = record_items
)
/datum/tgui_module/persistence_panel/proc/get_generic_records_data()
var/list/generics_by_type = list()
var/custom_types = typesof(/singleton/persistent_type/generic) - /singleton/persistent_type/generic
for(var/type_path in custom_types)
var/singleton/persistent_type/generic/type_instance = GET_SINGLETON(type_path)
if(!type_instance)
continue
var/type_key = "[type_instance.type]"
if(!generics_by_type[type_key])
generics_by_type[type_key] = build_generic_type_data(type_instance)
append_generic_attribute_data(type_instance, generics_by_type[type_key]["attributes"])
var/list/generics = list()
for(var/type_key in generics_by_type)
generics += list(generics_by_type[type_key])
return generics
/datum/tgui_module/persistence_panel/proc/build_generic_type_data(var/singleton/persistent_type/generic/type_instance)
var/list/type_parts = return_typenames(type_instance.type)
return list(
"type" = type_parts[length(type_parts)],
"title" = type_instance.title,
"description" = type_instance.description,
"requires_attribute" = type_instance.requires_attribute,
"attributes" = list()
)
/datum/tgui_module/persistence_panel/proc/append_generic_attribute_data(var/singleton/persistent_type/generic/type_instance, var/list/attribute_groups_generic)
if(type_instance.requires_attribute)
var/list/attributes = SSpersistence.genericGetAllAttributesForType(type_instance)
for(var/attribute in attributes)
var/datum/persistent_generic/generic_entry = SSpersistence.genericLoad(type_instance, attribute, TRUE)
if(!generic_entry)
continue
attribute_groups_generic += list(list(
"attribute" = attribute,
"created_at" = generic_entry.created_at,
"expires_at" = generic_entry.expires_at,
"json" = json_encode(generic_entry.content)
))
return
var/datum/persistent_generic/generic_entry = SSpersistence.genericLoad(type_instance, null, TRUE)
if(generic_entry)
attribute_groups_generic += list(list(
"attribute" = null,
"created_at" = generic_entry.created_at,
"expires_at" = generic_entry.expires_at,
"json" = json_encode(generic_entry.content)
))
/datum/tgui_module/persistence_panel/proc/get_persistent_type_display_name(var/singleton/persistent_type/type_instance)
var/list/type_parts = return_typenames(type_instance.type)
var/type_name = type_parts[length(type_parts)]
if(istype(type_instance, /singleton/persistent_type/history/character))
return "Character - [type_name]"
return type_name
/datum/tgui_module/persistence_panel/proc/can_use_persistence_panel(var/mob/user)
return check_rights(R_ADMIN|R_MOD|R_DEV, 0, user)