Files
Bubberstation/code/controllers/subsystem/processing/station.dm
SkyratBot 031e483e4d [MIRROR] New station trait job: Human AI (#26823)
* New station trait job: Human AI (#81681)

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 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.

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

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.

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

---------

Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com>

* Oh right

* so this works

* whoooops

---------

Co-authored-by: John Willard <53777086+JohnFulpWillard@users.noreply.github.com>
Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com>
Co-authored-by: Useroth <37159550+Useroth@users.noreply.github.com>
Co-authored-by: Pinta <68373373+softcerv@users.noreply.github.com>
2024-03-31 22:39:22 -04:00

179 lines
7.4 KiB
Plaintext

PROCESSING_SUBSYSTEM_DEF(station)
name = "Station"
init_order = INIT_ORDER_STATION
flags = SS_BACKGROUND
runlevels = RUNLEVEL_GAME
wait = 5 SECONDS
///A list of currently active station traits
var/list/station_traits = list()
///Assoc list of trait type || assoc list of traits with weighted value. Used for picking traits from a specific category.
var/list/selectable_traits_by_types = list(STATION_TRAIT_POSITIVE = list(), STATION_TRAIT_NEUTRAL = list(), STATION_TRAIT_NEGATIVE = list())
///Currently active announcer. Starts as a type but gets initialized after traits are selected
var/datum/centcom_announcer/announcer = /datum/centcom_announcer/default
///A list of trait roles that should be protected from antag
var/list/antag_protected_roles = list()
///A list of trait roles that should never be able to roll antag
var/list/antag_restricted_roles = list()
/// Assosciative list of station goal type -> goal instance
var/list/datum/station_goal/goals_by_type = list()
/datum/controller/subsystem/processing/station/Initialize()
//If doing unit tests we don't do none of that trait shit ya know?
// Autowiki also wants consistent outputs, for example making sure the vending machine page always reports the normal products
#if !defined(UNIT_TESTS) && !defined(AUTOWIKI)
SetupTraits()
//display_lobby_traits() SKYRAT EDIT REMOVAL
#endif
announcer = new announcer() //Initialize the station's announcer datum
SSparallax.post_station_setup() //Apply station effects that parallax might have
return SS_INIT_SUCCESS
/datum/controller/subsystem/processing/station/Recover()
station_traits = SSstation.station_traits
selectable_traits_by_types = SSstation.selectable_traits_by_types
announcer = SSstation.announcer
antag_protected_roles = SSstation.antag_protected_roles
antag_restricted_roles = SSstation.antag_restricted_roles
goals_by_type = SSstation.goals_by_type
..()
/// This gets called by SSdynamic during initial gamemode setup.
/// This is done because for a greenshift we want all goals to be generated
/datum/controller/subsystem/processing/station/proc/generate_station_goals(goal_budget)
var/list/possible = subtypesof(/datum/station_goal)
var/goal_weights = 0
var/chosen_goals = list()
var/is_planetary = SSmapping.is_planetary()
while(possible.len && goal_weights < goal_budget)
var/datum/station_goal/picked = pick_n_take(possible)
if(picked::requires_space && is_planetary)
continue
goal_weights += initial(picked.weight)
chosen_goals += picked
for(var/chosen in chosen_goals)
new chosen()
/// Returns all station goals that are currently active
/datum/controller/subsystem/processing/station/proc/get_station_goals()
var/list/goals = list()
for(var/goal_type in goals_by_type)
goals += goals_by_type[goal_type]
return goals
/// Returns a specific station goal by type
/datum/controller/subsystem/processing/station/proc/get_station_goal(goal_type)
return goals_by_type[goal_type]
///Rolls for the amount of traits and adds them to the traits list
/datum/controller/subsystem/processing/station/proc/SetupTraits()
if (CONFIG_GET(flag/forbid_station_traits))
return
if (fexists(FUTURE_STATION_TRAITS_FILE))
var/forced_traits_contents = file2text(FUTURE_STATION_TRAITS_FILE)
fdel(FUTURE_STATION_TRAITS_FILE)
var/list/forced_traits_text_paths = json_decode(forced_traits_contents)
forced_traits_text_paths = SANITIZE_LIST(forced_traits_text_paths)
for (var/trait_text_path in forced_traits_text_paths)
var/station_trait_path = text2path(trait_text_path)
if (!ispath(station_trait_path, /datum/station_trait) || station_trait_path == /datum/station_trait)
var/message = "Invalid station trait path [station_trait_path] was requested in the future station traits!"
log_game(message)
message_admins(message)
continue
setup_trait(station_trait_path)
return
for(var/i in subtypesof(/datum/station_trait))
var/datum/station_trait/trait_typepath = i
// If forced, (probably debugging), just set it up now, keep it out of the pool.
if(initial(trait_typepath.force))
setup_trait(trait_typepath)
continue
if(initial(trait_typepath.abstract_type) == trait_typepath)
continue //Dont add abstract ones to it
if(!(initial(trait_typepath.trait_flags) & STATION_TRAIT_PLANETARY) && SSmapping.is_planetary()) // we're on a planet but we can't do planet ;_;
continue
if(!(initial(trait_typepath.trait_flags) & STATION_TRAIT_SPACE_BOUND) && !SSmapping.is_planetary()) //we're in space but we can't do space ;_;
continue
if(!(initial(trait_typepath.trait_flags) & STATION_TRAIT_REQUIRES_AI) && !CONFIG_GET(flag/allow_ai)) //can't have AI traits without AI
continue
selectable_traits_by_types[initial(trait_typepath.trait_type)][trait_typepath] = initial(trait_typepath.weight)
var/positive_trait_budget = text2num(pick_weight(CONFIG_GET(keyed_list/positive_station_traits)))
var/neutral_trait_budget = text2num(pick_weight(CONFIG_GET(keyed_list/neutral_station_traits)))
var/negative_trait_budget = text2num(pick_weight(CONFIG_GET(keyed_list/negative_station_traits)))
#ifdef MAP_TEST
positive_trait_budget = 0
neutral_trait_budget = 0
negative_trait_budget = 0
#endif
pick_traits(STATION_TRAIT_POSITIVE, positive_trait_budget)
pick_traits(STATION_TRAIT_NEUTRAL, neutral_trait_budget)
pick_traits(STATION_TRAIT_NEGATIVE, negative_trait_budget)
/**
* Picks traits of a specific category (e.g. bad or good), initializes them, adds them to the list of traits,
* then removes them from possible traits as to not roll twice and subtracts their cost from the budget.
* All until the whole budget is spent or no more traits can be picked with it.
*/
/datum/controller/subsystem/processing/station/proc/pick_traits(trait_sign, budget)
if(!budget)
return
///A list of traits of the same trait sign
var/list/selectable_traits = selectable_traits_by_types[trait_sign]
while(budget)
///Remove any station trait with a cost bigger than the budget
for(var/datum/station_trait/proto_trait as anything in selectable_traits)
if(initial(proto_trait.cost) > budget)
selectable_traits -= proto_trait
///We have spare budget but no trait that can be bought with what's left of it
if(!length(selectable_traits))
return
//Rolls from the table for the specific trait type
var/datum/station_trait/trait_type = pick_weight(selectable_traits)
selectable_traits -= trait_type
budget -= initial(trait_type.cost)
setup_trait(trait_type)
///Creates a given trait of a specific type, while also removing any blacklisted ones from the future pool.
/datum/controller/subsystem/processing/station/proc/setup_trait(datum/station_trait/trait_type)
var/datum/station_trait/trait_instance = new trait_type()
station_traits += trait_instance
log_game("Station Trait: [trait_instance.name] chosen for this round.")
if(!trait_instance.blacklist)
return
for(var/i in trait_instance.blacklist)
var/datum/station_trait/trait_to_remove = i
selectable_traits_by_types[initial(trait_to_remove.trait_type)] -= trait_to_remove
/* SKYRAT EDIT REMOVAL
/// Update station trait lobby buttons for clients who joined before we initialised this subsystem
/datum/controller/subsystem/processing/station/proc/display_lobby_traits()
for (var/mob/dead/new_player/player as anything in GLOB.new_player_list)
var/datum/hud/new_player/observer_hud = player.hud_used
if (!istype(observer_hud))
continue
observer_hud.add_station_trait_buttons()
observer_hud.show_hud(observer_hud.hud_version)
*/