Files
BloopandGitHub 183c5af2e4 Adds flag for virtual areas, fixes being able to send funds from virtualspace to real accounts (#94071)
## About The Pull Request

Fixes https://github.com/tgstation/tgstation/issues/90641
Fixes https://github.com/tgstation/tgstation/issues/88366

Eliminates worries over virtualspace currency being sent to real
accounts.

When I was looking into why there were no flags for bitrunning areas.
Then I saw this mess:

<img width="929" height="889" alt="Code_2we2QjDyFp"
src="https://github.com/user-attachments/assets/8a807bfe-b566-4057-a8ea-2b306325687d"
/>

Not having enough space / being too lazy to refactor this is a silly
reason to not include flags for something like these virtual areas where
it can be quite helpful. Fortunately I am not too lazy ~~in this
moment~~ so here we go:

It was fairly logical to move over some of these to a separate flag,
which I've called `area_flags_mapping` since they pertain to maploading
things and terrain generation mostly. `area_flags` stays reserved for
general properties and now has more room than it did before for you
people to fill it with.

In doing this it's also neatened up the code quit a bit, as UNIQUE_AREA
was kind of everywhere and now that it's implied by default less areas
need to have it defined (or explicitly un-defined).

<details> <summary> Working as intended </summary>

<img width="787" height="448" alt="dreamseeker_p0Qts36tG1"
src="https://github.com/user-attachments/assets/25056f34-8d43-4be2-a293-e53df7a7d1db"
/>

<img width="383" height="59" alt="dreamseeker_Ek7TXCcpbA"
src="https://github.com/user-attachments/assets/89622974-9467-4cdb-8345-d684f7c9004b"
/>

</details>

## Why It's Good For The Game

Fixes an exploit, improves the area flags situation slightly.

## Changelog

🆑
fix: you can no longer send money from virtualspace to a real account
code: adds a flag for virtual areas so they can easily be checked, as
well as an easy helper proc, 'is_area_virtual(your_area)'
/🆑
2025-11-22 12:24:41 -07:00

346 lines
11 KiB
Plaintext

/*****************************Survival Pod********************************/
/area/misc/survivalpod
name = "\improper Emergency Shelter"
icon_state = "away"
static_lighting = TRUE
requires_power = FALSE
default_gravity = STANDARD_GRAVITY
area_flags = BLOBS_ALLOWED
flags_1 = CAN_BE_DIRTY_1
//Survival Capsule
/obj/item/survivalcapsule
name = "bluespace shelter capsule"
desc = "An emergency shelter stored within a pocket of bluespace."
icon_state = "capsule"
icon = 'icons/obj/mining.dmi'
w_class = WEIGHT_CLASS_TINY
///The id we use to fetch the template datum
var/template_id = "shelter_alpha"
///The template datum we use to load the shelter
var/datum/map_template/shelter/template
///If true, this capsule is active and will deploy the area if conditions are met.
var/used = FALSE
///Will this capsule yeet mobs back once the area is deployed?
var/yeet_back = TRUE
/obj/item/survivalcapsule/proc/get_template()
if(template)
return
template = SSmapping.shelter_templates[template_id]
if(!template)
WARNING("Shelter template ([template_id]) not found!")
qdel(src)
/obj/item/survivalcapsule/Destroy()
template = null // without this, capsules would be one use. per round.
. = ..()
/obj/item/survivalcapsule/examine(mob/user)
. = ..()
get_template()
. += "This capsule has the [template.name] stored."
. += template.description
/obj/item/survivalcapsule/interact(mob/living/user)
. = ..()
if(.)
return .
//Can't grab when capsule is New() because templates aren't loaded then
get_template()
if(used)
return FALSE
loc.visible_message(span_warning("[src] begins to shake. Stand back!"))
used = TRUE
addtimer(CALLBACK(src, PROC_REF(expand), user), 5 SECONDS)
if(iscarbon(user))
var/mob/living/carbon/carbon = user
carbon.throw_mode_on(THROW_MODE_TOGGLE)
return TRUE
/// Expands the capsule into a full shelter, placing the template at the item's location (NOT triggerer's location)
/obj/item/survivalcapsule/proc/expand(mob/triggerer)
if(QDELETED(src))
return
var/turf/deploy_location = get_turf(src)
var/status = template.check_deploy(deploy_location, src, get_ignore_flags())
if(status != SHELTER_DEPLOY_ALLOWED)
fail_feedback(status)
used = FALSE
return
if(yeet_back)
yote_nearby(deploy_location)
template.load(deploy_location, centered = TRUE)
trigger_admin_alert(triggerer, deploy_location)
playsound(src, 'sound/effects/phasein.ogg', 100, TRUE)
new /obj/effect/particle_effect/fluid/smoke(get_turf(src))
qdel(src)
/// Returns a bitfield used to ignore some checks in template.check_deploy()
/obj/item/survivalcapsule/proc/get_ignore_flags()
return NONE
///Returns a message including the reason why it couldn't be deployed
/obj/item/survivalcapsule/proc/fail_feedback(status)
switch(status)
if(SHELTER_DEPLOY_BAD_AREA)
loc.visible_message(span_warning("[src] will not function in this area."))
if(SHELTER_DEPLOY_BAD_TURFS, SHELTER_DEPLOY_ANCHORED_OBJECTS, SHELTER_DEPLOY_OUTSIDE_MAP, SHELTER_DEPLOY_BANNED_OBJECTS)
loc.visible_message(span_warning("[src] doesn't have room to deploy! You need to clear a [template.width]x[template.height] area!"))
/// Throws any mobs near the deployed location away from the item / shelter
/// Does some math to make closer mobs get thrown further
/obj/item/survivalcapsule/proc/yote_nearby(turf/deploy_location)
var/width = template.width
var/height = template.height
var/base_x_throw_distance = ceil(width / 2)
var/base_y_throw_distance = ceil(height / 2)
for(var/mob/living/did_not_stand_back in range(loc, "[width]x[height]"))
var/dir_to_center = get_dir(deploy_location, did_not_stand_back) || pick(GLOB.alldirs)
// Aiming to throw the target just enough to get them out of the range of the shelter
// IE: Stronger if they're closer, weaker if they're further away
var/throw_dist = 0
var/x_component = abs(did_not_stand_back.x - deploy_location.x)
var/y_component = abs(did_not_stand_back.y - deploy_location.y)
if(ISDIAGONALDIR(dir_to_center))
throw_dist = ceil(sqrt(base_x_throw_distance ** 2 + base_y_throw_distance ** 2) - (sqrt(x_component ** 2 + y_component ** 2)))
else if(dir_to_center & (NORTH|SOUTH))
throw_dist = base_y_throw_distance - y_component + 1
else if(dir_to_center & (EAST|WEST))
throw_dist = base_x_throw_distance - x_component + 1
did_not_stand_back.Paralyze(3 SECONDS)
did_not_stand_back.Knockdown(6 SECONDS)
did_not_stand_back.throw_at(
target = get_edge_target_turf(did_not_stand_back, dir_to_center),
range = throw_dist,
speed = 3,
force = MOVE_FORCE_VERY_STRONG,
)
/// Logs if the capsule was triggered, by default only if it happened on non-lavaland
/obj/item/survivalcapsule/proc/trigger_admin_alert(mob/triggerer, turf/trigger_loc)
//only report capsules away from the mining/lavaland level
if(is_mining_level(trigger_loc.z))
return
message_admins("[ADMIN_LOOKUPFLW(triggerer)] activated a bluespace capsule away from the mining level! [ADMIN_VERBOSEJMP(trigger_loc)]")
log_admin("[key_name(triggerer)] activated a bluespace capsule away from the mining level at [AREACOORD(trigger_loc)]")
//Non-default pods
/obj/item/survivalcapsule/luxury
name = "luxury bluespace shelter capsule"
desc = "An exorbitantly expensive luxury suite stored within a pocket of bluespace."
template_id = "shelter_beta"
/obj/item/survivalcapsule/luxuryelite
name = "luxury elite bar capsule"
desc = "A luxury bar in a capsule. Bartender required and not included."
template_id = "shelter_charlie"
/obj/item/survivalcapsule/bathroom
name = "emergency relief capsule"
desc = "Provides vital emergency support to employees who are caught short in the field."
template_id = "shelter_toilet"
//Pod objects
//Window
/obj/structure/window/reinforced/shuttle/survival_pod
name = "pod window"
icon = 'icons/obj/smooth_structures/pod_window.dmi'
icon_state = "pod_window-0"
base_icon_state = "pod_window"
smoothing_flags = SMOOTH_BITMASK
smoothing_groups = SMOOTH_GROUP_SHUTTLE_PARTS + SMOOTH_GROUP_SURVIVAL_TITANIUM_POD
canSmoothWith = SMOOTH_GROUP_SURVIVAL_TITANIUM_POD
/obj/structure/window/reinforced/survival_pod
name = "pod window"
icon = 'icons/obj/mining_zones/survival_pod.dmi'
icon_state = "pwindow"
MAPPING_DIRECTIONAL_HELPERS(/obj/structure/window/reinforced/survival_pod/spawner, 0)
//Door
/obj/machinery/door/airlock/survival_pod
name = "Airlock"
icon = 'icons/obj/doors/airlocks/survival/survival.dmi'
overlays_file = 'icons/obj/doors/airlocks/survival/survival_overlays.dmi'
assemblytype = /obj/structure/door_assembly/door_assembly_pod
smoothing_groups = SMOOTH_GROUP_AIRLOCK + SMOOTH_GROUP_SURVIVAL_TITANIUM_POD
/obj/machinery/door/airlock/survival_pod/glass
opacity = FALSE
glass = TRUE
/obj/structure/door_assembly/door_assembly_pod
name = "pod airlock assembly"
icon = 'icons/obj/doors/airlocks/survival/survival.dmi'
base_name = "pod airlock"
overlays_file = 'icons/obj/doors/airlocks/survival/survival_overlays.dmi'
airlock_type = /obj/machinery/door/airlock/survival_pod
glass_type = /obj/machinery/door/airlock/survival_pod/glass
//Windoor
/obj/machinery/door/window/survival_pod
icon = 'icons/obj/mining_zones/survival_pod.dmi'
icon_state = "windoor"
base_state = "windoor"
MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/door/window/survival_pod/left, 0)
//Table
/obj/structure/table/survival_pod
icon = 'icons/obj/mining_zones/survival_pod.dmi'
icon_state = "table"
smoothing_flags = NONE
smoothing_groups = null
canSmoothWith = null
can_flip = FALSE
//Sleeper
/obj/machinery/sleeper/survival_pod
icon = 'icons/obj/mining_zones/survival_pod.dmi'
icon_state = "sleeper"
base_icon_state = "sleeper"
/obj/machinery/sleeper/survival_pod/update_overlays()
. = ..()
if(!state_open)
. += "sleeper_cover"
//Lifeform Stasis Unit
/obj/machinery/stasis/survival_pod
icon = 'icons/obj/mining_zones/survival_pod.dmi'
buckle_lying = 270
//Computer
/obj/item/gps/computer
name = "pod computer"
icon = 'icons/obj/mining_zones/pod_computer.dmi'
icon_state = "pod_computer"
anchored = TRUE
density = TRUE
pixel_y = -32
/obj/item/gps/computer/wrench_act(mob/living/user, obj/item/I)
..()
user.visible_message(span_warning("[user] disassembles [src]."),
span_notice("You start to disassemble [src]..."), span_hear("You hear clanking and banging noises."))
if(I.use_tool(src, user, 20, volume=50))
new /obj/item/gps(loc)
qdel(src)
return TRUE
/obj/item/gps/computer/attack_hand(mob/user, list/modifiers)
. = ..()
if(.)
return
attack_self(user)
//Bed
/obj/structure/bed/pod
icon = 'icons/obj/mining_zones/survival_pod.dmi'
icon_state = "bed"
/obj/structure/bed/double/pod
icon = 'icons/obj/mining_zones/survival_pod.dmi'
icon_state = "bed_double"
//Survival Storage Unit
/obj/machinery/smartfridge/survival_pod
name = "survival pod storage"
desc = "A heated storage unit."
icon_state = "donkvendor"
icon = 'icons/obj/mining_zones/donkvendor.dmi'
base_build_path = /obj/machinery/smartfridge/survival_pod
light_range = 5
light_power = 1.2
light_color = COLOR_VERY_PALE_LIME_GREEN
max_n_of_items = 10
pixel_y = -4
/obj/machinery/smartfridge/survival_pod/welder_act(mob/living/user, obj/item/tool)
return NONE
/obj/machinery/smartfridge/survival_pod/wrench_act(mob/living/user, obj/item/tool)
return NONE
/obj/machinery/smartfridge/survival_pod/screwdriver_act(mob/living/user, obj/item/tool)
return NONE
/obj/machinery/smartfridge/survival_pod/crowbar_act(mob/living/user, obj/item/tool)
return NONE
/obj/machinery/smartfridge/survival_pod/Initialize(mapload)
AddElement(/datum/element/update_icon_blocker)
return ..()
/obj/machinery/smartfridge/survival_pod/preloaded/Initialize(mapload)
. = ..()
for(var/i in 1 to 5)
var/obj/item/food/donkpocket/warm/W = new(src)
load(W)
if(prob(50))
var/obj/item/storage/dice/D = new(src)
load(D)
else
var/obj/item/instrument/guitar/G = new(src)
load(G)
/obj/machinery/smartfridge/survival_pod/accept_check(obj/item/O)
return isitem(O)
//Fluff
/obj/structure/tubes
icon_state = "tubes"
icon = 'icons/obj/mining_zones/survival_pod.dmi'
name = "tubes"
anchored = TRUE
layer = BELOW_MOB_LAYER
density = FALSE
/obj/item/fakeartefact
name = "expensive forgery"
icon = 'icons/hud/screen_gen.dmi'
icon_state = "x2"
var/static/possible = list(
/obj/item/ship_in_a_bottle,
/obj/item/gun/energy/pulse,
/obj/item/book/granter/martial/carp,
/obj/item/melee/supermatter_sword,
/obj/item/shield/changeling,
/obj/item/lava_staff,
/obj/item/energy_katana,
/obj/item/hierophant_club,
/obj/item/his_grace,
/obj/item/gun/energy/minigun,
/obj/item/gun/ballistic/automatic/l6_saw,
/obj/item/gun/magic/staff/chaos,
/obj/item/gun/magic/staff/spellblade,
/obj/item/gun/magic/wand/death,
/obj/item/gun/magic/wand/fireball,
/obj/item/stack/telecrystal/twenty,
/obj/item/nuke_core,
/obj/item/banhammer,
)
/obj/item/fakeartefact/Initialize(mapload)
. = ..()
var/obj/item/I = pick(possible)
name = initial(I.name)
icon = initial(I.icon)
desc = initial(I.desc)
icon_state = initial(I.icon_state)
inhand_icon_state = initial(I.inhand_icon_state)
lefthand_file = initial(I.lefthand_file)
righthand_file = initial(I.righthand_file)
cut_overlays() //to get rid of the big blue x