Adds admin panel for achievement metadata cleanup (#92345)

This commit is contained in:
AnturK
2025-08-26 07:11:47 +02:00
committed by GitHub
parent 3ef5a3d75f
commit 53a034c5b0
5 changed files with 182 additions and 0 deletions

View File

@@ -161,3 +161,6 @@
#define CHEF_TOURISTS_SERVED "Tourists Served As Chef"
#define BARTENDER_TOURISTS_SERVED "Tourists Served As Bartender"
/// Value in metadata version that signifies the achievement is archived
#define ACHIEVEMENT_ARCHIVED_VERSION 9999

View File

@@ -101,3 +101,36 @@ SUBSYSTEM_DEF(achievements)
if(to_update.len)
SSdbcore.MassInsert(format_table_name("achievement_metadata"),to_update,duplicate_key = TRUE)
var/list/orphaned_keys = get_orphaned_keys(FALSE)
if(orphaned_keys.len)
message_admins("Achievement metadata found without matching achievement, use Achievement-Admin-Panel verb to cleanup if necessary")
/// returns list of metadata keys and versions in db with no matching achievement datum, either deleted achievements, or from server with code ahead of us.
/datum/controller/subsystem/achievements/proc/get_orphaned_keys(include_archived = TRUE)
. = list()
var/list/current_metadata = list()
// Fetch all keys from the db
var/datum/db_query/Q = SSdbcore.NewQuery("SELECT achievement_key,achievement_version FROM [format_table_name("achievement_metadata")]")
if(!Q.Execute(async = TRUE))
qdel(Q)
return
else
while(Q.NextRow())
current_metadata[Q.item[1]] = Q.item[2]
qdel(Q)
var/list/achievements_by_db_id = list()
for(var/datum/award/award as anything in subtypesof(/datum/award))
if(!initial(award.database_id)) // abstract type
continue
achievements_by_db_id[award.database_id] = TRUE
for(var/key in current_metadata)
if(achievements_by_db_id[key])
continue
if(!include_archived && current_metadata[key] == ACHIEVEMENT_ARCHIVED_VERSION)
continue
.[key] = current_metadata[key]

View File

@@ -0,0 +1,73 @@
// Panel for achievement management
/datum/achievement_admin_panel
var/list/orphaned_keys
/datum/achievement_admin_panel/proc/reload_data()
if(!SSachievements.achievements_enabled)
return
orphaned_keys = SSachievements.get_orphaned_keys()
/datum/achievement_admin_panel/ui_data()
. = list()
var/list/orphaned_only = list()
var/list/archived_only = list()
for(var/key in orphaned_keys)
if(orphaned_keys[key] == ACHIEVEMENT_ARCHIVED_VERSION)
archived_only += key
else
orphaned_only += key
.["orphaned_keys"] = orphaned_only
.["archived_keys"] = archived_only
/datum/achievement_admin_panel/ui_state(mob/user)
return ADMIN_STATE(R_ADMIN)
/datum/achievement_admin_panel/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "AchievementsAdminPanel")
ui.open()
/datum/achievement_admin_panel/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
switch (action)
if("archive")
var/achievement_key = params["key"]
archive_achievement(achievement_key)
reload_data()
return TRUE
if("cleanup_orphan")
var/achievement_key = params["key"]
cleanup_outdated_achievement(achievement_key)
reload_data()
return TRUE
/datum/achievement_admin_panel/proc/cleanup_outdated_achievement(achievement_key)
// ensure key is actually orphaned just in case
if(!(achievement_key in orphaned_keys))
return
log_admin("[key_name_admin(usr)] has deleted orphaned achievement metadata for key [achievement_key].")
message_admins("[key_name_admin(usr)] has deleted orphaned achievement metadata for key [achievement_key].")
SSdbcore.QuerySelect(list(
SSdbcore.NewQuery("DELETE FROM [format_table_name("achievement_metadata")] WHERE achievement_key = :key", list("key" = achievement_key)),
SSdbcore.NewQuery("DELETE FROM [format_table_name("achievements")] WHERE achievement_key = :key", list("key" = achievement_key)),
), warn = TRUE, qdel = TRUE)
/datum/achievement_admin_panel/proc/archive_achievement(achievement_key)
// ensure key is actually orphaned just in case
if(!(achievement_key in orphaned_keys))
return
log_admin("[key_name_admin(usr)] has archived orphaned achievement metadata for key [achievement_key].")
message_admins("[key_name_admin(usr)] has archived orphaned achievement metadata for key [achievement_key].")
SSdbcore.QuerySelect(list(
SSdbcore.NewQuery("UPDATE [format_table_name("achievement_metadata")] SET achievement_version = :version WHERE achievement_key = :key", list("key" = achievement_key, "version" = ACHIEVEMENT_ARCHIVED_VERSION)),
), warn = TRUE, qdel = TRUE)
ADMIN_VERB(achievements_cleanup, R_ADMIN, "Achievements Admin Panel", "View achievements management panel.", ADMIN_CATEGORY_MAIN)
var/datum/achievement_admin_panel/panel = new /datum/achievement_admin_panel()
panel.reload_data()
panel.ui_interact(user.mob)

View File

@@ -866,6 +866,7 @@
#include "code\datums\world_topic.dm"
#include "code\datums\achievements\_achievement_data.dm"
#include "code\datums\achievements\_awards.dm"
#include "code\datums\achievements\admin_panel.dm"
#include "code\datums\achievements\boss_achievements.dm"
#include "code\datums\achievements\boss_scores.dm"
#include "code\datums\achievements\job_achievements.dm"

View File

@@ -0,0 +1,72 @@
import { Button, LabeledList, NoticeBox, Section } from 'tgui-core/components';
import { useBackend } from '../backend';
import { Window } from '../layouts';
type Data = {
orphaned_keys: string[];
archived_keys: string[];
};
export const AchievementsAdminPanel = (props) => {
const { act, data } = useBackend<Data>();
const { orphaned_keys } = data;
return (
<Window title="Achievements Admin Panel" width={540} height={680}>
<Window.Content scrollable>
<Section title="Orphaned achievements">
<NoticeBox>
These achievements are present in the database but are missing
definitions in code. Most likely these were removed and can be
cleaned up safely. If you're sharing the same database on multiple
servers it's possible these come from a server with later version of
the code than this one.
</NoticeBox>
<LabeledList>
{orphaned_keys.map((key) => (
<LabeledList.Item
key={key}
label=""
buttons={
<>
<Button.Confirm
onClick={() => act('archive', { key: key })}
>
Archive
</Button.Confirm>
<Button.Confirm
onClick={() => act('cleanup_orphan', { key: key })}
>
Cleanup
</Button.Confirm>
</>
}
>
{key}
</LabeledList.Item>
))}
</LabeledList>
</Section>
<Section title="Archived achievements">
<NoticeBox>Archived achievements in the database.</NoticeBox>
<LabeledList>
{orphaned_keys.map((key) => (
<LabeledList.Item
key={key}
label=""
buttons={
<Button.Confirm
onClick={() => act('cleanup_orphan', { key: key })}
>
Cleanup
</Button.Confirm>
}
>
{key}
</LabeledList.Item>
))}
</LabeledList>
</Section>
</Window.Content>
</Window>
);
};