From e584690f9909afae1037c2ce1c6e06cc59d7be06 Mon Sep 17 00:00:00 2001 From: BartDrown <40639741+BartDrown@users.noreply.github.com> Date: Sun, 29 Mar 2026 11:29:59 +0200 Subject: [PATCH 1/7] feat: add TGUI for ATM machine --- code/modules/economy/ATM.dm | 471 +++++++++++--------------- tgui/packages/tgui/interfaces/ATM.tsx | 447 ++++++++++++++++++++++++ 2 files changed, 642 insertions(+), 276 deletions(-) create mode 100644 tgui/packages/tgui/interfaces/ATM.tsx 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/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 ? ( + + ) : ( + + )} + +
+ )} +
+
+ ); +}; From 13d0cc27010748ffea9e10fc76b9c38aaa33f00b Mon Sep 17 00:00:00 2001 From: BartDrown <40639741+BartDrown@users.noreply.github.com> Date: Sun, 29 Mar 2026 11:30:41 +0200 Subject: [PATCH 2/7] feat: add TGUI for player memory notes --- code/datums/mind.dm | 58 +++- code/datums/uplink/uplink_sources.dm | 2 +- code/game/antagonist/station/contractor.dm | 2 +- code/modules/modular_computers/NTNet/NTNet.dm | 2 +- tgui/packages/tgui/interfaces/Memory.tsx | 297 ++++++++++++++++++ 5 files changed, 345 insertions(+), 16 deletions(-) create mode 100644 tgui/packages/tgui/interfaces/Memory.tsx 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/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/tgui/packages/tgui/interfaces/Memory.tsx b/tgui/packages/tgui/interfaces/Memory.tsx new file mode 100644 index 00000000000..a4bf7b74017 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Memory.tsx @@ -0,0 +1,297 @@ +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 && ( + <> + + + + )} +
+ )} +
+ )} +
+
+
+ ); +}; From ce947cedbaa37e2fe18e10e63c369a384f20b402 Mon Sep 17 00:00:00 2001 From: BartDrown <40639741+BartDrown@users.noreply.github.com> Date: Sun, 29 Mar 2026 11:33:59 +0200 Subject: [PATCH 3/7] feat: add TGUI for player memory notes --- code/game/machinery/vending.dm | 67 +++++++++++++++++++---- tgui/packages/tgui/interfaces/Vending.tsx | 61 ++++++++++++++++++++- 2 files changed, 115 insertions(+), 13 deletions(-) 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/tgui/packages/tgui/interfaces/Vending.tsx b/tgui/packages/tgui/interfaces/Vending.tsx index b87c854e37d..1208b9ac629 100644 --- a/tgui/packages/tgui/interfaces/Vending.tsx +++ b/tgui/packages/tgui/interfaces/Vending.tsx @@ -1,9 +1,12 @@ +import { useState } from 'react'; + import { useBackend } from 'tgui/backend'; import { BlockQuote, Box, Button, Icon, + Input, LabeledList, Modal, NoticeBox, @@ -53,6 +56,8 @@ interface VendingData { markup?: number; speaker?: string; advertisement?: string; + needsPin: boolean; + pinMode: string; } const managing = (managingData: ErrorData) => { @@ -255,6 +260,59 @@ 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 +355,8 @@ export const Vending = (props: any) => { - {(data.isVending && pay(data.vendingData)) || null} + {(data.needsPin && pinModal()) || null} + {(data.isVending && !data.needsPin && pay(data.vendingData)) || null} ); }; From 6fc7af81164b1b40c8ce59e4caa34a8e154e5033 Mon Sep 17 00:00:00 2001 From: BartDrown <40639741+BartDrown@users.noreply.github.com> Date: Sun, 29 Mar 2026 11:34:41 +0200 Subject: [PATCH 4/7] chore: add DMI merge tool, expand bureaucracy.dmi with icons from monke --- icons/obj/bureaucracy.dmi | Bin 15298 -> 39125 bytes tools/merge_dmi.py | 248 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 tools/merge_dmi.py diff --git a/icons/obj/bureaucracy.dmi b/icons/obj/bureaucracy.dmi index 91448131a3bc87d17dc52d79c042fb75c01c5af1..4b869bcbdc9adfe4216d5b04536d2ed7b9e4ff02 100755 GIT binary patch literal 39125 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 NXKWnd6l+ZE{{Tnm@yP%H literal 15298 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 d4?9s + +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() From aa797f766fae345609df190afe28fb2290463c2e Mon Sep 17 00:00:00 2001 From: BartDrown <40639741+BartDrown@users.noreply.github.com> Date: Sun, 29 Mar 2026 18:22:18 +0200 Subject: [PATCH 5/7] chore: adjust to linter --- tgui/packages/tgui/interfaces/Memory.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tgui/packages/tgui/interfaces/Memory.tsx b/tgui/packages/tgui/interfaces/Memory.tsx index a4bf7b74017..c57d44bb3ec 100644 --- a/tgui/packages/tgui/interfaces/Memory.tsx +++ b/tgui/packages/tgui/interfaces/Memory.tsx @@ -1,8 +1,6 @@ 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 = { @@ -73,7 +71,6 @@ const CopyToken = ({ borderBottom: `1px dashed ${color}`, userSelect: 'none', }} - title="Click to copy" > {value} {copied && ( @@ -110,7 +107,7 @@ const styleMemory = (raw: string): string => { copyTokens.length = 0; return raw .replace( - /([a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,})/g, + /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g, (_, email) => `${makeCopyToken(email, '#6ab0de')}`, ) .replace( From db977b0bc9e606e380cc8b1d095b165585c141ff Mon Sep 17 00:00:00 2001 From: BartDrown <40639741+BartDrown@users.noreply.github.com> Date: Sun, 29 Mar 2026 18:51:28 +0200 Subject: [PATCH 6/7] chore: ts import sorting --- tgui/packages/tgui/interfaces/Memory.tsx | 16 +++++++--------- tgui/packages/tgui/interfaces/Vending.tsx | 1 - 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/tgui/packages/tgui/interfaces/Memory.tsx b/tgui/packages/tgui/interfaces/Memory.tsx index c57d44bb3ec..bae248c8b57 100644 --- a/tgui/packages/tgui/interfaces/Memory.tsx +++ b/tgui/packages/tgui/interfaces/Memory.tsx @@ -1,6 +1,7 @@ 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 = { @@ -37,13 +38,7 @@ const makeCopyToken = (value: string, color: string): string => { return `${COPY_SENTINEL}${idx}${COPY_SENTINEL}`; }; -const CopyToken = ({ - value, - color, -}: { - value: string; - color: string; -}) => { +const CopyToken = ({ value, color }: { value: string; color: string }) => { const [copied, setCopied] = useState(false); const handleClick = () => { @@ -108,7 +103,8 @@ const styleMemory = (raw: string): string => { return raw .replace( /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g, - (_, email) => `${makeCopyToken(email, '#6ab0de')}`, + (_, email) => + `${makeCopyToken(email, '#6ab0de')}`, ) .replace( /((?:password|pin) is )([^\s<.,]+)/gi, @@ -170,7 +166,9 @@ const MemoryLines = ({ html }: { html: string }) => { if (line.empty) { return ; } - const parts = line.content.split(new RegExp(`${COPY_SENTINEL}(\\d+)${COPY_SENTINEL}`)); + const parts = line.content.split( + new RegExp(`${COPY_SENTINEL}(\\d+)${COPY_SENTINEL}`), + ); return ( {parts.map((part, j) => { diff --git a/tgui/packages/tgui/interfaces/Vending.tsx b/tgui/packages/tgui/interfaces/Vending.tsx index 1208b9ac629..6723afc9917 100644 --- a/tgui/packages/tgui/interfaces/Vending.tsx +++ b/tgui/packages/tgui/interfaces/Vending.tsx @@ -1,5 +1,4 @@ import { useState } from 'react'; - import { useBackend } from 'tgui/backend'; import { BlockQuote, From 1099717a4664be972da4a93909f88c6f4cf1f88a Mon Sep 17 00:00:00 2001 From: BartDrown <40639741+BartDrown@users.noreply.github.com> Date: Sun, 29 Mar 2026 20:49:39 +0200 Subject: [PATCH 7/7] chore: prettier autofix --- tgui/packages/tgui/interfaces/Vending.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tgui/packages/tgui/interfaces/Vending.tsx b/tgui/packages/tgui/interfaces/Vending.tsx index 6723afc9917..fa6f1d145ac 100644 --- a/tgui/packages/tgui/interfaces/Vending.tsx +++ b/tgui/packages/tgui/interfaces/Vending.tsx @@ -274,7 +274,9 @@ const pinModal = () => { - {data.pinMode === 'manage' ? 'Authorization Required' : 'PIN Required'} + {data.pinMode === 'manage' + ? 'Authorization Required' + : 'PIN Required'} Enter the PIN for this account to continue.