Replace the alt click menu with the RPG Lootpanel (#17938)

* Port lootpanel without removing old obj panel

* Rip out the loot panel leaving examine tab intact

* some fixes

* we want nice icons

* that

* Switch to more robust hotkey detection

* Add a reminder to ctrl-r

---------

Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com>
This commit is contained in:
ShadowLarkens
2025-07-10 15:49:37 -07:00
committed by GitHub
parent 91950a8d99
commit b7969a971d
31 changed files with 1261 additions and 492 deletions
+80
View File
@@ -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
+54
View File
@@ -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)
+19
View File
@@ -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()
+48
View File
@@ -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("\
<span class='bolddanger'>Your version of Byond doesn't support fast image loading.</span>\n\
Detected: [version].[build]\n\
Required version for this feature: <b>515.1635</b> or later.\n\
Visit <a href=\"https://secure.byond.com/download\">BYOND's website</a> 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)
+89
View File
@@ -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)
+40
View File
@@ -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--
+47
View File
@@ -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