diff --git a/code/__DEFINES/atmospherics/atmos_piping.dm b/code/__DEFINES/atmospherics/atmos_piping.dm index c864b59bee4..4b866879805 100644 --- a/code/__DEFINES/atmospherics/atmos_piping.dm +++ b/code/__DEFINES/atmospherics/atmos_piping.dm @@ -8,6 +8,15 @@ #define SOUTH_SHORTPIPE (1<<5) #define EAST_SHORTPIPE (1<<6) #define WEST_SHORTPIPE (1<<7) +// Helpers to convert cardinals to and from pipe bitfields +// Assumes X_FULLPIPE = X, X_SHORTPIPE >> 4 = X as above +#define FULLPIPE_TO_CARDINALS(bitfield) ((bitfield) & ALL_CARDINALS) +#define SHORTPIPE_TO_CARDINALS(bitfield) (((bitfield) >> 4) & ALL_CARDINALS) +#define CARDINAL_TO_FULLPIPES(cardinals) (cardinals) +#define CARDINAL_TO_SHORTPIPES(cardinals) ((cardinals) << 4) +// A pipe is a stub if it only has zero or one permitted direction. For a regular pipe this is nonsensical, and there are no pipe sprites for this, so it is not allowed. +#define ISSTUB(bits) !((bits) & (bits - 1)) +#define ISNOTSTUB(bits) ((bits) & (bits - 1)) //Atmos pipe limits /// (kPa) What pressure pumps and powered equipment max out at. #define MAX_OUTPUT_PRESSURE 4500 diff --git a/code/game/machinery/pipe/construction.dm b/code/game/machinery/pipe/construction.dm index 9bfc7417646..ae795d0e77d 100644 --- a/code/game/machinery/pipe/construction.dm +++ b/code/game/machinery/pipe/construction.dm @@ -157,35 +157,28 @@ Buildable meters var/obj/machinery/atmospherics/fakeA = pipe_type var/flags = initial(fakeA.pipe_flags) - var/pipe_count = 0 + var/list/potentially_conflicting_machines = list() + // Work out which machines we would potentially conflict with for(var/obj/machinery/atmospherics/machine in loc) - if(machine.piping_layer != piping_layer) + // skip checks if we don't overlap layers, either by being on the same layer or by something being on all layers + if(machine.piping_layer != piping_layer && !((machine.pipe_flags | flags) & PIPING_ALL_LAYER)) continue - pipe_count += 1 - for(var/obj/machinery/atmospherics/machine in loc) - if((machine.pipe_flags & flags & PIPING_ONE_PER_TURF)) //Only one dense/requires density object per tile, eg connectors/cryo/heater/coolers. + // Only one dense/requires density object per tile, eg connectors/cryo/heater/coolers. + if((machine.pipe_flags & flags & PIPING_ONE_PER_TURF)) to_chat(user, span_warning("Something is hogging the tile!")) return TRUE + potentially_conflicting_machines += machine - if(pipe_count == 1 && istype(machine, /obj/machinery/atmospherics/pipe/smart) && ispath(pipe_type, /obj/machinery/atmospherics/pipe/smart) && lowertext(machine.pipe_color) != lowertext(pipe_color) && machine.connection_num < 3) - var/direction = machine.dir - if((direction & EAST|WEST || direction & SOUTH|NORTH) && !ISDIAGONALDIR(direction)) - pipe_type = /obj/machinery/atmospherics/pipe/bridge_pipe - if(EWCOMPONENT(direction)) - dir = NORTH - if(NSCOMPONENT(direction)) - dir = EAST - continue - - if(flags & PIPING_BRIDGE && !(machine.pipe_flags & PIPING_BRIDGE) && check_ninety_degree_dir(machine)) //continue if we are placing a bridge pipe over a normal pipe only (prevent duplicates) - continue - - if((machine.piping_layer != piping_layer) && !((machine.pipe_flags | flags) & PIPING_ALL_LAYER)) //don't continue if either pipe goes across all layers - continue - - if(machine.GetInitDirections() & SSair.get_init_dirs(pipe_type, fixed_dir(), p_init_dir)) // matches at least one direction on either type of pipe - to_chat(user, span_warning("There is already a pipe at that location!")) - return TRUE + // See if we would conflict with any of the potentially interacting machines + for(var/obj/machinery/atmospherics/machine as anything in potentially_conflicting_machines) + // if the pipes have any directions in common, we can't place it that way. + var/our_init_dirs = SSair.get_init_dirs(pipe_type, fixed_dir(), p_init_dir) + if(machine.GetInitDirections() & our_init_dirs) + // We have a conflict! + if (length(potentially_conflicting_machines) != 1 || !try_smart_reconfiguration(machine, our_init_dirs, user)) + // No solutions found + to_chat(user, span_warning("There is already a pipe at that location!")) + return TRUE // no conflicts found var/obj/machinery/atmospherics/built_machine = new pipe_type(loc, , , p_init_dir) @@ -201,6 +194,83 @@ Buildable meters qdel(src) +/** + * Attempt to automatically resolve a pipe conflict by reconfiguring any smart pipes involved. + * + * Constraints: + * - A smart pipe cannot have current connections reconfigured. + * - A smart pipe cannot have fewer than two directions in which it will connect. + * - A smart pipe, existing or new, will not automatically reconfigure itself to permit directions it was not previously permitting. + */ +/obj/item/pipe/proc/try_smart_reconfiguration(obj/machinery/atmospherics/machine, our_init_dirs, mob/living/user) + // If we're a smart pipe, we might be able to solve this by placing down a more constrained version of ourselves. + var/obj/machinery/atmospherics/pipe/smart/other_smart_pipe = machine + if(ispath(pipe_type, /obj/machinery/atmospherics/pipe/smart/)) + // If we're conflicting with another smart pipe, see if we can negotiate. + if(istype(other_smart_pipe)) + // Two smart pipes. This is going to get complicated. + // Check to see whether the already placed pipe is bent or not. + if (ISDIAGONALDIR(other_smart_pipe.dir)) + // The other pipe is bent, with at least two current connections. See if we can bounce off it as a bent pipe in the other direction. + var/opposing_dir = our_init_dirs & ~other_smart_pipe.connections + if (ISNOTSTUB(opposing_dir)) + // We only get here if both smart pipes have two directions. + p_init_dir = opposing_dir + other_smart_pipe.SetInitDirections(other_smart_pipe.connections) + other_smart_pipe.update_pipe_icon() + return TRUE + // We're left with one or no available directions if we look at the complement of the other smart pipe's live connections. + // There's nothing further we can do. + return FALSE + else + // The other pipe is straight. See if we can go over it in a perpindicular direction. + // Note that the other pipe cannot be unconnected, since we have a conflict. + if(EWCOMPONENT(other_smart_pipe.dir)) + if ((NORTH|SOUTH) & ~p_init_dir) + // Not allowed to connect this way + return FALSE + if (~other_smart_pipe.GetInitDirections() & (EAST|WEST)) + // Not allowed to reconfigure the other pipe this way + return FALSE + p_init_dir = NORTH|SOUTH + other_smart_pipe.SetInitDirections(EAST|WEST) + other_smart_pipe.update_pipe_icon() + return TRUE + if (NSCOMPONENT(other_smart_pipe.dir)) + if ((EAST|WEST) & ~p_init_dir) + // Not allowed to connect this way + return FALSE + if (~other_smart_pipe.GetInitDirections() & (NORTH|SOUTH)) + // Not allowed to reconfigure the other pipe this way + return FALSE + p_init_dir = EAST|WEST + other_smart_pipe.SetInitDirections(NORTH|SOUTH) + other_smart_pipe.update_pipe_icon() + return TRUE + return FALSE + // We're not dealing with another smart pipe. See if we can become the complement of the conflicting machine. + var/opposing_dir = our_init_dirs & ~machine.GetInitDirections() + if (ISNOTSTUB(opposing_dir)) + // We have at least two permitted directions in the complement. Use them. + p_init_dir = opposing_dir + return TRUE + return FALSE + + else if(istype(other_smart_pipe)) + // We're not a smart pipe ourselves, but we are conflicting with a smart pipe. We might be able to solve this by constraining the smart pipe. + if (our_init_dirs & other_smart_pipe.connections) + // We needed to go where a smart pipe already had connections, nothing further we can do + return FALSE + var/opposing_dir = other_smart_pipe.GetInitDirections() & ~our_init_dirs + if (ISNOTSTUB(opposing_dir)) + // At least two directions remain for that smart pipe, reconfigure it + other_smart_pipe.SetInitDirections(opposing_dir) + other_smart_pipe.update_pipe_icon() + return TRUE + return FALSE + // No smart pipes involved, the conflict can't be solved this way. + return FALSE + /obj/item/pipe/proc/build_pipe(obj/machinery/atmospherics/A) A.setDir(fixed_dir()) A.SetInitDirections(p_init_dir) diff --git a/code/game/objects/items/RPD.dm b/code/game/objects/items/RPD.dm index 34099cdeac0..869d5581216 100644 --- a/code/game/objects/items/RPD.dm +++ b/code/game/objects/items/RPD.dm @@ -10,7 +10,7 @@ RPD #define BUILD_MODE (1<<0) #define WRENCH_MODE (1<<1) #define DESTROY_MODE (1<<2) - +#define REPROGRAM_MODE (1<<3) GLOBAL_LIST_INIT(atmos_pipe_recipes, list( "Pipes" = list( @@ -216,6 +216,8 @@ GLOBAL_LIST_INIT(transit_tube_recipes, list( var/transit_build_speed = 0.5 SECONDS ///Speed of removal of unwrenched devices var/destroy_speed = 0.5 SECONDS + ///Speed of reprogramming connectable directions of smart pipes + var/reprogram_speed = 0.5 SECONDS ///Category currently active (Atmos, disposal, transit) var/category = ATMOS_CATEGORY ///Piping layer we are going to spawn the atmos device in @@ -231,7 +233,7 @@ GLOBAL_LIST_INIT(transit_tube_recipes, list( ///Stores the first transit device var/static/datum/pipe_info/first_transit ///The modes that are allowed for the RPD - var/mode = BUILD_MODE | DESTROY_MODE | WRENCH_MODE + var/mode = BUILD_MODE | DESTROY_MODE | WRENCH_MODE | REPROGRAM_MODE /// Bitflags for upgrades var/upgrade_flags @@ -409,12 +411,12 @@ GLOBAL_LIST_INIT(transit_tube_recipes, list( playeffect = FALSE if("mode") var/n = text2num(params["mode"]) - if(mode & n) - mode &= ~n - else - mode |= n + mode ^= n if("init_dir_setting") - p_init_dir ^= text2dir(params["dir_flag"]) + var/target_dir = p_init_dir ^ text2dir(params["dir_flag"]) + // Refuse to create a smart pipe that can only connect in one direction (it would act weirdly and lack an icon) + if (ISNOTSTUB(target_dir)) + p_init_dir = target_dir if("init_reset") p_init_dir = ALL_CARDINALS if(playeffect) @@ -457,6 +459,42 @@ GLOBAL_LIST_INIT(transit_tube_recipes, list( qdel(attack_target) return + if(mode & REPROGRAM_MODE) + var/obj/machinery/atmospherics/pipe/smart/S = attack_target + if(istype(S)) + if (S.dir == ALL_CARDINALS) + to_chat(user, span_warning("\The [S] has no unconnected directions!")) + return + var/target_init_dir = S.GetInitDirections() + if (target_init_dir == p_init_dir) + to_chat(user, span_warning("\The [S] is already in this configuration!")) + return + // Check for differences in unconnected directions + var/target_differences = (p_init_dir ^ target_init_dir) & ~S.connections + if (target_differences) + to_chat(user, span_notice("You start reprogramming \the [S]...")) + playsound(get_turf(src), 'sound/machines/click.ogg', 50, TRUE) + if(do_after(user, reprogram_speed, target = S)) + // Double check to make sure that nothing has changed. If anything we were about to change is now connected, abort + if (target_differences & S.connections) + to_chat(user, span_warning("\The [src]'s screen flashes a warning: Can't configure a pipe in a currently connected direction.")) + return + var/new_dir = (S.GetInitDirections() & ~target_differences) | target_differences + // Don't make a smart pipe with only one connection + if (ISSTUB(new_dir)) + to_chat(user, span_warning("\The [src]'s screen flashes a warning: Can't configure a pipe to only connect in one direction.")) + return + S.SetInitDirections(new_dir) + S.update_pipe_icon() + user.visible_message(span_notice("[user] reprograms the \the [S]."),span_notice("You reprogram \the [S].")) + return + to_chat(user, span_warning("\The [S] is already in this configuration for its unconnected directions!")) + return + var/obj/item/pipe/quaternary/I = attack_target + if(istype(I) && ispath(I.pipe_type, /obj/machinery/atmospherics/pipe/smart)) + I.p_init_dir = p_init_dir + I.update() + if(mode & BUILD_MODE) switch(category) //if we've gotten this var, the target is valid if(ATMOS_CATEGORY) //Making pipes @@ -584,6 +622,7 @@ GLOBAL_LIST_INIT(transit_tube_recipes, list( #undef BUILD_MODE #undef DESTROY_MODE #undef WRENCH_MODE +#undef REPROGRAM_MODE /obj/item/rpd_upgrade name = "RPD advanced design disk" diff --git a/code/modules/atmospherics/machinery/atmosmachinery.dm b/code/modules/atmospherics/machinery/atmosmachinery.dm index 180bcf61b6e..962abaaf004 100644 --- a/code/modules/atmospherics/machinery/atmosmachinery.dm +++ b/code/modules/atmospherics/machinery/atmosmachinery.dm @@ -58,9 +58,6 @@ ///The bitflag that's being checked on ventcrawling. Default is to allow ventcrawling and seeing pipes. var/vent_movement = VENTCRAWL_ALLOWED | VENTCRAWL_CAN_SEE - ///Store the smart pipes connections, used for pipe construction - var/connection_num = 0 - /obj/machinery/atmospherics/LateInitialize() . = ..() update_name() diff --git a/code/modules/atmospherics/machinery/pipes/pipes.dm b/code/modules/atmospherics/machinery/pipes/pipes.dm index 5bfc9474040..49a9e88482d 100644 --- a/code/modules/atmospherics/machinery/pipes/pipes.dm +++ b/code/modules/atmospherics/machinery/pipes/pipes.dm @@ -93,32 +93,16 @@ /obj/machinery/atmospherics/pipe/proc/update_pipe_icon() icon = 'icons/obj/atmospherics/pipes/pipes_bitmask.dmi' + var/connections = NONE var/bitfield = NONE for(var/i in 1 to device_type) if(!nodes[i]) continue var/obj/machinery/atmospherics/node = nodes[i] var/connected_dir = get_dir(src, node) - switch(connected_dir) - if(NORTH) - bitfield |= NORTH_FULLPIPE - if(SOUTH) - bitfield |= SOUTH_FULLPIPE - if(EAST) - bitfield |= EAST_FULLPIPE - if(WEST) - bitfield |= WEST_FULLPIPE - for(var/cardinal in GLOB.cardinals) - if(initialize_directions & cardinal && !(bitfield & cardinal)) - switch(cardinal) - if(NORTH) - bitfield |= NORTH_SHORTPIPE - if(SOUTH) - bitfield |= SOUTH_SHORTPIPE - if(EAST) - bitfield |= EAST_SHORTPIPE - if(WEST) - bitfield |= WEST_SHORTPIPE + connections |= connected_dir + bitfield = CARDINAL_TO_FULLPIPES(connections) + bitfield |= CARDINAL_TO_SHORTPIPES(initialize_directions & ~connections) icon_state = "[bitfield]_[piping_layer]" /obj/machinery/atmospherics/pipe/update_icon() diff --git a/code/modules/atmospherics/machinery/pipes/smart.dm b/code/modules/atmospherics/machinery/pipes/smart.dm index 4a39488be0e..347c44a2c02 100644 --- a/code/modules/atmospherics/machinery/pipes/smart.dm +++ b/code/modules/atmospherics/machinery/pipes/smart.dm @@ -10,72 +10,45 @@ GLOBAL_LIST_INIT(atmos_components, typecacheof(list(/obj/machinery/atmospherics) device_type = QUATERNARY construction_type = /obj/item/pipe/quaternary pipe_state = "manifold4w" - connection_num = 0 - var/connections + var/connections = NONE /obj/machinery/atmospherics/pipe/smart/update_pipe_icon() icon = 'icons/obj/atmospherics/pipes/pipes_bitmask.dmi' var/bitfield = NONE - var/bits = 0 - connections = NONE - connection_num = 0 + for(var/i in 1 to device_type) if(!nodes[i]) continue var/obj/machinery/atmospherics/node = nodes[i] var/connected_dir = get_dir(src, node) connections |= connected_dir - connection_num += 1 - bits++ - switch(connected_dir) - if(NORTH) - bitfield |= NORTH_FULLPIPE - if(SOUTH) - bitfield |= SOUTH_FULLPIPE - if(EAST) - bitfield |= EAST_FULLPIPE - if(WEST) - bitfield |= WEST_FULLPIPE - //If we dont have enough bits to make a proper sprite, add some shortpipe bits - if(bits < 2) - var/list/bits_to_add = list() - var/list/iterate_list = list() - if(bits == 1) - iterate_list += REVERSE_DIR(bitfield) - iterate_list += GLOB.cardinals - for(var/cardinal in iterate_list) - if(!(bitfield & cardinal) && !(cardinal in bits_to_add) && initialize_directions & cardinal) - bits_to_add += cardinal - bits++ - if(bits >= 2) - break - for(var/direction in bits_to_add) - switch(direction) - if(NORTH) - bitfield |= NORTH_SHORTPIPE - if(SOUTH) - bitfield |= SOUTH_SHORTPIPE - if(EAST) - bitfield |= EAST_SHORTPIPE - if(WEST) - bitfield |= WEST_SHORTPIPE + bitfield = CARDINAL_TO_FULLPIPES(connections) + dir = check_binary_direction(connections) + + // If we dont have enough bits to make a proper sprite, add some shortpipe bits + + // Smart pipe icons differ from classic pipe icons in that we stop adding + // short pipe directions as soon as we find a valid sprite, rather than + // adding in all connectable directions. + // This prevents a lot of visual clutter, though it does make it harder to + // notice completely disconnected pipes. + if(ISSTUB(connections)) + var/bits_to_add = NONE + if(connections != NONE) + bits_to_add |= REVERSE_DIR(connections) & initialize_directions + var/candidates = initialize_directions + var/shift = 0 + // Note that candidates "should" never reach 0, as stub pipes are not allowed and break things + while (ISSTUB(connections | bits_to_add) && (candidates >> shift) != 0) + bits_to_add |= candidates & (1 << shift) + shift += 1 + bitfield |= CARDINAL_TO_SHORTPIPES(bits_to_add) icon_state = "[bitfield]_[piping_layer]" - switch(connection_num) - if(0) - dir = SOUTH - if(1) - dir = connections - if(2) - dir = check_binary_direction(connections) - if(3) - dir = connections - if(4) - dir = NORTH | SOUTH | EAST | WEST /obj/machinery/atmospherics/pipe/smart/SetInitDirections(init_dir) if(init_dir) - initialize_directions = init_dir + initialize_directions = init_dir else initialize_directions = ALL_CARDINALS diff --git a/tgui/packages/tgui/interfaces/RapidPipeDispenser.js b/tgui/packages/tgui/interfaces/RapidPipeDispenser.js index 171f72bdd8c..9ada9a0cf22 100644 --- a/tgui/packages/tgui/interfaces/RapidPipeDispenser.js +++ b/tgui/packages/tgui/interfaces/RapidPipeDispenser.js @@ -34,6 +34,10 @@ const TOOLS = [ name: 'Destroy', bitmask: 4, }, + { + name: 'Reprogram', + bitmask: 8, + }, ]; const SelectionSection = (props, context) => {