diff --git a/code/game/objects/items/weapons/circuitboards/machinery/network.dm b/code/game/objects/items/weapons/circuitboards/machinery/network.dm
index 614673fdabe..93a2d0787d4 100644
--- a/code/game/objects/items/weapons/circuitboards/machinery/network.dm
+++ b/code/game/objects/items/weapons/circuitboards/machinery/network.dm
@@ -106,4 +106,18 @@
additional_spawn_components = list(
/obj/item/stock_parts/power/apc/buildable = 1
- )
\ No newline at end of file
+ )
+
+/obj/item/stock_parts/circuitboard/trade_controller
+ name = "circuitboard (trade control device)"
+ build_path = /obj/machinery/network/trade_controller
+ origin_tech = "{'programming':4,'magnets':3}"
+ req_components = list(
+ /obj/item/stock_parts/capacitor = 1,
+ /obj/item/stock_parts/scanning_module = 2
+ )
+
+/obj/item/stock_parts/circuitboard/telepad
+ name = "circuitboard (cargo telepad)"
+ build_path = /obj/machinery/telepad_cargo
+
diff --git a/code/modules/economy/cargo_telepads.dm b/code/modules/economy/cargo_telepads.dm
new file mode 100644
index 00000000000..93e3e2f051a
--- /dev/null
+++ b/code/modules/economy/cargo_telepads.dm
@@ -0,0 +1,28 @@
+/obj/machinery/telepad_cargo
+ name = "cargo telepad"
+ desc = "A telepad used to recieve imports and send exports."
+ icon = 'icons/obj/machines/telepad.dmi'
+ icon_state = "pad-idle"
+ anchored = 1
+ use_power = 1
+ idle_power_usage = 20
+ active_power_usage = 500
+ var/telepad_id = 0
+ obj_flags = OBJ_FLAG_ANCHORABLE
+
+/obj/machinery/telepad_cargo/Initialize()
+ telepad_id = random_id(type,10000,99999)
+ . = ..()
+
+/obj/machinery/telepad_cargo/attackby(obj/item/O as obj, mob/user as mob, params)
+ if(IS_MULTITOOL(O))
+ var/id = input(user, "Enter a new telepad ID", "Telepad ID") as text|null
+ id = sanitize(id)
+ if(CanInteract(user, DefaultTopicState()) && id) telepad_id = id
+ return TRUE
+
+ . = ..()
+
+/obj/machinery/telepad_cargo/examine(mob/user)
+ . = ..()
+ to_chat(user, "It has a telepad ID of [telepad_id].")
\ No newline at end of file
diff --git a/code/modules/modular_computers/hardware/trade_controller.dm b/code/modules/modular_computers/hardware/trade_controller.dm
new file mode 100644
index 00000000000..01263743057
--- /dev/null
+++ b/code/modules/modular_computers/hardware/trade_controller.dm
@@ -0,0 +1,89 @@
+/datum/extension/network_device/trade_controller
+ var/obj/effect/overmap/trade_beacon/linked_beacon
+ var/import_tax = 0
+ var/export_tax = 0
+ var/beacon_restrict = 0 // Restriction to ALL beacon functions, import/export/log. 0 = no restricton, 1 = restricted to the network
+ var/log_restrict = 0 // Restriction of the transaction log. 0 = no restricton, 1 = restricted to the network
+ var/access = 0 // Which access will allow for general beacon functions
+ var/log_access = 0 // which access will allow for viewing the transaction log
+
+/datum/extension/network_device/trade_controller/proc/Unlink()
+ if(linked_beacon)
+ linked_beacon.linked_controller = null
+ linked_beacon = null
+/datum/extension/network_device/trade_controller/Destroy()
+ Unlink()
+ . = ..()
+
+/datum/extension/network_device/trade_controller/disconnect(net_down)
+ Unlink()
+ . = ..()
+
+/datum/extension/network_device/trade_controller/proc/toggle_beacon_restrict()
+ if(!linked_beacon)
+ beacon_restrict = 0
+ return
+ beacon_restrict = !beacon_restrict
+ var/datum/computer_network/network = get_network()
+ if(network.trade_controller == src)
+ network.add_log("Trade Control Device toggled beacon restriction for [linked_beacon.name].", network_tag)
+ return 1
+
+
+/datum/extension/network_device/trade_controller/proc/select_beacon_access(var/mob/M, var/obj/machinery/network/trade_controller/machine)
+ var/list/access_choices = list()
+ var/datum/computer_network/network = get_network()
+ var/datum/extension/network_device/acl/D
+ if(network)
+ D = network.access_controller
+ if(!linked_beacon)
+ access = 0
+ return
+ if(!D)
+ to_chat(M, SPAN_WARNING("Cannot restrict access! No access controller detected on network!"))
+ return
+ for(var/parent_group in D.group_dict)
+ access_choices |= parent_group
+ for(var/child_group in D.group_dict[parent_group])
+ access_choices |= child_group
+ access_choices |= "*NONE*"
+ var/chosen = input(M, "Select an Access for the Trade Beacon.", "Beacon Access Selection") as null|anything in access_choices
+ if(chosen && (!machine || machine.CanUseTopic(M)))
+ if(chosen == "*NONE*")
+ access = 0
+ network.add_log("Trade Control Device unset beacon access for [linked_beacon.name].", network_tag)
+ else
+ access = chosen
+ network.add_log("Trade Control Device set beacon access to [chosen] for [linked_beacon.name].", network_tag)
+ return 1
+/datum/extension/network_device/trade_controller/proc/select_import_tax(var/mob/M, var/obj/machinery/network/trade_controller/machine)
+ var/datum/computer_network/network = get_network()
+ if(!linked_beacon)
+ import_tax = 0
+ return
+ var/chosen_tax = input(M, "Enter the import tax% to charge for this beacon. (30% MAX)", "Import Tax") as num|null
+ if(!machine || machine.CanUseTopic(M))
+ import_tax = clamp(chosen_tax, 0, 30)
+ network.add_log("Trade Control Device changed the import tax rate to [import_tax] for [linked_beacon.name].", network_tag)
+ return 1
+
+/datum/extension/network_device/trade_controller/proc/select_export_tax(var/mob/M, var/obj/machinery/network/trade_controller/machine)
+ var/datum/computer_network/network = get_network()
+ if(!linked_beacon)
+ export_tax = 0
+ return
+ var/chosen_tax = input(M, "Enter the export tax% to charge for this beacon. (30% MAX)", "Export Tax") as num|null
+ if(!machine || machine.CanUseTopic(M))
+ export_tax = clamp(chosen_tax, 0, 30)
+ network.add_log("Trade Control Device changed the export tax rate to [export_tax] for [linked_beacon.name].", network_tag)
+ return 1
+
+/datum/extension/network_device/trade_controller/proc/toggle_log_restrict()
+ if(!linked_beacon)
+ log_restrict = 0
+ return
+ log_restrict = !log_restrict
+ var/datum/computer_network/network = get_network()
+ if(network.trade_controller == src)
+ network.add_log("Trade Control Device toggled transaction log restriction for [linked_beacon.name].", network_tag)
+ return 1
\ No newline at end of file
diff --git a/code/modules/modular_computers/networking/_network.dm b/code/modules/modular_computers/networking/_network.dm
index ec7eb0b4723..cd4150fa4ef 100644
--- a/code/modules/modular_computers/networking/_network.dm
+++ b/code/modules/modular_computers/networking/_network.dm
@@ -24,6 +24,8 @@
var/datum/money_account/parent/network/parent_account
var/list/datum/extension/network_device/money_cube/money_cubes = list()
+ var/datum/extension/network_device/trade_controller/trade_controller
+
var/network_features_enabled = NET_ALL_FEATURES
var/intrusion_detection_enabled
var/intrusion_detection_alarm
@@ -108,6 +110,7 @@
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
@@ -117,6 +120,12 @@
parent_account.adjust_money(cube.stored_money)
cube.stored_money = 0
+ else if(istype(D, /datum/extension/network_device/trade_controller))
+ if(trade_controller)
+ return FALSE
+ trade_controller = D
+ add_log("New Trade Control set", newtag)
+
D.network_tag = newtag
devices |= D
devices_by_tag[D.network_tag] = D
diff --git a/code/modules/modular_computers/networking/machinery/trade_controller.dm b/code/modules/modular_computers/networking/machinery/trade_controller.dm
new file mode 100644
index 00000000000..b8dae3a196b
--- /dev/null
+++ b/code/modules/modular_computers/networking/machinery/trade_controller.dm
@@ -0,0 +1,104 @@
+/obj/machinery/network/trade_controller
+ name = "trade control device"
+ desc = "A device used for controlling trade beacons and adding import/export tax. It must remain in a gravity well near the trade beacon"
+ icon = 'icons/obj/machines/tcomms/hub.dmi'
+ icon_state = "hub"
+ network_device_type = /datum/extension/network_device/trade_controller
+ main_template = "trade_controller.tmpl"
+ construct_state = /decl/machine_construction/default/panel_closed
+ uncreated_component_parts = null
+ base_type = /obj/machinery/network/trade_controller
+
+
+/obj/machinery/network/trade_controller/OnTopic(mob/user, href_list, datum/topic_state/state)
+ . = ..()
+ if(.)
+ return
+ var/datum/extension/network_device/trade_controller/D = get_extension(src, /datum/extension/network_device)
+ var/datum/computer_network/network = D.get_network()
+ if(!network || network.trade_controller != D)
+ error = "NETWORK ERROR: Connection lost. Another trade controller may be active on the network."
+ return TOPIC_REFRESH
+
+ if(href_list["connect_beacon"])
+ var/list/possible_trade_beacons = get_adjacent_trade_beacons()
+ possible_trade_beacons += "*DISCONNECT*"
+ var/selected_beacon = input(usr, "Select a nearby trade beacon.", "Pick a beacon") as null|anything in possible_trade_beacons
+ if(!CanInteract(user, global.default_topic_state) || !selected_beacon)
+ return TOPIC_REFRESH
+ if(selected_beacon == "*DISCONNECT*")
+ D.Unlink()
+ else
+ var/obj/effect/overmap/trade_beacon/a = selected_beacon
+ if(a.linked_controller)
+ to_chat(user, SPAN_NOTICE("This beacon is already connected to a trade control device. The existing trade control device must be removed first."))
+ return TOPIC_REFRESH
+ else
+ a.linked_controller = D
+ D.linked_beacon = a
+ return TOPIC_REFRESH
+
+ if(href_list["toggle_beacon_restrict"])
+ D.toggle_beacon_restrict()
+ return TOPIC_REFRESH
+ if(href_list["select_beacon_access"])
+ D.select_beacon_access(usr, src)
+ return TOPIC_REFRESH
+ if(href_list["change_import_tax"])
+ D.select_import_tax(usr, src)
+ return TOPIC_REFRESH
+ if(href_list["change_export_tax"])
+ D.select_export_tax(usr, src)
+ return TOPIC_REFRESH
+ if(href_list["toggle_log_restrict"])
+ D.toggle_log_restrict()
+ return TOPIC_REFRESH
+
+ if(href_list["info"])
+ switch(href_list["info"])
+ if("connect_beacon")
+ to_chat(user, SPAN_NOTICE("The Trade Controller can take control of trade beacons that you are within 1 tile of in space. A beacon can only be controlled by one Trade Controller, and will remaiin controlled until that controller is brought offline."))
+ if("restrict_beacon_access")
+ to_chat(user, SPAN_NOTICE("If Beacon restriction is turned on, only devices connected to this network and with the appropriate access will be able to use the beacon for imports, exports or viewing the transaction log."))
+ if("select_beacon_access")
+ to_chat(user, SPAN_NOTICE("You can set this access to any group in the network, in which case it will be required to access the Import/Export/Log functions for the beacon. If this is unset any device on this network will be able to access the beacon."))
+ if("import_tax")
+ to_chat(user, SPAN_NOTICE("This is a percentage added to the cost of all imports done, except those done by the primary money account on this network. The added percentage will be deposited into the primary money account."))
+ if("export_tax")
+ to_chat(user, SPAN_NOTICE("This is a percentage taken from the revenue of all exports done, except those done by the primary money account on this network. The added percentage will be deposited into the primary money account."))
+ if("restrict_log_access")
+ to_chat(user, SPAN_NOTICE("If Transaction Log restriction is turned on, only devices connected to this network and with the appropriate access will be able to view the transaction log."))
+ if("select_log_access")
+ to_chat(user, SPAN_NOTICE("You can set this access to any group in the network, in which case it will be required to access the Log functions for the beacon. If this is unset any device on this network will be able to access the transaction log."))
+
+/obj/machinery/network/trade_controller/ui_data(mob/user, ui_key)
+ . = ..()
+
+ if(error)
+ .["error"] = error
+ return
+
+ var/datum/extension/network_device/trade_controller/D = get_extension(src, /datum/extension/network_device)
+ if(!D.get_network())
+ .["connected"] = FALSE
+ return
+ .["connected"] = TRUE
+ if(D.linked_beacon)
+ .["connected_beacon"] = D.linked_beacon.name
+ .["valid_beacon"] = TRUE
+ .["beacon_restrict"] = D.beacon_restrict
+ if(D.beacon_restrict)
+ if(D.access)
+ .["selected_beacon_access"] = D.access
+ else
+ .["selected_beacon_access"] = "*NONE*"
+ .["import_tax"] = D.import_tax
+ .["export_tax"] = D.export_tax
+ .["log_restrict"] = D.log_restrict
+ if(D.log_restrict)
+ if(D.log_access)
+ .["selected_log_access"] = D.log_access
+ else
+ .["selected_log_access"] = "*NONE*"
+ else
+ .["connected_beacon"] = "*DISCONNECTED*"
\ No newline at end of file
diff --git a/icons/obj/machines/telepad.dmi b/icons/obj/machines/telepad.dmi
new file mode 100644
index 00000000000..14667b036aa
Binary files /dev/null and b/icons/obj/machines/telepad.dmi differ
diff --git a/mods/persistence/_persistence.dme b/mods/persistence/_persistence.dme
index 063093a7ba0..128a828b7f3 100644
--- a/mods/persistence/_persistence.dme
+++ b/mods/persistence/_persistence.dme
@@ -173,6 +173,7 @@
#include "modules\modular_computers\file_system\directory.dm"
#include "modules\modular_computers\file_system\programs\cloning.dm"
#include "modules\modular_computers\file_system\programs\rent_management.dm"
+#include "modules\modular_computers\file_system\programs\trade_beacon_program.dm"
#include "modules\modular_computers\file_system\reports\crew_record.dm"
#include "modules\modular_computers\file_system\reports\report.dm"
#include "modules\modular_computers\hardware\battery_module.dm"
diff --git a/mods/persistence/controllers/subsystems/trade_beacons.dm b/mods/persistence/controllers/subsystems/trade_beacons.dm
new file mode 100644
index 00000000000..741d368ae95
--- /dev/null
+++ b/mods/persistence/controllers/subsystems/trade_beacons.dm
@@ -0,0 +1,28 @@
+SUBSYSTEM_DEF(trade_beacons)
+ name = "Trade Beacons"
+ wait = 5 MINUTES
+ priority = SS_PRIORITY_MONEY_ACCOUNTS
+ var/list/all_trade_beacons = list()
+ var/list/wanted_trade_beacons = list(/obj/effect/overmap/trade_beacon/test_beacon, /obj/effect/overmap/trade_beacon/test_beacon2) // list(//obj/effect/overmap/trade_beacon/example, /obj/effect/overmap/trade_beacon/steel, /obj/effect/overmap/trade_beacon/xandahar)
+ var/last_cycle = 0
+
+/datum/controller/subsystem/trade_beacons/Destroy()
+ QDEL_NULL_LIST(all_trade_beacons)
+ . = ..()
+
+/datum/controller/subsystem/trade_beacons/Initialize()
+ all_trade_beacons = list()
+#ifndef UNIT_TEST
+ for(var/x in wanted_trade_beacons)
+ var/obj/effect/overmap/trade_beacon/beacon = new x()
+ all_trade_beacons |= beacon
+#endif
+ . = ..()
+
+/datum/controller/subsystem/trade_beacons/fire(resumed = FALSE)
+ if(REALTIMEOFDAY >= (last_cycle + 2 HOURS))
+ last_cycle = REALTIMEOFDAY
+ for(var/obj/effect/overmap/trade_beacon/x in all_trade_beacons)
+ x.regenerate_imports()
+ x.regenerate_exports()
+ return
\ No newline at end of file
diff --git a/mods/persistence/modules/modular_computers/file_system/programs/trade_beacon_program.dm b/mods/persistence/modules/modular_computers/file_system/programs/trade_beacon_program.dm
new file mode 100644
index 00000000000..4420339e751
--- /dev/null
+++ b/mods/persistence/modules/modular_computers/file_system/programs/trade_beacon_program.dm
@@ -0,0 +1,566 @@
+/datum/computer_file/program/trade_management
+ filename = "trademnrger"
+ filedesc = "Import/Export Management"
+ extended_desc = "This program allows you to process imports or exports from nearby trade beacons."
+
+ program_icon_state = "generic"
+ program_key_state = "generic_key"
+ program_menu_icon = "cart"
+
+ available_on_network = 1
+
+ size = 12
+ nanomodule_path = /datum/nano_module/program/trade_management
+ category = PROG_SUPPLY
+
+ var/obj/effect/overmap/trade_beacon/selected_beacon
+
+ var/import_menu = 1
+ var/export_menu = 0
+ var/log_menu = 0
+ var/datum/beacon_import/selected_import
+ var/datum/beacon_export/selected_export
+ var/payment_type = 1 // 1 == personal account 2 == chargestick 3 == network parent account
+
+ var/list/detected_telepads = list()
+ var/obj/selected_telepad
+
+/datum/computer_file/program/trade_management/proc/detect_telepads()
+ detected_telepads = list()
+ for(var/obj/machinery/telepad_cargo/c in orange(3, get_turf(computer)))
+ if(c.anchored)
+ detected_telepads |= c
+ return
+
+/datum/computer_file/program/trade_management/on_startup(mob/living/user, datum/extension/interactive/os/new_host)
+ . = ..()
+ detect_telepads()
+
+
+/datum/nano_module/program/trade_management
+ name = "Import/Export Management"
+
+
+/datum/computer_file/program/trade_management/proc/spawnImport()
+ var/turf/T = get_turf(selected_telepad)
+ flick("pad-beam", selected_telepad)
+ . = selected_import.spawnImport(T)
+ if(selected_import.remaining_stock < 1)
+ selected_beacon.active_imports.Remove(selected_import)
+ selected_import = null
+/datum/computer_file/program/trade_management/proc/checkImport()
+ var/turf/T = get_turf(selected_telepad)
+ flick("pad-beam", selected_telepad)
+ return selected_import.checkImport(T)
+
+/datum/computer_file/program/trade_management/proc/takeExport()
+ flick("pad-beam", selected_telepad)
+ . = selected_export.takeExport()
+ if(selected_export.remaining_stock < 1)
+ selected_beacon.active_exports.Remove(selected_export)
+ selected_export = null
+/datum/computer_file/program/trade_management/proc/checkExport()
+ var/turf/T = get_turf(selected_telepad)
+ return selected_export.checkExport(T)
+
+/datum/computer_file/program/trade_management/proc/resetExport()
+ return selected_export.resetExport()
+
+/datum/computer_file/program/trade_management/proc/buyImport()
+ var/import_tax = 0
+ if(!selected_import || !selected_beacon)
+ return
+ if(!(selected_import in selected_beacon.active_imports))
+ selected_import = null
+ return
+ if(!selected_telepad)
+ to_chat(usr, SPAN_WARNING("You must select a telepad to send the import to."))
+ return TOPIC_REFRESH
+ if(selected_beacon && selected_beacon.linked_controller)
+ import_tax = selected_beacon.linked_controller.import_tax
+ var/cost = selected_import.get_cost(import_tax)
+ var/tax_portion = selected_import.get_tax(import_tax)
+ switch(payment_type)
+ if(1)
+ var/datum/computer_file/data/account/A = computer.get_account()
+ if(A)
+
+ var/oerr = checkImport()
+ if(oerr)
+ to_chat(usr, SPAN_WARNING("Import purchase failed: [oerr]"))
+ return TOPIC_HANDLED
+ var/datum/money_account/child/network/money_account = A.money_account
+ if(money_account)
+ var/terr = money_account.can_afford(cost, selected_beacon.beacon_account)
+ if(terr)
+ to_chat(usr, SPAN_WARNING("Import purchase failed: [terr]"))
+ return TOPIC_HANDLED
+ var/final_cost = cost-tax_portion
+ var/err = money_account.transfer(selected_beacon.beacon_account, final_cost, "Import Purchase ([selected_import.name] from [selected_beacon.name])")
+ if(err)
+ to_chat(usr, SPAN_WARNING("Import purchase failed: [err]."))
+ return TOPIC_HANDLED
+ else
+ if(tax_portion)
+ var/datum/computer_network/network = selected_beacon.linked_controller.get_network()
+ if(network && network.banking_mainframe && network.banking_mainframe.get_parent_account())
+ err = money_account.transfer(network.banking_mainframe.get_parent_account(), tax_portion, "Import Purchase Tax ([selected_import.name] from [selected_beacon.name])")
+ if(err)
+ selected_beacon.beacon_account.transfer(money_account, final_cost, "Refunded Import Purchase ([selected_import.name] from [selected_beacon.name])")
+ else
+ spawnImport()
+ else
+ spawnImport()
+ else
+ to_chat(usr, SPAN_WARNING("No banking account associated with this network. Contact your system administrator.."))
+ else
+ to_chat(usr, SPAN_WARNING("You must login to a valid account on your network to pay digitally."))
+ if(2)
+ var/obj/item/stock_parts/computer/charge_stick_slot/charge_slot = computer.get_component(PART_MSTICK)
+ if(!charge_slot || !charge_slot.stored_stick)
+ to_chat(usr, SPAN_WARNING("No charge stick is inserted into the computer."))
+ return TOPIC_REFRESH
+ var/obj/item/charge_stick/stick = charge_slot.stored_stick
+ if(cost > stick.loaded_worth)
+ to_chat(usr, SPAN_WARNING("The loaded charge stick does not have enough credits stored!"))
+ return TOPIC_HANDLED
+ var/oerr = checkImport()
+ if(oerr)
+ to_chat(usr, SPAN_WARNING("Import purchase failed: [oerr]"))
+ return TOPIC_HANDLED
+ stick.adjust_worth(-(cost))
+ if(tax_portion)
+ var/datum/computer_network/network = selected_beacon.linked_controller.get_network()
+ if(network && network.banking_mainframe && network.banking_mainframe.get_parent_account())
+ var/datum/money_account/child/network/money_account = network.banking_mainframe.get_parent_account()
+ money_account.deposit(tax_portion, "Import Purchase Tax ([selected_import.name] from [selected_beacon.name])")
+ spawnImport()
+ if(3)
+ var/datum/computer_network/net = computer.get_network()
+ var/list/accesses = computer.get_access(usr)
+ if(net && net.banking_mainframe)
+ if(!net.banking_mainframe.has_access(accesses))
+ to_chat(usr, SPAN_WARNING("You do not have access to the parent account on your current network."))
+ return TOPIC_REFRESH
+ else
+ var/datum/money_account/parent/network/money_account = net.banking_mainframe.get_parent_account()
+ if(money_account)
+ var/terr = money_account.can_afford(cost, selected_beacon.beacon_account)
+ if(terr)
+ to_chat(usr, SPAN_WARNING("Import purchase failed: [terr]"))
+ return TOPIC_HANDLED
+ var/oerr = checkImport()
+ if(oerr)
+ to_chat(usr, SPAN_WARNING("Import purchase failed: [oerr]"))
+ return TOPIC_HANDLED
+ if(tax_portion)
+ var/datum/computer_network/network = selected_beacon.linked_controller.get_network()
+ if(network && network.banking_mainframe && network.banking_mainframe.get_parent_account())
+ if(network.banking_mainframe.get_parent_account() == money_account) // dont pay tax from the account into the same account
+ tax_portion = 0
+ cost = selected_import.get_cost(0)
+ else // remove tax if we cant find a parent account to pay it to.
+ tax_portion = 0
+ cost = selected_import.get_cost(0)
+ if(money_account.money < cost)
+ to_chat(usr, SPAN_WARNING("Import purchase failed: Insufficent funds."))
+ return TOPIC_HANDLED
+ var/final_cost = cost-tax_portion
+ var/err = money_account.transfer(selected_beacon.beacon_account, final_cost, "Import Purchase ([selected_import.name] from [selected_beacon.name])")
+ if(err)
+ to_chat(usr, SPAN_WARNING("Import purchase failed: [err]."))
+ return TOPIC_HANDLED
+ else
+ if(tax_portion)
+ var/datum/computer_network/network = selected_beacon.linked_controller.get_network()
+ if(network && network.banking_mainframe && network.banking_mainframe.get_parent_account())
+ err = money_account.transfer(network.banking_mainframe.get_parent_account(), tax_portion, "Import Purchase Tax ([selected_import.name] from [selected_beacon.name])")
+ if(err)
+ selected_beacon.beacon_account.transfer(money_account, final_cost, "Refunded Import Purchase ([selected_import.name] from [selected_beacon.name])")
+ else
+ spawnImport()
+ else
+ spawnImport()
+ else
+ to_chat(usr, SPAN_WARNING("The network does not have a central account."))
+ return TOPIC_REFRESH
+ return TOPIC_REFRESH
+
+/datum/computer_file/program/trade_management/proc/buyExport()
+ var/export_tax = 0
+ if(!selected_export || !selected_beacon)
+ return TOPIC_REFRESH
+ if(!(selected_export in selected_beacon.active_exports))
+ selected_export = null
+ return
+ if(!selected_telepad)
+ to_chat(usr, SPAN_WARNING("You must select a telepad to send the export to."))
+ return TOPIC_REFRESH
+ if(selected_beacon && selected_beacon.linked_controller)
+ export_tax = selected_beacon.linked_controller.export_tax
+ var/cost = selected_export.get_cost(export_tax)
+ var/tax_portion = selected_export.get_tax(export_tax)
+ switch(payment_type)
+ if(1)
+ var/datum/computer_file/data/account/A = computer.get_account()
+ if(A)
+ var/datum/money_account/child/network/money_account = A.money_account
+ if(money_account)
+ var/e_err = checkExport()
+ if(e_err)
+ to_chat(usr, SPAN_WARNING("Export failed: [e_err]."))
+ resetExport()
+ return TOPIC_HANDLED
+ var/err = money_account.deposit(cost, "Export ([selected_export.name] from [selected_beacon.name])")
+ if(err)
+ to_chat(usr, SPAN_WARNING("Export failed: [err]."))
+ resetExport()
+ return TOPIC_HANDLED
+ else
+
+ if(tax_portion) // SUCCESS
+ var/datum/computer_network/network = selected_beacon.linked_controller.get_network()
+ if(network && network.banking_mainframe && network.banking_mainframe.get_parent_account())
+ var/datum/money_account/acc = network.banking_mainframe.get_parent_account()
+ err = acc.deposit(tax_portion, "Export Tax ([selected_export.name] from [selected_beacon.name])")
+ var/result = takeExport()
+ if(result)
+ to_chat(usr, SPAN_WARNING("Export failed: [e_err]."))
+ else
+ to_chat(usr, SPAN_WARNING("No banking account associated with this network. Contact your system administrator."))
+ return TOPIC_REFRESH
+ else
+ to_chat(usr, SPAN_WARNING("You must login to a valid account on your network to pay digitally."))
+ return TOPIC_REFRESH
+ if(2)
+ var/obj/item/stock_parts/computer/charge_stick_slot/charge_slot = computer.get_component(PART_MSTICK)
+ if(!charge_slot || !charge_slot.stored_stick)
+ to_chat(usr, SPAN_WARNING("No charge stick is inserted into the computer."))
+ return TOPIC_REFRESH
+ var/obj/item/charge_stick/stick = charge_slot.stored_stick
+ var/e_err = checkExport()
+ if(e_err)
+ to_chat(usr, SPAN_WARNING("Export failed: [e_err]."))
+ resetExport()
+ return TOPIC_HANDLED
+ stick.adjust_worth(cost)
+ if(tax_portion)
+ var/datum/computer_network/network = selected_beacon.linked_controller.get_network()
+ if(network && network.banking_mainframe && network.banking_mainframe.get_parent_account())
+ var/datum/money_account/acc = network.banking_mainframe.get_parent_account()
+ acc.deposit(tax_portion, "Export Tax ([selected_export.name] from [selected_beacon.name])")
+ var/result = takeExport()
+ if(result)
+ to_chat(usr, SPAN_WARNING("Export failed: [e_err]."))
+ if(3)
+ var/datum/computer_network/net = computer.get_network()
+ var/list/accesses = computer.get_access(usr)
+ if(net && net.banking_mainframe)
+ if(!net.banking_mainframe.has_access(accesses))
+ to_chat(usr, SPAN_WARNING("You do not have access to the parent account on your current network."))
+ return TOPIC_REFRESH
+ else
+ var/datum/money_account/parent/network/money_account = net.banking_mainframe.get_parent_account()
+ if(money_account)
+ if(tax_portion)
+ var/datum/computer_network/network = selected_beacon.linked_controller.get_network()
+ if(network && network.banking_mainframe && network.banking_mainframe.get_parent_account())
+ if(network.banking_mainframe.get_parent_account() == money_account) // dont pay tax from the account into the same account
+ tax_portion = 0
+ cost = selected_export.get_cost(0)
+ else // remove tax if we cant find a parent account to pay it to.
+ tax_portion = 0
+ cost = selected_export.get_cost(0)
+ var/e_err = checkExport()
+ if(e_err)
+ to_chat(usr, SPAN_WARNING("Export failed: [e_err]."))
+ resetExport()
+ return TOPIC_HANDLED
+ var/err = money_account.deposit(cost, "Export ([selected_export.name] to [selected_beacon.name])")
+ if(err)
+ to_chat(usr, SPAN_WARNING("Export failed: [err]."))
+ resetExport()
+ return TOPIC_HANDLED
+
+ else // SUCCESS
+ if(tax_portion)
+ var/datum/computer_network/network = selected_beacon.linked_controller.get_network()
+ if(network && network.banking_mainframe && network.banking_mainframe.get_parent_account())
+ var/datum/money_account/acc = network.banking_mainframe.get_parent_account()
+ err = acc.deposit(tax_portion, "Export Tax ([selected_export.name] from [selected_beacon.name])")
+ var/result = takeExport()
+ if(result)
+ to_chat(usr, SPAN_WARNING("Export failed: [e_err]."))
+
+ else
+ to_chat(usr, SPAN_WARNING("The network does not have a central account."))
+ return TOPIC_REFRESH
+ return TOPIC_REFRESH
+
+
+/datum/nano_module/program/trade_management/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_file/program/trade_management/PRG = program
+ var/datum/extension/interactive/os/computer = PRG.computer
+ var/list/telepads[0]
+ var/update_time = time2text((SStrade_beacons.last_cycle+2 HOURS),"hh:mm")
+ for(var/obj/machinery/telepad_cargo/x in PRG.detected_telepads)
+ var/selected = 0
+ if(x == PRG.selected_telepad) selected = 1
+ telepads.Add(list(list(
+ "telepad_name" = "[x.name] ([x.telepad_id])",
+ "selected" = selected,
+ "telepad_ref" = any2ref(x),
+ )))
+ if(!telepads.len)
+ telepads.Add(list(list(
+ "telepad_name" = "No telepads detected!",
+ "selected" = 1,
+ )))
+ data["telepads"] = telepads
+ data["update_time"] = update_time
+ var/obj/effect/overmap/trade_beacon/selected_beacon = PRG.selected_beacon
+ if(selected_beacon)
+ data["selected_beacon"] = TRUE
+ data["beacon_name"] = selected_beacon.name
+ var/access = 1
+ if(selected_beacon && selected_beacon.linked_controller && selected_beacon.linked_controller.get_network())
+ var/datum/computer_network/network = selected_beacon.linked_controller.get_network()
+ data["controlling_network"] = network.network_id
+ if(selected_beacon.linked_controller.beacon_restrict)
+ if(selected_beacon.linked_controller.get_network() == PRG.computer.get_network())
+ var/list/accesses = computer.get_access(user)
+ if(selected_beacon.linked_controller.access && !(selected_beacon.linked_controller.access in accesses))
+ access = 0
+ else
+ access = 0
+
+ if(access)
+ data["beacon_access"] = 1
+ if(PRG.import_menu)
+ data["import"] = 1
+ var/import_tax = 0
+ if(selected_beacon && selected_beacon.linked_controller)
+ import_tax = selected_beacon.linked_controller.import_tax
+
+ data["import_tax"] = import_tax
+ var/list/imports[0]
+ for(var/datum/beacon_import/x in selected_beacon.active_imports)
+ imports.Add(list(list(
+ "import_name" = x.name,
+ "import_cost" = x.get_cost(import_tax),
+ "import_remaining" = x.remaining_stock,
+ "import_ref" = any2ref(x),
+ )))
+ data["imports"] = imports
+
+ else if(PRG.export_menu)
+ data["export"] = 1
+ var/export_tax = 0
+ if(selected_beacon && selected_beacon.linked_controller)
+ export_tax = selected_beacon.linked_controller.export_tax
+ data["export_tax"] = export_tax
+ var/list/exports[0]
+ for(var/datum/beacon_export/x in selected_beacon.active_exports)
+ exports.Add(list(list(
+ "export_name" = x.name,
+ "export_cost" = x.get_cost(export_tax),
+ "export_remaining" = x.remaining_stock,
+ "export_ref" = any2ref(x),
+ )))
+ data["exports"] = exports
+ else if(PRG.log_menu)
+ data["log"] = 1
+ else if(PRG.selected_import)
+ switch(PRG.payment_type)
+ if(1)
+ data["personal_account_sel"] = 1
+ if(2)
+ data["charge_stick_sel"] = 1
+ if(3)
+ data["network_account_sel"] = 1
+
+ var/import_tax = 0
+ if(selected_beacon && selected_beacon.linked_controller)
+ import_tax = selected_beacon.linked_controller.import_tax
+
+ data["selected_import"] = PRG.selected_import.name
+ data["import_cost"] = PRG.selected_import.get_cost(import_tax)
+ data["import_desc"] = PRG.selected_import.desc
+
+ var/datum/computer_file/data/account/A = PRG.computer.get_account()
+ if(A)
+ var/datum/money_account/child/network/money_account = A.money_account
+ if(money_account)
+ data["personal_account"] = money_account.account_name
+ data["personal_account_balance"] = money_account.money
+ else
+ data["personal_account_error"] = "This account has no valid bank account associated with it on this network. Check your login details or contact a system administrator."
+
+ else
+ data["personal_account_error"] = "This account has no valid bank account associated with it on this network. Check your login details or contact a system administrator."
+
+ var/obj/item/stock_parts/computer/charge_stick_slot/charge_slot = computer.get_component(PART_MSTICK)
+ if(!charge_slot || !charge_slot.is_functional())
+ data["charge_stick_error"] = "No functional charge stick slot found!"
+ else if(!charge_slot.stored_stick)
+ data["charge_stick_error"] = "No charge stick inserted!"
+ else
+ data["charge_stick"] = "Charge Stick"
+ data["charge_stick_balance"] = charge_slot.stored_stick.loaded_worth
+ var/datum/computer_network/net = PRG.computer.get_network()
+ var/list/accesses = computer.get_access(user)
+ if(net && net.banking_mainframe)
+ if(!net.banking_mainframe.has_access(accesses))
+ data["network_account_error"] = "You do not have access to the parent account on your current network."
+ else
+ var/datum/money_account/parent/network/bank_account = net.banking_mainframe.get_parent_account()
+ if(!bank_account)
+ data["network_account_error"] = "The network does not have a parent account created."
+ else
+ data["network_account"] = bank_account.account_name
+ data["network_account_balance"] = bank_account.money
+ else
+ data["network_account_error"] = "The network does not have a banking mainframe."
+
+
+ else if(PRG.selected_export)
+ switch(PRG.payment_type)
+ if(1)
+ data["personal_account_sel"] = 1
+ if(2)
+ data["charge_stick_sel"] = 1
+ if(3)
+ data["network_account_sel"] = 1
+
+ var/export_tax = 0
+ if(selected_beacon && selected_beacon.linked_controller)
+ export_tax = selected_beacon.linked_controller.export_tax
+
+ data["selected_export"] = PRG.selected_export.name
+ data["export_cost"] = PRG.selected_export.get_cost(export_tax)
+ data["export_desc"] = PRG.selected_export.desc
+
+ var/datum/computer_file/data/account/A = PRG.computer.get_account()
+ if(A)
+ var/datum/money_account/child/network/money_account = A.money_account
+ if(money_account)
+ data["personal_account"] = money_account.account_name
+ data["personal_account_balance"] = money_account.money
+ else
+ data["personal_account_error"] = "No bank account."
+
+ else
+ data["personal_account_error"] = "Not logged in."
+
+ var/obj/item/stock_parts/computer/charge_stick_slot/charge_slot = computer.get_component(PART_MSTICK)
+ if(!charge_slot || !charge_slot.is_functional())
+ data["personal_account_error"] = "Noslot found!"
+ else if(!charge_slot.stored_stick)
+ data["personal_account_error"] = "No charge stick!"
+ else
+ data["charge_stick"] = "Charge Stick"
+ data["charge_stick_balance"] = charge_slot.stored_stick.loaded_worth
+ var/datum/computer_network/net = PRG.computer.get_network()
+ var/list/accesses = computer.get_access(user)
+ if(net && net.banking_mainframe)
+ if(!net.banking_mainframe.has_access(accesses))
+ data["network_account_error"] = "No access."
+ else
+ var/datum/money_account/parent/network/bank_account = net.banking_mainframe.get_parent_account()
+ if(!bank_account)
+ data["network_account_error"] = "No Network Account"
+ else
+ data["network_account"] = bank_account.account_name
+ data["network_account_balance"] = bank_account.money
+ else
+ data["network_account_error"] = "No Network Bank."
+
+
+ else
+ data["beacon_name"] = "*DISCONNECTED*"
+
+
+ ui = SSnano.try_update_ui(user, src, ui_key, ui, data, force_open)
+ if (!ui)
+ ui = new(user, src, ui_key, "trade_management.tmpl", name, 600, 750, state = state)
+ ui.auto_update_layout = 1
+ ui.set_initial_data(data)
+ ui.set_auto_update(1)
+ ui.open()
+
+/datum/computer_file/program/trade_management/Topic(href, href_list, state)
+ . = ..()
+ if(.)
+ return
+
+ if(href_list["select_beacon"])
+ var/obj/hold = computer.get_physical_host()
+ var/list/possible_trade_beacons = hold.get_adjacent_trade_beacons()
+ possible_trade_beacons += "*DISCONNECT*"
+ var/beacon = input(usr, "Select a nearby trade beacon.", "Pick a beacon") as null|anything in possible_trade_beacons
+ if(!CanInteract(usr, global.default_topic_state))
+ return TOPIC_REFRESH
+ if(beacon == "*DISCONNECT*")
+ selected_beacon = null
+ else if(beacon)
+ selected_beacon = beacon
+
+ return TOPIC_REFRESH
+ if(href_list["import"])
+ import_menu = 1
+ export_menu = 0
+ log_menu = 0
+ selected_import = null
+ selected_export = null
+ return TOPIC_REFRESH
+ if(href_list["export"])
+ import_menu = 0
+ export_menu = 1
+ log_menu = 0
+ selected_import = null
+ selected_export = null
+ return TOPIC_REFRESH
+ if(href_list["log"])
+ import_menu = 0
+ export_menu = 0
+ log_menu = 1
+ selected_import = null
+ selected_export = null
+ return TOPIC_REFRESH
+ if(href_list["select_import"])
+ import_menu = 0
+ export_menu = 0
+ log_menu = 0
+ selected_import = locate(href_list["select_import"])
+ selected_export = null
+ return TOPIC_REFRESH
+ if(href_list["select_export"])
+ import_menu = 0
+ export_menu = 0
+ log_menu = 0
+ selected_import = null
+ selected_export = locate(href_list["select_export"])
+ return TOPIC_REFRESH
+ if(href_list["select_personal_account"])
+ payment_type = 1
+ return TOPIC_REFRESH
+ if(href_list["select_charge_stick"])
+ payment_type = 2
+ return TOPIC_REFRESH
+ if(href_list["select_network_account"])
+ payment_type = 3
+ return TOPIC_REFRESH
+ if(href_list["select_telepad"])
+ detect_telepads()
+ var/obj/T = locate(href_list["select_telepad"])
+ if(T && (T in detected_telepads))
+ selected_telepad = T
+ return TOPIC_REFRESH
+ if(href_list["refresh_telepad"])
+ detect_telepads()
+ return TOPIC_REFRESH
+ if(href_list["buy_import"])
+ return buyImport()
+ if(href_list["buy_export"])
+ return buyExport()
diff --git a/mods/persistence/modules/modular_computers/hardware/trade_controller.dm b/mods/persistence/modules/modular_computers/hardware/trade_controller.dm
new file mode 100644
index 00000000000..01263743057
--- /dev/null
+++ b/mods/persistence/modules/modular_computers/hardware/trade_controller.dm
@@ -0,0 +1,89 @@
+/datum/extension/network_device/trade_controller
+ var/obj/effect/overmap/trade_beacon/linked_beacon
+ var/import_tax = 0
+ var/export_tax = 0
+ var/beacon_restrict = 0 // Restriction to ALL beacon functions, import/export/log. 0 = no restricton, 1 = restricted to the network
+ var/log_restrict = 0 // Restriction of the transaction log. 0 = no restricton, 1 = restricted to the network
+ var/access = 0 // Which access will allow for general beacon functions
+ var/log_access = 0 // which access will allow for viewing the transaction log
+
+/datum/extension/network_device/trade_controller/proc/Unlink()
+ if(linked_beacon)
+ linked_beacon.linked_controller = null
+ linked_beacon = null
+/datum/extension/network_device/trade_controller/Destroy()
+ Unlink()
+ . = ..()
+
+/datum/extension/network_device/trade_controller/disconnect(net_down)
+ Unlink()
+ . = ..()
+
+/datum/extension/network_device/trade_controller/proc/toggle_beacon_restrict()
+ if(!linked_beacon)
+ beacon_restrict = 0
+ return
+ beacon_restrict = !beacon_restrict
+ var/datum/computer_network/network = get_network()
+ if(network.trade_controller == src)
+ network.add_log("Trade Control Device toggled beacon restriction for [linked_beacon.name].", network_tag)
+ return 1
+
+
+/datum/extension/network_device/trade_controller/proc/select_beacon_access(var/mob/M, var/obj/machinery/network/trade_controller/machine)
+ var/list/access_choices = list()
+ var/datum/computer_network/network = get_network()
+ var/datum/extension/network_device/acl/D
+ if(network)
+ D = network.access_controller
+ if(!linked_beacon)
+ access = 0
+ return
+ if(!D)
+ to_chat(M, SPAN_WARNING("Cannot restrict access! No access controller detected on network!"))
+ return
+ for(var/parent_group in D.group_dict)
+ access_choices |= parent_group
+ for(var/child_group in D.group_dict[parent_group])
+ access_choices |= child_group
+ access_choices |= "*NONE*"
+ var/chosen = input(M, "Select an Access for the Trade Beacon.", "Beacon Access Selection") as null|anything in access_choices
+ if(chosen && (!machine || machine.CanUseTopic(M)))
+ if(chosen == "*NONE*")
+ access = 0
+ network.add_log("Trade Control Device unset beacon access for [linked_beacon.name].", network_tag)
+ else
+ access = chosen
+ network.add_log("Trade Control Device set beacon access to [chosen] for [linked_beacon.name].", network_tag)
+ return 1
+/datum/extension/network_device/trade_controller/proc/select_import_tax(var/mob/M, var/obj/machinery/network/trade_controller/machine)
+ var/datum/computer_network/network = get_network()
+ if(!linked_beacon)
+ import_tax = 0
+ return
+ var/chosen_tax = input(M, "Enter the import tax% to charge for this beacon. (30% MAX)", "Import Tax") as num|null
+ if(!machine || machine.CanUseTopic(M))
+ import_tax = clamp(chosen_tax, 0, 30)
+ network.add_log("Trade Control Device changed the import tax rate to [import_tax] for [linked_beacon.name].", network_tag)
+ return 1
+
+/datum/extension/network_device/trade_controller/proc/select_export_tax(var/mob/M, var/obj/machinery/network/trade_controller/machine)
+ var/datum/computer_network/network = get_network()
+ if(!linked_beacon)
+ export_tax = 0
+ return
+ var/chosen_tax = input(M, "Enter the export tax% to charge for this beacon. (30% MAX)", "Export Tax") as num|null
+ if(!machine || machine.CanUseTopic(M))
+ export_tax = clamp(chosen_tax, 0, 30)
+ network.add_log("Trade Control Device changed the export tax rate to [export_tax] for [linked_beacon.name].", network_tag)
+ return 1
+
+/datum/extension/network_device/trade_controller/proc/toggle_log_restrict()
+ if(!linked_beacon)
+ log_restrict = 0
+ return
+ log_restrict = !log_restrict
+ var/datum/computer_network/network = get_network()
+ if(network.trade_controller == src)
+ network.add_log("Trade Control Device toggled transaction log restriction for [linked_beacon.name].", network_tag)
+ return 1
\ No newline at end of file
diff --git a/mods/persistence/modules/trade_beacons/exports.dm b/mods/persistence/modules/trade_beacons/exports.dm
new file mode 100644
index 00000000000..7757cf2009e
--- /dev/null
+++ b/mods/persistence/modules/trade_beacons/exports.dm
@@ -0,0 +1,81 @@
+
+/datum/beacon_export
+ var/name = "Beacon Export"
+ var/list/required_items = list() // structure is list(/obj/item/example = 1, /obj/item/multi = 2) list(type = number required).
+ var/cost = 1000
+ var/remaining_stock = 5
+ var/desc = "Export Description"
+ var/obj/checked_closet
+/datum/beacon_export/proc/get_cost(var/tax)
+ if(!tax) return cost
+ return cost-((tax/100)*cost)
+
+/datum/beacon_export/proc/get_tax(var/tax)
+ if(!tax) return 0
+ return (tax/100)*cost
+
+/datum/beacon_export/proc/takeExport()
+ if(checked_closet)
+ checked_closet.forceMove(null)
+ qdel(checked_closet)
+ checked_closet = null
+ remaining_stock--
+/datum/beacon_export/proc/resetExport()
+ checked_closet = null
+
+
+/datum/beacon_export/proc/checkExport(var/turf/L)
+ if(!L) return "No valid location detected"
+ var/obj/closet_found = 0
+ var/valid_items = list()
+ for(var/obj/structure/closet/C in L.contents)
+ closet_found = C
+ for(var/item_type in required_items)
+ var/ind = required_items[item_type]
+ if(!ind) ind = 1
+ var/found = 0
+ for(var/obj/o in C.contents)
+ if(istype(o, item_type))
+ if(isstack(o))
+ var/obj/item/stack/s = o
+ ind -= s.amount
+ if(ind >= 0)
+ valid_items |= o
+ else
+ ind--
+ valid_items |= o
+ if(ind < 0)
+ return "Too many items included ([o.name]). Exact orders only."
+ if(ind == 0)
+ found = 1
+ break
+ if(!found)
+ return "Incomplete Export Detected ([item_type])"
+ break // only check 1, the for loop is just a convient way to search
+ if(!closet_found)
+ return "No container detected"
+ else
+ var/obj/invalid_item = 0
+ for(var/obj/o in closet_found.contents)
+ if(!(o in valid_items))
+ invalid_item = o
+ break
+ if(invalid_item)
+ return "Too many items included ([invalid_item.name]). Exact orders only."
+ checked_closet = closet_found
+ return // SUCCESS
+
+
+/////////////////////////////////////////
+
+/datum/beacon_export/example
+ required_items = list(/obj/item/stack/cable_coil = 10, /obj/item/plunger = 2)
+ name = "Example Export"
+ desc = "This is a debug export to test sending stacked items and multiple single items. It requires a ten stack of cable coil and two plungers to complete."
+ cost = 500
+
+/datum/beacon_export/xanaducrystal
+ required_items = list(/obj/item/plunger = 1)
+ name = "Xanadu Crystal"
+ desc = "The rare Xanadu Crystal is much prized across the Galaxy."
+ cost = 5000
\ No newline at end of file
diff --git a/mods/persistence/modules/trade_beacons/imports.dm b/mods/persistence/modules/trade_beacons/imports.dm
new file mode 100644
index 00000000000..2d7cfcf2eb8
--- /dev/null
+++ b/mods/persistence/modules/trade_beacons/imports.dm
@@ -0,0 +1,53 @@
+/datum/beacon_import
+ var/name = "Beacon Import"
+ var/list/provided_items = list() // structure is list(/obj/item/example = 1, /obj/item/multi = 2) list(type = number provided).
+ var/container_type = /obj/structure/closet/crate
+ var/cost = 1000
+ var/remaining_stock = 5 // how many are left to buy this cycle
+ var/desc = "Import Desription"
+
+/datum/beacon_import/proc/get_cost(var/tax)
+ if(!tax) return cost
+ return cost+((tax/100)*cost)
+
+/datum/beacon_import/proc/get_tax(var/tax)
+ if(!tax) return 0
+ return (tax/100)*cost
+
+/datum/beacon_import/proc/checkImport(var/turf/L)
+ if(!L) return "Invalid telepad."
+ for(var/obj/i in L.contents)
+ if(!istype(i, /obj/machinery)) // machines are ok
+ return "Telepad obstructed"
+/datum/beacon_import/proc/spawnImport(var/turf/L)
+ if(!L) return
+ var/obj/structure/container = new container_type(L)
+ for(var/item_type in provided_items)
+ var/ind = provided_items[item_type]
+ if(!ind) ind = 1
+ var/obj/item/stack/stacking = 0
+ for(var/i=1;i<=ind;i++)
+ if(stacking)
+ stacking.add(1)
+ else
+ var/obj/o = new item_type(container)
+ if(isstack(o))
+ stacking = o
+ remaining_stock--
+
+
+//////////////////////////////////////////////////////////////////
+
+/datum/beacon_import/steel
+ provided_items = list(/obj/item/stack/material/sheet/mapped/steel = 50)
+ name = "Steel (50 Sheets)"
+ cost = 500
+ desc = "A stack of fifty steel sheets. Fifty sheets of grey steel."
+
+
+/datum/beacon_import/example
+ provided_items = list(/obj/item/stack/cable_coil/single = 10, /obj/item/plunger = 2)
+ name = "Example Import"
+ cost = 150
+ desc = "This is a debug import that tests stacks and multiple single items. It sends 10 pieces of cable coil, stacked and 2 plungers."
+
diff --git a/mods/persistence/modules/trade_beacons/trade_beacons.dm b/mods/persistence/modules/trade_beacons/trade_beacons.dm
new file mode 100644
index 00000000000..c159e2e0b7a
--- /dev/null
+++ b/mods/persistence/modules/trade_beacons/trade_beacons.dm
@@ -0,0 +1,113 @@
+/atom/proc/get_adjacent_trade_beacons()
+ var/list/adjacent_trade_beacons = list()
+ var/atom/target = src
+ if(!istype(src, /turf))
+ target = get_turf(src)
+ var/obj/ob = target.get_owning_overmap_object()
+ if(ob)
+ var/turf/T = get_turf(ob)
+ if(T)
+ for(var/obj/beacon in SStrade_beacons.all_trade_beacons)
+ var/turf/beacon_turf = get_turf(beacon)
+ if(beacon_turf && beacon_turf.Adjacent(T))
+ adjacent_trade_beacons |= beacon
+ return adjacent_trade_beacons
+
+/obj/effect/overmap/trade_beacon
+ name = "Trade Beacon"
+ var/list/possible_imports = list() // structure is list(/datum/beacon_import/example = 100) list(type = percentage chance of appearing).
+ var/list/possible_exports = list()
+ var/list/active_imports = list()
+ var/list/active_exports = list()
+ icon = 'icons/obj/supplybeacon.dmi'
+ icon_state = "beacon_active"
+ var/datum/extension/network_device/trade_controller/linked_controller
+ var/datum/money_account/beacon_account
+ var/start_x = 0
+ var/start_y = 0
+
+
+/obj/effect/overmap/trade_beacon/Destroy()
+ if(SStrade_beacons && SStrade_beacons.all_trade_beacons)
+ SStrade_beacons.all_trade_beacons.Remove(src)
+ QDEL_NULL(beacon_account)
+ QDEL_NULL_LIST(active_imports)
+ QDEL_NULL_LIST(active_exports)
+ ..()
+
+
+/obj/effect/overmap/trade_beacon/proc/move_to_starting_location()
+ var/datum/overmap/overmap = global.overmaps_by_name[overmap_id]
+ var/location
+ if(!overmap)
+ return
+ if(start_x && start_y)
+ location = locate(start_x, start_y, overmap.assigned_z)
+ else
+ var/list/candidate_turfs = block(
+ locate(OVERMAP_EDGE, OVERMAP_EDGE, overmap.assigned_z),
+ locate(overmap.map_size_x - OVERMAP_EDGE, overmap.map_size_y - OVERMAP_EDGE, overmap.assigned_z)
+ )
+
+ candidate_turfs = where(candidate_turfs, /proc/can_not_locate, /obj/effect/overmap)
+ location = SAFEPICK(candidate_turfs) || locate(
+ rand(OVERMAP_EDGE, overmap.map_size_x - OVERMAP_EDGE),
+ rand(OVERMAP_EDGE, overmap.map_size_y - OVERMAP_EDGE),
+ overmap.assigned_z
+ )
+
+ forceMove(location)
+
+
+/obj/effect/overmap/trade_beacon/Initialize()
+ beacon_account = new()
+ beacon_account.account_name = name
+ move_to_starting_location()
+ regenerate_imports()
+ regenerate_exports()
+ . = ..()
+
+/obj/effect/overmap/trade_beacon/proc/regenerate_imports()
+ if(!active_imports) active_imports = list()
+ active_imports.Cut()
+ for(var/import in possible_imports)
+ if(!prob(possible_imports[import])) continue
+ active_imports |= new import()
+
+
+
+/obj/effect/overmap/trade_beacon/proc/regenerate_exports()
+ if(!active_exports) active_exports = list()
+ active_exports.Cut()
+ for(var/export in possible_exports)
+ if(!prob(possible_exports[export])) continue
+ active_exports |= new export()
+
+//////////////////////////////// BEACONS
+
+
+/obj/effect/overmap/trade_beacon/test_beacon
+ start_x = 28
+ start_y = 23
+ possible_imports = list(
+ /datum/beacon_import/example = 100,
+ /datum/beacon_import/steel = 100
+ )
+ possible_exports = list(
+ /datum/beacon_export/example = 100,
+ /datum/beacon_export/xanaducrystal = 100
+ )
+ name = "Kleibkhar Debug Trade Beacon"
+
+/obj/effect/overmap/trade_beacon/test_beacon2
+ start_x = 27
+ start_y = 22
+ possible_imports = list(
+ /datum/beacon_import/example = 100,
+ /datum/beacon_import/steel = 100
+ )
+ possible_exports = list(
+ /datum/beacon_export/example = 100,
+ /datum/beacon_export/xanaducrystal = 100
+ )
+ name = "Kleibkhar Debug Trade Beacon Secondus"
diff --git a/nano/templates/trade_beacon.tmpl b/nano/templates/trade_beacon.tmpl
new file mode 100644
index 00000000000..c3a6cb42692
--- /dev/null
+++ b/nano/templates/trade_beacon.tmpl
@@ -0,0 +1,68 @@
+
+
+
{{:data.title}}
+
+
+
+ Linked Network:
+
+
+ {{:data.network}}
{{:helper.link('Disconnect from Network', '', {'disconnect' : 1}, null, null)}}
+
+
+
+
+ Tax Rate:
+
+
+ {{:helper.link( data.tax, '', {'change_tax' : 1}, data.authed ? null : 'disabled', null)}}%
+
+
+
+{{:helper.link('View Imports', 'cart', {'set_screen' : 1}, data.screen == 1 ? 'selected' : null)}}
+{{:helper.link('View Exports', 'calculator', {'set_screen' : 2}, data.screen == 2 ? 'selected' : null)}}
+{{if data.screen == 1}}
+Items Available to Import
+ {{for data.categories}}
+ {{:helper.link(value, null, {'select_category' : value}, data.category == value ? 'selected' : null)}}
+ {{/for}}
+ {{if data.category}}
+
+ {{#def.common_supply_header}}
+ {{for data.possible_purchases}}
+
+ | {{:value.name}}
+ | {{:value.cost}} {{:data.currency}}
+ |
+ {{if data.showing_contents_of && data.showing_contents_of == value.ref}}
+ {{:helper.link('Close', null, {'hide_contents' : 1})}}
+ |
| Contents
+ {{for data.contents_of_order :content :content_index}}
+ |
|---|
+ {{:content.name}} × {{:content.amount}}
+ {{/for}}
+
+ |
{{#def.common_supply_header}}
+ {{else}}
+ {{:helper.link('Contents', null, {'show_contents' : value.ref})}}
+ {{/if}}
+ {{/for}}
+
+ {{/if}}
+
+{{else data.screen == 2}}
+ Items Available to Export
+
+ {{#def.common_supply_header}}
+ {{for data.exports}}
+
+ | {{:value.name}}
+ | {{:value.cost}} {{:data.currency}}
+ |
+ {{/for}}
+ |
+
+{{/if}}
diff --git a/nano/templates/trade_controller.tmpl b/nano/templates/trade_controller.tmpl
new file mode 100644
index 00000000000..4e1c56db602
--- /dev/null
+++ b/nano/templates/trade_controller.tmpl
@@ -0,0 +1,49 @@
+{{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}}
+
+ Welcome to the Trade control device. You may control a nearby trade beacon, adding taxes or restricting access as required.
+
+ {{:helper.link("NETWORK SETTINGS", null, { "settings" : 1 })}}
+
+ {{if !data.connected}}
+ Disconnected from network.
+ {{else}}
+ < | Setting | Toggle | Info
+ | | CONNECTED BEACON:
+ | {{:helper.link(data.connected_beacon, null, { "connect_beacon" : 1 })}}
+ | {{:helper.link('?', null, { "info" : "connect_beacon"})}}
+ {{if data.valid_beacon}}
+
+ |
| RESTRICT BEACON
+ | {{:helper.link(data.beacon_restrict ? 'ON' : 'OFF', null, { "toggle_beacon_restrict" : 1 })}}
+ | {{:helper.link('?', null, { "info" : "restrict_beacon_access"})}}
+ {{if data.beacon_restrict}}
+ |
| BEACON ACCESS
+ | {{:helper.link(data.selected_beacon_access, null, { "select_beacon_access" : 1 })}}
+ | {{:helper.link('?', null, { "info" : "select_beacon_access"})}}
+ {{/if}}
+ |
| IMPORT TARIFF %
+ | {{:helper.link(data.import_tax, null, { "change_import_tax" : 1 })}}
+ | {{:helper.link('?', null, { "info" : "import_tax"})}}
+ |
| EXPORT TARIFF %
+ | {{:helper.link(data.export_tax, null, { "change_export_tax" : 1 })}}
+ | {{:helper.link('?', null, { "info" : "export_tax"})}}
+ |
| RESTRICT TRANSACTION LOG
+ | {{:helper.link(data.log_restrict ? 'ON' : 'OFF', null, { "toggle_log_restrict" : 1 })}}
+ | {{:helper.link('?', null, { "info" : "restrict_log_access"})}}
+ {{if data.log_restrict}}
+ |
| TRANSACTION LOG ACCESS
+ | {{:helper.link(data.selected_log_access, null, { "select_log_access" : 1 })}}
+ | {{:helper.link('?', null, { "info" : "select_log_access"})}}
+ {{/if}}
+ {{/if}}
+ |
+ {{/if}}
+{{/if}}
\ No newline at end of file
diff --git a/nano/templates/trade_management.tmpl b/nano/templates/trade_management.tmpl
new file mode 100644
index 00000000000..36b6057d033
--- /dev/null
+++ b/nano/templates/trade_management.tmpl
@@ -0,0 +1,179 @@
+
+
+ Connected Trade Beacon:
+
+
+ {{:helper.link(data.beacon_name, '', {'select_beacon' : 1 })}}
+
+
+
+ | Telepad
+ {{for data.telepads}}
+ |
+ | {{:helper.link(value.telepad_name, '', {'select_telepad' : value.telepad_ref}, value.selected ? 'selected' : null)}}
+ {{/for}}
+ |
+{{:helper.link('Rescan for Telepads', '', { 'refresh_telepad' : 1 })}}
+
+
+
+ Inventories Reset:
+
+
+ {{:data.update_time}}
+
+
+
+{{if !data.selected_beacon}}
+ No Trade Beacon Connected! You must connect to a trade beacon to use this program.
+{{else}}
+ {{if data.beacon_access}}
+ {{:helper.link('Import', null, { 'import' : 1 }, data.import ? 'selected' : null)}}
+ {{:helper.link('Export', null, { 'export' : 1 }, data.export ? 'selected' : null)}}
+ {{:helper.link('Transaction Log', null, { 'log' : 1 }, data.log ? 'selected' : null)}}
+
+ {{if data.import}}
+ Import Selection
+ {{if data.import_tax}}
+
+ Current Import Tariff %:
+
+
+ {{:data.import_tax}}
+
+
+ The beacon is under control the control of the network '{{:data.controlling_network}}' which is taxing all imports from the beacon by {{:data.import_tax}}%. This increase is automatically included in the prices here.
+
+ {{/if}}
+
+ | Import Pack | Cost$ | Remaining Stock
+ {{for data.imports}}
+ |
|---|
+ | {{:helper.link(value.import_name, '', {'select_import' : value.import_ref})}}
+ | {{:value.import_cost}}
+ | {{:value.import_remaining}}
+ {{/for}}
+ |
+ {{/if}}
+ {{if data.export}}
+ Export Selection
+
+ {{if data.export_tax}}
+
+ Current Export Tariff %:
+
+
+ {{:data.export_tax}}
+
+
+ The beacon is under control the control of the network '{{:data.controlling_network}}' which is taxing all exports from the beacon by {{:data.export_tax}}% This deduction is already included in the payments listed here.
+
+ {{/if}}
+
+ | Export Pack | Payment$ | Remaining Demand
+ {{for data.exports}}
+ |
|---|
+ | {{:helper.link(value.export_name, '', {'select_export' : value.export_ref})}}
+ | {{:value.export_cost}}
+ | {{:value.export_remaining}}
+ {{/for}}
+ |
+ {{/if}}
+
+
+ {{if data.log}}
+ {{if data.log_access}}
+ Transaction Log
+
+ | Transaction | Payment |
+ {{for data.transactions}}
+ |
|---|
+ | {{:value.transaction}}
+ | {{:value.transaction_cost}}
+ {{/for}}
+ |
+ {{else}}
+ The beacon is under control the control of the network '{{:data.controlling_network}}' which has restricted access to the transaction log.
+ {{/if}}
+ {{/if}}
+
+ {{if data.selected_import}}
+ {{:data.selected_import}} | ${{:data.import_cost} | R:{{:data.import.remaining}}
+
+ {{:data.import_desc}}
+
+
+
+ | Purchase Option | Balance$
+ |
|---|
+ {{if data.personal_account}}
+ | {{:helper.link(data.personal_account, '', {'select_personal_account' : 1}, data.personal_account_sel ? 'selected' : null)}}
+ | ${{:data.personal_account_balance}}
+ {{else}}
+ | {{:data.personal_account_error}}
+ | *N/A*
+ {{/if}}
+ |
+ {{if data.charge_stick}}
+ | {{:helper.link('Charge Stick', '', {'select_charge_stick' : 1}, data.charge_stick_sel ? 'selected' : null)}}
+ | ${{:data.charge_stick_balance}}
+ {{else}}
+ | {{:data.charge_stick_error}}
+ | *N/A*
+ {{/if}}
+ |
+ {{if data.network_account}}
+ | {{:helper.link(data.network_account, '', {'select_network_account' : 1}, data.network_account_sel ? 'selected' : null)}}
+ | ${{:data.network_account_balance}}
+ {{else}}
+ | {{:data.network_account_error}}
+ | *N/A*
+ {{/if}}
+ |
+
+ {{:helper.link('Purchase Import', 'cart', { 'buy_import' : 1 })}}
+ {{:helper.link('Back', 'arrowthickstop-1-w', {'import' : 1})}}
+ {{/if}}
+
+
+ {{if data.selected_export}}
+ {{:data.selected_export}} ${{:data.export_cost}}
+
+ {{:data.export_desc}}
+
+
+
+ | Purchase Option | Balance$
+ |
|---|
+ {{if data.personal_account}}
+ | {{:helper.link(data.personal_account, '', {'select_personal_account' : 1}, data.personal_account_sel ? 'selected' : null)}}
+ | ${{:data.personal_account_balance}}
+ {{else}}
+ | {{:data.personal_account_error}}
+ | *N/A*
+ {{/if}}
+ |
+ {{if data.charge_stick}}
+ | {{:helper.link('Charge Stick', '', {'select_charge_stick' : 1}, data.charge_stick_sel ? 'selected' : null)}}
+ | ${{:data.charge_stick_balance}}
+ {{else}}
+ | {{:data.charge_stick_error}}
+ | *N/A*
+ {{/if}}
+ |
+ {{if data.network_account}}
+ | {{:helper.link(data.network_account, '', {'select_network_account' : 1}, data.network_account_sel ? 'selected' : null)}}
+ | ${{:data.network_account_balance}}
+ {{else}}
+ | {{:data.network_account_error}}
+ | *N/A*
+ {{/if}}
+ |
+
+ {{:helper.link('Process Export', 'cart', { 'buy_export' : 1 }, null)}}
+ {{:helper.link('Back', 'arrowthickstop-1-w', {'export' : 1})}}
+ {{/if}}
+ {{else}}
+ The beacon is under control the control of the network '{{:data.controlling_network}}' which has restricted access.
+ {{/if}}
+{{/if}}
\ No newline at end of file
diff --git a/nebula.dme b/nebula.dme
index 44caf023f65..99d4de4e068 100644
--- a/nebula.dme
+++ b/nebula.dme
@@ -1860,6 +1860,7 @@
#include "code\modules\detectivework\tools\sample_kits\fingerprinting.dm"
#include "code\modules\detectivework\tools\sample_kits\swabs.dm"
#include "code\modules\economy\_worth.dm"
+#include "code\modules\economy\cargo_telepads.dm"
#include "code\modules\economy\worth_ammo.dm"
#include "code\modules\economy\worth_cash.dm"
#include "code\modules\economy\worth_clothing.dm"
@@ -2650,6 +2651,7 @@
#include "code\modules\modular_computers\hardware\portable_hard_drive.dm"
#include "code\modules\modular_computers\hardware\processor_unit.dm"
#include "code\modules\modular_computers\hardware\tesla_link.dm"
+#include "code\modules\modular_computers\hardware\trade_controller.dm"
#include "code\modules\modular_computers\hardware\scanners\scanner.dm"
#include "code\modules\modular_computers\hardware\scanners\scanner_atmos.dm"
#include "code\modules\modular_computers\hardware\scanners\scanner_medical.dm"
@@ -2685,6 +2687,7 @@
#include "code\modules\modular_computers\networking\machinery\modem.dm"
#include "code\modules\modular_computers\networking\machinery\relay.dm"
#include "code\modules\modular_computers\networking\machinery\router.dm"
+#include "code\modules\modular_computers\networking\machinery\trade_controller.dm"
#include "code\modules\modular_computers\networking\machinery\wall_relay.dm"
#include "code\modules\modular_computers\networking\machinery\wall_router.dm"
#include "code\modules\modular_computers\networking\NTNRC\conversation.dm"
@@ -3551,5 +3554,9 @@
#include "maps\~mapsystem\maps_unit_testing.dm"
#include "maps\~unit_tests\unit_testing.dm"
#include "mods\_modpack.dm"
+#include "mods\persistence\controllers\subsystems\trade_beacons.dm"
+#include "mods\persistence\modules\trade_beacons\exports.dm"
+#include "mods\persistence\modules\trade_beacons\imports.dm"
+#include "mods\persistence\modules\trade_beacons\trade_beacons.dm"
#include "~code\global_init.dm"
// END_INCLUDE