diff --git a/code/__DEFINES/logging.dm b/code/__DEFINES/logging.dm index 7aafdb5c41a..9e0da264669 100644 --- a/code/__DEFINES/logging.dm +++ b/code/__DEFINES/logging.dm @@ -54,6 +54,25 @@ #define LOGSRC_CKEY "Ckey" #define LOGSRC_MOB "Mob" +// Log header keys +#define LOG_HEADER_CATEGORY "category" +#define LOG_HEADER_INIT_TIMESTAMP "timestamp" +#define LOG_HEADER_ROUND_ID "round_id" + +// Log data keys +#define LOG_ENTRY_MESSAGE "message" +#define LOG_ENTRY_TIMESTAMP "timestamp" +#define LOG_ENTRY_DATA "data" + +// Log json keys +#define LOG_JSON_CATEGORY "category" +#define LOG_JSON_ENTRIES "entries" +#define LOG_JSON_LOGGING_START "log_start" + +// Log categories +#define LOG_CATEGORY_NOT_FOUND "invalid_category" +#define LOG_CATEGORY_TARGET_ZONE_SWITCH "target_zone_switch" + //wrapper macros for easier grepping #define DIRECT_OUTPUT(A, B) A << B #define DIRECT_INPUT(A, B) A >> B diff --git a/code/__HELPERS/logging/_logging.dm b/code/__HELPERS/logging/_logging.dm index 0c9044504e7..c1e65aad4d2 100644 --- a/code/__HELPERS/logging/_logging.dm +++ b/code/__HELPERS/logging/_logging.dm @@ -128,6 +128,7 @@ GLOBAL_LIST_INIT(testing_global_profiler, list("_PROFILE_NAME" = "Global")) /* Close open log handles. This should be called as late as possible, and no logging should hapen after. */ /proc/shutdown_logging() rustg_log_close_all() + GLOB.logger.shutdown_logging() /* Helper procs for building detailed log lines */ /proc/key_name(whom, include_link = null, include_name = TRUE) diff --git a/code/__HELPERS/logging/game.dm b/code/__HELPERS/logging/game.dm index ed3318fe8b4..e28a04a8a31 100644 --- a/code/__HELPERS/logging/game.dm +++ b/code/__HELPERS/logging/game.dm @@ -32,3 +32,4 @@ /proc/log_vote(text) if (CONFIG_GET(flag/log_vote)) WRITE_LOG(GLOB.world_game_log, "VOTE: [text]") + diff --git a/code/__HELPERS/type2type.dm b/code/__HELPERS/type2type.dm index e9df60b699e..820ddc91be8 100644 --- a/code/__HELPERS/type2type.dm +++ b/code/__HELPERS/type2type.dm @@ -372,3 +372,7 @@ GLOBAL_LIST_INIT(modulo_angle_to_dir, list(NORTH,NORTHEAST,EAST,SOUTHEAST,SOUTH, /// for use inside of browse() calls to html assets that might be loaded on a cdn. /proc/url2htmlloader(url) return {""} + +/// Formats a larger number to correct textual representation without losing data +/proc/big_number_to_text(number) + return num2text(number, INFINITY) diff --git a/code/_globalvars/logging.dm b/code/_globalvars/logging.dm index 3badd787349..1aeafa36e80 100644 --- a/code/_globalvars/logging.dm +++ b/code/_globalvars/logging.dm @@ -146,4 +146,3 @@ GLOBAL_PROTECT(world_uplink_log) GLOBAL_VAR(world_virus_log) GLOBAL_PROTECT(world_virus_log) - diff --git a/code/_onclick/hud/screen_objects.dm b/code/_onclick/hud/screen_objects.dm index d2715bcb86a..0f1bbc62667 100644 --- a/code/_onclick/hud/screen_objects.dm +++ b/code/_onclick/hud/screen_objects.dm @@ -512,11 +512,13 @@ return BODY_ZONE_PRECISE_EYES return BODY_ZONE_HEAD -/atom/movable/screen/zone_sel/proc/set_selected_zone(choice, mob/user) +/atom/movable/screen/zone_sel/proc/set_selected_zone(choice, mob/user, should_log = TRUE) if(user != hud?.mymob) return if(choice != hud.mymob.zone_selected) + if(should_log) + hud.mymob.log_manual_zone_selected_update("screen_hud", new_target = choice) hud.mymob.zone_selected = choice update_appearance() SEND_SIGNAL(user, COMSIG_MOB_SELECTED_ZONE_SET, choice) diff --git a/code/controllers/configuration/entries/general.dm b/code/controllers/configuration/entries/general.dm index d6f90346c2a..fd108cfdc28 100644 --- a/code/controllers/configuration/entries/general.dm +++ b/code/controllers/configuration/entries/general.dm @@ -103,6 +103,9 @@ /// log voting /datum/config_entry/flag/log_vote +/// log manual zone switching +/datum/config_entry/flag/log_zone_switch + /// log client whisper /datum/config_entry/flag/log_whisper diff --git a/code/datums/components/surgery_initiator.dm b/code/datums/components/surgery_initiator.dm index ab54855f0dd..dee7cfdff01 100644 --- a/code/datums/components/surgery_initiator.dm +++ b/code/datums/components/surgery_initiator.dm @@ -208,7 +208,7 @@ return TRUE var/atom/movable/screen/zone_sel/zone_selector = user.hud_used?.zone_select - zone_selector?.set_selected_zone(zone, user) + zone_selector?.set_selected_zone(zone, user, should_log = FALSE) return TRUE if ("start_surgery") diff --git a/code/datums/json_savefile.dm b/code/datums/json_savefile.dm index 5c498b2441f..8167dd5fd7d 100644 --- a/code/datums/json_savefile.dm +++ b/code/datums/json_savefile.dm @@ -57,6 +57,9 @@ GENERAL_PROTECT_DATUM(/datum/json_savefile) if(path) rustg_file_write(json_encode(tree), path) +/datum/json_savefile/serialize_list(list/options) + return tree.Copy() + /// Traverses the entire dir tree of the given savefile and dynamically assembles the tree from it /datum/json_savefile/proc/import_byond_savefile(savefile/savefile) tree.Cut() diff --git a/code/datums/keybinding/mob.dm b/code/datums/keybinding/mob.dm index eb4fa2f31c3..dd6d7e4582b 100644 --- a/code/datums/keybinding/mob.dm +++ b/code/datums/keybinding/mob.dm @@ -106,132 +106,99 @@ M.toggle_move_intent() return TRUE -/datum/keybinding/mob/target_head_cycle +/datum/keybinding/mob/target/down(client/user) + . = ..() + if(.) + return . + + var/original = user.mob.zone_selected + switch(keybind_signal) + if(COMSIG_KB_MOB_TARGETCYCLEHEAD_DOWN) + user.body_toggle_head() + if(COMSIG_KB_MOB_TARGETEYES_DOWN) + user.body_eyes() + if(COMSIG_KB_MOB_TARGETMOUTH_DOWN) + user.body_mouth() + if(COMSIG_KB_MOB_TARGETRIGHTARM_DOWN) + user.body_r_arm() + if(COMSIG_KB_MOB_TARGETBODYCHEST_DOWN) + user.body_chest() + if(COMSIG_KB_MOB_TARGETLEFTARM_DOWN) + user.body_l_arm() + if(COMSIG_KB_MOB_TARGETRIGHTLEG_DOWN) + user.body_r_leg() + if(COMSIG_KB_MOB_TARGETBODYGROIN_DOWN) + user.body_groin() + if(COMSIG_KB_MOB_TARGETLEFTLEG_DOWN) + user.body_l_leg() + else + stack_trace("Target keybind pressed but not implemented! '[keybind_signal]'") + return FALSE + user.mob.log_manual_zone_selected_update("keybind", old_target = original) + +/datum/keybinding/mob/target/head_cycle hotkey_keys = list("Numpad8") name = "target_head_cycle" full_name = "Target: Cycle Head" description = "Pressing this key targets the head, and continued presses will cycle to the eyes and mouth. This will impact where you hit people, and can be used for surgery." keybind_signal = COMSIG_KB_MOB_TARGETCYCLEHEAD_DOWN -/datum/keybinding/mob/target_head_cycle/down(client/user) - . = ..() - if(.) - return - user.body_toggle_head() - return TRUE - -/datum/keybinding/mob/target_eyes +/datum/keybinding/mob/target/eyes hotkey_keys = list("Numpad7") name = "target_eyes" full_name = "Target: Eyes" description = "Pressing this key targets the eyes. This will impact where you hit people, and can be used for surgery." keybind_signal = COMSIG_KB_MOB_TARGETEYES_DOWN -/datum/keybinding/mob/target_eyes/down(client/user) - . = ..() - if(.) - return - user.body_eyes() - return TRUE - -/datum/keybinding/mob/target_mouth +/datum/keybinding/mob/target/mouth hotkey_keys = list("Numpad9") name = "target_mouths" full_name = "Target: Mouth" description = "Pressing this key targets the mouth. This will impact where you hit people, and can be used for surgery." keybind_signal = COMSIG_KB_MOB_TARGETMOUTH_DOWN -/datum/keybinding/mob/target_mouth/down(client/user) - . = ..() - if(.) - return - user.body_mouth() - return TRUE - -/datum/keybinding/mob/target_r_arm +/datum/keybinding/mob/target/r_arm hotkey_keys = list("Numpad4") name = "target_r_arm" full_name = "Target: right arm" description = "Pressing this key targets the right arm. This will impact where you hit people, and can be used for surgery." keybind_signal = COMSIG_KB_MOB_TARGETRIGHTARM_DOWN -/datum/keybinding/mob/target_r_arm/down(client/user) - . = ..() - if(.) - return - user.body_r_arm() - return TRUE - -/datum/keybinding/mob/target_body_chest +/datum/keybinding/mob/target/body_chest hotkey_keys = list("Numpad5") name = "target_body_chest" full_name = "Target: Body" description = "Pressing this key targets the body. This will impact where you hit people, and can be used for surgery." keybind_signal = COMSIG_KB_MOB_TARGETBODYCHEST_DOWN -/datum/keybinding/mob/target_body_chest/down(client/user) - . = ..() - if(.) - return - user.body_chest() - return TRUE - -/datum/keybinding/mob/target_left_arm +/datum/keybinding/mob/target/left_arm hotkey_keys = list("Numpad6") name = "target_left_arm" full_name = "Target: left arm" description = "Pressing this key targets the body. This will impact where you hit people, and can be used for surgery." keybind_signal = COMSIG_KB_MOB_TARGETLEFTARM_DOWN -/datum/keybinding/mob/target_left_arm/down(client/user) - . = ..() - if(.) - return - user.body_l_arm() - return TRUE - -/datum/keybinding/mob/target_right_leg +/datum/keybinding/mob/target/right_leg hotkey_keys = list("Numpad1") name = "target_right_leg" full_name = "Target: Right leg" description = "Pressing this key targets the right leg. This will impact where you hit people, and can be used for surgery." keybind_signal = COMSIG_KB_MOB_TARGETRIGHTLEG_DOWN -/datum/keybinding/mob/target_right_leg/down(client/user) - . = ..() - if(.) - return - user.body_r_leg() - return TRUE - -/datum/keybinding/mob/target_body_groin +/datum/keybinding/mob/target/body_groin hotkey_keys = list("Numpad2") name = "target_body_groin" full_name = "Target: Groin" description = "Pressing this key targets the groin. This will impact where you hit people, and can be used for surgery." keybind_signal = COMSIG_KB_MOB_TARGETBODYGROIN_DOWN -/datum/keybinding/mob/target_body_groin/down(client/user) - . = ..() - if(.) - return - user.body_groin() - return TRUE - -/datum/keybinding/mob/target_left_leg +/datum/keybinding/mob/target/left_leg hotkey_keys = list("Numpad3") name = "target_left_leg" full_name = "Target: left leg" description = "Pressing this key targets the left leg. This will impact where you hit people, and can be used for surgery." keybind_signal = COMSIG_KB_MOB_TARGETLEFTLEG_DOWN -/datum/keybinding/mob/target_left_leg/down(client/user) - . = ..() - if(.) - return - user.body_l_leg() - return TRUE - /datum/keybinding/mob/prevent_movement hotkey_keys = list("Alt") name = "block_movement" diff --git a/code/game/world.dm b/code/game/world.dm index f1cef7089a0..34c653978f0 100644 --- a/code/game/world.dm +++ b/code/game/world.dm @@ -133,6 +133,7 @@ GLOBAL_VAR(restart_counter) GLOB.picture_logging_prefix = "O_[override_dir]_" GLOB.picture_log_directory = "data/picture_logs/[override_dir]" + GLOB.logger.init_logging() GLOB.demo_log = "[GLOB.log_directory]/demo.log" GLOB.dynamic_log = "[GLOB.log_directory]/dynamic.log" GLOB.filter_log = "[GLOB.log_directory]/filters.log" @@ -165,7 +166,6 @@ GLOBAL_VAR(restart_counter) GLOB.world_uplink_log = "[GLOB.log_directory]/uplink.log" GLOB.world_virus_log = "[GLOB.log_directory]/virus.log" - #ifdef UNIT_TESTS GLOB.test_log = "[GLOB.log_directory]/tests.log" start_log(GLOB.test_log) diff --git a/code/modules/logging/categories/log_category_target_zone_switch.dm b/code/modules/logging/categories/log_category_target_zone_switch.dm new file mode 100644 index 00000000000..2e5b9cc3e1f --- /dev/null +++ b/code/modules/logging/categories/log_category_target_zone_switch.dm @@ -0,0 +1,3 @@ +/datum/log_category/target_zone_switch + category = LOG_CATEGORY_TARGET_ZONE_SWITCH + config_flag = /datum/config_entry/flag/log_zone_switch diff --git a/code/modules/logging/log_category.dm b/code/modules/logging/log_category.dm new file mode 100644 index 00000000000..943ebabf3d3 --- /dev/null +++ b/code/modules/logging/log_category.dm @@ -0,0 +1,32 @@ +/// The main datum that contains all log entries for a category +/datum/log_category + /// The category this datum contains + var/category + /// If set this config flag is checked to enable this log category + var/config_flag + /// List of all entries, in chronological order of when they were added + var/list/entries = list() + +/// Backup log category to catch attempts to log to a category that doesn't exist +/datum/log_category/backup_category_not_found + category = LOG_CATEGORY_NOT_FOUND + +/// Add an entry to this category. It is very important that any data you provide doesn't hold references to anything! +/datum/log_category/proc/add_entry(message, list/data) + var/list/entry = list( + LOG_ENTRY_MESSAGE = message, + LOG_ENTRY_TIMESTAMP = big_number_to_text(rustg_unix_timestamp()), + ) + if(data) + entry[LOG_ENTRY_DATA] = data + + entries += list(entry) + write_entry(entry) + +/// Allows for category specific file splitting. Needs to accept a null entry for the default file. +/datum/log_category/proc/get_output_file(list/entry) + return "[GLOB.log_directory]/[category].json" + +/// Writes an entry to the output file for the category +/datum/log_category/proc/write_entry(list/entry) + rustg_file_append("[json_encode(entry)]\n", get_output_file(entry)) diff --git a/code/modules/logging/log_holder.dm b/code/modules/logging/log_holder.dm new file mode 100644 index 00000000000..c2357ad2cfa --- /dev/null +++ b/code/modules/logging/log_holder.dm @@ -0,0 +1,70 @@ +GLOBAL_DATUM_INIT(logger, /datum/log_holder, new) +GLOBAL_PROTECT(logger) + +/** + * Main datum to manage logging actions + */ +/datum/log_holder + /// Round ID, if set, that logging is initialized for + var/round_id + /// When the log_holder first initialized + var/logging_start_timestamp + + /// Associative: category -> datum + var/list/datum/log_category/log_categories + /// typecache list for categories that exist but are disabled + var/list/disabled_categories + + var/initialized = FALSE + var/shutdown = FALSE + +/// Assembles basic information for logging, creating the log category datums and checking for config flags as required +/datum/log_holder/proc/init_logging() + if(initialized) + CRASH("Attempted to call init_logging twice!") + initialized = TRUE + + round_id = GLOB.round_id + logging_start_timestamp = rustg_unix_timestamp() + log_categories = list() + disabled_categories = list() + for(var/datum/log_category/category_type as anything in subtypesof(/datum/log_category)) + var/category = initial(category_type.category) + if(!category) + continue + + if(category in log_categories) + stack_trace("Found two identical log category type definitions! [category_type]") + continue + + var/config_flag = initial(category_type.config_flag) + if(config_flag && !config.Get(config_flag)) + disabled_categories[category] = TRUE + continue + category_type = log_categories[category] = new category_type + var/list/log_start_entry = list( + LOG_HEADER_CATEGORY = category, + LOG_HEADER_INIT_TIMESTAMP = big_number_to_text(logging_start_timestamp), + LOG_HEADER_ROUND_ID = big_number_to_text(GLOB.round_id), + ) + rustg_file_write("[json_encode(log_start_entry)]\n", category_type.get_output_file(null)) + +/// Tells the log_holder to not allow any more logging to be done, and dumps all categories to their json file +/datum/log_holder/proc/shutdown_logging() + if(shutdown) + CRASH("Attempted to call shutdown_logging twice!") + shutdown = TRUE + +/// This is Log because log is a byond internal proc +/datum/log_holder/proc/Log(category, message, list/data) + if(!initialized || shutdown) + CRASH("Attempted to perform logging before initializion or after shutdown!") + + if(disabled_categories[category]) + return + + var/datum/log_category/log_category = log_categories[category] + if(!log_category) + Log(LOG_CATEGORY_NOT_FOUND, message, data) + CRASH("Attempted to log to a category that doesn't exist! [category]") + log_category.add_entry(message, data) diff --git a/code/modules/mob/mob_helpers.dm b/code/modules/mob/mob_helpers.dm index 17155a9f3e9..78693f347ec 100644 --- a/code/modules/mob/mob_helpers.dm +++ b/code/modules/mob/mob_helpers.dm @@ -474,3 +474,46 @@ else if(iscarbon(mob) || isAI(mob) || isbrain(mob)) divided_health = abs(HEALTH_THRESHOLD_DEAD - mob.health) / abs(HEALTH_THRESHOLD_DEAD - mob.maxHealth) return divided_health * 100 + +/** + * Generates a log message when a user manually changes their targeted zone. + * Only need to one of new_target or old_target, and the other will be auto populated with the current selected zone. + */ +/mob/proc/log_manual_zone_selected_update(source, new_target, old_target) + if(!new_target && !old_target) + CRASH("Called log_manual_zone_selected_update without specifying a new or old target") + + old_target ||= zone_selected + new_target ||= zone_selected + if(old_target == new_target) + return + + var/list/data = list( + "new_target" = new_target, + "old_target" = old_target, + ) + + if(mind?.assigned_role) + data["assigned_role"] = mind.assigned_role.title + if(job) + data["assigned_job"] = job + + var/atom/handitem = get_active_held_item() + if(handitem) + data["active_item"] = list( + "type" = handitem.type, + "name" = handitem.name, + ) + + var/atom/offhand = get_inactive_held_item() + if(offhand) + data["offhand_item"] = list( + "type" = offhand.type, + "name" = offhand.name, + ) + + GLOB.logger.Log( + LOG_CATEGORY_TARGET_ZONE_SWITCH, + "[key_name(src)] manually changed selected zone", + data, + ) diff --git a/config/config.txt b/config/config.txt index 35e1baebe07..103e3959fee 100644 --- a/config/config.txt +++ b/config/config.txt @@ -106,6 +106,9 @@ LOG_GAME ## log player votes LOG_VOTE +## log manual target zone switching +LOG_ZONE_SWITCH + ## log client Whisper LOG_WHISPER diff --git a/tgstation.dme b/tgstation.dme index 5b917c79470..537009e79ac 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -3562,6 +3562,9 @@ #include "code\modules\lighting\lighting_source.dm" #include "code\modules\lighting\lighting_turf.dm" #include "code\modules\lighting\static_lighting_area.dm" +#include "code\modules\logging\log_category.dm" +#include "code\modules\logging\log_holder.dm" +#include "code\modules\logging\categories\log_category_target_zone_switch.dm" #include "code\modules\mafia\_defines.dm" #include "code\modules\mafia\controller.dm" #include "code\modules\mafia\map_pieces.dm"