diff --git a/code/datums/datumvars.dm b/code/datums/datumvars.dm index 01e94bffe15..5b159ce5e2c 100644 --- a/code/datums/datumvars.dm +++ b/code/datums/datumvars.dm @@ -951,6 +951,14 @@ log_admin("[key_name(usr)] has added [amount] units of [chosen_id] to \the [A]") message_admins("[key_name(usr)] has added [amount] units of [chosen_id] to \the [A]") + else if(href_list["editreagents"]) + if(!check_rights(R_DEBUG|R_ADMIN)) + return + + var/atom/A = locateUID(href_list["editreagents"]) + + try_open_reagent_editor(A) + else if(href_list["explode"]) if(!check_rights(R_DEBUG|R_EVENT)) return diff --git a/code/game/atoms.dm b/code/game/atoms.dm index 5a7fd8a98a0..7b85b1d2b30 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -1164,6 +1164,7 @@ GLOBAL_LIST_EMPTY(blood_splatter_icons) if(curturf) .["Jump to turf"] = "?_src_=holder;adminplayerobservecoodjump=1;X=[curturf.x];Y=[curturf.y];Z=[curturf.z]" .["Add reagent"] = "?_src_=vars;addreagent=[UID()]" + .["Edit reagents"] = "?_src_=vars;editreagents=[UID()]" .["Trigger explosion"] = "?_src_=vars;explode=[UID()]" .["Trigger EM pulse"] = "?_src_=vars;emp=[UID()]" diff --git a/code/modules/admin/reagents_editor.dm b/code/modules/admin/reagents_editor.dm new file mode 100644 index 00000000000..058bff14336 --- /dev/null +++ b/code/modules/admin/reagents_editor.dm @@ -0,0 +1,103 @@ +/datum/reagents_editor + var/atom/target + /// Indexed by target.UID + var/static/list/datum/reagents_editor/editors = list() + +/datum/reagents_editor/New(atom/target) + src.target = target + +/datum/reagents_editor/ui_state(mob/user) + return GLOB.admin_state + +/datum/reagents_editor/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "ReagentsEditor") + ui.open() + ui.set_autoupdate(FALSE) + +/datum/reagents_editor/ui_close(mob/user) + var/open_uis = SStgui.open_uis_by_src[src.UID()] + if(isnull(open_uis) || !islist(open_uis) || length(open_uis) <= 1) + // Remove after everyone closes UI to avoid memory leak + editors -= target.UID() + +/datum/reagents_editor/ui_static_data(mob/user) + . = ..() + + var/list/reagents_information = list() + .["reagentsInformation"] = reagents_information + for(var/id in GLOB.chemical_reagents_list) + var/datum/reagent/R = GLOB.chemical_reagents_list[id] + reagents_information[id] = list( + "name" = R.name, + ) + +/datum/reagents_editor/ui_data(mob/user) + . = ..() + + var/list/reagents = list() + .["reagents"] = reagents + for(var/datum/reagent/R in target.reagents.reagent_list) + reagents[R.id] = list( + "volume" = R.volume, + "uid" = R.UID(), + ) + +// This interface intentionally emulates VV. +// It should, therefore, doesn't place restrictions on any actions, which includes but +// is not limited to: adding a reagent which overfills the container and adding blood +// with a non-existent blood type. It may also do things unconventionally, such as +// directly appending reagents to the list rather than using reagents.add_reagent to +// bypass reagent reactions. +/datum/reagents_editor/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + + if(.) + return + + . = TRUE + + switch(action) + if("add_reagent") + var/reagent_id = params["reagentID"] + var/datum/reagent/reagent_prototype = GLOB.chemical_reagents_list[reagent_id] + if(isnull(reagent_prototype)) + return FALSE + var/new_volume = tgui_input_number(ui.user, "How much units of the reagent do you want to add?", "Add Reagent", 0, 1E100, -1E100) + var/datum/reagent/reagent = target.reagents.get_reagent_by_id(reagent_id) + if(isnull(reagent)) + reagent = new reagent_prototype.type() + reagent.holder = target.reagents + reagent.on_new() + if(ishuman(target) && target.reagents.can_metabolize(target, reagent)) + reagent.on_mob_add(target) + target.reagents.reagent_list += reagent + + reagent.volume = new_volume + + if("edit_volume") + var/reagent_uid = params["uid"] + var/datum/reagent/reagent = locateUID(reagent_uid) + if(isnull(reagent)) + return FALSE + var/new_volume = tgui_input_number(ui.user, "How much units of the reagent do you want to be in the container?", "Edit Reagent Volume", 0, 1E100, -1E100) + if(isnull(new_volume)) + return + reagent.volume = new_volume + + if("delete_reagent") + var/reagent_uid = params["uid"] + var/datum/reagent/reagent = locateUID(reagent_uid) + if(isnull(reagent)) + return FALSE + target.reagents.reagent_list -= reagent + + if("update_total") + target.reagents.update_total() + + if("react_reagents") + target.reagents.handle_reactions() + + else + . = FALSE diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index e66ed538b06..28575df9f21 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -1259,6 +1259,17 @@ debug_variables(stat_item) message_admins("Admin [key_name_admin(usr)] is debugging the [stat_item] [class].") +/client/proc/try_open_reagent_editor(atom/target) + var/target_UID = target.UID() + var/datum/reagents_editor/editor + // editors is static, it can be accessed using a null reference + editor = editor.editors[target_UID] + if(!editor) + editor = new /datum/reagents_editor(target) + editor.editors[target_UID] = editor + + editor.ui_interact(mob) + #undef LIMITER_SIZE #undef CURRENT_SECOND #undef SECOND_COUNT diff --git a/code/modules/reagents/chemistry/reagents_holder.dm b/code/modules/reagents/chemistry/reagents_holder.dm index 413b54262be..a1319d355e8 100644 --- a/code/modules/reagents/chemistry/reagents_holder.dm +++ b/code/modules/reagents/chemistry/reagents_holder.dm @@ -810,6 +810,15 @@ /datum/reagents/proc/get_reagent(type) . = locate(type) in reagent_list +/datum/reagents/proc/get_reagent_by_id(id) + var/list/cached_reagents = reagent_list + for(var/A in cached_reagents) + var/datum/reagent/R = A + if(R.id == id) + return R + + return + /datum/reagents/proc/remove_all_type(reagent_type, amount, strict = FALSE, safety = TRUE) // Removes all reagent of X type. @strict set to 1 determines whether the childs of the type are included. if(!isnum(amount)) return TRUE diff --git a/paradise.dme b/paradise.dme index 6eda50e2c5c..b4ef1c70cee 100644 --- a/paradise.dme +++ b/paradise.dme @@ -1366,6 +1366,7 @@ #include "code\modules\admin\mute.dm" #include "code\modules\admin\outfits.dm" #include "code\modules\admin\player_panel.dm" +#include "code\modules\admin\reagents_editor.dm" #include "code\modules\admin\secrets.dm" #include "code\modules\admin\sql_notes.dm" #include "code\modules\admin\topic.dm" diff --git a/tgui/packages/tgui/interfaces/ReagentsEditor.tsx b/tgui/packages/tgui/interfaces/ReagentsEditor.tsx new file mode 100644 index 00000000000..e4fa714f149 --- /dev/null +++ b/tgui/packages/tgui/interfaces/ReagentsEditor.tsx @@ -0,0 +1,205 @@ +import { Component } from 'inferno'; +import { useBackend } from '../backend'; +import { Button, Icon, Input, Section, Stack, Table } from '../components'; +import { Window } from '../layouts'; +import { createSearch } from 'common/string'; + +type StaticReagentInformation = { + name: string; +}; + +type VolatileReagentInformation = { + volume: number | null; + uid: string; +}; + +type FilteredReagentInformation = StaticReagentInformation & { + id: string; +} & Partial; + +type StaticData = { + reagentsInformation: Record; +}; + +type VolatileData = { + reagents: Record; +}; + +type ReagentsEditorData = StaticData & VolatileData; + +type ReagentsEditorState = { + searchText: string; +}; + +// The linter is raising false-positives for unused state +/* eslint-disable react/no-unused-state */ +export class ReagentsEditor extends Component<{}, ReagentsEditorState> { + constructor(props: {}) { + super(props); + this.state = { + searchText: '', + }; + } + + handleSearchChange = (e: Event) => { + const target = e.target as HTMLInputElement; + this.setState({ searchText: target.value }); + }; + + override render(props: {}, state: ReagentsEditorState, context: unknown) { + const { + act, + data: { reagentsInformation, reagents: reagentsInContainer }, + } = useBackend(this.context); + + let reagents = Object.entries(reagentsInContainer) + .map( + ([reagentID, reagent]): FilteredReagentInformation => ({ + ...reagent, + ...reagentsInformation[reagentID], + id: reagentID, + }) + ) + .sort((a, b) => a.name.localeCompare(b.name)); + if (state.searchText !== '') { + reagents = reagents.concat( + Object.entries(reagentsInformation) + .filter( + ([reagentID, _]) => reagentsInContainer[reagentID] === undefined + ) + .map( + ([reagentID, reagent]): FilteredReagentInformation => ({ + ...reagent, + id: reagentID, + }) + ) + .sort((a, b) => a.name.localeCompare(b.name)) + ); + } + + const reagentsRows = reagents + .filter(({ id: reagentID, name }) => + createSearch(state.searchText, () => `${reagentID}|${name}`)({}) + ) + .map((reagent) => { + const { volume, uid } = reagent; + return volume === undefined ? ( + + ) : ( + + ); + }); + + return ( + + +
+ + + + + + + +
+
+
+ ); + } +} + +// Row for a reagent with non-zero volume +const PresentReagentRow = ( + { + reagent: { id: reagentID, name, uid, volume }, + }: { reagent: FilteredReagentInformation }, + context: unknown +) => { + const { act } = useBackend(context); + return ( + + +
+ + act('delete_reagent', { + uid, + }) + } + mr="0.5em" + /> +
+ + {volume === null ? `NULL` : `${volume}u`} + +
+ + {reagentID} ({name}) + +
+ ); +}; + +// Row for a reagent with zero volume +const AbsentReagentRow = ( + { reagent: { id: reagentID, name } }: { reagent: FilteredReagentInformation }, + context: unknown +) => { + const { act } = useBackend(context); + return ( + + +