From cbba031a7d70e81e948fbb4a9ca67ac3d97cb97e Mon Sep 17 00:00:00 2001 From: CHOMPStation2 <58959929+CHOMPStation2@users.noreply.github.com> Date: Sun, 11 Feb 2024 13:52:51 -0700 Subject: [PATCH] [MIRROR] Size Toy Fun (#7727) Co-authored-by: Heroman3003 <31296024+Heroman3003@users.noreply.github.com> Co-authored-by: CHOMPStation2 --- code/__defines/_compile_options.dm | 84 +- code/game/machinery/doors/firedoor.dm | 1050 ++++++++--------- .../under/accessories/accessory_vr.dm | 153 ++- code/modules/research/message_server.dm | 910 +++++++------- code/modules/vore/resizing/sizegun_slow_vr.dm | 55 +- code/modules/vore/resizing/sizegun_vr.dm | 9 + code/modules/xgm/xgm_gas_mixture.dm | 980 +++++++-------- icons/inventory/accessory/item_vr.dmi | Bin 19538 -> 19657 bytes 8 files changed, 1720 insertions(+), 1521 deletions(-) diff --git a/code/__defines/_compile_options.dm b/code/__defines/_compile_options.dm index 50ff3fbf2b..cf266542c2 100644 --- a/code/__defines/_compile_options.dm +++ b/code/__defines/_compile_options.dm @@ -1,42 +1,42 @@ -#define BACKGROUND_ENABLED 0 // The default value for all uses of set background. Set background can cause gradual lag and is recommended you only turn this on if necessary. - // 1 will enable set background. 0 will disable set background. - -#define PRELOAD_RSC 1 /*set to: - 0 to allow using external resources or on-demand behaviour; - 1 to use the default behaviour (preload compiled in recourses, not player uploaded ones); - 2 for preloading absolutely everything; - */ - -// ZAS Compile Options -//#define ZASDBG // Uncomment to turn on super detailed ZAS debugging that probably won't even compile. -#define MULTIZAS // Uncomment to turn on Multi-Z ZAS Support! - -// Movement Compile Options -//#define CARDINAL_INPUT_ONLY // Uncomment to disable diagonal player movement (restore previous cardinal-moves-only behavior) - -// Comment/Uncomment this to turn off/on shuttle code debugging logs -#define DEBUG_SHUTTLES - -// If we are doing the map test build, do not include the main maps, only the submaps. -#if MAP_TEST - #define USING_MAP_DATUM /datum/map - #define MAP_OVERRIDE 1 -#endif - -///Used to find the sources of harddels, quite laggy, don't be surpised if it freezes your client for a good while -//#define REFERENCE_TRACKING -#ifdef REFERENCE_TRACKING - -///Should we be logging our findings or not -#define REFERENCE_TRACKING_LOG - -///Used for doing dry runs of the reference finder, to test for feature completeness -//#define REFERENCE_TRACKING_DEBUG - -///Run a lookup on things hard deleting by default. -//#define GC_FAILURE_HARD_LOOKUP -#ifdef GC_FAILURE_HARD_LOOKUP -#define FIND_REF_NO_CHECK_TICK -#endif //ifdef GC_FAILURE_HARD_LOOKUP - -#endif //ifdef REFERENCE_TRACKING +#define BACKGROUND_ENABLED 0 // The default value for all uses of set background. Set background can cause gradual lag and is recommended you only turn this on if necessary. + // 1 will enable set background. 0 will disable set background. + +#define PRELOAD_RSC 1 /*set to: + 0 to allow using external resources or on-demand behaviour; + 1 to use the default behaviour (preload compiled in recourses, not player uploaded ones); + 2 for preloading absolutely everything; + */ + +// ZAS Compile Options +//#define ZASDBG // Uncomment to turn on super detailed ZAS debugging that probably won't even compile. +#define MULTIZAS // Uncomment to turn on Multi-Z ZAS Support! + +// Movement Compile Options +//#define CARDINAL_INPUT_ONLY // Uncomment to disable diagonal player movement (restore previous cardinal-moves-only behavior) + +// Comment/Uncomment this to turn off/on shuttle code debugging logs +#define DEBUG_SHUTTLES + +// If we are doing the map test build, do not include the main maps, only the submaps. +#if MAP_TEST + #define USING_MAP_DATUM /datum/map + #define MAP_OVERRIDE 1 +#endif + +///Used to find the sources of harddels, quite laggy, don't be surpised if it freezes your client for a good while +//#define REFERENCE_TRACKING +#ifdef REFERENCE_TRACKING + +///Should we be logging our findings or not +#define REFERENCE_TRACKING_LOG + +///Used for doing dry runs of the reference finder, to test for feature completeness +//#define REFERENCE_TRACKING_DEBUG + +///Run a lookup on things hard deleting by default. +//#define GC_FAILURE_HARD_LOOKUP +#ifdef GC_FAILURE_HARD_LOOKUP +#define FIND_REF_NO_CHECK_TICK +#endif //ifdef GC_FAILURE_HARD_LOOKUP + +#endif //ifdef REFERENCE_TRACKING diff --git a/code/game/machinery/doors/firedoor.dm b/code/game/machinery/doors/firedoor.dm index 140ad4ef16..1bd54feb97 100644 --- a/code/game/machinery/doors/firedoor.dm +++ b/code/game/machinery/doors/firedoor.dm @@ -1,525 +1,525 @@ - -#define FIREDOOR_MAX_PRESSURE_DIFF 25 // kPa -#define FIREDOOR_MAX_TEMP 50 // °C -#define FIREDOOR_MIN_TEMP 0 - -// Bitflags -#define FIREDOOR_ALERT_HOT 1 -#define FIREDOOR_ALERT_COLD 2 -// Not used #define FIREDOOR_ALERT_LOWPRESS 4 - -/obj/machinery/door/firedoor - name = "\improper Emergency Shutter" - desc = "Emergency air-tight shutter, capable of sealing off breached areas." - icon = 'icons/obj/doors/DoorHazard.dmi' - icon_state = "door_open" - req_one_access = list(access_eva) //access_atmospherics, access_engine_equip) - opacity = 0 - density = FALSE - layer = DOOR_OPEN_LAYER - 0.01 - open_layer = DOOR_OPEN_LAYER - 0.01 // Just below doors when open - closed_layer = DOOR_CLOSED_LAYER + 0.01 // Just above doors when closed - - //These are frequenly used with windows, so make sure zones can pass. - //Generally if a firedoor is at a place where there should be a zone boundery then there will be a regular door underneath it. - block_air_zones = 0 - - var/blocked = 0 - var/prying = 0 - var/lockdown = 0 // When the door has detected a problem, it locks. - var/pdiff_alert = 0 - var/pdiff = 0 - var/nextstate = null - var/net_id - var/list/areas_added - var/list/users_to_open = new - var/next_process_time = 0 - - var/hatch_open = 0 - - power_channel = ENVIRON - use_power = USE_POWER_IDLE - idle_power_usage = 5 - - var/list/tile_info[4] - var/list/dir_alerts[4] // 4 dirs, bitflags - - // MUST be in same order as FIREDOOR_ALERT_* - var/list/ALERT_STATES=list( - "hot", - "cold" - ) - var/open_sound = 'sound/machines/firelockopen.ogg' //CHOMPEdit firedoor sound variable. - var/close_sound = 'sound/machines/firelockclose.ogg' //CHOMPEdit firedoor sound variable. - -/obj/machinery/door/firedoor/Initialize() - . = ..() - //Delete ourselves if we find extra mapped in firedoors - for(var/obj/machinery/door/firedoor/F in loc) - if(F != src) - log_debug("Duplicate firedoors at [x],[y],[z]") - return INITIALIZE_HINT_QDEL - - var/area/A = get_area(src) - ASSERT(istype(A)) - - LAZYADD(A.all_doors, src) - areas_added = list(A) - - for(var/direction in cardinal) - A = get_area(get_step(src,direction)) - if(istype(A) && !(A in areas_added)) - LAZYADD(A.all_doors, src) - areas_added += A - -/obj/machinery/door/firedoor/Destroy() - for(var/area/A in areas_added) - LAZYREMOVE(A.all_doors, src) - . = ..() - -/obj/machinery/door/firedoor/get_material() - return get_material_by_name(MAT_STEEL) - -/obj/machinery/door/firedoor/examine(mob/user) - . = ..() - - if(!Adjacent(user)) - return . - - if(pdiff >= FIREDOOR_MAX_PRESSURE_DIFF) - . += "WARNING: Current pressure differential is [pdiff]kPa! Opening door may result in injury!" - - . += "Sensor readings:" - for(var/index = 1; index <= tile_info.len; index++) - var/o = "  " - switch(index) - if(1) - o += "NORTH: " - if(2) - o += "SOUTH: " - if(3) - o += "EAST: " - if(4) - o += "WEST: " - if(tile_info[index] == null) - o += "DATA UNAVAILABLE" - . += o - continue - var/celsius = convert_k2c(tile_info[index][1]) - var/pressure = tile_info[index][2] - o += "" - o += "[celsius]°C " - o += "" - o += "[pressure]kPa" - . += o - - if(islist(users_to_open) && users_to_open.len) - var/users_to_open_string = users_to_open[1] - if(users_to_open.len >= 2) - for(var/i = 2 to users_to_open.len) - users_to_open_string += ", [users_to_open[i]]" - . += "These people have opened \the [src] during an alert: [users_to_open_string]." - -/obj/machinery/door/firedoor/Bumped(atom/AM) - if(p_open || operating) - return - if(!density) - return ..() - if(istype(AM, /obj/mecha)) - var/obj/mecha/mecha = AM - if(mecha.occupant) - var/mob/M = mecha.occupant - if(world.time - M.last_bumped <= 10) return //Can bump-open one airlock per second. This is to prevent popup message spam. - M.last_bumped = world.time - attack_hand(M) - return 0 - -/obj/machinery/door/firedoor/attack_hand(mob/user as mob) - add_fingerprint(user) - if(operating) - return//Already doing something. - - if(istype(user, /mob/living/carbon/human)) - var/mob/living/carbon/human/X = user - if(istype(X.species, /datum/species/xenos)) - src.attack_alien(user) - return - - if(blocked) - to_chat(user, "\The [src] is welded solid!") - return - - var/alarmed = lockdown - for(var/area/A in areas_added) //Checks if there are fire alarms in any areas associated with that firedoor - if(A.firedoors_closed) - alarmed = 1 - - var/answer = tgui_alert(user, "Would you like to [density ? "open" : "close"] this [src.name]?[ alarmed && density ? "\nNote that by doing so, you acknowledge any damages from opening this\n[src.name] as being your own fault, and you will be held accountable under the law." : ""]",\ - "\The [src]", list("Yes, [density ? "open" : "close"]", "No")) - if(answer == "No") - return - if(user.incapacitated() || (get_dist(src, user) > 1 && !issilicon(user))) - to_chat(user, "Sorry, you must remain able bodied and close to \the [src] in order to use it.") - return - if(density && (stat & (BROKEN|NOPOWER))) //can still close without power - to_chat(user, "\The [src] is not functioning, you'll have to force it open manually.") - return - - if(alarmed && density && lockdown && !allowed(user)) - to_chat(user, "Access denied. Please wait for authorities to arrive, or for the alert to clear.") - return - else - user.visible_message("\The [src] [density ? "open" : "close"]s for \the [user].",\ - "\The [src] [density ? "open" : "close"]s.",\ - "You hear a beep, and a door opening.") - - var/needs_to_close = 0 - if(density) - if(alarmed) - // Accountability! - users_to_open |= user.name - needs_to_close = !issilicon(user) - spawn() - open() - else - spawn() - close() - - if(needs_to_close) - spawn(50) - alarmed = 0 - for(var/area/A in areas_added) //Just in case a fire alarm is turned off while the firedoor is going through an autoclose cycle - if(A.firedoors_closed) - alarmed = 1 - if(alarmed) - nextstate = FIREDOOR_CLOSED - close() - -/obj/machinery/door/firedoor/attack_alien(var/mob/user) //Familiar, right? Doors. - if(istype(user, /mob/living/carbon/human)) - var/mob/living/carbon/human/X = user - if(istype(X.species, /datum/species/xenos)) - if(src.blocked) - visible_message("\The [user] begins digging into \the [src] internals!") - if(do_after(user,5 SECONDS,src)) - playsound(src, 'sound/machines/door/airlock_creaking.ogg', 100, 1) - src.blocked = 0 - update_icon() - open(1) - else if(src.density) - visible_message("\The [user] begins forcing \the [src] open!") - if(do_after(user, 2 SECONDS,src)) - playsound(src, 'sound/machines/door/airlock_creaking.ogg', 100, 1) - visible_message("\The [user] forces \the [src] open!") - open(1) - else - visible_message("\The [user] forces \the [src] closed!") - close(1) - else - visible_message("\The [user] strains fruitlessly to force \the [src] [density ? "open" : "closed"].") - return - ..() - -/obj/machinery/door/firedoor/attack_generic(var/mob/living/user, var/damage) - if(stat & (BROKEN|NOPOWER)) - if(damage >= STRUCTURE_MIN_DAMAGE_THRESHOLD) - var/time_to_force = (2 + (2 * blocked)) * 5 - if(src.density) - visible_message("\The [user] starts forcing \the [src] open!") - user.set_AI_busy(TRUE) // If the mob doesn't have an AI attached, this won't do anything. - if(do_after(user, time_to_force, src)) - visible_message("\The [user] forces \the [src] open!") - src.blocked = 0 - open(1) - user.set_AI_busy(FALSE) - else - time_to_force = (time_to_force / 2) - visible_message("\The [user] starts forcing \the [src] closed!") - user.set_AI_busy(TRUE) // If the mob doesn't have an AI attached, this won't do anything. - if(do_after(user, time_to_force, src)) - visible_message("\The [user] forces \the [src] closed!") - close(1) - user.set_AI_busy(FALSE) - else - visible_message("\The [user] strains fruitlessly to force \the [src] [density ? "open" : "closed"].") - return - ..() - -/obj/machinery/door/firedoor/attackby(obj/item/weapon/C as obj, mob/user as mob) - add_fingerprint(user) - if(istype(C, /obj/item/taperoll)) - return //Don't open the door if we're putting tape on it to tell people 'don't open the door'. - if(operating) - return//Already doing something. - if(C.has_tool_quality(TOOL_WELDER) && !repairing) - if(prying) - to_chat(user, "Someone's busy prying that [density ? "open" : "closed"]!") - var/obj/item/weapon/weldingtool/W = C.get_welder() - if(W.remove_fuel(0, user)) - blocked = !blocked - user.visible_message("\The [user] [blocked ? "welds" : "unwelds"] \the [src] with \a [W].",\ - "You [blocked ? "weld" : "unweld"] \the [src] with \the [W].",\ - "You hear something being welded.") - playsound(src, W.usesound, 100, 1) - update_icon() - return - - if(density && C.has_tool_quality(TOOL_SCREWDRIVER)) - hatch_open = !hatch_open - playsound(src, C.usesound, 50, 1) - user.visible_message("[user] has [hatch_open ? "opened" : "closed"] \the [src] maintenance hatch.", - "You have [hatch_open ? "opened" : "closed"] the [src] maintenance hatch.") - update_icon() - return - - if(blocked && C.has_tool_quality(TOOL_CROWBAR) && !repairing) - if(!hatch_open) - to_chat(user, "You must open the maintenance hatch first!") - else - user.visible_message("[user] is removing the electronics from \the [src].", - "You start to remove the electronics from [src].") - if(do_after(user,30)) - if(blocked && density && hatch_open) - playsound(src, C.usesound, 50, 1) - user.visible_message("[user] has removed the electronics from \the [src].", - "You have removed the electronics from [src].") - - if (stat & BROKEN) - new /obj/item/weapon/circuitboard/broken(src.loc) - else - new/obj/item/weapon/circuitboard/airalarm(src.loc) - - var/obj/structure/firedoor_assembly/FA = new/obj/structure/firedoor_assembly(src.loc) - FA.anchored = TRUE - FA.density = TRUE - FA.wired = 1 - FA.glass = glass - FA.update_icon() - qdel(src) - return - - if(blocked) - to_chat(user, "\The [src] is welded shut!") - return - - if(C.pry == 1) - if(operating) - return - - if(blocked && C.has_tool_quality(TOOL_CROWBAR)) - user.visible_message("\The [user] pries at \the [src] with \a [C], but \the [src] is welded in place!",\ - "You try to pry \the [src] [density ? "open" : "closed"], but it is welded in place!",\ - "You hear someone struggle and metal straining.") - return - - if(istype(C,/obj/item/weapon/material/twohanded/fireaxe)) - var/obj/item/weapon/material/twohanded/fireaxe/F = C - if(!F.wielded) - return - - if(prying) - to_chat(user, "Someone's already prying that [density ? "open" : "closed"].") - return - - user.visible_message("\The [user] starts to force \the [src] [density ? "open" : "closed"] with \a [C]!",\ - "You start forcing \the [src] [density ? "open" : "closed"] with \the [C]!",\ - "You hear metal strain.") - prying = 1 - update_icon() - playsound(src, C.usesound, 100, 1) - if(do_after(user,30 * C.toolspeed)) - if(C.has_tool_quality(TOOL_CROWBAR)) - if(stat & (BROKEN|NOPOWER) || !density) - user.visible_message("\The [user] forces \the [src] [density ? "open" : "closed"] with \a [C]!",\ - "You force \the [src] [density ? "open" : "closed"] with \the [C]!",\ - "You hear metal strain, and a door [density ? "open" : "close"].") - else - user.visible_message("\The [user] forces \the [ blocked ? "welded" : "" ] [src] [density ? "open" : "closed"] with \a [C]!",\ - "You force \the [ blocked ? "welded" : "" ] [src] [density ? "open" : "closed"] with \the [C]!",\ - "You hear metal strain and groan, and a door [density ? "opening" : "closing"].") - if(density) - spawn(0) - open(1) - else - spawn(0) - close() - prying = 0 - update_icon() - return - - return ..() - -// CHECK PRESSURE -/obj/machinery/door/firedoor/process() - ..() - - if(!density) - return PROCESS_KILL - if(next_process_time <= world.time) - next_process_time = world.time + 100 // 10 second delays between process updates - var/changed = 0 - lockdown=0 - // Pressure alerts - pdiff = getOPressureDifferential(src.loc) - if(pdiff >= FIREDOOR_MAX_PRESSURE_DIFF) - lockdown = 1 - if(!pdiff_alert) - pdiff_alert = 1 - changed = 1 // update_icon() - else - if(pdiff_alert) - pdiff_alert = 0 - changed = 1 // update_icon() - - tile_info = getCardinalAirInfo(src.loc,list("temperature","pressure")) - var/old_alerts = dir_alerts - for(var/index = 1; index <= 4; index++) - var/list/tileinfo=tile_info[index] - if(tileinfo==null) - continue // Bad data. - var/celsius = convert_k2c(tileinfo[1]) - - var/alerts=0 - - // Temperatures - if(celsius >= FIREDOOR_MAX_TEMP) - alerts |= FIREDOOR_ALERT_HOT - lockdown = 1 - else if(celsius <= FIREDOOR_MIN_TEMP) - alerts |= FIREDOOR_ALERT_COLD - lockdown = 1 - - dir_alerts[index]=alerts - - if(dir_alerts != old_alerts) - changed = 1 - if(changed) - update_icon() - -/obj/machinery/door/firedoor/proc/latetoggle() - if(operating || !nextstate) - return - switch(nextstate) - if(FIREDOOR_OPEN) - nextstate = null - - open() - if(FIREDOOR_CLOSED) - nextstate = null - close() - return - -/obj/machinery/door/firedoor/close() - latetoggle() - . = ..() - // Queue us for processing when we are closed! - if(density) - START_MACHINE_PROCESSING(src) - -/obj/machinery/door/firedoor/open(var/forced = 0) - if(hatch_open) - hatch_open = 0 - visible_message("The maintenance hatch of \the [src] closes.") - update_icon() - - if(!forced) - if(stat & (BROKEN|NOPOWER)) - return //needs power to open unless it was forced - else - use_power(360) - else - if(usr && usr.ckey) - log_admin("[usr]([usr.ckey]) has forced open an emergency shutter.") - message_admins("[usr]([usr.ckey]) has forced open an emergency shutter.") - latetoggle() - return ..() - -/obj/machinery/door/firedoor/do_animate(animation) - switch(animation) - if("opening") - flick("door_opening", src) - playsound(src, open_sound, 37, 1) //CHOMPEdit var - if("closing") - playsound(src, close_sound, 37, 1) //CHOMPEdit var - flick("door_closing", src) - return - - -/obj/machinery/door/firedoor/update_icon() - cut_overlays() - if(density) - icon_state = "door_closed" - if(prying) - icon_state = "prying_closed" - if(hatch_open) - add_overlay("hatch") - if(blocked) - add_overlay("welded") - if(pdiff_alert) - add_overlay("palert") - if(dir_alerts) - for(var/d=1;d<=4;d++) - var/cdir = cardinal[d] - for(var/i=1;i<=ALERT_STATES.len;i++) - if(dir_alerts[d] & (1<<(i-1))) - add_overlay(new/icon(icon,"alert_[ALERT_STATES[i]]", dir=cdir)) - else - icon_state = "door_open" - if(prying) - icon_state = "prying_open" - if(blocked) - add_overlay("welded_open") - return - -//These are playing merry hell on ZAS. Sorry fellas :( - -/obj/machinery/door/firedoor/border_only -/* - icon = 'icons/obj/doors/edge_Doorfire.dmi' - glass = 1 //There is a glass window so you can see through the door - //This is needed due to BYOND limitations in controlling visibility - heat_proof = 1 - air_properties_vary_with_direction = 1 - - CanPass(atom/movable/mover, turf/target) - if(istype(mover) && mover.checkpass(PASSGLASS)) - return 1 - if(get_dir(loc, target) == dir) //Make sure looking at appropriate border - return !density - else - return 1 - - CheckExit(atom/movable/mover as mob|obj, turf/target as turf) - if(istype(mover) && mover.checkpass(PASSGLASS)) - return 1 - if(get_dir(loc, target) == dir) - return !density - else - return 1 - - - update_nearby_tiles(need_rebuild) - if(!air_master) return 0 - - var/turf/simulated/source = loc - var/turf/simulated/destination = get_step(source,dir) - - update_heat_protection(loc) - - if(istype(source)) air_master.tiles_to_update += source - if(istype(destination)) air_master.tiles_to_update += destination - return 1 -*/ - -/obj/machinery/door/firedoor/multi_tile - icon = 'icons/obj/doors/DoorHazard2x1.dmi' - width = 2 - open_sound = 'sound/machines/firewide1o.ogg' //CHOMPEdit - close_sound = 'sound/machines/firewide1c.ogg' //CHOMPEdit - -/obj/machinery/door/firedoor/glass - name = "\improper Emergency Glass Shutter" - desc = "Emergency air-tight shutter, capable of sealing off breached areas. This one has a resilient glass window, allowing you to see the danger." - icon = 'icons/obj/doors/DoorHazardGlass.dmi' - icon_state = "door_open" - glass = 1 + +#define FIREDOOR_MAX_PRESSURE_DIFF 25 // kPa +#define FIREDOOR_MAX_TEMP 50 // °C +#define FIREDOOR_MIN_TEMP 0 + +// Bitflags +#define FIREDOOR_ALERT_HOT 1 +#define FIREDOOR_ALERT_COLD 2 +// Not used #define FIREDOOR_ALERT_LOWPRESS 4 + +/obj/machinery/door/firedoor + name = "\improper Emergency Shutter" + desc = "Emergency air-tight shutter, capable of sealing off breached areas." + icon = 'icons/obj/doors/DoorHazard.dmi' + icon_state = "door_open" + req_one_access = list(access_eva) //access_atmospherics, access_engine_equip) + opacity = 0 + density = FALSE + layer = DOOR_OPEN_LAYER - 0.01 + open_layer = DOOR_OPEN_LAYER - 0.01 // Just below doors when open + closed_layer = DOOR_CLOSED_LAYER + 0.01 // Just above doors when closed + + //These are frequenly used with windows, so make sure zones can pass. + //Generally if a firedoor is at a place where there should be a zone boundery then there will be a regular door underneath it. + block_air_zones = 0 + + var/blocked = 0 + var/prying = 0 + var/lockdown = 0 // When the door has detected a problem, it locks. + var/pdiff_alert = 0 + var/pdiff = 0 + var/nextstate = null + var/net_id + var/list/areas_added + var/list/users_to_open = new + var/next_process_time = 0 + + var/hatch_open = 0 + + power_channel = ENVIRON + use_power = USE_POWER_IDLE + idle_power_usage = 5 + + var/list/tile_info[4] + var/list/dir_alerts[4] // 4 dirs, bitflags + + // MUST be in same order as FIREDOOR_ALERT_* + var/list/ALERT_STATES=list( + "hot", + "cold" + ) + var/open_sound = 'sound/machines/firelockopen.ogg' //CHOMPEdit firedoor sound variable. + var/close_sound = 'sound/machines/firelockclose.ogg' //CHOMPEdit firedoor sound variable. + +/obj/machinery/door/firedoor/Initialize() + . = ..() + //Delete ourselves if we find extra mapped in firedoors + for(var/obj/machinery/door/firedoor/F in loc) + if(F != src) + log_debug("Duplicate firedoors at [x],[y],[z]") + return INITIALIZE_HINT_QDEL + + var/area/A = get_area(src) + ASSERT(istype(A)) + + LAZYADD(A.all_doors, src) + areas_added = list(A) + + for(var/direction in cardinal) + A = get_area(get_step(src,direction)) + if(istype(A) && !(A in areas_added)) + LAZYADD(A.all_doors, src) + areas_added += A + +/obj/machinery/door/firedoor/Destroy() + for(var/area/A in areas_added) + LAZYREMOVE(A.all_doors, src) + . = ..() + +/obj/machinery/door/firedoor/get_material() + return get_material_by_name(MAT_STEEL) + +/obj/machinery/door/firedoor/examine(mob/user) + . = ..() + + if(!Adjacent(user)) + return . + + if(pdiff >= FIREDOOR_MAX_PRESSURE_DIFF) + . += "WARNING: Current pressure differential is [pdiff]kPa! Opening door may result in injury!" + + . += "Sensor readings:" + for(var/index = 1; index <= tile_info.len; index++) + var/o = "  " + switch(index) + if(1) + o += "NORTH: " + if(2) + o += "SOUTH: " + if(3) + o += "EAST: " + if(4) + o += "WEST: " + if(tile_info[index] == null) + o += "DATA UNAVAILABLE" + . += o + continue + var/celsius = convert_k2c(tile_info[index][1]) + var/pressure = tile_info[index][2] + o += "" + o += "[celsius]°C " + o += "" + o += "[pressure]kPa" + . += o + + if(islist(users_to_open) && users_to_open.len) + var/users_to_open_string = users_to_open[1] + if(users_to_open.len >= 2) + for(var/i = 2 to users_to_open.len) + users_to_open_string += ", [users_to_open[i]]" + . += "These people have opened \the [src] during an alert: [users_to_open_string]." + +/obj/machinery/door/firedoor/Bumped(atom/AM) + if(p_open || operating) + return + if(!density) + return ..() + if(istype(AM, /obj/mecha)) + var/obj/mecha/mecha = AM + if(mecha.occupant) + var/mob/M = mecha.occupant + if(world.time - M.last_bumped <= 10) return //Can bump-open one airlock per second. This is to prevent popup message spam. + M.last_bumped = world.time + attack_hand(M) + return 0 + +/obj/machinery/door/firedoor/attack_hand(mob/user as mob) + add_fingerprint(user) + if(operating) + return//Already doing something. + + if(istype(user, /mob/living/carbon/human)) + var/mob/living/carbon/human/X = user + if(istype(X.species, /datum/species/xenos)) + src.attack_alien(user) + return + + if(blocked) + to_chat(user, "\The [src] is welded solid!") + return + + var/alarmed = lockdown + for(var/area/A in areas_added) //Checks if there are fire alarms in any areas associated with that firedoor + if(A.firedoors_closed) + alarmed = 1 + + var/answer = tgui_alert(user, "Would you like to [density ? "open" : "close"] this [src.name]?[ alarmed && density ? "\nNote that by doing so, you acknowledge any damages from opening this\n[src.name] as being your own fault, and you will be held accountable under the law." : ""]",\ + "\The [src]", list("Yes, [density ? "open" : "close"]", "No")) + if(answer == "No") + return + if(user.incapacitated() || (get_dist(src, user) > 1 && !issilicon(user))) + to_chat(user, "Sorry, you must remain able bodied and close to \the [src] in order to use it.") + return + if(density && (stat & (BROKEN|NOPOWER))) //can still close without power + to_chat(user, "\The [src] is not functioning, you'll have to force it open manually.") + return + + if(alarmed && density && lockdown && !allowed(user)) + to_chat(user, "Access denied. Please wait for authorities to arrive, or for the alert to clear.") + return + else + user.visible_message("\The [src] [density ? "open" : "close"]s for \the [user].",\ + "\The [src] [density ? "open" : "close"]s.",\ + "You hear a beep, and a door opening.") + + var/needs_to_close = 0 + if(density) + if(alarmed) + // Accountability! + users_to_open |= user.name + needs_to_close = !issilicon(user) + spawn() + open() + else + spawn() + close() + + if(needs_to_close) + spawn(50) + alarmed = 0 + for(var/area/A in areas_added) //Just in case a fire alarm is turned off while the firedoor is going through an autoclose cycle + if(A.firedoors_closed) + alarmed = 1 + if(alarmed) + nextstate = FIREDOOR_CLOSED + close() + +/obj/machinery/door/firedoor/attack_alien(var/mob/user) //Familiar, right? Doors. + if(istype(user, /mob/living/carbon/human)) + var/mob/living/carbon/human/X = user + if(istype(X.species, /datum/species/xenos)) + if(src.blocked) + visible_message("\The [user] begins digging into \the [src] internals!") + if(do_after(user,5 SECONDS,src)) + playsound(src, 'sound/machines/door/airlock_creaking.ogg', 100, 1) + src.blocked = 0 + update_icon() + open(1) + else if(src.density) + visible_message("\The [user] begins forcing \the [src] open!") + if(do_after(user, 2 SECONDS,src)) + playsound(src, 'sound/machines/door/airlock_creaking.ogg', 100, 1) + visible_message("\The [user] forces \the [src] open!") + open(1) + else + visible_message("\The [user] forces \the [src] closed!") + close(1) + else + visible_message("\The [user] strains fruitlessly to force \the [src] [density ? "open" : "closed"].") + return + ..() + +/obj/machinery/door/firedoor/attack_generic(var/mob/living/user, var/damage) + if(stat & (BROKEN|NOPOWER)) + if(damage >= STRUCTURE_MIN_DAMAGE_THRESHOLD) + var/time_to_force = (2 + (2 * blocked)) * 5 + if(src.density) + visible_message("\The [user] starts forcing \the [src] open!") + user.set_AI_busy(TRUE) // If the mob doesn't have an AI attached, this won't do anything. + if(do_after(user, time_to_force, src)) + visible_message("\The [user] forces \the [src] open!") + src.blocked = 0 + open(1) + user.set_AI_busy(FALSE) + else + time_to_force = (time_to_force / 2) + visible_message("\The [user] starts forcing \the [src] closed!") + user.set_AI_busy(TRUE) // If the mob doesn't have an AI attached, this won't do anything. + if(do_after(user, time_to_force, src)) + visible_message("\The [user] forces \the [src] closed!") + close(1) + user.set_AI_busy(FALSE) + else + visible_message("\The [user] strains fruitlessly to force \the [src] [density ? "open" : "closed"].") + return + ..() + +/obj/machinery/door/firedoor/attackby(obj/item/weapon/C as obj, mob/user as mob) + add_fingerprint(user) + if(istype(C, /obj/item/taperoll)) + return //Don't open the door if we're putting tape on it to tell people 'don't open the door'. + if(operating) + return//Already doing something. + if(C.has_tool_quality(TOOL_WELDER) && !repairing) + if(prying) + to_chat(user, "Someone's busy prying that [density ? "open" : "closed"]!") + var/obj/item/weapon/weldingtool/W = C.get_welder() + if(W.remove_fuel(0, user)) + blocked = !blocked + user.visible_message("\The [user] [blocked ? "welds" : "unwelds"] \the [src] with \a [W].",\ + "You [blocked ? "weld" : "unweld"] \the [src] with \the [W].",\ + "You hear something being welded.") + playsound(src, W.usesound, 100, 1) + update_icon() + return + + if(density && C.has_tool_quality(TOOL_SCREWDRIVER)) + hatch_open = !hatch_open + playsound(src, C.usesound, 50, 1) + user.visible_message("[user] has [hatch_open ? "opened" : "closed"] \the [src] maintenance hatch.", + "You have [hatch_open ? "opened" : "closed"] the [src] maintenance hatch.") + update_icon() + return + + if(blocked && C.has_tool_quality(TOOL_CROWBAR) && !repairing) + if(!hatch_open) + to_chat(user, "You must open the maintenance hatch first!") + else + user.visible_message("[user] is removing the electronics from \the [src].", + "You start to remove the electronics from [src].") + if(do_after(user,30)) + if(blocked && density && hatch_open) + playsound(src, C.usesound, 50, 1) + user.visible_message("[user] has removed the electronics from \the [src].", + "You have removed the electronics from [src].") + + if (stat & BROKEN) + new /obj/item/weapon/circuitboard/broken(src.loc) + else + new/obj/item/weapon/circuitboard/airalarm(src.loc) + + var/obj/structure/firedoor_assembly/FA = new/obj/structure/firedoor_assembly(src.loc) + FA.anchored = TRUE + FA.density = TRUE + FA.wired = 1 + FA.glass = glass + FA.update_icon() + qdel(src) + return + + if(blocked) + to_chat(user, "\The [src] is welded shut!") + return + + if(C.pry == 1) + if(operating) + return + + if(blocked && C.has_tool_quality(TOOL_CROWBAR)) + user.visible_message("\The [user] pries at \the [src] with \a [C], but \the [src] is welded in place!",\ + "You try to pry \the [src] [density ? "open" : "closed"], but it is welded in place!",\ + "You hear someone struggle and metal straining.") + return + + if(istype(C,/obj/item/weapon/material/twohanded/fireaxe)) + var/obj/item/weapon/material/twohanded/fireaxe/F = C + if(!F.wielded) + return + + if(prying) + to_chat(user, "Someone's already prying that [density ? "open" : "closed"].") + return + + user.visible_message("\The [user] starts to force \the [src] [density ? "open" : "closed"] with \a [C]!",\ + "You start forcing \the [src] [density ? "open" : "closed"] with \the [C]!",\ + "You hear metal strain.") + prying = 1 + update_icon() + playsound(src, C.usesound, 100, 1) + if(do_after(user,30 * C.toolspeed)) + if(C.has_tool_quality(TOOL_CROWBAR)) + if(stat & (BROKEN|NOPOWER) || !density) + user.visible_message("\The [user] forces \the [src] [density ? "open" : "closed"] with \a [C]!",\ + "You force \the [src] [density ? "open" : "closed"] with \the [C]!",\ + "You hear metal strain, and a door [density ? "open" : "close"].") + else + user.visible_message("\The [user] forces \the [ blocked ? "welded" : "" ] [src] [density ? "open" : "closed"] with \a [C]!",\ + "You force \the [ blocked ? "welded" : "" ] [src] [density ? "open" : "closed"] with \the [C]!",\ + "You hear metal strain and groan, and a door [density ? "opening" : "closing"].") + if(density) + spawn(0) + open(1) + else + spawn(0) + close() + prying = 0 + update_icon() + return + + return ..() + +// CHECK PRESSURE +/obj/machinery/door/firedoor/process() + ..() + + if(!density) + return PROCESS_KILL + if(next_process_time <= world.time) + next_process_time = world.time + 100 // 10 second delays between process updates + var/changed = 0 + lockdown=0 + // Pressure alerts + pdiff = getOPressureDifferential(src.loc) + if(pdiff >= FIREDOOR_MAX_PRESSURE_DIFF) + lockdown = 1 + if(!pdiff_alert) + pdiff_alert = 1 + changed = 1 // update_icon() + else + if(pdiff_alert) + pdiff_alert = 0 + changed = 1 // update_icon() + + tile_info = getCardinalAirInfo(src.loc,list("temperature","pressure")) + var/old_alerts = dir_alerts + for(var/index = 1; index <= 4; index++) + var/list/tileinfo=tile_info[index] + if(tileinfo==null) + continue // Bad data. + var/celsius = convert_k2c(tileinfo[1]) + + var/alerts=0 + + // Temperatures + if(celsius >= FIREDOOR_MAX_TEMP) + alerts |= FIREDOOR_ALERT_HOT + lockdown = 1 + else if(celsius <= FIREDOOR_MIN_TEMP) + alerts |= FIREDOOR_ALERT_COLD + lockdown = 1 + + dir_alerts[index]=alerts + + if(dir_alerts != old_alerts) + changed = 1 + if(changed) + update_icon() + +/obj/machinery/door/firedoor/proc/latetoggle() + if(operating || !nextstate) + return + switch(nextstate) + if(FIREDOOR_OPEN) + nextstate = null + + open() + if(FIREDOOR_CLOSED) + nextstate = null + close() + return + +/obj/machinery/door/firedoor/close() + latetoggle() + . = ..() + // Queue us for processing when we are closed! + if(density) + START_MACHINE_PROCESSING(src) + +/obj/machinery/door/firedoor/open(var/forced = 0) + if(hatch_open) + hatch_open = 0 + visible_message("The maintenance hatch of \the [src] closes.") + update_icon() + + if(!forced) + if(stat & (BROKEN|NOPOWER)) + return //needs power to open unless it was forced + else + use_power(360) + else + if(usr && usr.ckey) + log_admin("[usr]([usr.ckey]) has forced open an emergency shutter.") + message_admins("[usr]([usr.ckey]) has forced open an emergency shutter.") + latetoggle() + return ..() + +/obj/machinery/door/firedoor/do_animate(animation) + switch(animation) + if("opening") + flick("door_opening", src) + playsound(src, open_sound, 37, 1) //CHOMPEdit var + if("closing") + playsound(src, close_sound, 37, 1) //CHOMPEdit var + flick("door_closing", src) + return + + +/obj/machinery/door/firedoor/update_icon() + cut_overlays() + if(density) + icon_state = "door_closed" + if(prying) + icon_state = "prying_closed" + if(hatch_open) + add_overlay("hatch") + if(blocked) + add_overlay("welded") + if(pdiff_alert) + add_overlay("palert") + if(dir_alerts) + for(var/d=1;d<=4;d++) + var/cdir = cardinal[d] + for(var/i=1;i<=ALERT_STATES.len;i++) + if(dir_alerts[d] & (1<<(i-1))) + add_overlay(new/icon(icon,"alert_[ALERT_STATES[i]]", dir=cdir)) + else + icon_state = "door_open" + if(prying) + icon_state = "prying_open" + if(blocked) + add_overlay("welded_open") + return + +//These are playing merry hell on ZAS. Sorry fellas :( + +/obj/machinery/door/firedoor/border_only +/* + icon = 'icons/obj/doors/edge_Doorfire.dmi' + glass = 1 //There is a glass window so you can see through the door + //This is needed due to BYOND limitations in controlling visibility + heat_proof = 1 + air_properties_vary_with_direction = 1 + + CanPass(atom/movable/mover, turf/target) + if(istype(mover) && mover.checkpass(PASSGLASS)) + return 1 + if(get_dir(loc, target) == dir) //Make sure looking at appropriate border + return !density + else + return 1 + + CheckExit(atom/movable/mover as mob|obj, turf/target as turf) + if(istype(mover) && mover.checkpass(PASSGLASS)) + return 1 + if(get_dir(loc, target) == dir) + return !density + else + return 1 + + + update_nearby_tiles(need_rebuild) + if(!air_master) return 0 + + var/turf/simulated/source = loc + var/turf/simulated/destination = get_step(source,dir) + + update_heat_protection(loc) + + if(istype(source)) air_master.tiles_to_update += source + if(istype(destination)) air_master.tiles_to_update += destination + return 1 +*/ + +/obj/machinery/door/firedoor/multi_tile + icon = 'icons/obj/doors/DoorHazard2x1.dmi' + width = 2 + open_sound = 'sound/machines/firewide1o.ogg' //CHOMPEdit + close_sound = 'sound/machines/firewide1c.ogg' //CHOMPEdit + +/obj/machinery/door/firedoor/glass + name = "\improper Emergency Glass Shutter" + desc = "Emergency air-tight shutter, capable of sealing off breached areas. This one has a resilient glass window, allowing you to see the danger." + icon = 'icons/obj/doors/DoorHazardGlass.dmi' + icon_state = "door_open" + glass = 1 diff --git a/code/modules/clothing/under/accessories/accessory_vr.dm b/code/modules/clothing/under/accessories/accessory_vr.dm index c674328193..48ab4635f4 100644 --- a/code/modules/clothing/under/accessories/accessory_vr.dm +++ b/code/modules/clothing/under/accessories/accessory_vr.dm @@ -512,7 +512,14 @@ /obj/item/clothing/accessory/collar/shock/bluespace/relaymove(var/mob/living/user,var/direction) return //For some reason equipping this item was triggering this proc, putting the wearer inside of the collars belly for some reason. -/obj/item/clothing/accessory/collar/shock/bluespace/attackby(var/obj/item/device/assembly/signaler/component, mob/user as mob) +/obj/item/clothing/accessory/collar/shock/bluespace/attackby(var/obj/item/component, mob/user as mob) + if (component.has_tool_quality(TOOL_WRENCH)) + to_chat(user, "You crack the bluespace crystal [src].") + var/turf/T = get_turf(src) + new /obj/item/clothing/accessory/collar/shock/bluespace/malfunctioning(T) + user.drop_from_inventory(src) + qdel(src) + return if (!istype(component,/obj/item/device/assembly/signaler)) ..() return @@ -536,7 +543,14 @@ target_size = 1 on = 1 -/obj/item/clothing/accessory/collar/shock/bluespace/modified/attackby(var/obj/item/device/assembly/signaler/component, mob/user as mob) +/obj/item/clothing/accessory/collar/shock/bluespace/modified/attackby(var/obj/item/component, mob/user as mob) + if (component.has_tool_quality(TOOL_WRENCH)) + to_chat(user, "You crack the bluespace crystal [src], the attached signaler disconnects.") + var/turf/T = get_turf(src) + new /obj/item/clothing/accessory/collar/shock/bluespace/malfunctioning(T) + user.drop_from_inventory(src) + qdel(src) + return if (!istype(component,/obj/item/device/assembly/signaler)) ..() return @@ -644,6 +658,141 @@ s.start() return +//bluespace collar malfunctioning (random size) + +/obj/item/clothing/accessory/collar/shock/bluespace/malfunctioning + name = "Bluespace collar" + desc = "A collar that can manipulate the size of the wearer, and can be modified when unequiped. It has a crack on the crystal." + icon_state = "collar_size_malf" + item_state = "collar_size" + overlay_state = "collar_size" + target_size = 1 + on = 1 + var/currently_shrinking = 0 + +/obj/item/clothing/accessory/collar/shock/bluespace/malfunctioning/attackby(var/obj/item/component, mob/user as mob) + if (!istype(component,/obj/item/device/assembly/signaler)) + ..() + return + to_chat(user, "The signaler doesn't respond to the connection attempt [src].") + return + +/obj/item/clothing/accessory/collar/shock/bluespace/malfunctioning/attack_self(mob/user as mob, flag1) + if(!istype(user, /mob/living/carbon/human)) + return + user.set_machine(src) + var/dat = {" + Frequency/Code for collar:
+ Frequency: + - + - [format_frequency(frequency)] + + + +
+ + Code: + - + - [code] + + + +
+ + Tag: + Set tag
+ + Size: + Input Disabled!
+
"} + user << browse(dat, "window=radio") + onclose(user, "radio") + return + +/obj/item/clothing/accessory/collar/shock/bluespace/malfunctioning/Topic(href, href_list) + if(usr.stat || usr.restrained()) + return + if(((istype(usr, /mob/living/carbon/human) && ((!( ticker ) || (ticker && ticker.mode != "monkey")) && usr.contents.Find(src))) || (usr.contents.Find(master) || (in_range(src, usr) && istype(loc, /turf))))) + usr.set_machine(src) + if(href_list["freq"]) + var/new_frequency = sanitize_frequency(frequency + text2num(href_list["freq"])) + set_frequency(new_frequency) + if(href_list["tag"]) + var/str = copytext(reject_bad_text(tgui_input_text(usr,"Tag text?","Set tag","",MAX_NAME_LEN)),1,MAX_NAME_LEN) + if(!str || !length(str)) + to_chat(usr,"[name]'s tag set to be blank.") + name = initial(name) + desc = initial(desc) + else + to_chat(usr,"You set the [name]'s tag to '[str]'.") + name = initial(name) + " ([str])" + desc = initial(desc) + " The tag says \"[str]\"." + else + if(href_list["code"]) + code += text2num(href_list["code"]) + code = round(code) + code = min(100, code) + code = max(1, code) + if(!( master )) + if(istype(loc, /mob)) + attack_self(loc) + else + for(var/mob/M in viewers(1, src)) + if(M.client) + attack_self(M) + else + if(istype(master.loc, /mob)) + attack_self(master.loc) + else + for(var/mob/M in viewers(1, master)) + if(M.client) + attack_self(M) + else + usr << browse(null, "window=radio") + return + return + +/obj/item/clothing/accessory/collar/shock/bluespace/malfunctioning/receive_signal(datum/signal/signal) + if(!signal) + return + target_size = (rand(25,200)) /100 + if(on) + var/mob/M = null + if(ismob(loc)) + M = loc + if(ismob(loc.loc)) + M = loc.loc // This is about as terse as I can make my solution to the whole 'collar won't work when attached as accessory' thing. + var/mob/living/carbon/human/H = M + var/datum/effect/effect/system/spark_spread/s = new /datum/effect/effect/system/spark_spread + if(!H.resizable) + H.visible_message("The space around [H] compresses for a moment but then nothing happens.","The space around you distorts but nothing happens to you.") + return + if (target_size < 0.25) + H.visible_message("The collar on [H] flickers, but fizzles out.","Your collar flickers, but is not powerful enough to shrink you that small.") + return + if(currently_shrinking == 0) + if(!(world.time - last_activated > 10 SECONDS)) + to_chat(M, "\The [src] flickers. It seems to be recharging.") + return + last_activated = world.time + original_size = H.size_multiplier + currently_shrinking = 1 + H.resize(target_size, ignore_prefs = FALSE) //In case someone else tries to put it on you. + H.visible_message("The space around [H] distorts as they change size!","The space around you distorts as you change size!") + log_admin("Admin [key_name(M)]'s size was altered by a bluespace collar.") + s.set_up(3, 1, M) + s.start() + else if(currently_shrinking == 1) + if(original_size == null) + H.visible_message("The space around [H] twists and turns for a moment but then nothing happens.","The space around you distorts but stay the same size.") + return + last_activated = world.time + H.resize(original_size, ignore_prefs = FALSE) + original_size = null + currently_shrinking = 0 + H.visible_message("The space around [H] distorts as they return to their original size!","The space around you distorts as you return to your original size!") + log_admin("Admin [key_name(M)]'s size was altered by a bluespace collar.") + to_chat(M, "\The [src] flickers. It is now recharging and will be ready again in ten seconds.") + s.set_up(3, 1, M) + s.start() + return + //Machete Holsters /obj/item/clothing/accessory/holster/machete name = "machete sheath" diff --git a/code/modules/research/message_server.dm b/code/modules/research/message_server.dm index adfbef6938..ad0585200a 100644 --- a/code/modules/research/message_server.dm +++ b/code/modules/research/message_server.dm @@ -1,455 +1,455 @@ -#define MESSAGE_SERVER_SPAM_REJECT 1 -#define MESSAGE_SERVER_DEFAULT_SPAM_LIMIT 10 - -var/global/list/obj/machinery/message_server/message_servers = list() - -/datum/data_pda_msg - var/recipient = "Unspecified" //name of the person - var/sender = "Unspecified" //name of the sender - var/message = "Blank" //transferred message - -/datum/data_pda_msg/New(var/param_rec = "",var/param_sender = "",var/param_message = "") - - if(param_rec) - recipient = param_rec - if(param_sender) - sender = param_sender - if(param_message) - message = param_message - -/datum/data_rc_msg - var/rec_dpt = "Unspecified" //name of the person - var/send_dpt = "Unspecified" //name of the sender - var/message = "Blank" //transferred message - var/stamp = "Unstamped" - var/id_auth = "Unauthenticated" - var/priority = "Normal" - -/datum/data_rc_msg/New(var/param_rec = "",var/param_sender = "",var/param_message = "",var/param_stamp = "",var/param_id_auth = "",var/param_priority) - if(param_rec) - rec_dpt = param_rec - if(param_sender) - send_dpt = param_sender - if(param_message) - message = param_message - if(param_stamp) - stamp = param_stamp - if(param_id_auth) - id_auth = param_id_auth - if(param_priority) - switch(param_priority) - if(1) - priority = "Normal" - if(2) - priority = "High" - if(3) - priority = "Extreme" - else - priority = "Undetermined" - -/obj/machinery/message_server - icon = 'icons/obj/machines/research.dmi' - icon_state = "server" - name = "Messaging Server" - desc = "Facilitates both PDA messages and request console functions." - density = TRUE - anchored = TRUE - unacidable = TRUE - use_power = USE_POWER_IDLE - idle_power_usage = 10 - active_power_usage = 100 - - var/list/datum/data_pda_msg/pda_msgs = list() - var/list/datum/data_rc_msg/rc_msgs = list() - var/active = 1 - var/decryptkey = "password" - - //Spam filtering stuff - var/list/spamfilter = list("You have won", "your prize", "male enhancement", "shitcurity", \ - "are happy to inform you", "account number", "enter your PIN") - //Messages having theese tokens will be rejected by server. Case sensitive - var/spamfilter_limit = MESSAGE_SERVER_DEFAULT_SPAM_LIMIT //Maximal amount of tokens - - var/datum/looping_sound/tcomms/soundloop // CHOMPStation Add: Hummy noises - var/noisy = FALSE // CHOMPStation Add: Hummy noises - -/obj/machinery/message_server/New() - message_servers += src - decryptkey = GenerateKey() - send_pda_message("System Administrator", "system", "This is an automated message. The messaging system is functioning correctly.") - - // CHOMPAdd: PDA Messaging Server humming - soundloop = new(list(src), FALSE) - if(prob(60)) // 60% chance to change the midloop - if(prob(40)) - soundloop.mid_sounds = list('sound/machines/tcomms/tcomms_02.ogg' = 1) - soundloop.mid_length = 40 - else if(prob(20)) - soundloop.mid_sounds = list('sound/machines/tcomms/tcomms_03.ogg' = 1) - soundloop.mid_length = 10 - else - soundloop.mid_sounds = list('sound/machines/tcomms/tcomms_04.ogg' = 1) - soundloop.mid_length = 30 - // CHOMPAdd End - ..() - return - -/obj/machinery/message_server/Destroy() - message_servers -= src - QDEL_NULL(soundloop) // CHOMPStation Add: Hummy noises - ..() - return - -/obj/machinery/message_server/examine(mob/user, distance, infix, suffix) - . = ..() - . += "It appears to be [active ? "online" : "offline"]." - -/obj/machinery/message_server/proc/GenerateKey() - //Feel free to move to Helpers. - var/newKey - newKey += pick("the", "if", "of", "as", "in", "a", "you", "from", "to", "an", "too", "little", "snow", "dead", "drunk", "rosebud", "duck", "al", "le") - newKey += pick("diamond", "beer", "mushroom", "assistant", "clown", "captain", "twinkie", "security", "nuke", "small", "big", "escape", "yellow", "gloves", "monkey", "engine", "nuclear", "ai") - newKey += pick("1", "2", "3", "4", "5", "6", "7", "8", "9", "0") - return newKey - -/obj/machinery/message_server/process() - //if(decryptkey == "password") - // decryptkey = generateKey() - if(active && (stat & (BROKEN|NOPOWER))) - active = 0 - soundloop.stop() // CHOMPStation Add: Hummy noises - noisy = FALSE // CHOMPStation Add: Hummy noises - return - if(!noisy && active) // CHOMPStation Add: Hummy noises - soundloop.start() // CHOMPStation Add: Hummy noises - noisy = TRUE // CHOMPStation Add: Hummy noises - update_icon() - return - -/obj/machinery/message_server/proc/send_pda_message(var/recipient = "",var/sender = "",var/message = "") - var/result - for (var/token in spamfilter) - if (findtextEx(message,token)) - message = "[message]" //Rejected messages will be indicated by red color. - result = token //Token caused rejection (if there are multiple, last will be chosen>. - pda_msgs += new/datum/data_pda_msg(recipient,sender,message) - return result - -/obj/machinery/message_server/proc/send_rc_message(var/recipient = "",var/sender = "",var/message = "",var/stamp = "", var/id_auth = "", var/priority = 1) - rc_msgs += new/datum/data_rc_msg(recipient,sender,message,stamp,id_auth) - var/authmsg = "[message]\n" - if (id_auth) - authmsg += "([id_auth])\n" - if (stamp) - authmsg += "([stamp])\n" - for (var/obj/machinery/requests_console/Console in allConsoles) - if (ckey(Console.department) == ckey(recipient)) - if(Console.inoperable()) - Console.message_log += list(list("Message lost due to console failure.","Please contact [station_name()] system adminsitrator or AI for technical assistance.")) - continue - if(Console.newmessagepriority < priority) - Console.newmessagepriority = priority - Console.icon_state = "req_comp[priority]" - switch(priority) - if(2) - if(!Console.silent) - playsound(Console, 'sound/machines/twobeep.ogg', 50, 1) - Console.audible_message(text("\icon[Console][bicon(Console)] *The Requests Console beeps: 'PRIORITY Alert in [sender]'"),,5, runemessage = "beep! beep!") - Console.message_log += list(list("High Priority message from [sender]", "[authmsg]")) - else - if(!Console.silent) - playsound(Console, 'sound/machines/twobeep.ogg', 50, 1) - Console.audible_message(text("\icon[Console][bicon(Console)] *The Requests Console beeps: 'Message from [sender]'"),,4, runemessage = "beep beep") - Console.message_log += list(list("Message from [sender]", "[authmsg]")) - Console.set_light(2) - - -/obj/machinery/message_server/attack_hand(user as mob) -// to_chat(user, "There seem to be some parts missing from this server. They should arrive on the station in a few days, give or take a few CentCom delays.") - to_chat(user, "You toggle PDA message passing from [active ? "On" : "Off"] to [active ? "Off" : "On"].") - active = !active - update_icon() - - return - -/obj/machinery/message_server/attackby(obj/item/weapon/O as obj, mob/living/user as mob) - if (active && !(stat & (BROKEN|NOPOWER)) && (spamfilter_limit < MESSAGE_SERVER_DEFAULT_SPAM_LIMIT*2) && \ - istype(O,/obj/item/weapon/circuitboard/message_monitor)) - spamfilter_limit += round(MESSAGE_SERVER_DEFAULT_SPAM_LIMIT / 2) - user.drop_item() - qdel(O) - to_chat(user, "You install additional memory and processors into message server. Its filtering capabilities been enhanced.") - else - ..(O, user) - -/obj/machinery/message_server/update_icon() - if((stat & (BROKEN|NOPOWER))) - icon_state = "server-nopower" - else if (!active) - icon_state = "server-off" - else - icon_state = "server-on" - - return - - -/datum/feedback_variable - var/variable - var/value - var/details - -/datum/feedback_variable/vv_edit_var(var_name, var_value) - if(var_name == NAMEOF(src, variable) || var_name == NAMEOF(src, value) || var_name == NAMEOF(src, details)) - return FALSE - return ..() - -/datum/feedback_variable/New(var/param_variable,var/param_value = 0) - variable = param_variable - value = param_value - -/datum/feedback_variable/proc/inc(var/num = 1) - if(isnum(value)) - value += num - else - value = text2num(value) - if(isnum(value)) - value += num - else - value = num - -/datum/feedback_variable/proc/dec(var/num = 1) - if(isnum(value)) - value -= num - else - value = text2num(value) - if(isnum(value)) - value -= num - else - value = -num - -/datum/feedback_variable/proc/set_value(var/num) - if(isnum(num)) - value = num - -/datum/feedback_variable/proc/get_value() - return value - -/datum/feedback_variable/proc/get_variable() - return variable - -/datum/feedback_variable/proc/set_details(var/text) - if(istext(text)) - details = text - -/datum/feedback_variable/proc/add_details(var/text) - if(istext(text)) - if(!details) - details = text - else - details += " [text]" - -/datum/feedback_variable/proc/get_details() - return details - -/datum/feedback_variable/proc/get_parsed() - return list(variable,value,details) - -var/obj/machinery/blackbox_recorder/blackbox - -/obj/machinery/blackbox_recorder - icon = 'icons/obj/stationobjs.dmi' - icon_state = "blackbox" - name = "Blackbox Recorder" - desc = "Records all radio communications, as well as various other information in case of the worst." - density = TRUE - anchored = TRUE - unacidable = TRUE - use_power = USE_POWER_IDLE - idle_power_usage = 10 - active_power_usage = 100 - var/list/messages = list() //Stores messages of non-standard frequencies - var/list/messages_admin = list() - - var/list/msg_common = list() - var/list/msg_science = list() - var/list/msg_command = list() - var/list/msg_medical = list() - var/list/msg_engineering = list() - var/list/msg_security = list() - var/list/msg_deathsquad = list() - var/list/msg_syndicate = list() - var/list/msg_raider = list() - var/list/msg_cargo = list() - var/list/msg_service = list() - var/list/msg_explorer = list() - - var/list/datum/feedback_variable/feedback = new() - - //Only one can exist in the world! -/obj/machinery/blackbox_recorder/New() - if(blackbox) - if(istype(blackbox,/obj/machinery/blackbox_recorder)) - qdel(src) - blackbox = src - -/obj/machinery/blackbox_recorder/Destroy() - var/turf/T = locate(1,1,2) - if(T) - blackbox = null - var/obj/machinery/blackbox_recorder/BR = new/obj/machinery/blackbox_recorder(T) - BR.msg_common = msg_common - BR.msg_science = msg_science - BR.msg_command = msg_command - BR.msg_medical = msg_medical - BR.msg_engineering = msg_engineering - BR.msg_security = msg_security - BR.msg_deathsquad = msg_deathsquad - BR.msg_syndicate = msg_syndicate - BR.msg_cargo = msg_cargo - BR.msg_service = msg_service - BR.feedback = feedback - BR.messages = messages - BR.messages_admin = messages_admin - if(blackbox != BR) - blackbox = BR - ..() - -/obj/machinery/blackbox_recorder/proc/find_feedback_datum(var/variable) - for(var/datum/feedback_variable/FV in feedback) - if(FV.get_variable() == variable) - return FV - var/datum/feedback_variable/FV = new(variable) - feedback += FV - return FV - -/obj/machinery/blackbox_recorder/proc/get_round_feedback() - return feedback - -/obj/machinery/blackbox_recorder/proc/round_end_data_gathering() - - var/pda_msg_amt = 0 - var/rc_msg_amt = 0 - - for(var/obj/machinery/message_server/MS in machines) - if(MS.pda_msgs.len > pda_msg_amt) - pda_msg_amt = MS.pda_msgs.len - if(MS.rc_msgs.len > rc_msg_amt) - rc_msg_amt = MS.rc_msgs.len - - feedback_set_details("radio_usage","") - - feedback_add_details("radio_usage","COM-[msg_common.len]") - feedback_add_details("radio_usage","SCI-[msg_science.len]") - feedback_add_details("radio_usage","HEA-[msg_command.len]") - feedback_add_details("radio_usage","MED-[msg_medical.len]") - feedback_add_details("radio_usage","ENG-[msg_engineering.len]") - feedback_add_details("radio_usage","SEC-[msg_security.len]") - feedback_add_details("radio_usage","DTH-[msg_deathsquad.len]") - feedback_add_details("radio_usage","SYN-[msg_syndicate.len]") - feedback_add_details("radio_usage","CAR-[msg_cargo.len]") - feedback_add_details("radio_usage","SRV-[msg_service.len]") - feedback_add_details("radio_usage","OTH-[messages.len]") - feedback_add_details("radio_usage","PDA-[pda_msg_amt]") - feedback_add_details("radio_usage","RC-[rc_msg_amt]") - - - feedback_set_details("round_end","[time2text(world.realtime)]") //This one MUST be the last one that gets set. - -/obj/machinery/blackbox_recorder/vv_edit_var(var_name, var_value) - var/static/list/blocked_vars //hacky as fuck kill me - if(!blocked_vars) - var/obj/machinery/M = new - var/list/parent_vars = M.vars.Copy() - blocked_vars = vars.Copy() - parent_vars - if(var_name in blocked_vars) - return FALSE - return ..() - -//This proc is only to be called at round end. -/obj/machinery/blackbox_recorder/proc/save_all_data_to_sql() - if(!feedback) return - - round_end_data_gathering() //round_end time logging and some other data processing - establish_db_connection() - if(!SSdbcore.IsConnected()) return //CHOMPEdit TGSQL - var/round_id - - var/DBQuery/query = SSdbcore.NewQuery("SELECT MAX(round_id) AS round_id FROM erro_feedback") //CHOMPEdit TGSQL - query.Execute() - while(query.NextRow()) - round_id = query.item[1] - qdel(query) //CHOMPEdit TGSQL - if(!isnum(round_id)) - round_id = text2num(round_id) - round_id++ - - for(var/datum/feedback_variable/FV in feedback) - var/list/sqlargs = list("t_roundid" = round_id, "t_variable" = "[FV.get_variable()]", "t_value" = "[FV.get_value()]", "t_details" = "[FV.get_details()]") //CHOMPEdit TGSQL - var/sql = "INSERT INTO erro_feedback VALUES (null, Now(), :t_roundid, :t_variable, :t_value, :t_details)" //CHOMPEdit TGSQL - var/DBQuery/query_insert = SSdbcore.NewQuery(sql, sqlargs) //CHOMPEdit TGSQL - query_insert.Execute() - qdel(query_insert) //CHOMPEdit TGSQL - -// Sanitize inputs to avoid SQL injection attacks //CHOMPEdit NOTE: This is not secure. Basic filters like this are pretty easy to bypass. Use the format for arguments used in the above. -/proc/sql_sanitize_text(var/text) - text = replacetext(text, "'", "''") - text = replacetext(text, ";", "") - text = replacetext(text, "&", "") - return text - -/proc/feedback_set(var/variable,var/value) - if(!blackbox) return - - variable = sql_sanitize_text(variable) - - var/datum/feedback_variable/FV = blackbox.find_feedback_datum(variable) - - if(!FV) return - - FV.set_value(value) - -/proc/feedback_inc(var/variable,var/value) - if(!blackbox) return - - variable = sql_sanitize_text(variable) - - var/datum/feedback_variable/FV = blackbox.find_feedback_datum(variable) - - if(!FV) return - - FV.inc(value) - -/proc/feedback_dec(var/variable,var/value) - if(!blackbox) return - - variable = sql_sanitize_text(variable) - - var/datum/feedback_variable/FV = blackbox.find_feedback_datum(variable) - - if(!FV) return - - FV.dec(value) - -/proc/feedback_set_details(var/variable,var/details) - if(!blackbox) return - - variable = sql_sanitize_text(variable) - details = sql_sanitize_text(details) - - var/datum/feedback_variable/FV = blackbox.find_feedback_datum(variable) - - if(!FV) return - - FV.set_details(details) - -/proc/feedback_add_details(var/variable,var/details) - if(!blackbox) return - - variable = sql_sanitize_text(variable) - details = sql_sanitize_text(details) - - var/datum/feedback_variable/FV = blackbox.find_feedback_datum(variable) - - if(!FV) return - - FV.add_details(details) +#define MESSAGE_SERVER_SPAM_REJECT 1 +#define MESSAGE_SERVER_DEFAULT_SPAM_LIMIT 10 + +var/global/list/obj/machinery/message_server/message_servers = list() + +/datum/data_pda_msg + var/recipient = "Unspecified" //name of the person + var/sender = "Unspecified" //name of the sender + var/message = "Blank" //transferred message + +/datum/data_pda_msg/New(var/param_rec = "",var/param_sender = "",var/param_message = "") + + if(param_rec) + recipient = param_rec + if(param_sender) + sender = param_sender + if(param_message) + message = param_message + +/datum/data_rc_msg + var/rec_dpt = "Unspecified" //name of the person + var/send_dpt = "Unspecified" //name of the sender + var/message = "Blank" //transferred message + var/stamp = "Unstamped" + var/id_auth = "Unauthenticated" + var/priority = "Normal" + +/datum/data_rc_msg/New(var/param_rec = "",var/param_sender = "",var/param_message = "",var/param_stamp = "",var/param_id_auth = "",var/param_priority) + if(param_rec) + rec_dpt = param_rec + if(param_sender) + send_dpt = param_sender + if(param_message) + message = param_message + if(param_stamp) + stamp = param_stamp + if(param_id_auth) + id_auth = param_id_auth + if(param_priority) + switch(param_priority) + if(1) + priority = "Normal" + if(2) + priority = "High" + if(3) + priority = "Extreme" + else + priority = "Undetermined" + +/obj/machinery/message_server + icon = 'icons/obj/machines/research.dmi' + icon_state = "server" + name = "Messaging Server" + desc = "Facilitates both PDA messages and request console functions." + density = TRUE + anchored = TRUE + unacidable = TRUE + use_power = USE_POWER_IDLE + idle_power_usage = 10 + active_power_usage = 100 + + var/list/datum/data_pda_msg/pda_msgs = list() + var/list/datum/data_rc_msg/rc_msgs = list() + var/active = 1 + var/decryptkey = "password" + + //Spam filtering stuff + var/list/spamfilter = list("You have won", "your prize", "male enhancement", "shitcurity", \ + "are happy to inform you", "account number", "enter your PIN") + //Messages having theese tokens will be rejected by server. Case sensitive + var/spamfilter_limit = MESSAGE_SERVER_DEFAULT_SPAM_LIMIT //Maximal amount of tokens + + var/datum/looping_sound/tcomms/soundloop // CHOMPStation Add: Hummy noises + var/noisy = FALSE // CHOMPStation Add: Hummy noises + +/obj/machinery/message_server/New() + message_servers += src + decryptkey = GenerateKey() + send_pda_message("System Administrator", "system", "This is an automated message. The messaging system is functioning correctly.") + + // CHOMPAdd: PDA Messaging Server humming + soundloop = new(list(src), FALSE) + if(prob(60)) // 60% chance to change the midloop + if(prob(40)) + soundloop.mid_sounds = list('sound/machines/tcomms/tcomms_02.ogg' = 1) + soundloop.mid_length = 40 + else if(prob(20)) + soundloop.mid_sounds = list('sound/machines/tcomms/tcomms_03.ogg' = 1) + soundloop.mid_length = 10 + else + soundloop.mid_sounds = list('sound/machines/tcomms/tcomms_04.ogg' = 1) + soundloop.mid_length = 30 + // CHOMPAdd End + ..() + return + +/obj/machinery/message_server/Destroy() + message_servers -= src + QDEL_NULL(soundloop) // CHOMPStation Add: Hummy noises + ..() + return + +/obj/machinery/message_server/examine(mob/user, distance, infix, suffix) + . = ..() + . += "It appears to be [active ? "online" : "offline"]." + +/obj/machinery/message_server/proc/GenerateKey() + //Feel free to move to Helpers. + var/newKey + newKey += pick("the", "if", "of", "as", "in", "a", "you", "from", "to", "an", "too", "little", "snow", "dead", "drunk", "rosebud", "duck", "al", "le") + newKey += pick("diamond", "beer", "mushroom", "assistant", "clown", "captain", "twinkie", "security", "nuke", "small", "big", "escape", "yellow", "gloves", "monkey", "engine", "nuclear", "ai") + newKey += pick("1", "2", "3", "4", "5", "6", "7", "8", "9", "0") + return newKey + +/obj/machinery/message_server/process() + //if(decryptkey == "password") + // decryptkey = generateKey() + if(active && (stat & (BROKEN|NOPOWER))) + active = 0 + soundloop.stop() // CHOMPStation Add: Hummy noises + noisy = FALSE // CHOMPStation Add: Hummy noises + return + if(!noisy && active) // CHOMPStation Add: Hummy noises + soundloop.start() // CHOMPStation Add: Hummy noises + noisy = TRUE // CHOMPStation Add: Hummy noises + update_icon() + return + +/obj/machinery/message_server/proc/send_pda_message(var/recipient = "",var/sender = "",var/message = "") + var/result + for (var/token in spamfilter) + if (findtextEx(message,token)) + message = "[message]" //Rejected messages will be indicated by red color. + result = token //Token caused rejection (if there are multiple, last will be chosen>. + pda_msgs += new/datum/data_pda_msg(recipient,sender,message) + return result + +/obj/machinery/message_server/proc/send_rc_message(var/recipient = "",var/sender = "",var/message = "",var/stamp = "", var/id_auth = "", var/priority = 1) + rc_msgs += new/datum/data_rc_msg(recipient,sender,message,stamp,id_auth) + var/authmsg = "[message]\n" + if (id_auth) + authmsg += "([id_auth])\n" + if (stamp) + authmsg += "([stamp])\n" + for (var/obj/machinery/requests_console/Console in allConsoles) + if (ckey(Console.department) == ckey(recipient)) + if(Console.inoperable()) + Console.message_log += list(list("Message lost due to console failure.","Please contact [station_name()] system adminsitrator or AI for technical assistance.")) + continue + if(Console.newmessagepriority < priority) + Console.newmessagepriority = priority + Console.icon_state = "req_comp[priority]" + switch(priority) + if(2) + if(!Console.silent) + playsound(Console, 'sound/machines/twobeep.ogg', 50, 1) + Console.audible_message(text("\icon[Console][bicon(Console)] *The Requests Console beeps: 'PRIORITY Alert in [sender]'"),,5, runemessage = "beep! beep!") + Console.message_log += list(list("High Priority message from [sender]", "[authmsg]")) + else + if(!Console.silent) + playsound(Console, 'sound/machines/twobeep.ogg', 50, 1) + Console.audible_message(text("\icon[Console][bicon(Console)] *The Requests Console beeps: 'Message from [sender]'"),,4, runemessage = "beep beep") + Console.message_log += list(list("Message from [sender]", "[authmsg]")) + Console.set_light(2) + + +/obj/machinery/message_server/attack_hand(user as mob) +// to_chat(user, "There seem to be some parts missing from this server. They should arrive on the station in a few days, give or take a few CentCom delays.") + to_chat(user, "You toggle PDA message passing from [active ? "On" : "Off"] to [active ? "Off" : "On"].") + active = !active + update_icon() + + return + +/obj/machinery/message_server/attackby(obj/item/weapon/O as obj, mob/living/user as mob) + if (active && !(stat & (BROKEN|NOPOWER)) && (spamfilter_limit < MESSAGE_SERVER_DEFAULT_SPAM_LIMIT*2) && \ + istype(O,/obj/item/weapon/circuitboard/message_monitor)) + spamfilter_limit += round(MESSAGE_SERVER_DEFAULT_SPAM_LIMIT / 2) + user.drop_item() + qdel(O) + to_chat(user, "You install additional memory and processors into message server. Its filtering capabilities been enhanced.") + else + ..(O, user) + +/obj/machinery/message_server/update_icon() + if((stat & (BROKEN|NOPOWER))) + icon_state = "server-nopower" + else if (!active) + icon_state = "server-off" + else + icon_state = "server-on" + + return + + +/datum/feedback_variable + var/variable + var/value + var/details + +/datum/feedback_variable/vv_edit_var(var_name, var_value) + if(var_name == NAMEOF(src, variable) || var_name == NAMEOF(src, value) || var_name == NAMEOF(src, details)) + return FALSE + return ..() + +/datum/feedback_variable/New(var/param_variable,var/param_value = 0) + variable = param_variable + value = param_value + +/datum/feedback_variable/proc/inc(var/num = 1) + if(isnum(value)) + value += num + else + value = text2num(value) + if(isnum(value)) + value += num + else + value = num + +/datum/feedback_variable/proc/dec(var/num = 1) + if(isnum(value)) + value -= num + else + value = text2num(value) + if(isnum(value)) + value -= num + else + value = -num + +/datum/feedback_variable/proc/set_value(var/num) + if(isnum(num)) + value = num + +/datum/feedback_variable/proc/get_value() + return value + +/datum/feedback_variable/proc/get_variable() + return variable + +/datum/feedback_variable/proc/set_details(var/text) + if(istext(text)) + details = text + +/datum/feedback_variable/proc/add_details(var/text) + if(istext(text)) + if(!details) + details = text + else + details += " [text]" + +/datum/feedback_variable/proc/get_details() + return details + +/datum/feedback_variable/proc/get_parsed() + return list(variable,value,details) + +var/obj/machinery/blackbox_recorder/blackbox + +/obj/machinery/blackbox_recorder + icon = 'icons/obj/stationobjs.dmi' + icon_state = "blackbox" + name = "Blackbox Recorder" + desc = "Records all radio communications, as well as various other information in case of the worst." + density = TRUE + anchored = TRUE + unacidable = TRUE + use_power = USE_POWER_IDLE + idle_power_usage = 10 + active_power_usage = 100 + var/list/messages = list() //Stores messages of non-standard frequencies + var/list/messages_admin = list() + + var/list/msg_common = list() + var/list/msg_science = list() + var/list/msg_command = list() + var/list/msg_medical = list() + var/list/msg_engineering = list() + var/list/msg_security = list() + var/list/msg_deathsquad = list() + var/list/msg_syndicate = list() + var/list/msg_raider = list() + var/list/msg_cargo = list() + var/list/msg_service = list() + var/list/msg_explorer = list() + + var/list/datum/feedback_variable/feedback = new() + + //Only one can exist in the world! +/obj/machinery/blackbox_recorder/New() + if(blackbox) + if(istype(blackbox,/obj/machinery/blackbox_recorder)) + qdel(src) + blackbox = src + +/obj/machinery/blackbox_recorder/Destroy() + var/turf/T = locate(1,1,2) + if(T) + blackbox = null + var/obj/machinery/blackbox_recorder/BR = new/obj/machinery/blackbox_recorder(T) + BR.msg_common = msg_common + BR.msg_science = msg_science + BR.msg_command = msg_command + BR.msg_medical = msg_medical + BR.msg_engineering = msg_engineering + BR.msg_security = msg_security + BR.msg_deathsquad = msg_deathsquad + BR.msg_syndicate = msg_syndicate + BR.msg_cargo = msg_cargo + BR.msg_service = msg_service + BR.feedback = feedback + BR.messages = messages + BR.messages_admin = messages_admin + if(blackbox != BR) + blackbox = BR + ..() + +/obj/machinery/blackbox_recorder/proc/find_feedback_datum(var/variable) + for(var/datum/feedback_variable/FV in feedback) + if(FV.get_variable() == variable) + return FV + var/datum/feedback_variable/FV = new(variable) + feedback += FV + return FV + +/obj/machinery/blackbox_recorder/proc/get_round_feedback() + return feedback + +/obj/machinery/blackbox_recorder/proc/round_end_data_gathering() + + var/pda_msg_amt = 0 + var/rc_msg_amt = 0 + + for(var/obj/machinery/message_server/MS in machines) + if(MS.pda_msgs.len > pda_msg_amt) + pda_msg_amt = MS.pda_msgs.len + if(MS.rc_msgs.len > rc_msg_amt) + rc_msg_amt = MS.rc_msgs.len + + feedback_set_details("radio_usage","") + + feedback_add_details("radio_usage","COM-[msg_common.len]") + feedback_add_details("radio_usage","SCI-[msg_science.len]") + feedback_add_details("radio_usage","HEA-[msg_command.len]") + feedback_add_details("radio_usage","MED-[msg_medical.len]") + feedback_add_details("radio_usage","ENG-[msg_engineering.len]") + feedback_add_details("radio_usage","SEC-[msg_security.len]") + feedback_add_details("radio_usage","DTH-[msg_deathsquad.len]") + feedback_add_details("radio_usage","SYN-[msg_syndicate.len]") + feedback_add_details("radio_usage","CAR-[msg_cargo.len]") + feedback_add_details("radio_usage","SRV-[msg_service.len]") + feedback_add_details("radio_usage","OTH-[messages.len]") + feedback_add_details("radio_usage","PDA-[pda_msg_amt]") + feedback_add_details("radio_usage","RC-[rc_msg_amt]") + + + feedback_set_details("round_end","[time2text(world.realtime)]") //This one MUST be the last one that gets set. + +/obj/machinery/blackbox_recorder/vv_edit_var(var_name, var_value) + var/static/list/blocked_vars //hacky as fuck kill me + if(!blocked_vars) + var/obj/machinery/M = new + var/list/parent_vars = M.vars.Copy() + blocked_vars = vars.Copy() - parent_vars + if(var_name in blocked_vars) + return FALSE + return ..() + +//This proc is only to be called at round end. +/obj/machinery/blackbox_recorder/proc/save_all_data_to_sql() + if(!feedback) return + + round_end_data_gathering() //round_end time logging and some other data processing + establish_db_connection() + if(!SSdbcore.IsConnected()) return //CHOMPEdit TGSQL + var/round_id + + var/DBQuery/query = SSdbcore.NewQuery("SELECT MAX(round_id) AS round_id FROM erro_feedback") //CHOMPEdit TGSQL + query.Execute() + while(query.NextRow()) + round_id = query.item[1] + qdel(query) //CHOMPEdit TGSQL + if(!isnum(round_id)) + round_id = text2num(round_id) + round_id++ + + for(var/datum/feedback_variable/FV in feedback) + var/list/sqlargs = list("t_roundid" = round_id, "t_variable" = "[FV.get_variable()]", "t_value" = "[FV.get_value()]", "t_details" = "[FV.get_details()]") //CHOMPEdit TGSQL + var/sql = "INSERT INTO erro_feedback VALUES (null, Now(), :t_roundid, :t_variable, :t_value, :t_details)" //CHOMPEdit TGSQL + var/DBQuery/query_insert = SSdbcore.NewQuery(sql, sqlargs) //CHOMPEdit TGSQL + query_insert.Execute() + qdel(query_insert) //CHOMPEdit TGSQL + +// Sanitize inputs to avoid SQL injection attacks //CHOMPEdit NOTE: This is not secure. Basic filters like this are pretty easy to bypass. Use the format for arguments used in the above. +/proc/sql_sanitize_text(var/text) + text = replacetext(text, "'", "''") + text = replacetext(text, ";", "") + text = replacetext(text, "&", "") + return text + +/proc/feedback_set(var/variable,var/value) + if(!blackbox) return + + variable = sql_sanitize_text(variable) + + var/datum/feedback_variable/FV = blackbox.find_feedback_datum(variable) + + if(!FV) return + + FV.set_value(value) + +/proc/feedback_inc(var/variable,var/value) + if(!blackbox) return + + variable = sql_sanitize_text(variable) + + var/datum/feedback_variable/FV = blackbox.find_feedback_datum(variable) + + if(!FV) return + + FV.inc(value) + +/proc/feedback_dec(var/variable,var/value) + if(!blackbox) return + + variable = sql_sanitize_text(variable) + + var/datum/feedback_variable/FV = blackbox.find_feedback_datum(variable) + + if(!FV) return + + FV.dec(value) + +/proc/feedback_set_details(var/variable,var/details) + if(!blackbox) return + + variable = sql_sanitize_text(variable) + details = sql_sanitize_text(details) + + var/datum/feedback_variable/FV = blackbox.find_feedback_datum(variable) + + if(!FV) return + + FV.set_details(details) + +/proc/feedback_add_details(var/variable,var/details) + if(!blackbox) return + + variable = sql_sanitize_text(variable) + details = sql_sanitize_text(details) + + var/datum/feedback_variable/FV = blackbox.find_feedback_datum(variable) + + if(!FV) return + + FV.add_details(details) diff --git a/code/modules/vore/resizing/sizegun_slow_vr.dm b/code/modules/vore/resizing/sizegun_slow_vr.dm index 648c0f91c7..9a993e7a87 100644 --- a/code/modules/vore/resizing/sizegun_slow_vr.dm +++ b/code/modules/vore/resizing/sizegun_slow_vr.dm @@ -3,7 +3,7 @@ /obj/item/device/slow_sizegun name = "gradual size gun" - desc = "A highly advanced ray gun, designed for progressive and gradual changing of size." + desc = "A highly advanced ray gun, designed for progressive and gradual changing of size. Size trading can be toggled on via alt-clicking." icon = 'icons/obj/gun_vr.dmi' icon_state = "sizegun-old-0" var/base_icon_state = "sizegun-old" @@ -17,6 +17,7 @@ var/dorm_size = TRUE var/size_increment = 0.01 var/current_target + var/trading = 0 /obj/item/device/slow_sizegun/update_icon() icon_state = "[base_icon_state]-[sizeshift_mode]" @@ -50,6 +51,9 @@ if(unresizable) return TRUE + if(trading == 1 && !(user.resizable)) + return TRUE + if(!(target.has_large_resize_bounds()) && (target.size_multiplier >= RESIZE_MAXIMUM) && sizeshift_mode == SIZE_GROW) return TRUE @@ -62,6 +66,18 @@ if(target.size_multiplier <= RESIZE_MINIMUM_DORMS && sizeshift_mode == SIZE_SHRINK) return TRUE + if(trading == 1 && !(user.has_large_resize_bounds()) && (user.size_multiplier >= RESIZE_MAXIMUM) && sizeshift_mode == SIZE_GROW) + return TRUE + + if(trading == 1 && user.size_multiplier >= RESIZE_MAXIMUM_DORMS && sizeshift_mode == SIZE_GROW) + return TRUE + + if(trading == 1 && !(user.has_large_resize_bounds()) && (user.size_multiplier <= RESIZE_MINIMUM) && sizeshift_mode == SIZE_SHRINK) + return TRUE + + if(trading == 1 && user.size_multiplier <= RESIZE_MINIMUM_DORMS && sizeshift_mode == SIZE_SHRINK) + return TRUE + return FALSE /obj/item/device/slow_sizegun/afterattack(atom/target, mob/user, proximity_flag) @@ -81,6 +97,7 @@ return var/mob/living/L = target + var/mob/living/U = user if(get_dist(target, user) > beam_range) to_chat(user, span("warning", "You are too far away from \the [target] to affect it. Get closer.")) @@ -100,6 +117,9 @@ if(!(L.resizable)) unresizable = TRUE + if(trading == 1 && !(user.resizable)) + unresizable = TRUE + if(unresizable) to_chat(user, span("warning", "\the [target] is immune to resizing.")) @@ -119,14 +139,25 @@ var/active_hand = user.get_active_hand() - while(!should_stop(target, user, active_hand)) - stoplag(3) + if (trading == 0) + while(!should_stop(target, user, active_hand)) + stoplag(3) - if(sizeshift_mode == SIZE_SHRINK) - L.resize((L.size_multiplier - size_increment), uncapped = L.has_large_resize_bounds(), aura_animation = FALSE) - else if(sizeshift_mode == SIZE_GROW) - L.resize((L.size_multiplier + size_increment), uncapped = L.has_large_resize_bounds(), aura_animation = FALSE) + if(sizeshift_mode == SIZE_SHRINK) + L.resize((L.size_multiplier - size_increment), uncapped = L.has_large_resize_bounds(), aura_animation = FALSE) + else if(sizeshift_mode == SIZE_GROW) + L.resize((L.size_multiplier + size_increment), uncapped = L.has_large_resize_bounds(), aura_animation = FALSE) + if (trading == 1) + while(!should_stop(target, user, active_hand)) + stoplag(3) + + if(sizeshift_mode == SIZE_SHRINK) + L.resize((L.size_multiplier - size_increment), uncapped = L.has_large_resize_bounds(), aura_animation = FALSE) + U.resize((U.size_multiplier + size_increment), uncapped = U.has_large_resize_bounds(), aura_animation = FALSE) + else if(sizeshift_mode == SIZE_GROW) + L.resize((L.size_multiplier + size_increment), uncapped = L.has_large_resize_bounds(), aura_animation = FALSE) + U.resize((U.size_multiplier - size_increment), uncapped = U.has_large_resize_bounds(), aura_animation = FALSE) busy = FALSE current_target = null @@ -219,3 +250,13 @@ /obj/item/device/slow_sizegun/proc/color_box(list/box_segments, new_color, new_time) for(var/i in box_segments) animate(i, color = new_color, time = new_time) + +//Alt click to activate size trading + +/obj/item/device/slow_sizegun/AltClick(mob/user) + if (trading == 0) + trading = 1 + to_chat(user, span("notice", "\The [src] will now trade your targets size for your own.")) + else + trading = 0 + to_chat(user, span("notice", "\The [src] will no longer trade your targets size for your own.")) diff --git a/code/modules/vore/resizing/sizegun_vr.dm b/code/modules/vore/resizing/sizegun_vr.dm index c5fc1f3648..982d5185f9 100644 --- a/code/modules/vore/resizing/sizegun_vr.dm +++ b/code/modules/vore/resizing/sizegun_vr.dm @@ -26,11 +26,20 @@ /obj/item/weapon/gun/energy/sizegun/New() ..() verbs += PROC_REF(select_size) + verbs += PROC_REF(spin_dial) /obj/item/weapon/gun/energy/sizegun/attack_self(mob/user) . = ..() select_size() +/obj/item/weapon/gun/energy/sizegun/proc/spin_dial() + set name = "Spin Size Dial" + set category = "Object" + set src in view(1) + + size_set_to = (rand(25,200)) /100 + usr.visible_message("\The [usr] spins the size dial to a random value!","You spin the dial to a random value!") + /obj/item/weapon/gun/energy/sizegun/consume_next_projectile() . = ..() var/obj/item/projectile/beam/sizelaser/G = . diff --git a/code/modules/xgm/xgm_gas_mixture.dm b/code/modules/xgm/xgm_gas_mixture.dm index fd52efca22..a89d2d8c35 100644 --- a/code/modules/xgm/xgm_gas_mixture.dm +++ b/code/modules/xgm/xgm_gas_mixture.dm @@ -1,490 +1,490 @@ -/datum/gas_mixture - //Associative list of gas moles. - //Gases with 0 moles are not tracked and are pruned by update_values() - var/list/gas - //Temperature in Kelvin of this gas mix. - var/temperature = 0 - - //Sum of all the gas moles in this mix. Updated by update_values() - var/total_moles = 0 - //Volume of this mix. - var/volume = CELL_VOLUME - //Size of the group this gas_mixture is representing. 1 for singletons. - var/group_multiplier = 1 - - //List of active tile overlays for this gas_mixture. Updated by check_tile_graphic() - var/list/graphic - -/datum/gas_mixture/New(vol = CELL_VOLUME) - volume = vol - gas = list() - -//Takes a gas string and the amount of moles to adjust by. Calls update_values() if update isn't 0. -/datum/gas_mixture/proc/adjust_gas(gasid, moles, update = 1) - if(moles == 0) - return - - if (group_multiplier != 1) - gas[gasid] += moles/group_multiplier - else - gas[gasid] += moles - - if(update) - update_values() - - -//Same as adjust_gas(), but takes a temperature which is mixed in with the gas. -/datum/gas_mixture/proc/adjust_gas_temp(gasid, moles, temp, update = 1) - if(moles == 0) - return - - if(moles > 0 && abs(temperature - temp) > MINIMUM_TEMPERATURE_DELTA_TO_CONSIDER) - var/self_heat_capacity = heat_capacity() - var/giver_heat_capacity = gas_data.specific_heat[gasid] * moles - var/combined_heat_capacity = giver_heat_capacity + self_heat_capacity - if(combined_heat_capacity != 0) - temperature = (temp * giver_heat_capacity + temperature * self_heat_capacity) / combined_heat_capacity - - if (group_multiplier != 1) - gas[gasid] += moles/group_multiplier - else - gas[gasid] += moles - - if(update) - update_values() - - -//Variadic version of adjust_gas(). Takes any number of gas and mole pairs and applies them. -/datum/gas_mixture/proc/adjust_multi() - ASSERT(!(args.len % 2)) - - for(var/i = 1; i < args.len; i += 2) - adjust_gas(args[i], args[i+1], update = 0) - - update_values() - - -//Variadic version of adjust_gas_temp(). Takes any number of gas, mole and temperature associations and applies them. -/datum/gas_mixture/proc/adjust_multi_temp() - ASSERT(!(args.len % 3)) - - for(var/i = 1; i < args.len; i += 3) - adjust_gas_temp(args[i], args[i + 1], args[i + 2], update = 0) - - update_values() - - -//Merges all the gas from another mixture into this one. Respects group_multipliers and adjusts temperature correctly. -//Does not modify giver in any way. -/datum/gas_mixture/proc/merge(const/datum/gas_mixture/giver) - if(!giver) - return - - if(abs(temperature-giver.temperature)>MINIMUM_TEMPERATURE_DELTA_TO_CONSIDER) - var/self_heat_capacity = heat_capacity() - var/giver_heat_capacity = giver.heat_capacity() - var/combined_heat_capacity = giver_heat_capacity + self_heat_capacity - if(combined_heat_capacity != 0) - temperature = (giver.temperature*giver_heat_capacity + temperature*self_heat_capacity)/combined_heat_capacity - - if((group_multiplier != 1)||(giver.group_multiplier != 1)) - for(var/g in giver.gas) - gas[g] += giver.gas[g] * giver.group_multiplier / group_multiplier - else - for(var/g in giver.gas) - gas[g] += giver.gas[g] - - update_values() - - -// Used to equalize the mixture between two zones before sleeping an edge. -/datum/gas_mixture/proc/equalize(datum/gas_mixture/sharer) - var/our_heatcap = heat_capacity() - var/share_heatcap = sharer.heat_capacity() - - // Special exception: there isn't enough air around to be worth processing this edge next tick, zap both to zero. - if(total_moles + sharer.total_moles <= MINIMUM_AIR_TO_SUSPEND) - gas.Cut() - sharer.gas.Cut() - - for(var/g in gas|sharer.gas) - var/comb = gas[g] + sharer.gas[g] - comb /= volume + sharer.volume - gas[g] = comb * volume - sharer.gas[g] = comb * sharer.volume - - if(our_heatcap + share_heatcap) - temperature = ((temperature * our_heatcap) + (sharer.temperature * share_heatcap)) / (our_heatcap + share_heatcap) - sharer.temperature = temperature - - update_values() - sharer.update_values() - - return 1 - - -//Returns the heat capacity of the gas mix based on the specific heat of the gases. -/datum/gas_mixture/proc/heat_capacity() - . = 0 - for(var/g in gas) - . += gas_data.specific_heat[g] * gas[g] - . *= group_multiplier - - -//Adds or removes thermal energy. Returns the actual thermal energy change, as in the case of removing energy we can't go below TCMB. -/datum/gas_mixture/proc/add_thermal_energy(var/thermal_energy) - if (total_moles == 0) - return 0 - - var/heat_capacity = heat_capacity() - if (thermal_energy < 0) - if (temperature < TCMB) - return 0 - var/thermal_energy_limit = -(temperature - TCMB)*heat_capacity //ensure temperature does not go below TCMB - thermal_energy = max( thermal_energy, thermal_energy_limit ) //thermal_energy and thermal_energy_limit are negative here. - temperature += thermal_energy/heat_capacity - return thermal_energy - -//Returns the thermal energy change required to get to a new temperature -/datum/gas_mixture/proc/get_thermal_energy_change(var/new_temperature) - return heat_capacity()*(max(new_temperature, 0) - temperature) - - -//Technically vacuum doesn't have a specific entropy. Just use a really big number (infinity would be ideal) here so that it's easy to add gas to vacuum and hard to take gas out. -#define SPECIFIC_ENTROPY_VACUUM 150000 - - -//Returns the ideal gas specific entropy of the whole mix. This is the entropy per mole of /mixed/ gas. -/datum/gas_mixture/proc/specific_entropy() - if (!gas.len || total_moles == 0) - return SPECIFIC_ENTROPY_VACUUM - - . = 0 - for(var/g in gas) - . += gas[g] * specific_entropy_gas(g) - . /= total_moles - - -/* - It's arguable whether this should even be called entropy anymore. It's more "based on" entropy than actually entropy now. - - Returns the ideal gas specific entropy of a specific gas in the mix. This is the entropy due to that gas per mole of /that/ gas in the mixture, not the entropy due to that gas per mole of gas mixture. - - For the purposes of SS13, the specific entropy is just a number that tells you how hard it is to move gas. You can replace this with whatever you want. - Just remember that returning a SMALL number == adding gas to this gas mix is HARD, taking gas away is EASY, and that returning a LARGE number means the opposite (so a vacuum should approach infinity). - - So returning a constant/(partial pressure) would probably do what most players expect. Although the version I have implemented below is a bit more nuanced than simply 1/P in that it scales in a way - which is bit more realistic (natural log), and returns a fairly accurate entropy around room temperatures and pressures. -*/ -/datum/gas_mixture/proc/specific_entropy_gas(var/gasid) - if (!(gasid in gas) || gas[gasid] == 0) - return SPECIFIC_ENTROPY_VACUUM //that gas isn't here - - //group_multiplier gets divided out in volume/gas[gasid] - also, V/(m*T) = R/(partial pressure) - var/molar_mass = gas_data.molar_mass[gasid] - var/specific_heat = gas_data.specific_heat[gasid] - return R_IDEAL_GAS_EQUATION * ( log( (IDEAL_GAS_ENTROPY_CONSTANT*volume/(gas[gasid] * temperature)) * (molar_mass*specific_heat*temperature)**(2/3) + 1 ) + 15 ) - - //alternative, simpler equation - //var/partial_pressure = gas[gasid] * R_IDEAL_GAS_EQUATION * temperature / volume - //return R_IDEAL_GAS_EQUATION * ( log (1 + IDEAL_GAS_ENTROPY_CONSTANT/partial_pressure) + 20 ) - - -//Updates the total_moles count and trims any empty gases. -/datum/gas_mixture/proc/update_values() - total_moles = 0 - for(var/g in gas) - if(gas[g] <= 0) - gas -= g - else - total_moles += gas[g] - - -//Returns the pressure of the gas mix. Only accurate if there have been no gas modifications since update_values() has been called. -/datum/gas_mixture/proc/return_pressure() - if(volume) - return total_moles * R_IDEAL_GAS_EQUATION * temperature / volume - return 0 - - -//Removes moles from the gas mixture and returns a gas_mixture containing the removed air. -/datum/gas_mixture/proc/remove(amount) - amount = min(amount, total_moles * group_multiplier) //Can not take more air than the gas mixture has! - if(amount <= 0) - return null - - var/datum/gas_mixture/removed = new - - for(var/g in gas) - removed.gas[g] = QUANTIZE((gas[g] / total_moles) * amount) - gas[g] -= removed.gas[g] / group_multiplier - - removed.temperature = temperature - update_values() - removed.update_values() - - return removed - - -//Removes a ratio of gas from the mixture and returns a gas_mixture containing the removed air. -/datum/gas_mixture/proc/remove_ratio(ratio, out_group_multiplier = 1) - if(ratio <= 0) - return null - out_group_multiplier = between(1, out_group_multiplier, group_multiplier) - - ratio = min(ratio, 1) - - var/datum/gas_mixture/removed = new - removed.group_multiplier = out_group_multiplier - - for(var/g in gas) - removed.gas[g] = (gas[g] * ratio * group_multiplier / out_group_multiplier) - gas[g] = gas[g] * (1 - ratio) - - removed.temperature = temperature - removed.volume = volume * group_multiplier / out_group_multiplier - update_values() - removed.update_values() - - return removed - -//Removes a volume of gas from the mixture and returns a gas_mixture containing the removed air with the given volume -/datum/gas_mixture/proc/remove_volume(removed_volume) - var/datum/gas_mixture/removed = remove_ratio(removed_volume/(volume*group_multiplier), 1) - removed.volume = removed_volume - return removed - -//Removes moles from the gas mixture, limited by a given flag. Returns a gax_mixture containing the removed air. -/datum/gas_mixture/proc/remove_by_flag(flag, amount) - if(!flag || amount <= 0) - return - - var/sum = 0 - for(var/g in gas) - if(gas_data.flags[g] & flag) - sum += gas[g] - - var/datum/gas_mixture/removed = new - - for(var/g in gas) - if(gas_data.flags[g] & flag) - removed.gas[g] = QUANTIZE((gas[g] / sum) * amount) - gas[g] -= removed.gas[g] / group_multiplier - - removed.temperature = temperature - update_values() - removed.update_values() - - return removed - -//Returns the amount of gas that has the given flag, in moles -/datum/gas_mixture/proc/get_by_flag(flag) - . = 0 - for(var/g in gas) - if(gas_data.flags[g] & flag) - . += gas[g] - -//Copies gas and temperature from another gas_mixture. -/datum/gas_mixture/proc/copy_from(const/datum/gas_mixture/sample) - gas = sample.gas.Copy() - temperature = sample.temperature - - update_values() - - return 1 - - -//Checks if we are within acceptable range of another gas_mixture to suspend processing or merge. -/datum/gas_mixture/proc/compare(const/datum/gas_mixture/sample, var/vacuum_exception = 0) - if(!sample) return 0 - - if(vacuum_exception) - // Special case - If one of the two is zero pressure, the other must also be zero. - // This prevents suspending processing when an air-filled room is next to a vacuum, - // an edge case which is particually obviously wrong to players - if(total_moles == 0 && sample.total_moles != 0 || sample.total_moles == 0 && total_moles != 0) - return 0 - - var/list/marked = list() - for(var/g in gas) - if((abs(gas[g] - sample.gas[g]) > MINIMUM_AIR_TO_SUSPEND) && \ - ((gas[g] < (1 - MINIMUM_AIR_RATIO_TO_SUSPEND) * sample.gas[g]) || \ - (gas[g] > (1 + MINIMUM_AIR_RATIO_TO_SUSPEND) * sample.gas[g]))) - return 0 - marked[g] = 1 - - for(var/g in sample.gas) - if(!marked[g]) - if((abs(gas[g] - sample.gas[g]) > MINIMUM_AIR_TO_SUSPEND) && \ - ((gas[g] < (1 - MINIMUM_AIR_RATIO_TO_SUSPEND) * sample.gas[g]) || \ - (gas[g] > (1 + MINIMUM_AIR_RATIO_TO_SUSPEND) * sample.gas[g]))) - return 0 - - if(total_moles > MINIMUM_AIR_TO_SUSPEND) - if((abs(temperature - sample.temperature) > MINIMUM_TEMPERATURE_DELTA_TO_SUSPEND) && \ - ((temperature < (1 - MINIMUM_TEMPERATURE_RATIO_TO_SUSPEND)*sample.temperature) || \ - (temperature > (1 + MINIMUM_TEMPERATURE_RATIO_TO_SUSPEND)*sample.temperature))) - return 0 - - return 1 - - -/datum/gas_mixture/proc/react() - zburn(null, force_burn=0, no_check=0) //could probably just call zburn() here with no args but I like being explicit. - - -//Rechecks the gas_mixture and adjusts the graphic list if needed. -//Two lists can be passed by reference if you need know specifically which graphics were added and removed. -/datum/gas_mixture/proc/check_tile_graphic(list/graphic_add = null, list/graphic_remove = null) - var/list/cur_graphic = graphic // Cache for sanic speed - for(var/g in gas_data.overlay_limit) - if(cur_graphic && cur_graphic.Find(gas_data.tile_overlay[g])) - //Overlay is already applied for this gas, check if it's still valid. - if(gas[g] <= gas_data.overlay_limit[g]) - LAZYADD(graphic_remove, gas_data.tile_overlay[g]) - else - //Overlay isn't applied for this gas, check if it's valid and needs to be added. - if(gas[g] > gas_data.overlay_limit[g]) - LAZYADD(graphic_add, gas_data.tile_overlay[g]) - - . = 0 - //Apply changes - if(LAZYLEN(graphic_add)) - LAZYADD(graphic, graphic_add) - . = 1 - if(LAZYLEN(graphic_remove)) - LAZYREMOVE(graphic, graphic_remove) - . = 1 - - -//Simpler version of merge(), adjusts gas amounts directly and doesn't account for temperature or group_multiplier. -/datum/gas_mixture/proc/add(datum/gas_mixture/right_side) - for(var/g in right_side.gas) - gas[g] += right_side.gas[g] - - update_values() - return 1 - - -//Simpler version of remove(), adjusts gas amounts directly and doesn't account for group_multiplier. -/datum/gas_mixture/proc/subtract(datum/gas_mixture/right_side) - for(var/g in right_side.gas) - gas[g] -= right_side.gas[g] - - update_values() - return 1 - - -//Multiply all gas amounts by a factor. -/datum/gas_mixture/proc/multiply(factor) - for(var/g in gas) - gas[g] *= factor - - update_values() - return 1 - - -//Divide all gas amounts by a factor. -/datum/gas_mixture/proc/divide(factor) - for(var/g in gas) - gas[g] /= factor - - update_values() - return 1 - - -//Shares gas with another gas_mixture based on the amount of connecting tiles and a fixed lookup table. -/datum/gas_mixture/proc/share_ratio(datum/gas_mixture/other, connecting_tiles, share_size = null, one_way = 0) - var/static/list/sharing_lookup_table = list(0.30, 0.40, 0.48, 0.54, 0.60, 0.66) - //Shares a specific ratio of gas between mixtures using simple weighted averages. - var/ratio = sharing_lookup_table[6] - - var/size = max(1, group_multiplier) - if(isnull(share_size)) share_size = max(1, other.group_multiplier) - - var/full_heat_capacity = heat_capacity() - var/s_full_heat_capacity = other.heat_capacity() - - var/list/avg_gas = list() - - for(var/g in gas) - avg_gas[g] += gas[g] * size - - for(var/g in other.gas) - avg_gas[g] += other.gas[g] * share_size - - for(var/g in avg_gas) - avg_gas[g] /= (size + share_size) - - var/temp_avg = 0 - if(full_heat_capacity + s_full_heat_capacity) - temp_avg = (temperature * full_heat_capacity + other.temperature * s_full_heat_capacity) / (full_heat_capacity + s_full_heat_capacity) - - //WOOT WOOT TOUCH THIS AND YOU ARE A MISCHIEVIOUS LITTLE ELF - if(sharing_lookup_table.len >= connecting_tiles) //6 or more interconnecting tiles will max at 42% of air moved per tick. - ratio = sharing_lookup_table[connecting_tiles] - //WOOT WOOT TOUCH THIS AND YOU ARE A MISCHIEVIOUS LITTLE ELF - - for(var/g in avg_gas) - gas[g] = max(0, (gas[g] - avg_gas[g]) * (1 - ratio) + avg_gas[g]) - if(!one_way) - other.gas[g] = max(0, (other.gas[g] - avg_gas[g]) * (1 - ratio) + avg_gas[g]) - - temperature = max(0, (temperature - temp_avg) * (1-ratio) + temp_avg) - if(!one_way) - other.temperature = max(0, (other.temperature - temp_avg) * (1-ratio) + temp_avg) - - update_values() - other.update_values() - - return compare(other) - - -//A wrapper around share_ratio for spacing gas at the same rate as if it were going into a large airless room. -/datum/gas_mixture/proc/share_space(datum/gas_mixture/unsim_air) - return share_ratio(unsim_air, unsim_air.group_multiplier, max(1, max(group_multiplier + 3, 1) + unsim_air.group_multiplier), one_way = 1) - - -//Equalizes a list of gas mixtures. Used for pipe networks. -/proc/equalize_gases(list/datum/gas_mixture/gases) - //Calculate totals from individual components - var/total_volume = 0 - var/total_thermal_energy = 0 - var/total_heat_capacity = 0 - - var/list/total_gas = list() - for(var/datum/gas_mixture/gasmix in gases) - total_volume += gasmix.volume - var/temp_heatcap = gasmix.heat_capacity() - total_thermal_energy += gasmix.temperature * temp_heatcap - total_heat_capacity += temp_heatcap - for(var/g in gasmix.gas) - total_gas[g] += gasmix.gas[g] - - if(total_volume > 0) - var/datum/gas_mixture/combined = new(total_volume) - combined.gas = total_gas - - //Calculate temperature - if(total_heat_capacity > 0) - combined.temperature = total_thermal_energy / total_heat_capacity - combined.update_values() - - //Allow for reactions - combined.react() - - //Average out the gases - for(var/g in combined.gas) - combined.gas[g] /= total_volume - - //Update individual gas_mixtures - for(var/datum/gas_mixture/gasmix in gases) - gasmix.gas = combined.gas.Copy() - gasmix.temperature = combined.temperature - gasmix.multiply(gasmix.volume) - - return 1 - -/datum/gas_mixture/proc/get_mass() - for(var/g in gas) - . += gas[g] * gas_data.molar_mass[g] * group_multiplier +/datum/gas_mixture + //Associative list of gas moles. + //Gases with 0 moles are not tracked and are pruned by update_values() + var/list/gas + //Temperature in Kelvin of this gas mix. + var/temperature = 0 + + //Sum of all the gas moles in this mix. Updated by update_values() + var/total_moles = 0 + //Volume of this mix. + var/volume = CELL_VOLUME + //Size of the group this gas_mixture is representing. 1 for singletons. + var/group_multiplier = 1 + + //List of active tile overlays for this gas_mixture. Updated by check_tile_graphic() + var/list/graphic + +/datum/gas_mixture/New(vol = CELL_VOLUME) + volume = vol + gas = list() + +//Takes a gas string and the amount of moles to adjust by. Calls update_values() if update isn't 0. +/datum/gas_mixture/proc/adjust_gas(gasid, moles, update = 1) + if(moles == 0) + return + + if (group_multiplier != 1) + gas[gasid] += moles/group_multiplier + else + gas[gasid] += moles + + if(update) + update_values() + + +//Same as adjust_gas(), but takes a temperature which is mixed in with the gas. +/datum/gas_mixture/proc/adjust_gas_temp(gasid, moles, temp, update = 1) + if(moles == 0) + return + + if(moles > 0 && abs(temperature - temp) > MINIMUM_TEMPERATURE_DELTA_TO_CONSIDER) + var/self_heat_capacity = heat_capacity() + var/giver_heat_capacity = gas_data.specific_heat[gasid] * moles + var/combined_heat_capacity = giver_heat_capacity + self_heat_capacity + if(combined_heat_capacity != 0) + temperature = (temp * giver_heat_capacity + temperature * self_heat_capacity) / combined_heat_capacity + + if (group_multiplier != 1) + gas[gasid] += moles/group_multiplier + else + gas[gasid] += moles + + if(update) + update_values() + + +//Variadic version of adjust_gas(). Takes any number of gas and mole pairs and applies them. +/datum/gas_mixture/proc/adjust_multi() + ASSERT(!(args.len % 2)) + + for(var/i = 1; i < args.len; i += 2) + adjust_gas(args[i], args[i+1], update = 0) + + update_values() + + +//Variadic version of adjust_gas_temp(). Takes any number of gas, mole and temperature associations and applies them. +/datum/gas_mixture/proc/adjust_multi_temp() + ASSERT(!(args.len % 3)) + + for(var/i = 1; i < args.len; i += 3) + adjust_gas_temp(args[i], args[i + 1], args[i + 2], update = 0) + + update_values() + + +//Merges all the gas from another mixture into this one. Respects group_multipliers and adjusts temperature correctly. +//Does not modify giver in any way. +/datum/gas_mixture/proc/merge(const/datum/gas_mixture/giver) + if(!giver) + return + + if(abs(temperature-giver.temperature)>MINIMUM_TEMPERATURE_DELTA_TO_CONSIDER) + var/self_heat_capacity = heat_capacity() + var/giver_heat_capacity = giver.heat_capacity() + var/combined_heat_capacity = giver_heat_capacity + self_heat_capacity + if(combined_heat_capacity != 0) + temperature = (giver.temperature*giver_heat_capacity + temperature*self_heat_capacity)/combined_heat_capacity + + if((group_multiplier != 1)||(giver.group_multiplier != 1)) + for(var/g in giver.gas) + gas[g] += giver.gas[g] * giver.group_multiplier / group_multiplier + else + for(var/g in giver.gas) + gas[g] += giver.gas[g] + + update_values() + + +// Used to equalize the mixture between two zones before sleeping an edge. +/datum/gas_mixture/proc/equalize(datum/gas_mixture/sharer) + var/our_heatcap = heat_capacity() + var/share_heatcap = sharer.heat_capacity() + + // Special exception: there isn't enough air around to be worth processing this edge next tick, zap both to zero. + if(total_moles + sharer.total_moles <= MINIMUM_AIR_TO_SUSPEND) + gas.Cut() + sharer.gas.Cut() + + for(var/g in gas|sharer.gas) + var/comb = gas[g] + sharer.gas[g] + comb /= volume + sharer.volume + gas[g] = comb * volume + sharer.gas[g] = comb * sharer.volume + + if(our_heatcap + share_heatcap) + temperature = ((temperature * our_heatcap) + (sharer.temperature * share_heatcap)) / (our_heatcap + share_heatcap) + sharer.temperature = temperature + + update_values() + sharer.update_values() + + return 1 + + +//Returns the heat capacity of the gas mix based on the specific heat of the gases. +/datum/gas_mixture/proc/heat_capacity() + . = 0 + for(var/g in gas) + . += gas_data.specific_heat[g] * gas[g] + . *= group_multiplier + + +//Adds or removes thermal energy. Returns the actual thermal energy change, as in the case of removing energy we can't go below TCMB. +/datum/gas_mixture/proc/add_thermal_energy(var/thermal_energy) + if (total_moles == 0) + return 0 + + var/heat_capacity = heat_capacity() + if (thermal_energy < 0) + if (temperature < TCMB) + return 0 + var/thermal_energy_limit = -(temperature - TCMB)*heat_capacity //ensure temperature does not go below TCMB + thermal_energy = max( thermal_energy, thermal_energy_limit ) //thermal_energy and thermal_energy_limit are negative here. + temperature += thermal_energy/heat_capacity + return thermal_energy + +//Returns the thermal energy change required to get to a new temperature +/datum/gas_mixture/proc/get_thermal_energy_change(var/new_temperature) + return heat_capacity()*(max(new_temperature, 0) - temperature) + + +//Technically vacuum doesn't have a specific entropy. Just use a really big number (infinity would be ideal) here so that it's easy to add gas to vacuum and hard to take gas out. +#define SPECIFIC_ENTROPY_VACUUM 150000 + + +//Returns the ideal gas specific entropy of the whole mix. This is the entropy per mole of /mixed/ gas. +/datum/gas_mixture/proc/specific_entropy() + if (!gas.len || total_moles == 0) + return SPECIFIC_ENTROPY_VACUUM + + . = 0 + for(var/g in gas) + . += gas[g] * specific_entropy_gas(g) + . /= total_moles + + +/* + It's arguable whether this should even be called entropy anymore. It's more "based on" entropy than actually entropy now. + + Returns the ideal gas specific entropy of a specific gas in the mix. This is the entropy due to that gas per mole of /that/ gas in the mixture, not the entropy due to that gas per mole of gas mixture. + + For the purposes of SS13, the specific entropy is just a number that tells you how hard it is to move gas. You can replace this with whatever you want. + Just remember that returning a SMALL number == adding gas to this gas mix is HARD, taking gas away is EASY, and that returning a LARGE number means the opposite (so a vacuum should approach infinity). + + So returning a constant/(partial pressure) would probably do what most players expect. Although the version I have implemented below is a bit more nuanced than simply 1/P in that it scales in a way + which is bit more realistic (natural log), and returns a fairly accurate entropy around room temperatures and pressures. +*/ +/datum/gas_mixture/proc/specific_entropy_gas(var/gasid) + if (!(gasid in gas) || gas[gasid] == 0) + return SPECIFIC_ENTROPY_VACUUM //that gas isn't here + + //group_multiplier gets divided out in volume/gas[gasid] - also, V/(m*T) = R/(partial pressure) + var/molar_mass = gas_data.molar_mass[gasid] + var/specific_heat = gas_data.specific_heat[gasid] + return R_IDEAL_GAS_EQUATION * ( log( (IDEAL_GAS_ENTROPY_CONSTANT*volume/(gas[gasid] * temperature)) * (molar_mass*specific_heat*temperature)**(2/3) + 1 ) + 15 ) + + //alternative, simpler equation + //var/partial_pressure = gas[gasid] * R_IDEAL_GAS_EQUATION * temperature / volume + //return R_IDEAL_GAS_EQUATION * ( log (1 + IDEAL_GAS_ENTROPY_CONSTANT/partial_pressure) + 20 ) + + +//Updates the total_moles count and trims any empty gases. +/datum/gas_mixture/proc/update_values() + total_moles = 0 + for(var/g in gas) + if(gas[g] <= 0) + gas -= g + else + total_moles += gas[g] + + +//Returns the pressure of the gas mix. Only accurate if there have been no gas modifications since update_values() has been called. +/datum/gas_mixture/proc/return_pressure() + if(volume) + return total_moles * R_IDEAL_GAS_EQUATION * temperature / volume + return 0 + + +//Removes moles from the gas mixture and returns a gas_mixture containing the removed air. +/datum/gas_mixture/proc/remove(amount) + amount = min(amount, total_moles * group_multiplier) //Can not take more air than the gas mixture has! + if(amount <= 0) + return null + + var/datum/gas_mixture/removed = new + + for(var/g in gas) + removed.gas[g] = QUANTIZE((gas[g] / total_moles) * amount) + gas[g] -= removed.gas[g] / group_multiplier + + removed.temperature = temperature + update_values() + removed.update_values() + + return removed + + +//Removes a ratio of gas from the mixture and returns a gas_mixture containing the removed air. +/datum/gas_mixture/proc/remove_ratio(ratio, out_group_multiplier = 1) + if(ratio <= 0) + return null + out_group_multiplier = between(1, out_group_multiplier, group_multiplier) + + ratio = min(ratio, 1) + + var/datum/gas_mixture/removed = new + removed.group_multiplier = out_group_multiplier + + for(var/g in gas) + removed.gas[g] = (gas[g] * ratio * group_multiplier / out_group_multiplier) + gas[g] = gas[g] * (1 - ratio) + + removed.temperature = temperature + removed.volume = volume * group_multiplier / out_group_multiplier + update_values() + removed.update_values() + + return removed + +//Removes a volume of gas from the mixture and returns a gas_mixture containing the removed air with the given volume +/datum/gas_mixture/proc/remove_volume(removed_volume) + var/datum/gas_mixture/removed = remove_ratio(removed_volume/(volume*group_multiplier), 1) + removed.volume = removed_volume + return removed + +//Removes moles from the gas mixture, limited by a given flag. Returns a gax_mixture containing the removed air. +/datum/gas_mixture/proc/remove_by_flag(flag, amount) + if(!flag || amount <= 0) + return + + var/sum = 0 + for(var/g in gas) + if(gas_data.flags[g] & flag) + sum += gas[g] + + var/datum/gas_mixture/removed = new + + for(var/g in gas) + if(gas_data.flags[g] & flag) + removed.gas[g] = QUANTIZE((gas[g] / sum) * amount) + gas[g] -= removed.gas[g] / group_multiplier + + removed.temperature = temperature + update_values() + removed.update_values() + + return removed + +//Returns the amount of gas that has the given flag, in moles +/datum/gas_mixture/proc/get_by_flag(flag) + . = 0 + for(var/g in gas) + if(gas_data.flags[g] & flag) + . += gas[g] + +//Copies gas and temperature from another gas_mixture. +/datum/gas_mixture/proc/copy_from(const/datum/gas_mixture/sample) + gas = sample.gas.Copy() + temperature = sample.temperature + + update_values() + + return 1 + + +//Checks if we are within acceptable range of another gas_mixture to suspend processing or merge. +/datum/gas_mixture/proc/compare(const/datum/gas_mixture/sample, var/vacuum_exception = 0) + if(!sample) return 0 + + if(vacuum_exception) + // Special case - If one of the two is zero pressure, the other must also be zero. + // This prevents suspending processing when an air-filled room is next to a vacuum, + // an edge case which is particually obviously wrong to players + if(total_moles == 0 && sample.total_moles != 0 || sample.total_moles == 0 && total_moles != 0) + return 0 + + var/list/marked = list() + for(var/g in gas) + if((abs(gas[g] - sample.gas[g]) > MINIMUM_AIR_TO_SUSPEND) && \ + ((gas[g] < (1 - MINIMUM_AIR_RATIO_TO_SUSPEND) * sample.gas[g]) || \ + (gas[g] > (1 + MINIMUM_AIR_RATIO_TO_SUSPEND) * sample.gas[g]))) + return 0 + marked[g] = 1 + + for(var/g in sample.gas) + if(!marked[g]) + if((abs(gas[g] - sample.gas[g]) > MINIMUM_AIR_TO_SUSPEND) && \ + ((gas[g] < (1 - MINIMUM_AIR_RATIO_TO_SUSPEND) * sample.gas[g]) || \ + (gas[g] > (1 + MINIMUM_AIR_RATIO_TO_SUSPEND) * sample.gas[g]))) + return 0 + + if(total_moles > MINIMUM_AIR_TO_SUSPEND) + if((abs(temperature - sample.temperature) > MINIMUM_TEMPERATURE_DELTA_TO_SUSPEND) && \ + ((temperature < (1 - MINIMUM_TEMPERATURE_RATIO_TO_SUSPEND)*sample.temperature) || \ + (temperature > (1 + MINIMUM_TEMPERATURE_RATIO_TO_SUSPEND)*sample.temperature))) + return 0 + + return 1 + + +/datum/gas_mixture/proc/react() + zburn(null, force_burn=0, no_check=0) //could probably just call zburn() here with no args but I like being explicit. + + +//Rechecks the gas_mixture and adjusts the graphic list if needed. +//Two lists can be passed by reference if you need know specifically which graphics were added and removed. +/datum/gas_mixture/proc/check_tile_graphic(list/graphic_add = null, list/graphic_remove = null) + var/list/cur_graphic = graphic // Cache for sanic speed + for(var/g in gas_data.overlay_limit) + if(cur_graphic && cur_graphic.Find(gas_data.tile_overlay[g])) + //Overlay is already applied for this gas, check if it's still valid. + if(gas[g] <= gas_data.overlay_limit[g]) + LAZYADD(graphic_remove, gas_data.tile_overlay[g]) + else + //Overlay isn't applied for this gas, check if it's valid and needs to be added. + if(gas[g] > gas_data.overlay_limit[g]) + LAZYADD(graphic_add, gas_data.tile_overlay[g]) + + . = 0 + //Apply changes + if(LAZYLEN(graphic_add)) + LAZYADD(graphic, graphic_add) + . = 1 + if(LAZYLEN(graphic_remove)) + LAZYREMOVE(graphic, graphic_remove) + . = 1 + + +//Simpler version of merge(), adjusts gas amounts directly and doesn't account for temperature or group_multiplier. +/datum/gas_mixture/proc/add(datum/gas_mixture/right_side) + for(var/g in right_side.gas) + gas[g] += right_side.gas[g] + + update_values() + return 1 + + +//Simpler version of remove(), adjusts gas amounts directly and doesn't account for group_multiplier. +/datum/gas_mixture/proc/subtract(datum/gas_mixture/right_side) + for(var/g in right_side.gas) + gas[g] -= right_side.gas[g] + + update_values() + return 1 + + +//Multiply all gas amounts by a factor. +/datum/gas_mixture/proc/multiply(factor) + for(var/g in gas) + gas[g] *= factor + + update_values() + return 1 + + +//Divide all gas amounts by a factor. +/datum/gas_mixture/proc/divide(factor) + for(var/g in gas) + gas[g] /= factor + + update_values() + return 1 + + +//Shares gas with another gas_mixture based on the amount of connecting tiles and a fixed lookup table. +/datum/gas_mixture/proc/share_ratio(datum/gas_mixture/other, connecting_tiles, share_size = null, one_way = 0) + var/static/list/sharing_lookup_table = list(0.30, 0.40, 0.48, 0.54, 0.60, 0.66) + //Shares a specific ratio of gas between mixtures using simple weighted averages. + var/ratio = sharing_lookup_table[6] + + var/size = max(1, group_multiplier) + if(isnull(share_size)) share_size = max(1, other.group_multiplier) + + var/full_heat_capacity = heat_capacity() + var/s_full_heat_capacity = other.heat_capacity() + + var/list/avg_gas = list() + + for(var/g in gas) + avg_gas[g] += gas[g] * size + + for(var/g in other.gas) + avg_gas[g] += other.gas[g] * share_size + + for(var/g in avg_gas) + avg_gas[g] /= (size + share_size) + + var/temp_avg = 0 + if(full_heat_capacity + s_full_heat_capacity) + temp_avg = (temperature * full_heat_capacity + other.temperature * s_full_heat_capacity) / (full_heat_capacity + s_full_heat_capacity) + + //WOOT WOOT TOUCH THIS AND YOU ARE A MISCHIEVIOUS LITTLE ELF + if(sharing_lookup_table.len >= connecting_tiles) //6 or more interconnecting tiles will max at 42% of air moved per tick. + ratio = sharing_lookup_table[connecting_tiles] + //WOOT WOOT TOUCH THIS AND YOU ARE A MISCHIEVIOUS LITTLE ELF + + for(var/g in avg_gas) + gas[g] = max(0, (gas[g] - avg_gas[g]) * (1 - ratio) + avg_gas[g]) + if(!one_way) + other.gas[g] = max(0, (other.gas[g] - avg_gas[g]) * (1 - ratio) + avg_gas[g]) + + temperature = max(0, (temperature - temp_avg) * (1-ratio) + temp_avg) + if(!one_way) + other.temperature = max(0, (other.temperature - temp_avg) * (1-ratio) + temp_avg) + + update_values() + other.update_values() + + return compare(other) + + +//A wrapper around share_ratio for spacing gas at the same rate as if it were going into a large airless room. +/datum/gas_mixture/proc/share_space(datum/gas_mixture/unsim_air) + return share_ratio(unsim_air, unsim_air.group_multiplier, max(1, max(group_multiplier + 3, 1) + unsim_air.group_multiplier), one_way = 1) + + +//Equalizes a list of gas mixtures. Used for pipe networks. +/proc/equalize_gases(list/datum/gas_mixture/gases) + //Calculate totals from individual components + var/total_volume = 0 + var/total_thermal_energy = 0 + var/total_heat_capacity = 0 + + var/list/total_gas = list() + for(var/datum/gas_mixture/gasmix in gases) + total_volume += gasmix.volume + var/temp_heatcap = gasmix.heat_capacity() + total_thermal_energy += gasmix.temperature * temp_heatcap + total_heat_capacity += temp_heatcap + for(var/g in gasmix.gas) + total_gas[g] += gasmix.gas[g] + + if(total_volume > 0) + var/datum/gas_mixture/combined = new(total_volume) + combined.gas = total_gas + + //Calculate temperature + if(total_heat_capacity > 0) + combined.temperature = total_thermal_energy / total_heat_capacity + combined.update_values() + + //Allow for reactions + combined.react() + + //Average out the gases + for(var/g in combined.gas) + combined.gas[g] /= total_volume + + //Update individual gas_mixtures + for(var/datum/gas_mixture/gasmix in gases) + gasmix.gas = combined.gas.Copy() + gasmix.temperature = combined.temperature + gasmix.multiply(gasmix.volume) + + return 1 + +/datum/gas_mixture/proc/get_mass() + for(var/g in gas) + . += gas[g] * gas_data.molar_mass[g] * group_multiplier diff --git a/icons/inventory/accessory/item_vr.dmi b/icons/inventory/accessory/item_vr.dmi index de59e5e17d2f5fed0d226910bea4874e0ba00f71..5a85d7bb33a2221c1d7f22e73a8d8583ae0f82d6 100644 GIT binary patch literal 19657 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 Ji*@b7{ueX{0Zjk^ literal 19538 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 Bla>Gg