diff --git a/code/controllers/subsystem/persistence/_persistence.dm b/code/controllers/subsystem/persistence/_persistence.dm
index df225210f02e..fb8d74ac0a6f 100644
--- a/code/controllers/subsystem/persistence/_persistence.dm
+++ b/code/controllers/subsystem/persistence/_persistence.dm
@@ -73,7 +73,7 @@ SUBSYSTEM_DEF(persistence)
load_panic_bunker() //SKYRAT EDIT ADDITION - PANICBUNKER
load_tram_counter()
load_adventures()
- load_storyteller_type() //BUBBER EDIT ADD - Storyteller
+ // load_storyteller_type() //BUBBER EDIT ADD - Storyteller // BUG EDIT - Unified
return SS_INIT_SUCCESS
///Collects all data to persist.
diff --git a/code/controllers/subsystem/statpanel.dm b/code/controllers/subsystem/statpanel.dm
index 535cbd33503a..92d9ab778676 100644
--- a/code/controllers/subsystem/statpanel.dm
+++ b/code/controllers/subsystem/statpanel.dm
@@ -45,7 +45,7 @@ SUBSYSTEM_DEF(statpanels)
"Time Dilation: [round(SStime_track.time_dilation_current,1)]% AVG:([round(SStime_track.time_dilation_avg_fast,1)]%, [round(SStime_track.time_dilation_avg,1)]%, [round(SStime_track.time_dilation_avg_slow,1)]%)",
"Map: [SSmapping.current_map?.map_name || "Loading..."]",
cached ? "Next Map: [cached.map_name]" : null,
- "Storyteller: [SSgamemode.storyteller ? SSgamemode.storyteller.name : "N/A"]", // BUBBER EDIT ADDITION
+ // "Storyteller: [SSgamemode.storyteller ? SSgamemode.storyteller.name : "N/A"]", // BUBBER EDIT ADDITION // BUG REMOVAL
"Round ID: [GLOB.round_id ? GLOB.round_id : "NULL"]",
"Connected: [GLOB.clients.len] | Active: [active_players]/[CONFIG_GET(number/hard_popcap)] | Observing: [observing_players]", //BUBBER EDIT: ACTIVE AND OBSERVING PLAYERS
" ",
diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm
index 7c3ba10474d3..a6f96aa0cd8e 100644
--- a/code/controllers/subsystem/ticker.dm
+++ b/code/controllers/subsystem/ticker.dm
@@ -172,6 +172,7 @@ SUBSYSTEM_DEF(ticker)
send2chat(new /datum/tgs_message_content("<@&[CONFIG_GET(string/game_alert_role_id)]> Round **[GLOB.round_id]** starting on [SSmapping.current_map.map_name], [CONFIG_GET(string/servername)]! \nIf you wish to be pinged for game related stuff, go to <#[CONFIG_GET(string/role_assign_channel_id)]> and assign yourself the roles."), CONFIG_GET(string/channel_announce_new_game)) // SKYRAT EDIT - Role ping and round ID in game-alert
// SKYRAT EDIT END
current_state = GAME_STATE_PREGAME
+ /* BUGSTATION REMOVAL START
// BUBBERSTATION EDIT START
var/storyteller = CONFIG_GET(string/default_storyteller)
if(storyteller)
@@ -179,6 +180,7 @@ SUBSYSTEM_DEF(ticker)
else
SSvote.initiate_vote(/datum/vote/storyteller, "Storyteller Vote", forced = TRUE)
// BUBBERSTATION EDIT END
+ BUGSTATION REMOVAL END */
SStitle.change_title_screen() //SKYRAT EDIT ADDITION - Title screen
addtimer(CALLBACK(SStitle, TYPE_PROC_REF(/datum/controller/subsystem/title, change_title_screen)), 1 SECONDS) //SKYRAT EDIT ADDITION - Title screen
//Everyone who wants to be an observer is now spawned
@@ -257,10 +259,15 @@ SUBSYSTEM_DEF(ticker)
//Configure mode and assign player to antagonists
var/can_continue = FALSE
// can_continue = SSdynamic.pre_setup() //Choose antagonists // BUBBER EDIT - STORYTELLER (note: maybe disable)
+ /* BUG REMOVAL START
//BUBBER EDIT BEGIN - STORYTELLER
SSgamemode.init_storyteller()
can_continue = SSgamemode.pre_setup()
//BUBBER EDIT END - STORYTELLER
+ BUG REMOVAL END */
+ // BUG ADDITION START
+ can_continue = SSunified.pre_setup()
+ // BUG ADDITION END
CHECK_TICK
SEND_GLOBAL_SIGNAL(COMSIG_GLOB_PRE_JOBS_ASSIGNED, src)
@@ -330,7 +337,7 @@ SUBSYSTEM_DEF(ticker)
/datum/controller/subsystem/ticker/proc/PostSetup()
set waitfor = FALSE
SSdynamic.post_setup()
- SSgamemode.post_setup() // BUBBER EDIT - Storyteller
+ SSunified.post_setup() // BUBBER EDIT - Storyteller // BUG EDIT - Unified
GLOB.start_state = new /datum/station_state()
GLOB.start_state.count()
diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm
index 6ada66d3199f..4754e46f2813 100644
--- a/code/modules/admin/topic.dm
+++ b/code/modules/admin/topic.dm
@@ -114,7 +114,8 @@
if(!check_rights(R_ADMIN))
return
//SSdynamic.admin_panel() // BUBBER EDIT - STORYTELLER
- SSgamemode.ui_interact(usr) // BUBBER EDIT - STORYTELLER
+ //SSgamemode.ui_interact(usr) // BUBBER EDIT - STORYTELLER // BUG EDIT - Unified
+ SSunified.admin_panel(usr) // BUG EDIT - Unified
else if(href_list["call_shuttle"])
if(!check_rights(R_ADMIN))
return
diff --git a/code/modules/events/_event.dm b/code/modules/events/_event.dm
index 0e2edb42f620..37f6f0c5609e 100644
--- a/code/modules/events/_event.dm
+++ b/code/modules/events/_event.dm
@@ -42,9 +42,11 @@
var/map_flags = NONE
/datum/round_event_control/New()
+ SHOULD_CALL_PARENT(TRUE) // BUG EDIT
+ . = ..() // BUG EDIT
if(config && !wizardevent) // Magic is unaffected by configs
earliest_start = CEILING(earliest_start * CONFIG_GET(number/events_min_time_mul), 1)
- min_players = CEILING(min_players * CONFIG_GET(number/events_min_players_mul), 1)
+ // min_players = CEILING(min_players * CONFIG_GET(number/events_min_players_mul), 1) // BUG EDIT
if(!length(admin_setup))
return
var/list/admin_setup_types = admin_setup.Copy()
@@ -74,13 +76,13 @@
SHOULD_CALL_PARENT(TRUE)
if(occurrences >= max_occurrences)
return FALSE
- if(!(roundstart ^ SSticker.HasRoundStarted())) // BUBBER EDIT: Roundstart checks added
+ if(roundstart && !SSticker.HasRoundStarted()) // BUBBER EDIT: Roundstart checks added // BUG EDIT
return FALSE
- if(weight == 0) // BUBBER EDIT: Weight check added
+ if(calculated_weight == 0 && unified_cost != 0) // BUBBER EDIT: Weight check added // BUG EDIT
return FALSE
if(!allow_magic && wizardevent != SSevents.wizardmode)
return FALSE
- if(players_amt < min_players)
+ if(players_amt < CEILING(min_players * CONFIG_GET(number/events_min_players_mul), 1)) // BUG EDIT
return FALSE
if(holidayID && !check_holidays(holidayID))
return FALSE
@@ -92,6 +94,11 @@
if (dynamic_should_hijack && SSdynamic.random_event_hijacked != HIJACKED_NOTHING)
return FALSE
+ // BUG ADDITION START
+ if(calculated_cost > SSunified.points)
+ return FALSE
+ // BUG ADDITION END
+
return TRUE
/datum/round_event_control/proc/preRunEvent()
diff --git a/code/modules/events/ghost_role/_ghost_role.dm b/code/modules/events/ghost_role/_ghost_role.dm
index 336fb4c03bbd..a77f2bfd17e8 100644
--- a/code/modules/events/ghost_role/_ghost_role.dm
+++ b/code/modules/events/ghost_role/_ghost_role.dm
@@ -55,11 +55,13 @@
if(MAP_ERROR)
message_admins("[role_name] cannot be spawned due to a map error.")
kill()
+ SSunified.refund_failed_event(control) // BUG ADDITION
return
if(NOT_ENOUGH_PLAYERS)
message_admins("[role_name] cannot be spawned due to lack of players signing up.")
deadchat_broadcast(" did not get enough candidates ([minimum_required]) to spawn.", "[role_name]", message_type=DEADCHAT_ANNOUNCEMENT)
kill()
+ SSunified.refund_failed_event(control) // BUG ADDITION
return
if(SUCCESSFUL_SPAWN)
message_admins("[role_name] spawned successfully.")
diff --git a/modular_zubbers/code/modules/storyteller/_events/_event.dm b/modular_zubbers/code/modules/storyteller/_events/_event.dm
index 232381eddb3b..2bbc83f7fd2b 100644
--- a/modular_zubbers/code/modules/storyteller/_events/_event.dm
+++ b/modular_zubbers/code/modules/storyteller/_events/_event.dm
@@ -58,20 +58,31 @@
if(SSticker.HasRoundStarted())
if(roundstart)
if(!can_run_post_roundstart)
- return "Fire Schedule"
+ return "Fire" // BUG EDIT
return "Fire Schedule"
else
- return "Fire Schedule Force Next"
+ return "Fire Schedule" // BUG EDIT
else
if(roundstart)
- return "Force Roundstart"
+ return "Fire" // BUG EDIT
else
- return "Fire Schedule Force Next"
+ return "Schedule" // BUG EDIT
/datum/round_event_control/Topic(href, href_list)
. = ..()
switch(href_list["action"])
+ // BUG EDIT START
if("force_next")
message_admins("[key_name_admin(usr)] has forced scheduled event [src.name].")
log_admin_private("[key_name(usr)] has forced scheduled event [src.name].")
SSgamemode.force_event(src)
+ if("fire")
+ message_admins("[key_name_admin(usr)] has fired event [src.name].")
+ log_admin_private("[key_name(usr)] has fired event [src.name].")
+ run_event(admin_forced = TRUE)
+ if("schedule")
+ var/delay = input(usr, "Enter the time in seconds to run the event in.", "Schedule Event") as null|num
+ message_admins("[key_name_admin(usr)] has scheduled event [src.name] to run in [delay] seconds.")
+ log_admin_private("[key_name(usr)] has scheduled event [src.name] to run in [delay] seconds.")
+ SSunified.schedule_event(src, delay SECONDS, calculated_cost, TRUE, FALSE)
+ // BUG EDIT END
diff --git a/modular_zubbers/code/modules/storyteller/event_defines/crewset/_antagonist_event.dm b/modular_zubbers/code/modules/storyteller/event_defines/crewset/_antagonist_event.dm
index ab2959cb3e76..0bfaeaedc655 100644
--- a/modular_zubbers/code/modules/storyteller/event_defines/crewset/_antagonist_event.dm
+++ b/modular_zubbers/code/modules/storyteller/event_defines/crewset/_antagonist_event.dm
@@ -67,7 +67,7 @@
. = ..()
if(!.)
return
- if(!roundstart && !SSgamemode.can_inject_antags())
+ if(!roundstart && !SSunified.can_inject_antags()) // BUG EDIT
return FALSE
if(!get_antag_amount())
return FALSE
@@ -80,7 +80,7 @@
/datum/round_event_control/antagonist/proc/get_candidates()
var/round_started = SSticker.HasRoundStarted()
- var/list/candidates = SSgamemode.get_candidates(antag_flag, pick_roundstart_players = !round_started, restricted_roles = restricted_roles)
+ var/list/candidates = SSunified.get_candidates(antag_flag, pick_roundstart_players = !round_started, restricted_roles = restricted_roles) // BUG EDIT
return candidates
/datum/round_event_control/antagonist/solo
@@ -88,7 +88,7 @@
/datum/round_event_control/antagonist/proc/get_antag_amount()
- var/people = SSgamemode.get_correct_popcount()
+ var/people = SSunified.get_correct_popcount() // BUG EDIT
var/amount = base_antags + FLOOR(people / denominator, 1)
if(antag_datum && maximum_antags_global > 0)
diff --git a/modular_zubbers/code/modules/storyteller/gamemode.dm b/modular_zubbers/code/modules/storyteller/gamemode.dm
index d8e7170da439..7334f6d531c9 100644
--- a/modular_zubbers/code/modules/storyteller/gamemode.dm
+++ b/modular_zubbers/code/modules/storyteller/gamemode.dm
@@ -688,9 +688,11 @@ SUBSYSTEM_DEF(gamemode)
if((storyboy.population_min && storyboy.population_min > client_amount) || (storyboy.population_max && storyboy.population_max < client_amount))
continue
choices += storyboy.name
+ /* BUG REMOVAL START
///Because the vote subsystem is dumb and does not support any descriptions, we dump them into world.
to_chat(world, span_notice("[storyboy.name]"))
to_chat(world, span_notice("[storyboy.desc]"))
+ BUG REMOVAL END */
return choices
/datum/controller/subsystem/gamemode/proc/storyteller_vote_result(winner_name)
diff --git a/modular_zubbers/code/modules/storyteller/scheduled_event.dm b/modular_zubbers/code/modules/storyteller/scheduled_event.dm
index ab196d7baf47..f0a38eb01b08 100644
--- a/modular_zubbers/code/modules/storyteller/scheduled_event.dm
+++ b/modular_zubbers/code/modules/storyteller/scheduled_event.dm
@@ -1,4 +1,4 @@
-///Scheduled event datum for SSgamemode to put events into.
+///Scheduled event datum for SSunified to put events into. // BUG EDIT
/datum/scheduled_event
/// What event are scheduling.
var/datum/round_event_control/event
@@ -35,7 +35,7 @@
/// For admins who want to reschedule the event.
/datum/scheduled_event/proc/reschedule(new_time)
start_time = new_time
- alerted_admins = FALSE
+ // alerted_admins = FALSE // BUG EDIT
/datum/scheduled_event/proc/get_href_actions()
var/round_started = SSticker.HasRoundStarted()
@@ -54,15 +54,43 @@
message_admins("Scheduled Event: [event] was unable to run and has been refunded.")
log_admin("Scheduled Event: [event] was unable to run and has been refunded.")
- SSgamemode.refund_scheduled_event(src)
+ SSunified.refund_scheduled_event(src) // BUG EDIT
return
///Trigger the event and remove the scheduled datum
message_admins("Scheduled Event: [event] successfully triggered.")
- SSgamemode.TriggerEvent(event)
- SSgamemode.remove_scheduled_event(src)
+ SSunified.TriggerEvent(event) // BUG EDIT
+ SSunified.remove_scheduled_event(src) // BUG EDIT
/datum/scheduled_event/Destroy()
remove_occurence()
event = null
return ..()
+
+/datum/scheduled_event/Topic(href, href_list)
+ . = ..()
+ if(QDELETED(src))
+ return
+ var/round_started = SSticker.HasRoundStarted()
+ switch(href_list["action"])
+ if("cancel")
+ message_admins("[key_name_admin(usr)] cancelled scheduled event [event.name].")
+ log_admin_private("[key_name(usr)] cancelled scheduled event [event.name].")
+ SSunified.remove_scheduled_event(src) // BUG EDIT
+ if("refund")
+ message_admins("[key_name_admin(usr)] refunded scheduled event [event.name].")
+ log_admin_private("[key_name(usr)] refunded scheduled event [event.name].")
+ SSunified.refund_scheduled_event(src) // BUG EDIT
+ if("reschedule")
+ var/new_schedule = input(usr, "New schedule time (in seconds):", "Reschedule Event") as num|null
+ if(isnull(new_schedule) || QDELETED(src))
+ return
+ start_time = world.time + new_schedule * 1 SECONDS
+ message_admins("[key_name_admin(usr)] rescheduled event [event.name] to [new_schedule] seconds.")
+ log_admin_private("[key_name(usr)] rescheduled event [event.name] to [new_schedule] seconds.")
+ if("fire")
+ if(!round_started)
+ return
+ message_admins("[key_name_admin(usr)] has fired scheduled event [event.name].")
+ log_admin_private("[key_name(usr)] has fired scheduled event [event.name].")
+ try_fire()
diff --git a/modular_zubbers/code/modules/storyteller/storyteller_vote.dm b/modular_zubbers/code/modules/storyteller/storyteller_vote.dm
index 42a4ca665e32..85606cec71a0 100644
--- a/modular_zubbers/code/modules/storyteller/storyteller_vote.dm
+++ b/modular_zubbers/code/modules/storyteller/storyteller_vote.dm
@@ -51,10 +51,12 @@ We then just check what the last one is in SSgamemode.storyteller_vote_choices()
#define STORYTELLER_LAST_FILEPATH "data/storyteller_last_round.txt"
+/* BUG REMOVAL START - unified
/// Extends collect_data
/datum/controller/subsystem/persistence/collect_data()
. = ..()
collect_storyteller_type()
+BUG REMOVAL END */
/// Loads last storyteller into last_storyteller_type
/datum/controller/subsystem/persistence/proc/load_storyteller_type()
diff --git a/modular_zzbug/code/__DEFINES/unified_defines.dm b/modular_zzbug/code/__DEFINES/unified_defines.dm
new file mode 100644
index 000000000000..4e004757387d
--- /dev/null
+++ b/modular_zzbug/code/__DEFINES/unified_defines.dm
@@ -0,0 +1,29 @@
+#define TAG_ENGINEERING "engineering"
+#define TAG_MEDICAL "medical"
+#define TAG_SECURITY "security"
+#define TAG_SCIENCE "science"
+#define TAG_WIZARD "wizard"
+
+#define UNIFIED_WAIT_TIME 20 SECONDS
+
+#define UNIFIED_PANEL_MAIN "Main"
+#define UNIFIED_PANEL_VARIABLES "Variables"
+
+#define COST_VERY_MINOR 5
+#define COST_MINOR 10
+#define COST_MODERATE 20
+#define COST_SEMIMAJOR 30
+#define COST_MAJOR 60
+#define COST_SUPERMAJOR BASE_POINTS * 1.25
+
+#define WEIGHT_NORMAL 10
+#define WEIGHT_LESS_LIKELY 7.5
+#define WEIGHT_UNLIKELY 5
+
+// TODO add to config
+#define BASE_POINTS 120
+
+#define STARTING_DELAY 10 * BASE_POINTS/starting_points MINUTES
+#define SCHEDULE_DELAY 3 MINUTES
+
+#define COOLDOWN_MULT (120/BASE_POINTS) * (BASE_POINTS/starting_points)
diff --git a/modular_zzbug/code/modules/unified/_event.dm b/modular_zzbug/code/modules/unified/_event.dm
new file mode 100644
index 000000000000..753bf3b2a2f2
--- /dev/null
+++ b/modular_zzbug/code/modules/unified/_event.dm
@@ -0,0 +1,9 @@
+/datum/round_event_control
+ /// This event's cost in the Unified system. Equates to a cooldown in minutes for the event.
+ var/unified_cost = 0
+
+ /// The cooldown in minutes to use instead of the cost.
+ var/cooldown_override = 0
+
+ /// Last calculated weight that Unified assigned this event
+ var/calculated_cost = 0
diff --git a/modular_zzbug/code/modules/unified/divergency_report.dm b/modular_zzbug/code/modules/unified/divergency_report.dm
new file mode 100644
index 000000000000..f85f038eda5d
--- /dev/null
+++ b/modular_zzbug/code/modules/unified/divergency_report.dm
@@ -0,0 +1,70 @@
+/datum/controller/subsystem/unified/proc/send_trait_report()
+ . = "Central Command Status Summary
"
+
+ . += ""
+ if(starting_points < BASE_POINTS * 0.75)
+ . += "Low"
+ else if(starting_points < BASE_POINTS * 1.25)
+ . += "Moderate"
+ else
+ . += "High"
+ . += " station threat detected."
+
+ SSstation.generate_station_goals(20)
+
+ var/list/station_goals = SSstation.get_station_goals()
+
+ if(!length(station_goals))
+ . += "
No assigned goals.
"
+ else
+ . += generate_station_goal_report(station_goals)
+ if(!SSstation.station_traits.len)
+ . += "
No identified shift divergencies.
"
+ else
+ . += generate_station_trait_report()
+
+ . += "
This concludes your shift-start evaluation. Have a secure shift!
\
+ This label certifies an Intern has reviewed the above before sending. This document is the property of Nanotrasen Corporation.
"
+
+ print_command_report(., "Central Command Status Summary", announce = FALSE)
+ priority_announce("Hello, crew of [station_name()]. Our intern has finished their shift-start divergency and goals evaluation, which has been sent to your communications console. Have a secure shift!", "Divergency Report", SSstation.announcer.get_rand_report_sound())
+
+
+
+/*
+ * Generate a list of station goals available to purchase to report to the crew.
+ *
+ * Returns a formatted string all station goals that are available to the station.
+ */
+/datum/controller/subsystem/unified/proc/generate_station_goal_report(var/list/station_goals)
+ . = "
Special Orders for [station_name()]:
"
+ var/list/goal_reports = list()
+ for(var/datum/station_goal/station_goal as anything in station_goals)
+ station_goal.on_report()
+ goal_reports += station_goal.get_report()
+
+ . += goal_reports.Join("
")
+ return
+/*
+ * Generate a list of active station traits to report to the crew.
+ *
+ * Returns a formatted string of all station traits (that are shown) affecting the station.
+ */
+/datum/controller/subsystem/unified/proc/generate_station_trait_report()
+ if(!SSstation.station_traits.len)
+ return
+ . = "
Identified shift divergencies:
"
+ for(var/datum/station_trait/station_trait as anything in SSstation.station_traits)
+ if(!station_trait.show_in_report)
+ continue
+ . += "[station_trait.get_report()]
"
+ return
+
+/*/datum/controller/subsystem/unified/proc/generate_station_goals()
+ var/list/possible = subtypesof(/datum/station_goal)
+ var/goal_weights = 0
+ while(possible.len && goal_weights < 1) // station goal budget is 1
+ var/datum/station_goal/picked = pick_n_take(possible)
+ goal_weights += initial(picked.weight)
+ SSstation.goals_by_type += new picked // does this still work?
+*/
diff --git a/modular_zzbug/code/modules/unified/event_overrides.dm b/modular_zzbug/code/modules/unified/event_overrides.dm
new file mode 100644
index 000000000000..2268de5b00e1
--- /dev/null
+++ b/modular_zzbug/code/modules/unified/event_overrides.dm
@@ -0,0 +1,650 @@
+// SCRUBBERS EVENTS
+
+/datum/round_event_control/scrubber_overflow/threatening
+ max_occurrences = 0
+
+/datum/round_event_control/scrubber_overflow/catastrophic
+ max_occurrences = 0
+
+/datum/round_event_control/scrubber_overflow/every_vent
+ max_occurrences = 0
+
+/datum/round_event_control/scrubber_overflow/ices
+ max_occurrences = 0
+
+// MUNDANE EVENTS
+
+/datum/round_event_control/bitrunning_glitch
+ unified_cost = COST_VERY_MINOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/sentience
+ unified_cost = COST_VERY_MINOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/aurora_caelus
+ unified_cost = 0
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/camera_failure
+ unified_cost = COST_MINOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/space_dust
+ unified_cost = COST_MINOR
+ max_occurrences = 0 // space dust is a nothing event
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/space_dust/New()
+ . = ..()
+ tags |= list(TAG_ENGINEERING)
+
+/datum/round_event_control/electrical_storm
+ unified_cost = COST_MINOR
+ weight = WEIGHT_UNLIKELY // it's annoying
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/fake_virus
+ unified_cost = COST_MINOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/falsealarm
+ unified_cost = COST_MINOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/market_crash
+ unified_cost = COST_MINOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/mice_migration
+ unified_cost = COST_MINOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/mice_migration/New()
+ . = ..()
+ tags |= list(TAG_ENGINEERING)
+
+/datum/round_event_control/wisdomcow
+ unified_cost = 0
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/shuttle_loan
+ unified_cost = COST_MINOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/mass_hallucination
+ unified_cost = COST_MINOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/stray_cargo
+ unified_cost = COST_MINOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/stray_cargo/syndicate
+ weight = WEIGHT_LESS_LIKELY
+
+/datum/round_event_control/grey_tide
+ unified_cost = COST_MINOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/grey_tide/New()
+ . = ..()
+ tags |= list(TAG_ENGINEERING)
+
+/datum/round_event_control/gravity_generator_blackout
+ unified_cost = COST_MINOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/gravity_generator_blackout/New()
+ . = ..()
+ tags |= list(TAG_ENGINEERING)
+
+/datum/round_event_control/shuttle_insurance
+ unified_cost = COST_MINOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/tram_malfunction
+ unified_cost = COST_MINOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/tram_malfunction/New()
+ . = ..()
+ tags |= list(TAG_ENGINEERING)
+
+/datum/round_event_control/grid_check
+ unified_cost = COST_MINOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/bureaucratic_error
+ unified_cost = COST_MINOR
+ weight = WEIGHT_UNLIKELY // It's annoying
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/vent_clog
+ unified_cost = COST_MINOR
+ weight = WEIGHT_NORMAL
+
+// /datum/round_event_control/anomaly
+
+/datum/round_event_control/anomaly/New()
+ . = ..()
+ tags |= list(TAG_SCIENCE)
+
+/datum/round_event_control/anomaly/anomaly_hallucination
+ unified_cost = COST_VERY_MINOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/anomaly/anomaly_grav
+ unified_cost = COST_VERY_MINOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/anomaly/anomaly_grav/high
+ unified_cost = COST_MINOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/anomaly/anomaly_grav/high/New()
+ . = ..()
+ tags |= list(TAG_ENGINEERING)
+
+/datum/round_event_control/anomaly/anomaly_bioscrambler
+ unified_cost = COST_MINOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/anomaly/anomaly_bioscrambler/New()
+ . = ..()
+ tags |= list(TAG_MEDICAL)
+
+/datum/round_event_control/anomaly/anomaly_bluespace
+ unified_cost = COST_MINOR
+ weight = WEIGHT_LESS_LIKELY // Our stuff being teleported away is kinda annoying but not too bad
+
+/datum/round_event_control/vent_clog/strange
+ unified_cost = COST_MINOR
+ max_occurrences = 0 // none of this for now, not sure about the animals in it
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/scrubber_overflow
+ unified_cost = COST_MINOR
+ weight = WEIGHT_NORMAL
+
+// MODERATE EVENTS
+
+/datum/round_event_control/meteor_wave/dust_storm
+ unified_cost = COST_MODERATE
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/brain_trauma
+ unified_cost = COST_MODERATE
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/brain_trauma/New()
+ . = ..()
+ tags |= list(TAG_MEDICAL)
+
+/datum/round_event_control/supermatter_surge
+ unified_cost = COST_MODERATE
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/supermatter_surge/New()
+ . = ..()
+ tags |= list(TAG_ENGINEERING)
+
+/datum/round_event_control/brand_intelligence
+ unified_cost = COST_MODERATE
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/brand_intelligence/New()
+ . = ..()
+ tags |= list(TAG_ENGINEERING, TAG_MEDICAL)
+
+/datum/round_event_control/carp_migration
+ unified_cost = COST_MODERATE
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/carp_migration/New()
+ . = ..()
+ tags |= list(TAG_SECURITY, TAG_MEDICAL)
+
+/datum/round_event_control/communications_blackout
+ unified_cost = COST_MODERATE
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/ion_storm
+ unified_cost = COST_MODERATE
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/processor_overload
+ unified_cost = COST_MODERATE
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/processor_overload/New()
+ . = ..()
+ tags |= list(TAG_ENGINEERING)
+
+/datum/round_event_control/radiation_leak
+ unified_cost = COST_MODERATE
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/radiation_leak/New()
+ . = ..()
+ tags |= list(TAG_ENGINEERING, TAG_MEDICAL)
+
+/datum/round_event_control/sandstorm
+ unified_cost = COST_MODERATE
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/sandstorm/New()
+ . = ..()
+ tags |= list(TAG_ENGINEERING)
+
+/datum/round_event_control/shuttle_catastrophe
+ unified_cost = COST_MODERATE
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/spacevine
+ unified_cost = COST_MODERATE
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/portal_storm_syndicate
+ unified_cost = COST_MODERATE
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/portal_storm_syndicate/New()
+ . = ..()
+ tags |= list(TAG_SECURITY, TAG_MEDICAL)
+
+// BUG EDIT START
+/datum/round_event_control/radiation_storm
+ unified_cost = COST_MODERATE
+ weight = WEIGHT_NORMAL
+ max_occurrences = 2
+
+/datum/round_event_control/radiation_storm/New()
+ . = ..()
+ tags |= list(TAG_MEDICAL)
+
+/datum/round_event_control/wormholes
+ unified_cost = COST_MODERATE
+ max_occurrences = 2 // BUG: more than two wormholes would be pretty annoying, like the radstorm
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/wormholes/New()
+ . = ..()
+ tags |= list(TAG_MEDICAL)
+
+/datum/round_event_control/heart_attack
+ unified_cost = COST_MODERATE
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/heart_attack/New()
+ . = ..()
+ tags |= list(TAG_MEDICAL)
+
+/datum/round_event_control/appendicitis
+ unified_cost = COST_MODERATE
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/appendicitis/New()
+ . = ..()
+ tags |= list(TAG_MEDICAL)
+
+/datum/round_event_control/disease_outbreak
+ unified_cost = COST_MODERATE
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/disease_outbreak/New()
+ . = ..()
+ tags |= list(TAG_MEDICAL)
+
+/datum/round_event_control/disease_outbreak/advanced
+ unified_cost = COST_MODERATE
+ weight = WEIGHT_UNLIKELY
+
+/datum/round_event_control/anomaly/anomaly_dimensional
+ unified_cost = COST_MODERATE
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/anomaly/anomaly_dimensional/New()
+ . = ..()
+ tags |= list(TAG_ENGINEERING)
+
+/datum/round_event_control/anomaly/anomaly_ectoplasm
+ unified_cost = COST_MODERATE
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/anomaly/anomaly_flux
+ unified_cost = COST_MODERATE
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/anomaly/anomaly_flux/New()
+ . = ..()
+ tags |= list(TAG_ENGINEERING)
+
+/datum/round_event_control/vent_clog/major
+ unified_cost = COST_MODERATE
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/vent_clog/major/New()
+ . = ..()
+ tags |= list(TAG_SECURITY)
+
+/datum/round_event_control/vent_clog/critical
+ unified_cost = COST_MODERATE
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/vent_clog/critical/New()
+ . = ..()
+ tags |= list(TAG_SECURITY)
+
+/datum/round_event_control/mold
+ unified_cost = COST_MAJOR
+ weight = WEIGHT_UNLIKELY
+
+/datum/round_event_control/mold/New()
+ . = ..()
+ tags |= list(TAG_MEDICAL, TAG_ENGINEERING, TAG_SECURITY)
+
+// MAJOR EVENTS
+
+/datum/round_event_control/pirates
+ unified_cost = COST_MAJOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/pirates/New()
+ . = ..()
+ tags |= list(TAG_MEDICAL, TAG_ENGINEERING, TAG_SECURITY)
+
+/datum/round_event_control/alien_infestation
+ unified_cost = COST_MAJOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/alien_infestation/New()
+ . = ..()
+ tags |= list(TAG_MEDICAL, TAG_SECURITY)
+
+/datum/round_event_control/space_dragon
+ unified_cost = COST_MAJOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/space_dragon/New()
+ . = ..()
+ tags |= list(TAG_MEDICAL, TAG_ENGINEERING, TAG_SECURITY)
+
+/datum/round_event_control/cortical_borer
+ unified_cost = COST_MAJOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/cortical_borer/New()
+ . = ..()
+ tags |= list(TAG_MEDICAL, TAG_SECURITY)
+
+/datum/round_event_control/earthquake
+ unified_cost = COST_SEMIMAJOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/earthquake/New()
+ . = ..()
+ tags |= list(TAG_ENGINEERING)
+
+/datum/round_event_control/blob
+ unified_cost = COST_SUPERMAJOR
+ weight = WEIGHT_UNLIKELY
+
+/datum/round_event_control/blob/New()
+ . = ..()
+ tags |= list(TAG_MEDICAL, TAG_ENGINEERING)
+
+/datum/round_event_control/meteor_wave
+ unified_cost = COST_SEMIMAJOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/meteor_wave/New()
+ . = ..()
+ tags |= list(TAG_MEDICAL, TAG_ENGINEERING)
+
+/datum/round_event_control/meteor_wave/meaty
+ unified_cost = COST_SEMIMAJOR
+ weight = WEIGHT_UNLIKELY // meat meteors?? how queer
+
+/datum/round_event_control/meteor_wave/ices
+ max_occurrences = 0
+
+/datum/round_event_control/immovable_rod
+ unified_cost = COST_SEMIMAJOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/immovable_rod/New()
+ . = ..()
+ tags |= list(TAG_MEDICAL, TAG_ENGINEERING)
+
+/datum/round_event_control/stray_meteor
+ unified_cost = COST_SEMIMAJOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/stray_meteor/New()
+ . = ..()
+ tags |= list(TAG_ENGINEERING)
+
+/datum/round_event_control/anomaly/anomaly_vortex
+ unified_cost = COST_SEMIMAJOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/anomaly/anomaly_vortex/New()
+ . = ..()
+ tags |= list(TAG_ENGINEERING)
+
+/datum/round_event_control/anomaly/anomaly_pyro
+ unified_cost = COST_SEMIMAJOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/anomaly/anomaly_pyro/New()
+ . = ..()
+ tags |= list(TAG_ENGINEERING)
+
+/datum/round_event_control/spider_infestation
+ unified_cost = COST_MAJOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/spider_infestation/New()
+ . = ..()
+ tags |= list(TAG_MEDICAL, TAG_SECURITY)
+
+/datum/round_event_control/revenant
+ min_players = 20
+ unified_cost = COST_MAJOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/abductor
+ unified_cost = COST_MAJOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/abductor/New()
+ . = ..()
+ tags |= list(TAG_MEDICAL, TAG_SECURITY)
+
+/datum/round_event_control/fugitives
+ unified_cost = COST_MAJOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/fugitives/New()
+ . = ..()
+ tags |= list(TAG_MEDICAL, TAG_SECURITY)
+
+/datum/round_event_control/voidwalker
+ unified_cost = COST_MAJOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/voidwalker/New()
+ . = ..()
+ tags |= list(TAG_MEDICAL, TAG_SECURITY)
+
+/datum/round_event_control/cme
+ unified_cost = COST_MAJOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/cme/New()
+ . = ..()
+ tags |= list(TAG_ENGINEERING, TAG_SCIENCE)
+
+/datum/round_event_control/stray_cargo/changeling_zombie
+ unified_cost = COST_MAJOR
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/stray_cargo/changeling_zombie/New()
+ . = ..()
+ tags |= list(TAG_MEDICAL, TAG_SECURITY)
+
+// ANTAGS
+
+/datum/round_event_control/antagonist
+ unified_cost = COST_SEMIMAJOR
+ cooldown_override = 10
+
+/datum/round_event_control/antagonist/solo/bloodsucker
+ min_players = 0
+ base_antags = 1
+ maximum_antags = INFINITY
+ maximum_antags_global = 0
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/antagonist/solo/bloodsucker/New()
+ . = ..()
+ tags |= list(TAG_MEDICAL, TAG_SECURITY)
+
+/datum/round_event_control/antagonist/solo/changeling
+ min_players = 0
+ base_antags = 1
+ maximum_antags = INFINITY
+ maximum_antags_global = 0
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/antagonist/solo/changeling/New()
+ . = ..()
+ tags |= list(TAG_MEDICAL, TAG_SECURITY)
+
+/datum/round_event_control/antagonist/solo/heretic
+ min_players = 0
+ base_antags = 1
+ maximum_antags = INFINITY
+ maximum_antags_global = 0
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/antagonist/solo/heretic/New()
+ . = ..()
+ tags |= list(TAG_MEDICAL, TAG_ENGINEERING, TAG_SECURITY)
+
+/datum/round_event_control/antagonist/solo/malf
+ min_players = 0
+ base_antags = 1
+ maximum_antags = INFINITY
+ maximum_antags_global = 0
+ weight = WEIGHT_NORMAL
+
+ min_players = 20
+
+/datum/round_event_control/antagonist/solo/malf/New()
+ . = ..()
+ tags |= list(TAG_MEDICAL, TAG_ENGINEERING, TAG_SECURITY)
+
+/datum/round_event_control/antagonist/team/nuke_ops
+ unified_cost = COST_SUPERMAJOR
+ weight = WEIGHT_UNLIKELY
+ min_players = 0
+ base_antags = 2
+ maximum_antags = INFINITY
+ maximum_antags_global = 5
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/antagonist/team/nuke_ops/New()
+ . = ..()
+ tags |= list(TAG_MEDICAL, TAG_ENGINEERING, TAG_SECURITY)
+
+/datum/round_event_control/antagonist/obsessed
+ unified_cost = COST_MODERATE
+ min_players = 0
+ base_antags = 1
+ maximum_antags = INFINITY
+ maximum_antags_global = 0
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/antagonist/obsessed/New()
+ . = ..()
+ tags |= list(TAG_MEDICAL, TAG_SECURITY)
+
+/datum/round_event_control/antagonist/obsessed/midround
+ name = "Obsessed (Midround)"
+ roundstart = FALSE
+
+/datum/round_event_control/antagonist/solo/spy
+ min_players = 0
+ base_antags = 1
+ maximum_antags = INFINITY
+ maximum_antags_global = 0
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/antagonist/solo/spy/New()
+ . = ..()
+ tags |= list(TAG_MEDICAL, TAG_ENGINEERING, TAG_SECURITY)
+
+/datum/round_event_control/antagonist/solo/traitor
+ min_players = 0
+ base_antags = 1
+ maximum_antags = INFINITY
+ maximum_antags_global = 0
+ weight = WEIGHT_NORMAL
+
+/datum/round_event_control/antagonist/solo/traitor/New()
+ . = ..()
+ tags |= list(TAG_MEDICAL, TAG_ENGINEERING, TAG_SECURITY)
+
+// WIZARD EVENTS
+
+// /datum/round_event_control/wizard
+
+/datum/round_event_control/wizard/New()
+ . = ..()
+ tags = list(TAG_WIZARD)
+
+// MISC
+
+/datum/round_event_control/operative
+ // Don't override weight for operative!
+ unified_cost = COST_MAJOR
+
+/datum/round_event_control/operative/New()
+ . = ..()
+ tags |= list(TAG_MEDICAL, TAG_ENGINEERING, TAG_SECURITY)
+
+/datum/round_event_control/changeling
+ max_occurrences = 0 // TODO add an antag version of this
+ //unified_cost = COST_MAJOR
+ //cooldown_override = 5
+/*
+/datum/round_event_control/changeling/New()
+ tags |= list(TAG_MEDICAL, TAG_SECURITY)
+*/
+/datum/round_event_control/nightmare
+ max_occurrences = 0 // TODO add an antag version of this
+ //unified_cost = COST_MAJOR
+/*
+/datum/round_event_control/nightmare/New()
+ tags |= list(TAG_MEDICAL, TAG_ENGINEERING, TAG_SECURITY)
+*/
+/datum/round_event_control/wizard_dice
+ unified_cost = COST_MODERATE
+ weight = WEIGHT_NORMAL
+
+// /datum/round_event_control/wizard_dice/New()
+
+/datum/round_event_control/space_ninja
+ max_occurrences = 0 // TODO add an antag version of this
+ unified_cost = COST_MAJOR
+/*
+/datum/round_event_control/space_ninja/New()
+ tags |= list(TAG_MEDICAL, TAG_SECURITY)
+*/
+/datum/round_event_control/obsessed
+ max_occurrences = 0 // We already have an antag version of this event
+
+/datum/round_event_control/morph
+ max_occurrences = 0 // TODO add an antag version of this
diff --git a/modular_zzbug/code/modules/unified/unified.dm b/modular_zzbug/code/modules/unified/unified.dm
new file mode 100644
index 000000000000..860ec6ae4443
--- /dev/null
+++ b/modular_zzbug/code/modules/unified/unified.dm
@@ -0,0 +1,887 @@
+#define INIT_ORDER_UNIFIED 70
+
+SUBSYSTEM_DEF(unified)
+ name = "Unified"
+ init_order = INIT_ORDER_UNIFIED
+ runlevels = RUNLEVEL_GAME
+ flags = SS_BACKGROUND | SS_KEEP_TIMING
+ wait = 2 SECONDS
+
+ /// Next process for our storyteller. The wait time is STORYTELLER_WAIT_TIME
+ var/next_process = 0
+
+ /// Our total event point budget for the shift. Initialized in pre_setup()
+ var/points = 0
+ /// Our starting event point budget for the shift. Initialized in pre_setup()
+ var/starting_points = 0
+
+ /// The times after which we can schedule another event
+ var/list/cooldown_dates = list(0, 0)
+
+ /// Whether we allow pop scaling. This is configured by config, or the storyteller UI
+ var/allow_pop_scaling = TRUE
+
+ /// Events that we have scheduled to run in the nearby future
+ var/list/scheduled_events = list()
+
+ /// For admins to force events (though they can still invoke them freely outside of the track system)
+ var/datum/round_event_control/forced_next_event
+
+ var/list/control = list() //list of all datum/round_event_control. Used for selecting events based on weight and occurrences.
+ var/list/running = list() //list of all existing /datum/round_event
+ var/list/currentrun = list()
+
+ /// List of all uncategorized events, because they were wizard or holiday events
+ var/list/uncategorized = list()
+
+ var/list/holidays //List of all holidays occuring today or null if no holidays
+
+ /// Event frequency multiplier, it exists because wizard, eugh.
+ var/event_frequency_multiplier = 1
+
+ /// Current preview page for the statistics UI.
+ var/statistics_track_page = EVENT_TRACK_MUNDANE
+ /// Page of the UI panel.
+ var/panel_page = UNIFIED_PANEL_MAIN
+ /// Whether we are viewing the roundstart events or not
+ var/roundstart_event_view = TRUE
+
+ /// Whether the gamemode has been halted
+ var/halted = FALSE
+
+ /// Ready players for roundstart events.
+ var/ready_players = 0
+ var/active_players = 0
+ var/head_crew = 0
+ var/eng_crew = 0
+ var/sec_crew = 0
+ var/med_crew = 0
+ var/sci_crew = 0
+
+ var/wizardmode = FALSE
+
+ var/storyteller_voted = FALSE
+
+ /// Whether we account for currently filled jobs when scheduling events TODO add to config
+ var/allow_job_weighting = TRUE
+
+ // TODO add to config
+ var/antag_divisor = 8
+
+ /// % chance of having an antag created at roundstart
+ var/roundstart_event_chance = 40
+
+ /// List of all datum/round_event_control with roundstart=true.
+ var/list/roundstart_control = list()
+
+/datum/controller/subsystem/unified/Initialize(time, zlevel)
+ for(var/type in typesof(/datum/round_event_control))
+ var/datum/round_event_control/event = new type()
+ if(!event.typepath || !event.name || !event.valid_for_map())
+ continue //don't want this one! leave it for the garbage collector
+ if(event.roundstart)
+ roundstart_control += event
+ else
+ control += event //add it to the list of all events (controls)
+ getHoliday()
+
+ load_config_vars()
+ load_event_config_vars()
+
+/datum/controller/subsystem/unified/fire(resumed = FALSE)
+ if(!resumed)
+ src.currentrun = running.Copy()
+
+ ///Handle scheduled events
+ for(var/datum/scheduled_event/sch_event in scheduled_events)
+ if(world.time >= sch_event.start_time)
+ sch_event.try_fire()
+ else if(!sch_event.alerted_admins && world.time >= sch_event.start_time - 1 MINUTES)
+ ///Alert admins 1 minute before running and allow them to cancel or refund the event, once again.
+ sch_event.alerted_admins = TRUE
+ message_admins("Scheduled Event: [sch_event.event] will run in [(sch_event.start_time - world.time) / 10] seconds. (CANCEL) (REFUND)")
+
+ if(!halted)
+ for(var/i in 1 to cooldown_dates.len)
+ if(cooldown_dates[i] <= world.time)
+ buy_event(control, i)
+
+ //cache for sanic speed (lists are references anyways)
+ var/list/currentrun = src.currentrun
+
+ while(currentrun.len)
+ var/datum/thing = currentrun[currentrun.len]
+ currentrun.len--
+ if(thing)
+ thing.process(wait * 0.1)
+ else
+ running.Remove(thing)
+ if (MC_TICK_CHECK)
+ return
+
+/datum/controller/subsystem/unified/proc/buy_event(list/events, cooldown_num = 0, run_now = FALSE)
+ . = FALSE
+
+ var/datum/round_event_control/picked_event
+ var/candidate_events = events
+ var/player_pop = SSunified.get_correct_popcount()
+ calculate_weights(candidate_events)
+ calculate_costs(candidate_events)
+ var/list/valid_events = list()
+ // Determine which events are valid to pick
+
+ for(var/datum/round_event_control/event as anything in candidate_events)
+ if(isnull(event))
+ continue
+ if(event.unified_cost == 0) // free events are handled in handle_free_events
+ continue
+ if(event.can_spawn_event(player_pop))
+ valid_events[event] = event.calculated_weight
+ if(!length(valid_events))
+ if(SSticker.HasRoundStarted())
+ message_admins("Unified failed to pick an event. Suspending.")
+ log_admin("Unified failed to pick an event. Suspending.")
+ halted = TRUE;
+ return
+ picked_event = pick_weight(valid_events)
+ if(!picked_event)
+ message_admins("WARNING: Unified picked a null from event pool. Aborting event roll.")
+ log_admin("WARNING: Unified picked a null from event pool. Aborting event roll.")
+ stack_trace("WARNING: Unified picked a null from event pool.")
+ return
+
+ points -= picked_event.calculated_cost // we already know that valid events cost less than our current points
+ message_admins("Unified purchased and triggered [picked_event] event for [picked_event.calculated_cost] cost.")
+ log_admin("Unified purchased and triggered [picked_event] event for [picked_event.calculated_cost] cost.")
+ if(picked_event.roundstart || run_now)
+ TriggerEvent(picked_event)
+ else
+ schedule_event(picked_event, 3 MINUTES, picked_event.calculated_cost) // We already randomize cooldown, we don't need to randomize this
+ if(cooldown_num)
+ var/cooldown // in minutes
+ if(picked_event.cooldown_override)
+ cooldown = rand(picked_event.cooldown_override*0.5, picked_event.cooldown_override*1.5)
+ else
+ cooldown = rand(picked_event.calculated_cost*0.5, picked_event.calculated_cost*1.5)
+ cooldown *= COOLDOWN_MULT // the higher the starting points, the lower the cooldown
+ cooldown_dates[cooldown_num] = world.time + cooldown MINUTES // convert cooldown to deciseconds
+
+ . = TRUE
+
+/datum/controller/subsystem/unified/proc/calculate_costs(list/events)
+ for(var/datum/round_event_control/event in events)
+ var/total_cost = event.unified_cost
+ /*
+ if(allow_job_weighting)
+ var/job_cost = 1
+ if((TAG_ENGINEERING in event.tags) && eng_crew == 0)
+ job_cost *= 2
+ if((TAG_MEDICAL in event.tags) && med_crew == 0)
+ job_cost *= 2
+ if((TAG_SECURITY in event.tags) && sec_crew == 0)
+ job_cost *= 2
+ if((TAG_SCIENCE in event.tags) && sci_crew == 0)
+ job_cost *= 2
+ total_cost *= job_cost
+ */
+ if(istype(event, /datum/round_event_control/antagonist))
+ total_cost /= get_antag_cap()
+ event.calculated_cost = total_cost
+
+/datum/controller/subsystem/unified/proc/calculate_weights(list/events)
+ for(var/datum/round_event_control/event in events)
+ var/weight_total = event.weight
+ if(allow_job_weighting)
+ if((TAG_ENGINEERING in event.tags) && eng_crew == 0)
+ weight_total = 0
+ else if((TAG_MEDICAL in event.tags) && med_crew == 0)
+ weight_total = 0
+ else if((TAG_SECURITY in event.tags) && sec_crew == 0)
+ weight_total = 0
+ else if((TAG_SCIENCE in event.tags) && sci_crew == 0)
+ weight_total = 0
+ /// Apply occurence multipliers if able
+ var/occurences = event.get_occurences()
+ if(occurences)
+ ///If the event has occured already, apply a penalty multiplier based on amount of occurences
+ weight_total *= event.reoccurence_penalty_multiplier ** occurences
+ /// Write it
+ event.calculated_weight = weight_total
+
+/// Gets the number of antagonists the antagonist injection events will stop rolling after.
+/datum/controller/subsystem/unified/proc/get_antag_cap()
+ return round(max(min(get_correct_popcount() / antag_divisor + sec_crew,sec_crew*1.5),ANTAG_CAP_FLAT))
+
+/// Whether events can inject more antagonists into the round
+/datum/controller/subsystem/unified/proc/can_inject_antags()
+ return (get_antag_cap() > length(GLOB.antagonists))
+
+/// Gets candidates for antagonist roles.
+
+/// Todo: Split into get_candidates and post_get_candidates
+/datum/controller/subsystem/unified/proc/get_candidates(
+ special_role_flag,
+ pick_observers,
+ pick_roundstart_players,
+ required_time,
+ inherit_required_time = TRUE,
+ no_antags = TRUE,
+ list/restricted_roles,
+ )
+
+
+ var/list/candidates = list()
+ var/list/candidate_candidates = list() //lol
+ if(pick_roundstart_players)
+ for(var/mob/dead/new_player/player in GLOB.new_player_list)
+ if(player.ready == PLAYER_READY_TO_PLAY && player.mind && player.check_preferences())
+ candidate_candidates += player
+ else if(pick_observers)
+ for(var/mob/player as anything in GLOB.dead_mob_list)
+ candidate_candidates += player
+ else
+ for(var/datum/record/locked/manifest_log as anything in GLOB.manifest.locked)
+ var/datum/mind/player_mind = manifest_log.mind_ref.resolve()
+ var/mob/living/player = player_mind.current
+ if(isnull(player))
+ continue
+ candidate_candidates += player
+
+
+ for(var/mob/candidate as anything in candidate_candidates)
+ if(QDELETED(candidate) || !candidate.key || !candidate.client || !candidate.mind)
+ continue
+ if(no_antags && candidate.mind.special_role)
+ continue
+ if(restricted_roles && (candidate.mind.assigned_role.title in restricted_roles))
+ continue
+ if(special_role_flag)
+ if(!(candidate.client.prefs) || !(special_role_flag in candidate.client.prefs.be_special))
+ continue
+
+ var/time_to_check
+ if(required_time)
+ time_to_check = required_time
+ else if (inherit_required_time)
+ time_to_check = GLOB.special_roles[special_role_flag]
+
+ if(time_to_check && candidate.client.get_remaining_days(time_to_check) > 0)
+ continue
+
+ if(special_role_flag && is_banned_from(candidate.ckey, list(special_role_flag, ROLE_SYNDICATE)))
+ continue
+ if(is_banned_from(candidate.client.ckey, BAN_ANTAGONIST))
+ continue
+ if(!candidate.client?.prefs?.read_preference(/datum/preference/toggle/be_antag))
+ continue
+ candidates += candidate
+ return candidates
+
+/// Gets the correct popcount, returning READY people if roundstart, and active people if not.
+/datum/controller/subsystem/unified/proc/get_correct_popcount()
+ if(SSticker.HasRoundStarted())
+ update_crew_infos()
+ return active_players
+ else
+ update_ready_crew_infos()
+ return ready_players
+
+/// Refunds and removes a scheduled event
+/datum/controller/subsystem/unified/proc/refund_scheduled_event(datum/scheduled_event/refunded)
+ points += refunded.cost
+ remove_scheduled_event(refunded)
+
+/// Refunds a failed event
+/datum/controller/subsystem/unified/proc/refund_failed_event(datum/round_event_control/failed)
+ points += failed.calculated_cost
+
+/// Schedules an event.
+/datum/controller/subsystem/unified/proc/force_event(datum/round_event_control/event)
+ forced_next_event = event
+
+/// Removes a scheduled event.
+/datum/controller/subsystem/unified/proc/remove_scheduled_event(datum/scheduled_event/removed)
+ scheduled_events -= removed
+ qdel(removed)
+
+/// Because roundstart events need 2 steps of firing for purposes of antags, here is the first step handled, happening before occupation division.
+/datum/controller/subsystem/unified/proc/handle_pre_setup_roundstart_events()
+ if(halted)
+ message_admins("WARNING: Didn't roll any events (including antagonists) due to Unified being halted.")
+ return
+ for(var/i in 1 to get_antag_cap())
+ if(prob(roundstart_event_chance))
+ buy_event(roundstart_control)
+
+/// Second step of handlind roundstart events, happening after people spawn.
+/datum/controller/subsystem/unified/proc/handle_post_setup_roundstart_events()
+ /// Start all roundstart events on post_setup immediately
+ for(var/datum/round_event/event as anything in running)
+ if(!event.control.roundstart)
+ continue
+ ASYNC
+ event.try_start()
+
+/// Schedules an event to run later.
+/datum/controller/subsystem/unified/proc/schedule_event(datum/round_event_control/passed_event, passed_time, passed_cost, passed_ignore, passed_announce)
+ var/datum/scheduled_event/scheduled = new (passed_event, world.time + passed_time, passed_cost, passed_ignore, passed_announce)
+ message_admins("Event: [passed_event] has been scheduled to run in [passed_time / 10] seconds. (CANCEL) (REFUND)")
+ scheduled_events += scheduled
+
+/datum/controller/subsystem/unified/proc/update_crew_infos()
+ // Very similar logic to `get_active_player_count()`
+ active_players = 0
+ head_crew = 0
+ eng_crew = 0
+ med_crew = 0
+ sec_crew = 0
+ sci_crew = 0
+ for(var/mob/player_mob as anything in GLOB.player_list)
+ if(!player_mob.client)
+ continue
+ if(player_mob.stat) //If they're alive
+ continue
+ if(player_mob.client.is_afk()) //If afk
+ continue
+ if(!ishuman(player_mob))
+ continue
+ active_players++
+ if(player_mob.mind?.assigned_role)
+ var/datum/job/player_role = player_mob.mind.assigned_role
+ if(player_role.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND)
+ head_crew++
+ if(player_role.departments_bitflags & DEPARTMENT_BITFLAG_ENGINEERING)
+ eng_crew++
+ if(player_role.departments_bitflags & DEPARTMENT_BITFLAG_MEDICAL)
+ med_crew++
+ if(player_role.departments_bitflags & DEPARTMENT_BITFLAG_SECURITY)
+ sec_crew++
+ if(player_role.departments_bitflags & DEPARTMENT_BITFLAG_SCIENCE)
+ sci_crew++
+ // update_pop_scaling() TODO FIX
+
+/datum/controller/subsystem/unified/proc/update_ready_crew_infos()
+ // Very similar logic to `get_active_player_count()`
+ ready_players = 0
+ head_crew = 0
+ eng_crew = 0
+ med_crew = 0
+ sec_crew = 0
+ sci_crew = 0
+ var/list/players = list()
+
+ //This fills the readied players list that the job estimation panel uses.
+ for(var/mob/dead/new_player/player as anything in GLOB.new_player_list)
+ if(player.ready == PLAYER_READY_TO_PLAY)
+ players[player.key] = player
+ ready_players++
+ ready_players++
+ sortTim(players, GLOBAL_PROC_REF(cmp_text_asc))
+
+ for(var/ckey in players)
+ var/mob/dead/new_player/player = players[ckey]
+ var/datum/preferences/prefs = player.client?.prefs
+ var/datum/job/J = prefs?.get_highest_priority_job()
+ if(J.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND)
+ head_crew++
+ if(J.departments_bitflags & DEPARTMENT_BITFLAG_ENGINEERING)
+ eng_crew++
+ if(J.departments_bitflags & DEPARTMENT_BITFLAG_MEDICAL)
+ med_crew++
+ if(J.departments_bitflags & DEPARTMENT_BITFLAG_SECURITY)
+ sec_crew++
+ if(J.departments_bitflags & DEPARTMENT_BITFLAG_SCIENCE)
+ sci_crew++
+
+/* TODO FIX
+/datum/controller/subsystem/unified/proc/update_pop_scaling()
+ for(var/track in event_tracks)
+ var/low_pop_bound = min_pop_thresholds[track]
+ var/high_pop_bound = pop_scale_thresholds[track]
+ var/scale_penalty = pop_scale_penalties[track]
+
+ var/perceived_pop = min(max(low_pop_bound, active_players), high_pop_bound)
+
+ var/divisor = high_pop_bound - low_pop_bound
+ /// If the bounds are equal, we'd be dividing by zero or worse, if upper is smaller than lower, we'd be increasing the factor, just make it 1 and continue.
+ /// this is only a problem for bad configs
+ if(divisor <= 0)
+ current_pop_scale_multipliers[track] = 1
+ continue
+ var/scalar = (perceived_pop - low_pop_bound) / divisor
+ var/penalty = scale_penalty - (scale_penalty * scalar)
+ var/calculated_multiplier = 1 - (penalty / 100)
+
+ current_pop_scale_multipliers[track] = calculated_multiplier
+*/
+
+/datum/controller/subsystem/unified/proc/TriggerEvent(datum/round_event_control/event)
+ . = event.preRunEvent()
+ if(. == EVENT_CANT_RUN)//we couldn't run this event for some reason, set its max_occurrences to 0
+ event.max_occurrences = 0
+ else if(. == EVENT_READY)
+ event.run_event(random = TRUE) // fallback to dynamic
+
+///Resets frequency multiplier.
+/datum/controller/subsystem/unified/proc/resetFrequency()
+ event_frequency_multiplier = 1
+
+
+//////////////
+// HOLIDAYS //
+//////////////
+//Uncommenting ALLOW_HOLIDAYS in config.txt will enable holidays
+
+//It's easy to add stuff. Just add a holiday datum in code/modules/holiday/holidays.dm
+//You can then check if it's a special day in any code in the game by doing if(SSunified.holidays["Groundhog Day"])
+
+//You can also make holiday random events easily thanks to Pete/Gia's system.
+//simply make a random event normally, then assign it a holidayID string which matches the holiday's name.
+//Anything with a holidayID, which isn't in the holidays list, will never occur.
+
+//Please, Don't spam stuff up with stupid stuff (key example being april-fools Pooh/ERP/etc),
+//And don't forget: CHECK YOUR CODE!!!! We don't want any zero-day bugs which happen only on holidays and never get found/fixed!
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+//ALSO, MOST IMPORTANTLY: Don't add stupid stuff! Discuss bonus content with Project-Heads first please!//
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+
+//sets up the holidays and holidays list
+/datum/controller/subsystem/unified/proc/getHoliday()
+ if(!CONFIG_GET(flag/allow_holidays))
+ return // Holiday stuff was not enabled in the config!
+ for(var/H in subtypesof(/datum/holiday))
+ var/datum/holiday/holiday = new H()
+ var/delete_holiday = TRUE
+ for(var/timezone in holiday.timezones)
+ var/time_in_timezone = world.realtime + timezone HOURS
+
+ var/YYYY = text2num(time2text(time_in_timezone, "YYYY")) // get the current year
+ var/MM = text2num(time2text(time_in_timezone, "MM")) // get the current month
+ var/DD = text2num(time2text(time_in_timezone, "DD")) // get the current day
+ var/DDD = time2text(time_in_timezone, "DDD") // get the current weekday
+
+ if(holiday.shouldCelebrate(DD, MM, YYYY, DDD))
+ holiday.celebrate()
+ LAZYSET(holidays, holiday.name, holiday)
+ delete_holiday = FALSE
+ break
+ if(delete_holiday)
+ qdel(holiday)
+
+ if(holidays)
+ holidays = shuffle(holidays)
+ // regenerate station name because holiday prefixes.
+ set_station_name(new_station_name())
+ world.update_status()
+
+/datum/controller/subsystem/unified/proc/toggleWizardmode()
+ wizardmode = !wizardmode //TODO: decide what to do with wiz events
+ message_admins("Summon Events has been [wizardmode ? "enabled, events will occur [SSunified.event_frequency_multiplier] times as fast" : "disabled"]!")
+ log_game("Summon Events was [wizardmode ? "enabled" : "disabled"]!")
+
+///Attempts to select players for special roles the mode might have.
+/datum/controller/subsystem/unified/proc/pre_setup()
+ // We need to do this to prevent some niche fuckery... and make dep. orders work. Lol
+ SSjob.reset_occupations()
+ handle_pre_setup_roundstart_events()
+ starting_points = rand(BASE_POINTS*0.5, BASE_POINTS*1.5)
+ points = starting_points
+ cooldown_dates[1] = world.time + STARTING_DELAY - SCHEDULE_DELAY
+ for(var/i in 2 to cooldown_dates.len)
+ cooldown_dates[i] = cooldown_dates[i-1] + rand(0, STARTING_DELAY)
+ log_game("Unified: Point budget is [starting_points], starting cooldown is [round((STARTING_DELAY - SCHEDULE_DELAY) / (1 MINUTES), 0.01)] minutes.")
+ message_admins("Unified: Point budget is [starting_points], starting cooldown is [round((STARTING_DELAY - SCHEDULE_DELAY) / (1 MINUTES), 0.01)] minutes.")
+ return TRUE
+
+///Everyone should now be on the station and have their normal gear. This is the place to give the special roles extra things
+/datum/controller/subsystem/unified/proc/post_setup(report) //Gamemodes can override the intercept report. Passing TRUE as the argument will force a report.
+ if(!report)
+ report = !CONFIG_GET(flag/no_intercept_report)
+ addtimer(CALLBACK(src, PROC_REF(display_roundstart_logout_report)), ROUNDSTART_LOGOUT_REPORT_TIME)
+
+ if(SSdbcore.Connect())
+ var/list/to_set = list()
+ var/arguments = list()
+ to_set += "game_mode = :game_mode"
+ arguments["game_mode"] = "Unified"
+ if(GLOB.revdata.originmastercommit)
+ to_set += "commit_hash = :commit_hash"
+ arguments["commit_hash"] = GLOB.revdata.originmastercommit
+ if(to_set.len)
+ arguments["round_id"] = GLOB.round_id
+ var/datum/db_query/query_round_game_mode = SSdbcore.NewQuery(
+ "UPDATE [format_table_name("round")] SET [to_set.Join(", ")] WHERE id = :round_id",
+ arguments
+ )
+ query_round_game_mode.Execute()
+ qdel(query_round_game_mode)
+ addtimer(CALLBACK(src, PROC_REF(send_trait_report)), rand(1 MINUTES, 5 MINUTES))
+ handle_post_setup_roundstart_events()
+ roundstart_event_view = FALSE
+ return TRUE
+
+
+///Handles late-join antag assignments
+/datum/controller/subsystem/unified/proc/make_antag_chance(mob/living/carbon/human/character)
+ return
+
+/datum/controller/subsystem/unified/proc/check_finished(force_ending) //to be called by SSticker
+ if(!SSticker.setup_done)
+ return FALSE
+ if(SSshuttle.emergency && (SSshuttle.emergency.mode == SHUTTLE_ENDGAME))
+ return TRUE
+ if(GLOB.station_was_nuked)
+ return TRUE
+ if(force_ending)
+ return TRUE
+
+//////////////////////////
+//Reports player logouts//
+//////////////////////////
+/datum/controller/subsystem/unified/proc/display_roundstart_logout_report()
+ var/list/msg = list("[span_boldnotice("Roundstart logout report")]\n\n")
+ for(var/i in GLOB.mob_living_list)
+ var/mob/living/L = i
+ var/mob/living/carbon/C = L
+ var/mob/living/carbon/human/H = C
+ if (istype(C) && !C.last_mind)
+ continue // never had a client
+
+ if(L.ckey && !GLOB.directory[L.ckey])
+ msg += "[L.name] ([L.key]), the [L.job] (Disconnected)\n"
+
+
+ if(L.ckey && L.client)
+ var/failed = FALSE
+ if(L.client.inactivity >= (ROUNDSTART_LOGOUT_REPORT_TIME / 2)) //Connected, but inactive (alt+tabbed or something)
+ msg += "[L.name] ([L.key]), the [L.job] (Connected, Inactive)\n"
+ failed = TRUE //AFK client
+ if(!failed && L.stat)
+ if(H.get_dnr()) //Suicider
+ msg += "[L.name] ([L.key]), the [L.job] ([span_boldannounce("Suicide")])\n"
+ failed = TRUE //Disconnected client
+ if(!failed && (L.stat == UNCONSCIOUS || L.stat == HARD_CRIT))
+ msg += "[L.name] ([L.key]), the [L.job] (Dying)\n"
+ failed = TRUE //Unconscious
+ if(!failed && L.stat == DEAD)
+ msg += "[L.name] ([L.key]), the [L.job] (Dead)\n"
+ failed = TRUE //Dead
+
+ continue //Happy connected client
+ for(var/mob/dead/observer/D in GLOB.dead_mob_list)
+ if(D.mind && D.mind.current == L)
+ if(L.stat == DEAD)
+ if(H.get_dnr()) //Suicider
+ msg += "[L.name] ([ckey(D.mind.key)]), the [L.job] ([span_boldannounce("Suicide")])\n"
+ continue //Disconnected client
+ else
+ msg += "[L.name] ([ckey(D.mind.key)]), the [L.job] (Dead)\n"
+ continue //Dead mob, ghost abandoned
+ else
+ if(D.can_reenter_corpse)
+ continue //Adminghost, or cult/wizard ghost
+ else
+ msg += "[L.name] ([ckey(D.mind.key)]), the [L.job] ([span_boldannounce("Ghosted")])\n"
+ continue //Ghosted while alive
+
+
+ for (var/C in GLOB.admins)
+ to_chat(C, msg.Join())
+
+//Set result and news report here
+/datum/controller/subsystem/unified/proc/set_round_result()
+ SSticker.mode_result = "undefined"
+ if(GLOB.station_was_nuked)
+ SSticker.news_report = STATION_DESTROYED_NUKE
+ if(EMERGENCY_ESCAPED_OR_ENDGAMED)
+ SSticker.news_report = STATION_EVACUATED
+ if(SSshuttle.emergency.is_hijacked())
+ SSticker.news_report = SHUTTLE_HIJACK
+
+/// Loads json event config values from events.txt
+/datum/controller/subsystem/unified/proc/load_event_config_vars()
+ var/json_file = file("[global.config.directory]/events.json")
+ if(!fexists(json_file))
+ return
+ var/list/decoded = json_decode(file2text(json_file))
+ for(var/event_text_path in decoded)
+ var/event_path = text2path(event_text_path)
+ var/datum/round_event_control/event
+ for(var/datum/round_event_control/iterated_event as anything in control)
+ if(iterated_event.type == event_path)
+ event = iterated_event
+ break
+ if(!event)
+ continue
+ var/list/var_list = decoded[event_text_path]
+ for(var/variable in var_list)
+ var/value = var_list[variable]
+ switch(variable)
+ if("weight")
+ event.weight = value
+ if("min_players")
+ event.min_players = value
+ if("max_occurrences")
+ event.max_occurrences = value
+ if("earliest_start")
+ event.earliest_start = value * (1 MINUTES)
+ if("cost")
+ event.cost = value
+ if("reoccurence_penalty_multiplier")
+ event.reoccurence_penalty_multiplier = value
+ if("shared_occurence_type")
+ if(!isnull(value))
+ value = text2path(value)
+ event.shared_occurence_type = value
+
+/// Loads config values from game_options.txt
+/datum/controller/subsystem/unified/proc/load_config_vars()
+ allow_pop_scaling = CONFIG_GET(flag/allow_storyteller_pop_scaling)
+
+/// Panel containing information, variables and controls about the gamemode and scheduled event
+/datum/controller/subsystem/unified/proc/admin_panel(mob/user)
+ update_crew_infos()
+ var/list/dat = list()
+ var/active_pop = get_correct_popcount()
+ dat += " HALT Unified Event Panel Refresh"
+ dat += "
Active Players: [active_pop] (Head: [head_crew], Sec: [sec_crew], Eng: [eng_crew], Med: [med_crew]) - Antag Cap: [get_antag_cap()]"
+ dat += "
"
+ dat += "Main"
+ dat += " Variables"
+ dat += "
"
+ switch(panel_page)
+ if(UNIFIED_PANEL_VARIABLES)
+ /*
+ dat += "Reload Config Vars Configs located in game_options.txt."
+ dat += "
Point Gains Multipliers (only over time):"
+ dat += "
This affects points gained over time towards scheduling new events of the tracks."
+ for(var/track in event_tracks)
+ dat += "
[track]: [point_gain_multipliers[track]]"
+ dat += "
"
+
+ dat += "Roundstart Points Multipliers:"
+ dat += "
This affects points generated for roundstart events and antagonists."
+ for(var/track in event_tracks)
+ dat += "
[track]: [roundstart_point_multipliers[track]]"
+ dat += "
"
+ */
+ /* TODO FIX
+ dat += "Minimum Population for Tracks:"
+ dat += "
This are the minimum population caps for events to be able to run."
+ for(var/track in event_tracks)
+ dat += "
[track]: [min_pop_thresholds[track]]"
+ dat += "
"
+
+ dat += "Point Thresholds:"
+ dat += "
Those are thresholds the tracks require to reach with points to make an event."
+ for(var/track in event_tracks)
+ dat += "
[track]: [point_thresholds[track]]"
+ */
+ if(UNIFIED_PANEL_MAIN)
+ var/background_cl = "#23273C"
+ dat += "Point Budget:
"
+ dat += "[points]/[starting_points]"
+ dat += "Cooldowns:
"
+ for(var/i in 1 to cooldown_dates.len)
+ dat += "[max(0, round((cooldown_dates[i] - world.time) / (1 MINUTES), 0.01))] minutes Reset Cooldown
"
+
+ dat += "Scheduled Events:
"
+ dat += ""
+ dat += ""
+ dat += "| Name | "
+ dat += "Cost | "
+ dat += "Time | "
+ dat += "Actions | "
+ dat += "
"
+ var/sorted_scheduled = list()
+ for(var/datum/scheduled_event/scheduled as anything in scheduled_events)
+ sorted_scheduled[scheduled] = scheduled.start_time
+ sortTim(sorted_scheduled, cmp=/proc/cmp_numeric_asc, associative = TRUE)
+ var/even = TRUE
+ for(var/datum/scheduled_event/scheduled as anything in sorted_scheduled)
+ even = !even
+ background_cl = even ? "#17191C" : "#23273C"
+ dat += ""
+ dat += "| [scheduled.event.name] | " //Name
+ dat += "[scheduled.event.calculated_cost] | " //Cost
+ var/time = "[(scheduled.start_time - world.time) / (1 SECONDS)] s."
+ dat += "[time] | " //Time
+ dat += "[scheduled.get_href_actions()] | " //Actions
+ dat += "
"
+ dat += "
"
+
+ dat += "Running Events:
"
+ dat += ""
+ dat += ""
+ dat += "| Name | "
+ dat += "Actions | "
+ dat += "
"
+ even = TRUE
+ for(var/datum/round_event/event as anything in running)
+ even = !even
+ background_cl = even ? "#17191C" : "#23273C"
+ dat += ""
+ dat += "| [event.control.name] | " //Name
+ dat += "-TBA- | " //Actions
+ dat += "
"
+ dat += "
"
+
+ var/datum/browser/popup = new(user, "unified_admin_panel", "Unified Panel", 670, 650)
+ popup.set_content(dat.Join())
+ popup.open()
+
+ /// Panel containing information and actions regarding events
+/datum/controller/subsystem/unified/proc/event_panel(mob/user)
+ var/list/dat = list()
+ dat += "
Roundstart Events"
+ dat += "
Avg. event intervals: "
+ dat += "
"
+ /// Create event info and stats table
+ dat += ""
+ dat += ""
+ dat += "| Name | "
+ dat += "Tags | "
+ dat += "Base Cost | "
+ dat += "M.Pop | "
+ dat += "M.Time | "
+ dat += "Can Occur | "
+ dat += "Weight | "
+ dat += "Actions | "
+ dat += "
"
+ var/even = TRUE
+ var/total_weight = 0
+ var/list/assoc_spawn_weight = list()
+ var/active_pop = get_correct_popcount()
+ for(var/datum/round_event_control/event as anything in (control + roundstart_control))
+ if(event.roundstart != roundstart_event_view)
+ continue
+ if(event.can_spawn_event(active_pop))
+ total_weight += event.calculated_weight
+ assoc_spawn_weight[event] = event.calculated_weight
+ else
+ assoc_spawn_weight[event] = 0
+ sortTim(assoc_spawn_weight, cmp=/proc/cmp_numeric_dsc, associative = TRUE)
+ for(var/datum/round_event_control/event as anything in assoc_spawn_weight)
+ even = !even
+ var/background_cl = even ? "#17191C" : "#23273C"
+ dat += ""
+ dat += "| [event.name] | " //Name
+ dat += "" //Tags
+ for(var/tag in event.tags)
+ dat += "[tag] "
+ dat += " | "
+ dat += "[event.unified_cost] | " //Cost
+ dat += "[event.min_players] | " //Minimum pop
+ dat += "[event.earliest_start / (1 MINUTES)] m. | " //Minimum time
+ dat += "[assoc_spawn_weight[event] ? "Yes" : "No"] | " //Can happen?
+ var/weight_string = "([event.calculated_weight] /raw.[event.weight])"
+ if(assoc_spawn_weight[event])
+ var/percent = round((event.calculated_weight / total_weight) * 100)
+ weight_string = "[percent]% - [weight_string]"
+ dat += "[weight_string] | " //Weight
+ dat += "[event.get_href_actions()] | " //Actions
+ dat += "
"
+ dat += "
"
+ var/datum/browser/popup = new(user, "unified_event_panel", "Event Panel", 1000, 600)
+ popup.set_content(dat.Join())
+ popup.open()
+
+/datum/controller/subsystem/unified/Topic(href, href_list)
+ . = ..()
+ var/mob/user = usr
+ if(!check_rights(R_ADMIN))
+ return
+ switch(href_list["panel"])
+ if("main")
+ switch(href_list["action"])
+ if("reset_cooldown")
+ cooldown_dates[text2num(href_list["number"])] = world.time
+ if("halt_storyteller")
+ halted = !halted
+ message_admins("[key_name_admin(usr)] has [halted ? "HALTED" : "un-halted"] Unified.")
+ /* TODO FIX
+ if("vars")
+ var/track = href_list["track"]
+ switch(href_list["var"])
+ /*if("pts_multiplier")
+ var/new_value = input(usr, "New value:", "Set new value") as num|null
+ if(isnull(new_value) || new_value < 0)
+ return
+ message_admins("[key_name_admin(usr)] set point gain multiplier for [track] track to [new_value].")
+ point_gain_multipliers[track] = new_value
+ if("roundstart_pts")
+ var/new_value = input(usr, "New value:", "Set new value") as num|null
+ if(isnull(new_value) || new_value < 0)
+ return
+ message_admins("[key_name_admin(usr)] set roundstart pts multiplier for [track] track to [new_value].")
+ roundstart_point_multipliers[track] = new_value
+ */
+ if("min_pop")
+ var/new_value = input(usr, "New value:", "Set new value") as num|null
+ if(isnull(new_value) || new_value < 0)
+ return
+ message_admins("[key_name_admin(usr)] set minimum population for [track] track to [new_value].")
+ min_pop_thresholds[track] = new_value
+ if("pts_threshold")
+ var/new_value = input(usr, "New value:", "Set new value") as num|null
+ if(isnull(new_value) || new_value < 0)
+ return
+ message_admins("[key_name_admin(usr)] set point threshold of [track] track to [new_value].")
+ point_thresholds[track] = new_value
+ */
+ if("reload_config_vars")
+ message_admins("[key_name_admin(usr)] reloaded unified config vars.")
+ load_config_vars()
+ if("tab")
+ var/tab = href_list["tab"]
+ panel_page = tab
+ if("open_stats")
+ event_panel(user)
+ return
+ if("change_points")
+ var/new_points = input(usr, "New point budget:", "Change Points") as num|null
+ if(isnull(new_points))
+ return
+ points = new_points
+ if("change_starting_points")
+ var/new_starting_points = input(usr, "New starting point budget:", "Change Starting Points") as num|null
+ if(isnull(new_starting_points))
+ return
+ starting_points = new_starting_points
+ /* TODO FIX
+ if("track_action")
+ var/track = href_list["track"]
+ if(!(track in event_tracks))
+ return
+ switch(href_list["track_action"])
+ if("remove_forced")
+ if(forced_next_events[track])
+ var/datum/round_event_control/event = forced_next_events[track]
+ message_admins("[key_name_admin(usr)] removed forced event [event.name] from track [track].")
+ forced_next_events -= track
+ if("set_pts")
+ var/set_pts = input(usr, "New point amount ([point_thresholds[track]]+ invokes event):", "Set points for [track]") as num|null
+ if(isnull(set_pts))
+ return
+ event_track_points[track] = set_pts
+ message_admins("[key_name_admin(usr)] set points of [track] track to [set_pts].")
+ log_admin_private("[key_name(usr)] set points of [track] track to [set_pts].")
+ if("next_event")
+ message_admins("[key_name_admin(usr)] invoked next event for [track] track.")
+ log_admin_private("[key_name(usr)] invoked next event for [track] track.")
+ event_track_points[track] = point_thresholds[track]
+ if(storyteller)
+ storyteller.handle_tracks()
+ */
+ admin_panel(user)
+ if("stats")
+ switch(href_list["action"])
+ if("set_roundstart")
+ roundstart_event_view = !roundstart_event_view
+ if("set_cat")
+ var/new_category = href_list["cat"]
+ if(new_category in EVENT_PANEL_TRACKS)
+ statistics_track_page = new_category
+ event_panel(user)
diff --git a/tgstation.dme b/tgstation.dme
index c8d99a0fbacd..c4d82125eb05 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -9444,12 +9444,17 @@
#include "modular_zubbers\master_files\skyrat\modules\deforest_medical_items\code\cargo_packs.dm"
#include "modular_zubbers\master_files\skyrat\modules\opposing_force\code\opposing_force_subsystem.dm"
#include "modular_zubbers\master_files\skyrat\modules\verbs\code\subtle.dm"
+#include "modular_zzbug\code\__DEFINES\unified_defines.dm"
#include "modular_zzbug\code\game\objects\items\plushes.dm"
#include "modular_zzbug\code\modules\client\preferences\preferences.dm"
#include "modular_zzbug\code\modules\events\appendicitis.dm"
#include "modular_zzbug\code\modules\mob\dead\new_player\body_markings.dm"
#include "modular_zzbug\code\modules\research\designs\weapon_designs.dm"
#include "modular_zzbug\code\modules\research\techweb\all_nodes.dm"
+#include "modular_zzbug\code\modules\unified\_event.dm"
+#include "modular_zzbug\code\modules\unified\divergency_report.dm"
+#include "modular_zzbug\code\modules\unified\event_overrides.dm"
+#include "modular_zzbug\code\modules\unified\unified.dm"
#include "modular_zzbug\master_files\code\modules\client\examine_tgui.dm"
#include "modular_zzbug\master_files\code\modules\research\techweb\all_nodes.dm"
// END_INCLUDE