From d0e8bf35dec2328751579ec8b68fc45f98ffaffb Mon Sep 17 00:00:00 2001 From: esainane Date: Mon, 30 Aug 2021 00:42:53 +1200 Subject: [PATCH] Many smart pipe fixes (#60981) * Smart pipes now actually go over smart pipes This was ostensibly the main feature of smart pipes, so should be fairly important Parentheses are technically not required around the |, but I think it's important to emphasise the importance of parentheses around bitwise operators as this was apparently broken since 3e8407c4716b Also purge var/connections and var/connection_num, since it's literally only used for a `machine.connection_num < 3` check which is completely redundant, and we already build a bitfield tracking active directions. * Smart pipes bridge over straight pipe types This improves bridging to now go over most devices where possible, letting you, for example, bridge over a perpindicular layer adapter. * Pipe construction: Better documentation Cleans up smart pipe documentation to match what's actually happening. No functional changes. * Skip pipes on different layers earlier This prevents a pipe being made into a bridge perpindicular to a pipe on a completely different layer, then failing to turn into a bridge perpindicular to a relevant pipe. * Pop superfluous parentheses * Prevent creation of stub pipes Setting the RPD to create a smart pipe that can only connect in one direction would cause it to create invisible pipes that would still block the placement of other pipes. * The RPD can now reprogram smart pipes The RPD can only affect smart pipes in directions that they are not currently connected to. This makes it easier to adjust designs after the fact, including prevening round-start pipes from eagerly linking to a grey layer adapter. * Even smarter pipes No more turning into bridge pipes. We now only try to be smart if placement would fail, and we do our best to find a solution whenever there is at least one smart pipe involved, regardless of whether we're currently placing a smart pipe or not. We never reconfigure any directions that a smart pipe is currently connected, and we never reconfigure a smart pipe to have one or less usable directions. * Smartly go across perpindicular layer adapters Also works when we're placing a layer adapter perpindicular to a promiscuous smart pipe * Pipes: Factor out loops and some bitfiddling Create and use helpers with documented purposes over inline bitfiddling, when it makes sense. Many loops and switch statements were recreating information that was already there. Some relationships between pipe bitfield states were already assumed. This centralises the functionality that relies upon these assumptions, places them where the bits are defined, and documents them. Rewrite some bitwise operations to be more idiomatic. * Smart pipes: Debugging output I normally clean history before pushing changes to any project, but I feel like this should be saved. * Revert "Smart pipes: Debugging output" This reverts commit bb3aa76cf6d08e4d0951113a26fc9d48b6bc1735. * Add trailing comma The lack of this was making the linter sad --- code/__DEFINES/atmospherics/atmos_piping.dm | 9 ++ code/game/machinery/pipe/construction.dm | 118 ++++++++++++++---- code/game/objects/items/RPD.dm | 53 ++++++-- .../atmospherics/machinery/atmosmachinery.dm | 3 - .../atmospherics/machinery/pipes/pipes.dm | 24 +--- .../atmospherics/machinery/pipes/smart.dm | 75 ++++------- .../tgui/interfaces/RapidPipeDispenser.js | 4 + 7 files changed, 181 insertions(+), 105 deletions(-) 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) => {