/
SSblackbox.dm
335 lines (304 loc) · 10.3 KB
/
SSblackbox.dm
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
// Dont touch this subsystem unless you ABSOLUTELY know what you are doing
SUBSYSTEM_DEF(blackbox)
name = "Blackbox"
// On Master.Shutdown(), it shuts down subsystems in the REVERSE order
// The database SS has INIT_ORDER_DBCORE=20, and this SS has INIT_ORDER_BLACKBOX=19
// So putting this ensures it shuts down in the right order
init_order = INIT_ORDER_BLACKBOX
wait = 10 MINUTES
runlevels = RUNLEVEL_LOBBY | RUNLEVELS_DEFAULT
offline_implications = "Player count and admin count statistics will no longer be logged to the database. No immediate action is needed."
cpu_display = SS_CPUDISPLAY_LOW
/// List of all recorded feedback
var/list/datum/feedback_variable/feedback = list()
/// Is it time to stop tracking stats?
var/sealed = FALSE
/// List of highest tech levels attained that isn't lost lost by destruction of RD computers
var/list/research_levels = list()
/// Associative list of any feedback variables that have had their format changed since creation and their current version, remember to update this
var/list/versions = list()
/datum/controller/subsystem/blackbox/Initialize()
if(!SSdbcore.IsConnected())
flags |= SS_NO_FIRE // Disable firing if SQL is disabled
/datum/controller/subsystem/blackbox/fire(resumed = 0)
sql_poll_players()
/datum/controller/subsystem/blackbox/proc/sql_poll_players()
var/datum/db_query/statquery = SSdbcore.NewQuery(
"INSERT INTO legacy_population (playercount, admincount, time, server_id) VALUES (:playercount, :admincount, NOW(), :server_id)",
list(
"playercount" = length(GLOB.clients),
"admincount" = length(GLOB.admins),
"server_id" = GLOB.configuration.system.instance_id
)
)
statquery.warn_execute()
qdel(statquery)
/datum/controller/subsystem/blackbox/Recover()
feedback = SSblackbox.feedback
sealed = SSblackbox.sealed
//no touchie
/datum/controller/subsystem/blackbox/can_vv_get(var_name)
if(var_name == "feedback")
return debug_variable(var_name, deepCopyList(feedback), 0, src)
return ..()
/datum/controller/subsystem/blackbox/vv_edit_var(var_name, var_value)
switch(var_name)
if("feedback")
return FALSE
if("sealed")
if(var_value)
return Seal()
return FALSE
return ..()
/**
* Shutdown Helper
*
* Dumps all feedback stats to the DB. Doesnt get much simpler than that.
*/
/datum/controller/subsystem/blackbox/Shutdown()
sealed = FALSE
for(var/obj/machinery/message_server/MS in GLOB.message_servers)
if(length(MS.pda_msgs))
record_feedback("tally", "radio_usage", length(MS.pda_msgs), "PDA")
if(length(MS.rc_msgs))
record_feedback("tally", "radio_usage", length(MS.rc_msgs), "request console")
if(length(research_levels))
record_feedback("associative", "high_research_level", 1, research_levels)
if(!SSdbcore.IsConnected())
return
var/list/datum/db_query/queries = list()
for(var/datum/feedback_variable/FV in feedback)
var/sqlversion = 1
if(FV.key in versions)
sqlversion = versions[FV.key]
var/datum/db_query/query_feedback_save = SSdbcore.NewQuery({"
INSERT IGNORE INTO feedback (datetime, round_id, key_name, key_type, version, json)
VALUES (NOW(), :rid, :keyname, :keytype, :version, :json)"}, list(
"rid" = text2num(GLOB.round_id),
"keyname" = FV.key,
"keytype" = FV.key_type,
"version" = text2num(sqlversion),
"json" = json_encode(FV.json)
))
queries += query_feedback_save
SSdbcore.MassExecute(queries, TRUE, TRUE)
/**
* Blackbox Sealer
*
* Seals the blackbox, preventing new data from being stored. This is to avoid data being bloated during end round grief
*/
/datum/controller/subsystem/blackbox/proc/Seal()
if(sealed)
return FALSE
log_game("Blackbox sealed")
sealed = TRUE
return TRUE
/**
* Research level broadcast logging helper
*
* This is called on R&D updates for a safe way of logging tech levels if an R&D console is destroyed
*
* Arguments:
* * tech - Research technology name
* * level - Research technology level
*/
/datum/controller/subsystem/blackbox/proc/log_research(tech, level)
if(!(tech in research_levels) || research_levels[tech] < level)
research_levels[tech] = level
/**
* Radio broadcast logging helper
*
* Called during [/proc/broadcast_message()] to log a message to the blackbox.
* Translates the specific frequency to a name
*
* Arguments:
* * freq - Frequency of the transmission
*/
/datum/controller/subsystem/blackbox/proc/LogBroadcast(freq)
if(sealed)
return
switch(freq)
if(PUB_FREQ)
record_feedback("tally", "radio_usage", 1, "common")
if(SCI_FREQ)
record_feedback("tally", "radio_usage", 1, "science")
if(COMM_FREQ)
record_feedback("tally", "radio_usage", 1, "command")
if(MED_FREQ)
record_feedback("tally", "radio_usage", 1, "medical")
if(ENG_FREQ)
record_feedback("tally", "radio_usage", 1, "engineering")
if(SEC_FREQ)
record_feedback("tally", "radio_usage", 1, "security")
if(DTH_FREQ)
record_feedback("tally", "radio_usage", 1, "deathsquad")
if(SYND_FREQ)
record_feedback("tally", "radio_usage", 1, "syndicate")
if(SYNDTEAM_FREQ)
record_feedback("tally", "radio_usage", 1, "syndicate team")
if(SUP_FREQ)
record_feedback("tally", "radio_usage", 1, "supply")
if(SRV_FREQ)
record_feedback("tally", "radio_usage", 1, "service")
if(PROC_FREQ)
record_feedback("tally", "radio_usage", 1, "procedure")
else
record_feedback("tally", "radio_usage", 1, "other")
/**
* Helper to find and return a feeedback datum
*
* Pass in a feedback datum key and key_type to do a lookup.
* It will create the feedback datum if it doesnt exist
*
* Arguments:
* * key - Key of the variable to lookup
* * key_type - Type of feedback to be recorded if the feedback datum cant be found
*/
/datum/controller/subsystem/blackbox/proc/find_feedback_datum(key, key_type)
for(var/datum/feedback_variable/FV in feedback)
if(FV.key == key)
return FV
var/datum/feedback_variable/FV = new(key, key_type)
feedback += FV
return FV
/**
* Main feedback recording proc
*
* This is the bulk of this subsystem and is in charge of creating and using the variables.
* See .github/USING_FEEDBACK_DATA.md for instructions
* Note that feedback is not recorded to the DB during this function. That happens at round end.
*
* Arguments:
* * key_type - Type of key. Either "text", "amount", "tally", "nested tally", "associative"
* * key - Key of the data to be used (EG: "admin_verb")
* * increment - If using "amount", how much to increment why
* * data - The actual data to logged
* * overwrite - Do we want to overwrite the existing key
* * ignore_seal - Does the feedback go in regardless of blackbox sealed status? (EG: map vote results)
*/
/datum/controller/subsystem/blackbox/proc/record_feedback(key_type, key, increment, data, overwrite, ignore_seal)
if((sealed && !ignore_seal) || !key_type || !istext(key) || !isnum(increment || !data))
return
var/datum/feedback_variable/FV = find_feedback_datum(key, key_type)
switch(key_type)
if("text")
if(!istext(data))
return
if(!islist(FV.json["data"]))
FV.json["data"] = list()
if(overwrite)
FV.json["data"] = data
else
FV.json["data"] |= data
if("amount")
FV.json["data"] += increment
if("tally")
if(!islist(FV.json["data"]))
FV.json["data"] = list()
FV.json["data"]["[data]"] += increment
if("nested tally")
if(!islist(data))
return
if(!islist(FV.json["data"]))
FV.json["data"] = list()
FV.json["data"] = record_feedback_recurse_list(FV.json["data"], data, increment)
if("associative")
if(!islist(data))
return
if(!islist(FV.json["data"]))
FV.json["data"] = list()
var/pos = length(FV.json["data"]) + 1
FV.json["data"]["[pos]"] = list()
for(var/i in data)
FV.json["data"]["[pos]"]["[i]"] = "[data[i]]"
/**
* Recursive list recorder
*
* Used by the above proc for nested tallies
*
* Arguments:
* * L - List to use
* * key_list - List of keys to add
* * increment - How much to increase by
* * depth - Depth to use
*/
/datum/controller/subsystem/blackbox/proc/record_feedback_recurse_list(list/L, list/key_list, increment, depth = 1)
if(depth == length(key_list))
if(L.Find(key_list[depth]))
L["[key_list[depth]]"] += increment
else
var/list/list_found_index = list(key_list[depth] = increment)
L += list_found_index
else
if(!L.Find(key_list[depth]))
var/list/list_go_down = list(key_list[depth] = list())
L += list_go_down
L["[key_list[depth-1]]"] = .(L["[key_list[depth]]"], key_list, increment, ++depth)
return L
/**
* # feedback_variable
*
* Datum to hold feedback data, which gets logged at round end
*
* Holds all the information being logged
*/
/datum/feedback_variable
var/key
var/key_type
var/list/json = list()
// Basically just takes some args and sets them
/datum/feedback_variable/New(new_key, new_key_type)
key = new_key
key_type = new_key_type
/**
* Death reporting proc
*
* Called when humans and cyborgs die, and logs death info to the `death` table
*
* Arguments:
* * L - The human or cyborg to be logged
*/
/datum/controller/subsystem/blackbox/proc/ReportDeath(mob/living/L)
if(sealed)
return
if(!SSdbcore.IsConnected())
return
if(!L)
return
if(!L.key || !L.mind)
return
var/area/placeofdeath = get_area(L.loc)
var/podname = "Unknown"
if(placeofdeath)
podname = placeofdeath.name
// Empty string is important here!
var/laname = ""
var/lakey = ""
if(L.lastattacker)
laname = L.lastattacker
if(L.lastattackerckey)
lakey = L.lastattackerckey
var/datum/db_query/deathquery = SSdbcore.NewQuery({"
INSERT INTO death (name, byondkey, job, special, pod, tod, laname, lakey, gender, bruteloss, fireloss, brainloss, oxyloss, coord, server_id, death_rid, last_words)
VALUES (:name, :key, :job, :special, :pod, NOW(), :laname, :lakey, :gender, :bruteloss, :fireloss, :brainloss, :oxyloss, :coord, :server_id, :rid, :last_words)"},
list(
"name" = L.real_name,
"key" = L.key,
"job" = L.mind.assigned_role,
"special" = L.mind.special_role || "",
"pod" = podname,
"laname" = laname,
"lakey" = lakey,
"gender" = L.gender,
"bruteloss" = L.getBruteLoss(),
"fireloss" = L.getFireLoss(),
"brainloss" = L.getBrainLoss(),
"oxyloss" = L.getOxyLoss(),
"coord" = "[L.x], [L.y], [L.z]",
"server_id" = GLOB.configuration.system.instance_id,
"rid" = GLOB.round_id,
"last_words" = length(L.say_log) > 0 ? L.say_log[length(L.say_log)] : null
)
)
deathquery.warn_execute()
qdel(deathquery)