diff --git a/SQL/database_changelog.txt b/SQL/database_changelog.txt
index 434568779af..9f0f2c50c07 100644
--- a/SQL/database_changelog.txt
+++ b/SQL/database_changelog.txt
@@ -1,15 +1,34 @@
Any time you make a change to the schema files, remember to increment the database schema version. Generally increment the minor number, major should be reserved for significant changes to the schema. Both values go up to 255.
-The latest database version is 3.4; The query to update the schema revision table is:
+The latest database version is 4.0; The query to update the schema revision table is:
-INSERT INTO `schema_revision` (`major`, `minor`) VALUES (3, 4);
+INSERT INTO `schema_revision` (`major`, `minor`) VALUES (4, 0);
or
-INSERT INTO `SS13_schema_revision` (`major`, `minor`) VALUES (3, 4);
+INSERT INTO `SS13_schema_revision` (`major`, `minor`) VALUES (4, 0);
In any query remember to add a prefix to the table names if you use one.
----------------------------------------------------
+12 November 2017, by Jordie0608
+Modified feedback table to use json, a python script is used to migrate data to this new format.
+
+See the file 'feedback_conversion_2017-11-12.py' for instructions on how to use the script.
+
+A new json feedback table can be created with:
+CREATE TABLE `feedback` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `datetime` datetime NOT NULL,
+ `round_id` int(11) unsigned NOT NULL,
+ `key_name` varchar(32) NOT NULL,
+ `key_type` enum('text', 'amount', 'tally', 'nested tally', 'associative') NOT NULL,
+ `version` tinyint(3) unsigned NOT NULL,
+ `json` json NOT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=MyISAM
+
+----------------------------------------------------
+
28 August 2017, by MrStonedOne
Modified table 'messages', adding a deleted column and editing all indexes to include it
diff --git a/SQL/feedback_conversion_2017-11-12.py b/SQL/feedback_conversion_2017-11-12.py
new file mode 100644
index 00000000000..b8577460b32
--- /dev/null
+++ b/SQL/feedback_conversion_2017-11-12.py
@@ -0,0 +1,532 @@
+#Python 3+ Script for jsonifying feedback table data as of 2017-11-12 made by Jordie0608
+#Apologies for the boilerplated and squirrely code in parts, this has been my first foray into python
+#
+#Before starting ensure you have installed the mysqlclient package https://github.com/PyMySQL/mysqlclient-python
+#It can be downloaded from command line with pip:
+#pip install mysqlclient
+#
+#You will also have to create a new feedback table for inserting converted data to per the schema:
+#CREATE TABLE `feedback_new` (
+# `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+# `datetime` datetime NOT NULL,
+# `round_id` int(11) unsigned NOT NULL,
+# `key_name` varchar(32) NOT NULL,
+# `key_type` enum('text', 'amount', 'tally', 'nested tally', 'associative') NOT NULL,
+# `version` tinyint(3) unsigned NOT NULL,
+# `json` json NOT NULL,
+# PRIMARY KEY (`id`)
+#) ENGINE=MyISAM
+#This is to prevent the destruction of legacy data and allow rollbacks to be performed in the event of an error during conversion
+#Once conversion is complete remember to rename the old and new feedback tables; it's up to you if you want to keep the old table
+#
+#To view the parameters for this script, execute it with the argument --help
+#All the positional arguments are required, remember to include prefixes in your table names if you use them
+#An example of the command used to execute this script from powershell:
+#python feedback_conversion_2017-11-12.py "localhost" "root" "password" "feedback" "SS13_feedback" "SS13_feedback_new"
+#I found that this script would complete conversion of 10000 rows approximately every 2-3 seconds
+#Depending on the size of your feedback table and the computer used it may take several minutes for the script to finish
+#
+#The script has been tested to complete with tgstation's feedback table as of 2017-10-23 01:34:06
+#Due to the complexity of data that has potentially changed formats multiple times and suffered errors when recording I cannot guarantee it'll always execute successfully
+#In the event of an error the new feedback table is automatically truncated
+#The source table is never modified so you don't have to worry about losing any data due to errors
+#Note that some feedback keys are renamed or coalesced into one, additionnaly some have been entirely removed
+#
+#While this script can be run with your game server(s) active, it may interfere with other database operations and any feedback created after the script has started won't be converted
+
+import MySQLdb
+import argparse
+import json
+import re
+import sys
+from datetime import datetime
+
+def parse_text(details):
+ if not details:
+ return
+ if details.startswith('"') and details.endswith('"'):
+ details = details[1:-1] #the first and last " aren't removed by splitting the dictionary
+ details = details.split('" | "')
+ else:
+ if "_" in details:
+ details = details.split(' ')
+ return details
+
+def parse_tally(details):
+ if not details:
+ return
+ overflowed = None
+ if len(details) >= 65535: #a string this long means the data hit the 64KB character limit of TEXT columns
+ overflowed = True
+ if details.startswith('"') and details.endswith('"'):
+ details = details[details.find('"')+1:details.rfind('"')] #unlike others some of the tally data has extra characters to remove
+ split_details = details.split('" | "')
+ else:
+ split_details = details.split(' ')
+ if overflowed:
+ split_details = split_details[:-1] #since the string overflowed the last element will be incomplete and needs to be ignored
+ details = {}
+ for i in split_details:
+ increment = 1
+ if '|' in i and i[i.find('|')+1:]:
+ increment = float(i[i.find('|')+1:])
+ i = i[:i.find('|')]
+ if i in details:
+ details[i] += increment
+ else:
+ details[i] = increment
+ for i in details:
+ details[i] = '{0:g}'.format(details[i]) #remove .0 from floats that have it to conform with DM
+ return details
+
+def parse_nested(var_name, details):
+ if not details:
+ return
+ #group by data before pipe
+ if var_name in ("admin_toggle", "preferences_verb", "changeling_objective", "cult_objective", "traitor_objective", "wizard_objective", "mining_equipment_bought", "vending_machine_usage", "changeling_powers", "wizard_spell_improved", "testmerged_prs"):
+ if details.startswith('"') and details.endswith('"'):
+ details = details[1:-1]
+ split_details = details.split('" | "')
+ else:
+ split_details = details.split(' ')
+ details = {}
+ for i in split_details:
+ if "|" in i and i[:i.find('|')] not in details:
+ details[i[:i.find('|')]] = {}
+ elif "|" not in i and i[i.find('|')+1:] not in details:
+ details[i[i.find('|')+1:]] = 0
+ for i in split_details:
+ if "|" in i:
+ if details[i[:i.find('|')]] is not dict:
+ continue
+ if i[i.find('|')+1:] in details[i[:i.find('|')]]:
+ details[i[:i.find('|')]][i[i.find('|')+1:]] += 1
+ else:
+ details[i[:i.find('|')]][i[i.find('|')+1:]] = 1
+ else:
+ if i in details and type(details[i]) is not dict: #sometimes keys that should have a value after a pipe just don't and would otherwise error here
+ details[i] += 1
+ return details
+ #group by data after pipe
+ elif var_name in ("cargo_imports", "traitor_uplink_items_bought", "export_sold_cost", "item_used_for_combat", "played_url"):
+ if details.startswith('"') and details.endswith('"'):
+ details = details[1:-1]
+ split_details = details.split('" | "')
+ else:
+ split_details = details.split(' ')
+ details = {}
+ for i in split_details:
+ if i == i[i.rfind('|')+1:]: #there's no pipe and data to group by, so we fill it in
+ i = "{0}|missing data".format(i)
+ details[i[i.rfind('|')+1:]] = {}
+ for i in split_details:
+ if i == i[i.rfind('|')+1:]:
+ i = "{0}|missing data".format(i)
+ if i[:i.find('|')] in details[i[i.rfind('|')+1:]]:
+ details[i[i.rfind('|')+1:]][i[:i.find('|')]] += 1
+ else:
+ details[i[i.rfind('|')+1:]][i[:i.find('|')]] = 1
+ return details
+ elif var_name == "hivelord_core":
+ if details.startswith('"') and details.endswith('"'):
+ details = details[1:-1]
+ split_details = details.split('" | "')
+ else:
+ split_details = details.split(' ')
+ details = {}
+ for i in split_details:
+ if i[:i.find('|')] not in details:
+ details[i[:i.find('|')]] = {}
+ if "used" in i:
+ if "used" not in details:
+ details[i[:i.find('|')]]["used"] = {}
+ for i in split_details:
+ if "used" in i:
+ if "used" not in details[i[:i.find('|')]]:
+ details[i[:i.find('|')]]["used"] = {}
+ details[i[:i.find('|')]]["used"][i[i.rfind('|')+1:]] = 1
+ else:
+ if i[i.rfind('|')+1:] in details[i[:i.find('|')]]["used"]:
+ details[i[:i.find('|')]]["used"][i[i.rfind('|')+1:]] += 1
+ else:
+ details[i[:i.find('|')]]["used"][i[i.rfind('|')+1:]] = 1
+ elif "|" in i:
+ if i[i.find('|')+1:] in details[i[:i.find('|')]]:
+ details[i[:i.find('|')]][i[i.find('|')+1:]] += 1
+ else:
+ details[i[:i.find('|')]][i[i.find('|')+1:]] = 1
+ return details
+ elif var_name == "job_preferences":
+ if details.startswith('"') and details.endswith('"'):
+ details = details[1:-1]
+ split_details = details.split('|-" | "|')
+ else:
+ split_details = details.split('|- |')
+ details = {}
+ for i in split_details:
+ if i.startswith('|'):
+ i = i[1:]
+ if i[:i.find('|')] not in details:
+ details[i[:i.find('|')]] = {}
+ for i in split_details:
+ if i.startswith('|'):
+ i = i[1:]
+ if i.endswith('-'):
+ i = i[:-2]
+ sub_split = i.split('|')
+ job = sub_split[0]
+ sub_split = sub_split[1:]
+ for o in sub_split:
+ details[job][o[:o.find('=')].lower()] = o[o.find('=')+1:]
+ return details
+
+def parse_associative(var_name, details):
+ if not details:
+ return
+ if var_name == "colonies_dropped":
+ if details.startswith('"') and details.endswith('"'):
+ details = details[1:-1]
+ split_details = details.split('|')
+ details = {}
+ details["1"] = {"x" : split_details[0], "y" : split_details[1], "z" : split_details[2]}
+ return details
+ elif var_name == "commendation":
+ if details.startswith('"') and details.endswith('"'):
+ details = details[1:-1]
+ if '}" | "{' in details:
+ split_details = details.split('}" | "{')
+ else:
+ split_details = details.split('} {')
+ details = {}
+ for i in split_details:
+ params = []
+ sub_split = i.split(',')
+ for o in sub_split:
+ o = re.sub('[^A-Za-z0-9 ]', '', o[o.find(':')+1:]) #remove all the formatting and escaped characters from being pre-encoded as json
+ params.append(o)
+ details[len(details)+1] = {"commender" : params[0], "commendee" : params[1], "medal" : params[2], "reason" : params[3]}
+ return details
+ elif var_name == "high_research_level":
+ if details.startswith('"') and details.endswith('"'):
+ details = details[1:-1]
+ split_details = details.split('" | "')
+ else:
+ split_details = details.split(' ')
+ details = {}
+ levels = {}
+ for i in split_details:
+ x = {i[:-1] : i[-1:]}
+ levels.update(x)
+ details["1"] = levels
+ return details
+
+def parse_special(var_name, var_value, details):
+ #old data is essentially a tally in text form
+ if var_name == "immortality_talisman":
+ if details.startswith('"') and details.endswith('"'):
+ split_details = details.split('" | "')
+ else:
+ split_details = details.split(' ')
+ return len(split_details)
+ #now records channel names, so we have to fill in whats missing
+ elif var_name == "newscaster_channels":
+ details = ["missing data"]
+ details *= var_value
+ return details
+ #all the channels got renamed, plus we ignore any with an amount of zero
+ elif var_name == "radio_usage":
+ if details.startswith('"') and details.endswith('"'):
+ details = details[details.find('C'):-1]
+ split_details = details.split('" | "')
+ else:
+ split_details = details.split(' ')
+ details = {}
+ new_keys = {"COM":"common", "SCI":"science", "HEA":"command", "MED":"medical", "ENG":"engineering", "SEC":"security", "DTH":"centcom", "SYN":"syndicate", "SRV":"service", "CAR":"supply", "OTH":"other", "PDA":"PDA", "RC":"request console"}
+ for i in split_details:
+ if i.endswith('0'):
+ continue
+ if i[:i.find('-')] not in new_keys:
+ continue
+ details[new_keys[i[:i.find('-')]]] = i[i.find('-')+1:]
+ return details
+ #all of the data tracked by this is invalid due to recording the incorrect type
+ elif var_name == "shuttle_gib":
+ return {"missing data":1}
+ #all records have a prefix of 'slimebirth_' that needs to be removed
+ elif var_name == "slime_babies_born":
+ if details.startswith('"') and details.endswith('"'):
+ details = details[1:-1]
+ split_details = details.split('" | "')
+ else:
+ split_details = details.split(' ')
+ details = {}
+ for i in split_details:
+ if i[i.find('_')+1:].replace('_', ' ') in details:
+ details[i[i.find('_')+1:].replace('_', ' ')] += 1
+ else:
+ details[i[i.find('_')+1:].replace('_', ' ')] = 1
+ return details
+ #spaces were replaced by underscores, we need to undo this
+ elif var_name == "slime_core_harvested":
+ if details.startswith('"') and details.endswith('"'):
+ details = details[1:-1]
+ split_details = details.split('" | "')
+ else:
+ split_details = details.split(' ')
+ details = {}
+ for i in split_details:
+ if i.replace('_', ' ') in details:
+ details[i.replace('_', ' ')] += 1
+ else:
+ details[i.replace('_', ' ')] = 1
+ return details
+
+def parse_multirow(var_name, var_value, details, multirows_completed):
+ if var_name in ("ahelp_close", "ahelp_icissue", "ahelp_reject", "ahelp_reopen", "ahelp_resolve", "ahelp_unresolved"):
+ ahelp_vars = {"ahelp_close":"closed", "ahelp_icissue":"IC", "ahelp_reject":"rejected", "ahelp_reopen":"reopened", "ahelp_resolve":"resolved", "ahelp_unresolved":"unresolved"}
+ details = {ahelp_vars[var_name]:var_value}
+ del ahelp_vars[var_name]
+ query_where = "round_id = {0} AND (".format(query_row[2])
+ for c, i in enumerate(ahelp_vars):
+ if c:
+ query_where += " OR "
+ query_where += "var_name = \"{0}\"".format(i)
+ query_where += ")"
+ cursor.execute("SELECT var_name, var_value FROM {0} WHERE {1}".format(current_table, query_where))
+ rows = cursor.fetchall()
+ if rows:
+ for r in rows:
+ details[ahelp_vars[r[0]]] = r[1]
+ keys = list(ahelp_vars.keys())
+ keys.append(var_name)
+ multirows_completed += keys
+ return details
+ elif var_name in ("alert_comms_blue", "alert_comms_green"):
+ level_vars = {"alert_comms_blue":"1", "alert_comms_green":"0"}
+ details = {level_vars[var_name]:var_value}
+ del level_vars[var_name]
+ i = list(level_vars)[0]
+ cursor.execute("SELECT var_value FROM {0} WHERE round_id = {1} AND var_name = \"{2}\"".format(current_table, query_row[2], i))
+ row = cursor.fetchone()
+ if row:
+ details[level_vars[i]] = row[0]
+ keys = list(level_vars.keys())
+ keys.append(var_name)
+ multirows_completed += keys
+ return details
+ elif var_name in ("alert_keycard_auth_bsa", "alert_keycard_auth_maint"):
+ auth_vars = {"alert_keycard_auth_maint":("emergency maintenance access", "enabled"), "alert_keycard_auth_bsa":("bluespace artillery", "unlocked")}
+ i = list(auth_vars[var_name])
+ details = {i[0]:{i[1]:var_value}}
+ del auth_vars[var_name]
+ i = list(auth_vars)[0]
+ cursor.execute("SELECT var_value FROM {0} WHERE round_id = {1} AND var_name = \"{2}\"".format(current_table, query_row[2], i))
+ row = cursor.fetchone()
+ if row:
+ o = list(auth_vars[i])
+ details[o[0]] = {o[1]:row[0]}
+ keys = list(auth_vars.keys())
+ keys.append(var_name)
+ multirows_completed += keys
+ return details
+ elif var_name in ("arcade_loss_hp_emagged", "arcade_loss_hp_normal", "arcade_loss_mana_emagged", "arcade_loss_mana_normal", "arcade_win_emagged", "arcade_win_normal"):
+ result_vars = {"arcade_loss_hp_emagged":("loss", "hp", "emagged"), "arcade_loss_hp_normal":("loss", "hp", "normal"), "arcade_loss_mana_emagged":("loss", "mana", "emagged"), "arcade_loss_mana_normal":("loss", "mana", "normal"), "arcade_win_emagged":("win", "emagged"), "arcade_win_normal":("win", "normal")}
+ i = list(result_vars[var_name])
+ del result_vars[var_name]
+ if i[0] == "loss":
+ details = {i[0]:{i[1]:{i[2]:var_value}}}
+ else:
+ details = {i[0]:{i[1]:var_value}}
+ query_where = "round_id = {0} AND (".format(query_row[2])
+ for c, i in enumerate(result_vars):
+ if c:
+ query_where += " OR "
+ query_where += "var_name = \"{0}\"".format(i)
+ query_where += ")"
+ cursor.execute("SELECT var_name, var_value FROM {0} WHERE {1}".format(current_table, query_where))
+ rows = cursor.fetchall()
+ if rows:
+ for r in rows:
+ i = list(result_vars[r[0]])
+ if i[0] not in details:
+ details[i[0]] = {}
+ if i[0] == "loss":
+ if i[1] not in details[i[0]]:
+ details[i[0]][i[1]] = {}
+ details[i[0]][i[1]][i[2]] = r[1]
+ else:
+ details[i[0]][i[1]] = r[1]
+ keys = list(result_vars.keys())
+ keys.append(var_name)
+ multirows_completed += keys
+ return details
+ elif var_name in ("cyborg_engineering", "cyborg_janitor", "cyborg_medical", "cyborg_miner", "cyborg_peacekeeper", "cyborg_security", "cyborg_service", "cyborg_standard"):
+ module_vars = {"cyborg_engineering":"Engineering", "cyborg_janitor":"Janitor", "cyborg_medical":"Medical", "cyborg_miner":"Miner", "cyborg_peacekeeper":"Peacekeeper", "cyborg_security":"Security", "cyborg_service":"Service", "cyborg_standard":"Standard"}
+ details = {module_vars[var_name]:var_value}
+ del module_vars[var_name]
+ query_where = "round_id = {0} AND (".format(query_row[2])
+ for c, i in enumerate(module_vars):
+ if c:
+ query_where += " OR "
+ query_where += "var_name = \"{0}\"".format(i)
+ query_where += ")"
+ cursor.execute("SELECT var_name, var_value FROM {0} WHERE {1}".format(current_table, query_where))
+ rows = cursor.fetchall()
+ if rows:
+ for r in rows:
+ details[module_vars[r[0]]] = r[1]
+ keys = list(module_vars.keys())
+ keys.append(var_name)
+ multirows_completed += keys
+ return details
+ elif var_name in ("escaped_human", "escaped_total", "round_end_clients", "round_end_ghosts", "survived_human", "survived_total"):
+ round_vars = {"escaped_human":("escapees", "human"), "escaped_total":("escapees", "total"), "round_end_clients":("clients"), "round_end_ghosts":("ghosts"), "survived_human":("survivors", "human"), "survived_total":("survivors", "total")}
+ if var_name in ("round_end_clients", "round_end_ghosts"):
+ i = round_vars[var_name]
+ details = {i:var_value}
+ else:
+ i = list(round_vars[var_name])
+ details = {i[0]:{i[1]:var_value}}
+ del round_vars[var_name]
+ query_where = "round_id = {0} AND (".format(query_row[2])
+ for c, i in enumerate(round_vars):
+ if c:
+ query_where += " OR "
+ query_where += "var_name = \"{0}\"".format(i)
+ query_where += ")"
+ cursor.execute("SELECT var_name, var_value FROM {0} WHERE {1}".format(current_table, query_where))
+ rows = cursor.fetchall()
+ if rows:
+ for r in rows:
+ if r[0] in ("round_end_clients", "round_end_ghosts"):
+ i = round_vars[r[0]]
+ details[i] = r[1]
+ else:
+ i = list(round_vars[r[0]])
+ if i[0] not in details:
+ details[i[0]] = {}
+ details[i[0]][i[1]] = r[1]
+ keys = list(round_vars.keys())
+ keys.append(var_name)
+ multirows_completed += keys
+ return details
+ elif var_name in ("mecha_durand_created", "mecha_firefighter_created", "mecha_gygax_created", "mecha_honker_created", "mecha_odysseus_created", "mecha_phazon_created", "mecha_ripley_created"):
+ mecha_vars ={"mecha_durand_created":"Durand", "mecha_firefighter_created":"APLU \"Firefighter\"", "mecha_gygax_created":"Gygax", "mecha_honker_created":"H.O.N.K", "mecha_odysseus_created":"Odysseus", "mecha_phazon_created":"Phazon", "mecha_ripley_created":"APLU \"Ripley\""}
+ details = {mecha_vars[var_name]:var_value}
+ del mecha_vars[var_name]
+ query_where = "round_id = {0} AND (".format(query_row[2])
+ for c, i in enumerate(mecha_vars):
+ if c:
+ query_where += " OR "
+ query_where += "var_name = \"{0}\"".format(i)
+ query_where += ")"
+ cursor.execute("SELECT var_name, var_value FROM {0} WHERE {1}".format(current_table, query_where))
+ rows = cursor.fetchall()
+ if rows:
+ for r in rows:
+ details[mecha_vars[r[0]]] = r[1]
+ keys = list(mecha_vars.keys())
+ keys.append(var_name)
+ multirows_completed += keys
+ return details
+
+def pick_parsing(var_name, var_value, details, multirows_completed):
+ if var_name in text_keys:
+ return parse_text(details)
+ elif var_name in amount_keys:
+ return var_value
+ elif var_name in simple_tallies:
+ return parse_tally(details)
+ elif var_name in nested_tallies:
+ return parse_nested(var_name, details)
+ elif var_name in associatives:
+ return parse_associative(var_name, details)
+ elif var_name in special_cases:
+ return parse_special(var_name, var_value, details)
+ elif var_name in multirow:
+ return parse_multirow(var_name, var_value, details, multirows_completed)
+ else:
+ return False
+
+if sys.version_info[0] < 3:
+ raise Exception("Python must be at least version 3 for this script.")
+text_keys = ["religion_book", "religion_deity", "religion_name", "shuttle_fasttravel", "shuttle_manipulator", "shuttle_purchase", "shuttle_reason", "station_renames"]
+amount_keys = ["admin_cookies_spawned", "cyborg_ais_created", "cyborg_frames_built", "cyborg_mmis_filled", "newscaster_newspapers_printed", "newscaster_stories", "nuclear_challenge_mode"]
+simple_tallies = ["admin_secrets_fun_used", "admin_verb", "assembly_made", "brother_success", "cell_used", "changeling_power_purchase", "changeling_success", "chaplain_weapon", "chemical_reaction", "circuit_printed", "clockcult_scripture_recited", "contamination", "cult_runes_scribed", "engine_started", "event_admin_cancelled", "event_ran", "food_harvested", "food_made", "gun_fired", "handcuffs", "item_deconstructed", "item_printed", "jaunter", "lazarus_injector", "megafauna_kills", "mining_voucher_redeemed", "mobs_killed_mining", "object_crafted", "ore_mined", "pick_used_mining", "slime_cores_used", "surgeries_completed", "time_dilation_current", "traitor_random_uplink_items_gotten", "traitor_success", "voice_of_god", "warp_cube", "wisp_lantern", "wizard_spell_learned", "wizard_success", "zone_targeted"]
+nested_tallies = ["admin_toggle", "cargo_imports", "changeling_objective", "changeling_powers", "cult_objective", "export_sold_cost", "hivelord_core", "item_used_for_combat", "job_preferences", "mining_equipment_bought", "played_url", "preferences_verb", "testmerged_prs", "traitor_objective", "traitor_uplink_items_bought", "vending_machine_usage", "wizard_objective", "wizard_spell_improved"]
+associatives = ["colonies_dropped", "commendation", "high_research_level"]
+special_cases = ["immortality_talisman", "newscaster_channels", "radio_usage", "shuttle_gib", "slime_babies_born", "slime_core_harvested"]
+multirow = ["ahelp_close", "ahelp_icissue", "ahelp_reject", "ahelp_reopen", "ahelp_resolve", "ahelp_unresolved", "alert_comms_blue", "alert_comms_green", "alert_keycard_auth_bsa", "alert_keycard_auth_maint", "arcade_loss_hp_emagged", "arcade_loss_hp_normal", "arcade_loss_mana_emagged", "arcade_loss_mana_normal", "arcade_win_emagged", "arcade_win_normal", "cyborg_engineering", "cyborg_janitor", "cyborg_medical", "cyborg_miner", "cyborg_peacekeeper", "cyborg_security", "cyborg_service", "cyborg_standard", "escaped_human", "escaped_total", "mecha_durand_created", "mecha_firefighter_created", "mecha_gygax_created", "mecha_honker_created", "mecha_odysseus_created", "mecha_phazon_created", "mecha_ripley_created", "round_end_clients", "round_end_ghosts", "survived_human", "survived_total"]
+renames = {"ahelp_stats":["ahelp_close", "ahelp_icissue", "ahelp_reject", "ahelp_reopen", "ahelp_resolve", "ahelp_unresolved"], "ais_created":["cyborg_ais_created"], "arcade_results":["arcade_loss_hp_emagged", "arcade_loss_hp_normal", "arcade_loss_mana_emagged", "arcade_loss_mana_normal", "arcade_win_emagged", "arcade_win_normal"], "cyborg_modules":["cyborg_engineering", "cyborg_janitor", "cyborg_medical", "cyborg_miner", "cyborg_peacekeeper", "cyborg_security", "cyborg_service", "cyborg_standard"], "immortality_talisman_uses":["immortality_talisman"], "keycard_auths":["alert_keycard_auth_bsa", "alert_keycard_auth_maint"], "mechas_created":["mecha_durand_created", "mecha_firefighter_created", "mecha_gygax_created", "mecha_honker_created", "mecha_odysseus_created", "mecha_phazon_created", "mecha_ripley_created"], "mmis_filled":["cyborg_mmis_filled"], "newspapers_printed":["newscaster_newspapers_printed"], "round_end_stats":["escaped_human", "escaped_total", "round_end_clients", "round_end_ghosts", "survived_human", "survived_total"], "security_level_changes":["alert_comms_blue", "alert_comms_green"]}
+key_types = {"amount":["ais_created", "immortality_talisman_uses", "mmis_filled", "newspapers_printed", "admin_cookies_spawned", "cyborg_frames_built", "newscaster_stories", "nuclear_challenge_mode"],
+"associative":["colonies_dropped", "commendation", "high_research_level"],
+"nested tally":["admin_toggle", "arcade_results", "cargo_imports", "changeling_objective", "changeling_powers", "cult_objective", "export_sold_cost", "hivelord_core", "item_used_for_combat", "job_preferences", "keycard_auths", "mining_equipment_bought", "played_url", "preferences_verb", "round_end_stats", "testmerged_prs", "traitor_objective", "traitor_uplink_items_bought", "vending_machine_usage", "wizard_objective", "wizard_spell_improved"],
+"tally":[ "admin_secrets_fun_used", "admin_verb", "ahelp_stats", "assembly_made", "brother_success", "cell_used", "changeling_power_purchase", "changeling_success", "chaplain_weapon", "chemical_reaction", "circuit_printed", "clockcult_scripture_recited", "contamination", "cult_runes_scribed", "cyborg_modules", "engine_started", "event_admin_cancelled", "event_ran", "food_harvested", "food_made", "gun_fired", "handcuffs", "item_deconstructed", "item_printed", "jaunter", "lazarus_injector", "mechas_created", "megafauna_kills", "mining_voucher_redeemed", "mobs_killed_mining", "object_crafted", "ore_mined", "pick_used_mining", "radio_usage", "security_level_changes", "shuttle_gib", "slime_babies_born", "slime_cores_used", "slime_core_harvested", "surgeries_completed", "time_dilation_current", "traitor_random_uplink_items_gotten", "traitor_success", "voice_of_god", "warp_cube", "wisp_lantern", "wizard_spell_learned", "wizard_success", "zone_targeted"],
+"text":["shuttle_fasttravel", "shuttle_manipulator", "shuttle_purchase", "shuttle_reason", "newscaster_channels", "religion_book", "religion_deity", "religion_name", "station_renames"]}
+multirows_completed = []
+query_values = ""
+current_round = 0
+parser = argparse.ArgumentParser()
+parser.add_argument("address", help="MySQL server address (use localhost for the current computer)")
+parser.add_argument("username", help="MySQL login username")
+parser.add_argument("password", help="MySQL login username")
+parser.add_argument("database", help="Database name")
+parser.add_argument("curtable", help="Name of the current feedback table (remember prefixes if you use them)")
+parser.add_argument("newtable", help="Name of the new table to insert to, can't be same as the source table (remember prefixes)")
+args = parser.parse_args()
+db=MySQLdb.connect(host=args.address, user=args.username, passwd=args.password, db=args.database)
+cursor=db.cursor()
+current_table = args.curtable
+new_table = args.newtable
+cursor.execute("SELECT max(id) FROM {0}".format(current_table))
+query_id = cursor.fetchone()
+max_id = query_id[0]
+start_time = datetime.now()
+print("Beginning conversion at {0}".format(start_time.strftime("%Y-%m-%d %H:%M:%S")))
+try:
+ for current_id in range(max_id):
+ if current_id % 10000 == 0:
+ cur_time = datetime.now()
+ print("Reached row ID {0} Duration: {1}".format(current_id, cur_time - start_time))
+ cursor.execute("SELECT * FROM {0} WHERE id = {1}".format(current_table, current_id))
+ query_row = cursor.fetchone()
+ if not query_row:
+ continue
+ else:
+ if current_round != query_row[2]:
+ multirows_completed.clear()
+ if query_values:
+ query_values = query_values[:-1]
+ query_values += ';'
+ cursor.execute("INSERT INTO {0} (datetime, round_id, key_name, key_type, version, json) VALUES {1}".format(new_table, query_values))
+ query_values = ""
+ current_round = query_row[2]
+ if query_row[3] in multirows_completed:
+ continue
+ parsed_data = pick_parsing(query_row[3], query_row[4], query_row[5], multirows_completed)
+ if not parsed_data:
+ continue
+ json_data = {}
+ json_data["data"] = parsed_data
+ new_key = query_row[3]
+ for r in renames:
+ if new_key in renames[r]:
+ new_key = r
+ break
+ new_key_type = None
+ for t in key_types:
+ if new_key in key_types[t]:
+ new_key_type = t
+ break
+ dequoted_json = re.sub("\'", "\\'", json.dumps(json_data))
+ query_values += "('{0}',{1},'{2}','{3}',{4},'{5}'),".format(query_row[1], query_row[2], new_key, new_key_type, 1, dequoted_json)
+ end_time = datetime.now()
+ print("Conversion completed at {0}".format(datetime.now().strftime("%Y-%m-%d %H:%M:%S")))
+ print("Script duration: {0}".format(end_time - start_time))
+except Exception as e:
+ end_time = datetime.now()
+ print("Error encountered on row ID {0} at {1}".format(current_id, datetime.now().strftime("%Y-%m-%d %H:%M:%S")))
+ print("Script duration: {0}".format(end_time - start_time))
+ cursor.execute("TRUNCATE {0} ".format(new_table))
+ raise e
+cursor.close()
diff --git a/SQL/tgstation_schema.sql b/SQL/tgstation_schema.sql
index e463fa6ace3..8ec2fcfc416 100644
--- a/SQL/tgstation_schema.sql
+++ b/SQL/tgstation_schema.sql
@@ -163,12 +163,13 @@ DROP TABLE IF EXISTS `feedback`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `feedback` (
- `id` int(11) NOT NULL AUTO_INCREMENT,
- `time` datetime NOT NULL,
- `round_id` int(8) NOT NULL,
- `var_name` varchar(32) NOT NULL,
- `var_value` int(16) DEFAULT NULL,
- `details` text,
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `datetime` datetime NOT NULL,
+ `round_id` int(11) unsigned NOT NULL,
+ `key_name` varchar(32) NOT NULL,
+ `key_type` enum('text', 'amount', 'tally', 'nested tally', 'associative') NOT NULL,
+ `version` tinyint(3) unsigned NOT NULL,
+ `json` json NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
diff --git a/SQL/tgstation_schema_prefixed.sql b/SQL/tgstation_schema_prefixed.sql
index 01031c017b9..8bc768967de 100644
--- a/SQL/tgstation_schema_prefixed.sql
+++ b/SQL/tgstation_schema_prefixed.sql
@@ -163,12 +163,13 @@ DROP TABLE IF EXISTS `SS13_feedback`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `SS13_feedback` (
- `id` int(11) NOT NULL AUTO_INCREMENT,
- `time` datetime NOT NULL,
- `round_id` int(8) NOT NULL,
- `var_name` varchar(32) NOT NULL,
- `var_value` int(16) DEFAULT NULL,
- `details` text,
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `datetime` datetime NOT NULL,
+ `round_id` int(11) unsigned NOT NULL,
+ `key_name` varchar(32) NOT NULL,
+ `version` tinyint(3) unsigned NOT NULL,
+ `key_type` enum('text', 'amount', 'tally', 'nested tally', 'associative') NOT NULL,
+ `json` json NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
diff --git a/code/_compile_options.dm b/code/_compile_options.dm
index 1f20614c17b..a0b13211f60 100644
--- a/code/_compile_options.dm
+++ b/code/_compile_options.dm
@@ -79,5 +79,5 @@
//Update this whenever the db schema changes
//make sure you add an update to the schema_version stable in the db changelog
-#define DB_MAJOR_VERSION 3
-#define DB_MINOR_VERSION 4
+#define DB_MAJOR_VERSION 4
+#define DB_MINOR_VERSION 0
diff --git a/code/controllers/admin.dm b/code/controllers/admin.dm
index fa10d398dd9..19fef285973 100644
--- a/code/controllers/admin.dm
+++ b/code/controllers/admin.dm
@@ -45,9 +45,9 @@ INITIALIZE_IMMEDIATE(/obj/effect/statclick)
switch(controller)
if("Master")
Recreate_MC()
- SSblackbox.add_details("admin_verb","Restart Master Controller")
+ SSblackbox.record_feedback("tally", "admin_verb", 1, "Restart Master Controller")
if("Failsafe")
new /datum/controller/failsafe()
- SSblackbox.add_details("admin_verb","Restart Failsafe Controller")
+ SSblackbox.record_feedback("tally", "admin_verb", 1, "Restart Failsafe Controller")
message_admins("Admin [key_name_admin(usr)] has restarted the [controller] controller.")
diff --git a/code/controllers/subsystem/blackbox.dm b/code/controllers/subsystem/blackbox.dm
index fe7cf330700..f7abc72cbd9 100644
--- a/code/controllers/subsystem/blackbox.dm
+++ b/code/controllers/subsystem/blackbox.dm
@@ -5,21 +5,11 @@ SUBSYSTEM_DEF(blackbox)
runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME
init_order = INIT_ORDER_BLACKBOX
- var/list/msg_common = list()
- var/list/msg_science = list()
- var/list/msg_command = list()
- var/list/msg_medical = list()
- var/list/msg_engineering = list()
- var/list/msg_security = list()
- var/list/msg_deathsquad = list()
- var/list/msg_syndicate = list()
- var/list/msg_service = list()
- var/list/msg_cargo = list()
- var/list/msg_other = list()
-
var/list/feedback = list() //list of datum/feedback_variable
var/triggertime = 0
var/sealed = FALSE //time to stop tracking stats?
+ var/list/research_levels = list() //list of highest tech levels attained that isn't lost lost by destruction of RD computers
+ var/list/versions = list() //associative list of any feedback variables that have had their format changed since creation and their current version, remember to update this
/datum/controller/subsystem/blackbox/Initialize()
@@ -44,20 +34,7 @@ SUBSYSTEM_DEF(blackbox)
/datum/controller/subsystem/blackbox/Recover()
- msg_common = SSblackbox.msg_common
- msg_science = SSblackbox.msg_science
- msg_command = SSblackbox.msg_command
- msg_medical = SSblackbox.msg_medical
- msg_engineering = SSblackbox.msg_engineering
- msg_security = SSblackbox.msg_security
- msg_deathsquad = SSblackbox.msg_deathsquad
- msg_syndicate = SSblackbox.msg_syndicate
- msg_service = SSblackbox.msg_service
- msg_cargo = SSblackbox.msg_cargo
- msg_other = SSblackbox.msg_other
-
feedback = SSblackbox.feedback
-
sealed = SSblackbox.sealed
//no touchie
@@ -71,32 +48,14 @@ SUBSYSTEM_DEF(blackbox)
/datum/controller/subsystem/blackbox/Shutdown()
sealed = FALSE
- set_val("ahelp_unresolved", GLOB.ahelp_tickets.active_tickets.len)
-
- var/pda_msg_amt = 0
- var/rc_msg_amt = 0
-
+ record_feedback("tally", "ahelp_stats", GLOB.ahelp_tickets.active_tickets.len, "unresolved")
for (var/obj/machinery/message_server/MS in GLOB.message_servers)
- if (MS.pda_msgs.len > pda_msg_amt)
- pda_msg_amt = MS.pda_msgs.len
- if (MS.rc_msgs.len > rc_msg_amt)
- rc_msg_amt = MS.rc_msgs.len
-
- set_details("radio_usage","")
-
- add_details("radio_usage","COM-[msg_common.len]")
- add_details("radio_usage","SCI-[msg_science.len]")
- add_details("radio_usage","HEA-[msg_command.len]")
- add_details("radio_usage","MED-[msg_medical.len]")
- add_details("radio_usage","ENG-[msg_engineering.len]")
- add_details("radio_usage","SEC-[msg_security.len]")
- add_details("radio_usage","DTH-[msg_deathsquad.len]")
- add_details("radio_usage","SYN-[msg_syndicate.len]")
- add_details("radio_usage","SRV-[msg_service.len]")
- add_details("radio_usage","CAR-[msg_cargo.len]")
- add_details("radio_usage","OTH-[msg_other.len]")
- add_details("radio_usage","PDA-[pda_msg_amt]")
- add_details("radio_usage","RC-[rc_msg_amt]")
+ if (MS.pda_msgs.len)
+ record_feedback("tally", "radio_usage", MS.pda_msgs.len, "PDA")
+ if (MS.rc_msgs.len)
+ record_feedback("tally", "radio_usage", MS.rc_msgs.len, "request console")
+ if(research_levels.len)
+ SSblackbox.record_feedback("associative", "high_research_level", 1, research_levels)
if (!SSdbcore.Connect())
return
@@ -104,79 +63,182 @@ SUBSYSTEM_DEF(blackbox)
var/list/sqlrowlist = list()
for (var/datum/feedback_variable/FV in feedback)
- sqlrowlist += list(list("time" = "Now()", "round_id" = GLOB.round_id, "var_name" = "'[sanitizeSQL(FV.get_variable())]'", "var_value" = FV.get_value(), "details" = "'[sanitizeSQL(FV.get_details())]'"))
+ var/sqlversion = 1
+ if(FV.key in versions)
+ sqlversion = versions[FV.key]
+ sqlrowlist += list(list("datetime" = "Now()", "round_id" = GLOB.round_id, "key_name" = "'[sanitizeSQL(FV.key)]'", "key_type" = "'[FV.key_type]'", "version" = "[sqlversion]", "json" = "'[sanitizeSQL(json_encode(FV.json))]'"))
if (!length(sqlrowlist))
return
SSdbcore.MassInsert(format_table_name("feedback"), sqlrowlist, ignore_errors = TRUE, delayed = TRUE)
+/datum/controller/subsystem/blackbox/proc/Seal()
+ if(sealed)
+ return
+ if(IsAdminAdvancedProcCall())
+ message_admins("[key_name_admin(usr)] sealed the blackbox!")
+ log_game("Blackbox sealed[IsAdminAdvancedProcCall() ? " by [key_name(usr)]" : ""].")
+ sealed = TRUE
-/datum/controller/subsystem/blackbox/proc/LogBroadcast(blackbox_msg, freq)
+/datum/controller/subsystem/blackbox/proc/log_research(tech, level)
+ if(!(tech in research_levels) || research_levels[tech] < level)
+ research_levels[tech] = level
+
+/datum/controller/subsystem/blackbox/proc/LogBroadcast(freq)
if(sealed)
return
switch(freq)
if(1459)
- msg_common += blackbox_msg
- if(1351)
- msg_science += blackbox_msg
- if(1353)
- msg_command += blackbox_msg
- if(1355)
- msg_medical += blackbox_msg
- if(1357)
- msg_engineering += blackbox_msg
- if(1359)
- msg_security += blackbox_msg
- if(1441)
- msg_deathsquad += blackbox_msg
- if(1213)
- msg_syndicate += blackbox_msg
- if(1349)
- msg_service += blackbox_msg
- if(1347)
- msg_cargo += blackbox_msg
+ record_feedback("tally", "radio_usage", 1, "common")
+ if(GLOB.SCI_FREQ)
+ record_feedback("tally", "radio_usage", 1, "science")
+ if(GLOB.COMM_FREQ)
+ record_feedback("tally", "radio_usage", 1, "command")
+ if(GLOB.MED_FREQ)
+ record_feedback("tally", "radio_usage", 1, "medical")
+ if(GLOB.ENG_FREQ)
+ record_feedback("tally", "radio_usage", 1, "engineering")
+ if(GLOB.SEC_FREQ)
+ record_feedback("tally", "radio_usage", 1, "security")
+ if(GLOB.SYND_FREQ)
+ record_feedback("tally", "radio_usage", 1, "syndicate")
+ if(GLOB.SERV_FREQ)
+ record_feedback("tally", "radio_usage", 1, "service")
+ if(GLOB.SUPP_FREQ)
+ record_feedback("tally", "radio_usage", 1, "supply")
+ if(GLOB.CENTCOM_FREQ)
+ record_feedback("tally", "radio_usage", 1, "centcom")
+ if(GLOB.AIPRIV_FREQ)
+ record_feedback("tally", "radio_usage", 1, "ai private")
+ if(GLOB.REDTEAM_FREQ)
+ record_feedback("tally", "radio_usage", 1, "CTF red team")
+ if(GLOB.BLUETEAM_FREQ)
+ record_feedback("tally", "radio_usage", 1, "CTF blue team")
else
- msg_other += blackbox_msg
+ record_feedback("tally", "radio_usage", 1, "other")
-/datum/controller/subsystem/blackbox/proc/find_feedback_datum(variable)
+/datum/controller/subsystem/blackbox/proc/find_feedback_datum(key, key_type)
for(var/datum/feedback_variable/FV in feedback)
- if(FV.get_variable() == variable)
+ if(FV.key == key)
return FV
- var/datum/feedback_variable/FV = new(variable)
+ var/datum/feedback_variable/FV = new(key, key_type)
feedback += FV
return FV
+/*
+feedback data can be recorded in 5 formats:
+"text"
+ used for simple single-string records i.e. the current map
+ further calls to the same key will append saved data unless the overwrite argument is true or it already exists
+ when encoded calls made with overwrite will lack square brackets
+ calls: SSblackbox.record_feedback("text", "example", 1, "sample text")
+ SSblackbox.record_feedback("text", "example", 1, "other text")
+ json: {"data":["sample text","other text"]}
+"amount"
+ used to record simple counts of data i.e. the number of ahelps recieved
+ further calls to the same key will add or subtract (if increment argument is a negative) from the saved amount
+ calls: SSblackbox.record_feedback("amount", "example", 8)
+ SSblackbox.record_feedback("amount", "example", 2)
+ json: {"data":10}
+"tally"
+ used to track the number of occurances of multiple related values i.e. how many times each type of gun is fired
+ further calls to the same key will:
+ add or subtract from the saved value of the data key if it already exists
+ append the key and it's value if it doesn't exist
+ calls: SSblackbox.record_feedback("tally", "example", 1, "sample data")
+ SSblackbox.record_feedback("tally", "example", 4, "sample data")
+ SSblackbox.record_feedback("tally", "example", 2, "other data")
+ json: {"data":{"sample data":5,"other data":2}}
+"nested tally"
+ used to track the number of occurances of structured semi-relational values i.e. the results of arcade machines
+ similar to running total, but related values are nested in a multi-dimensional array built
+ the final element in the data list is used as the tracking key, all prior elements are used for nesting
+ further calls to the same key will:
+ add or subtract from the saved value of the data key if it already exists in the same multi-dimensional position
+ append the key and it's value if it doesn't exist
+ calls: SSblackbox.record_feedback("nested tally", "example", 1, list("fruit", "orange", "apricot"))
+ SSblackbox.record_feedback("nested tally", "example", 2, list("fruit", "orange", "orange"))
+ SSblackbox.record_feedback("nested tally", "example", 3, list("fruit", "orange", "apricot"))
+ SSblackbox.record_feedback("nested tally", "example", 10, list("fruit", "red", "apple"))
+ SSblackbox.record_feedback("nested tally", "example", 1, list("vegetable", "orange", "carrot"))
+ json: {"data":{"fruit":{"orange":{"apricot":4,"orange":2},"red":{"apple":10}},"vegetable":{"orange":{"carrot":1}}}}
+ tracking values associated with a number can't merge with a nesting value, trying to do so will append the list
+ call: SSblackbox.record_feedback("nested tally", "example", 3, list("fruit", "orange"))
+ json: {"data":{"fruit":{"orange":{"apricot":4,"orange":2},"red":{"apple":10},"orange":3},"vegetable":{"orange":{"carrot":1}}}}
+"associative"
+ used to record text that's associated with a value i.e. coordinates
+ further calls to the same key will append a new list to existing data
+ calls: SSblackbox.record_feedback("associative", "example", 1, list("text" = "example", "path" = /obj/item, "number" = 4))
+ SSblackbox.record_feedback("associative", "example", 1, list("number" = 7, "text" = "example", "other text" = "sample"))
+ json: {"data":{"1":{"text":"example","path":"/obj/item","number":"4"},"2":{"number":"7","text":"example","other text":"sample"}}}
-/datum/controller/subsystem/blackbox/proc/set_val(variable, value)
- if(sealed)
+Versioning
+ If the format of a feedback variable is ever changed, i.e. how many levels of nesting are used or a new type of data is added to it, add it to the versions list
+ When feedback is being saved if a key is in the versions list the value specified there will be used, otherwise all keys are assumed to be version = 1
+ versions is an associative list, remember to use the same string in it as defined on a feedback variable, example:
+ list/versions = list("round_end_stats" = 4,
+ "admin_toggle" = 2,
+ "gun_fired" = 2)
+*/
+/datum/controller/subsystem/blackbox/proc/record_feedback(key_type, key, increment, data, overwrite)
+ if(sealed || !key_type || !istext(key) || !isnum(increment || !data))
return
- var/datum/feedback_variable/FV = find_feedback_datum(variable)
- FV.set_value(value)
+ 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() //in 512 "pos" can be replaced with "[FV.json["data"].len+1]"
+ for(var/i in data)
+ FV.json["data"]["[pos]"]["[i]"] = "[data[i]]" //and here with "[FV.json["data"].len]"
-/datum/controller/subsystem/blackbox/proc/inc(variable, value)
- if(sealed)
- return
- var/datum/feedback_variable/FV = find_feedback_datum(variable)
- FV.inc(value)
+/datum/controller/subsystem/blackbox/proc/record_feedback_recurse_list(list/L, list/key_list, increment, depth = 1)
+ if(depth == key_list.len)
+ if(L.Find(key_list[depth]))
+ L["[key_list[depth]]"] += increment
+ else
+ var/list/LFI = list(key_list[depth] = increment)
+ L += LFI
+ else
+ if(!L.Find(key_list[depth]))
+ var/list/LGD = list(key_list[depth] = list())
+ L += LGD
+ L["[key_list[depth-1]]"] = .(L["[key_list[depth]]"], key_list, increment, ++depth)
+ return L
-/datum/controller/subsystem/blackbox/proc/dec(variable,value)
- if(sealed)
- return
- var/datum/feedback_variable/FV = find_feedback_datum(variable)
- FV.dec(value)
+/datum/feedback_variable
+ var/key
+ var/key_type
+ var/list/json = list()
-/datum/controller/subsystem/blackbox/proc/set_details(variable,details)
- if(sealed)
- return
- var/datum/feedback_variable/FV = find_feedback_datum(variable)
- FV.set_details(details)
-
-/datum/controller/subsystem/blackbox/proc/add_details(variable,details)
- if(sealed)
- return
- var/datum/feedback_variable/FV = find_feedback_datum(variable)
- FV.add_details(details)
+/datum/feedback_variable/New(new_key, new_key_type)
+ key = new_key
+ key_type = new_key_type
/datum/controller/subsystem/blackbox/proc/ReportDeath(mob/living/L)
if(sealed)
@@ -208,69 +270,3 @@ SUBSYSTEM_DEF(blackbox)
var/map = sanitizeSQL(SSmapping.config.map_name)
var/datum/DBQuery/query_report_death = SSdbcore.NewQuery("INSERT INTO [format_table_name("death")] (pod, x_coord, y_coord, z_coord, mapname, server_ip, server_port, round_id, tod, job, special, name, byondkey, laname, lakey, bruteloss, fireloss, brainloss, oxyloss, toxloss, cloneloss, staminaloss, last_words, suicide) VALUES ('[sqlpod]', '[x_coord]', '[y_coord]', '[z_coord]', '[map]', INET_ATON(IF('[world.internet_address]' LIKE '', '0', '[world.internet_address]')), '[world.port]', [GLOB.round_id], '[SQLtime()]', '[sqljob]', '[sqlspecial]', '[sqlname]', '[sqlkey]', '[laname]', '[lakey]', [sqlbrute], [sqlfire], [sqlbrain], [sqloxy], [sqltox], [sqlclone], [sqlstamina], '[last_words]', [suicide])")
query_report_death.Execute()
-
-/datum/controller/subsystem/blackbox/proc/Seal()
- if(sealed)
- return
- if(IsAdminAdvancedProcCall())
- var/msg = "[key_name_admin(usr)] sealed the blackbox!"
- message_admins(msg)
- log_game("Blackbox sealed[IsAdminAdvancedProcCall() ? " by [key_name(usr)]" : ""].")
- sealed = TRUE
-
-//feedback variable datum, for storing all kinds of data
-/datum/feedback_variable
- var/variable
- var/value
- var/list/details
-
-/datum/feedback_variable/New(param_variable, param_value = 0)
- variable = param_variable
- value = param_value
-
-/datum/feedback_variable/proc/inc(num = 1)
- if (isnum(value))
- value += num
- else
- value = text2num(value)
- if (isnum(value))
- value += num
- else
- value = num
-
-/datum/feedback_variable/proc/dec(num = 1)
- if (isnum(value))
- value -= num
- else
- value = text2num(value)
- if (isnum(value))
- value -= num
- else
- value = -num
-
-/datum/feedback_variable/proc/set_value(num)
- if (isnum(num))
- value = num
-
-/datum/feedback_variable/proc/get_value()
- if (!isnum(value))
- return 0
- return value
-
-/datum/feedback_variable/proc/get_variable()
- return variable
-
-/datum/feedback_variable/proc/set_details(deets)
- details = list("\"[deets]\"")
-
-/datum/feedback_variable/proc/add_details(deets)
- if (!details)
- set_details(deets)
- else
- details += "\"[deets]\""
-
-/datum/feedback_variable/proc/get_details()
- return details ? details.Join(" | ") : null
-
-/datum/feedback_variable/proc/get_parsed()
- return list(variable,value,details.Join(" | "))
diff --git a/code/controllers/subsystem/job.dm b/code/controllers/subsystem/job.dm
index dff38a87c6f..c910ed573c1 100644
--- a/code/controllers/subsystem/job.dm
+++ b/code/controllers/subsystem/job.dm
@@ -450,36 +450,37 @@ SUBSYSTEM_DEF(job)
/datum/controller/subsystem/job/proc/HandleFeedbackGathering()
for(var/datum/job/job in occupations)
- var/tmp_str = "|[job.title]|"
-
- var/level1 = 0 //high
- var/level2 = 0 //medium
- var/level3 = 0 //low
- var/level4 = 0 //never
- var/level5 = 0 //banned
- var/level6 = 0 //account too young
+ var/high = 0 //high
+ var/medium = 0 //medium
+ var/low = 0 //low
+ var/never = 0 //never
+ var/banned = 0 //banned
+ var/young = 0 //account too young
for(var/mob/dead/new_player/player in GLOB.player_list)
if(!(player.ready == PLAYER_READY_TO_PLAY && player.mind && !player.mind.assigned_role))
continue //This player is not ready
if(jobban_isbanned(player, job.title))
- level5++
+ banned++
continue
if(!job.player_old_enough(player.client))
- level6++
+ young++
continue
if(job.required_playtime_remaining(player.client))
- level6++
+ young++
continue
if(player.client.prefs.GetJobDepartment(job, 1) & job.flag)
- level1++
+ high++
else if(player.client.prefs.GetJobDepartment(job, 2) & job.flag)
- level2++
+ medium++
else if(player.client.prefs.GetJobDepartment(job, 3) & job.flag)
- level3++
- else level4++ //not selected
-
- tmp_str += "HIGH=[level1]|MEDIUM=[level2]|LOW=[level3]|NEVER=[level4]|BANNED=[level5]|YOUNG=[level6]|-"
- SSblackbox.add_details("job_preferences",tmp_str)
+ low++
+ else never++ //not selected
+ SSblackbox.record_feedback("nested tally", "job_preferences", high, list("[job.title]", "high"))
+ SSblackbox.record_feedback("nested tally", "job_preferences", medium, list("[job.title]", "medium"))
+ SSblackbox.record_feedback("nested tally", "job_preferences", low, list("[job.title]", "low"))
+ SSblackbox.record_feedback("nested tally", "job_preferences", never, list("[job.title]", "never"))
+ SSblackbox.record_feedback("nested tally", "job_preferences", banned, list("[job.title]", "banned"))
+ SSblackbox.record_feedback("nested tally", "job_preferences", young, list("[job.title]", "young"))
/datum/controller/subsystem/job/proc/PopcapReached()
var/hpc = CONFIG_GET(number/hard_popcap)
diff --git a/code/controllers/subsystem/radiation.dm b/code/controllers/subsystem/radiation.dm
index 7883bf1d68e..0b69e003fc6 100644
--- a/code/controllers/subsystem/radiation.dm
+++ b/code/controllers/subsystem/radiation.dm
@@ -13,6 +13,6 @@ PROCESSING_SUBSYSTEM_DEF(radiation)
return
warned_atoms[ref] = TRUE
var/atom/master = contamination.parent
- SSblackbox.add_details("contaminated", "[master.type]")
+ SSblackbox.record_feedback("tally", "contaminated", 1, master.type)
var/msg = "has become contamintaed with enough radiation to contaminate other objects. || Source: [contamination.source] || Strength: [contamination.strength]"
master.investigate_log(msg, INVESTIGATE_RADIATION)
\ No newline at end of file
diff --git a/code/controllers/subsystem/shuttle.dm b/code/controllers/subsystem/shuttle.dm
index 6d851a4ded5..3a3eb2af961 100644
--- a/code/controllers/subsystem/shuttle.dm
+++ b/code/controllers/subsystem/shuttle.dm
@@ -165,7 +165,7 @@ SUBSYSTEM_DEF(shuttle)
var/mob/M = I
if(M.stat != DEAD)
++alive
-
+
var/total = GLOB.joined_player_list.len
if(alive / total <= threshold)
@@ -245,7 +245,7 @@ SUBSYSTEM_DEF(shuttle)
log_game("[key_name(user)] has called the shuttle.")
if(call_reason)
- SSblackbox.add_details("shuttle_reason", call_reason)
+ SSblackbox.record_feedback("text", "shuttle_reason", 1, "[call_reason]")
log_game("Shuttle call reason: [call_reason]")
message_admins("[key_name_admin(user)] has called the shuttle. (TRIGGER CENTCOM RECALL)")
diff --git a/code/controllers/subsystem/time_track.dm b/code/controllers/subsystem/time_track.dm
index fecba4769cb..5ccfb8af193 100644
--- a/code/controllers/subsystem/time_track.dm
+++ b/code/controllers/subsystem/time_track.dm
@@ -35,4 +35,4 @@ SUBSYSTEM_DEF(time_track)
last_tick_realtime = current_realtime
last_tick_byond_time = current_byondtime
last_tick_tickcount = current_tickcount
- SSblackbox.add_details("time_dilation_current", time_dilation_current)
\ No newline at end of file
+ SSblackbox.record_feedback("tally", "time_dilation_current", 1, time_dilation_current)
diff --git a/code/datums/antagonists/changeling.dm b/code/datums/antagonists/changeling.dm
index 7a3dba2cf30..21f8d1073df 100644
--- a/code/datums/antagonists/changeling.dm
+++ b/code/datums/antagonists/changeling.dm
@@ -179,7 +179,7 @@
to_chat(owner.current, "We have removed our evolutions from this form, and are now ready to readapt.")
reset_powers()
canrespec = 0
- SSblackbox.add_details("changeling_power_purchase","Readapt")
+ SSblackbox.record_feedback("tally", "changeling_power_purchase", 1, "Readapt")
return 1
else
to_chat(owner.current, "You lack the power to readapt your evolutions!")
@@ -279,7 +279,7 @@
if(stored_profiles.len > dna_max)
if(!push_out_profile())
return
-
+
if(!first_prof)
first_prof = prof
@@ -480,4 +480,4 @@
/datum/antagonist/changeling/xenobio
name = "Xenobio Changeling"
give_objectives = FALSE
- you_are_greet = FALSE
\ No newline at end of file
+ you_are_greet = FALSE
diff --git a/code/datums/components/archaeology.dm b/code/datums/components/archaeology.dm
index 7c61613c412..d0799503453 100644
--- a/code/datums/components/archaeology.dm
+++ b/code/datums/components/archaeology.dm
@@ -38,7 +38,7 @@
to_chat(user, "You dig a hole.")
gets_dug()
dug = TRUE
- SSblackbox.add_details("pick_used_mining",W.type)
+ SSblackbox.record_feedback("tally", "pick_used_mining", 1, W.type)
return TRUE
return FALSE
diff --git a/code/datums/helper_datums/construction_datum.dm b/code/datums/helper_datums/construction_datum.dm
index 7768a929daf..313a93943c6 100644
--- a/code/datums/helper_datums/construction_datum.dm
+++ b/code/datums/helper_datums/construction_datum.dm
@@ -68,6 +68,7 @@
var/obj/item/oldcell = locate (/obj/item/stock_parts/cell) in m
QDEL_NULL(oldcell)
m.CheckParts(holder.contents)
+ SSblackbox.record_feedback("tally", "mechas_created", 1, m.name)
QDEL_NULL(holder)
/datum/construction/proc/set_desc(index as num)
diff --git a/code/datums/helper_datums/getrev.dm b/code/datums/helper_datums/getrev.dm
index 6ef3def5476..a9cf11fa7fc 100644
--- a/code/datums/helper_datums/getrev.dm
+++ b/code/datums/helper_datums/getrev.dm
@@ -23,7 +23,7 @@
if(line)
var/tmcommit = testmerge[line]["commit"]
log_world("Test merge active of PR #[line] commit [tmcommit]")
- SSblackbox.add_details("testmerged_prs","[line]|[tmcommit]")
+ SSblackbox.record_feedback("nested tally", "testmerged_prs", 1, list("[line]", "[tmcommit]"))
log_world("Based off origin/master commit [originmastercommit]")
else
log_world(originmastercommit)
diff --git a/code/game/gamemodes/brother/traitor_bro.dm b/code/game/gamemodes/brother/traitor_bro.dm
index ff83a3764c3..be662ed35e3 100644
--- a/code/game/gamemodes/brother/traitor_bro.dm
+++ b/code/game/gamemodes/brother/traitor_bro.dm
@@ -1,6 +1,6 @@
/datum/objective_team/brother_team
- name = "brotherhood"
- member_name = "blood brother"
+ name = "brotherhood"
+ member_name = "blood brother"
var/list/objectives = list()
var/meeting_area
@@ -71,7 +71,7 @@
num_teams = max(1, round(num_players() / bsc))
for(var/j = 1 to num_teams)
- if(possible_brothers.len < min_team_size || antag_candidates.len <= required_enemies)
+ if(possible_brothers.len < min_team_size || antag_candidates.len <= required_enemies)
break
var/datum/objective_team/brother_team/team = new
var/team_size = prob(10) ? min(3, possible_brothers.len) : 2
@@ -114,18 +114,19 @@
for(var/datum/objective/objective in team.objectives)
if(objective.check_completion())
text += "
Objective #[objective_count]: [objective.explanation_text] Success!"
- SSblackbox.add_details("traitor_objective","[objective.type]|SUCCESS")
+ SSblackbox.record_feedback("nested tally", "traitor_objective", 1, list("[objective.type]", "SUCCESS"))
else
text += "
Objective #[objective_count]: [objective.explanation_text] Fail."
- SSblackbox.add_details("traitor_objective","[objective.type]|FAIL")
+ SSblackbox.record_feedback("nested tally", "traitor_objective", 1, list("[objective.type]", "FAIL"))
win = FALSE
objective_count++
if(win)
text += "
The blood brothers were successful!"
- SSblackbox.add_details("brother_success","SUCCESS")
+ SSblackbox.record_feedback("tally", "brother_success", 1, "SUCCESS")
else
text += "
The blood brothers have failed!"
- SSblackbox.add_details("brother_success","FAIL")
+ SSblackbox.record_feedback("tally", "brother_success", 1, "FAIL")
+
text += "
"
to_chat(world, text)
diff --git a/code/game/gamemodes/changeling/changeling.dm b/code/game/gamemodes/changeling/changeling.dm
index da188d9ff74..4b0e753a82a 100644
--- a/code/game/gamemodes/changeling/changeling.dm
+++ b/code/game/gamemodes/changeling/changeling.dm
@@ -115,19 +115,19 @@ GLOBAL_VAR(changeling_team_objective_type) //If this is not null, we hand our th
for(var/datum/objective/objective in changeling.objectives)
if(objective.check_completion())
text += "
Objective #[count]: [objective.explanation_text] Success!"
- SSblackbox.add_details("changeling_objective","[objective.type]|SUCCESS")
+ SSblackbox.record_feedback("nested tally", "changeling_objective", 1, list("[objective.type]", "SUCCESS"))
else
text += "
Objective #[count]: [objective.explanation_text] Fail."
- SSblackbox.add_details("changeling_objective","[objective.type]|FAIL")
+ SSblackbox.record_feedback("nested tally", "changeling_objective", 1, list("[objective.type]", "FAIL"))
changelingwin = 0
count++
if(changelingwin)
text += "
The changeling was successful!"
- SSblackbox.add_details("changeling_success","SUCCESS")
+ SSblackbox.record_feedback("tally", "changeling_success", 1, "SUCCESS")
else
text += "
The changeling has failed."
- SSblackbox.add_details("changeling_success","FAIL")
+ SSblackbox.record_feedback("tally", "changeling_success", 1, "FAIL")
text += "
"
to_chat(world, text)
diff --git a/code/game/gamemodes/changeling/changeling_power.dm b/code/game/gamemodes/changeling/changeling_power.dm
index ee6a0bee8c4..5b07e0e3698 100644
--- a/code/game/gamemodes/changeling/changeling_power.dm
+++ b/code/game/gamemodes/changeling/changeling_power.dm
@@ -19,7 +19,7 @@
/obj/effect/proc_holder/changeling/proc/on_purchase(mob/user, is_respec)
if(!is_respec)
- SSblackbox.add_details("changeling_power_purchase",name)
+ SSblackbox.record_feedback("tally", "changeling_power_purchase", 1, name)
/obj/effect/proc_holder/changeling/proc/on_refund(mob/user)
return
@@ -35,7 +35,7 @@
return
var/datum/antagonist/changeling/c = user.mind.has_antag_datum(/datum/antagonist/changeling)
if(sting_action(user, target))
- SSblackbox.add_details("changeling_powers",name)
+ SSblackbox.record_feedback("nested tally", "changeling_powers", 1, list("[name]"))
sting_feedback(user, target)
c.chem_charges -= chemical_cost
diff --git a/code/game/gamemodes/changeling/powers/absorb.dm b/code/game/gamemodes/changeling/powers/absorb.dm
index d7ef971406d..4584dc5a670 100644
--- a/code/game/gamemodes/changeling/powers/absorb.dm
+++ b/code/game/gamemodes/changeling/powers/absorb.dm
@@ -41,13 +41,13 @@
to_chat(target, "You feel a sharp stabbing pain!")
target.take_overall_damage(40)
- SSblackbox.add_details("changeling_powers","Absorb DNA|[i]")
+ SSblackbox.record_feedback("nested tally", "changeling_powers", 1, list("Absorb DNA", "[i]"))
if(!do_mob(user, target, 150))
to_chat(user, "Our absorption of [target] has been interrupted!")
changeling.isabsorbing = 0
return
- SSblackbox.add_details("changeling_powers","Absorb DNA|4")
+ SSblackbox.record_feedback("nested tally", "changeling_powers", 1, list("Absorb DNA", "4"))
user.visible_message("[user] sucks the fluids from [target]!", "We have absorbed [target].")
to_chat(target, "You are absorbed by the changeling!")
diff --git a/code/game/gamemodes/changeling/powers/linglink.dm b/code/game/gamemodes/changeling/powers/linglink.dm
index c129f035a29..d6ceb4edd0a 100644
--- a/code/game/gamemodes/changeling/powers/linglink.dm
+++ b/code/game/gamemodes/changeling/powers/linglink.dm
@@ -57,7 +57,7 @@
to_chat(target, "You can now communicate in the changeling hivemind, say \":g message\" to communicate!")
target.reagents.add_reagent("salbutamol", 40) // So they don't choke to death while you interrogate them
sleep(1800)
- SSblackbox.add_details("changeling_powers","Hivemind Link|[i]")
+ SSblackbox.record_feedback("nested tally", "changeling_powers", 1, list("[name]", "[i]"))
if(!do_mob(user, target, 20))
to_chat(user, "Our link with [target] has ended!")
changeling.islinking = 0
diff --git a/code/game/gamemodes/clock_cult/clock_scripture.dm b/code/game/gamemodes/clock_cult/clock_scripture.dm
index 8a019ed74ba..94983923f94 100644
--- a/code/game/gamemodes/clock_cult/clock_scripture.dm
+++ b/code/game/gamemodes/clock_cult/clock_scripture.dm
@@ -66,7 +66,7 @@ Applications: 8 servants, 3 caches, and 100 CV
else
successful = TRUE
if(slab && !slab.no_cost && !GLOB.ratvar_awakens) //if the slab exists and isn't debug and ratvar isn't up, log the scripture as being used
- SSblackbox.add_details("clockcult_scripture_recited", name)
+ SSblackbox.record_feedback("tally", "clockcult_scripture_recited", 1, name)
if(slab)
slab.busy = null
post_recital()
diff --git a/code/game/gamemodes/cult/cult.dm b/code/game/gamemodes/cult/cult.dm
index 1391f1dbb5e..f20dd258029 100644
--- a/code/game/gamemodes/cult/cult.dm
+++ b/code/game/gamemodes/cult/cult.dm
@@ -237,27 +237,27 @@
if("survive")
if(!check_survive())
explanation = "Make sure at least [acolytes_needed] acolytes escape on the shuttle. ([acolytes_survived] escaped) Success!"
- SSblackbox.add_details("cult_objective","cult_survive|SUCCESS|[acolytes_needed]")
+ SSblackbox.record_feedback("nested tally", "cult_objective", 1, list("cult_survive", "SUCCESS"))
SSticker.news_report = CULT_ESCAPE
else
explanation = "Make sure at least [acolytes_needed] acolytes escape on the shuttle. ([acolytes_survived] escaped) Fail."
- SSblackbox.add_details("cult_objective","cult_survive|FAIL|[acolytes_needed]")
+ SSblackbox.record_feedback("nested tally", "cult_objective", 1, list("cult_survive", "FAIL"))
SSticker.news_report = CULT_FAILURE
if("sacrifice")
if(GLOB.sac_complete)
explanation = "Sacrifice [GLOB.sac_mind], the [GLOB.sac_mind.assigned_role]. Success!"
- SSblackbox.add_details("cult_objective","cult_sacrifice|SUCCESS")
+ SSblackbox.record_feedback("nested tally", "cult_objective", 1, list("cult_sacrifice", "SUCCESS"))
else
explanation = "Sacrifice [GLOB.sac_mind], the [GLOB.sac_mind.assigned_role]. Fail."
- SSblackbox.add_details("cult_objective","cult_sacrifice|FAIL")
+ SSblackbox.record_feedback("nested tally", "cult_objective", 1, list("cult_sacrifice", "FAIL"))
if("eldergod")
if(!eldergod)
explanation = "Summon Nar-Sie. The summoning can only be accomplished in [english_list(GLOB.summon_spots)].Success!"
- SSblackbox.add_details("cult_objective","cult_narsie|SUCCESS")
+ SSblackbox.record_feedback("nested tally", "cult_objective", 1, list("cult_narsie", "SUCCESS"))
SSticker.news_report = CULT_SUMMON
else
explanation = "Summon Nar-Sie. The summoning can only be accomplished in [english_list(GLOB.summon_spots)]Fail."
- SSblackbox.add_details("cult_objective","cult_narsie|FAIL")
+ SSblackbox.record_feedback("nested tally", "cult_objective", 1, list("cult_narsie", "FAIL"))
SSticker.news_report = CULT_FAILURE
text += "
Objective #[obj_count]: [explanation]"
@@ -297,18 +297,18 @@
if(GLOB.sac_mind)
if(GLOB.sac_complete)
explanation = "Sacrifice [GLOB.sac_mind], the [GLOB.sac_mind.assigned_role]. Success!"
- SSblackbox.add_details("cult_objective","cult_sacrifice|SUCCESS")
+ SSblackbox.record_feedback("nested tally", "cult_objective", 1, list("cult_sacrifice", "SUCCESS"))
else
explanation = "Sacrifice [GLOB.sac_mind], the [GLOB.sac_mind.assigned_role]. Fail."
- SSblackbox.add_details("cult_objective","cult_sacrifice|FAIL")
+ SSblackbox.record_feedback("nested tally", "cult_objective", 1, list("cult_sacrifice", "FAIL"))
if("eldergod")
if(!eldergod)
explanation = "Summon Nar-Sie. Success!"
- SSblackbox.add_details("cult_objective","cult_narsie|SUCCESS")
+ SSblackbox.record_feedback("nested tally", "cult_objective", 1, list("cult_narsie", "SUCCESS"))
SSticker.news_report = CULT_SUMMON
else
explanation = "Summon Nar-Sie. Fail."
- SSblackbox.add_details("cult_objective","cult_narsie|FAIL")
+ SSblackbox.record_feedback("nested tally", "cult_objective", 1, list("cult_narsie", "FAIL"))
SSticker.news_report = CULT_FAILURE
text += "
Objective #[obj_count]: [explanation]"
to_chat(world, text)
diff --git a/code/game/gamemodes/cult/ritual.dm b/code/game/gamemodes/cult/ritual.dm
index b9b1fb4fa0e..0992393581e 100644
--- a/code/game/gamemodes/cult/ritual.dm
+++ b/code/game/gamemodes/cult/ritual.dm
@@ -246,7 +246,7 @@ This file contains the arcane tome files.
var/obj/effect/rune/R = new rune_to_scribe(Turf, chosen_keyword)
R.add_mob_blood(user)
to_chat(user, "The [lowertext(R.cultist_name)] rune [R.cultist_desc]")
- SSblackbox.add_details("cult_runes_scribed", R.cultist_name)
+ SSblackbox.record_feedback("tally", "cult_runes_scribed", 1, R.cultist_name)
/obj/item/tome/proc/check_rune_turf(turf/T, mob/user)
if(isspaceturf(T))
diff --git a/code/game/gamemodes/game_mode.dm b/code/game/gamemodes/game_mode.dm
index 0d49a250419..803e73b9e04 100644
--- a/code/game/gamemodes/game_mode.dm
+++ b/code/game/gamemodes/game_mode.dm
@@ -266,18 +266,19 @@
if(isobserver(M))
ghosts++
- if(clients > 0)
- SSblackbox.set_val("round_end_clients",clients)
- if(ghosts > 0)
- SSblackbox.set_val("round_end_ghosts",ghosts)
- if(surviving_humans > 0)
- SSblackbox.set_val("survived_human",surviving_humans)
- if(surviving_total > 0)
- SSblackbox.set_val("survived_total",surviving_total)
- if(escaped_humans > 0)
- SSblackbox.set_val("escaped_human",escaped_humans)
- if(escaped_total > 0)
- SSblackbox.set_val("escaped_total",escaped_total)
+ if(clients)
+ SSblackbox.record_feedback("nested tally", "round_end_stats", clients, list("clients"))
+ if(ghosts)
+ SSblackbox.record_feedback("nested tally", "round_end_stats", ghosts, list("ghosts"))
+ if(surviving_humans)
+ SSblackbox.record_feedback("nested tally", "round_end_stats", surviving_humans, list("survivors", "human"))
+ if(surviving_total)
+ SSblackbox.record_feedback("nested tally", "round_end_stats", surviving_total, list("survivors", "total"))
+ if(escaped_humans)
+ SSblackbox.record_feedback("nested tally", "round_end_stats", escaped_humans, list("escapees", "human"))
+ if(escaped_total)
+ SSblackbox.record_feedback("nested tally", "round_end_stats", escaped_total, list("escapees", "total"))
+
send2irc("Server", "Round just ended.")
if(cult.len && !istype(SSticker.mode, /datum/game_mode/cult))
datum_cult_completion()
diff --git a/code/game/gamemodes/nuclear/nuclear_challenge.dm b/code/game/gamemodes/nuclear/nuclear_challenge.dm
index 6e7a5673248..c9314ece1a0 100644
--- a/code/game/gamemodes/nuclear/nuclear_challenge.dm
+++ b/code/game/gamemodes/nuclear/nuclear_challenge.dm
@@ -59,7 +59,8 @@
U.hidden_uplink.telecrystals = CHALLENGE_TELECRYSTALS
U.hidden_uplink.set_gamemode(/datum/game_mode/nuclear)
CONFIG_SET(number/shuttle_refuel_delay, max(CONFIG_GET(number/shuttle_refuel_delay), CHALLENGE_SHUTTLE_DELAY))
- SSblackbox.set_val("nuclear_challenge_mode",1)
+ SSblackbox.record_feedback("amount", "nuclear_challenge_mode", 1)
+
qdel(src)
/obj/item/device/nuclear_challenge/proc/check_allowed(mob/living/user)
diff --git a/code/game/gamemodes/traitor/traitor.dm b/code/game/gamemodes/traitor/traitor.dm
index e0c630d3a6f..a72facc59ea 100644
--- a/code/game/gamemodes/traitor/traitor.dm
+++ b/code/game/gamemodes/traitor/traitor.dm
@@ -115,10 +115,10 @@
for(var/datum/objective/objective in traitor.objectives)
if(objective.check_completion())
objectives += "
Objective #[count]: [objective.explanation_text] Success!"
- SSblackbox.add_details("traitor_objective","[objective.type]|SUCCESS")
+ SSblackbox.record_feedback("nested tally", "traitor_objective", 1, list("[objective.type]", "SUCCESS"))
else
objectives += "
Objective #[count]: [objective.explanation_text] Fail."
- SSblackbox.add_details("traitor_objective","[objective.type]|FAIL")
+ SSblackbox.record_feedback("nested tally", "traitor_objective", 1, list("[objective.type]", "FAIL"))
traitorwin = FALSE
count++
@@ -139,10 +139,10 @@
if(traitorwin)
text += "
The [special_role_text] was successful!"
- SSblackbox.add_details("traitor_success","SUCCESS")
+ SSblackbox.record_feedback("tally", "traitor_success", 1, "SUCCESS")
else
text += "
The [special_role_text] has failed!"
- SSblackbox.add_details("traitor_success","FAIL")
+ SSblackbox.record_feedback("tally", "traitor_success", 1, "FAIL")
SEND_SOUND(traitor.current, 'sound/ambience/ambifailure.ogg')
text += "
"
diff --git a/code/game/gamemodes/wizard/spellbook.dm b/code/game/gamemodes/wizard/spellbook.dm
index d88f28f53eb..75c3305b074 100644
--- a/code/game/gamemodes/wizard/spellbook.dm
+++ b/code/game/gamemodes/wizard/spellbook.dm
@@ -57,10 +57,10 @@
aspell.name = "Instant [aspell.name]"
if(aspell.spell_level >= aspell.level_max)
to_chat(user, "This spell cannot be strengthened any further.")
- SSblackbox.add_details("wizard_spell_improved", "[name]|[aspell.level]")
+ SSblackbox.record_feedback("nested tally", "wizard_spell_improved", 1, list("[name]", "[aspell.level]"))
return 1
//No same spell found - just learn it
- SSblackbox.add_details("wizard_spell_learned", name)
+ SSblackbox.record_feedback("tally", "wizard_spell_learned", 1, name)
user.mind.AddSpell(S)
to_chat(user, "You have learned [S.name].")
return 1
@@ -265,7 +265,7 @@
/datum/spellbook_entry/item/Buy(mob/living/carbon/human/user,obj/item/spellbook/book)
new item_path(get_turf(user))
- SSblackbox.add_details("wizard_spell_learned", name)
+ SSblackbox.record_feedback("tally", "wizard_spell_learned", 1, name)
return 1
/datum/spellbook_entry/item/GetInfo()
@@ -464,7 +464,7 @@
return TRUE
/datum/spellbook_entry/summon/ghosts/Buy(mob/living/carbon/human/user, obj/item/spellbook/book)
- SSblackbox.add_details("wizard_spell_learned", name)
+ SSblackbox.record_feedback("tally", "wizard_spell_learned", 1, name)
new /datum/round_event/wizard/ghost()
active = TRUE
to_chat(user, "You have cast summon ghosts!")
@@ -481,7 +481,7 @@
return !CONFIG_GET(flag/no_summon_guns)
/datum/spellbook_entry/summon/guns/Buy(mob/living/carbon/human/user,obj/item/spellbook/book)
- SSblackbox.add_details("wizard_spell_learned", name)
+ SSblackbox.record_feedback("tally", "wizard_spell_learned", 1, name)
rightandwrong(0, user, 25)
active = 1
playsound(get_turf(user), 'sound/magic/castsummon.ogg', 50, 1)
@@ -498,7 +498,7 @@
return !CONFIG_GET(flag/no_summon_magic)
/datum/spellbook_entry/summon/magic/Buy(mob/living/carbon/human/user,obj/item/spellbook/book)
- SSblackbox.add_details("wizard_spell_learned", name)
+ SSblackbox.record_feedback("tally", "wizard_spell_learned", 1, name)
rightandwrong(1, user, 25)
active = 1
playsound(get_turf(user), 'sound/magic/castsummon.ogg', 50, 1)
@@ -516,7 +516,7 @@
return !CONFIG_GET(flag/no_summon_events)
/datum/spellbook_entry/summon/events/Buy(mob/living/carbon/human/user,obj/item/spellbook/book)
- SSblackbox.add_details("wizard_spell_learned", name)
+ SSblackbox.record_feedback("tally", "wizard_spell_learned", 1, name)
summonevents()
times++
playsound(get_turf(user), 'sound/magic/castsummon.ogg', 50, 1)
diff --git a/code/game/gamemodes/wizard/wizard.dm b/code/game/gamemodes/wizard/wizard.dm
index 25999ea61cc..b8e0e61d158 100644
--- a/code/game/gamemodes/wizard/wizard.dm
+++ b/code/game/gamemodes/wizard/wizard.dm
@@ -91,19 +91,19 @@
for(var/datum/objective/objective in wizard.objectives)
if(objective.check_completion())
text += "
Objective #[count]: [objective.explanation_text] Success!"
- SSblackbox.add_details("wizard_objective","[objective.type]|SUCCESS")
+ SSblackbox.record_feedback("nested tally", "wizard_objective", 1, list("[objective.type]", "SUCCESS"))
else
text += "
Objective #[count]: [objective.explanation_text] Fail."
- SSblackbox.add_details("wizard_objective","[objective.type]|FAIL")
+ SSblackbox.record_feedback("nested tally", "wizard_objective", 1, list("[objective.type]", "FAIL"))
wizardwin = 0
count++
if(wizard.current && wizard.current.stat!=2 && wizardwin)
text += "
The wizard was successful!"
- SSblackbox.add_details("wizard_success","SUCCESS")
+ SSblackbox.record_feedback("tally", "wizard_success", 1, "SUCCESS")
else
text += "
The wizard has failed!"
- SSblackbox.add_details("wizard_success","FAIL")
+ SSblackbox.record_feedback("tally", "wizard_success", 1, "FAIL")
if(wizard.spell_list.len>0)
text += "
[wizard.name] used the following spells: "
var/i = 1
diff --git a/code/game/machinery/computer/arcade.dm b/code/game/machinery/computer/arcade.dm
index 3eb9c6d9a2c..607fc388b83 100644
--- a/code/game/machinery/computer/arcade.dm
+++ b/code/game/machinery/computer/arcade.dm
@@ -228,7 +228,6 @@
playsound(loc, 'sound/arcade/win.ogg', 50, 1, extrarange = -3, falloff = 10)
if(emagged)
- SSblackbox.inc("arcade_win_emagged")
new /obj/effect/spawner/newbomb/timer/syndicate(loc)
new /obj/item/clothing/head/collectable/petehat(loc)
message_admins("[key_name_admin(usr)] has outbombed Cuban Pete and been awarded a bomb.")
@@ -236,8 +235,9 @@
Reset()
emagged = FALSE
else
- SSblackbox.inc("arcade_win_normal")
prizevend()
+ SSblackbox.record_feedback("nested tally", "arcade_results", 1, list("win", (emagged ? "emagged":"normal")))
+
else if (emagged && (turtle >= 4))
var/boomamt = rand(5,10)
@@ -258,10 +258,8 @@
temp = "You have been drained! GAME OVER"
playsound(loc, 'sound/arcade/lose.ogg', 50, 1, extrarange = -3, falloff = 10)
if(emagged)
- SSblackbox.inc("arcade_loss_mana_emagged")
usr.gib()
- else
- SSblackbox.inc("arcade_loss_mana_normal")
+ SSblackbox.record_feedback("nested tally", "arcade_results", 1, list("loss", "mana", (emagged ? "emagged":"normal")))
else if ((enemy_hp <= 10) && (enemy_mp > 4))
temp = "[enemy_name] heals for 4 health!"
@@ -280,10 +278,8 @@
temp = "You have been crushed! GAME OVER"
playsound(loc, 'sound/arcade/lose.ogg', 50, 1, extrarange = -3, falloff = 10)
if(emagged)
- SSblackbox.inc("arcade_loss_hp_emagged")
usr.gib()
- else
- SSblackbox.inc("arcade_loss_hp_normal")
+ SSblackbox.record_feedback("nested tally", "arcade_results", 1, list("loss", "hp", (emagged ? "emagged":"normal")))
blocked = FALSE
return
diff --git a/code/game/machinery/computer/communications.dm b/code/game/machinery/computer/communications.dm
index ed8a2f3aece..6223011736b 100755
--- a/code/game/machinery/computer/communications.dm
+++ b/code/game/machinery/computer/communications.dm
@@ -115,11 +115,6 @@
//Only notify the admins if an actual change happened
log_game("[key_name(usr)] has changed the security level to [get_security_level()].")
message_admins("[key_name_admin(usr)] has changed the security level to [get_security_level()].")
- switch(GLOB.security_level)
- if(SEC_LEVEL_GREEN)
- SSblackbox.inc("alert_comms_green",1)
- if(SEC_LEVEL_BLUE)
- SSblackbox.inc("alert_comms_blue",1)
tmp_alertlevel = 0
else
to_chat(usr, "You are not authorized to do this!")
@@ -178,7 +173,7 @@
SSshuttle.points -= S.credit_cost
minor_announce("[usr.name] has purchased [S.name] for [S.credit_cost] credits." , "Shuttle Purchase")
message_admins("[key_name_admin(usr)] purchased [S.name].")
- SSblackbox.add_details("shuttle_purchase", S.name)
+ SSblackbox.record_feedback("text", "shuttle_purchase", 1, "[S.name]")
else
to_chat(usr, "Something went wrong! The shuttle exchange system seems to be down.")
else
@@ -387,11 +382,6 @@
//Only notify the admins if an actual change happened
log_game("[key_name(usr)] has changed the security level to [get_security_level()].")
message_admins("[key_name_admin(usr)] has changed the security level to [get_security_level()].")
- switch(GLOB.security_level)
- if(SEC_LEVEL_GREEN)
- SSblackbox.inc("alert_comms_green",1)
- if(SEC_LEVEL_BLUE)
- SSblackbox.inc("alert_comms_blue",1)
tmp_alertlevel = 0
aistate = STATE_DEFAULT
if("ai-changeseclevel")
diff --git a/code/game/machinery/newscaster.dm b/code/game/machinery/newscaster.dm
index 577e7d23ebd..18acd9ece79 100644
--- a/code/game/machinery/newscaster.dm
+++ b/code/game/machinery/newscaster.dm
@@ -533,7 +533,7 @@ GLOBAL_LIST_EMPTY(allCasters)
if(choice=="Confirm")
scan_user(usr)
GLOB.news_network.CreateFeedChannel(channel_name, scanned_user, c_locked)
- SSblackbox.inc("newscaster_channels",1)
+ SSblackbox.record_feedback("text", "newscaster_channels", 1, "[channel_name]")
screen=5
updateUsrDialog()
else if(href_list["set_channel_receiving"])
@@ -556,7 +556,7 @@ GLOBAL_LIST_EMPTY(allCasters)
screen=6
else
GLOB.news_network.SubmitArticle("[parsemarkdown(msg, usr)]", scanned_user, channel_name, photo, 0, allow_comments)
- SSblackbox.inc("newscaster_stories",1)
+ SSblackbox.record_feedback("amount", "newscaster_stories", 1)
screen=4
msg = ""
updateUsrDialog()
@@ -846,7 +846,7 @@ GLOBAL_LIST_EMPTY(allCasters)
return
/obj/machinery/newscaster/proc/print_paper()
- SSblackbox.inc("newscaster_newspapers_printed",1)
+ SSblackbox.record_feedback("amount", "newspapers_printed", 1)
var/obj/item/newspaper/NEWSPAPER = new /obj/item/newspaper
for(var/datum/newscaster/feed_channel/FC in GLOB.news_network.network_channels)
NEWSPAPER.news_content += FC
diff --git a/code/game/machinery/telecomms/broadcasting.dm b/code/game/machinery/telecomms/broadcasting.dm
index 8ede0a7852a..aebccdddd2e 100644
--- a/code/game/machinery/telecomms/broadcasting.dm
+++ b/code/game/machinery/telecomms/broadcasting.dm
@@ -142,9 +142,7 @@
if(length(receive))
// --- This following recording is intended for research and feedback in the use of department radio channels ---
-
- var/blackbox_msg = "[AM] [AM.say_quote(message, spans)]"
- SSblackbox.LogBroadcast(blackbox_msg, freq)
+ SSblackbox.LogBroadcast(freq)
sleep(50)
if(!QDELETED(virt)) //It could happen to YOU
diff --git a/code/game/machinery/vending.dm b/code/game/machinery/vending.dm
index 7d6037323d5..91b93631d8f 100644
--- a/code/game/machinery/vending.dm
+++ b/code/game/machinery/vending.dm
@@ -510,7 +510,7 @@
if(icon_vend) //Show the vending animation if needed
flick(icon_vend,src)
new R.product_path(get_turf(src))
- SSblackbox.add_details("vending_machine_usage","[type]|[R.product_path]")
+ SSblackbox.record_feedback("nested tally", "vending_machine_usage", 1, list("[type]", "[R.product_path]"))
vend_ready = 1
return
diff --git a/code/game/mecha/mecha_construction_paths.dm b/code/game/mecha/mecha_construction_paths.dm
index d8f435190fa..c8de01badce 100644
--- a/code/game/mecha/mecha_construction_paths.dm
+++ b/code/game/mecha/mecha_construction_paths.dm
@@ -304,13 +304,6 @@
holder.icon_state = "ripley14"
return TRUE
-/datum/construction/reversible/mecha/ripley/spawn_mecha_result()
- ..()
- SSblackbox.inc("mecha_ripley_created",1)
- return
-
-
-
/datum/construction/mecha/gygax_chassis
steps = list(list("key"=/obj/item/mecha_parts/part/gygax_torso), //1
list("key"=/obj/item/mecha_parts/part/gygax_left_arm), //2
@@ -617,11 +610,6 @@
holder.icon_state = "gygax20"
return TRUE
-/datum/construction/reversible/mecha/gygax/spawn_mecha_result()
- ..()
- SSblackbox.inc("mecha_gygax_created",1)
- return
-
/datum/construction/mecha/firefighter_chassis
steps = list(list("key"=/obj/item/mecha_parts/part/ripley_torso), //1
list("key"=/obj/item/mecha_parts/part/ripley_left_arm), //2
@@ -866,13 +854,6 @@
holder.icon_state = "fireripley15"
return TRUE
-/datum/construction/reversible/mecha/firefighter/spawn_mecha_result()
- ..()
- SSblackbox.inc("mecha_firefighter_created",1)
- return
-
-
-
/datum/construction/mecha/honker_chassis
steps = list(list("key"=/obj/item/mecha_parts/part/honker_torso), //1
list("key"=/obj/item/mecha_parts/part/honker_left_arm), //2
@@ -953,10 +934,6 @@
qdel(used_atom)
return TRUE
-/datum/construction/mecha/honker/spawn_mecha_result()
- ..()
- SSblackbox.inc("mecha_honker_created",1)
-
/datum/construction/mecha/durand_chassis
steps = list(list("key"=/obj/item/mecha_parts/part/durand_torso), //1
list("key"=/obj/item/mecha_parts/part/durand_left_arm), //2
@@ -1264,10 +1241,6 @@
holder.icon_state = "durand20"
return TRUE
-/datum/construction/reversible/mecha/durand/spawn_mecha_result()
- ..()
- SSblackbox.inc("mecha_durand_created",1)
-
//PHAZON
/datum/construction/mecha/phazon_chassis
@@ -1620,10 +1593,6 @@
spawn_mecha_result()
return TRUE
-/datum/construction/reversible/mecha/phazon/spawn_mecha_result()
- ..()
- SSblackbox.inc("mecha_phazon_created",1)
-
//ODYSSEUS
/datum/construction/mecha/odysseus_chassis
@@ -1857,7 +1826,3 @@
user.visible_message("[user] unfastens the external armor layer.", "You unfasten the external armor layer.")
holder.icon_state = "odysseus14"
return TRUE
-
-/datum/construction/reversible/mecha/odysseus/spawn_mecha_result()
- ..()
- SSblackbox.inc("mecha_odysseus_created",1)
diff --git a/code/game/objects/items/charter.dm b/code/game/objects/items/charter.dm
index 2527b573d37..c550a0c8726 100644
--- a/code/game/objects/items/charter.dm
+++ b/code/game/objects/items/charter.dm
@@ -45,7 +45,7 @@
if(response_timer_id)
to_chat(user, "You're still waiting for approval from your employers about your proposed name change, it'd be best to wait for now.")
return
-
+
if(!new_name)
return
log_game("[key_name(user)] has proposed to name the station as \
@@ -85,7 +85,7 @@
name = "station charter for [station_name()]"
desc = "An official document entrusting the governance of \
[station_name()] and surrounding space to Captain [uname]."
- SSblackbox.set_details("station_renames","[station_name()]")
+ SSblackbox.record_feedback("text", "station_renames", 1, "[station_name()]")
if(!unlimited_uses)
used = TRUE
@@ -112,7 +112,7 @@
log_game("[ukey] has renamed the planet as [station_name()].")
name = "banner of [station_name()]"
desc = "The banner bears the official coat of arms of Nanotrasen, signifying that [station_name()] has been claimed by Captain [uname] in the name of the company."
- SSblackbox.set_details("station_renames","[station_name()]")
+ SSblackbox.record_feedback("text", "station_renames", 1, "[station_name()]")
if(!unlimited_uses)
used = TRUE
diff --git a/code/game/objects/items/handcuffs.dm b/code/game/objects/items/handcuffs.dm
index b4e5befae29..270c278e603 100644
--- a/code/game/objects/items/handcuffs.dm
+++ b/code/game/objects/items/handcuffs.dm
@@ -47,7 +47,7 @@
if(do_mob(user, C, 30) && (C.get_num_arms() >= 2 || C.get_arm_ignore()))
apply_cuffs(C,user)
to_chat(user, "You handcuff [C].")
- SSblackbox.add_details("handcuffs","[type]")
+ SSblackbox.record_feedback("tally", "handcuffs", 1, type)
add_logs(user, C, "handcuffed")
else
@@ -293,7 +293,7 @@
C.legcuffed = src
src.loc = C
C.update_inv_legcuffed()
- SSblackbox.add_details("handcuffs","[type]")
+ SSblackbox.record_feedback("tally", "handcuffs", 1, type)
else if(isanimal(L))
var/mob/living/simple_animal/SA = L
if(SA.mob_size > MOB_SIZE_TINY)
@@ -354,7 +354,7 @@
C.legcuffed = src
src.loc = C
C.update_inv_legcuffed()
- SSblackbox.add_details("handcuffs","[type]")
+ SSblackbox.record_feedback("tally", "handcuffs", 1, type)
to_chat(C, "\The [src] ensnares you!")
C.Knockdown(knockdown)
diff --git a/code/game/objects/items/holy_weapons.dm b/code/game/objects/items/holy_weapons.dm
index 216df3c2d8b..21e3f26cd7d 100644
--- a/code/game/objects/items/holy_weapons.dm
+++ b/code/game/objects/items/holy_weapons.dm
@@ -42,7 +42,7 @@
SSreligion.holy_weapon_type = holy_weapon.type
- SSblackbox.set_details("chaplain_weapon","[choice]")
+ SSblackbox.record_feedback("tally", "chaplain_weapon", 1, "[choice]")
if(holy_weapon)
holy_weapon.reskinned = TRUE
diff --git a/code/game/objects/items/robot/robot_parts.dm b/code/game/objects/items/robot/robot_parts.dm
index 9c0199e70f1..09e3ccdeb05 100644
--- a/code/game/objects/items/robot/robot_parts.dm
+++ b/code/game/objects/items/robot/robot_parts.dm
@@ -58,7 +58,7 @@
if(src.l_arm && src.r_arm)
if(src.l_leg && src.r_leg)
if(src.chest && src.head)
- SSblackbox.inc("cyborg_frames_built",1)
+ SSblackbox.record_feedback("amount", "cyborg_frames_built", 1)
return 1
return 0
@@ -234,9 +234,7 @@
qdel(O.mmi)
O.mmi = W //and give the real mmi to the borg.
O.updatename()
-
- SSblackbox.inc("cyborg_birth",1)
-
+ SSblackbox.record_feedback("amount", "cyborg_birth", 1)
forceMove(O)
O.robot_suit = src
@@ -335,4 +333,3 @@
add_fingerprint(usr)
Interact(usr)
-
diff --git a/code/game/objects/items/storage/book.dm b/code/game/objects/items/storage/book.dm
index 0712fec2288..a00e97b8fe9 100644
--- a/code/game/objects/items/storage/book.dm
+++ b/code/game/objects/items/storage/book.dm
@@ -65,7 +65,7 @@ GLOBAL_LIST_INIT(bibleitemstates, list("bible", "koran", "scrapbook", "bible",
SSreligion.bible_icon_state = B.icon_state
SSreligion.bible_item_state = B.item_state
- SSblackbox.set_details("religion_book","[biblename]")
+ SSblackbox.record_feedback("text", "religion_book", 1, "[biblename]")
usr << browse(null, "window=editicon")
/obj/item/storage/book/bible/proc/bless(mob/living/carbon/human/H, mob/living/user)
diff --git a/code/game/objects/structures/ai_core.dm b/code/game/objects/structures/ai_core.dm
index d01cd5a191e..4a529571a4e 100644
--- a/code/game/objects/structures/ai_core.dm
+++ b/code/game/objects/structures/ai_core.dm
@@ -185,7 +185,7 @@
if(brain.force_replace_ai_name)
A.fully_replace_character_name(A.name, brain.replacement_ai_name())
- SSblackbox.inc("cyborg_ais_created",1)
+ SSblackbox.record_feedback("amount", "ais_created", 1)
qdel(src)
else
state = AI_READY_CORE
diff --git a/code/game/turfs/simulated/minerals.dm b/code/game/turfs/simulated/minerals.dm
index 1a5d0b78ec7..cbd5b23bf30 100644
--- a/code/game/turfs/simulated/minerals.dm
+++ b/code/game/turfs/simulated/minerals.dm
@@ -66,7 +66,7 @@
if(ismineralturf(src))
to_chat(user, "You finish cutting into the rock.")
gets_drilled(user)
- SSblackbox.add_details("pick_used_mining","[P.type]")
+ SSblackbox.record_feedback("tally", "pick_used_mining", 1, P.type)
else
return attack_hand(user)
@@ -75,7 +75,7 @@
var/i
for(i in 1 to mineralAmt)
new mineralType(src)
- SSblackbox.add_details("ore_mined",mineralType)
+ SSblackbox.record_feedback("tally", "ore_mined", 1, mineralType)
for(var/obj/effect/temp_visual/mining_overlay/M in src)
qdel(M)
ChangeTurf(turf_type, FALSE, defer_change)
diff --git a/code/modules/admin/NewBan.dm b/code/modules/admin/NewBan.dm
index bd32533a7fc..ef222f683d3 100644
--- a/code/modules/admin/NewBan.dm
+++ b/code/modules/admin/NewBan.dm
@@ -146,7 +146,6 @@ GLOBAL_PROTECT(Banlist)
ban_unban_log_save("[key_name(usr)] unbanned [key]")
log_admin_private("[key_name(usr)] unbanned [key]")
message_admins("[key_name_admin(usr)] unbanned: [key]")
- SSblackbox.inc("ban_unban",1)
usr.client.holder.DB_ban_unban( ckey(key), BANTYPE_ANY_FULLBAN)
for (var/A in GLOB.Banlist.dir)
GLOB.Banlist.cd = "/base/[A]"
@@ -235,4 +234,3 @@ GLOBAL_PROTECT(Banlist)
GLOB.Banlist.cd = "/base"
for (var/A in GLOB.Banlist.dir)
RemoveBan(A)
-
diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm
index fb646059bf4..230a77b2235 100644
--- a/code/modules/admin/admin.dm
+++ b/code/modules/admin/admin.dm
@@ -173,7 +173,7 @@
body += "