diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 2c6d5d072dece..915dda724d53c 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -125,7 +125,8 @@
# tralezab
/code/__DEFINES/basic_mobs.dm @tralezab
-/code/datums/ai @tralezab
+/code/datums/ai/ @tralezab
+/code/modules/religion/ @tralezab
# Watermelon914
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 4704e8c20ed33..904de8b36c9a9 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -3,6 +3,11 @@ name: Bug report
about: Create a report to help reproduce and fix the issue
---
+
+## Issue Summary
+
+
+
## Round ID:
[span_name("[signal.format_target()]")]: \"[signal.format_message()]\"")
+ var/ghost_message = span_game_say("[span_name("[source]")] [rigged ? "(as [span_name(fake_name)]) Rigged " : ""]PDA Message --> [span_name("[signal.format_target()]")]: \"[signal.format_message()]\"")
var/list/message_listeners = GLOB.dead_player_list + GLOB.current_observers_list
for(var/mob/listener as anything in message_listeners)
if(!(get_chat_toggles(listener) & CHAT_GHOSTPDA))
continue
- to_chat(listener, "[FOLLOW_LINK(listener, sender)] [ghost_message]")
+ to_chat(listener, "[FOLLOW_LINK(listener, source)] [ghost_message]")
- to_chat(sender, span_info("PDA message sent to [signal.format_target()]: \"[message]\""))
+ if(sender)
+ to_chat(sender, span_info("PDA message sent to [signal.format_target()]: \"[message]\""))
if (alert_able && !alert_silenced)
computer.send_sound()
COOLDOWN_START(src, last_text, 1 SECONDS)
+ SEND_SIGNAL(computer, COMSIG_MODULAR_PDA_MESSAGE_SENT, source, signal)
+
selected_image = null
return TRUE
@@ -637,6 +662,7 @@
var/sender_ref = signal.data["ref"]
+
// don't create a new chat for rigged messages, make it a one off notif
if(!is_rigged)
var/datum/pda_message/message = new(signal.data["message"], FALSE, station_time_timestamp(PDA_MESSAGE_TIMESTAMP_FORMAT), signal.data["photo"], signal.data["everyone"])
@@ -657,6 +683,14 @@
if(computer.loc && isliving(computer.loc))
receievers += computer.loc
+ // resolving w/o nullcheck here, assume the messenger exists if a real person sent a message
+ var/datum/computer_file/program/messenger/sender_messenger = chat.recipient?.resolve()
+
+ var/sender_title = is_fake_user ? STRINGIFY_PDA_TARGET(fake_name, fake_job) : get_messenger_name(sender_messenger)
+ var/sender_name = is_fake_user ? fake_name : sender_messenger.computer.saved_identification
+
+ SEND_SIGNAL(computer, COMSIG_MODULAR_PDA_MESSAGE_RECEIVED, signal, fake_job || sender_messenger?.computer.saved_job , sender_name)
+
for(var/mob/living/messaged_mob as anything in receievers)
if(messaged_mob.stat >= UNCONSCIOUS)
continue
@@ -670,11 +704,6 @@
else
reply = "(Reply)"
- // resolving w/o nullcheck here, assume the messenger exists if a real person sent a message
- var/datum/computer_file/program/messenger/sender_messenger = chat.recipient?.resolve()
-
- var/sender_title = is_fake_user ? STRINGIFY_PDA_TARGET(fake_name, fake_job) : get_messenger_name(sender_messenger)
- var/sender_name = is_fake_user ? fake_name : sender_messenger.computer.saved_identification
if (isAI(messaged_mob))
sender_title = "[sender_title]"
diff --git a/code/modules/modular_computers/file_system/programs/newscasterapp.dm b/code/modules/modular_computers/file_system/programs/newscasterapp.dm
index 47e4f65d48f01..ed1c440f411cd 100644
--- a/code/modules/modular_computers/file_system/programs/newscasterapp.dm
+++ b/code/modules/modular_computers/file_system/programs/newscasterapp.dm
@@ -1,13 +1,12 @@
/datum/computer_file/program/newscaster
filename = "newscasterapp"
filedesc = "Newscaster"
- transfer_access = list(ACCESS_LIBRARY)
- category = PROGRAM_CATEGORY_CREW
- program_icon_state = "bountyboard"
+ download_access = list(ACCESS_LIBRARY)
+ downloader_category = PROGRAM_CATEGORY_GAMES
+ program_open_overlay = "bountyboard"
extended_desc = "This program allows any user to access the Newscaster network from anywhere."
size = 2
- requires_ntnet = TRUE
- available_on_ntnet = TRUE
+ program_flags = PROGRAM_ON_NTNET_STORE | PROGRAM_REQUIRES_NTNET
tgui_id = "NtosNewscaster"
program_icon = "newspaper"
///The UI we use for the newscaster
@@ -28,4 +27,5 @@
return newscaster_ui.ui_static_data(user)
/datum/computer_file/program/newscaster/ui_act(action, params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
return newscaster_ui.ui_act(action, params, ui, state)
diff --git a/code/modules/modular_computers/file_system/programs/notepad.dm b/code/modules/modular_computers/file_system/programs/notepad.dm
index 01afaa08c19e0..95def6f8e9643 100644
--- a/code/modules/modular_computers/file_system/programs/notepad.dm
+++ b/code/modules/modular_computers/file_system/programs/notepad.dm
@@ -1,13 +1,14 @@
/datum/computer_file/program/notepad
filename = "notepad"
filedesc = "Notepad"
- category = PROGRAM_CATEGORY_MISC
- program_icon_state = "generic"
+ downloader_category = PROGRAM_CATEGORY_DEVICE
+ program_open_overlay = "generic"
extended_desc = "Jot down your work-safe thoughts and what not."
size = 2
tgui_id = "NtosNotepad"
program_icon = "book"
- usage_flags = PROGRAM_TABLET
+ can_run_on_flags = PROGRAM_ALL
+ circuit_comp_type = /obj/item/circuit_component/mod_program/notepad
var/written_note = "Congratulations on your station upgrading to the new NtOS and Thinktronic based collaboration effort, \
bringing you the best in electronics and software since 2467!\n\
@@ -19,7 +20,8 @@
Quarter - Either sides of Aft\n\
Bow - Either sides of Fore"
-/datum/computer_file/program/notepad/ui_act(action, list/params, datum/tgui/ui)
+/datum/computer_file/program/notepad/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
switch(action)
if("UpdateNote")
written_note = params["newnote"]
@@ -31,3 +33,39 @@
data["note"] = written_note
return data
+
+/obj/item/circuit_component/mod_program/notepad
+ associated_program = /datum/computer_file/program/notepad
+ ///When the input is received, the written note will be set to its value.
+ var/datum/port/input/set_text
+ ///The written note output, sent everytime notes are updated.
+ var/datum/port/output/updated_text
+ ///Pinged whenever the text is updated
+ var/datum/port/output/updated
+
+/obj/item/circuit_component/mod_program/notepad/populate_ports()
+ . = ..()
+ set_text = add_input_port("Set Notes", PORT_TYPE_STRING)
+ updated_text = add_output_port("Notes", PORT_TYPE_STRING)
+ updated = add_output_port("Updated", PORT_TYPE_SIGNAL)
+
+/obj/item/circuit_component/mod_program/notepad/register_shell(atom/movable/shell)
+ . = ..()
+ RegisterSignal(associated_program, COMSIG_UI_ACT, PROC_REF(on_note_updated))
+
+/obj/item/circuit_component/mod_program/notepad/unregister_shell()
+ UnregisterSignal(associated_program, COMSIG_UI_ACT)
+ return ..()
+
+/obj/item/circuit_component/mod_program/notepad/proc/on_note_updated(datum/source, mob/user, action, list/params)
+ SIGNAL_HANDLER
+ if(action == "UpdateNote")
+ updated_text.set_output(params["newnote"])
+ updated.set_output(COMPONENT_SIGNAL)
+
+/obj/item/circuit_component/mod_program/notepad/input_received(datum/port/port)
+ var/datum/computer_file/program/notepad/pad = associated_program
+ pad.written_note = set_text.value
+ SStgui.update_uis(pad.computer)
+ updated_text.set_output(pad.written_note)
+ updated.set_output(COMPONENT_SIGNAL)
diff --git a/code/modules/modular_computers/file_system/programs/nt_pay.dm b/code/modules/modular_computers/file_system/programs/nt_pay.dm
index 8724375d07bba..4e3fa5d3fb718 100644
--- a/code/modules/modular_computers/file_system/programs/nt_pay.dm
+++ b/code/modules/modular_computers/file_system/programs/nt_pay.dm
@@ -1,52 +1,33 @@
+#define NT_PAY_STATUS_NO_ACCOUNT 0
+#define NT_PAY_STATUS_DEPT_ACCOUNT 1
+#define NT_PAY_STATUS_INVALID_TOKEN 2
+#define NT_PAY_SATUS_SENDER_IS_RECEIVER 3
+#define NT_PAY_STATUS_INVALID_MONEY 4
+#define NT_PAY_STATUS_SUCCESS 5
+
/datum/computer_file/program/nt_pay
filename = "ntpay"
filedesc = "Nanotrasen Pay System"
- category = PROGRAM_CATEGORY_MISC
- program_icon_state = "generic"
+ downloader_category = PROGRAM_CATEGORY_DEVICE
+ program_open_overlay = "generic"
extended_desc = "An application that locally (in your sector) helps to transfer money or track your expenses and profits."
size = 2
tgui_id = "NtosPay"
program_icon = "money-bill-wave"
- usage_flags = PROGRAM_ALL
+ can_run_on_flags = PROGRAM_ALL
+ circuit_comp_type = /obj/item/circuit_component/mod_program/nt_pay
///Reference to the currently logged in user.
var/datum/bank_account/current_user
- ///Pay token, by which we can send credits
- var/token
- ///Amount of credits, which we sends
- var/money_to_send = 0
///Pay token what we want to find
var/wanted_token
-/datum/computer_file/program/nt_pay/ui_act(action, list/params, datum/tgui/ui)
+/datum/computer_file/program/nt_pay/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
switch(action)
if("Transaction")
- if(IS_DEPARTMENTAL_ACCOUNT(current_user))
- return to_chat(usr, span_notice("The app is unable to withdraw from that card."))
-
- token = params["token"]
- money_to_send = params["amount"]
- var/datum/bank_account/recipient
- if(!token)
- return to_chat(usr, span_notice("You need to enter your transfer target's pay token."))
- if(!money_to_send)
- return to_chat(usr, span_notice("You need to specify how much you're sending."))
- if(token == current_user.pay_token)
- return to_chat(usr, span_notice("You can't send credits to yourself."))
-
- for(var/account as anything in SSeconomy.bank_accounts_by_id)
- var/datum/bank_account/acc = SSeconomy.bank_accounts_by_id[account]
- if(acc.pay_token == token)
- recipient = acc
- break
-
- if(!recipient)
- return to_chat(usr, span_notice("The app can't find who you're trying to pay. Did you enter the pay token right?"))
- if(!current_user.has_money(money_to_send) || money_to_send < 1)
- return current_user.bank_card_talk("You cannot afford it.")
-
- recipient.bank_card_talk("You received [money_to_send] credit(s). Reason: transfer from [current_user.account_holder]")
- recipient.transfer_money(current_user, money_to_send)
- current_user.bank_card_talk("You send [money_to_send] credit(s) to [recipient.account_holder]. Now you have [current_user.account_balance] credit(s)")
+ var/token = params["token"]
+ var/money_to_send = params["amount"]
+ make_payment(token, money_to_send, usr)
if("GetPayToken")
wanted_token = null
@@ -58,8 +39,6 @@
if(!wanted_token)
return wanted_token = "Account \"[params["wanted_name"]]\" not found."
-
-
/datum/computer_file/program/nt_pay/ui_data(mob/user)
var/list/data = list()
@@ -74,3 +53,136 @@
data["transaction_list"] = current_user.transaction_history
return data
+
+///Wrapper and signal for the main payment function of this program
+/datum/computer_file/program/nt_pay/proc/make_payment(token, money_to_send, mob/user)
+ var/payment_result = _pay(token, money_to_send, user)
+ SEND_SIGNAL(computer, COMSIG_MODULAR_COMPUTER_NT_PAY_RESULT, payment_result)
+
+/datum/computer_file/program/nt_pay/proc/_pay(token, money_to_send, mob/user)
+ money_to_send = round(money_to_send)
+
+ if(IS_DEPARTMENTAL_ACCOUNT(current_user))
+ if(user)
+ to_chat(user, span_notice("The app is unable to withdraw from that card."))
+ return NT_PAY_STATUS_DEPT_ACCOUNT
+
+ var/datum/bank_account/recipient
+ if(!token)
+ if(user)
+ to_chat(user, span_notice("You need to enter your transfer target's pay token."))
+ return NT_PAY_STATUS_INVALID_TOKEN
+ if(money_to_send <= 0)
+ if(user)
+ to_chat(user, span_notice("You need to specify how much you're sending."))
+ return NT_PAY_STATUS_INVALID_MONEY
+ if(token == current_user.pay_token)
+ if(user)
+ to_chat(user, span_notice("You can't send credits to yourself."))
+ return NT_PAY_SATUS_SENDER_IS_RECEIVER
+
+ for(var/account as anything in SSeconomy.bank_accounts_by_id)
+ var/datum/bank_account/acc = SSeconomy.bank_accounts_by_id[account]
+ if(acc.pay_token == token)
+ recipient = acc
+ break
+
+ if(!recipient)
+ if(user)
+ to_chat(user, span_notice("The app can't find who you're trying to pay. Did you enter the pay token right?"))
+ return NT_PAY_STATUS_INVALID_TOKEN
+ if(!current_user.has_money(money_to_send) || money_to_send < 1)
+ current_user.bank_card_talk("You cannot afford it.")
+ return NT_PAY_STATUS_INVALID_MONEY
+
+ recipient.bank_card_talk("You received [money_to_send] credit(s). Reason: transfer from [current_user.account_holder]")
+ recipient.transfer_money(current_user, money_to_send)
+ for(var/obj/item/card/id/id_card as anything in recipient.bank_cards)
+ SEND_SIGNAL(id_card, COMSIG_ID_CARD_NTPAY_MONEY_RECEIVED, computer, money_to_send)
+
+ current_user.bank_card_talk("You send [money_to_send] credit(s) to [recipient.account_holder]. Now you have [current_user.account_balance] credit(s)")
+
+ return NT_PAY_STATUS_SUCCESS
+
+
+/obj/item/circuit_component/mod_program/nt_pay
+ associated_program = /datum/computer_file/program/nt_pay
+ circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL
+
+ ///Circuit variables. This one is for the token we want to pay
+ var/datum/port/input/token_port
+ ///The port for the money to send
+ var/datum/port/input/money_port
+ ///Let's us know if the payment has gone through or not.
+ var/datum/port/output/payment_status
+ ///The device from which the payment was received
+ var/datum/port/output/payment_device
+ ///Amount of a received payment
+ var/datum/port/output/payment_amount
+ ///Pinged whether a payment is received
+ var/datum/port/output/payment_received
+
+/obj/item/circuit_component/mod_program/nt_pay/register_shell(atom/movable/shell)
+ . = ..()
+ var/obj/item/modular_computer/modpc = associated_program.computer
+ RegisterSignal(modpc, COMSIG_MODULAR_COMPUTER_NT_PAY_RESULT, PROC_REF(on_payment_done))
+ RegisterSignal(modpc, COMSIG_MODULAR_COMPUTER_INSERTED_ID, PROC_REF(register_id))
+ if(modpc.computer_id_slot)
+ register_id(inserted_id = modpc.computer_id_slot)
+
+/obj/item/circuit_component/mod_program/nt_pay/unregister_shell()
+ var/obj/item/modular_computer/modpc = associated_program.computer
+ UnregisterSignal(modpc, list(COMSIG_MODULAR_COMPUTER_NT_PAY_RESULT, COMSIG_MODULAR_COMPUTER_INSERTED_ID))
+ if(modpc.computer_id_slot)
+ UnregisterSignal(modpc.computer_id_slot, list(COMSIG_ID_CARD_NTPAY_MONEY_RECEIVED, COMSIG_MOVABLE_MOVED))
+ return ..()
+
+/obj/item/circuit_component/mod_program/nt_pay/proc/register_id(datum/source, obj/item/card/inserted_id, mob/user)
+ SIGNAL_HANDLER
+ RegisterSignal(inserted_id, COMSIG_ID_CARD_NTPAY_MONEY_RECEIVED, PROC_REF(on_payment_received))
+ RegisterSignal(inserted_id, COMSIG_MOVABLE_MOVED, PROC_REF(unregister_id))
+
+/obj/item/circuit_component/mod_program/nt_pay/proc/unregister_id(obj/item/card/gone)
+ SIGNAL_HANDLER
+ UnregisterSignal(gone, list(COMSIG_ID_CARD_NTPAY_MONEY_RECEIVED, COMSIG_MOVABLE_MOVED))
+
+/obj/item/circuit_component/mod_program/nt_pay/populate_ports()
+ . = ..()
+ token_port = add_input_port("Token", PORT_TYPE_STRING)
+ money_port = add_input_port("Amount", PORT_TYPE_NUMBER)
+ payment_status = add_output_port("Status", PORT_TYPE_NUMBER)
+ payment_device = add_output_port("Payment Sender", PORT_TYPE_ATOM)
+ payment_amount = add_output_port("Received Amount", PORT_TYPE_NUMBER)
+ payment_received = add_output_port("Received Payment", PORT_TYPE_SIGNAL)
+
+/obj/item/circuit_component/mod_program/nt_pay/get_ui_notices()
+ . = ..()
+ . += create_ui_notice("Outputs require inserted ID", "orange")
+ . += create_ui_notice("NT-Pay Statuses:")
+ . += create_ui_notice("Success - [NT_PAY_STATUS_SUCCESS]", "green")
+ . += create_ui_notice("Fail (No Account) - [NT_PAY_STATUS_NO_ACCOUNT]", "red")
+ . += create_ui_notice("Fail (Dept Account) - [NT_PAY_STATUS_DEPT_ACCOUNT]", "red")
+ . += create_ui_notice("Fail (Invalid Token) - [NT_PAY_STATUS_INVALID_TOKEN]", "red")
+ . += create_ui_notice("Fail (Sender = Receiver) - [NT_PAY_SATUS_SENDER_IS_RECEIVER]", "red")
+ . += create_ui_notice("Fail (Invalid Amount) - [NT_PAY_STATUS_INVALID_MONEY]", "red")
+
+/obj/item/circuit_component/mod_program/nt_pay/input_received(datum/port/port)
+ var/datum/computer_file/program/nt_pay/program = associated_program
+ program.make_payment(token_port.value, money_port.value)
+
+/obj/item/circuit_component/mod_program/nt_pay/proc/on_payment_done(datum/source, payment_result)
+ SIGNAL_HANDLER
+ payment_status.set_output(payment_result)
+
+/obj/item/circuit_component/mod_program/nt_pay/proc/on_payment_received(datum/source, obj/item/modular_computer/computer, money_received)
+ SIGNAL_HANDLER
+ payment_device.set_output(computer)
+ payment_amount.set_output(money_received)
+ payment_received.set_output(COMPONENT_SIGNAL)
+
+#undef NT_PAY_STATUS_NO_ACCOUNT
+#undef NT_PAY_STATUS_DEPT_ACCOUNT
+#undef NT_PAY_STATUS_INVALID_TOKEN
+#undef NT_PAY_SATUS_SENDER_IS_RECEIVER
+#undef NT_PAY_STATUS_INVALID_MONEY
+#undef NT_PAY_STATUS_SUCCESS
diff --git a/code/modules/modular_computers/file_system/programs/ntdownloader.dm b/code/modules/modular_computers/file_system/programs/ntdownloader.dm
index c9723d905b5f1..3fbc7843f1883 100644
--- a/code/modules/modular_computers/file_system/programs/ntdownloader.dm
+++ b/code/modules/modular_computers/file_system/programs/ntdownloader.dm
@@ -1,32 +1,39 @@
/datum/computer_file/program/ntnetdownload
filename = "ntsoftwarehub"
filedesc = "NT Software Hub"
- program_icon_state = "generic"
+ program_open_overlay = "generic"
extended_desc = "This program allows downloads of software from official NT repositories"
undeletable = TRUE
size = 4
- requires_ntnet = TRUE
- available_on_ntnet = FALSE
+ program_flags = PROGRAM_REQUIRES_NTNET
tgui_id = "NtosNetDownloader"
program_icon = "download"
+ ///The program currently being downloaded.
var/datum/computer_file/program/downloaded_file
+ ///Boolean on whether the `downloaded_file` is being downloaded from the Syndicate store,
+ ///in which case it will appear as 'ENCRYPTED' in logs, rather than display file name.
var/hacked_download = FALSE
- var/download_completion = FALSE //GQ of downloaded data.
- var/download_netspeed = 0
- var/downloaderror = ""
+ ///How much of the data has been downloaded.
+ var/download_completion
+ ///The error message being displayed to the user, if necessary. Null if there isn't one.
+ var/downloaderror
+ ///The list of categories to display in the UI, in order of which they appear.
var/static/list/show_categories = list(
- PROGRAM_CATEGORY_CREW,
- PROGRAM_CATEGORY_ENGI,
- PROGRAM_CATEGORY_SCI,
- PROGRAM_CATEGORY_SUPL,
- PROGRAM_CATEGORY_MISC,
+ PROGRAM_CATEGORY_DEVICE,
+ PROGRAM_CATEGORY_EQUIPMENT,
+ PROGRAM_CATEGORY_GAMES,
+ PROGRAM_CATEGORY_SECURITY,
+ PROGRAM_CATEGORY_ENGINEERING,
+ PROGRAM_CATEGORY_SUPPLY,
+ PROGRAM_CATEGORY_SCIENCE,
)
/datum/computer_file/program/ntnetdownload/kill_program(mob/user)
- . = ..()
+ abort_file_download()
ui_header = null
+ . = ..()
/datum/computer_file/program/ntnetdownload/proc/begin_file_download(filename)
if(downloaded_file)
@@ -38,7 +45,7 @@
return FALSE
// Attempting to download antag only program, but without having emagged/syndicate computer. No.
- if(PRG.available_on_syndinet && !(computer.obj_flags & EMAGGED))
+ if((PRG.program_flags & PROGRAM_ON_SYNDINET_STORE) && !(computer.obj_flags & EMAGGED))
return FALSE
if(!computer || !computer.can_store_file(PRG))
@@ -83,7 +90,7 @@
if(download_completion >= downloaded_file.size)
complete_file_download()
// Download speed according to connectivity state. NTNet server is assumed to be on unlimited speed so we're limited by our local connectivity
- download_netspeed = 0
+ var/download_netspeed
// Speed defines are found in misc.dm
switch(ntnet_status)
if(NTNET_LOW_SIGNAL)
@@ -92,9 +99,13 @@
download_netspeed = NTNETSPEED_HIGHSIGNAL
if(NTNET_ETHERNET_SIGNAL)
download_netspeed = NTNETSPEED_ETHERNET
- download_completion += download_netspeed
+ if(download_netspeed)
+ if(HAS_TRAIT(computer, TRAIT_MODPC_HALVED_DOWNLOAD_SPEED))
+ download_netspeed *= 0.5
+ download_completion += download_netspeed
/datum/computer_file/program/ntnetdownload/ui_act(action, params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
switch(action)
if("PRG_downloadfile")
if(!downloaded_file)
@@ -103,7 +114,6 @@
if("PRG_reseterror")
if(downloaderror)
download_completion = FALSE
- download_netspeed = FALSE
downloaded_file = null
downloaderror = ""
return TRUE
@@ -121,7 +131,6 @@
data["downloadname"] = downloaded_file.filename
data["downloaddesc"] = downloaded_file.filedesc
data["downloadsize"] = downloaded_file.size
- data["downloadspeed"] = download_netspeed
data["downloadcompletion"] = round(download_completion, 0.1)
data["disk_size"] = computer.max_capacity
@@ -129,35 +138,27 @@
data["emagged"] = (computer.obj_flags & EMAGGED)
var/list/repo = SSmodular_computers.available_antag_software | SSmodular_computers.available_station_software
- var/list/program_categories = list()
for(var/datum/computer_file/program/programs as anything in repo)
- if(!(programs.category in program_categories))
- program_categories.Add(programs.category)
data["programs"] += list(list(
"icon" = programs.program_icon,
"filename" = programs.filename,
"filedesc" = programs.filedesc,
"fileinfo" = programs.extended_desc,
- "category" = programs.category,
+ "category" = programs.downloader_category,
"installed" = !!computer.find_file_by_name(programs.filename),
"compatible" = check_compatibility(programs),
"size" = programs.size,
- "access" = programs.can_run(user, transfer = TRUE, access = access),
- "verifiedsource" = programs.available_on_ntnet,
+ "access" = programs.can_run(user, downloading = TRUE, access = access),
+ "verifiedsource" = !!(programs.program_flags & PROGRAM_ON_NTNET_STORE),
))
- data["categories"] = show_categories & program_categories
+ data["categories"] = show_categories
return data
-/datum/computer_file/program/ntnetdownload/proc/check_compatibility(datum/computer_file/program/P)
- var/hardflag = computer.hardware_flag
-
- if(P?.is_supported_by_hardware(hardware_flag = hardflag, loud = FALSE))
- return TRUE
- return FALSE
-
-/datum/computer_file/program/ntnetdownload/kill_program(mob/user)
- abort_file_download()
- return ..()
+///Checks if a provided `program_to_check` is compatible to be downloaded on our computer.
+/datum/computer_file/program/ntnetdownload/proc/check_compatibility(datum/computer_file/program/program_to_check)
+ if(!program_to_check || !program_to_check.is_supported_by_hardware(hardware_flag = computer.hardware_flag, loud = FALSE))
+ return FALSE
+ return TRUE
diff --git a/code/modules/modular_computers/file_system/programs/portrait_printer.dm b/code/modules/modular_computers/file_system/programs/portrait_printer.dm
index 68c94e87e8d32..0e69dd4969da7 100644
--- a/code/modules/modular_computers/file_system/programs/portrait_printer.dm
+++ b/code/modules/modular_computers/file_system/programs/portrait_printer.dm
@@ -12,12 +12,12 @@
/datum/computer_file/program/portrait_printer
filename = "PortraitPrinter"
filedesc = "Marlowe Treeby's Art Galaxy"
- category = PROGRAM_CATEGORY_CREW
- program_icon_state = "dummy"
+ downloader_category = PROGRAM_CATEGORY_EQUIPMENT
+ program_open_overlay = "dummy"
extended_desc = "This program connects to a Spinward Sector community art site for viewing and printing art."
- transfer_access = list(ACCESS_LIBRARY)
- usage_flags = PROGRAM_CONSOLE
- requires_ntnet = TRUE
+ download_access = list(ACCESS_LIBRARY)
+ can_run_on_flags = PROGRAM_CONSOLE
+ program_flags = PROGRAM_ON_NTNET_STORE | PROGRAM_REQUIRES_NTNET
size = 9
tgui_id = "NtosPortraitPrinter"
program_icon = "paint-brush"
@@ -44,6 +44,7 @@
)
/datum/computer_file/program/portrait_printer/ui_act(action, params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
switch(action)
if("search")
if(search_string != params["to_search"])
diff --git a/code/modules/modular_computers/file_system/programs/powermonitor.dm b/code/modules/modular_computers/file_system/programs/powermonitor.dm
index e82821d75e4f2..c5a8eba952b2a 100644
--- a/code/modules/modular_computers/file_system/programs/powermonitor.dm
+++ b/code/modules/modular_computers/file_system/programs/powermonitor.dm
@@ -3,13 +3,13 @@
/datum/computer_file/program/power_monitor
filename = "ampcheck"
filedesc = "AmpCheck"
- category = PROGRAM_CATEGORY_ENGI
- program_icon_state = "power_monitor"
+ downloader_category = PROGRAM_CATEGORY_ENGINEERING
+ program_open_overlay = "power_monitor"
extended_desc = "This program connects to sensors around the station to provide information about electrical systems"
ui_header = "power_norm.gif"
- transfer_access = list(ACCESS_ENGINEERING)
- usage_flags = PROGRAM_CONSOLE
- requires_ntnet = FALSE
+ download_access = list(ACCESS_ENGINEERING)
+ can_run_on_flags = PROGRAM_CONSOLE
+ program_flags = PROGRAM_ON_NTNET_STORE | PROGRAM_REQUIRES_NTNET
size = 8
tgui_id = "NtosPowerMonitor"
program_icon = "plug"
diff --git a/code/modules/modular_computers/file_system/programs/radar.dm b/code/modules/modular_computers/file_system/programs/radar.dm
index c230614339d49..b506777f3de7a 100644
--- a/code/modules/modular_computers/file_system/programs/radar.dm
+++ b/code/modules/modular_computers/file_system/programs/radar.dm
@@ -1,12 +1,21 @@
+///The selected target is not trackable
+#define RADAR_NOT_TRACKABLE 0
+///The selected target is trackable
+#define RADAR_TRACKABLE 1
+///The selected target is trackable, even if subtypes would normally consider it untrackable.
+#define RADAR_TRACKABLE_ANYWAY 2
+
+///If the target is something it shouldn't be normally tracking, this is the maximum distance within with it an be tracked.
+#define MAX_RADAR_CIRCUIT_DISTANCE 18
+
/datum/computer_file/program/radar //generic parent that handles most of the process
filename = "genericfinder"
filedesc = "debug_finder"
- category = PROGRAM_CATEGORY_CREW
+ downloader_category = PROGRAM_CATEGORY_EQUIPMENT
ui_header = "borg_mon.gif" //DEBUG -- new icon before PR
- program_icon_state = "radarntos"
- requires_ntnet = TRUE
- available_on_ntnet = FALSE
- usage_flags = PROGRAM_LAPTOP | PROGRAM_TABLET
+ program_open_overlay = "radarntos"
+ program_flags = PROGRAM_REQUIRES_NTNET
+ can_run_on_flags = PROGRAM_LAPTOP | PROGRAM_PDA
size = 5
tgui_id = "NtosRadar"
///List of trackable entities. Updated by the scan() proc.
@@ -15,7 +24,7 @@
var/selected
///Used to store when the next scan is available.
COOLDOWN_DECLARE(next_scan)
- ///Used to keep track of the last value program_icon_state was set to, to prevent constant unnecessary update_appearance() calls
+ ///Used to keep track of the last value program_open_overlay was set to, to prevent constant unnecessary update_appearance() calls
var/last_icon_state = ""
///Used by the tgui interface, themed NT or Syndicate.
var/arrowstyle = "ntosradarpointer.png"
@@ -59,12 +68,13 @@
return data
/datum/computer_file/program/radar/ui_act(action, params, datum/tgui/ui, datum/ui_state/state)
-
+ . = ..()
switch(action)
if("selecttarget")
var/selected_new_ref = params["ref"]
if(selected_new_ref in trackable_object_refs())
selected = selected_new_ref
+ SEND_SIGNAL(computer, COMSIG_MODULAR_COMPUTER_RADAR_SELECTED, selected)
return TRUE
if("scan")
@@ -134,13 +144,22 @@
**arg1 is the atom being evaluated.
*/
/datum/computer_file/program/radar/proc/trackable(atom/movable/signal)
- if(!signal || !computer)
- return FALSE
+ SHOULD_CALL_PARENT(TRUE)
+ if(isnull(signal) || isnull(computer))
+ return RADAR_NOT_TRACKABLE
var/turf/here = get_turf(computer)
var/turf/there = get_turf(signal)
if(!here || !there)
- return FALSE //I was still getting a runtime even after the above check while scanning, so fuck it
- return (there.z == here.z) || (is_station_level(here.z) && is_station_level(there.z))
+ return RADAR_NOT_TRACKABLE //I was still getting a runtime even after the above check while scanning, so fuck it
+ if(there.z != here.z && (!is_station_level(here.z) || !is_station_level(there.z)))
+ return RADAR_NOT_TRACKABLE
+ var/trackable_signal = SEND_SIGNAL(computer, COMSIG_MODULAR_COMPUTER_RADAR_TRACKABLE, signal, here, there)
+ switch(trackable_signal)
+ if(COMPONENT_RADAR_TRACK_ANYWAY)
+ return RADAR_TRACKABLE_ANYWAY
+ if(COMPONENT_RADAR_DONT_TRACK)
+ return RADAR_NOT_TRACKABLE
+ return RADAR_TRACKABLE
/**
*
@@ -169,22 +188,25 @@
*return an atom reference.
*/
/datum/computer_file/program/radar/proc/find_atom()
- return
+ SHOULD_CALL_PARENT(TRUE)
+ var/list/atom_container = list(null)
+ SEND_SIGNAL(computer, COMSIG_MODULAR_COMPUTER_RADAR_FIND_ATOM, atom_container)
+ return atom_container[1]
//We use SSfastprocess for the program icon state because it runs faster than process_tick() does.
/datum/computer_file/program/radar/process()
if(computer.active_program != src)
- STOP_PROCESSING(SSfastprocess, src) //We're not the active program, it's time to stop.
- return
+ //We're not the active program, it's time to stop.
+ return PROCESS_KILL
if(!selected)
return
var/atom/movable/signal = find_atom()
if(!trackable(signal))
- program_icon_state = "[initial(program_icon_state)]lost"
- if(last_icon_state != program_icon_state)
+ program_open_overlay = "[initial(program_open_overlay)]lost"
+ if(last_icon_state != program_open_overlay)
computer.update_appearance()
- last_icon_state = program_icon_state
+ last_icon_state = program_open_overlay
return
var/here_turf = get_turf(computer)
@@ -192,17 +214,17 @@
var/trackdistance = get_dist_euclidian(here_turf, target_turf)
switch(trackdistance)
if(0)
- program_icon_state = "[initial(program_icon_state)]direct"
+ program_open_overlay = "[initial(program_open_overlay)]direct"
if(1 to 12)
- program_icon_state = "[initial(program_icon_state)]close"
+ program_open_overlay = "[initial(program_open_overlay)]close"
if(13 to 24)
- program_icon_state = "[initial(program_icon_state)]medium"
+ program_open_overlay = "[initial(program_open_overlay)]medium"
if(25 to INFINITY)
- program_icon_state = "[initial(program_icon_state)]far"
+ program_open_overlay = "[initial(program_open_overlay)]far"
- if(last_icon_state != program_icon_state)
+ if(last_icon_state != program_open_overlay)
computer.update_appearance()
- last_icon_state = program_icon_state
+ last_icon_state = program_open_overlay
computer.setDir(get_dir(here_turf, target_turf))
//We can use process_tick to restart fast processing, since the computer will be running this constantly either way.
@@ -219,13 +241,13 @@
filename = "lifeline"
filedesc = "Lifeline"
extended_desc = "This program allows for tracking of crew members via their suit sensors."
- requires_ntnet = TRUE
- transfer_access = list(ACCESS_MEDICAL)
- available_on_ntnet = TRUE
+ program_flags = PROGRAM_ON_NTNET_STORE | PROGRAM_REQUIRES_NTNET
+ download_access = list(ACCESS_MEDICAL)
program_icon = "heartbeat"
+ circuit_comp_type = /obj/item/circuit_component/mod_program/radar/medical
/datum/computer_file/program/radar/lifeline/find_atom()
- return locate(selected) in GLOB.human_list
+ return ..() || (locate(selected) in GLOB.human_list)
/datum/computer_file/program/radar/lifeline/scan()
objects = list()
@@ -245,29 +267,31 @@
objects += list(crewinfo)
/datum/computer_file/program/radar/lifeline/trackable(mob/living/carbon/human/humanoid)
+ . = ..()
+ if(. == RADAR_TRACKABLE_ANYWAY)
+ return RADAR_TRACKABLE_ANYWAY
if(!humanoid || !istype(humanoid))
- return FALSE
- if(..())
- if (istype(humanoid.w_uniform, /obj/item/clothing/under))
- var/obj/item/clothing/under/uniform = humanoid.w_uniform
- if(uniform.has_sensor && uniform.sensor_mode >= SENSOR_COORDS) // Suit sensors must be on maximum
- return TRUE
- return FALSE
+ return RADAR_NOT_TRACKABLE
+ if(!istype(humanoid.w_uniform, /obj/item/clothing/under))
+ return RADAR_NOT_TRACKABLE
+ var/obj/item/clothing/under/uniform = humanoid.w_uniform
+ if(uniform.has_sensor && uniform.sensor_mode >= SENSOR_COORDS) // Suit sensors must be on maximum
+ return RADAR_TRACKABLE
///Tracks all janitor equipment
/datum/computer_file/program/radar/custodial_locator
filename = "custodiallocator"
filedesc = "Custodial Locator"
extended_desc = "This program allows for tracking of custodial equipment."
- requires_ntnet = TRUE
- transfer_access = list(ACCESS_JANITOR)
- available_on_ntnet = TRUE
+ program_flags = PROGRAM_ON_NTNET_STORE | PROGRAM_REQUIRES_NTNET
+ download_access = list(ACCESS_JANITOR)
program_icon = "broom"
size = 2
detomatix_resistance = DETOMATIX_RESIST_MINOR
+ circuit_comp_type = /obj/item/circuit_component/mod_program/radar/janitor
/datum/computer_file/program/radar/custodial_locator/find_atom()
- return locate(selected) in GLOB.janitor_devices
+ return ..() || (locate(selected) in GLOB.janitor_devices)
/datum/computer_file/program/radar/custodial_locator/scan()
objects = list()
@@ -284,8 +308,8 @@
var/obj/structure/mop_bucket/janitorialcart/janicart = custodial_tools
tool_name = "[janicart.name] - Water level: [janicart.reagents.total_volume] / [janicart.reagents.maximum_volume]"
- if(istype(custodial_tools, /mob/living/simple_animal/bot/cleanbot))
- var/mob/living/simple_animal/bot/cleanbot/cleanbots = custodial_tools
+ if(istype(custodial_tools, /mob/living/basic/bot/cleanbot))
+ var/mob/living/basic/bot/cleanbot/cleanbots = custodial_tools
tool_name = "[cleanbots.name] - [cleanbots.bot_mode_flags & BOT_MODE_ON ? "Online" : "Offline"]"
var/list/tool_information = list(
@@ -302,16 +326,14 @@
/datum/computer_file/program/radar/fission360
filename = "fission360"
filedesc = "Fission360"
- category = PROGRAM_CATEGORY_MISC
- program_icon_state = "radarsyndicate"
+ program_open_overlay = "radarsyndicate"
extended_desc = "This program allows for tracking of nuclear authorization disks and warheads."
- requires_ntnet = FALSE
- available_on_ntnet = FALSE
- available_on_syndinet = TRUE
+ program_flags = PROGRAM_ON_SYNDINET_STORE
tgui_id = "NtosRadarSyndicate"
program_icon = "bomb"
arrowstyle = "ntosradarpointerS.png"
pointercolor = "red"
+ circuit_comp_type = /obj/item/circuit_component/mod_program/radar/nukie
/datum/computer_file/program/radar/fission360/on_start(mob/living/user)
. = ..()
@@ -329,7 +351,7 @@
return ..()
/datum/computer_file/program/radar/fission360/find_atom()
- return SSpoints_of_interest.get_poi_atom_by_ref(selected)
+ return ..() || SSpoints_of_interest.get_poi_atom_by_ref(selected)
/datum/computer_file/program/radar/fission360/scan()
objects = list()
@@ -385,3 +407,131 @@
span_danger("[computer] vibrates and lets out an ominous alarm. Uh oh."),
span_notice("[computer] begins to vibrate rapidly. Wonder what that means..."),
)
+
+
+/**
+ * Base circuit for the radar program.
+ * The abstract radar doesn't have this, nor this one is associated to it, so
+ * make sure to specify the associate_program and circuit_comp_type of subtypes,
+ */
+/obj/item/circuit_component/mod_program/radar
+
+ ///The target to track
+ var/datum/port/input/target
+ ///The selected target, from the app
+ var/datum/port/output/selected_by_app
+ /// The result from the output
+ var/datum/port/output/x_pos
+ var/datum/port/output/y_pos
+
+ circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL
+
+/obj/item/circuit_component/mod_program/radar/populate_ports()
+ . = ..()
+ target = add_input_port("Target", PORT_TYPE_ATOM)
+ selected_by_app = add_output_port("Selected From Program", PORT_TYPE_ATOM)
+ x_pos = add_output_port("X", PORT_TYPE_NUMBER)
+ y_pos = add_output_port("Y", PORT_TYPE_NUMBER)
+
+/obj/item/circuit_component/mod_program/radar/register_shell(atom/movable/shell)
+ . = ..()
+ RegisterSignal(associated_program.computer, COMSIG_MODULAR_COMPUTER_RADAR_TRACKABLE, PROC_REF(can_track))
+ RegisterSignal(associated_program.computer, COMSIG_MODULAR_COMPUTER_RADAR_FIND_ATOM, PROC_REF(get_atom))
+ RegisterSignal(associated_program.computer, COMSIG_MODULAR_COMPUTER_RADAR_SELECTED, PROC_REF(on_selected))
+
+/obj/item/circuit_component/mod_program/radar/unregister_shell()
+ UnregisterSignal(associated_program.computer, list(
+ COMSIG_MODULAR_COMPUTER_RADAR_TRACKABLE,
+ COMSIG_MODULAR_COMPUTER_RADAR_FIND_ATOM,
+ COMSIG_MODULAR_COMPUTER_RADAR_SELECTED,
+ ))
+ return ..()
+
+/obj/item/circuit_component/mod_program/radar/get_ui_notices()
+ . = ..()
+ . += create_ui_notice("Max range for unsupported entities: [MAX_RADAR_CIRCUIT_DISTANCE] tiles", "orange", FA_ICON_BULLSEYE)
+
+///Set the selected ref of the program to the target (if it exists) and update the x/y pos ports (if trackable) when triggered.
+/obj/item/circuit_component/mod_program/radar/input_received(datum/port/port)
+ var/datum/computer_file/program/radar/radar = associated_program
+ var/atom/radar_atom = radar.find_atom()
+ if(target.value != radar_atom)
+ radar.selected = REF(target.value)
+ SStgui.update_uis(radar.computer)
+ if(radar.trackable(radar_atom))
+ var/turf/turf = get_turf(radar_atom)
+ x_pos.set_output(turf.x)
+ y_pos.set_output(turf.y)
+ else
+ x_pos.set_output(null)
+ y_pos.set_output(null)
+
+/**
+ * Check if we can track the object. When making different definitions of this proc for subtypes, include typical
+ * targets as an exception to this (e.g humans for lifeline) so that even if they're coming from a circuit input
+ * they won't get filtered by the maximum distance, because they're "supported entities".
+ */
+/obj/item/circuit_component/mod_program/radar/proc/can_track(datum/source, atom/signal, signal_turf, computer_turf)
+ SIGNAL_HANDLER
+ if(target.value && get_dist_euclidian(computer_turf, signal_turf) > MAX_RADAR_CIRCUIT_DISTANCE)
+ return COMPONENT_RADAR_DONT_TRACK
+ return COMPONENT_RADAR_TRACK_ANYWAY
+
+///Return the value of the target port.
+/obj/item/circuit_component/mod_program/radar/proc/get_atom(datum/source, list/atom_container)
+ SIGNAL_HANDLER
+ atom_container[1] = target.value
+
+/**
+ * When a target is selected by the app, reset the target port, update the x/pos ports (if trackable)
+ * and set selected_by_app port to the target atom.
+ */
+/obj/item/circuit_component/mod_program/radar/proc/on_selected(datum/source, selected_ref)
+ SIGNAL_HANDLER
+ target.set_value(null)
+ var/datum/computer_file/program/radar/radar = associated_program
+ var/atom/selected_atom = radar.find_atom()
+ selected_by_app.set_output(selected_atom)
+ if(radar.trackable(selected_atom))
+ var/turf/turf = get_turf(radar.selected)
+ x_pos.set_output(turf.x)
+ y_pos.set_output(turf.y)
+ else
+ x_pos.set_output(null)
+ y_pos.set_output(null)
+
+ trigger_output.set_output(COMPONENT_SIGNAL)
+
+
+/obj/item/circuit_component/mod_program/radar/medical
+ associated_program = /datum/computer_file/program/radar/lifeline
+
+/obj/item/circuit_component/mod_program/radar/medical/can_track(datum/source, atom/signal, signal_turf, computer_turf)
+ if(target.value in GLOB.human_list)
+ return NONE
+ return ..()
+
+/obj/item/circuit_component/mod_program/radar/janitor
+ associated_program = /datum/computer_file/program/radar/custodial_locator
+
+/obj/item/circuit_component/mod_program/radar/janitor/can_track(datum/source, atom/signal, signal_turf, computer_turf)
+ if(target.value in GLOB.janitor_devices)
+ return NONE
+ return ..()
+/obj/item/circuit_component/mod_program/radar/nukie
+ associated_program = /datum/computer_file/program/radar/fission360
+
+/obj/item/circuit_component/mod_program/radar/nukie/can_track(datum/source, atom/signal, signal_turf, computer_turf)
+ if(target.value in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/nuclearbomb))
+ return NONE
+ if(target.value in SSpoints_of_interest.real_nuclear_disks)
+ return NONE
+ if(target.value == SSshuttle.getShuttle("syndicate"))
+ return NONE
+ return ..()
+
+#undef MAX_RADAR_CIRCUIT_DISTANCE
+
+#undef RADAR_NOT_TRACKABLE
+#undef RADAR_TRACKABLE
+#undef RADAR_TRACKABLE_ANYWAY
diff --git a/code/modules/modular_computers/file_system/programs/records.dm b/code/modules/modular_computers/file_system/programs/records.dm
index 9b5617364c0aa..063c19d35e18b 100644
--- a/code/modules/modular_computers/file_system/programs/records.dm
+++ b/code/modules/modular_computers/file_system/programs/records.dm
@@ -2,13 +2,13 @@
filename = "ntrecords"
filedesc = "Records"
extended_desc = "Allows the user to view several basic records from the crew."
- category = PROGRAM_CATEGORY_MISC
+ downloader_category = PROGRAM_CATEGORY_SECURITY
program_icon = "clipboard"
- program_icon_state = "crew"
+ program_open_overlay = "crew"
tgui_id = "NtosRecords"
size = 4
- usage_flags = PROGRAM_TABLET | PROGRAM_LAPTOP
- available_on_ntnet = FALSE
+ can_run_on_flags = PROGRAM_PDA | PROGRAM_LAPTOP
+ program_flags = NONE
detomatix_resistance = DETOMATIX_RESIST_MINOR
var/mode
@@ -18,16 +18,16 @@
filename = "medrecords"
program_icon = "book-medical"
extended_desc = "Allows the user to view several basic medical records from the crew."
- transfer_access = list(ACCESS_MEDICAL, ACCESS_FLAG_COMMAND)
- available_on_ntnet = TRUE
+ download_access = list(ACCESS_MEDICAL, ACCESS_FLAG_COMMAND)
+ program_flags = PROGRAM_ON_NTNET_STORE
mode = "medical"
/datum/computer_file/program/records/security
filedesc = "Security Records"
filename = "secrecords"
extended_desc = "Allows the user to view several basic security records from the crew."
- transfer_access = list(ACCESS_SECURITY, ACCESS_FLAG_COMMAND)
- available_on_ntnet = TRUE
+ download_access = list(ACCESS_SECURITY, ACCESS_FLAG_COMMAND)
+ program_flags = PROGRAM_ON_NTNET_STORE
mode = "security"
/datum/computer_file/program/records/proc/GetRecordsReadable()
diff --git a/code/modules/modular_computers/file_system/programs/robocontrol.dm b/code/modules/modular_computers/file_system/programs/robocontrol.dm
index 52bfafdcf8e97..00bd00f3e67b5 100644
--- a/code/modules/modular_computers/file_system/programs/robocontrol.dm
+++ b/code/modules/modular_computers/file_system/programs/robocontrol.dm
@@ -2,10 +2,10 @@
/datum/computer_file/program/robocontrol
filename = "botkeeper"
filedesc = "BotKeeper"
- category = PROGRAM_CATEGORY_SCI
- program_icon_state = "robot"
+ downloader_category = PROGRAM_CATEGORY_SCIENCE
+ program_open_overlay = "robot"
extended_desc = "A remote controller used for giving basic commands to non-sentient robots."
- requires_ntnet = TRUE
+ program_flags = PROGRAM_ON_NTNET_STORE | PROGRAM_REQUIRES_NTNET
size = 6
tgui_id = "NtosRoboControl"
program_icon = "robot"
@@ -83,7 +83,8 @@
return data
-/datum/computer_file/program/robocontrol/ui_act(action, list/params, datum/tgui/ui)
+/datum/computer_file/program/robocontrol/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
var/mob/current_user = ui.user
var/obj/item/card/id/id_card = computer?.computer_id_slot
diff --git a/code/modules/modular_computers/file_system/programs/robotact.dm b/code/modules/modular_computers/file_system/programs/robotact.dm
index e64691ccba2a2..8a2a824d004d2 100644
--- a/code/modules/modular_computers/file_system/programs/robotact.dm
+++ b/code/modules/modular_computers/file_system/programs/robotact.dm
@@ -1,14 +1,13 @@
/datum/computer_file/program/robotact
filename = "robotact"
filedesc = "RoboTact"
- category = PROGRAM_CATEGORY_SCI
+ downloader_category = PROGRAM_CATEGORY_SCIENCE
extended_desc = "A built-in app for cyborg self-management and diagnostics."
ui_header = "robotact.gif" //DEBUG -- new icon before PR
- program_icon_state = "command"
- requires_ntnet = FALSE
- available_on_ntnet = FALSE
+ program_open_overlay = "command"
+ program_flags = NONE
undeletable = TRUE
- usage_flags = PROGRAM_TABLET
+ can_run_on_flags = PROGRAM_PDA
size = 5
tgui_id = "NtosRobotact"
program_icon = "terminal"
@@ -21,7 +20,7 @@
if(.)
var/obj/item/modular_computer/pda/silicon/tablet = computer
if(tablet.device_theme == PDA_THEME_SYNDICATE)
- program_icon_state = "command-syndicate"
+ program_open_overlay = "command-syndicate"
return TRUE
return FALSE
@@ -85,6 +84,7 @@
return data
/datum/computer_file/program/robotact/ui_act(action, params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
//Implied type, memes
var/obj/item/modular_computer/pda/silicon/tablet = computer
var/mob/living/silicon/robot/cyborg = tablet.silicon_owner
diff --git a/code/modules/modular_computers/file_system/programs/secureye.dm b/code/modules/modular_computers/file_system/programs/secureye.dm
index 6e3e69cdccfc5..c9e4fc087f364 100644
--- a/code/modules/modular_computers/file_system/programs/secureye.dm
+++ b/code/modules/modular_computers/file_system/programs/secureye.dm
@@ -3,13 +3,13 @@
/datum/computer_file/program/secureye
filename = "secureye"
filedesc = "SecurEye"
- category = PROGRAM_CATEGORY_MISC
+ downloader_category = PROGRAM_CATEGORY_SECURITY
ui_header = "borg_mon.gif"
- program_icon_state = "generic"
+ program_open_overlay = "generic"
extended_desc = "This program allows access to standard security camera networks."
- requires_ntnet = TRUE
- transfer_access = list(ACCESS_SECURITY)
- usage_flags = PROGRAM_CONSOLE | PROGRAM_LAPTOP
+ program_flags = PROGRAM_ON_NTNET_STORE | PROGRAM_REQUIRES_NTNET
+ download_access = list(ACCESS_SECURITY)
+ can_run_on_flags = PROGRAM_CONSOLE | PROGRAM_LAPTOP
size = 5
tgui_id = "NtosSecurEye"
program_icon = "eye"
@@ -39,12 +39,9 @@
filename = "syndeye"
filedesc = "SyndEye"
extended_desc = "This program allows for illegal access to security camera networks."
- transfer_access = list()
- available_on_ntnet = FALSE
- available_on_syndinet = TRUE
- requires_ntnet = FALSE
- usage_flags = PROGRAM_ALL
- unique_copy = TRUE
+ download_access = list()
+ can_run_on_flags = PROGRAM_ALL
+ program_flags = PROGRAM_ON_SYNDINET_STORE | PROGRAM_UNIQUE_COPY
network = list("ss13", "mine", "rd", "labor", "ordnance", "minisat")
spying = TRUE
diff --git a/code/modules/modular_computers/file_system/programs/signalcommander.dm b/code/modules/modular_computers/file_system/programs/signalcommander.dm
index e8140b62b17c2..1e6e3e54051fb 100644
--- a/code/modules/modular_computers/file_system/programs/signalcommander.dm
+++ b/code/modules/modular_computers/file_system/programs/signalcommander.dm
@@ -1,13 +1,15 @@
/datum/computer_file/program/signal_commander
filename = "signaler"
filedesc = "SignalCommander"
- category = PROGRAM_CATEGORY_MISC
- program_icon_state = "signal"
+ downloader_category = PROGRAM_CATEGORY_EQUIPMENT
+ program_open_overlay = "signal"
extended_desc = "A small built-in frequency app that sends out signaller signals with the appropriate hardware."
size = 2
tgui_id = "NtosSignaler"
program_icon = "satellite-dish"
- usage_flags = PROGRAM_TABLET | PROGRAM_LAPTOP
+ can_run_on_flags = PROGRAM_PDA | PROGRAM_LAPTOP
+ program_flags = /datum/computer_file/program::program_flags | PROGRAM_CIRCUITS_RUN_WHEN_CLOSED
+ circuit_comp_type = /obj/item/circuit_component/mod_program/signaler
///What is the saved signal frequency?
var/signal_frequency = FREQ_SIGNALER
/// What is the saved signal code?
@@ -36,10 +38,11 @@
data["maxFrequency"] = MAX_FREE_FREQ
return data
-/datum/computer_file/program/signal_commander/ui_act(action, list/params)
+/datum/computer_file/program/signal_commander/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
switch(action)
if("signal")
- INVOKE_ASYNC(src, PROC_REF(signal))
+ INVOKE_ASYNC(src, PROC_REF(signal), usr)
. = TRUE
if("freq")
var/new_signal_frequency = sanitize_frequency(unformat_frequency(params["freq"]), TRUE)
@@ -56,27 +59,65 @@
signal_code = initial(signal_code)
. = TRUE
-/datum/computer_file/program/signal_commander/proc/signal()
+/datum/computer_file/program/signal_commander/proc/signal(atom/source)
if(!radio_connection)
return
+ var/mob/user
+ var/obj/item/circuit_component/signaling
+ if(ismob(source))
+ user = source
+ else if(istype(source, /obj/item/circuit_component))
+ signaling = source
+
if(!COOLDOWN_FINISHED(src, signal_cooldown))
- computer.balloon_alert(usr, "cooling down!")
+ if(user)
+ computer.balloon_alert(user, "cooling down!")
return
COOLDOWN_START(src, signal_cooldown, signal_cooldown_time)
- computer.balloon_alert(usr, "signaled")
+ if(user)
+ computer.balloon_alert(user, "signaled")
var/time = time2text(world.realtime,"hh:mm:ss")
var/turf/T = get_turf(computer)
-
- var/logging_data = "[time] : [key_name(usr)] used the computer '[initial(computer.name)]' @ location ([T.x],[T.y],[T.z]) : [format_frequency(signal_frequency)]/[signal_code]"
+ var/user_deets
+ if(signaling)
+ user_deets = "[signaling.parent.get_creator()]"
+ else
+ user_deets = "[key_name(usr)]"
+ var/logging_data = "[time] : [user_deets] used the computer '[initial(computer.name)]' @ location ([T.x],[T.y],[T.z]) : [format_frequency(signal_frequency)]/[signal_code]"
add_to_signaler_investigate_log(logging_data)
- var/datum/signal/signal = new(list("code" = signal_code), logging_data = logging_data)
+ var/datum/signal/signal = new(list("code" = signal_code, "key" = signaling?.parent.owner_id), logging_data = logging_data)
radio_connection.post_signal(computer, signal)
/datum/computer_file/program/signal_commander/proc/set_frequency(new_frequency)
SSradio.remove_object(computer, signal_frequency)
signal_frequency = new_frequency
radio_connection = SSradio.add_object(computer, signal_frequency, RADIO_SIGNALER)
+
+/obj/item/circuit_component/mod_program/signaler
+ associated_program = /datum/computer_file/program/signal_commander
+ circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL
+
+ /// Frequency input
+ var/datum/port/input/freq
+ /// Signal input
+ var/datum/port/input/code
+
+/obj/item/circuit_component/mod_program/signaler/populate_ports()
+ . = ..()
+ freq = add_input_port("Frequency", PORT_TYPE_NUMBER, trigger = PROC_REF(set_freq), default = FREQ_SIGNALER)
+ code = add_input_port("Code", PORT_TYPE_NUMBER, trigger = PROC_REF(set_code), default = DEFAULT_SIGNALER_CODE)
+
+/obj/item/circuit_component/mod_program/signaler/proc/set_freq(datum/port/port)
+ var/datum/computer_file/program/signal_commander/signaler = associated_program
+ signaler.set_frequency(clamp(freq.value, MIN_FREE_FREQ, MAX_FREE_FREQ))
+
+/obj/item/circuit_component/mod_program/signaler/proc/set_code(datum/port/port)
+ var/datum/computer_file/program/signal_commander/signaler = associated_program
+ signaler.signal_code = round(clamp(code.value, 1, 100))
+
+/obj/item/circuit_component/mod_program/signaler/input_received(datum/port/port)
+ INVOKE_ASYNC(associated_program, TYPE_PROC_REF(/datum/computer_file/program/signal_commander, signal), src)
diff --git a/code/modules/modular_computers/file_system/programs/skill_tracker.dm b/code/modules/modular_computers/file_system/programs/skill_tracker.dm
index c68cffb337401..bd208dcef524b 100644
--- a/code/modules/modular_computers/file_system/programs/skill_tracker.dm
+++ b/code/modules/modular_computers/file_system/programs/skill_tracker.dm
@@ -1,13 +1,13 @@
/datum/computer_file/program/skill_tracker
filename = "skilltracker"
filedesc = "ExperTrak Skill Tracker"
- category = PROGRAM_CATEGORY_MISC
- program_icon_state = "generic"
+ downloader_category = PROGRAM_CATEGORY_DEVICE
+ program_open_overlay = "generic"
extended_desc = "Scan and view your current marketable job skills."
size = 2
tgui_id = "NtosSkillTracker"
program_icon = "medal"
- usage_flags = PROGRAM_TABLET // Must be a handheld device to read read your chakras or whatever
+ can_run_on_flags = PROGRAM_PDA // Must be a handheld device to read read your chakras or whatever
/datum/computer_file/program/skill_tracker/ui_data(mob/user)
var/list/data = list()
@@ -50,7 +50,8 @@
return null
-/datum/computer_file/program/skill_tracker/ui_act(action, params, datum/tgui/ui)
+/datum/computer_file/program/skill_tracker/ui_act(action, params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
switch(action)
if("PRG_reward")
var/skill_type = find_skilltype(params["skill"])
diff --git a/code/modules/modular_computers/file_system/programs/sm_monitor.dm b/code/modules/modular_computers/file_system/programs/sm_monitor.dm
index 0ba8a72140779..72ab4d094c084 100644
--- a/code/modules/modular_computers/file_system/programs/sm_monitor.dm
+++ b/code/modules/modular_computers/file_system/programs/sm_monitor.dm
@@ -1,12 +1,12 @@
/datum/computer_file/program/supermatter_monitor
filename = "ntcims"
filedesc = "NT CIMS"
- category = PROGRAM_CATEGORY_ENGI
+ downloader_category = PROGRAM_CATEGORY_ENGINEERING
ui_header = "smmon_0.gif"
- program_icon_state = "smmon_0"
+ program_open_overlay = "smmon_0"
extended_desc = "Crystal Integrity Monitoring System, connects to specially calibrated supermatter sensors to provide information on the status of supermatter-based engines."
- requires_ntnet = TRUE
- transfer_access = list(ACCESS_CONSTRUCTION)
+ program_flags = PROGRAM_ON_NTNET_STORE | PROGRAM_REQUIRES_NTNET
+ download_access = list(ACCESS_CONSTRUCTION)
size = 5
tgui_id = "NtosSupermatter"
program_icon = "radiation"
@@ -55,6 +55,7 @@
return data
/datum/computer_file/program/supermatter_monitor/ui_act(action, params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
switch(action)
if("PRG_refresh")
refresh()
@@ -109,6 +110,6 @@
if(last_status != new_status)
last_status = new_status
ui_header = "smmon_[last_status].gif"
- program_icon_state = "smmon_[last_status]"
+ program_open_overlay = "smmon_[last_status]"
if(istype(computer))
computer.update_appearance()
diff --git a/code/modules/modular_computers/file_system/programs/statusdisplay.dm b/code/modules/modular_computers/file_system/programs/statusdisplay.dm
index d55bafb2e9c19..6136ab9355b59 100644
--- a/code/modules/modular_computers/file_system/programs/statusdisplay.dm
+++ b/code/modules/modular_computers/file_system/programs/statusdisplay.dm
@@ -2,15 +2,15 @@
filename = "statusdisplay"
filedesc = "Status Display"
program_icon = "signal"
- program_icon_state = "generic"
- requires_ntnet = TRUE
+ program_open_overlay = "generic"
size = 1
+ circuit_comp_type = /obj/item/circuit_component/mod_program/status
extended_desc = "An app used to change the message on the station status displays."
tgui_id = "NtosStatus"
- usage_flags = PROGRAM_ALL
- available_on_ntnet = FALSE
+ can_run_on_flags = PROGRAM_ALL
+ program_flags = PROGRAM_REQUIRES_NTNET
var/upper_text = ""
var/lower_text = ""
@@ -43,16 +43,16 @@
* * upper - Top text
* * lower - Bottom text
*/
-/datum/computer_file/program/status/proc/post_message(upper, lower)
+/datum/computer_file/program/status/proc/post_message(upper, lower, log_usr = key_name(usr))
post_status("message", upper, lower)
- log_game("[key_name(usr)] has changed the station status display message to \"[upper] [lower]\" [loc_name(usr)]")
+ log_game("[log_usr] has changed the station status display message to \"[upper] [lower]\" [loc_name(usr)]")
/**
* Post a picture to status displays
* Arguments:
* * picture - The picture name
*/
-/datum/computer_file/program/status/proc/post_picture(picture)
+/datum/computer_file/program/status/proc/post_picture(picture, log_usr = key_name(usr))
if (!(picture in GLOB.status_display_approved_pictures))
return
if(picture in GLOB.status_display_state_pictures)
@@ -71,9 +71,10 @@
else
post_status("alert", picture)
- log_game("[key_name(usr)] has changed the station status display message to \"[picture]\" [loc_name(usr)]")
+ log_game("[log_usr] has changed the station status display message to \"[picture]\" [loc_name(usr)]")
-/datum/computer_file/program/status/ui_act(action, list/params, datum/tgui/ui)
+/datum/computer_file/program/status/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
switch(action)
if("setStatusMessage")
upper_text = reject_bad_text(params["upperText"] || "", MAX_STATUS_LINE_LENGTH)
@@ -95,3 +96,31 @@
data["lowerText"] = lower_text
return data
+
+
+/obj/item/circuit_component/mod_program/status
+ associated_program = /datum/computer_file/program/status
+ circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL
+
+ ///When the trigger is signaled, this will be the upper text of status displays.
+ var/datum/port/input/upper_text
+ ///When the trigger is signaled, this will be the bottom text.
+ var/datum/port/input/bottom_text
+ ///A list port that, when signaled, will set the status image to one of its values
+ var/datum/port/input/status_display_pics
+
+/obj/item/circuit_component/mod_program/status/populate_ports()
+ . = ..()
+ upper_text = add_input_port("Upper text", PORT_TYPE_STRING)
+ bottom_text = add_input_port("Bottom text", PORT_TYPE_STRING)
+
+/obj/item/circuit_component/mod_program/status/populate_options()
+ status_display_pics = add_option_port("Set Status Display Picture", GLOB.status_display_approved_pictures, trigger = PROC_REF(set_picture))
+
+/obj/item/circuit_component/mod_program/status/proc/set_picture(datum/port/port)
+ var/datum/computer_file/program/status/status = associated_program
+ INVOKE_ASYNC(status, TYPE_PROC_REF(/datum/computer_file/program/status, post_picture), status_display_pics.value, parent.get_creator())
+
+/obj/item/circuit_component/mod_program/status/input_received(datum/port/port)
+ var/datum/computer_file/program/status/status = associated_program
+ INVOKE_ASYNC(status, TYPE_PROC_REF(/datum/computer_file/program/status, post_message), upper_text.value, bottom_text.value, parent.get_creator())
diff --git a/code/modules/modular_computers/file_system/programs/techweb.dm b/code/modules/modular_computers/file_system/programs/techweb.dm
index 77d0a0900e4a5..bf9e7b1e9b8b9 100644
--- a/code/modules/modular_computers/file_system/programs/techweb.dm
+++ b/code/modules/modular_computers/file_system/programs/techweb.dm
@@ -1,15 +1,15 @@
/datum/computer_file/program/science
filename = "experi_track"
filedesc = "Nanotrasen Science Hub"
- category = PROGRAM_CATEGORY_SCI
- program_icon_state = "research"
+ downloader_category = PROGRAM_CATEGORY_SCIENCE
+ program_open_overlay = "research"
extended_desc = "Connect to the internal science server in order to assist in station research efforts."
- requires_ntnet = TRUE
+ program_flags = PROGRAM_ON_NTNET_STORE | PROGRAM_REQUIRES_NTNET
size = 10
tgui_id = "NtosTechweb"
program_icon = "atom"
- required_access = list(ACCESS_COMMAND, ACCESS_RESEARCH)
- transfer_access = list(ACCESS_RESEARCH)
+ run_access = list(ACCESS_COMMAND, ACCESS_RESEARCH)
+ download_access = list(ACCESS_RESEARCH)
/// Reference to global science techweb
var/datum/techweb/stored_research
/// Access needed to lock/unlock the console
@@ -88,7 +88,8 @@
)
return data
-/datum/computer_file/program/science/ui_act(action, list/params)
+/datum/computer_file/program/science/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
// Check if the console is locked to block any actions occuring
if (locked && action != "toggleLock")
computer.say("Console is locked, cannot perform further actions.")
@@ -110,7 +111,8 @@
/datum/computer_file/program/science/ui_static_data(mob/user)
. = list(
- "static_data" = list()
+ "static_data" = list(),
+ "point_types_abbreviations" = SSresearch.point_types,
)
// Build node cache...
diff --git a/code/modules/modular_computers/file_system/programs/theme_selector.dm b/code/modules/modular_computers/file_system/programs/theme_selector.dm
index 9bc15a1a00b90..6190f9b15abaf 100644
--- a/code/modules/modular_computers/file_system/programs/theme_selector.dm
+++ b/code/modules/modular_computers/file_system/programs/theme_selector.dm
@@ -2,12 +2,10 @@
filename = "themeify"
filedesc = "Themeify"
extended_desc = "This program allows configuration of your device's theme."
- program_icon_state = "generic"
+ program_open_overlay = "generic"
undeletable = TRUE
size = 0
- header_program = TRUE
- available_on_ntnet = TRUE
- requires_ntnet = FALSE
+ program_flags = PROGRAM_ON_NTNET_STORE | PROGRAM_HEADER
tgui_id = "NtosThemeConfigure"
program_icon = "paint-roller"
@@ -25,6 +23,7 @@
return data
/datum/computer_file/program/themeify/ui_act(action, params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
switch(action)
if("PRG_change_theme")
var/selected_theme = params["selected_theme"]
diff --git a/code/modules/modular_computers/file_system/programs/wirecarp.dm b/code/modules/modular_computers/file_system/programs/wirecarp.dm
index 712d1e92cdafe..f5a0a374aac44 100644
--- a/code/modules/modular_computers/file_system/programs/wirecarp.dm
+++ b/code/modules/modular_computers/file_system/programs/wirecarp.dm
@@ -1,17 +1,18 @@
/datum/computer_file/program/ntnetmonitor
filename = "wirecarp"
filedesc = "WireCarp"
- category = PROGRAM_CATEGORY_MISC
- program_icon_state = "comm_monitor"
+ downloader_category = PROGRAM_CATEGORY_SECURITY
+ program_open_overlay = "comm_monitor"
extended_desc = "This program monitors stationwide NTNet network, provides access to logging systems, and allows for configuration changes"
size = 12
- requires_ntnet = TRUE
- required_access = list(ACCESS_NETWORK) //NETWORK CONTROL IS A MORE SECURE PROGRAM.
- available_on_ntnet = TRUE
+ run_access = list(ACCESS_NETWORK) //NETWORK CONTROL IS A MORE SECURE PROGRAM.
+ program_flags = PROGRAM_ON_NTNET_STORE | PROGRAM_REQUIRES_NTNET
tgui_id = "NtosNetMonitor"
program_icon = "network-wired"
+ circuit_comp_type = /obj/item/circuit_component/mod_program/ntnetmonitor
-/datum/computer_file/program/ntnetmonitor/ui_act(action, list/params, datum/tgui/ui)
+/datum/computer_file/program/ntnetmonitor/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
switch(action)
if("resetIDS")
SSmodular_computers.intrusion_detection_alarm = FALSE
@@ -20,7 +21,7 @@
SSmodular_computers.intrusion_detection_enabled = !SSmodular_computers.intrusion_detection_enabled
return TRUE
if("toggle_relay")
- var/obj/machinery/ntnet_relay/target_relay = locate(params["ref"]) in GLOB.ntnet_relays
+ var/obj/machinery/ntnet_relay/target_relay = locate(params["ref"]) in SSmachines.get_machines_by_type(/obj/machinery/ntnet_relay)
if(!istype(target_relay))
return
target_relay.set_relay_enabled(!target_relay.relay_enabled)
@@ -39,7 +40,7 @@
var/list/data = list()
data["ntnetrelays"] = list()
- for(var/obj/machinery/ntnet_relay/relays as anything in GLOB.ntnet_relays)
+ for(var/obj/machinery/ntnet_relay/relays as anything in SSmachines.get_machines_by_type(/obj/machinery/ntnet_relay))
var/list/relay_data = list()
relay_data["is_operational"] = !!relays.is_operational
relay_data["name"] = relays.name
@@ -51,7 +52,7 @@
data["idsalarm"] = SSmodular_computers.intrusion_detection_alarm
data["ntnetlogs"] = list()
- for(var/i in SSmodular_computers.logs)
+ for(var/i in SSmodular_computers.modpc_logs)
data["ntnetlogs"] += list(list("entry" = i))
data["tablets"] = list()
@@ -67,3 +68,65 @@
data["tablets"] += list(tablet_data)
return data
+
+/obj/item/circuit_component/mod_program/ntnetmonitor
+ associated_program = /datum/computer_file/program/ntnetmonitor
+ circuit_flags = CIRCUIT_FLAG_OUTPUT_SIGNAL
+ ///The stored NTnet relay or PDA to be used as the target of triggers
+ var/datum/port/input/target
+ ///Sets `intrusion_detection_alarm` when triggered
+ var/datum/port/input/toggle_ids
+ ///Toggles the target ntnet relay on/off when triggered
+ var/datum/port/input/toggle_relay
+ ///Purges modpc logs when triggered
+ var/datum/port/input/purge_logs
+ ///Toggles the spam mode of the target PDA when triggered
+ var/datum/port/input/toggle_mass_pda
+ ///Toggle mime mode of the target PDA when triggered
+ var/datum/port/input/toggle_mime_mode
+ ///Returns a list of all PDA Messengers when the "Get Messengers" input is pinged
+ var/datum/port/output/all_messengers
+ ///See above
+ var/datum/port/input/get_pdas
+
+/obj/item/circuit_component/mod_program/ntnetmonitor/populate_ports()
+ . = ..()
+ target = add_input_port("Target Messenger/Relay", PORT_TYPE_ATOM)
+ toggle_ids = add_input_port("Toggle IDS Status", PORT_TYPE_SIGNAL, trigger = PROC_REF(toggle_ids))
+ toggle_relay = add_input_port("Toggle NTnet Relay", PORT_TYPE_SIGNAL, trigger = PROC_REF(toggle_relay))
+ purge_logs = add_input_port("Purge Logs", PORT_TYPE_SIGNAL, trigger = PROC_REF(purge_logs))
+ toggle_mass_pda = add_input_port("Toggle Mass Messenger", PORT_TYPE_SIGNAL, trigger = PROC_REF(toggle_pda_stuff))
+ toggle_mime_mode = add_input_port("Toggle Mime Mode", PORT_TYPE_SIGNAL, trigger = PROC_REF(toggle_pda_stuff))
+ get_pdas = add_input_port("Get PDAs", PORT_TYPE_SIGNAL, trigger = PROC_REF(get_pdas))
+ all_messengers = add_output_port("List of PDAs", PORT_TYPE_LIST(PORT_TYPE_ATOM))
+
+/obj/item/circuit_component/mod_program/ntnetmonitor/proc/get_pdas(datum/port/port)
+ var/list/computers_with_messenger = list()
+ for(var/messenger_ref as anything in GLOB.pda_messengers)
+ var/datum/computer_file/program/messenger/messenger = GLOB.pda_messengers[messenger_ref]
+ computers_with_messenger |= WEAKREF(messenger.computer)
+ all_messengers.set_output(computers_with_messenger)
+
+/obj/item/circuit_component/mod_program/ntnetmonitor/proc/toggle_ids(datum/port/port)
+ SSmodular_computers.intrusion_detection_enabled = !SSmodular_computers.intrusion_detection_enabled
+
+/obj/item/circuit_component/mod_program/ntnetmonitor/proc/toggle_relay(datum/port/port)
+ var/obj/machinery/ntnet_relay/target_relay = target.value
+ if(!istype(target_relay))
+ return
+ target_relay.set_relay_enabled(!target_relay.relay_enabled)
+
+/obj/item/circuit_component/mod_program/ntnetmonitor/proc/purge_logs(datum/port/port)
+ SSmodular_computers.purge_logs()
+
+/obj/item/circuit_component/mod_program/ntnetmonitor/proc/toggle_pda_stuff(datum/port/port)
+ var/obj/item/modular_computer/computer = target.value
+ if(!istype(computer))
+ return
+ var/datum/computer_file/program/messenger/target_messenger = locate() in computer.stored_files
+ if(isnull(target_messenger))
+ return
+ if(COMPONENT_TRIGGERED_BY(toggle_mass_pda, port))
+ target_messenger.spam_mode = !target_messenger.spam_mode
+ if(COMPONENT_TRIGGERED_BY(toggle_mime_mode, port))
+ target_messenger.mime_mode = !target_messenger.mime_mode
diff --git a/code/modules/movespeed/modifiers/items.dm b/code/modules/movespeed/modifiers/items.dm
index 433200e322319..6bdf2f31760d5 100644
--- a/code/modules/movespeed/modifiers/items.dm
+++ b/code/modules/movespeed/modifiers/items.dm
@@ -17,5 +17,12 @@
/datum/movespeed_modifier/sphere
multiplicative_slowdown = -0.5
+/datum/movespeed_modifier/hook_jawed
+ multiplicative_slowdown = 4
+
/datum/movespeed_modifier/shooting_assistant
multiplicative_slowdown = 0.5
+
+/datum/movespeed_modifier/binocs_wielded
+ multiplicative_slowdown = 1.5
+
diff --git a/code/modules/movespeed/modifiers/mobs.dm b/code/modules/movespeed/modifiers/mobs.dm
index 49358223e3508..b782f2fc9593d 100644
--- a/code/modules/movespeed/modifiers/mobs.dm
+++ b/code/modules/movespeed/modifiers/mobs.dm
@@ -81,8 +81,8 @@
blacklisted_movetypes = FLOATING
variable = TRUE
-/datum/movespeed_modifier/shove
- multiplicative_slowdown = SHOVE_SLOWDOWN_STRENGTH
+/datum/movespeed_modifier/staggered
+ multiplicative_slowdown = STAGGERED_SLOWDOWN_STRENGTH
/datum/movespeed_modifier/human_carry
multiplicative_slowdown = HUMAN_CARRY_SLOWDOWN
diff --git a/code/modules/movespeed/modifiers/status_effects.dm b/code/modules/movespeed/modifiers/status_effects.dm
index 65245880ef42b..4768f66a544f4 100644
--- a/code/modules/movespeed/modifiers/status_effects.dm
+++ b/code/modules/movespeed/modifiers/status_effects.dm
@@ -53,3 +53,6 @@
/datum/movespeed_modifier/status_effect/midas_blight/gold
multiplicative_slowdown = 2
+
+/datum/movespeed_modifier/status_effect/guardian_shield
+ multiplicative_slowdown = 1
diff --git a/code/modules/pai/card.dm b/code/modules/pai/card.dm
index a652b745c9e50..da3bfe4e0ce14 100644
--- a/code/modules/pai/card.dm
+++ b/code/modules/pai/card.dm
@@ -26,7 +26,6 @@
if(!pai.encrypt_mod)
to_chat(user, span_alert("Encryption Key ports not configured."))
return
- user.set_machine(src)
pai.radio.attackby(used, user, params)
to_chat(user, span_notice("You insert [used] into the [src]."))
return
@@ -35,7 +34,6 @@
/obj/item/pai_card/attack_self(mob/user)
if(!in_range(src, user))
return
- user.set_machine(src)
ui_interact(user)
/obj/item/pai_card/Destroy()
@@ -234,7 +232,16 @@
playsound(src, 'sound/machines/ping.ogg', 20, TRUE)
balloon_alert(user, "pAI assistance requested")
var/mutable_appearance/alert_overlay = mutable_appearance('icons/obj/aicards.dmi', "pai")
- notify_ghosts("[user] is requesting a pAI companion! Use the pAI button to submit yourself as one.", source = user, alert_overlay = alert_overlay, action = NOTIFY_ORBIT, flashwindow = FALSE, header = "pAI Request!", ignore_key = POLL_IGNORE_PAI)
+
+ notify_ghosts(
+ "[user] is requesting a pAI companion! Use the pAI button to submit yourself as one.",
+ source = user,
+ header = "pAI Request!",
+ alert_overlay = alert_overlay,
+ notify_flags = NOTIFY_CATEGORY_NOFLASH,
+ ignore_key = POLL_IGNORE_PAI,
+ )
+
addtimer(VARSET_CALLBACK(src, request_spam, FALSE), PAI_SPAM_TIME, TIMER_UNIQUE | TIMER_STOPPABLE | TIMER_CLIENT_TIME | TIMER_DELETE_ME)
return TRUE
diff --git a/code/modules/pai/debug.dm b/code/modules/pai/debug.dm
index dde6fc4be058a..089dcedfabba5 100644
--- a/code/modules/pai/debug.dm
+++ b/code/modules/pai/debug.dm
@@ -28,7 +28,7 @@
card.set_personality(pai)
if(SSpai.candidates[key])
SSpai.candidates -= key
- SSblackbox.record_feedback("tally", "admin_verb", 1, "Make pAI") // If you are copy-pasting this, ensure the 4th parameter is unique to the new proc!
+ BLACKBOX_LOG_ADMIN_VERB("Make pAI")
/**
* Creates a new pAI.
diff --git a/code/modules/pai/hud.dm b/code/modules/pai/hud.dm
index 523d57d17b31c..1a71b5235b610 100644
--- a/code/modules/pai/hud.dm
+++ b/code/modules/pai/hud.dm
@@ -94,6 +94,7 @@
if(LAZYACCESS(modifiers, RIGHT_CLICK))
pAI.host_scan(PAI_SCAN_MASTER)
return TRUE
+
/atom/movable/screen/pai/crew_manifest
name = "Crew Manifest"
icon_state = "manifest"
diff --git a/code/modules/pai/pai.dm b/code/modules/pai/pai.dm
index 3998470f74878..400cf3660b2b8 100644
--- a/code/modules/pai/pai.dm
+++ b/code/modules/pai/pai.dm
@@ -31,7 +31,7 @@
/// If someone has enabled/disabled the pAIs ability to holo
var/can_holo = TRUE
- /// Whether this pAI can recieve radio messages
+ /// Whether this pAI can receive radio messages
var/can_receive = TRUE
/// Whether this pAI can transmit radio messages
var/can_transmit = TRUE
@@ -71,8 +71,6 @@
// Onboard Items
/// Atmospheric analyzer
var/obj/item/analyzer/atmos_analyzer
- /// Health analyzer
- var/obj/item/healthanalyzer/host_scan
/// GPS
var/obj/item/gps/pai/internal_gps
/// Music Synthesizer
@@ -82,6 +80,9 @@
/// Remote signaler
var/obj/item/assembly/signaler/internal/signaler
+ ///The messeenger ability that pAIs get when they are put in a PDA.
+ var/datum/action/innate/pai/messenger/messenger_ability
+
// Static lists
/// List of all available downloads
var/static/list/available_software = list(
@@ -151,9 +152,9 @@
return ..(target, action_bitflags)
/mob/living/silicon/pai/Destroy()
+ QDEL_NULL(messenger_ability)
QDEL_NULL(atmos_analyzer)
QDEL_NULL(hacking_cable)
- QDEL_NULL(host_scan)
QDEL_NULL(instrument)
QDEL_NULL(internal_gps)
QDEL_NULL(newscaster)
@@ -193,8 +194,6 @@
atmos_analyzer = null
else if(gone == aicamera)
aicamera = null
- else if(gone == host_scan)
- host_scan = null
else if(gone == internal_gps)
internal_gps = null
else if(gone == instrument)
@@ -216,6 +215,8 @@
/mob/living/silicon/pai/Initialize(mapload)
. = ..()
+ if(istype(loc, /obj/item/modular_computer))
+ give_messenger_ability()
START_PROCESSING(SSfastprocess, src)
GLOB.pai_list += src
make_laws()
@@ -272,6 +273,15 @@
held_state = "[chassis]"
return ..()
+/mob/living/silicon/pai/set_stat(new_stat)
+ . = ..()
+ update_stat()
+
+/mob/living/silicon/pai/on_knockedout_trait_loss(datum/source)
+ . = ..()
+ set_stat(CONSCIOUS)
+ update_stat()
+
/**
* Resolves the weakref of the pai's master.
* If the master has been deleted, calls reset_software().
@@ -461,3 +471,14 @@
if (new_distance < HOLOFORM_MIN_RANGE || new_distance > HOLOFORM_MAX_RANGE)
return
leash.set_distance(new_distance)
+
+///Gives the messenger ability to the pAI, creating a new one if it doesn't have one already.
+/mob/living/silicon/pai/proc/give_messenger_ability()
+ if(!messenger_ability)
+ messenger_ability = new(src)
+ messenger_ability.Grant(src)
+
+///Removes the messenger ability from the pAI, but does not delete it.
+/mob/living/silicon/pai/proc/remove_messenger_ability()
+ if(messenger_ability)
+ messenger_ability.Remove(src)
diff --git a/code/modules/pai/software.dm b/code/modules/pai/software.dm
index 103056a5535b3..9876df5a2646a 100644
--- a/code/modules/pai/software.dm
+++ b/code/modules/pai/software.dm
@@ -38,7 +38,7 @@
return TRUE
// Software related ui actions
if(available_software[action] && !installed_software.Find(action))
- balloon_alert(usr, "software unavailable")
+ balloon_alert(ui.user, "software unavailable!")
return FALSE
switch(action)
if("Atmospheric Sensor")
@@ -116,8 +116,6 @@
atmos_analyzer = new(src)
if("Digital Messenger")
create_modularInterface()
- if("Host Scan")
- host_scan = new(src)
if("Internal GPS")
internal_gps = new(src)
if("Music Synthesizer")
@@ -193,28 +191,27 @@
* @returns {boolean} - TRUE if the scan was successful, FALSE otherwise.
*/
/mob/living/silicon/pai/proc/host_scan(mode)
- if(isnull(mode))
- return FALSE
- if(mode == PAI_SCAN_TARGET)
- var/mob/living/target = get_holder()
- if(!target || !isliving(target))
- balloon_alert(src, "not being carried")
- return FALSE
- host_scan.attack(target, src)
- return TRUE
- if(mode == PAI_SCAN_MASTER)
- if(!master_ref)
- balloon_alert(src, "no master detected")
- return FALSE
- var/mob/living/resolved_master = find_master()
- if(!resolved_master)
- balloon_alert(src, "cannot locate master")
- return FALSE
- if(!is_valid_z_level(get_turf(src), get_turf(resolved_master)))
- balloon_alert(src, "master out of range")
- return FALSE
- host_scan.attack(resolved_master, src)
- return TRUE
+ switch(mode)
+ if(PAI_SCAN_TARGET)
+ var/mob/living/target = get_holder()
+ if(!isliving(target))
+ balloon_alert(src, "not being carried!")
+ return FALSE
+ healthscan(src, target)
+ return TRUE
+
+ if(PAI_SCAN_MASTER)
+ var/mob/living/resolved_master = find_master()
+ if(isnull(resolved_master))
+ balloon_alert(src, "no master detected!")
+ return FALSE
+ if(!is_valid_z_level(get_turf(src), get_turf(resolved_master)))
+ balloon_alert(src, "master out of range!")
+ return FALSE
+ healthscan(src, resolved_master)
+ return TRUE
+
+ stack_trace("Invalid mode passed to host scan: [mode || "null"]")
return FALSE
/**
diff --git a/code/modules/paperwork/desk_bell.dm b/code/modules/paperwork/desk_bell.dm
index fda6b21295269..e193bbc98b102 100644
--- a/code/modules/paperwork/desk_bell.dm
+++ b/code/modules/paperwork/desk_bell.dm
@@ -69,7 +69,7 @@
playsound(user, 'sound/items/change_drill.ogg', 50, vary = TRUE)
broken_ringer = FALSE
times_rang = 0
- return TOOL_ACT_TOOLTYPE_SUCCESS
+ return ITEM_INTERACT_SUCCESS
return FALSE
return ..()
@@ -84,7 +84,7 @@
new/obj/item/stack/sheet/iron(drop_location())
new/obj/item/stack/sheet/iron(drop_location())
qdel(src)
- return TOOL_ACT_TOOLTYPE_SUCCESS
+ return ITEM_INTERACT_SUCCESS
return ..()
/// Check if the clapper breaks, and if it does, break it
diff --git a/code/modules/paperwork/fax.dm b/code/modules/paperwork/fax.dm
index a03c79f44d066..f9eafa901aa51 100644
--- a/code/modules/paperwork/fax.dm
+++ b/code/modules/paperwork/fax.dm
@@ -120,7 +120,7 @@ GLOBAL_VAR_INIT(nt_fax_department, pick("NT HR Department", "NT Legal Department
/obj/machinery/fax/wrench_act(mob/living/user, obj/item/tool)
. = ..()
default_unfasten_wrench(user, tool)
- return TOOL_ACT_TOOLTYPE_SUCCESS
+ return ITEM_INTERACT_SUCCESS
/**
* Open and close the wire panel.
@@ -139,16 +139,16 @@ GLOBAL_VAR_INIT(nt_fax_department, pick("NT HR Department", "NT Legal Department
return
var/new_fax_name = tgui_input_text(user, "Enter a new name for the fax machine.", "New Fax Name", , 128)
if (!new_fax_name)
- return TOOL_ACT_TOOLTYPE_SUCCESS
+ return ITEM_INTERACT_SUCCESS
if (new_fax_name != fax_name)
if (fax_name_exist(new_fax_name))
// Being able to set the same name as another fax machine will give a lot of gimmicks for the traitor.
if (syndicate_network != TRUE && !(obj_flags & EMAGGED))
to_chat(user, span_warning("There is already a fax machine with this name on the network."))
- return TOOL_ACT_TOOLTYPE_SUCCESS
+ return ITEM_INTERACT_SUCCESS
user.log_message("renamed [fax_name] (fax machine) to [new_fax_name].", LOG_GAME)
fax_name = new_fax_name
- return TOOL_ACT_TOOLTYPE_SUCCESS
+ return ITEM_INTERACT_SUCCESS
/obj/machinery/fax/attackby(obj/item/item, mob/user, params)
if (jammed && clear_jam(item, user))
@@ -174,7 +174,7 @@ GLOBAL_VAR_INIT(nt_fax_department, pick("NT HR Department", "NT Legal Department
var/obj/item/reagent_containers/spray/clean_spray = item
if(!clean_spray.reagents.has_reagent(/datum/reagent/space_cleaner, clean_spray.amount_per_transfer_from_this))
return FALSE
- clean_spray.reagents.remove_reagent(/datum/reagent/space_cleaner, clean_spray.amount_per_transfer_from_this, 1)
+ clean_spray.reagents.remove_reagent(/datum/reagent/space_cleaner, clean_spray.amount_per_transfer_from_this)
playsound(loc, 'sound/effects/spray3.ogg', 50, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE)
user.visible_message(span_notice("[user] cleans \the [src]."), span_notice("You clean \the [src]."))
jammed = FALSE
@@ -537,11 +537,12 @@ GLOBAL_VAR_INIT(nt_fax_department, pick("NT HR Department", "NT Legal Department
target_fax.receive(fax_item, sender)
else if(force) //no fax machines but we really gotte send? SEND A FAX MACHINE
- var/obj/machinery/fax/new_fax_machine = new ()
- send_supply_pod_to_area(new_fax_machine, area_type, force_pod_type)
+ var/obj/machinery/fax/new_fax_machine = new()
+ if(!send_supply_pod_to_area(new_fax_machine, area_type, force_pod_type))
+ stack_trace("Attempted to forcibly send a fax to [area_type], however the area does not exist or has no valid dropoff spot for a fax machine")
+ return FALSE
addtimer(CALLBACK(new_fax_machine, TYPE_PROC_REF(/obj/machinery/fax, receive), fax_item, sender), 10 SECONDS)
else
return FALSE
return TRUE
-
diff --git a/code/modules/paperwork/filingcabinet.dm b/code/modules/paperwork/filingcabinet.dm
index ed99e7ea179dc..140bdffcf8767 100644
--- a/code/modules/paperwork/filingcabinet.dm
+++ b/code/modules/paperwork/filingcabinet.dm
@@ -38,7 +38,7 @@
I.forceMove(src)
/obj/structure/filingcabinet/deconstruct(disassembled = TRUE)
- if(!(flags_1 & NODECONSTRUCT_1))
+ if(!(obj_flags & NO_DECONSTRUCTION))
new /obj/item/stack/sheet/iron(loc, 2)
for(var/obj/item/I in src)
I.forceMove(loc)
diff --git a/code/modules/paperwork/paper_cutter.dm b/code/modules/paperwork/paper_cutter.dm
index 9586ec6e86184..9878249a6d12d 100644
--- a/code/modules/paperwork/paper_cutter.dm
+++ b/code/modules/paperwork/paper_cutter.dm
@@ -109,7 +109,7 @@
tool.play_tool_sound(src)
balloon_alert(user, "[blade_secured ? "un" : ""]secured")
blade_secured = !blade_secured
- return TOOL_ACT_TOOLTYPE_SUCCESS
+ return ITEM_INTERACT_SUCCESS
/obj/item/papercutter/attackby(obj/item/inserted_item, mob/user, params)
if(istype(inserted_item, /obj/item/paper))
diff --git a/code/modules/paperwork/paper_premade.dm b/code/modules/paperwork/paper_premade.dm
index 47588a65706c7..b72ce806dd3a7 100644
--- a/code/modules/paperwork/paper_premade.dm
+++ b/code/modules/paperwork/paper_premade.dm
@@ -167,8 +167,8 @@
/////////// Lavaland
/obj/item/paper/fluff/stations/lavaland/orm_notice
- name = "URGENT!"
- default_raw_text = "A hastily written note has been scribbled here...
Please use the ore redemption machine in the cargo office for smelting. PLEASE!
--The Research Staff"
+ name = "URGENT! RENOVATIONS!"
+ default_raw_text = "A hastily written note has been scribbled here...
Please use the ore redemption machine smelter and refinery in the cargo office for smelting. PLEASE! Leave boulders alone for the BRM to pick up!
--The Research Staff"
/////////// Space Ruins
diff --git a/code/modules/paperwork/pen.dm b/code/modules/paperwork/pen.dm
index 69af56d341902..5ee432b365ebe 100644
--- a/code/modules/paperwork/pen.dm
+++ b/code/modules/paperwork/pen.dm
@@ -31,6 +31,38 @@
var/requires_gravity = TRUE // can you use this to write in zero-g
embedding = list(embed_chance = 50)
sharpness = SHARP_POINTY
+ var/dart_insert_icon = 'icons/obj/weapons/guns/toy.dmi'
+ var/dart_insert_casing_icon_state = "overlay_pen"
+ var/dart_insert_projectile_icon_state = "overlay_pen_proj"
+
+/obj/item/pen/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/dart_insert, \
+ dart_insert_icon, \
+ dart_insert_casing_icon_state, \
+ dart_insert_icon, \
+ dart_insert_projectile_icon_state, \
+ CALLBACK(src, PROC_REF(get_dart_var_modifiers))\
+ )
+ RegisterSignal(src, COMSIG_DART_INSERT_ADDED, PROC_REF(on_inserted_into_dart))
+ RegisterSignal(src, COMSIG_DART_INSERT_REMOVED, PROC_REF(on_removed_from_dart))
+
+/obj/item/pen/proc/on_inserted_into_dart(datum/source, obj/projectile/dart, mob/user, embedded = FALSE)
+ SIGNAL_HANDLER
+
+/obj/item/pen/proc/get_dart_var_modifiers()
+ return list(
+ "damage" = max(5, throwforce),
+ "speed" = max(0, throw_speed - 3),
+ "embedding" = embedding,
+ "armour_penetration" = armour_penetration,
+ "wound_bonus" = wound_bonus,
+ "bare_wound_bonus" = bare_wound_bonus,
+ "demolition_mod" = demolition_mod,
+ )
+
+/obj/item/pen/proc/on_removed_from_dart(datum/source, obj/projectile/dart, mob/user)
+ SIGNAL_HANDLER
/obj/item/pen/suicide_act(mob/living/user)
user.visible_message(span_suicide("[user] is scribbling numbers all over [user.p_them()]self with [src]! It looks like [user.p_theyre()] trying to commit sudoku..."))
@@ -69,7 +101,7 @@
if("#FF0000")
colour = "#00FF00"
chosen_color = "green"
- throw_speed = initial(throw_speed)
+ throw_speed--
if("#00FF00")
colour = "#0000FF"
chosen_color = "blue"
@@ -84,6 +116,8 @@
icon_state = "pen-fountain"
font = FOUNTAIN_PEN_FONT
requires_gravity = FALSE // fancy spess pens
+ dart_insert_casing_icon_state = "overlay_fountainpen"
+ dart_insert_projectile_icon_state = "overlay_fountainpen_proj"
/obj/item/pen/charcoal
name = "charcoal stylus"
@@ -113,13 +147,23 @@
custom_materials = list(/datum/material/gold = SMALL_MATERIAL_AMOUNT*7.5)
sharpness = SHARP_EDGED
resistance_flags = FIRE_PROOF
- unique_reskin = list("Oak" = "pen-fountain-o",
- "Gold" = "pen-fountain-g",
- "Rosewood" = "pen-fountain-r",
- "Black and Silver" = "pen-fountain-b",
- "Command Blue" = "pen-fountain-cb"
- )
+ unique_reskin = list(
+ "Oak" = "pen-fountain-o",
+ "Gold" = "pen-fountain-g",
+ "Rosewood" = "pen-fountain-r",
+ "Black and Silver" = "pen-fountain-b",
+ "Command Blue" = "pen-fountain-cb"
+ )
embedding = list("embed_chance" = 75)
+ dart_insert_casing_icon_state = "overlay_fountainpen_gold"
+ dart_insert_projectile_icon_state = "overlay_fountainpen_gold_proj"
+ var/list/overlay_reskin = list(
+ "Oak" = "overlay_fountainpen_gold",
+ "Gold" = "overlay_fountainpen_gold",
+ "Rosewood" = "overlay_fountainpen_gold",
+ "Black and Silver" = "overlay_fountainpen",
+ "Command Blue" = "overlay_fountainpen_gold"
+ )
/obj/item/pen/fountain/captain/Initialize(mapload)
. = ..()
@@ -128,12 +172,19 @@
effectiveness = 115, \
)
//the pen is mightier than the sword
+ RegisterSignal(src, COMSIG_DART_INSERT_PARENT_RESKINNED, PROC_REF(reskin_dart_insert))
/obj/item/pen/fountain/captain/reskin_obj(mob/M)
..()
if(current_skin)
desc = "It's an expensive [current_skin] fountain pen. The nib is quite sharp."
+/obj/item/pen/fountain/captain/proc/reskin_dart_insert(datum/component/dart_insert/insert_comp)
+ if(!istype(insert_comp)) //You really shouldn't be sending this signal from anything other than a dart_insert component
+ return
+ insert_comp.casing_overlay_icon_state = overlay_reskin[current_skin]
+ insert_comp.projectile_overlay_icon_state = "[overlay_reskin[current_skin]]_proj"
+
/obj/item/pen/attack_self(mob/living/carbon/user)
. = ..()
if(.)
@@ -185,7 +236,7 @@
label.remove_label()
label.apply_label()
to_chat(user, span_notice("You have successfully renamed \the [oldname] to [O]."))
- O.renamedByPlayer = TRUE
+ ADD_TRAIT(O, TRAIT_WAS_RENAMED, PEN_LABEL_TRAIT)
O.update_appearance(UPDATE_ICON)
if(penchoice == "Description")
@@ -198,7 +249,7 @@
else
O.AddComponent(/datum/component/rename, O.name, input)
to_chat(user, span_notice("You have successfully changed [O]'s description."))
- O.renamedByPlayer = TRUE
+ ADD_TRAIT(O, TRAIT_WAS_RENAMED, PEN_LABEL_TRAIT)
O.update_appearance(UPDATE_ICON)
if(penchoice == "Reset")
@@ -214,7 +265,7 @@
label.apply_label()
to_chat(user, span_notice("You have successfully reset [O]'s name and description."))
- O.renamedByPlayer = FALSE
+ REMOVE_TRAIT(O, TRAIT_WAS_RENAMED, PEN_LABEL_TRAIT)
O.update_appearance(UPDATE_ICON)
/obj/item/pen/get_writing_implement_details()
@@ -247,6 +298,23 @@
reagents.add_reagent(/datum/reagent/toxin/mutetoxin, 15)
reagents.add_reagent(/datum/reagent/toxin/staminatoxin, 10)
+/obj/item/pen/sleepy/on_inserted_into_dart(datum/source, obj/item/ammo_casing/dart, mob/user)
+ . = ..()
+ var/obj/projectile/proj = dart.loaded_projectile
+ RegisterSignal(proj, COMSIG_PROJECTILE_SELF_ON_HIT, PROC_REF(on_dart_hit))
+
+/obj/item/pen/sleepy/on_removed_from_dart(datum/source, obj/item/ammo_casing/dart, obj/projectile/proj, mob/user)
+ . = ..()
+ if(istype(proj))
+ UnregisterSignal(proj, COMSIG_PROJECTILE_SELF_ON_HIT)
+
+/obj/item/pen/sleepy/proc/on_dart_hit(datum/source, atom/movable/firer, atom/target, angle, hit_limb, blocked)
+ SIGNAL_HANDLER
+ var/mob/living/carbon/carbon_target = target
+ if(!istype(carbon_target) || blocked == 100)
+ return
+ if(carbon_target.can_inject(target_zone = hit_limb))
+ reagents.trans_to(carbon_target, reagents.total_volume, transferred_by = firer, methods = INJECT)
/*
* (Alan) Edaggers
*/
@@ -262,6 +330,7 @@
light_power = 0.75
light_color = COLOR_SOFT_RED
light_on = FALSE
+ dart_insert_projectile_icon_state = "overlay_edagger"
/// The real name of our item when extended.
var/hidden_name = "energy dagger"
/// The real desc of our item when extended.
@@ -287,6 +356,62 @@
RegisterSignal(src, COMSIG_TRANSFORMING_ON_TRANSFORM, PROC_REF(on_transform))
RegisterSignal(src, COMSIG_DETECTIVE_SCANNED, PROC_REF(on_scan))
+/obj/item/pen/edagger/on_inserted_into_dart(datum/source, obj/item/ammo_casing/dart, mob/user)
+ . = ..()
+ var/datum/component/transforming/transform_comp = GetComponent(/datum/component/transforming)
+ if(HAS_TRAIT(src, TRAIT_TRANSFORM_ACTIVE))
+ transform_comp.do_transform(src, user)
+ RegisterSignal(dart.loaded_projectile, COMSIG_PROJECTILE_FIRE, PROC_REF(on_containing_dart_fired))
+ RegisterSignal(dart.loaded_projectile, COMSIG_PROJECTILE_ON_SPAWN_DROP, PROC_REF(on_containing_dart_drop))
+ RegisterSignal(dart.loaded_projectile, COMSIG_PROJECTILE_ON_SPAWN_EMBEDDED, PROC_REF(on_containing_dart_embedded))
+
+/obj/item/pen/edagger/on_removed_from_dart(datum/source, obj/item/ammo_casing/dart, obj/projectile/projectile, mob/user)
+ . = ..()
+ if(istype(dart))
+ UnregisterSignal(dart, list(COMSIG_ITEM_UNEMBEDDED, COMSIG_ITEM_FAILED_EMBED))
+ if(istype(projectile))
+ UnregisterSignal(projectile, list(COMSIG_PROJECTILE_FIRE, COMSIG_PROJECTILE_ON_SPAWN_DROP, COMSIG_PROJECTILE_ON_SPAWN_EMBEDDED))
+
+/obj/item/pen/edagger/get_dart_var_modifiers()
+ . = ..()
+ var/datum/component/transforming/transform_comp = GetComponent(/datum/component/transforming)
+ .["damage"] = max(5, transform_comp.throwforce_on)
+ .["speed"] = max(0, transform_comp.throw_speed_on - 3)
+ var/list/embed_params = .["embedding"]
+ embed_params["embed_chance"] = 100
+
+/obj/item/pen/edagger/proc/on_containing_dart_fired(obj/projectile/source)
+ SIGNAL_HANDLER
+ playsound(source, 'sound/weapons/saberon.ogg', 5, TRUE)
+ var/datum/component/transforming/transform_comp = GetComponent(/datum/component/transforming)
+ source.hitsound = transform_comp.hitsound_on
+ source.set_light(light_range, light_power, light_color, l_on = TRUE)
+
+/obj/item/pen/edagger/proc/on_containing_dart_drop(datum/source, obj/item/ammo_casing/new_casing)
+ SIGNAL_HANDLER
+ playsound(new_casing, 'sound/weapons/saberoff.ogg', 5, TRUE)
+
+/obj/item/pen/edagger/proc/on_containing_dart_embedded(datum/source, obj/item/ammo_casing/new_casing)
+ SIGNAL_HANDLER
+ RegisterSignal(new_casing, COMSIG_ITEM_UNEMBEDDED, PROC_REF(on_embedded_removed))
+ RegisterSignal(new_casing, COMSIG_ITEM_FAILED_EMBED, PROC_REF(on_containing_dart_failed_embed))
+
+/obj/item/pen/edagger/proc/on_containing_dart_failed_embed(obj/item/ammo_casing/source)
+ SIGNAL_HANDLER
+ playsound(source, 'sound/weapons/saberoff.ogg', 5, TRUE)
+ UnregisterSignal(source, list(COMSIG_ITEM_UNEMBEDDED, COMSIG_ITEM_FAILED_EMBED))
+
+/obj/item/pen/edagger/proc/on_embedded_removed(obj/item/ammo_casing/source, mob/living/carbon/victim)
+ SIGNAL_HANDLER
+ playsound(source, 'sound/weapons/saberoff.ogg', 5, TRUE)
+ UnregisterSignal(source, list(COMSIG_ITEM_UNEMBEDDED, COMSIG_ITEM_FAILED_EMBED))
+ victim.visible_message(
+ message = span_warning("The blade of the [hidden_name] retracts as the [source.name] is removed from [victim]!"),
+ self_message = span_warning("The blade of the [hidden_name] retracts as the [source.name] is removed from you!"),
+ blind_message = span_warning("You hear an energy blade retract!"),
+ vision_distance = 1
+ )
+
/obj/item/pen/edagger/suicide_act(mob/living/user)
if(HAS_TRAIT(src, TRAIT_TRANSFORM_ACTIVE))
user.visible_message(span_suicide("[user] forcefully rams the pen into their mouth!"))
@@ -348,6 +473,25 @@
toolspeed = 10 //You will never willingly choose to use one of these over a shovel.
font = FOUNTAIN_PEN_FONT
colour = "#0000FF"
+ dart_insert_casing_icon_state = "overlay_survivalpen"
+ dart_insert_projectile_icon_state = "overlay_survivalpen_proj"
+
+/obj/item/pen/survival/on_inserted_into_dart(datum/source, obj/item/ammo_casing/dart, mob/user)
+ . = ..()
+ RegisterSignal(dart.loaded_projectile, COMSIG_PROJECTILE_SELF_ON_HIT, PROC_REF(on_dart_hit))
+
+/obj/item/pen/survival/on_removed_from_dart(datum/source, obj/item/ammo_casing/dart, obj/projectile/proj, mob/user)
+ . = ..()
+ if(istype(proj))
+ UnregisterSignal(proj, COMSIG_PROJECTILE_SELF_ON_HIT)
+
+/obj/item/pen/survival/proc/on_dart_hit(obj/projectile/source, atom/movable/firer, atom/target)
+ var/turf/target_turf = get_turf(target)
+ if(!target_turf)
+ target_turf = get_turf(src)
+ if(ismineralturf(target_turf))
+ var/turf/closed/mineral/mineral_turf = target_turf
+ mineral_turf.gets_drilled(firer, TRUE)
/obj/item/pen/destroyer
name = "Fine Tipped Pen"
@@ -362,6 +506,7 @@
desc = "A pen with an extendable screwdriver tip. This one has a yellow cap."
icon_state = "pendriver"
toolspeed = 1.2 // gotta have some downside
+ dart_insert_projectile_icon_state = "overlay_pendriver"
/obj/item/pen/screwdriver/get_all_tool_behaviours()
return list(TOOL_SCREWDRIVER)
diff --git a/code/modules/paperwork/photocopier.dm b/code/modules/paperwork/photocopier.dm
index 0f122c104eb81..a6cb83aafc0e1 100644
--- a/code/modules/paperwork/photocopier.dm
+++ b/code/modules/paperwork/photocopier.dm
@@ -91,6 +91,7 @@ GLOBAL_LIST_INIT(paper_blanks, init_paper_blanks())
. = ..()
toner_cartridge = new(src)
setup_components()
+ AddElement(/datum/element/elevation, pixel_shift = 8) //enough to look like your bums are on the machine.
/// Simply adds the necessary components for this to function.
/obj/machinery/photocopier/proc/setup_components()
@@ -282,6 +283,9 @@ GLOBAL_LIST_INIT(paper_blanks, init_paper_blanks())
/// Will invoke `do_copy_loop` asynchronously. Passes the supplied arguments on to it.
/obj/machinery/photocopier/proc/do_copies(datum/callback/copy_cb, mob/user, paper_use, toner_use, copies_amount)
+ if(machine_stat & (BROKEN|NOPOWER))
+ return
+
busy = TRUE
update_use_power(ACTIVE_POWER_USE)
// fucking god proc
@@ -535,7 +539,7 @@ GLOBAL_LIST_INIT(paper_blanks, init_paper_blanks())
/obj/machinery/photocopier/wrench_act(mob/living/user, obj/item/tool)
. = ..()
default_unfasten_wrench(user, tool)
- return TOOL_ACT_TOOLTYPE_SUCCESS
+ return ITEM_INTERACT_SUCCESS
/obj/machinery/photocopier/attackby(obj/item/object, mob/user, params)
if(istype(object, /obj/item/paper) || istype(object, /obj/item/photo) || istype(object, /obj/item/documents))
@@ -673,7 +677,7 @@ GLOBAL_LIST_INIT(paper_blanks, init_paper_blanks())
/obj/item/toner
name = "toner cartridge"
desc = "A small, lightweight cartridge of Nanotrasen ValueBrand toner. Fits photocopiers and autopainters alike."
- icon = 'icons/obj/device.dmi'
+ icon = 'icons/obj/service/bureaucracy.dmi'
icon_state = "tonercartridge"
grind_results = list(/datum/reagent/iodine = 40, /datum/reagent/iron = 10)
var/charges = 5
diff --git a/code/modules/paperwork/ticketmachine.dm b/code/modules/paperwork/ticketmachine.dm
index a5902a9df5a20..c24b8fd73e8b2 100644
--- a/code/modules/paperwork/ticketmachine.dm
+++ b/code/modules/paperwork/ticketmachine.dm
@@ -40,7 +40,7 @@
return ..()
/obj/machinery/ticket_machine/deconstruct(disassembled = TRUE)
- if(!(flags_1 & NODECONSTRUCT_1))
+ if(!(obj_flags & NO_DECONSTRUCTION))
new /obj/item/wallframe/ticket_machine(loc)
qdel(src)
diff --git a/code/modules/photography/camera/camera.dm b/code/modules/photography/camera/camera.dm
index b168aaf54daf8..dae12f33bd4cf 100644
--- a/code/modules/photography/camera/camera.dm
+++ b/code/modules/photography/camera/camera.dm
@@ -16,7 +16,7 @@
light_power = FLASH_LIGHT_POWER
light_on = FALSE
w_class = WEIGHT_CLASS_SMALL
- flags_1 = CONDUCT_1
+ obj_flags = CONDUCTS_ELECTRICITY
slot_flags = ITEM_SLOT_NECK
custom_materials = list(/datum/material/iron =SMALL_MATERIAL_AMOUNT*0.5, /datum/material/glass = SMALL_MATERIAL_AMOUNT*1.5)
custom_price = PAYCHECK_CREW * 2
@@ -121,6 +121,9 @@
return FALSE
else if(!(get_turf(target) in get_hear(world.view, user)))
return FALSE
+ else if(isliving(loc))
+ if(!(get_turf(target) in view(world.view, loc)))
+ return FALSE
else //user is an atom or null
if(!(get_turf(target) in view(world.view, user || src)))
return FALSE
@@ -241,27 +244,30 @@
printpicture(user, picture)
/obj/item/camera/proc/printpicture(mob/user, datum/picture/picture) //Normal camera proc for creating photos
- if(!user)
- return
pictures_left--
var/obj/item/photo/new_photo = new(get_turf(src), picture)
- if(in_range(new_photo, user) && user.put_in_hands(new_photo)) //needed because of TK
- to_chat(user, span_notice("[pictures_left] photos left."))
-
- if(can_customise)
- var/customise = tgui_alert(user, "Do you want to customize the photo?", "Customization", list("Yes", "No"))
- if(customise == "Yes")
- var/name1 = tgui_input_text(user, "Set a name for this photo, or leave blank.", "Name", max_length = 32)
- var/desc1 = tgui_input_text(user, "Set a description to add to photo, or leave blank.", "Description", max_length = 128)
- var/caption = tgui_input_text(user, "Set a caption for this photo, or leave blank.", "Caption", max_length = 256)
- if(name1)
- picture.picture_name = name1
- if(desc1)
- picture.picture_desc = "[desc1] - [picture.picture_desc]"
- if(caption)
- picture.caption = caption
- else if(default_picture_name)
- picture.picture_name = default_picture_name
+ if(user)
+ if(in_range(new_photo, user) && user.put_in_hands(new_photo)) //needed because of TK
+ to_chat(user, span_notice("[pictures_left] photos left."))
+
+ if(can_customise)
+ var/customise = tgui_alert(user, "Do you want to customize the photo?", "Customization", list("Yes", "No"))
+ if(customise == "Yes")
+ var/name1 = tgui_input_text(user, "Set a name for this photo, or leave blank.", "Name", max_length = 32)
+ var/desc1 = tgui_input_text(user, "Set a description to add to photo, or leave blank.", "Description", max_length = 128)
+ var/caption = tgui_input_text(user, "Set a caption for this photo, or leave blank.", "Caption", max_length = 256)
+ if(name1)
+ picture.picture_name = name1
+ if(desc1)
+ picture.picture_desc = "[desc1] - [picture.picture_desc]"
+ if(caption)
+ picture.caption = caption
+ else if(default_picture_name)
+ picture.picture_name = default_picture_name
+ else if(isliving(loc))
+ var/mob/living/holder = loc
+ if(holder.put_in_hands(new_photo))
+ to_chat(holder, span_notice("[pictures_left] photos left."))
new_photo.set_picture(picture, TRUE, TRUE)
if(CONFIG_GET(flag/picture_logging_camera))
@@ -327,6 +333,6 @@
return
if(!camera.can_target(target))
return
- INVOKE_ASYNC(camera, TYPE_PROC_REF(/obj/item/camera, captureimage), target, null, camera.picture_size_y - 1, camera.picture_size_y - 1)
+ INVOKE_ASYNC(camera, TYPE_PROC_REF(/obj/item/camera, captureimage), target, null, camera.picture_size_x - 1, camera.picture_size_y - 1)
#undef CAMERA_PICTURE_SIZE_HARD_LIMIT
diff --git a/code/modules/photography/photos/album.dm b/code/modules/photography/photos/album.dm
index 35d7f27017cdb..ddc896fe758fb 100644
--- a/code/modules/photography/photos/album.dm
+++ b/code/modules/photography/photos/album.dm
@@ -9,6 +9,7 @@
inhand_icon_state = "album"
lefthand_file = 'icons/mob/inhands/items/books_lefthand.dmi'
righthand_file = 'icons/mob/inhands/items/books_righthand.dmi'
+ storage_type = /datum/storage/photo_album
resistance_flags = FLAMMABLE
w_class = WEIGHT_CLASS_SMALL
flags_1 = PREVENT_CONTENTS_EXPLOSION_1
@@ -16,13 +17,11 @@
/obj/item/storage/photo_album/Initialize(mapload)
. = ..()
- atom_storage.set_holdable(list(/obj/item/photo))
- atom_storage.max_total_storage = 42
- atom_storage.max_slots = 21
- LAZYADD(SSpersistence.photo_albums, src)
+ if (!SSpersistence.initialized)
+ LAZYADD(SSpersistence.queued_photo_albums, src)
/obj/item/storage/photo_album/Destroy()
- LAZYREMOVE(SSpersistence.photo_albums, src)
+ LAZYREMOVE(SSpersistence.queued_photo_albums, src)
return ..()
/obj/item/storage/photo_album/proc/get_picture_id_list()
@@ -41,9 +40,9 @@
//Manual loading, DO NOT USE FOR HARDCODED/MAPPED IN ALBUMS. This is for if an album needs to be loaded mid-round from an ID.
/obj/item/storage/photo_album/proc/persistence_load()
- var/list/data = SSpersistence.get_photo_albums()
- if(data[persistence_id])
- populate_from_id_list(data[persistence_id])
+ var/list/data = SSpersistence.photo_albums_database.get_key(persistence_id)
+ if (!isnull(data))
+ populate_from_id_list(data)
/obj/item/storage/photo_album/proc/populate_from_id_list(list/ids)
var/list/current_ids = get_picture_id_list()
@@ -55,6 +54,32 @@
if(!atom_storage?.attempt_insert(P, override = TRUE))
qdel(P)
+/datum/storage/photo_album
+ max_total_storage = 42
+ max_slots = 21
+
+/datum/storage/photo_album/New(
+ atom/parent,
+ max_slots,
+ max_specific_storage,
+ max_total_storage,
+)
+ . = ..()
+ set_holdable(/obj/item/photo)
+
+/datum/storage/photo_album/proc/save_everything()
+ var/obj/item/storage/photo_album/album = parent
+ ASSERT(istype(album))
+ SSpersistence.photo_albums_database.set_key(album.persistence_id, album.get_picture_id_list())
+
+/datum/storage/photo_album/handle_enter(datum/source, obj/item/arrived)
+ . = ..()
+ save_everything()
+
+/datum/storage/photo_album/handle_exit(datum/source, obj/item/gone)
+ . = ..()
+ save_everything()
+
/obj/item/storage/photo_album/hos
name = "photo album (Head of Security)"
icon_state = "album_blue"
diff --git a/code/modules/photography/photos/frame.dm b/code/modules/photography/photos/frame.dm
index c42664af269d9..4fbe3e034d88c 100644
--- a/code/modules/photography/photos/frame.dm
+++ b/code/modules/photography/photos/frame.dm
@@ -56,7 +56,7 @@
var/obj/structure/sign/picture_frame/PF = O
PF.copy_overlays(src)
if(displayed)
- PF.framed = displayed
+ PF.set_and_save_framed(displayed)
if(contents.len)
var/obj/item/I = pick(contents)
I.forceMove(PF)
@@ -70,27 +70,19 @@
resistance_flags = FLAMMABLE
var/obj/item/photo/framed
var/persistence_id
- var/del_id_on_destroy = FALSE
var/art_value = OK_ART
var/can_decon = TRUE
-#define FRAME_DEFINE(id) /obj/structure/sign/picture_frame/##id/persistence_id = #id
-
-//Put default persistent frame defines here!
-
-#undef FRAME_DEFINE
-
/obj/structure/sign/picture_frame/Initialize(mapload, dir, building)
. = ..()
AddElement(/datum/element/art, art_value)
- LAZYADD(SSpersistence.photo_frames, src)
+ if (!SSpersistence.initialized)
+ LAZYADD(SSpersistence.queued_photo_frames, src)
if(dir)
setDir(dir)
/obj/structure/sign/picture_frame/Destroy()
- LAZYREMOVE(SSpersistence.photo_frames, src)
- if(persistence_id && del_id_on_destroy)
- SSpersistence.remove_photo_frames(persistence_id)
+ LAZYREMOVE(SSpersistence.queued_photo_frames, src)
return ..()
/obj/structure/sign/picture_frame/proc/get_photo_id()
@@ -99,9 +91,9 @@
//Manual loading, DO NOT USE FOR HARDCODED/MAPPED IN ALBUMS. This is for if an album needs to be loaded mid-round from an ID.
/obj/structure/sign/picture_frame/proc/persistence_load()
- var/list/data = SSpersistence.get_photo_frames()
- if(data[persistence_id])
- load_from_id(data[persistence_id])
+ var/list/data = SSpersistence.photo_frames_database.get_key(persistence_id)
+ if(!isnull(data))
+ load_from_id(data)
/obj/structure/sign/picture_frame/proc/load_from_id(id)
var/obj/item/photo/old/P = load_photo_from_disk(id)
@@ -113,6 +105,15 @@
framed = P
update_appearance()
+/// Given a photo (or null), will change the contained picture, and queue a persistent save.
+/obj/structure/sign/picture_frame/proc/set_and_save_framed(obj/item/photo/photo)
+ framed = photo
+
+ if (isnull(persistence_id))
+ return
+
+ SSpersistence.photo_frames_database.set_key(persistence_id, photo?.picture?.id)
+
/obj/structure/sign/picture_frame/examine(mob/user)
. = ..()
if(in_range(src, user))
@@ -141,9 +142,9 @@
tool.play_tool_sound(src)
framed.forceMove(drop_location())
user.visible_message(span_warning("[user] cuts away [framed] from [src]!"))
- framed = null
+ set_and_save_framed(null)
update_appearance()
- return TOOL_ACT_TOOLTYPE_SUCCESS
+ return ITEM_INTERACT_SUCCESS
/obj/structure/sign/picture_frame/attackby(obj/item/I, mob/user, params)
@@ -155,7 +156,7 @@
var/obj/item/photo/P = I
if(!user.transferItemToLoc(P, src))
return
- framed = P
+ set_and_save_framed(P)
update_appearance()
return TRUE
..()
@@ -173,11 +174,11 @@
. += framed
/obj/structure/sign/picture_frame/deconstruct(disassembled = TRUE)
- if(!(flags_1 & NODECONSTRUCT_1))
+ if(!(obj_flags & NO_DECONSTRUCTION))
var/obj/item/wallframe/picture/F = new /obj/item/wallframe/picture(loc)
if(framed)
F.displayed = framed
- framed = null
+ set_and_save_framed(null)
if(contents.len)
var/obj/item/I = pick(contents)
I.forceMove(F)
@@ -277,7 +278,6 @@
/obj/structure/sign/picture_frame/portrait/bar
persistence_id = "frame_bar"
- del_id_on_destroy = TRUE
///Generates a persistence id unique to the current map. Every bar should feel a little bit different after all.
/obj/structure/sign/picture_frame/portrait/bar/Initialize(mapload)
diff --git a/code/modules/plumbing/plumbers/_plumb_machinery.dm b/code/modules/plumbing/plumbers/_plumb_machinery.dm
index be75cf20479dc..dcfa5faac5cbb 100644
--- a/code/modules/plumbing/plumbers/_plumb_machinery.dm
+++ b/code/modules/plumbing/plumbers/_plumb_machinery.dm
@@ -32,7 +32,7 @@
/obj/machinery/plumbing/wrench_act(mob/living/user, obj/item/tool)
. = ..()
default_unfasten_wrench(user, tool)
- return TOOL_ACT_TOOLTYPE_SUCCESS
+ return ITEM_INTERACT_SUCCESS
/obj/machinery/plumbing/plunger_act(obj/item/plunger/P, mob/living/user, reinforced)
to_chat(user, span_notice("You start furiously plunging [name]."))
diff --git a/code/modules/plumbing/plumbers/bottler.dm b/code/modules/plumbing/plumbers/bottler.dm
index bba9238fa2681..8e0158b61da78 100644
--- a/code/modules/plumbing/plumbers/bottler.dm
+++ b/code/modules/plumbing/plumbers/bottler.dm
@@ -79,7 +79,7 @@
return PROCESS_KILL
///see if machine has enough to fill, is anchored down and has any inputspot objects to pick from
- if(reagents.total_volume + 0.01 >= wanted_amount && anchored && length(inputspot.contents))
+ if(reagents.total_volume >= wanted_amount && anchored && length(inputspot.contents))
use_power(active_power_usage * seconds_per_tick)
var/obj/AM = pick(inputspot.contents)///pick a reagent_container that could be used
//allowed containers
@@ -91,13 +91,13 @@
var/obj/item/B = AM
///see if it would overflow else inject
if((B.reagents.total_volume + wanted_amount) <= B.reagents.maximum_volume)
- reagents.trans_to(B, wanted_amount, transferred_by = src)
+ reagents.trans_to(B, wanted_amount)
B.forceMove(goodspot)
return
///glass was full so we move it away
AM.forceMove(badspot)
else if(istype(AM, /obj/item/slime_extract)) ///slime extracts need inject
AM.forceMove(goodspot)
- reagents.trans_to(AM, wanted_amount, transferred_by = src, methods = INJECT)
+ reagents.trans_to(AM, wanted_amount, methods = INJECT)
else if(istype(AM, /obj/item/slimecross/industrial)) ///no need to move slimecross industrial things
- reagents.trans_to(AM, wanted_amount, transferred_by = src, methods = INJECT)
+ reagents.trans_to(AM, wanted_amount, methods = INJECT)
diff --git a/code/modules/plumbing/plumbers/filter.dm b/code/modules/plumbing/plumbers/filter.dm
index 4e4a282bd1dcd..633f70830f016 100644
--- a/code/modules/plumbing/plumbers/filter.dm
+++ b/code/modules/plumbing/plumbers/filter.dm
@@ -38,11 +38,12 @@
switch(action)
if("add")
var/which = params["which"]
- var/selected_reagent = tgui_input_list(usr, "Select [which] reagent", "Reagent", GLOB.chemical_name_list)
+
+ var/selected_reagent = tgui_input_list(usr, "Select [which] reagent", "Reagent", GLOB.name2reagent)
if(!selected_reagent)
return TRUE
- var/chem_id = get_chem_id(selected_reagent)
+ var/datum/reagent/chem_id = GLOB.name2reagent[selected_reagent]
if(!chem_id)
return TRUE
diff --git a/code/modules/plumbing/plumbers/iv_drip.dm b/code/modules/plumbing/plumbers/iv_drip.dm
index 1db36c137e6d7..4bf4d71ec9f9f 100644
--- a/code/modules/plumbing/plumbers/iv_drip.dm
+++ b/code/modules/plumbing/plumbers/iv_drip.dm
@@ -7,9 +7,9 @@
density = TRUE
use_internal_storage = TRUE
-/obj/machinery/iv_drip/plumbing/Initialize(mapload)
+/obj/machinery/iv_drip/plumbing/Initialize(mapload, bolt, layer)
. = ..()
- AddComponent(/datum/component/plumbing/iv_drip, anchored)
+ AddComponent(/datum/component/plumbing/iv_drip, bolt, layer)
AddComponent(/datum/component/simple_rotation)
/obj/machinery/iv_drip/plumbing/add_context(atom/source, list/context, obj/item/held_item, mob/living/user)
@@ -33,9 +33,8 @@
return FALSE //Alt click is used for rotation
/obj/machinery/iv_drip/plumbing/wrench_act(mob/living/user, obj/item/tool)
- . = ..()
if(default_unfasten_wrench(user, tool) == SUCCESSFUL_UNFASTEN)
- return TOOL_ACT_TOOLTYPE_SUCCESS
+ return ITEM_INTERACT_SUCCESS
/obj/machinery/iv_drip/plumbing/deconstruct(disassembled = TRUE)
qdel(src)
diff --git a/code/modules/plumbing/plumbers/pill_press.dm b/code/modules/plumbing/plumbers/pill_press.dm
index f325d12b48d52..26835e7c92d6f 100644
--- a/code/modules/plumbing/plumbers/pill_press.dm
+++ b/code/modules/plumbing/plumbers/pill_press.dm
@@ -78,7 +78,7 @@
return
//shift & check to account for floating point inaccuracies
- if(reagents.total_volume + 0.01 >= current_volume)
+ if(reagents.total_volume >= current_volume)
var/obj/item/reagent_containers/container = locate(packaging_type)
container = new container(src)
var/suffix
diff --git a/code/modules/plumbing/plumbers/plumbing_buffer.dm b/code/modules/plumbing/plumbers/plumbing_buffer.dm
index b2bb21bc24e36..7b3ef306d0419 100644
--- a/code/modules/plumbing/plumbers/plumbing_buffer.dm
+++ b/code/modules/plumbing/plumbers/plumbing_buffer.dm
@@ -33,11 +33,11 @@
SIGNAL_HANDLER
if(!buffer_net)
return
- if(reagents.total_volume + CHEMICAL_QUANTISATION_LEVEL >= activation_volume && mode == UNREADY)
+ if(reagents.total_volume >= activation_volume && mode == UNREADY)
mode = IDLE
buffer_net.check_active()
- else if(reagents.total_volume + CHEMICAL_QUANTISATION_LEVEL < activation_volume && mode != UNREADY)
+ else if(reagents.total_volume < activation_volume && mode != UNREADY)
mode = UNREADY
buffer_net.check_active()
diff --git a/code/modules/plumbing/plumbers/reaction_chamber.dm b/code/modules/plumbing/plumbers/reaction_chamber.dm
index c1e5637840877..2b56bfb4ae6c9 100644
--- a/code/modules/plumbing/plumbers/reaction_chamber.dm
+++ b/code/modules/plumbing/plumbers/reaction_chamber.dm
@@ -3,9 +3,6 @@
/// coefficient to convert temperature to joules. same lvl as acclimator
#define HEATER_COEFFICIENT 0.05
-/// maximum number of attempts the reaction chamber will make to balance the ph(More means better results but higher tick usage)
-#define MAX_PH_ADJUSTMENTS 5
-
/obj/machinery/plumbing/reaction_chamber
name = "mixing chamber"
desc = "Keeps chemicals separated until given conditions are met."
@@ -107,32 +104,34 @@
switch(action)
if("add")
- var/selected_reagent = tgui_input_list(ui.user, "Select reagent", "Reagent", GLOB.chemical_name_list)
+ var/selected_reagent = tgui_input_list(ui.user, "Select reagent", "Reagent", GLOB.name2reagent)
if(!selected_reagent)
- return TRUE
+ return FALSE
- var/input_reagent = get_chem_id(selected_reagent)
+ var/datum/reagent/input_reagent = GLOB.name2reagent[selected_reagent]
if(!input_reagent)
- return TRUE
+ return FALSE
if(!required_reagents.Find(input_reagent))
var/input_amount = text2num(params["amount"])
- if(input_amount)
+ if(!isnull(input_amount))
required_reagents[input_reagent] = input_amount
-
- return TRUE
+ return TRUE
+ return FALSE
if("remove")
var/reagent = get_chem_id(params["chem"])
if(reagent)
required_reagents.Remove(reagent)
- return TRUE
+ return TRUE
+ return FALSE
if("temperature")
var/target = text2num(params["target"])
- if(target != null)
+ if(!isnull(target))
target_temperature = clamp(target, 0, 1000)
- return TRUE
+ return TRUE
+ return FALSE
var/result = handle_ui_act(action, params, ui, state)
if(isnull(result))
@@ -173,8 +172,7 @@
return ..()
/obj/machinery/plumbing/reaction_chamber/chem/handle_reagents(seconds_per_tick)
- var/ph_balance_attempts = 0
- while(ph_balance_attempts < MAX_PH_ADJUSTMENTS && (reagents.ph < acidic_limit || reagents.ph > alkaline_limit))
+ if(reagents.ph < acidic_limit || reagents.ph > alkaline_limit)
//no power
if(machine_stat & NOPOWER)
return
@@ -194,14 +192,13 @@
return
//transfer buffer and handle reactions
- var/ph_change = (reagents.ph > alkaline_limit ? (reagents.ph - alkaline_limit) : (acidic_limit - reagents.ph))
- var/buffer_amount = ((ph_change * reagents.total_volume) / (BUFFER_IONIZING_STRENGTH * num_of_reagents))
- if(!buffer.trans_to(reagents, buffer_amount * seconds_per_tick))
+ var/ph_change = max((reagents.ph > alkaline_limit ? (reagents.ph - alkaline_limit) : (acidic_limit - reagents.ph)), 0.25)
+ var/buffer_amount = ((ph_change * reagents.total_volume) / (BUFFER_IONIZING_STRENGTH * num_of_reagents)) * seconds_per_tick
+ if(!buffer.trans_to(reagents, buffer_amount))
return
//some power for accurate ph balancing & keep track of attempts made
- use_power(active_power_usage * 0.03 * buffer_amount * seconds_per_tick)
- ph_balance_attempts += 1
+ use_power(active_power_usage * 0.03 * buffer_amount)
/obj/machinery/plumbing/reaction_chamber/chem/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
@@ -227,4 +224,3 @@
return FALSE
#undef HEATER_COEFFICIENT
-#undef MAX_PH_ADJUSTMENTS
diff --git a/code/modules/plumbing/plumbers/synthesizer.dm b/code/modules/plumbing/plumbers/synthesizer.dm
index 3dddd648e6165..0e9cb0c1b1125 100644
--- a/code/modules/plumbing/plumbers/synthesizer.dm
+++ b/code/modules/plumbing/plumbers/synthesizer.dm
@@ -2,7 +2,6 @@
/obj/machinery/plumbing/synthesizer
name = "chemical synthesizer"
desc = "Produces a single chemical at a given volume. Must be plumbed. Most effective when working in unison with other chemical synthesizers, heaters and filters."
-
icon_state = "synthesizer"
icon = 'icons/obj/pipes_n_cables/hydrochem/plumbers.dmi'
active_power_usage = BASE_MACHINE_ACTIVE_CONSUMPTION * 2
@@ -52,10 +51,13 @@
/obj/machinery/plumbing/synthesizer/process(seconds_per_tick)
if(machine_stat & NOPOWER || !reagent_id || !amount)
return
- if(reagents.total_volume >= amount*seconds_per_tick*0.5) //otherwise we get leftovers, and we need this to be precise
+
+ //otherwise we get leftovers, and we need this to be precise
+ if(reagents.total_volume >= amount)
return
- reagents.add_reagent(reagent_id, amount*seconds_per_tick*0.5)
- use_power(active_power_usage * amount * seconds_per_tick * 0.5)
+ reagents.add_reagent(reagent_id, amount)
+
+ use_power(active_power_usage)
/obj/machinery/plumbing/synthesizer/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
@@ -63,8 +65,13 @@
ui = new(user, src, "ChemSynthesizer", name)
ui.open()
+/obj/machinery/plumbing/synthesizer/ui_static_data(mob/user)
+ . = ..()
+ .["possible_amounts"] = possible_amounts
+
/obj/machinery/plumbing/synthesizer/ui_data(mob/user)
- var/list/data = list()
+ . = list()
+ .["amount"] = amount
var/is_hallucinating = FALSE
if(isliving(user))
@@ -72,36 +79,35 @@
is_hallucinating = !!living_user.has_status_effect(/datum/status_effect/hallucination)
var/list/chemicals = list()
- for(var/A in dispensable_reagents)
- var/datum/reagent/R = GLOB.chemical_reagents_list[A]
- if(R)
- var/chemname = R.name
+ for(var/reagentID in dispensable_reagents)
+ var/datum/reagent/reagent = GLOB.chemical_reagents_list[reagentID]
+ if(reagent)
+ var/chemname = reagent.name
if(is_hallucinating && prob(5))
chemname = "[pick_list_replacements("hallucination.json", "chemicals")]"
- chemicals.Add(list(list("title" = chemname, "id" = ckey(R.name))))
- data["chemicals"] = chemicals
- data["amount"] = amount
- data["possible_amounts"] = possible_amounts
+ chemicals += list(list("title" = chemname, "id" = reagent.name))
+ .["chemicals"] = chemicals
- data["current_reagent"] = ckey(initial(reagent_id.name))
- return data
+ .["current_reagent"] = initial(reagent_id.name)
/obj/machinery/plumbing/synthesizer/ui_act(action, params)
. = ..()
if(.)
return
- . = TRUE
+
switch(action)
if("amount")
var/new_amount = text2num(params["target"])
if(new_amount in possible_amounts)
amount = new_amount
. = TRUE
+
if("select")
var/new_reagent = GLOB.name2reagent[params["reagent"]]
if(new_reagent in dispensable_reagents)
reagent_id = new_reagent
. = TRUE
+
update_appearance()
reagents.clear_reagents()
diff --git a/code/modules/power/apc/apc_appearance.dm b/code/modules/power/apc/apc_appearance.dm
index 06e0452efada7..41547288a0b73 100644
--- a/code/modules/power/apc/apc_appearance.dm
+++ b/code/modules/power/apc/apc_appearance.dm
@@ -18,11 +18,6 @@
set_light(light_on_range)
return
- if(update_state & UPSTATE_BLUESCREEN)
- set_light_color(LIGHT_COLOR_BLUE)
- set_light(light_on_range)
- return
-
set_light(0)
/obj/machinery/power/apc/update_icon_state()
@@ -39,9 +34,6 @@
if(update_state & UPSTATE_BROKE)
icon_state = "apc-b"
return ..()
- if(update_state & UPSTATE_BLUESCREEN)
- icon_state = "apcemag"
- return ..()
if(update_state & UPSTATE_WIREEXP)
icon_state = "apcewires"
return ..()
@@ -85,8 +77,6 @@
if(cell)
new_update_state |= UPSTATE_CELL_IN
- else if((obj_flags & EMAGGED) || malfai)
- new_update_state |= UPSTATE_BLUESCREEN
else if(panel_open)
new_update_state |= UPSTATE_WIREEXP
@@ -116,3 +106,16 @@
// Used in process so it doesn't update the icon too much
/obj/machinery/power/apc/proc/queue_icon_update()
icon_update_needed = TRUE
+
+// Shows a dark-blue interface for a moment. Shouldn't appear on cameras.
+/obj/machinery/power/apc/proc/flicker_hacked_icon()
+ var/image/hacker_image = image(icon = 'icons/obj/machines/wallmounts.dmi', loc = src, icon_state = "apcemag", layer = FLOAT_LAYER)
+ var/list/mobs_to_show = list()
+ // Collecting mobs the APC can see for this animation, rather than mobs that can see the APC. Important distinction, intended such that mobs on camera / with XRAY cannot see the flicker.
+ for(var/mob/viewer in view(src))
+ if(viewer.client)
+ mobs_to_show += viewer.client
+ if(malfai?.client)
+ mobs_to_show |= malfai.client
+ flick_overlay_global(hacker_image, mobs_to_show, 1 SECONDS)
+ hacked_flicker_counter = rand(3, 5) //The counter is decrimented in the process() proc, which runs every two seconds.
diff --git a/code/modules/power/apc/apc_attack.dm b/code/modules/power/apc/apc_attack.dm
index aaa63c05d853a..509eb4f05b90d 100644
--- a/code/modules/power/apc/apc_attack.dm
+++ b/code/modules/power/apc/apc_attack.dm
@@ -299,6 +299,8 @@
return TRUE
/obj/machinery/power/apc/proc/set_broken()
+ if(machine_stat & BROKEN)
+ return
if(malfai && operating)
malfai.malf_picker.processing_time = clamp(malfai.malf_picker.processing_time - 10,0,1000)
operating = FALSE
diff --git a/code/modules/power/apc/apc_main.dm b/code/modules/power/apc/apc_main.dm
index 3f5a7590c39a6..450142b7e92e4 100644
--- a/code/modules/power/apc/apc_main.dm
+++ b/code/modules/power/apc/apc_main.dm
@@ -71,6 +71,8 @@
var/malfhack = FALSE //New var for my changes to AI malf. --NeoFite
///Reference to our ai hacker
var/mob/living/silicon/ai/malfai = null //See above --NeoFite
+ ///Counter for displaying the hacked overlay to mobs within view
+ var/hacked_flicker_counter = 0
///State of the electronics inside (missing, installed, secured)
var/has_electronics = APC_ELECTRONICS_MISSING
///used for the Blackout malf module
@@ -154,6 +156,10 @@
offset_old = pixel_x
pixel_x = -APC_PIXEL_OFFSET
+ hud_list = list(
+ MALF_APC_HUD = image(icon = 'icons/mob/huds/hud.dmi', icon_state = "apc_hacked", pixel_x = src.pixel_x, pixel_y = src.pixel_y)
+ )
+
//Assign it to its area. If mappers already assigned an area string fast load the area from it else get the current area
var/area/our_area = get_area(loc)
if(areastring)
@@ -227,7 +233,6 @@
QDEL_NULL(cell)
if(terminal)
disconnect_terminal()
-
return ..()
/obj/machinery/power/apc/proc/assign_to_area(area/target_area = get_area(src))
@@ -295,7 +300,7 @@
. += "The cover is closed."
/obj/machinery/power/apc/deconstruct(disassembled = TRUE)
- if(flags_1 & NODECONSTRUCT_1)
+ if(obj_flags & NO_DECONSTRUCTION)
return
if(!(machine_stat & BROKEN))
set_broken()
@@ -458,11 +463,13 @@
update()
if("emergency_lighting")
emergency_lights = !emergency_lights
- for(var/obj/machinery/light/L in area)
- if(!initial(L.no_low_power)) //If there was an override set on creation, keep that override
- L.no_low_power = emergency_lights
- INVOKE_ASYNC(L, TYPE_PROC_REF(/obj/machinery/light/, update), FALSE)
- CHECK_TICK
+ for (var/list/zlevel_turfs as anything in area.get_zlevel_turf_lists())
+ for(var/turf/area_turf as anything in zlevel_turfs)
+ for(var/obj/machinery/light/area_light in area_turf)
+ if(!initial(area_light.no_low_power)) //If there was an override set on creation, keep that override
+ area_light.no_low_power = emergency_lights
+ INVOKE_ASYNC(area_light, TYPE_PROC_REF(/obj/machinery/light/, update), FALSE)
+ CHECK_TICK
return TRUE
/obj/machinery/power/apc/ui_close(mob/user)
@@ -482,6 +489,11 @@
force_update = TRUE
return
+ if(obj_flags & EMAGGED || malfai)
+ hacked_flicker_counter = hacked_flicker_counter - 1
+ if(hacked_flicker_counter <= 0)
+ flicker_hacked_icon()
+
//dont use any power from that channel if we shut that power channel off
lastused_light = APC_CHANNEL_IS_ON(lighting) ? area.power_usage[AREA_USAGE_LIGHT] + area.power_usage[AREA_USAGE_STATIC_LIGHT] : 0
lastused_equip = APC_CHANNEL_IS_ON(equipment) ? area.power_usage[AREA_USAGE_EQUIP] + area.power_usage[AREA_USAGE_STATIC_EQUIP] : 0
@@ -655,10 +667,12 @@
INVOKE_ASYNC(src, PROC_REF(break_lights))
/obj/machinery/power/apc/proc/break_lights()
- for(var/obj/machinery/light/breaked_light in area)
- breaked_light.on = TRUE
- breaked_light.break_light_tube()
- stoplag()
+ for (var/list/zlevel_turfs as anything in area.get_zlevel_turf_lists())
+ for(var/turf/area_turf as anything in zlevel_turfs)
+ for(var/obj/machinery/light/breaked_light in area_turf)
+ breaked_light.on = TRUE
+ breaked_light.break_light_tube()
+ stoplag()
/obj/machinery/power/apc/should_atmos_process(datum/gas_mixture/air, exposed_temperature)
return (exposed_temperature > 2000)
diff --git a/code/modules/power/apc/apc_malf.dm b/code/modules/power/apc/apc_malf.dm
index 3b0703688043d..62134de146e82 100644
--- a/code/modules/power/apc/apc_malf.dm
+++ b/code/modules/power/apc/apc_malf.dm
@@ -37,7 +37,7 @@
if(!is_station_level(z))
return
malf.ShutOffDoomsdayDevice()
- occupier = new /mob/living/silicon/ai(src, malf.laws, malf) //DEAR GOD WHY? //IKR????
+ occupier = new /mob/living/silicon/ai(src, malf.laws.copy_lawset(), malf) //DEAR GOD WHY? //IKR????
occupier.adjustOxyLoss(malf.getOxyLoss())
if(!findtext(occupier.name, "APC Copy"))
occupier.name = "[malf.name] APC Copy"
diff --git a/code/modules/power/apc/apc_power_proc.dm b/code/modules/power/apc/apc_power_proc.dm
index b49c0ba0a74d9..52a671f00f5ae 100644
--- a/code/modules/power/apc/apc_power_proc.dm
+++ b/code/modules/power/apc/apc_power_proc.dm
@@ -138,8 +138,10 @@
if(nightshift_lights == on)
return //no change
nightshift_lights = on
- for(var/obj/machinery/light/night_light in area)
- if(night_light.nightshift_allowed)
- night_light.nightshift_enabled = nightshift_lights
- night_light.update(FALSE)
- CHECK_TICK
+ for (var/list/zlevel_turfs as anything in area.get_zlevel_turf_lists())
+ for(var/turf/area_turf as anything in zlevel_turfs)
+ for(var/obj/machinery/light/night_light in area_turf)
+ if(night_light.nightshift_allowed)
+ night_light.nightshift_enabled = nightshift_lights
+ night_light.update(FALSE)
+ CHECK_TICK
diff --git a/code/modules/power/apc/apc_tool_act.dm b/code/modules/power/apc/apc_tool_act.dm
index 55c9b34a35086..e712f47446481 100644
--- a/code/modules/power/apc/apc_tool_act.dm
+++ b/code/modules/power/apc/apc_tool_act.dm
@@ -77,6 +77,7 @@
return
toggle_panel_open()
balloon_alert(user, "wires [panel_open ? "exposed" : "unexposed"]")
+ W.play_tool_sound(src)
update_appearance()
return
@@ -130,7 +131,7 @@
if(welder.use_tool(src, user, 4 SECONDS, volume = 50))
update_integrity(min(atom_integrity += 50,max_integrity))
balloon_alert(user, "repaired")
- return TOOL_ACT_TOOLTYPE_SUCCESS
+ return ITEM_INTERACT_SUCCESS
//disassembling the frame
if(!opened || has_electronics || terminal)
@@ -220,6 +221,7 @@
locked = FALSE
balloon_alert(user, "interface damaged")
update_appearance()
+ flicker_hacked_icon()
return TRUE
// damage and destruction acts
diff --git a/code/modules/power/cable.dm b/code/modules/power/cable.dm
index 07948e3c4d08f..4480dc981efc3 100644
--- a/code/modules/power/cable.dm
+++ b/code/modules/power/cable.dm
@@ -142,7 +142,7 @@ GLOBAL_LIST_INIT(wire_node_generating_types, typecacheof(list(/obj/structure/gri
return ..() // then go ahead and delete the cable
/obj/structure/cable/deconstruct(disassembled = TRUE)
- if(!(flags_1 & NODECONSTRUCT_1))
+ if(!(obj_flags & NO_DECONSTRUCTION))
var/obj/item/stack/cable_coil/cable = new(drop_location(), 1)
cable.set_cable_color(cable_color)
qdel(src)
@@ -439,7 +439,7 @@ GLOBAL_LIST_INIT(wire_node_generating_types, typecacheof(list(/obj/structure/gri
throw_speed = 3
throw_range = 5
mats_per_unit = list(/datum/material/iron=SMALL_MATERIAL_AMOUNT*0.1, /datum/material/glass=SMALL_MATERIAL_AMOUNT*0.1)
- flags_1 = CONDUCT_1
+ obj_flags = CONDUCTS_ELECTRICITY
slot_flags = ITEM_SLOT_BELT
attack_verb_continuous = list("whips", "lashes", "disciplines", "flogs")
attack_verb_simple = list("whip", "lash", "discipline", "flog")
@@ -464,7 +464,7 @@ GLOBAL_LIST_INIT(wire_node_generating_types, typecacheof(list(/obj/structure/gri
/obj/item/stack/cable_coil/examine(mob/user)
. = ..()
- . += "Ctrl+Click to change the layer you are placing on."
+ . += "Use it in hand to change the layer you are placing on, amongst other things."
/obj/item/stack/cable_coil/update_name()
. = ..()
diff --git a/code/modules/power/cell.dm b/code/modules/power/cell.dm
index 0d0bb09c9975d..ec6c23b00c6f6 100644
--- a/code/modules/power/cell.dm
+++ b/code/modules/power/cell.dm
@@ -20,12 +20,12 @@
throw_speed = 2
throw_range = 5
w_class = WEIGHT_CLASS_SMALL
+ custom_materials = list(/datum/material/iron=SMALL_MATERIAL_AMOUNT*7, /datum/material/glass=SMALL_MATERIAL_AMOUNT*0.5)
+ grind_results = list(/datum/reagent/lithium = 15, /datum/reagent/iron = 5, /datum/reagent/silicon = 5)
///Current charge in cell units
var/charge = 0
///Maximum charge in cell units
var/maxcharge = STANDARD_CELL_CHARGE
- custom_materials = list(/datum/material/iron=SMALL_MATERIAL_AMOUNT*7, /datum/material/glass=SMALL_MATERIAL_AMOUNT*0.5)
- grind_results = list(/datum/reagent/lithium = 15, /datum/reagent/iron = 5, /datum/reagent/silicon = 5)
///If the cell has been booby-trapped by injecting it with plasma. Chance on use() to explode.
var/rigged = FALSE
///If the power cell was damaged by an explosion, chance for it to become corrupted and function the same as rigged.
diff --git a/code/modules/power/floodlight.dm b/code/modules/power/floodlight.dm
index 7155ce6aa2383..d3c5c1de569ae 100644
--- a/code/modules/power/floodlight.dm
+++ b/code/modules/power/floodlight.dm
@@ -261,7 +261,7 @@
connect_to_network()
else
disconnect_from_network()
- return TOOL_ACT_TOOLTYPE_SUCCESS
+ return ITEM_INTERACT_SUCCESS
/obj/machinery/power/floodlight/screwdriver_act(mob/living/user, obj/item/tool)
. = ..()
diff --git a/code/modules/power/generator.dm b/code/modules/power/generator.dm
deleted file mode 100644
index 6fa17d8dbe91a..0000000000000
--- a/code/modules/power/generator.dm
+++ /dev/null
@@ -1,232 +0,0 @@
-/obj/machinery/power/generator
- name = "thermoelectric generator"
- desc = "It's a high efficiency thermoelectric generator."
- icon_state = "teg"
- density = TRUE
- use_power = NO_POWER_USE
-
- circuit = /obj/item/circuitboard/machine/generator
-
- var/obj/machinery/atmospherics/components/binary/circulator/cold_circ
- var/obj/machinery/atmospherics/components/binary/circulator/hot_circ
-
- var/lastgen = 0
- var/lastgenlev = -1
- var/lastcirc = "00"
-
-
-/obj/machinery/power/generator/Initialize(mapload)
- . = ..()
- AddComponent(/datum/component/simple_rotation)
- find_circs()
- connect_to_network()
- SSair.start_processing_machine(src)
- update_appearance()
-
-/obj/machinery/power/generator/Destroy()
- kill_circs()
- SSair.stop_processing_machine(src)
- return ..()
-
-/obj/machinery/power/generator/update_overlays()
- . = ..()
- if(machine_stat & (NOPOWER|BROKEN))
- return
-
- var/L = min(round(lastgenlev / 100000), 11)
- if(L != 0)
- . += mutable_appearance('icons/obj/machines/engine/other.dmi', "teg-op[L]")
- if(hot_circ && cold_circ)
- . += "teg-oc[lastcirc]"
-
-
-#define GENRATE 800 // generator output coefficient from Q
-
-/obj/machinery/power/generator/process_atmos()
-
- if(!cold_circ || !hot_circ)
- return
-
- if(powernet)
- var/datum/gas_mixture/cold_air = cold_circ.return_transfer_air()
- var/datum/gas_mixture/hot_air = hot_circ.return_transfer_air()
-
- if(cold_air && hot_air)
-
- var/cold_air_heat_capacity = cold_air.heat_capacity()
- var/hot_air_heat_capacity = hot_air.heat_capacity()
-
- var/delta_temperature = hot_air.temperature - cold_air.temperature
-
-
- if(delta_temperature > 0 && cold_air_heat_capacity > 0 && hot_air_heat_capacity > 0)
- var/efficiency = 0.65
-
- var/energy_transfer = delta_temperature*hot_air_heat_capacity*cold_air_heat_capacity/(hot_air_heat_capacity+cold_air_heat_capacity)
-
- var/heat = energy_transfer*(1-efficiency)
- lastgen += energy_transfer*efficiency
-
- hot_air.temperature = hot_air.temperature - energy_transfer/hot_air_heat_capacity
- cold_air.temperature = cold_air.temperature + heat/cold_air_heat_capacity
-
- //add_avail(lastgen) This is done in process now
- // update icon overlays only if displayed level has changed
-
- if(hot_air)
- var/datum/gas_mixture/hot_circ_air1 = hot_circ.airs[1]
- hot_circ_air1.merge(hot_air)
-
- if(cold_air)
- var/datum/gas_mixture/cold_circ_air1 = cold_circ.airs[1]
- cold_circ_air1.merge(cold_air)
-
- update_appearance()
-
- var/circ = "[cold_circ?.last_pressure_delta > 0 ? "1" : "0"][hot_circ?.last_pressure_delta > 0 ? "1" : "0"]"
- if(circ != lastcirc)
- lastcirc = circ
- update_appearance()
-
- src.updateDialog()
-
-/obj/machinery/power/generator/process()
- //Setting this number higher just makes the change in power output slower, it doesnt actualy reduce power output cause **math**
- var/power_output = round(lastgen / 10)
- add_avail(power_output)
- lastgenlev = power_output
- lastgen -= power_output
- ..()
-
-/obj/machinery/power/generator/proc/get_menu(include_link = TRUE)
- var/t = ""
- if(!powernet)
- t += "Unable to connect to the power network!"
- else if(cold_circ && hot_circ)
- var/datum/gas_mixture/cold_circ_air1 = cold_circ.airs[1]
- var/datum/gas_mixture/cold_circ_air2 = cold_circ.airs[2]
- var/datum/gas_mixture/hot_circ_air1 = hot_circ.airs[1]
- var/datum/gas_mixture/hot_circ_air2 = hot_circ.airs[2]
-
- t += "
[icon2html(loaded_item, usr)] | [loaded_item.name] Eject |
` element.
- See inherited props: [Box](#box)
- `collapsing: boolean` - Collapses table cell to the smallest possible size,
-and stops any text inside from wrapping.
+ and stops any text inside from wrapping.
### `Tabs`
@@ -1059,9 +1023,7 @@ Tabs also support a vertical configuration. This is usually paired with
```jsx
{nodes} );
};
-export const ListOfButtonsWithLinkEvent = () => {
- const nodes: JSX.Element[] = [];
- for (let i = 0; i < 100; i++) {
- const node = (
-
- );
- nodes.push(node);
- }
- render({nodes} );
-};
-
export const ListOfButtonsWithIcons = () => {
const nodes: JSX.Element[] = [];
for (let i = 0; i < 100; i++) {
diff --git a/tgui/packages/tgui-bench/tests/DisposalUnit.test.tsx b/tgui/packages/tgui-bench/tests/DisposalUnit.test.tsx
index 1ae610e2e2e15..376d27a9115b5 100644
--- a/tgui/packages/tgui-bench/tests/DisposalUnit.test.tsx
+++ b/tgui/packages/tgui-bench/tests/DisposalUnit.test.tsx
@@ -1,22 +1,19 @@
-import { StoreProvider, configureStore } from 'tgui/store';
-
+import { backendUpdate, setGlobalStore } from 'tgui/backend';
import { DisposalUnit } from 'tgui/interfaces/DisposalUnit';
-import { backendUpdate } from 'tgui/backend';
import { createRenderer } from 'tgui/renderer';
+import { configureStore } from 'tgui/store';
const store = configureStore({ sideEffects: false });
const renderUi = createRenderer((dataJson: string) => {
+ setGlobalStore(store);
+
store.dispatch(
backendUpdate({
data: Byond.parseJson(dataJson),
- })
- );
- return (
- \n'
- + messagesHtml
- + ' \n'
- + '\n'
- + '\n';
- // Create and send a nice blob
- const blob = new Blob([pageHtml]);
- const timestamp = new Date()
- .toISOString()
- .substring(0, 19)
- .replace(/[-:]/g, '')
- .replace('T', '-');
- window.navigator.msSaveBlob(blob, `ss13-chatlog-${timestamp}.html`);
- }
-}
-
-// Make chat renderer global so that we can continue using the same
-// instance after hot code replacement.
-if (!window.__chatRenderer__) {
- window.__chatRenderer__ = new ChatRenderer();
-}
-
-/** @type {ChatRenderer} */
-export const chatRenderer = window.__chatRenderer__;
diff --git a/tgui/packages/tgui-panel/chat/renderer.jsx b/tgui/packages/tgui-panel/chat/renderer.jsx
new file mode 100644
index 0000000000000..899839e56cc11
--- /dev/null
+++ b/tgui/packages/tgui-panel/chat/renderer.jsx
@@ -0,0 +1,625 @@
+/**
+ * @file
+ * @copyright 2020 Aleksej Komarov
+ * @license MIT
+ */
+
+import { EventEmitter } from 'common/events';
+import { classes } from 'common/react';
+import { render } from 'react-dom';
+import { Tooltip } from 'tgui/components';
+import { createLogger } from 'tgui/logging';
+
+import {
+ COMBINE_MAX_MESSAGES,
+ COMBINE_MAX_TIME_WINDOW,
+ IMAGE_RETRY_DELAY,
+ IMAGE_RETRY_LIMIT,
+ IMAGE_RETRY_MESSAGE_AGE,
+ MAX_PERSISTED_MESSAGES,
+ MAX_VISIBLE_MESSAGES,
+ MESSAGE_PRUNE_INTERVAL,
+ MESSAGE_TYPE_INTERNAL,
+ MESSAGE_TYPE_UNKNOWN,
+ MESSAGE_TYPES,
+} from './constants';
+import { canPageAcceptType, createMessage, isSameMessage } from './model';
+import { highlightNode, linkifyNode } from './replaceInTextNode';
+
+const logger = createLogger('chatRenderer');
+
+// We consider this as the smallest possible scroll offset
+// that is still trackable.
+const SCROLL_TRACKING_TOLERANCE = 24;
+
+// List of injectable component names to the actual type
+export const TGUI_CHAT_COMPONENTS = {
+ Tooltip,
+};
+
+// List of injectable attibute names mapped to their proper prop
+// We need this because attibutes don't support lowercase names
+export const TGUI_CHAT_ATTRIBUTES_TO_PROPS = {
+ position: 'position',
+ content: 'content',
+};
+
+const findNearestScrollableParent = (startingNode) => {
+ const body = document.body;
+ let node = startingNode;
+ while (node && node !== body) {
+ // This definitely has a vertical scrollbar, because it reduces
+ // scrollWidth of the element. Might not work if element uses
+ // overflow: hidden.
+ if (node.scrollWidth < node.offsetWidth) {
+ return node;
+ }
+ node = node.parentNode;
+ }
+ return window;
+};
+
+const createHighlightNode = (text, color) => {
+ const node = document.createElement('span');
+ node.className = 'Chat__highlight';
+ node.setAttribute('style', 'background-color:' + color);
+ node.textContent = text;
+ return node;
+};
+
+const createMessageNode = () => {
+ const node = document.createElement('div');
+ node.className = 'ChatMessage';
+ return node;
+};
+
+const createReconnectedNode = () => {
+ const node = document.createElement('div');
+ node.className = 'Chat__reconnected';
+ return node;
+};
+
+const handleImageError = (e) => {
+ setTimeout(() => {
+ /** @type {HTMLImageElement} */
+ const node = e.target;
+ const attempts = parseInt(node.getAttribute('data-reload-n'), 10) || 0;
+ if (attempts >= IMAGE_RETRY_LIMIT) {
+ logger.error(`failed to load an image after ${attempts} attempts`);
+ return;
+ }
+ const src = node.src;
+ node.src = null;
+ node.src = src + '#' + attempts;
+ node.setAttribute('data-reload-n', attempts + 1);
+ }, IMAGE_RETRY_DELAY);
+};
+
+/**
+ * Assigns a "times-repeated" badge to the message.
+ */
+const updateMessageBadge = (message) => {
+ const { node, times } = message;
+ if (!node || !times) {
+ // Nothing to update
+ return;
+ }
+ const foundBadge = node.querySelector('.Chat__badge');
+ const badge = foundBadge || document.createElement('div');
+ badge.textContent = times;
+ badge.className = classes(['Chat__badge', 'Chat__badge--animate']);
+ requestAnimationFrame(() => {
+ badge.className = 'Chat__badge';
+ });
+ if (!foundBadge) {
+ node.appendChild(badge);
+ }
+};
+
+class ChatRenderer {
+ constructor() {
+ /** @type {HTMLElement} */
+ this.loaded = false;
+ /** @type {HTMLElement} */
+ this.rootNode = null;
+ this.queue = [];
+ this.messages = [];
+ this.visibleMessages = [];
+ this.page = null;
+ this.events = new EventEmitter();
+ // Scroll handler
+ /** @type {HTMLElement} */
+ this.scrollNode = null;
+ this.scrollTracking = true;
+ this.handleScroll = (type) => {
+ const node = this.scrollNode;
+ const height = node.scrollHeight;
+ const bottom = node.scrollTop + node.offsetHeight;
+ const scrollTracking =
+ Math.abs(height - bottom) < SCROLL_TRACKING_TOLERANCE;
+ if (scrollTracking !== this.scrollTracking) {
+ this.scrollTracking = scrollTracking;
+ this.events.emit('scrollTrackingChanged', scrollTracking);
+ logger.debug('tracking', this.scrollTracking);
+ }
+ };
+ this.ensureScrollTracking = () => {
+ if (this.scrollTracking) {
+ this.scrollToBottom();
+ }
+ };
+ // Periodic message pruning
+ setInterval(() => this.pruneMessages(), MESSAGE_PRUNE_INTERVAL);
+ }
+
+ isReady() {
+ return this.loaded && this.rootNode && this.page;
+ }
+
+ mount(node) {
+ // Mount existing root node on top of the new node
+ if (this.rootNode) {
+ node.appendChild(this.rootNode);
+ }
+ // Initialize the root node
+ else {
+ this.rootNode = node;
+ }
+ // Find scrollable parent
+ this.scrollNode = findNearestScrollableParent(this.rootNode);
+ this.scrollNode.addEventListener('scroll', this.handleScroll);
+ setImmediate(() => {
+ this.scrollToBottom();
+ });
+ // Flush the queue
+ this.tryFlushQueue();
+ }
+
+ onStateLoaded() {
+ this.loaded = true;
+ this.tryFlushQueue();
+ }
+
+ tryFlushQueue() {
+ if (this.isReady() && this.queue.length > 0) {
+ this.processBatch(this.queue);
+ this.queue = [];
+ }
+ }
+
+ assignStyle(style = {}) {
+ for (let key of Object.keys(style)) {
+ this.rootNode.style.setProperty(key, style[key]);
+ }
+ }
+
+ setHighlight(highlightSettings, highlightSettingById) {
+ this.highlightParsers = null;
+ if (!highlightSettings) {
+ return;
+ }
+ highlightSettings.map((id) => {
+ const setting = highlightSettingById[id];
+ const text = setting.highlightText;
+ const highlightColor = setting.highlightColor;
+ const highlightWholeMessage = setting.highlightWholeMessage;
+ const matchWord = setting.matchWord;
+ const matchCase = setting.matchCase;
+ const allowedRegex = /^[a-z0-9_\-$/^[\s\]\\]+$/gi;
+ const regexEscapeCharacters = /[!#$%^&*)(+=.<>{}[\]:;'"|~`_\-\\/]/g;
+ const lines = String(text)
+ .split(',')
+ .map((str) => str.trim())
+ .filter(
+ (str) =>
+ // Must be longer than one character
+ str &&
+ str.length > 1 &&
+ // Must be alphanumeric (with some punctuation)
+ allowedRegex.test(str) &&
+ // Reset lastIndex so it does not mess up the next word
+ ((allowedRegex.lastIndex = 0) || true),
+ );
+ let highlightWords;
+ let highlightRegex;
+ // Nothing to match, reset highlighting
+ if (lines.length === 0) {
+ return;
+ }
+ let regexExpressions = [];
+ // Organize each highlight entry into regex expressions and words
+ for (let line of lines) {
+ // Regex expression syntax is /[exp]/
+ if (line.charAt(0) === '/' && line.charAt(line.length - 1) === '/') {
+ const expr = line.substring(1, line.length - 1);
+ // Check if this is more than one character
+ if (/^(\[.*\]|\\.|.)$/.test(expr)) {
+ continue;
+ }
+ regexExpressions.push(expr);
+ } else {
+ // Lazy init
+ if (!highlightWords) {
+ highlightWords = [];
+ }
+ // We're not going to let regex characters fuck up our RegEx operation.
+ line = line.replace(regexEscapeCharacters, '\\$&');
+
+ highlightWords.push(line);
+ }
+ }
+ const regexStr = regexExpressions.join('|');
+ const flags = 'g' + (matchCase ? '' : 'i');
+ // We wrap this in a try-catch to ensure that broken regex doesn't break
+ // the entire chat.
+ try {
+ // setting regex overrides matchword
+ if (regexStr) {
+ highlightRegex = new RegExp('(' + regexStr + ')', flags);
+ } else {
+ const pattern = `${matchWord ? '\\b' : ''}(${highlightWords.join(
+ '|',
+ )})${matchWord ? '\\b' : ''}`;
+ highlightRegex = new RegExp(pattern, flags);
+ }
+ } catch {
+ // We just reset it if it's invalid.
+ highlightRegex = null;
+ }
+ // Lazy init
+ if (!this.highlightParsers) {
+ this.highlightParsers = [];
+ }
+ this.highlightParsers.push({
+ highlightWords,
+ highlightRegex,
+ highlightColor,
+ highlightWholeMessage,
+ });
+ });
+ }
+
+ scrollToBottom() {
+ // scrollHeight is always bigger than scrollTop and is
+ // automatically clamped to the valid range.
+ this.scrollNode.scrollTop = this.scrollNode.scrollHeight;
+ }
+
+ changePage(page) {
+ if (!this.isReady()) {
+ this.page = page;
+ this.tryFlushQueue();
+ return;
+ }
+ this.page = page;
+ // Fast clear of the root node
+ this.rootNode.textContent = '';
+ this.visibleMessages = [];
+ // Re-add message nodes
+ const fragment = document.createDocumentFragment();
+ let node;
+ for (let message of this.messages) {
+ if (canPageAcceptType(page, message.type)) {
+ node = message.node;
+ fragment.appendChild(node);
+ this.visibleMessages.push(message);
+ }
+ }
+ if (node) {
+ this.rootNode.appendChild(fragment);
+ node.scrollIntoView();
+ }
+ }
+
+ getCombinableMessage(predicate) {
+ const now = Date.now();
+ const len = this.visibleMessages.length;
+ const from = len - 1;
+ const to = Math.max(0, len - COMBINE_MAX_MESSAGES);
+ for (let i = from; i >= to; i--) {
+ const message = this.visibleMessages[i];
+
+ const matches =
+ // Is not an internal message
+ !message.type.startsWith(MESSAGE_TYPE_INTERNAL) &&
+ // Text payload must fully match
+ isSameMessage(message, predicate) &&
+ // Must land within the specified time window
+ now < message.createdAt + COMBINE_MAX_TIME_WINDOW;
+ if (matches) {
+ return message;
+ }
+ }
+ return null;
+ }
+
+ processBatch(batch, options = {}) {
+ const { prepend, notifyListeners = true } = options;
+ const now = Date.now();
+ // Queue up messages until chat is ready
+ if (!this.isReady()) {
+ if (prepend) {
+ this.queue = [...batch, ...this.queue];
+ } else {
+ this.queue = [...this.queue, ...batch];
+ }
+ return;
+ }
+ // Insert messages
+ const fragment = document.createDocumentFragment();
+ const countByType = {};
+ let node;
+ for (let payload of batch) {
+ const message = createMessage(payload);
+ // Combine messages
+ const combinable = this.getCombinableMessage(message);
+ if (combinable) {
+ combinable.times = (combinable.times || 1) + 1;
+ updateMessageBadge(combinable);
+ continue;
+ }
+ // Reuse message node
+ if (message.node) {
+ node = message.node;
+ }
+ // Reconnected
+ else if (message.type === 'internal/reconnected') {
+ node = createReconnectedNode();
+ }
+ // Create message node
+ else {
+ node = createMessageNode();
+ // Payload is plain text
+ if (message.text) {
+ node.textContent = message.text;
+ }
+ // Payload is HTML
+ else if (message.html) {
+ node.innerHTML = message.html;
+ } else {
+ logger.error('Error: message is missing text payload', message);
+ }
+ // Get all nodes in this message that want to be rendered like jsx
+ const nodes = node.querySelectorAll('[data-component]');
+ for (let i = 0; i < nodes.length; i++) {
+ const childNode = nodes[i];
+ const targetName = childNode.getAttribute('data-component');
+ // Let's pull out the attibute info we need
+ let outputProps = {};
+ for (let j = 0; j < childNode.attributes.length; j++) {
+ const attribute = childNode.attributes[j];
+
+ let working_value = attribute.nodeValue;
+ // We can't do the "if it has no value it's truthy" trick
+ // Because getAttribute returns "", not null. Hate IE
+ if (working_value === '$true') {
+ working_value = true;
+ } else if (working_value === '$false') {
+ working_value = false;
+ } else if (!isNaN(working_value)) {
+ const parsed_float = parseFloat(working_value);
+ if (!isNaN(parsed_float)) {
+ working_value = parsed_float;
+ }
+ }
+
+ let canon_name = attribute.nodeName.replace('data-', '');
+ // html attributes don't support upper case chars, so we need to map
+ canon_name = TGUI_CHAT_ATTRIBUTES_TO_PROPS[canon_name];
+ outputProps[canon_name] = working_value;
+ }
+ const oldHtml = { __html: childNode.innerHTML };
+ while (childNode.firstChild) {
+ childNode.removeChild(childNode.firstChild);
+ }
+ const Element = TGUI_CHAT_COMPONENTS[targetName];
+ /* eslint-disable react/no-danger */
+ render(
+ \n' +
+ messagesHtml +
+ ' \n' +
+ '\n' +
+ '\n';
+ // Create and send a nice blob
+ const blob = new Blob([pageHtml]);
+ const timestamp = new Date()
+ .toISOString()
+ .substring(0, 19)
+ .replace(/[-:]/g, '')
+ .replace('T', '-');
+ window.navigator.msSaveBlob(blob, `ss13-chatlog-${timestamp}.html`);
+ }
+}
+
+// Make chat renderer global so that we can continue using the same
+// instance after hot code replacement.
+if (!window.__chatRenderer__) {
+ window.__chatRenderer__ = new ChatRenderer();
+}
+
+/** @type {ChatRenderer} */
+export const chatRenderer = window.__chatRenderer__;
diff --git a/tgui/packages/tgui-panel/chat/replaceInTextNode.js b/tgui/packages/tgui-panel/chat/replaceInTextNode.js
index 753997b3b821c..4c91b604dacf7 100644
--- a/tgui/packages/tgui-panel/chat/replaceInTextNode.js
+++ b/tgui/packages/tgui-panel/chat/replaceInTextNode.js
@@ -149,7 +149,7 @@ export const highlightNode = (
node,
regex,
words,
- createNode = createHighlightNode
+ createNode = createHighlightNode,
) => {
if (!createNode) {
createNode = createHighlightNode;
@@ -171,8 +171,8 @@ export const highlightNode = (
// Linkify
// --------------------------------------------------------
-// prettier-ignore
-const URL_REGEX = /(?:(?:https?:\/\/)|(?:www\.))(?:[^ ]*?\.[^ ]*?)+[-A-Za-z0-9+&@#/%?=~_|$!:,.;(){}]+/ig;
+const URL_REGEX =
+ /(?:(?:https?:\/\/)|(?:www\.))(?:[^ ]*?\.[^ ]*?)+[-A-Za-z0-9+&@#/%?=~_|$!:,.;(){}]+/gi;
/**
* Highlights the text in the node based on the provided regular expression.
diff --git a/tgui/packages/tgui-panel/chat/selectors.js b/tgui/packages/tgui-panel/chat/selectors.js
deleted file mode 100644
index 6352b7cddf0af..0000000000000
--- a/tgui/packages/tgui-panel/chat/selectors.js
+++ /dev/null
@@ -1,17 +0,0 @@
-/**
- * @file
- * @copyright 2020 Aleksej Komarov
- * @license MIT
- */
-
-import { map } from 'common/collections';
-
-export const selectChat = (state) => state.chat;
-
-export const selectChatPages = (state) =>
- map((id) => state.chat.pageById[id])(state.chat.pages);
-
-export const selectCurrentChatPage = (state) =>
- state.chat.pageById[state.chat.currentPageId];
-
-export const selectChatPageById = (id) => (state) => state.chat.pageById[id];
diff --git a/tgui/packages/tgui-panel/chat/selectors.ts b/tgui/packages/tgui-panel/chat/selectors.ts
new file mode 100644
index 0000000000000..2908f661264a2
--- /dev/null
+++ b/tgui/packages/tgui-panel/chat/selectors.ts
@@ -0,0 +1,17 @@
+/**
+ * @file
+ * @copyright 2020 Aleksej Komarov
+ * @license MIT
+ */
+
+import { map } from 'common/collections';
+
+export const selectChat = (state) => state.chat;
+
+export const selectChatPages = (state) =>
+ map((id: string) => state.chat.pageById[id])(state.chat.pages);
+
+export const selectCurrentChatPage = (state) =>
+ state.chat.pageById[state.chat.currentPageId];
+
+export const selectChatPageById = (id) => (state) => state.chat.pageById[id];
diff --git a/tgui/packages/tgui-panel/game/actions.js b/tgui/packages/tgui-panel/game/actions.ts
similarity index 100%
rename from tgui/packages/tgui-panel/game/actions.js
rename to tgui/packages/tgui-panel/game/actions.ts
diff --git a/tgui/packages/tgui-panel/game/constants.js b/tgui/packages/tgui-panel/game/constants.js
deleted file mode 100644
index f40a529a101aa..0000000000000
--- a/tgui/packages/tgui-panel/game/constants.js
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * @file
- * @copyright 2020 Aleksej Komarov
- * @license MIT
- */
-
-export const CONNECTION_LOST_AFTER = 20000;
diff --git a/tgui/packages/tgui-panel/game/constants.ts b/tgui/packages/tgui-panel/game/constants.ts
new file mode 100644
index 0000000000000..d692e26c213f1
--- /dev/null
+++ b/tgui/packages/tgui-panel/game/constants.ts
@@ -0,0 +1,7 @@
+/**
+ * @file
+ * @copyright 2020 Aleksej Komarov
+ * @license MIT
+ */
+
+export const CONNECTION_LOST_AFTER = 45000;
diff --git a/tgui/packages/tgui-panel/game/hooks.js b/tgui/packages/tgui-panel/game/hooks.js
deleted file mode 100644
index 859aaa09a4072..0000000000000
--- a/tgui/packages/tgui-panel/game/hooks.js
+++ /dev/null
@@ -1,12 +0,0 @@
-/**
- * @file
- * @copyright 2020 Aleksej Komarov
- * @license MIT
- */
-
-import { useSelector } from 'common/redux';
-import { selectGame } from './selectors';
-
-export const useGame = (context) => {
- return useSelector(context, selectGame);
-};
diff --git a/tgui/packages/tgui-panel/game/hooks.ts b/tgui/packages/tgui-panel/game/hooks.ts
new file mode 100644
index 0000000000000..40a74ff44a068
--- /dev/null
+++ b/tgui/packages/tgui-panel/game/hooks.ts
@@ -0,0 +1,13 @@
+/**
+ * @file
+ * @copyright 2020 Aleksej Komarov
+ * @license MIT
+ */
+
+import { useSelector } from 'tgui/backend';
+
+import { selectGame } from './selectors';
+
+export const useGame = () => {
+ return useSelector(selectGame);
+};
diff --git a/tgui/packages/tgui-panel/game/index.js b/tgui/packages/tgui-panel/game/index.ts
similarity index 100%
rename from tgui/packages/tgui-panel/game/index.js
rename to tgui/packages/tgui-panel/game/index.ts
diff --git a/tgui/packages/tgui-panel/game/middleware.js b/tgui/packages/tgui-panel/game/middleware.js
index 53dd45bb46e0c..1cf80a7ac63a1 100644
--- a/tgui/packages/tgui-panel/game/middleware.js
+++ b/tgui/packages/tgui-panel/game/middleware.js
@@ -6,8 +6,8 @@
import { pingSoft, pingSuccess } from '../ping/actions';
import { connectionLost, connectionRestored, roundRestarted } from './actions';
-import { selectGame } from './selectors';
import { CONNECTION_LOST_AFTER } from './constants';
+import { selectGame } from './selectors';
const withTimestamp = (action) => ({
...action,
diff --git a/tgui/packages/tgui-panel/game/reducer.js b/tgui/packages/tgui-panel/game/reducer.js
deleted file mode 100644
index 97535524c560b..0000000000000
--- a/tgui/packages/tgui-panel/game/reducer.js
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * @file
- * @copyright 2020 Aleksej Komarov
- * @license MIT
- */
-
-import { connectionLost } from './actions';
-import { connectionRestored } from './actions';
-
-const initialState = {
- // TODO: This is where round info should be.
- roundId: null,
- roundTime: null,
- roundRestartedAt: null,
- connectionLostAt: null,
-};
-
-export const gameReducer = (state = initialState, action) => {
- const { type, payload, meta } = action;
- if (type === 'roundrestart') {
- return {
- ...state,
- roundRestartedAt: meta.now,
- };
- }
- if (type === connectionLost.type) {
- return {
- ...state,
- connectionLostAt: meta.now,
- };
- }
- if (type === connectionRestored.type) {
- return {
- ...state,
- connectionLostAt: null,
- };
- }
- return state;
-};
diff --git a/tgui/packages/tgui-panel/game/reducer.ts b/tgui/packages/tgui-panel/game/reducer.ts
new file mode 100644
index 0000000000000..c446db8f029b0
--- /dev/null
+++ b/tgui/packages/tgui-panel/game/reducer.ts
@@ -0,0 +1,39 @@
+/**
+ * @file
+ * @copyright 2020 Aleksej Komarov
+ * @license MIT
+ */
+
+import { connectionLost } from './actions';
+import { connectionRestored } from './actions';
+
+const initialState = {
+ // TODO: This is where round info should be.
+ roundId: null,
+ roundTime: null,
+ roundRestartedAt: null,
+ connectionLostAt: null,
+};
+
+export const gameReducer = (state = initialState, action) => {
+ const { type, meta } = action;
+ if (type === 'roundrestart') {
+ return {
+ ...state,
+ roundRestartedAt: meta.now,
+ };
+ }
+ if (type === connectionLost.type) {
+ return {
+ ...state,
+ connectionLostAt: meta.now,
+ };
+ }
+ if (type === connectionRestored.type) {
+ return {
+ ...state,
+ connectionLostAt: null,
+ };
+ }
+ return state;
+};
diff --git a/tgui/packages/tgui-panel/game/selectors.js b/tgui/packages/tgui-panel/game/selectors.ts
similarity index 100%
rename from tgui/packages/tgui-panel/game/selectors.js
rename to tgui/packages/tgui-panel/game/selectors.ts
diff --git a/tgui/packages/tgui-panel/index.js b/tgui/packages/tgui-panel/index.js
deleted file mode 100644
index 6bc6b32c4622b..0000000000000
--- a/tgui/packages/tgui-panel/index.js
+++ /dev/null
@@ -1,114 +0,0 @@
-/**
- * @file
- * @copyright 2020 Aleksej Komarov
- * @license MIT
- */
-
-// Themes
-import './styles/main.scss';
-import './styles/themes/light.scss';
-
-import { perf } from 'common/perf';
-import { combineReducers } from 'common/redux';
-import { setupHotReloading } from 'tgui-dev-server/link/client.cjs';
-import { setupGlobalEvents } from 'tgui/events';
-import { captureExternalLinks } from 'tgui/links';
-import { createRenderer } from 'tgui/renderer';
-import { configureStore, StoreProvider } from 'tgui/store';
-import { audioMiddleware, audioReducer } from './audio';
-import { chatMiddleware, chatReducer } from './chat';
-import { gameMiddleware, gameReducer } from './game';
-import { setupPanelFocusHacks } from './panelFocus';
-import { pingMiddleware, pingReducer } from './ping';
-import { settingsMiddleware, settingsReducer } from './settings';
-import { telemetryMiddleware } from './telemetry';
-
-perf.mark('inception', window.performance?.timing?.navigationStart);
-perf.mark('init');
-
-const store = configureStore({
- reducer: combineReducers({
- audio: audioReducer,
- chat: chatReducer,
- game: gameReducer,
- ping: pingReducer,
- settings: settingsReducer,
- }),
- middleware: {
- pre: [
- chatMiddleware,
- pingMiddleware,
- telemetryMiddleware,
- settingsMiddleware,
- audioMiddleware,
- gameMiddleware,
- ],
- },
-});
-
-const renderApp = createRenderer(() => {
- const { Panel } = require('./Panel');
- return (
-
-
- );
-};
diff --git a/tgui/packages/tgui-panel/ping/PingIndicator.jsx b/tgui/packages/tgui-panel/ping/PingIndicator.jsx
new file mode 100644
index 0000000000000..549cd09cf74a8
--- /dev/null
+++ b/tgui/packages/tgui-panel/ping/PingIndicator.jsx
@@ -0,0 +1,28 @@
+/**
+ * @file
+ * @copyright 2020 Aleksej Komarov
+ * @license MIT
+ */
+
+import { Color } from 'common/color';
+import { toFixed } from 'common/math';
+import { useSelector } from 'tgui/backend';
+import { Box } from 'tgui/components';
+
+import { selectPing } from './selectors';
+
+export const PingIndicator = (props) => {
+ const ping = useSelector(selectPing);
+ const color = Color.lookup(ping.networkQuality, [
+ new Color(220, 40, 40),
+ new Color(220, 200, 40),
+ new Color(60, 220, 40),
+ ]);
+ const roundtrip = ping.roundtrip ? toFixed(ping.roundtrip) : '--';
+ return (
+
+
+ );
+};
diff --git a/tgui/packages/tgui-panel/ping/actions.js b/tgui/packages/tgui-panel/ping/actions.ts
similarity index 100%
rename from tgui/packages/tgui-panel/ping/actions.js
rename to tgui/packages/tgui-panel/ping/actions.ts
diff --git a/tgui/packages/tgui-panel/ping/constants.js b/tgui/packages/tgui-panel/ping/constants.ts
similarity index 100%
rename from tgui/packages/tgui-panel/ping/constants.js
rename to tgui/packages/tgui-panel/ping/constants.ts
diff --git a/tgui/packages/tgui-panel/ping/index.js b/tgui/packages/tgui-panel/ping/index.ts
similarity index 100%
rename from tgui/packages/tgui-panel/ping/index.js
rename to tgui/packages/tgui-panel/ping/index.ts
diff --git a/tgui/packages/tgui-panel/ping/reducer.js b/tgui/packages/tgui-panel/ping/reducer.js
deleted file mode 100644
index b1e3d679cbcc3..0000000000000
--- a/tgui/packages/tgui-panel/ping/reducer.js
+++ /dev/null
@@ -1,46 +0,0 @@
-/**
- * @file
- * @copyright 2020 Aleksej Komarov
- * @license MIT
- */
-
-import { clamp01, scale } from 'common/math';
-import { pingFail, pingSuccess } from './actions';
-import { PING_MAX_FAILS, PING_ROUNDTRIP_BEST, PING_ROUNDTRIP_WORST } from './constants';
-
-export const pingReducer = (state = {}, action) => {
- const { type, payload } = action;
-
- if (type === pingSuccess.type) {
- const { roundtrip } = payload;
- const prevRoundtrip = state.roundtripAvg || roundtrip;
- const roundtripAvg = Math.round(prevRoundtrip * 0.4 + roundtrip * 0.6);
- const networkQuality =
- 1 - scale(roundtripAvg, PING_ROUNDTRIP_BEST, PING_ROUNDTRIP_WORST);
- return {
- roundtrip,
- roundtripAvg,
- failCount: 0,
- networkQuality,
- };
- }
-
- if (type === pingFail.type) {
- const { failCount = 0 } = state;
- const networkQuality = clamp01(
- state.networkQuality - failCount / PING_MAX_FAILS
- );
- const nextState = {
- ...state,
- failCount: failCount + 1,
- networkQuality,
- };
- if (failCount > PING_MAX_FAILS) {
- nextState.roundtrip = undefined;
- nextState.roundtripAvg = undefined;
- }
- return nextState;
- }
-
- return state;
-};
diff --git a/tgui/packages/tgui-panel/ping/reducer.ts b/tgui/packages/tgui-panel/ping/reducer.ts
new file mode 100644
index 0000000000000..10531d23d8477
--- /dev/null
+++ b/tgui/packages/tgui-panel/ping/reducer.ts
@@ -0,0 +1,58 @@
+/**
+ * @file
+ * @copyright 2020 Aleksej Komarov
+ * @license MIT
+ */
+
+import { clamp01, scale } from 'common/math';
+
+import { pingFail, pingSuccess } from './actions';
+import {
+ PING_MAX_FAILS,
+ PING_ROUNDTRIP_BEST,
+ PING_ROUNDTRIP_WORST,
+} from './constants';
+
+type PingState = {
+ roundtrip: number | undefined;
+ roundtripAvg: number | undefined;
+ failCount: number;
+ networkQuality: number;
+};
+
+export const pingReducer = (state = {} as PingState, action) => {
+ const { type, payload } = action;
+
+ if (type === pingSuccess.type) {
+ const { roundtrip } = payload;
+ const prevRoundtrip = state.roundtripAvg || roundtrip;
+ const roundtripAvg = Math.round(prevRoundtrip * 0.4 + roundtrip * 0.6);
+ const networkQuality =
+ 1 - scale(roundtripAvg, PING_ROUNDTRIP_BEST, PING_ROUNDTRIP_WORST);
+ return {
+ roundtrip,
+ roundtripAvg,
+ failCount: 0,
+ networkQuality,
+ };
+ }
+
+ if (type === pingFail.type) {
+ const { failCount = 0 } = state;
+ const networkQuality = clamp01(
+ state.networkQuality - failCount / PING_MAX_FAILS,
+ );
+ const nextState: PingState = {
+ ...state,
+ failCount: failCount + 1,
+ networkQuality,
+ };
+ if (failCount > PING_MAX_FAILS) {
+ nextState.roundtrip = undefined;
+ nextState.roundtripAvg = undefined;
+ }
+ return nextState;
+ }
+
+ return state;
+};
diff --git a/tgui/packages/tgui-panel/ping/selectors.js b/tgui/packages/tgui-panel/ping/selectors.ts
similarity index 100%
rename from tgui/packages/tgui-panel/ping/selectors.js
rename to tgui/packages/tgui-panel/ping/selectors.ts
diff --git a/tgui/packages/tgui-panel/reconnect.tsx b/tgui/packages/tgui-panel/reconnect.tsx
index ecfd76716925b..6d3e6d9759e26 100644
--- a/tgui/packages/tgui-panel/reconnect.tsx
+++ b/tgui/packages/tgui-panel/reconnect.tsx
@@ -21,7 +21,8 @@ export const ReconnectButton = () => {
color="white"
onClick={() => {
Byond.command('.reconnect');
- }}>
+ }}
+ >
Reconnect
>
diff --git a/tgui/packages/tgui-panel/settings/SettingsPanel.js b/tgui/packages/tgui-panel/settings/SettingsPanel.js
deleted file mode 100644
index 90abe4e36b26a..0000000000000
--- a/tgui/packages/tgui-panel/settings/SettingsPanel.js
+++ /dev/null
@@ -1,311 +0,0 @@
-/**
- * @file
- * @copyright 2020 Aleksej Komarov
- * @license MIT
- */
-
-import { toFixed } from 'common/math';
-import { useLocalState } from 'tgui/backend';
-import { useDispatch, useSelector } from 'common/redux';
-import { Box, Button, ColorBox, Divider, Dropdown, Flex, Input, LabeledList, NumberInput, Section, Stack, Tabs, TextArea } from 'tgui/components';
-import { ChatPageSettings } from '../chat';
-import { rebuildChat, saveChatToDisk } from '../chat/actions';
-import { THEMES } from '../themes';
-import { changeSettingsTab, updateSettings, addHighlightSetting, removeHighlightSetting, updateHighlightSetting } from './actions';
-import { SETTINGS_TABS, FONTS, MAX_HIGHLIGHT_SETTINGS } from './constants';
-import { selectActiveTab, selectSettings, selectHighlightSettings, selectHighlightSettingById } from './selectors';
-
-export const SettingsPanel = (props, context) => {
- const activeTab = useSelector(context, selectActiveTab);
- const dispatch = useDispatch(context);
- return (
-
+
+
+
{checked &&
@@ -205,7 +211,8 @@ const MenuItem = (props) => {
'MenuBar__MenuItem',
'MenuBar__hover',
])}
- onClick={() => onClick(value)}>
+ onClick={() => onClick(value)}
+ >
{displayText}
);
diff --git a/tgui/packages/tgui/components/Modal.js b/tgui/packages/tgui/components/Modal.js
deleted file mode 100644
index 9cf990c45bb96..0000000000000
--- a/tgui/packages/tgui/components/Modal.js
+++ /dev/null
@@ -1,22 +0,0 @@
-/**
- * @file
- * @copyright 2020 Aleksej Komarov
- * @license MIT
- */
-
-import { classes } from 'common/react';
-import { computeBoxClassName, computeBoxProps } from './Box';
-import { Dimmer } from './Dimmer';
-
-export const Modal = (props) => {
- const { className, children, ...rest } = props;
- return (
-
- {children}
-
-
+ {children}
+
+
- {
- (animated && !dragging && !suppressingFlicker) ?
- (
- );
-
- return (
-
-
-
- {contentElement}
- {
- if (!editing) {
- return;
- }
- const value = clamp(parseFloat(e.target.value), minValue, maxValue);
- if (Number.isNaN(value)) {
- this.setState({
- editing: false,
- });
- return;
- }
- this.setState({
- editing: false,
- value,
- });
- this.suppressFlicker();
- if (onChange) {
- onChange(e, value);
- }
- if (onDrag) {
- onDrag(e, value);
- }
- }}
- onKeyDown={(e) => {
- if (e.keyCode === 13) {
- // prettier-ignore
- const value = clamp(
- parseFloat(e.target.value),
- minValue,
- maxValue
- );
- if (Number.isNaN(value)) {
- this.setState({
- editing: false,
- });
- return;
- }
- this.setState({
- editing: false,
- value,
- });
- this.suppressFlicker();
- if (onChange) {
- onChange(e, value);
- }
- if (onDrag) {
- onDrag(e, value);
- }
- return;
- }
- if (e.keyCode === 27) {
- this.setState({
- editing: false,
- });
- return;
- }
- }}
- />
-
+ {animated && !dragging && !suppressingFlicker ? (
+
+ );
+
+ return (
+
+
+
+ {contentElement}
+ {
+ if (!editing) {
+ return;
+ }
+ const value = clamp(parseFloat(e.target.value), minValue, maxValue);
+ if (Number.isNaN(value)) {
+ this.setState({
+ editing: false,
+ });
+ return;
+ }
+ this.setState({
+ editing: false,
+ value,
+ });
+ this.suppressFlicker();
+ if (onChange) {
+ onChange(e, value);
+ }
+ if (onDrag) {
+ onDrag(e, value);
+ }
+ }}
+ onKeyDown={(e) => {
+ if (e.keyCode === 13) {
+ // prettier-ignore
+ const value = clamp(
+ parseFloat(e.target.value),
+ minValue,
+ maxValue
+ );
+ if (Number.isNaN(value)) {
+ this.setState({
+ editing: false,
+ });
+ return;
+ }
+ this.setState({
+ editing: false,
+ value,
+ });
+ this.suppressFlicker();
+ if (onChange) {
+ onChange(e, value);
+ }
+ if (onDrag) {
+ onDrag(e, value);
+ }
+ return;
+ }
+ if (e.keyCode === 27) {
+ this.setState({
+ editing: false,
+ });
+ return;
+ }
+ }}
+ />
+ {
+ setReferenceElement(node);
+ parentRef.current = node;
+ }}
+ >
+ {children}
+
+ {isOpen && (
+ {
+ setPopperElement(node);
+ popperRef.current = node;
+ }}
+ style={{ ...styles.popper, zIndex: 5 }}
+ {...attributes.popper}
+ >
+ {content}
+
+ )}
+ >
+ );
}
diff --git a/tgui/packages/tgui/components/ProgressBar.js b/tgui/packages/tgui/components/ProgressBar.js
deleted file mode 100644
index 86116732c3f22..0000000000000
--- a/tgui/packages/tgui/components/ProgressBar.js
+++ /dev/null
@@ -1,66 +0,0 @@
-/**
- * @file
- * @copyright 2020 Aleksej Komarov
- * @license MIT
- */
-
-import { clamp01, scale, keyOfMatchingRange, toFixed } from 'common/math';
-import { classes, pureComponentHooks } from 'common/react';
-import { computeBoxClassName, computeBoxProps } from './Box';
-import { CSS_COLORS } from '../constants';
-
-export const ProgressBar = (props) => {
- const {
- className,
- value,
- minValue = 0,
- maxValue = 1,
- color,
- ranges = {},
- children,
- ...rest
- } = props;
- const scaledValue = scale(value, minValue, maxValue);
- const hasContent = children !== undefined;
- // prettier-ignore
- const effectiveColor = color
- || keyOfMatchingRange(value, ranges)
- || 'default';
-
- // We permit colors to be in hex format, rgb()/rgba() format,
- // a name for a color-
-
-
- );
-};
-
-ProgressBar.defaultHooks = pureComponentHooks;
diff --git a/tgui/packages/tgui/components/ProgressBar.tsx b/tgui/packages/tgui/components/ProgressBar.tsx
new file mode 100644
index 0000000000000..ce2f071ba086d
--- /dev/null
+++ b/tgui/packages/tgui/components/ProgressBar.tsx
@@ -0,0 +1,79 @@
+/**
+ * @file
+ * @copyright 2020 Aleksej Komarov
+ * @license MIT
+ */
+
+import { clamp01, keyOfMatchingRange, scale, toFixed } from 'common/math';
+import { classes } from 'common/react';
+import { PropsWithChildren } from 'react';
+
+import { CSS_COLORS } from '../constants';
+import { BoxProps, computeBoxClassName, computeBoxProps } from './Box';
+
+type Props = {
+ value: number;
+} & Partial<{
+ backgroundColor: string;
+ className: string;
+ color: string;
+ height: string | number;
+ maxValue: number;
+ minValue: number;
+ ranges: Record
- {hasContent ? children : toFixed(scaledValue * 100) + '%'}
-
-
+
+
+ );
+};
diff --git a/tgui/packages/tgui/components/RestrictedInput.js b/tgui/packages/tgui/components/RestrictedInput.js
deleted file mode 100644
index 1b082296ca51e..0000000000000
--- a/tgui/packages/tgui/components/RestrictedInput.js
+++ /dev/null
@@ -1,176 +0,0 @@
-import { classes } from 'common/react';
-import { clamp } from 'common/math';
-import { Component, createRef } from 'inferno';
-import { Box } from './Box';
-import { KEY_ESCAPE, KEY_ENTER } from 'common/keycodes';
-
-const DEFAULT_MIN = 0;
-const DEFAULT_MAX = 10000;
-
-/**
- * Takes a string input and parses integers or floats from it.
- * If none: Minimum is set.
- * Else: Clamps it to the given range.
- */
-const getClampedNumber = (value, minValue, maxValue, allowFloats) => {
- const minimum = minValue || DEFAULT_MIN;
- const maximum = maxValue || maxValue === 0 ? maxValue : DEFAULT_MAX;
- if (!value || !value.length) {
- return String(minimum);
- }
- let parsedValue = allowFloats
- ? parseFloat(value.replace(/[^\-\d.]/g, ''))
- : parseInt(value.replace(/[^\-\d]/g, ''), 10);
- if (isNaN(parsedValue)) {
- return String(minimum);
- } else {
- return String(clamp(parsedValue, minimum, maximum));
- }
-};
-
-export class RestrictedInput extends Component {
- constructor() {
- super();
- this.inputRef = createRef();
- this.state = {
- editing: false,
- };
- this.handleBlur = (e) => {
- const { editing } = this.state;
- if (editing) {
- this.setEditing(false);
- }
- };
- this.handleChange = (e) => {
- const { maxValue, minValue, onChange, allowFloats } = this.props;
- e.target.value = getClampedNumber(
- e.target.value,
- minValue,
- maxValue,
- allowFloats
- );
- if (onChange) {
- onChange(e, +e.target.value);
- }
- };
- this.handleFocus = (e) => {
- const { editing } = this.state;
- if (!editing) {
- this.setEditing(true);
- }
- };
- this.handleInput = (e) => {
- const { editing } = this.state;
- const { onInput } = this.props;
- if (!editing) {
- this.setEditing(true);
- }
- if (onInput) {
- onInput(e, +e.target.value);
- }
- };
- this.handleKeyDown = (e) => {
- const { maxValue, minValue, onChange, onEnter, allowFloats } = this.props;
- if (e.keyCode === KEY_ENTER) {
- const safeNum = getClampedNumber(
- e.target.value,
- minValue,
- maxValue,
- allowFloats
- );
- this.setEditing(false);
- if (onChange) {
- onChange(e, +safeNum);
- }
- if (onEnter) {
- onEnter(e, +safeNum);
- }
- e.target.blur();
- return;
- }
- if (e.keyCode === KEY_ESCAPE) {
- if (this.props.onEscape) {
- this.props.onEscape(e);
- return;
- }
- this.setEditing(false);
- e.target.value = this.props.value;
- e.target.blur();
- return;
- }
- };
- }
-
- componentDidMount() {
- const { maxValue, minValue, allowFloats } = this.props;
- const nextValue = this.props.value?.toString();
- const input = this.inputRef.current;
- if (input) {
- input.value = getClampedNumber(
- nextValue,
- minValue,
- maxValue,
- allowFloats
- );
- }
- if (this.props.autoFocus || this.props.autoSelect) {
- setTimeout(() => {
- input.focus();
-
- if (this.props.autoSelect) {
- input.select();
- }
- }, 1);
- }
- }
-
- componentDidUpdate(prevProps, _) {
- const { maxValue, minValue, allowFloats } = this.props;
- const { editing } = this.state;
- const prevValue = prevProps.value?.toString();
- const nextValue = this.props.value?.toString();
- const input = this.inputRef.current;
- if (input && !editing) {
- if (nextValue !== prevValue && nextValue !== input.value) {
- input.value = getClampedNumber(
- nextValue,
- minValue,
- maxValue,
- allowFloats
- );
- }
- }
- }
-
- setEditing(editing) {
- this.setState({ editing });
- }
-
- render() {
- const { props } = this;
- const { onChange, onEnter, onInput, value, ...boxProps } = props;
- const { className, fluid, monospace, ...rest } = boxProps;
- return (
-
+ {hasContent ? children : toFixed(scaledValue * 100) + '%'}
+
+ .
-
- .
+
+
-
-
-
+
+
+ {
className,
computeBoxClassName(rest),
])}
- {...computeBoxProps(rest)}>
+ {...computeBoxProps(rest)}
+ ref={forwardedRef}
+ >
{hasTitle && (
{title}
@@ -91,13 +108,14 @@ export class Section extends Component
);
- }
-}
+ },
+);
diff --git a/tgui/packages/tgui/components/Slider.js b/tgui/packages/tgui/components/Slider.js
deleted file mode 100644
index 84ee8421a9a67..0000000000000
--- a/tgui/packages/tgui/components/Slider.js
+++ /dev/null
@@ -1,126 +0,0 @@
-/**
- * @file
- * @copyright 2020 Aleksej Komarov
- * @license MIT
- */
-
-import { clamp01, keyOfMatchingRange, scale } from 'common/math';
-import { classes } from 'common/react';
-import { computeBoxClassName, computeBoxProps } from './Box';
-import { DraggableControl } from './DraggableControl';
-import { NumberInput } from './NumberInput';
-
-export const Slider = (props) => {
- // IE8: I don't want to support a yet another component on IE8.
- if (Byond.IS_LTE_IE8) {
- return
+ ref={contentRef}
+ >
{children}
-
-
-
- );
- }}
-
-
-
- {dragging && (
-
- {displayElement}
- )}
-
- {hasContent ? children : displayElement}
-
- {inputElement}
-
+
+
+
+ );
+ }}
+
+
+
+ {dragging && (
+
+ {displayElement}
+ )}
+
+ {hasContent ? children : displayElement}
+
+ {inputElement}
+ {
);
};
-type StackItemProps = FlexProps & {
- innerRef?: RefObject {props.titleSubtext}
- );
-};
-
-TableCell.defaultHooks = pureComponentHooks;
-
-Table.Row = TableRow;
-Table.Cell = TableCell;
diff --git a/tgui/packages/tgui/components/Table.jsx b/tgui/packages/tgui/components/Table.jsx
new file mode 100644
index 0000000000000..31a0a82d041c1
--- /dev/null
+++ b/tgui/packages/tgui/components/Table.jsx
@@ -0,0 +1,60 @@
+/**
+ * @file
+ * @copyright 2020 Aleksej Komarov
+ * @license MIT
+ */
+
+import { classes } from 'common/react';
+
+import { computeBoxClassName, computeBoxProps } from './Box';
+
+export const Table = (props) => {
+ const { className, collapsing, children, ...rest } = props;
+ return (
+ |
+ );
+};
+
+Table.Row = TableRow;
+Table.Cell = TableCell;
diff --git a/tgui/packages/tgui/components/Tabs.js b/tgui/packages/tgui/components/Tabs.js
deleted file mode 100644
index 46d1b078a85eb..0000000000000
--- a/tgui/packages/tgui/components/Tabs.js
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
- * @file
- * @copyright 2020 Aleksej Komarov
- * @license MIT
- */
-
-import { canRender, classes } from 'common/react';
-import { computeBoxClassName, computeBoxProps } from './Box';
-import { Icon } from './Icon';
-
-export const Tabs = (props) => {
- const { className, vertical, fill, fluid, children, ...rest } = props;
- return (
- |
- {children}
-
- );
-};
-
-const Tab = (props) => {
- const {
- className,
- selected,
- color,
- icon,
- leftSlot,
- rightSlot,
- children,
- ...rest
- } = props;
- return (
-
- {(canRender(leftSlot) &&
- );
-};
-
-Tabs.Tab = Tab;
diff --git a/tgui/packages/tgui/components/Tabs.tsx b/tgui/packages/tgui/components/Tabs.tsx
new file mode 100644
index 0000000000000..84779bcf74829
--- /dev/null
+++ b/tgui/packages/tgui/components/Tabs.tsx
@@ -0,0 +1,90 @@
+/**
+ * @file
+ * @copyright 2020 Aleksej Komarov
+ * @license MIT
+ */
+
+import { canRender, classes } from 'common/react';
+import { PropsWithChildren, ReactNode } from 'react';
+
+import { BoxProps, computeBoxClassName, computeBoxProps } from './Box';
+import { Icon } from './Icon';
+
+type Props = Partial<{
+ className: string;
+ fill: boolean;
+ fluid: boolean;
+ vertical: boolean;
+}> &
+ BoxProps &
+ PropsWithChildren;
+
+type TabProps = Partial<{
+ className: string;
+ color: string;
+ icon: string;
+ leftSlot: ReactNode;
+ onClick: (e?) => void;
+ rightSlot: ReactNode;
+ selected: boolean;
+}> &
+ BoxProps &
+ PropsWithChildren;
+
+export const Tabs = (props: Props) => {
+ const { className, vertical, fill, fluid, children, ...rest } = props;
+
+ return (
+ {leftSlot} ) ||
- (!!icon && (
-
-
- ))}
- {children}
- {canRender(rightSlot) && {rightSlot} }
-
+ {children}
+
+ );
+};
+
+const Tab = (props: TabProps) => {
+ const {
+ className,
+ selected,
+ color,
+ icon,
+ leftSlot,
+ rightSlot,
+ children,
+ ...rest
+ } = props;
+
+ return (
+
+ {(canRender(leftSlot) &&
+ );
+};
+
+Tabs.Tab = Tab;
diff --git a/tgui/packages/tgui/components/TextArea.js b/tgui/packages/tgui/components/TextArea.js
deleted file mode 100644
index 3fe2cd985d4a8..0000000000000
--- a/tgui/packages/tgui/components/TextArea.js
+++ /dev/null
@@ -1,237 +0,0 @@
-/**
- * @file
- * @copyright 2020 Aleksej Komarov
- * @author Warlockd
- * @license MIT
- */
-
-import { classes } from 'common/react';
-import { Component, createRef } from 'inferno';
-import { Box } from './Box';
-import { toInputValue } from './Input';
-import { KEY_ENTER, KEY_ESCAPE, KEY_TAB } from 'common/keycodes';
-
-export class TextArea extends Component {
- constructor(props, context) {
- super(props, context);
- this.textareaRef = props.innerRef || createRef();
- this.state = {
- editing: false,
- scrolledAmount: 0,
- };
- const { dontUseTabForIndent = false } = props;
- this.handleOnInput = (e) => {
- const { editing } = this.state;
- const { onInput } = this.props;
- if (!editing) {
- this.setEditing(true);
- }
- if (onInput) {
- onInput(e, e.target.value);
- }
- };
- this.handleOnChange = (e) => {
- const { editing } = this.state;
- const { onChange } = this.props;
- if (editing) {
- this.setEditing(false);
- }
- if (onChange) {
- onChange(e, e.target.value);
- }
- };
- this.handleKeyPress = (e) => {
- const { editing } = this.state;
- const { onKeyPress } = this.props;
- if (!editing) {
- this.setEditing(true);
- }
- if (onKeyPress) {
- onKeyPress(e, e.target.value);
- }
- };
- this.handleKeyDown = (e) => {
- const { editing } = this.state;
- const { onChange, onInput, onEnter, onKey } = this.props;
- if (e.keyCode === KEY_ENTER) {
- this.setEditing(false);
- if (onChange) {
- onChange(e, e.target.value);
- }
- if (onInput) {
- onInput(e, e.target.value);
- }
- if (onEnter) {
- onEnter(e, e.target.value);
- }
- if (this.props.selfClear) {
- e.target.value = '';
- e.target.blur();
- }
- return;
- }
- if (e.keyCode === KEY_ESCAPE) {
- if (this.props.onEscape) {
- this.props.onEscape(e);
- }
- this.setEditing(false);
- if (this.props.selfClear) {
- e.target.value = '';
- } else {
- e.target.value = toInputValue(this.props.value);
- e.target.blur();
- }
- return;
- }
- if (!editing) {
- this.setEditing(true);
- }
- // Custom key handler
- if (onKey) {
- onKey(e, e.target.value);
- }
- if (!dontUseTabForIndent) {
- const keyCode = e.keyCode || e.which;
- if (keyCode === KEY_TAB) {
- e.preventDefault();
- const { value, selectionStart, selectionEnd } = e.target;
- e.target.value =
- value.substring(0, selectionStart) +
- '\t' +
- value.substring(selectionEnd);
- e.target.selectionEnd = selectionStart + 1;
- if (onInput) {
- onInput(e, e.target.value);
- }
- }
- }
- };
- this.handleFocus = (e) => {
- const { editing } = this.state;
- if (!editing) {
- this.setEditing(true);
- }
- };
- this.handleBlur = (e) => {
- const { editing } = this.state;
- const { onChange } = this.props;
- if (editing) {
- this.setEditing(false);
- if (onChange) {
- onChange(e, e.target.value);
- }
- }
- };
- this.handleScroll = (e) => {
- const { displayedValue } = this.props;
- const input = this.textareaRef.current;
- if (displayedValue && input) {
- this.setState({
- scrolledAmount: input.scrollTop,
- });
- }
- };
- }
-
- componentDidMount() {
- const nextValue = this.props.value;
- const input = this.textareaRef.current;
- if (input) {
- input.value = toInputValue(nextValue);
- }
- if (this.props.autoFocus || this.props.autoSelect) {
- setTimeout(() => {
- input.focus();
-
- if (this.props.autoSelect) {
- input.select();
- }
- }, 1);
- }
- }
-
- componentDidUpdate(prevProps, prevState) {
- const prevValue = prevProps.value;
- const nextValue = this.props.value;
- const input = this.textareaRef.current;
- if (input && typeof nextValue === 'string' && prevValue !== nextValue) {
- input.value = toInputValue(nextValue);
- }
- }
-
- setEditing(editing) {
- this.setState({ editing });
- }
-
- getValue() {
- return this.textareaRef.current && this.textareaRef.current.value;
- }
-
- render() {
- // Input only props
- const {
- onChange,
- onKeyDown,
- onKeyPress,
- onInput,
- onFocus,
- onBlur,
- onEnter,
- value,
- maxLength,
- placeholder,
- scrollbar,
- noborder,
- displayedValue,
- ...boxProps
- } = this.props;
- // Box props
- const { className, fluid, nowrap, ...rest } = boxProps;
- const { scrolledAmount } = this.state;
- return (
- {leftSlot} ) ||
+ (!!icon && (
+
+
+ ))}
+ {children}
+ {canRender(rightSlot) && {rightSlot} }
+
- {displayedValue}
-
-
+
+ )}
+
+ {displayedValue}
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/components/index.js b/tgui/packages/tgui/components/index.js
deleted file mode 100644
index fa613161ea1c3..0000000000000
--- a/tgui/packages/tgui/components/index.js
+++ /dev/null
@@ -1,49 +0,0 @@
-/**
- * @file
- * @copyright 2020 Aleksej Komarov
- * @license MIT
- */
-
-export { AnimatedNumber } from './AnimatedNumber';
-export { Autofocus } from './Autofocus';
-export { Blink } from './Blink';
-export { BlockQuote } from './BlockQuote';
-export { Box } from './Box';
-export { Button } from './Button';
-export { ByondUi } from './ByondUi';
-export { Chart } from './Chart';
-export { Collapsible } from './Collapsible';
-export { ColorBox } from './ColorBox';
-export { Dimmer } from './Dimmer';
-export { Divider } from './Divider';
-export { DraggableControl } from './DraggableControl';
-export { Dropdown } from './Dropdown';
-export { Flex } from './Flex';
-export { FitText } from './FitText';
-export { Grid } from './Grid';
-export { Icon } from './Icon';
-export { InfinitePlane } from './InfinitePlane';
-export { Input } from './Input';
-export { KeyListener } from './KeyListener';
-export { Knob } from './Knob';
-export { LabeledControls } from './LabeledControls';
-export { LabeledList } from './LabeledList';
-export { MenuBar } from './MenuBar';
-export { Modal } from './Modal';
-export { NoticeBox } from './NoticeBox';
-export { NumberInput } from './NumberInput';
-export { ProgressBar } from './ProgressBar';
-export { Popper } from './Popper';
-export { RestrictedInput } from './RestrictedInput';
-export { RoundGauge } from './RoundGauge';
-export { Section } from './Section';
-export { Slider } from './Slider';
-export { StyleableSection } from './StyleableSection';
-export { Stack } from './Stack';
-export { Table } from './Table';
-export { Tabs } from './Tabs';
-export { TextArea } from './TextArea';
-export { TimeDisplay } from './TimeDisplay';
-export { TrackOutsideClicks } from './TrackOutsideClicks';
-export { Tooltip } from './Tooltip';
-export { Dialog } from './Dialog';
diff --git a/tgui/packages/tgui/components/index.ts b/tgui/packages/tgui/components/index.ts
new file mode 100644
index 0000000000000..1a5f477d256b5
--- /dev/null
+++ b/tgui/packages/tgui/components/index.ts
@@ -0,0 +1,51 @@
+/**
+ * @file
+ * @copyright 2020 Aleksej Komarov
+ * @license MIT
+ */
+
+export { AnimatedNumber } from './AnimatedNumber';
+export { Autofocus } from './Autofocus';
+export { Blink } from './Blink';
+export { BlockQuote } from './BlockQuote';
+export { Box } from './Box';
+export { Button } from './Button';
+export { ByondUi } from './ByondUi';
+export { Chart } from './Chart';
+export { Collapsible } from './Collapsible';
+export { ColorBox } from './ColorBox';
+export { Dialog } from './Dialog';
+export { Dimmer } from './Dimmer';
+export { Divider } from './Divider';
+export { DraggableControl } from './DraggableControl';
+export { Dropdown } from './Dropdown';
+export { FitText } from './FitText';
+export { Flex } from './Flex';
+export { Grid } from './Grid';
+export { Icon } from './Icon';
+export { Image } from './Image';
+export { InfinitePlane } from './InfinitePlane';
+export { Input } from './Input';
+export { KeyListener } from './KeyListener';
+export { Knob } from './Knob';
+export { LabeledControls } from './LabeledControls';
+export { LabeledList } from './LabeledList';
+export { MenuBar } from './MenuBar';
+export { Modal } from './Modal';
+export { NoticeBox } from './NoticeBox';
+export { NumberInput } from './NumberInput';
+export { Popper } from './Popper';
+export { ProgressBar } from './ProgressBar';
+export { RestrictedInput } from './RestrictedInput';
+export { RoundGauge } from './RoundGauge';
+export { Section } from './Section';
+export { Slider } from './Slider';
+export { Stack } from './Stack';
+export { StyleableSection } from './StyleableSection';
+export { Table } from './Table';
+export { Tabs } from './Tabs';
+export { TextArea } from './TextArea';
+export { TimeDisplay } from './TimeDisplay';
+export { Tooltip } from './Tooltip';
+export { TrackOutsideClicks } from './TrackOutsideClicks';
+export { VirtualList } from './VirtualList';
diff --git a/tgui/packages/tgui/constants.ts b/tgui/packages/tgui/constants.ts
index db6b33ca7d20d..0d58cb5cbe3ec 100644
--- a/tgui/packages/tgui/constants.ts
+++ b/tgui/packages/tgui/constants.ts
@@ -28,6 +28,7 @@ export const COLORS = {
science: '#9b59b6',
engineering: '#f1c40f',
cargo: '#f39c12',
+ service: '#7cc46a',
centcom: '#00c100',
other: '#c38312',
},
@@ -47,25 +48,28 @@ export const COLORS = {
// Colors defined in CSS
export const CSS_COLORS = [
+ 'average',
+ 'bad',
'black',
- 'white',
- 'red',
- 'orange',
- 'yellow',
- 'olive',
- 'green',
- 'teal',
'blue',
- 'violet',
- 'purple',
- 'pink',
'brown',
- 'grey',
'good',
- 'average',
- 'bad',
+ 'green',
+ 'grey',
'label',
-];
+ 'olive',
+ 'orange',
+ 'pink',
+ 'purple',
+ 'red',
+ 'teal',
+ 'transparent',
+ 'violet',
+ 'white',
+ 'yellow',
+] as const;
+
+export type CssColor = (typeof CSS_COLORS)[number];
/* IF YOU CHANGE THIS KEEP IT IN SYNC WITH CHAT CSS */
export const RADIO_CHANNELS = [
diff --git a/tgui/packages/tgui/contants.test.ts b/tgui/packages/tgui/contants.test.ts
index e71a8b6d770ee..abc5134956383 100644
--- a/tgui/packages/tgui/contants.test.ts
+++ b/tgui/packages/tgui/contants.test.ts
@@ -1,4 +1,9 @@
-import { getGasColor, getGasFromId, getGasFromPath, getGasLabel } from './constants';
+import {
+ getGasColor,
+ getGasFromId,
+ getGasFromPath,
+ getGasLabel,
+} from './constants';
describe('gas helper functions', () => {
it('should get the proper gas label', () => {
diff --git a/tgui/packages/tgui/debug/KitchenSink.js b/tgui/packages/tgui/debug/KitchenSink.js
deleted file mode 100644
index 246b4f50b478f..0000000000000
--- a/tgui/packages/tgui/debug/KitchenSink.js
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * @file
- * @copyright 2020 Aleksej Komarov
- * @license MIT
- */
-
-import { useLocalState } from '../backend';
-import { Flex, Section, Tabs } from '../components';
-import { Pane, Window } from '../layouts';
-
-const r = require.context('../stories', false, /\.stories\.js$/);
-
-/**
- * @returns {{
- * meta: {
- * title: string,
- * render: () => any,
- * },
- * }[]}
- */
-const getStories = () => r.keys().map((path) => r(path));
-
-export const KitchenSink = (props, context) => {
- const { panel } = props;
- const [theme] = useLocalState(context, 'kitchenSinkTheme');
- const [pageIndex, setPageIndex] = useLocalState(context, 'pageIndex', 0);
- const stories = getStories();
- const story = stories[pageIndex];
- const Layout = panel ? Pane : Window;
- return (
-
+ {Array.isArray(children) ? children.slice(0, visibleElements) : null}
+
+
+
|