From 74f6f2f08dd86a8b1c1f6b5e555477e14aa5dc0a Mon Sep 17 00:00:00 2001 From: CHOMPStation2StaffMirrorBot <94713762+CHOMPStation2StaffMirrorBot@users.noreply.github.com> Date: Wed, 8 Apr 2026 09:27:17 -0700 Subject: [PATCH] [MIRROR] Check Access List Mapper Verb (#12647) Co-authored-by: Will <7099514+Willburd@users.noreply.github.com> Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com> --- code/modules/admin/access_viewer.dm | 81 +++++++++++++ code/modules/admin/holder2.dm | 2 + code/modules/admin/verbs/admingame.dm | 9 ++ code/modules/unit_tests/mapping.dm | 31 +++++ tgui/packages/tgui-panel/settings/themes.ts | 66 +++++------ .../AccessViewer/AccessTypeDisplay.tsx | 29 +++++ .../tgui/interfaces/AccessViewer/constants.ts | 26 +++++ .../tgui/interfaces/AccessViewer/index.tsx | 106 ++++++++++++++++++ .../tgui/interfaces/AccessViewer/types.ts | 18 +++ vorestation.dme | 1 + 10 files changed, 336 insertions(+), 33 deletions(-) create mode 100644 code/modules/admin/access_viewer.dm create mode 100644 tgui/packages/tgui/interfaces/AccessViewer/AccessTypeDisplay.tsx create mode 100644 tgui/packages/tgui/interfaces/AccessViewer/constants.ts create mode 100644 tgui/packages/tgui/interfaces/AccessViewer/index.tsx create mode 100644 tgui/packages/tgui/interfaces/AccessViewer/types.ts diff --git a/code/modules/admin/access_viewer.dm b/code/modules/admin/access_viewer.dm new file mode 100644 index 0000000000..c14e6a5bf1 --- /dev/null +++ b/code/modules/admin/access_viewer.dm @@ -0,0 +1,81 @@ + +/* + Tgui panel for admins editing the access list of various machines. +*/ +/datum/access_viewer + var/datum/weakref/focused_obj + +/datum/access_viewer/Destroy(force) + focused_obj = null + . = ..() + +/datum/access_viewer/tgui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "AccessViewer", "Access Viewer") + ui.open() + +/datum/access_viewer/tgui_state(mob/user) + return ADMIN_STATE(R_DEBUG) + +/datum/access_viewer/tgui_act(action, params, datum/tgui/ui) + if(..() || !check_rights_for(ui.user.client, R_DEBUG)) + return FALSE + + var/obj/machinery/req_thing = focused_obj?.resolve() + if(!req_thing) + return FALSE + + switch(action) + if("req_all") + var/set_id = text2num(params["set_id"]) + if(!set_id) + return FALSE + if(!req_thing.req_access) + req_thing.req_access = list() + if(set_id in req_thing.req_access) + req_thing.req_access -= set_id + else + req_thing.req_access += set_id + return TRUE + + if("req_one") + var/set_id = text2num(params["set_id"]) + if(!set_id) + return FALSE + if(!req_thing.req_one_access) + req_thing.req_one_access = list() + if(set_id in req_thing.req_one_access) + req_thing.req_one_access -= set_id + else + req_thing.req_one_access += set_id + return TRUE + +/datum/access_viewer/tgui_static_data(mob/user) + var/list/data = list() + var/list/access_list = list() + for(var/datum/access/dat as anything in subtypesof(/datum/access)) + access_list += list( + list( + "id" = dat.id, + "name" = dat.desc, + "region" = dat.region, + "access_type" = dat.access_type, + ) + ) + data["access_list"] = access_list + return data + +/datum/access_viewer/tgui_data(mob/user) + var/list/data = list() + // Check if the object still exists + var/obj/machinery/req_thing = focused_obj?.resolve() + if(req_thing) + data["name"] = req_thing.name + data["coords"] = "[req_thing.x].[req_thing.y].[req_thing.z]" + data["req_access"] = req_thing.req_access ? req_thing.req_access : list() + data["req_one_access"] = req_thing.req_one_access ? req_thing.req_one_access : list() + return data + +/datum/access_viewer/proc/set_access_focus(obj/machinery/req_thing) + focused_obj = WEAKREF(req_thing) diff --git a/code/modules/admin/holder2.dm b/code/modules/admin/holder2.dm index 882a02275a..519bfa96f9 100644 --- a/code/modules/admin/holder2.dm +++ b/code/modules/admin/holder2.dm @@ -35,6 +35,8 @@ GLOBAL_PROTECT(href_token) var/datum/spawn_menu/spawn_menu var/datum/spawnpanel/spawn_panel + var/datum/access_viewer/access_view_menu + /// A lazylist of tagged datums, for quick reference with the View Tags verb var/list/tagged_datums diff --git a/code/modules/admin/verbs/admingame.dm b/code/modules/admin/verbs/admingame.dm index 2698d451a7..de805b7cba 100644 --- a/code/modules/admin/verbs/admingame.dm +++ b/code/modules/admin/verbs/admingame.dm @@ -5,3 +5,12 @@ ADMIN_VERB(spawn_panel, R_SPAWN, "Spawn Panel", "Spawn Panel (TGUI).", ADMIN_CAT user.holder.spawn_panel = panel panel.tgui_interact(user.mob) feedback_add_details("admin_verb","SP") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! + +ADMIN_VERB_AND_CONTEXT_MENU(machine_access_check, R_DEBUG, "Check Access List", "Read and edit the access list of a machine.", ADMIN_CATEGORY_GAME, obj/machinery/req_thing in world) + var/datum/access_viewer/panel = user.holder.access_view_menu + if(!panel) + panel = new() + user.holder.access_view_menu = panel + panel.set_access_focus(req_thing) + panel.tgui_interact(user.mob) + feedback_add_details("admin_verb","SR") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! diff --git a/code/modules/unit_tests/mapping.dm b/code/modules/unit_tests/mapping.dm index 7c42f958a3..baafc25c91 100644 --- a/code/modules/unit_tests/mapping.dm +++ b/code/modules/unit_tests/mapping.dm @@ -17,3 +17,34 @@ // continue TEST_FAIL(log_entry) + +/* Should probably be done as a linter thing instead +/// Checks all machines for legal access numbers +/datum/unit_test/all_access_id_must_have_existing_datums + +/datum/unit_test/all_access_id_must_have_existing_datums/Run() + var/failed = FALSE + var/list/access_datums = SSaccess.get_all_access_datums_by_id() + + for(var/obj/machinery/thing in world) + failed += validate_list(thing.req_access, thing, "req_access") + failed += validate_list(thing.req_one_access, thing, "req_one_access") + if(failed) + TEST_FAIL("Machinery had an illegal access id.") + +/datum/unit_test/proc/validate_list(var/list/access_list, var/obj/machinery/thing, name_list) + if(!access_list) + return FALSE // null is legal + + if(!islist(access_list)) + TEST_NOTICE(src, "Access - [thing] ([thing.x].[thing.y].[thing.z]) had a [name_list] that was not a list or null.") + return TRUE // Was something other than null or a list... illegal + + var/failed = FALSE + for(var/access in access_list) + if(!SSaccess.get_access_by_id(access)) + TEST_NOTICE(src, "Access - [thing] ([thing.x].[thing.y].[thing.z]) had a [name_list] with a non-existant id [access].") + failed = TRUE // has a non-existant id, illegal + + return failed +*/ diff --git a/tgui/packages/tgui-panel/settings/themes.ts b/tgui/packages/tgui-panel/settings/themes.ts index e42c527078..14f274b5b0 100644 --- a/tgui/packages/tgui-panel/settings/themes.ts +++ b/tgui/packages/tgui-panel/settings/themes.ts @@ -37,8 +37,8 @@ export function setClientTheme(name) { 'rpanewindow.background-color': 'none', 'rpanewindow.text-color': '#000000', 'mainvsplit.background-color': 'none', - 'info.tab-background-color': 'none', - 'info.tab-text-color': '#000000', + //'info.tab-background-color': 'none', + //'info.tab-text-color': '#000000', 'discord.background-color': 'none', 'discord.text-color': '#000000', 'mapb.background-color': 'none', @@ -57,47 +57,47 @@ export function setClientTheme(name) { return Byond.winset({ // Main windows - 'infowindow.background-color': 'none', - 'infowindow.text-color': '#000000', - 'info.background-color': 'none', - 'info.text-color': '#000000', + //'infowindow.background-color': 'none', + //'infowindow.text-color': '#000000', + //'info.background-color': 'none', + //'info.text-color': '#000000', 'browseroutput.background-color': 'none', 'browseroutput.text-color': '#000000', 'outputwindow.background-color': 'none', 'outputwindow.text-color': '#000000', 'mainwindow.background-color': 'none', - 'split.background-color': 'none', + //'split.background-color': 'none', // Buttons 'changelog.background-color': 'none', 'changelog.text-color': '#000000', - 'rules.background-color': 'none', - 'rules.text-color': '#000000', - 'wiki.background-color': 'none', - 'wiki.text-color': '#000000', - 'forum.background-color': 'none', - 'forum.text-color': '#000000', + //'rules.background-color': 'none', + //'rules.text-color': '#000000', + //'wiki.background-color': 'none', + //'wiki.text-color': '#000000', + //'forum.background-color': 'none', + //'forum.text-color': '#000000', 'github.background-color': 'none', 'github.text-color': '#000000', - 'report-issue.background-color': 'none', - 'report-issue.text-color': '#000000', + //'report-issue.background-color': 'none', + //'report-issue.text-color': '#000000', // Status and verb tabs 'output.background-color': 'none', 'output.text-color': '#000000', 'statwindow.background-color': 'none', 'statwindow.text-color': '#000000', - 'stat.background-color': '#FFFFFF', - 'stat.tab-background-color': 'none', - 'stat.text-color': '#000000', - 'stat.tab-text-color': '#000000', - 'stat.prefix-color': '#000000', - 'stat.suffix-color': '#000000', + //'stat.background-color': '#FFFFFF', + //'stat.tab-background-color': 'none', + //'stat.text-color': '#000000', + //'stat.tab-text-color': '#000000', + //'stat.prefix-color': '#000000', + //'stat.suffix-color': '#000000', // Say, OOC, me Buttons etc. 'saybutton.background-color': 'none', 'saybutton.text-color': '#000000', - 'oocbutton.background-color': 'none', - 'oocbutton.text-color': '#000000', - 'mebutton.background-color': 'none', - 'mebutton.text-color': '#000000', + //'oocbutton.background-color': 'none', + //'oocbutton.text-color': '#000000', + //'mebutton.background-color': 'none', + //'mebutton.text-color': '#000000', 'asset_cache_browser.background-color': 'none', 'asset_cache_browser.text-color': '#000000', 'tooltip.background-color': 'none', @@ -114,8 +114,8 @@ export function setClientTheme(name) { 'rpanewindow.background-color': COLOR_DARK_BG_DARKER, 'rpanewindow.text-color': COLOR_DARK_TEXT, 'mainvsplit.background-color': COLOR_DARK_BG, - 'info.tab-background-color': COLOR_DARK_BG, - 'info.tab-text-color': COLOR_DARK_TEXT, + //'info.tab-background-color': COLOR_DARK_BG, + //'info.tab-text-color': COLOR_DARK_TEXT, 'mapb.background-color': '#494949', 'mapb.text-color': COLOR_DARK_TEXT, 'discord.background-color': '#494949', @@ -136,8 +136,8 @@ export function setClientTheme(name) { // Main windows 'infowindow.background-color': COLOR_DARK_BG, 'infowindow.text-color': COLOR_DARK_TEXT, - 'info.background-color': COLOR_DARK_BG, - 'info.text-color': COLOR_DARK_TEXT, + //'info.background-color': COLOR_DARK_BG, + //'info.text-color': COLOR_DARK_TEXT, 'browseroutput.background-color': COLOR_DARK_BG, 'browseroutput.text-color': COLOR_DARK_TEXT, 'outputwindow.background-color': COLOR_DARK_BG, @@ -147,16 +147,16 @@ export function setClientTheme(name) { // Buttons 'changelog.background-color': '#494949', 'changelog.text-color': COLOR_DARK_TEXT, - 'rules.background-color': '#494949', - 'rules.text-color': COLOR_DARK_TEXT, + //'rules.background-color': '#494949', + //'rules.text-color': COLOR_DARK_TEXT, 'wiki.background-color': '#494949', 'wiki.text-color': COLOR_DARK_TEXT, 'forum.background-color': '#494949', 'forum.text-color': COLOR_DARK_TEXT, 'github.background-color': '#3a3a3a', 'github.text-color': COLOR_DARK_TEXT, - 'report-issue.background-color': '#492020', - 'report-issue.text-color': COLOR_DARK_TEXT, + //'report-issue.background-color': '#492020', + //'report-issue.text-color': COLOR_DARK_TEXT, // Status and verb tabs 'output.background-color': COLOR_DARK_BG_DARKER, 'output.text-color': COLOR_DARK_TEXT, diff --git a/tgui/packages/tgui/interfaces/AccessViewer/AccessTypeDisplay.tsx b/tgui/packages/tgui/interfaces/AccessViewer/AccessTypeDisplay.tsx new file mode 100644 index 0000000000..c4dce6e658 --- /dev/null +++ b/tgui/packages/tgui/interfaces/AccessViewer/AccessTypeDisplay.tsx @@ -0,0 +1,29 @@ +import { Box, Tooltip } from 'tgui-core/components'; +import { ACCESS_FLAGS } from './constants'; + +export const AccessTypeDisplay = (props: { type: number }) => { + const { type } = props; + + const names = ACCESS_FLAGS.filter((flag) => type & flag.bit).map( + (f) => f.label, + ); + const allFlags = ACCESS_FLAGS.reduce((acc, f) => acc | f.bit, 0); + + const label = + names.length === 0 + ? 'None' + : type === allFlags + ? 'All' + : names.length > 1 + ? 'Multiple' + : names[0]; + + const tooltip = + names.length > 1 || type === allFlags ? names.join(', ') : undefined; + + return ( + + {label} + + ); +}; diff --git a/tgui/packages/tgui/interfaces/AccessViewer/constants.ts b/tgui/packages/tgui/interfaces/AccessViewer/constants.ts new file mode 100644 index 0000000000..66170af902 --- /dev/null +++ b/tgui/packages/tgui/interfaces/AccessViewer/constants.ts @@ -0,0 +1,26 @@ +export enum region_ids { + None = -1, + All = 0, + Security, + Medbay, + Research, + Engineering, + Command, + General, + Supply, +} + +enum access_types { + None = 0, + Centcom = 1, + Station = 2, + Syndicate = 4, + Private = 8, +} + +export const ACCESS_FLAGS = [ + { bit: access_types.Centcom, label: 'Centcom' }, + { bit: access_types.Station, label: 'Station' }, + { bit: access_types.Syndicate, label: 'Syndicate' }, + { bit: access_types.Private, label: 'Private' }, +]; diff --git a/tgui/packages/tgui/interfaces/AccessViewer/index.tsx b/tgui/packages/tgui/interfaces/AccessViewer/index.tsx new file mode 100644 index 0000000000..b098dba68a --- /dev/null +++ b/tgui/packages/tgui/interfaces/AccessViewer/index.tsx @@ -0,0 +1,106 @@ +import { Fragment, useState } from 'react'; +import { useBackend } from 'tgui/backend'; +import { Window } from 'tgui/layouts'; +import { Button, Divider, Input, Section, Table } from 'tgui-core/components'; +import { createSearch } from 'tgui-core/string'; +import { AccessTypeDisplay } from './AccessTypeDisplay'; +import { region_ids } from './constants'; +import type { Access, Data } from './types'; + +export const AccessViewer = (props) => { + const { act, data } = useBackend(); + const { access_list, name, coords, req_access, req_one_access } = data; + const [searchText, setSearchText] = useState(''); + + const access_regions: Record = { + [region_ids.None]: [], + [region_ids.Security]: [], + [region_ids.Medbay]: [], + [region_ids.Research]: [], + [region_ids.Engineering]: [], + [region_ids.Command]: [], + [region_ids.General]: [], + [region_ids.Supply]: [], + }; + + const search = createSearch(searchText.toLowerCase(), (entry) => + `${entry.id}: ${entry.name}`.toLowerCase(), + ); + + access_list.forEach((entry) => { + entry.has_req = req_access?.includes(entry.id); + entry.has_req_one = req_one_access?.includes(entry.id); + + if (search(entry)) { + access_regions[entry.region].push(entry); + } + }); + + return ( + + +
+ } + > + + {Object.keys(access_regions).map((key, index) => ( + + {index > 0 && ( + + + + + + )} + + {region_ids[key]} + + + + + + + {access_regions[key].map((entry: Access) => ( + + {`${entry.id}: ${entry.name}`} + + act('req_all', { set_id: entry.id })} + > + Requires + + + + act('req_one', { set_id: entry.id })} + > + Have Any + + + + + + + ))} + + ))} +
+
+
+
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/AccessViewer/types.ts b/tgui/packages/tgui/interfaces/AccessViewer/types.ts new file mode 100644 index 0000000000..46a2c5d344 --- /dev/null +++ b/tgui/packages/tgui/interfaces/AccessViewer/types.ts @@ -0,0 +1,18 @@ +import type { BooleanLike } from 'tgui-core/react'; + +export type Data = { + access_list: Access[]; + name?: string; + coords?: string; + req_access?: number[]; + req_one_access?: number[]; +}; + +export type Access = { + name: string; + id: number; + region: number; + access_type: number; + has_req: BooleanLike; + has_req_one: BooleanLike; +}; diff --git a/vorestation.dme b/vorestation.dme index df860d7d64..09ebe9dd0a 100644 --- a/vorestation.dme +++ b/vorestation.dme @@ -2269,6 +2269,7 @@ #include "code\game\turfs\unsimulated\walls.dm" #include "code\js\byjax.dm" #include "code\matrices\color_matrix.dm" +#include "code\modules\admin\access_viewer.dm" #include "code\modules\admin\admin.dm" #include "code\modules\admin\admin_attack_log.dm" #include "code\modules\admin\admin_investigate.dm"