Files
Bubberstation/code/modules/cargo/orderconsole.dm
John Willard 9ac81e1a64 New station trait job: Human AI (#81681)
## About The Pull Request

This PR does many things, I'll try to explain the basic/background stuff
to the main thing first:

1. Adds a new remote that allows a human to function like an AI. It
controls a fly that will fly around the station slowly, and when it
reaches a machine then the person can interact with it as if they were
an AI. This required changing a lot of silicon/AI checks with one that
also checks for this remote, and some messing with shared ui state.
2. Moves req_access from the obj and bot to ``/atom/movable`` which lets
it be shared between the two, no more copy-paste and one side lacking
features/checks/signals the other has.
3. Adds a check for AI config for AI-related station traits, which was
lacking prior

Now for the good part...
Adds a new station trait that replaces the AI with a Human.
This person is equipped with an AI headset (including Binary), an
advanced camera console, an omni door wand, the machine controller, and
their laws.
They are immune to the SAT's turrets (even if set to target borgs) and
are slow outside of the SAT, mimicing the actions of the AI.

They interact with the world through their advanced camera console,
which allows them to do most AI stuff needed, and the holopad they can
connect to without having to ring first (like Command can).

They are given a paper with the laws they must follow, but since they
are human they are able to bend it. Cyborgs that run the default lawset
are "slaved" to them via an unremovable law 0, so the Human AI can bend
the laws if they really need to (for their own survival n such), and
make the cyborgs obey their commands above laws, but in general this
shouldn't be a frequent occurrence. This does take into account the
unique AI trait, so it's not guaranteed Asimov.

When this station trait rolls, all Intellicards, AI uploads, and AI core
boards are destroyed and are unresearchable. They can be spawned by
admins in-game if necessary. Maybe in the future we can also exclude
Oldstation from this but I haven't really decided.

Extra perks:

Human AI spawns with a Robotic voicebox (unless they are a body purist)
and teleport blocking implant, so they can't use teleporters to bypass
their on-station slowdown.
They also have an infinite laser pointer that can be used to blind
through their camera console. This is unfortunately nerfed from the
recent borg balance PR that removed its stun. This was meant to be the
alternative to no longer being able to permanently lock borgs down like
AIs can (or more than one, for that matter).
They aren't affected by Roburgers, Acid, and Fuel's toxicity.
Bots salute them like they do Beepsky (which is now a trait)
They spawn with SyndEye to replace the AI's tracking ability
They do not have a bank account

### The machine remote

The machine remote has a little fly in it that flies to the machines it
is pointed to, working as the arms and legs of the Human AI. It scans
the machine and punches in the action the AI does, and is how the AI
accesses basically anything. This fly slowly moves from one machine to
the next, and can be recalled with Alt Click.
It works on machines and bots.

### Video (Low quality to fit Github)


https://github.com/tgstation/tgstation/assets/53777086/e16509f8-8bed-42b5-9fbf-7e37165a11e8

## Why It's Good For The Game

I've seen a funny screenshot one day of a person replacing the AI by
using a bunch of door remotes, camera console, crew monitoring console,
and a few other things. I've been thinking about that for a few years
and really wanted to make it official if not easier to make possible,
because it is an incredibly funny interaction.
This makes it a reality, and while they aren't as powerful as regular
AIs, I think it makes for better and funnier in-game moments. With the
same weight as Cargorilla (1), I hope this wouldn't be rolling too often
and ruin rounds, but instead show off the different capabilities that
Humans and AIs can do, to do the job of an AI. You win some you lose
some.

## Changelog

🆑 JohnFulpWillard, Tattax
add: Adds a new station trait job: The Human AI.
/🆑

---------

Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com>
2024-03-09 23:48:39 +01:00

452 lines
16 KiB
Plaintext

/obj/machinery/computer/cargo
name = "supply console"
desc = "Used to order supplies, approve requests, and control the shuttle."
icon_screen = "supply"
circuit = /obj/item/circuitboard/computer/cargo
light_color = COLOR_BRIGHT_ORANGE
///Can the supply console send the shuttle back and forth? Used in the UI backend.
var/can_send = TRUE
///Can this console only send requests?
var/requestonly = FALSE
///Can you approve requests placed for cargo? Works differently between the app and the computer.
var/can_approve_requests = TRUE
var/contraband = FALSE
var/self_paid = FALSE
var/safety_warning = "For safety and ethical reasons, the automated supply shuttle cannot transport live organisms, \
human remains, classified nuclear weaponry, mail, undelivered departmental order crates, syndicate bombs, \
homing beacons, unstable eigenstates, fax machines, or machinery housing any form of artificial intelligence."
var/blockade_warning = "Bluespace instability detected. Shuttle movement impossible."
/// radio used by the console to send messages on supply channel
var/obj/item/radio/headset/radio
/// var that tracks message cooldown
var/message_cooldown
var/list/loaded_coupons
/// var that makes express console use rockets
var/is_express = FALSE
///The name of the shuttle template being used as the cargo shuttle. 'cargo' is default and contains critical code. Don't change this unless you know what you're doing.
var/cargo_shuttle = "cargo"
///The docking port called when returning to the station.
var/docking_home = "cargo_home"
///The docking port called when leaving the station.
var/docking_away = "cargo_away"
///If this console can loan the cargo shuttle. Set to false to disable.
var/stationcargo = TRUE
///The account this console processes and displays. Independent from the account the shuttle processes.
var/cargo_account = ACCOUNT_CAR
///Interface name for the ui_interact call for different subtypes.
var/interface_type = "Cargo"
/obj/machinery/computer/cargo/request
name = "supply request console"
desc = "Used to request supplies from cargo."
icon_screen = "request"
circuit = /obj/item/circuitboard/computer/cargo/request
can_send = FALSE
can_approve_requests = FALSE
requestonly = TRUE
/obj/machinery/computer/cargo/Initialize(mapload)
. = ..()
radio = new /obj/item/radio/headset/headset_cargo(src)
/obj/machinery/computer/cargo/Destroy()
QDEL_NULL(radio)
return ..()
/obj/machinery/computer/cargo/attacked_by(obj/item/I, mob/living/user)
if(istype(I,/obj/item/trade_chip))
var/obj/item/trade_chip/contract = I
contract.try_to_unlock_contract(user)
return TRUE
else
return ..()
/obj/machinery/computer/cargo/emag_act(mob/user, obj/item/card/emag/emag_card)
if(obj_flags & EMAGGED)
return FALSE
if(user)
if (emag_card)
user.visible_message(span_warning("[user] swipes [emag_card] through [src]!"))
to_chat(user, span_notice("You adjust [src]'s routing and receiver spectrum, unlocking special supplies and contraband."))
obj_flags |= EMAGGED
contraband = TRUE
// This also permanently sets this on the circuit board
var/obj/item/circuitboard/computer/cargo/board = circuit
board.contraband = TRUE
board.obj_flags |= EMAGGED
update_static_data(user)
return TRUE
/obj/machinery/computer/cargo/on_construction(mob/user)
. = ..()
circuit.configure_machine(src)
/obj/machinery/computer/cargo/ui_interact(mob/user, datum/tgui/ui)
. = ..()
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, interface_type, name)
ui.open()
/obj/machinery/computer/cargo/ui_data()
var/list/data = list()
data["department"] = "Cargo" // Hardcoded here, for customization in budgetordering.dm AKA NT IRN
data["location"] = SSshuttle.supply.getStatusText()
var/datum/bank_account/D = SSeconomy.get_dep_account(cargo_account)
if(D)
data["points"] = D.account_balance
data["grocery"] = SSshuttle.chef_groceries.len
data["away"] = SSshuttle.supply.getDockedId() == docking_away
data["self_paid"] = self_paid
data["docked"] = SSshuttle.supply.mode == SHUTTLE_IDLE
data["loan"] = !!SSshuttle.shuttle_loan
data["loan_dispatched"] = SSshuttle.shuttle_loan && SSshuttle.shuttle_loan.dispatched
data["can_send"] = can_send
data["can_approve_requests"] = can_approve_requests
var/message = "Remember to stamp and send back the supply manifests."
if(SSshuttle.centcom_message)
message = SSshuttle.centcom_message
if(SSshuttle.supply_blocked)
message = blockade_warning
data["message"] = message
var/list/amount_by_name = list()
var/cart_list = list()
for(var/datum/supply_order/order in SSshuttle.shopping_list)
if(cart_list[order.pack.name])
amount_by_name[order.pack.name] += 1
cart_list[order.pack.name][1]["amount"]++
cart_list[order.pack.name][1]["cost"] += order.get_final_cost()
if(order.department_destination)
cart_list[order.pack.name][1]["dep_order"]++
if(!isnull(order.paying_account))
cart_list[order.pack.name][1]["paid"]++
continue
amount_by_name[order.pack.name] += 1
cart_list[order.pack.name] = list(list(
"cost_type" = order.cost_type,
"object" = order.pack.name,
"cost" = order.get_final_cost(),
"id" = order.id,
"amount" = 1,
"orderer" = order.orderer,
"paid" = !isnull(order.paying_account) ? 1 : 0, //number of orders purchased privatly
"dep_order" = order.department_destination ? 1 : 0, //number of orders purchased by a department
"can_be_cancelled" = order.can_be_cancelled,
))
data["cart"] = list()
for(var/item_id in cart_list)
data["cart"] += cart_list[item_id]
data["requests"] = list()
for(var/datum/supply_order/order in SSshuttle.request_list)
var/datum/supply_pack/pack = order.pack
amount_by_name[pack.name] += 1
data["requests"] += list(list(
"object" = pack.name,
"cost" = pack.get_cost(),
"orderer" = order.orderer,
"reason" = order.reason,
"id" = order.id,
))
data["amount_by_name"] = amount_by_name
return data
/obj/machinery/computer/cargo/ui_static_data(mob/user)
var/list/data = list()
data["max_order"] = CARGO_MAX_ORDER
data["supplies"] = list()
for(var/pack in SSshuttle.supply_packs)
var/datum/supply_pack/P = SSshuttle.supply_packs[pack]
if(!data["supplies"][P.group])
data["supplies"][P.group] = list(
"name" = P.group,
"packs" = list()
)
if((P.hidden && !(obj_flags & EMAGGED)) || (P.contraband && !contraband) || (P.special && !P.special_enabled) || P.drop_pod_only)
continue
data["supplies"][P.group]["packs"] += list(list(
"name" = P.name,
"cost" = P.get_cost(),
"id" = pack,
"desc" = P.desc || P.name, // If there is a description, use it. Otherwise use the pack's name.
"goody" = P.goody,
"access" = P.access,
))
return data
/**
* adds an supply pack to the checkout cart
* * user - the mobe doing this order
* * id - the type of pack to order
* * amount - the amount to order. You may not order more then 10 things at once
*/
/obj/machinery/computer/cargo/proc/add_item(mob/user, id, amount = 1)
if(is_express)
return
id = text2path(id) || id
var/datum/supply_pack/pack = SSshuttle.supply_packs[id]
if(!istype(pack))
CRASH("Unknown supply pack id given by order console ui. ID: [id]")
if(amount > CARGO_MAX_ORDER || amount < 1) // Holy shit fuck off
CRASH("Invalid amount passed into add_item")
if((pack.hidden && !(obj_flags & EMAGGED)) || (pack.contraband && !contraband) || pack.drop_pod_only || (pack.special && !pack.special_enabled))
return
var/name = "*None Provided*"
var/rank = "*None Provided*"
var/ckey = user.ckey
if(ishuman(user))
var/mob/living/carbon/human/human = user
name = human.get_authentification_name()
rank = human.get_assignment(hand_first = TRUE)
else if(HAS_SILICON_ACCESS(user))
name = user.real_name
rank = "Silicon"
var/datum/bank_account/account
if(self_paid && isliving(user))
var/mob/living/living_user = user
var/obj/item/card/id/id_card = living_user.get_idcard(TRUE)
if(!istype(id_card))
say("No ID card detected.")
return
if(IS_DEPARTMENTAL_CARD(id_card))
say("The [src] rejects [id_card].")
return
account = id_card.registered_account
if(!istype(account))
say("Invalid bank account.")
return
var/list/access = id_card.GetAccess()
if(pack.access_view && !(pack.access_view in access))
say("[id_card] lacks the requisite access for this purchase.")
return
// The list we are operating on right now
var/list/working_list = SSshuttle.shopping_list
var/reason = ""
if(requestonly && !self_paid)
working_list = SSshuttle.request_list
reason = tgui_input_text(user, "Reason", name)
if(isnull(reason))
return
if(pack.goody && !self_paid)
playsound(src, 'sound/machines/buzz-sigh.ogg', 50, FALSE)
say("ERROR: Small crates may only be purchased by private accounts.")
return
var/similar_count = SSshuttle.supply.get_order_count(pack)
if(similar_count == OVER_ORDER_LIMIT)
playsound(src, 'sound/machines/buzz-sigh.ogg', 50, FALSE)
say("ERROR: No more then [CARGO_MAX_ORDER] of any pack may be ordered at once")
return
amount = clamp(amount, 1, CARGO_MAX_ORDER - similar_count)
for(var/count in 1 to amount)
var/obj/item/coupon/applied_coupon
for(var/obj/item/coupon/coupon_check in loaded_coupons)
if(pack.type == coupon_check.discounted_pack)
say("Coupon found! [round(coupon_check.discount_pct_off * 100)]% off applied!")
coupon_check.moveToNullspace()
applied_coupon = coupon_check
break
var/datum/supply_order/order = new(
pack = pack ,
orderer = name,
orderer_rank = rank,
orderer_ckey = ckey,
reason = reason,
paying_account = account,
coupon = applied_coupon,
)
working_list += order
if(self_paid)
say("Order processed. The price will be charged to [account.account_holder]'s bank account on delivery.")
if(requestonly && message_cooldown < world.time)
var/message = amount == 1 ? "A new order has been requested." : "[amount] order has been requested."
radio.talk_into(src, message, RADIO_CHANNEL_SUPPLY)
message_cooldown = world.time + 30 SECONDS
. = TRUE
/**
* removes an item from the checkout cart
* * id - the id of the cart item to remove
*/
/obj/machinery/computer/cargo/proc/remove_item(id)
for(var/datum/supply_order/order in SSshuttle.shopping_list)
if(order.id != id)
continue
if(order.department_destination)
say("Only the department that ordered this item may cancel it.")
return FALSE
if(order.applied_coupon)
say("Coupon refunded.")
order.applied_coupon.forceMove(get_turf(src))
SSshuttle.shopping_list -= order
qdel(order)
return TRUE
return FALSE
/**
* maps the ordename displayed on the ui to its supply pack id
* * order_name - the name of the order
*/
/obj/machinery/computer/cargo/proc/name_to_id(order_name)
for(var/pack in SSshuttle.supply_packs)
var/datum/supply_pack/supply = SSshuttle.supply_packs[pack]
if(order_name == supply.name)
return pack
return null
/obj/machinery/computer/cargo/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
switch(action)
if("send")
if(!SSshuttle.supply.canMove())
say(safety_warning)
return
if(SSshuttle.supply_blocked)
say(blockade_warning)
return
if(SSshuttle.supply.getDockedId() == docking_home)
SSshuttle.moveShuttle(cargo_shuttle, docking_away, TRUE)
say("The supply shuttle is departing.")
ui.user.investigate_log("sent the supply shuttle away.", INVESTIGATE_CARGO)
else
//create the paper from the SSshuttle.shopping_list
if(length(SSshuttle.shopping_list))
var/obj/item/paper/requisition_paper = new(get_turf(src))
requisition_paper.name = "requisition form - [station_time_timestamp()]"
var/requisition_text = "<h2>[station_name()] Supply Requisition</h2>"
requisition_text += "<hr/>"
requisition_text += "Time of Order: [station_time_timestamp()]<br/><br/>"
for(var/datum/supply_order/order as anything in SSshuttle.shopping_list)
requisition_text += "<b>[order.pack.name]</b></br>"
requisition_text += "- Order ID: [order.id]</br>"
var/restrictions = SSid_access.get_access_desc(order.pack.access)
if(restrictions)
requisition_text += "- Access Restrictions: [restrictions]</br>"
requisition_text += "- Ordered by: [order.orderer] ([order.orderer_rank])</br>"
var/paying_account = order.paying_account
if(paying_account)
requisition_text += "- Paid Privately by: [order.paying_account.account_holder]<br/>"
var/reason = order.reason
if(reason)
requisition_text += "- Reason Given: [reason]</br>"
requisition_text += "</br></br>"
requisition_paper.add_raw_text(requisition_text)
requisition_paper.color = "#9ef5ff"
requisition_paper.update_appearance()
ui.user.investigate_log("called the supply shuttle.", INVESTIGATE_CARGO)
say("The supply shuttle has been called and will arrive in [SSshuttle.supply.timeLeft(600)] minutes.")
SSshuttle.moveShuttle(cargo_shuttle, docking_home, TRUE)
. = TRUE
if("loan")
if(!SSshuttle.shuttle_loan)
return
if(SSshuttle.supply_blocked)
say(blockade_warning)
return
else if(SSshuttle.supply.mode != SHUTTLE_IDLE)
return
else if(SSshuttle.supply.getDockedId() != docking_away)
return
else if(stationcargo != TRUE)
return
else
SSshuttle.shuttle_loan.loan_shuttle()
say("The supply shuttle has been loaned to CentCom.")
ui.user.investigate_log("accepted a shuttle loan event.", INVESTIGATE_CARGO)
ui.user.log_message("accepted a shuttle loan event.", LOG_GAME)
. = TRUE
if("add")
return add_item(ui.user, params["id"])
if("add_by_name")
var/supply_pack_id = name_to_id(params["order_name"])
if(!supply_pack_id)
return
return add_item(ui.user, supply_pack_id)
if("remove")
var/order_name = params["order_name"]
//try removing atleast one item with the specified name. An order may not be removed if it was from the department
for(var/datum/supply_order/order in SSshuttle.shopping_list)
if(order.pack.name != order_name)
continue
if(remove_item(order.id))
return TRUE
return TRUE
if("modify")
var/order_name = params["order_name"]
//clear out all orders with the above mentioned order_name name to make space for the new amount
for(var/datum/supply_order/order in SSshuttle.shopping_list) //find corresponding order id for the order name
if(order.pack.name == order_name)
remove_item(order.id)
//now add the new amount stuff
var/amount = text2num(params["amount"])
if(amount == 0)
return TRUE
if(amount > CARGO_MAX_ORDER)
return
var/supply_pack_id = name_to_id(order_name) //map order name to supply pack id for adding
if(!supply_pack_id)
return
return add_item(ui.user, supply_pack_id, amount)
if("clear")
//create copy of list else we will get runtimes when iterating & removing items on the same list SSshuttle.shopping_list
var/list/shopping_cart = SSshuttle.shopping_list.Copy()
for(var/datum/supply_order/cancelled_order in shopping_cart)
if(cancelled_order.department_destination || !cancelled_order.can_be_cancelled)
continue //don't cancel other department's orders or orders that can't be cancelled
remove_item(cancelled_order.id) //remove & properly refund any coupons attached with this order
if("approve")
var/id = text2num(params["id"])
for(var/datum/supply_order/SO in SSshuttle.request_list)
if(SO.id == id)
SSshuttle.request_list -= SO
SSshuttle.shopping_list += SO
. = TRUE
break
if("deny")
var/id = text2num(params["id"])
for(var/datum/supply_order/SO in SSshuttle.request_list)
if(SO.id == id)
SSshuttle.request_list -= SO
. = TRUE
break
if("denyall")
SSshuttle.request_list.Cut()
. = TRUE
if("toggleprivate")
self_paid = !self_paid
. = TRUE
if(.)
post_signal(cargo_shuttle)
/obj/machinery/computer/cargo/proc/post_signal(command)
var/datum/radio_frequency/frequency = SSradio.return_frequency(FREQ_STATUS_DISPLAYS)
if(!frequency)
return
var/datum/signal/status_signal = new(list("command" = command))
frequency.post_signal(src, status_signal)