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 (
+
+
+
+ );
+}
diff --git a/tgui/packages/tgui/interfaces/LootPanel/types.ts b/tgui/packages/tgui/interfaces/LootPanel/types.ts
new file mode 100644
index 0000000000..f17b02b0c1
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/LootPanel/types.ts
@@ -0,0 +1,13 @@
+export type SearchItem = {
+ name: string;
+ path: string;
+ ref: string;
+} & Partial<{
+ icon: string;
+ icon_state: string;
+}>;
+
+export type SearchGroup = {
+ amount: number;
+ item: SearchItem;
+};
diff --git a/vorestation.dme b/vorestation.dme
index c609906e0d..fef5d80c1e 100644
--- a/vorestation.dme
+++ b/vorestation.dme
@@ -3032,6 +3032,13 @@
#include "code\modules\looking_glass\lg_console.dm"
#include "code\modules\looking_glass\lg_imageholder.dm"
#include "code\modules\looking_glass\lg_turfs.dm"
+#include "code\modules\lootpanel\_lootpanel.dm"
+#include "code\modules\lootpanel\contents.dm"
+#include "code\modules\lootpanel\handlers.dm"
+#include "code\modules\lootpanel\misc.dm"
+#include "code\modules\lootpanel\search_object.dm"
+#include "code\modules\lootpanel\ss_looting.dm"
+#include "code\modules\lootpanel\ui.dm"
#include "code\modules\lore_codex\codex.dm"
#include "code\modules\lore_codex\pages.dm"
#include "code\modules\lore_codex\lore_data_yw\important_locations.dm"