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 de59e5e17d..5a85d7bb33 100644 Binary files a/icons/inventory/accessory/item_vr.dmi and b/icons/inventory/accessory/item_vr.dmi differ