diff --git a/code/__defines/computers.dm b/code/__defines/computers.dm
index 710816cec40..bffa88fba25 100644
--- a/code/__defines/computers.dm
+++ b/code/__defines/computers.dm
@@ -8,8 +8,9 @@
#define NET_FEATURE_RECORDS BITFLAG(5) // Modifying accounts, viewing crew records etc.
#define NET_FEATURE_FILESYSTEM BITFLAG(6) // Accessing mainframe filesystems.
#define NET_FEATURE_DECK BITFLAG(7) // Control of docking beacons, supply, deck control.
+#define NET_FEATURE_FINANCE BITFLAG(8) // Money transfers, other finance.
-#define NET_ALL_FEATURES (NET_FEATURE_SOFTWAREDOWNLOAD|NET_FEATURE_COMMUNICATION|NET_FEATURE_SYSTEMCONTROL|NET_FEATURE_SECURITY|NET_FEATURE_ACCESS|NET_FEATURE_RECORDS|NET_FEATURE_FILESYSTEM|NET_FEATURE_DECK)
+#define NET_ALL_FEATURES (NET_FEATURE_SOFTWAREDOWNLOAD|NET_FEATURE_COMMUNICATION|NET_FEATURE_SYSTEMCONTROL|NET_FEATURE_SECURITY|NET_FEATURE_ACCESS|NET_FEATURE_RECORDS|NET_FEATURE_FILESYSTEM|NET_FEATURE_DECK|NET_FEATURE_FINANCE)
// Transfer speeds, used when downloading/uploading a file/program.
#define NETWORK_SPEED_BASE 1/NETWORK_BASE_BROADCAST_STRENGTH // GQ/s transfer speed, multiplied by signal power
@@ -44,6 +45,7 @@
#define PROG_UTIL "Utility"
#define PROG_SEC "Security"
#define PROG_MONITOR "Monitoring"
+#define PROG_FINANCE "Finances"
#define RECEIVER_WIRELESS 1
#define RECEIVER_STRONG_WIRELESS 2
@@ -74,4 +76,4 @@
#define OS_FILE_NO_WRITE -5
#define OS_HARDDRIVE_SPACE -6
#define OS_NETWORK_ERROR -7
-#define OS_BAD_NAME -8
\ No newline at end of file
+#define OS_BAD_NAME -8
\ No newline at end of file
diff --git a/code/__defines/machinery.dm b/code/__defines/machinery.dm
index b629f4a63de..464385ea1bd 100644
--- a/code/__defines/machinery.dm
+++ b/code/__defines/machinery.dm
@@ -181,3 +181,4 @@ var/global/defer_powernet_rebuild = 0 // True if net rebuild will be called
#define PART_SCANNER /obj/item/stock_parts/computer/scanner // One of several optional scanner attachments.
#define PART_D_SLOT /obj/item/stock_parts/computer/drive_slot // Portable drive slot.
#define PART_MSTICK /obj/item/stock_parts/computer/charge_stick_slot // Charge-slot component for transactions /w charge sticks.
+#define PART_MPRINTER /obj/item/stock_parts/computer/money_printer
\ No newline at end of file
diff --git a/code/__defines/subsystem-priority.dm b/code/__defines/subsystem-priority.dm
index 54c87443595..dc075751506 100644
--- a/code/__defines/subsystem-priority.dm
+++ b/code/__defines/subsystem-priority.dm
@@ -36,6 +36,7 @@
#define SS_PRIORITY_GHOST_IMAGES 10 // Updates ghost client images.
#define SS_PRIORITY_ZCOPY 10 // Builds appearances for Z-Mimic.
#define SS_PRIORITY_PROJECTILES 10 // Projectile processing!
+#define SS_PRIORITY_MONEY_ACCOUNTS 10 // Money accounts processing.
// SS_BACKGROUND
#define SS_PRIORITY_OBJECTS 100 // processing_objects processing.
diff --git a/code/controllers/configuration.dm b/code/controllers/configuration.dm
index 51a5217e7b6..b4bf0921fd4 100644
--- a/code/controllers/configuration.dm
+++ b/code/controllers/configuration.dm
@@ -254,6 +254,16 @@ var/global/list/gamemode_cache = list()
var/dex_malus_brainloss_threshold = 30 //The threshold of when brainloss begins to affect dexterity.
+ // Economy variables
+ var/withdraw_period = 1 DAY
+ var/interest_period = 1 DAY
+
+ var/interest_mod_delay = 2 DAYS
+ var/withdraw_mod_delay = 3 DAYS
+ var/transaction_mod_delay = 2 DAYS
+ var/fractional_reserve_mod_delay = 3 DAYS
+ var/anti_tamper_mod_delay = 2 DAYS
+
var/static/list/protected_vars = list(
"comms_password",
"ban_comms_password",
@@ -905,6 +915,22 @@ var/global/list/gamemode_cache = list()
if("dexterity_malus_brainloss_threshold")
config.dex_malus_brainloss_threshold = text2num(value)
+ // Economy config.
+ if("withdraw_period")
+ config.withdraw_period = value DAYS
+ if("interest_period")
+ config.interest_period = value DAYS
+
+ if("interest_mod_delay")
+ config.interest_mod_delay = value DAYS
+ if("withdraw_mod_delay")
+ config.withdraw_mod_delay = value DAYS
+ if("transaction_mod_delay")
+ config.transaction_mod_delay = value DAYS
+ if("fractional_reserve_mod_delay")
+ config.fractional_reserve_mod_delay = value DAYS
+ if("anti_tamper_mod_delay")
+ config.anti_tamper_mod_delay = value DAYS
else
log_misc("Unknown setting in configuration: '[name]'")
diff --git a/code/controllers/subsystems/accounts.dm b/code/controllers/subsystems/accounts.dm
new file mode 100644
index 00000000000..bfbfc26983f
--- /dev/null
+++ b/code/controllers/subsystems/accounts.dm
@@ -0,0 +1,73 @@
+SUBSYSTEM_DEF(money_accounts)
+ name = "Money Accounts"
+ wait = 5 MINUTES
+ priority = SS_PRIORITY_MONEY_ACCOUNTS
+ var/list/datum/money_account/all_accounts = list()
+
+ // Independent, globally accessible accounts not tied to a network
+ var/list/datum/money_account/all_glob_accounts = list()
+
+ var/list/datum/money_account/escrow/all_escrow_accounts = list()
+
+ var/list/processing_accounts
+
+ var/adjustment = 0
+
+/datum/controller/subsystem/money_accounts/fire(resumed = FALSE)
+
+ if(!resumed)
+ processing_accounts = all_accounts.Copy()
+
+ var/current_time = REALTIMEOFDAY + adjustment
+ while(processing_accounts.len)
+ var/datum/money_account/curr_account = processing_accounts[processing_accounts.len]
+ processing_accounts.len--
+
+ if(length(curr_account.pending_modifications))
+ for(var/datum/account_modification/pending_mod in curr_account.pending_modifications)
+ if(pending_mod.start_time + pending_mod.mod_delay <= current_time)
+ pending_mod.modify_account()
+
+ if(istype(curr_account, /datum/money_account/child))
+ var/datum/money_account/child/curr_child = curr_account
+
+ if(curr_child.withdrawal_limit && (curr_child.last_withdraw_period + config.withdraw_period <= current_time))
+ curr_child.current_withdrawal = 0
+ curr_child.last_withdraw_period = current_time
+
+ if(curr_child.interest_rate && (curr_child.last_interest_period + config.interest_period <= current_time))
+ curr_child.accrue_interest()
+ curr_child.last_interest_period = current_time
+
+ if(MC_TICK_CHECK)
+ return
+
+/datum/controller/subsystem/money_accounts/proc/accel_day()
+ adjustment += 1 DAY
+
+/datum/controller/subsystem/money_accounts/proc/decel_day()
+ adjustment -= 1 DAY
+
+/datum/controller/subsystem/money_accounts/proc/get_or_add_escrow(account_id, account_pin, account_provider)
+ var/datum/money_account/escrow/existing = get_escrow(account_id, account_pin, account_provider)
+ if(existing)
+ return existing
+
+ existing = new()
+ existing.account_id = account_id
+ existing.remote_access_pin = account_pin
+ existing.owner_name = account_provider
+
+ all_escrow_accounts |= existing
+ return existing
+
+/datum/controller/subsystem/money_accounts/proc/get_escrow(account_id, account_pin, account_provider)
+ for(var/datum/money_account/escrow/e_account in all_escrow_accounts)
+ if((e_account.account_id == account_id) && e_account.remote_access_pin == account_pin)
+ if(!account_provider || e_account.owner_name == account_provider)
+ return e_account
+
+/datum/controller/subsystem/money_accounts/proc/get_escrow_provider_ids()
+ . = list()
+ for(var/datum/money_account/escrow/e_account in all_escrow_accounts)
+ . |= e_account.owner_name
\ No newline at end of file
diff --git a/code/controllers/subsystems/jobs.dm b/code/controllers/subsystems/jobs.dm
index 82d9af389e1..9c697c13448 100644
--- a/code/controllers/subsystems/jobs.dm
+++ b/code/controllers/subsystems/jobs.dm
@@ -517,7 +517,7 @@ SUBSYSTEM_DEF(jobs)
var/datum/money_account/department_account = department_accounts[job.primary_department]
if(department_account)
- remembered_info += "Your department's account number is: #[department_account.account_number]
"
+ remembered_info += "Your department's account number is: #[department_account.account_id]
"
remembered_info += "Your department's account pin is: [department_account.remote_access_pin]
"
remembered_info += "Your department's account funds are: [department_account.format_value_by_currency(department_account.money)]
"
diff --git a/code/controllers/subsystems/ticker.dm b/code/controllers/subsystems/ticker.dm
index 95447d95722..29ea4aa9a8f 100644
--- a/code/controllers/subsystems/ticker.dm
+++ b/code/controllers/subsystems/ticker.dm
@@ -420,10 +420,10 @@ Helpers
if(dronecount)
to_world("There [dronecount>1 ? "were" : "was"] [dronecount] industrious maintenance drone\s at the end of this round.")
- if(all_money_accounts.len)
- var/datum/money_account/max_profit = all_money_accounts[1]
- var/datum/money_account/max_loss = all_money_accounts[1]
- for(var/datum/money_account/D in all_money_accounts)
+ if(SSmoney_accounts.all_glob_accounts.len)
+ var/datum/money_account/max_profit = SSmoney_accounts.all_glob_accounts[1]
+ var/datum/money_account/max_loss = SSmoney_accounts.all_glob_accounts[1]
+ for(var/datum/money_account/D in SSmoney_accounts.all_glob_accounts)
if(D == vendor_account) //yes we know you get lots of money
continue
var/saldo = D.get_balance()
diff --git a/code/datums/outfits/jobs/job.dm b/code/datums/outfits/jobs/job.dm
index a7b79f91b94..b1d7ced48ce 100644
--- a/code/datums/outfits/jobs/job.dm
+++ b/code/datums/outfits/jobs/job.dm
@@ -19,5 +19,5 @@
return
if(H.mind)
if(H.mind.initial_account)
- C.associated_account_number = H.mind.initial_account.account_number
+ C.associated_account_id = H.mind.initial_account.account_id
return C
diff --git a/code/game/jobs/job/_job.dm b/code/game/jobs/job/_job.dm
index 587b156d685..3ceccebbdb1 100644
--- a/code/game/jobs/job/_job.dm
+++ b/code/game/jobs/job/_job.dm
@@ -1,5 +1,5 @@
/datum/job
- var/title // The name of the job
+ var/title // The name of the job
var/list/software_on_spawn = list() // Defines the software files that spawn on tablets and labtops
var/list/department_types = list() // What departments the job is in.
var/autoset_department = TRUE // If department list is empty, use map default.
@@ -150,14 +150,14 @@
return // You are too poor for an account.
//give them an account in the station database
- var/datum/money_account/M = create_account("[H.real_name]'s account", H.real_name, money_amount)
+ var/datum/money_account/M = create_glob_account("[H.real_name]'s account", H.real_name, money_amount)
var/cash_on_hand = create_cash_on_hand(H, M)
// Store their financial info.
if(H.mind)
var/remembered_info = ""
- remembered_info += "Your account number is: #[M.account_number]
"
+ remembered_info += "Your account number is: #[M.account_id]
"
remembered_info += "Your account pin is: [M.remote_access_pin]
"
- remembered_info += "Your account funds are: [M.format_value_by_currency(M.money)]
"
+ remembered_info += "Your account funds are: [M.format_value_by_currency(M.get_balance())]
"
if(M.transaction_log.len)
var/datum/transaction/T = M.transaction_log[1]
remembered_info += "Your account was created: [T.time], [T.date] at [T.get_source_name()]
"
diff --git a/code/game/machinery/_machines_base/stock_parts/money_printer.dm b/code/game/machinery/_machines_base/stock_parts/money_printer.dm
new file mode 100644
index 00000000000..65e405a5845
--- /dev/null
+++ b/code/game/machinery/_machines_base/stock_parts/money_printer.dm
@@ -0,0 +1,84 @@
+/obj/item/stock_parts/computer/money_printer
+ name = "cryptographic micro-printer"
+ desc = "A micro-printer capable of scanning, recycling, and printing cryptographically secured bank notes on ultra thin plastic."
+ icon_state = "printer"
+ material = /decl/material/solid/plastic
+ matter = list(
+ /decl/material/solid/fiberglass = MATTER_AMOUNT_REINFORCEMENT,
+ /decl/material/solid/silicon = MATTER_AMOUNT_REINFORCEMENT
+ )
+ max_health = ITEM_HEALTH_NO_DAMAGE
+ external_slot = TRUE
+
+ var/stored_plastic = 0 // This could be capped, but it'd place an annoying limit on the amount of money you can insert at a given time.
+
+/obj/item/stock_parts/computer/money_printer/examine(mob/user)
+ . = ..()
+ if(Adjacent(user))
+ to_chat(user, "It has [round(stored_plastic) / SHEET_MATERIAL_AMOUNT] sheets of plastic in storage.")
+
+/obj/item/stock_parts/computer/money_printer/attackby(obj/item/W, mob/user)
+ . = ..()
+ if(istype(W, /obj/item/stack/material) && W.get_material_type() == /decl/material/solid/plastic)
+ var/obj/item/stack/material/stack = W
+
+ stored_plastic += SHEET_MATERIAL_AMOUNT * stack.amount
+ to_chat(user, "You insert the plastic into \the [src]'s storage.")
+ user.drop_from_inventory(stack)
+ qdel(stack)
+
+ if(IS_SCREWDRIVER(W) && !istype(loc, /obj/machinery))
+ to_chat(user, "You pry out the plastic reserves of \the [src].")
+ SSmaterials.create_object(/decl/material/solid/plastic, get_turf(src), round(stored_plastic / SHEET_MATERIAL_AMOUNT))
+ stored_plastic = 0
+
+ if(istype(W, /obj/item/cash))
+ if(!loc)
+ return
+ var/datum/extension/interactive/os/current_os = get_extension(loc, /datum/extension/interactive/os)
+ if(!current_os)
+ to_chat(user, SPAN_WARNING("\The [src] must be installed before it can be used!"))
+ return
+ var/obj/item/cash/receiving = W
+ var/decl/currency/receiving_currency = GET_DECL(receiving.currency)
+ if(receiving_currency.material != /decl/material/solid/plastic)
+ to_chat(user, SPAN_WARNING("\The [src] cannot accept cash of this currency!"))
+ return
+
+ var/amount_taken = current_os.process_cash(receiving, usr)
+ if(!amount_taken)
+ return
+ else
+ playsound(get_turf(src), pick('sound/items/polaroid1.ogg', 'sound/items/polaroid2.ogg'), 50, 1)
+ to_chat(usr, SPAN_NOTICE("You insert [FLOOR(amount_taken / receiving_currency.absolute_value)] [receiving_currency.name] into \the [src]."))
+ receiving.adjust_worth(-amount_taken)
+
+ stored_plastic += amount_taken*max(1, round(SHEET_MATERIAL_AMOUNT/10))
+
+ return
+
+/obj/item/stock_parts/computer/money_printer/proc/can_print(amount, currency_type)
+ // TODO: Support for non-plastic currencies
+ var/decl/currency/printed_currency = GET_DECL(currency_type)
+ if(printed_currency.material != /decl/material/solid/plastic)
+ return FALSE
+ return (stored_plastic >= amount*max(1, round(SHEET_MATERIAL_AMOUNT/10)))
+
+// BRRRR
+/obj/item/stock_parts/computer/money_printer/proc/print_money(amount, currency_type, mob/user)
+ if(!can_print(amount, currency_type))
+ return FALSE
+
+ stored_plastic -= amount*max(1, round(SHEET_MATERIAL_AMOUNT/10))
+
+ var/obj/item/cash/cash = new(get_turf(src))
+ cash.set_currency(currency_type)
+ cash.adjust_worth(amount)
+
+ if(user)
+ user.put_in_hands(cash)
+
+ return TRUE
+
+/obj/item/stock_parts/computer/money_printer/filled
+ stored_plastic = 50*SHEET_MATERIAL_AMOUNT
\ No newline at end of file
diff --git a/code/game/objects/items/weapons/cards_ids.dm b/code/game/objects/items/weapons/cards_ids.dm
index 14a859a1484..fa7927c30cf 100644
--- a/code/game/objects/items/weapons/cards_ids.dm
+++ b/code/game/objects/items/weapons/cards_ids.dm
@@ -156,7 +156,7 @@ var/global/const/NO_EMAG_ACT = -50
slot_flags = SLOT_ID
var/list/access = list()
var/registered_name = "Unknown" // The name registered_name on the card
- var/associated_account_number = 0
+ var/associated_account_id = 0
// Associated network account. For normal IDs this is simply informational, but for network enabled IDs this is used for group-based access.
var/list/associated_network_account = list("login" = "", "password" = "")
diff --git a/code/game/objects/items/weapons/circuitboards/machinery/network.dm b/code/game/objects/items/weapons/circuitboards/machinery/network.dm
index 2d0397c012e..614673fdabe 100644
--- a/code/game/objects/items/weapons/circuitboards/machinery/network.dm
+++ b/code/game/objects/items/weapons/circuitboards/machinery/network.dm
@@ -93,4 +93,17 @@
/obj/item/stock_parts/scanning_module = 1,
/obj/item/stock_parts/subspace/transmitter = 1,
/obj/item/stock_parts/micro_laser/high = 1
- )
\ No newline at end of file
+ )
+
+/obj/item/stock_parts/circuitboard/banking_mainframe
+ name = "circuitboard (banking mainframe)"
+ build_path = /obj/machinery/network/bank
+ origin_tech = "{'programming':4,'magnets':3}"
+ req_components = list(
+ /obj/item/stock_parts/capacitor = 1,
+ /obj/item/stock_parts/scanning_module = 2
+ )
+
+ additional_spawn_components = list(
+ /obj/item/stock_parts/power/apc/buildable = 1
+ )
\ No newline at end of file
diff --git a/code/modules/economy/cael/ATM.dm b/code/modules/economy/cael/ATM.dm
index 4abffb17519..2ca59f862a4 100644
--- a/code/modules/economy/cael/ATM.dm
+++ b/code/modules/economy/cael/ATM.dm
@@ -15,7 +15,7 @@
var/datum/money_account/authenticated_account
var/number_incorrect_tries = 0
- var/previous_account_number = 0
+ var/previous_account_id = 0
var/max_pin_attempts = 3
var/ticks_left_locked_down = 0
var/ticks_left_timeout = 0
@@ -90,7 +90,7 @@
if(!user.unEquip(idcard, src))
return
held_card = idcard
- if(authenticated_account && held_card.associated_account_number != authenticated_account.account_number)
+ if(authenticated_account && held_card.associated_account_id != authenticated_account.account_id)
authenticated_account = null
attack_hand(user)
@@ -99,7 +99,10 @@
var/obj/item/cash/dolla = I
//deposit the cash
- if(authenticated_account.deposit(dolla.absolute_worth, "Credit deposit", machine_id))
+ var/err = authenticated_account.deposit(dolla.absolute_worth, "Credit deposit", machine_id)
+ if(err)
+ to_chat(user, "Cash deposit failed: [err].")
+ else
playsound(loc, pick('sound/items/polaroid1.ogg', 'sound/items/polaroid2.ogg'), 50, 1)
to_chat(user, "You insert [I] into [src].")
@@ -112,7 +115,10 @@
if(lock.locked)
to_chat(user, SPAN_WARNING("Cannot transfer funds from a locked [stick.name]."))
else
- if(authenticated_account.deposit(stick.loaded_worth, "Credit deposit", machine_id))
+ var/err = authenticated_account.deposit(stick.loaded_worth, "Credit deposit", machine_id)
+ if(err)
+ to_chat(user, "Cash deposit failed: [err].")
+ else
playsound(loc, pick('sound/items/polaroid1.ogg', 'sound/items/polaroid2.ogg'), 50, 1)
to_chat(user, "You insert [I] into [src].")
@@ -193,7 +199,7 @@
t += ""
t += "Print
"
if(TRANSFER_FUNDS)
- t += "Account balance: [authenticated_account.format_value_by_currency(authenticated_account.money)]
"
+ t += "Account balance: [authenticated_account.format_value_by_currency(authenticated_account.get_balance())]
"
t += "
"
else
- t += "Account balance: [authenticated_account.format_value_by_currency(authenticated_account.money)]"
+ t += "Account balance: [authenticated_account.format_value_by_currency(authenticated_account.get_balance())]"
t += ""
if(user?.mind?.initial_account)
- t += "You recall your personal account number is #[user.mind.initial_account.account_number] and your PIN is [user.mind.initial_account.remote_access_pin].
"
+ t += "You recall your personal account number is #[user.mind.initial_account.account_id] and your PIN is [user.mind.initial_account.remote_access_pin].
"
var/datum/browser/written_digital/popup = new(user, "ATM", machine_id)
popup.set_content(jointext(t,null))
@@ -257,12 +263,16 @@
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"])
+ else if(transfer_amount <= authenticated_account.get_balance())
+ var/target_account_id = text2num(href_list["target_acc_number"])
var/transfer_purpose = href_list["purpose"]
- var/datum/money_account/target_account = get_account(target_account_number)
- if(target_account && authenticated_account.transfer(target_account, transfer_amount, transfer_purpose))
- to_chat(usr, "[html_icon(src)]Funds transfer successful.")
+ var/datum/money_account/target_account = get_glob_account(target_account_id)
+ if(target_account)
+ var/err = authenticated_account.transfer(target_account, transfer_amount, transfer_purpose)
+ if(err)
+ to_chat(usr, "[html_icon(src)]Funds transfer failed: [err].")
+ else
+ to_chat(usr, "[html_icon(src)]Funds transfer successful.")
else
to_chat(usr, "[html_icon(src)]Funds transfer failed.")
@@ -287,36 +297,36 @@
var/tried_account_num = text2num(href_list["account_num"])
//We WILL need an account number entered manually if security is high enough, do not automagic account number
if(!tried_account_num && login_card && (account_security_level != 2))
- tried_account_num = login_card.associated_account_number
+ tried_account_num = login_card.associated_account_id
var/tried_pin = text2num(href_list["account_pin"])
//We'll need more information if an account's security is greater than zero so let's find out what the security setting is
var/datum/money_account/D
//Below is to avoid a runtime
if(tried_account_num)
- D = get_account(tried_account_num)
+ D = get_glob_account(tried_account_num)
if(D)
account_security_level = D.security_level
- authenticated_account = attempt_account_access(tried_account_num, tried_pin, (login_card?.associated_account_number == tried_account_num))
+ authenticated_account = attempt_account_access(tried_account_num, tried_pin, (login_card?.associated_account_id == tried_account_num))
if(!authenticated_account)
number_incorrect_tries++
//let's not count an incorrect try on someone who just needs to put in more information
- if(previous_account_number == tried_account_num && tried_pin)
+ if(previous_account_id == tried_account_num && tried_pin)
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)
+ var/datum/money_account/failed_account = get_glob_account(tried_account_num)
if(failed_account)
failed_account.log_msg("Unauthorized login attempt", machine_id)
else
to_chat(usr, "[html_icon(src)] Incorrect pin/account combination entered, [max_pin_attempts - number_incorrect_tries] attempts remaining.")
- previous_account_number = tried_account_num
+ previous_account_id = tried_account_num
playsound(src, 'sound/machines/buzz-sigh.ogg', 50, 1)
else
to_chat(usr, "[html_icon(src)] Unable to log in to account, additional information may be required.")
@@ -331,7 +341,7 @@
to_chat(usr, "[html_icon(src)] Access granted. Welcome user '[authenticated_account.owner_name].'")
- previous_account_number = tried_account_num
+ previous_account_id = tried_account_num
if("e_withdrawal")
var/amount = max(text2num(href_list["funds_amount"]),0)
amount = round(amount, 0.01)
@@ -343,41 +353,43 @@
alert("That amount exceeds the maximum amount holdable by charge sticks from this machine ([cur.format_value(initial(E.max_worth))]).")
else if(authenticated_account && amount > 0)
//create an entry in the account transaction log
- if(authenticated_account.withdraw(amount, "Credit withdrawal", machine_id))
+ var/err = authenticated_account.withdraw(amount, "Credit withdrawal", machine_id)
+ if(err)
+ to_chat(usr, "[html_icon(src)]Withdrawal failed: [err].")
+ else
playsound(src, 'sound/machines/chime.ogg', 50, 1)
E = new charge_stick_type(loc)
E.adjust_worth(amount)
E.creator = authenticated_account.owner_name
usr.put_in_hands(E)
- else
- to_chat(usr, "[html_icon(src)]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)
- //remove the money
- if(authenticated_account.withdraw(amount, "Credit withdrawal", machine_id))
+ var/err = authenticated_account.withdraw(amount, "Credit withdrawal", machine_id)
+ if(err)
+ to_chat(usr, "[html_icon(src)]Withdrawal failed: [err].")
+ else
playsound(src, 'sound/machines/chime.ogg', 50, 1)
var/obj/item/cash/cash = new(get_turf(usr))
cash.adjust_worth(amount)
usr.put_in_hands(src)
- else
- to_chat(usr, "[html_icon(src)]You don't have enough funds to do that!")
if("balance_statement")
if(authenticated_account)
var/txt
txt = "Automated Teller Account Statement
"
txt += "Account holder: [authenticated_account.owner_name]
"
- txt += "Account number: [authenticated_account.account_number]
"
- txt += "Balance: [authenticated_account.format_value_by_currency(authenticated_account.money)]
"
+ txt += "Account number: [authenticated_account.account_id]
"
+ txt += "Balance: [authenticated_account.format_value_by_currency(authenticated_account.get_balance())]
"
txt += "Date and time: [stationtime2text()], [stationdate2text()]
"
txt += "Service terminal ID: [machine_id]
"
var/obj/item/paper/R = new(src.loc, null, txt, "Account balance: [authenticated_account.owner_name]")
R.apply_custom_stamp(
- overlay_image('icons/obj/bureaucracy.dmi', "paper_stamp-boss", flags = RESET_COLOR),
+ overlay_image('icons/obj/bureaucracy.dmi', "paper_stamp-boss", flags = RESET_COLOR),
"by the [machine_id]")
if(prob(50))
@@ -387,10 +399,10 @@
if ("print_transaction")
if(authenticated_account)
var/txt
-
+
txt = "Transaction logs
"
txt += "Account holder: [authenticated_account.owner_name]
"
- txt += "Account number: [authenticated_account.account_number]
"
+ txt += "Account number: [authenticated_account.account_id]
"
txt += "Date and time: [stationtime2text()], [stationdate2text()]
"
txt += "Service terminal ID: [machine_id]
"
txt += ""
@@ -414,7 +426,7 @@
txt += "
"
var/obj/item/paper/R = new(src.loc, null, txt, "Transaction logs: [authenticated_account.owner_name]")
R.apply_custom_stamp(
- overlay_image('icons/obj/bureaucracy.dmi', "paper_stamp-boss", flags = RESET_COLOR),
+ overlay_image('icons/obj/bureaucracy.dmi', "paper_stamp-boss", flags = RESET_COLOR),
"by the [machine_id]")
if(prob(50))
diff --git a/code/modules/economy/cael/Accounts.dm b/code/modules/economy/cael/Accounts.dm
index ca6ce1288cb..9d8d08ce270 100644
--- a/code/modules/economy/cael/Accounts.dm
+++ b/code/modules/economy/cael/Accounts.dm
@@ -1,7 +1,7 @@
/datum/money_account
var/account_name = ""
var/owner_name = ""
- var/account_number = 0
+ var/account_id = 0
var/remote_access_pin = 0
var/money = 0
var/list/transaction_log = list()
@@ -12,10 +12,22 @@
var/account_type = ACCOUNT_TYPE_PERSONAL
var/currency
+ var/list/pending_modifications = list() // Modifications pending for the account.
+ // For non parent/child accounts this has no implementation.
+
/datum/money_account/New(var/account_type)
account_type = account_type ? account_type : ACCOUNT_TYPE_PERSONAL
if(!ispath(currency, /decl/currency))
currency = global.using_map.default_currency
+ SSmoney_accounts.all_accounts |= src
+
+/datum/money_account/Destroy(force)
+ SSmoney_accounts.all_accounts -= src
+ for(var/datum/account_modification/pending_mod in pending_modifications)
+ qdel(pending_mod)
+
+ transaction_log.Cut() // Transactions are shared, but don't contain references to accounts after performance
+ . = ..()
/datum/money_account/proc/format_value_by_currency(var/amt)
var/decl/currency/cur = GET_DECL(currency)
@@ -23,12 +35,19 @@
// is_source inverts the amount.
/datum/money_account/proc/add_transaction(var/datum/transaction/T, is_source = FALSE)
- money = max(is_source ? money - T.amount : money + T.amount, 0)
+ var/money_adjusted = is_source ? -T.amount : T.amount
+ adjust_money(money_adjusted)
transaction_log += T
+ return money_adjusted
/datum/money_account/proc/get_balance()
return money
+// Returns null on success, or an error string otherwise
+/datum/money_account/proc/can_afford(amount, /datum/money_account/receiver)
+ if(money < amount)
+ return "Insufficient funds"
+
/datum/money_account/proc/log_msg(msg, machine_id)
var/datum/transaction/log/T = new(src, msg, machine_id)
return T.perform()
@@ -45,57 +64,14 @@
var/datum/transaction/T = new(src, to_account, amount, purpose)
return T.perform()
+// Adjusts the money in the account by the given amount. Regardless of implementation, this can not fail.
+/datum/money_account/proc/adjust_money(amount)
+ money = max(money + amount, 0)
-/proc/create_account(var/account_name = "Default account name", var/owner_name, var/starting_funds = 0, var/account_type = ACCOUNT_TYPE_PERSONAL, var/obj/machinery/computer/account_database/source_db)
-
- //create a new account
- var/datum/money_account/M = new()
- M.account_name = account_name
- M.owner_name = (owner_name ? owner_name : account_name)
- M.account_type = account_type
- M.remote_access_pin = rand(1111, 111111)
-
- //create an entry in the account transaction log for when it was created
- //note that using the deposit proc on the account isn't really feasible because we need to change the transaction data before performing it
- var/datum/transaction/singular/T = new(M, (source_db ? source_db.machine_id : "NTGalaxyNet Terminal #[rand(111,1111)]"), starting_funds, "Account creation")
- if(!source_db)
- //set a random date, time and location some time over the past few decades
- T.date = "[num2text(rand(1,31))] [pick("January","February","March","April","May","June","July","August","September","October","November","December")], [global.using_map.game_year - rand(8,18)]"
- T.time = "[rand(0,24)]:[rand(11,59)]"
-
- M.account_number = random_id("station_account_number", 111111, 999999)
- else
- M.account_number = next_account_number
- next_account_number += rand(1,25)
-
- //create a sealed package containing the account details
- var/txt
- txt += "Account details (confidential)
"
- txt += "Account holder: [M.owner_name]
"
- txt += "Account number: [M.account_number]
"
- txt += "Account pin: [M.remote_access_pin]
"
- txt += "Starting balance: [M.format_value_by_currency(M.money)]
"
- txt += "Date and time: [stationtime2text()], [stationdate2text()]
"
- txt += "Creation terminal ID: [source_db.machine_id]
"
- txt += "Authorised officer overseeing creation: [source_db.held_card.registered_name]
"
-
- var/obj/item/paper/R = new /obj/item/paper(null, null, txt, "Account information: [M.account_name]")
- R.apply_custom_stamp(overlay_image('icons/obj/bureaucracy.dmi', icon_state = "paper_stamp-boss", flags = RESET_COLOR), "by the Accounts Database")
- new /obj/item/parcel(source_db.loc, null, R)
-
- //add the account
- T.perform()
- all_money_accounts.Add(M)
-
- return M
-
-//this returns the first account datum that matches the supplied accnum/pin combination, it returns null if the combination did not match any account
-/proc/attempt_account_access(var/attempt_account_number, var/attempt_pin_number, var/valid_card)
- var/datum/money_account/D = get_account(attempt_account_number)
- if(D && (D.security_level != 2 || valid_card) && (!D.security_level || D.remote_access_pin == attempt_pin_number) )
- return D
+/datum/money_account/proc/has_mod_type(type)
+ for(var/datum/account_modification/pending in pending_modifications)
+ if(ispath(pending.type, type))
+ return TRUE
-/proc/get_account(var/account_number)
- for(var/datum/money_account/D in all_money_accounts)
- if(D.account_number == account_number)
- return D
+/datum/money_account/proc/format_account_id()
+ return isnum(account_id) ? "#[account_id]" : account_id
\ No newline at end of file
diff --git a/code/modules/economy/cael/Accounts_DB.dm b/code/modules/economy/cael/Accounts_DB.dm
index b6ccb270f14..2219a304bbc 100644
--- a/code/modules/economy/cael/Accounts_DB.dm
+++ b/code/modules/economy/cael/Accounts_DB.dm
@@ -57,14 +57,14 @@
data["machine_id"] = machine_id
data["creating_new_account"] = creating_new_account
data["detailed_account_view"] = !!detailed_account_view
- data["station_account_number"] = station_account.account_number
+ data["station_account_id"] = station_account.account_id
data["transactions"] = null
data["accounts"] = null
if (detailed_account_view)
- data["account_number"] = detailed_account_view.account_number
+ data["account_id"] = detailed_account_view.account_id
data["owner_name"] = detailed_account_view.owner_name
- data["money"] = detailed_account_view.money
+ data["money"] = detailed_account_view.get_balance()
data["suspended"] = detailed_account_view.suspended
var/list/trx[0]
@@ -81,10 +81,10 @@
data["transactions"] = trx
var/list/accounts[0]
- for(var/i=1, i<=all_money_accounts.len, i++)
- var/datum/money_account/D = all_money_accounts[i]
+ for(var/i=1, i<= length(SSmoney_accounts.all_glob_accounts), i++)
+ var/datum/money_account/D = SSmoney_accounts.all_glob_accounts[i]
accounts.Add(list(list(\
- "account_number"=D.account_number,\
+ "account_id"=D.account_id,\
"owner_name"=D.owner_name,\
"suspended"=D.suspended ? "SUSPENDED" : "",\
"account_index"=i)))
@@ -121,10 +121,10 @@
starting_funds = clamp(starting_funds, 0, station_account.money) // Not authorized to put the station in debt.
starting_funds = min(starting_funds, fund_cap) // Not authorized to give more than the fund cap.
- var/datum/money_account/new_account = create_account("[account_name]'s Personal Account", account_name, starting_funds, ACCOUNT_TYPE_PERSONAL, src)
+ var/datum/money_account/new_account = create_glob_account("[account_name]'s Personal Account", account_name, starting_funds, ACCOUNT_TYPE_PERSONAL, src)
if(starting_funds > 0)
//subtract the money
- station_account.money -= starting_funds
+ station_account.adjust_money(-starting_funds)
//create a transaction log entry
new_account.deposit(starting_funds, "New account activation", machine_id)
@@ -150,8 +150,8 @@
if("view_account_detail")
var/index = text2num(href_list["account_index"])
- if(index && index <= all_money_accounts.len)
- detailed_account_view = all_money_accounts[index]
+ if(index && index <= length(SSmoney_accounts.all_glob_accounts))
+ detailed_account_view = SSmoney_accounts.all_glob_accounts[index]
if("view_accounts_list")
detailed_account_view = null
@@ -168,12 +168,12 @@
var/text
var/obj/item/paper/P = new(loc)
if (detailed_account_view)
- P.SetName("account #[detailed_account_view.account_number] details")
- var/title = "Account #[detailed_account_view.account_number] Details"
+ P.SetName("account #[detailed_account_view.account_id] details")
+ var/title = "Account #[detailed_account_view.account_id] Details"
text = {"
[accounting_letterhead(title)]
Holder: [detailed_account_view.owner_name]
- Balance: [detailed_account_view.format_value_by_currency(detailed_account_view.money)]
+ Balance: [detailed_account_view.format_value_by_currency(detailed_account_view.get_balance())]
Status: [detailed_account_view.suspended ? "Suspended" : "Active"]
Transactions: ([detailed_account_view.transaction_log.len])
@@ -222,13 +222,13 @@
"}
- for(var/i=1, i<=all_money_accounts.len, i++)
- var/datum/money_account/D = all_money_accounts[i]
+ for(var/i=1, i<= length(SSmoney_accounts.all_glob_accounts), i++)
+ var/datum/money_account/D = SSmoney_accounts.all_glob_accounts[i]
text += {"
- | #[D.account_number] |
+ #[D.account_id] |
[D.owner_name] |
- [D.format_value_by_currency(D.money)] |
+ [D.format_value_by_currency(D.get_balance())] |
[D.suspended ? "Suspended" : "Active"] |
"}
diff --git a/code/modules/economy/cael/EFTPOS.dm b/code/modules/economy/cael/EFTPOS.dm
index b82e5eb2a39..87dfcddcdc6 100644
--- a/code/modules/economy/cael/EFTPOS.dm
+++ b/code/modules/economy/cael/EFTPOS.dm
@@ -35,10 +35,10 @@
txt += "2. Lock the new transaction. If you want to modify or cancel the transaction, you simply have to reset your EFTPOS device.
"
txt += "3. Give the EFTPOS device to your customer, he/she must finish the transaction by swiping their ID card or a charge card with enough funds.
"
txt += "4. If everything is done correctly, the money will be transferred. To unlock the device you will have to reset the EFTPOS device.
"
-
+
var/obj/item/paper/R = new(src.loc, null, txt, "Steps to success: Correct EFTPOS Usage")
R.apply_custom_stamp(
- overlay_image('icons/obj/bureaucracy.dmi', icon_state = "paper_stamp-boss", flags = RESET_COLOR),
+ overlay_image('icons/obj/bureaucracy.dmi', icon_state = "paper_stamp-boss", flags = RESET_COLOR),
"by \the [src]")
//by default, connect to the station account
@@ -46,14 +46,14 @@
linked_account = station_account
/obj/item/eftpos/proc/print_reference()
- var/obj/item/paper/R = new(src.loc, null,
- "[eftpos_name] reference
Access code: [access_code]
Do not lose or misplace this code.
",
+ var/obj/item/paper/R = new(src.loc, null,
+ "[eftpos_name] reference
Access code: [access_code]
Do not lose or misplace this code.
",
"Reference: [eftpos_name]")
R.apply_custom_stamp(
- overlay_image('icons/obj/bureaucracy.dmi', icon_state = "paper_stamp-boss", flags = RESET_COLOR),
+ overlay_image('icons/obj/bureaucracy.dmi', icon_state = "paper_stamp-boss", flags = RESET_COLOR),
"by the [src]")
-
-
+
+
var/obj/item/parcel/D = new(R.loc, null, R, "EFTPOS access code")
D.attach_label(usr, null, "EFTPOS access code")
@@ -203,11 +203,11 @@
if(linked_account)
if(!linked_account.suspended)
var/attempt_pin = ""
- var/datum/money_account/D = get_account(C.associated_account_number)
+ var/datum/money_account/D = get_glob_account(C.associated_account_id)
if(D && D.security_level)
attempt_pin = input("Enter pin code", "EFTPOS transaction") as num
D = null
- D = attempt_account_access(C.associated_account_number, attempt_pin, TRUE)
+ D = attempt_account_access(C.associated_account_id, attempt_pin, TRUE)
if(D)
//transfer the money
if(D.transfer(linked_account, transaction_amount, "[transaction_purpose] (via [eftpos_name]/[machine_id])"))
diff --git a/code/modules/economy/cael/Transactions.dm b/code/modules/economy/cael/Transactions.dm
index 5b1867f3dff..90df67aa58d 100644
--- a/code/modules/economy/cael/Transactions.dm
+++ b/code/modules/economy/cael/Transactions.dm
@@ -11,8 +11,12 @@
var/datum/money_account/target = null
var/datum/money_account/source = null
+ // After the transaction is performed these are created and target/source nulled for GC purposes.
+ var/target_name
+ var/source_name
+
// Can also do negative amounts to transfer from target to source
-/datum/transaction/New(_source, _target, _amount, _purpose)
+/datum/transaction/New(datum/money_account/_source, datum/money_account/_target, _amount, _purpose)
..()
date = stationdate2text()
@@ -24,36 +28,48 @@
source = _source
target = _target
+/datum/transaction/Destroy(force)
+ target = null
+ source = null
+ . = ..()
+
+// Whether or not the transaction is valid. Returns null on success, error message on failure.
/datum/transaction/proc/valid()
// None of the involved accounts can be suspended
- if(target.suspended || source.suspended)
- return FALSE
-
+ if(target.suspended)
+ return "Account [target.format_account_id()] ([target.account_name]) is suspended"
+ if(source.suspended)
+ return "Account [source.format_account_id()] ([source.account_name]) is suspended"
// The payer must be able to afford the transaction
- if(!source_can_afford())
- return FALSE
-
- return TRUE
+ var/afford_error = source.can_afford(amount, target)
+ if(afford_error)
+ return "Account [source.format_account_id()] ([source.account_name]) cannot afford the transaction. [afford_error]"
// Whether or not the source account can afford the transaction
/datum/transaction/proc/source_can_afford()
return (source.money >= amount)
/datum/transaction/proc/get_target_name()
- return target.account_name
+ return target ? target.account_name : target_name
/datum/transaction/proc/get_source_name()
- return source.account_name
+ return source ? source.account_name : source_name
-// Performs the transaction on both the source and target accounts
+// Performs the transaction on both the source and target accounts.
+// Returns null on success, error on failure.
/datum/transaction/proc/perform()
- if(!valid())
- return FALSE
+ var/error = valid()
+ if(error)
+ return error
target.add_transaction(src)
source.add_transaction(src, TRUE)
- return TRUE
+ target_name = get_target_name()
+ source_name = get_source_name()
+
+ target = null
+ source = null
/*
Transactions that only involve one account
@@ -65,27 +81,37 @@
return (amount > 0)
/datum/transaction/singular/valid()
- return !source.suspended && source_can_afford()
-
-/datum/transaction/singular/source_can_afford()
- return source.money + amount >= 0
+ if(source.suspended)
+ return "Account [source.format_account_id()] is suspended"
+ if(!is_deposit())
+ var/afford_error = source.can_afford(-amount, target)
+ if(afford_error)
+ return "Account [source.format_account_id()] cannot afford the transaction. [afford_error]"
// For deposits: returns the name of the account the money was deposited to. For withdrawals: returns the machine ID of the machine the withdrawal was made at
/datum/transaction/singular/get_target_name()
- return (is_deposit() ? source.account_name : target)
+ if(target && source)
+ return (is_deposit() ? source.account_name : target)
+ return target_name
// For deposits: returns the machine ID of the machine the deposit was made to. For withdrawals: returns the name of the account the money was withdrawn from
/datum/transaction/singular/get_source_name()
- return (is_deposit() ? target : source.account_name)
+ if(target && source)
+ return (is_deposit() ? target : source.account_name)
+ return source_name
/datum/transaction/singular/perform()
- if(!valid())
- return FALSE
+ var/error = valid()
+ if(error)
+ return error
source.add_transaction(src)
- return TRUE
+ target_name = get_target_name()
+ source_name = get_source_name()
+ target = null
+ source = null
/*
Log messages
These should only be made through the logmsg proc of the account you want to create a log on!
@@ -94,20 +120,22 @@
/datum/transaction/log
var/machine_id = "ERRID#?"
-/datum/transaction/log/New(account, message, _machine_id)
+/datum/transaction/log/New(datum/money_account/account, message, _machine_id)
machine_id = _machine_id
-
+ target_name = account.account_name
..(null, account, 0, "LOG: [message]")
/datum/transaction/log/valid()
- return TRUE
+ return
/datum/transaction/log/get_source_name()
- return machine_id
+ return machine_id ? machine_id : "LOG"
/datum/transaction/log/perform()
- if(!valid())
- return FALSE
+ var/error = valid()
+ if(error)
+ return error
target.add_transaction(src)
- return TRUE
+
+ target = null
\ No newline at end of file
diff --git a/code/modules/economy/cael/account_mod.dm b/code/modules/economy/cael/account_mod.dm
new file mode 100644
index 00000000000..a2f6bfcb5a0
--- /dev/null
+++ b/code/modules/economy/cael/account_mod.dm
@@ -0,0 +1,231 @@
+// Holder for a modification happening to an account, or some other temporary condition.
+/datum/account_modification
+ var/name = "Account modification"
+ var/start_time
+ var/mod_delay
+
+ var/datum/money_account/affecting
+ var/suspends_withdrawal_limit = FALSE // Whether or not the account modification puts a hold on withdrawal limits
+ var/allow_early = TRUE // Whether or not the target account can choose to activate the modification earlier
+ var/allow_cancel = TRUE // Whether or not a modification can be cancelled prior to activation.
+
+/datum/account_modification/New(n_affecting)
+ start_time = REALTIMEOFDAY
+ affecting = n_affecting
+ . = ..()
+
+/datum/account_modification/Destroy(force)
+ affecting.pending_modifications -= src
+ affecting = null
+ . = ..()
+
+/datum/account_modification/proc/modify_account()
+ SHOULD_CALL_PARENT(TRUE)
+ affecting.pending_modifications -= src
+ qdel(src)
+
+// Obtains input from the user for creation, modifying variables as necessary. CanInteract() and access etc. must be checked elsewhere.
+/datum/account_modification/proc/prompt_creation(mob/user)
+
+// Returns a human readable notification describing the change.
+/datum/account_modification/proc/get_notification()
+ if(suspends_withdrawal_limit)
+ return "As a result, withdrawal limits have been suspended."
+
+// Returns a list of UI data for tabulated viewing
+/datum/account_modification/proc/get_ui_data()
+ return list("name" = name, "desc" = get_short_desc(), "allow_early" = allow_early, "allow_cancel" = allow_cancel, "suspends_wlimit" = suspends_withdrawal_limit, "countdown" = get_readable_countdown())
+
+/datum/account_modification/proc/get_short_desc()
+
+/datum/account_modification/proc/get_readable_countdown()
+ return minutes_to_readable((start_time + mod_delay - REALTIMEOFDAY)/(1 MINUTES))
+
+/datum/account_modification/modify_interest
+ name = "Interest rate modification"
+ var/new_interest
+
+/datum/account_modification/modify_interest/prompt_creation(mob/user)
+ var/n_interest = input(user, "Enter the new interest rate (between -1 and 1):", "Interest Rate") as num
+ new_interest = clamp(n_interest, -1, 1)
+
+ var/datum/money_account/child/affected_child = affecting
+ if(istype(affected_child))
+ if(new_interest < affected_child.interest_rate)
+ suspends_withdrawal_limit = TRUE
+ else
+ log_error("An interest account modification was created for a non-child account!")
+
+/datum/account_modification/modify_interest/New(n_affecting)
+ mod_delay = config.interest_mod_delay
+ . = ..()
+
+/datum/account_modification/modify_interest/modify_account()
+ var/datum/money_account/child/affected_child = affecting
+ if(istype(affected_child))
+ affected_child.interest_rate = new_interest
+ else
+ log_error("An interest account modification was queued for a non-child account!")
+
+ ..()
+
+/datum/account_modification/modify_interest/get_notification()
+ var/datum/money_account/child/affected_child = affecting
+ if(!istype(affected_child))
+ return
+ . = "In [get_readable_countdown()], the interest rate on your account will be [affected_child.interest_rate > new_interest ? "lowered" : "raised"] to [new_interest]."
+ var/parent_notif = ..()
+ if(istext(parent_notif))
+ return . + " " + parent_notif
+
+/datum/account_modification/modify_interest/get_short_desc()
+ var/datum/money_account/child/affected_child = affecting
+ return "[affected_child.interest_rate > new_interest ? "Lowers" : "Raises"] interest rate to [new_interest]"
+
+/datum/account_modification/modify_withdrawal
+ name = "Withdrawal limit modification"
+ var/new_withdrawal_limit
+
+/datum/account_modification/modify_withdrawal/prompt_creation(mob/user)
+ var/n_withdrawal = input(user, "Enter the new withdrawal limit:", "Withdrawal limit") as num
+ new_withdrawal_limit = max(0, FLOOR(n_withdrawal))
+
+ var/datum/money_account/child/affected_child = affecting
+ if(istype(affected_child))
+ if(new_withdrawal_limit < affected_child.withdrawal_limit)
+ // Check to see if the user could get out all their money in time with the NEW withdrawal limit in the modification delay period.
+ // We check against the new withdrawal limit so that suddenly changing the withdrawal limit extremely low isn't as effective.
+ suspends_withdrawal_limit = (affected_child.money / affected_child.withdrawal_limit) > (affected_child.withdrawal_limit * new_withdrawal_limit/mod_delay)
+ else
+ log_error("A withdrawal limit account modification was created for a non-child account!")
+
+/datum/account_modification/modify_withdrawal/New(n_affecting)
+ mod_delay = config.withdraw_mod_delay
+ . = ..()
+
+/datum/account_modification/modify_withdrawal/modify_account()
+ var/datum/money_account/child/affected_child = affecting
+ if(istype(affected_child))
+ affected_child.withdrawal_limit = new_withdrawal_limit
+ else
+ log_error("A withdrawal limit account modification was queued for an non-child account!")
+ ..()
+
+/datum/account_modification/modify_withdrawal/get_notification()
+ var/datum/money_account/child/affected_child = affecting
+ if(!istype(affected_child))
+ return
+ . = "In [get_readable_countdown()], the withdrawal limit on your account will be [affected_child.withdrawal_limit > new_withdrawal_limit ? "lowered" : "raised"] to [new_withdrawal_limit]."
+ var/parent_notif = ..()
+ if(istext(parent_notif))
+ return . + " " + parent_notif
+
+/datum/account_modification/modify_withdrawal/get_short_desc()
+ var/datum/money_account/child/affected_child = affecting
+ return "[affected_child.withdrawal_limit > new_withdrawal_limit ? "Lowers" : "Raises"] withdrawal limit to [new_withdrawal_limit]"
+
+/datum/account_modification/modify_transaction
+ name = "Transaction fee modification"
+ var/new_transaction_fee
+
+/datum/account_modification/modify_transaction/prompt_creation(mob/user)
+ new_transaction_fee = input(user, "Enter the new transaction fee:", "Transaction fee") as num
+ new_transaction_fee = max(0, FLOOR(new_transaction_fee))
+
+
+/datum/account_modification/modify_transaction/New(n_affecting)
+ mod_delay = config.transaction_mod_delay
+ . = ..()
+
+/datum/account_modification/modify_transaction/modify_account()
+ var/datum/money_account/child/affected_child = affecting
+ if(istype(affected_child))
+ affected_child.transaction_fee = new_transaction_fee
+ else
+ log_error("A transaction fee account modification was queued for an non-child account!")
+ ..()
+
+/datum/account_modification/modify_transaction/get_notification()
+ var/datum/money_account/child/affected_child = affecting
+ if(!istype(affected_child))
+ return
+ . = "In [get_readable_countdown()], the transaction fee on your account will be [affected_child.transaction_fee > new_transaction_fee ? "lowered" : "raised"] to [new_transaction_fee]."
+ var/parent_notif = ..()
+ if(istext(parent_notif))
+ return . + " " + parent_notif
+
+/datum/account_modification/modify_transaction/get_short_desc()
+ var/datum/money_account/child/affected_child = affecting
+ return "[affected_child.transaction_fee > new_transaction_fee ? "Lowers" : "Raises"] transaction fee to [new_transaction_fee]"
+
+/datum/account_modification/suspend_limit
+ name = "Suspend withdrawal limit"
+ suspends_withdrawal_limit = TRUE
+ mod_delay = 1 DAY
+
+/datum/account_modification/suspend_limit/get_notification()
+ return "The withdrawal limit on your account has been suspended for [get_readable_countdown()]"
+
+/datum/account_modification/suspend_limit/get_short_desc()
+ return "Suspends withdrawal limit on account for [get_readable_countdown()]"
+/datum/account_modification/modify_fractional_reserve
+ name = "Fractional reserve modification"
+ var/new_fractional_reserve
+ allow_early = FALSE
+
+/datum/account_modification/modify_fractional_reserve/prompt_creation(mob/user)
+ new_fractional_reserve = input(user, "Enter the new fractional reserve (between 0 and 1):", "Fractional reserve") as num
+ new_fractional_reserve = clamp(new_fractional_reserve, 0, 1)
+
+ var/datum/money_account/parent/affected_parent = affecting
+ if(istype(affected_parent))
+ if(new_fractional_reserve < affected_parent.fractional_reserve)
+ suspends_withdrawal_limit = TRUE
+ else
+ log_error("A fractional reserve account modification was created for a non-parent account!")
+
+/datum/account_modification/modify_fractional_reserve/New(n_affecting, n_frac_reserve)
+ mod_delay = config.fractional_reserve_mod_delay
+ . = ..()
+
+/datum/account_modification/modify_fractional_reserve/modify_account()
+ var/datum/money_account/parent/affected_parent = affecting
+ if(istype(affected_parent))
+ affected_parent.fractional_reserve = new_fractional_reserve
+ else
+ log_error("A fractional reserve account modification was queued for an non-parent account!")
+ ..()
+
+/datum/account_modification/modify_fractional_reserve/get_notification()
+ var/datum/money_account/parent/affected_parent = affecting
+ if(!istype(affected_parent))
+ return
+ . = "In [get_readable_countdown()], the fractional reserve of [affected_parent.account_name] will be [affected_parent.fractional_reserve > new_fractional_reserve ? "lowered" : "raised"] to [new_fractional_reserve]."
+ var/parent_notif = ..()
+ if(istext(parent_notif))
+ return . + " " + parent_notif
+
+/datum/account_modification/modify_fractional_reserve/get_short_desc()
+ var/datum/money_account/parent/affected_parent = affecting
+ return "[affected_parent.fractional_reserve > new_fractional_reserve ? "Lowers" : "Raises"] fractional reserve to [new_fractional_reserve]"
+
+/datum/account_modification/theft_prevention
+ name = "Theft prevention measures"
+ allow_early = FALSE
+ allow_cancel = FALSE
+
+/datum/account_modification/theft_prevention/New()
+ mod_delay = config.anti_tamper_mod_delay
+ . = ..()
+
+/datum/account_modification/theft_prevention/get_notification()
+ var/datum/money_account/parent/affected_parent = affecting
+ if(!istype(affected_parent))
+ return
+ . = "Due to a recent theft, enhanced anti-theft measures are in place for [get_readable_countdown()]. Further tampering with money storage devices will trigger an escrow panic."
+ var/parent_notif = ..()
+ if(istext(parent_notif))
+ return . + " " + parent_notif
+
+/datum/account_modification/theft_prevention/get_short_desc()
+ return "Enhanced anti-theft measures are in place."
\ No newline at end of file
diff --git a/code/modules/economy/cael/escrow.dm b/code/modules/economy/cael/escrow.dm
new file mode 100644
index 00000000000..a12e3d422d9
--- /dev/null
+++ b/code/modules/economy/cael/escrow.dm
@@ -0,0 +1,20 @@
+// An account which temporarily holds money ejected from elsewhere.
+/datum/money_account/escrow
+
+
+/datum/money_account/escrow/Destroy(force)
+ SSmoney_accounts.all_escrow_accounts -= src
+ . = ..()
+
+/datum/money_account/escrow/deposit(amount, purpose, machine_id)
+ return "Account [format_account_id()] ([account_name]) does not allow deposits"
+
+/datum/money_account/escrow/withdraw(amount, purpose, machine_id)
+ . = ..()
+ if(money <= 0)
+ qdel(src)
+
+/datum/money_account/escrow/transfer(to_account, amount, purpose)
+ . = ..()
+ if(money <= 0)
+ qdel(src)
\ No newline at end of file
diff --git a/code/modules/economy/cael/global_accounts.dm b/code/modules/economy/cael/global_accounts.dm
new file mode 100644
index 00000000000..d91ff4f3827
--- /dev/null
+++ b/code/modules/economy/cael/global_accounts.dm
@@ -0,0 +1,54 @@
+// Global accounts accessible from anywhere. Basically, the default player and station accounts accessible through ATMs
+/proc/create_glob_account(var/account_name = "Default account name", var/owner_name, var/starting_funds = 0, var/account_type = ACCOUNT_TYPE_PERSONAL, var/obj/machinery/computer/account_database/source_db)
+
+ //create a new account
+ var/datum/money_account/M = new()
+ M.account_name = account_name
+ M.owner_name = (owner_name ? owner_name : account_name)
+ M.account_type = account_type
+ M.remote_access_pin = rand(1111, 111111)
+
+ //create an entry in the account transaction log for when it was created
+ //note that using the deposit proc on the account isn't really feasible because we need to change the transaction data before performing it
+ var/datum/transaction/singular/T = new(M, (source_db ? source_db.machine_id : "NTGalaxyNet Terminal #[rand(111,1111)]"), starting_funds, "Account creation")
+ if(!source_db)
+ //set a random date, time and location some time over the past few decades
+ T.date = "[num2text(rand(1,31))] [pick("January","February","March","April","May","June","July","August","September","October","November","December")], [global.using_map.game_year - rand(8,18)]"
+ T.time = "[rand(0,24)]:[rand(11,59)]"
+
+ M.account_id = random_id("station_account_number", 111111, 999999)
+ else
+ M.account_id = next_account_number
+ next_account_number += rand(1,25)
+
+ //create a sealed package containing the account details
+ var/txt
+ txt += "Account details (confidential)
"
+ txt += "Account holder: [M.owner_name]
"
+ txt += "Account number: [M.format_account_id()]
"
+ txt += "Account pin: [M.remote_access_pin]
"
+ txt += "Starting balance: [M.format_value_by_currency(M.money)]
"
+ txt += "Date and time: [stationtime2text()], [stationdate2text()]
"
+ txt += "Creation terminal ID: [source_db.machine_id]
"
+ txt += "Authorised officer overseeing creation: [source_db.held_card.registered_name]
"
+
+ var/obj/item/paper/R = new /obj/item/paper(null, null, txt, "Account information: [M.account_name]")
+ R.apply_custom_stamp(overlay_image('icons/obj/bureaucracy.dmi', icon_state = "paper_stamp-boss", flags = RESET_COLOR), "by the Accounts Database")
+ new /obj/item/parcel(source_db.loc, null, R)
+
+ //add the account
+ T.perform()
+ SSmoney_accounts.all_glob_accounts.Add(M)
+
+ return M
+
+//this returns the first account datum that matches the supplied accnum/pin combination, it returns null if the combination did not match any account
+/proc/attempt_account_access(var/attempt_account_id, var/attempt_pin_number, var/valid_card)
+ var/datum/money_account/D = get_glob_account(attempt_account_id)
+ if(D && (D.security_level != 2 || valid_card) && (!D.security_level || D.remote_access_pin == attempt_pin_number) )
+ return D
+
+/proc/get_glob_account(var/account_id)
+ for(var/datum/money_account/D in SSmoney_accounts.all_glob_accounts)
+ if(D.account_id == account_id)
+ return D
diff --git a/code/modules/economy/cael/related_accounts.dm b/code/modules/economy/cael/related_accounts.dm
new file mode 100644
index 00000000000..88e5de2f3ad
--- /dev/null
+++ b/code/modules/economy/cael/related_accounts.dm
@@ -0,0 +1,164 @@
+// Parent accounts hold the true money distributed among its child accounts. Child accounts check against
+// and add/subtract to the parent accounts during transactions. Parent accounts may only be required to
+// keep a certain amount of the total money in its child accounts (fractional reserve) as actual funds.
+/datum/money_account/parent
+ var/fractional_reserve = 1
+ var/child_totals
+
+ var/open_escrow_on_destroy = TRUE // Whether or not escrow accounts will be opened for child accounts in Destroy().
+
+ var/list/datum/money_account/child/children = list()
+
+/datum/money_account/parent/Destroy(force)
+ QDEL_NULL(children)
+ . = ..()
+
+/datum/money_account/parent/can_afford(amount, datum/money_account/receiver)
+ . = ..()
+ if(.)
+ return
+ // If the receiver is a child account, then the money itself isn't moving, but we need to be able
+ // to afford the new fractional reserve requirement.
+ if(receiver in children)
+ if(fractional_reserve*(child_totals + amount) > money)
+ return "Transaction violates fractional reserve requirements"
+ else if(fractional_reserve*child_totals > money - amount)
+ return "Transaction violates fractional reserve requirements"
+
+// Dumps as much money as possible into globally available, withdraw only escrow accounts.
+/datum/money_account/parent/proc/escrow_panic()
+ if(!child_totals)
+ return
+
+ for(var/datum/money_account/child/to_escrow in children)
+ to_escrow.on_escrow()
+
+// Returns the maximum amount that can be withdrawn from the account with current fractional reserve requirement.
+/datum/money_account/parent/proc/get_max_withdraw()
+ return max(0, money - fractional_reserve*child_totals)
+
+// Child accounts hold a 'fake' amount of money which actually reflects a share in the parent account.
+// Child accounts can receive interest from the parent account at fixed intervals, and may be subject to a withdrawal limit.
+// If the parent account only requires a fractional reserve, then the sum of all the money in the children accounts
+// may not equal the money in the parent account.
+/datum/money_account/child
+
+ var/withdrawal_limit = 0 // Maximum amount of money which can be withdrawn in a period. Defaults to no limit.
+ var/current_withdrawal = 0 // Amount of money withdrawn within the last period.
+ var/last_withdraw_period
+
+ var/transaction_fee = 0 // Fee paid per transaction/withdraw out of the account. Transaction fees do not apply
+ // between parent/child and children with the same parent account.
+
+ var/interest_rate = 0
+ var/last_interest_period
+
+ var/datum/money_account/parent/parent_account
+
+/datum/money_account/child/New(account_type, p_account, n_interest, n_withdrawal_limit, n_transaction_fee)
+ parent_account = p_account
+ parent_account.children |= src
+ interest_rate = n_interest
+ withdrawal_limit = n_withdrawal_limit
+ transaction_fee = n_transaction_fee
+ last_withdraw_period = REALTIMEOFDAY
+ last_interest_period = REALTIMEOFDAY
+ . = ..()
+
+/datum/money_account/child/Destroy(force)
+ parent_account.children -= src
+ parent_account.child_totals -= money
+ parent_account = null
+ . = ..()
+
+/datum/money_account/child/can_afford(amount, datum/money_account/receiver)
+ if(!parent_account)
+ // CASH GONE.
+ return "Transaction failed. Contact your financial provider"
+ . = ..()
+ if(.)
+ return
+
+ if(withdrawal_limit && !withdrawal_limit_suspended())
+ if(current_withdrawal + amount > withdrawal_limit)
+ return "Transaction goes over withdrawal limit"
+
+ if(amount + transaction_fee > money)
+ return "Transaction fee cannot be afforded"
+
+ // Accounts with the same parent can continue passing around monopoly money even if the parent is insolvent.
+ if(receiver in parent_account.children)
+ return
+
+ // Likely due to theft of some sort.
+ if(parent_account.money < amount)
+ return "Transaction failed. Financial provider may be insolvent"
+
+/datum/money_account/child/add_transaction(datum/transaction/T, is_source)
+ var/amount_adjusted = ..()
+ // Parent accounts are technically allowed to run negative as a measure of 'debt' to child accounts.
+ // This should not occur from normal transactions
+ if(parent_account)
+ if(amount_adjusted < 0)
+ // Transaction fee doesn't apply to accounts with the same parent.
+ var/adj_transaction_fee = T.target && (T.target in (parent_account.children | parent_account)) ? 0 : transaction_fee
+ // Remove the transaction fee, if it exists.
+ adjust_money(-adj_transaction_fee)
+
+ current_withdrawal += abs(T.amount)
+
+ parent_account.adjust_money(T.amount)
+ parent_account.child_totals += (T.amount - adj_transaction_fee)
+ else
+ parent_account.adjust_money(T.amount)
+ parent_account.child_totals += T.amount
+
+/datum/money_account/child/proc/accrue_interest()
+ if(!interest_rate || !parent_account)
+ return
+
+ var/interest_amount = round(interest_rate*money)
+ if(interest_amount == 0)
+ return
+
+ // This just reduces the amount of money in the child account and the total, but we process it as a normal
+ // transaction for logging purposes.
+ if(interest_amount < 0)
+ var/err = transfer(parent_account, -interest_amount, "Interest payment")
+ if(err)
+ parent_account.log_msg("Interest did not accrue for account [format_account_id()]: [err].")
+ return
+
+ // This checks if accruing interest would violate reserve limits etc.
+ var/err = parent_account.transfer(src, interest_amount, "Interest payment")
+ if(err)
+ log_msg("Interest did not accrue for this period: [err]. Contact your financial provider.")
+ return
+
+/datum/money_account/child/proc/withdrawal_limit_suspended()
+ for(var/datum/account_modification/pending_mod in pending_modifications)
+ if(pending_mod.suspends_withdrawal_limit)
+ return TRUE
+
+ if(parent_account)
+ for(var/datum/account_modification/pending_mod in parent_account.pending_modifications)
+ if(pending_mod.suspends_withdrawal_limit)
+ return TRUE
+
+// Returns the amount actually escrowed
+/datum/money_account/child/proc/on_escrow()
+ if(!parent_account || !money) // Nothing to escrow!
+ return
+
+ var/solvency = max(1, parent_account.money / parent_account.child_totals)
+
+ var/escrowed_money = FLOOR(min(solvency*money, parent_account.money))
+ var/datum/money_account/escrow/child_escrow = SSmoney_accounts.get_or_add_escrow(account_id, remote_access_pin, parent_account.owner_name)
+ child_escrow.money += escrowed_money
+
+ money -= escrowed_money
+
+ parent_account.money -= escrowed_money
+ parent_account.child_totals -= escrowed_money
+
+ return escrowed_money
\ No newline at end of file
diff --git a/code/modules/events/money_hacker.dm b/code/modules/events/money_hacker.dm
index 4806c8c166b..b6b02e5014b 100644
--- a/code/modules/events/money_hacker.dm
+++ b/code/modules/events/money_hacker.dm
@@ -7,8 +7,8 @@ var/global/account_hack_attempted = 0
/datum/event/money_hacker/setup()
end_time = world.time + 6000
- if(all_money_accounts.len)
- affected_account = pick(all_money_accounts)
+ if(SSmoney_accounts.all_glob_accounts.len)
+ affected_account = pick(SSmoney_accounts.all_glob_accounts)
account_hack_attempted = 1
else
@@ -22,7 +22,7 @@ var/global/account_hack_attempted = 0
break
if(MS)
// Hide the account number for now since it's all you need to access a standard-security account. Change when that's no longer the case.
- var/message = "A brute force hack has been detected (in progress since [stationtime2text()]). The target of the attack is: Financial account #[affected_account.account_number], \
+ var/message = "A brute force hack has been detected (in progress since [stationtime2text()]). The target of the attack is: Financial account [affected_account.format_account_id()], \
without intervention this attack will succeed in approximately 10 minutes. Required intervention: temporary suspension of affected accounts until the attack has ceased. \
Notifications will be sent as updates occur."
var/my_department = "[location_name()] Firewall Subroutines"
@@ -54,7 +54,7 @@ var/global/account_hack_attempted = 0
var/time1 = rand(0, 99999999)
var/time2 = "[round(time1 / 36000)+12]:[(time1 / 600 % 60) < 10 ? add_zero(time1 / 600 % 60, 1) : time1 / 600 % 60]"
T.time = pick("", stationtime2text(), time2)
-
+
T.perform()
var/obj/machinery/network/message_server/MS
@@ -64,4 +64,3 @@ var/global/account_hack_attempted = 0
var/my_department = "[location_name()] Firewall Subroutines"
MS.send_rc_message("XO's Desk", my_department, message, "", "", 2)
break
-
\ No newline at end of file
diff --git a/code/modules/events/money_lotto.dm b/code/modules/events/money_lotto.dm
index 0cd4a07d1fb..8e433309571 100644
--- a/code/modules/events/money_lotto.dm
+++ b/code/modules/events/money_lotto.dm
@@ -7,8 +7,8 @@
/datum/event/money_lotto/start()
winner_sum = pick(5000, 10000, 50000, 100000, 500000, 1000000, 1500000)
if(prob(50))
- if(all_money_accounts.len)
- winner_account = pick(all_money_accounts)
+ if(SSmoney_accounts.all_glob_accounts.len)
+ winner_account = pick(SSmoney_accounts.all_glob_accounts)
winner_name = winner_account.owner_name
deposit_success = winner_account.deposit(winner_sum, "Nyx Daily Loan Lottery winner!", "Biesel TCD Terminal #[rand(111,333)]")
else
diff --git a/code/modules/fabrication/designs/general/designs_general.dm b/code/modules/fabrication/designs/general/designs_general.dm
index 7d63d375f6b..19c531f7efb 100644
--- a/code/modules/fabrication/designs/general/designs_general.dm
+++ b/code/modules/fabrication/designs/general/designs_general.dm
@@ -161,3 +161,6 @@
path = /obj/item/stack/package_wrap
/datum/fabricator_recipe/gift_wrapper
path = /obj/item/stack/package_wrap/gift
+
+/datum/fabricator_recipe/network_pos
+ path = /obj/item/network_pos
diff --git a/code/modules/fabrication/designs/protolathe/designs_computer_accessories.dm b/code/modules/fabrication/designs/protolathe/designs_computer_accessories.dm
index 90d67bb6b3d..b0476a4fb4e 100644
--- a/code/modules/fabrication/designs/protolathe/designs_computer_accessories.dm
+++ b/code/modules/fabrication/designs/protolathe/designs_computer_accessories.dm
@@ -10,7 +10,7 @@
/datum/fabricator_recipe/protolathe/comp_acc/mstickbroadcaster
path = /obj/item/stock_parts/computer/charge_stick_slot/broadcaster
-
+
/datum/fabricator_recipe/protolathe/comp_acc/nanoprinter
path = /obj/item/stock_parts/computer/nano_printer
@@ -32,6 +32,9 @@
/datum/fabricator_recipe/protolathe/comp_acc/medical_scanner
path = /obj/item/stock_parts/computer/scanner/medical
+/datum/fabricator_recipe/protolathe/comp_acc/money_printer
+ path = /obj/item/stock_parts/computer/money_printer
+
//////////////////////////////////////////////////////////////////
// Frames
//////////////////////////////////////////////////////////////////
diff --git a/code/modules/goals/definitions/personal.dm b/code/modules/goals/definitions/personal.dm
index 8707f55f4f5..9df3591d9fe 100644
--- a/code/modules/goals/definitions/personal.dm
+++ b/code/modules/goals/definitions/personal.dm
@@ -28,7 +28,7 @@
/datum/goal/money/update_strings()
target_amount = rand(100, 200)
var/datum/mind/mind = owner
- for(var/datum/money_account/acct in all_money_accounts)
+ for(var/datum/money_account/acct in SSmoney_accounts.all_glob_accounts)
if(acct.owner_name == mind.current.real_name)
target_amount = acct.get_balance() * rand(2,3)
break
@@ -36,7 +36,7 @@
/datum/goal/money/check_success()
var/datum/mind/mind = owner
- for(var/datum/money_account/acct in all_money_accounts)
+ for(var/datum/money_account/acct in SSmoney_accounts.all_glob_accounts)
if(acct.owner_name == mind.current.real_name)
return acct.get_balance() > target_amount
return FALSE
\ No newline at end of file
diff --git a/code/modules/modular_computers/file_system/program.dm b/code/modules/modular_computers/file_system/program.dm
index d0e5ce447d9..ed32d402c6e 100644
--- a/code/modules/modular_computers/file_system/program.dm
+++ b/code/modules/modular_computers/file_system/program.dm
@@ -176,6 +176,9 @@
else
return -1
+// Returns the amount of cash taken, if any.
+/datum/computer_file/program/proc/process_cash(var/obj/item/cash/received_cash)
+
/datum/nano_module/program
available_to_ai = FALSE
var/datum/computer_file/program/program = null // Program-Based computer program that runs this nano module. Defaults to null.
diff --git a/code/modules/modular_computers/file_system/programs/command/card.dm b/code/modules/modular_computers/file_system/programs/command/card.dm
index 1866a762abd..58d48b67a1d 100644
--- a/code/modules/modular_computers/file_system/programs/command/card.dm
+++ b/code/modules/modular_computers/file_system/programs/command/card.dm
@@ -32,7 +32,7 @@
if(card_slot)
var/obj/item/card/id/id_card = card_slot.stored_card
data["has_id"] = !!id_card
- data["id_account_number"] = id_card ? id_card.associated_account_number : null
+ data["id_account_id"] = id_card ? id_card.associated_account_id : null
data["network_account_login"] = id_card ? id_card.associated_network_account["login"] : null
data["network_account_password"] = id_card ? stars(id_card.associated_network_account["password"], 0) : null
data["id_rank"] = id_card && id_card.assignment ? id_card.assignment : "Unassigned"
@@ -160,7 +160,7 @@
For: [id_card.registered_name ? id_card.registered_name : "Unregistered"]
Assignment: [id_card.assignment]
- Account Number: #[id_card.associated_account_number]
+ Account Number: #[id_card.associated_account_id]
Network account: [id_card.associated_network_account["login"]]
Network password: [stars(id_card.associated_network_account["password"], 0)]
Blood Type: [id_card.blood_type]
@@ -214,8 +214,8 @@
else
computer.show_error(usr, "Invalid name entered!")
else if(href_list["account"])
- var/account_num = text2num(input("Enter account number.", "Account", id_card.associated_account_number))
- id_card.associated_account_number = account_num
+ var/account_num = text2num(input("Enter account number.", "Account", id_card.associated_account_id))
+ id_card.associated_account_id = account_num
else if(href_list["alogin"])
var/account_login = input("Enter network account login.", "Network account login", id_card.associated_network_account["login"])
id_card.associated_network_account["login"] = account_login
diff --git a/code/modules/modular_computers/file_system/programs/command/finances.dm b/code/modules/modular_computers/file_system/programs/command/finances.dm
new file mode 100644
index 00000000000..c9985d898d2
--- /dev/null
+++ b/code/modules/modular_computers/file_system/programs/command/finances.dm
@@ -0,0 +1,573 @@
+#define STATE_ERROR -1
+#define STATE_MENU 0
+#define STATE_PARENT 1
+#define STATE_CHILD 2
+
+#define TRANSACTIONS_PER_PAGE 5
+/datum/computer_file/program/finances
+ filename = "financeman"
+ filedesc = "Financial management program"
+ nanomodule_path = /datum/nano_module/program/finances
+ program_icon_state = "id"
+ program_key_state = "id_key"
+ program_menu_icon = "key"
+ extended_desc = "Program for managing finances exchanged and stored on the network."
+ size = 8
+ category = PROG_FINANCE
+
+ var/static/list/parent_mod_types = list(/datum/account_modification/modify_fractional_reserve)
+ var/static/list/child_mod_types = list( /datum/account_modification/modify_interest,
+ /datum/account_modification/modify_transaction,
+ /datum/account_modification/modify_withdrawal,
+ /datum/account_modification/suspend_limit)
+
+/datum/nano_module/program/finances
+ name = "Financial management program"
+ var/prog_state = STATE_MENU
+ var/datum/computer_file/data/account/selected_account
+
+ var/log_page = 0
+
+ // For parent account transfers.
+ var/transfer_amount
+ var/transfer_purpose
+ var/target_account
+ var/target_network
+
+/datum/nano_module/program/finances/ui_interact(mob/user, ui_key, datum/nanoui/ui, force_open, datum/nanoui/master_ui, datum/topic_state/state)
+ . = ..()
+
+ var/list/data = host.initial_data()
+ var/datum/computer_network/network = get_network()
+ var/datum/extension/network_device/bank/banking_mainframe
+ if(!network)
+ data["error"] = "Unable to connect to the network. Check network connectivity."
+ prog_state = STATE_ERROR
+ else
+ var/list/accesses = get_access(user)
+ banking_mainframe = network.banking_mainframe
+ if(!banking_mainframe || !banking_mainframe.has_access(accesses))
+ data["error"] = "A banking mainframe does not exist on the network, or you lack the required access."
+ prog_state = STATE_ERROR
+
+ if(prog_state == STATE_ERROR && !data["error"])
+ prog_state = STATE_MENU
+
+ switch(prog_state)
+ if(STATE_PARENT)
+ var/datum/money_account/parent/network/bank_account = banking_mainframe.get_parent_account()
+ if(!bank_account)
+ data["create_parent"] = TRUE
+ else
+ data["parent_name"] = bank_account.account_name
+ data["parent_balance"] = bank_account.format_value_by_currency(bank_account.money)
+ data["child_totals"] = bank_account.format_value_by_currency(bank_account.child_totals)
+ data["fractional_reserve"] = "[bank_account.fractional_reserve*100] %"
+ data["no_child_accounts"] = length(bank_account.children)
+
+ data["pending_parent_mods"] = list()
+ var/index = 0
+ for(var/datum/account_modification/pending_mod in bank_account.pending_modifications)
+ index++
+ data["pending_parent_mods"] += list(pending_mod.get_ui_data() + list("index" = index))
+
+ data["admin_accounts"] = banking_mainframe.admin_accounts
+
+ // Transactions from parent
+ data["trans_amount"] = transfer_amount
+ data["trans_purpose"] = transfer_purpose
+ data["trans_account"] = target_account
+ data["trans_network"] = target_network
+
+ // Parent transaction log
+ data["parent_transactions"] = list()
+ var/list/transaction_log = reverselist(bank_account.transaction_log)
+ var/log_length = length(transaction_log)
+ log_page = clamp(log_page, 0, CEILING(log_length/TRANSACTIONS_PER_PAGE))
+
+ data["next_page"] = log_length > (log_page + 1)*TRANSACTIONS_PER_PAGE
+ data["prev_page"] = log_page != 0
+ transaction_log = transaction_log.Copy(min(log_length, log_page*TRANSACTIONS_PER_PAGE + 1), min(log_length+1, (log_page + 1)*TRANSACTIONS_PER_PAGE + 1))
+ for(var/datum/transaction/logged in transaction_log)
+ data["parent_transactions"] += list(list(
+ "date" = logged.date,
+ "time" = logged.time,
+ "purpose" = logged.purpose,
+ "amount" = bank_account.format_value_by_currency(logged.amount),
+ "target" = logged.get_target_name(),
+ "source" = logged.get_source_name()
+ ))
+
+ if(STATE_CHILD)
+ if(!istype(selected_account))
+ var/list/accounts = network.get_accounts()
+ data["accounts"] = list()
+ for(var/datum/computer_file/data/account/A in accounts)
+ data["accounts"] += list(list(
+ "account" = A.login,
+ "fullname" = A.fullname,
+ "money" = A.money_account ? A.money_account.format_value_by_currency(A.money_account.get_balance()) : "No financial account"
+ ))
+ var/datum/money_account/parent/network/bank_account = banking_mainframe.get_parent_account()
+ if(!bank_account)
+ data["auto_accounts"] = FALSE
+ else
+ data["auto_accounts"] = banking_mainframe.auto_money_accounts
+ data["auto_interest_rate"] = banking_mainframe.auto_interest_rate
+ data["auto_withdrawal_limit"] = bank_account.format_value_by_currency(banking_mainframe.auto_withdrawal_limit)
+ data["auto_transaction_fee"] = bank_account.format_value_by_currency(banking_mainframe.auto_transaction_fee)
+
+ else if(selected_account.money_account)
+ var/datum/money_account/child/selected_m_account = selected_account.money_account
+ data["child_name"] = selected_m_account.account_name
+ data["child_balance"] = selected_m_account.format_value_by_currency(selected_m_account.get_balance())
+ data["interest_rate"] = selected_m_account.interest_rate
+ data["withdrawal_limit"] = selected_m_account.format_value_by_currency(selected_m_account.withdrawal_limit)
+ data["current_withdrawal"] = selected_m_account.format_value_by_currency(selected_m_account.current_withdrawal)
+ data["transaction_fee"] = selected_m_account.format_value_by_currency(selected_m_account.transaction_fee)
+
+ data["pending_child_mods"] = list()
+ var/index = 0
+ for(var/datum/account_modification/pending_mod in selected_m_account.pending_modifications)
+ index++
+ data["pending_child_mods"] += list(pending_mod.get_ui_data() + list("index" = index))
+
+ // Child Transactions
+ data["transactions"] = list()
+ var/list/transaction_log = reverselist(selected_m_account.transaction_log)
+ var/log_length = length(transaction_log)
+ log_page = clamp(log_page, 0, CEILING(log_length/TRANSACTIONS_PER_PAGE))
+
+ data["next_page"] = log_length > (log_page + 1)*TRANSACTIONS_PER_PAGE
+ data["prev_page"] = log_page != 0
+ transaction_log = transaction_log.Copy(min(log_length, log_page*TRANSACTIONS_PER_PAGE + 1), min(log_length + 1, (log_page + 1)*TRANSACTIONS_PER_PAGE + 1))
+ for(var/datum/transaction/logged in transaction_log)
+ data["child_transactions"] += list(list(
+ "date" = logged.date,
+ "time" = logged.time,
+ "purpose" = logged.purpose,
+ "amount" = selected_m_account.format_value_by_currency(logged.amount),
+ "target" = logged.get_target_name(),
+ "source" = logged.get_source_name()
+ ))
+ data["prog_state"] = prog_state
+ ui = SSnano.try_update_ui(user, src, ui_key, ui, data, force_open)
+ if (!ui)
+ ui = new(user, src, ui_key, "finance_management.tmpl", name, 600, 700, state = state)
+ ui.auto_update_layout = 1
+ ui.set_initial_data(data)
+ ui.open()
+
+/datum/computer_file/program/finances/Topic(href, href_list, state)
+ if(..())
+ return TOPIC_REFRESH
+
+ var/mob/user = usr
+
+ var/datum/computer_network/network = computer.get_network()
+ var/datum/nano_module/program/finances/module = NM
+ if(!network)
+ return TOPIC_REFRESH
+
+ var/list/accesses = module.get_access(user)
+ var/datum/extension/network_device/bank/banking_mainframe = network.banking_mainframe
+ if(!banking_mainframe || !banking_mainframe.has_access(accesses))
+ return TOPIC_REFRESH
+
+ var/datum/money_account/parent/network/bank_account = banking_mainframe.get_parent_account()
+
+ if(href_list["next_page"])
+ module.log_page += 1
+ return TOPIC_REFRESH
+ if(href_list["prev_page"])
+ module.log_page = max(module.log_page - 1, 0)
+ return TOPIC_REFRESH
+
+ switch(module.prog_state)
+ if(STATE_ERROR)
+ if(href_list["back"])
+ module.prog_state = STATE_MENU
+ return TOPIC_REFRESH
+
+ if(STATE_MENU)
+ if(href_list["parent_mode"])
+ module.log_page = 0
+ module.prog_state = STATE_PARENT
+ return TOPIC_REFRESH
+ if(href_list["child_mode"])
+ module.prog_state = STATE_CHILD
+ return TOPIC_REFRESH
+
+ if(STATE_PARENT)
+ if(href_list["back"])
+ module.prog_state = STATE_MENU
+ return TOPIC_REFRESH
+
+ if(href_list["create_parent_account"])
+ if(bank_account)
+ return TOPIC_REFRESH
+ var/name = input(user, "Enter the name for the parent financial account (this may be changed later):", "Account name") as text|null
+ name = sanitize(name)
+ if(!CanInteract(user,state))
+ return TOPIC_HANDLED
+
+ if(!name)
+ to_chat(user, SPAN_WARNING("Invalid name!"))
+ return TOPIC_HANDLED
+
+ var/fractional_reserve = input(user, "Enter the fractional reserve for the parent financial account (between 0 and 1):", "Fractional Reserve") as num|null
+ fractional_reserve = clamp(fractional_reserve, 0, 1)
+ if(!CanInteract(user, state))
+ return TOPIC_HANDLED
+
+ var/confirmation = alert(user, "You are about to create a parent financial account with name '[name]' and a fractional reserve of '[fractional_reserve]'. Are you sure?", "Finalize account", "No", "Yes")
+ if(confirmation != "Yes" || !CanInteract(user, state))
+ return TOPIC_HANDLED
+
+ var/success = banking_mainframe.create_parent_account(name, fractional_reserve)
+ if(!success)
+ to_chat(user, SPAN_WARNING("Unable to create parent financial account!"))
+ return TOPIC_REFRESH
+
+ if(!bank_account)
+ return TOPIC_REFRESH
+
+ if(href_list["withdrawal"])
+ var/obj/item/stock_parts/computer/money_printer/printer = computer.get_component(PART_MPRINTER)
+ if(!printer)
+ to_chat(user, SPAN_WARNING("There's no cryptographic printer installed!"))
+ return TOPIC_HANDLED
+
+ var/amount = input(user, "Input the amount of cash to withdrawal:", "Cash withdrawal", 0) as num|null
+ if(!amount || !CanInteract(user,state))
+ return TOPIC_HANDLED
+
+ if(printer.can_print(amount, banking_mainframe.get_currency()))
+ var/err = banking_mainframe.withdraw(amount, "Cash withdrawal", computer.get_nid())
+ if(err)
+ to_chat(user, SPAN_WARNING("Cash withdrawal failed: [err]."))
+ else
+ printer.print_money(amount, banking_mainframe.get_currency(), user)
+
+ return TOPIC_REFRESH
+ else
+ to_chat(user, "\The [printer] does not have enough stored plastic!")
+ return TOPIC_HANDLED
+
+ if(href_list["add_modification"])
+ var/list/options = list()
+ for(var/mod in parent_mod_types)
+ var/datum/account_modification/mod_type = mod
+ options[initial(mod_type.name)] = mod
+ var/chosen_mod = input(user, "Select the type of policy modification you would like to perform:", "Policy modification") as anything in options|null
+ if(!chosen_mod || !CanInteract(user, state))
+ return TOPIC_HANDLED
+
+ var/chosen_mod_type = options[chosen_mod]
+ if(banking_mainframe.has_mod_type(chosen_mod_type))
+ to_chat(user, "A modification of that type has already been queued!")
+ return TOPIC_HANDLED
+ var/datum/account_modification/new_mod = new chosen_mod_type(bank_account)
+ new_mod.prompt_creation(user)
+ var/withdrawal_warning = new_mod.suspends_withdrawal_limit ? " During this time, withdrawal limits will be suspended." : ""
+ var/confirm = alert(user, "You are about to create a [lowertext(new_mod.name)]. It will activate in [new_mod.get_readable_countdown()].[withdrawal_warning] Are you sure?", "Confirm", "No", "Yes")
+
+ if(confirm != "Yes" || !CanInteract(user, state))
+ qdel(new_mod)
+ return TOPIC_REFRESH
+ bank_account.pending_modifications += new_mod
+
+ var/datum/computer_file/data/email_message/notification = new()
+ notification.title = "Notification of financial policy modification"
+ notification.source = "financial-services@[network.network_id]"
+ notification.stored_data = "A change to your financial provider's policies has been initiated. [new_mod.get_notification()] Please contact your financial provider for more information."
+
+ network.email_child_accounts(notification)
+ banking_mainframe.email_admin_accounts(notification)
+
+ return TOPIC_REFRESH
+
+ if(href_list["activate_modification"])
+ var/index = text2num(href_list["activate_modification"])
+ var/list/pending_mods = banking_mainframe.get_pending_modifications()
+ if(length(pending_mods) < index)
+ return TOPIC_REFRESH
+
+ var/datum/account_modification/pending_mod = pending_mods[index]
+ if(!pending_mod.allow_early)
+ return TOPIC_HANDLED
+
+ var/confirm = alert(user, "Are you sure you would like to activate this policy modification early?", "Early activation", "No", "Yes")
+ if(confirm != "Yes" || !CanInteract(user, state))
+ return TOPIC_HANDLED
+
+ pending_mod.modify_account()
+ return TOPIC_REFRESH
+
+ if(href_list["cancel_modification"])
+ var/index = text2num(href_list["cancel_modification"])
+ var/list/pending_mods = banking_mainframe.get_pending_modifications()
+ if(length(pending_mods) < index)
+ return TOPIC_REFRESH
+ var/datum/account_modification/pending_mod = pending_mods[index]
+ var/confirm = alert(user, "Are you sure you would like to cancel this policy modification?", "Cancel modification", "No", "Yes")
+ if(confirm != "Yes" || !CanInteract(user, state))
+ return TOPIC_HANDLED
+
+ var/datum/computer_file/data/email_message/notification = new()
+ notification.title = "Cancellation of financial policy modification"
+ notification.source = "financial-services@[network.network_id]"
+ notification.stored_data = "Your financial provider has cancelled a pending change to financial policy: [lowertext(pending_mod.name)]. Please contact your financial provider for more information."
+
+ network.email_child_accounts(notification)
+ banking_mainframe.email_admin_accounts(notification)
+
+ qdel(pending_mod)
+ return TOPIC_REFRESH
+
+ if(href_list["transfer_account"])
+ var/account = input(user, "Enter the account login you wish to transfer funds to. Leave blank if you wish to transfer money to a network's parent account:") as text|null
+ if(!account)
+ return TOPIC_HANDLED
+
+ module.target_account = sanitize_for_account(account)
+ return TOPIC_REFRESH
+
+ if(href_list["transfer_network"])
+ var/target_network = input(user, "Enter the network you wish to transfer funds to. Leave blank if you wish to transfer money on the current network:") as text|null
+ if(!target_network)
+ return TOPIC_HANDLED
+
+ module.target_network = sanitize(target_network)
+ return TOPIC_REFRESH
+
+ if(href_list["transfer_amount"])
+ var/amount = input(user, "Enter the amount to transfer:") as num|null
+ if(!amount)
+ return TOPIC_HANDLED
+
+ module.transfer_amount = round(abs(amount))
+ return TOPIC_REFRESH
+
+ if(href_list["transfer_purpose"])
+ var/purpose = input(user, "Enter the purpose for this transaction:") as text|null
+ if(!purpose)
+ return
+ module.transfer_purpose = sanitize(purpose)
+ return TOPIC_REFRESH
+
+ if(href_list["perform_transfer"])
+ if(!module.transfer_amount)
+ to_chat(user, SPAN_WARNING("You must enter an amount to transfer!"))
+ return TOPIC_HANDLED
+
+ var/datum/money_account/target_account = network.get_financial_account(module.target_account, module.target_network ? module.target_network : network.network_id)
+ if(!istype(target_account))
+ to_chat(user, SPAN_WARNING("Unable to perform transaction. [target_account]."))
+ return TOPIC_HANDLED
+
+ var/err = bank_account.transfer(target_account, module.transfer_amount, module.transfer_purpose)
+ if(err)
+ to_chat(user, SPAN_WARNING("Funds transfer failed: [err]."))
+ return TOPIC_HANDLED
+ else
+ to_chat(user, SPAN_NOTICE("Funds transfer successful!"))
+ module.transfer_amount = 0
+ module.transfer_purpose = null
+ module.target_account = null
+ module.target_account = null
+ return TOPIC_REFRESH
+
+ if(href_list["add_admin"])
+ var/admin_login = input(user, "Enter the login of the admin you would like to add.") as text
+ if(!admin_login || !CanInteract(user, state))
+ return TOPIC_HANDLED
+
+ var/success = banking_mainframe.add_admin_account(admin_login)
+ if(success)
+ return TOPIC_REFRESH
+
+ to_chat(user, SPAN_WARNING("An account with that login could not be found."))
+ return TOPIC_HANDLED
+
+ if(href_list["remove_admin"])
+ banking_mainframe.remove_admin_account(href_list["remove_admin"])
+ return TOPIC_REFRESH
+
+ if(STATE_CHILD)
+ if(!bank_account)
+ return TOPIC_REFRESH
+ if(href_list["back"])
+ if(!module.selected_account)
+ module.prog_state = STATE_MENU
+ else
+ module.log_page = 0
+ module.selected_account = null
+ return TOPIC_REFRESH
+
+ if(module.selected_account)
+ var/datum/money_account/child/network/selected_child = module.selected_account.money_account
+ if(!selected_child)
+ module.selected_account = null
+ return TOPIC_REFRESH
+
+ if(href_list["add_modification"])
+ var/list/options = list()
+ for(var/mod in child_mod_types)
+ var/datum/account_modification/mod_type = mod
+ options[initial(mod_type.name)] = mod
+ var/chosen_mod = input(user, "Select the type of client modification you would like to perform:", "Client modification") as anything in options
+ if(!chosen_mod || !CanInteract(user, state))
+ return TOPIC_HANDLED
+
+ var/chosen_mod_type = options[chosen_mod]
+ if(selected_child.has_mod_type(chosen_mod_type))
+ to_chat(user, "A modification of that type has already been queued!")
+
+ var/datum/account_modification/new_mod = new chosen_mod_type(selected_child)
+ new_mod.prompt_creation(user)
+ var/withdrawal_warning = new_mod.suspends_withdrawal_limit ? " During this time, withdrawal limits will be suspended." : ""
+ var/confirm = alert(user, "You are about to create a [lowertext(new_mod.name)]. It will activate in [new_mod.get_readable_countdown()].[withdrawal_warning] Are you sure?", "Confirm", "No", "Yes")
+
+ if(confirm != "Yes" || !CanInteract(user, state))
+ qdel(new_mod)
+ return TOPIC_HANDLED
+ selected_child.pending_modifications += new_mod
+
+ var/datum/computer_file/data/email_message/notification = new()
+ notification.title = "Notification of financial modification"
+ notification.source = "financial-services@[network.network_id]"
+ notification.stored_data = "A change to your client account has been initiated. [new_mod.get_notification()] Please contact your financial provider for more information."
+
+ network.receive_email(module.selected_account, "financial-services@[network.network_id]", notification)
+ return TOPIC_REFRESH
+
+ if(href_list["cancel_modification"])
+ var/index = text2num(href_list["cancel_modification"])
+ var/list/pending_mods = selected_child.pending_modifications
+ if(length(pending_mods) < index)
+ return TOPIC_REFRESH
+
+ var/datum/account_modification/pending_mod = pending_mods[index]
+ if(!pending_mod.allow_cancel)
+ return TOPIC_HANDLED
+ var/confirm = alert(user, "Are you sure you would like to cancel this policy modification?", "Cancel modification", "No", "Yes")
+ if(confirm != "Yes" || !CanInteract(user, state))
+ return TOPIC_HANDLED
+
+ var/datum/computer_file/data/email_message/notification = new()
+ notification.title = "Cancellation of financial policy modification"
+ notification.source = "financial-services@[network.network_id]"
+ notification.stored_data = "Your financial provider has cancelled a pending change to your client account: [lowertext(pending_mod.name)]."
+
+ network.receive_email(module.selected_account, "financial-services@[network.network_id]", notification)
+ qdel(pending_mod)
+ return TOPIC_REFRESH
+
+ if(href_list["close_account"])
+ var/confirm = alert(user, "Are you SURE you want to close this account? The owner will be notified by e-mail, and their outstanding balance moved to an escrow account.", "Confirm account closure", "No", "Yes")
+ if(confirm != "Yes" || !CanInteract(user, state))
+ return TOPIC_HANDLED
+
+ var/datum/computer_file/data/email_message/notification = new()
+ notification.title = "Notification of account closure"
+ notification.source = "financial-services@[network.network_id]"
+ if(selected_child.money)
+ notification.stored_data = "Your financial provider has closed your financial account. An escrow account has been opened for you containing some or all of your outstanding balance of your account. \
+ Escrow accounts are accessible from any financial terminal using your prior account information, and the financial provider ID '[bank_account.owner_name]'."
+ else
+ notification.stored_data = "Your financial provider has closed your financial account. Please contact your financial provider for more information."
+
+ network.receive_email(module.selected_account, "financial-services@[network.network_id]", notification)
+
+ // Don't send the normal escrow panic e-mails.
+ selected_child.on_escrow(TRUE)
+ module.selected_account = null
+ qdel(selected_child)
+ to_chat(user, SPAN_NOTICE("Account successfully closed!"))
+ return TOPIC_REFRESH
+ else
+ if(href_list["toggle_auto_accounts"])
+ banking_mainframe.auto_money_accounts = !banking_mainframe.auto_money_accounts
+ return TOPIC_REFRESH
+
+ if(href_list["change_auto_withdrawal"])
+ var/n_withdrawal = input(user, "Enter the new preset withdrawal limit:", "Withdrawal limit") as num
+ if(!CanInteract(user, state))
+ return TOPIC_HANDLED
+ banking_mainframe.auto_withdrawal_limit = max(0, FLOOR(n_withdrawal))
+ return TOPIC_REFRESH
+
+ if(href_list["change_auto_interest"])
+ var/n_interest = input(user, "Enter the new preset interest rate (between -1 and 1):", "Interest Rate") as num
+ if(!CanInteract(user, state))
+ return TOPIC_HANDLED
+ banking_mainframe.auto_interest_rate = clamp(n_interest, -1, 1)
+ return TOPIC_REFRESH
+
+ if(href_list["change_auto_transaction"])
+ var/n_withdrawal = input(user, "Enter the new preset transaction fee:", "Transaction fee") as num
+ if(!CanInteract(user, state))
+ return TOPIC_HANDLED
+ banking_mainframe.auto_transaction_fee = max(0, FLOOR(n_withdrawal))
+ return TOPIC_REFRESH
+
+ if(href_list["select_account"])
+ var/datum/computer_file/data/account/selected = network.find_account_by_login(href_list["select_account"])
+ if(selected)
+ . = TOPIC_REFRESH
+ if(!selected.money_account)
+ var/confirm = alert(user, "This network account does not have an attached client account. Would you like to create one?", "Create client account", "No", "Yes")
+ if(confirm == "Yes" && CanInteract(user, state))
+
+ var/interest = input(user, "Enter the interest rate for this account (between -1 and 1):") as num
+ interest = clamp(interest, -1, 1)
+ if(!CanInteract(user, state))
+ return
+ var/withdrawal_limit = input(user, "Enter the withdrawal limit for this account:") as num
+ withdrawal_limit = max(0, FLOOR(withdrawal_limit))
+ if(!CanInteract(user, state))
+ return
+ var/transaction_fee = input(user, "Enter the transaction fee for this account:") as num
+ transaction_fee = max(0, FLOOR(transaction_fee))
+ if(!CanInteract(user, state))
+ return
+
+ if(!selected || selected.money_account || !bank_account)
+ return
+
+ var/confirm2 = alert(user, "Create a client account for '[selected.login]' with an interest rate of [interest], a withdrawal limit of [withdrawal_limit] and a transaction fee of [transaction_fee]?", "Create client account", "No", "Yes")
+ if(confirm2 != "Yes")
+ return
+
+ selected.money_account = new(null, bank_account, interest, withdrawal_limit, transaction_fee, selected)
+ else
+ return TOPIC_HANDLED
+ module.selected_account = selected
+
+/datum/computer_file/program/finances/process_cash(obj/item/cash/received, mob/user)
+ var/datum/computer_network/network = computer.get_network()
+ if(!network)
+ return
+ var/datum/nano_module/program/accounts/module = NM
+ if(module.prog_state != STATE_PARENT) // Can only deposit into the parent account.
+ return
+
+ var/list/accesses = module.get_access(user)
+ var/datum/extension/network_device/bank/banking_mainframe = network.banking_mainframe
+ if(!banking_mainframe || !banking_mainframe.has_access(accesses))
+ return // Shouldn't be able to access this UI regardless.
+
+ var/err = banking_mainframe.deposit(received.absolute_worth, "Cash deposit", computer.get_nid())
+ if(err)
+ to_chat(user, SPAN_WARNING("Cash deposit failed: [err]."))
+ else
+ SSnano.update_uis(module)
+ return received.absolute_worth
+
+#undef TRANSACTIONS_PER_PAGE
+
+#undef STATE_ERROR
+#undef STATE_MENU
+#undef STATE_PARENT
+#undef STATE_CHILD
\ No newline at end of file
diff --git a/code/modules/modular_computers/file_system/programs/generic/atm.dm b/code/modules/modular_computers/file_system/programs/generic/atm.dm
new file mode 100644
index 00000000000..0004966bc7c
--- /dev/null
+++ b/code/modules/modular_computers/file_system/programs/generic/atm.dm
@@ -0,0 +1,331 @@
+#define STATE_LOGIN 0
+#define STATE_MAIN 1
+#define STATE_TRANSFER 2
+#define STATE_LOG 3
+
+#define TRANSACTIONS_PER_PAGE 8
+/datum/computer_file/program/atm
+ filename = "atmprog"
+ filedesc = "ATM Client"
+ extended_desc = "This program may be used to manage finances attached to your network account."
+ program_icon_state = "generic"
+ program_key_state = "generic_key"
+ program_menu_icon = "mail-closed"
+ size = 3
+ available_on_network = 1
+ usage_flags = PROGRAM_ALL
+ category = PROG_FINANCE
+
+ nanomodule_path = /datum/nano_module/program/atm
+
+/datum/computer_file/program/atm/Topic(href, href_list, state)
+ if(..())
+ return TOPIC_REFRESH
+ var/mob/living/user = usr
+ var/datum/computer_file/data/account/current_account = computer.get_account()
+ var/datum/computer_network/network = computer.get_network()
+ var/datum/nano_module/program/atm/module = NM
+
+ if(href_list["access_escrow"])
+ var/provider_id = input(user, "Enter the network ID for the account which was escrowed.")
+ if(!provider_id || !CanInteract(user, state))
+ return TOPIC_HANDLED
+
+ var/escrow_login = input(user, "Enter the login for the account which was escrowed.")
+ if(!escrow_login || !CanInteract(user, state))
+ return TOPIC_HANDLED
+
+ var/escrow_pass = input(user, "Enter the PIN for the account which was escrowed. If a PIN was not set, enter the account password.")
+ if(!escrow_pass || !CanInteract(user, state))
+ return TOPIC_HANDLED
+
+ var/datum/money_account/escrow/escrowed_account = SSmoney_accounts.get_escrow(escrow_login, escrow_pass, provider_id)
+ if(!escrowed_account)
+ to_chat(user, SPAN_WARNING("An escrow account matching the input information could not be found."))
+ return TOPIC_HANDLED
+
+ var/confirm = alert(user, "An escrow account matching the input information was found. Would you like to withdraw the available balance and close the account?", "Close escrow account", "No", "Yes")
+ if(confirm == "Yes" && CanInteract(user, state))
+ var/obj/item/stock_parts/computer/money_printer/printer = computer.get_component(PART_MPRINTER)
+ if(!printer)
+ to_chat(user, SPAN_WARNING("There's no cryptographic printer installed!"))
+ return TOPIC_HANDLED
+
+ if(printer.can_print(escrowed_account.money, escrowed_account.currency))
+ // This shouldn't occur barring admin intervention, but just in case.
+ var/money_to_print = escrowed_account.money
+ var/err = escrowed_account.withdraw(escrowed_account.money, "Cash withdrawal", computer.get_nid())
+ if(err)
+ to_chat(user, SPAN_WARNING("Cash withdrawal failed: [err]."))
+ else
+ printer.print_money(money_to_print, escrowed_account.currency, user)
+ qdel(escrowed_account)
+ return TOPIC_REFRESH
+ else
+ to_chat(user, "\The [printer] does not have enough stored plastic!")
+ return TOPIC_HANDLED
+ return TOPIC_HANDLED
+
+ var/datum/money_account/child/network/current_money_account = current_account?.money_account
+ if(!current_money_account)
+ return TOPIC_REFRESH
+
+ if(href_list["change_mode"])
+ if(module.prog_mode == STATE_LOGIN)
+ return TOPIC_REFRESH
+
+ switch(href_list["change_mode"])
+ if("main")
+ module.prog_mode = STATE_MAIN
+ if("transfer")
+ module.prog_mode = STATE_TRANSFER
+ if("log")
+ module.prog_mode = STATE_LOG
+
+ return TOPIC_REFRESH
+
+ switch(module.prog_mode)
+ if(STATE_LOGIN)
+ if(href_list["pin_entry"])
+ var/pin = input(user, "Enter the PIN for this account:", "PIN entry") as text|null
+ pin = sanitize(pin)
+ if(!pin || !CanInteract(user, state))
+ return TOPIC_HANDLED
+ if(pin == current_money_account.remote_access_pin)
+ to_chat(user, SPAN_NOTICE("Login successful. Welcome [current_account.fullname]!"))
+ module.current_pin = pin
+ module.prog_mode = STATE_MAIN
+ return TOPIC_REFRESH
+
+ if(STATE_MAIN) // Withdrawals, activating modifications early, changing security level.
+ if(href_list["withdrawal"])
+ var/obj/item/stock_parts/computer/money_printer/printer = computer.get_component(PART_MPRINTER)
+ if(!printer)
+ to_chat(user, SPAN_WARNING("There's no cryptographic printer installed!"))
+ return TOPIC_HANDLED
+
+ var/amount = input(user, "Input the amount of cash to withdrawal:", "Cash withdrawal", 0) as num|null
+
+ if(!amount || !CanInteract(user,state))
+ return TOPIC_HANDLED
+
+ if(printer.can_print(amount, current_money_account.currency))
+ var/err = current_money_account.withdraw(amount, "Cash withdrawal", computer.get_nid())
+ if(err)
+ to_chat(user, SPAN_WARNING("Cash withdrawal failed: [err]."))
+ else
+ printer.print_money(amount, current_money_account.currency, user)
+
+ return TOPIC_REFRESH
+ else
+ to_chat(user, "\The [printer] does not have enough stored plastic!")
+ return TOPIC_HANDLED
+
+ if(href_list["activate_modification"])
+ var/index = text2num(href_list["activate_modification"])
+ var/list/pending_mods = current_money_account.pending_modifications
+ if(length(pending_mods) < index)
+ return TOPIC_REFRESH
+
+ var/datum/account_modification/pending_mod = pending_mods[index]
+ if(!pending_mod.allow_early)
+ return TOPIC_HANDLED
+
+ var/confirm = alert(user, "Are you sure you would like to activate this client modification early?", "Early activation", "No", "Yes")
+ if(confirm != "Yes" || !CanInteract(user, state))
+ return TOPIC_HANDLED
+
+ pending_mod.modify_account()
+ return TOPIC_REFRESH
+
+ if(href_list["change_sec_level"])
+ if(current_money_account.security_level == 0)
+ var/new_pin = input("Enter the PIN you'd like to add to the account:") as text|null
+ new_pin = sanitize(new_pin)
+ if(!new_pin)
+ return TOPIC_HANDLED
+ var/confirm = alert(user, "You are about to set the PIN for this account to '[new_pin]'. Are you sure?", "Add PIN", "No", "Yes")
+ if(confirm == "Yes" && CanInteract(user, state))
+ current_money_account.security_level = 1
+ current_money_account.remote_access_pin = new_pin
+ module.current_pin = new_pin
+ return TOPIC_REFRESH
+ else
+ current_money_account.security_level = 0
+ current_money_account.remote_access_pin = null
+ module.current_pin = null
+ return TOPIC_REFRESH
+
+ if(STATE_TRANSFER)
+ if(href_list["transfer_account"])
+ var/account = input(user, "Enter the account login you wish to transfer funds to. Leave blank if you wish to transfer money to a network's parent account:") as text|null
+ if(!account)
+ return TOPIC_HANDLED
+
+ module.target_account = sanitize_for_account(account)
+ return TOPIC_REFRESH
+ if(href_list["transfer_network"])
+ var/target_network = input(user, "Enter the network you wish to transfer funds to. Leave blank if you wish to transfer money on the current network:") as text|null
+ if(!target_network)
+ return TOPIC_HANDLED
+
+ module.target_network = sanitize(target_network)
+ return TOPIC_REFRESH
+
+ if(href_list["transfer_amount"])
+ var/amount = input(user, "Enter the amount to transfer:") as num|null
+ if(!amount)
+ return TOPIC_HANDLED
+
+ module.transfer_amount = round(abs(amount)) // No negative values.
+ return TOPIC_REFRESH
+
+ if(href_list["transfer_purpose"])
+ var/purpose = input(user, "Enter the purpose for this transaction:") as text|null
+ if(!purpose)
+ return
+ module.transfer_purpose = sanitize(purpose)
+ return TOPIC_REFRESH
+
+ if(href_list["perform_transfer"])
+ if(!module.transfer_amount)
+ to_chat(user, SPAN_WARNING("You must enter an amount to transfer!"))
+ return TOPIC_HANDLED
+
+ var/datum/money_account/target_account = network.get_financial_account(module.target_account, module.target_network ? module.target_network : network.network_id)
+ if(!istype(target_account))
+ to_chat(user, SPAN_WARNING("Unable to perform transaction. [target_account]"))
+ return TOPIC_HANDLED
+
+ var/err = current_money_account.transfer(target_account, module.transfer_amount, module.transfer_purpose)
+ if(err)
+ to_chat(user, SPAN_WARNING("Funds transfer failed: [err]."))
+ return TOPIC_HANDLED
+ else
+ to_chat(user, SPAN_NOTICE("Funds transfer successful!"))
+ module.transfer_amount = 0
+ module.transfer_purpose = null
+ module.target_account = null
+ module.target_account = null
+
+ module.prog_mode = STATE_MAIN
+ return TOPIC_REFRESH
+
+ if(STATE_LOG)
+ if(href_list["next_page"])
+ module.log_page += 1
+ return TOPIC_REFRESH
+ if(href_list["prev_page"])
+ module.log_page = max(module.log_page - 1, 0)
+ return TOPIC_REFRESH
+
+/datum/nano_module/program/atm
+ var/transfer_amount
+ var/transfer_purpose
+ var/target_account
+ var/target_network
+
+ var/prog_mode = STATE_LOGIN
+ var/current_pin
+
+ var/log_page = 0
+
+/datum/nano_module/program/atm/ui_interact(mob/user, ui_key = "main", var/datum/nanoui/ui = null, var/force_open = 1, var/datum/topic_state/state = global.default_topic_state)
+ var/list/data = host.initial_data()
+ var/datum/computer_file/data/account/current_account = program.computer.get_account()
+ var/datum/money_account/child/network/current_money_account = current_account?.money_account
+ if(!istype(current_account))
+ prog_mode = STATE_LOGIN
+ data["login_prompt"] = "No account logged in. Please login through your system to proceed."
+ else if(!current_money_account)
+ prog_mode = STATE_LOGIN
+ data["login_prompt"] = "No financial account is associated with this network account. Please contact your financial provider."
+ else if(current_money_account.security_level > 0 && current_pin != current_money_account.remote_access_pin)
+ current_pin = null
+ prog_mode = STATE_LOGIN
+ data["login_prompt"] = "Enter the remote access pin for this account:"
+ data["prompt_pin"] = TRUE
+ else
+ if(prog_mode == STATE_LOGIN)
+ prog_mode = STATE_MAIN
+
+ switch(prog_mode)
+ if(STATE_MAIN)
+ data["account_name"] = current_money_account.account_name
+ data["balance"] = current_money_account.format_value_by_currency(current_money_account.get_balance())
+ data["interest_rate"] = current_money_account.interest_rate
+ data["withdrawal_limit"] = current_money_account.format_value_by_currency(current_money_account.withdrawal_limit)
+ data["current_withdrawal"] = current_money_account.format_value_by_currency(current_money_account.current_withdrawal)
+ data["transaction_fee"] = current_money_account.format_value_by_currency(current_money_account.transaction_fee)
+
+ if(length(current_money_account.pending_modifications))
+ data["pending_mods"] = list()
+ var/index = 0
+ for(var/datum/account_modification/pending_mod in current_money_account.pending_modifications)
+ index++
+ data["pending_mods"] += list(pending_mod.get_ui_data() + list("index" = index))
+
+ data["pin_secured"] = current_money_account.security_level >= 1
+
+ if(STATE_TRANSFER)
+ data["trans_amount"] = transfer_amount
+ data["trans_purpose"] = transfer_purpose
+ data["trans_account"] = target_account
+ data["trans_network"] = target_network
+
+ if(STATE_LOG)
+ data["transactions"] = list()
+ var/list/transaction_log = reverselist(current_money_account.transaction_log)
+ var/log_length = length(transaction_log)
+ log_page = clamp(log_page, 0, CEILING(log_length/TRANSACTIONS_PER_PAGE))
+
+ data["next_page"] = log_length > (log_page + 1)*TRANSACTIONS_PER_PAGE
+ data["prev_page"] = log_page != 0
+ transaction_log = transaction_log.Copy(min(log_length, log_page*TRANSACTIONS_PER_PAGE + 1), min(log_length + 1, (log_page + 1)*TRANSACTIONS_PER_PAGE + 1))
+ for(var/datum/transaction/logged in transaction_log)
+ data["transactions"] += list(list(
+ "date" = logged.date,
+ "time" = logged.time,
+ "purpose" = logged.purpose,
+ "amount" = current_money_account.format_value_by_currency(logged.amount),
+ "target" = logged.get_target_name(),
+ "source" = logged.get_source_name()
+ ))
+
+ if(prog_mode == STATE_LOGIN && length(SSmoney_accounts.all_escrow_accounts))
+ data["escrow_providers"] = english_list(SSmoney_accounts.get_escrow_provider_ids())
+
+ data["prog_mode"] = prog_mode
+ ui = SSnano.try_update_ui(user, src, ui_key, ui, data, force_open)
+ if (!ui)
+ ui = new(user, src, ui_key, "atm_program.tmpl", "Automated Teller Utility", 600, 450, state = state)
+ if(host.update_layout())
+ ui.auto_update_layout = 1
+ ui.set_auto_update(1)
+ ui.set_initial_data(data)
+ ui.open()
+
+/datum/computer_file/program/atm/process_cash(obj/item/cash/received, mob/user)
+ var/datum/nano_module/program/atm/module = NM
+ if(module.prog_mode == STATE_LOGIN) // Can only deposit while logged in.
+ return
+
+ var/datum/computer_file/data/account/current_account = computer.get_account()
+ var/datum/money_account/child/network/current_money_account = current_account?.money_account
+
+ if(!current_money_account)
+ return
+
+ var/err = current_money_account.deposit(received.absolute_worth, "Cash deposit", computer.get_nid())
+ if(err)
+ to_chat(user, SPAN_WARNING("Cash deposit failed: [err]."))
+ else
+ SSnano.update_uis(module)
+ return received.absolute_worth
+
+#undef TRANSACTIONS_PER_PAGE
+
+#undef STATE_LOGIN
+#undef STATE_MAIN
+#undef STATE_TRANSFER
+#undef STATE_LOG
\ No newline at end of file
diff --git a/code/modules/modular_computers/file_system/programs/generic/email_client.dm b/code/modules/modular_computers/file_system/programs/generic/email_client.dm
index dd73361bea8..dc19be186ed 100644
--- a/code/modules/modular_computers/file_system/programs/generic/email_client.dm
+++ b/code/modules/modular_computers/file_system/programs/generic/email_client.dm
@@ -184,7 +184,7 @@
if(message_source)
data["folder"] = folder
var/list/all_messages = list()
- for(var/datum/computer_file/data/email_message/message in message_source)
+ for(var/datum/computer_file/data/email_message/message in reverselist(message_source))
all_messages.Add(list(list(
"title" = message.title,
"body" = digitalPencode2html(message.stored_data),
diff --git a/code/modules/modular_computers/file_system/programs/generic/folding.dm b/code/modules/modular_computers/file_system/programs/generic/folding.dm
index f6767f081dc..3025669b2fe 100644
--- a/code/modules/modular_computers/file_system/programs/generic/folding.dm
+++ b/code/modules/modular_computers/file_system/programs/generic/folding.dm
@@ -45,7 +45,7 @@
if(!I)
to_chat(usr, SPAN_WARNING("Unable to locate ID card for transaction."))
return TOPIC_HANDLED
- var/datum/money_account/account = get_account(I.associated_account_number)
+ var/datum/money_account/account = get_glob_account(I.associated_account_id)
var/earned = current_interval * (SCIENCE_MONEY_PER_MINUTE * computer.get_processing_power())
account.deposit(earned, "Completed FOLDING@SPACE project.")
to_chat(usr, SPAN_NOTICE("Transferred [earned] to your account."))
diff --git a/code/modules/modular_computers/file_system/programs/generic/ntdownloader.dm b/code/modules/modular_computers/file_system/programs/generic/ntdownloader.dm
index 54aadb3a8b0..5c886e87632 100644
--- a/code/modules/modular_computers/file_system/programs/generic/ntdownloader.dm
+++ b/code/modules/modular_computers/file_system/programs/generic/ntdownloader.dm
@@ -151,7 +151,7 @@
// Only those programs our user can run will show in the list
if(!P.can_run(get_access(user), user, FALSE) && P.requires_access_to_download)
continue
- if(!P.is_supported_by_hardware(program.computer.get_hardware_flag(), user, TRUE))
+ if(!P.is_supported_by_hardware(program.computer.get_hardware_flag(), user, FALSE))
continue
category_list.Add(list(list(
"filename" = P.filename,
diff --git a/code/modules/modular_computers/networking/_network.dm b/code/modules/modular_computers/networking/_network.dm
index 50f2649419e..bc56b357eb2 100644
--- a/code/modules/modular_computers/networking/_network.dm
+++ b/code/modules/modular_computers/networking/_network.dm
@@ -20,6 +20,10 @@
var/datum/extension/network_device/modem/modem
var/datum/extension/network_device/acl/access_controller
+ var/datum/extension/network_device/bank/banking_mainframe
+ var/datum/money_account/parent/network/parent_account
+ var/list/datum/extension/network_device/money_cube/money_cubes = list()
+
var/network_features_enabled = NET_ALL_FEATURES
var/intrusion_detection_enabled
var/intrusion_detection_alarm
@@ -34,6 +38,17 @@
SSnetworking.networks[network_id] = src
/datum/computer_network/Destroy()
+ // Backup the parent account on the bank mainframe, if it exists
+ if(parent_account)
+ money_to_storage()
+
+ if(banking_mainframe)
+ banking_mainframe.backup = parent_account
+ parent_account = null
+ else // Too bad! Trigger an escrow panic and delete the parent account.
+ trigger_escrow_panic()
+ QDEL_NULL(parent_account)
+
for(var/datum/extension/network_device/D in devices)
D.disconnect(TRUE)
QDEL_NULL_LIST(chat_channels)
@@ -49,7 +64,7 @@
return FALSE
if(D in devices)
return TRUE
-
+
if(!check_connection(D))
return FALSE
@@ -77,7 +92,31 @@
else if(istype(D, /datum/extension/network_device/camera))
var/datum/extension/network_device/camera/C = D
add_camera_to_channels(C, C.channels)
-
+ else if(istype(D, /datum/extension/network_device/bank))
+ if(banking_mainframe)
+ return FALSE
+ var/datum/extension/network_device/bank/B = D
+ if(B.backup)
+ if(!parent_account)
+ parent_account = B.backup
+ parent_account.owner_name = network_id
+ add_log("Recovered financial account from backup", newtag)
+ else if(parent_account != B.backup)
+ return FALSE
+
+ B.backup = null
+
+ banking_mainframe = D
+ add_log("New banking mainframe set", newtag)
+ else if(istype(D, /datum/extension/network_device/money_cube))
+ if(!parent_account)
+ return FALSE
+ var/datum/extension/network_device/money_cube/cube = D
+ money_cubes |= cube
+ if(banking_mainframe)
+ parent_account.adjust_money(cube.stored_money)
+ cube.stored_money = 0
+
D.network_tag = newtag
devices |= D
devices_by_tag[D.network_tag] = D
@@ -95,6 +134,8 @@
else if(D in relays)
relays -= D
add_log("Relay OFFLINE", D.network_tag)
+ else if(D in money_cubes)
+ money_cubes -= D
else if(istype(D, /datum/extension/network_device/camera))
var/datum/extension/network_device/camera/C = D
remove_camera_from_channels(C, C.channels)
@@ -108,9 +149,15 @@
if(!router)
add_log("Router offline, network shutting down", D.network_tag)
qdel(src)
- if(D == access_controller)
+ else if(D == access_controller)
access_controller = null
add_log("Access controller offline. Network security offline.", D.network_tag)
+ else if(D == modem)
+ modem = null
+ add_log("Modem offline. PLEXUS access disabled.", D.network_tag)
+ else if(D == banking_mainframe)
+ banking_mainframe = null
+ add_log("Banking mainframe offline. Financial services disabled.")
return TRUE
/datum/computer_network/proc/get_unique_tag(nettag)
@@ -161,7 +208,7 @@
if(!broadcast_strength)
continue
- // For long ranged devices, checking to make sure there's at least a functional broadcaster somewhere.
+ // For long ranged devices, checking to make sure there's at least a functional broadcaster somewhere.
functional_broadcaster = TRUE
var/d_z = get_z(D.holder)
@@ -169,7 +216,7 @@
if(!ARE_Z_CONNECTED(d_z, b_z))
continue
-
+
if(d_z != b_z) // If the broadcaster is not in the same z-level as the device, the broadcast strength is halved.
broadcast_strength = round(broadcast_strength/2)
var/distance = get_dist(get_turf(B.holder), get_turf(D.holder))
@@ -222,7 +269,7 @@
var/datum/computer_network/target_network = SSnetworking.networks[target_id]
if(!target_network)
return
-
+
if(target_network == src)
if(network_features_enabled & feature)
return target_network
@@ -286,6 +333,15 @@
results += device.holder
return results
+/datum/computer_network/proc/get_device_extensions_by_type(type, list/accesses)
+ var/list/results = list()
+ var/bypass_auth = !accesses
+ for(var/datum/extension/network_device/device in devices)
+ if(istype(device, type))
+ if(bypass_auth || device.has_access(accesses))
+ results += device
+ return results
+
/datum/computer_network/proc/get_tags_by_type(var/type)
var/list/results = list()
for(var/tag in devices_by_tag)
diff --git a/code/modules/modular_computers/networking/accounts/_network_accounts.dm b/code/modules/modular_computers/networking/accounts/_network_accounts.dm
index 9ef74773dd8..41980d51d64 100644
--- a/code/modules/modular_computers/networking/accounts/_network_accounts.dm
+++ b/code/modules/modular_computers/networking/accounts/_network_accounts.dm
@@ -25,7 +25,7 @@
continue
if(E.backup)
result[E.login] = E
-
+
return result
/datum/computer_network/proc/add_account(datum/computer_file/data/account/acc, accesses)
@@ -55,7 +55,11 @@
if(add_account(EA, accesses))
if(user)
user.store_account_credentials(EA.login, EA.password, network_id)
- return TRUE
+ . = TRUE
+
+ if(banking_mainframe && parent_account && banking_mainframe.auto_money_accounts)
+ EA.money_account = new(null, parent_account, banking_mainframe.auto_interest_rate, banking_mainframe.auto_withdrawal_limit, banking_mainframe.auto_transaction_fee, EA)
+
return FALSE
/datum/computer_network/proc/rename_account(old_login, desired_login)
diff --git a/code/modules/modular_computers/networking/accounts/account.dm b/code/modules/modular_computers/networking/accounts/account.dm
index 91aa2dfbe47..0c6170ed2ea 100644
--- a/code/modules/modular_computers/networking/accounts/account.dm
+++ b/code/modules/modular_computers/networking/accounts/account.dm
@@ -24,6 +24,8 @@
var/notification_sound = "*beep*"
var/backup = FALSE // Backups are not returned when searching for accounts, but can be recovered using the accounts program.
+ var/datum/money_account/child/network/money_account
+
copy_string = "(Backup)"
/datum/computer_file/data/account/calculate_size()
@@ -54,6 +56,10 @@
groups.Cut()
parent_groups.Cut()
+ if(money_account)
+ money_account.on_escrow(TRUE) // Don't bother sending an e-mail since it's about to be deleted anyway.
+ QDEL_NULL(money_account)
+
QDEL_NULL_LIST(inbox)
QDEL_NULL_LIST(outbox)
QDEL_NULL_LIST(spam)
@@ -66,7 +72,15 @@
/datum/computer_file/data/account/proc/all_incoming_emails()
return (inbox | spam | deleted)
-/datum/computer_file/data/account/proc/receive_mail(var/datum/computer_file/data/email_message/received_message, var/datum/computer_network/network)
+/datum/computer_file/data/account/proc/receive_mail(datum/computer_file/data/email_message/received_message)
+ inbox.Add(received_message)
+
+ for(var/weakref/os_ref in logged_in_os)
+ var/datum/extension/interactive/os/os = os_ref.resolve()
+ if(istype(os))
+ os.mail_received(received_message)
+ else
+ logged_in_os -= os_ref
/datum/computer_file/data/account/Clone(rename)
. = ..(TRUE) // We always rename the file since a copied account is always a backup.
diff --git a/code/modules/modular_computers/networking/accounts/id_card.dm b/code/modules/modular_computers/networking/accounts/id_card.dm
index ff02525b5a6..ea5f9eb8ab5 100644
--- a/code/modules/modular_computers/networking/accounts/id_card.dm
+++ b/code/modules/modular_computers/networking/accounts/id_card.dm
@@ -1,5 +1,4 @@
/obj/item/card/id/network
- var/network_id // The network_id that this card is paired to.
var/weakref/current_account
color = COLOR_GRAY80
detail_color = COLOR_SKY_BLUE
@@ -22,11 +21,11 @@
for(var/group in access_account.parent_groups) // Membership in a child group grants access to anything with an access requirement set to the parent group.
. += "[group].[location]"
-/obj/item/card/id/network/proc/resolve_account()
+/obj/item/card/id/network/proc/resolve_account(net_feature = NET_FEATURE_ACCESS)
if(!current_account)
return
var/datum/extension/network_device/D = get_extension(src, /datum/extension/network_device)
- var/datum/computer_network/network = D.get_network(NET_FEATURE_ACCESS)
+ var/datum/computer_network/network = D.get_network(net_feature)
var/login = associated_network_account["login"]
var/password = associated_network_account["password"]
@@ -86,7 +85,7 @@
current_account = null
return TOPIC_REFRESH
-
+
if(href_list["login_account"])
if(login_account())
to_chat(usr, SPAN_NOTICE("Account successfully logged in."))
@@ -112,6 +111,11 @@
current_account = weakref(check_account)
return TRUE
+/obj/item/card/id/network/proc/get_network_id()
+ var/datum/extension/network_device/D = get_extension(src, /datum/extension/network_device)
+ var/datum/computer_network/network = D?.get_network()
+ return network?.network_id
+
/obj/item/card/id/network/verb/adjust_settings()
set name = "Adjust Settings"
set category = "Object"
diff --git a/code/modules/modular_computers/networking/device_types/bank.dm b/code/modules/modular_computers/networking/device_types/bank.dm
new file mode 100644
index 00000000000..79cadea0239
--- /dev/null
+++ b/code/modules/modular_computers/networking/device_types/bank.dm
@@ -0,0 +1,227 @@
+// Interaction with the network's parent account should go through this device. Wraps many normal functions of accounts.
+/datum/extension/network_device/bank
+ var/datum/money_account/parent/backup // If the network goes down, the bank device can hold onto it until it's recovered.
+
+ var/list/admin_accounts = list() // Currently these accounts don't have any additional access, but are alerted when something goes wrong.
+
+ var/auto_money_accounts = FALSE // Whether creating a network account will auto-create a money account
+
+ // Preset values for automatically generated money accounts
+ var/auto_interest_rate
+ var/auto_withdrawal_limit
+ var/auto_transaction_fee
+
+/datum/extension/network_device/bank/Destroy()
+ backup?.escrow_panic()
+ qdel(backup)
+ . = ..()
+
+/datum/extension/network_device/bank/connect()
+ . = ..()
+ if(.)
+ // Setup for pre-mapped banking.
+ var/datum/computer_network/net = SSnetworking.networks[network_id]
+ if(!net)
+ return
+ var/obj/machinery/network/bank/bank_holder = holder
+ if(!istype(bank_holder))
+ return
+ if(!net.parent_account && bank_holder.preset_account_name)
+ create_parent_account(bank_holder.preset_account_name, bank_holder.preset_fractional_reserve)
+
+ if(bank_holder.auto_money_accounts)
+ auto_money_accounts = TRUE
+ auto_interest_rate = bank_holder.auto_interest_rate
+ auto_withdrawal_limit = bank_holder.auto_withdrawal_limit
+ auto_transaction_fee = bank_holder.auto_transaction_fee
+
+
+/datum/extension/network_device/bank/disconnect(net_down)
+ var/datum/computer_network/net = SSnetworking.networks[network_id]
+ if(!net || !net.parent_account) // At this point the network should still exist, something has gone wrong.
+ return ..()
+
+ // Send out a warning e-mail to admin accounts.
+ var/datum/computer_file/data/email_message/warning = new()
+
+ warning.title = "URGENT: Financial services have entered recovery mode"
+ warning.source = "financial_services@[network_id]"
+ warning.stored_data = "Due to unknown circumstances, the banking mainframe on the network has been disconnected. All non-physical\
+ assets have been frozen awaiting the reactivation of the banking mainframe."
+ email_admin_accounts(warning)
+
+ // Store the money within money cubes on the network.
+ var/num_cubes = length(net.money_cubes)
+ var/money_per_cube = FLOOR(net.parent_account.money / num_cubes)
+ for(var/i in 1 to net.money_cubes)
+ var/datum/extension/network_device/money_cube/cube = net.money_cubes[i]
+ cube.stored_money += money_per_cube
+
+ if(i == num_cubes)
+ cube.stored_money += net.parent_account.money % num_cubes
+
+ net.parent_account.money = 0
+
+ . = ..()
+
+/datum/extension/network_device/bank/proc/theft_alert(datum/extension/network_device/money_cube/cube, lost_funds)
+ var/datum/computer_file/data/email_message/theft_email = new()
+ var/datum/computer_network/network = get_network()
+ var/datum/money_account/parent/victim = get_parent_account()
+ if(!victim)
+ return
+
+ theft_email.title = "URGENT: Theft of finances detected!"
+ theft_email.source = "financial_services@[network_id]"
+ theft_email.stored_data = "A theft has been detected by our automated loss prevention systems, leading to a loss of [victim.format_value_by_currency(lost_funds)]. \
+ The compromised financial storage device had the ID tag '[cube.network_tag]'. Loss prevention measures have been activated, and will be triggered if further losses occur."
+
+ email_admin_accounts(theft_email)
+
+ if(network)
+ var/datum/computer_file/data/email_message/child_theft_email = new()
+ child_theft_email.title = "Notice of financial loss prevention"
+ child_theft_email.source = "financial_services@[network_id]"
+ child_theft_email.stored_data = "Automated loss prevention systems have been triggered on the network, and withdrawal limits on all accounts have been temporarily lifted. Please contact your financial provider for more information."
+
+ network.email_child_accounts(child_theft_email)
+
+ var/datum/account_modification/theft_prevention/mod = new(victim)
+ victim.pending_modifications += mod
+
+/datum/extension/network_device/bank/proc/get_parent_account()
+ var/datum/computer_network/network = get_network()
+
+ return network?.parent_account
+
+/datum/extension/network_device/bank/proc/create_parent_account(account_name, fractional_reserve)
+ var/datum/computer_network/network = get_network()
+
+ if(!network || network.parent_account)
+ return FALSE
+
+ network.parent_account = new(null, network.network_id)
+ network.parent_account.account_name = account_name
+ network.parent_account.account_id = account_name // Not used for actual access, but useful for display.
+ network.parent_account.fractional_reserve = fractional_reserve
+ return TRUE
+
+// For internal use only, does not check fractional reserve limits. Otherwise, use transactions.
+/datum/extension/network_device/bank/proc/adjust_money(amount)
+ var/datum/money_account/parent/parent_account = get_parent_account()
+ parent_account?.adjust_money(amount)
+
+/datum/extension/network_device/bank/proc/log_msg(msg, machine_id)
+ var/datum/money_account/parent/parent_account = get_parent_account()
+
+ if(!parent_account)
+ return "Unable to access account. Contact your financial provider"
+
+ return parent_account.log_msg(msg, machine_id)
+
+/datum/extension/network_device/bank/proc/deposit(amount, purpose, machine_id)
+ var/datum/money_account/parent/parent_account = get_parent_account()
+
+ if(!parent_account)
+ return "Unable to access account. Contact your financial provider"
+
+ return parent_account.deposit(amount, purpose, machine_id)
+
+/datum/extension/network_device/bank/proc/withdraw(amount, purpose, machine_id)
+ var/datum/money_account/parent/parent_account = get_parent_account()
+
+ if(!parent_account)
+ return "Unable to access account. Contact your financial provider"
+
+ return parent_account.withdraw(amount, purpose, machine_id)
+
+/datum/extension/network_device/bank/proc/transfer(to_account, amount, purpose)
+ var/datum/money_account/parent/parent_account = get_parent_account()
+
+ if(!parent_account)
+ return "Unable to access account. Contact your financial provider"
+
+ return parent_account.transfer(to_account, amount, purpose)
+
+/datum/extension/network_device/bank/proc/rename_account(new_name)
+ var/datum/money_account/parent/parent_account = get_parent_account()
+
+ if(parent_account)
+ parent_account.account_name = sanitize(new_name)
+
+// Getters follow
+/datum/extension/network_device/bank/proc/get_balance()
+ var/datum/money_account/parent/parent_account = get_parent_account()
+ return parent_account?.money
+
+/datum/extension/network_device/bank/proc/get_formatted_balance()
+ var/datum/money_account/parent/parent_account = get_parent_account()
+ if(!parent_account)
+ return
+
+ return parent_account.format_value_by_currency(parent_account.money)
+
+/datum/extension/network_device/bank/proc/get_currency()
+ var/datum/money_account/parent/parent_account = get_parent_account()
+ return parent_account?.currency
+
+/datum/extension/network_device/bank/proc/get_account_name()
+ var/datum/money_account/parent/parent_account = get_parent_account()
+
+ return parent_account?.account_name
+
+/datum/extension/network_device/bank/proc/get_child_accounts()
+ var/datum/money_account/parent/parent_account = get_parent_account()
+ return parent_account?.children
+
+/datum/extension/network_device/bank/proc/get_child_totals()
+ var/datum/money_account/parent/parent_account = get_parent_account()
+ return parent_account?.child_totals
+
+/datum/extension/network_device/bank/proc/get_formatted_child_totals()
+ var/datum/money_account/parent/parent_account = get_parent_account()
+ if(!parent_account)
+ return
+
+ return parent_account.format_value_by_currency(parent_account.child_totals)
+
+/datum/extension/network_device/bank/proc/get_fractional_reserve()
+ var/datum/money_account/parent/parent_account = get_parent_account()
+ return parent_account?.fractional_reserve
+
+/datum/extension/network_device/bank/proc/get_pending_modifications()
+ var/datum/money_account/parent/parent_account = get_parent_account()
+ return parent_account?.pending_modifications
+
+/datum/extension/network_device/bank/proc/get_transaction_log()
+ var/datum/money_account/parent/parent_account = get_parent_account()
+ return parent_account?.transaction_log
+
+/datum/extension/network_device/bank/proc/has_mod_type(type)
+ var/datum/money_account/parent/parent_account = get_parent_account()
+ return parent_account?.has_mod_type(type)
+
+/datum/extension/network_device/bank/proc/add_admin_account(var/account_login)
+ var/datum/computer_network/net = get_network()
+ if(!net)
+ return FALSE
+ var/datum/computer_file/data/account/admin = net.find_account_by_login(account_login)
+ if(admin)
+ admin_accounts |= account_login
+ return TRUE
+
+/datum/extension/network_device/bank/proc/remove_admin_account(account_login)
+ admin_accounts -= account_login
+
+/datum/extension/network_device/bank/proc/email_admin_accounts(datum/computer_file/data/email_message/to_send)
+ var/datum/computer_network/net = get_network()
+ if(!net)
+ return
+
+ for(var/admin_login in admin_accounts)
+ var/datum/computer_file/data/account/admin = net.find_account_by_login(admin_login)
+ if(!admin)
+ admin_accounts -= admin_login
+ continue
+
+ net.receive_email(admin, "financial-services@[net.network_id]", to_send, FALSE)
\ No newline at end of file
diff --git a/code/modules/modular_computers/networking/emails/_email.dm b/code/modules/modular_computers/networking/emails/_email.dm
index 87872ea6cae..9c611ee0e1c 100644
--- a/code/modules/modular_computers/networking/emails/_email.dm
+++ b/code/modules/modular_computers/networking/emails/_email.dm
@@ -21,21 +21,15 @@
add_log("EMAIL LOG: [sender.login]@[network_id] -> [recipient.login]@[recipient_network.network_id] title: [sent.title].")
return TRUE
-/datum/computer_network/proc/receive_email(datum/computer_file/data/account/recipient, sender_address, datum/computer_file/data/email_message/received)
- if(!(recipient in get_accounts_unsorted()))
+/datum/computer_network/proc/receive_email(datum/computer_file/data/account/recipient, sender_address, datum/computer_file/data/email_message/received, safety = TRUE)
+ if(safety && !(recipient in get_accounts_unsorted()))
return FALSE
-
+
var/datum/computer_file/data/email_message/received_copy = received.Clone()
received_copy.set_timestamp()
- recipient.inbox.Add(received_copy)
-
- for(var/weakref/os_ref in recipient.logged_in_os)
- var/datum/extension/interactive/os/os = os_ref.resolve()
- if(istype(os))
- os.mail_received(received_copy)
- else
- recipient.logged_in_os -= os_ref
-
+
+ recipient.receive_mail(received_copy)
+
if(recipient.broadcaster)
for(var/datum/computer_file/data/account/email_account in get_accounts_unsorted())
if(email_account.broadcaster)
diff --git a/code/modules/modular_computers/networking/finance/money_accounts.dm b/code/modules/modular_computers/networking/finance/money_accounts.dm
new file mode 100644
index 00000000000..d58d2aec9ae
--- /dev/null
+++ b/code/modules/modular_computers/networking/finance/money_accounts.dm
@@ -0,0 +1,107 @@
+// Money accounts specifically for use with networks. Child accounts are attached to user accounts.
+/datum/money_account/parent/network
+ var/datum/computer_file/data/email_message/bankrupt_email
+ var/datum/computer_file/data/email_message/escrow_email
+
+ var/get_network_error = FALSE // Network accounts need at least one money storage device and a banking mainframe to function.
+
+/datum/money_account/parent/network/New(account_type, network_id)
+ owner_name = network_id
+ if(owner_name)
+ generate_emails()
+ . = ..()
+
+/datum/money_account/parent/network/deposit(amount, purpose, machine_id)
+ var/err = get_network_error()
+ if(err)
+ return err
+ . = ..()
+
+/datum/money_account/parent/network/withdraw(amount, purpose, machine_id)
+ var/err = get_network_error()
+ if(err)
+ return err
+ . = ..()
+
+/datum/money_account/parent/network/transfer(to_account, amount, purpose)
+ var/err = get_network_error()
+ if(err)
+ return err
+ . = ..()
+
+/datum/money_account/parent/network/proc/get_network_error()
+ var/datum/computer_network/net = SSnetworking.networks[owner_name]
+ if(!net || !net.banking_mainframe)
+ return "The financial system is currently in recovery mode. Please contact your financial provider for further information"
+ if(!length(net.money_cubes))
+ return "Finances are currently frozen due to a lack of financial storage devices. Please contact your financial provider for further information"
+
+// Generates e-mails to send in the case escrow accounts are generated.
+/datum/money_account/parent/network/proc/generate_emails()
+ bankrupt_email = new()
+ bankrupt_email.title = "URGENT: Notification of financial insolvency"
+ bankrupt_email.source = "financial_services@[owner_name]"
+ bankrupt_email.stored_data = "Due to unforseen circumstances, an escrow panic was triggered for your financial provider. \
+ Unfortunately, due to acute insolvency, an escrow account for your outstanding balance was \
+ unable to be created. Please contact your financial provider or legal services for further information."
+ escrow_email = new()
+ escrow_email.title = "URGENT: Notification of financial issue"
+ escrow_email.source = "financial_services@[owner_name]"
+ escrow_email.stored_data = "Due to unforseen circumstances, an escrow panic was triggered for your financial provider. \
+ An escrow account has been opened for you containing some or all of your outstanding balance of your account. \
+ Escrow accounts are accessible from any financial terminal using your prior account information, \
+ and the financial provider ID '[owner_name]'. Please contact your financial provider for further information."
+/datum/money_account/child/network
+ var/weakref/network_account
+
+/datum/money_account/child/network/New(account_type, p_account, n_interest, n_withdrawal_limit, n_transaction_fee, datum/computer_file/data/account/attached_account)
+ if(attached_account)
+ account_name = "[attached_account.fullname]'s account"
+ account_id = attached_account.login
+ remote_access_pin = attached_account.password // Temporary 'pin', not actually referenced until security level is changed.
+ network_account = weakref(attached_account)
+ . = ..()
+
+/datum/money_account/child/network/Destroy(force)
+ var/datum/computer_file/data/account/attached_account = network_account.resolve()
+ if(attached_account)
+ attached_account.money_account = null
+ . = ..()
+
+/datum/money_account/child/network/on_escrow(ignore_email = FALSE)
+ if(!money || ignore_email) // We didn't have any money to escrow, so leave it be.
+ return ..()
+ . = ..() // The amount escrowed.
+ // On escrow, send e-mails explaining the situation to players. These are magic e-mails that don't check normal network requirements.
+ var/datum/money_account/parent/network/net_parent = parent_account
+ if(!istype(net_parent) || !net_parent.escrow_email || !net_parent.bankrupt_email)
+ return
+
+ var/datum/computer_file/data/account/attached_account = network_account.resolve()
+ if(!istype(attached_account))
+ return
+
+ var/datum/computer_file/data/email_message/to_send = . > 0 ? net_parent.escrow_email.Clone() : net_parent.bankrupt_email.Clone()
+ to_send.set_timestamp()
+ attached_account.receive_mail(to_send)
+
+/datum/money_account/child/network/deposit(amount, purpose, machine_id)
+ var/datum/money_account/parent/network/net_parent = parent_account
+ var/err = net_parent.get_network_error()
+ if(err)
+ return err
+ . = ..()
+
+/datum/money_account/child/network/withdraw(amount, purpose, machine_id)
+ var/datum/money_account/parent/network/net_parent = parent_account
+ var/err = net_parent.get_network_error()
+ if(err)
+ return err
+ . = ..()
+
+/datum/money_account/child/network/transfer(to_account, amount, purpose)
+ var/datum/money_account/parent/network/net_parent = parent_account
+ var/err = net_parent.get_network_error()
+ if(err)
+ return err
+ . = ..()
diff --git a/code/modules/modular_computers/networking/finance/money_cube.dm b/code/modules/modular_computers/networking/finance/money_cube.dm
new file mode 100644
index 00000000000..019fb4ae444
--- /dev/null
+++ b/code/modules/modular_computers/networking/finance/money_cube.dm
@@ -0,0 +1,154 @@
+/obj/item/money_cube
+ name = "cryptographic currency storage device"
+ desc = "A digital currency storage device."
+
+ icon = 'icons/obj/items/money_cube.dmi'
+ icon_state = ICON_STATE_WORLD
+
+ w_class = ITEM_SIZE_LARGE // Not actually that large, but quite heavy.
+
+ origin_tech = "{'programming':2,'magnets':3,'materials':3}"
+
+ material = /decl/material/solid/metal/titanium
+ matter = list(
+ /decl/material/solid/metal/platinum = MATTER_AMOUNT_PRIMARY,
+ /decl/material/solid/metal/stainlesssteel = MATTER_AMOUNT_REINFORCEMENT,
+ /decl/material/solid/silicon = MATTER_AMOUNT_REINFORCEMENT
+ )
+
+ // These cubes can't manually connect to a network. Either set these or require players to connect one to a financial database
+ var/initial_network
+ var/initial_network_key
+
+/obj/item/money_cube/Initialize(ml, material_key)
+ . = ..()
+ set_extension(src, /datum/extension/network_device/money_cube, initial_network, initial_network_key, RECEIVER_STRONG_WIRELESS, initial_network ? TRUE : FALSE)
+
+/obj/item/money_cube/examine(mob/user, distance, infix, suffix)
+ . = ..()
+ var/datum/extension/network_device/money_cube/cube = get_extension(src, /datum/extension/network_device)
+ if(cube.check_connection())
+ to_chat(user, "It appears to be active.")
+ else
+ to_chat(user, "It must be activated by connection to a financial database prior to use.")
+
+/obj/item/money_cube/attackby(obj/item/W, mob/user)
+ . = ..()
+ if(IS_MULTITOOL(W))
+ if(user.skill_check(SKILL_FINANCE, SKILL_ADEPT) || user.skill_check(SKILL_DEVICES, SKILL_EXPERT))
+ var/datum/extension/network_device/money_cube/cube = get_extension(src, /datum/extension/network_device)
+ if(!cube || (!cube.check_connection() && !cube.stored_money))
+ to_chat(user, SPAN_WARNING("\The [src] doesn't respond to your atttempts to interface with it."))
+ return
+ user.visible_message("[usr] begins fiddling with \the [src]!")
+ if(do_after(user, 30 SECONDS, src)) // This takes quite awhile.
+ remove_cash(user)
+ else
+ to_chat(user, SPAN_WARNING("You have no idea how to interface with \the [src]!"))
+ return
+
+/obj/item/money_cube/proc/remove_cash(mob/user)
+ var/datum/extension/network_device/money_cube/cube = get_extension(src, /datum/extension/network_device)
+ if(!cube)
+ return
+
+ var/amount_to_dump = 0
+ var/currency_to_dump
+ var/datum/computer_network/network = cube.get_network()
+ if(!network)
+ amount_to_dump = cube.stored_money
+ cube.stored_money = 0
+ else
+ // Remove the money cube's imaginary share in the parent account's money.
+ if(network.banking_mainframe)
+ var/datum/extension/network_device/bank/parent_bank = network.banking_mainframe
+
+ var/num_cubes = length(network.money_cubes)
+ if(!num_cubes) // Cube is aware of the network but not vice versa. Something has gone wrong.
+ return
+
+ currency_to_dump = parent_bank.get_currency()
+ // At most, only half the money stored can be stolen.
+ amount_to_dump = round(parent_bank.get_balance() / max(num_cubes, 2)) + cube.stored_money
+ parent_bank.adjust_money(-amount_to_dump)
+ cube.stored_money = 0 // This should be 0 regardless, but just in case.
+
+ if(num_cubes == 1 || parent_bank.has_mod_type(/datum/account_modification/theft_prevention))
+ network.trigger_escrow_panic()
+ else
+ parent_bank.theft_alert(cube, amount_to_dump)
+
+ else // There's no active bank device, tampering will the cube will drop some money but trigger a total escrow panic.
+ amount_to_dump = FLOOR(cube.stored_money/2)
+ cube.stored_money -= amount_to_dump
+ network.trigger_escrow_panic()
+
+ cube.disconnect()
+ cube.network_id = null
+ cube.key = null
+ if(amount_to_dump)
+ var/obj/item/cash/cash = new(get_turf(src))
+ cash.set_currency(currency_to_dump ? currency_to_dump : global.using_map.default_currency)
+ cash.adjust_worth(amount_to_dump)
+ if(user)
+ // This should dispense a single holographic banknote or something eventually
+ user.visible_message("\The [src] emits a warning tone before rapidly dispensing cash from its internal printer!")
+ update_icon()
+
+/obj/item/money_cube/afterattack(atom/target, mob/user, proximity_flag, click_parameters)
+ . = ..()
+ if(istype(target, /obj/machinery/network/bank))
+ var/datum/extension/network_device/money_cube/cube = get_extension(src, /datum/extension/network_device)
+ var/datum/extension/network_device/bank/attacked_bank = get_extension(target, /datum/extension/network_device)
+ if(!attacked_bank || !attacked_bank.check_connection())
+ to_chat(user, SPAN_WARNING("\The [target] appears to be non-functional!"))
+ return
+
+ cube.set_new_id(attacked_bank.network_id)
+ cube.set_new_key(attacked_bank.key)
+
+ if(cube.check_connection())
+ playsound(user, 'sound/machines/chime.ogg', 30, 1)
+ to_chat(user, SPAN_NOTICE("\The [src] pings as it successfully links to \the [target]!"))
+ update_icon()
+ else
+ to_chat(user, SPAN_WARNING("\The [src] fails to connect to \the [target]."))
+
+/obj/item/money_cube/on_update_icon()
+ . = ..()
+ icon_state = get_world_inventory_state()
+ var/datum/extension/network_device/money_cube/cube = get_extension(src, /datum/extension/network_device)
+ var/active = cube?.check_connection()
+ if(active)
+ icon_state = "[icon_state]-active"
+
+/datum/extension/network_device/money_cube
+ var/stored_money // Money temporarily stored in the case that the parent account on the network vanishes.
+ internet_allowed = TRUE
+
+/datum/extension/network_device/money_cube/disconnect(net_down)
+ var/obj/H = holder
+ H.queue_icon_update()
+ if(!stored_money)
+ return ..()
+
+ var/datum/computer_network/net = SSnetworking.networks[network_id]
+ if(!net_down)
+ // Divide up the stored money among other cubes. If no other cubes exist, trigger a partial/full escrow panic.
+ if(!net)
+ return ..()
+ var/list/other_cubes = net.money_cubes - src
+ var/num_cubes = length(other_cubes)
+ if(num_cubes)
+ var/money_per_cube = FLOOR(stored_money / num_cubes)
+ for(var/i in 1 to num_cubes)
+ var/datum/extension/network_device/money_cube/other_cube = other_cubes[other_cubes.len]
+ if(i != num_cubes)
+ other_cube.stored_money += money_per_cube
+ stored_money -= money_per_cube
+ else
+ other_cube.stored_money += stored_money
+ stored_money = 0
+ else
+ net.trigger_escrow_panic()
+ . = ..()
\ No newline at end of file
diff --git a/code/modules/modular_computers/networking/finance/network_finance.dm b/code/modules/modular_computers/networking/finance/network_finance.dm
new file mode 100644
index 00000000000..6f5394ebdb3
--- /dev/null
+++ b/code/modules/modular_computers/networking/finance/network_finance.dm
@@ -0,0 +1,75 @@
+// Rather than keeping track of all the money stored in each money cube, we assume that money is distributed evenly among them,
+// and only call this when the banking mainframe or network goes down.
+/datum/computer_network/proc/money_to_storage(var/additional_money)
+ if(!parent_account)
+ return
+
+ var/total_money = parent_account.money + additional_money
+ if(!total_money)
+ return
+
+ var/num_cubes = length(money_cubes)
+
+ if(!num_cubes) // No money cubes on the network
+ return
+
+ var/money_per_cube = FLOOR(total_money / num_cubes)
+
+ for(var/i in 1 to num_cubes)
+ var/datum/extension/network_device/money_cube/cube = money_cubes[i]
+ cube.stored_money += money_per_cube
+ if(i == num_cubes)
+ cube.stored_money += total_money % num_cubes
+
+ parent_account.money = 0
+
+// Re-collect all money from money cubes and trigger an escrow panic.
+/datum/computer_network/proc/trigger_escrow_panic()
+ if(!parent_account)
+ return
+
+ var/cube_money = 0
+ for(var/datum/extension/network_device/money_cube/cube in money_cubes)
+ cube_money += cube.stored_money
+ cube.stored_money = 0
+
+ parent_account.money += cube_money
+ parent_account.escrow_panic()
+
+ if(banking_mainframe && length(banking_mainframe.admin_accounts))
+ var/datum/computer_file/data/email_message/escrow_email = new()
+ escrow_email.title = "URGENT: Escrow panic triggered"
+ escrow_email.source = "financial_services@[network_id]"
+ escrow_email.stored_data = "Due to the potential for extreme financial loss, an escrow panic has been automatically triggered for the parent financial account on the network.\
+ Escrow accounts have been opened for all client accounts on the network."
+
+ var/debt = parent_account.child_totals // Money still remains in children accounts.
+ if(debt)
+ escrow_email.stored_data += " Due to acute insolvency, a debt of [parent_account.format_value_by_currency(debt)] has been incurred for client accounts."
+
+ banking_mainframe.email_admin_accounts(escrow_email)
+
+/datum/computer_network/proc/email_child_accounts(datum/computer_file/data/email_message/to_send)
+ for(var/datum/computer_file/data/account/child in get_accounts_unsorted())
+ if(child.money_account)
+ receive_email(child, "financial-services@[network_id]", to_send, FALSE)
+
+// Returns money account datum on success. Returns error text on failure.
+/datum/computer_network/proc/get_financial_account(target_login, target_network_id)
+ . = "Unable to locate financial account over the network. Please try again later"
+ var/datum/computer_network/target_network = get_internet_connection(target_network_id, NET_FEATURE_FINANCE)
+ if(!target_network || !target_network.parent_account)
+ return
+
+ if(!target_login)
+ return target_network.parent_account
+
+ var/datum/computer_file/data/account/target_account = target_network.find_account_by_login(target_login)
+
+ if(!target_account)
+ return
+
+ if(!target_account.money_account)
+ return
+
+ return target_account.money_account
\ No newline at end of file
diff --git a/code/modules/modular_computers/networking/finance/network_pos.dm b/code/modules/modular_computers/networking/finance/network_pos.dm
new file mode 100644
index 00000000000..ac9c2ed0a3a
--- /dev/null
+++ b/code/modules/modular_computers/networking/finance/network_pos.dm
@@ -0,0 +1,215 @@
+// Variation of the EFTPOS for network use
+// TODO: Could possibly be turned into a modular program or similar.
+/obj/item/network_pos
+ name = "network point of sale system"
+ desc = "A network enabled point of sale system, used to perform quick transactions."
+ icon = 'icons/obj/items/device/eftpos.dmi'
+ icon_state = "eftpos"
+ material = /decl/material/solid/plastic
+ matter = list(/decl/material/solid/silicon = MATTER_AMOUNT_REINFORCEMENT, /decl/material/solid/metal/copper = MATTER_AMOUNT_REINFORCEMENT)
+
+ var/account_id
+ var/account_provider // Target network ID.
+
+ var/transaction_amount
+ var/transaction_purpose
+ var/transaction_authorized
+
+/obj/item/network_pos/Initialize(ml, material_key)
+ . = ..()
+ set_extension(src, /datum/extension/network_device)
+
+/obj/item/network_pos/attack_self(var/mob/user)
+ ui_interact(user)
+ return TRUE
+
+/obj/item/network_pos/attackby(obj/item/W, mob/user)
+ . = ..()
+ if(istype(W, /obj/item/card/id/network))
+ if(!transaction_amount)
+ to_chat(user, SPAN_NOTICE("No transaction in progress!"))
+ return
+ var/obj/item/card/id/network/payment_card = W
+ var/payment_id = payment_card.associated_network_account["login"]
+ var/payment_network = payment_card.get_network_id()
+
+ if(!payment_id)
+ to_chat(user, SPAN_WARNING("\The [src] flashes an error: no user account detected!"))
+ return
+ var/datum/extension/network_device/device = get_extension(src, /datum/extension/network_device)
+ var/datum/computer_network/net = device?.get_network()
+ if(!net)
+ to_chat(user, SPAN_WARNING("\The [src] flashes a network connection error."))
+ return
+ if(!account_id)
+ to_chat(user, SPAN_WARNING("\The [src] flashes an error: no account connected!"))
+ return
+ if(!transaction_authorized)
+ to_chat(user, SPAN_WARNING("\The [src] flashes an error: transaction is not authorized!"))
+ return
+ var/datum/money_account/target_account = get_money_account()
+ if(!istype(target_account))
+ to_chat(user, SPAN_WARNING("\The [src] flashes an error: unable to find connected account!"))
+ return
+ var/datum/money_account/payment_account = net.get_financial_account(payment_id, payment_network)
+ if(!istype(payment_account))
+ to_chat(user, SPAN_WARNING("\The [src] flashes an error: unable to find user account!"))
+ return
+ if(payment_account.currency != target_account.currency)
+ to_chat(user, SPAN_WARNING("\The [src] flashes an error: accounts do not hold the same currency."))
+ return
+
+ // Hold this here so no one can pull a fast one by rapidly changing the amount.
+ var/old_transaction = transaction_amount
+ var/confirm = alert(user, "Confirm transaction: [payment_account.format_value_by_currency(old_transaction)]", "Confirm Transaction", "Cancel", "Confirm")
+ if(confirm != "Confirm" || !CanInteract(usr, DefaultTopicState()))
+ return
+
+ if(payment_account.security_level > 0)
+ var/pin = input("Enter the PIN for this account:", "PIN Entry") as text
+ pin = sanitize(pin)
+ if(pin != payment_account.remote_access_pin)
+ to_chat(user, SPAN_WARNING("\The [src] flashes an error: incorrect PIN!"))
+ return
+
+ var/err = payment_account.transfer(target_account, old_transaction, transaction_purpose)
+ if(!err)
+ visible_message(SPAN_NOTICE("[html_icon(src)] \The [src] pings: transaction successful!"))
+ playsound(src, 'sound/machines/chime.ogg', 50, 1)
+
+ // Reset transaction info.
+ transaction_amount = null
+ transaction_purpose = null
+ transaction_authorized = FALSE
+ else
+ to_chat(user, SPAN_WARNING("\The [src] flashes an error: [err]"))
+ return
+
+ if(IS_PEN(W) || IS_MULTITOOL(W))
+ visible_message("\The [user] begins resetting \the [src] with \the [W].")
+ if(do_after(user, 5 SECONDS, src))
+ to_chat(user, SPAN_NOTICE("You reset \the [src]!"))
+ account_id = null
+ account_provider = null
+ transaction_authorized = FALSE
+
+/obj/item/network_pos/ui_interact(mob/user, ui_key = "main",var/datum/nanoui/ui = null)
+ var/data[0]
+ var/datum/extension/network_device/device = get_extension(src, /datum/extension/network_device)
+ if(!device)
+ data["error"] = "Error in device initialization: contact administrator."
+ else
+ var/datum/computer_network/net = device?.get_network()
+ if(!net)
+ data["error"] = "Unable to connect to network. Please check your network settings."
+ else
+ var/datum/money_account/target_account = get_money_account()
+
+ data["authorized"] = transaction_authorized
+ data["transaction_amount"] = transaction_amount ? transaction_amount : ""
+ data["transaction_purpose"] = transaction_purpose ? transaction_purpose : ""
+
+ if(target_account)
+ data["account_id"] = account_id
+ data["account_provider"] = account_provider ? account_provider : net.network_id
+
+ ui = SSnano.try_update_ui(user, src, ui_key, ui, data)
+ if (!ui)
+ ui = new(user, src, ui_key, "network_pos.tmpl", "Network POS Settings", 540, 326)
+ ui.set_initial_data(data)
+ ui.open()
+
+/obj/item/network_pos/OnTopic(mob/user, href_list, datum/topic_state/state)
+ var/datum/extension/network_device/device = get_extension(src, /datum/extension/network_device)
+ if(href_list["network_settings"])
+ device.ui_interact(user)
+ return TOPIC_HANDLED
+
+ var/datum/computer_network/net = device?.get_network()
+ if(!net)
+ return TOPIC_REFRESH
+
+ if(href_list["connect_account"])
+ if(!account_id)
+ var/new_id = input(user, "Enter the ID of the account you would like to connect to \the [src]:", "ID Entry") as text
+ new_id = sanitize(new_id)
+
+ var/new_provider = input(user, "Enter the ID of the network the account is located on. Leave blank to use device network:", "Network Entry") as text
+ var/datum/money_account/target_account = net.get_financial_account(new_id, new_provider ? new_provider : net.network_id)
+ if(!istype(target_account))
+ to_chat(user, SPAN_WARNING("An error occured: [target_account]"))
+ return TOPIC_REFRESH
+
+ if(target_account.security_level < 1)
+ to_chat(user, SPAN_WARNING("For security reasons, all accounts connected to \the [src] must have a PIN."))
+ return
+ account_id = new_id
+ account_provider = new_provider
+ return TOPIC_REFRESH
+ else
+ to_chat(user, SPAN_WARNING("You must reset \the [src] first!"))
+ return TOPIC_REFRESH
+
+ if(href_list["authorize_transaction"])
+ if(transaction_authorized)
+ transaction_authorized = FALSE
+ return TOPIC_REFRESH
+
+ var/datum/money_account/target_account = get_money_account()
+ if(!target_account)
+ return TOPIC_REFRESH
+
+ if(target_account.security_level < 1)
+ to_chat(user, SPAN_WARNING("For security reasons, all accounts connected to \the [src] must have a PIN."))
+ account_id = null
+ account_provider = null
+ return TOPIC_REFRESH
+
+ var/pin_entry = input(user, "Please enter the PIN for the connected account [account_id]:", "PIN Entry") as text
+ pin_entry = sanitize(pin_entry)
+ if(!pin_entry || !CanInteract(usr, DefaultTopicState()))
+ return TOPIC_HANDLED
+
+ if(pin_entry != target_account.remote_access_pin)
+ to_chat(user, SPAN_WARNING("Incorrect PIN! Please try again."))
+ return TOPIC_HANDLED
+
+ transaction_authorized = TRUE
+ playsound(src, 'sound/machines/chime.ogg', 50, 1)
+ return TOPIC_REFRESH
+
+ if(href_list["transaction_amount"])
+ var/amount = input(user, "Enter the amount to transfer:") as num|null
+
+ transaction_amount = round(amount)
+ transaction_authorized = FALSE
+ return TOPIC_REFRESH
+
+ if(href_list["transaction_purpose"])
+ var/purpose = input(user, "Enter the purpose for this transaction:") as text|null
+ if(!purpose)
+ return
+ transaction_purpose = sanitize(purpose)
+ return TOPIC_REFRESH
+
+ if(href_list["settings"])
+ var/datum/extension/network_device/D = get_extension(src, /datum/extension/network_device)
+ D.ui_interact(user)
+ return TOPIC_HANDLED
+
+/obj/item/network_pos/proc/get_money_account()
+ if(!account_id)
+ return
+ var/datum/extension/network_device/device = get_extension(src, /datum/extension/network_device)
+ var/datum/computer_network/net = device?.get_network()
+ if(!net) // If the net goes down, don't reset the authorization.
+ return
+ var/datum/money_account/target_account = net.get_financial_account(account_id, account_provider ? account_provider : net.network_id)
+
+ // Reset the system.
+ if(!istype(target_account))
+ account_id = null
+ account_provider = null
+ return
+
+ return target_account
\ No newline at end of file
diff --git a/code/modules/modular_computers/networking/machinery/bank.dm b/code/modules/modular_computers/networking/machinery/bank.dm
new file mode 100644
index 00000000000..11f09e36034
--- /dev/null
+++ b/code/modules/modular_computers/networking/machinery/bank.dm
@@ -0,0 +1,38 @@
+/obj/machinery/network/bank
+ name = "banking mainframe"
+ desc = "A mainframe used for managing network finances. It must be interfaced with remotely."
+ icon = 'icons/obj/machines/tcomms/hub.dmi'
+ icon_state = "hub"
+ network_device_type = /datum/extension/network_device/bank
+ main_template = "banking_mainframe.tmpl"
+ construct_state = /decl/machine_construction/default/panel_closed
+ uncreated_component_parts = null
+ base_type = /obj/machinery/network/bank
+
+ var/preset_account_name
+ var/preset_fractional_reserve
+
+ var/auto_money_accounts = FALSE
+ var/auto_interest_rate
+ var/auto_withdrawal_limit
+ var/auto_transaction_fee
+
+/obj/machinery/network/bank/ui_data(mob/user, ui_key)
+ . = ..()
+
+ var/datum/extension/network_device/bank = get_extension(src, /datum/extension/network_device)
+ if(!bank)
+ return
+
+ var/datum/computer_network/network = bank.get_network()
+ if(!network)
+ return
+
+ .["money_cubes"] = list()
+ for(var/datum/extension/network_device/money_cube/cube in network.money_cubes)
+ var/z = get_z(cube.holder)
+ var/obj/effect/overmap/visitable/cube_location = z ? global.overmap_sectors["[z]"] : null
+ .["money_cubes"] += list(list(
+ "tag" = cube.network_tag,
+ "location" = cube_location ? cube_location.name : "Unknown"
+ ))
\ No newline at end of file
diff --git a/code/modules/modular_computers/networking/machinery/modem.dm b/code/modules/modular_computers/networking/machinery/modem.dm
index deb1b71e07f..d91bb1cbc83 100644
--- a/code/modules/modular_computers/networking/machinery/modem.dm
+++ b/code/modules/modular_computers/networking/machinery/modem.dm
@@ -15,7 +15,8 @@
"Communication Systems" = NET_FEATURE_COMMUNICATION,
"Access systems" = NET_FEATURE_ACCESS,
"Security systems" = NET_FEATURE_SECURITY,
- "Filesystem access" = NET_FEATURE_FILESYSTEM
+ "Filesystem access" = NET_FEATURE_FILESYSTEM,
+ "Financial systems" = NET_FEATURE_FINANCE
)
/obj/machinery/network/modem/OnTopic(mob/user, href_list, datum/topic_state/state)
@@ -47,5 +48,5 @@
fdata["name"] = feature
fdata["enabled"] = M.allowed_features & feature_options[feature]
features.Add(list(fdata))
-
+
.["features"] = features
\ No newline at end of file
diff --git a/code/modules/modular_computers/os/_os.dm b/code/modules/modular_computers/os/_os.dm
index c4ad2b1c037..c1dc94e4fc8 100644
--- a/code/modules/modular_computers/os/_os.dm
+++ b/code/modules/modular_computers/os/_os.dm
@@ -17,7 +17,7 @@
var/default_icon = "generic" //Overlay icon for programs that have a screen overlay the host doesn't have.
// Used for deciding if various tray icons need to be updated
- var/last_battery_percent
+ var/last_battery_percent
var/last_world_time
var/list/last_header_icons
@@ -62,7 +62,7 @@
var/datum/computer_file/data/account/access_account = get_account()
if(access_account)
var/datum/computer_network/network = get_network()
- if(network)
+ if(network)
var/location = "[network.network_id]"
. += "[access_account.login]@[location]" // User access uses '@'
for(var/group in access_account.groups)
@@ -73,7 +73,7 @@
var/obj/item/card/id/I = user.GetIdCard()
if(I)
. += I.GetAccess(access_account?.login) // Ignore any access that's already on the user account.
-
+
// Returns the current account, if possible. User var is passed only for updating program access from ID, if no account is found.
/datum/extension/interactive/os/proc/get_account(var/mob/user)
if(!current_account)
@@ -137,7 +137,7 @@
var/new_password = sanitize(input(user, "Enter your account password:", "Account password", default_password) as text|null)
if(!new_password || !CanUseTopic(user, global.default_topic_state))
return
-
+
if(login_account(new_login, new_password, user))
to_chat(user, SPAN_NOTICE("Account login successful: Welcome [new_login]!"))
else
@@ -147,7 +147,7 @@
on = FALSE
for(var/datum/computer_file/program/P in running_programs)
kill_program(P, 1)
-
+
var/obj/item/stock_parts/computer/network_card/network_card = get_component(PART_NETWORK)
if(network_card)
var/datum/extension/network_device/D = get_extension(network_card, /datum/extension/network_device)
@@ -307,4 +307,15 @@
/datum/extension/interactive/os/proc/mail_received(datum/computer_file/data/email_message/received)
var/datum/computer_file/program/email_client/e_client = locate() in running_programs
if(e_client)
- e_client.mail_received(received)
\ No newline at end of file
+ e_client.mail_received(received)
+
+// Returns the amount of cash taken, if any.
+/datum/extension/interactive/os/proc/process_cash(obj/item/cash/received_cash, mob/user)
+ if(active_program)
+ return active_program.process_cash(received_cash, user)
+
+/datum/extension/interactive/os/proc/get_nid()
+ var/obj/item/stock_parts/computer/network_card/card = get_component(PART_NETWORK)
+ if(!card)
+ return
+ return card.get_nid()
\ No newline at end of file
diff --git a/code/modules/modular_computers/os/files.dm b/code/modules/modular_computers/os/files.dm
index 20c3dc26d23..131c2601ba9 100644
--- a/code/modules/modular_computers/os/files.dm
+++ b/code/modules/modular_computers/os/files.dm
@@ -548,7 +548,7 @@
left_to_transfer = max(0, left_to_transfer - get_transfer_speed())
if(!left_to_transfer)
if(copying)
- return transfer_to.store_file(transferring, directory_to, TRUE)
+ return transfer_to.store_file(transferring.Clone(), directory_to, TRUE)
else
. = transfer_from.delete_file(transferring) // Check if we can delete the file.
if(. == OS_FILE_SUCCESS)
diff --git a/config/example/game_options.txt b/config/example/game_options.txt
index f74b06332bc..ea383accca3 100644
--- a/config/example/game_options.txt
+++ b/config/example/game_options.txt
@@ -67,6 +67,19 @@ MONKEY_DELAY 0
ALIEN_DELAY 0
ANIMAL_DELAY 0
+### Economy ###
+
+## Values for these settings are given in days.
+
+WITHDRAW_PERIOD 1
+INTEREST_PERIOD 1
+
+## Delays for modifications to accounts.
+INTEREST_MOD_DELAY 2
+WITHDRAW_MOD_DELAY 3
+TRANSACTION_MOD_DELAY 2
+FRACTIONAL_RESERVE_MOD_DELAY 3
+ANTI_TAMPER_MOD_DELAY 2
### Miscellaneous ###
diff --git a/icons/obj/items/money_cube.dmi b/icons/obj/items/money_cube.dmi
new file mode 100644
index 00000000000..145490db0a8
Binary files /dev/null and b/icons/obj/items/money_cube.dmi differ
diff --git a/maps/~mapsystem/maps.dm b/maps/~mapsystem/maps.dm
index 5b79e29f1d6..eba9655b703 100644
--- a/maps/~mapsystem/maps.dm
+++ b/maps/~mapsystem/maps.dm
@@ -312,7 +312,7 @@ var/global/const/MAP_HAS_RANK = 2 //Rank system, also togglable
news_network.CreateFeedChannel("The Gibson Gazette", "Editor Mike Hammers", 1, 1)
if(!station_account)
- station_account = create_account("[station_name()] Primary Account", "[station_name()]", starting_money, ACCOUNT_TYPE_DEPARTMENT)
+ station_account = create_glob_account("[station_name()] Primary Account", "[station_name()]", starting_money, ACCOUNT_TYPE_DEPARTMENT)
for(var/job in allowed_jobs)
var/datum/job/J = SSjobs.get_by_path(job)
@@ -323,9 +323,9 @@ var/global/const/MAP_HAS_RANK = 2 //Rank system, also togglable
for(var/department in station_departments)
var/decl/department/dept = SSjobs.get_department_by_type(department)
if(istype(dept))
- department_accounts[department] = create_account("[dept.name] Account", "[dept.name]", department_money, ACCOUNT_TYPE_DEPARTMENT)
+ department_accounts[department] = create_glob_account("[dept.name] Account", "[dept.name]", department_money, ACCOUNT_TYPE_DEPARTMENT)
- department_accounts["Vendor"] = create_account("Vendor Account", "Vendor", 0, ACCOUNT_TYPE_DEPARTMENT)
+ department_accounts["Vendor"] = create_glob_account("Vendor Account", "Vendor", 0, ACCOUNT_TYPE_DEPARTMENT)
vendor_account = department_accounts["Vendor"]
/datum/map/proc/map_info(var/client/victim)
diff --git a/maps/~mapsystem/maps_currency.dm b/maps/~mapsystem/maps_currency.dm
index 5365ad66ddd..c8d82799763 100644
--- a/maps/~mapsystem/maps_currency.dm
+++ b/maps/~mapsystem/maps_currency.dm
@@ -17,7 +17,7 @@
credstick.creator = owner.real_name
credstick.currency = owner_account.currency
credstick.loaded_worth = min(credstick.max_worth, FLOOR(owner_account.money * transfer_mult))
- owner_account.money -= credstick.loaded_worth
+ owner_account.adjust_money(-credstick.loaded_worth)
return list(credstick)
/decl/starting_cash_choice/credstick/half
@@ -31,8 +31,8 @@
var/obj/item/cash/cash = new
cash.set_currency(owner_account.currency)
cash.adjust_worth(FLOOR(owner_account.money * transfer_mult))
- owner_account.money -= cash.absolute_worth
- return list(cash)
+ owner_account.adjust_money(cash.absolute_worth)
+ return list(cash)
/decl/starting_cash_choice/cash/half
name = "split between bank account and cash"
@@ -47,14 +47,14 @@
var/obj/item/cash/cash = new
cash.set_currency(owner_account.currency)
cash.adjust_worth(FLOOR(owner_account.money * transfer_mult))
- . += cash
+ . += cash
var/obj/item/charge_stick/credstick = new
credstick.creator = owner.real_name
credstick.currency = owner_account.currency
credstick.loaded_worth = min(credstick.max_worth, FLOOR(owner_account.money * transfer_mult))
. += credstick
- owner_account.money -= cash.absolute_worth
- owner_account.money -= credstick.loaded_worth
+ owner_account.adjust_money(-cash.absolute_worth)
+ owner_account.adjust_money(-credstick.loaded_worth)
/decl/starting_cash_choice/split/even
name = "split between bank account, cash and charge stick"
diff --git a/mods/persistence/modules/world_save/saved_vars/saved_misc.dm b/mods/persistence/modules/world_save/saved_vars/saved_misc.dm
index 8c23f7b4f8b..19c67c3abb6 100644
--- a/mods/persistence/modules/world_save/saved_vars/saved_misc.dm
+++ b/mods/persistence/modules/world_save/saved_vars/saved_misc.dm
@@ -150,7 +150,7 @@ SAVED_VAR(/datum/memory, _owner_ckey)
SAVED_VAR(/datum/money_account, account_name)
SAVED_VAR(/datum/money_account, owner_name)
-SAVED_VAR(/datum/money_account, account_number)
+SAVED_VAR(/datum/money_account, account_id)
SAVED_VAR(/datum/money_account, remote_access_pin)
SAVED_VAR(/datum/money_account, money)
SAVED_VAR(/datum/money_account, transaction_log)
@@ -745,7 +745,7 @@ SAVED_VAR(/obj/item/radio, listening)
///////////////////////////////////////////////////////////////////////////////
SAVED_VAR(/obj/item/card/id, access)
SAVED_VAR(/obj/item/card/id, registered_name)
-SAVED_VAR(/obj/item/card/id, associated_account_number)
+SAVED_VAR(/obj/item/card/id, associated_account_id)
SAVED_VAR(/obj/item/card/id, associated_network_account)
SAVED_VAR(/obj/item/card/id, age)
SAVED_VAR(/obj/item/card/id, blood_type)
diff --git a/nano/templates/accounts_terminal.tmpl b/nano/templates/accounts_terminal.tmpl
index ce3f41022b4..b7c3bf52ad8 100644
--- a/nano/templates/accounts_terminal.tmpl
+++ b/nano/templates/accounts_terminal.tmpl
@@ -62,7 +62,7 @@
Account Number:
- #{{:data.account_number}}
+ #{{:data.account_id}}
@@ -133,7 +133,7 @@
Payroll:
- {{:helper.link('Revoke', 'transferthick-e-w', {'choice' : 'revoke_payroll'}, data.account_number == data.station_account_number ? 'disabled' : null, 'linkDanger')}}
+ {{:helper.link('Revoke', 'transferthick-e-w', {'choice' : 'revoke_payroll'}, data.account_id == data.ME_number ? 'disabled' : null, 'linkDanger')}}
{{else}}
@@ -144,7 +144,7 @@
{{for data.accounts}}
- | {{:helper.link('#' + value.account_number, '', {'choice' : 'view_account_detail', 'account_index' : value.account_index})}} |
+ {{:helper.link('#' + value.account_id, '', {'choice' : 'view_account_detail', 'account_index' : value.account_index})}} |
{{:value.owner_name}} |
{{:value.suspended}} |
diff --git a/nano/templates/atm_program.tmpl b/nano/templates/atm_program.tmpl
new file mode 100644
index 00000000000..58424ea3093
--- /dev/null
+++ b/nano/templates/atm_program.tmpl
@@ -0,0 +1,140 @@
+{{if data.prog_mode == 0}}
+ Welcome to the automated teller utility:
+ {{:data.login_prompt}}
+ {{if data.prompt_pin}}
+ {{:helper.link('Enter PIN', 'key', {'pin_entry' : 1}, null)}}
+ {{/if}}
+ {{if data.escrow_providers}}
+
+ Escrow accounts are currently open for the following financial providers: {{:data.escrow_providers}}
+
+ {{:helper.link('Access Escrow Account', 'key', {'access_escrow' : 1}, null)}}
+ {{/if}}
+{{else}}
+
+ {{:helper.link('Account Info', null, {'change_mode' : "main"}, data.prog_mode == 1 ? 'selected' : null)}}
+ {{:helper.link('Transactions', null, {'change_mode' : "transfer"}, data.prog_mode == 2 ? 'selected' : null)}}
+ {{:helper.link('Logs', null, {'change_mode' : "log"}, data.prog_mode == 3 ? 'selected' : null)}}
+
+
+ {{if data.prog_mode == 1}}
+ {{:data.account_name}}
+ {{if data.pin_secured}}
+ Account is currently secured with a PIN.
+ {{:helper.link('Remove PIN', 'cancel', {'change_sec_level' : 1}, null)}}
+ {{else}}
+ Account is currently unsecured.
+ {{:helper.link('Add PIN', 'key', {'change_sec_level' : 1}, null)}}
+ {{/if}}
+
+
+
+ Current Balance:
+
+
+ {{:data.balance}}
+
+
+
+
+
+ Interest Rate:
+
+
+ {{:data.interest_rate}}
+
+
+
+
+
+ Withdrawal Limit:
+
+
+ {{:data.current_withdrawal}} / {{:data.withdrawal_limit}}
+
+
+
+
+
+ Transaction Fee:
+
+
+ {{:data.transaction_fee}}
+
+
+ {{:helper.link('Withdrawal', null, {'withdrawal' : 1}, null)}}
+ {{if data.pending_mods}}
+
+ Account modifications
+
+ | Description | Activation time | Early trigger | Cancel
+ {{for data.pending_mods}}
+ |
|---|
+ | {{:value.desc}} |
+ {{:value.countdown}} |
+
+ {{if value.allow_early}}
+ {{:helper.link('', 'alert', {'activate_modification' : value.index})}}
+ {{/if}}
+ |
+
+ {{if value.allow_cancel}}
+ {{:helper.link('', 'cancel', {'cancel_modification' : value.index})}}
+ {{/if}}
+ |
+ {{/for}}
+
+ {{/if}}
+ {{else data.prog_mode == 2}}
+ Transaction:
+
+
+ Transaction Amount:
+
+
+ {{:helper.link(data.trans_amount ? data.trans_amount : '', null, {'transfer_amount' : 1})}}
+
+
+
+
+ Target account:
+
+
+ {{:helper.link(data.trans_account ? data.trans_account : '', null, {'transfer_account' : 1})}}
+
+
+
+
+ Target network:
+
+
+ {{:helper.link(data.trans_network ? data.trans_network : '', null, {'transfer_network' : 1})}}
+
+
+
+
+ Transfer Purpose:
+
+
+ {{:data.trans_purpose ? data.trans_purpose : ''}}{{:helper.link('', 'pencil', {'transfer_purpose' : 1})}}
+
+
+
+ {{:helper.link('Perform transfer', null, {'perform_transfer' : 1})}}
+ {{else data.prog_mode == 3}}
+ Transaction Logs
+
+ | To | From | Amount | Purpose | Time
+ {{for data.transactions}}
+ |
|---|
+ | {{:value.target}} |
+ {{:value.source}} |
+ {{:value.amount}} |
+ {{:value.purpose}} |
+ {{:value.time}}, {{:value.date}} |
+ {{/for}}
+
+ {{:helper.link('<', null, {'prev_page' : 1}, data.prev_page ? null : 'disabled')}}
+ {{:helper.link('>', null, {'next_page' : 1}, data.next_page ? null : 'disabled')}}
+ {{/if}}
+{{/if}}
\ No newline at end of file
diff --git a/nano/templates/banking_mainframe.tmpl b/nano/templates/banking_mainframe.tmpl
new file mode 100644
index 00000000000..2891a7dee5d
--- /dev/null
+++ b/nano/templates/banking_mainframe.tmpl
@@ -0,0 +1,23 @@
+{{if data.error}}
+ An error has occured:
+ Additional information: {{:data.error}}
+ Please try again. If the problem persists contact your system administrator for assistance.
+
+ {{:helper.link('REFRESH', null, { "refresh" : 1 })}}
+ {{:helper.link("NETWORK SETTINGS", null, { "settings" : 1 })}}
+
+{{else}}
+ Financial management must be done remotely.
+
+ {{:helper.link("NETWORK SETTINGS", null, { "settings" : 1 })}}
+
+ Financial Storage Devices:
+
+ | Network Tag | Location
+ {{for data.money_cubes}}
+ |
|---|
+ | {{:value.tag}} |
+ {{:value.location}} |
+ {{/for}}
+
+{{/if}}
\ No newline at end of file
diff --git a/nano/templates/finance_management.tmpl b/nano/templates/finance_management.tmpl
new file mode 100644
index 00000000000..0332b4e2b30
--- /dev/null
+++ b/nano/templates/finance_management.tmpl
@@ -0,0 +1,260 @@
+{{if data.prog_state == -1}}
+
+
+ An error has occured:
+
+
+ {{:data.error}}
+
+
+ {{:helper.link('Go back', 'arrowthickstop-1-w', {'back' : 1}, null)}}
+{{else data.prog_state == 0}}
+ Welcome to the financial management utility:
+ {{:helper.link('Manage parent financial account', null, {'parent_mode' : 1}, null)}}
+ {{:helper.link('Manage client accounts', null, {'child_mode' : 1}, null)}}
+{{else data.prog_state == 1}}
+ {{if data.create_parent}}
+ No parent account currently exists on the network.
+ {{:helper.link('Create parent account', null, {'create_parent_account' : 1}, null)}}
+ {{else}}
+ Financial management: {{:data.parent_name}}
+
+
+ Current Balance:
+
+
+ {{:data.parent_balance}}
+
+
+
+
+ Client Balance Total:
+
+
+ {{:data.child_totals}}
+
+
+
+
+ Number of clients:
+
+
+ #{{:data.no_child_accounts}}
+
+
+
+
+ Fractional Reserve:
+
+
+ {{:data.fractional_reserve}}
+
+
+ {{:helper.link('Withdrawal', null, {'withdrawal' : 1}, null)}}
+
+ Account modifications
+
+ | Description | Activation time | Early trigger | Cancel
+ {{for data.pending_parent_mods}}
+ |
|---|
+ | {{:value.desc}} |
+ {{:value.countdown}} |
+
+ {{if value.allow_early}}
+ {{:helper.link('', 'alert', {'activate_modification' : value.index})}}
+ {{/if}}
+ |
+
+ {{if value.allow_cancel}}
+ {{:helper.link('', 'cancel', {'cancel_modification' : value.index})}}
+ {{/if}}
+ |
+ {{/for}}
+
+ {{:helper.link('Add modification', null, {'add_modification' : 1}, null)}}
+ Admin accounts
+ Admin accounts are notified in the case of financial incidents or changes to financial policy.
+
+ | Login | Remove
+ {{for data.admin_accounts}}
+ |
|---|
+ | {{:value}} |
+
+ {{:helper.link('', 'cancel', {'remove_admin' : value})}}
+ |
+ {{/for}}
+
+ {{:helper.link('Add admin', null, {'add_admin' : 1}, null)}}
+
+ Perform transaction:
+
+
+ Transaction Amount:
+
+
+ {{:helper.link(data.trans_amount ? data.trans_amount : '', null, {'transfer_amount' : 1})}}
+
+
+
+
+ Target account:
+
+
+ {{:helper.link(data.trans_account ? data.trans_account : '', null, {'transfer_account' : 1})}}
+
+
+
+
+ Target network:
+
+
+ {{:helper.link(data.trans_network ? data.trans_network : '', null, {'transfer_network' : 1})}}
+
+
+
+
+ Transfer Purpose:
+
+
+ {{:data.trans_purpose ? data.trans_purpose : ''}}{{:helper.link('', 'pencil', {'transfer_purpose' : 1})}}
+
+
+
+ {{:helper.link('Perform transfer', null, {'perform_transfer' : 1})}}
+
+ Transaction Logs:
+
+ | To | From | Amount | Purpose | Time
+ {{for data.parent_transactions}}
+ |
|---|
+ | {{:value.target}} |
+ {{:value.source}} |
+ {{:value.amount}} |
+ {{:value.purpose}} |
+ {{:value.time}}, {{:value.date}} |
+ {{/for}}
+
+ {{:helper.link('<', null, {'prev_page' : 1}, data.prev_page ? null : 'disabled')}}
+ {{:helper.link('>', null, {'next_page' : 1}, data.next_page ? null : 'disabled')}}
+
+ {{/if}}
+ {{:helper.link('Go back', 'arrowthickstop-1-w', {'back' : 1}, null)}}
+{{else data.prog_state == 2}}
+ {{if data.accounts}}
+ Client Accounts:
+
+ | Account Login | Real Name | Balance
+ {{for data.accounts}}
+ |
|---|
+ | {{:helper.link(value.account, '', {'select_account' : value.account})}}
+ | {{:value.fullname}}
+ | {{:value.money}}
+ {{/for}}
+ |
+
+
+
+ Auto-generate Client Accounts:
+
+
+ {{:helper.link(data.auto_accounts ? 'Enabled' : 'Disabled', null, {'toggle_auto_accounts' : 1}, null)}}
+
+
+ {{if data.auto_accounts}}
+
+
+ Preset interest rate:
+
+
+ {{:helper.link(data.auto_interest_rate, null, {'change_auto_interest' : 1}, null)}}
+
+
+
+
+ Preset withdrawal limit:
+
+
+ {{:helper.link(data.auto_withdrawal_limit, null, {'change_auto_withdrawal' : 1}, null)}}
+
+
+
+
+ Preset transaction fee:
+
+
+ {{:helper.link(data.auto_transaction_fee, null, {'change_auto_transaction' : 1}, null)}}
+
+
+ {{/if}}
+ {{:helper.link('Go back', 'arrowthickstop-1-w', {'back' : 1}, null)}}
+ {{else}}
+ Client account: {{:data.child_name}}
+
+
+ Current Balance:
+
+
+ {{:data.child_balance}}
+
+
+
+
+ Interest Rate:
+
+
+ {{:data.interest_rate}}
+
+
+
+
+ Withdrawal Limit:
+
+
+ {{:data.current_withdrawal}} / {{:data.withdrawal_limit}}
+
+
+
+
+ Transaction Fee:
+
+
+ {{:data.transaction_fee}}
+
+
+
+ {{:helper.link('Close account', 'alert', {'close_account' : 1}, null)}}
+
+ Account modifications
+
+ | Description | Activation time | Cancel
+ {{for data.pending_child_mods}}
+ |
|---|
+ | {{:value.desc}} |
+ {{:value.countdown}} |
+
+ {{if value.allow_cancel}}
+ {{:helper.link('', 'cancel', {'cancel_modification' : value.index})}}
+ {{/if}}
+ |
+ {{/for}}
+
+ {{:helper.link('Add modification', null, {'add_modification' : 1}, null)}}
+
+ Client Transaction Logs:
+
+ | To | From | Amount | Purpose | Time
+ {{for data.child_transactions}}
+ |
|---|
+ | {{:value.target}} |
+ {{:value.source}} |
+ {{:value.amount}} |
+ {{:value.purpose}} |
+ {{:value.time}}, {{:value.date}} |
+ {{/for}}
+
+ {{:helper.link('<', null, {'prev_page' : 1}, data.prev_page ? null : 'disabled')}}
+ {{:helper.link('>', null, {'next_page' : 1}, data.next_page ? null : 'disabled')}}
+
+ {{:helper.link('Go back', 'arrowthickstop-1-w', {'back' : 1}, null)}}
+ {{/if}}
+{{/if}}
diff --git a/nano/templates/identification_computer.tmpl b/nano/templates/identification_computer.tmpl
index 72277b1ea7d..0aeecefc098 100644
--- a/nano/templates/identification_computer.tmpl
+++ b/nano/templates/identification_computer.tmpl
@@ -57,7 +57,7 @@
Account Number:
- {{:helper.link(data.id_account_number, 'pencil', {'action' : 'edit', 'account' : 1})}}
+ {{:helper.link(data.id_account_id, 'pencil', {'action' : 'edit', 'account' : 1})}}
diff --git a/nano/templates/network_pos.tmpl b/nano/templates/network_pos.tmpl
new file mode 100644
index 00000000000..ec08ad3d117
--- /dev/null
+++ b/nano/templates/network_pos.tmpl
@@ -0,0 +1,43 @@
+
+ {{:helper.link("Network Settings", null, { "settings" : 1 })}}
+
+
+{{if data.error}}
+
+
+ An error has occured:
+
+
+ {{:data.error}}
+
+
+{{else}}
+
+
Transaction Amount:
+
+ {{:helper.link(data.transaction_amount, null, { "transaction_amount" : 1 })}}
+
+
+
+
Transaction Purpose:
+
+ {{:helper.link(data.transaction_purpose, null, { "transaction_purpose" : 1 })}}
+
+
+
+
Transaction Authorized:
+
+ {{if data.authorized}}
+ {{:helper.link('AUTHORIZED', 'check', {"authorize_transaction" : 1 })}}
+ {{else}}
+ {{:helper.link('UNAUTHORIZED', null, {"authorize_transaction" : 1 })}}
+ {{/if}}
+
+
+
+ {{if data.account_id}}
+ Connected to account '{{:data.account_id}}@{{:data.account_provider}}'
+ {{else}}
+ {{:helper.link('Connect Account', null, { "connect_account" : 1 })}}
+ {{/if}}
+{{/if}}
\ No newline at end of file
diff --git a/nebula.dme b/nebula.dme
index 545dd443671..44caf023f65 100644
--- a/nebula.dme
+++ b/nebula.dme
@@ -194,6 +194,7 @@
#include "code\controllers\evacuation\evacuation_predicate.dm"
#include "code\controllers\evacuation\evacuation_shuttle.dm"
#include "code\controllers\evacuation\~evac.dm"
+#include "code\controllers\subsystems\accounts.dm"
#include "code\controllers\subsystems\air.dm"
#include "code\controllers\subsystems\alarm.dm"
#include "code\controllers\subsystems\ambience.dm"
@@ -779,6 +780,7 @@
#include "code\game\machinery\_machines_base\stock_parts\disk_reader.dm"
#include "code\game\machinery\_machines_base\stock_parts\item_holder.dm"
#include "code\game\machinery\_machines_base\stock_parts\legacy_parts.dm"
+#include "code\game\machinery\_machines_base\stock_parts\money_printer.dm"
#include "code\game\machinery\_machines_base\stock_parts\network_lock.dm"
#include "code\game\machinery\_machines_base\stock_parts\network_receiver.dm"
#include "code\game\machinery\_machines_base\stock_parts\shielding.dm"
@@ -1871,10 +1873,14 @@
#include "code\modules\economy\worth_stacks.dm"
#include "code\modules\economy\worth_vendomat.dm"
#include "code\modules\economy\cael\_economy_misc.dm"
+#include "code\modules\economy\cael\account_mod.dm"
#include "code\modules\economy\cael\Accounts.dm"
#include "code\modules\economy\cael\Accounts_DB.dm"
#include "code\modules\economy\cael\ATM.dm"
#include "code\modules\economy\cael\EFTPOS.dm"
+#include "code\modules\economy\cael\escrow.dm"
+#include "code\modules\economy\cael\global_accounts.dm"
+#include "code\modules\economy\cael\related_accounts.dm"
#include "code\modules\economy\cael\Transactions.dm"
#include "code\modules\emotes\emote_define.dm"
#include "code\modules\emotes\emote_mob.dm"
@@ -2594,6 +2600,7 @@
#include "code\modules\modular_computers\file_system\programs\command\accounts.dm"
#include "code\modules\modular_computers\file_system\programs\command\card.dm"
#include "code\modules\modular_computers\file_system\programs\command\comm.dm"
+#include "code\modules\modular_computers\file_system\programs\command\finances.dm"
#include "code\modules\modular_computers\file_system\programs\engineering\alarm_monitor.dm"
#include "code\modules\modular_computers\file_system\programs\engineering\atmos_control.dm"
#include "code\modules\modular_computers\file_system\programs\engineering\network_monitoring.dm"
@@ -2602,6 +2609,7 @@
#include "code\modules\modular_computers\file_system\programs\engineering\shields_monitor.dm"
#include "code\modules\modular_computers\file_system\programs\engineering\shutoff_valve.dm"
#include "code\modules\modular_computers\file_system\programs\engineering\supermatter_monitor.dm"
+#include "code\modules\modular_computers\file_system\programs\generic\atm.dm"
#include "code\modules\modular_computers\file_system\programs\generic\camera.dm"
#include "code\modules\modular_computers\file_system\programs\generic\configurator.dm"
#include "code\modules\modular_computers\file_system\programs\generic\crew_manifest.dm"
@@ -2656,6 +2664,7 @@
#include "code\modules\modular_computers\networking\accounts\id_card.dm"
#include "code\modules\modular_computers\networking\device_types\_network_device.dm"
#include "code\modules\modular_computers\networking\device_types\acl.dm"
+#include "code\modules\modular_computers\networking\device_types\bank.dm"
#include "code\modules\modular_computers\networking\device_types\broadcaster.dm"
#include "code\modules\modular_computers\networking\device_types\id_card.dm"
#include "code\modules\modular_computers\networking\device_types\mainframe.dm"
@@ -2665,8 +2674,13 @@
#include "code\modules\modular_computers\networking\device_types\stock_part.dm"
#include "code\modules\modular_computers\networking\emails\_email.dm"
#include "code\modules\modular_computers\networking\emails\email_message.dm"
+#include "code\modules\modular_computers\networking\finance\money_accounts.dm"
+#include "code\modules\modular_computers\networking\finance\money_cube.dm"
+#include "code\modules\modular_computers\networking\finance\network_finance.dm"
+#include "code\modules\modular_computers\networking\finance\network_pos.dm"
#include "code\modules\modular_computers\networking\machinery\_network_machine.dm"
#include "code\modules\modular_computers\networking\machinery\acl.dm"
+#include "code\modules\modular_computers\networking\machinery\bank.dm"
#include "code\modules\modular_computers\networking\machinery\mainframe.dm"
#include "code\modules\modular_computers\networking\machinery\modem.dm"
#include "code\modules\modular_computers\networking\machinery\relay.dm"