Files
Will 7c84aaee4b Paicontroller subsystem port (#19165)
* to subsystem

* wip subsystem pain

* some big recoding

* seperated

* oop

* fucking lua

* working on tgui foundation

* compile fixes

* nice and clean

* type

* inviting works

* unit testing

* agony

* test good

* typo

* that should be the basics

* Complete recode of pai saving

* restore fluff text

* migration

* pai notification automation

* swap

* length

* more fixes

* pai sprite datums

* type 13 uses all four directions

* eye handling

* use accessors

* working on icontests

* fix

* hologram eye test

* oop

* wait that doesn't work at all

* text

* eye colors and holo eyes

* lots of fixes

* fix those up

* organized

* belly cap

* that too

* death sprites

* resting eyes

* resting flag

* bad check here

* fixing icon files

* proper file

* garble

* fix some states

* pai death

* collapse pai files into single file

* allow some restricted pai interactions

* lamp

* block invites button

* collapse in remaining vr file

* this is actually needed

* documenting and organizing

* update to TGprefs

* remove gun toggle verb on pai

* forbid default name joins

* pai cable retraction

* various pai fixes

* icon forge pai

* pai eyes

* type 13 iconstate

* directionless

* small fix

* pai dir fix

* missing icon

* no fresh pais

* oops

* fixed pai join

* fixed change chassis

* pai UI

* .

* .

* reuse

* .

* .

* .

* push that

* Horro rmasks

* pai invite data change

* splitting

* use refs

* initial

* with PAI

* soem more

* .

* .

* .

* .

* ugh

* next

* pai invite

* only preview

* that

* use from char pref

* fixed pai invite not working

* small tgui fixes

* remove temp code

* .

* pai comments properly sent

* ooc

* horror

* need to use this for now

* .

* fix

* lol

* raw mob spawn fix

* pai software

* push

---------

Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com>
2026-03-03 23:59:27 +01:00

225 lines
8.0 KiB
Plaintext

#define PAI_DELAY_TIME 1 MINUTE
////////////////////////////////
//// Pai join and management subsystem
////////////////////////////////
SUBSYSTEM_DEF(pai)
name = "Pai"
wait = 4 SECONDS
dependencies = list(
/datum/controller/subsystem/atoms
)
VAR_PRIVATE/list/datum/pai_sprite/pai_chassis_sprites = list()
VAR_PRIVATE/list/current_run = list()
VAR_PRIVATE/list/pai_ghosts = list()
VAR_PRIVATE/list/asked = list()
/datum/controller/subsystem/pai/Initialize()
// Get all software setup
for(var/type in subtypesof(/datum/pai_software))
var/datum/pai_software/P = new type()
GLOB.pai_software_by_key[P.id] = P
if(P.default)
GLOB.default_pai_software[P.id] = P
// Get all valid chassis types
for(var/datum/pai_sprite/sprite as anything in subtypesof(/datum/pai_sprite))
if(!initial(sprite.sprite_icon) || initial(sprite.hidden))
continue
pai_chassis_sprites[initial(sprite.name)] = new sprite()
return SS_INIT_SUCCESS
/datum/controller/subsystem/pai/stat_entry(msg)
msg = "C:[length(pai_ghosts)]"
return ..()
/datum/controller/subsystem/pai/fire(resumed)
if(!resumed)
pai_ghosts.Cut()
current_run = GLOB.observer_mob_list.Copy()
while(length(current_run))
if(MC_TICK_CHECK)
return
var/mob/observer/ghost = current_run[length(current_run)]
current_run.len--
if(!invite_valid(ghost))
continue
// Create candidate
pai_ghosts[REF(ghost)] = WEAKREF(ghost)
/datum/controller/subsystem/pai/proc/get_chassis_list()
RETURN_TYPE(/list/datum/pai_sprite)
SHOULD_NOT_OVERRIDE(TRUE)
return pai_chassis_sprites
/datum/controller/subsystem/pai/proc/chassis_data(id_name)
RETURN_TYPE(/datum/pai_sprite)
SHOULD_NOT_OVERRIDE(TRUE)
if(!(id_name in pai_chassis_sprites))
return pai_chassis_sprites[PAI_DEFAULT_CHASSIS]
return pai_chassis_sprites[id_name]
/datum/controller/subsystem/pai/proc/invite_valid(mob/user)
SHOULD_NOT_OVERRIDE(TRUE)
if(!user.client?.prefs || !user.ckey)
return FALSE
if(!user.MayRespawn())
return FALSE
if(jobban_isbanned(user, "pAI"))
return FALSE
if(!(user.client.prefs.be_special & BE_PAI))
return FALSE
if(check_is_delayed(REF(user)))
return FALSE
if(check_is_already_pai(user.ckey))
return FALSE
if(user.client.prefs.read_preference(/datum/preference/text/pai_name) == PAI_UNSET) // Forbid unset name
return FALSE
return TRUE
/datum/controller/subsystem/pai/proc/check_is_delayed(ghost_ref)
SHOULD_NOT_OVERRIDE(TRUE)
if(ghost_ref in asked)
if(world.time < asked[ghost_ref] + PAI_DELAY_TIME)
return TRUE
return FALSE
/datum/controller/subsystem/pai/proc/check_is_already_pai(check_ckey)
SHOULD_NOT_OVERRIDE(TRUE)
return (check_ckey in GLOB.paikeys)
/datum/controller/subsystem/pai/proc/get_ghost_from_ref(ghost_ref)
RETURN_TYPE(/mob/observer)
SHOULD_NOT_OVERRIDE(TRUE)
PRIVATE_PROC(TRUE)
if(!ghost_ref)
return null
var/datum/weakref/WF = pai_ghosts[ghost_ref]
return WF?.resolve()
/datum/controller/subsystem/pai/proc/get_invite_list_data()
RETURN_TYPE(/list)
SHOULD_NOT_OVERRIDE(TRUE)
var/list/data = list()
for(var/ghost_ref in pai_ghosts)
var/mob/observer/ghost = get_ghost_from_ref(ghost_ref)
if(!istype(ghost) || !ghost.client?.prefs)
continue
var/datum/preferences/pref = ghost.client.prefs
var/datum/asset/spritesheet_batched/pai_icons/spritesheet = get_asset_datum(/datum/asset/spritesheet_batched/pai_icons)
var/chassis = pref.read_preference(/datum/preference/text/pai_chassis)
var/datum/pai_sprite/sprite_datum = SSpai.chassis_data(chassis)
var/css_class = sanitize_css_class_name("[sprite_datum.type]")
UNTYPED_LIST_ADD(data, list(
"ref" = REF(ghost),
"name" = pref.read_preference(/datum/preference/text/pai_name),
"gender" = pref.read_preference(/datum/preference/choiced/gender/biological), // Cannot use identifying yet due to byond limits
"role" = TextPreview(pref.read_preference(/datum/preference/text/pai_role), 152),
"ad" = TextPreview(pref.read_preference(/datum/preference/text/pai_ad), 244),
"eyecolor" = pref.read_preference(/datum/preference/color/pai_eye_color),
"chassis" = chassis,
"emotion" = pref.read_preference(/datum/preference/text/pai_emotion),
"sprite_datum_class" = css_class,
"sprite_datum_size" = spritesheet.icon_size_id(css_class + "S"), // just get the south icon's size, the rest will be the same
))
return data
/datum/controller/subsystem/pai/proc/get_detailed_invite_data(var/ghost_ref)
RETURN_TYPE(/list)
SHOULD_NOT_OVERRIDE(TRUE)
if(!(ghost_ref in pai_ghosts))
return null
var/datum/weakref/WF = pai_ghosts[ghost_ref]
var/mob/observer/ghost = WF?.resolve()
if(!istype(ghost) || !ghost.client?.prefs)
return null
var/datum/preferences/pref = ghost.client.prefs
var/datum/asset/spritesheet_batched/pai_icons/spritesheet = get_asset_datum(/datum/asset/spritesheet_batched/pai_icons)
var/chassis = pref.read_preference(/datum/preference/text/pai_chassis)
var/datum/pai_sprite/sprite_datum = SSpai.chassis_data(chassis)
var/css_class = sanitize_css_class_name("[sprite_datum.type]")
return list(
"ref" = ghost_ref,
"name" = pref.read_preference(/datum/preference/text/pai_name),
"gender" = pref.read_preference(/datum/preference/choiced/gender/biological), // Cannot use identifying yet due to byond limits
// Description
"role" = pref.read_preference(/datum/preference/text/pai_role),
"description" = pref.read_preference(/datum/preference/text/pai_description),
"ad" = pref.read_preference(/datum/preference/text/pai_ad),
"comments" = pref.read_preference(/datum/preference/text/pai_comments),
// Appearance
"eyecolor" = pref.read_preference(/datum/preference/color/pai_eye_color),
"chassis" = chassis,
"emotion" = pref.read_preference(/datum/preference/text/pai_emotion),
// Sprites
"sprite_datum_class" = css_class,
"sprite_datum_size" = spritesheet.icon_size_id(css_class + "S"), // just get the south icon's size, the rest will be the same
)
/datum/controller/subsystem/pai/proc/invite_ghost(mob/inquirer, ghost_ref, obj/item/paicard/card)
SHOULD_NOT_OVERRIDE(TRUE)
// Is our card legal to inhabit?
if(QDELETED(card) || card.pai || card.is_damage_critical())
to_chat(inquirer, span_warning("This [card] can no longer be used to house a pAI."))
return
// Check if the ghost stopped existing
var/mob/observer/ghost = get_ghost_from_ref(ghost_ref)
if(!isobserver(ghost) || !ghost.client)
to_chat(inquirer, span_warning("This pAI has gone offline."))
return
// Time delay if the ghost cancels your invite.
if(check_is_delayed(REF(ghost)))
to_chat(inquirer, span_notice("This pAI is responding to a request, but may become available again shortly..."))
return
asked[REF(ghost)] = world.time
// Can't play, still respawning
var/time_till_respawn = ghost.time_till_respawn()
if(time_till_respawn == -1 || time_till_respawn)
to_chat(inquirer, span_warning("This pAI is still downloading..."))
return
// Send it!
to_chat(inquirer, span_info("A request has been sent!"))
var/client/target = ghost.client
var/response = tgui_alert(target, "[inquirer] is requesting a pAI personality. Would you like to play as a personal AI?", "pAI Request", list("Yes", "No", "Never for this round"))
if(!response || !target || !isobserver(target.mob) || ghost != target.mob)
return // Nice try smartass
if(check_is_already_pai(target.ckey))
to_chat(inquirer, span_warning("This pAI has already been downloaded."))
return
if(QDELETED(card) || card.pai)
to_chat(inquirer, span_warning("This [card] can no longer be used to house a pAI."))
return
switch(response)
if("Yes")
var/new_pai = card.ghost_inhabit(target.mob, TRUE)
to_chat(inquirer, span_info("[new_pai] has accepted your pAI request!"))
return
if("Never for this round")
SSpai.block_pai_invites(REF(ghost))
to_chat(inquirer, span_warning("The pAI denied the request."))
/datum/controller/subsystem/pai/proc/block_pai_invites(ghost_ref)
SHOULD_NOT_OVERRIDE(TRUE)
asked[ghost_ref] = world.time + 99 HOURS // We never want to be asked again
/datum/controller/subsystem/pai/proc/clear_pai_block_delay(ghost_ref)
SHOULD_NOT_OVERRIDE(TRUE)
asked -= ghost_ref
#undef PAI_DELAY_TIME