From e3c909cf6e4174c52891b1d072d770978406b857 Mon Sep 17 00:00:00 2001 From: Mothblocks <35135081+Mothblocks@users.noreply.github.com> Date: Wed, 22 Sep 2021 14:23:04 -0700 Subject: [PATCH] Add admin panel for station traits (#61546) Admins can now revert (most) station traits before a round starts, as well as setup the station traits that will run next round. Previously, admins had no control over this system, making it sometimes painful for running planned events. It is not feasible to update the station traits of the current round, considering some apply pre-roundstart, but it is doable to edit the station traits of the next round. --- code/__DEFINES/dcs/signals.dm | 3 + code/__DEFINES/station.dm | 3 + .../subsystem/processing/station.dm | 19 ++ code/controllers/subsystem/ticker.dm | 2 + code/datums/station_traits/_station_trait.dm | 24 +- code/datums/station_traits/admin_panel.dm | 133 +++++++++ code/datums/station_traits/negative_traits.dm | 13 + code/datums/station_traits/neutral_traits.dm | 3 + code/datums/station_traits/positive_traits.dm | 13 +- code/game/objects/effects/landmarks.dm | 11 +- code/modules/admin/admin_verbs.dm | 1 + tgstation.dme | 1 + .../tgui/interfaces/StationTraitsPanel.tsx | 268 ++++++++++++++++++ 13 files changed, 487 insertions(+), 7 deletions(-) create mode 100644 code/datums/station_traits/admin_panel.dm create mode 100644 tgui/packages/tgui/interfaces/StationTraitsPanel.tsx diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm index 87b63eda36f..9a3f2e0fc67 100644 --- a/code/__DEFINES/dcs/signals.dm +++ b/code/__DEFINES/dcs/signals.dm @@ -1479,4 +1479,7 @@ ///Called when the ticker sets up the game for start #define COMSIG_TICKER_ENTER_SETTING_UP "comsig_ticker_enter_setting_up" +/// Called when the round has started, but before GAME_STATE_PLAYING +#define COMSIG_TICKER_ROUND_STARTING "comsig_ticker_round_starting" + #define COMSIG_GREYSCALE_CONFIG_REFRESHED "greyscale_config_refreshed" diff --git a/code/__DEFINES/station.dm b/code/__DEFINES/station.dm index 8feef00342d..0f6c4db003e 100644 --- a/code/__DEFINES/station.dm +++ b/code/__DEFINES/station.dm @@ -4,3 +4,6 @@ #define STATION_TRAIT_ABSTRACT (1<<0) + +/// The data file that future station traits are stored in +#define FUTURE_STATION_TRAITS_FILE "data/future_station_traits.json" diff --git a/code/controllers/subsystem/processing/station.dm b/code/controllers/subsystem/processing/station.dm index 38bf57d2779..84387913e63 100644 --- a/code/controllers/subsystem/processing/station.dm +++ b/code/controllers/subsystem/processing/station.dm @@ -25,6 +25,25 @@ PROCESSING_SUBSYSTEM_DEF(station) ///Rolls for the amount of traits and adds them to the traits list /datum/controller/subsystem/processing/station/proc/SetupTraits() + if (fexists(FUTURE_STATION_TRAITS_FILE)) + var/forced_traits_contents = file2text(FUTURE_STATION_TRAITS_FILE) + fdel(FUTURE_STATION_TRAITS_FILE) + + var/list/forced_traits_text_paths = json_decode(forced_traits_contents) + forced_traits_text_paths = SANITIZE_LIST(forced_traits_text_paths) + + for (var/trait_text_path in forced_traits_text_paths) + var/station_trait_path = text2path(trait_text_path) + if (!ispath(station_trait_path, /datum/station_trait) || station_trait_path == /datum/station_trait) + var/message = "Invalid station trait path [station_trait_path] was requested in the future station traits!" + log_game(message) + message_admins(message) + continue + + setup_trait(station_trait_path) + + return + for(var/i in subtypesof(/datum/station_trait)) var/datum/station_trait/trait_typepath = i diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm index 6d6a038f3a1..27c3a7ae0fa 100755 --- a/code/controllers/subsystem/ticker.dm +++ b/code/controllers/subsystem/ticker.dm @@ -253,6 +253,8 @@ SUBSYSTEM_DEF(ticker) cb.InvokeAsync() LAZYCLEARLIST(round_start_events) + SEND_SIGNAL(src, COMSIG_TICKER_ROUND_STARTING) + log_world("Game start took [(world.timeofday - init_start)/10]s") round_start_time = world.time SSdbcore.SetRoundStart() diff --git a/code/datums/station_traits/_station_trait.dm b/code/datums/station_traits/_station_trait.dm index dde92376f65..5fbf8611d57 100644 --- a/code/datums/station_traits/_station_trait.dm +++ b/code/datums/station_traits/_station_trait.dm @@ -20,20 +20,38 @@ var/blacklist ///Extra flags for station traits such as it being abstract var/trait_flags - + /// Whether or not this trait can be reverted by an admin + var/can_revert = TRUE /datum/station_trait/New() . = ..() - SSticker.OnRoundstart(CALLBACK(src, .proc/on_round_start)) + + RegisterSignal(SSticker, COMSIG_TICKER_ROUND_STARTING, .proc/on_round_start) + if(trait_processes) START_PROCESSING(SSstation, src) if(trait_to_give) ADD_TRAIT(SSstation, trait_to_give, STATION_TRAIT) -///Proc ran when round starts. Use this for roundstart effects. +/datum/station_trait/Destroy() + SSstation.station_traits -= src + return ..() + +/// Proc ran when round starts. Use this for roundstart effects. /datum/station_trait/proc/on_round_start() + SIGNAL_HANDLER return ///type of info the centcom report has on this trait, if any. /datum/station_trait/proc/get_report() return "[name] - [report_message]" + +/// Will attempt to revert the station trait, used by admins. +/datum/station_trait/proc/revert() + if (!can_revert) + CRASH("revert() was called on [type], which can't be reverted!") + + if (trait_to_give) + REMOVE_TRAIT(SSstation, trait_to_give, STATION_TRAIT) + + qdel(src) diff --git a/code/datums/station_traits/admin_panel.dm b/code/datums/station_traits/admin_panel.dm new file mode 100644 index 00000000000..02eca48b54f --- /dev/null +++ b/code/datums/station_traits/admin_panel.dm @@ -0,0 +1,133 @@ +/// Opens the station traits admin panel +/datum/admins/proc/station_traits_panel() + set name = "Modify Station Traits" + set category = "Admin.Events" + + var/static/datum/station_traits_panel/station_traits_panel = new + station_traits_panel.ui_interact(usr) + +/datum/station_traits_panel + var/static/list/future_traits + +/datum/station_traits_panel/ui_data(mob/user) + var/list/data = list() + + data["too_late_to_revert"] = too_late_to_revert() + + var/list/current_station_traits = list() + for (var/datum/station_trait/station_trait as anything in SSstation.station_traits) + current_station_traits += list(list( + "name" = station_trait.name, + "can_revert" = station_trait.can_revert, + "ref" = REF(station_trait), + )) + + data["current_traits"] = current_station_traits + data["future_station_traits"] = future_traits + + return data + +/datum/station_traits_panel/ui_static_data(mob/user) + var/list/data = list() + + var/list/valid_station_traits = list() + + for (var/datum/station_trait/station_trait_path as anything in subtypesof(/datum/station_trait)) + valid_station_traits += list(list( + "name" = initial(station_trait_path.name), + "path" = station_trait_path, + )) + + data["valid_station_traits"] = valid_station_traits + + return data + +/datum/station_traits_panel/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if (.) + return + + switch (action) + if ("revert") + var/ref = params["ref"] + if (!ref) + return TRUE + + var/datum/station_trait/station_trait = locate(ref) + + if (!istype(station_trait)) + return TRUE + + if (too_late_to_revert()) + to_chat(usr, span_warning("It's too late to revert station traits, the round has already started!")) + return TRUE + + if (!station_trait.can_revert) + stack_trace("[station_trait.type] can't be reverted, but was requested anyway.") + return TRUE + + var/message = "[key_name(usr)] reverted the station trait [station_trait.name] ([station_trait.type])" + log_admin(message) + message_admins(message) + + station_trait.revert() + + return TRUE + if ("setup_future_traits") + if (too_late_for_future_traits()) + to_chat(usr, span_warning("It's too late to add future station traits, the round is already over!")) + return TRUE + + var/list/new_future_traits = list() + var/list/station_trait_names = list() + + for (var/station_trait_text in params["station_traits"]) + var/datum/station_trait/station_trait_path = text2path(station_trait_text) + if (!ispath(station_trait_path, /datum/station_trait) || station_trait_path == /datum/station_trait) + log_admin("[key_name(usr)] tried to set an invalid future station trait: [station_trait_text]") + to_chat(usr, span_warning("Invalid future station trait: [station_trait_text]")) + return TRUE + + station_trait_names += initial(station_trait_path.name) + + new_future_traits += list(list( + "name" = initial(station_trait_path.name), + "path" = station_trait_path, + )) + + var/message = "[key_name(usr)] has prepared the following station traits for next round: [station_trait_names.Join(", ") || "None"]" + log_admin(message) + message_admins(message) + + future_traits = new_future_traits + rustg_file_write(json_encode(params["station_traits"]), FUTURE_STATION_TRAITS_FILE) + + return TRUE + if ("clear_future_traits") + if (!future_traits) + to_chat(usr, span_warning("There are no future station traits.")) + return TRUE + + var/message = "[key_name(usr)] has cleared the station traits for next round." + log_admin(message) + message_admins(message) + + fdel(FUTURE_STATION_TRAITS_FILE) + future_traits = null + + return TRUE + +/datum/station_traits_panel/proc/too_late_for_future_traits() + return SSticker.current_state >= GAME_STATE_FINISHED + +/datum/station_traits_panel/proc/too_late_to_revert() + return SSticker.current_state >= GAME_STATE_PLAYING + +/datum/station_traits_panel/ui_status(mob/user, datum/ui_state/state) + return check_rights_for(user.client, R_FUN) ? UI_INTERACTIVE : UI_CLOSE + +/datum/station_traits_panel/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "StationTraitsPanel") + ui.open() diff --git a/code/datums/station_traits/negative_traits.dm b/code/datums/station_traits/negative_traits.dm index ab1796e35ee..fb2679090f4 100644 --- a/code/datums/station_traits/negative_traits.dm +++ b/code/datums/station_traits/negative_traits.dm @@ -48,6 +48,11 @@ . = ..() RegisterSignal(SSdcs, COMSIG_GLOB_JOB_AFTER_LATEJOIN_SPAWN, .proc/on_job_after_spawn) +/datum/station_trait/hangover/revert() + for (var/obj/effect/landmark/start/hangover/hangover_spot in GLOB.start_landmarks_list) + QDEL_LIST(hangover_spot.debris) + + return ..() /datum/station_trait/hangover/proc/on_job_after_spawn(datum/source, datum/job/job, mob/living/spawned_mob) SIGNAL_HANDLER @@ -89,6 +94,8 @@ blacklist = list(/datum/station_trait/filled_maint) trait_to_give = STATION_TRAIT_EMPTY_MAINT + // This station trait is checked when loot drops initialize, so it's too late + can_revert = FALSE /datum/station_trait/overflow_job_bureaucracy name = "Overflow bureaucracy mistake" @@ -156,6 +163,12 @@ name = "Revenge of Pun Pun" trait_type = STATION_TRAIT_NEGATIVE weight = 2 + + // Way too much is done on atoms SS to be reverted, and it'd look + // kinda clunky on round start. It's not impossible to make this work, + // but it's a project for...someone else. + can_revert = FALSE + var/static/list/weapon_types /datum/station_trait/revenge_of_pun_pun/New() diff --git a/code/datums/station_traits/neutral_traits.dm b/code/datums/station_traits/neutral_traits.dm index 3fbbe3aa962..65f4aaf5a4a 100644 --- a/code/datums/station_traits/neutral_traits.dm +++ b/code/datums/station_traits/neutral_traits.dm @@ -13,6 +13,9 @@ report_message = "System's local planet has irregular atmospherical properties" trait_to_give = STATION_TRAIT_UNNATURAL_ATMOSPHERE + // This station trait modifies the atmosphere, which is too far past the time admins are able to revert it + can_revert = FALSE + /datum/station_trait/unique_ai name = "Unique AI" trait_type = STATION_TRAIT_NEUTRAL diff --git a/code/datums/station_traits/positive_traits.dm b/code/datums/station_traits/positive_traits.dm index 6afe9510f17..f8dce57eb78 100644 --- a/code/datums/station_traits/positive_traits.dm +++ b/code/datums/station_traits/positive_traits.dm @@ -114,6 +114,9 @@ blacklist = list(/datum/station_trait/empty_maint) trait_to_give = STATION_TRAIT_FILLED_MAINT + // This station trait is checked when loot drops initialize, so it's too late + can_revert = FALSE + /datum/station_trait/quick_shuttle name = "Quick Shuttle" trait_type = STATION_TRAIT_POSITIVE @@ -141,7 +144,6 @@ . = ..() deathrattle_group = new("[department_name] group") blacklist += subtypesof(/datum/station_trait/deathrattle_department) - type //All but ourselves - name = "deathrattled [department_name]" report_message = "All members of [department_name] have received an implant to notify each other if one of them dies. This should help improve job-safety!" RegisterSignal(SSdcs, COMSIG_GLOB_JOB_AFTER_SPAWN, .proc/on_job_after_spawn) @@ -158,49 +160,56 @@ /datum/station_trait/deathrattle_department/service + name = "Deathrattled Service" trait_flags = NONE weight = 1 department_to_apply_to = DEPARTMENT_BITFLAG_SERVICE department_name = "Service" /datum/station_trait/deathrattle_department/cargo + name = "Deathrattled Cargo" trait_flags = NONE weight = 1 department_to_apply_to = DEPARTMENT_BITFLAG_CARGO department_name = "Cargo" /datum/station_trait/deathrattle_department/engineering + name = "Deathrattled Engineering" trait_flags = NONE weight = 1 department_to_apply_to = DEPARTMENT_BITFLAG_ENGINEERING department_name = "Engineering" /datum/station_trait/deathrattle_department/command + name = "Deathrattled Command" trait_flags = NONE weight = 1 department_to_apply_to = DEPARTMENT_BITFLAG_COMMAND department_name = "Command" /datum/station_trait/deathrattle_department/science + name = "Deathrattled Science" trait_flags = NONE weight = 1 department_to_apply_to = DEPARTMENT_BITFLAG_SCIENCE department_name = "Science" /datum/station_trait/deathrattle_department/security + name = "Deathrattled Security" trait_flags = NONE weight = 1 department_to_apply_to = DEPARTMENT_BITFLAG_SECURITY department_name = "Security" /datum/station_trait/deathrattle_department/medical + name = "Deathrattled Medical" trait_flags = NONE weight = 1 department_to_apply_to = DEPARTMENT_BITFLAG_MEDICAL department_name = "Medical" /datum/station_trait/deathrattle_all - name = "deathrattled station" + name = "Deathrattled Station" trait_type = STATION_TRAIT_POSITIVE show_in_report = TRUE weight = 1 diff --git a/code/game/objects/effects/landmarks.dm b/code/game/objects/effects/landmarks.dm index 75259eecdf8..0c4a08c1597 100644 --- a/code/game/objects/effects/landmarks.dm +++ b/code/game/objects/effects/landmarks.dm @@ -451,16 +451,23 @@ INITIALIZE_IMMEDIATE(/obj/effect/landmark/start/new_player) name = "hangover spawn" icon_state = "hangover_spawn" + /// A list of everything this hangover spawn created + var/list/debris = list() + /obj/effect/landmark/start/hangover/Initialize() . = ..() return INITIALIZE_HINT_LATELOAD +/obj/effect/landmark/start/hangover/Destroy() + debris = null + return ..() + /obj/effect/landmark/start/hangover/LateInitialize() . = ..() if(!HAS_TRAIT(SSstation, STATION_TRAIT_HANGOVER)) return if(prob(60)) - new /obj/effect/decal/cleanable/vomit(get_turf(src)) + debris += new /obj/effect/decal/cleanable/vomit(get_turf(src)) if(prob(70)) var/bottle_count = rand(1, 3) for(var/index in 1 to bottle_count) @@ -474,7 +481,7 @@ INITIALIZE_IMMEDIATE(/obj/effect/landmark/start/new_player) break if(dense_object) continue - new /obj/item/reagent_containers/food/drinks/bottle/beer/almost_empty(turf_to_spawn_on) + debris += new /obj/item/reagent_containers/food/drinks/bottle/beer/almost_empty(turf_to_spawn_on) ///Spawns the mob with some drugginess/drunkeness, and some disgust. /obj/effect/landmark/start/hangover/proc/make_hungover(mob/hangover_mob) diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index 00230b9e12a..5254fd64b89 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -103,6 +103,7 @@ GLOBAL_LIST_INIT(admin_verbs_fun, list( /client/proc/show_tip, /client/proc/smite, /client/proc/admin_away, + /datum/admins/proc/station_traits_panel, )) GLOBAL_PROTECT(admin_verbs_fun) GLOBAL_LIST_INIT(admin_verbs_spawn, list(/datum/admins/proc/spawn_atom, /datum/admins/proc/podspawn_atom, /datum/admins/proc/spawn_cargo, /datum/admins/proc/spawn_objasmob, /client/proc/respawn_character, /datum/admins/proc/beaker_panel)) diff --git a/tgstation.dme b/tgstation.dme index c2a5c397012..b3afeb99f5c 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -911,6 +911,7 @@ #include "code\datums\skills\gaming.dm" #include "code\datums\skills\mining.dm" #include "code\datums\station_traits\_station_trait.dm" +#include "code\datums\station_traits\admin_panel.dm" #include "code\datums\station_traits\negative_traits.dm" #include "code\datums\station_traits\neutral_traits.dm" #include "code\datums\station_traits\positive_traits.dm" diff --git a/tgui/packages/tgui/interfaces/StationTraitsPanel.tsx b/tgui/packages/tgui/interfaces/StationTraitsPanel.tsx new file mode 100644 index 00000000000..a137017b404 --- /dev/null +++ b/tgui/packages/tgui/interfaces/StationTraitsPanel.tsx @@ -0,0 +1,268 @@ +import { filterMap } from "common/collections"; +import { exhaustiveCheck } from "common/exhaustive"; +import { BooleanLike } from "common/react"; +import { useBackend, useLocalState } from "../backend"; +import { Box, Button, Divider, Dropdown, Stack, Tabs } from "../components"; +import { Window } from "../layouts"; + +type CurrentStationTrait = { + can_revert: BooleanLike, + name: string, + ref: string, +} + +type ValidStationTrait = { + name: string, + path: string, +} + +type StationTraitsData = { + current_traits: CurrentStationTrait[], + future_station_traits?: ValidStationTrait[], + too_late_to_revert: BooleanLike, + valid_station_traits: ValidStationTrait[], +}; + +enum Tab { + SetupFutureStationTraits, + ViewStationTraits, +} + +const FutureStationTraitsPage = (props, context) => { + const { act, data } = useBackend(context); + const { future_station_traits } = data; + + const [selectedTrait, setSelectedTrait] = useLocalState( + context, + "selectedFutureTrait", + null, + ); + + const traitsByName = Object.fromEntries( + data.valid_station_traits.map(trait => { + return [trait.name, trait.path]; + }) + ); + + const traitNames = Object.keys(traitsByName); + traitNames.sort(); + + return ( + + + + + + + + + + + + + + { + Array.isArray(future_station_traits) + ? ( + future_station_traits.length > 0 + ? ( + + {future_station_traits.map(trait => ( + + + + {trait.name} + + + + + + + + ))} + + ) : ( + <> + + No station traits will run next round. + + + + + + + ) + ) + : ( + <> + + No future station traits are planned. + + + + + + + ) + } + + ); +}; + +const ViewStationTraitsPage = (props, context) => { + const { act, data } = useBackend(context); + + return data.current_traits.length > 0 ? ( + + {data.current_traits.map(stationTrait => ( + + + + {stationTrait.name} + + + + act("revert", { + ref: stationTrait.ref, + })} + /> + + + + ))} + + ) : ( + + There are no active station traits. + + ); +}; + +export const StationTraitsPanel = (props, context) => { + const [currentTab, setCurrentTab] = useLocalState( + context, + "station_traits_tab", + Tab.ViewStationTraits, + ); + + let currentPage; + + switch (currentTab) { + case Tab.SetupFutureStationTraits: + currentPage = ; + break; + case Tab.ViewStationTraits: + currentPage = ; + break; + default: + exhaustiveCheck(currentTab); + } + + return ( + + + + setCurrentTab(Tab.ViewStationTraits)} + > + View + + + setCurrentTab(Tab.SetupFutureStationTraits)} + > + Edit + + + + + + {currentPage} + + + ); +};