diff --git a/.github/MC_tab.md b/.github/MC_tab.md new file mode 100644 index 000000000000..01ad1e6aa174 --- /dev/null +++ b/.github/MC_tab.md @@ -0,0 +1,36 @@ +The MC tab hold information on how the game is performing. Here's a crash course on what the most important of those numbers mean. + +If you already know what these numbers mean and you want to see them update faster than the default refresh rate of once every 2 seconds, you can enable the admin pref to make the MC tab refresh every 4 deciseconds. Please don't do this unless you actually need that information at a faster refresh rate since updating every subsystems information is expensive. + +# Main Entries: + + * CPU: What percentage of a tick the game is using before starting the next tick. If this is above 100 it means we are over budget. + + * TickCount: How many ticks should have elapsed since the start of the game if no ticks were ever delayed from starting. + + * TickDrift: How many ticks since the game started that have been delayed. Essentially this is how many ticks the game is running behind. If this is increasing then the game is currently not able to keep up with demand. + + * Internal Tick Usage: You might have heard of this referred to as "maptick". It's how much of the tick that an internal byond function called SendMaps() has taken recently. The higher this is the less time our code has to run. SendMaps() deals with sending players updates of their view of the game world so it has to run every tick but it's expensive so ideally this is optimized as much as possible. You can see a more detailed breakdown of the cost of SendMaps by looking at the profiler in the debug tab -> "Send Maps Profile". + +# Master Controller Entry: + + * TickRate: How many Byond ticks go between each master controller iteration. By default this is 1 meaning the MC runs once every byond tick. But certain configurations can increase this slightly. + + * Iteration: How many times the MC has ran since starting. + + * TickLimit: This SHOULD be what percentage of the tick the MC can use when it starts a run, however currently it just represents how much of the tick the MC can use by the time that SSstatpanels fires. Someone should fix that. + +# Subsystem Entries: + +Subsystems will typically have a base stat entry of the form: +[ ] Name 12ms|28%(2%)|3 + +The brackets hold a letter if the subsystem is in a state other than idle. + +The first numbered entry is the cost of the subsystem, which is a running average of how many milliseconds the subsystem takes to complete a full run. This is increased every time the subsystem resumes an uncompleted run or starts a new run and decays when runs take less time. If this balloons to huge values then it means that the amount of work the subsystem needs to complete in a run is far greater than the amount of time it actually has to execute in whenever it is its turn to fire. + +The second numbered entry is like cost, but in percentage of an ideal tick this subsystem takes to complete a run. They both represent the same data. + +The third entry (2%) is how much time this subsystem spent executing beyond the time it was allocated by the MC. This is bad, it means that this subsystem doesn't yield when it's taking too much time and makes the job of the MC harder. The MC will attempt to account for this but it is better for all subsystems to be able to correctly yield when their turn is done. + +The fourth entry represents how many times this subsystem fires before it completes a run. diff --git a/check_regex.yaml b/check_regex.yaml index df64dec9aae1..f19d67f7d16f 100644 --- a/check_regex.yaml +++ b/check_regex.yaml @@ -38,7 +38,7 @@ standards: - exactly: [ - 295, + 273, "non-bitwise << uses", '(? target_mob.see_invisible) - continue - if(turf_content in overrides) - continue - if(turf_content.IsObscured()) - continue - if(length(turfitems) < 30) // only create images for the first 30 items on the turf, for performance reasons - if(!(REF(turf_content) in cached_images)) - cached_images += REF(turf_content) - turf_content.RegisterSignal(turf_content, COMSIG_PARENT_QDELETING, TYPE_PROC_REF(/atom, remove_from_cache)) // we reset cache if anything in it gets deleted - if(ismob(turf_content) || length(turf_content.overlays) > 2) - turfitems[++turfitems.len] = list("[turf_content.name]", REF(turf_content), costly_icon2html(turf_content, target, sourceonly=TRUE)) - else - turfitems[++turfitems.len] = list("[turf_content.name]", REF(turf_content), icon2html(turf_content, target, sourceonly=TRUE)) - else - turfitems[++turfitems.len] = list("[turf_content.name]", REF(turf_content)) - else - turfitems[++turfitems.len] = list("[turf_content.name]", REF(turf_content)) - turfitems = url_encode(json_encode(turfitems)) - target << output("[turfitems];", "statbrowser:update_listedturf") + var/mob/target_mob = target.mob + if((target.stat_tab in target.spell_tabs) || !length(target.spell_tabs) && (length(target_mob.mob_spell_list) || length(target_mob.mind?.spell_list))) + if(num_fires % default_wait == 0) + set_spells_tab(target, target_mob) + + // Handle the examined turf of the stat panel, if it's been long enough, or if we've generated new images for it + var/turf/listed_turf = target_mob?.listed_turf + if(listed_turf && num_fires % default_wait == 0) + if(target.stat_tab == listed_turf.name || !(listed_turf.name in target.panel_tabs)) + set_turf_examine_tab(target, target_mob) + if(MC_TICK_CHECK) return +/datum/controller/subsystem/statpanels/proc/set_status_tab(client/target) + if(!global_data)//statbrowser hasnt fired yet and we were called from immediate_send_stat_data() + return + + target.stat_panel.send_message("update_stat", list( + "global_data" = global_data, + "ping_str" = "Ping: [round(target.lastping, 1)]ms (Average: [round(target.avgping, 1)]ms)", + "other_str" = target.mob?.get_status_tab_items(), + )) + +/datum/controller/subsystem/statpanels/proc/set_MC_tab(client/target) + var/turf/eye_turf = get_turf(target.eye) + var/coord_entry = COORD(eye_turf) + if(!mc_data) + generate_mc_data() + target.stat_panel.send_message("update_mc", list("mc_data" = mc_data, "coord_entry" = coord_entry)) + +/datum/controller/subsystem/statpanels/proc/set_tickets_tab(client/target) + var/list/ahelp_tickets = GLOB.ahelp_tickets.stat_entry() + target.stat_panel.send_message("update_tickets", ahelp_tickets) + var/datum/interview_manager/m = GLOB.interviews + + // get open interview count + var/dc = 0 + for (var/ckey in m.open_interviews) + var/datum/interview/current_interview = m.open_interviews[ckey] + if (current_interview && !current_interview.owner) + dc++ + var/stat_string = "([m.open_interviews.len - dc] online / [dc] disconnected)" + + // Prepare each queued interview + var/list/queued = list() + for (var/datum/interview/queued_interview in m.interview_queue) + queued += list(list( + "ref" = REF(queued_interview), + "status" = "\[[queued_interview.pos_in_queue]\]: [queued_interview.owner_ckey][!queued_interview.owner ? " (DC)": ""] \[INT-[queued_interview.id]\]" + )) + + var/list/data = list( + "status" = list( + "Active:" = "[m.open_interviews.len] [stat_string]", + "Queued:" = "[m.interview_queue.len]", + "Closed:" = "[m.closed_interviews.len]"), + "interviews" = queued + ) + + // Push update + target.stat_panel.send_message("update_interviews", data) + +/datum/controller/subsystem/statpanels/proc/set_SDQL2_tab(client/target) + var/list/sdql2A = list() + sdql2A[++sdql2A.len] = list("", "Access Global SDQL2 List", REF(GLOB.sdql2_vv_statobj)) + var/list/sdql2B = list() + for(var/datum/SDQL2_query/query as anything in GLOB.sdql2_queries) + sdql2B = query.generate_stat() + + sdql2A += sdql2B + target.stat_panel.send_message("update_sdql2", sdql2A) + +/datum/controller/subsystem/statpanels/proc/set_spells_tab(client/target, mob/target_mob) + var/list/proc_holders = target_mob.get_proc_holders() + target.spell_tabs.Cut() + + for(var/proc_holder_list as anything in proc_holders) + target.spell_tabs |= proc_holder_list[1] + + target.stat_panel.send_message("update_spells", list(spell_tabs = target.spell_tabs, proc_holders_encoded = proc_holders)) + +/datum/controller/subsystem/statpanels/proc/set_turf_examine_tab(client/target, mob/target_mob) + var/list/overrides = list() + for(var/image/target_image as anything in target.images) + if(!target_image.loc || target_image.loc.loc != target_mob.listed_turf || !target_image.override) + continue + overrides += target_image.loc + + var/list/atoms_to_display = list(target_mob.listed_turf) + for(var/atom/movable/turf_content as anything in target_mob.listed_turf) + if(turf_content.mouse_opacity == MOUSE_OPACITY_TRANSPARENT) + continue + if(turf_content.invisibility > target_mob.see_invisible) + continue + if(turf_content in overrides) + continue + if(turf_content.IsObscured()) + continue + atoms_to_display += turf_content + + /// Set the atoms we're meant to display + var/datum/object_window_info/obj_window = target.obj_window + obj_window.atoms_to_show = atoms_to_display + START_PROCESSING(SSobj_tab_items, obj_window) + refresh_client_obj_view(target) + +/datum/controller/subsystem/statpanels/proc/refresh_client_obj_view(client/refresh) + var/list/turf_items = return_object_images(refresh) + if(!length(turf_items) || !refresh.mob?.listed_turf) + return + refresh.stat_panel.send_message("update_listedturf", turf_items) + +#define OBJ_IMAGE_LOADING "statpanels obj loading temporary" +/// Returns all our ready object tab images +/// Returns a list in the form list(list(object_name, object_ref, loaded_image), ...) +/datum/controller/subsystem/statpanels/proc/return_object_images(client/load_from) + // You might be inclined to think that this is a waste of cpu time, since we + // A: Double iterate over atoms in the build case, or + // B: Generate these lists over and over in the refresh case + // It's really not very hot. The hot portion of this code is genuinely mostly in the image generation + // So it's ok to pay a performance cost for cleanliness here + + // No turf? go away + if(!load_from.mob?.listed_turf) + return list() + var/datum/object_window_info/obj_window = load_from.obj_window + var/list/already_seen = obj_window.atoms_to_images + var/list/to_make = obj_window.atoms_to_imagify + var/list/turf_items = list() + for(var/atom/turf_item as anything in obj_window.atoms_to_show) + // First, we fill up the list of refs to display + // If we already have one, just use that + var/existing_image = already_seen[turf_item] + if(existing_image == OBJ_IMAGE_LOADING) + continue + // We already have it. Success! + if(existing_image) + turf_items[++turf_items.len] = list("[turf_item.name]", REF(turf_item), existing_image) + continue + // Now, we're gonna queue image generation out of those refs + to_make += turf_item + already_seen[turf_item] = OBJ_IMAGE_LOADING + obj_window.RegisterSignal(turf_item, COMSIG_PARENT_QDELETING, /datum/object_window_info/proc/viewing_atom_deleted) // we reset cache if anything in it gets deleted + return turf_items + +#undef OBJ_IMAGE_LOADING /datum/controller/subsystem/statpanels/proc/generate_mc_data() - var/list/mc_data = list( + mc_data = list( list("CPU:", world.cpu), list("Instances:", "[num2text(world.contents.len, 10)]"), list("World Time:", "[world.time]"), @@ -162,45 +235,147 @@ SUBSYSTEM_DEF(statpanels) for(var/datum/controller/subsystem/sub_system as anything in Master.subsystems) mc_data[++mc_data.len] = list("\[[sub_system.state_letter()]][sub_system.name]", sub_system.stat_entry(), text_ref(sub_system)) mc_data[++mc_data.len] = list("Camera Net", "Cameras: [GLOB.cameranet.cameras.len] | Chunks: [GLOB.cameranet.chunks.len]", text_ref(GLOB.cameranet)) - mc_data_encoded = url_encode(json_encode(mc_data)) -/atom/proc/remove_from_cache() - SSstatpanels.cached_images -= REF(src) +///immediately update the active statpanel tab of the target client +/datum/controller/subsystem/statpanels/proc/immediate_send_stat_data(client/target) + if(!target.stat_panel.is_ready()) + return FALSE + + if(target.stat_tab == "Status") + set_status_tab(target) + return TRUE + + var/mob/target_mob = target.mob + if((target.stat_tab in target.spell_tabs) || !length(target.spell_tabs) && (length(target_mob.mob_spell_list) || length(target_mob.mind?.spell_list))) + set_spells_tab(target, target_mob) + return TRUE + + if(target_mob?.listed_turf) + if(!target_mob.TurfAdjacent(target_mob.listed_turf)) + target_mob.set_listed_turf(null) + + else if(target.stat_tab == target_mob?.listed_turf.name || !(target_mob?.listed_turf.name in target.panel_tabs)) + set_turf_examine_tab(target, target_mob) + return TRUE + + if(!target.holder) + return FALSE + + if(target.stat_tab == "MC") + set_MC_tab(target) + return TRUE -/// verbs that send information from the browser UI -/client/verb/set_tab(tab as text|null) - set name = "Set Tab" - set hidden = TRUE + if(target.stat_tab == "Tickets") + set_tickets_tab(target) + return TRUE - stat_tab = tab + if(!length(GLOB.sdql2_queries) && ("SDQL2" in target.panel_tabs)) + target.stat_panel.send_message("remove_sdql2") -/client/verb/send_tabs(tabs as text|null) - set name = "Send Tabs" - set hidden = TRUE + else if(length(GLOB.sdql2_queries) && target.stat_tab == "SDQL2") + set_SDQL2_tab(target) - panel_tabs |= tabs +/// Stat panel window declaration +/client/var/datum/tgui_window/stat_panel -/client/verb/remove_tabs(tabs as text|null) - set name = "Remove Tabs" - set hidden = TRUE +/// Datum that holds and tracks info about a client's object window +/// Really only exists because I want to be able to do logic with signals +/// And need a safe place to do the registration +/datum/object_window_info + /// list of atoms to show to our client via the object tab, at least currently + var/list/atoms_to_show = list() + /// list of atom -> image string for objects we have had in the right click tab + /// this is our caching + var/list/atoms_to_images = list() + /// list of atoms to turn into images for the object tab + var/list/atoms_to_imagify = list() + /// Our owner client + var/client/parent + /// Are we currently tracking a turf? + var/actively_tracking = FALSE - panel_tabs -= tabs +/datum/object_window_info/New(client/parent) + . = ..() + src.parent = parent -/client/verb/reset_tabs() - set name = "Reset Tabs" - set hidden = TRUE +/datum/object_window_info/Destroy(force, ...) + atoms_to_show = null + atoms_to_images = null + atoms_to_imagify = null + parent.obj_window = null + parent = null + STOP_PROCESSING(SSobj_tab_items, src) + return ..() + +/// Takes a client, attempts to generate object images for it +/// We will update the client with any improvements we make when we're done +/datum/object_window_info/process(delta_time) + // Cache the datum access for sonic speed + var/list/to_make = atoms_to_imagify + var/list/newly_seen = atoms_to_images + var/index = 0 + for(index in 1 to length(to_make)) + var/atom/thing = to_make[index] + + var/generated_string + if(ismob(thing) || length(thing.overlays) > 2) + generated_string = costly_icon2html(thing, parent, sourceonly=TRUE) + else + generated_string = icon2html(thing, parent, sourceonly=TRUE) + + newly_seen[thing] = generated_string + if(TICK_CHECK) + to_make.Cut(1, index + 1) + index = 0 + break + // If we've not cut yet, do it now + if(index) + to_make.Cut(1, index + 1) + SSstatpanels.refresh_client_obj_view(parent) + if(!length(to_make)) + return PROCESS_KILL + +/datum/object_window_info/proc/start_turf_tracking() + if(actively_tracking) + stop_turf_tracking() + var/static/list/connections = list( + COMSIG_MOVABLE_MOVED = PROC_REF(on_mob_move), + COMSIG_MOB_LOGOUT = PROC_REF(on_mob_logout), + ) + AddComponent(/datum/component/connect_mob_behalf, parent, connections) + actively_tracking = TRUE - panel_tabs = list() +/datum/object_window_info/proc/stop_turf_tracking() + qdel(GetComponent(/datum/component/connect_mob_behalf)) + actively_tracking = FALSE -/client/verb/panel_ready() - set name = "Panel Ready" - set hidden = TRUE +/datum/object_window_info/proc/on_mob_move(mob/source) + SIGNAL_HANDLER + var/turf/listed = source.listed_turf + if(!listed || !source.TurfAdjacent(listed)) + source.set_listed_turf(null) - statbrowser_ready = TRUE - init_verbs() +/datum/object_window_info/proc/on_mob_logout(mob/source) + SIGNAL_HANDLER + on_mob_move(parent.mob) -/client/verb/update_verbs() - set name = "Update Verbs" - set hidden = TRUE +/// Clears any cached object window stuff +/// We use hard refs cause we'd need a signal for this anyway. Cleaner this way +/datum/object_window_info/proc/viewing_atom_deleted(atom/deleted) + SIGNAL_HANDLER + atoms_to_show -= deleted + atoms_to_imagify -= deleted + atoms_to_images -= deleted - init_verbs() +/mob/proc/set_listed_turf(turf/new_turf) + listed_turf = new_turf + if(!client) + return + if(!client.obj_window) + client.obj_window = new(client) + if(listed_turf) + client.stat_panel.send_message("create_listedturf", listed_turf.name) + client.obj_window.start_turf_tracking() + else + client.stat_panel.send_message("remove_listedturf") + client.obj_window.stop_turf_tracking() diff --git a/code/datums/components/connect_mob_behalf.dm b/code/datums/components/connect_mob_behalf.dm new file mode 100644 index 000000000000..1c1a8a652342 --- /dev/null +++ b/code/datums/components/connect_mob_behalf.dm @@ -0,0 +1,59 @@ +/// This component behaves similar to connect_loc_behalf, but working off clients and mobs instead of loc +/// To be clear, we hook into a signal on a tracked client's mob +/// We retain the ability to react to that signal on a seperate listener, which makes this quite powerful +/datum/component/connect_mob_behalf + dupe_mode = COMPONENT_DUPE_UNIQUE + + /// An assoc list of signal -> procpath to register to the mob our client "owns" + var/list/connections + /// The master client we're working with + var/client/tracked + /// The mob we're currently tracking + var/mob/tracked_mob + +/datum/component/connect_mob_behalf/Initialize(client/tracked, list/connections) + . = ..() + if (!istype(tracked)) + return COMPONENT_INCOMPATIBLE + src.connections = connections + src.tracked = tracked + +/datum/component/connect_mob_behalf/RegisterWithParent() + RegisterSignal(tracked, COMSIG_PARENT_QDELETING, PROC_REF(handle_tracked_qdel)) + update_signals() + +/datum/component/connect_mob_behalf/UnregisterFromParent() + unregister_signals() + UnregisterSignal(tracked, COMSIG_PARENT_QDELETING) + + tracked = null + tracked_mob = null + +/datum/component/connect_mob_behalf/proc/handle_tracked_qdel() + SIGNAL_HANDLER + qdel(src) + +/datum/component/connect_mob_behalf/proc/update_signals() + unregister_signals() + // Yes this is a runtime silencer + // We could be in a position where logout is sent to two things, one thing intercepts it, then deletes the client's new mob + // It's rare, and the same check in connect_loc_behalf is more fruitful, but it's still worth doing + if(QDELETED(tracked?.mob)) + return + tracked_mob = tracked.mob + RegisterSignal(tracked_mob, COMSIG_MOB_LOGOUT, PROC_REF(on_logout)) + for (var/signal in connections) + parent.RegisterSignal(tracked_mob, signal, connections[signal]) + +/datum/component/connect_mob_behalf/proc/unregister_signals() + if(isnull(tracked_mob)) + return + + parent.UnregisterSignal(tracked_mob, connections) + UnregisterSignal(tracked_mob, COMSIG_MOB_LOGOUT) + + tracked_mob = null + +/datum/component/connect_mob_behalf/proc/on_logout(mob/source) + SIGNAL_HANDLER + update_signals() diff --git a/code/datums/mind.dm b/code/datums/mind.dm index af7d411245e2..fc91d2c71de1 100644 --- a/code/datums/mind.dm +++ b/code/datums/mind.dm @@ -728,7 +728,7 @@ if(istype(S, spell)) spell_list -= S qdel(S) - current?.client << output(null, "statbrowser:check_spells") + current?.client.stat_panel.send_message("check_spells") /datum/mind/proc/RemoveAllSpells() for(var/obj/effect/proc_holder/S in spell_list) diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index a853f66963af..986399d63299 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -766,7 +766,7 @@ GLOBAL_PROTECT(admin_verbs_hideable) set name = "Debug Stat Panel" set category = "Debug" - src << output("", "statbrowser:create_debug") + src.stat_panel.send_message("create_debug") #ifdef SENDMAPS_PROFILE /client/proc/display_sendmaps() diff --git a/code/modules/antagonists/borer/borer.dm b/code/modules/antagonists/borer/borer.dm index 41e8b644fa53..d4af47670412 100644 --- a/code/modules/antagonists/borer/borer.dm +++ b/code/modules/antagonists/borer/borer.dm @@ -52,7 +52,7 @@ B.victim.adjustOrganLoss(ORGAN_SLOT_BRAIN, rand(5, 10)) to_chat(src, "With an immense exertion of will, you regain control of your body!") to_chat(B, "You feel control of the host brain ripped from your grasp, and retract your probosci before the wild neural impulses can damage you.") - B.detatch() + B.detach() GLOBAL_LIST_EMPTY(borers) GLOBAL_VAR_INIT(total_borer_hosts_needed, 3) @@ -568,7 +568,7 @@ GLOBAL_VAR_INIT(total_borer_hosts_needed, 3) return if(controlling) - detatch() + detach() if(src.mind.language_holder) var/datum/language_holder/language_holder = src.mind.language_holder @@ -721,6 +721,8 @@ GLOBAL_VAR_INIT(total_borer_hosts_needed, 3) victim.med_hud_set_status() + RegisterSignal(victim, COMSIG_MOB_GET_STATUS_TAB_ITEMS, PROC_REF(get_borer_stat_panel)) + /mob/living/simple_animal/borer/verb/punish() set category = "Borer" set name = "Punish" @@ -756,7 +758,6 @@ GLOBAL_VAR_INIT(total_borer_hosts_needed, 3) /mob/living/carbon/proc/release_control() - set category = "Borer" set name = "Release Control" set desc = "Release control of your host's body." @@ -764,8 +765,12 @@ GLOBAL_VAR_INIT(total_borer_hosts_needed, 3) var/mob/living/simple_animal/borer/B = has_brain_worms() if(B && B.host_brain) to_chat(B, "You withdraw your probosci, releasing control of [B.host_brain]") + B.detach() - B.detatch() +/mob/living/simple_animal/borer/proc/get_borer_stat_panel(mob/living/source, list/items) + SIGNAL_HANDLER + items += "Borer Body Health: [health]" + items += "Chemicals: [chemicals]" //Check for brain worms in head. /mob/proc/has_brain_worms() @@ -801,7 +806,7 @@ GLOBAL_VAR_INIT(total_borer_hosts_needed, 3) to_chat(src, "You need 200 chemicals stored to reproduce.") return -/mob/living/simple_animal/borer/proc/detatch() +/mob/living/simple_animal/borer/proc/detach() if(!victim || !controlling) return @@ -829,6 +834,8 @@ GLOBAL_VAR_INIT(total_borer_hosts_needed, 3) log_game("[src]/([src.ckey]) released control of [victim]/([victim.ckey]") + UnregisterSignal(victim, COMSIG_MOB_GET_STATUS_TAB_ITEMS) + qdel(host_brain) /mob/living/simple_animal/borer/proc/toggle_leap() diff --git a/code/modules/antagonists/changeling/powers/panacea.dm b/code/modules/antagonists/changeling/powers/panacea.dm index 5c1010aaf69a..573ebd127fde 100644 --- a/code/modules/antagonists/changeling/powers/panacea.dm +++ b/code/modules/antagonists/changeling/powers/panacea.dm @@ -29,7 +29,7 @@ var/mob/living/simple_animal/borer/B = user.has_brain_worms() //WS Begin - Borers if(B) if(B.controlling) - B.detatch() + B.detach() B.leave_victim() if(iscarbon(user)) var/mob/living/carbon/C = user diff --git a/code/modules/assembly/health.dm b/code/modules/assembly/health.dm index 2a07737e2c63..7bc5adc33de2 100644 --- a/code/modules/assembly/health.dm +++ b/code/modules/assembly/health.dm @@ -14,6 +14,13 @@ . += "Use it in hand to turn it off/on and Alt-click to swap between \"detect death\" mode and \"detect critical state\" mode." . += "[src.scanning ? "The sensor is on and you can see [health_scan] displayed on the screen" : "The sensor is off"]." +/obj/item/assembly/health/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change) + . = ..() + if(iscarbon(old_loc)) + UnregisterSignal(old_loc, COMSIG_MOB_GET_STATUS_TAB_ITEMS) + if(iscarbon(loc)) + RegisterSignal(loc, COMSIG_MOB_GET_STATUS_TAB_ITEMS, PROC_REF(get_status_tab_item)) + /obj/item/assembly/health/activate() if(!..()) return FALSE//Cooldown check @@ -73,3 +80,7 @@ . = ..() to_chat(user, "You toggle [src] [src.scanning ? "off" : "on"].") toggle_scan() + +/obj/item/assembly/health/proc/get_status_tab_item(mob/living/carbon/source, list/items) + SIGNAL_HANDLER + items += "Health: [round((source.health / source.maxHealth) * 100)]%" diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm index de655ece5f1a..64e2476b400f 100644 --- a/code/modules/client/client_defines.dm +++ b/code/modules/client/client_defines.dm @@ -150,15 +150,14 @@ /// our current tab var/stat_tab - /// whether our browser is ready or not yet - var/statbrowser_ready = FALSE - /// list of all tabs var/list/panel_tabs = list() /// list of tabs containing spells and abilities var/list/spell_tabs = list() ///A lazy list of atoms we've examined in the last EXAMINE_MORE_TIME (default 1.5) seconds, so that we will call [atom/proc/examine_more()] instead of [atom/proc/examine()] on them when examining var/list/recent_examines + ///Our object window datum. It stores info about and handles behavior for the object tab + var/datum/object_window_info/obj_window var/list/parallax_layers var/list/parallax_layers_cached diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index 8db2e8454828..711af230fd9d 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -80,7 +80,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( if(tgui_Topic(href_list)) return if(href_list["reload_statbrowser"]) - src << browse(file('html/statbrowser.html'), "window=statbrowser") + stat_panel.reinitialize() // Log all hrefs log_href("[src] (usr:[usr]\[[COORD(usr)]\]) : [hsrc ? "[hsrc] " : ""][href]") @@ -226,8 +226,12 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( GLOB.clients += src GLOB.directory[ckey] = src + // Instantiate stat panel + stat_panel = new(src, "statbrowser") + stat_panel.subscribe(src, PROC_REF(on_stat_panel_message)) + // Instantiate tgui panel - tgui_panel = new(src) + tgui_panel = new(src, "browseroutput") GLOB.ahelp_tickets.client_login(src) GLOB.interviews.client_login(src) @@ -346,9 +350,15 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( if(SSinput.initialized) set_macros() - // Initialize tgui panel - src << browse(file('html/statbrowser.html'), "window=statbrowser") + // Initialize stat panel + stat_panel.initialize( + inline_html = file2text('html/statbrowser.html'), + inline_js = file2text('html/statbrowser.js'), + inline_css = file2text('html/statbrowser.css'), + ) addtimer(CALLBACK(src, PROC_REF(check_panel_loaded)), 30 SECONDS) + + // Initialize tgui panel tgui_panel.initialize() if(alert_mob_dupe_login) @@ -506,6 +516,8 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( SSserver_maint.UpdateHubStatus() if(credits) QDEL_LIST(credits) + if(obj_window) + QDEL_NULL(obj_window) if(holder) adminGreet(1) holder.owner = null @@ -1082,12 +1094,10 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( var/list/verbstoprocess = verbs.Copy() if(mob?.client?.prefs.broadcast_login_logout) verbstoprocess += mob.verbs - for(var/AM in mob.contents) - var/atom/movable/thing = AM + for(var/atom/movable/thing as anything in mob.contents) verbstoprocess += thing.verbs panel_tabs.Cut() // panel_tabs get reset in init_verbs on JS side anyway - for(var/thing in verbstoprocess) - var/procpath/verb_to_init = thing + for(var/procpath/verb_to_init as anything in verbstoprocess) if(!verb_to_init) continue if(verb_to_init.hidden) @@ -1096,10 +1106,10 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( continue panel_tabs |= verb_to_init.category verblist[++verblist.len] = list(verb_to_init.category, verb_to_init.name) - src << output("[url_encode(json_encode(panel_tabs))];[url_encode(json_encode(verblist))]", "statbrowser:init_verbs") + src.stat_panel.send_message("init_verbs", list(panel_tabs = panel_tabs, verblist = verblist)) /client/proc/check_panel_loaded() - if(statbrowser_ready) + if(stat_panel.is_ready()) return to_chat(src, "Statpanel failed to load, click here to reload the panel ") @@ -1140,3 +1150,20 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( SSambience.ambience_listening_clients[src] = world.time + 10 SECONDS //Just wait 10 seconds before the next one aight mate? cheers. else SSambience.ambience_listening_clients -= src + +/** + * Handles incoming messages from the stat-panel TGUI. + */ +/client/proc/on_stat_panel_message(type, payload) + switch(type) + if("Update-Verbs") + init_verbs() + if("Remove-Tabs") + panel_tabs -= payload["tab"] + if("Send-Tabs") + panel_tabs |= payload["tab"] + if("Reset-Tabs") + panel_tabs = list() + if("Set-Tab") + stat_tab = payload["tab"] + SSstatpanels.immediate_send_stat_data(src) diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm index 3fa8bfc5266b..b52052cd15ec 100644 --- a/code/modules/client/preferences.dm +++ b/code/modules/client/preferences.dm @@ -48,7 +48,6 @@ GLOBAL_LIST_EMPTY(preferences_datums) var/tgui_fancy = TRUE var/tgui_lock = FALSE var/windowflashing = TRUE - var/crew_objectives = TRUE var/toggles = TOGGLES_DEFAULT var/db_flags var/chat_toggles = TOGGLES_DEFAULT_CHAT @@ -1112,6 +1111,7 @@ GLOBAL_LIST_EMPTY(preferences_datums) dat += "Hide Radio Messages: [(chat_toggles & CHAT_RADIO)?"Shown":"Hidden"]
" dat += "Hide Prayers: [(chat_toggles & CHAT_PRAYER)?"Shown":"Hidden"]
" dat += "Split Admin Tabs: [(toggles & SPLIT_ADMIN_TABS)?"Enabled":"Disabled"]
" + dat += "Fast MC Refresh: [(toggles & FAST_MC_REFRESH)?"Enabled":"Disabled"]
" dat += "Ignore Being Summoned as Cult Ghost: [(toggles & ADMIN_IGNORE_CULT_GHOST)?"Don't Allow Being Summoned":"Allow Being Summoned"]
" dat += "Briefing Officer Outfit: [brief_outfit]
" if(CONFIG_GET(flag/allow_admin_asaycolor)) @@ -2215,6 +2215,8 @@ GLOBAL_LIST_EMPTY(preferences_datums) user.client.toggle_hear_radio() if("toggle_split_admin_tabs") toggles ^= SPLIT_ADMIN_TABS + if("toggle_fast_mc_refresh") + toggles ^= FAST_MC_REFRESH if("toggle_prayers") user.client.toggleprayers() if("toggle_deadmin_always") diff --git a/code/modules/mob/dead/dead.dm b/code/modules/mob/dead/dead.dm index ee74d0475a34..c09a3c8bd4c8 100644 --- a/code/modules/mob/dead/dead.dm +++ b/code/modules/mob/dead/dead.dm @@ -37,12 +37,8 @@ INITIALIZE_IMMEDIATE(/mob/dead) /mob/dead/get_status_tab_items() . = ..() - . += "" - . += "Game Mode: [SSticker.hide_mode ? "Secret" : "[GLOB.master_mode]"]" - if(SSticker.HasRoundStarted()) return - var/time_remaining = SSticker.GetTimeLeft() if(time_remaining > 0) . += "Time To Start: [round(time_remaining/10)]s" diff --git a/code/modules/mob/dead/new_player/new_player.dm b/code/modules/mob/dead/new_player/new_player.dm index d7865c9d2276..9baa46f526b5 100644 --- a/code/modules/mob/dead/new_player/new_player.dm +++ b/code/modules/mob/dead/new_player/new_player.dm @@ -493,8 +493,13 @@ /mob/dead/new_player/proc/register_for_interview() // First we detain them by removing all the verbs they have on client for (var/procpath/client_verb as anything in client.verbs) - if(!(client_verb in GLOB.client_verbs_required)) - remove_verb(client, client_verb) + if(client_verb in GLOB.client_verbs_required) + continue + remove_verb(client, client_verb) + + // Then remove those on their mob as well + for (var/procpath/verb_path as anything in verbs) + remove_verb(src, verb_path) // Then we create the interview form and show it to the client var/datum/interview/I = GLOB.interviews.interview_for_client(client) diff --git a/code/modules/mob/living/carbon/alien/alien.dm b/code/modules/mob/living/carbon/alien/alien.dm index 9dfd2484402e..23e937acf102 100644 --- a/code/modules/mob/living/carbon/alien/alien.dm +++ b/code/modules/mob/living/carbon/alien/alien.dm @@ -86,10 +86,6 @@ /mob/living/carbon/alien/IsAdvancedToolUser() return has_fine_manipulation -/mob/living/carbon/alien/get_status_tab_items() - . = ..() - . += "Intent: [a_intent]" - /mob/living/carbon/alien/getTrail() if(getBruteLoss() < 200) return pick (list("xltrails_1", "xltrails2")) diff --git a/code/modules/mob/living/carbon/alien/organs.dm b/code/modules/mob/living/carbon/alien/organs.dm index 8faa15b83929..cb7c7adafc4b 100644 --- a/code/modules/mob/living/carbon/alien/organs.dm +++ b/code/modules/mob/living/carbon/alien/organs.dm @@ -83,17 +83,23 @@ else owner.adjustPlasma(plasma_rate * 0.1) -/obj/item/organ/alien/plasmavessel/Insert(mob/living/carbon/M, special = 0) +/obj/item/organ/alien/plasmavessel/Insert(mob/living/carbon/organ_owner, special = 0) ..() - if(isalien(M)) - var/mob/living/carbon/alien/A = M - A.updatePlasmaDisplay() + if(isalien(organ_owner)) + var/mob/living/carbon/alien/target_alien = organ_owner + target_alien.updatePlasmaDisplay() + RegisterSignal(organ_owner, COMSIG_MOB_GET_STATUS_TAB_ITEMS, PROC_REF(get_status_tab_item)) -/obj/item/organ/alien/plasmavessel/Remove(mob/living/carbon/M, special = 0) +/obj/item/organ/alien/plasmavessel/Remove(mob/living/carbon/organ_owner, special = 0) ..() - if(isalien(M)) - var/mob/living/carbon/alien/A = M - A.updatePlasmaDisplay() + if(isalien(organ_owner)) + var/mob/living/carbon/alien/organ_owner_alien = organ_owner + organ_owner_alien.updatePlasmaDisplay() + UnregisterSignal(organ_owner, COMSIG_MOB_GET_STATUS_TAB_ITEMS) + +/obj/item/organ/alien/plasmavessel/proc/get_status_tab_item(mob/living/carbon/source, list/items) + SIGNAL_HANDLER + items += "Plasma Stored: [storedPlasma]/[max_plasma]" #define QUEEN_DEATH_DEBUFF_DURATION 2400 diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm index 82c27e95174b..5b316dad9cb6 100644 --- a/code/modules/mob/living/carbon/carbon.dm +++ b/code/modules/mob/living/carbon/carbon.dm @@ -28,6 +28,9 @@ if(!held_index) held_index = (active_hand_index % held_items.len)+1 + if(!isnum(held_index)) + CRASH("You passed [held_index] into swap_hand instead of a number. WTF man") + var/oindex = active_hand_index active_hand_index = held_index if(hud_used) @@ -415,14 +418,6 @@ var/turf/target = get_turf(loc) I.safe_throw_at(target,I.throw_range,I.throw_speed,src, force = move_force) -/mob/living/carbon/get_status_tab_items() - . = ..() - var/obj/item/organ/alien/plasmavessel/vessel = getorgan(/obj/item/organ/alien/plasmavessel) - if(vessel) - . += "Plasma Stored: [vessel.storedPlasma]/[vessel.max_plasma]" - if(locate(/obj/item/assembly/health) in src) - . += "Health: [health]" - /mob/living/carbon/get_proc_holders() . = ..() . += add_abilities_to_panel() diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index 4bfe35b47060..cd80b13bcda6 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -61,7 +61,8 @@ . = ..() . += "Intent: [a_intent]" . += "Move Mode: [m_intent]" - if (internal) + + if (internal) //TODO: Refactor this to use the signal on tanks if (!internal.air_contents) qdel(internal) else @@ -69,32 +70,6 @@ . += "Internal Atmosphere Info: [internal.name]" . += "Tank Pressure: [internal.air_contents.return_pressure()]" . += "Distribution Pressure: [internal.distribute_pressure]" - /*WS begin - no cells in suits - if(istype(wear_suit, /obj/item/clothing/suit/space)) - var/obj/item/clothing/suit/space/S = wear_suit - . += "Thermal Regulator: [S.thermal_on ? "on" : "off"]" - . += "Cell Charge: [S.cell ? "[round(S.cell.percent(), 0.1)]%" : "!invalid!"]" - */ - var/mob/living/simple_animal/borer/B = has_brain_worms() //WS Begin - Borers - if(B && B.controlling) - . += "Borer Body Health: [B.health]" - . += "Chemicals: [B.chemicals]" //WS End - - if(mind) - var/datum/antagonist/changeling/changeling = mind.has_antag_datum(/datum/antagonist/changeling) - if(changeling) - . += "" - . += "Chemical Storage: [changeling.chem_charges]/[changeling.chem_storage]" - . += "Absorbed DNA: [changeling.absorbedcount]" - - //WS Begin - Display Ethereal Charge - if(istype(src)) - var/datum/species/ethereal/eth_species = src.dna?.species - if(istype(eth_species)) - var/obj/item/organ/stomach/ethereal/stomach = src.getorganslot(ORGAN_SLOT_STOMACH) - if(istype(stomach)) - . += "Crystal Charge: [round((stomach.crystal_charge / ETHEREAL_CHARGE_SCALING_MULTIPLIER), 0.1)]%" - //WS End //NINJACODE if(istype(wear_suit, /obj/item/clothing/suit/space/space_ninja)) //Only display if actually a ninja. diff --git a/code/modules/mob/living/carbon/human/species_types/vampire.dm b/code/modules/mob/living/carbon/human/species_types/vampire.dm index ebc923c01075..070894a92bee 100644 --- a/code/modules/mob/living/carbon/human/species_types/vampire.dm +++ b/code/modules/mob/living/carbon/human/species_types/vampire.dm @@ -132,3 +132,15 @@ charge_max = 50 cooldown_min = 50 shapeshift_type = /mob/living/simple_animal/hostile/retaliate/bat + +/obj/item/organ/internal/heart/vampire/Insert(mob/living/carbon/receiver, special, drop_if_replaced) + . = ..() + RegisterSignal(receiver, COMSIG_MOB_GET_STATUS_TAB_ITEMS, PROC_REF(get_status_tab_item)) + +/obj/item/organ/internal/heart/vampire/Remove(mob/living/carbon/heartless, special) + . = ..() + UnregisterSignal(heartless, COMSIG_MOB_GET_STATUS_TAB_ITEMS) + +/obj/item/organ/internal/heart/vampire/proc/get_status_tab_item(mob/living/carbon/source, list/items) + SIGNAL_HANDLER + items += "Blood Level: [source.blood_volume]/[BLOOD_VOLUME_MAXIMUM]" diff --git a/code/modules/mob/living/silicon/robot/robot.dm b/code/modules/mob/living/silicon/robot/robot.dm index 44bfe5626754..4164844c0d8e 100644 --- a/code/modules/mob/living/silicon/robot/robot.dm +++ b/code/modules/mob/living/silicon/robot/robot.dm @@ -323,7 +323,6 @@ /mob/living/silicon/robot/get_status_tab_items() . = ..() - . += "" if(cell) . += "Charge Left: [cell.charge]/[cell.maxcharge]" else diff --git a/code/modules/mob/living/simple_animal/parrot.dm b/code/modules/mob/living/simple_animal/parrot.dm index d63c300e8ba6..bc22f78ae7a3 100644 --- a/code/modules/mob/living/simple_animal/parrot.dm +++ b/code/modules/mob/living/simple_animal/parrot.dm @@ -155,9 +155,7 @@ /mob/living/simple_animal/parrot/get_status_tab_items() . = ..() - . += "" . += "Held Item: [held_item]" - . += "Mode: [a_intent]" /mob/living/simple_animal/parrot/Hear(message, atom/movable/speaker, message_langs, raw_message, radio_freq, list/spans, list/message_mods = list()) . = ..() diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm index c21a2a6f365d..e4ead25880f9 100644 --- a/code/modules/mob/living/simple_animal/simple_animal.dm +++ b/code/modules/mob/living/simple_animal/simple_animal.dm @@ -374,8 +374,8 @@ /mob/living/simple_animal/get_status_tab_items() . = ..() - . += "" . += "Health: [round((health / maxHealth) * 100)]%" + . += "Intent: [a_intent]" /mob/living/simple_animal/proc/drop_loot() if(loot.len) diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index 9af72b034998..5a2e3731a703 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -843,7 +843,8 @@ /// Adds this list to the output to the stat browser /mob/proc/get_status_tab_items() - . = list() + . = list("") //we want to offset unique stuff from standard stuff + SEND_SIGNAL(src, COMSIG_MOB_GET_STATUS_TAB_ITEMS, .) /// Gets all relevant proc holders for the browser statpenl /mob/proc/get_proc_holders() @@ -990,7 +991,7 @@ mob_spell_list -= S qdel(S) if(client) - client << output(null, "statbrowser:check_spells") + client.stat_panel.send_message("check_spells") ///Return any anti magic atom on this mob that matches the magic type /mob/proc/anti_magic_check(magic = TRUE, holy = FALSE, tinfoil = FALSE, chargecost = 1, self = FALSE) diff --git a/code/modules/surgery/organs/augments_chest.dm b/code/modules/surgery/organs/augments_chest.dm index dc95ab97cece..2cbdf9823eb6 100644 --- a/code/modules/surgery/organs/augments_chest.dm +++ b/code/modules/surgery/organs/augments_chest.dm @@ -214,9 +214,8 @@ return TRUE // Priority 3: use internals tank. - var/obj/item/tank/I = owner.internal - if(I && I.air_contents && I.air_contents.total_moles() >= num) - T.assume_air_moles(I.air_contents, num) + if(owner.internal?.air_contents?.total_moles() >= num) + T.assume_air_moles(owner.internal.air_contents, num) toggle(silent = TRUE) return FALSE diff --git a/code/modules/surgery/organs/stomach.dm b/code/modules/surgery/organs/stomach.dm index 2e2403db14c6..bf9346b5dcd9 100644 --- a/code/modules/surgery/organs/stomach.dm +++ b/code/modules/surgery/organs/stomach.dm @@ -107,16 +107,22 @@ ..() adjust_charge(-ETHEREAL_CHARGE_FACTOR) -/obj/item/organ/stomach/ethereal/Insert(mob/living/carbon/M, special = 0) +/obj/item/organ/stomach/ethereal/Insert(mob/living/carbon/organ_owner, special = 0) ..() - RegisterSignal(owner, COMSIG_PROCESS_BORGCHARGER_OCCUPANT, PROC_REF(charge)) - RegisterSignal(owner, COMSIG_LIVING_ELECTROCUTE_ACT, PROC_REF(on_electrocute)) - -/obj/item/organ/stomach/ethereal/Remove(mob/living/carbon/M, special = 0) - UnregisterSignal(owner, COMSIG_PROCESS_BORGCHARGER_OCCUPANT) - UnregisterSignal(owner, COMSIG_LIVING_ELECTROCUTE_ACT) + RegisterSignal(organ_owner, COMSIG_PROCESS_BORGCHARGER_OCCUPANT, PROC_REF(charge)) + RegisterSignal(organ_owner, COMSIG_LIVING_ELECTROCUTE_ACT, PROC_REF(on_electrocute)) + RegisterSignal(organ_owner, COMSIG_MOB_GET_STATUS_TAB_ITEMS, PROC_REF(get_status_tab_item)) + +/obj/item/organ/stomach/ethereal/Remove(mob/living/carbon/organ_owner, special = 0) + UnregisterSignal(organ_owner, COMSIG_PROCESS_BORGCHARGER_OCCUPANT) + UnregisterSignal(organ_owner, COMSIG_LIVING_ELECTROCUTE_ACT) + UnregisterSignal(organ_owner, COMSIG_MOB_GET_STATUS_TAB_ITEMS) ..() +/obj/item/organ/stomach/ethereal/proc/get_status_tab_item(mob/living/carbon/source, list/items) + SIGNAL_HANDLER + items += "Crystal Charge: [round((crystal_charge / ETHEREAL_CHARGE_SCALING_MULTIPLIER), 0.1)]%" + /obj/item/organ/stomach/ethereal/proc/charge(datum/source, amount, repairs) adjust_charge((amount * ETHEREAL_CHARGE_SCALING_MULTIPLIER) / 70) //WS Edit -- Ethereal Charge Scaling diff --git a/code/modules/tgui/tgui.dm b/code/modules/tgui/tgui.dm index a79966f69ba1..95875473133b 100644 --- a/code/modules/tgui/tgui.dm +++ b/code/modules/tgui/tgui.dm @@ -92,8 +92,9 @@ window.acquire_lock(src) if(!window.is_ready()) window.initialize( + strict_mode = TRUE, fancy = user.client.prefs.tgui_fancy, - inline_assets = list( + assets = list( get_asset_datum(/datum/asset/simple/tgui_common), get_asset_datum(/datum/asset/simple/tgui), )) diff --git a/code/modules/tgui/tgui_window.dm b/code/modules/tgui/tgui_window.dm index 62574cb1aacd..844ba6239a0f 100644 --- a/code/modules/tgui/tgui_window.dm +++ b/code/modules/tgui/tgui_window.dm @@ -18,8 +18,12 @@ var/message_queue var/sent_assets = list() // Vars passed to initialize proc (and saved for later) - var/inline_assets - var/fancy + var/initial_strict_mode + var/initial_fancy + var/initial_assets + var/initial_inline_html + var/initial_inline_js + var/initial_inline_css var/mouse_event_macro_set = FALSE /** @@ -45,21 +49,30 @@ * state. You can begin sending messages right after initializing. Messages * will be put into the queue until the window finishes loading. * - * optional inline_assets list List of assets to inline into the html. - * optional inline_html string Custom HTML to inject. - * optional fancy bool If TRUE, will hide the window titlebar. + * optional strict_mode bool - Enables strict error handling and BSOD. + * optional fancy bool - If TRUE and if this is NOT a panel, will hide the window titlebar. + * optional assets list - List of assets to load during initialization. + * optional inline_html string - Custom HTML to inject. + * optional inline_js string - Custom JS to inject. + * optional inline_css string - Custom CSS to inject. */ /datum/tgui_window/proc/initialize( - inline_assets = list(), + strict_mode = FALSE, + fancy = FALSE, + assets = list(), inline_html = "", - fancy = FALSE) + inline_js = "", + inline_css = "") log_tgui(client, context = "[id]/initialize", window = src) if(!client) return - src.inline_assets = inline_assets - src.fancy = fancy + src.initial_fancy = fancy + src.initial_assets = assets + src.initial_inline_html = inline_html + src.initial_inline_js = inline_js + src.initial_inline_css = inline_css status = TGUI_WINDOW_LOADING fatally_errored = FALSE // Build window options @@ -72,9 +85,10 @@ // Generate page html var/html = SStgui.basehtml html = replacetextEx(html, "\[tgui:windowId]", id) - // Inject inline assets + html = replacetextEx(html, "\[tgui:strictMode]", strict_mode) + // Inject assets var/inline_assets_str = "" - for(var/datum/asset/asset in inline_assets) + for(var/datum/asset/asset in assets) var/mappings = asset.get_url_mappings() for(var/name in mappings) var/url = mappings[name] @@ -87,8 +101,17 @@ if(length(inline_assets_str)) inline_assets_str = "\n" html = replacetextEx(html, "\n", inline_assets_str) - // Inject custom HTML - html = replacetextEx(html, "\n", inline_html) + // Inject inline HTML + if (inline_html) + html = replacetextEx(html, "", inline_html) + // Inject inline JS + if (inline_js) + inline_js = "" + html = replacetextEx(html, "", inline_js) + // Inject inline CSS + if (inline_css) + inline_css = "" + html = replacetextEx(html, "", inline_css) // Open the window client << browse(html, "window=[id];[options]") // Detect whether the control is a browser @@ -97,6 +120,20 @@ if(!is_browser) winset(client, id, "on-close=\"uiclose [id]\"") +/** + * public + * + * Reinitializes the panel with previous data used for initialization. + */ +/datum/tgui_window/proc/reinitialize() + initialize( + strict_mode = initial_strict_mode, + fancy = initial_fancy, + assets = initial_assets, + inline_html = initial_inline_html, + inline_js = initial_inline_js, + inline_css = initial_inline_css) + /** * public * @@ -320,7 +357,7 @@ client << link(href_list["url"]) if("cacheReloaded") // Reinitialize - initialize(inline_assets = inline_assets, fancy = fancy) + reinitialize() // Resend the assets for(var/asset in sent_assets) send_asset(asset) diff --git a/code/modules/tgui_panel/tgui_panel.dm b/code/modules/tgui_panel/tgui_panel.dm index fdd74389c837..44fbffd917ce 100644 --- a/code/modules/tgui_panel/tgui_panel.dm +++ b/code/modules/tgui_panel/tgui_panel.dm @@ -13,9 +13,9 @@ var/broken = FALSE var/initialized_at -/datum/tgui_panel/New(client/client) +/datum/tgui_panel/New(client/client, id) src.client = client - window = new(client, "browseroutput") + window = new(client, id) window.subscribe(src, PROC_REF(on_message)) /datum/tgui_panel/Del() @@ -42,10 +42,12 @@ sleep(1) initialized_at = world.time // Perform a clean initialization - window.initialize(inline_assets = list( - get_asset_datum(/datum/asset/simple/tgui_common), - get_asset_datum(/datum/asset/simple/tgui_panel), - )) + window.initialize( + strict_mode = TRUE, + assets = list( + get_asset_datum(/datum/asset/simple/tgui_common), + get_asset_datum(/datum/asset/simple/tgui_panel), + )) window.send_asset(get_asset_datum(/datum/asset/simple/namespaced/fontawesome)) window.send_asset(get_asset_datum(/datum/asset/spritesheet/chat)) request_telemetry() diff --git a/html/statbrowser.css b/html/statbrowser.css new file mode 100644 index 000000000000..dc693f42f756 --- /dev/null +++ b/html/statbrowser.css @@ -0,0 +1,227 @@ +body { + font-family: Verdana, Geneva, Tahoma, sans-serif; + font-size: 12px !important; + margin: 0 !important; + padding: 0 !important; + overflow-x: hidden; + overflow-y: scroll; +} + +body.dark { + background-color: #131313; + color: #b2c4dd; + scrollbar-base-color: #1c1c1c; + scrollbar-face-color: #3b3b3b; + scrollbar-3dlight-color: #252525; + scrollbar-highlight-color: #252525; + scrollbar-track-color: #1c1c1c; + scrollbar-arrow-color: #929292; + scrollbar-shadow-color: #3b3b3b; +} + +#menu { + background-color: #F0F0F0; + position: fixed; + width: 100%; + z-index: 100; +} + +.dark #menu { + background-color: #202020; +} + +#statcontent { + padding: 7px 7px 7px 7px; +} + +a { + color: black; + text-decoration: none +} + +.dark a { + color: #b2c4dd; +} + +a:hover, +.dark a:hover { + text-decoration: underline; +} + +ul { + list-style-type: none; + margin: 0; + padding: 0; + background-color: #333; +} + +li { + float: left; +} + +li a { + display: block; + color: white; + text-align: center; + padding: 14px 16px; + text-decoration: none; +} + +li a:hover:not(.active) { + background-color: #111; +} + +.button-container { + display: inline-flex; + flex-wrap: wrap-reverse; + flex-direction: row; + align-items: flex-start; + overflow-x: hidden; + white-space: pre-wrap; + padding: 0 4px; +} + +.button { + background-color: #dfdfdf; + border: 1px solid #cecece; + border-bottom-width: 2px; + color: rgba(0, 0, 0, 0.7); + padding: 6px 4px 4px; + text-align: center; + text-decoration: none; + font-size: 12px; + margin: 0; + cursor: pointer; + transition-duration: 100ms; + order: 3; + min-width: 40px; +} + +.dark button { + background-color: #222222; + border-color: #343434; + color: rgba(255, 255, 255, 0.5); +} + +.button:hover { + background-color: #ececec; + transition-duration: 0; +} + +.dark button:hover { + background-color: #2e2e2e; +} + +.button:active, +.button.active { + background-color: #ffffff; + color: black; + border-top-color: #cecece; + border-left-color: #cecece; + border-right-color: #cecece; + border-bottom-color: #ffffff; +} + +.dark .button:active, +.dark .button.active { + background-color: #444444; + color: white; + border-top-color: #343434; + border-left-color: #343434; + border-right-color: #343434; + border-bottom-color: #ffffff; +} + +.grid-container { + margin: -2px; + margin-right: -15px; +} + +.grid-item { + position: relative; + display: inline-block; + width: 100%; + box-sizing: border-box; + overflow: visible; + padding: 3px 2px; + text-decoration: none; +} + +@media only screen and (min-width: 300px) { + .grid-item { + width: 50%; + } +} + +@media only screen and (min-width: 430px) { + .grid-item { + width: 33%; + } +} + +@media only screen and (min-width: 560px) { + .grid-item { + width: 25%; + } +} + +@media only screen and (min-width: 770px) { + .grid-item { + width: 20%; + } +} + +.grid-item:hover { + z-index: 1; +} + +.grid-item:hover .grid-item-text { + width: auto; + text-decoration: underline; +} + +.grid-item-text { + display: inline-block; + width: 100%; + background-color: #ffffff; + margin: 0 -6px; + padding: 0 6px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + pointer-events: none; +} + +.dark .grid-item-text { + background-color: #131313; +} + +.link { + display: inline; + background: none; + border: none; + padding: 7px 14px; + color: black; + text-decoration: none; + cursor: pointer; + font-size: 13px; + margin: 2px 2px; +} + +.dark .link { + color: #abc6ec; +} + +.link:hover { + text-decoration: underline; +} + +img { + -ms-interpolation-mode: nearest-neighbor; + image-rendering: pixelated; +} + +.interview_panel_controls, +.interview_panel_stats { + margin-bottom: 10px; +} diff --git a/html/statbrowser.html b/html/statbrowser.html index 2bb7f8259afb..1aea8811d58a 100644 --- a/html/statbrowser.html +++ b/html/statbrowser.html @@ -1,1280 +1,3 @@ - - - - Stat Browser - - - - - - - - -
-
- - - + +
+
diff --git a/html/statbrowser.js b/html/statbrowser.js new file mode 100644 index 000000000000..d024d50b8c3d --- /dev/null +++ b/html/statbrowser.js @@ -0,0 +1,1003 @@ +// Polyfills and compatibility ------------------------------------------------ +var decoder = decodeURIComponent || unescape; +if (!Array.prototype.includes) { + Array.prototype.includes = function (thing) { + for (var i = 0; i < this.length; i++) { + if (this[i] == thing) return true; + } + return false; + } +} +if (!String.prototype.trim) { + String.prototype.trim = function () { + return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''); + }; +} + +// Status panel implementation ------------------------------------------------ +var status_tab_parts = ["Loading..."]; +var current_tab = null; +var mc_tab_parts = [["Loading...", ""]]; +var href_token = null; +var spells = []; +var spell_tabs = []; +var verb_tabs = []; +var verbs = [["", ""]]; // list with a list inside +var tickets = []; +var interviewManager = { status: "", interviews: [] }; +var sdql2 = []; +var permanent_tabs = []; // tabs that won't be cleared by wipes +var turfcontents = []; +var turfname = ""; +var imageRetryDelay = 500; +var imageRetryLimit = 50; +var menu = document.getElementById('menu'); +var under_menu = document.getElementById('under_menu'); +var statcontentdiv = document.getElementById('statcontent'); +var storedimages = []; +var split_admin_tabs = false; + +// Any BYOND commands that could result in the client's focus changing go through this +// to ensure that when we relinquish our focus, we don't do it after the result of +// a command has already taken focus for itself. +function run_after_focus(callback) { + setTimeout(callback, 0); +} + +function createStatusTab(name) { + if (name.indexOf(".") != -1) { + var splitName = name.split("."); + if (split_admin_tabs && splitName[0] === "Admin") + name = splitName[1]; + else + name = splitName[0]; + } + if (document.getElementById(name) || name.trim() == "") { + return; + } + if (!verb_tabs.includes(name) && !permanent_tabs.includes(name)) { + return; + } + var B = document.createElement("BUTTON"); + B.onclick = function () { + tab_change(name); + this.blur(); + }; + B.id = name; + B.textContent = name; + B.className = "button"; + //ORDERING ALPHABETICALLY + B.style.order = name.charCodeAt(0); + if (name == "Status" || name == "MC") { + B.style.order = name == "Status" ? 1 : 2; + } + //END ORDERING + menu.appendChild(B); + SendTabToByond(name); + under_menu.style.height = menu.clientHeight + 'px'; +} + +function removeStatusTab(name) { + if (!document.getElementById(name) || permanent_tabs.includes(name)) { + return; + } + for (var i = verb_tabs.length - 1; i >= 0; --i) { + if (verb_tabs[i] == name) { + verb_tabs.splice(i, 1); + } + } + menu.removeChild(document.getElementById(name)); + TakeTabFromByond(name); + under_menu.style.height = menu.clientHeight + 'px'; +} + +function sortVerbs() { + verbs.sort(function (a, b) { + var selector = a[0] == b[0] ? 1 : 0; + if (a[selector].toUpperCase() < b[selector].toUpperCase()) { + return 1; + } + else if (a[selector].toUpperCase() > b[selector].toUpperCase()) { + return -1; + } + return 0; + }) +} + +window.onresize = function () { + under_menu.style.height = menu.clientHeight + 'px'; +} + +function addPermanentTab(name) { + if (!permanent_tabs.includes(name)) { + permanent_tabs.push(name); + } + createStatusTab(name); +} + +function removePermanentTab(name) { + for (var i = permanent_tabs.length - 1; i >= 0; --i) { + if (permanent_tabs[i] == name) { + permanent_tabs.splice(i, 1); + } + } + removeStatusTab(name); +} + +function checkStatusTab() { + for (var i = 0; i < menu.children.length; i++) { + if (!verb_tabs.includes(menu.children[i].id) && !permanent_tabs.includes(menu.children[i].id)) { + menu.removeChild(menu.children[i]); + } + } +} + +function remove_verb(v) { + var verb_to_remove = v; // to_remove = [verb:category, verb:name] + for (var i = verbs.length - 1; i >= 0; i--) { + var part_to_remove = verbs[i]; + if (part_to_remove[1] == verb_to_remove[1]) { + verbs.splice(i, 1) + } + } +} + +function check_verbs() { + for (var v = verb_tabs.length - 1; v >= 0; v--) { + verbs_cat_check(verb_tabs[v]); + } +} + +function verbs_cat_check(cat) { + var tabCat = cat; + if (cat.indexOf(".") != -1) { + var splitName = cat.split("."); + if (split_admin_tabs && splitName[0] === "Admin") + tabCat = splitName[1]; + else + tabCat = splitName[0]; + } + var verbs_in_cat = 0; + var verbcat = ""; + if (!verb_tabs.includes(tabCat)) { + removeStatusTab(tabCat); + return; + } + for (var v = 0; v < verbs.length; v++) { + var part = verbs[v]; + verbcat = part[0]; + if (verbcat.indexOf(".") != -1) { + var splitName = verbcat.split("."); + if (split_admin_tabs && splitName[0] === "Admin") + verbcat = splitName[1]; + else + verbcat = splitName[0]; + } + if (verbcat != tabCat || verbcat.trim() == "") { + continue; + } + else { + verbs_in_cat = 1; + break; // we only need one + } + } + if (verbs_in_cat != 1) { + removeStatusTab(tabCat); + if (current_tab == tabCat) + tab_change("Status"); + } +} + +function findVerbindex(name, verblist) { + for (var i = 0; i < verblist.length; i++) { + var part = verblist[i]; + if (part[1] == name) + return i; + } +} +function wipe_verbs() { + verbs = [["", ""]]; + verb_tabs = []; + checkStatusTab(); // remove all empty verb tabs +} + +function update_verbs() { + wipe_verbs(); + Byond.sendMessage("Update-Verbs"); +} + +function SendTabsToByond() { + var tabstosend = []; + tabstosend = tabstosend.concat(permanent_tabs, verb_tabs); + for (var i = 0; i < tabstosend.length; i++) { + SendTabToByond(tabstosend[i]); + } +} + +function SendTabToByond(tab) { + Byond.sendMessage("Send-Tabs", {tab: tab}); +} + +//Byond can't have this tab anymore since we're removing it +function TakeTabFromByond(tab) { + Byond.sendMessage("Remove-Tabs", {tab: tab}); +} + +function spell_cat_check(cat) { + var spells_in_cat = 0; + var spellcat = ""; + for (var s = 0; s < spells.length; s++) { + var spell = spells[s]; + spellcat = spell[0]; + if (spellcat == cat) { + spells_in_cat++; + } + } + if (spells_in_cat < 1) { + removeStatusTab(cat); + } +} + +function tab_change(tab) { + if (tab == current_tab) return; + if (document.getElementById(current_tab)) + document.getElementById(current_tab).className = "button"; // disable active on last button + current_tab = tab; + set_byond_tab(tab); + if (document.getElementById(tab)) + document.getElementById(tab).className = "button active"; // make current button active + var spell_tabs_thingy = (spell_tabs.includes(tab)); + var verb_tabs_thingy = (verb_tabs.includes(tab)); + if (tab == "Status") { + draw_status(); + } else if (tab == "MC") { + draw_mc(); + } else if (spell_tabs_thingy) { + draw_spells(tab); + } else if (verb_tabs_thingy) { + draw_verbs(tab); + } else if (tab == "Debug Stat Panel") { + draw_debug(); + } else if (tab == "Tickets") { + draw_tickets(); + draw_interviews(); + } else if (tab == "SDQL2") { + draw_sdql2(); + } else if (tab == turfname) { + draw_listedturf(); + } else { + statcontentdiv.textContext = "Loading..."; + } + Byond.winset(Byond.windowId, { + 'is-visible': true, + }); +} + +function set_byond_tab(tab) { + Byond.sendMessage("Set-Tab", {tab: tab}); +} + +function draw_debug() { + statcontentdiv.textContent = ""; + var wipeverbstabs = document.createElement("div"); + var link = document.createElement("a"); + link.onclick = function () { wipe_verbs() }; + link.textContent = "Wipe All Verbs"; + wipeverbstabs.appendChild(link); + document.getElementById("statcontent").appendChild(wipeverbstabs); + var wipeUpdateVerbsTabs = document.createElement("div"); + var updateLink = document.createElement("a"); + updateLink.onclick = function () { update_verbs() }; + updateLink.textContent = "Wipe and Update All Verbs"; + wipeUpdateVerbsTabs.appendChild(updateLink); + document.getElementById("statcontent").appendChild(wipeUpdateVerbsTabs); + var text = document.createElement("div"); + text.textContent = "Verb Tabs:"; + document.getElementById("statcontent").appendChild(text); + var table1 = document.createElement("table"); + for (var i = 0; i < verb_tabs.length; i++) { + var part = verb_tabs[i]; + // Hide subgroups except admin subgroups if they are split + if (verb_tabs[i].lastIndexOf(".") != -1) { + var splitName = verb_tabs[i].split("."); + if (split_admin_tabs && splitName[0] === "Admin") + part = splitName[1]; + else + continue; + } + var tr = document.createElement("tr"); + var td1 = document.createElement("td"); + td1.textContent = part; + var a = document.createElement("a"); + a.onclick = function (part) { + return function () { removeStatusTab(part) }; + }(part); + a.textContent = " Delete Tab " + part; + td1.appendChild(a); + tr.appendChild(td1); + table1.appendChild(tr); + } + document.getElementById("statcontent").appendChild(table1); + var header2 = document.createElement("div"); + header2.textContent = "Verbs:"; + document.getElementById("statcontent").appendChild(header2); + var table2 = document.createElement("table"); + for (var v = 0; v < verbs.length; v++) { + var part2 = verbs[v]; + var trr = document.createElement("tr"); + var tdd1 = document.createElement("td"); + tdd1.textContent = part2[0]; + var tdd2 = document.createElement("td"); + tdd2.textContent = part2[1]; + trr.appendChild(tdd1); + trr.appendChild(tdd2); + table2.appendChild(trr); + } + document.getElementById("statcontent").appendChild(table2); + var text3 = document.createElement("div"); + text3.textContent = "Permanent Tabs:"; + document.getElementById("statcontent").appendChild(text3); + var table3 = document.createElement("table"); + for (var i = 0; i < permanent_tabs.length; i++) { + var part3 = permanent_tabs[i]; + var trrr = document.createElement("tr"); + var tddd1 = document.createElement("td"); + tddd1.textContent = part3; + trrr.appendChild(tddd1); + table3.appendChild(trrr); + } + document.getElementById("statcontent").appendChild(table3); + +} +function draw_status() { + if (!document.getElementById("Status")) { + createStatusTab("Status"); + current_tab = "Status"; + } + statcontentdiv.textContent = ''; + for (var i = 0; i < status_tab_parts.length; i++) { + if (status_tab_parts[i].trim() == "") { + document.getElementById("statcontent").appendChild(document.createElement("br")); + } else { + var div = document.createElement("div"); + div.textContent = status_tab_parts[i]; + document.getElementById("statcontent").appendChild(div); + } + } + if (verb_tabs.length == 0 || !verbs) { + Byond.command("Fix-Stat-Panel"); + } +} + +function draw_mc() { + statcontentdiv.textContent = ""; + var table = document.createElement("table"); + for (var i = 0; i < mc_tab_parts.length; i++) { + var part = mc_tab_parts[i]; + var tr = document.createElement("tr"); + var td1 = document.createElement("td"); + td1.textContent = part[0]; + var td2 = document.createElement("td"); + if (part[2]) { + var a = document.createElement("a"); + a.href = "?_src_=vars;admin_token=" + href_token + ";Vars=" + part[2]; + a.textContent = part[1]; + td2.appendChild(a); + } else { + td2.textContent = part[1]; + } + tr.appendChild(td1); + tr.appendChild(td2); + table.appendChild(tr); + } + document.getElementById("statcontent").appendChild(table); +} + +function remove_tickets() { + if (tickets) { + tickets = []; + removePermanentTab("Tickets"); + if (current_tab == "Tickets") + tab_change("Status"); + } + checkStatusTab(); +} + +function remove_sdql2() { + if (sdql2) { + sdql2 = []; + removePermanentTab("SDQL2"); + if (current_tab == "SDQL2") + tab_change("Status"); + } + checkStatusTab(); +} + +function remove_interviews() { + if (tickets) { + tickets = []; + } + checkStatusTab(); +} + +function iconError(e) { + if(current_tab != turfname) { + return; + } + setTimeout(function () { + var node = e.target; + var current_attempts = Number(node.getAttribute("data-attempts")) || 0 + if (current_attempts > imageRetryLimit) { + return; + } + var src = node.src; + node.src = null; + node.src = src + '#' + current_attempts; + node.setAttribute("data-attempts", current_attempts + 1) + draw_listedturf(); + }, imageRetryDelay); +} + +function draw_listedturf() { + statcontentdiv.textContent = ""; + var table = document.createElement("table"); + for (var i = 0; i < turfcontents.length; i++) { + var part = turfcontents[i]; + if (storedimages[part[1]] == null && part[2]) { + var img = document.createElement("img"); + img.src = part[2]; + img.id = part[1]; + storedimages[part[1]] = part[2]; + img.onerror = iconError; + table.appendChild(img); + } else { + var img = document.createElement("img"); + img.onerror = iconError; + img.src = storedimages[part[1]]; + img.id = part[1]; + table.appendChild(img); + } + var b = document.createElement("div"); + var clickcatcher = ""; + b.className = "link"; + b.onmousedown = function (part) { + // The outer function is used to close over a fresh "part" variable, + // rather than every onmousedown getting the "part" of the last entry. + return function (e) { + e.preventDefault(); + clickcatcher = "?src=" + part[1]; + switch (e.button) { + case 1: + clickcatcher += ";statpanel_item_click=middle" + break; + case 2: + clickcatcher += ";statpanel_item_click=right" + break; + default: + clickcatcher += ";statpanel_item_click=left" + } + if (e.shiftKey) { + clickcatcher += ";statpanel_item_shiftclick=1"; + } + if (e.ctrlKey) { + clickcatcher += ";statpanel_item_ctrlclick=1"; + } + if (e.altKey) { + clickcatcher += ";statpanel_item_altclick=1"; + } + window.location.href = clickcatcher; + } + }(part); + b.textContent = part[0]; + table.appendChild(b); + table.appendChild(document.createElement("br")); + } + document.getElementById("statcontent").appendChild(table); +} + +function remove_listedturf() { + removePermanentTab(turfname); + checkStatusTab(); + if (current_tab == turfname) { + tab_change("Status"); + } +} + +function remove_mc() { + removeStatusTab("MC"); + if (current_tab == "MC") { + tab_change("Status"); + } +}; + +function draw_sdql2() { + statcontentdiv.textContent = ""; + var table = document.createElement("table"); + for (var i = 0; i < sdql2.length; i++) { + var part = sdql2[i]; + var tr = document.createElement("tr"); + var td1 = document.createElement("td"); + td1.textContent = part[0]; + var td2 = document.createElement("td"); + if (part[2]) { + var a = document.createElement("a"); + a.href = "?src=" + part[2] + ";statpanel_item_click=left"; + a.textContent = part[1]; + td2.appendChild(a); + } else { + td2.textContent = part[1]; + } + tr.appendChild(td1); + tr.appendChild(td2); + table.appendChild(tr); + } + document.getElementById("statcontent").appendChild(table); +} + +function draw_tickets() { + statcontentdiv.textContent = ""; + var table = document.createElement("table"); + if (!tickets) { + return; + } + for (var i = 0; i < tickets.length; i++) { + var part = tickets[i]; + var tr = document.createElement("tr"); + var td1 = document.createElement("td"); + td1.textContent = part[0]; + var td2 = document.createElement("td"); + if (part[2]) { + var a = document.createElement("a"); + a.href = "?_src_=holder;admin_token=" + href_token + ";ahelp=" + part[2] + ";ahelp_action=ticket;statpanel_item_click=left;action=ticket"; + a.textContent = part[1]; + td2.appendChild(a); + } else if (part[3]) { + var a = document.createElement("a"); + a.href = "?src=" + part[3] + ";statpanel_item_click=left"; + a.textContent = part[1]; + td2.appendChild(a); + } else { + td2.textContent = part[1]; + } + tr.appendChild(td1); + tr.appendChild(td2); + table.appendChild(tr); + } + document.getElementById("statcontent").appendChild(table); +} + +function draw_interviews() { + var body = document.createElement("div"); + var header = document.createElement("h3"); + header.textContent = "Interviews"; + body.appendChild(header); + var manDiv = document.createElement("div"); + manDiv.className = "interview_panel_controls" + var manLink = document.createElement("a"); + manLink.textContent = "Open Interview Manager Panel"; + manLink.href = "?_src_=holder;admin_token=" + href_token + ";interview_man=1;statpanel_item_click=left"; + manDiv.appendChild(manLink); + body.appendChild(manDiv); + + // List interview stats + var statsDiv = document.createElement("table"); + statsDiv.className = "interview_panel_stats"; + for (var key in interviewManager.status) { + var d = document.createElement("div"); + var tr = document.createElement("tr"); + var stat_name = document.createElement("td"); + var stat_text = document.createElement("td"); + stat_name.textContent = key; + stat_text.textContent = interviewManager.status[key]; + tr.appendChild(stat_name); + tr.appendChild(stat_text); + statsDiv.appendChild(tr); + } + body.appendChild(statsDiv); + document.getElementById("statcontent").appendChild(body); + + // List interviews if any are open + var table = document.createElement("table"); + table.className = "interview_panel_table"; + if (!interviewManager) { + return; + } + for (var i = 0; i < interviewManager.interviews.length; i++) { + var part = interviewManager.interviews[i]; + var tr = document.createElement("tr"); + var td = document.createElement("td"); + var a = document.createElement("a"); + a.textContent = part["status"]; + a.href = "?_src_=holder;admin_token=" + href_token + ";interview=" + part["ref"] + ";statpanel_item_click=left"; + td.appendChild(a); + tr.appendChild(td); + table.appendChild(tr); + } + document.getElementById("statcontent").appendChild(table); +} + +function draw_spells(cat) { + statcontentdiv.textContent = ""; + var table = document.createElement("table"); + for (var i = 0; i < spells.length; i++) { + var part = spells[i]; + if (part[0] != cat) continue; + var tr = document.createElement("tr"); + var td1 = document.createElement("td"); + td1.textContent = part[1]; + var td2 = document.createElement("td"); + if (part[3]) { + var a = document.createElement("a"); + a.href = "?src=" + part[3] + ";statpanel_item_click=left"; + a.textContent = part[2]; + td2.appendChild(a); + } else { + td2.textContent = part[2]; + } + tr.appendChild(td1); + tr.appendChild(td2); + table.appendChild(tr); + } + document.getElementById("statcontent").appendChild(table); +} + +function make_verb_onclick(command) { + return function () { + run_after_focus(function () { + Byond.command(command); + }); + }; +} + +function draw_verbs(cat) { + statcontentdiv.textContent = ""; + var table = document.createElement("div"); + var additions = {}; // additional sub-categories to be rendered + table.className = "grid-container"; + sortVerbs(); + if (split_admin_tabs && cat.lastIndexOf(".") != -1) { + var splitName = cat.split("."); + if (splitName[0] === "Admin") + cat = splitName[1]; + } + verbs.reverse(); // sort verbs backwards before we draw + for (var i = 0; i < verbs.length; ++i) { + var part = verbs[i]; + var name = part[0]; + if (split_admin_tabs && name.lastIndexOf(".") != -1) { + var splitName = name.split("."); + if (splitName[0] === "Admin") + name = splitName[1]; + } + var command = part[1]; + + if (command && name.lastIndexOf(cat, 0) != -1 && (name.length == cat.length || name.charAt(cat.length) == ".")) { + var subCat = name.lastIndexOf(".") != -1 ? name.split(".")[1] : null; + if (subCat && !additions[subCat]) { + var newTable = document.createElement("div"); + newTable.className = "grid-container"; + additions[subCat] = newTable; + } + + var a = document.createElement("a"); + a.href = "#"; + a.onclick = make_verb_onclick(command.replace(/\s/g, "-")); + a.className = "grid-item"; + var t = document.createElement("span"); + t.textContent = command; + t.className = "grid-item-text"; + a.appendChild(t); + (subCat ? additions[subCat] : table).appendChild(a); + } + } + + // Append base table to view + var content = document.getElementById("statcontent"); + content.appendChild(table); + + // Append additional sub-categories if relevant + for (var cat in additions) { + if (additions.hasOwnProperty(cat)) { + // do addition here + var header = document.createElement("h3"); + header.textContent = cat; + content.appendChild(header); + content.appendChild(additions[cat]); + } + } +} + +function set_theme(which) { + if (which == "light") { + document.body.className = ""; + set_style_sheet("browserOutput_white"); + } else if (which == "dark") { + document.body.className = "dark"; + set_style_sheet("browserOutput"); + } +} + +function set_style_sheet(sheet) { + if (document.getElementById("goonStyle")) { + var currentSheet = document.getElementById("goonStyle"); + currentSheet.parentElement.removeChild(currentSheet); + } + var head = document.getElementsByTagName('head')[0]; + var sheetElement = document.createElement("link"); + sheetElement.id = "goonStyle"; + sheetElement.rel = "stylesheet"; + sheetElement.type = "text/css"; + sheetElement.href = sheet + ".css"; + sheetElement.media = 'all'; + head.appendChild(sheetElement); +} + +function restoreFocus() { + run_after_focus(function () { + Byond.winset('map', { + focus: true, + }); + }); +} + +function getCookie(cname) { + var name = cname + '='; + var ca = document.cookie.split(';'); + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) == ' ') c = c.substring(1); + if (c.indexOf(name) === 0) { + return decoder(c.substring(name.length, c.length)); + } + } + return ''; +} + +function add_verb_list(payload) { + var to_add = payload; // list of a list with category and verb inside it + to_add.sort(); // sort what we're adding + for (var i = 0; i < to_add.length; i++) { + var part = to_add[i]; + if (!part[0]) + continue; + var category = part[0]; + if (category.indexOf(".") != -1) { + var splitName = category.split("."); + if (split_admin_tabs && splitName[0] === "Admin") + category = splitName[1]; + else + category = splitName[0]; + } + if (findVerbindex(part[1], verbs)) + continue; + if (verb_tabs.includes(category)) { + verbs.push(part); + if (current_tab == category) { + draw_verbs(category); // redraw if we added a verb to the tab we're currently in + } + } else if (category) { + verb_tabs.push(category); + verbs.push(part); + createStatusTab(category); + } + } +}; + +function init_spells() { + var cat = ""; + for (var i = 0; i < spell_tabs.length; i++) { + cat = spell_tabs[i]; + if (cat.length > 0) { + verb_tabs.push(cat); + createStatusTab(cat); + } + } +} + +document.addEventListener("mouseup", restoreFocus); +document.addEventListener("keyup", restoreFocus); + +if (!current_tab) { + addPermanentTab("Status"); + tab_change("Status"); +} + +window.onload = function () { + Byond.command("Update-Verbs"); +}; + +Byond.subscribeTo('update_spells', function (payload) { + spell_tabs = payload.spell_tabs; + var do_update = false; + if (spell_tabs.includes(current_tab)) { + do_update = true; + } + init_spells(); + if (payload.verblist) { + spells = payload.verblist; + if (do_update) { + draw_spells(current_tab); + } + } else { + remove_spells(); + } +}); + +Byond.subscribeTo('remove_verb_list', function (v) { + var to_remove = v; + for (var i = 0; i < to_remove.length; i++) { + remove_verb(to_remove[i]); + } + check_verbs(); + sortVerbs(); + if (verb_tabs.includes(current_tab)) + draw_verbs(current_tab); +}); + +// passes a 2D list of (verbcategory, verbname) creates tabs and adds verbs to respective list +// example (IC, Say) +Byond.subscribeTo('init_verbs', function (payload) { + wipe_verbs(); // remove all verb categories so we can replace them + checkStatusTab(); // remove all status tabs + verb_tabs = payload.panel_tabs; + verb_tabs.sort(); // sort it + var do_update = false; + var cat = ""; + for (var i = 0; i < verb_tabs.length; i++) { + cat = verb_tabs[i]; + createStatusTab(cat); // create a category if the verb doesn't exist yet + } + if (verb_tabs.includes(current_tab)) { + do_update = true; + } + if (payload.verblist) { + add_verb_list(payload.verblist); + sortVerbs(); // sort them + if (do_update) { + draw_verbs(current_tab); + } + } + SendTabsToByond(); +}); + +Byond.subscribeTo('update_stat', function (payload) { + status_tab_parts = [payload.ping_str]; + var parsed = payload.global_data; + + for (var i = 0; i < parsed.length; i++) if (parsed[i] != null) status_tab_parts.push(parsed[i]); + + parsed = payload.other_str; + + for (var i = 0; i < parsed.length; i++) if (parsed[i] != null) status_tab_parts.push(parsed[i]); + + if (current_tab == "Status") { + draw_status(); + } else if (current_tab == "Debug Stat Panel") { + draw_debug(); + } +}); + +Byond.subscribeTo('update_mc', function (payload) { + mc_tab_parts = payload.mc_data; + mc_tab_parts.splice(0, 0, ["Location:", payload.coord_entry]); + + if (!verb_tabs.includes("MC")) { + verb_tabs.push("MC"); + } + + createStatusTab("MC"); + + if (current_tab == "MC") { + draw_mc(); + } +}); + +Byond.subscribeTo('remove_spells', function () { + for (var s = 0; s < spell_tabs.length; s++) { + removeStatusTab(spell_tabs[s]); + } +}); + +Byond.subscribeTo('init_spells', function () { + var cat = ""; + for (var i = 0; i < spell_tabs.length; i++) { + cat = spell_tabs[i]; + if (cat.length > 0) { + verb_tabs.push(cat); + createStatusTab(cat); + } + } +}); + +Byond.subscribeTo('check_spells', function () { + for (var v = 0; v < spell_tabs.length; v++) { + spell_cat_check(spell_tabs[v]); + } +}); + +Byond.subscribeTo('create_debug', function () { + if (!document.getElementById("Debug Stat Panel")) { + addPermanentTab("Debug Stat Panel"); + } else { + removePermanentTab("Debug Stat Panel"); + } +}); + +Byond.subscribeTo('create_listedturf', function (TN) { + remove_listedturf(); // remove the last one if we had one + turfname = TN; + addPermanentTab(turfname); + tab_change(turfname); +}); + +Byond.subscribeTo('remove_admin_tabs', function () { + href_token = null; + remove_mc(); + remove_tickets(); + remove_sdql2(); + remove_interviews(); +}); + +Byond.subscribeTo('update_listedturf', function (TC) { + turfcontents = TC; + if (current_tab == turfname) { + draw_listedturf(); + } +}); + +Byond.subscribeTo('update_interviews', function (I) { + interviewManager = I; + if (current_tab == "Tickets") { + draw_interviews(); + } +}); + +Byond.subscribeTo('update_split_admin_tabs', function (status) { + status = (status == true); + + if (split_admin_tabs !== status) { + if (split_admin_tabs === true) { + removeStatusTab("Events"); + removeStatusTab("Fun"); + removeStatusTab("Game"); + } + update_verbs(); + } + split_admin_tabs = status; +}); + +Byond.subscribeTo('add_admin_tabs', function (ht) { + href_token = ht; + addPermanentTab("MC"); + addPermanentTab("Tickets"); +}); + +Byond.subscribeTo('update_sdql2', function (S) { + sdql2 = S; + if (sdql2.length > 0 && !verb_tabs.includes("SDQL2")) { + verb_tabs.push("SDQL2"); + addPermanentTab("SDQL2"); + } + if (current_tab == "SDQL2") { + draw_sdql2(); + } +}); + +Byond.subscribeTo('update_tickets', function (T) { + tickets = T; + if (!verb_tabs.includes("Tickets")) { + verb_tabs.push("Tickets"); + addPermanentTab("Tickets"); + } + if (current_tab == "Tickets") { + draw_tickets(); + } +}); + +Byond.subscribeTo('remove_listedturf', remove_listedturf); + +Byond.subscribeTo('remove_sdql2', remove_sdql2); + +Byond.subscribeTo('remove_mc', remove_mc); + +Byond.subscribeTo('add_verb_list', add_verb_list); diff --git a/shiptest.dme b/shiptest.dme index 0f1e73571449..29479c092022 100644 --- a/shiptest.dme +++ b/shiptest.dme @@ -375,6 +375,7 @@ #include "code\controllers\subsystem\processing\nanites.dm" #include "code\controllers\subsystem\processing\networks.dm" #include "code\controllers\subsystem\processing\obj.dm" +#include "code\controllers\subsystem\processing\obj_tab_items.dm" #include "code\controllers\subsystem\processing\processing.dm" #include "code\controllers\subsystem\processing\projectiles.dm" #include "code\controllers\subsystem\processing\quirks.dm" @@ -458,6 +459,7 @@ #include "code\datums\components\chasm.dm" #include "code\datums\components\connect_containers.dm" #include "code\datums\components\connect_loc_behalf.dm" +#include "code\datums\components\connect_mob_behalf.dm" #include "code\datums\components\connect_range.dm" #include "code\datums\components\construction.dm" #include "code\datums\components\creamed.dm" diff --git a/tgui/packages/tgui/interfaces/NtosRoboControl.js b/tgui/packages/tgui/interfaces/NtosRoboControl.js index f6bdb42bd86c..b206018c4e2e 100644 --- a/tgui/packages/tgui/interfaces/NtosRoboControl.js +++ b/tgui/packages/tgui/interfaces/NtosRoboControl.js @@ -83,7 +83,7 @@ const RobotInfo = (props, context) => {