Files
Paradise/code/controllers/subsystem/SSticker.dm
S34N 22fbd9aced TGUIv4 - Core backend updates, TGchat, Asset cache updates (#23726)
* TGUI upgrade project: Port initial TGUI 4.0 changes from TG (#23440)

* tgui4.0

* bugfix for un-interaction

* fix for input not resetting on close

* NTOS restore

* fix all interfaces having scrollbars, fix colours

* bundle update

* dep bumps

* bumps the deps as much as possible

* button regression fix

* TGUI test map rev 1

* fix theme and some component regression

* fix login screen regression

* fixes regression with uplink cart

* bundle

* fix regressions

* fix the input issue, again

* regression fixes, stylesheet edition, hash restore

* fixes GPS BSOD

* draggable control regression

* dev server dep regression

* byondUI regression fix

* section regression fix

* fix secure storage weirdness

* Fixed mining vendor scrolling

* Arthri review pass 1

* mining vendor double-scroll

* fix for RPD, AirAlarm, and Radio

* arthri review pass 2

* arthri review pass 3

* sanitize var setting, fix colours, delete unused file

* adds CI to branch

* god damn AI making random changes

* I should remember to compile when changes are made

* fixes scrollbar issues

* fix camera console, fix flexGrow for sections

* CI fix

* nanomap fix

* Update code/modules/atmospherics/machinery/airalarm.dm

* restore margin

* style semicolon

* TGUIv4 - Moves TGUI Window Sizing from DM to JS (#23524)

* Initial commit

* Transferring the remaining windows to JS

* Resizing part 1

* Resizing part 2

* Some reverts and polish

* Forgot that

* undeployed nuke window size

---------

Co-authored-by: Aylong <alexanderkitsa@gmail.com>

* TGUIv4: Remove force_open (#23537)

* First state fixes

* Removes "force_open"

* TGUIv4 - TGUI Version 4.1 (#23547)

* Reduced Logging Changes

* setSharedState early return for non-interactive UI

* TGUI version bump to 4.1

* Fix sending updates for non-interactive UIs

* Scalablue UI Stylesheet Changes

* CSS Tgui Bundle

* JavaScript TGUI Style Changes

* Update tgui/packages/tgui/styles/base.scss

Co-authored-by: Aylong <69762909+Aylong220@users.noreply.github.com>

* Additional changes to Input and Button scalability

* TGUI logging proc update

* Indentation fix for logging proc

* Update code/__HELPERS/_logging.dm

Co-authored-by: S34N <12197162+S34NW@users.noreply.github.com>

* Additional log_tgui changes

---------

Co-authored-by: Aylong <69762909+Aylong220@users.noreply.github.com>
Co-authored-by: S34N <12197162+S34NW@users.noreply.github.com>

* dev server hotfix (#23584)

* ui_interact object constructor argument check (#23594)

* feat: add smart asset cache, add CDN support for assets, properly group assets (#23585)

* feat: add smart asset cache, add CDN support for assets, properly group assets

* fix: make tgui actually work

* fix: keeping local name for `tgui.bundle.js` and `tgui.bundle.css` for debug and dev server functioning

* fix: make `fontawesome` assets finally work

* fix: make proper identation for `if`

* fix: add `resolveAsset` to `NanoMap.js`

* refactor: update `claw_game` and `chess` to new asset framework

* refactor/tweak: don't use string concatenation for browser `content` and `head_content`, use list instead. Don't use `common.css` for paper UI, keep local name for stamp image assets, replace hard ref of browser to `atom` (rare case) to `UID`

* refactor: remove redundant debug logs

* refactor: remove space betwee `if` and `(`

* refactor: remove one more redundant log, properly reload UI resources

* rafactor: change names of asset files

* fix: adjust existing UIs to properly use assets

* fix: properly pass args to to `onclose` proc

* Update code/modules/asset_cache/transports/asset_transport.dm

Co-authored-by: S34N <12197162+S34NW@users.noreply.github.com>

* Update code/modules/asset_cache/transports/asset_transport.dm

Co-authored-by: S34N <12197162+S34NW@users.noreply.github.com>

---------

Co-authored-by: S34N <12197162+S34NW@users.noreply.github.com>

* TGUI 4.2: TGchat + Refactor Asset Delivery + Yarn Berry (#23643)

* tgchat

* Little cleanup

* Refactor Asset Delivery (Part 2)

* Little tweaks

* fix code styling issues

* fix file name duplications

* Browser window options fix @gaxeer

* transfer valve fixes

* yarn berry initial

* vsc tasks

* yarn berry working

* node 20

* fix dev server (lol wrong yarn)

* bloody regressions

* fixes that damn scrolltracking bug

* Some tweaks and flexGrow deletions

It still doesn't work as it should, which makes me nervous

* remove unneeded deps

* 514 regression fix

* change stuff to not conflict with other servers

* name it as requested

---------

Co-authored-by: S34N <12197162+S34NW@users.noreply.github.com>

* revert test_TGUI map addition

* TGUI 4.3: Stack, Webpack 5 & UI Tweaks, Yarn 3.6.4, TS support, Jest (#23677)

* Stack & UI's Refactor

Almost final, polish required

Stack & UI's Refactor (Part 1)

Stack & UI's Refactor (Part 2)

Stack & UI's Refactor (Part 3)

* Prettier

Maybe bad

* ClearChat & some tweaks

* Adds a Chat Reliability Layer https://github.com/tgstation/tgstation/pull/79479

* Fix chat BSOD

https://github.com/tgstation/tgstation/pull/79821

* WebPack 5 (Didn't work)

I hate this shit

* Yarn 3.6.4

* make it all work

* revert snowflake fix

* Stories

* adds TS support

* re-enable test and prettier

* update yarn sdk's

* Fix some box regression

* ping/reply

* Fixes regressions and some things

* Zebra and Fix chat button transfer

+rebuild

* make VSC use the proper local typescript lib

* Popper Tooltips https://github.com/tgstation/tgstation/pull/58980

* Popper Tooltips performancy fixes

* Dropdown v2 https://github.com/tgstation/tgstation/pull/75164

Without Icon.tsx

* BB test map

* run build

---------

Co-authored-by: S34N <12197162+S34NW@users.noreply.github.com>

* we moved these

* change test values

* ignore scss files

* change hash function

* TGUIv4: TGchat fix traitor codeword highlighting (#23720)

* Codeword highlighting for TGUIv4

* null safeties

* Apply suggestions from code review

Co-authored-by: DGamerL <108773801+DGamerL@users.noreply.github.com>

---------

Co-authored-by: DGamerL <108773801+DGamerL@users.noreply.github.com>

* TGUIv4 - updates standalone build tooling (#23721)

* swap `xxhash64` for `md4`

* remove hash function

* remove old file

* TGUI 4.3+4.4: Newscaster fix, `inline-block` replacements and other fixes (#23722)

* TGUI 4.3 fixes

Newscaster fix + inline-blocks

* Be gone Box

* Mod Chat -> Mentor Chat

* Some warning filters

* Rebuild

* Delete tgui-common.bundle.js

* please work

* try it like this

* 4.3.1

* 4.3.2

* fix

* i forgot to build

* 4.4.0

* give me more diff details please so I can fix you

* its not binary pls git

* test without svgs

* removes an un-needed SVG file

* inlined SVGs test

* fix code styling

* comment these

---------

Co-authored-by: S34N <12197162+S34NW@users.noreply.github.com>

* TGUIv4 - Removes resizable and improves drag code (#23719)

* Initial commit

* bundle rebuild

* Fixes Radio Sizing

* Updates Radio sizing to fix wrapping issues

* fix dev server

---------

Co-authored-by: S34N <12197162+S34NW@users.noreply.github.com>

* fix hotkeys not being passed to BYOND

* Apply suggestions from code review

* bump deps

* forgot these deps

* ci fix

* ChemDispenser fixes and HoloControl cooldown

* Station Traits TGUI

+Rebuild

* CI

* fix CI for real this time

* I have no idea

* fix station trait panel

* Update code/controllers/subsystem/SSping.dm

Co-authored-by: DGamerL <108773801+DGamerL@users.noreply.github.com>

* Update code/controllers/subsystem/SSticker.dm

Co-authored-by: DGamerL <108773801+DGamerL@users.noreply.github.com>

* Update code/controllers/subsystem/SSping.dm

Co-authored-by: DGamerL <108773801+DGamerL@users.noreply.github.com>

* deterministic module IDs

* TGUI fixes. Again... (#23762)

* TGUI Fixes: Part 1

ORM icon for reinforced glass now showing correctly

Mining vendor moved to Stack

Exofab UI buttons descended from the heavens to the earth

* TGUI Fixes: Part 2

DestTagger now works, forgot import LabeledList

PowerMonitor graph displays correct

Request Console updated to 2.0, BUT, problem with message priority, it was there before the changes, maybe even before the project, I'll have to check it out

* TGUI Fixes: Part 2

Fixed `onEnter` input

Fixed Cloning Console storage (Im dumbass)

Some windows resizing

Fixed bad picture in security and medical records console

Fixed scrollable dropdown

Added translucent color for buttons

* Fucking uplink DONE, and Button.Input fix

* NT Recruiter and Uplink polish

* Del LabeledList from dest. tagger

* ChemMaster and OreRedemption log deletion

* Update test_tiny.dmm

* Translucent button story

* Filter for SQL error

* del: `log_debug` in `hear_say.dm`

* Some little fixes and SpecMenu Stack

* Panel.js: mx -> mr

* fix ghost spawners

* null check

---------

Co-authored-by: S34N <12197162+S34NW@users.noreply.github.com>

* fix material name in protolathe

* fix various UI interacts

* card machine runtime fix

* remove legacy folders

* TGUIv4 - TGchat Theme Adjustments (#23772)

* Initial commit

* Changes rule grouping

* Robot class style changes and webpack config

* Reverts webpack/font changes and subs robot font

* revert a state changed in error

* fix uninteractable UIs due to chunky fingers

* TGUIv4: More fixes, tweaks and fixes for tweaks (#23781)

* I fuckin hate PDA

* Vending

* Add `|` to highlight splitters and some light mode tweaks

* Teleporter rewrite

* APC and AiAirlock (God forgive me)

* clarify highlight syntax

---------

Co-authored-by: S34N <12197162+S34NW@users.noreply.github.com>

* TGUIv4 - Abductor Experiment Machine TGUI (#23776)

* Experiment Console TGUI

* Update for TGUIv4

* UI Size adjustment

* ui state and ghost attack update

* remove this

---------

Co-authored-by: S34N <12197162+S34NW@users.noreply.github.com>

* fix

* TGUIv4 - Adjustments, Fixes, and Tweaks (#23785)

* Photocopier layout adjustments

* Autolathe Text and Button spacing adjustment

* Hotkey fix for moving bug

* Removes rule sets for visited links

* Fixes ordering multiples on cargo console

* fix possible wonky json payloads

* fix admin log input list stuff

* TGUIv4: Reconnect fix and some other fixes + tweaks (#23790)

* Fix reconnecting and transfer it to TSX

* RPD polish

* RCD, AccessList, GuestPass

* Focus reset fix when camera switched. God forgive me again...

* Purple box and identation fixes

For votes and health scanner

* ChemMaster and ChemDispenser

ChemMaster - translucent pills buttons and no grow produce section
ChemDuspenser - dynamic height

* ShuttleManipulator Tabs

* RCD buttons

* make input bar mode more prominent

* fix chem master icons

* tab fix

* Properly sanitizes loaded messages in tgui chat

* TGUIv4 - Adjustments, Fixes, and Tweaks v2 (#23795)

* Fixes icon spacing for nanobank tabs

* Tweaks Dropdown styling

* Fixes sending stuff to old chat

* OpenDream TGUI fix

* Resizes Station Traits window

* Adjusts health analyzer messages for chat tabs

* Revert "Properly sanitizes loaded messages in tgui chat"

This reverts commit 4c32a7094a.

* Revert "Revert "Properly sanitizes loaded messages in tgui chat""

This reverts commit 24afa55922.

* Changes how tgui handles static data

* Reenable no-undef rule. Enable Format On Save for VSCode (#23803)

* Enable Format On Save for VSCode

* Re-enable and fix no-undef

---------

Co-authored-by: Arthri <41360489+a@users.noreply.github.com>

* pda scanner message improvements

* fix mentor/admin PMs being sent to the "unsorted" category

* fix garbled custom vote messages

* moves cyborg analyser to use chatboxes

* fixes regression

* Prevent F5 reload

* TGUIv4: Themes polish and some new ones + misc fixes (#23814)

* Little themes tweaks

* NTOS chat theme

* Update tgui-panel.bundle.js

* Capitalize themes

* Paradise theme v1.0

* Little StationTraitsPanel cleanup

* Update member_content.dmi

* NoCapitalize

* Rebuild

* Delete Paradise.scss

* Create paradise.scss

* Forgot

* Rewritten Security Records Console

For Jesus fucking christ...

* Vending and Wires sizes tweaks

* ThermoMachine

* Syndicate Theme

* Little darker

* Rewritten Medical Records Console

* PDA Nanobank fix

* Syndicate Theme Darker

* Rebuild TGUI

* Wires section grow fix

* fixes the enshittified paradise icon

* Sirryan+Warrior reviews

* bloody define comments

* comment correction

* unprivate these

* errant . begone (how did this not cause a compiler error)

* TGUIv4 - Adjustments, Fixes, and Tweaks v3 (#23807)

* Fixes ERT Manager silenced message function

* Adjusts progress bar on ExosuitFabricator

* Sets Dropdown selected to align left

* Removes the use of self_state select interfaces

* Exosuit Fabricator style adjustments

* fix sextractor UI

* remove empty tochat string

* fix human air alarm interactions

* fix modals

* add F12/IEChooser

---------

Co-authored-by: S34N <12197162+S34NW@users.noreply.github.com>

---------

Co-authored-by: Burzah <116982774+Burzah@users.noreply.github.com>
Co-authored-by: Aylong <alexanderkitsa@gmail.com>
Co-authored-by: Aylong <69762909+Aylong220@users.noreply.github.com>
Co-authored-by: Gaxeer <44334376+Gaxeer@users.noreply.github.com>
Co-authored-by: DGamerL <108773801+DGamerL@users.noreply.github.com>
Co-authored-by: Arthri <41360489+Arthri@users.noreply.github.com>
Co-authored-by: Arthri <41360489+a@users.noreply.github.com>
2024-01-21 19:20:35 +01:00

870 lines
33 KiB
Plaintext

SUBSYSTEM_DEF(ticker)
name = "Ticker"
init_order = INIT_ORDER_TICKER
priority = FIRE_PRIORITY_TICKER
flags = SS_KEEP_TIMING
runlevels = RUNLEVEL_LOBBY | RUNLEVEL_SETUP | RUNLEVEL_GAME
offline_implications = "The game is no longer aware of when the round ends. Immediate server restart recommended."
cpu_display = SS_CPUDISPLAY_LOW
wait = 1 SECONDS
/// Time the game should start, relative to world.time
var/round_start_time = 0
/// Time that the round started
var/time_game_started = 0
/// Default timeout for if world.Reboot() doesnt have a time specified
var/const/restart_timeout = 75 SECONDS
/// Current status of the game. See code\__DEFINES\game.dm
var/current_state = GAME_STATE_STARTUP
/// Do we want to force-start as soon as we can
var/force_start = FALSE
/// Do we want to force-end as soon as we can
var/force_ending = FALSE
/// Leave here at FALSE ! setup() will take care of it when needed for Secret mode -walter0o
var/hide_mode = FALSE
/// Our current game mode
var/datum/game_mode/mode = null
/// The current pick of lobby music played in the lobby
var/login_music
/// List of all minds in the game. Used for objective tracking
var/list/datum/mind/minds = list()
/// icon_state the chaplain has chosen for his bible
var/Bible_icon_state
/// item_state the chaplain has chosen for his bible
var/Bible_item_state
/// Name of the bible
var/Bible_name
/// Name of the bible deity
var/Bible_deity_name
/// Cult data. Here instead of cult for adminbus purposes
var/datum/cult_info/cultdat = null
/// If set to nonzero, ALL players who latejoin or declare-ready join will have random appearances/genders
var/random_players = FALSE
/// Did we broadcast the tip of the round yet?
var/tipped = FALSE
/// What will be the tip of the round?
var/selected_tip
/// This is used for calculations for the statpanel
var/pregame_timeleft
/// If set to TRUE, the round will not restart on it's own
var/delay_end = FALSE
/// Global holder for triple AI mode
var/triai = FALSE
/// Holder for inital autotransfer vote timer
var/next_autotransfer = 0
/// Used for station explosion cinematic
var/obj/screen/cinematic = null
/// Spam Prevention. Announce round end only once.
var/round_end_announced = FALSE
/// Is the ticker currently processing? If FALSE, roundstart is delayed
var/ticker_going = TRUE
/// Gamemode result (For things like cult or nukies which can end multiple ways)
var/mode_result = "undefined"
/// Server end state (Did we end properly or reboot or nuke or what)
var/end_state = "undefined"
/// Time the real reboot kicks in
var/real_reboot_time = 0
/// Datum used to generate the end of round scoreboard.
var/datum/scoreboard/score = null
/// List of ckeys who had antag rolling issues flagged
var/list/flagged_antag_rollers = list()
/datum/controller/subsystem/ticker/Initialize()
login_music = pick(\
'sound/music/thunderdome.ogg',\
'sound/music/space.ogg',\
'sound/music/title1.ogg',\
'sound/music/title2.ogg',\
'sound/music/title3.ogg',)
/datum/controller/subsystem/ticker/fire()
switch(current_state)
if(GAME_STATE_STARTUP)
// This is ran as soon as the MC starts firing, and should only run ONCE, unless startup fails
round_start_time = world.time + (GLOB.configuration.general.lobby_time SECONDS)
to_chat(world, "<B><span class='darkmblue'>Welcome to the pre-game lobby!</span></B>")
to_chat(world, "Please, setup your character and select ready. Game will start in [GLOB.configuration.general.lobby_time] seconds")
current_state = GAME_STATE_PREGAME
fire() // TG says this is a good idea
for(var/mob/new_player/N in GLOB.player_list)
if(N.client)
N.new_player_panel_proc() // to enable the observe option
if(GAME_STATE_PREGAME)
if(!SSticker.ticker_going) // This has to be referenced like this, and I dont know why. If you dont put SSticker. it will break
return
// This is so we dont have sleeps in controllers, because that is a bad, bad thing
if(!delay_end)
pregame_timeleft = max(0, round_start_time - world.time) // Normal lobby countdown when roundstart was not delayed
else
pregame_timeleft = max(0, pregame_timeleft - wait) // If roundstart was delayed, we should resume the countdown where it left off
if(pregame_timeleft <= 600 && !tipped) // 60 seconds
send_tip_of_the_round()
tipped = TRUE
if(pregame_timeleft <= 0 || force_start)
current_state = GAME_STATE_SETTING_UP
Master.SetRunLevel(RUNLEVEL_SETUP)
if(GAME_STATE_SETTING_UP)
if(!setup()) // Setup failed
current_state = GAME_STATE_STARTUP
Master.SetRunLevel(RUNLEVEL_LOBBY)
if(GAME_STATE_PLAYING)
delay_end = FALSE // reset this in case round start was delayed
mode.process()
if(world.time > next_autotransfer)
SSvote.start_vote(new /datum/vote/crew_transfer)
next_autotransfer = world.time + GLOB.configuration.vote.autotransfer_interval_time
var/game_finished = SSshuttle.emergency.mode >= SHUTTLE_ENDGAME || mode.station_was_nuked
if(GLOB.configuration.gamemode.disable_certain_round_early_end)
mode.check_finished() // some modes contain var-changing code in here, so call even if we don't uses result
else
game_finished |= mode.check_finished()
if(game_finished || force_ending)
current_state = GAME_STATE_FINISHED
if(GAME_STATE_FINISHED)
if(SSshuttle.emergency.mode >= SHUTTLE_ENDGAME && !mode.station_was_nuked)
event_blackbox(outcome = ROUND_END_CREW_TRANSFER)
current_state = GAME_STATE_FINISHED
Master.SetRunLevel(RUNLEVEL_POSTGAME) // This shouldnt process more than once, but you never know
auto_toggle_ooc(TRUE) // Turn it on
declare_completion()
addtimer(CALLBACK(src, PROC_REF(call_reboot)), 5 SECONDS)
// Start a map vote IF
// - Map rotate doesnt have a mode for today and map voting is enabled
// - Map rotate has a mode for the day and it ISNT full random
if(((!SSmaprotate.setup_done) && GLOB.configuration.vote.enable_map_voting) || (SSmaprotate.setup_done && (SSmaprotate.rotation_mode != MAPROTATION_MODE_FULL_RANDOM)))
SSvote.start_vote(new /datum/vote/map)
else
// Pick random map
var/list/pickable_types = list()
for(var/x in subtypesof(/datum/map))
var/datum/map/M = x
if(initial(M.voteable) && length(GLOB.clients) >= initial(M.min_players_random))
pickable_types += M
var/datum/map/target_map = pick(pickable_types)
SSmapping.next_map = new target_map
to_chat(world, "<span class='interface'>Map for next round: [SSmapping.next_map.fluff_name] ([SSmapping.next_map.technical_name])</span>")
/datum/controller/subsystem/ticker/proc/call_reboot()
if(mode.station_was_nuked)
reboot_helper("Station destroyed by Nuclear Device.", "nuke")
else
reboot_helper("Round ended.", "proper completion")
/datum/controller/subsystem/ticker/proc/setup()
cultdat = setupcult()
score = new()
// Create and announce mode
if(GLOB.master_mode == "secret")
hide_mode = TRUE
var/list/datum/game_mode/runnable_modes
if(GLOB.master_mode == "random" || GLOB.master_mode == "secret")
runnable_modes = GLOB.configuration.gamemode.get_runnable_modes()
if(!length(runnable_modes))
to_chat(world, "<B>Unable to choose playable game mode.</B> Reverting to pre-game lobby.")
force_start = FALSE
current_state = GAME_STATE_PREGAME
Master.SetRunLevel(RUNLEVEL_LOBBY)
return FALSE
if(GLOB.secret_force_mode != "secret")
var/datum/game_mode/M = GLOB.configuration.gamemode.pick_mode(GLOB.secret_force_mode)
if(M.can_start())
mode = GLOB.configuration.gamemode.pick_mode(GLOB.secret_force_mode)
SSjobs.ResetOccupations()
if(!mode)
mode = pickweight(runnable_modes)
if(mode)
var/mtype = mode.type
mode = new mtype
else
mode = GLOB.configuration.gamemode.pick_mode(GLOB.master_mode)
if(!mode.can_start())
to_chat(world, "<B>Unable to start [mode.name].</B> Not enough players, [mode.required_players] players needed. Reverting to pre-game lobby.")
mode = null
current_state = GAME_STATE_PREGAME
force_start = FALSE
SSjobs.ResetOccupations()
Master.SetRunLevel(RUNLEVEL_LOBBY)
return FALSE
// Randomise characters now. This avoids rare cases where a human is set as a changeling then they randomise to an IPC
for(var/mob/new_player/player in GLOB.player_list)
if(player.client.prefs.toggles2 & PREFTOGGLE_2_RANDOMSLOT)
player.client.prefs.load_random_character_slot(player.client)
// Lets check if people who ready should or shouldnt be
for(var/mob/new_player/P in GLOB.player_list)
// Not logged in
if(!P.client)
continue
// Not ready
if(!P.ready)
continue
// Not set to return if nothing available
if(P.client.prefs.active_character.alternate_option != RETURN_TO_LOBBY)
continue
var/has_antags = (length(P.client.prefs.be_special) > 0)
if(!P.client.prefs.active_character.check_any_job())
to_chat(P, "<span class='danger'>You have no jobs enabled, along with return to lobby if job is unavailable. This makes you ineligible for any round start role, please update your job preferences.</span>")
if(has_antags)
// We add these to a list so we can deal with them as a batch later
// A lot of DB tracking stuff needs doing, so we may as well async it
flagged_antag_rollers |= P.ckey
P.ready = FALSE
//Configure mode and assign player to special mode stuff
mode.pre_pre_setup()
var/can_continue = FALSE
can_continue = mode.pre_setup() //Setup special modes. This also does the antag fishing checks.
if(!can_continue)
QDEL_NULL(mode)
to_chat(world, "<B>Error setting up [GLOB.master_mode].</B> Reverting to pre-game lobby.")
current_state = GAME_STATE_PREGAME
force_start = FALSE
SSjobs.ResetOccupations()
Master.SetRunLevel(RUNLEVEL_LOBBY)
return FALSE
// Enable highpop slots just before we distribute jobs.
var/playercount = length(GLOB.clients)
var/highpop_trigger = 80
if(playercount >= highpop_trigger)
log_debug("Playercount: [playercount] versus trigger: [highpop_trigger] - loading highpop job config")
SSjobs.LoadJobs(TRUE)
else
log_debug("Playercount: [playercount] versus trigger: [highpop_trigger] - keeping standard job config")
SSjobs.DivideOccupations() //Distribute jobs
if(hide_mode)
var/list/modes = new
for(var/datum/game_mode/M in runnable_modes)
modes += M.name
modes = sortList(modes)
to_chat(world, "<B>The current game mode is - Secret!</B>")
to_chat(world, "<B>Possibilities:</B> [english_list(modes)]")
else
mode.announce()
// Behold, a rough way of figuring out what takes 10 years
var/watch = start_watch()
create_characters() // Create player characters and transfer clients
log_debug("Creating characters took [stop_watch(watch)]s")
// Gather everyones minds
for(var/mob/living/player in GLOB.player_list)
if(player.mind)
minds += player.mind
watch = start_watch()
equip_characters() // Apply outfits and loadouts to the characters
log_debug("Equipping characters took [stop_watch(watch)]s")
watch = start_watch()
GLOB.data_core.manifest() // Create the manifest
log_debug("Manifest creation took [stop_watch(watch)]s")
SEND_SIGNAL(src, COMSIG_TICKER_ROUND_STARTING, world.time)
// Update the MC and state to game playing
current_state = GAME_STATE_PLAYING
Master.SetRunLevel(RUNLEVEL_GAME)
// Generate the list of empty playable AI cores in the world
for(var/obj/effect/landmark/start/ai/A in GLOB.landmarks_list)
if(locate(/mob/living) in get_turf(A))
continue
GLOB.empty_playable_ai_cores += new /obj/structure/AIcore/deactivated(get_turf(A))
// Setup pregenerated newsfeeds
setup_news_feeds()
// Generate code phrases and responses
if(!GLOB.syndicate_code_phrase)
var/temp_syndicate_code_phrase = generate_code_phrase(return_list = TRUE)
var/codewords = jointext(temp_syndicate_code_phrase, "|")
var/regex/codeword_match = new("([codewords])", "ig")
GLOB.syndicate_code_phrase_regex = codeword_match
temp_syndicate_code_phrase = jointext(temp_syndicate_code_phrase, ", ")
GLOB.syndicate_code_phrase = temp_syndicate_code_phrase
if(!GLOB.syndicate_code_response)
var/temp_syndicate_code_response = generate_code_phrase(return_list = TRUE)
var/codewords = jointext(temp_syndicate_code_response, "|")
var/regex/codeword_match = new("([codewords])", "ig")
GLOB.syndicate_code_response_regex = codeword_match
temp_syndicate_code_response = jointext(temp_syndicate_code_response, ", ")
GLOB.syndicate_code_response = temp_syndicate_code_response
// Run post setup stuff
mode.post_setup()
// Delete starting landmarks (not AI ones because we need those for AI-ize)
for(var/obj/effect/landmark/start/S in GLOB.landmarks_list)
if(!istype(S, /obj/effect/landmark/start/ai))
qdel(S)
SSdbcore.SetRoundStart()
to_chat(world, "<span class='darkmblue'><B>Enjoy the game!</B></span>")
SEND_SOUND(world, sound('sound/AI/welcome.ogg'))
if(SSholiday.holidays)
to_chat(world, "<span class='darkmblue'>and...</span>")
for(var/holidayname in SSholiday.holidays)
var/datum/holiday/holiday = SSholiday.holidays[holidayname]
to_chat(world, "<h4>[holiday.greet()]</h4>")
GLOB.discord_manager.send2discord_simple_noadmins("**\[Info]** Round has started")
auto_toggle_ooc(FALSE) // Turn it off
time_game_started = world.time
// Sets the auto shuttle vote to happen after the config duration
next_autotransfer = world.time + GLOB.configuration.vote.autotransfer_initial_time
for(var/mob/new_player/N in GLOB.mob_list)
if(N.client)
N.new_player_panel_proc()
SSnightshift.check_nightshift(TRUE)
#ifdef UNIT_TESTS
// Run map tests first in case unit tests futz with map state
GLOB.test_runner.RunMap()
GLOB.test_runner.Run()
#endif
// Do this 10 second after roundstart because of roundstart lag, and make it more visible
addtimer(CALLBACK(src, PROC_REF(handle_antagfishing_reporting)), 10 SECONDS)
return TRUE
/datum/controller/subsystem/ticker/proc/station_explosion_cinematic(nuke_site = NUKE_SITE_ON_STATION, override = null)
if(cinematic)
return //already a cinematic in progress!
auto_toggle_ooc(TRUE) // Turn it on
//initialise our cinematic screen object
cinematic = new /obj/screen(src)
cinematic.icon = 'icons/effects/station_explosion.dmi'
cinematic.icon_state = "station_intact"
cinematic.layer = 21
cinematic.mouse_opacity = MOUSE_OPACITY_TRANSPARENT
cinematic.screen_loc = "1,1"
if(nuke_site == NUKE_SITE_ON_STATION)
// Kill everyone on z-level 1 except for mobs in freezers and
// malfunctioning AIs.
for(var/mob/M in GLOB.mob_list)
if(M.stat != DEAD)
var/turf/T = get_turf(M)
if(T && is_station_level(T.z) && !istype(M.loc, /obj/structure/closet/secure_closet/freezer) && !(issilicon(M) && override == "AI malfunction"))
to_chat(M, "<span class='danger'><B>The blast wave from the explosion tears you atom from atom!</B></span>")
var/mob/ghost = M.ghostize()
M.dust() //no mercy
if(ghost && ghost.client) //Play the victims an uninterrupted cinematic.
ghost.client.screen += cinematic
CHECK_TICK
if(M && M.client) //Play the survivors a cinematic.
M.client.screen += cinematic
else
for(var/mob/M in GLOB.mob_list)
if(M.client)
M.client.screen += cinematic //show every client the cinematic
switch(nuke_site)
//Now animate the cinematic
if(NUKE_SITE_ON_STATION)
// station was destroyed
if(mode && !override)
override = mode.name
switch(override)
if("nuclear emergency") //Nuke Ops successfully bombed the station
flick("intro_nuke", cinematic)
sleep(35)
flick("station_explode_fade_red", cinematic)
SEND_SOUND(world, sound('sound/effects/explosion_distant.ogg'))
cinematic.icon_state = "summary_nukewin"
if("AI malfunction") //Malf (screen,explosion,summary)
flick("intro_malf", cinematic)
sleep(76)
flick("station_explode_fade_red", cinematic)
SEND_SOUND(world, sound('sound/effects/explosion_distant.ogg'))
cinematic.icon_state = "summary_malf"
else //Station nuked (nuke,explosion,summary)
flick("intro_nuke", cinematic)
sleep(35)
flick("station_explode_fade_red", cinematic)
SEND_SOUND(world, sound('sound/effects/explosion_distant.ogg'))
cinematic.icon_state = "summary_selfdes"
if(NUKE_SITE_ON_STATION_ZLEVEL)
// nuke was nearby but (mostly) missed
if(mode && !override)
override = mode.name
switch(override)
if("nuclear emergency") //Nuke wasn't on station when it blew up
flick("intro_nuke", cinematic)
sleep(35)
SEND_SOUND(world, sound('sound/effects/explosion_distant.ogg'))
flick("station_intact_fade_red", cinematic)
cinematic.icon_state = "summary_nukefail"
if("fake") //The round isn't over, we're just freaking people out for fun
flick("intro_nuke", cinematic)
sleep(35)
SEND_SOUND(world, sound('sound/items/bikehorn.ogg'))
flick("summary_selfdes", cinematic)
else
flick("intro_nuke", cinematic)
sleep(35)
SEND_SOUND(world, sound('sound/effects/explosion_distant.ogg'))
if(NUKE_SITE_OFF_STATION_ZLEVEL, NUKE_SITE_INVALID)
// nuke was nowhere nearby
// TODO: a really distant explosion animation
sleep(50)
SEND_SOUND(world, sound('sound/effects/explosion_distant.ogg'))
//If its actually the end of the round, wait for it to end.
//Otherwise if its a verb it will continue on afterwards.
spawn(300)
QDEL_NULL(cinematic) //end the cinematic
/datum/controller/subsystem/ticker/proc/create_characters()
for(var/mob/new_player/player in GLOB.player_list)
if(player.ready && player.mind)
if(player.mind.assigned_role == "AI")
player.close_spawn_windows()
var/mob/living/silicon/ai/ai_character = player.AIize()
ai_character.moveToAILandmark()
else if(!player.mind.assigned_role)
continue
else
player.create_character()
qdel(player)
/datum/controller/subsystem/ticker/proc/equip_characters()
for(var/mob/living/carbon/human/player in GLOB.player_list)
if(player && player.mind && player.mind.assigned_role && player.mind.assigned_role != player.mind.special_role)
SSjobs.AssignRank(player, player.mind.assigned_role, FALSE)
SSjobs.EquipRank(player, player.mind.assigned_role, FALSE)
equip_cuis(player)
/datum/controller/subsystem/ticker/proc/equip_cuis(mob/living/carbon/human/H)
if(!H.client)
return // If they are spawning without a client (somehow), they *cant* have a CUI list
for(var/datum/custom_user_item/cui in H.client.cui_entries)
// Skip items with invalid character names
if((cui.characer_name != H.real_name) && !cui.all_characters_allowed)
continue
var/ok = FALSE
if(!cui.all_jobs_allowed)
var/alt_blocked = FALSE
if(H.mind.role_alt_title)
if(!(H.mind.role_alt_title in cui.allowed_jobs))
alt_blocked = TRUE
if(!(H.mind.assigned_role in cui.allowed_jobs) || alt_blocked)
continue
var/obj/item/I = new cui.object_typepath()
var/name_override = cui.item_name_override
var/desc_override = cui.item_desc_override
if(name_override)
I.name = name_override
if(desc_override)
I.desc = desc_override
if(isstorage(H.back)) // Try to place it in something on the mob's back
var/obj/item/storage/S = H.back
if(length(S.contents) < S.storage_slots)
I.forceMove(H.back)
ok = TRUE
to_chat(H, "<span class='notice'>Your [I.name] has been added to your [H.back.name].</span>")
if(!ok)
for(var/obj/item/storage/S in H.contents) // Try to place it in any item that can store stuff, on the mob.
if(length(S.contents) < S.storage_slots)
I.forceMove(S)
ok = TRUE
to_chat(H, "<span class='notice'>Your [I.name] has been added to your [S.name].</span>")
break
if(!ok) // Finally, since everything else failed, place it on the ground
var/turf/T = get_turf(H)
if(T)
I.forceMove(T)
to_chat(H, "<span class='notice'>Your [I.name] is on the [T.name] below you.</span>")
else
to_chat(H, "<span class='notice'>Your [I.name] couldnt spawn anywhere on you or even on the floor below you. Please file a bug report.</span>")
qdel(I)
/datum/controller/subsystem/ticker/proc/send_tip_of_the_round()
var/m
if(selected_tip)
m = selected_tip
else
var/list/randomtips = file2list("strings/tips.txt")
var/list/memetips = file2list("strings/sillytips.txt")
if(randomtips.len && prob(95))
m = pick(randomtips)
else if(memetips.len)
m = pick(memetips)
if(m)
to_chat(world, "<span class='purple'><b>Tip of the round: </b>[html_encode(m)]</span>")
/datum/controller/subsystem/ticker/proc/declare_completion()
GLOB.nologevent = TRUE //end of round murder and shenanigans are legal; there's no need to jam up attack logs past this point.
set_observer_default_invisibility(0) //spooks things up
//Round statistics report
var/datum/station_state/ending_station_state = new /datum/station_state()
ending_station_state.count()
var/station_integrity = min(round( 100.0 * GLOB.start_state.score(ending_station_state), 0.1), 100.0)
var/list/end_of_round_info = list()
end_of_round_info += "<BR>[TAB]Shift Duration: <B>[round(ROUND_TIME / 36000)]:[add_zero("[ROUND_TIME / 600 % 60]", 2)]:[ROUND_TIME / 100 % 6][ROUND_TIME / 100 % 10]</B>"
end_of_round_info += "<BR>[TAB]Station Integrity: <B>[mode.station_was_nuked ? "<font color='red'>Destroyed</font>" : "[station_integrity]%"]</B>"
end_of_round_info += "<BR>"
//Silicon laws report
for(var/mob/living/silicon/ai/aiPlayer in GLOB.mob_list)
var/ai_ckey = safe_get_ckey(aiPlayer)
if(aiPlayer.stat != DEAD)
end_of_round_info += "<b>[aiPlayer.name] (Played by: [ai_ckey])'s laws at the end of the game were:</b>"
else
end_of_round_info += "<b>[aiPlayer.name] (Played by: [ai_ckey])'s laws when it was deactivated were:</b>"
aiPlayer.laws_sanity_check()
for(var/datum/ai_law/law as anything in aiPlayer.laws.sorted_laws)
if(law == aiPlayer.laws.zeroth_law)
end_of_round_info += "<span class='danger'>[law.get_index()]. [law.law]</span>"
else
end_of_round_info += "[law.get_index()]. [law.law]"
if(length(aiPlayer.connected_robots))
end_of_round_info += "<b>The AI's loyal minions were:</b> "
for(var/mob/living/silicon/robot/robo in aiPlayer.connected_robots)
var/robo_ckey = safe_get_ckey(robo)
end_of_round_info += "[robo.name][robo.stat ? " (Deactivated)" : ""] (Played by: [robo_ckey])"
var/dronecount = 0
for(var/mob/living/silicon/robot/robo in GLOB.mob_list)
if(isdrone(robo))
dronecount++
continue
var/robo_ckey = safe_get_ckey(robo)
if(!robo.connected_ai)
if(robo.stat != DEAD)
end_of_round_info += "<b>[robo.name] (Played by: [robo_ckey]) survived as an AI-less borg! Its laws were:</b>"
else
end_of_round_info += "<b>[robo.name] (Played by: [robo_ckey]) was unable to survive the rigors of being a cyborg without an AI. Its laws were:</b>"
robo.laws_sanity_check()
for(var/datum/ai_law/law as anything in robo.laws.sorted_laws)
if(law == robo.laws.zeroth_law)
end_of_round_info += "<span class='danger'>[law.get_index()]. [law.law]</span>"
else
end_of_round_info += "[law.get_index()]. [law.law]"
if(dronecount)
end_of_round_info += "<b>There [dronecount > 1 ? "were" : "was"] [dronecount] industrious maintenance [dronecount > 1 ? "drones" : "drone"] this round.</b>"
if(length(mode.eventmiscs))
for(var/datum/mind/eventmind in mode.eventmiscs)
end_of_round_info += printeventplayer(eventmind)
end_of_round_info += printobjectives(eventmind)
end_of_round_info += "<br>"
mode.declare_completion()//To declare normal completion.
end_of_round_info += mode.get_end_of_round_antagonist_statistics()
for(var/datum/team/team in GLOB.antagonist_teams)
team.on_round_end()
// Save the data before end of the round griefing
SSpersistent_data.save()
to_chat(world, end_of_round_info.Join("<br>"))
// Display the scoreboard window
score.scoreboard()
// Declare the completion of the station goals
mode.declare_station_goal_completion()
//Ask the event manager to print round end information
SSevents.RoundEnd()
//make big obvious note in game logs that round ended
log_game("///////////////////////////////////////////////////////")
log_game("///////////////////// ROUND ENDED /////////////////////")
log_game("///////////////////////////////////////////////////////")
// Add AntagHUD to everyone, see who was really evil the whole time!
for(var/datum/atom_hud/antag/H in GLOB.huds)
for(var/m in GLOB.player_list)
var/mob/M = m
H.add_hud_to(M)
var/static/list/base_encouragement_messages = list(
"Keep on keeping on!",
"Great job!",
"Keep up the good work!",
"Nice going!"
)
var/static/list/special_encouragement_messages = list(
"Outstanding!",
"This is going on the fridge!",
"Looks like you're popular!",
"That's what we like to see!",
"Hell yeah, brother!",
"Honestly, quite incredible!"
)
// Tell people how many kudos they got this round
// Besides, what's another loop over the /entire player list/
var/kudos_message
for(var/mob/M in GLOB.player_list)
var/kudos = M.mind?.kudos_received_from
if(length(kudos))
kudos_message = pick(length(kudos) > 5 ? special_encouragement_messages : base_encouragement_messages)
to_chat(M, "<span class='green big'>You received <b>[length(M.mind.kudos_received_from)]</b> kudos from other players this round! [kudos_message]</span>")
// Seal the blackbox, stop collecting info
SSblackbox.Seal()
SSdbcore.SetRoundEnd()
return TRUE
/datum/controller/subsystem/ticker/proc/HasRoundStarted()
return current_state >= GAME_STATE_PLAYING
/datum/controller/subsystem/ticker/proc/IsRoundInProgress()
return current_state == GAME_STATE_PLAYING
/datum/controller/subsystem/ticker/proc/setup_news_feeds()
var/datum/feed_channel/newChannel = new /datum/feed_channel
newChannel.channel_name = "Station Announcements Log"
newChannel.author = "Automated Announcement Listing"
newChannel.icon = "bullhorn"
newChannel.frozen = TRUE
newChannel.admin_locked = TRUE
GLOB.news_network.channels += newChannel
newChannel = new /datum/feed_channel
newChannel.channel_name = "Public Station Announcements"
newChannel.author = "Automated Announcement Listing"
newChannel.icon = "users"
newChannel.is_public = TRUE
GLOB.news_network.channels += newChannel
newChannel = new /datum/feed_channel
newChannel.channel_name = "Nyx Daily"
newChannel.author = "CentComm Minister of Information"
newChannel.icon = "meteor"
newChannel.frozen = TRUE
newChannel.admin_locked = TRUE
GLOB.news_network.channels += newChannel
newChannel = new /datum/feed_channel
newChannel.channel_name = "The Gibson Gazette"
newChannel.author = "Editor Mike Hammers"
newChannel.icon = "star"
newChannel.frozen = TRUE
newChannel.admin_locked = TRUE
GLOB.news_network.channels += newChannel
for(var/loc_type in subtypesof(/datum/trade_destination))
var/datum/trade_destination/D = new loc_type
GLOB.weighted_randomevent_locations[D] = D.viable_random_events.len
GLOB.weighted_mundaneevent_locations[D] = D.viable_mundane_events.len
// Easy handler to make rebooting the world not a massive sleep in world/Reboot()
/datum/controller/subsystem/ticker/proc/reboot_helper(reason, end_string, delay)
// Admins delayed round end. Just alert and dont bother with anything else.
if(delay_end)
to_chat(world, "<span class='boldannounce'>An admin has delayed the round end.</span>")
return
if(!isnull(delay))
// Delay time was present. Use that.
delay = max(0, delay)
else
// Use default restart timeout
delay = restart_timeout
to_chat(world, "<span class='boldannounce'>Rebooting world in [delay/10] [delay > 10 ? "seconds" : "second"]. [reason]</span>")
real_reboot_time = world.time + delay
UNTIL(world.time > real_reboot_time) // Hold it here
// And if we re-delayed, bail again
if(delay_end)
to_chat(world, "<span class='boldannounce'>Reboot was cancelled by an admin.</span>")
return
if(end_string)
end_state = end_string
// Play a haha funny noise
var/round_end_sound = pick(GLOB.round_end_sounds)
var/sound_length = GLOB.round_end_sounds[round_end_sound]
SEND_SOUND(world, sound(round_end_sound))
sleep(sound_length)
world.Reboot()
// Timers invoke this async
/datum/controller/subsystem/ticker/proc/handle_antagfishing_reporting()
// This needs the DB
if(!SSdbcore.IsConnected())
return
// Dont need to do anything
if(!length(flagged_antag_rollers))
return
// Records themselves
var/list/datum/antag_record/records = list()
// Queries to load data (executed as async batch)
var/list/datum/db_query/load_queries = list()
// Queries to save data (executed as async batch)
var/list/datum/db_query/save_queries = list()
for(var/ckey in flagged_antag_rollers)
var/datum/antag_record/AR = new /datum/antag_record(ckey)
records[ckey] = AR
load_queries[ckey] = AR.get_load_query()
// Explanation for parameters:
// TRUE: We want warnings if these fail
// FALSE: Do NOT qdel() queries here, otherwise they wont be read. At all.
// TRUE: This is an assoc list, so it needs to prepare for that
SSdbcore.MassExecute(load_queries, TRUE, FALSE, TRUE)
// Report on things
var/list/log_text = list("The following players attempted to roll antag with no jobs (total infractions listed)")
for(var/ckey in flagged_antag_rollers)
var/datum/antag_record/AR = records[ckey]
AR.handle_data(load_queries[ckey])
save_queries[ckey] = AR.get_save_query()
log_text += "<small>- <a href='?priv_msg=[ckey]'>[ckey]</a>: [AR.infraction_count]</small>"
log_text += "Investigation advised if there are a high number of infractions"
message_admins(log_text.Join("<br>"))
// Now do a ton of saves
SSdbcore.MassExecute(save_queries, TRUE, TRUE, TRUE)
// And cleanup
QDEL_LIST_ASSOC_VAL(load_queries)
records.Cut()
flagged_antag_rollers.Cut()
/// This proc is for recording biohazard events, and blackboxing if they lived, died, or ended the round. This currently applies to: Terror spiders, Xenomorphs, and Blob.
/datum/controller/subsystem/ticker/proc/event_blackbox(outcome = ROUND_END_CREW_TRANSFER)
for(var/I in SSevents.biohazards_this_round)
switch(I)
if(TS_INFESTATION_GREEN_SPIDER, TS_INFESTATION_PRINCE_SPIDER, TS_INFESTATION_WHITE_SPIDER, TS_INFESTATION_PRINCESS_SPIDER, TS_INFESTATION_QUEEN_SPIDER)
var/output = "unknown spider type"
switch(I)
if(TS_INFESTATION_GREEN_SPIDER)
output = "Green Terrors"
if(TS_INFESTATION_PRINCE_SPIDER)
output = "Prince Terror"
if(TS_INFESTATION_WHITE_SPIDER)
output = "White Terrors"
if(TS_INFESTATION_PRINCESS_SPIDER)
output = "Princess Terrors"
if(TS_INFESTATION_QUEEN_SPIDER)
output = "Queen Terrors"
var/spiders = 0
for(var/mob/living/simple_animal/hostile/poison/terror_spider/S in GLOB.ts_spiderlist)
if(S.ckey)
spiders++
if(spiders >= 5 || (output == "Prince Terror" && spiders == 1)) //If a prince lives, record as win.
switch(outcome)
if(ROUND_END_NUCLEAR)
SSblackbox.record_feedback("tally", "Biohazard nuclear victories", 1, output)
if(ROUND_END_CREW_TRANSFER)
SSblackbox.record_feedback("tally", "Biohazard survives to normal round end", 1, output)
if(ROUND_END_FORCED)
SSblackbox.record_feedback("tally", "Biohazard survives to admin round end", 1, output)
else
switch(outcome)
if(ROUND_END_NUCLEAR)
SSblackbox.record_feedback("tally", "Biohazard dies station nuked", 1, output)
if(ROUND_END_CREW_TRANSFER)
SSblackbox.record_feedback("tally", "Biohazard dies normal end", 1, output)
if(ROUND_END_FORCED)
SSblackbox.record_feedback("tally", "Biohazard dies admin round end", 1, output)
if("Xenomorphs")
if(length(SSticker.mode.xenos) > 5)
switch(outcome)
if(ROUND_END_NUCLEAR)
SSblackbox.record_feedback("tally", "Biohazard nuclear victories", 1, "Xenomorphs")
if(ROUND_END_CREW_TRANSFER)
SSblackbox.record_feedback("tally", "Biohazard survives to normal round end", 1, "Xenomorphs")
if(ROUND_END_FORCED)
SSblackbox.record_feedback("tally", "Biohazard survives to admin round end", 1, "Xenomorphs")
else
switch(outcome)
if(ROUND_END_NUCLEAR)
SSblackbox.record_feedback("tally", "Biohazard dies station nuked", 1, "Xenomorphs")
if(ROUND_END_CREW_TRANSFER)
SSblackbox.record_feedback("tally", "Biohazard dies normal end", 1, "Xenomorphs")
if(ROUND_END_FORCED)
SSblackbox.record_feedback("tally", "Biohazard dies admin round end", 1, "Xenomorphs")
if("Blob")
if(length(SSticker.mode.blob_overminds))
switch(outcome)
if(ROUND_END_NUCLEAR)
SSblackbox.record_feedback("tally", "Biohazard nuclear victories", 1, "Blob")
if(ROUND_END_CREW_TRANSFER)
SSblackbox.record_feedback("tally", "Biohazard survives to normal round end", 1, "Blob")
if(ROUND_END_FORCED)
SSblackbox.record_feedback("tally", "Biohazard survives to admin round end", 1, "Blob")
else
switch(outcome)
if(ROUND_END_NUCLEAR)
SSblackbox.record_feedback("tally", "Biohazard dies station nuked", 1, "Blob")
if(ROUND_END_CREW_TRANSFER)
SSblackbox.record_feedback("tally", "Biohazard dies normal end", 1, "Blob")
if(ROUND_END_FORCED)
SSblackbox.record_feedback("tally", "Biohazard dies admin round end", 1, "Blob")