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"