diff --git a/citadel.dme b/citadel.dme index f24b6d45ee8..3f57a16bbe4 100644 --- a/citadel.dme +++ b/citadel.dme @@ -145,6 +145,7 @@ #include "code\__DEFINES\dcs\signals\signals_atom\signals_atom_x_act.dm" #include "code\__DEFINES\dcs\signals\signals_item\signals_item_economy.dm" #include "code\__DEFINES\dcs\signals\signals_mob\signals_mob_main.dm" +#include "code\__DEFINES\dcs\signals\signals_mob\signals_mob_organs.dm" #include "code\__DEFINES\economy\economy.dm" #include "code\__DEFINES\economy\payment.dm" #include "code\__DEFINES\economy\worth.dm" @@ -464,6 +465,7 @@ #include "code\datums\components\_component.dm" #include "code\datums\components\anti_magic.dm" #include "code\datums\components\orbiter.dm" +#include "code\datums\components\simple_access.dm" #include "code\datums\components\slaved_atom_to_loc.dm" #include "code\datums\components\crafting\crafting.dm" #include "code\datums\components\crafting\guncrafting.dm" @@ -498,6 +500,7 @@ #include "code\datums\looping_sounds\machinery_sounds.dm" #include "code\datums\looping_sounds\sequence.dm" #include "code\datums\looping_sounds\weather_sounds.dm" +#include "code\datums\mocking\client.dm" #include "code\datums\observation\_debug.dm" #include "code\datums\observation\_defines.dm" #include "code\datums\observation\observation.dm" @@ -3734,9 +3737,12 @@ #include "code\modules\tgui\external.dm" #include "code\modules\tgui\modal.dm" #include "code\modules\tgui\states.dm" +#include "code\modules\tgui\status_composers.dm" #include "code\modules\tgui\tgui.dm" #include "code\modules\tgui\tgui_alert.dm" #include "code\modules\tgui\tgui_input_list.dm" +#include "code\modules\tgui\tgui_input_number.dm" +#include "code\modules\tgui\tgui_input_text.dm" #include "code\modules\tgui\tgui_window.dm" #include "code\modules\tgui\modules\_base.dm" #include "code\modules\tgui\modules\alarm.dm" @@ -3756,11 +3762,14 @@ #include "code\modules\tgui\states\hands.dm" #include "code\modules\tgui\states\human_adjacent.dm" #include "code\modules\tgui\states\inventory.dm" +#include "code\modules\tgui\states\never.dm" +#include "code\modules\tgui\states\new_player.dm" #include "code\modules\tgui\states\not_incapacitated.dm" #include "code\modules\tgui\states\notcontained.dm" #include "code\modules\tgui\states\ntos.dm" #include "code\modules\tgui\states\observer.dm" #include "code\modules\tgui\states\physical.dm" +#include "code\modules\tgui\states\reverse_contained.dm" #include "code\modules\tgui\states\self.dm" #include "code\modules\tgui\states\zlevel.dm" #include "code\modules\tgui_panel\audio.dm" diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm index 17f9c2d5f07..b9a935fd6af 100644 --- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm +++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm @@ -50,10 +50,10 @@ ////#define COMSIG_MOB_MIND_TRANSFERRED_INTO "mob_mind_transferred_into" /// From base of obj/allowed(mob/M): (/obj) returns ACCESS_ALLOWED if mob has id access to the obj -////#define COMSIG_MOB_TRIED_ACCESS "tried_access" - ////#define ACCESS_ALLOWED (1<<0) - ////#define ACCESS_DISALLOWED (1<<1) - ////#define LOCKED_ATOM_INCOMPATIBLE (1<<2) +#define COMSIG_MOB_TRIED_ACCESS "tried_access" + #define ACCESS_ALLOWED (1<<0) + #define ACCESS_DISALLOWED (1<<1) + #define LOCKED_ATOM_INCOMPATIBLE (1<<2) /// From base of mob/can_cast_magic(): (mob/user, magic_flags, charge_cost) ////#define COMSIG_MOB_RESTRICT_MAGIC "mob_cast_magic" diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_organs.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_organs.dm index 20e288fcded..169a53fb45a 100644 --- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_organs.dm +++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_organs.dm @@ -7,7 +7,7 @@ /// Called on the organ when it is implanted into someone (mob/living/carbon/receiver) ////#define COMSIG_ORGAN_IMPLANTED "comsig_organ_implanted" /// Called on the organ when it is removed from someone (mob/living/carbon/old_owner) -////#define COMSIG_ORGAN_REMOVED "comsig_organ_remove +#define COMSIG_ORGAN_REMOVED "comsig_organ_remove" /// From /item/organ/proc/Insert() (/obj/item/organ/) ////#define COMSIG_CARBON_GAIN_ORGAN "carbon_gain_organ" /// From /item/organ/proc/Remove() (/obj/item/organ/) diff --git a/code/__DEFINES/dcs/signals/signals_object.dm b/code/__DEFINES/dcs/signals/signals_object.dm index af93b14fea7..77e438a2f43 100644 --- a/code/__DEFINES/dcs/signals/signals_object.dm +++ b/code/__DEFINES/dcs/signals/signals_object.dm @@ -76,8 +76,8 @@ ////#define COMSIG_FIREALARM_ON_RESET "firealarm_reset" //! /obj access signals -////#define COMSIG_OBJ_ALLOWED "door_try_to_activate" - ////#define COMPONENT_OBJ_ALLOW (1<<0) +#define COMSIG_OBJ_ALLOWED "door_try_to_activate" + #define COMPONENT_OBJ_ALLOW (1<<0) ////#define COMSIG_AIRLOCK_SHELL_ALLOWED "airlock_shell_try_allowed" ////#define COMPONENT_AIRLOCK_SHELL_ALLOW (1<<0) @@ -246,7 +246,7 @@ ////#define COMSIG_IMPLANT_IMPLANTED "implant_implanted" /// Called on implants, after an implant has been removed: (mob/living/source, silent, special) -////#define COMSIG_IMPLANT_REMOVED "implant_removed" +#define COMSIG_IMPLANT_REMOVED "implant_removed" /// Called as a mindshield is implanted: (mob/user) ////#define COMSIG_PRE_MINDSHIELD_IMPLANT "pre_mindshield_implant" diff --git a/code/__DEFINES/tgs.config.dm b/code/__DEFINES/tgs.config.dm index a2fcc7855ab..8c0f5d3fa2c 100644 --- a/code/__DEFINES/tgs.config.dm +++ b/code/__DEFINES/tgs.config.dm @@ -3,7 +3,7 @@ #define TGS_DEFINE_AND_SET_GLOBAL(Name, Value) GLOBAL_VAR_INIT(##Name, ##Value); GLOBAL_PROTECT(##Name) #define TGS_READ_GLOBAL(Name) GLOB.##Name #define TGS_WRITE_GLOBAL(Name, Value) GLOB.##Name = ##Value -#define TGS_WORLD_ANNOUNCE(message) to_chat(world, "[html_encode(##message)]") +#define TGS_WORLD_ANNOUNCE(message) to_chat(world, SPAN_BOLDANNOUNCE("[html_encode(##message)]")) #define TGS_INFO_LOG(message) log_world("TGS Info: [##message]") #define TGS_WARNING_LOG(message) log_world("TGS Warn: [##message]") #define TGS_ERROR_LOG(message) log_world("TGS Error: [##message]") diff --git a/code/__DEFINES/tgui.dm b/code/__DEFINES/tgui.dm index 116f97170fb..8fe571c3537 100644 --- a/code/__DEFINES/tgui.dm +++ b/code/__DEFINES/tgui.dm @@ -14,6 +14,8 @@ /// Maximum ping timeout allowed to detect zombie windows #define TGUI_PING_TIMEOUT 4 SECONDS +/// Used for rate-limiting to prevent DoS by excessively refreshing a TGUI window +#define TGUI_REFRESH_FULL_UPDATE_COOLDOWN 5 SECONDS /// Window does not exist #define TGUI_WINDOW_CLOSED 0 @@ -28,11 +30,13 @@ #define TGUI_WINDOW_INDEX(window_id) text2num(copytext(window_id, 13)) /// Creates a message packet for sending via output() +// This is {"type":type,"payload":payload}, but pre-encoded. This is much faster +// than doing it the normal way. +// To ensure this is correct, this is unit tested in tgui_create_message. #define TGUI_CREATE_MESSAGE(type, payload) ( \ - url_encode(json_encode(list( \ - "type" = type, \ - "payload" = payload, \ - )))) + "%7b%22type%22%3a%22[type]%22%2c%22payload%22%3a[url_encode(json_encode(payload))]%7d" \ +) + /// Max length for Modal Input #define UI_MODAL_INPUT_MAX_LENGTH 1024 diff --git a/code/__HELPERS/_logging.dm b/code/__HELPERS/_logging.dm index 759b2ab5f03..d079ff8d1e1 100644 --- a/code/__HELPERS/_logging.dm +++ b/code/__HELPERS/_logging.dm @@ -469,3 +469,16 @@ GLOBAL_LIST_INIT(testing_global_profiler, list("_PROFILE_NAME" = "Global")) /client/proc/simple_info_line() return "[key_name(src)] ([AREACOORD(mob)])" + +/** + * Writes to a special log file if the log_suspicious_login config flag is set, + * which is intended to contain all logins that failed under suspicious circumstances. + * + * Mirrors this log entry to log_access when access_log_mirror is TRUE, so this proc + * doesn't need to be used alongside log_access and can replace it where appropriate. + */ +/proc/log_suspicious_login(text, access_log_mirror = TRUE) + if (CONFIG_GET(flag/log_suspicious_login)) + WRITE_LOG(GLOB.world_suspicious_login_log, "SUSPICIOUS_ACCESS: [text]") + if(access_log_mirror) + log_access(text) diff --git a/code/__HELPERS/chat.dm b/code/__HELPERS/chat.dm index def684c5099..230dd03361e 100644 --- a/code/__HELPERS/chat.dm +++ b/code/__HELPERS/chat.dm @@ -59,3 +59,19 @@ In TGS3 it will always be sent to all connected designated game chats. if(channels_to_use.len) world.TgsChatBroadcast(message, channels_to_use) + +/** + * Sends a message to TGS admin chat channels. + * + * category - The category of the mssage. + * message - The message to send. + */ +/proc/send2adminchat(category, message, embed_links = FALSE) + category = replacetext(replacetext(category, "\proper", ""), "\improper", "") + message = replacetext(replacetext(message, "\proper", ""), "\improper", "") + if(!embed_links) + message = GLOB.has_discord_embeddable_links.Replace(replacetext(message, "`", ""), " ```$1``` ") + world.TgsTargetedChatBroadcast("[category] | [message]", TRUE) + +/// Handles text formatting for item use hints in examine text +#define EXAMINE_HINT(text) ("" + text + "") diff --git a/code/__HELPERS/mobs.dm b/code/__HELPERS/mobs.dm index 7c090c2de0b..f7641dad1fb 100644 --- a/code/__HELPERS/mobs.dm +++ b/code/__HELPERS/mobs.dm @@ -305,3 +305,7 @@ Proc for attack log creation, because really why not else . = getCompoundIcon(desired) cached_character_icons[cachekey] = . + +/// Gets the client of the mob, allowing for mocking of the client. +/// You only need to use this if you know you're going to be mocking clients somewhere else. +#define GET_CLIENT(mob) (##mob.client || ##mob.mock_client) diff --git a/code/__HELPERS/view.dm b/code/__HELPERS/view.dm index d9b929dd7fc..e3921b0d129 100644 --- a/code/__HELPERS/view.dm +++ b/code/__HELPERS/view.dm @@ -140,17 +140,12 @@ return CONFIG_GET(string/default_view_square) */ /proc/getviewsize(view) - var/viewX - var/viewY if(isnum(view)) var/totalviewrange = 1 + 2 * view - viewX = totalviewrange - viewY = totalviewrange + return list(totalviewrange, totalviewrange) else var/list/viewrangelist = splittext(view,"x") - viewX = text2num(viewrangelist[1]) - viewY = text2num(viewrangelist[2]) - return list(viewX, viewY) + return list(text2num(viewrangelist[1]), text2num(viewrangelist[2])) /proc/in_view_range(mob/user, atom/A) var/list/view_range = getviewsize(user.client.view) diff --git a/code/_globalvars/logging.dm b/code/_globalvars/logging.dm index 096954a6903..4cebb3f88eb 100644 --- a/code/_globalvars/logging.dm +++ b/code/_globalvars/logging.dm @@ -32,12 +32,14 @@ GLOBAL_VAR(subsystem_log) GLOBAL_PROTECT(subsystem_log) GLOBAL_VAR(tgui_log) GLOBAL_PROTECT(tgui_log) -/////Picture logging +/// Picture logging GLOBAL_VAR(picture_log_directory) GLOBAL_PROTECT(picture_log_directory) - GLOBAL_VAR_INIT(picture_logging_id, 1) GLOBAL_PROTECT(picture_logging_id) GLOBAL_VAR(picture_logging_prefix) GLOBAL_PROTECT(picture_logging_prefix) -///// +/// Log associated with [/proc/log_suspicious_login()] +/// Intended to hold all logins that failed due to suspicious circumstances such as ban detection, CID randomisation etc. +GLOBAL_VAR(world_suspicious_login_log) +GLOBAL_PROTECT(world_suspicious_login_log) diff --git a/code/_globalvars/tgui.dm b/code/_globalvars/tgui.dm index 5598079ccc7..d3a7fdf55aa 100644 --- a/code/_globalvars/tgui.dm +++ b/code/_globalvars/tgui.dm @@ -1,2 +1,3 @@ //GLOBAL_DATUM(crew_manifest_tgui, /datum/crew_manifest) GLOBAL_DATUM(changelog_tgui, /datum/changelog) +//GLOBAL_DATUM(hotkeys_tgui, /datum/hotkeys_help) diff --git a/code/controllers/configuration/entries/logging.dm b/code/controllers/configuration/entries/logging.dm index dfffa03d547..d34d018c19c 100644 --- a/code/controllers/configuration/entries/logging.dm +++ b/code/controllers/configuration/entries/logging.dm @@ -1,2 +1,94 @@ /datum/config_entry/flag/emergency_tgui_logging config_entry_value = FALSE + +/// log messages sent in OOC +/datum/config_entry/flag/log_ooc + +/// log login/logout +/datum/config_entry/flag/log_access + +/// Config entry which special logging of failed logins under suspicious circumstances. +/datum/config_entry/flag/log_suspicious_login + +/// log client say +/datum/config_entry/flag/log_say + +/// log admin actions +/datum/config_entry/flag/log_admin + protection = CONFIG_ENTRY_LOCKED + +/// log prayers +/datum/config_entry/flag/log_prayer + +/// log silicons +/datum/config_entry/flag/log_silicon + +/datum/config_entry/flag/log_law + deprecated_by = /datum/config_entry/flag/log_silicon + +/datum/config_entry/flag/log_law/DeprecationUpdate(value) + return value + +/// log usage of tools +/datum/config_entry/flag/log_tools + +/// log game events +/datum/config_entry/flag/log_game + +/// log mech data +/datum/config_entry/flag/log_mecha + +/// log virology data +/datum/config_entry/flag/log_virus + +/// log assets +/datum/config_entry/flag/log_asset + +/// log voting +/datum/config_entry/flag/log_vote + +/// log client whisper +/datum/config_entry/flag/log_whisper + +/// log attack messages +/datum/config_entry/flag/log_attack + +/// log emotes +/datum/config_entry/flag/log_emote + +/// log economy actions +/datum/config_entry/flag/log_econ + +/// log traitor objectives +/datum/config_entry/flag/log_traitor + +/// log admin chat messages +/datum/config_entry/flag/log_adminchat + protection = CONFIG_ENTRY_LOCKED + +/// log pda messages +/datum/config_entry/flag/log_pda + +/// log uplink/spellbook/codex ciatrix purchases and refunds +/datum/config_entry/flag/log_uplink + +/// log telecomms messages +/datum/config_entry/flag/log_telecomms + +/// log certain expliotable parrots and other such fun things in a JSON file of twitter valid phrases. +/datum/config_entry/flag/log_twitter + +/// log all world.Topic() calls +/datum/config_entry/flag/log_world_topic + +/// log crew manifest to separate file +/datum/config_entry/flag/log_manifest + +/// log roundstart divide occupations debug information to a file +/datum/config_entry/flag/log_job_debug + +/// log shuttle related actions, ie shuttle computers, shuttle manipulator, emergency console +/datum/config_entry/flag/log_shuttle + +/// logs all timers in buckets on automatic bucket reset (Useful for timer debugging) +/datum/config_entry/flag/log_timers_on_bucket_reset diff --git a/code/datums/components/simple_access.dm b/code/datums/components/simple_access.dm new file mode 100644 index 00000000000..fd82d723327 --- /dev/null +++ b/code/datums/components/simple_access.dm @@ -0,0 +1,33 @@ +///This component allows us to give a mob access without giving them an ID card. +/datum/component/simple_access + dupe_mode = COMPONENT_DUPE_ALLOWED + ///list of accesses we are allowed to access via this component + var/list/access + +/datum/component/simple_access/Initialize(list/new_access, atom/donor_atom) + . = ..() + if(!ismob(parent)) + return COMPONENT_INCOMPATIBLE + access = new_access + RegisterSignal(parent, COMSIG_MOB_TRIED_ACCESS, .proc/on_tried_access) + if(!donor_atom) + return + if(istype(donor_atom, /obj/item/organ)) + RegisterSignal(donor_atom, COMSIG_ORGAN_REMOVED, .proc/on_donor_removed) + else if(istype(donor_atom, /obj/item/implant)) + RegisterSignal(donor_atom, COMSIG_IMPLANT_REMOVED, .proc/on_donor_removed) + RegisterSignal(donor_atom, COMSIG_PARENT_QDELETING, .proc/on_donor_removed) + +/datum/component/simple_access/proc/on_tried_access(datum/source, atom/locked_thing) + SIGNAL_HANDLER + if(!isobj(locked_thing)) + return LOCKED_ATOM_INCOMPATIBLE + var/obj/locked_object = locked_thing + if(locked_object.check_access_list(access)) + return ACCESS_ALLOWED + else + return ACCESS_DISALLOWED + +/datum/component/simple_access/proc/on_donor_removed(datum/source) + SIGNAL_HANDLER + qdel(src) diff --git a/code/datums/mocking/client.dm b/code/datums/mocking/client.dm new file mode 100644 index 00000000000..fd99e34520c --- /dev/null +++ b/code/datums/mocking/client.dm @@ -0,0 +1,13 @@ +/// This should match the interface of /client wherever necessary. +/datum/client_interface + /// Player preferences datum for the client + var/datum/preferences/prefs + + /// The view of the client, similar to /client/var/view. + var/view = "15x15" + + /// Objects on the screen of the client + var/list/screen = list() + + /// The mob the client controls + var/mob/mob diff --git a/code/defines/procs/dbcore.dm b/code/defines/procs/dbcore.dm index 6d459a60a53..a6f75ce5679 100644 --- a/code/defines/procs/dbcore.dm +++ b/code/defines/procs/dbcore.dm @@ -37,7 +37,7 @@ var/DB_SERVER = "" // This is the location of your MySQL server (localhost is US var/DB_PORT = 3306 // This is the port your MySQL server is running on (3306 is the default) */ -DBConnection +/DBConnection var/_db_con // This variable contains a reference to the actual database connection. var/dbi // This variable is a string containing the DBI MySQL requires. var/user // This variable contains the username data. @@ -47,76 +47,93 @@ DBConnection var/server = "" var/port = 3306 -DBConnection/New(dbi_handler,username,password_handler,cursor_handler) +/DBConnection/New(dbi_handler,username,password_handler,cursor_handler) src.dbi = dbi_handler src.user = username src.password = password_handler src.default_cursor = cursor_handler _db_con = _dm_db_new_con() -DBConnection/proc/Connect(dbi_handler=src.dbi,user_handler=src.user,password_handler=src.password,cursor_handler) +/DBConnection/proc/Connect(dbi_handler=src.dbi,user_handler=src.user,password_handler=src.password,cursor_handler) if(!sqllogging) return 0 - if(!src) return 0 + if(!src) + return 0 cursor_handler = src.default_cursor if(!cursor_handler) cursor_handler = Default_Cursor return _dm_db_connect(_db_con,dbi_handler,user_handler,password_handler,cursor_handler,null) -DBConnection/proc/Disconnect() return _dm_db_close(_db_con) +/DBConnection/proc/Disconnect() + return _dm_db_close(_db_con) -DBConnection/proc/IsConnected() - if(!sqllogging) return 0 +/DBConnection/proc/IsConnected() + if(!sqllogging) + return 0 var/success = _dm_db_is_connected(_db_con) return success -DBConnection/proc/Quote(str) return _dm_db_quote(_db_con,str) +/DBConnection/proc/Quote(str) + return _dm_db_quote(_db_con,str) -DBConnection/proc/ErrorMsg() return _dm_db_error_msg(_db_con) -DBConnection/proc/SelectDB(database_name,dbi) +/DBConnection/proc/ErrorMsg() + return _dm_db_error_msg(_db_con) + +/DBConnection/proc/SelectDB(database_name,dbi) if(IsConnected()) Disconnect() //return Connect("[dbi?"[dbi]":"dbi:mysql:[database_name]:[DB_SERVER]:[DB_PORT]"]",user,password) return Connect("[dbi?"[dbi]":"dbi:mysql:[database_name]:[sqladdress]:[sqlport]"]",user,password) -DBConnection/proc/NewQuery(sql_query,cursor_handler=src.default_cursor) return new/DBQuery(sql_query,src,cursor_handler) +/DBConnection/proc/NewQuery(sql_query,cursor_handler=src.default_cursor) + return new/DBQuery(sql_query,src,cursor_handler) -DBQuery/New(sql_query,DBConnection/connection_handler,cursor_handler) - if(sql_query) src.sql = sql_query - if(connection_handler) src.db_connection = connection_handler - if(cursor_handler) src.default_cursor = cursor_handler +/DBQuery/New(sql_query,DBConnection/connection_handler,cursor_handler) + if(sql_query) + src.sql = sql_query + if(connection_handler) + src.db_connection = connection_handler + if(cursor_handler) + src.default_cursor = cursor_handler _db_query = _dm_db_new_query() return ..() -DBQuery - var/sql // The sql query being executed. +/DBQuery + /// The sql query being executed. + var/sql var/default_cursor - var/list/columns //list of DB Columns populated by Columns() + /// List of DB Columns populated by Columns() + var/list/columns var/list/conversions - var/list/item[0] //list of data values populated by NextRow() + /// List of data values populated by NextRow() + var/list/item[0] var/DBConnection/db_connection var/_db_query -DBQuery/proc/Connect(DBConnection/connection_handler) src.db_connection = connection_handler +/DBQuery/proc/Connect(DBConnection/connection_handler) src.db_connection = connection_handler -DBQuery/proc/Execute(sql_query=src.sql,cursor_handler=default_cursor) +/DBQuery/proc/Execute(sql_query=src.sql,cursor_handler=default_cursor) Close() return _dm_db_execute(_db_query,sql_query,db_connection._db_con,cursor_handler,null) -DBQuery/proc/NextRow() return _dm_db_next_row(_db_query,item,conversions) +/DBQuery/proc/NextRow() + return _dm_db_next_row(_db_query,item,conversions) -DBQuery/proc/RowsAffected() return _dm_db_rows_affected(_db_query) +/DBQuery/proc/RowsAffected() + return _dm_db_rows_affected(_db_query) -DBQuery/proc/RowCount() return _dm_db_row_count(_db_query) +/DBQuery/proc/RowCount() + return _dm_db_row_count(_db_query) -DBQuery/proc/ErrorMsg() return _dm_db_error_msg(_db_query) +/DBQuery/proc/ErrorMsg() + return _dm_db_error_msg(_db_query) -DBQuery/proc/Columns() +/DBQuery/proc/Columns() if(!columns) columns = _dm_db_columns(_db_query,/DBColumn) return columns -DBQuery/proc/GetRowData() +/DBQuery/proc/GetRowData() var/list/columns = Columns() var/list/results if(columns.len) @@ -127,23 +144,26 @@ DBQuery/proc/GetRowData() results[C] = src.item[(cur_col.position+1)] return results -DBQuery/proc/Close() +/DBQuery/proc/Close() item.len = 0 columns = null conversions = null return _dm_db_close(_db_query) -DBQuery/proc/Quote(str) +/DBQuery/proc/Quote(str) return db_connection.Quote(str) -DBQuery/proc/SetConversion(column,conversion) - if(istext(column)) column = columns.Find(column) - if(!conversions) conversions = new/list(column) - else if(conversions.len < column) conversions.len = column +/DBQuery/proc/SetConversion(column,conversion) + if(istext(column)) + column = columns.Find(column) + if(!conversions) + conversions = new/list(column) + else if(conversions.len < column) + conversions.len = column conversions[column] = conversion -DBColumn +/DBColumn var/name var/table var/position //1-based index into item data @@ -152,7 +172,7 @@ DBColumn var/length var/max_length -DBColumn/New(name_handler,table_handler,position_handler,type_handler,flag_handler,length_handler,max_length_handler) +/DBColumn/New(name_handler,table_handler,position_handler,type_handler,flag_handler,length_handler,max_length_handler) src.name = name_handler src.table = table_handler src.position = position_handler @@ -163,21 +183,34 @@ DBColumn/New(name_handler,table_handler,position_handler,type_handler,flag_handl return ..() -DBColumn/proc/SqlTypeName(type_handler=src.sql_type) +/DBColumn/proc/SqlTypeName(type_handler=src.sql_type) switch(type_handler) - if(TINYINT) return "TINYINT" - if(SMALLINT) return "SMALLINT" - if(MEDIUMINT) return "MEDIUMINT" - if(INTEGER) return "INTEGER" - if(BIGINT) return "BIGINT" - if(FLOAT) return "FLOAT" - if(DOUBLE) return "DOUBLE" - if(DATE) return "DATE" - if(DATETIME) return "DATETIME" - if(TIMESTAMP) return "TIMESTAMP" - if(TIME) return "TIME" - if(STRING) return "STRING" - if(BLOB) return "BLOB" + if(TINYINT) + return "TINYINT" + if(SMALLINT) + return "SMALLINT" + if(MEDIUMINT) + return "MEDIUMINT" + if(INTEGER) + return "INTEGER" + if(BIGINT) + return "BIGINT" + if(FLOAT) + return "FLOAT" + if(DOUBLE) + return "DOUBLE" + if(DATE) + return "DATE" + if(DATETIME) + return "DATETIME" + if(TIMESTAMP) + return "TIMESTAMP" + if(TIME) + return "TIME" + if(STRING) + return "STRING" + if(BLOB) + return "BLOB" #undef Default_Cursor diff --git a/code/game/machinery/air_alarm.dm b/code/game/machinery/air_alarm.dm index 8e15ec8f8cc..22d250d781e 100644 --- a/code/game/machinery/air_alarm.dm +++ b/code/game/machinery/air_alarm.dm @@ -529,7 +529,7 @@ var/list/data = list( "locked" = locked, "siliconUser" = issilicon(user), - "remoteUser" = !!ui.parent_ui, + "remoteUser" = ui.parent_ui, "danger_level" = danger_level, "target_temperature" = "[target_temperature - T0C]C", "rcon" = rcon_setting, diff --git a/code/game/machinery/computer/atmos_control.dm b/code/game/machinery/computer/atmos_control.dm index d55cf42a403..e09e1247ea7 100644 --- a/code/game/machinery/computer/atmos_control.dm +++ b/code/game/machinery/computer/atmos_control.dm @@ -4,16 +4,20 @@ /obj/machinery/computer/atmoscontrol name = "\improper Central Atmospherics Computer" + desc = "Control the station's atmospheric systems from afar! Certified atmospherics technicians only." icon_keyboard = "generic_key" icon_screen = "comm_logs" light_color = "#00b000" density = TRUE anchored = TRUE circuit = /obj/item/circuitboard/atmoscontrol - req_access = list(access_ce) + req_access = list(access_atmospherics) var/list/monitored_alarm_ids = null var/datum/tgui_module/atmos_control/atmos_control +/obj/machinery/computer/atmoscontrol/Initialize(mapload) + . = ..() + /obj/machinery/computer/atmoscontrol/laptop //TODO: Change name to PCU and update mapdata to include replacement computers name = "\improper Atmospherics PCU" desc = "A personal computer unit. It seems to have only the Atmosphereics Control program installed." @@ -22,7 +26,7 @@ icon_keyboard = "pcu_key" density = FALSE light_color = "#00cc00" - density = 0 + density = FALSE /obj/machinery/computer/atmoscontrol/attack_ai(mob/user) ui_interact(user) @@ -34,14 +38,15 @@ /obj/machinery/computer/atmoscontrol/emag_act(remaining_carges, mob/user) if(!emagged) - user.visible_message( \ - SPAN_WARNING("\The [user] does something \the [src], causing the screen to flash!"), \ - SPAN_WARNING("You cause the screen to flash as you gain full control."), \ - "You hear an electronic warble.") + user.visible_message( + SPAN_WARNING("\The [user] does something \the [src], causing the screen to flash!"),\ + SPAN_WARNING("You cause the screen to flash as you gain full control."),\ + SPAN_NOTICE("You hear an electronic warble.") + ) atmos_control.emagged = TRUE return TRUE -/obj/machinery/computer/atmoscontrol/ui_interact(var/mob/user) +/obj/machinery/computer/atmoscontrol/ui_interact(mob/user) if(!atmos_control) atmos_control = new(src, req_access, req_one_access, monitored_alarm_ids) atmos_control.ui_interact(user) diff --git a/code/game/machinery/embedded_controller/airlock_docking_controller_multi.dm b/code/game/machinery/embedded_controller/airlock_docking_controller_multi.dm index e496c4c28c0..3b178d11861 100644 --- a/code/game/machinery/embedded_controller/airlock_docking_controller_multi.dm +++ b/code/game/machinery/embedded_controller/airlock_docking_controller_multi.dm @@ -30,6 +30,7 @@ ) /obj/machinery/embedded_controller/radio/docking_port_multi/ui_act(action, params) + . = ..() return //Apparently we swallow all input (this is corrected legacy code) //a docking port based on an airlock diff --git a/code/game/objects/items/weapons/id_cards/station_ids.dm b/code/game/objects/items/weapons/id_cards/station_ids.dm index 6b3bae86572..9fa976c258c 100644 --- a/code/game/objects/items/weapons/id_cards/station_ids.dm +++ b/code/game/objects/items/weapons/id_cards/station_ids.dm @@ -8,8 +8,10 @@ SPECIES_TESHARI = 'icons/mob/clothing/species/teshari/id.dmi' ) - var/access = list() - var/registered_name = "Unknown" // The name registered_name on the card + /// Access levels held by this card. + var/list/access = list() + /// The name registered_name on the card. + var/registered_name = "Unknown" slot_flags = SLOT_ID | SLOT_EARS var/age = "\[UNSET\]" @@ -97,7 +99,7 @@ return /obj/item/card/id/GetAccess() - return access + return access.Copy() /obj/item/card/id/GetID() return src diff --git a/code/game/objects/objs.dm b/code/game/objects/objs.dm index 9dcfeaddb3d..c0dff5da495 100644 --- a/code/game/objects/objs.dm +++ b/code/game/objects/objs.dm @@ -25,6 +25,10 @@ var/show_examine = TRUE // Does this pop up on a mob when the mob is examined? var/register_as_dangerous_object = FALSE // Should this tell its turf that it is dangerous automatically? + // Access levels, used in modules\jobs\access.dm + var/list/req_access + var/list/req_one_access + /obj/Initialize(mapload) if(register_as_dangerous_object) register_dangerous_to_step() diff --git a/code/modules/ai/interfaces.dm b/code/modules/ai/interfaces.dm index eea4aff46a7..cc6d1943ef7 100644 --- a/code/modules/ai/interfaces.dm +++ b/code/modules/ai/interfaces.dm @@ -57,8 +57,8 @@ /mob/living/proc/IGetID() /mob/living/simple_mob/IGetID() - if(myid) - return myid.GetID() + if(access_card) + return access_card.GetID() /mob/living/proc/instasis() diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index 50a5e815c24..23b71cea63a 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -169,7 +169,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( GLOB.directory[ckey] = src // Instantiate tgui panel - tgui_panel = new(src) + tgui_panel = new(src, "browseroutput") GLOB.ahelp_tickets.ClientLogin(src) SSserver_maint.UpdateHubStatus() @@ -295,9 +295,15 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( set_macros() update_movement_keys() + // Initialize stat panel + // stat_panel.initialize( + // inline_html = file2text('html/statbrowser.html'), + // inline_js = file2text('html/statbrowser.js'), + // inline_css = file2text('html/statbrowser.css'), + // ) + // Initialize tgui panel tgui_panel.initialize() - // src << browse(file('html/statbrowser.html'), "window=statbrowser") //if(alert_mob_dupe_login) // spawn() diff --git a/code/modules/jobs/access.dm b/code/modules/jobs/access.dm index caa313457ee..c6bb80b2848 100644 --- a/code/modules/jobs/access.dm +++ b/code/modules/jobs/access.dm @@ -1,8 +1,5 @@ -/obj/var/list/req_access -/obj/var/list/req_one_access - -//returns 1 if this mob has sufficient access to use this object +/// Returns 1 if this mob has sufficient access to use this object /obj/proc/allowed(mob/M) if(IsAdminGhost(M)) return TRUE @@ -29,8 +26,10 @@ return check_access_list(I ? I.GetAccess() : list()) /obj/proc/check_access_list(var/list/L) - if(!L) return 0 - if(!istype(L, /list)) return 0 + if(!L) + return 0 + if(!istype(L, /list)) + return 0 return has_access(req_access, req_one_access, L) /proc/has_access(var/list/req_access, var/list/req_one_access, var/list/accesses) diff --git a/code/modules/mob/living/carbon/human/species/station/protean/protean_blob.dm b/code/modules/mob/living/carbon/human/species/station/protean/protean_blob.dm index 0a8098a9b97..dbad1c2c8cd 100644 --- a/code/modules/mob/living/carbon/human/species/station/protean/protean_blob.dm +++ b/code/modules/mob/living/carbon/human/species/station/protean/protean_blob.dm @@ -64,7 +64,7 @@ /mob/living/simple_mob/protean_blob/Initialize(mapload, mob/living/carbon/human/H) . = ..() mob_radio = new(src) - myid = new(src) + access_card = new(src) if(H) humanform = H updatehealth() @@ -344,10 +344,10 @@ for(var/obj/item/pda/P in things_to_not_drop) if(P.id) var/obj/item/card/id/PID = P.id - blob.myid.access += PID.access + blob.access_card.access += PID.access for(var/obj/item/card/id/I in things_to_not_drop) - blob.myid.access += I.access + blob.access_card.access += I.access if(w_uniform && istype(w_uniform,/obj/item/clothing)) //No webbings tho. We do this after in case a suit was in the way var/obj/item/clothing/uniform = w_uniform diff --git a/code/modules/mob/living/simple_mob/simple_mob.dm b/code/modules/mob/living/simple_mob/simple_mob.dm index a3b56ac759b..90779be0b35 100644 --- a/code/modules/mob/living/simple_mob/simple_mob.dm +++ b/code/modules/mob/living/simple_mob/simple_mob.dm @@ -89,7 +89,7 @@ /// The list of lootable objects to drop, with "/path = prob%" structure var/list/loot_list = list() /// An ID card if they have one to give them access to stuff. - var/obj/item/card/id/myid + var/obj/item/card/id/access_card //* Mob environment settings *// /// Minimum "okay" temperature in kelvin @@ -280,8 +280,8 @@ /mob/living/simple_mob/Destroy() default_language = null - if(myid) - QDEL_NULL(myid) + if(access_card) + QDEL_NULL(access_card) friends.Cut() languages.Cut() diff --git a/code/modules/mob/living/simple_mob/simple_mob_vr.dm b/code/modules/mob/living/simple_mob/simple_mob_vr.dm index 90eea87bb5e..a0516bfbb21 100644 --- a/code/modules/mob/living/simple_mob/simple_mob_vr.dm +++ b/code/modules/mob/living/simple_mob/simple_mob_vr.dm @@ -56,8 +56,8 @@ //For all those ID-having mobs /mob/living/simple_mob/GetIdCard() - if(myid) - return myid + if(access_card) + return access_card // Update fullness based on size & quantity of belly contents /mob/living/simple_mob/proc/update_fullness() diff --git a/code/modules/mob/mob_defines.dm b/code/modules/mob/mob_defines.dm index 550d6737988..a0a6d859247 100644 --- a/code/modules/mob/mob_defines.dm +++ b/code/modules/mob/mob_defines.dm @@ -296,3 +296,6 @@ var/in_enclosed_vehicle = 0 var/last_radio_sound = -INFINITY + + /// A mock client, provided by tests and friends + var/datum/client_interface/mock_client diff --git a/code/modules/mob/mob_helpers.dm b/code/modules/mob/mob_helpers.dm index 599544f57d4..a1f3eb02be9 100644 --- a/code/modules/mob/mob_helpers.dm +++ b/code/modules/mob/mob_helpers.dm @@ -87,8 +87,9 @@ proc/getsensorlevel(A) /** * Returns true if the user should have admin AI level access + *! TO-BE-DEPRICATED */ -/proc/IsAdminGhost(var/mob/user) +/proc/IsAdminGhost(mob/user) if(!user) //Are they a mob? Auto interface updates call this with a null src return if(!user.client) // Do they have a client? @@ -101,6 +102,26 @@ proc/getsensorlevel(A) return return TRUE +/// Is the passed in mob a ghost with admin powers, doesn't check for AI interact like isAdminGhost() used to +/proc/isAdminObserver(mob/user) + if(!user) //Are they a mob? Auto interface updates call this with a null src + return + if(!user.client) // Do they have a client? + return + if(!isobserver(user)) // Are they a ghost? + return + if(!check_rights_for(user.client, R_ADMIN)) // Are they allowed? + return + return TRUE + +/// Is the passed in mob an admin ghost WITH AI INTERACT enabled +/proc/isAdminGhostAI(mob/user) + if(!isAdminObserver(user)) + return + if(!user.client.AI_Interact) // Do they have it enabled? + return + return TRUE + /** * Returns true if the AI has silicon control with those flags */ diff --git a/code/modules/modular_computers/file_system/programs/engineering/atmos_control.dm b/code/modules/modular_computers/file_system/programs/engineering/atmos_control.dm index 65894c4d80a..0908d1119e2 100644 --- a/code/modules/modular_computers/file_system/programs/engineering/atmos_control.dm +++ b/code/modules/modular_computers/file_system/programs/engineering/atmos_control.dm @@ -1,103 +1,14 @@ /datum/computer_file/program/atmos_control filename = "atmoscontrol" filedesc = "Atmosphere Control" - nanomodule_path = /datum/nano_module/atmos_control + tguimodule_path = /datum/tgui_module/atmos_control/ntos program_icon_state = "atmos_control" program_key_state = "atmos_key" program_menu_icon = "shuffle" extended_desc = "This program allows remote control of air alarms. This program can not be run on tablet computers." required_access = access_atmospherics - requires_ntnet = 1 + requires_ntnet = TRUE network_destination = "atmospheric control system" requires_ntnet_feature = NTNET_SYSTEMCONTROL usage_flags = PROGRAM_LAPTOP | PROGRAM_CONSOLE size = 17 - -/datum/nano_module/atmos_control - name = "Atmospherics Control" - var/obj/access = new() - var/emagged = 0 - var/ui_ref - var/list/monitored_alarms = list() - -/datum/nano_module/atmos_control/New(atmos_computer, req_access, req_one_access, monitored_alarm_ids) - ..() - access.req_access = req_access - access.req_one_access = req_one_access - - if(monitored_alarm_ids) - for(var/obj/machinery/alarm/alarm in GLOB.machines) - if(alarm.alarm_id && (alarm.alarm_id in monitored_alarm_ids)) - monitored_alarms += alarm - // machines may not yet be ordered at this point - monitored_alarms = dd_sortedObjectList(monitored_alarms) - -/datum/nano_module/atmos_control/Topic(href, href_list) - if(..()) - return 1 - - if(href_list["alarm"]) - if(ui_ref) - var/obj/machinery/alarm/alarm = locate(href_list["alarm"]) in (monitored_alarms.len ? monitored_alarms : GLOB.machines) - if(alarm) - var/datum/topic_state/TS = generate_state(alarm) - alarm.nano_ui_interact(usr, master_ui = ui_ref, state = TS) - return 1 - -/datum/nano_module/atmos_control/nano_ui_interact(mob/user, ui_key = "main", var/datum/nanoui/ui = null, var/force_open = 1, var/master_ui = null, var/datum/topic_state/state = default_state) - var/list/data = host.initial_data() - var/alarms[0] - var/turf/T = get_turf(nano_host()) - - // TODO: Move these to a cache, similar to cameras - for(var/obj/machinery/alarm/alarm in (monitored_alarms.len ? monitored_alarms : GLOB.machines)) - if(!monitored_alarms.len && alarm.alarms_hidden) - continue - alarms[++alarms.len] = list( - "name" = sanitize(alarm.name), - "ref"= "\ref[alarm]", - "danger" = max(alarm.danger_level, alarm.alarm_area.atmosalm), - "x" = alarm.x, - "y" = alarm.y, - "z" = alarm.z) - data["alarms"] = alarms - data["map_levels"] = GLOB.using_map.get_map_levels(T.z) - - ui = SSnanoui.try_update_ui(user, src, ui_key, ui, data, force_open) - if(!ui) - ui = new(user, src, ui_key, "atmos_control.tmpl", src.name, 625, 625, state = state) - if(host.update_layout()) // This is necessary to ensure the status bar remains updated along with rest of the UI. - ui.auto_update_layout = 1 - // adding a template with the key "mapContent" enables the map ui functionality - ui.add_template("mapContent", "atmos_control_map_content.tmpl") - // adding a template with the key "mapHeader" replaces the map header content - ui.add_template("mapHeader", "atmos_control_map_header.tmpl") - ui.set_initial_data(data) - ui.open() - ui.set_auto_update(0) - ui_ref = ui - -/datum/nano_module/atmos_control/proc/generate_state(air_alarm) - var/datum/topic_state/air_alarm/state = new() - state.atmos_control = src - state.air_alarm = air_alarm - return state - -/datum/topic_state/air_alarm - var/datum/nano_module/atmos_control/atmos_control = null - var/obj/machinery/alarm/air_alarm = null - -/datum/topic_state/air_alarm/can_use_topic(var/src_object, var/mob/user) - if(has_access(user)) - return UI_INTERACTIVE - return UI_UPDATE - -/datum/topic_state/air_alarm/href_list(var/mob/user) - var/list/extra_href = list() - extra_href["remote_connection"] = 1 - extra_href["remote_access"] = has_access(user) - - return extra_href - -/datum/topic_state/air_alarm/proc/has_access(var/mob/user) - return user && (isAI(user) || atmos_control.access.allowed(user) || atmos_control.emagged || air_alarm.rcon_setting == RCON_YES || (air_alarm.alarm_area.atmosalm && air_alarm.rcon_setting == RCON_AUTO) || (access_ce in user.GetAccess())) diff --git a/code/modules/tgui/external.dm b/code/modules/tgui/external.dm index 0b65cbfd273..f8dd0d372d6 100644 --- a/code/modules/tgui/external.dm +++ b/code/modules/tgui/external.dm @@ -13,9 +13,11 @@ * * required user mob The mob who opened/is using the UI. * optional ui datum/tgui The UI to be updated, if it exists. + * + *! ## To-Be-Deprecated. * optional parent_ui datum/tgui A parent UI that, when closed, closes this UI as well. */ -/datum/proc/ui_interact(mob/user, datum/tgui/ui = null, datum/tgui/parent_ui = null) +/datum/proc/ui_interact(mob/user, datum/tgui/ui, datum/tgui/parent_ui) return FALSE // Not implemented. /** @@ -55,7 +57,7 @@ * happens to change static data. * * required user the mob currently interacting with the ui - * optional tgui ui to be updated + * optional ui tgui to be updated */ /datum/proc/update_static_data(mob/user, datum/tgui/ui) if(!ui) @@ -63,6 +65,17 @@ if(ui) ui.send_full_update() +/** + * public + * + * Will force an update on static data for all viewers. + * Should be done manually whenever something happens to + * change static data. + */ +/datum/proc/update_static_data_for_all_viewers() + for (var/datum/tgui/window as anything in SStgui.open_uis_by_src[REF(src)]) + window.send_full_update() + /** * public * @@ -75,7 +88,7 @@ * return bool If the user's input has been handled and the UI should update. */ /datum/proc/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) - //SHOULD_CALL_PARENT(TRUE) + SHOULD_CALL_PARENT(TRUE) SEND_SIGNAL(src, COMSIG_UI_ACT, usr, action) // If UI is not interactive or usr calling Topic is not the UI user, bail. if(!ui || ui.status != UI_INTERACTIVE) @@ -147,26 +160,7 @@ * client/verb/uiclose(), which closes the ui window */ /datum/proc/ui_close(mob/user) - -/** - * verb - * - * Used by a client to fix broken TGUI windows caused by opening a UI window before assets load. - * Probably not very performant and forcibly destroys a bunch of windows, so it has some warnings attached. - * Conveniently, also allows devs to force a dev server reattach without relogging, since it yeets windows. - */ -/client/verb/tgui_fix_white() - set desc = "Only use this if you have a broken TGUI window occupying your screen!" - set name = "Fix TGUI" - set category = "OOC" - - if(alert(src, "Only use this verb if you have a white TGUI window stuck on your screen.", "Fix TGUI", "Continue", "Nevermind") != "Continue") - return - - SStgui.close_user_uis(mob) - if(alert(src, "Did that fix the problem?", "Fix TGUI", "Yes", "No") == "No") - SStgui.force_close_all_windows(mob) - alert(src, "UIs should be fixed now. If not, please cry to your nearest coder.", "Fix TGUI") + SIGNAL_HANDLER /** * verb diff --git a/code/modules/tgui/modules/atmos_control.dm b/code/modules/tgui/modules/atmos_control.dm index c36494b4c8a..f8e29840389 100644 --- a/code/modules/tgui/modules/atmos_control.dm +++ b/code/modules/tgui/modules/atmos_control.dm @@ -39,7 +39,8 @@ get_asset_datum(/datum/asset/simple/nanomaps), ) -/datum/tgui_module/atmos_control/ui_interact(mob/user, datum/tgui/ui = null) +/datum/tgui_module/atmos_control/ui_interact(mob/user, datum/tgui/ui) + . = ..() ui = SStgui.try_update_ui(user, src, ui) if(!ui) ui = new(user, src, tgui_id, name) @@ -89,8 +90,8 @@ return state /datum/ui_state/air_alarm_remote - var/datum/tgui_module/atmos_control/atmos_control = null - var/obj/machinery/alarm/air_alarm = null + var/datum/tgui_module/atmos_control/atmos_control = null + var/obj/machinery/alarm/air_alarm = null /datum/ui_state/air_alarm_remote/can_use_topic(src_object, mob/user) if(!atmos_control.ui_ref) @@ -100,7 +101,7 @@ return UI_INTERACTIVE return UI_UPDATE -/datum/ui_state/air_alarm_remote/proc/has_access(var/mob/user) +/datum/ui_state/air_alarm_remote/proc/has_access(mob/user) return user && (isAI(user) || atmos_control.access.allowed(user) || atmos_control.emagged || air_alarm.rcon_setting == RCON_YES || (air_alarm.alarm_area.atmosalm && air_alarm.rcon_setting == RCON_AUTO) || (access_ce in user.GetAccess())) /datum/ui_state/air_alarm_remote/Destroy() diff --git a/code/modules/tgui/states.dm b/code/modules/tgui/states.dm index c7840e851cd..0c92ab41748 100644 --- a/code/modules/tgui/states.dm +++ b/code/modules/tgui/states.dm @@ -80,6 +80,8 @@ /mob/living/silicon/ai/shared_ui_interaction(src_object) // Disable UIs if the AI is unpowered. + // if(apc_override == src_object) //allows AI to (eventually) use the interface for their own APC even when out of power + // return UI_INTERACTIVE if(lacks_power()) return UI_DISABLED return ..() @@ -87,10 +89,42 @@ /mob/living/silicon/robot/shared_ui_interaction(src_object) // Disable UIs if the object isn't installed in the borg AND the borg is either locked, has a dead cell, or no cell. var/atom/device = src_object - if((istype(device) && device.loc != src) && (!cell || cell.charge <= 0)) // || locked_down + if((istype(device) && device.loc != src) && (!cell || cell.charge <= 0 || lockcharge)) return UI_DISABLED return ..() +/** + * public + * + * Distance versus interaction check. + * + * required src_object atom/movable The object which owns the UI. + * + * return UI_state The state of the UI. + */ +/mob/living/proc/shared_living_ui_distance(atom/movable/src_object, viewcheck = TRUE, allow_tk = TRUE) + // If the object is obscured, close it. + if(viewcheck && !(src_object in view(src))) + return UI_CLOSE + var/dist = get_dist(src_object, src) + // Open and interact if 1-0 tiles away. + if(dist <= 1) + return UI_INTERACTIVE + // View only if 2-3 tiles away. + else if(dist <= 2) + return UI_UPDATE + // Disable if 5 tiles away. + else if(dist <= 5) + return UI_DISABLED + // Otherwise, we got nothing. + return UI_CLOSE + +/mob/living/carbon/human/shared_living_ui_distance(atom/movable/src_object, viewcheck = TRUE, allow_tk = TRUE) + if(allow_tk && (TK in mutations)) + // if(allow_tk && dna.check_mutation(/datum/mutation/human/telekinesis) && tkMaxRangeCheck(src, src_object)) + return UI_INTERACTIVE + return ..() + /** * public * @@ -106,34 +140,3 @@ /atom/proc/contents_ui_distance(src_object, mob/living/user) // Just call this mob's check. return user.shared_living_ui_distance(src_object) - -/** - * public - * - * Distance versus interaction check. - * - * required src_object atom/movable The object which owns the UI. - * - * return UI_state The state of the UI. - */ -/mob/living/proc/shared_living_ui_distance(atom/movable/src_object, viewcheck = TRUE) - // If the object is obscured, close it. - if(viewcheck && !(src_object in view(src))) - return UI_CLOSE - var/dist = get_dist(src_object, src) - // Open and interact if 1-0 tiles away. - if(dist <= 1) - return UI_INTERACTIVE - // View only if 2-3 tiles away. - else if(dist <= 2) - return UI_UPDATE - // Disable if 5 tiles away. - else if(dist <= 5) - return UI_DISABLED - // Otherwise, we got nothing. - return UI_CLOSE - -/mob/living/carbon/human/shared_living_ui_distance(atom/movable/src_object, viewcheck = TRUE) - // if(dna.check_mutation(TK) && tkMaxRangeCheck(src, src_object)) - // return UI_INTERACTIVE - return ..() diff --git a/code/modules/tgui/states/admin.dm b/code/modules/tgui/states/admin.dm index c4573e9df8e..e6b7109e7eb 100644 --- a/code/modules/tgui/states/admin.dm +++ b/code/modules/tgui/states/admin.dm @@ -12,6 +12,6 @@ GLOBAL_DATUM_INIT(admin_state, /datum/ui_state/admin_state, new) /datum/ui_state/admin_state/can_use_topic(src_object, mob/user) - if(check_rights(R_ADMIN, FALSE, user.client)) + if(check_rights_for(user.client, R_ADMIN)) return UI_INTERACTIVE return UI_CLOSE diff --git a/code/modules/tgui/states/debug.dm b/code/modules/tgui/states/debug.dm index 2e9c0f3d014..6c600b38cea 100644 --- a/code/modules/tgui/states/debug.dm +++ b/code/modules/tgui/states/debug.dm @@ -1,6 +1,6 @@ GLOBAL_DATUM_INIT(debug_state, /datum/ui_state/debug_state, new) /datum/ui_state/debug_state/can_use_topic(src_object, mob/user) - if(check_rights(R_DEBUG, FALSE, user.client)) + if(check_rights_for(user.client, R_DEBUG)) return UI_INTERACTIVE return UI_CLOSE diff --git a/code/modules/tgui/states/default.dm b/code/modules/tgui/states/default.dm index 8a6c521cf46..f25f85b785f 100644 --- a/code/modules/tgui/states/default.dm +++ b/code/modules/tgui/states/default.dm @@ -20,15 +20,10 @@ GLOBAL_DATUM_INIT(default_state, /datum/ui_state/default, new) /mob/living/default_can_use_topic(src_object) . = shared_ui_interaction(src_object) - if(. > UI_CLOSE && loc) - . = min(., loc.contents_ui_distance(src_object, src)) // Check the distance... - if(. == UI_INTERACTIVE) // Non-human living mobs can only look, not touch. - return UI_UPDATE - -/mob/living/carbon/human/default_can_use_topic(src_object) - . = shared_ui_interaction(src_object) - if(. > UI_CLOSE) + if(. > UI_CLOSE && loc) //must not be in nullspace. . = min(., shared_living_ui_distance(src_object)) // Check the distance... + if(. == UI_INTERACTIVE && !IsAdvancedToolUser()) // Non-human living mobs can only look, not touch. + return UI_UPDATE /mob/living/silicon/robot/default_can_use_topic(src_object) . = shared_ui_interaction(src_object) @@ -67,11 +62,6 @@ GLOBAL_DATUM_INIT(default_state, /datum/ui_state/default, new) return UI_CLOSE -/mob/living/simple_animal/default_can_use_topic(src_object) - . = shared_ui_interaction(src_object) - if(. > UI_CLOSE) - . = min(., shared_living_ui_distance(src_object)) //simple animals can only use things they're near. - /mob/living/silicon/pai/default_can_use_topic(src_object) // pAIs can only use themselves and the owner's radio. if((src_object == src || src_object == radio) && !stat) diff --git a/code/modules/tgui/states/new_player.dm b/code/modules/tgui/states/new_player.dm index cf6f83ed3a1..f74c2643827 100644 --- a/code/modules/tgui/states/new_player.dm +++ b/code/modules/tgui/states/new_player.dm @@ -10,4 +10,3 @@ GLOBAL_DATUM_INIT(new_player_state, /datum/ui_state/new_player_state, new) if(isnewplayer(user) || check_rights_for(user.client, R_ADMIN)) return UI_INTERACTIVE return UI_CLOSE - diff --git a/code/modules/tgui/states/reverse_contained.dm b/code/modules/tgui/states/reverse_contained.dm new file mode 100644 index 00000000000..b6c432b0783 --- /dev/null +++ b/code/modules/tgui/states/reverse_contained.dm @@ -0,0 +1,16 @@ +/** + *! Not copyrighted, but magatsuchi made it. + */ + +/** + * tgui state: reverse_contained_state + * + * Checks if src_object is inside of user. + */ + +GLOBAL_DATUM_INIT(reverse_contained_state, /datum/ui_state/reverse_contained_state, new) + +/datum/ui_state/reverse_contained_state/can_use_topic(atom/src_object, mob/user) + if(!user.contains(src_object)) + return UI_CLOSE + return user.shared_ui_interaction(src_object) diff --git a/code/modules/tgui/status_composers.dm b/code/modules/tgui/status_composers.dm new file mode 100644 index 00000000000..73f3b4ac37d --- /dev/null +++ b/code/modules/tgui/status_composers.dm @@ -0,0 +1,111 @@ +/// The sane defaults for a UI such as a computer or a machine. +/proc/default_ui_state(mob/user, atom/source) + return min( + ui_status_user_is_abled(user, source), + // ui_status_user_has_free_hands(user, source), + ui_status_user_is_advanced_tool_user(user), + ui_status_only_living(user), + max( + ui_status_user_is_adjacent(user, source), + ui_status_silicon_has_access(user, source), + ) + ) + +/// Returns a UI status such that users adjacent to source will be able to interact, +/// far away users will be able to see, and anyone farther won't see anything. +/// Dead users will receive updates no matter what, though you likely want to add +/// a [`ui_status_only_living`] check for finer observer interactions. +/proc/ui_status_user_is_adjacent(mob/user, atom/source, allow_tk = TRUE) + if (isliving(user)) + var/mob/living/living_user = user + return living_user.shared_living_ui_distance(source, allow_tk = allow_tk) + else + return UI_UPDATE + +/// Returns a UI status such that the dead will be able to watch, but not interact. +/proc/ui_status_only_living(mob/user, source) + if(isliving(user)) + return UI_INTERACTIVE + + if(isobserver(user)) + // If they turn on ghost AI control, admins can always interact. + if(isAdminGhostAI(user)) + return UI_INTERACTIVE + + // Regular ghosts can always at least view if in range. + var/datum/client_interface/client = GET_CLIENT(user) + if(client) + var/clientviewlist = getviewsize(client.view) + if(get_dist(source, user) < max(clientviewlist[1], clientviewlist[2])) + return UI_UPDATE + + return UI_CLOSE + +/// Returns a UI status such that users with debilitating conditions, such as +/// being dead or not having power for silicons, will not be able to interact. +/// Being dead will disable UI, being incapacitated will continue updating it, +/// and anything else will make it interactive. +/proc/ui_status_user_is_abled(mob/user, atom/source) + return user.shared_ui_interaction(source) + +/// Returns a UI status such that those without blocked hands will be able to interact, +/// but everyone else can only watch. +// /proc/ui_status_user_has_free_hands(mob/user, atom/source) +// return HAS_TRAIT(user, TRAIT_HANDS_BLOCKED) ? UI_UPDATE : UI_INTERACTIVE + +/// Returns a UI status such that advanced tool users will be able to interact, +/// but everyone else can only watch. +/proc/ui_status_user_is_advanced_tool_user(mob/user) + return user.IsAdvancedToolUser() ? UI_INTERACTIVE : UI_UPDATE + +/// Returns a UI status such that silicons will be able to interact with whatever +/// they would have access to if this was a machine. For example, AIs can +/// interact if there's cameras with wireless control is enabled. +/proc/ui_status_silicon_has_access(mob/user, atom/source) + if (!issilicon(user)) + return UI_CLOSE + var/mob/living/silicon/silicon_user = user + return silicon_user.get_ui_access(source) + +/// Returns a UI status representing this silicon's capability to access +/// the given source. Called by `ui_status_silicon_has_access`. +/mob/living/silicon/proc/get_ui_access(atom/source) + return UI_CLOSE + +/mob/living/silicon/robot/get_ui_access(atom/source) + // Robots can interact with anything they can see. + var/list/clientviewlist = getviewsize(client.view) + if(get_dist(src, source) <= min(clientviewlist[1],clientviewlist[2])) + return UI_INTERACTIVE + return UI_DISABLED // Otherwise they can keep the UI open. + +/mob/living/silicon/ai/get_ui_access(atom/source) + // The AI can interact with anything it can see nearby, or with cameras while wireless control is enabled. + if(!control_disabled && can_see(source)) + return UI_INTERACTIVE + return UI_CLOSE + +/mob/living/silicon/pai/get_ui_access(atom/source) + // pAIs can only use themselves and the owner's radio. + if((source == src || source == radio) && !stat) + return UI_INTERACTIVE + else + return UI_CLOSE + +/// Returns UI_INTERACTIVE if the user is conscious and lying down. +/// Returns UI_UPDATE otherwise. +/proc/ui_status_user_is_conscious_and_lying_down(mob/user) + if (!isliving(user)) + return UI_UPDATE + + var/mob/living/living_user = user + return (living_user.lying == TRUE && living_user.stat == CONSCIOUS) \ + ? UI_INTERACTIVE \ + : UI_UPDATE + +/// Return UI_INTERACTIVE if the user is strictly adjacent to the target atom, whether they can see it or not. +/// Return UI_CLOSE otherwise. +/proc/ui_status_user_strictly_adjacent(mob/user, atom/target) + if(get_dist(target, user) > 1) + return UI_CLOSE + return UI_INTERACTIVE diff --git a/code/modules/tgui/tgui.dm b/code/modules/tgui/tgui.dm index 9db3b85284f..54d95af20a0 100644 --- a/code/modules/tgui/tgui.dm +++ b/code/modules/tgui/tgui.dm @@ -31,8 +31,14 @@ var/closing = FALSE /// The status/visibility of the UI. var/status = UI_INTERACTIVE + /// Timed refreshing state + var/refreshing = FALSE /// Topic state used to determine status/interactability. var/datum/ui_state/state = null + /// Rate limit client refreshes to prevent DoS. + COOLDOWN_DECLARE(refresh_cooldown) + +//! ## Legacy Defines - To-Be-Deprecated. /// The map z-level to display. var/map_z_level = 1 /// The Parent UI @@ -49,10 +55,12 @@ * required src_object datum The object or datum which owns the UI. * required interface string The interface used to render the UI. * optional title string The title of the UI. - * optional parent_ui datum/tgui The parent of this UI. * optional ui_x int Deprecated: Window width. * optional ui_y int Deprecated: Window height. * + *! To-Be-Deprecated + * optional parent_ui datum/tgui The parent of this UI. + * * return datum/tgui The requested UI. */ /datum/tgui/New(mob/user, datum/src_object, interface, title, ui_x, ui_y) @@ -65,7 +73,8 @@ src.interface = interface if(title) src.title = title - src.state = src_object.ui_state() + src.state = src_object.ui_state(user) + //! To-Be-Deprecated src.parent_ui = parent_ui if(parent_ui) parent_ui.children += src @@ -73,6 +82,11 @@ if(ui_x && ui_y) src.window_size = list(ui_x, ui_y) +/datum/tgui/Destroy() + user = null + src_object = null + return ..() + /** * public * @@ -95,6 +109,7 @@ window.acquire_lock(src) if(!window.is_ready()) window.initialize( + strict_mode = TRUE, fancy = user.client.prefs.tgui_fancy, assets = list( get_asset_datum(/datum/asset/simple/tgui), @@ -123,13 +138,10 @@ * * optional can_be_suspended bool */ -/datum/tgui/proc/close(can_be_suspended = TRUE, logout = FALSE) +/datum/tgui/proc/close(can_be_suspended = TRUE) if(closing) return closing = TRUE - for(var/datum/tgui/child in children) - child.close(can_be_suspended, logout) - children.Cut() // If we don't have window_id, open proc did not have the opportunity // to finish, therefore it's safe to skip this whole block. if(window) @@ -137,11 +149,11 @@ // and we want to keep them around, to allow user to read // the error message properly. window.release_lock() - window.close(can_be_suspended, logout) + window.close(can_be_suspended) src_object.ui_close(user) SStgui.on_close(src) state = null - parent_ui = null + parent_ui = null //! To-Be-Deprecated qdel(src) /** @@ -189,11 +201,17 @@ /datum/tgui/proc/send_full_update(custom_data, force) if(!user.client || !initialized || closing) return + if(!COOLDOWN_FINISHED(src, refresh_cooldown)) + refreshing = TRUE + addtimer(CALLBACK(src, .proc/send_full_update), TGUI_REFRESH_FULL_UPDATE_COOLDOWN, TIMER_UNIQUE) + return + refreshing = FALSE var/should_update_data = force || status >= UI_UPDATE window.send_message("update", get_payload( custom_data, with_data = should_update_data, with_static_data = TRUE)) + COOLDOWN_START(src, refresh_cooldown, TGUI_REFRESH_FULL_UPDATE_COOLDOWN) /** * public @@ -224,6 +242,7 @@ "title" = title, "status" = status, "interface" = interface, + "refreshing" = refreshing, "window" = list( "key" = window_key, "size" = window_size, @@ -261,7 +280,7 @@ return var/datum/host = src_object.ui_host(user) // If the object or user died (or something else), abort. - if(!src_object || !host || !user || !window) + if(QDELETED(src_object) || QDELETED(host) || QDELETED(user) || QDELETED(window)) close(can_be_suspended = FALSE) return // Validate ping @@ -273,7 +292,7 @@ return // Update through a normal call to ui_interact if(status != UI_DISABLED && (autoupdate || force)) - src_object.ui_interact(user, src, parent_ui) + src_object.ui_interact(user, src, parent_ui) //! parent_ui To-Be-Deprecated return // Update status only var/needs_update = process_status() @@ -285,9 +304,10 @@ /** - Sets the current map z level of the tgui window (so we know which Z we need to be interacting with.) + * public + * + * Sets the current map z level of the tgui window (so we know which Z we need to be interacting with.) */ - /datum/tgui/proc/set_map_z_level(nz) map_z_level = nz @@ -300,7 +320,7 @@ /datum/tgui/proc/process_status() var/prev_status = status status = src_object.ui_status(user, state) - if(parent_ui) + if(parent_ui) //! parent_ui To-Be-Deprecated status = min(status, parent_ui.status) return prev_status != status @@ -322,6 +342,9 @@ return FALSE switch(type) if("ready") + // Send a full update when the user manually refreshes the UI + if(initialized) + send_full_update() initialized = TRUE if("ping/reply") initialized = TRUE diff --git a/code/modules/tgui/tgui_alert.dm b/code/modules/tgui/tgui_alert.dm index 1a86cca705b..f16fe5fab5c 100644 --- a/code/modules/tgui/tgui_alert.dm +++ b/code/modules/tgui/tgui_alert.dm @@ -8,8 +8,9 @@ * * title - The of the alert modal, shown on the top of the TGUI window. * * buttons - The options that can be chosen by the user, each string is assigned a button on the UI. * * timeout - The timeout of the alert, after which the modal will close and qdel itself. Set to zero for no timeout. + * * autofocus - The bool that controls if this alert should grab window focus. */ -/proc/tgui_alert(mob/user, message, title, list/buttons, timeout = 60 SECONDS) +/proc/tgui_alert(mob/user, message = "", title, list/buttons = list("Ok"), timeout = 0, autofocus = TRUE) if (!user) user = usr if (!istype(user)) @@ -18,7 +19,13 @@ user = client.mob else return - var/datum/tgui_modal/alert = new(user, message, title, buttons, timeout) + // Client does NOT have tgui_input on: Returns regular input + // if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input)) + // if(length(buttons) == 2) + // return alert(user, message, title, buttons[1], buttons[2]) + // if(length(buttons) == 3) + // return alert(user, message, title, buttons[1], buttons[2], buttons[3]) + var/datum/tgui_modal/alert = new(user, message, title, buttons, timeout, autofocus) alert.ui_interact(user) alert.wait() if (alert) @@ -35,9 +42,10 @@ * * title - The of the alert modal, shown on the top of the TGUI window. * * buttons - The options that can be chosen by the user, each string is assigned a button on the UI. * * callback - The callback to be invoked when a choice is made. - * * timeout - The timeout of the alert, after which the modal will close and qdel itself. Set to zero for no timeout. + * * timeout - The timeout of the alert, after which the modal will close and qdel itself. Disabled by default, can be set to seconds otherwise. + * * autofocus - The bool that controls if this alert should grab window focus. */ -/proc/tgui_alert_async(mob/user, message, title, list/buttons, datum/callback/callback, timeout = 60 SECONDS) +/proc/tgui_alert_async(mob/user, message = "", title, list/buttons = list("Ok"), datum/callback/callback, timeout = 0, autofocus = TRUE) if (!user) user = usr if (!istype(user)) @@ -46,7 +54,13 @@ user = client.mob else return - var/datum/tgui_modal/async/alert = new(user, message, title, buttons, callback, timeout) + // Client does NOT have tgui_input on: Returns regular input + // if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input)) + // if(length(buttons) == 2) + // return alert(user, message, title, buttons[1], buttons[2]) + // if(length(buttons) == 3) + // return alert(user, message, title, buttons[1], buttons[2], buttons[3]) + var/datum/tgui_modal/async/alert = new(user, message, title, buttons, callback, timeout, autofocus) alert.ui_interact(user) /** @@ -68,13 +82,16 @@ var/start_time /// The lifespan of the tgui_modal, after which the window will close and delete itself. var/timeout + /// The bool that controls if this modal should grab window focus + var/autofocus /// Boolean field describing if the tgui_modal was closed by the user. var/closed -/datum/tgui_modal/New(mob/user, message, title, list/buttons, timeout) - src.title = title - src.message = message +/datum/tgui_modal/New(mob/user, message, title, list/buttons, timeout, autofocus) + src.autofocus = autofocus src.buttons = buttons.Copy() + src.message = message + src.title = title if (timeout) src.timeout = timeout start_time = world.time @@ -90,7 +107,7 @@ * the window was closed by the user. */ /datum/tgui_modal/proc/wait() - while (!choice && !closed) + while (!choice && !closed && !QDELETED(src)) stoplag(1) /datum/tgui_modal/ui_interact(mob/user, datum/tgui/ui) @@ -106,13 +123,17 @@ /datum/tgui_modal/ui_state(mob/user) return GLOB.always_state -/datum/tgui_modal/ui_data(mob/user) - . = list( - "title" = title, - "message" = message, - "buttons" = buttons - ) +/datum/tgui_modal/ui_static_data(mob/user) + . = list() + .["autofocus"] = autofocus + .["buttons"] = buttons + .["message"] = message + .["large_buttons"] = FALSE//user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_large) + .["swapped_buttons"] = FALSE//user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_swapped) + .["title"] = title +/datum/tgui_modal/ui_data(mob/user) + . = list() if(timeout) .["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS)) @@ -123,10 +144,18 @@ switch(action) if("choose") if (!(params["choice"] in buttons)) - return - choice = params["choice"] + CRASH("[usr] entered a non-existent button choice: [params["choice"]]") + set_choice(params["choice"]) + closed = TRUE SStgui.close_uis(src) return TRUE + if("cancel") + closed = TRUE + SStgui.close_uis(src) + return TRUE + +/datum/tgui_modal/proc/set_choice(choice) + src.choice = choice /** * # async tgui_modal @@ -137,24 +166,18 @@ /// The callback to be invoked by the tgui_modal upon having a choice made. var/datum/callback/callback -/datum/tgui_modal/async/New(mob/user, message, title, list/buttons, callback, timeout) - ..(user, title, message, buttons, timeout) +/datum/tgui_modal/async/New(mob/user, message, title, list/buttons, callback, timeout, autofocus) + ..(user, message, title, buttons, timeout, autofocus) src.callback = callback /datum/tgui_modal/async/Destroy(force, ...) QDEL_NULL(callback) . = ..() -/datum/tgui_modal/async/ui_close(mob/user) +/datum/tgui_modal/async/set_choice(choice) . = ..() - qdel(src) - -/datum/tgui_modal/async/ui_act(action, list/params) - . = ..() - if (!. || choice == null) - return - callback.InvokeAsync(choice) - qdel(src) + if(!isnull(src.choice)) + callback?.InvokeAsync(src.choice) /datum/tgui_modal/async/wait() return diff --git a/code/modules/tgui/tgui_input_list.dm b/code/modules/tgui/tgui_input_list.dm index 242b69a9347..5a56bdf0cbb 100644 --- a/code/modules/tgui/tgui_input_list.dm +++ b/code/modules/tgui/tgui_input_list.dm @@ -6,13 +6,14 @@ * * user - The user to show the input box to. * * message - The content of the input box, shown in the body of the TGUI window. * * title - The title of the input box, shown on the top of the TGUI window. - * * buttons - The options that can be chosen by the user, each string is assigned a button on the UI. - * * timeout - The timeout of the input box, after which the input box will close and qdel itself. Set to zero for no timeout. + * * items - The options that can be chosen by the user, each string is assigned a button on the UI. + * * default - If an option is already preselected on the UI. Current values, etc. + * * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout. */ -/proc/tgui_input_list(mob/user, message, title, list/buttons, timeout = 0) +/proc/tgui_input_list(mob/user, message, title = "Select", list/items, default, timeout = 0) if (!user) user = usr - if(!length(buttons)) + if(!length(items)) return if (!istype(user)) if (istype(user, /client)) @@ -20,7 +21,10 @@ user = client.mob else return - var/datum/tgui_list_input/input = new(user, message, title, buttons, timeout) + /// Client does NOT have tgui_input on: Returns regular input + // if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input)) + // return input(user, message, title, default) as null|anything in items + var/datum/tgui_list_input/input = new(user, message, title, items, default, timeout) input.ui_interact(user) input.wait() if (input) @@ -35,14 +39,15 @@ * * user - The user to show the input box to. * * message - The content of the input box, shown in the body of the TGUI window. * * title - The title of the input box, shown on the top of the TGUI window. - * * buttons - The options that can be chosen by the user, each string is assigned a button on the UI. + * * items - The options that can be chosen by the user, each string is assigned a button on the UI. + * * default - If an option is already preselected on the UI. Current values, etc. * * callback - The callback to be invoked when a choice is made. * * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout. */ -/proc/tgui_input_list_async(mob/user, message, title, list/buttons, datum/callback/callback, timeout = 60 SECONDS) +/proc/tgui_input_list_async(mob/user, message, title = "Select", list/items, default, datum/callback/callback, timeout = 60 SECONDS) if (!user) user = usr - if(!length(buttons)) + if(!length(items)) return if (!istype(user)) if (istype(user, /client)) @@ -50,7 +55,10 @@ user = client.mob else return - var/datum/tgui_list_input/async/input = new(user, message, title, buttons, callback, timeout) + /// Client does NOT have tgui_input on: Returns regular input + // if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input)) + // return input(user, message, title) as null|anything in items + var/datum/tgui_list_input/async/input = new(user, message, title, items, default, callback, timeout) input.ui_interact(user) /** @@ -64,12 +72,14 @@ var/title /// The textual body of the TGUI window var/message - /// The list of buttons (responses) provided on the TGUI window - var/list/buttons + /// The list of items (responses) provided on the TGUI window + var/list/items /// Buttons (strings specifically) mapped to the actual value (e.g. a mob or a verb) - var/list/buttons_map + var/list/items_map /// The button that the user has pressed, null if no selection has been made var/choice + /// The default button to be selected + var/default /// The time at which the tgui_list_input was created, for displaying timeout progress. var/start_time /// The lifespan of the tgui_list_input, after which the window will close and delete itself. @@ -77,21 +87,28 @@ /// Boolean field describing if the tgui_list_input was closed by the user. var/closed -/datum/tgui_list_input/New(mob/user, message, title, list/buttons, timeout) +/datum/tgui_list_input/New(mob/user, message, title, list/items, default, timeout) src.title = title src.message = message - src.buttons = list() - src.buttons_map = list() + src.items = list() + src.items_map = list() + src.default = default + var/list/repeat_items = list() // Gets rid of illegal characters var/static/regex/whitelistedWords = regex(@{"([^\u0020-\u8000]+)"}) - for(var/i in buttons) + for(var/i in items) + if(!i) + continue + var/string_key = whitelistedWords.Replace("[i]", "") - src.buttons += string_key - src.buttons_map[string_key] = i + //avoids duplicated keys E.g: when areas have the same name + string_key = avoid_assoc_duplicate_keys(string_key, repeat_items) + src.items += string_key + src.items_map[string_key] = i if (timeout) src.timeout = timeout @@ -100,7 +117,7 @@ /datum/tgui_list_input/Destroy(force, ...) SStgui.close_uis(src) - QDEL_NULL(buttons) + QDEL_NULL(items) . = ..() /** @@ -114,7 +131,7 @@ /datum/tgui_list_input/ui_interact(mob/user, datum/tgui/ui) ui = SStgui.try_update_ui(user, src, ui) if(!ui) - ui = new(user, src, "ListInput") + ui = new(user, src, "ListInputModal") ui.open() /datum/tgui_list_input/ui_close(mob/user) @@ -125,11 +142,13 @@ return GLOB.always_state /datum/tgui_list_input/ui_static_data(mob/user) - . = list( - "title" = title, - "message" = message, - "buttons" = buttons - ) + . = list() + .["init_value"] = default || items[1] + .["items"] = items + .["large_buttons"] = FALSE//user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_large) + .["message"] = message + .["swapped_buttons"] = FALSE//user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_swapped) + .["title"] = title /datum/tgui_list_input/ui_data(mob/user) . = list() @@ -141,17 +160,21 @@ if (.) return switch(action) - if("choose") - if (!(params["choice"] in buttons)) + if("submit") + if (!(params["entry"] in items)) return - choice = buttons_map[params["choice"]] + set_choice(items_map[params["entry"]]) + closed = TRUE SStgui.close_uis(src) return TRUE if("cancel") - SStgui.close_uis(src) closed = TRUE + SStgui.close_uis(src) return TRUE +/datum/tgui_list_input/proc/set_choice(choice) + src.choice = choice + /** * # async tgui_list_input * @@ -161,24 +184,18 @@ /// The callback to be invoked by the tgui_list_input upon having a choice made. var/datum/callback/callback -/datum/tgui_list_input/async/New(mob/user, message, title, list/buttons, callback, timeout) - ..(user, title, message, buttons, timeout) +/datum/tgui_list_input/async/New(mob/user, message, title, list/items, default, callback, timeout) + ..(user, message, title, items, default, timeout) src.callback = callback /datum/tgui_list_input/async/Destroy(force, ...) QDEL_NULL(callback) . = ..() -/datum/tgui_list_input/async/ui_close(mob/user) +/datum/tgui_list_input/async/set_choice(choice) . = ..() - qdel(src) - -/datum/tgui_list_input/async/ui_act(action, list/params) - . = ..() - if (!. || choice == null) - return - callback.InvokeAsync(choice) - qdel(src) + if(!isnull(src.choice)) + callback?.InvokeAsync(src.choice) /datum/tgui_list_input/async/wait() return diff --git a/code/modules/tgui/tgui_input_number.dm b/code/modules/tgui/tgui_input_number.dm new file mode 100644 index 00000000000..839b2f2c5ec --- /dev/null +++ b/code/modules/tgui/tgui_input_number.dm @@ -0,0 +1,209 @@ +/** + * Creates a TGUI window with a number input. Returns the user's response as num | null. + * + * This proc should be used to create windows for number entry that the caller will wait for a response from. + * If tgui fancy chat is turned off: Will return a normal input. If a max or min value is specified, will + * validate the input inside the UI and ui_act. + * + * Arguments: + * * user - The user to show the number input to. + * * message - The content of the number input, shown in the body of the TGUI window. + * * title - The title of the number input modal, shown on the top of the TGUI window. + * * default - The default (or current) value, shown as a placeholder. Users can press refresh with this. + * * max_value - Specifies a maximum value. If none is set, any number can be entered. Pressing "max" defaults to 1000. + * * min_value - Specifies a minimum value. Often 0. + * * timeout - The timeout of the number input, after which the modal will close and qdel itself. Set to zero for no timeout. + * * round_value - whether the inputted number is rounded down into an integer. + */ +/proc/tgui_input_number(mob/user, message, title = "Number Input", default = 0, max_value = 10000, min_value = 0, timeout = 0, round_value = TRUE) + if (!user) + user = usr + if (!istype(user)) + if (istype(user, /client)) + var/client/client = user + user = client.mob + else + return + // Client does NOT have tgui_input on: Returns regular input + // if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input)) + // var/input_number = input(user, message, title, default) as null|num + // return clamp(round_value ? round(input_number) : input_number, min_value, max_value) + var/datum/tgui_input_number/number_input = new(user, message, title, default, max_value, min_value, timeout, round_value) + number_input.ui_interact(user) + number_input.wait() + if (number_input) + . = number_input.entry + qdel(number_input) + +/** + * Creates an asynchronous TGUI number input window with an associated callback. + * + * This proc should be used to create number inputs that invoke a callback with the user's entry. + * + * Arguments: + * * user - The user to show the number input to. + * * message - The content of the number input, shown in the body of the TGUI window. + * * title - The title of the number input modal, shown on the top of the TGUI window. + * * default - The default (or current) value, shown as a placeholder. Users can press refresh with this. + * * max_value - Specifies a maximum value. If none is set, any number can be entered. Pressing "max" defaults to 1000. + * * min_value - Specifies a minimum value. Often 0. + * * callback - The callback to be invoked when a choice is made. + * * timeout - The timeout of the number input, after which the modal will close and qdel itself. Set to zero for no timeout. + * * round_value - whether the inputted number is rounded down into an integer. + */ +/proc/tgui_input_number_async(mob/user, message, title = "Number Input", default = 0, max_value = 10000, min_value = 0, datum/callback/callback, timeout = 60 SECONDS, round_value = TRUE) + if (!user) + user = usr + if (!istype(user)) + if (istype(user, /client)) + var/client/client = user + user = client.mob + else + return + // Client does NOT have tgui_input on: Returns regular input + // if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input)) + // var/input_number = input(user, message, title, default) as null|num + // return clamp(round_value ? round(input_number) : input_number, min_value, max_value) + var/datum/tgui_input_number/async/number_input = new(user, message, title, default, max_value, min_value, callback, timeout, round_value) + number_input.ui_interact(user) + +/** + * # tgui_input_number + * + * Datum used for instantiating and using a TGUI-controlled number input that prompts the user with + * a message and has an input for number entry. + */ +/datum/tgui_input_number + /// Boolean field describing if the tgui_input_number was closed by the user. + var/closed + /// The default (or current) value, shown as a default. Users can press reset with this. + var/default + /// The entry that the user has return_typed in. + var/entry + /// The maximum value that can be entered. + var/max_value + /// The prompt's body, if any, of the TGUI window. + var/message + /// The minimum value that can be entered. + var/min_value + /// The time at which the number input was created, for displaying timeout progress. + var/start_time + /// The lifespan of the number input, after which the window will close and delete itself. + var/timeout + /// The title of the TGUI window + var/title + /// Whether the submitted number is rounded down into an integer. + var/round_value + + +/datum/tgui_input_number/New(mob/user, message, title, default, max_value, min_value, timeout, round_value) + src.default = default + src.max_value = max_value + src.message = message + src.min_value = min_value + src.title = title + src.round_value = round_value + if (timeout) + src.timeout = timeout + start_time = world.time + QDEL_IN(src, timeout) + /// Checks for empty numbers - bank accounts, etc. + if(max_value == 0) + src.min_value = 0 + if(default) + src.default = 0 + /// Sanity check + if(default < min_value) + src.default = min_value + if(default > max_value) + CRASH("Default value is greater than max value.") + +/datum/tgui_input_number/Destroy(force, ...) + SStgui.close_uis(src) + . = ..() + +/** + * Waits for a user's response to the tgui_input_number's prompt before returning. Returns early if + * the window was closed by the user. + */ +/datum/tgui_input_number/proc/wait() + while (!entry && !closed && !QDELETED(src)) + stoplag(1) + +/datum/tgui_input_number/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "NumberInputModal") + ui.open() + +/datum/tgui_input_number/ui_close(mob/user) + . = ..() + closed = TRUE + +/datum/tgui_input_number/ui_state(mob/user) + return GLOB.always_state + +/datum/tgui_input_number/ui_static_data(mob/user) + . = list() + .["init_value"] = default // Default is a reserved keyword + .["large_buttons"] = FALSE//user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_large) + .["max_value"] = max_value + .["message"] = message + .["min_value"] = min_value + .["swapped_buttons"] = FALSE//user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_swapped) + .["title"] = title + +/datum/tgui_input_number/ui_data(mob/user) + . = list() + if(timeout) + .["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS)) + +/datum/tgui_input_number/ui_act(action, list/params) + . = ..() + if (.) + return + switch(action) + if("submit") + if(!isnum(params["entry"])) + CRASH("A non number was input into tgui input number by [usr]") + var/choice = round_value ? round(params["entry"]) : params["entry"] + if(choice > max_value) + CRASH("A number greater than the max value was input into tgui input number by [usr]") + if(choice < min_value) + CRASH("A number less than the min value was input into tgui input number by [usr]") + set_entry(choice) + closed = TRUE + SStgui.close_uis(src) + return TRUE + if("cancel") + closed = TRUE + SStgui.close_uis(src) + return TRUE + +/datum/tgui_input_number/proc/set_entry(entry) + src.entry = entry + +/** + * # async tgui_input_number + * + * An asynchronous version of tgui_input_number to be used with callbacks instead of waiting on user responses. + */ +/datum/tgui_input_number/async + /// The callback to be invoked by the tgui_input_number upon having a choice made. + var/datum/callback/callback + +/datum/tgui_input_number/async/New(mob/user, message, title, default, max_value, min_value, callback, timeout) + ..(user, message, title, default, max_value, min_value, timeout) + src.callback = callback + +/datum/tgui_input_number/async/Destroy(force, ...) + QDEL_NULL(callback) + . = ..() + +/datum/tgui_input_number/async/set_entry(entry) + . = ..() + if(!isnull(src.entry)) + callback?.InvokeAsync(src.entry) + +/datum/tgui_input_number/async/wait() + return diff --git a/code/modules/tgui/tgui_input_text.dm b/code/modules/tgui/tgui_input_text.dm new file mode 100644 index 00000000000..f506c6d360a --- /dev/null +++ b/code/modules/tgui/tgui_input_text.dm @@ -0,0 +1,213 @@ +/** + * Creates a TGUI window with a text input. Returns the user's response. + * + * This proc should be used to create windows for text entry that the caller will wait for a response from. + * If tgui fancy chat is turned off: Will return a normal input. If max_length is specified, will return + * stripped_multiline_input. + * + * Arguments: + * * user - The user to show the text input to. + * * message - The content of the text input, shown in the body of the TGUI window. + * * title - The title of the text input modal, shown on the top of the TGUI window. + * * default - The default (or current) value, shown as a placeholder. + * * max_length - Specifies a max length for input. MAX_MESSAGE_LEN is default (1024) + * * multiline - Bool that determines if the input box is much larger. Good for large messages, laws, etc. + * * encode - Toggling this determines if input is filtered via html_encode. Setting this to FALSE gives raw input. + * * timeout - The timeout of the textbox, after which the modal will close and qdel itself. Set to zero for no timeout. + */ +/proc/tgui_input_text(mob/user, message = "", title = "Text Input", default, max_length = MAX_MESSAGE_LEN, multiline = FALSE, encode = TRUE, timeout = 0) + if (!user) + user = usr + if (!istype(user)) + if (istype(user, /client)) + var/client/client = user + user = client.mob + else + return + // Client does NOT have tgui_input on: Returns regular input + // if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input)) + // if(encode) + // if(multiline) + // return stripped_multiline_input(user, message, title, default, max_length) + // else + // return stripped_input(user, message, title, default, max_length) + // else + // if(multiline) + // return input(user, message, title, default) as message|null + // else + // return input(user, message, title, default) as text|null + var/datum/tgui_input_text/text_input = new(user, message, title, default, max_length, multiline, encode, timeout) + text_input.ui_interact(user) + text_input.wait() + if (text_input) + . = text_input.entry + qdel(text_input) + +/** + * Creates an asynchronous TGUI text input window with an associated callback. + * + * This proc should be used to create text inputs that invoke a callback with the user's entry. + * Arguments: + * * user - The user to show the text input to. + * * message - The content of the text input, shown in the body of the TGUI window. + * * title - The title of the text input modal, shown on the top of the TGUI window. + * * default - The default (or current) value, shown as a placeholder. + * * max_length - Specifies a max length for input. + * * multiline - Bool that determines if the input box is much larger. Good for large messages, laws, etc. + * * encode - If toggled, input is filtered via html_encode. Setting this to FALSE gives raw input. + * * callback - The callback to be invoked when a choice is made. + */ +/proc/tgui_input_text_async(mob/user, message = "", title = "Text Input", default, max_length = MAX_MESSAGE_LEN, multiline = FALSE, encode = TRUE, datum/callback/callback, timeout = 60 SECONDS) + if (!user) + user = usr + if (!istype(user)) + if (istype(user, /client)) + var/client/client = user + user = client.mob + else + return + // Client does NOT have tgui_input on: Returns regular input + // if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input)) + // if(encode) + // if(multiline) + // return stripped_multiline_input(user, message, title, default, max_length) + // else + // return stripped_input(user, message, title, default, max_length) + // else + // if(multiline) + // return input(user, message, title, default) as message|null + // else + // return input(user, message, title, default) as text|null + var/datum/tgui_input_text/async/text_input = new(user, message, title, default, max_length, multiline, encode, callback, timeout) + text_input.ui_interact(user) + +/** + * # tgui_input_text + * + * Datum used for instantiating and using a TGUI-controlled text input that prompts the user with + * a message and has an input for text entry. + */ +/datum/tgui_input_text + /// Boolean field describing if the tgui_input_text was closed by the user. + var/closed + /// The default (or current) value, shown as a default. + var/default + /// Whether the input should be stripped using html_encode + var/encode + /// The entry that the user has return_typed in. + var/entry + /// The maximum length for text entry + var/max_length + /// The prompt's body, if any, of the TGUI window. + var/message + /// Multiline input for larger input boxes. + var/multiline + /// The time at which the text input was created, for displaying timeout progress. + var/start_time + /// The lifespan of the text input, after which the window will close and delete itself. + var/timeout + /// The title of the TGUI window + var/title + + +/datum/tgui_input_text/New(mob/user, message, title, default, max_length, multiline, encode, timeout) + src.default = default + src.encode = encode + src.max_length = max_length + src.message = message + src.multiline = multiline + src.title = title + if (timeout) + src.timeout = timeout + start_time = world.time + QDEL_IN(src, timeout) + +/datum/tgui_input_text/Destroy(force, ...) + SStgui.close_uis(src) + . = ..() + +/** + * Waits for a user's response to the tgui_input_text's prompt before returning. Returns early if + * the window was closed by the user. + */ +/datum/tgui_input_text/proc/wait() + while (!entry && !closed && !QDELETED(src)) + stoplag(1) + +/datum/tgui_input_text/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "TextInputModal") + ui.open() + +/datum/tgui_input_text/ui_close(mob/user) + . = ..() + closed = TRUE + +/datum/tgui_input_text/ui_state(mob/user) + return GLOB.always_state + +/datum/tgui_input_text/ui_static_data(mob/user) + . = list() + .["large_buttons"] = FALSE//user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_large) + .["max_length"] = max_length + .["message"] = message + .["multiline"] = multiline + .["placeholder"] = default // Default is a reserved keyword + .["swapped_buttons"] = FALSE//user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_swapped) + .["title"] = title + +/datum/tgui_input_text/ui_data(mob/user) + . = list() + if(timeout) + .["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS)) + +/datum/tgui_input_text/ui_act(action, list/params) + . = ..() + if (.) + return + switch(action) + if("submit") + if(max_length) + if(length(params["entry"]) > max_length) + CRASH("[usr] typed a text string longer than the max length") + if(encode && (length(html_encode(params["entry"])) > max_length)) + to_chat(usr, SPAN_NOTICE("Input uses special characters, thus reducing the maximum length.")) + set_entry(params["entry"]) + closed = TRUE + SStgui.close_uis(src) + return TRUE + if("cancel") + closed = TRUE + SStgui.close_uis(src) + return TRUE + +/datum/tgui_input_text/proc/set_entry(entry) + if(!isnull(entry)) + var/converted_entry = encode ? html_encode(entry) : entry + src.entry = trim(converted_entry, max_length) + +/** + * # async tgui_input_text + * + * An asynchronous version of tgui_input_text to be used with callbacks instead of waiting on user responses. + */ +/datum/tgui_input_text/async + // The callback to be invoked by the tgui_input_text upon having a choice made. + var/datum/callback/callback + +/datum/tgui_input_text/async/New(mob/user, message, title, default, max_length, multiline, encode, callback, timeout) + ..(user, message, title, default, max_length, multiline, encode, timeout) + src.callback = callback + +/datum/tgui_input_text/async/Destroy(force, ...) + QDEL_NULL(callback) + . = ..() + +/datum/tgui_input_text/async/set_entry(entry) + . = ..() + if(!isnull(src.entry)) + callback?.InvokeAsync(src.entry) + +/datum/tgui_input_text/async/wait() + return diff --git a/code/modules/tgui/tgui_window.dm b/code/modules/tgui/tgui_window.dm index 7f668a8e296..48cfaa5441e 100644 --- a/code/modules/tgui/tgui_window.dm +++ b/code/modules/tgui/tgui_window.dm @@ -18,6 +18,7 @@ var/message_queue var/sent_assets = list() // Vars passed to initialize proc (and saved for later) + var/initial_strict_mode var/initial_fancy var/initial_assets var/initial_inline_html @@ -47,11 +48,15 @@ * state. You can begin sending messages right after initializing. Messages * will be put into the queue until the window finishes loading. * - * optional assets list List of assets to inline into the html. - * optional inline_html string Custom HTML to inject. - * optional fancy bool If TRUE, will hide the window titlebar. + * optional strict_mode bool - Enables strict error handling and BSOD. + * optional fancy bool - If TRUE and if this is NOT a panel, will hide the window titlebar. + * optional assets list - List of assets to load during initialization. + * optional inline_html string - Custom HTML to inject. + * optional inline_js string - Custom JS to inject. + * optional inline_css string - Custom CSS to inject. */ /datum/tgui_window/proc/initialize( + strict_mode = FALSE, fancy = FALSE, assets = list(), inline_html = "", @@ -79,6 +84,7 @@ // Generate page html var/html = SStgui.basehtml html = replacetextEx(html, "\[tgui:windowId]", id) + html = replacetextEx(html, "\[tgui:strictMode]", strict_mode) // Inject assets var/inline_assets_str = "" for(var/datum/asset/asset in assets) @@ -99,7 +105,7 @@ html = replacetextEx(html, "", inline_html) // Inject inline JS if (inline_js) - inline_js = "" + inline_js = "" html = replacetextEx(html, "", inline_js) // Inject inline CSS if (inline_css) @@ -113,6 +119,20 @@ if(!is_browser) winset(client, id, "on-close=\"uiclose [id]\"") +/** + * public + * + * Reinitializes the panel with previous data used for initialization. + */ +/datum/tgui_window/proc/reinitialize() + initialize( + strict_mode = initial_strict_mode, + fancy = initial_fancy, + assets = initial_assets, + inline_html = initial_inline_html, + inline_js = initial_inline_js, + inline_css = initial_inline_css) + /** * public * @@ -291,6 +311,18 @@ : "[id].browser:update") message_queue = null +/** + * public + * + * Replaces the inline HTML content. + * + * required inline_html string HTML to inject + */ +/datum/tgui_window/proc/replace_html(inline_html = "") + client << output(url_encode(inline_html), is_browser \ + ? "[id]:replaceHtml" \ + : "[id].browser:replaceHtml") + /** * private * @@ -334,12 +366,7 @@ client << link(href_list["url"]) if("cacheReloaded") // Reinitialize - initialize( - fancy = initial_fancy, - assets = initial_assets, - inline_html = initial_inline_html, - inline_js = initial_inline_js, - inline_css = initial_inline_css) + reinitialize() // Resend the assets for(var/asset in sent_assets) send_asset(asset) diff --git a/code/modules/tgui_panel/tgui_panel.dm b/code/modules/tgui_panel/tgui_panel.dm index e0057f1e86d..32c1d841a7d 100644 --- a/code/modules/tgui_panel/tgui_panel.dm +++ b/code/modules/tgui_panel/tgui_panel.dm @@ -13,9 +13,9 @@ var/broken = FALSE var/initialized_at -/datum/tgui_panel/New(client/client) +/datum/tgui_panel/New(client/client, id) src.client = client - window = new(client, "browseroutput") + window = new(client, id) window.subscribe(src, .proc/on_message) /datum/tgui_panel/Del() @@ -42,9 +42,11 @@ sleep(1) initialized_at = world.time // Perform a clean initialization - window.initialize(assets = list( - get_asset_datum(/datum/asset/simple/tgui_panel), - )) + window.initialize( + strict_mode = TRUE, + assets = list( + get_asset_datum(/datum/asset/simple/tgui_panel), + )) window.send_asset(get_asset_datum(/datum/asset/simple/namespaced/fontawesome)) window.send_asset(get_asset_datum(/datum/asset/simple/namespaced/tgfont)) window.send_asset(get_asset_datum(/datum/asset/spritesheet/chat)) diff --git a/modular_citadel/code/modules/shadekin/shadekin.dm b/modular_citadel/code/modules/shadekin/shadekin.dm deleted file mode 100644 index 6337eea24d7..00000000000 --- a/modular_citadel/code/modules/shadekin/shadekin.dm +++ /dev/null @@ -1,457 +0,0 @@ -/*TODO: - - Get shadekins OFF the simple mobs and into something more stable like the human species template. - - Make some adjustments for better RP and Combat authenticity. - - Pray every night that Verysoft doesn't get mad for messing with the shadekins to the degree we might be doing. - -*/ - -/mob/living/simple_animal/shadekin //Spawning the prototype spawns a random one, see initialize() - name = "shadekin" - desc = "A strange, undocumented creature. It doesn't look like anything you've seen before." - icon = 'icons/mob/vore_shadekin.dmi' - icon_state = "map_example" - icon_living = "map_example" - faction = "shadekin" - ui_icons = 'icons/mob/shadekin_hud.dmi' - - maxHealth = 200 - health = 200 - - move_to_delay = 2 - speed = -1 - see_in_dark = 10 //SHADEkin - has_hands = TRUE //paws - seedarkness = FALSE //SHAAAADEkin - attack_sound = 'sound/weapons/bladeslice.ogg' - has_langs = list(LANGUAGE_GALCOM,LANGUAGE_SHADEKIN) - - investigates = TRUE - reacts = TRUE - run_at_them = FALSE - cooperative = FALSE - - melee_damage_lower = 10 - melee_damage_upper = 20 - - min_oxy = 0 - max_oxy = 0 - min_tox = 0 - max_tox = 0 - min_co2 = 0 - max_co2 = 0 - min_n2 = 0 - max_n2 = 0 - minbodytemp = 0 - maxbodytemp = 600 //Used to be 900, reduced to 600 for purposes of making them a bit more vulnerable. - Nylon - - speak_chance = 2 - speak = list("Marrr.", "Marrr?", "Marrr!") - emote_hear = list("chrrrrrs", "wurbles", "wrrrrbles") - emote_see = list("tailtwitches", "earflicks") - say_maybe_target = list("...mar?") - say_got_target = list("MAR!!!") - response_help = "pets the" - response_disarm = "bops the" - response_harm = "hits the" - attacktext = list("mauled","slashed","clawed") - friendly = list("boops", "paws", "mars softly at", "sniffs on") - reactions = list("Mar?" = "Marrr!", "Mar!" = "Marrr???", "Mar." = "Marrr.") - - vore_active = TRUE - vore_pounce_chance = 10 - vore_icons = SA_ICON_LIVING - swallowTime = 2 SECONDS - vore_escape_chance = 25 - - //None, they stay as their defaults. - vore_digest_chance = 0 - vore_absorb_chance = 0 - vore_bump_chance = 0 //They follow people, this would be DENGEROUS - - var/eye_state = RED_EYES //Eye color/energy gain/loss mode - var/eye_icon_state = null //Default, changed in init - var/eye_desc //Eye color description added to examine - - var/mob/living/carbon/human/henlo_human //Human we're stalking currently - - //Behavior - var/stalker = TRUE //Do we creep up on humans - var/shy_approach = FALSE //Do we creep up slowly on humans to boop them - - //Icon handling - var/image/tailimage //Cached tail image - - //Darknesssss - var/energy = 100 //For abilities - var/energy_adminbuse = FALSE //For adminbuse infinite energy - var/dark_gains = 0 //Last tick's change in energy - var/ability_flags = 0 //Flags for active abilities - var/atom/movable/screen/darkhud //Holder to update this icon - var/atom/movable/screen/energyhud //Holder to update this icon - - var/list/shadekin_abilities - -/mob/living/simple_animal/shadekin/Initialize(mapload) - //You spawned the prototype, and want a totally random one. - if(type == /mob/living/simple_animal/shadekin) - - //I'm told by VerySoft these are the liklihood values - var/list/sk_types = list( - /mob/living/simple_animal/shadekin/red = 20, //Actively seek people out to nom, so fairly common to see (relatively speaking), - /mob/living/simple_animal/shadekin/blue = 15, //Explorers that like to interact with people, so still fairly common, - /mob/living/simple_animal/shadekin/purple = 15, //Also explorers that may or may not homf people, - /mob/living/simple_animal/shadekin/yellow = 1 //Very rare, usually never leaves their home - ) - var/new_type = pickweight(sk_types) - - new new_type(loc) - return INITIALIZE_HINT_QDEL - - if(icon_state == "map_example") - icon_state = pick("white","dark","brown") - - icon_living = icon_state - - switch(eye_state) - if(BLUE_EYES) - eye_icon_state = "e_blue" - if(RED_EYES) - eye_icon_state = "e_red" - if(PURPLE_EYES) - eye_icon_state = "e_purple" - if(YELLOW_EYES) - eye_icon_state = "e_yellow" - if(GREEN_EYES) - eye_icon_state = "e_green" - if(ORANGE_EYES) - eye_icon_state = "e_orange" - else - eye_icon_state = "e_red" - - tailimage = image('icons/mob/vore_shadekin64.dmi',null,icon_state) - tailimage.pixel_x = -16 - - if(eye_desc) - desc += " This one has [eye_desc]!" - - var/list/ability_types = subtypesof(/obj/effect/shadekin_ability) - shadekin_abilities = list() - for(var/type in ability_types) - shadekin_abilities += new type(src) - - update_icon() - - return ..() - -/mob/living/simple_animal/shadekin/Destroy() - QDEL_LIST_NULL(shadekin_abilities) - . = ..() - -/mob/living/simple_animal/shadekin/init_vore() - if(LAZYLEN(vore_organs)) - return - - var/obj/belly/B = new /obj/belly(src) - vore_selected = B - B.immutable = 1 - B.name = vore_stomach_name ? vore_stomach_name : "stomach" - B.desc = vore_stomach_flavor ? vore_stomach_flavor : "Your surroundings are warm, soft, and slimy. Makes sense, considering you're inside \the [name]." - B.digest_mode = vore_default_mode - B.escapable = vore_escape_chance > 0 - B.escapechance = vore_escape_chance - B.digestchance = vore_digest_chance - B.absorbchance = vore_absorb_chance - B.human_prey_swallow_time = swallowTime - B.nonhuman_prey_swallow_time = swallowTime - B.vore_verb = "swallow" - // TODO - Customizable per mob - B.emote_lists[DM_HOLD] = list( - "The walls gently squeeze against you. The wet sounds of shifting flesh against your form fill the air.", - "The hot, humid air rushes around you for a moment as the creature urps. The walls clench in around you for a moment, before relaxxing again.", - "Your body is soaked in the fluids that cling to the churning walls. They squeeze across your form gently, conforming to your shape.", - "You can feel the world around you shift and sway as the creature moves! The flesh is stretchy, doughy. You can sink into it a little ways before it bounces back, curling you into a small shape." - ) - B.emote_lists[DM_DIGEST] = list( - "The walls slop thick slime across your body! It tingles briefly before the sting and ache sets in!", - "The sound of your body slipping and sliding against the powerfully churning stomach fills the air!", - "The grip of that stomach is harsh. Eagerly mushing and rubbing that slime into your body in attempts to break you down!", - "The intense churning and grinding jostles your around within the thick slime as you're slowly broken down!" - ) - B.emote_lists[DM_ABSORB] = list( - "The walls cling to you awfully close... It's almost like you're sinking into them.", - "You can feel the walls press in tightly against you, clinging to you posessively!", - "It almost feels like you're sinking into the soft, doughy flesh!", - "You can feel the walls press in around you. Almost molten, so squishy!!" - ) - B.emote_lists[DM_DRAIN] = list( - "The walls churn down on you heavily!! It's hard to move!", - "You can feel yourself getting weaker with every moment! The doughy walls sap your strength!", - "You're practically smothered in the oppressive heat of the creature's stomach!", - "It's hot, wet and tight!" - ) - B.emote_lists[DM_HEAL] = list( - "The walls pulse against you almost rhythmically. It feels nice, almost like a massage.", - "You're gently squeezed in pleasant warmth, softly churned.", - "The doughy feel of the heavy flesh clinging to you makes you feel a little stronger with every passing moment.", - "The flesh caresses across your body gently as you're held." - ) - B.digest_messages_prey = list( - "Your body is steadily softened more and more over time! Eventually you pass out. The creature's stomach rumbles powerfully as you are reduced to paste, processed for energy!", - "The creature's slimy gut lets out a heavy groan as you're slowly melted away. Gushing deeper through the creature.", - "The stinging and aching gives way to numbness as you're slowly smothered out. Your body is steadily reduced to nutrients and energy for the creature to continue on its way.", - "The chaos of being digested fades as you're snuffed out by a harsh clench! You're steadily broken down into a thick paste, processed and absorbed by the predator!" - ) - -/mob/living/simple_animal/shadekin/Life() - . = ..() - if(ability_flags & AB_PHASE_SHIFTED) - density = FALSE - - //Convert spare nutrition into energy at a certain ratio - if(. && nutrition > initial(nutrition) && energy < 100) - nutrition = max(0, nutrition-5) - energy = min(100,energy+1) - -/mob/living/simple_animal/shadekin/update_icon() - . = ..() - - cut_overlay(tailimage) - - tailimage.icon_state = icon_state - - add_overlay(tailimage) - add_overlay(eye_icon_state) - -/mob/living/simple_animal/shadekin/Stat() - . = ..() - if(statpanel("Shadekin")) - abilities_stat() - -/mob/living/simple_animal/shadekin/proc/abilities_stat() - for(var/A in shadekin_abilities) - var/obj/effect/shadekin_ability/ability = A - stat("[ability.ability_name]",ability.atom_button_text()) - -//They phase back to the dark when killed -/mob/living/simple_animal/shadekin/death(gibbed, deathmessage = "phases to somewhere far away!") - overlays = list() - icon_state = "" - flick("tp_out",src) - spawn(1 SECOND) - qdel(src) //Back from whence you came! - - . = ..(FALSE, deathmessage) - -//Blue-eyes want to nom people to heal them -/mob/living/simple_animal/shadekin/Found(var/atom/A) - if(specific_targets && isliving(A)) //Healing! - var/mob/living/L = A - var/health_percent = (L.health/L.maxHealth)*100 - if(health_percent <= 50) - return A - . = ..() - -//They reach nutritional equilibrium (important for blue-eyes healbelly) -/mob/living/simple_animal/shadekin/Life() - if((. = ..())) - handle_shade() - -/mob/living/simple_animal/shadekin/proc/handle_shade() - //Shifted kin don't gain/lose energy (and save time if we're at the cap) - var/darkness = 1 - - - var/turf/T = get_turf(src) - if(!T) - dark_gains = 0 - return - - var/brightness = T.get_lumcount() //Brightness in 0.0 to 1.0 - darkness = 1-brightness //Invert - - if(ability_flags & AB_PHASE_SHIFTED) - dark_gains = 0 - else - //Heal (very) slowly in good darkness -- Adjusted for better Gameplay. save an extra few decades just to head a bit per tick. - Nylon - if(darkness >= 0.75) - adjustFireLoss(-0.9) - adjustBruteLoss(-0.8) - adjustToxLoss(-1.4) - - switch(eye_state) - //Blue has constant, steady (slow) regen and ignores darkness. - if(BLUE_EYES) - dark_gains = 0.5 - //Red has extremely tiny energy buildup in dark, none in light, and hunts for energy. - if(RED_EYES) //Kinda hoping my math isn't wrong - Nylon - dark_gains = round((darkness - 0.75) * 1.5, 0.1) - //if(darkness >= 0.75) - //dark_gains = 0.25 - //Purple eyes have moderate gains in darkness and loss in light. - if(PURPLE_EYES) - dark_gains = round((darkness - 0.5) * 2, 0.1) - //Yellow has extreme gains in darkness and loss in light. - if(YELLOW_EYES) - dark_gains = round((darkness - 0.5) * 4, 0.1) - //Similar to blues, but passive is less, and affected by dark - if(GREEN_EYES) - dark_gains = 0.25 - dark_gains += round((darkness - 0.5), 0.1) - //More able to get energy out of the dark, worse attack gains tho - if(ORANGE_EYES) - if(darkness >= 0.65) //Some adjustments - Nylon - dark_gains = 0.60 - - energy = max(0,min(initial(energy),energy + dark_gains)) - - if(energy_adminbuse) //You Mega gay for using this - Nylon - energy = 100 - - //Update turf darkness hud - if(darkhud) - switch(darkness) - if(0.80 to 1.00) - darkhud.icon_state = "dark2" - if(0.60 to 0.80) - darkhud.icon_state = "dark1" - if(0.40 to 0.60) - darkhud.icon_state = "dark" - if(0.20 to 0.40) - darkhud.icon_state = "dark-1" - if(0.00 to 0.20) - darkhud.icon_state = "dark-2" - - //Update energy storage hud - if(energyhud) - switch(energy) - if(80 to INFINITY) - energyhud.icon_state = "energy0" - if(60 to 80) - energyhud.icon_state = "energy1" - if(40 to 60) - energyhud.icon_state = "energy2" - if(20 to 40) - energyhud.icon_state = "energy3" - if(0 to 20) - energyhud.icon_state = "energy4" - -//Friendly ones wander towards people, maybe shy-ly if they are set to shy -//This will be removed later when shadekins get ported over to a human species template but we'll keep this code just in case for reference. - Nylon -/mob/living/simple_animal/shadekin/handle_wander_movement() - if(isturf(src.loc) && !resting && !buckled && canmove) - lifes_since_move++ - if(lifes_since_move >= turns_per_move) - if(!(stop_when_pulled && pulledby)) - var/moving_to - - if(stalker) - //Sniff sniff. - var/list/humans = human_mobs(world.view) - - //Can we see the last person we were following? - if(henlo_human && !(henlo_human in humans)) - henlo_human = null - - //Can we find a new person to follow? - if(!henlo_human) - while(!henlo_human && humans.len) - henlo_human = pick(humans) - if(!isturf(henlo_human.loc)) - humans -= henlo_human - henlo_human = null - - //Boopable hunam? - if(henlo_human) - moving_to = get_dir(src,henlo_human) - - if((get_dist(src,henlo_human) <= 1)) - dir = moving_to - if(prob(speak_chance)) - visible_message("\The [src] [pick(friendly)] \the [henlo_human].") - shy_approach = FALSE //ACCLIMATED - lifes_since_move = 0 - return //No need to move - - if(shy_approach) - var/them_to_us = turn(moving_to,180) - if(abs(dir2angle(henlo_human.dir) - dir2angle(them_to_us)) <= 90) - dir = them_to_us - return //AAA! - - dir = moving_to - - //Random walk - if(!moving_to) - moving_to = pick(GLOB.cardinal) - dir = moving_to - - var/turf/T = get_step(src,moving_to) - if(avoid_turf(T)) - return - Move(T) - lifes_since_move = 0 - -/mob/living/simple_animal/shadekin/speech_bubble_appearance() - return "ghost" - -/mob/living/simple_animal/shadekin/DoPunch(var/atom/A) - . = ..(A) - if(isliving(A)) //We punched something! - var/mob/living/L = A - if(L.stat != DEAD) - var/gains = 0 - switch(eye_state) //Some adjustments, Originals will be commented - Nylon - if(RED_EYES) - gains = 9 //8 - if(BLUE_EYES) - gains = 1 //Blue eyes are meant to be limp wristed sluts :> - if(PURPLE_EYES) - gains = 5 //4 - if(YELLOW_EYES) - gains = 4 //4 - if(GREEN_EYES) - gains = 2 //1 - if(ORANGE_EYES) - gains = 6 //5 - - energy += gains - -//Special hud elements for darkness and energy gains -/mob/living/simple_animal/shadekin/extra_huds(var/datum/hud/hud,var/icon/ui_style,var/list/hud_elements) - //Darkness hud - darkhud = new /atom/movable/screen() - darkhud.icon = ui_style - darkhud.icon_state = "dark" - darkhud.name = "darkness" - darkhud.screen_loc = "CENTER-2:16,SOUTH:5" //Left of the left hand - darkhud.alpha = 150 - hud_elements |= darkhud - - //Energy hud - energyhud = new /atom/movable/screen() - energyhud.icon = ui_style - energyhud.icon_state = "energy0" - energyhud.name = "energy" - energyhud.screen_loc = "CENTER+1:16,SOUTH:5" //Right of the right hand - energyhud.alpha = 150 - hud_elements |= energyhud - -// When someone clicks us with an empty hand -/mob/living/simple_animal/shadekin/attack_hand(mob/living/carbon/human/M as mob) - . = ..() - if(M.a_intent == INTENT_HELP) - shy_approach = FALSE //ACCLIMATED - -/datum/language/shadekin - name = "Shadekin Empathy" - desc = "Shadekin seem to always know what the others are thinking. This is probably why." - colour = "changeling" - speech_verb = "mars" - ask_verb = "mars" - exclaim_verb = "mars" - key = "m" - machine_understands = 0 - flags = RESTRICTED | HIVEMIND diff --git a/nano/templates/atmos_control.tmpl b/nano/templates/atmos_control.tmpl deleted file mode 100644 index bb9846f490d..00000000000 --- a/nano/templates/atmos_control.tmpl +++ /dev/null @@ -1,10 +0,0 @@ -{{if data.map_levels.length}} -
- {{:helper.link('Show Air Alarms On Map', 'pin-s', {'showMap' : 1})}} -
-{{/if}} -
- {{for data.alarms}} - {{:helper.link(value.name, null, {'alarm' : value.ref}, null, value.danger == 2 ? 'redButton' : (value.danger == 1 ? 'yellowButton' : null))}} - {{/for}} -
diff --git a/nano/templates/atmos_control_map_content.tmpl b/nano/templates/atmos_control_map_content.tmpl deleted file mode 100644 index 931ddb974ce..00000000000 --- a/nano/templates/atmos_control_map_content.tmpl +++ /dev/null @@ -1,13 +0,0 @@ - -{{for data.alarms}} - {{if value.z == config.mapZLevel}} -
- -
- {{/if}} -{{/for}} diff --git a/nano/templates/atmos_control_map_header.tmpl b/nano/templates/atmos_control_map_header.tmpl deleted file mode 100644 index 7fea71a965f..00000000000 --- a/nano/templates/atmos_control_map_header.tmpl +++ /dev/null @@ -1,20 +0,0 @@ - -{{:helper.link('Show List', 'script', {'showMap' : 0})}} -{{if data.map_levels.length > 1}} -
- Z Level:  - {{for data.map_levels }} - {{:helper.link(value, null, {'mapZLevel' : value}, null, config.mapZLevel == value ? 'selected' : null)}} - {{/for}} -
-{{/if}} -
- Zoom Level:  - - - - -
diff --git a/tgui/.eslintrc.yml b/tgui/.eslintrc.yml index 24f6975e688..daf5f9936e9 100644 --- a/tgui/.eslintrc.yml +++ b/tgui/.eslintrc.yml @@ -376,6 +376,7 @@ rules: ignoreUrls: true, ignoreRegExpLiterals: true, ignoreStrings: true, + ignoreTemplateLiterals: true, }] ## Enforce a maximum number of lines per file # max-lines: error diff --git a/tgui/docs/tgui-for-custom-html-popups.md b/tgui/docs/tgui-for-custom-html-popups.md index c93bbc2de80..5fe0e8ff78c 100644 --- a/tgui/docs/tgui-for-custom-html-popups.md +++ b/tgui/docs/tgui-for-custom-html-popups.md @@ -261,3 +261,21 @@ Byond.winget(null, 'url').then((serverUrl) => { Byond.command('.quit'); }); ``` + +## Strict Mode + +Strict mode is a flag that you can set on tgui window. + +```dm +window.initialize(strict_mode = TRUE) +``` + +If `TRUE`, unhandled errors and common mistakes result in a blue screen of death with a stack trace of the error, which you can use to debug it. Bluescreened window stops handling incoming messages and closes the active instance of tgui datum if there was one, to avoid a massive spam of errors and help to deal with them one by one. + +It can be defined in `window.initialize()` in DM, as shown above, or changed in runtime at runtime via `Byond.strictMode` to `true` or `false`. + +```js +Byond.strictMode = true; +``` + +It is recommended that you keep this **ON** to detect hard to find bugs. diff --git a/tgui/global.d.ts b/tgui/global.d.ts index 73660d5b76e..c2e7b5ad00b 100644 --- a/tgui/global.d.ts +++ b/tgui/global.d.ts @@ -71,6 +71,18 @@ type ByondType = { */ IS_LTE_IE11: boolean; + /** + * If `true`, unhandled errors and common mistakes result in a blue screen + * of death, which stops this window from handling incoming messages and + * closes the active instance of tgui datum if there was one. + * + * It can be defined in window.initialize() in DM, or changed in runtime + * here via this property to `true` or `false`. + * + * It is recommended that you keep this ON to detect hard to find bugs. + */ + strictMode: boolean; + /** * Makes a BYOND call. * diff --git a/tgui/packages/tgui-bench/package.json b/tgui/packages/tgui-bench/package.json index 5dd7fd337c1..55458d7fa99 100644 --- a/tgui/packages/tgui-bench/package.json +++ b/tgui/packages/tgui-bench/package.json @@ -5,7 +5,7 @@ "dependencies": { "common": "workspace:*", "fastify": "^3.20.2", - "fastify-static": "^4.4.1", + "fastify-static": "^4.2.3", "inferno": "^7.4.8", "inferno-vnode-flags": "^7.4.8", "lodash": "^4.17.21", diff --git a/tgui/packages/tgui-dev-server/package.json b/tgui/packages/tgui-dev-server/package.json index 03d1559082e..8ee0e019ee0 100644 --- a/tgui/packages/tgui-dev-server/package.json +++ b/tgui/packages/tgui-dev-server/package.json @@ -4,7 +4,7 @@ "version": "4.3.0", "type": "module", "dependencies": { - "axios": "^0.21.2", + "axios": "^0.21.1", "glob": "^7.1.7", "source-map": "^0.7.3", "stacktrace-parser": "^0.1.10", diff --git a/tgui/packages/tgui/interfaces/APC.js b/tgui/packages/tgui/interfaces/APC.js index f25fbe42aa5..4ec2f72c649 100644 --- a/tgui/packages/tgui/interfaces/APC.js +++ b/tgui/packages/tgui/interfaces/APC.js @@ -1,28 +1,16 @@ -import { Fragment } from 'inferno'; import { useBackend } from '../backend'; -import { Box, Button, Dimmer, Icon, LabeledList, ProgressBar, Section } from '../components'; +import { Box, Button, Dimmer, Icon, LabeledList, NoticeBox, ProgressBar, Section } from '../components'; import { Window } from '../layouts'; import { InterfaceLockNoticeBox } from './common/InterfaceLockNoticeBox'; import { FullscreenNotice } from './common/FullscreenNotice'; export const APC = (props, context) => { - const { act, data } = useBackend(context); - - let body = ; - - if (data.gridCheck) { - body = ; - } else if (data.failTime) { - body = ; - } - return ( + height={445}> - {body} + ); @@ -52,21 +40,21 @@ const malfMap = { content: 'Override Programming', action: 'hack', }, - // 2: { - // icon: 'caret-square-down', - // content: 'Shunt Core Process', - // action: 'occupy', - // }, - // 3: { - // icon: 'caret-square-left', - // content: 'Return to Main Core', - // action: 'deoccupy', - // }, - // 4: { - // icon: 'caret-square-down', - // content: 'Shunt Core Process', - // action: 'occupy', - // }, + 2: { + icon: 'caret-square-down', + content: 'Shunt Core Process', + action: 'occupy', + }, + 3: { + icon: 'caret-square-left', + content: 'Return to Main Core', + action: 'deoccupy', + }, + 4: { + icon: 'caret-square-down', + content: 'Shunt Core Process', + action: 'occupy', + }, }; const ApcContent = (props, context) => { @@ -78,16 +66,32 @@ const ApcContent = (props, context) => { const chargingStatus = powerStatusMap[data.chargingStatus] || powerStatusMap[0]; const channelArray = data.powerChannels || []; - // const malfStatus = malfMap[data.malfStatus] || null; + const malfStatus = malfMap[data.malfStatus] || malfMap[0]; const adjustedCellChange = data.powerCellStatus / 100; - + if (data.failTime > 0) { + return ( + +

SYSTEM FAILURE

+ + I/O regulators malfunction detected! + Waiting for system reboot... + +
+ Automatic reboot in {data.failTime} seconds... +