diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm index da2320ac8c4..4c38ef43019 100644 --- a/code/__DEFINES/dcs/signals.dm +++ b/code/__DEFINES/dcs/signals.dm @@ -1481,4 +1481,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 f11be818042..e2169be4759 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 de6c2e2438f..9a029e2c193 100644 --- a/code/controllers/subsystem/ticker.dm +++ b/code/controllers/subsystem/ticker.dm @@ -263,6 +263,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 f9d8bba352f..18f4ed5e40f 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 c7c3379872e..2360125cef7 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 fd2c217f668..0a66a3c9349 100644 --- a/code/game/objects/effects/landmarks.dm +++ b/code/game/objects/effects/landmarks.dm @@ -462,16 +462,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) @@ -485,7 +492,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 8fa6dbbaa47..c16113e6598 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -111,6 +111,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, /client/proc/spawn_pollution, // SKYRAT EDIT ADDITION )) GLOBAL_PROTECT(admin_verbs_fun) diff --git a/tgstation.dme b/tgstation.dme index 5bc1bde5bcc..9880703cd8a 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -950,6 +950,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} + + + ); +};