diff --git a/code/__defines/subsystems.dm b/code/__defines/subsystems.dm index e498375e0a..cbe3917ed7 100644 --- a/code/__defines/subsystems.dm +++ b/code/__defines/subsystems.dm @@ -164,6 +164,7 @@ var/global/list/runlevel_flags = list(RUNLEVEL_LOBBY, RUNLEVEL_SETUP, RUNLEVEL_G #define INIT_ORDER_MAPRENAME -60 //Initiating after Ticker to ensure everything is loaded and everything we rely on us working #define INIT_ORDER_WIKI -61 #define INIT_ORDER_ATC -70 +#define INIT_ORDER_LOOT -80 #define INIT_ORDER_STATPANELS -98 #define INIT_ORDER_CHAT -100 //Should be last to ensure chat remains smooth during init. diff --git a/code/__defines/traits/declarations.dm b/code/__defines/traits/declarations.dm index bca9f5cf26..8936a3d55d 100644 --- a/code/__defines/traits/declarations.dm +++ b/code/__defines/traits/declarations.dm @@ -12,3 +12,6 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai /// Climbable trait, given and taken by the climbable element when added or removed. Exists to be easily checked via HAS_TRAIT(). #define TRAIT_CLIMBABLE "trait_climbable" + +/// Prevents the affected object from opening a loot window via alt click. See atom/AltClick() +#define TRAIT_ALT_CLICK_BLOCKER "no_alt_click" diff --git a/code/_global_vars/traits/_traits.dm b/code/_global_vars/traits/_traits.dm index 7a70e246b8..8b98c1c686 100644 --- a/code/_global_vars/traits/_traits.dm +++ b/code/_global_vars/traits/_traits.dm @@ -17,6 +17,9 @@ GLOBAL_LIST_INIT(radiation_levels, list( /* CHOMPRemove, see traits.dm GLOBAL_LIST_INIT(traits_by_type, list( + /atom = list( + "TRAIT_ALT_CLICK_BLOCKER" = TRAIT_ALT_CLICK_BLOCKER, + ), /mob = list( "TRAIT_THINKING_IN_CHARACTER" = TRAIT_THINKING_IN_CHARACTER, ), diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm index f16c35e216..deebc3e07a 100644 --- a/code/_onclick/click.dm +++ b/code/_onclick/click.dm @@ -293,11 +293,37 @@ A.AltClick(src) return +/** + * Alt click on an atom. + * Performs alt-click actions before attempting to open a loot window. + * Returns TRUE if successful, FALSE if not. + */ /atom/proc/AltClick(var/mob/user) - var/turf/T = get_turf(src) - if(T && user.TurfAdjacent(T)) - user.set_listed_turf(T) - return 1 + // if(!user.can_interact_with(src)) + // return FALSE + + // if(SEND_SIGNAL(src, COMSIG_CLICK_ALT, user) & COMPONENT_CANCEL_CLICK_ALT) + // return TRUE + + if(HAS_TRAIT(src, TRAIT_ALT_CLICK_BLOCKER) && !isobserver(user)) + return TRUE + + var/turf/tile = get_turf(src) + if(isnull(tile)) + return FALSE + + if(!isturf(loc) && !isturf(src)) + return FALSE + + if(!user.TurfAdjacent(tile)) + return FALSE + + var/datum/lootpanel/panel = user.client?.loot_panel + if(isnull(panel)) + return FALSE + + panel.open(tile) + return TRUE /mob/proc/TurfAdjacent(var/turf/T) return T.AdjacentQuick(src) diff --git a/code/_onclick/observer.dm b/code/_onclick/observer.dm index 0022bd4dd9..71de8a5bf5 100644 --- a/code/_onclick/observer.dm +++ b/code/_onclick/observer.dm @@ -41,7 +41,7 @@ if(modifiers["alt"]) // alt and alt-gr (rightalt) var/turf/T = get_turf(A) if(T && TurfAdjacent(T)) - set_listed_turf(T) + T.AltClick(src) return // You are responsible for checking config.ghost_interaction when you override this function // Not all of them require checking, see below diff --git a/code/controllers/subsystems/statpanel.dm b/code/controllers/subsystems/statpanel.dm index ad76988b5d..4f1cfeef13 100644 --- a/code/controllers/subsystems/statpanel.dm +++ b/code/controllers/subsystems/statpanel.dm @@ -9,7 +9,6 @@ SUBSYSTEM_DEF(statpanels) var/list/currentrun = list() var/list/global_data var/list/mc_data - var/list/cached_images = list() ///how many subsystem fires between most tab updates var/default_wait = 10 @@ -102,12 +101,6 @@ SUBSYSTEM_DEF(statpanels) if((num_fires % misc_wait == 0)) update_misc_tabs(target,target_mob) - var/datum/object_window_info/obj_window = target.obj_window - if(obj_window) - if(obj_window.flags & TURFLIST_UPDATE_QUEUED) - immediate_send_stat_data(target) - obj_window.flags = 0 - if(MC_TICK_CHECK) return @@ -153,14 +146,17 @@ SUBSYSTEM_DEF(statpanels) var/description_holders = target.description_holders var/list/examine_update = list() - if(!target.obj_window) - target.obj_window = new(target) - if(!target.examine_icon && !target.obj_window.examine_target && target.stat_tab == "Examine") - target.obj_window.examine_target = description_holders["icon"] - target.obj_window.atoms_to_show += target.obj_window.examine_target - START_PROCESSING(SSobj_tab_items, target.obj_window) - refresh_client_obj_view(target) - examine_update += "[target.examine_icon] " + span_giant("[description_holders["name"]]") //The name, written in big letters. + var/atom/atom_icon = description_holders["icon"] + var/shown_icon = target.examine_icon + if(!shown_icon) + if(ismob(atom_icon) || length(atom_icon.overlays) > 0) + var/force_south = FALSE + if(isliving(atom_icon)) + force_south = TRUE + shown_icon = costly_icon2html(atom_icon, target, sourceonly=TRUE, force_south = force_south) + else + shown_icon = icon2html(atom_icon, target, sourceonly=TRUE) + examine_update += " " + span_giant("[description_holders["name"]]") //The name, written in big letters. examine_update += "[description_holders["desc"]]" //the default examine text. if(description_holders["info"]) examine_update += span_blue(span_bold("[replacetext(description_holders["info"], "\n", "
")]")) + "
" //Blue, informative text. @@ -205,81 +201,6 @@ SUBSYSTEM_DEF(statpanels) //target.stat_panel.send_message("update_spells", list(spell_tabs = target.spell_tabs, actions = actions)) -/datum/controller/subsystem/statpanels/proc/set_turf_examine_tab(client/target, mob/target_mob) - if(!target)//statbrowser hasnt fired yet and we were called from immediate_send_stat_data() - return - var/list/overrides = list() - for(var/image/target_image as anything in target.images) - if(!target_image.loc || target_image.loc.loc != target.tracked_turf || !target_image.override) - continue - overrides += target_image.loc - - var/list/atoms_to_display = list(target.tracked_turf) - for(var/atom/movable/turf_content as anything in target.tracked_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 - if(!obj_window) - return // previous one no longer exists - 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) - if(turf_item == obj_window.examine_target) //not actually a turf item get trolled - load_from.examine_icon = "" - obj_window.examine_target = null - set_examine_tab(load_from) - continue - 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, TYPE_PROC_REF(/datum/object_window_info,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() mc_data = list( list("CPU:", world.cpu), @@ -324,16 +245,6 @@ SUBSYSTEM_DEF(statpanels) set_action_tabs(target, target_mob) return TRUE - // Handle turfs - - if(target.tracked_turf) - if(!target_mob.TurfAdjacent(target.tracked_turf)) - target_mob.set_listed_turf(null) - - else if(target.stat_tab == target.tracked_turf.name || !(target.tracked_turf.name in target.panel_tabs)) - set_turf_examine_tab(target, target_mob) - return TRUE - if(!target.holder) return FALSE @@ -351,136 +262,5 @@ SUBSYSTEM_DEF(statpanels) else if(length(GLOB.sdql2_queries) && target.stat_tab == "SDQL2") set_SDQL2_tab(target) -/atom/proc/remove_from_cache() - SIGNAL_HANDLER - SSstatpanels.cached_images -= REF(src) - /// Stat panel window declaration /client/var/datum/tgui_window/stat_panel -/// Turf examine turf -/client/var/turf/tracked_turf - -/// 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 - ///For reusing this logic for examines - var/atom/examine_target - var/flags = 0 - -/datum/object_window_info/New(client/parent) - . = ..() - src.parent = parent - -/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(seconds_per_tick) - // 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] - - if(!thing) // A null thing snuck in somehow - continue - - var/generated_string - if(ismob(thing) || length(thing.overlays) > 0) - var/force_south = FALSE - if(isliving(thing)) - force_south = TRUE - generated_string = costly_icon2html(thing, parent, sourceonly=TRUE, force_south = force_south) - 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(turf/new_turf) - if(parent.tracked_turf) - 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) - RegisterSignal(new_turf, COMSIG_ATOM_ENTERED, PROC_REF(turflist_changed)) - RegisterSignal(new_turf, COMSIG_ATOM_EXITED, PROC_REF(turflist_changed)) - parent.stat_panel.send_message("create_listedturf", new_turf) - parent.tracked_turf = new_turf - -/datum/object_window_info/proc/stop_turf_tracking() - if(GetComponent(/datum/component/connect_mob_behalf)) - qdel(GetComponent(/datum/component/connect_mob_behalf)) - if(parent.tracked_turf) - UnregisterSignal(parent.tracked_turf, COMSIG_ATOM_ENTERED) - UnregisterSignal(parent.tracked_turf, COMSIG_ATOM_EXITED) - parent.stat_panel.send_message("remove_listedturf") - parent.tracked_turf = null - -/datum/object_window_info/proc/on_mob_move(mob/source) - SIGNAL_HANDLER - if(!parent.tracked_turf || !source.TurfAdjacent(parent.tracked_turf)) - source.set_listed_turf(null) - -/datum/object_window_info/proc/on_mob_logout(mob/source) - SIGNAL_HANDLER - on_mob_move(parent.mob) - -/datum/object_window_info/proc/turflist_changed(mob/source) - SIGNAL_HANDLER - if(!parent)//statbrowser hasnt fired yet and we still have a pending action - return - if(!(flags & TURFLIST_UPDATED)) //Limit updates to 1 per tick - SSstatpanels.immediate_send_stat_data(parent) - flags |= TURFLIST_UPDATED - else if(!(flags & TURFLIST_UPDATE_QUEUED)) - flags |= TURFLIST_UPDATE_QUEUED - -/// 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 - -/mob/proc/set_listed_turf(turf/new_turf) - if(!client) - return - if(!client.obj_window) - client.obj_window = new(client) - if(client.tracked_turf == new_turf) - return - if(!new_turf) - client.obj_window.stop_turf_tracking() //Needs to go before listed_turf is set to null so signals can be removed - return - client.obj_window.start_turf_tracking(new_turf) diff --git a/code/datums/mind.dm b/code/datums/mind.dm index 515d1ec24a..2a73f3b4b9 100644 --- a/code/datums/mind.dm +++ b/code/datums/mind.dm @@ -99,7 +99,6 @@ if(new_character.client) new_character.client.init_verbs() // re-initialize character specific verbs - new_character.set_listed_turf(null) /datum/mind/proc/store_memory(new_text) memory += "[new_text]
" diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index 4d9475f6a2..126b4630d9 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -398,6 +398,7 @@ // called just as an item is picked up (loc is not yet changed) /obj/item/proc/pickup(mob/user) + SEND_SIGNAL(src, COMSIG_ITEM_PICKUP, user) pixel_x = 0 pixel_y = 0 return diff --git a/code/game/objects/items/stacks/stack.dm b/code/game/objects/items/stacks/stack.dm index 608a638750..ef58ff6f66 100644 --- a/code/game/objects/items/stacks/stack.dm +++ b/code/game/objects/items/stacks/stack.dm @@ -29,6 +29,7 @@ var/pass_color = FALSE // Will the item pass its own color var to the created item? Dyed cloth, wood, etc. var/strict_color_stacking = FALSE // Will the stack merge with other stacks that are different colors? (Dyed cloth, wood, etc) + var/is_building = FALSE /obj/item/stack/Initialize(mapload, var/starting_amount) . = ..() @@ -158,6 +159,9 @@ var/required = quantity*recipe.req_amount var/produced = min(quantity*recipe.res_amount, recipe.max_res_amount) + if(is_building) + return + if (!can_use(required)) if (produced>1) to_chat(user, span_warning("You haven't got enough [src] to build \the [produced] [recipe.title]\s!")) @@ -175,9 +179,12 @@ if (recipe.time) to_chat(user, span_notice("Building [recipe.title] ...")) + is_building = TRUE if (!do_after(user, recipe.time)) + is_building = FALSE return + is_building = FALSE if (use(required)) var/atom/O if(recipe.use_material) diff --git a/code/game/objects/trash_eating.dm b/code/game/objects/trash_eating.dm index f9bf7ae899..bd75c86ed6 100644 --- a/code/game/objects/trash_eating.dm +++ b/code/game/objects/trash_eating.dm @@ -181,7 +181,7 @@ to_chat(user, span_notice("You can taste the sweet flavor of digital friendship. Or maybe it is something else.")) /obj/item/reagent_containers/food/after_trash_eaten(var/mob/living/user) - if(!reagents.total_volume) + if(!reagents?.total_volume) to_chat(user, span_notice("You can taste the flavor of garbage and leftovers. Delicious?")) else to_chat(user, span_notice("You can taste the flavor of gluttonous waste of food.")) diff --git a/code/modules/client/client defines.dm b/code/modules/client/client defines.dm index 3bb474b5b7..6086afa9d3 100644 --- a/code/modules/client/client defines.dm +++ b/code/modules/client/client defines.dm @@ -184,3 +184,6 @@ /// The DPI scale of the client. 1 is equivalent to 100% window scaling, 2 will be 200% window scaling var/window_scaling + + /// Loot panel for the client + var/datum/lootpanel/loot_panel diff --git a/code/modules/client/client procs.dm b/code/modules/client/client procs.dm index 7ed4c158d7..eade271a71 100644 --- a/code/modules/client/client procs.dm +++ b/code/modules/client/client procs.dm @@ -305,6 +305,8 @@ tgui_say.initialize() tgui_shocker.initialize() + loot_panel = new(src) + connection_time = world.time connection_realtime = world.realtime connection_timeofday = world.timeofday @@ -392,6 +394,7 @@ GLOB.directory -= ckey GLOB.clients -= src + QDEL_NULL(loot_panel) ..() return QDEL_HINT_HARDDEL_NOW diff --git a/code/modules/client/preference_setup/general/13_nif.dm b/code/modules/client/preference_setup/general/13_nif.dm index 6cc9c48c44..6d42257812 100644 --- a/code/modules/client/preference_setup/general/13_nif.dm +++ b/code/modules/client/preference_setup/general/13_nif.dm @@ -53,7 +53,7 @@ /datum/category_item/player_setup_item/general/nif/copy_to_mob(var/mob/living/carbon/human/character) //If you had a NIF... - if(istype(character) && ispath(pref.nif_path) && pref.nif_durability) + if(istype(character) && ispath(pref.nif_path) && pref.nif_durability && !ismannequin(character)) new pref.nif_path(character, pref.nif_durability, pref.nif_savedata) /datum/category_item/player_setup_item/general/nif/tgui_data(mob/user, datum/tgui/ui, datum/tgui_state/state) diff --git a/code/modules/lootpanel/_lootpanel.dm b/code/modules/lootpanel/_lootpanel.dm new file mode 100644 index 0000000000..6331282b07 --- /dev/null +++ b/code/modules/lootpanel/_lootpanel.dm @@ -0,0 +1,80 @@ +/** + * ## Loot panel + * A datum that stores info containing the contents of a turf. + * Handles opening the lootpanel UI and searching the turf for items. + */ +/datum/lootpanel + /// The owner of the panel + var/client/owner + /// The list of all search objects indexed. + var/list/datum/search_object/contents = list() + /// The list of search_objects needing processed + var/list/datum/search_object/to_image = list() + /// We've been notified about client version + var/notified = FALSE + /// The turf being searched + var/turf/source_turf + + +/datum/lootpanel/New(client/owner) + . = ..() + + src.owner = owner + + +/datum/lootpanel/Destroy(force) + reset_contents() + owner = null + source_turf = null + + return ..() + + +/datum/lootpanel/tgui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "LootPanel") + ui.set_autoupdate(FALSE) + ui.open() + + +/datum/lootpanel/tgui_close(mob/user) + . = ..() + + source_turf = null + reset_contents() + + +/datum/lootpanel/tgui_data(mob/user) + var/list/data = list() + + data["contents"] = get_contents() + data["is_blind"] = !!user.is_blind() + data["searching"] = length(to_image) + + return data + + +/datum/lootpanel/tgui_status(mob/user, datum/tgui_state/state) + // note: different from /tg/, we prohibit non-viewers from trying to update the window and close it automatically for them + if(!(user in viewers(source_turf))) + return STATUS_CLOSE + + if(user.incapacitated()) + return STATUS_DISABLED + + return STATUS_INTERACTIVE + + +/datum/lootpanel/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state) + . = ..() + if(.) + return + + switch(action) + if("grab") + return grab(usr, params) + if("refresh") + return populate_contents() + + return FALSE diff --git a/code/modules/lootpanel/contents.dm b/code/modules/lootpanel/contents.dm new file mode 100644 index 0000000000..7121910354 --- /dev/null +++ b/code/modules/lootpanel/contents.dm @@ -0,0 +1,54 @@ +/// Adds the item to contents and to_image (if needed) +/datum/lootpanel/proc/add_to_index(datum/search_object/index) + RegisterSignal(index, COMSIG_PARENT_QDELETING, PROC_REF(on_searchable_deleted)) + if(isnull(index.icon)) + to_image += index + + contents += index + + +/// Used to populate contents and start generating if needed +/datum/lootpanel/proc/populate_contents() + if(length(contents)) + reset_contents() + + // Add source turf first + var/datum/search_object/source = new(owner, source_turf) + add_to_index(source) + + for(var/atom/thing as anything in source_turf.contents) + // validate + if(!istype(thing)) + stack_trace("Non-atom in the contents of [source_turf]!") + continue + if(QDELETED(thing)) + continue + if(thing.mouse_opacity == MOUSE_OPACITY_TRANSPARENT) + continue + // if(thing.IsObscured()) + // continue + if(thing.invisibility > owner.mob.see_invisible) + continue + + // convert + var/datum/search_object/index = new(owner, thing) + add_to_index(index) + + var/datum/tgui/window = SStgui.get_open_ui(owner.mob, src) + window?.send_update() + + if(length(to_image)) + SSlooting.backlog += src + + +/// For: Resetting to empty. Ignores the searchable qdel event +/datum/lootpanel/proc/reset_contents() + for(var/datum/search_object/index as anything in contents) + contents -= index + to_image -= index + + if(QDELETED(index)) + continue + + UnregisterSignal(index, COMSIG_PARENT_QDELETING) + qdel(index) diff --git a/code/modules/lootpanel/handlers.dm b/code/modules/lootpanel/handlers.dm new file mode 100644 index 0000000000..40a76974ed --- /dev/null +++ b/code/modules/lootpanel/handlers.dm @@ -0,0 +1,19 @@ +/// On contents change, either reset or update +/datum/lootpanel/proc/on_searchable_deleted(datum/search_object/source) + SIGNAL_HANDLER + + contents -= source + to_image -= source + + var/datum/tgui/window = SStgui.get_open_ui(owner.mob, src) +#if !defined(UNIT_TESTS) // we dont want to delete contents if we're testing + if(isnull(window)) + reset_contents() + return +#endif + + if(isturf(source.item)) + populate_contents() + return + + window?.send_update() diff --git a/code/modules/lootpanel/misc.dm b/code/modules/lootpanel/misc.dm new file mode 100644 index 0000000000..1f18790aa7 --- /dev/null +++ b/code/modules/lootpanel/misc.dm @@ -0,0 +1,48 @@ +/// Helper to open the panel +/datum/lootpanel/proc/open(turf/tile) + source_turf = tile + +#if !defined(OPENDREAM) && !defined(UNIT_TESTS) + if(!notified) + var/build = owner.byond_build + var/version = owner.byond_version + if(build < 515 || (build == 515 && version < 1635)) + to_chat(owner.mob, span_info("\ + Your version of Byond doesn't support fast image loading.\n\ + Detected: [version].[build]\n\ + Required version for this feature: 515.1635 or later.\n\ + Visit BYOND's website to get the latest version of BYOND.\n\ + ")) + + notified = TRUE +#endif + + populate_contents() + tgui_interact(owner.mob) + + +/** + * Called by SSlooting whenever this datum is added to its backlog. + * Iterates over to_image list to create icons, then removes them. + * Returns boolean - whether this proc has finished the queue or not. + */ +/datum/lootpanel/proc/process_images() + for(var/datum/search_object/index as anything in to_image) + to_image -= index + + if(QDELETED(index) || index.icon) + continue + + index.generate_icon(owner) + + if(TICK_CHECK) + break + + var/datum/tgui/window = SStgui.get_open_ui(owner.mob, src) + if(isnull(window)) + reset_contents() + return TRUE + + window.send_update() + + return !length(to_image) diff --git a/code/modules/lootpanel/search_object.dm b/code/modules/lootpanel/search_object.dm new file mode 100644 index 0000000000..60249f12f0 --- /dev/null +++ b/code/modules/lootpanel/search_object.dm @@ -0,0 +1,89 @@ +/** + * ## Search Object + * An object for content lists. Compacted item data. + */ +/datum/search_object + /// Item we're indexing + var/atom/item + /// Url to the image of the object + var/icon + /// Icon state, for inexpensive icons + var/icon_state + /// Name of the original object + var/name + /// Typepath of the original object for ui grouping + var/path + + +/datum/search_object/New(client/owner, atom/item) + . = ..() + + src.item = item + name = item.name + if(isobj(item)) + path = item.type + + if(isturf(item)) + RegisterSignal(item, COMSIG_TURF_CHANGE, PROC_REF(on_turf_change)) + else + // Lest we find ourselves here again, this is intentionally stupid. + // It tracks items going out and user actions, otherwise they can refresh the lootpanel. + // If this is to be made to track everything, we'll need to make a new signal to specifically create/delete a search object + RegisterSignals(item, list( + COMSIG_ITEM_PICKUP, + COMSIG_MOVABLE_MOVED, + COMSIG_PARENT_QDELETING, + ), PROC_REF(on_item_moved)) + + // Icon generation conditions ////////////// + // Condition 1: Icon is complex + if(ismob(item) || length(item.overlays) > 0) + return + + // Condition 2: Can't get icon path + if(!isfile(item.icon) || !length("[item.icon]")) + return + + // Condition 3: Using opendream +#if defined(OPENDREAM) || defined(UNIT_TESTS) + return +#endif + + // Condition 4: Using older byond version + var/build = owner.byond_build + var/version = owner.byond_version + if(build < 515 || (build == 515 && version < 1635)) + icon = "n/a" + return + + icon = "[item.icon]" + icon_state = item.icon_state + + +/datum/search_object/Destroy(force) + item = null + icon = null + + return ..() + + +/// Generates the icon for the search object. This is the expensive part. +/datum/search_object/proc/generate_icon(client/owner) + icon = costly_icon2html(item, owner, sourceonly = TRUE) + + +/// Parent item has been altered, search object no longer valid +/datum/search_object/proc/on_item_moved(atom/source) + SIGNAL_HANDLER + + if(QDELETED(src)) + return + + qdel(src) + + +/// Parent tile has been altered, entire search needs reset +/datum/search_object/proc/on_turf_change(turf/source, path, list/new_baseturfs, flags, list/post_change_callbacks) + SIGNAL_HANDLER + + post_change_callbacks += CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(qdel), src) diff --git a/code/modules/lootpanel/ss_looting.dm b/code/modules/lootpanel/ss_looting.dm new file mode 100644 index 0000000000..94e12f88f9 --- /dev/null +++ b/code/modules/lootpanel/ss_looting.dm @@ -0,0 +1,40 @@ + +/// Queues image generation for search objects without icons +SUBSYSTEM_DEF(looting) + name = "Loot Icon Generation" + flags = SS_NO_INIT + priority = FIRE_PRIORITY_PROCESS + runlevels = RUNLEVEL_LOBBY|RUNLEVELS_DEFAULT + wait = 0.5 SECONDS + /// Backlog of items. Gets put into processing + var/list/datum/lootpanel/backlog = list() + /// Actively processing items + var/list/datum/lootpanel/processing = list() + + +/datum/controller/subsystem/looting/stat_entry(msg) + msg = "P:[length(backlog)]" + return ..() + + +/datum/controller/subsystem/looting/fire(resumed) + if(!length(backlog)) + return + + if(!resumed) + processing = backlog + backlog = list() + + while(length(processing)) + var/datum/lootpanel/panel = processing[length(processing)] + if(QDELETED(panel) || !length(panel.to_image)) + processing.len-- + continue + + if(!panel.process_images()) + backlog += panel + + if(MC_TICK_CHECK) + return + + processing.len-- diff --git a/code/modules/lootpanel/ui.dm b/code/modules/lootpanel/ui.dm new file mode 100644 index 0000000000..14d41b02b7 --- /dev/null +++ b/code/modules/lootpanel/ui.dm @@ -0,0 +1,47 @@ +/// UI helper for converting the associative list to a list of lists +/datum/lootpanel/proc/get_contents() + var/list/items = list() + + for(var/datum/search_object/index as anything in contents) + UNTYPED_LIST_ADD(items, list( + "icon_state" = index.icon_state, + "icon" = index.icon, + "name" = index.name, + "path" = index.path, + "ref" = REF(index), + )) + + return items + + +/// Clicks an object from the contents. Validates the object and the user +/datum/lootpanel/proc/grab(mob/user, list/params) + var/ref = params["ref"] + if(isnull(ref)) + return FALSE + + var/datum/search_object/index = locate(ref) in contents + var/atom/thing = index?.item + if(QDELETED(index) || QDELETED(thing)) // Obj is gone + return FALSE + + if(thing != source_turf && !(locate(thing) in source_turf.contents)) + qdel(index) // Item has moved + return TRUE + + var/modifiers = "" + if(params["ctrl"]) + modifiers += "ctrl=1;" + if(params["middle"]) + modifiers += "middle=1;" + if(params["shift"]) + modifiers += "shift=1;" + if(params["alt"]) + modifiers += "alt=1;" + if(params["right"]) + modifiers += "right=1;" + + + user.ClickOn(thing, modifiers) + + return TRUE diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index 47efb73133..5892eeb3e8 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -744,11 +744,6 @@ var/mob/M = locate(href_list["lookmob"]) src.examinate(M) - if (href_list["clickitem"]) - var/obj/item/I = locate(href_list["clickitem"]) - if(src.client) - src.ClickOn(I) - if (href_list["flavor_change"]) switch(href_list["flavor_change"]) if("done") diff --git a/interface/skin.dmf b/interface/skin.dmf index d45872f4d8..810ee79ebe 100644 --- a/interface/skin.dmf +++ b/interface/skin.dmf @@ -271,754 +271,764 @@ macro "borghotkeymode" command = "planedown" macro "macro" - elem + elem "tab" name = "TAB" command = ".winset \"mainwindow.macro=hotkeymode hotkey_toggle.is-checked=true mapwindow.map.focus=true\"" - elem + elem "shift" name = "Shift" command = "KeyDown Shift" - elem + elem "shiftup" name = "Shift+UP" command = "KeyUp Shift" - elem + elem "ctrl" name = "Ctrl" command = "KeyDown Ctrl" - elem + elem "ctrlup" name = "Ctrl+UP" command = "KeyUp Ctrl" - elem + elem "alt" name = "Alt" command = "KeyDown Alt" - elem + elem "altup" name = "Alt+UP" command = "KeyUp Alt" - elem + elem "northeast" name = "NORTHEAST" command = ".northeast" - elem + elem "southeast" name = "SOUTHEAST" command = ".southeast" - elem + elem "southwest" name = "SOUTHWEST" command = ".southwest" - elem + elem "northwest" name = "NORTHWEST" command = ".northwest" - elem + elem "altwest" name = "ALT+WEST" command = "westfaceperm" - elem + elem "ctrlwest" name = "CTRL+WEST" command = "westface" - elem + elem "west" name = "West" command = "KeyDown West" - elem + elem "westup" name = "West+UP" command = "KeyUp West" - elem + elem "altnorth" name = "ALT+NORTH" command = "northfaceperm" - elem + elem "ctrlnorth" name = "CTRL+NORTH" command = "northface" - elem + elem "north" name = "North" command = "KeyDown North" - elem + elem "northup" name = "North+UP" command = "KeyUp North" - elem + elem "alteast" name = "ALT+EAST" command = "eastfaceperm" - elem + elem "ctrleast" name = "CTRL+EAST" command = "eastface" - elem + elem "east" name = "East" command = "KeyDown East" - elem + elem "eastup" name = "East+UP" command = "KeyUp East" - elem + elem "altsouth" name = "ALT+SOUTH" command = "southfaceperm" - elem + elem "ctrlsouth" name = "CTRL+SOUTH" command = "southface" - elem + elem "south" name = "South" command = "KeyDown South" - elem + elem "southup" name = "South+UP" command = "KeyUp South" - elem + elem "ctrlshiftnorth" name = "CTRL+SHIFT+NORTH" command = "shiftnorth" - elem + elem "ctrlshiftsouth" name = "CTRL+SHIFT+SOUTH" command = "shiftsouth" - elem + elem "ctrlshiftwest" name = "CTRL+SHIFT+WEST" command = "shiftwest" - elem + elem "ctrlshifteast" name = "CTRL+SHIFT+EAST" command = "shifteast" - elem + elem "insert" name = "INSERT" command = "a-intent right" - elem + elem "delete" name = "DELETE" command = "delete-key-pressed" - elem + elem "ctrl1" name = "CTRL+1" command = "a-intent help" - elem + elem "ctrl2" name = "CTRL+2" command = "a-intent disarm" - elem + elem "ctrl3" name = "CTRL+3" command = "a-intent grab" - elem + elem "ctrl4" name = "CTRL+4" command = "a-intent harm" - elem + elem "ctrla" name = "CTRL+A" command = "KeyDown A" - elem + elem "ctrlaup" name = "CTRL+A+UP" command = "KeyUp A" - elem + elem "ctrld" name = "CTRL+D" command = "KeyDown D" - elem + elem "ctrldup" name = "CTRL+D+UP" command = "KeyUp D" - elem + elem "ctrle" name = "CTRL+E" command = "quick-equip" - elem + elem "ctrlf" name = "CTRL+F" command = "a-intent left" - elem + elem "ctrlg" name = "CTRL+G" command = "a-intent right" - elem + elem "ctrlq" name = "CTRL+Q" command = ".northwest" - elem + elem "ctrlr" name = "CTRL+R" command = ".southwest" - elem + elem "ctrls" name = "CTRL+S" command = "KeyDown S" - elem + elem "ctrlsup" name = "CTRL+S+UP" command = "KeyUp S" - elem + elem "ctrlw" name = "CTRL+W" command = "KeyDown W" - elem + elem "ctrlwup" name = "CTRL+W+UP" command = "KeyUp W" - elem + elem "ctrlx" name = "CTRL+X" command = ".northeast" - elem + elem "ctrly" name = "CTRL+Y" command = "Activate-Held-Object" - elem + elem "ctrlz" name = "CTRL+Z" command = "Activate-Held-Object" - elem + elem "ctrlu" name = "CTRL+U" command = "Rest" - elem + elem "ctrlb" name = "CTRL+B" command = "Resist" - elem + elem "ctrlnumpad1" name = "CTRL+NUMPAD1" command = "body-r-leg" - elem + elem "ctrlnumpad2" name = "CTRL+NUMPAD2" command = "body-groin" - elem + elem "ctrlnumpad3" name = "CTRL+NUMPAD3" command = "body-l-leg" - elem + elem "ctrlnumpad4" name = "CTRL+NUMPAD4" command = "body-r-arm" - elem + elem "ctrlnumpad5" name = "CTRL+NUMPAD5" command = "body-chest" - elem + elem "ctrlnumpad6" name = "CTRL+NUMPAD6" command = "body-l-arm" - elem + elem "ctrlnumpad8" name = "CTRL+NUMPAD8" command = "body-toggle-head" - elem + elem "f1" name = "F1" command = "request-help" - elem + elem "ctrlshiftf1rep" name = "CTRL+SHIFT+F1+REP" command = ".options" - elem + elem "f2" name = "F2" command = "ooc" - elem + elem "f2rep" name = "F2+REP" command = ".screenshot auto" - elem + elem "shiftf2rep" name = "SHIFT+F2+REP" command = ".screenshot" - elem + elem "f3" name = "F3" command = "Say-verb" - elem + elem "f4" name = "F4" command = "Me-verb" - elem + elem "f5" name = "F5" command = "asay" - elem + elem "f6" name = "F6" command = "Player-Panel-New" - elem + elem "f7" name = "F7" command = "Admin-PM" - elem + elem "f8" name = "F8" command = "Invisimin" - elem + elem "f12" name = "F12" command = "F12" - elem + elem "ctrlshiftadd" name = "CTRL+SHIFT+ADD" command = "planeup" - elem + elem "ctrlshiftsubtract" name = "CTRL+SHIFT+SUBTRACT" command = "planedown" macro "hotkeymode" - elem + elem "tab" name = "TAB" command = ".winset \"mainwindow.macro=macro hotkey_toggle.is-checked=false input.focus=true\"" - elem + elem "shift" name = "Shift" command = "KeyDown Shift" - elem + elem "shiftup" name = "Shift+UP" command = "KeyUp Shift" - elem + elem "ctrl" name = "Ctrl" command = "KeyDown Ctrl" - elem + elem "ctrlup" name = "Ctrl+UP" command = "KeyUp Ctrl" - elem + elem "alt" name = "Alt" command = "KeyDown Alt" - elem + elem "altup" name = "Alt+UP" command = "KeyUp Alt" - elem + elem "northeast" name = "NORTHEAST" command = ".northeast" - elem + elem "southeast" name = "SOUTHEAST" command = ".southeast" - elem + elem "southwest" name = "SOUTHWEST" command = ".southwest" - elem + elem "northwest" name = "NORTHWEST" command = ".northwest" - elem + elem "altwest" name = "ALT+WEST" command = "westfaceperm" - elem + elem "ctrlwest" name = "CTRL+WEST" command = "westface" - elem + elem "west" name = "West" command = "KeyDown West" - elem + elem "westup" name = "West+UP" command = "KeyUp West" - elem + elem "altnorth" name = "ALT+NORTH" command = "northfaceperm" - elem + elem "ctrlnorth" name = "CTRL+NORTH" command = "northface" - elem + elem "north" name = "North" command = "KeyDown North" - elem + elem "northup" name = "North+UP" command = "KeyUp North" - elem + elem "alteast" name = "ALT+EAST" command = "eastfaceperm" - elem + elem "ctrleast" name = "CTRL+EAST" command = "eastface" - elem + elem "east" name = "East" command = "KeyDown East" - elem + elem "eastup" name = "East+UP" command = "KeyUp East" - elem + elem "altsouth" name = "ALT+SOUTH" command = "southfaceperm" - elem + elem "ctrlsouth" name = "CTRL+SOUTH" command = "southface" - elem + elem "south" name = "South" command = "KeyDown South" - elem + elem "southup" name = "South+UP" command = "KeyUp South" - elem + elem "ctrlshiftnorth" name = "CTRL+SHIFT+NORTH" command = "shiftnorth" - elem + elem "ctrlshiftsouth" name = "CTRL+SHIFT+SOUTH" command = "shiftsouth" - elem + elem "ctrlshiftwest" name = "CTRL+SHIFT+WEST" command = "shiftwest" - elem + elem "ctrlshifteast" name = "CTRL+SHIFT+EAST" command = "shifteast" - elem + elem "insert" name = "INSERT" command = "a-intent right" - elem + elem "delete" name = "DELETE" command = "delete-key-pressed" - elem + elem "one" name = "1" command = "a-intent help" - elem + elem "ctrl1" name = "CTRL+1" command = "a-intent help" - elem + elem "two" name = "2" command = "a-intent disarm" - elem + elem "ctrl2" name = "CTRL+2" command = "a-intent disarm" - elem + elem "three" name = "3" command = "a-intent grab" - elem + elem "ctrl3" name = "CTRL+3" command = "a-intent grab" - elem + elem "four" name = "4" command = "a-intent harm" - elem + elem "ctrl4" name = "CTRL+4" command = "a-intent harm" - elem + elem "five" name = "5" command = "Me-verb" - elem + elem "six" name = "6" command = "Subtle-verb" - elem + elem "a" name = "A" command = "KeyDown A" - elem + elem "aup" name = "A+UP" command = "KeyUp A" - elem + elem "d" name = "D" command = "KeyDown D" - elem + elem "dup" name = "D+UP" command = "KeyUp D" - elem + elem "e" name = "E" command = "quick-equip" - elem + elem "ctrle" name = "CTRL+E" command = "quick-equip" - elem + elem "f" name = "F" command = "a-intent left" - elem + elem "ctrlf" name = "CTRL+F" command = "a-intent left" - elem + elem "g" name = "G" command = "a-intent right" - elem + elem "ctrlg" name = "CTRL+G" command = "a-intent right" - elem + elem "h" name = "H" command = "holster" - elem + elem "ctrlh" name = "CTRL+H" command = "holster" - elem + elem "j" name = "J" command = "toggle-gun-mode" - elem + elem "ctrlj" name = "CTRL+J" command = "toggle-gun-mode" - elem + elem "q" name = "Q" command = ".northwest" - elem + elem "ctrlq" name = "CTRL+Q" command = ".northwest" - elem + elem "r" name = "R" command = ".southwest" - elem + elem "ctrlr" name = "CTRL+R" command = ".southwest" elem "s_key" name = "S" command = "KeyDown S" - elem + elem "sup" name = "S+UP" command = "KeyUp S" - elem + elem "t" name = "T" command = "Say-verb" elem "w_key" name = "W" command = "KeyDown W" - elem + elem "wup" name = "W+UP" command = "KeyUp W" - elem + elem "x" name = "X" command = ".northeast" - elem + elem "ctrlx" name = "CTRL+X" command = ".northeast" - elem + elem "y" name = "Y" command = "Whisper-verb" - elem + elem "ctrly" name = "CTRL+Y" command = "Whisper-verb" - elem + elem "z" name = "Z" command = "Activate-Held-Object" - elem + elem "ctrlz" name = "CTRL+Z" command = "Activate-Held-Object" - elem + elem "u" name = "U" command = "Rest" - elem + elem "shiftu" name = "SHIFT+U" command = "Rest-Left" - elem + elem "ctrlu" name = "CTRL+U" command = "Rest-Right" - elem + elem "b" name = "B" command = "Resist" - elem + elem "numpad1" name = "NUMPAD1" command = "body-r-leg" - elem + elem "numpad2" name = "NUMPAD2" command = "body-groin" - elem + elem "numpad3" name = "NUMPAD3" command = "body-l-leg" - elem + elem "numpad4" name = "NUMPAD4" command = "body-r-arm" - elem + elem "numpad5" name = "NUMPAD5" command = "body-chest" - elem + elem "numpad6" name = "NUMPAD6" command = "body-l-arm" - elem + elem "numpad8" name = "NUMPAD8" command = "body-toggle-head" - elem + elem "f1" name = "F1" command = "request-help" - elem + elem "ctrlshiftf1rep" name = "CTRL+SHIFT+F1+REP" command = ".options" - elem + elem "f2" name = "F2" command = "ooc" - elem + elem "f2rep" name = "F2+REP" command = ".screenshot auto" - elem + elem "shiftf2rep" name = "SHIFT+F2+REP" command = ".screenshot" - elem + elem "f3" name = "F3" command = "Say-verb" - elem + elem "f4" name = "F4" command = "Me-verb" - elem + elem "f5" name = "F5" command = "asay" - elem + elem "f6" name = "F6" command = "Player-Panel-New" - elem + elem "f7" name = "F7" command = "Admin-PM" - elem + elem "f8" name = "F8" command = "Invisimin" - elem + elem "f12" name = "F12" command = "F12" - elem + elem "ctrlshiftadd" name = "CTRL+SHIFT+ADD" command = "planeup" - elem + elem "ctrlshiftsubtract" name = "CTRL+SHIFT+SUBTRACT" command = "planedown" +<<<<<<< HEAD // elem // CHOMPREMOVE Start // name = "Space" // command = ".throwon" // elem // name = "Space+UP" // command = ".throwoff" // CHOMPREMOVE End +======= + elem "space" + name = "Space" + command = ".throwon" + elem "spaceup" + name = "Space+UP" + command = ".throwoff" + +>>>>>>> b7969a971d (Replace the alt click menu with the RPG Lootpanel (#17938)) macro "borgmacro" - elem + elem "tab" name = "TAB" command = ".winset \"mainwindow.macro=borghotkeymode hotkey_toggle.is-checked=true mapwindow.map.focus=true\"" - elem + elem "shift" name = "Shift" command = "KeyDown Shift" - elem + elem "shiftup" name = "Shift+UP" command = "KeyUp Shift" - elem + elem "ctrl" name = "Ctrl" command = "KeyDown Ctrl" - elem + elem "ctrlup" name = "Ctrl+UP" command = "KeyUp Ctrl" - elem + elem "alt" name = "Alt" command = "KeyDown Alt" - elem + elem "altup" name = "Alt+UP" command = "KeyUp Alt" - elem + elem "northeast" name = "NORTHEAST" command = ".northeast" - elem + elem "southeast" name = "SOUTHEAST" command = ".southeast" - elem + elem "southwest" name = "SOUTHWEST" command = ".southwest" - elem + elem "northwest" name = "NORTHWEST" command = ".northwest" - elem + elem "altwest" name = "ALT+WEST" command = "westfaceperm" - elem + elem "ctrlwest" name = "CTRL+WEST" command = "westface" - elem + elem "west" name = "West" command = "KeyDown West" - elem + elem "westup" name = "West+UP" command = "KeyUp West" - elem + elem "altnorth" name = "ALT+NORTH" command = "northfaceperm" - elem + elem "ctrlnorth" name = "CTRL+NORTH" command = "northface" - elem + elem "north" name = "North" command = "KeyDown North" - elem + elem "northup" name = "North+UP" command = "KeyUp North" - elem + elem "alteast" name = "ALT+EAST" command = "eastfaceperm" - elem + elem "ctrleast" name = "CTRL+EAST" command = "eastface" - elem + elem "east" name = "East" command = "KeyDown East" - elem + elem "eastup" name = "East+UP" command = "KeyUp East" - elem + elem "altsouth" name = "ALT+SOUTH" command = "southfaceperm" - elem + elem "ctrlsouth" name = "CTRL+SOUTH" command = "southface" - elem + elem "south" name = "South" command = "KeyDown South" - elem + elem "southup" name = "South+UP" command = "KeyUp South" - elem + elem "ctrlshiftnorth" name = "CTRL+SHIFT+NORTH" command = "shiftnorth" - elem + elem "ctrlshiftsouth" name = "CTRL+SHIFT+SOUTH" command = "shiftsouth" - elem + elem "ctrlshiftwest" name = "CTRL+SHIFT+WEST" command = "shiftwest" - elem + elem "ctrlshifteast" name = "CTRL+SHIFT+EAST" command = "shifteast" - elem + elem "insert" name = "INSERT" command = "a-intent right" - elem + elem "delete" name = "DELETE" command = "delete-key-pressed" - elem + elem "ctrl1" name = "CTRL+1" command = "toggle-module 1" - elem + elem "ctrl2" name = "CTRL+2" command = "toggle-module 2" - elem + elem "ctrl3" name = "CTRL+3" command = "toggle-module 3" - elem + elem "ctrl4" name = "CTRL+4" command = "a-intent left" - elem + elem "ctrla" name = "CTRL+A" command = "KeyDown A" - elem + elem "ctrlaup" name = "CTRL+A+UP" command = "KeyUp A" - elem + elem "ctrld" name = "CTRL+D" command = "KeyDown D" - elem + elem "ctrldup" name = "CTRL+D+UP" command = "KeyUp D" - elem + elem "ctrlf" name = "CTRL+F" command = "a-intent left" - elem + elem "ctrlg" name = "CTRL+G" command = "a-intent right" - elem + elem "ctrlq" name = "CTRL+Q" command = ".northwest" - elem + elem "ctrlr" name = "CTRL+R" command = ".southwest" - elem + elem "ctrls" name = "CTRL+S" command = "KeyDown S" - elem + elem "ctrlsup" name = "CTRL+S+UP" command = "KeyUp S" - elem + elem "ctrlw" name = "CTRL+W" command = "KeyDown W" - elem + elem "ctrlwup" name = "CTRL+W+UP" command = "KeyUp W" - elem + elem "ctrlx" name = "CTRL+X" command = ".northeast" - elem + elem "ctrly" name = "CTRL+Y" command = "Activate-Held-Object" - elem + elem "ctrlz" name = "CTRL+Z" command = "Robot-Activate-Held-Object" - elem + elem "ctrlu" name = "CTRL+U" command = "Rest" - elem + elem "ctrlnumpad1" name = "CTRL+NUMPAD1" command = "body-r-leg" - elem + elem "ctrlnumpad2" name = "CTRL+NUMPAD2" command = "body-groin" - elem + elem "ctrlnumpad3" name = "CTRL+NUMPAD3" command = "body-l-leg" - elem + elem "ctrlnumpad4" name = "CTRL+NUMPAD4" command = "body-r-arm" - elem + elem "ctrlnumpad5" name = "CTRL+NUMPAD5" command = "body-chest" - elem + elem "ctrlnumpad6" name = "CTRL+NUMPAD6" command = "body-l-arm" - elem + elem "ctrlnumpad8" name = "CTRL+NUMPAD8" command = "body-toggle-head" - elem + elem "f1" name = "F1" command = "request-help" - elem + elem "ctrlshiftf1rep" name = "CTRL+SHIFT+F1+REP" command = ".options" - elem + elem "f2" name = "F2" command = "ooc" - elem + elem "f2rep" name = "F2+REP" command = ".screenshot auto" - elem + elem "shiftf2rep" name = "SHIFT+F2+REP" command = ".screenshot" - elem + elem "f3" name = "F3" command = "Say-verb" - elem + elem "f4" name = "F4" command = "Me-verb" - elem + elem "f5" name = "F5" command = "asay" - elem + elem "f6" name = "F6" command = "Player-Panel-New" - elem + elem "f7" name = "F7" command = "Admin-PM" - elem + elem "f8" name = "F8" command = "Invisimin" - elem + elem "f12" name = "F12" command = "F12" - elem + elem "ctrlshiftadd" name = "CTRL+SHIFT+ADD" command = "planeup" - elem + elem "ctrlshiftsubtract" name = "CTRL+SHIFT+SUBTRACT" command = "planedown" diff --git a/tgui/packages/tgui/hotkeys.ts b/tgui/packages/tgui/hotkeys.ts new file mode 100644 index 0000000000..6a64ac4daf --- /dev/null +++ b/tgui/packages/tgui/hotkeys.ts @@ -0,0 +1,269 @@ +import { globalEvents, type KeyEvent } from 'tgui-core/events'; +import * as keycodes from 'tgui-core/keycodes'; + +import { logger } from './logging'; + +// BYOND macros, in `key: command` format. +const byondMacros: Record = {}; + +// Default set of acquired keys, which will not be sent to BYOND. +const hotKeysAcquired = [ + keycodes.KEY_ESCAPE, + keycodes.KEY_ENTER, + keycodes.KEY_SPACE, + keycodes.KEY_TAB, + keycodes.KEY_CTRL, + keycodes.KEY_SHIFT, + keycodes.KEY_UP, + keycodes.KEY_DOWN, + keycodes.KEY_LEFT, + keycodes.KEY_RIGHT, + keycodes.KEY_F5, +]; + +// State of passed-through keys. +const keyState: Record = {}; + +// Custom listeners for key events +const keyListeners: ((key: KeyEvent) => void)[] = []; + +/** + * Converts a browser keycode to BYOND keycode. + */ +function keyCodeToByond(keyCode: number) { + if (keyCode === 16) return 'Shift'; + if (keyCode === 17) return 'Ctrl'; + if (keyCode === 18) return 'Alt'; + if (keyCode === 33) return 'Northeast'; + if (keyCode === 34) return 'Southeast'; + if (keyCode === 35) return 'Southwest'; + if (keyCode === 36) return 'Northwest'; + if (keyCode === 37) return 'West'; + if (keyCode === 38) return 'North'; + if (keyCode === 39) return 'East'; + if (keyCode === 40) return 'South'; + if (keyCode === 45) return 'Insert'; + if (keyCode === 46) return 'Delete'; + + if ((keyCode >= 48 && keyCode <= 57) || (keyCode >= 65 && keyCode <= 90)) { + return String.fromCharCode(keyCode); + } + if (keyCode >= 96 && keyCode <= 105) { + return `Numpad${keyCode - 96}`; + } + if (keyCode >= 112 && keyCode <= 123) { + return `F${keyCode - 111}`; + } + if (keyCode === 188) return ','; + if (keyCode === 189) return '-'; + if (keyCode === 190) return '.'; +} + +/** + * Keyboard passthrough logic. This allows you to keep doing things + * in game while the browser window is focused. + */ +function handlePassthrough(key: KeyEvent) { + const keyString = String(key); + // In addition to F5, support reloading with Ctrl+R and Ctrl+F5 + if (keyString === 'Ctrl+F5' || keyString === 'Ctrl+R') { + location.reload(); + return; + } + // Prevent passthrough on Ctrl+F + if (keyString === 'Ctrl+F') { + return; + } + // NOTE: Alt modifier is pretty bad and sticky in IE11. + + if ( + key.event.defaultPrevented || + key.isModifierKey() || + hotKeysAcquired.includes(key.code) || + key.repeat // no repeating + ) { + return; + } + const byondKeyCode = keyCodeToByond(key.code); + if (!byondKeyCode) { + return; + } + let byondKeyCodeIdent = byondKeyCode; + if (key.isUp()) { + byondKeyCodeIdent += '+UP'; + } + // Macro + const macro = byondMacros[byondKeyCodeIdent]; + if (macro) { + return Byond.command(macro); + } + // KeyDown + if (key.isDown() && !keyState[byondKeyCode]) { + keyState[byondKeyCode] = true; + const command = keyPassthroughConfig.verbParamsFn( + keyPassthroughConfig.keyDownVerb, + byondKeyCode, + ); + return Byond.command(command); + } + // KeyUp + if (key.isUp() && keyState[byondKeyCode]) { + keyState[byondKeyCode] = false; + const command = keyPassthroughConfig.verbParamsFn( + keyPassthroughConfig.keyUpVerb, + byondKeyCode, + ); + return Byond.command(command); + } +} + +/** + * Acquires a lock on the hotkey, which prevents it from being + * passed through to BYOND. + */ +export function acquireHotKey(keyCode: number) { + hotKeysAcquired.push(keyCode); +} + +/** + * Makes the hotkey available to BYOND again. + */ +export function releaseHotKey(keyCode: number) { + const index = hotKeysAcquired.indexOf(keyCode); + if (index >= 0) { + hotKeysAcquired.splice(index, 1); + } +} + +export function releaseHeldKeys() { + for (const byondKeyCode in keyState) { + if (keyState[byondKeyCode]) { + keyState[byondKeyCode] = false; + Byond.command( + keyPassthroughConfig.verbParamsFn( + keyPassthroughConfig.keyUpVerb, + byondKeyCode, + ), + ); + } + } +} + +type ByondSkinMacro = { + command: string; + name: string; +}; + +let keyPassthroughConfig: KeyPassthroughConfig = { + keyDownVerb: 'KeyDown', + keyUpVerb: 'KeyUp', + verbParamsFn: (verb, keyCode) => `${verb} "${keyCode}"`, +}; + +export type KeyPassthroughConfig = { + keyUpVerb: string; + keyDownVerb: string; + verbParamsFn: (verb: string, keyCode: string) => string; +}; + +export function setupHotKeys(config?: KeyPassthroughConfig) { + if (config) { + keyPassthroughConfig = config; + } + // Read macros + Byond.winget(null, 'macros').then((data: string) => { + const separated = data.split(';'); + + const promises: Promise[] = []; + for (const set of separated) { + promises.push(Byond.winget(set + '.*')); + } + + Promise.all(promises).then((sets: Record[]) => { + // Group each macro by ref + const groupedByRef: Record = {}; + for (const set of sets) { + for (const key of Object.keys(set)) { + const keyPath = key.split('.'); + const ref = keyPath[1]; + const prop = keyPath[2]; + + if (ref && prop) { + // This piece of code imperatively adds each property to a + // ByondSkinMacro object in the order we meet it, which is hard + // to express safely in typescript. + if (!groupedByRef[ref]) { + groupedByRef[ref] = {} as any; + } + groupedByRef[ref][prop] = set[key]; + } + } + } + + // Insert macros + const escapedQuotRegex = /\\"/g; + + function unEscape(str: string) { + return str.substring(1, str.length - 1).replace(escapedQuotRegex, '"'); + } + + for (const ref of Object.keys(groupedByRef)) { + const macro = groupedByRef[ref]; + const byondKeyName = unEscape(macro.name); + byondMacros[byondKeyName] = unEscape(macro.command); + } + + logger.log(byondMacros); + }); + }); + + // Setup event handlers + globalEvents.on('window-blur', () => { + releaseHeldKeys(); + }); + globalEvents.on('input-focus', () => { + releaseHeldKeys(); + }); + startKeyPassthrough(); +} + +export function startKeyPassthrough() { + globalEvents.on('key', keyEvent); +} + +export function stopKeyPassthrough() { + globalEvents.off('key', keyEvent); +} + +function keyEvent(key: KeyEvent) { + for (const keyListener of keyListeners) { + keyListener(key); + } + handlePassthrough(key); +} + +/** + * Registers for any key events, such as key down or key up. + * This should be preferred over directly connecting to keydown/keyup + * as it lets tgui prevent the key from reaching BYOND. + * + * If using in a component, prefer KeyListener, which automatically handles + * stopping listening when unmounting. + * + * @param callback The function to call whenever a key event occurs + * @returns A callback to stop listening + */ +export function listenForKeyEvents(callback: (key: KeyEvent) => void) { + keyListeners.push(callback); + + let removed = false; + + return () => { + if (removed) { + return; + } + + removed = true; + keyListeners.splice(keyListeners.indexOf(callback), 1); + }; +} diff --git a/tgui/packages/tgui/index.tsx b/tgui/packages/tgui/index.tsx index 01bc0d5388..918897b63b 100644 --- a/tgui/packages/tgui/index.tsx +++ b/tgui/packages/tgui/index.tsx @@ -33,11 +33,11 @@ import './styles/themes/algae.scss'; import { perf } from 'common/perf'; import { setupGlobalEvents } from 'tgui-core/events'; -import { setupHotKeys } from 'tgui-core/hotkeys'; import { setupHotReloading } from 'tgui-dev-server/link/client'; import { App } from './App'; import { setGlobalStore } from './backend'; +import { setupHotKeys } from './hotkeys'; import { captureExternalLinks } from './links'; import { render } from './renderer'; import { configureStore } from './store'; diff --git a/tgui/packages/tgui/interfaces/LootPanel/GroupedContents.tsx b/tgui/packages/tgui/interfaces/LootPanel/GroupedContents.tsx new file mode 100644 index 0000000000..75c566c710 --- /dev/null +++ b/tgui/packages/tgui/interfaces/LootPanel/GroupedContents.tsx @@ -0,0 +1,26 @@ +import { Box } from 'tgui-core/components'; +import { createSearch } from 'tgui-core/string'; + +import { LootBox } from './LootBox'; +import type { SearchGroup, SearchItem } from './types'; + +type Props = { + contents: Record; + searchText: string; +}; + +export function GroupedContents(props: Props) { + const { contents, searchText } = props; + + const filteredContents: SearchGroup[] = Object.entries(contents) + .filter(createSearch(searchText, ([_, items]) => items[0].name)) + .map(([_, items]) => ({ amount: items.length, item: items[0] })); + + return ( + + {filteredContents.map((group) => ( + + ))} + + ); +} diff --git a/tgui/packages/tgui/interfaces/LootPanel/IconDisplay.tsx b/tgui/packages/tgui/interfaces/LootPanel/IconDisplay.tsx new file mode 100644 index 0000000000..16c2dc21a6 --- /dev/null +++ b/tgui/packages/tgui/interfaces/LootPanel/IconDisplay.tsx @@ -0,0 +1,44 @@ +import { DmIcon, Icon, Image } from 'tgui-core/components'; + +import type { SearchItem } from './types'; + +type Props = { + item: SearchItem; + size: Size; +}; + +type Size = { + height: number; + width: number; +}; + +export function IconDisplay(props: Props) { + const { + item: { icon, icon_state }, + size: { height, width }, + } = props; + + const fallback = ; + + if (!icon) { + return fallback; + } + + if (icon === 'n/a') { + return ; + } + + if (icon_state) { + return ( + + ); + } + + return ; +} diff --git a/tgui/packages/tgui/interfaces/LootPanel/LootBox.tsx b/tgui/packages/tgui/interfaces/LootPanel/LootBox.tsx new file mode 100644 index 0000000000..d064c2529d --- /dev/null +++ b/tgui/packages/tgui/interfaces/LootPanel/LootBox.tsx @@ -0,0 +1,78 @@ +import { useBackend } from 'tgui/backend'; +import { Button, Stack } from 'tgui-core/components'; +import type { BooleanLike } from 'tgui-core/react'; +import { capitalizeFirst } from 'tgui-core/string'; + +import { IconDisplay } from './IconDisplay'; +import type { SearchGroup, SearchItem } from './types'; + +type Data = { + is_blind: BooleanLike; +}; + +type Props = + | { + item: SearchItem; + } + | { + group: SearchGroup; + }; + +export function LootBox(props: Props) { + const { act, data } = useBackend(); + const { is_blind } = data; + + let amount = 0; + let item: SearchItem; + if ('group' in props) { + amount = props.group.amount; + item = props.group.item; + } else { + item = props.item; + } + + const name = !item.name ? '???' : capitalizeFirst(item.name); + + const content = ( + + ); + + if (is_blind) return content; + + return content; +} diff --git a/tgui/packages/tgui/interfaces/LootPanel/RawContents.tsx b/tgui/packages/tgui/interfaces/LootPanel/RawContents.tsx new file mode 100644 index 0000000000..959f90b9df --- /dev/null +++ b/tgui/packages/tgui/interfaces/LootPanel/RawContents.tsx @@ -0,0 +1,26 @@ +import { Box } from 'tgui-core/components'; +import { createSearch } from 'tgui-core/string'; + +import { LootBox } from './LootBox'; +import type { SearchItem } from './types'; + +type Props = { + contents: SearchItem[]; + searchText: string; +}; + +export function RawContents(props: Props) { + const { contents, searchText } = props; + + const filteredContents = contents.filter( + createSearch(searchText, (item: SearchItem) => item.name), + ); + + return ( + + {filteredContents.map((item) => ( + + ))} + + ); +} diff --git a/tgui/packages/tgui/interfaces/LootPanel/index.tsx b/tgui/packages/tgui/interfaces/LootPanel/index.tsx new file mode 100644 index 0000000000..d72370fb3b --- /dev/null +++ b/tgui/packages/tgui/interfaces/LootPanel/index.tsx @@ -0,0 +1,108 @@ +import { useState } from 'react'; +import { useMemo } from 'react'; +import { useBackend } from 'tgui/backend'; +import { Window } from 'tgui/layouts'; +import { Button, Input, Section, Stack } from 'tgui-core/components'; +import { isEscape } from 'tgui-core/keys'; +import { clamp } from 'tgui-core/math'; +import type { BooleanLike } from 'tgui-core/react'; + +import { GroupedContents } from './GroupedContents'; +import { RawContents } from './RawContents'; +import type { SearchItem } from './types'; + +type Data = { + contents: SearchItem[]; + searching: BooleanLike; +}; + +export function LootPanel(props) { + const { act, data } = useBackend(); + const { contents = [], searching } = data; + + // limitations: items with different stack counts, charges etc. + const contentsByPathName = useMemo(() => { + const acc: Record = {}; + + for (let i = 0; i < contents.length; i++) { + const item = contents[i]; + if (item.path) { + if (!acc[item.path + item.name]) { + acc[item.path + item.name] = []; + } + acc[item.path + item.name].push(item); + } else { + acc[item.ref] = [item]; + } + } + return acc; + }, [contents]); + + const [grouping, setGrouping] = useState(true); + const [searchText, setSearchText] = useState(''); + + const headerHeight = 38; + const itemHeight = 38; + const minHeight = headerHeight + itemHeight; + const maxHeight = headerHeight + itemHeight * 10; + const height: number = clamp( + headerHeight + + (!grouping ? contents.length : Object.keys(contentsByPathName).length) * + itemHeight, + minHeight, + maxHeight, + ); + + return ( + + +