Files
Leland Kemble dec8c784f6 Bitrunners can't leave domains while glitches remain in them (#96171)
## About The Pull Request

When bitrunners attempt to leave their domain via the ladder, they will
fail if there is an active, living bitrunning glitch antagonist within
the domain. Other methods of leaving such as death or taking the red
pill are unaffected.

they can also leave if the crate's delivered

sorta a sister PR to #95934

## Why It's Good For The Game

enter domain -> get alert about bitrunning glitch -> leave -> turn off
domain
What a fun gameplay loop, for everyone involved.
You'll fight the glitch and you'll like it. (liking it
[pending](https://github.com/tgstation/tgstation/pull/96138))

You can still leave if you REALLY, REALLY need to at the cost of a
little brain damage, but otherwise the literal only obstacle that will
ever be in your path should be something you have to engage with.

## Changelog
🆑

balance: Bitrunners can't back out of domains via the ladder while
bitrunning glitches remain in them

/🆑
2026-06-01 16:23:46 -04:00

248 lines
8.1 KiB
Plaintext

#define POLLING_COOLDOWN_TIME 2 MINUTES
/// Gives all current occupants a notification that the server is going down
/obj/machinery/quantum_server/proc/begin_shutdown(mob/user)
if(isnull(generated_domain))
return
if(!length(avatar_connection_refs))
balloon_alert_to_viewers("powering down domain...")
playsound(src, 'sound/machines/terminal/terminal_off.ogg', 40, vary = TRUE)
reset()
return
balloon_alert_to_viewers("notifying clients...")
playsound(src, 'sound/machines/terminal/terminal_alert.ogg', 100, vary = TRUE)
user.visible_message(
span_danger("[user] begins depowering the server!"),
span_notice("You start disconnecting clients..."),
span_danger("You hear frantic keying on a keyboard."),
)
SEND_SIGNAL(src, COMSIG_BITRUNNER_SHUTDOWN_ALERT, user)
if(!do_after(user, 20 SECONDS, src))
return
reset()
/// Links all the loading processes together - does validation for booting a map
/obj/machinery/quantum_server/proc/cold_boot_map(map_key, was_random_selection)
if(!is_ready)
return FALSE
if(isnull(map_key))
balloon_alert_to_viewers("no domain specified!")
return FALSE
if(generated_domain)
balloon_alert_to_viewers("stop the current domain first!")
return FALSE
if(length(avatar_connection_refs))
balloon_alert_to_viewers("all clients must disconnect!")
return FALSE
is_ready = FALSE
playsound(src, 'sound/machines/terminal/terminal_processing.ogg', 30, 2)
/// If any one of these fail, it reverts the entire process
if(!load_domain(map_key) || !load_map_items() || !load_mob_segments())
balloon_alert_to_viewers("initialization failed!")
scrub_vdom()
is_ready = TRUE
return FALSE
//We will want to record how the domain was selected. Either entirely randomly, with the name redacted, or with full information.
//Without this, it is difficult to determine what domains are selected more often intentionallly, vs unintentionally.
var/selection_type
if(was_random_selection)
selection_type = "random selection"
else
if(generated_domain.can_view_name(scanner_tier, points))
selection_type = "full information"
else
selection_type = "redacted information"
SSblackbox.record_feedback("nested tally", "bitrunning_domain_loaded", 1, list(selection_type, map_key))
is_ready = TRUE
var/spawn_chance = clamp((threat * glitch_chance), 5, threat_prob_max)
if(prob(spawn_chance))
setup_glitch()
playsound(src, 'sound/machines/terminal/terminal_insert_disc.ogg', 30, vary = TRUE)
balloon_alert_to_viewers("domain loaded.")
generated_domain.start_time = world.time
points -= generated_domain.cost
update_use_power(ACTIVE_POWER_USE)
update_appearance()
if(broadcasting)
start_broadcasting_network(BITRUNNER_CAMERA_NET)
if(generated_domain.announce_to_ghosts)
notify_ghosts("Bitrunners have loaded a domain that offers ghost interactions. Check the spawners menu for more information.",
src,
"Matrix Glitch",
)
return TRUE
/// Initializes a new domain if the given key is valid and the user has enough points
/obj/machinery/quantum_server/proc/load_domain(map_key)
for(var/datum/lazy_template/virtual_domain/available in SSbitrunning.all_domains)
if(map_key == available.key && points >= available.cost)
generated_domain = available
break
if(!generated_domain)
return FALSE
if(generated_domain.mission_min_candidates && (!COOLDOWN_FINISHED(src, polling_cooldown)))
say("Advanced NPC algorithms resetting, please wait [DisplayTimeText(polling_cooldown)] or load a different domain.")
playsound(src, "sound/machines/buzz-[pick("sigh", "two")].ogg", 50, TRUE)
return FALSE
var/list/mob/lucky_ghosts
if(generated_domain.mission_min_candidates)
playsound(src, 'sound/machines/chime.ogg', 50, TRUE)
say("Loading advanced NPCs...")
var/list/mob/candidates = SSpolling.poll_ghost_candidates("Do you want to play as a virtual [generated_domain.spawner_role] in a bitrunner domain?", ROLE_GHOST_ROLE, ROLE_GHOST_ROLE, 15 SECONDS, POLL_IGNORE_SHUTTLE_DENIZENS, TRUE)
for(var/amount in 1 to generated_domain.mission_max_candidates)
if(length(candidates)) // If no candidates, fails in code below anyways
LAZYADD(lucky_ghosts, pick_n_take(candidates))
if(length(lucky_ghosts) < generated_domain.mission_min_candidates)
notify_ghosts("Not enough candidates for [generated_domain.spawner_role]! Aborting mission!")
playsound(src, "sound/machines/buzz-[pick("sigh", "two")].ogg", 50, TRUE)
say("Error! Unable to load advanced NPCs. Please try again or select different domain.")
COOLDOWN_START(src, polling_cooldown, POLLING_COOLDOWN_TIME)
return FALSE
playsound(src, 'sound/machines/ping.ogg', 50, TRUE)
say("Success!")
generated_domain.load_advanced_npcs(lucky_ghosts)
RegisterSignal(generated_domain, COMSIG_LAZY_TEMPLATE_LOADED, PROC_REF(on_template_loaded))
generated_domain.lazy_load()
return TRUE
/// Loads in necessary map items like hololadder spawns, caches, etc
/obj/machinery/quantum_server/proc/load_map_items()
var/turf/goal_turfs = list()
var/turf/cache_turfs = list()
var/turf/curiosity_turfs = list()
for(var/obj/effect/landmark/bitrunning/thing in GLOB.landmarks_list)
if(istype(thing, /obj/effect/landmark/bitrunning/hololadder_spawn))
exit_turfs += get_turf(thing)
qdel(thing) // i'm worried about multiple servers getting confused so lets clean em up
continue
if(istype(thing, /obj/effect/landmark/bitrunning/cache_goal_turf))
var/turf/tile = get_turf(thing)
goal_turfs += tile
RegisterSignal(tile, COMSIG_ATOM_ENTERED, PROC_REF(on_goal_turf_entered))
RegisterSignal(tile, COMSIG_ATOM_EXAMINE, PROC_REF(on_goal_turf_examined))
qdel(thing)
continue
if(istype(thing, /obj/effect/landmark/bitrunning/cache_spawn))
cache_turfs += get_turf(thing)
qdel(thing)
continue
if(istype(thing, /obj/effect/mob_spawn))
generated_domain.ghost_spawners += thing
continue
if(istype(thing, /obj/effect/landmark/bitrunning/curiosity_spawn))
curiosity_turfs += get_turf(thing)
qdel(thing)
continue
if(istype(thing, /obj/effect/landmark/bitrunning/loot_signal))
generated_domain.main_crate_loc = thing.loc
qdel(thing)
continue
if(istype(thing, /obj/effect/landmark/bitrunning/permanent_exit))
var/turf/tile = get_turf(thing)
exit_turfs += tile
qdel(thing)
new /obj/structure/hololadder(tile)
if(!length(exit_turfs))
CRASH("Failed to find exit turfs on generated domain.")
if(!length(goal_turfs))
CRASH("Failed to find send turfs on generated domain.")
if(!attempt_spawn_cache(cache_turfs))
return FALSE
while(length(curiosity_turfs))
var/turf/picked_turf = attempt_spawn_curiosity(curiosity_turfs)
if(!picked_turf)
break
generated_domain.secondary_loot_generated += 1
curiosity_turfs -= picked_turf
return TRUE
/// Stops the current virtual domain and disconnects all users
/obj/machinery/quantum_server/proc/reset(fast = FALSE)
is_ready = FALSE
domain_complete = FALSE
sever_connections()
if(!fast)
notify_spawned_threats()
addtimer(CALLBACK(src, PROC_REF(scrub_vdom)), 15 SECONDS, TIMER_UNIQUE|TIMER_STOPPABLE)
else
scrub_vdom() // used in unit testing, no need to wait for callbacks
addtimer(CALLBACK(src, PROC_REF(cool_off)), ROUND_UP(server_cooldown_time * capacitor_coefficient), TIMER_UNIQUE|TIMER_STOPPABLE|TIMER_DELETE_ME)
update_appearance()
update_use_power(IDLE_POWER_USE)
domain_randomized = FALSE
retries_spent = 0
stop_broadcasting_network(BITRUNNER_CAMERA_NET)
/// Tries to clean up everything in the domain
/obj/machinery/quantum_server/proc/scrub_vdom()
sever_connections() /// just in case someone's connected
SEND_SIGNAL(src, COMSIG_BITRUNNER_DOMAIN_SCRUBBED) // avatar cleanup just in case
if(length(generated_domain.reservations))
var/datum/turf_reservation/res = generated_domain.reservations[1]
res.Release()
var/list/creatures = spawned_threat_refs + mutation_candidate_refs
for(var/datum/weakref/creature_ref as anything in creatures)
var/mob/living/creature = creature_ref?.resolve()
if(isnull(creature))
continue
qdel(creature)
generated_domain.secondary_loot_generated = 0
avatar_connection_refs.Cut()
exit_turfs = list()
generated_domain = null
mutation_candidate_refs.Cut()
spawned_threat_refs.Cut()
#undef POLLING_COOLDOWN_TIME