-
Notifications
You must be signed in to change notification settings - Fork 85
Expand file tree
/
Copy pathticker.dm
More file actions
920 lines (774 loc) · 36.3 KB
/
Copy pathticker.dm
File metadata and controls
920 lines (774 loc) · 36.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
#define ROUND_START_MUSIC_LIST "strings/round_start_sounds.txt"
#define SS_TICKER_TRAIT "SS_Ticker"
SUBSYSTEM_DEF(ticker)
name = "Ticker"
priority = FIRE_PRIORITY_TICKER
ss_flags = SS_KEEP_TIMING
runlevels = RUNLEVEL_LOBBY | RUNLEVEL_SETUP | RUNLEVEL_GAME | RUNLEVEL_POSTGAME
/// state of current round (used by process()) Use the defines GAME_STATE_* !
var/current_state = GAME_STATE_STARTUP
/// Boolean to track if round should be forcibly ended next ticker tick.
/// Set by admin intervention ([ADMIN_FORCE_END_ROUND])
/// or a "round-ending" event, like summoning Nar'Sie, a blob victory, the nuke going off, etc. ([FORCE_END_ROUND])
var/force_ending = END_ROUND_AS_NORMAL
/// If TRUE, there is no lobby phase, the game starts immediately.
#ifdef ABSOLUTE_MINIMUM
var/start_immediately = TRUE
#else
var/start_immediately = FALSE
#endif
/// Boolean to track and check if our subsystem setup is done.
var/setup_done = FALSE
var/login_music //music played in pregame lobby
var/round_end_sound //music/jingle played when the world reboots
var/round_end_sound_sent = TRUE //If all clients have loaded it
var/list/datum/mind/minds = list() //The characters in the game. Used for objective tracking.
var/delay_end = FALSE //if set true, the round will not restart on its own
var/admin_delay_notice = "" //a message to display to anyone who tries to restart the world after a delay
var/ready_for_reboot = FALSE //all roundend preparation done with, all that's left is reboot
var/tipped = FALSE //Did we broadcast the tip of the day yet?
var/selected_tip // What will be the tip of the day?
var/timeLeft //pregame timer
var/start_at
var/gametime_offset = 21 HOURS //Deciseconds to add to world.time for station time. // DARKPACK EDIT ADD - CITY_TIME
var/city_time_rate_multiplier = 2 //factor of station time progressal vs real time. // DARKPACK EDIT CHANGE - CITY_TIME
/// Num of players, used for pregame stats on statpanel
var/totalPlayers = 0
/// Num of ready players, used for pregame stats on statpanel (only viewable by admins)
var/totalPlayersReady = 0
/// Num of ready admins, used for pregame stats on statpanel (only viewable by admins)
var/total_admins_ready = 0
var/queue_delay = 0
var/list/queued_players = list() //used for join queues when the server exceeds the hard population cap
/// What is going to be reported to other stations at end of round?
var/news_report
var/roundend_check_paused = FALSE
var/round_start_time = 0
var/round_start_timeofday = 0 // DARKPACK EDIT ADD
var/list/round_start_events
var/list/round_end_events
var/mode_result = "undefined"
var/end_state = "undefined"
/// People who have been commended and will receive a heart
var/list/hearts
/// Why an emergency shuttle was called
var/emergency_reason
///The display of how much time is left before a reboot, given to all clients post-game.
var/atom/movable/screen/reboot_timer/reboot_hud
/// ID of round reboot timer, if it exists
var/reboot_timer = null
/datum/controller/subsystem/ticker/Initialize()
var/list/provisional_title_music = flist("[global.config.directory]/title_music/sounds/")
var/list/music = list()
var/use_rare_music = prob(1)
for(var/S in provisional_title_music)
var/lower = LOWER_TEXT(S)
var/list/L = splittext(lower,"+")
switch(L.len)
if(3) //rare+MAP+sound.ogg or MAP+rare.sound.ogg -- Rare Map-specific sounds
if(use_rare_music)
if(L[1] == "rare" && L[2] == SSmapping.current_map.map_name)
music += S
else if(L[2] == "rare" && L[1] == SSmapping.current_map.map_name)
music += S
if(2) //rare+sound.ogg or MAP+sound.ogg -- Rare sounds or Map-specific sounds
if((use_rare_music && L[1] == "rare") || (L[1] == SSmapping.current_map.map_name))
music += S
if(1) //sound.ogg -- common sound
if(L[1] == "exclude")
continue
music += S
var/old_login_music = trim(file2text("data/last_round_lobby_music.txt"))
if(length(music) > 1)
music -= old_login_music
for(var/S in music)
if(IS_SOUND_FILE(S))
continue
music -= S
if(!length(music))
music = world.file2list(ROUND_START_MUSIC_LIST, "\n")
if(length(music) > 1)
music -= old_login_music
set_lobby_music(pick(music))
else
set_lobby_music("[global.config.directory]/title_music/sounds/[pick(music)]")
if(!GLOB.syndicate_code_phrase)
GLOB.syndicate_code_phrase = generate_code_phrase(return_list=TRUE)
var/codewords = jointext(GLOB.syndicate_code_phrase, "|")
var/regex/codeword_match = new("([codewords])", "ig")
GLOB.syndicate_code_phrase_regex = codeword_match
if(!GLOB.syndicate_code_response)
GLOB.syndicate_code_response = generate_code_phrase(return_list=TRUE)
var/codewords = jointext(GLOB.syndicate_code_response, "|")
var/regex/codeword_match = new("([codewords])", "ig")
GLOB.syndicate_code_response_regex = codeword_match
start_at = world.time + (CONFIG_GET(number/lobby_countdown) * (1 SECONDS))
// DARKPACK EDIT ADD START - CITY_TIME
round_start_time = start_at // May be changed later, but prevents the time from jumping back when the round actually starts
gametime_offset = (CONFIG_GET(number/shift_time_start_hour) * (1 HOURS))
// DARKPACK EDIT ADD END
return SS_INIT_SUCCESS
/datum/controller/subsystem/ticker/fire()
switch(current_state)
if(GAME_STATE_STARTUP)
if(Master.initializations_finished_with_no_players_logged_in)
start_at = world.time + (CONFIG_GET(number/lobby_countdown) * (1 SECONDS))
for(var/client/C in GLOB.clients)
window_flash(C, ignorepref = TRUE) //let them know lobby has opened up.
to_chat(world, span_notice("<b>Welcome to [station_name()]!</b>"))
for(var/channel_tag in CONFIG_GET(str_list/channel_announce_new_game))
send2chat(new /datum/tgs_message_content("New round starting on [SSmapping.current_map.map_name]!"), channel_tag)
current_state = GAME_STATE_PREGAME
SEND_SIGNAL(src, COMSIG_TICKER_ENTER_PREGAME)
fire()
if(GAME_STATE_PREGAME)
//lobby stats for statpanels
if(isnull(timeLeft))
timeLeft = max(0, start_at - world.time)
totalPlayers = LAZYLEN(GLOB.new_player_list)
totalPlayersReady = 0
total_admins_ready = 0
for(var/mob/dead/new_player/player as anything in GLOB.new_player_list)
if(player.ready == PLAYER_READY_TO_PLAY)
++totalPlayersReady
if(player.client?.holder)
++total_admins_ready
if(start_immediately)
timeLeft = 0
//countdown
if(timeLeft < 0)
return
timeLeft -= wait
if(timeLeft <= 300 && !tipped)
send_tip_of_the_round(world, selected_tip)
tipped = TRUE
if(timeLeft <= 0)
SEND_SIGNAL(src, COMSIG_TICKER_ENTER_SETTING_UP)
current_state = GAME_STATE_SETTING_UP
Master.SetRunLevel(RUNLEVEL_SETUP)
if(start_immediately)
fire()
if(GAME_STATE_SETTING_UP)
if(!setup())
//setup failed
current_state = GAME_STATE_STARTUP
start_at = world.time + (CONFIG_GET(number/lobby_countdown) * (1 SECONDS))
timeLeft = null
Master.SetRunLevel(RUNLEVEL_LOBBY)
SEND_SIGNAL(src, COMSIG_TICKER_ERROR_SETTING_UP)
if(GAME_STATE_PLAYING)
check_queue()
if(!roundend_check_paused && (check_finished() || force_ending))
current_state = GAME_STATE_FINISHED
GLOB.canon_event = FALSE // We generally consider all events in postgame to be non-canon due to most servers having EORG // DARKPACK EDIT ADD
toggle_ooc(TRUE) // Turn it on
toggle_dooc(TRUE)
declare_completion(force_ending)
Master.SetRunLevel(RUNLEVEL_POSTGAME)
if(GAME_STATE_FINISHED)
if(ready_for_reboot)
if(isnull(reboot_timer))
reboot_hud.maptext = MAPTEXT_PIXELLARI("<center>Server reboot \n\ DELAYED</center>")
else
reboot_hud.maptext = MAPTEXT_PIXELLARI("<center>Server rebooting in:\n\ [DisplayTimeText(timeleft(SSticker.reboot_timer), 1)]</center>")
/datum/controller/subsystem/ticker/vv_edit_var(var_name, var_value)
if(var_name == NAMEOF(src, login_music))
set_lobby_music(var_value, override = TRUE)
return ..()
/// Checks if the round should be ending, called every ticker tick
/datum/controller/subsystem/ticker/proc/check_finished()
if(!setup_done)
return FALSE
if(SSshuttle.emergency && (SSshuttle.emergency.mode == SHUTTLE_ENDGAME))
return TRUE
if(GLOB.station_was_nuked)
return TRUE
if(GLOB.revolution_handler?.result == REVOLUTION_VICTORY)
return TRUE
// DARKPACK EDIT ADD START - CITY_TIME
if(SScity_time.roundend_started)
return TRUE
// DARKPACK EDIT ADD END
// DARKPACK EDIT ADD START - MASQUERADE
if(SSmasquerade.roundend_started)
return TRUE
// DARKPACK EDIT ADD END
return FALSE
/// Gets a list of players with their readied state so we can post it as a log
/datum/controller/subsystem/ticker/proc/get_player_ready_states()
var/list/player_states = list()
for(var/mob/dead/new_player/player as anything in GLOB.new_player_list)
player_states[player.ckey] = player.ready
return player_states
/datum/controller/subsystem/ticker/proc/setup()
to_chat(world, span_boldannounce("Starting game..."))
var/init_start = world.timeofday
var/list/players_and_readiness = get_player_ready_states()
log_game("Players and Readiness: [json_encode(players_and_readiness)]", players_and_readiness)
CHECK_TICK
//Configure mode and assign player to antagonists
var/can_continue = FALSE
can_continue = SSdynamic.select_roundstart_antagonists() //Choose antagonists
CHECK_TICK
SEND_GLOBAL_SIGNAL(COMSIG_GLOB_PRE_JOBS_ASSIGNED, src)
can_continue = can_continue && SSjob.divide_occupations() //Distribute jobs
CHECK_TICK
if(!GLOB.debugging_enabled)
if(!can_continue)
log_game("Game failed pre_setup")
to_chat(world, "<B>Error setting up game.</B> Reverting to pre-game lobby.")
SSjob.reset_occupations()
return FALSE
else
message_admins(span_notice("DEBUG: Bypassing prestart checks..."))
CHECK_TICK
// There may be various config settings that have been set or modified by this point.
// This is the point of no return before spawning in new players, let's run over the
// job trim singletons and update them based on any config settings.
SSid_access.refresh_job_trim_singletons()
CHECK_TICK
if(!CONFIG_GET(flag/ooc_during_round))
toggle_ooc(FALSE) // Turn it off
CHECK_TICK
GLOB.start_landmarks_list = shuffle(GLOB.start_landmarks_list) //Shuffle the order of spawn points so they dont always predictably spawn bottom-up and right-to-left
create_characters() //Create player characters
collect_minds()
equip_characters()
GLOB.manifest.build()
transfer_characters() //transfer keys to the new mobs
for(var/I in round_start_events)
var/datum/callback/cb = I
cb.InvokeAsync()
LAZYCLEARLIST(round_start_events)
round_start_time = world.time //otherwise round_start_time would be 0 for the signals
round_start_timeofday = world.timeofday // DARKPACK EDIT ADD
SEND_SIGNAL(src, COMSIG_TICKER_ROUND_STARTING, world.time)
log_world("Game start took [(world.timeofday - init_start)/10]s")
INVOKE_ASYNC(SSdbcore, TYPE_PROC_REF(/datum/controller/subsystem/dbcore,SetRoundStart))
to_chat(world, span_notice(span_bold("Another night in [station_name()], another dollar."))) // DARKPACK EDIT CHANGE
SEND_SOUND(world, sound(SSstation.announcer.get_rand_welcome_sound()))
current_state = GAME_STATE_PLAYING
Master.SetRunLevel(RUNLEVEL_GAME)
if(length(GLOB.holidays))
to_chat(world, span_notice("and..."))
for(var/holidayname in GLOB.holidays)
var/datum/holiday/holiday = GLOB.holidays[holidayname]
to_chat(world, span_info(holiday.greet()))
PostSetup()
return TRUE
/datum/controller/subsystem/ticker/proc/PostSetup()
set waitfor = FALSE
// Spawn traitors and stuff
for(var/datum/dynamic_ruleset/roundstart/ruleset in SSdynamic.queued_rulesets)
ruleset.execute()
SSdynamic.unqueue_ruleset(ruleset)
SSdynamic.executed_rulesets += ruleset
// Queue roundstart intercept report
if(!CONFIG_GET(flag/no_intercept_report))
GLOB.communications_controller.queue_roundstart_report()
// Queue admin logout report
var/roundstart_logout_timer = CONFIG_GET(number/roundstart_logout_report_time_average)
var/roundstart_report_variance = CONFIG_GET(number/roundstart_logout_report_time_variance)
var/randomized_callback_timer = rand((roundstart_logout_timer - roundstart_report_variance), (roundstart_logout_timer + roundstart_report_variance))
addtimer(CALLBACK(src, PROC_REF(display_roundstart_logout_report)), randomized_callback_timer)
GLOB.logout_timer_set = randomized_callback_timer
// Queue suicide slot handling
if(CONFIG_GET(flag/reopen_roundstart_suicide_roles))
var/delay = (CONFIG_GET(number/reopen_roundstart_suicide_roles_delay) * 1 SECONDS) || 4 MINUTES
addtimer(CALLBACK(src, PROC_REF(reopen_roundstart_suicide_roles)), delay)
// Handle database
if(SSdbcore.Connect())
var/list/to_set = list()
var/arguments = list()
if(GLOB.revdata.originmastercommit)
to_set += "commit_hash = :commit_hash"
arguments["commit_hash"] = GLOB.revdata.GetDatabaseCommitSha()
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)
GLOB.start_state = new /datum/station_state()
GLOB.start_state.count()
var/list/adm = get_admin_counts()
var/list/allmins = adm["present"]
send2adminchat("Server", "Round [GLOB.round_id ? "#[GLOB.round_id]" : ""] has started[allmins.len ? ".":" with no active admins online!"]")
setup_done = TRUE
for(var/i in GLOB.start_landmarks_list)
var/obj/effect/landmark/start/S = i
if(istype(S)) //we can not runtime here. not in this important of a proc.
S.after_round_start()
else
stack_trace("[S] [S.type] found in start landmarks list, which isn't a start landmark!")
// handle persistence stuff that requires ckeys, in this case hardcore mode and temporal scarring
for(var/i in GLOB.player_list)
if(!ishuman(i))
continue
var/mob/living/carbon/human/iter_human = i
iter_human.increment_scar_slot()
iter_human.load_persistent_scars()
iter_human.load_guestbook() // DARKPACK EDIT ADD
iter_human.update_soak() // DARKPACK EDIT ADDITION
if(!iter_human.hardcore_survival_score)
continue
if(iter_human.is_antag())
to_chat(iter_human, span_notice("You will gain [round(iter_human.hardcore_survival_score) * 2] hardcore random points if you greentext this round!"))
else
to_chat(iter_human, span_notice("You will gain [round(iter_human.hardcore_survival_score)] hardcore random points if you survive this round!"))
/datum/controller/subsystem/ticker/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
if (istype(C) && !C.last_mind)
continue // never had a client
if(L.ckey && !GLOB.directory[L.ckey])
msg += "<b>[L.name]</b> ([L.key]), the [L.job] (<font color='#ffcc00'><b>Disconnected</b></font>)\n"
if(L.ckey && L.client)
var/failed = FALSE
if(L.client.inactivity >= GLOB.logout_timer_set) //Connected, but inactive (alt+tabbed or something)
msg += "<b>[L.name]</b> ([L.key]), the [L.job] (<font color='#ffcc00'><b>Connected, Inactive</b></font>)\n"
failed = TRUE //AFK client
if(!failed && L.stat)
if(HAS_TRAIT(L, TRAIT_SUICIDED)) //Suicider
msg += "<b>[L.name]</b> ([L.key]), the [L.job] ([span_bolddanger("Suicide")])\n"
failed = TRUE //Disconnected client
if(!failed && (L.stat == UNCONSCIOUS || L.stat == HARD_CRIT))
msg += "<b>[L.name]</b> ([L.key]), the [L.job] (Dying)\n"
failed = TRUE //Unconscious
if(!failed && L.stat == DEAD)
msg += "<b>[L.name]</b> ([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(HAS_TRAIT(L, TRAIT_SUICIDED)) //Suicider
msg += "<b>[L.name]</b> ([ckey(D.mind.key)]), the [L.job] ([span_bolddanger("Suicide")])\n"
continue //Disconnected client
else
msg += "<b>[L.name]</b> ([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 += "<b>[L.name]</b> ([ckey(D.mind.key)]), the [L.job] ([span_bolddanger("Ghosted")])\n"
continue //Ghosted while alive
msg += "[span_boldnotice("Roundstart logout reported at: [DisplayTimeText(GLOB.logout_timer_set)]")]\n"
var/concatenated_message = msg.Join()
log_admin(concatenated_message)
to_chat(GLOB.admins, concatenated_message)
/datum/controller/subsystem/ticker/proc/reopen_roundstart_suicide_roles()
var/include_command = CONFIG_GET(flag/reopen_roundstart_suicide_roles_command_positions)
var/list/reopened_jobs = list()
for(var/mob/living/quitter in GLOB.suicided_mob_list)
var/datum/job/job = SSjob.get_job(quitter.job)
if(!job || !(job.job_flags & JOB_REOPEN_ON_ROUNDSTART_LOSS))
continue
if(!include_command && job.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND)
continue
job.current_positions = max(job.current_positions - 1, 0)
reopened_jobs += quitter.job
if(CONFIG_GET(flag/reopen_roundstart_suicide_roles_command_report))
if(reopened_jobs.len)
var/reopened_job_report_positions
for(var/dead_dudes_job in reopened_jobs)
reopened_job_report_positions = "[reopened_job_report_positions ? "[reopened_job_report_positions]\n":""][dead_dudes_job]"
var/suicide_command_report = {"
<font size = 3><b>[command_name()] Human Resources Board</b><br>
Notice of Personnel Change</font><hr>
To personnel management staff aboard [station_name()]:<br><br>
Our medical staff have detected a series of anomalies in the vital sensors
of some of the staff aboard your station.<br><br>
Further investigation into the situation on our end resulted in us discovering
a series of rather... unforturnate decisions that were made on the part of said staff.<br><br>
As such, we have taken the liberty to automatically reopen employment opportunities for the positions of the crew members
who have decided not to partake in our research. We will be forwarding their cases to our employment review board
to determine their eligibility for continued service with the company (and of course the
continued storage of cloning records within the central medical backup server.)<br><br>
<i>The following positions have been reopened on our behalf:<br><br>
[reopened_job_report_positions]</i>
"}
print_command_report(suicide_command_report, "Central Command Personnel Update")
//These callbacks will fire after roundstart key transfer
/datum/controller/subsystem/ticker/proc/OnRoundstart(datum/callback/cb)
if(!HasRoundStarted())
LAZYADD(round_start_events, cb)
else
cb.InvokeAsync()
//These callbacks will fire before roundend report
/datum/controller/subsystem/ticker/proc/OnRoundend(datum/callback/cb)
if(current_state >= GAME_STATE_FINISHED)
cb.InvokeAsync()
else
LAZYADD(round_end_events, cb)
/datum/controller/subsystem/ticker/proc/create_characters()
for(var/i in GLOB.new_player_list)
var/mob/dead/new_player/player = i
if(player.ready == PLAYER_READY_TO_PLAY && player.mind)
GLOB.joined_player_list += player.ckey
var/atom/destination = player.mind.assigned_role.get_roundstart_spawn_point()
if(!destination) // Failed to fetch a proper roundstart location, won't be going anywhere.
continue
player.create_character(destination)
CHECK_TICK
/datum/controller/subsystem/ticker/proc/collect_minds()
for(var/i in GLOB.new_player_list)
var/mob/dead/new_player/P = i
if(P.new_character && P.new_character.mind)
SSticker.minds += P.new_character.mind
CHECK_TICK
/datum/controller/subsystem/ticker/proc/equip_characters()
GLOB.security_officer_distribution = decide_security_officer_departments(
shuffle(GLOB.new_player_list),
shuffle(GLOB.available_depts),
)
var/captainless = TRUE
var/highest_rank = length(SSjob.chain_of_command) + 1
var/list/spare_id_candidates = list()
var/mob/dead/new_player/picked_spare_id_candidate
// Find a suitable player to hold captaincy.
for(var/mob/dead/new_player/new_player_mob as anything in GLOB.new_player_list)
if(is_banned_from(new_player_mob.ckey, list(JOB_CAPTAIN)))
CHECK_TICK
continue
if(!ishuman(new_player_mob.new_character))
continue
var/mob/living/carbon/human/new_player_human = new_player_mob.new_character
if(!new_player_human.mind || is_unassigned_job(new_player_human.mind.assigned_role))
continue
// Keep a rolling tally of who'll get the cap's spare ID vault code.
// Check assigned_role's priority and curate the candidate list appropriately.
var/player_assigned_role = new_player_human.mind.assigned_role.title
var/spare_id_priority = SSjob.chain_of_command[player_assigned_role]
if(spare_id_priority)
if(spare_id_priority < highest_rank)
spare_id_candidates.Cut()
spare_id_candidates += new_player_mob
highest_rank = spare_id_priority
else if(spare_id_priority == highest_rank)
spare_id_candidates += new_player_mob
CHECK_TICK
if(length(spare_id_candidates))
picked_spare_id_candidate = pick(spare_id_candidates)
for(var/mob/dead/new_player/new_player_mob as anything in GLOB.new_player_list)
if(QDELETED(new_player_mob) || !isliving(new_player_mob.new_character))
CHECK_TICK
continue
var/mob/living/new_player_living = new_player_mob.new_character
if(!new_player_living.mind)
CHECK_TICK
continue
var/datum/job/player_assigned_role = new_player_living.mind.assigned_role
if(player_assigned_role.job_flags & JOB_EQUIP_RANK)
SSjob.equip_rank(new_player_living, player_assigned_role, new_player_mob.client)
player_assigned_role.after_roundstart_spawn(new_player_living, new_player_mob.client)
if(picked_spare_id_candidate == new_player_mob)
captainless = FALSE
var/acting_captain = !is_captain_job(player_assigned_role)
SSjob.promote_to_captain(new_player_living, acting_captain)
OnRoundstart(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(minor_announce), player_assigned_role.get_captaincy_announcement(new_player_living)))
if(ishuman(new_player_living))
if(player_assigned_role.job_flags & JOB_ASSIGN_QUIRKS)
if(CONFIG_GET(flag/roundstart_traits))
if(new_player_mob.client?.prefs?.should_be_random_hardcore(player_assigned_role, new_player_living.mind))
new_player_mob.client.prefs.hardcore_random_setup(new_player_living)
SSquirks.AssignQuirks(new_player_living, new_player_mob.client)
else // clear any personalities the prefs added since our job clearly does not want them
new_player_living.clear_personalities()
if(ishuman(new_player_living))
SEND_SIGNAL(new_player_living, COMSIG_HUMAN_CHARACTER_SETUP_FINISHED)
CHECK_TICK
if(captainless)
for(var/mob/dead/new_player/new_player_mob as anything in GLOB.new_player_list)
var/mob/living/carbon/human/new_player_human = new_player_mob.new_character
if(new_player_human)
to_chat(new_player_mob, span_notice("Captainship not forced on anyone."))
CHECK_TICK
/datum/controller/subsystem/ticker/proc/decide_security_officer_departments(
list/new_players,
list/departments,
)
var/list/officer_mobs = list()
var/list/officer_preferences = list()
for (var/mob/dead/new_player/new_player_mob as anything in new_players)
var/mob/living/carbon/human/character = new_player_mob.new_character
if (istype(character) && is_security_officer_job(character.mind?.assigned_role))
officer_mobs += character
var/datum/client_interface/client = GET_CLIENT(new_player_mob)
var/preference = client?.prefs?.read_preference(/datum/preference/choiced/security_department)
officer_preferences += preference
var/distribution = get_officer_departments(officer_preferences, departments)
var/list/output = list()
for (var/index in 1 to officer_mobs.len)
output[REF(officer_mobs[index])] = distribution[index]
return output
/datum/controller/subsystem/ticker/proc/transfer_characters()
var/list/livings = list()
for(var/mob/dead/new_player/player as anything in GLOB.new_player_list)
var/mob/living = player.transfer_character()
if(living)
qdel(player)
ADD_TRAIT(living, TRAIT_NO_TRANSFORM, SS_TICKER_TRAIT)
if(living.client)
var/atom/movable/screen/splash/fade_out = new(null, null, living.client, TRUE)
fade_out.fade(TRUE)
living.client.init_verbs()
livings += living
if(livings.len)
addtimer(CALLBACK(src, PROC_REF(release_characters), livings), 3 SECONDS, TIMER_CLIENT_TIME)
/datum/controller/subsystem/ticker/proc/release_characters(list/livings)
for(var/mob/living/living_mob as anything in livings)
REMOVE_TRAIT(living_mob, TRAIT_NO_TRANSFORM, SS_TICKER_TRAIT)
/datum/controller/subsystem/ticker/proc/check_queue()
if(!queued_players.len)
return
var/hard_popcap = CONFIG_GET(number/hard_popcap)
if(!hard_popcap)
list_clear_nulls(queued_players)
for (var/mob/dead/new_player/new_player in queued_players)
to_chat(new_player, span_userdanger("The alive players limit has been released!<br><a href='byond://?src=[REF(new_player)];late_join=override'>[html_encode(">>Join Game<<")]</a>"))
SEND_SOUND(new_player, sound('sound/announcer/notice/notice1.ogg'))
GLOB.latejoin_menu.ui_interact(new_player)
queued_players.len = 0
queue_delay = 0
return
queue_delay++
var/mob/dead/new_player/next_in_line = queued_players[1]
switch(queue_delay)
if(5) //every 5 ticks check if there is a slot available
list_clear_nulls(queued_players)
if(living_player_count() < hard_popcap)
if(next_in_line?.client)
to_chat(next_in_line, span_userdanger("A slot has opened! You have approximately 20 seconds to join. <a href='byond://?src=[REF(next_in_line)];late_join=override'>\>\>Join Game\<\<</a>"))
SEND_SOUND(next_in_line, sound('sound/announcer/notice/notice1.ogg'))
next_in_line.ui_interact(next_in_line)
return
queued_players -= next_in_line //Client disconnected, remove he
queue_delay = 0 //No vacancy: restart timer
if(25 to INFINITY) //No response from the next in line when a vacancy exists, remove he
to_chat(next_in_line, span_danger("No response received. You have been removed from the line."))
queued_players -= next_in_line
queue_delay = 0
///Whether the game has started, including roundend.
/datum/controller/subsystem/ticker/proc/HasRoundStarted()
return current_state >= GAME_STATE_PLAYING
///Whether the game is currently in progress, excluding roundend
/datum/controller/subsystem/ticker/proc/IsRoundInProgress()
return current_state == GAME_STATE_PLAYING
///Whether the game is currently in progress, excluding roundend
/datum/controller/subsystem/ticker/proc/IsPostgame()
return current_state == GAME_STATE_FINISHED
/datum/controller/subsystem/ticker/Recover()
current_state = SSticker.current_state
force_ending = SSticker.force_ending
login_music = SSticker.login_music
round_end_sound = SSticker.round_end_sound
minds = SSticker.minds
delay_end = SSticker.delay_end
tipped = SSticker.tipped
selected_tip = SSticker.selected_tip
timeLeft = SSticker.timeLeft
totalPlayers = SSticker.totalPlayers
totalPlayersReady = SSticker.totalPlayersReady
total_admins_ready = SSticker.total_admins_ready
queue_delay = SSticker.queue_delay
queued_players = SSticker.queued_players
round_start_time = SSticker.round_start_time
queue_delay = SSticker.queue_delay
queued_players = SSticker.queued_players
if (Master) //Set Masters run level if it exists
switch (current_state)
if(GAME_STATE_SETTING_UP)
Master.SetRunLevel(RUNLEVEL_SETUP)
if(GAME_STATE_PLAYING)
Master.SetRunLevel(RUNLEVEL_GAME)
if(GAME_STATE_FINISHED)
Master.SetRunLevel(RUNLEVEL_POSTGAME)
/datum/controller/subsystem/ticker/proc/send_news_report()
var/news_message
var/news_source = "[CITY_NAME] News Network"
var/decoded_station_name = html_decode(station_name()) //decode station_name to avoid minor_announce double encode
var/decoded_emergency_reason = html_decode(emergency_reason)
switch(news_report)
// The nuke was detonated on the syndicate recon outpost
if(NUKE_SYNDICATE_BASE)
news_message = "In a daring raid, the heroic crew of [decoded_station_name] \
detonated a nuclear device in the heart of a terrorist base."
// The station was destroyed by nuke ops
if(STATION_DESTROYED_NUKE)
news_message = "We would like to reassure all employees that the reports of a Syndicate \
backed nuclear attack on [decoded_station_name] are, in fact, a hoax. Have a secure day!"
// The station was evacuated (normal result)
if(STATION_EVACUATED)
// Had an emergency reason supplied to pass along
if(emergency_reason)
news_message = "[decoded_station_name] has been evacuated after transmitting \
the following distress beacon:\n\n[decoded_emergency_reason]"
else
news_message = "The crew of [decoded_station_name] has been \
evacuated amid unconfirmed reports of enemy activity."
// A blob won
if(BLOB_WIN)
news_message = "[decoded_station_name] was overcome by an unknown biological outbreak, killing \
all crew on board. Don't let it happen to you! Remember, a clean work station is a safe work station."
// A blob was destroyed
if(BLOB_DESTROYED)
news_message = "[decoded_station_name] is currently undergoing decontamination procedures \
after the destruction of a biological hazard. As a reminder, any crew members experiencing \
cramps or bloating should report immediately to security for incineration."
// A certain percentage of all cultists managed to escape at the end of round
if(CULT_ESCAPE)
news_message = "Security Alert: A group of religious fanatics have escaped from [decoded_station_name]."
// Cult was completely or almost completely wiped out
if(CULT_FAILURE)
news_message = "Following the dismantling of a restricted cult aboard [decoded_station_name], \
we would like to remind all employees that worship outside of the Chapel is strictly prohibited, \
and cause for termination."
// Cult summoned Nar'sie
if(CULT_SUMMON)
news_message = "Company officials would like to clarify that [decoded_station_name] was scheduled \
to be decommissioned following meteor damage earlier this year. Earlier reports of an \
unknowable eldritch horror were made in error."
// Nuke detonated, but missed the station entirely
if(NUKE_MISS)
news_message = "The Syndicate have bungled a terrorist attack [decoded_station_name], \
detonating a nuclear weapon in empty space nearby."
// All nuke ops got killed
if(OPERATIVES_KILLED)
news_message = "Repairs to [decoded_station_name] are underway after an elite \
Syndicate death squad was wiped out by the crew."
// Nuke ops results inconclusive - Crew escaped without the disk, or nukies were left alive, or something
if(OPERATIVE_SKIRMISH)
news_message = "A skirmish between security forces and Syndicate agents aboard [decoded_station_name] \
ended with both sides bloodied but intact."
// Revolution victory
if(REVS_WIN)
news_message = "Company officials have reassured investors that despite a union led revolt \
aboard [decoded_station_name] there will be no wage increases for workers."
// Revolution defeat
if(REVS_LOSE)
news_message = "[decoded_station_name] quickly put down a misguided attempt at mutiny. \
Remember, unionizing is illegal!"
// All wizards (plus apprentices) have been killed
if(WIZARD_KILLED)
news_message = "Tensions have flared with the Space Wizard Federation following the death \
of one of their members aboard [decoded_station_name]."
// The station was nuked generically
if(STATION_NUKED)
// There was a blob on board, guess it was nuked to stop it
if(length(GLOB.overminds))
for(var/mob/eye/blob/overmind as anything in GLOB.overminds)
if(overmind.max_count < overmind.announcement_size)
continue
news_message = "[decoded_station_name] is currently undergoing decontanimation after a controlled \
burst of radiation was used to remove a biological ooze. All employees were safely evacuated prior, \
and are enjoying a relaxing vacation."
break
// A self destruct or something else
else
news_message = "[decoded_station_name] activated its self-destruct device for unknown reasons. \
Attempts to clone the Captain for arrest and execution are underway."
// The emergency escape shuttle was hijacked
if(SHUTTLE_HIJACK)
news_message = "During routine evacuation procedures, the emergency shuttle of [decoded_station_name] \
had its navigation protocols corrupted and went off course, but was recovered shortly after. \
The following distress beacon was sent prior to evacuation:\n\n[Gibberish(decoded_emergency_reason, FALSE, 8)]"
// A supermatter cascade triggered
if(SUPERMATTER_CASCADE)
news_message = "Officials are advising nearby colonies about a newly declared exclusion zone in \
the sector surrounding [decoded_station_name]."
if(news_message)
send2otherserver(news_source, news_message, "News_Report")
/datum/controller/subsystem/ticker/proc/GetTimeLeft()
if(isnull(SSticker.timeLeft))
return max(0, start_at - world.time)
return timeLeft
/datum/controller/subsystem/ticker/proc/SetTimeLeft(newtime)
if(newtime >= 0 && isnull(timeLeft)) //remember, negative means delayed
start_at = world.time + newtime
else
timeLeft = newtime
/datum/controller/subsystem/ticker/proc/SetRoundEndSound(the_sound)
set waitfor = FALSE
round_end_sound_sent = FALSE
round_end_sound = fcopy_rsc(the_sound)
for(var/thing in GLOB.clients)
var/client/C = thing
if (!C)
continue
C.Export("##action=load_rsc", round_end_sound)
round_end_sound_sent = TRUE
/datum/controller/subsystem/ticker/proc/Reboot(reason, end_string, delay)
set waitfor = FALSE
if(usr && !check_rights(R_SERVER, TRUE))
return
if(!delay)
delay = CONFIG_GET(number/round_end_countdown) * 10
var/skip_delay = check_rights()
if(delay_end && !skip_delay)
to_chat(world, span_boldannounce("An admin has delayed the round end."))
return
to_chat(world, span_boldannounce("Rebooting World in [DisplayTimeText(delay)]. [reason]"))
var/statspage = CONFIG_GET(string/roundstatsurl)
var/gamelogloc = CONFIG_GET(string/gamelogurl)
if(statspage)
to_chat(world, span_info("Round statistics and logs can be viewed <a href=\"[statspage][GLOB.round_id]\">at this website!</a>"))
else if(gamelogloc)
to_chat(world, span_info("Round logs can be located <a href=\"[gamelogloc]\">at this website!</a>"))
var/start_wait = world.time
UNTIL(round_end_sound_sent || (world.time - start_wait) > (delay * 2)) //don't wait forever
if(!isnull(reboot_timer)) //Override existing reboot timers.
deltimer(reboot_timer)
reboot_timer = addtimer(CALLBACK(src, PROC_REF(reboot_callback), reason, end_string), delay - (world.time - start_wait), TIMER_STOPPABLE)
/datum/controller/subsystem/ticker/proc/reboot_callback(reason, end_string)
if(end_string)
end_state = end_string
log_game(span_boldannounce("Rebooting World. [reason]"))
world.Reboot()
/**
* Deletes the current reboot timer and nulls the var
*
* Arguments:
* * user - the user that cancelled the reboot, may be null
*/
/datum/controller/subsystem/ticker/proc/cancel_reboot(mob/user)
if(!reboot_timer)
to_chat(user, span_warning("There is no pending reboot!"))
return FALSE
to_chat(world, span_boldannounce("An admin has delayed the round end."))
deltimer(reboot_timer)
reboot_timer = null
return TRUE
/datum/controller/subsystem/ticker/Shutdown()
gather_newscaster() //called here so we ensure the log is created even upon admin reboot
if(!round_end_sound)
round_end_sound = choose_round_end_song()
for(var/mob/M in GLOB.player_list)
var/pref_volume = M.client.prefs.read_preference(/datum/preference/numeric/volume/sound_midi)
if(pref_volume > 0)
SEND_SOUND(M.client, sound(round_end_sound, volume = pref_volume))
text2file(login_music, "data/last_round_lobby_music.txt")
/datum/controller/subsystem/ticker/proc/choose_round_end_song()
var/list/reboot_sounds = flist("[global.config.directory]/reboot_themes/")
var/list/possible_themes = list()
for(var/themes in reboot_sounds)
possible_themes += themes
if(possible_themes.len)
return "[global.config.directory]/reboot_themes/[pick(possible_themes)]"
/// Updates the lobby music
/// Does not update if override is FALSE and login_music is already set
/datum/controller/subsystem/ticker/proc/set_lobby_music(new_music, override = FALSE)
if(!override && login_music)
return
login_music = new_music
//we just overrode the song, let's update everyone.
if(override)
for(var/mob/dead/new_player/new_player as anything in GLOB.new_player_list)
new_player?.client.playtitlemusic()
#undef ROUND_START_MUSIC_LIST
#undef SS_TICKER_TRAIT