diff --git a/code/datums/mind.dm b/code/datums/mind.dm index bb7f5b6aa9b..2cc69c449cf 100644 --- a/code/datums/mind.dm +++ b/code/datums/mind.dm @@ -123,22 +123,54 @@ return output /datum/mind/proc/show_memory(mob/recipient) - var/output = "[current.real_name]'s Memory
" - output += memory + ui_interact(recipient) +/datum/mind/ui_state() + return GLOB.always_state + +/datum/mind/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "Memory") + ui.open() + +/datum/mind/ui_data(mob/user) + var/list/data = list() + data["name"] = current ? current.real_name : name + data["memory"] = memory + + var/list/obj_groups = list() for(var/datum/antagonist/A as anything in antagonist) if(!length(A.objectives)) - break - if(A.faction) - output += "
Your [A.faction.name] faction objectives:" - else - output += "
Your [A.role_text] objectives:" - output += "[A.print_objectives(FALSE)]" - output += print_individualobjectives() - - var/datum/browser/panel = new(recipient, "memory", "Memory", 333, 333) - panel.set_content(output) - panel.open() + continue + var/list/group = list() + group["label"] = A.faction ? "Your [A.faction.name] faction objectives:" : "Your [A.role_text] objectives:" + var/list/objs = list() + for(var/datum/objective/O in A.objectives) + objs += list(list("text" = O.explanation_text)) + group["objectives"] = objs + obj_groups += list(group) + data["objective_groups"] = obj_groups + + var/list/ind_objs = list() + var/ind_la_explanation + if(LAZYLEN(individual_objectives)) + var/obj_count = 1 + for(var/datum/individual_objective/objective in individual_objectives) + var/list/obj = list() + obj["num"] = obj_count + obj["name"] = objective.name + obj["desc"] = objective.get_description() + obj["limited_antag"] = objective.limited_antag ? TRUE : FALSE + if(objective.limited_antag) + obj["show_la"] = objective.show_la + ind_la_explanation = objective.la_explanation + ind_objs += list(obj) + obj_count++ + data["individual_objectives"] = ind_objs + data["la_explanation"] = ind_la_explanation + + return data /datum/mind/proc/edit_memory() if(!SSticker.IsRoundInProgress()) diff --git a/code/datums/uplink/uplink_sources.dm b/code/datums/uplink/uplink_sources.dm index 541b390304c..43ba290ad2a 100644 --- a/code/datums/uplink/uplink_sources.dm +++ b/code/datums/uplink/uplink_sources.dm @@ -59,7 +59,7 @@ GLOBAL_LIST_INIT(default_uplink_source_priority, list( R.hidden_uplink = T T.trigger_code = freq to_chat(M, span_notice("A portable object teleportation relay has been installed in your [R.name]. Simply dial the frequency [format_frequency(freq)] to unlock its hidden features.")) - M.mind.store_memory("Radio Freq: [format_frequency(freq)] ([R.name]).") + M.mind.store_memory("Radio Freq: [format_frequency(freq)] ([R.name]).
") /decl/uplink_source/implant name = "Implant" diff --git a/code/game/antagonist/station/contractor.dm b/code/game/antagonist/station/contractor.dm index 589bd85ad28..cc91f5c3890 100644 --- a/code/game/antagonist/station/contractor.dm +++ b/code/game/antagonist/station/contractor.dm @@ -56,7 +56,7 @@ to_chat(contractor_mob, "Code Phrase: [span_danger("[syndicate_code_phrase]")]") to_chat(contractor_mob, "Code Response: [span_danger("[syndicate_code_response]")]") contractor_mob.mind.store_memory("Code Phrase: [syndicate_code_phrase]") - contractor_mob.mind.store_memory("Code Response: [syndicate_code_response]") + contractor_mob.mind.store_memory("Code Response: [syndicate_code_response]
") to_chat(contractor_mob, "Use the code words, preferably in the order provided, during regular conversation, to identify other agents. Proceed with caution, however, as everyone is a potential foe.") diff --git a/code/game/machinery/vending.dm b/code/game/machinery/vending.dm index 7dd7e770ba5..66ffde4464f 100644 --- a/code/game/machinery/vending.dm +++ b/code/game/machinery/vending.dm @@ -115,6 +115,10 @@ var/purchase_message = "" var/purchase_error = FALSE + var/needs_pin = FALSE + var/obj/item/card/id/pending_pin_card + var/pending_pin_mode = "" // "purchase" or "manage" + /* Variables used to initialize the product list These are used for initialization only, and so are optional if @@ -411,11 +415,11 @@ // Enter PIN, so you can't loot a vending machine with only the owner's ID card (as long as they increased the sec level) if(user_account.security_level != 0) - var/attempt_pin = input("Enter pin code", "Vendor transaction") as num | null - user_account = attempt_account_access(ID.associated_account_number, attempt_pin, 2) - if(!user_account) - to_chat(user, span_warning("Unable to access account! Credentials are incorrect.")) - return + pending_pin_card = ID + pending_pin_mode = "manage" + needs_pin = TRUE + ui_interact(user) + return if(!machine_vendor_account) machine_vendor_account = user_account @@ -528,13 +532,11 @@ // Have the customer punch in the PIN before checking if there's enough money. Prevents people from figuring out acct is // empty at high security levels if(customer_account.security_level != 0) //If card requires pin authentication (ie seclevel 1 or 2) - var/attempt_pin = input("Enter pin code", "Vendor transaction") as num - customer_account = attempt_account_access(I.associated_account_number, attempt_pin, 2) - - if(!customer_account) - purchase_message = "Unable to access account: incorrect credentials." - purchase_error = TRUE - return FALSE + pending_pin_card = I + pending_pin_mode = "purchase" + needs_pin = TRUE + ui_interact(usr) + return FALSE if(currently_vending.price > customer_account.money) purchase_message = "Insufficient funds in account." @@ -646,6 +648,8 @@ "message" = purchase_message, "isError" = purchase_error ) + data["needsPin"] = needs_pin ? TRUE : FALSE + data["pinMode"] = pending_pin_mode var/list/listed_products = list() for(var/key = 1 to length(product_records)) @@ -767,6 +771,45 @@ if("cancelpurchase") currently_vending = null + needs_pin = FALSE + pending_pin_card = null + return TRUE + + if("submit_pin") + if(!needs_pin || !pending_pin_card) + return TRUE + var/datum/money_account/verified = attempt_account_access(pending_pin_card.associated_account_number, text2num(params["pin"]), 2) + if(!verified) + purchase_message = "Unable to access account: incorrect PIN." + purchase_error = TRUE + needs_pin = FALSE + pending_pin_card = null + return TRUE + if(pending_pin_mode == "purchase") + if(!currently_vending) + needs_pin = FALSE + return TRUE + if(currently_vending.price > verified.money) + purchase_message = "Insufficient funds in account." + purchase_error = TRUE + else + var/datum/transaction/T = new(-currently_vending.price, earnings_account.get_name(), "Purchase of [currently_vending.product_name]", src) + T.apply_to(verified) + credit_purchase(verified.owner_name) + needs_pin = FALSE + pending_pin_card = null + vend(currently_vending, usr) + return TRUE + else if(pending_pin_mode == "manage") + if(!machine_vendor_account) + machine_vendor_account = verified + earnings_account = verified + locked = !locked + playsound(usr.loc, 'sound/machines/id_swipe.ogg', 60, 1) + to_chat(usr, span_notice("You [locked ? "" : "un"]lock \the [src].")) + log_econ("[src] was [locked ? "" : "un"]locked by [usr].") + needs_pin = FALSE + pending_pin_card = null return TRUE if("togglevoice") diff --git a/code/modules/economy/ATM.dm b/code/modules/economy/ATM.dm index 40cb5c9c160..bc1b8c2b3e3 100644 --- a/code/modules/economy/ATM.dm +++ b/code/modules/economy/ATM.dm @@ -136,293 +136,212 @@ log transactions if(issilicon(user)) to_chat(user, span_red("[icon2html(src, user)] Artificial unit recognized. Artificial units do not currently receive monetary compensation, as per system banking regulation #1005.")) return - if (..()) + if(..()) return - if(get_dist(src,user) <= 1) - - //js replicated from obj/machinery/computer/card - var/dat = "

Automatic Teller Machine

" - dat += "For all your monetary needs!
" - dat += "This terminal is [machine_id]. Report this code when contacting IT Support
" - - if(emagged > 0) - dat += "Card: LOCKED

Unauthorized terminal access detected! This ATM has been locked. Please contact IT Support." - else - dat += "Card: [held_card ? held_card.name : "------"]

" - - if(ticks_left_locked_down > 0) - dat += span_alert("Maximum number of pin attempts exceeded! Access to this ATM has been temporarily disabled.") - else if(authenticated_account) - if(authenticated_account.suspended) - dat += span_red(span_bold("Access to this account has been suspended, and the funds within frozen.")) + if(get_dist(src, user) <= 1) + ui_interact(user) + +/obj/machinery/atm/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "ATM") + ui.set_autoupdate(TRUE) + ui.open() + +/obj/machinery/atm/ui_data(mob/user) + var/list/data = list() + data["machine_id"] = machine_id + data["emagged"] = emagged ? TRUE : FALSE + data["locked_down"] = ticks_left_locked_down > 0 ? TRUE : FALSE + data["has_card"] = held_card ? TRUE : FALSE + data["card_name"] = held_card ? held_card.name : null + data["card_account_number"] = held_card ? held_card.associated_account_number : null + data["authenticated"] = authenticated_account ? TRUE : FALSE + data["screen"] = view_screen + if(authenticated_account) + data["suspended"] = authenticated_account.suspended ? TRUE : FALSE + data["owner_name"] = authenticated_account.owner_name + data["balance"] = authenticated_account.money + data["account_number"] = authenticated_account.account_number + data["security_level"] = authenticated_account.security_level + var/list/logs = list() + for(var/datum/transaction/T in authenticated_account.transaction_log) + logs += list(list( + "date" = T.date, + "time" = T.time, + "target_name" = T.target_name, + "purpose" = T.purpose, + "amount" = T.amount, + "source_terminal" = T.source_terminal + )) + data["transaction_log"] = logs + return data + +/obj/machinery/atm/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if(.) + return TRUE + + switch(action) + if("attempt_auth") + if(ticks_left_locked_down) + return TRUE + var/tried_account_num = text2num(params["account_num"]) + var/tried_pin = text2num(params["account_pin"]) + var/card_match_check = held_card && held_card.associated_account_number == tried_account_num ? 2 : 1 + authenticated_account = attempt_account_access(tried_account_num, tried_pin, card_match_check, force_security = TRUE) + if(!authenticated_account) + number_incorrect_tries++ + if(previous_account_number == tried_account_num) + if(number_incorrect_tries > max_pin_attempts) + ticks_left_locked_down = 30 + playsound(src, 'sound/machines/buzz-two.ogg', 50, 1) + var/datum/money_account/failed_account = get_account(tried_account_num) + if(failed_account) + var/datum/transaction/T = new(0, failed_account.owner_name, "Unauthorised login attempt", machine_id) + T.apply_to(failed_account) + else + to_chat(usr, span_red("[icon2html(src, usr)] Incorrect PIN/account combination entered, [max_pin_attempts - number_incorrect_tries] attempts remaining.")) + previous_account_number = tried_account_num + playsound(src, 'sound/machines/buzz-sigh.ogg', 50, 1) else - switch(view_screen) - if(CHANGE_SECURITY_LEVEL) - dat += "Select a new security level for this account:

" - var/text = "Zero - Either the account number and pin, or card and pin are required to access this account. " - text += "Vending machine transactions will only require a card. EFTPOS transactions will require a card and ask for a pin, but not verify the pin is correct." - - if(authenticated_account.security_level != 0) - text = "[text]" - dat += "[text]
" - text = "One - An account number and pin must be manually entered to access this account and process transactions. Vending machine transactions will require card and pin." - if(authenticated_account.security_level != 1) - text = "[text]" - dat += "[text]
" - text = "Two - In addition to account number and pin, a card is required to access this account and process transactions." - if(authenticated_account.security_level != 2) - text = "[text]" - dat += "[text]

" - dat += "Back" - if(VIEW_TRANSACTION_LOGS) - dat += "Transaction logs
" - dat += "Back" - dat += "" - dat += "" - dat += "" - dat += "" - dat += "" - dat += "" - dat += "" - dat += "" - dat += "" - for(var/datum/transaction/T in authenticated_account.transaction_log) - dat += "" - dat += "" - dat += "" - dat += "" - dat += "" - dat += "" - dat += "" - dat += "" - dat += "
DateTimeTargetPurposeValueSource terminal ID
[T.date][T.time][T.target_name][T.purpose][num2text(T.amount,12)][CREDS][T.source_terminal]
" - dat += "Print
" - if(TRANSFER_FUNDS) - dat += "Account balance: [num2text(authenticated_account.money,12)][CREDS]
" - dat += "Back

" - dat += "
" - dat += "" - dat += "" - dat += "Target account number:
" - dat += "Funds to transfer:
" - dat += "Transaction purpose:
" - dat += "
" - dat += "
" - else - dat += "Welcome, [authenticated_account.owner_name].
" - dat += "Account balance: [num2text(authenticated_account.money, 12)][CREDS]" - dat += "
" - dat += "" - dat += " Cash Chargecard
" - dat += "" - dat += "
" - dat += "Change account security level
" - dat += "Make transfer
" - dat += "View transaction log
" - dat += "Print balance statement
" - dat += "Logout
" + to_chat(usr, span_red("[icon2html(src, usr)] Incorrect PIN/account combination entered.")) + number_incorrect_tries = 0 + else + playsound(src, 'sound/machines/twobeep.ogg', 50, 1) + ticks_left_timeout = 120 + view_screen = NO_SCREEN + var/datum/transaction/T = new(0, authenticated_account.owner_name, "Remote terminal access", machine_id) + T.apply_to(authenticated_account) + to_chat(usr, span_notice("Access granted. Welcome, '[authenticated_account.owner_name].'")) + previous_account_number = tried_account_num + + if("withdrawal") + if(!authenticated_account) + return TRUE + var/amount = round(max(text2num(params["funds_amount"]), 0), 0.01) + if(amount <= 0) + to_chat(usr, span_warning("That is not a valid amount.")) + return TRUE + if(amount > authenticated_account.money) + to_chat(usr, span_warning("You don't have enough funds to do that!")) + return TRUE + playsound(src, 'sound/machines/chime.ogg', 50, 1) + var/datum/transaction/T = new(-amount, authenticated_account.owner_name, "Credit withdrawal", machine_id) + if(T.apply_to(authenticated_account)) + spawn_money(amount, src.loc, usr) + + if("e_withdrawal") + if(!authenticated_account) + return TRUE + var/amount = round(max(text2num(params["funds_amount"]), 0), 0.01) + if(amount <= 0) + to_chat(usr, span_warning("That is not a valid amount.")) + return TRUE + if(amount > authenticated_account.money) + to_chat(usr, span_warning("You don't have enough funds to do that!")) + return TRUE + playsound(src, 'sound/machines/chime.ogg', 50, 1) + var/datum/transaction/T = new(-amount, authenticated_account.owner_name, "Credit withdrawal", machine_id) + if(T.apply_to(authenticated_account)) + spawn_ewallet(amount, src.loc, usr) + + if("transfer") + if(!authenticated_account) + return TRUE + var/transfer_amount = round(text2num(params["funds_amount"]), 0.01) + if(transfer_amount <= 0) + to_chat(usr, span_warning("That is not a valid amount.")) + return TRUE + if(transfer_amount > authenticated_account.money) + to_chat(usr, span_warning("You don't have enough funds to do that!")) + return TRUE + var/target_account_number = text2num(params["target_acc_number"]) + var/transfer_purpose = params["purpose"] + if(transfer_funds(authenticated_account.account_number, target_account_number, transfer_purpose, machine_id, transfer_amount)) + to_chat(usr, "[icon2html(src, usr)][span_info("Funds transfer successful.")]") else - dat += "
" - dat += "" - dat += "" - dat += "Account: " + to_chat(usr, "[icon2html(src, usr)][span_warning("Funds transfer failed.")]") + if("view_screen") + view_screen = text2num(params["view_screen"]) - if(held_card && held_card.associated_account_number) - dat += "" - else - dat += "" + if("change_security_level") + if(!authenticated_account) + return TRUE + authenticated_account.security_level = clamp(text2num(params["new_security_level"]), 0, 2) - dat += "
PIN:
" - dat += "
" - dat += "
" + if("insert_card") + if(!held_card) + if(emagged > 0) + to_chat(usr, span_red("[icon2html(src, usr)] The ATM card reader rejected your ID because this machine has been sabotaged!")) + else + var/obj/item/I = usr.get_active_held_item() + if(isidcard(I)) + usr.drop_item() + I.loc = src + held_card = I + else + release_held_id(usr) - user << browse(HTML_SKELETON_TITLE("ATM", dat),"window=atm;size=600x650") - else - user << browse(null,"window=atm") + if("logout") + authenticated_account = null -/obj/machinery/atm/Topic(href, href_list) - if (..()) - return - if(href_list["choice"]) - switch(href_list["choice"]) - if("transfer") - if(authenticated_account) - var/transfer_amount = text2num(href_list["funds_amount"]) - transfer_amount = round(transfer_amount, 0.01) - if(transfer_amount <= 0) - alert("That is not a valid amount.") - else if(transfer_amount <= authenticated_account.money) - var/target_account_number = text2num(href_list["target_acc_number"]) - var/transfer_purpose = href_list["purpose"] - if(transfer_funds(authenticated_account.account_number, target_account_number, transfer_purpose, machine_id, transfer_amount)) - to_chat(usr, "[icon2html(src, usr)][span_info("Funds transfer successful.")]") - else - to_chat(usr, "[icon2html(src, usr)][span_warning("Funds transfer failed.")]") + if("balance_statement") + if(!authenticated_account) + return TRUE + var/obj/item/paper/R = new(src.loc) + R.name = "Balance Statement - [authenticated_account.owner_name]" + R.icon_state = "slipfull" + R.info = "
ASTERS GUILD BANKING AUTHORITY
" + R.info += "Automated Teller Account Statement

" + R.info += "
" + R.info += "" + R.info += "" + R.info += "" + R.info += "" + R.info += "" + R.info += "
Account Holder:[authenticated_account.owner_name]
Account Number:[authenticated_account.account_number]
Date / Time:[current_date_string], [stationtime2text()]
Terminal ID:[machine_id]
" + R.info += "
" + R.info += "
AVAILABLE BALANCE: [authenticated_account.money][CREDS]
" + R.info += "
" + R.info += "
This statement is generated automatically by the Asters Guild Automated Teller Network.
" + R.info += "Please retain this document for your records.
" + var/image/stampoverlay = image('icons/obj/bureaucracy.dmi') + stampoverlay.icon_state = "paper_stamp-cent" + if(!R.stamped) + R.stamped = new + R.stamped += /obj/item/stamp + R.overlays += stampoverlay + R.stamps += "
This document has been certified by the Asters Guild Automated Teller Network." + playsound(loc, pick('sound/items/polaroid1.ogg','sound/items/polaroid2.ogg'), 50, 1) - else - to_chat(usr, span_warning("You don't have enough funds to do that!")) - if("view_screen") - view_screen = text2num(href_list["view_screen"]) - if("change_security_level") - if(authenticated_account) - var/new_sec_level = max( min(text2num(href_list["new_security_level"]), 2), 0) - authenticated_account.security_level = new_sec_level - if("attempt_auth") - if(!ticks_left_locked_down) - var/tried_account_num = text2num(href_list["account_num"]) - var/tried_pin = text2num(href_list["account_pin"]) - - var/card_match_check = held_card && held_card.associated_account_number == tried_account_num ? 2 : 1 - - authenticated_account = attempt_account_access(tried_account_num, tried_pin, card_match_check, force_security = TRUE) - if(!authenticated_account) - number_incorrect_tries++ - if(previous_account_number == tried_account_num) - if(number_incorrect_tries > max_pin_attempts) - //lock down the atm - ticks_left_locked_down = 30 - playsound(src, 'sound/machines/buzz-two.ogg', 50, 1) - - //create an entry in the account transaction log - var/datum/money_account/failed_account = get_account(tried_account_num) - if(failed_account) - //Just crazy - var/datum/transaction/T = new(0, failed_account.owner_name, "Unauthorised login attempt", machine_id) - T.apply_to(failed_account) - else - to_chat(usr, span_red("[icon2html(src, usr)] Incorrect pin/account combination entered, [max_pin_attempts - number_incorrect_tries] attempts remaining.")) - previous_account_number = tried_account_num - playsound(src, 'sound/machines/buzz-sigh.ogg', 50, 1) - else - to_chat(usr, span_red("[icon2html(src, usr)] incorrect pin/account combination entered.")) - number_incorrect_tries = 0 - else - playsound(src, 'sound/machines/twobeep.ogg', 50, 1) - ticks_left_timeout = 120 - view_screen = NO_SCREEN - - //create a transaction log entry - var/datum/transaction/T = new(0, authenticated_account.owner_name, "Remote terminal access", machine_id) - T.apply_to(authenticated_account) - - to_chat(usr, span_notice("Access granted. Welcome, '[authenticated_account.owner_name].'")) - - previous_account_number = tried_account_num - if("e_withdrawal") - var/amount = max(text2num(href_list["funds_amount"]),0) - amount = round(amount, 0.01) - if(amount <= 0) - alert("That is not a valid amount.") - else if(authenticated_account && amount > 0) - if(amount <= authenticated_account.money) - playsound(src, 'sound/machines/chime.ogg', 50, 1) - - - //remove the money - //create an entry in the account transaction log - var/datum/transaction/T = new(-amount, authenticated_account.owner_name, "Credit withdrawal", machine_id) - if(T.apply_to(authenticated_account)) - // spawn_money(amount,src.loc) - spawn_ewallet(amount,src.loc,usr) - else - to_chat(usr, span_warning("You don't have enough funds to do that!")) - if("withdrawal") - var/amount = max(text2num(href_list["funds_amount"]),0) - amount = round(amount, 0.01) - if(amount <= 0) - alert("That is not a valid amount.") - else if(authenticated_account && amount > 0) - if(amount <= authenticated_account.money) - playsound(src, 'sound/machines/chime.ogg', 50, 1) - - //create an entry in the account transaction log - var/datum/transaction/T = new(-amount, authenticated_account.owner_name, "Credit withdrawal", machine_id) - if(T.apply_to(authenticated_account)) - //remove the money - spawn_money(amount,src.loc,usr) + if("print_transaction") + if(!authenticated_account) + return TRUE + var/obj/item/paper/R = new(src.loc) + R.name = "Transaction logs: [authenticated_account.owner_name]" + R.info = "Transaction logs
" + R.info += "Account holder: [authenticated_account.owner_name]
" + R.info += "Account number: [authenticated_account.account_number]
" + R.info += "Date and time: [stationtime2text()], [current_date_string]

" + R.info += "Service terminal ID: [machine_id]
" + R.info += "" + R.info += "" + for(var/datum/transaction/T in authenticated_account.transaction_log) + R.info += "" + R.info += "
DateTimeTargetPurposeValueSource terminal ID
[T.date][T.time][T.target_name][T.purpose][T.amount][CREDS][T.source_terminal]
" + var/image/stampoverlay = image('icons/obj/bureaucracy.dmi') + stampoverlay.icon_state = "paper_stamp-cent" + if(!R.stamped) + R.stamped = new + R.stamped += /obj/item/stamp + R.overlays += stampoverlay + R.stamps += "
This paper has been stamped by the Automatic Teller Machine." + playsound(loc, pick('sound/items/polaroid1.ogg','sound/items/polaroid2.ogg'), 50, 1) - else - to_chat(usr, span_warning("You don't have enough funds to do that!")) - if("balance_statement") - if(authenticated_account) - var/obj/item/paper/R = new(src.loc) - R.name = "Account balance: [authenticated_account.owner_name]" - R.info = "Automated Teller Account Statement

" - R.info += "Account holder: [authenticated_account.owner_name]
" - R.info += "Account number: [authenticated_account.account_number]
" - R.info += "Balance: [authenticated_account.money][CREDS]
" - R.info += "Date and time: [stationtime2text()], [current_date_string]

" - R.info += "Service terminal ID: [machine_id]
" - - //stamp the paper - var/image/stampoverlay = image('icons/obj/bureaucracy.dmi') - stampoverlay.icon_state = "paper_stamp-cent" - if(!R.stamped) - R.stamped = new - R.stamped += /obj/item/stamp - R.overlays += stampoverlay - R.stamps += "
This paper has been stamped by the Automatic Teller Machine." - - playsound(loc, pick('sound/items/polaroid1.ogg','sound/items/polaroid2.ogg'), 50, 1) - if ("print_transaction") - if(authenticated_account) - var/obj/item/paper/R = new(src.loc) - R.name = "Transaction logs: [authenticated_account.owner_name]" - R.info = "Transaction logs
" - R.info += "Account holder: [authenticated_account.owner_name]
" - R.info += "Account number: [authenticated_account.account_number]
" - R.info += "Date and time: [stationtime2text()], [current_date_string]

" - R.info += "Service terminal ID: [machine_id]
" - R.info += "" - R.info += "" - R.info += "" - R.info += "" - R.info += "" - R.info += "" - R.info += "" - R.info += "" - R.info += "" - for(var/datum/transaction/T in authenticated_account.transaction_log) - R.info += "" - R.info += "" - R.info += "" - R.info += "" - R.info += "" - R.info += "" - R.info += "" - R.info += "" - R.info += "
DateTimeTargetPurposeValueSource terminal ID
[T.date][T.time][T.target_name][T.purpose][T.amount][CREDS][T.source_terminal]
" - - //stamp the paper - var/image/stampoverlay = image('icons/obj/bureaucracy.dmi') - stampoverlay.icon_state = "paper_stamp-cent" - if(!R.stamped) - R.stamped = new - R.stamped += /obj/item/stamp - R.overlays += stampoverlay - R.stamps += "
This paper has been stamped by the Automatic Teller Machine." - - playsound(loc, pick('sound/items/polaroid1.ogg','sound/items/polaroid2.ogg'), 50, 1) - - if("insert_card") - if(!held_card) - //this might happen if the user had the browser window open when somebody emagged it - if(emagged > 0) - to_chat(usr, span_red("[icon2html(src, usr)] The ATM card reader rejected your ID because this machine has been sabotaged!")) - else - var/obj/item/I = usr.get_active_held_item() - if (isidcard(I)) - usr.drop_item() - I.loc = src - held_card = I - else - release_held_id(usr) - if("logout") - authenticated_account = null - //usr << browse(null,"window=atm") playsound(loc, 'sound/machines/button.ogg', 100, 1) - src.attack_hand(usr) + return TRUE // put the currently held id on the ground or in the hand of the user /obj/machinery/atm/proc/release_held_id(mob/living/carbon/human/human_user as mob) diff --git a/code/modules/modular_computers/NTNet/NTNet.dm b/code/modules/modular_computers/NTNet/NTNet.dm index 129f54079bd..94968108670 100644 --- a/code/modules/modular_computers/NTNet/NTNet.dm +++ b/code/modules/modular_computers/NTNet/NTNet.dm @@ -266,7 +266,7 @@ var/global/datum/ntnet/ntnet_global = new() if(user.mind) user.mind.initial_email_login["login"] = EA.login user.mind.initial_email_login["password"] = EA.password - user.mind.store_memory("Your email account address is [EA.login] and the password is [EA.password].") + user.mind.store_memory("Your email account address is [EA.login] and the password is [EA.password].
") if(ishuman(user)) for(var/obj/item/modular_computer/C in user.GetAllContents()) var/datum/computer_file/program/email_client/P = C.getProgramByType(/datum/computer_file/program/email_client) diff --git a/icons/obj/bureaucracy.dmi b/icons/obj/bureaucracy.dmi index 91448131a3b..4b869bcbdc9 100755 Binary files a/icons/obj/bureaucracy.dmi and b/icons/obj/bureaucracy.dmi differ diff --git a/tgui/packages/tgui/interfaces/ATM.tsx b/tgui/packages/tgui/interfaces/ATM.tsx new file mode 100644 index 00000000000..c332e874abd --- /dev/null +++ b/tgui/packages/tgui/interfaces/ATM.tsx @@ -0,0 +1,447 @@ +import { useEffect, useState } from 'react'; +import { useBackend } from 'tgui/backend'; +import { + Box, + Button, + Input, + LabeledList, + NoticeBox, + NumberInput, + Section, + Stack, + Table, +} from 'tgui-core/components'; + +import { Window } from '../layouts'; + +const SCREEN_MAIN = 0; +const SCREEN_SECURITY = 1; +const SCREEN_TRANSFER = 2; +const SCREEN_LOGS = 3; + +type Transaction = { + date: string; + time: string; + target_name: string; + purpose: string; + amount: number; + source_terminal: string; +}; + +type ATMData = { + machine_id: string; + emagged: boolean; + locked_down: boolean; + has_card: boolean; + card_name: string | null; + card_account_number: number | null; + authenticated: boolean; + suspended: boolean; + owner_name: string; + balance: number; + account_number: number; + security_level: number; + screen: number; + transaction_log: Transaction[]; +}; + +const LoginScreen = () => { + const { act, data } = useBackend(); + const { card_account_number } = data; + const [accountNum, setAccountNum] = useState( + card_account_number ? String(card_account_number) : '', + ); + const [pin, setPin] = useState(''); + + useEffect(() => { + setAccountNum(card_account_number ? String(card_account_number) : ''); + }, [card_account_number]); + + const submit = () => { + if (!accountNum || !pin) return; + act('attempt_auth', { account_num: accountNum, account_pin: pin }); + setPin(''); + }; + + return ( +
+ + + setAccountNum(val)} + onEnter={submit} + /> + + + setPin(val)} + onEnter={submit} + /> + + + +
+ ); +}; + +const MainScreen = () => { + const { act, data } = useBackend(); + const { owner_name, balance } = data; + const [withdrawAmount, setWithdrawAmount] = useState(0); + const [withdrawType, setWithdrawType] = useState< + 'withdrawal' | 'e_withdrawal' + >('withdrawal'); + + return ( + <> +
+ + + + {balance} cr + + + +
+
+ + + + + + + + + setWithdrawAmount(val)} + /> + + + + + +
+
+ + + + + + + + + + + + + + + + + +
+ + ); +}; + +const SECURITY_LEVELS = [ + { + level: 0, + label: 'Level 0', + desc: 'Card or account number with PIN required. Vending machines only require a card.', + }, + { + level: 1, + label: 'Level 1', + desc: 'Account number and PIN must be entered manually. Vending machines require card and PIN.', + }, + { + level: 2, + label: 'Level 2', + desc: 'Card, account number, and PIN all required.', + }, +]; + +const SecurityScreen = () => { + const { act, data } = useBackend(); + const { security_level } = data; + + return ( +
act('view_screen', { view_screen: SCREEN_MAIN })} + > + Back + + } + > + + {SECURITY_LEVELS.map((l) => ( + + + + {l.desc} + + + ))} + +
+ ); +}; + +const TransferScreen = () => { + const { act, data } = useBackend(); + const { balance } = data; + const [targetAccount, setTargetAccount] = useState(''); + const [amount, setAmount] = useState(0); + const [purpose, setPurpose] = useState('Funds transfer'); + + return ( +
act('view_screen', { view_screen: SCREEN_MAIN })} + > + Back + + } + > + + {balance} cr + + setTargetAccount(val)} + /> + + + setAmount(val)} + /> + + + setPurpose(val)} /> + + + +
+ ); +}; + +const LogsScreen = () => { + const { act, data } = useBackend(); + const { transaction_log } = data; + + return ( +
+ + + + } + > + {!transaction_log || transaction_log.length === 0 ? ( + No transactions recorded. + ) : ( + + + Date + Time + Target + Purpose + Value + Terminal + + {transaction_log.map((t, i) => ( + + {t.date} + {t.time} + {t.target_name} + {t.purpose} + = 0 ? 'good' : 'bad'}> + {t.amount} cr + + {t.source_terminal} + + ))} +
+ )} +
+ ); +}; + +export const ATM = () => { + const { act, data } = useBackend(); + const { + machine_id, + emagged, + locked_down, + has_card, + card_name, + authenticated, + suspended, + screen, + } = data; + + return ( + + + + Terminal: {machine_id}. Report this code when contacting IT Support. + + {emagged ? ( + + CARD READER ERROR - Unauthorized terminal access detected! This ATM + has been locked. Please contact IT Support. + + ) : ( + + +
+ + + + + +
+
+ + {locked_down ? ( + + Maximum number of PIN attempts exceeded! Access to this ATM + has been temporarily disabled. + + ) : !authenticated ? ( + + ) : suspended ? ( + + Access to this account has been suspended, and the funds + within frozen. + + ) : screen === SCREEN_SECURITY ? ( + + ) : screen === SCREEN_TRANSFER ? ( + + ) : screen === SCREEN_LOGS ? ( + + ) : ( + + )} + +
+ )} +
+
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/Memory.tsx b/tgui/packages/tgui/interfaces/Memory.tsx new file mode 100644 index 00000000000..bae248c8b57 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Memory.tsx @@ -0,0 +1,292 @@ +import { Fragment, useState } from 'react'; +import { useBackend } from 'tgui/backend'; +import { Box, Divider, Section, Stack } from 'tgui-core/components'; + +import { Window } from '../layouts'; + +type ObjectiveGroup = { + label: string; + objectives: { text: string }[]; +}; + +type IndividualObjective = { + num: number; + name: string; + desc: string; + limited_antag: boolean; + show_la?: string; +}; + +type MemoryData = { + name: string; + memory: string; + objective_groups: ObjectiveGroup[]; + individual_objectives: IndividualObjective[]; + la_explanation?: string; +}; + +/** + * Wraps a copyable value with a placeholder so MemoryLines can replace it + * with an interactive CopyToken component after parsing. + */ +const COPY_SENTINEL = '\x00COPY\x00'; +const copyTokens: { value: string; color: string }[] = []; + +const makeCopyToken = (value: string, color: string): string => { + const idx = copyTokens.length; + copyTokens.push({ value, color }); + return `${COPY_SENTINEL}${idx}${COPY_SENTINEL}`; +}; + +const CopyToken = ({ value, color }: { value: string; color: string }) => { + const [copied, setCopied] = useState(false); + + const handleClick = () => { + navigator.clipboard?.writeText(value).catch(() => { + const el = document.createElement('textarea'); + el.value = value; + document.body.appendChild(el); + el.select(); + document.execCommand('copy'); + document.body.removeChild(el); + }); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + }; + + return ( + + {value} + {copied && ( + + ✓ copied + + )} + + ); +}; + +/** + * Highlights known sensitive fields and wraps copyable values with tokens. + * Copyable: email addresses, passwords, uplink passcodes. + * Code Phrase → green, Code Response → red. + */ +const styleMemory = (raw: string): string => { + copyTokens.length = 0; + return raw + .replace( + /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g, + (_, email) => + `${makeCopyToken(email, '#6ab0de')}`, + ) + .replace( + /((?:password|pin) is )([^\s<.,]+)/gi, + (_, prefix, val) => `${prefix}${makeCopyToken(val, '#2ecc71')}`, + ) + .replace( + /(<[Bb]>(?:Your )?(?:department's )?account number is:<\/[Bb]> #?)(\d+)/gi, + (_, prefix, val) => `${prefix}${makeCopyToken(val, '#f0c040')}`, + ) + .replace( + /(<[Bb]>(?:Your )?(?:department's )?account pin is:<\/[Bb]> )(\d+)/gi, + (_, prefix, val) => `${prefix}${makeCopyToken(val, '#2ecc71')}`, + ) + .replace( + /(<[Bb]>Uplink passcode:<\/[Bb]> )([^<.(]+?)(?=\s*\()/g, + (_, prefix, val) => `${prefix}${makeCopyToken(val.trim(), '#c678dd')}`, + ) + .replace( + /(<[Bb]>Radio Freq:<\/[Bb]> )([^<(]+?)(?=\s*\()/g, + (_, prefix, val) => `${prefix}${makeCopyToken(val.trim(), '#c678dd')}`, + ) + .replace( + /(<[Bb]>Code Phrase<\/[Bb]>: )([^<\n]+)/g, + (_, prefix, val) => + `${prefix}${val.trim()}`, + ) + .replace( + /(<[Bb]>Code Response<\/[Bb]>: )([^<\n]+)/g, + (_, prefix, val) => + `${prefix}${val.trim()}`, + ); +}; + +/** + * Splits a styled HTML string on
tags and renders each line, replacing + * COPY_SENTINEL tokens with interactive CopyToken components. + */ +const MemoryLines = ({ html }: { html: string }) => { + const styled = styleMemory(html); + const rawLines = styled.split(//i); + + // Collapse consecutive empty lines into a single gap marker + type Line = { empty: true } | { empty: false; content: string }; + const lines: Line[] = []; + for (const raw of rawLines) { + const isEmpty = raw.trim().length === 0; + if (isEmpty) { + if (lines.length > 0 && !lines[lines.length - 1].empty) { + lines.push({ empty: true }); + } + } else { + lines.push({ empty: false, content: raw }); + } + } + + return ( + <> + {lines.map((line, i) => { + if (line.empty) { + return ; + } + const parts = line.content.split( + new RegExp(`${COPY_SENTINEL}(\\d+)${COPY_SENTINEL}`), + ); + return ( + + {parts.map((part, j) => { + if (j % 2 === 1) { + const token = copyTokens[Number(part)]; + return ( + + + + ); + } + return ( + + ); + })} + + ); + })} + + ); +}; + +export const Memory = () => { + const { data } = useBackend(); + const { + name, + memory, + objective_groups, + individual_objectives, + la_explanation, + } = data; + + const hasObjectives = + (objective_groups && objective_groups.length > 0) || + (individual_objectives && individual_objectives.length > 0); + + return ( + + + + +
+ {memory ? ( + + ) : ( + No notes recorded. + )} +
+
+ {hasObjectives && ( + + {objective_groups && + objective_groups.map((group, i) => ( +
+ + {group.objectives.map((obj, j) => ( + + + + Objective {j + 1}: + + + + + ))} + +
+ ))} + {individual_objectives && individual_objectives.length > 0 && ( +
+ + {individual_objectives.map((obj) => ( + + + + #{obj.num} {obj.name} + {obj.limited_antag && obj.show_la && ( + + )} + : + + + + + ))} + + {la_explanation && ( + <> + + + + )} +
+ )} +
+ )} +
+
+
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/Vending.tsx b/tgui/packages/tgui/interfaces/Vending.tsx index b87c854e37d..fa6f1d145ac 100644 --- a/tgui/packages/tgui/interfaces/Vending.tsx +++ b/tgui/packages/tgui/interfaces/Vending.tsx @@ -1,9 +1,11 @@ +import { useState } from 'react'; import { useBackend } from 'tgui/backend'; import { BlockQuote, Box, Button, Icon, + Input, LabeledList, Modal, NoticeBox, @@ -53,6 +55,8 @@ interface VendingData { markup?: number; speaker?: string; advertisement?: string; + needsPin: boolean; + pinMode: string; } const managing = (managingData: ErrorData) => { @@ -255,6 +259,61 @@ const pay = (vendingProduct: VendingProductData) => { ); }; +const pinModal = () => { + const { act, data } = useBackend(); + const [pin, setPin] = useState(''); + + const submit = () => { + if (!pin) return; + act('submit_pin', { pin }); + setPin(''); + }; + + return ( + + + + + {data.pinMode === 'manage' + ? 'Authorization Required' + : 'PIN Required'} + + + Enter the PIN for this account to continue. + + setPin(val)} + onEnter={submit} + /> + + + + + + + + + + + + + + ); +}; + export const Vending = (props: any) => { const { act, data } = useBackend(); @@ -297,7 +356,8 @@ export const Vending = (props: any) => { - {(data.isVending && pay(data.vendingData)) || null} + {(data.needsPin && pinModal()) || null} + {(data.isVending && !data.needsPin && pay(data.vendingData)) || null} ); }; diff --git a/tools/merge_dmi.py b/tools/merge_dmi.py new file mode 100644 index 00000000000..330761ad15a --- /dev/null +++ b/tools/merge_dmi.py @@ -0,0 +1,248 @@ +#!/usr/bin/env python3 +""" +merge_dmi.py - Merges icon states from one DMI file into another. + +This script parses that metadata and the underlying sprite sheet to copy +icon states from a source DMI into a target DMI, skipping any states that +already exist in the target. + +Requirements: + pip install Pillow + +Usage: + python tools/merge_dmi.py + +The original target file is backed up as .bak before any changes +are written. Only states missing from the target are added, existing states +are never overwritten. Both DMI files must use the same icon size (width x +height); states from a differently-sized source are skipped. + +Example: + python tools/merge_dmi.py icons/obj/bureaucracy-monke.dmi icons/obj/bureaucracy.dmi +""" + +import io +import math +import shutil +import struct +import sys +import zlib + +from PIL import Image + + +def read_png_chunks(data): + """Read all PNG chunks from raw bytes, returning list of (type, data, crc).""" + if data[:8] != b'\x89PNG\r\n\x1a\n': + raise ValueError("Not a PNG file") + chunks = [] + pos = 8 + while pos < len(data): + length = struct.unpack('>I', data[pos:pos+4])[0] + chunk_type = data[pos+4:pos+8] + chunk_data = data[pos+8:pos+8+length] + crc = data[pos+8+length:pos+12+length] + chunks.append((chunk_type, chunk_data, crc)) + pos += 12 + length + return chunks + + +def get_dmi_description(filepath): + """Extract the BYOND DMI description string from a DMI file.""" + with open(filepath, 'rb') as f: + data = f.read() + for chunk_type, chunk_data, _ in read_png_chunks(data): + if chunk_type == b'zTXt': + null_pos = chunk_data.index(b'\x00') + if chunk_data[:null_pos].decode('latin-1') == 'Description': + compressed = chunk_data[null_pos+2:] # skip null + compression method byte + return zlib.decompress(compressed).decode('latin-1') + elif chunk_type == b'tEXt': + null_pos = chunk_data.index(b'\x00') + if chunk_data[:null_pos].decode('latin-1') == 'Description': + return chunk_data[null_pos+1:].decode('latin-1') + return None + + +def parse_dmi_description(desc): + """ + Parse the BYOND DMI description block into a header dict and a list of + state dicts. Each state dict contains: name, dirs, frames, raw_lines. + raw_lines preserves the original metadata lines so they can be + round-tripped exactly into the output file. + """ + header = {} + states = [] + current_state = None + + for line in desc.strip().split('\n'): + line = line.strip() + if line.startswith('# BEGIN DMI') or line.startswith('# END DMI'): + continue + if line.startswith('version'): + header['version'] = line.split('=')[1].strip() + elif line.startswith('width'): + header['width'] = int(line.split('=')[1].strip()) + elif line.startswith('height'): + header['height'] = int(line.split('=')[1].strip()) + elif line.startswith('state'): + if current_state is not None: + states.append(current_state) + name = line.split('=', 1)[1].strip().strip('"') + current_state = {'name': name, 'dirs': 1, 'frames': 1, 'raw_lines': [line]} + elif current_state is not None: + current_state['raw_lines'].append(line) + if line.startswith('dirs'): + current_state['dirs'] = int(line.split('=')[1].strip()) + elif line.startswith('frames'): + current_state['frames'] = int(line.split('=')[1].strip()) + + if current_state is not None: + states.append(current_state) + + return header, states + + +def get_state_frame_count(state): + return state['dirs'] * state['frames'] + + +def extract_state_images(dmi_path, header, states): + """ + Slice the sprite sheet into individual PIL images per state, returned as + a dict mapping state name -> list of frame images (in sheet order). + """ + img = Image.open(dmi_path).convert('RGBA') + w, h = header['width'], header['height'] + cols = img.width // w + + result = {} + frame_index = 0 + for state in states: + count = get_state_frame_count(state) + frames = [] + for _ in range(count): + col = frame_index % cols + row = frame_index // cols + box = (col * w, row * h, (col + 1) * w, (row + 1) * h) + frames.append(img.crop(box)) + frame_index += 1 + result[state['name']] = frames + + return result + + +def build_dmi_description(header, states): + """Reconstruct the BYOND DMI description string from header and state list.""" + lines = ['# BEGIN DMI'] + lines.append(f"version = {header['version']}") + lines.append(f"\twidth = {header['width']}") + lines.append(f"\theight = {header['height']}") + for state in states: + for line in state['raw_lines']: + lines.append(f"\t{line}" if not line.startswith('state') else line) + lines.append('# END DMI') + return '\n'.join(lines) + '\n' + + +def build_dmi(header, states, all_frames_map): + """ + Assemble a complete DMI file (as bytes) from the given states and frames. + Lays out frames in a near-square grid, embeds the description as a zTXt + chunk before the first IDAT chunk (matching BYOND's format). + """ + w, h = header['width'], header['height'] + total_frames = sum(get_state_frame_count(s) for s in states) + cols = math.ceil(math.sqrt(total_frames)) + + sheet = Image.new('RGBA', (cols * w, math.ceil(total_frames / cols) * h), (0, 0, 0, 0)) + frame_index = 0 + for state in states: + for frame in all_frames_map[state['name']]: + col = frame_index % cols + row = frame_index // cols + sheet.paste(frame, (col * w, row * h)) + frame_index += 1 + + # Encode as PNG first, then inject our Description zTXt chunk + png_bytes = io.BytesIO() + sheet.save(png_bytes, format='PNG', optimize=False) + chunks = read_png_chunks(png_bytes.getvalue()) + + desc = build_dmi_description(header, states) + keyword = b'Description\x00' + compressed_desc = zlib.compress(desc.encode('latin-1')) + ztxt_data = keyword + b'\x00' + compressed_desc + ztxt_crc = struct.pack('>I', zlib.crc32(b'zTXt' + ztxt_data) & 0xffffffff) + ztxt_chunk = struct.pack('>I', len(ztxt_data)) + b'zTXt' + ztxt_data + ztxt_crc + + result = b'\x89PNG\r\n\x1a\n' + desc_injected = False + for chunk_type, chunk_data, crc in chunks: + # Drop any existing Description chunk + if chunk_type in (b'zTXt', b'tEXt'): + null_pos = chunk_data.find(b'\x00') + if null_pos >= 0 and chunk_data[:null_pos] == b'Description': + continue + # Inject ours just before image data + if chunk_type == b'IDAT' and not desc_injected: + result += ztxt_chunk + desc_injected = True + result += struct.pack('>I', len(chunk_data)) + chunk_type + chunk_data + crc + + return result + + +def main(): + if len(sys.argv) != 3: + print(__doc__) + sys.exit(1) + + source_path = sys.argv[1] + target_path = sys.argv[2] + + print(f"Reading source: {source_path}") + src_header, src_states = parse_dmi_description(get_dmi_description(source_path)) + src_frames = extract_state_images(source_path, src_header, src_states) + print(f" {len(src_states)} states found") + + print(f"Reading target: {target_path}") + tgt_header, tgt_states = parse_dmi_description(get_dmi_description(target_path)) + tgt_frames = extract_state_images(target_path, tgt_header, tgt_states) + print(f" {len(tgt_states)} states found") + + tgt_state_names = {s['name'] for s in tgt_states} + to_add = [s for s in src_states if s['name'] not in tgt_state_names] + + if not to_add: + print("\nNothing to do - all source states already exist in target.") + return + + print(f"\nStates to add ({len(to_add)}): {[s['name'] for s in to_add]}") + + if src_header['width'] != tgt_header['width'] or src_header['height'] != tgt_header['height']: + print( + f"\nERROR: Icon sizes differ - " + f"source is {src_header['width']}x{src_header['height']}, " + f"target is {tgt_header['width']}x{tgt_header['height']}. Aborting." + ) + sys.exit(1) + + merged_states = tgt_states + to_add + merged_frames = {**tgt_frames, **{s['name']: src_frames[s['name']] for s in to_add}} + + print(f"\nBuilding merged DMI ({len(merged_states)} states total)...") + result = build_dmi(tgt_header, merged_states, merged_frames) + + backup_path = target_path + '.bak' + shutil.copy2(target_path, backup_path) + print(f"Backed up original to: {backup_path}") + + with open(target_path, 'wb') as f: + f.write(result) + + print(f"Done. Added {len(to_add)} new states to {target_path}") + + +if __name__ == '__main__': + main()