From ec8475d56df1cf01f1d0f37507a052bd569dbe30 Mon Sep 17 00:00:00 2001 From: silicons <2003111+silicons@users.noreply.github.com> Date: Sun, 6 Jul 2025 14:49:06 -0400 Subject: [PATCH] Adjacency groups (#7233) --- citadel.dme | 3 + code/__DEFINES/directional.dm | 19 ++- .../objects/object_systems/adjacency_group.dm | 19 +++ code/__DEFINES/objects/objects.dm | 2 +- code/__DEFINES/traits/objects.dm | 3 + code/game/objects/systems/adjacency_group.dm | 154 ++++++++++++++++++ code/modules/automata/types/wave.dm | 2 +- 7 files changed, 195 insertions(+), 7 deletions(-) create mode 100644 code/__DEFINES/objects/object_systems/adjacency_group.dm create mode 100644 code/__DEFINES/traits/objects.dm create mode 100644 code/game/objects/systems/adjacency_group.dm diff --git a/citadel.dme b/citadel.dme index 99c3345873b..98829f50ad9 100644 --- a/citadel.dme +++ b/citadel.dme @@ -309,6 +309,7 @@ #include "code\__DEFINES\mobs\wounds.dm" #include "code\__DEFINES\objects\objects.dm" #include "code\__DEFINES\objects\type_generation.dm" +#include "code\__DEFINES\objects\object_systems\adjacency_group.dm" #include "code\__DEFINES\overmaps\misc.dm" #include "code\__DEFINES\overmaps\overmap.dm" #include "code\__DEFINES\power\balancing.dm" @@ -378,6 +379,7 @@ #include "code\__DEFINES\traits\items.dm" #include "code\__DEFINES\traits\mob.dm" #include "code\__DEFINES\traits\movables.dm" +#include "code\__DEFINES\traits\objects.dm" #include "code\__DEFINES\traits\sources.dm" #include "code\__DEFINES\traits\unsorted.dm" #include "code\__DEFINES\turfs\change_turf.dm" @@ -2125,6 +2127,7 @@ #include "code\game\objects\structures\tables\table.dm" #include "code\game\objects\structures\tables\update_triggers.dm" #include "code\game\objects\systems\_system.dm" +#include "code\game\objects\systems\adjacency_group.dm" #include "code\game\objects\systems\cell_slot.dm" #include "code\game\objects\systems\storage\storage-filters.dm" #include "code\game\objects\systems\storage\storage-indirection.dm" diff --git a/code/__DEFINES/directional.dm b/code/__DEFINES/directional.dm index 7a23dbbc49d..bfe795faa62 100644 --- a/code/__DEFINES/directional.dm +++ b/code/__DEFINES/directional.dm @@ -57,16 +57,25 @@ pixel_x = -offset; \ } +//* Individual bits for storing common directions. Unlike BYOND bits, this can store northeast apart from north. *// +//* These should **not** be considered safe for serialization; these can change at any time as they sync to BYOND's. *// + // each dir plus diagonals in its own bit #define NORTH_BIT NORTH #define SOUTH_BIT SOUTH #define EAST_BIT EAST #define WEST_BIT WEST -#define NORTHEAST_BIT (1<<4) -#define NORTHWEST_BIT (1<<5) -#define SOUTHEAST_BIT (1<<6) -#define SOUTHWEST_BIT (1<<7) -#define ALL_DIRECTION_BITS (NORTH_BIT | SOUTH_BIT | NORTHEAST_BIT | NORTHWEST_BIT | SOUTHEAST_BIT | SOUTHWEST_BIT | WEST_BIT | EAST_BIT) +/// Not included in cardinal/diagonal direction bits. +#define UP_BIT UP +/// Not included in cardinal/diagonal direction bits. +#define DOWN_BIT DOWN +#define NORTHEAST_BIT (1<<6) +#define NORTHWEST_BIT (1<<7) +#define SOUTHEAST_BIT (1<<8) +#define SOUTHWEST_BIT (1<<9) +/// Not included in cardinal/diagonal direction bits. +#define ONTOP_BIT (1<<10) +#define CARDINAL_DIAGONAL_DIRECTION_BITS (NORTH_BIT | SOUTH_BIT | NORTHEAST_BIT | NORTHWEST_BIT | SOUTHEAST_BIT | SOUTHWEST_BIT | WEST_BIT | EAST_BIT) #define CARDINAL_DIRECTION_BITS (NORTH_BIT | SOUTH_BIT | EAST_BIT | WEST_BIT) #define DIAGONAL_DIRECTION_BITS (NORTHEAST_BIT | NORTHWEST_BIT | SOUTHEAST_BIT | SOUTHWEST_BIT) diff --git a/code/__DEFINES/objects/object_systems/adjacency_group.dm b/code/__DEFINES/objects/object_systems/adjacency_group.dm new file mode 100644 index 00000000000..2c650ce3097 --- /dev/null +++ b/code/__DEFINES/objects/object_systems/adjacency_group.dm @@ -0,0 +1,19 @@ + +//* This file is explicitly licensed under the MIT license. *// +//* Copyright (c) 2025 Citadel Station developers. *// + +#define OBJ_BIND_ADJACENCY_GROUP(PATH, VARNAME, KEY) \ +##PATH { \ + var/datum/object_system/adjacency_group/##VARNAME; \ +}; \ +##PATH/Initialize(mapload, ...) { \ + . = ..(); \ + if(. == INITIALIZE_HINT_QDEL) { \ + return; \ + }; \ + VARNAME = new(src, KEY); \ +}; \ +##PATH/Moved(...) { \ + VARNAME.parent_moved(); \ + return ..(); \ +} diff --git a/code/__DEFINES/objects/objects.dm b/code/__DEFINES/objects/objects.dm index 92cb1c25f74..61d3c1de7bb 100644 --- a/code/__DEFINES/objects/objects.dm +++ b/code/__DEFINES/objects/objects.dm @@ -1,7 +1,7 @@ //* This file is explicitly licensed under the MIT license. *// //* Copyright (c) 2024 Citadel Station Developers *// -//* /obj/var/hides_underfloor +//* /obj/var/hides_underfloor *// /// do not change these willy-nilly, these are strings for the map editor. diff --git a/code/__DEFINES/traits/objects.dm b/code/__DEFINES/traits/objects.dm new file mode 100644 index 00000000000..ccf8242a2ae --- /dev/null +++ b/code/__DEFINES/traits/objects.dm @@ -0,0 +1,3 @@ +/// A special trait key used for adjacency group registration. +/// This is used because status_traits already exists, and a full COMSIG system is too high in overhead. +#define TRAIT_OBJ_ADJACENCY_GROUP(KEY) "obj-adj-group-[KEY]" diff --git a/code/game/objects/systems/adjacency_group.dm b/code/game/objects/systems/adjacency_group.dm new file mode 100644 index 00000000000..97b8b101c40 --- /dev/null +++ b/code/game/objects/systems/adjacency_group.dm @@ -0,0 +1,154 @@ +//* This file is explicitly licensed under the MIT license. *// +//* Copyright (c) 2025 Citadel Station developers. *// + +/** + * Connects transitively to cardinals. + * + * Does something very, very evil to status_traits. + */ +/datum/object_system/adjacency_group + /// string key + var/key + /// current group + var/datum/adjacency_group/group + /// rebuild queued? + var/rebuild_queued = FALSE + /// connected directions + var/connected_directions = NONE + +/datum/object_system/adjacency_group/New(obj/parent, key) + src.key = key + ASSERT(key) + LAZYSET(parent.status_traits, TRAIT_OBJ_ADJACENCY_GROUP(src.key), src) + queue_rebuild() + return ..() + +/datum/object_system/adjacency_group/Destroy() + LAZYREMOVE(parent.status_traits, TRAIT_OBJ_ADJACENCY_GROUP(key)) + QDEL_NULL(group) + rebuild_queued = FALSE + return ..() + +/datum/object_system/adjacency_group/proc/parent_moved() + QDEL_NULL(group) + queue_rebuild() + +/datum/object_system/adjacency_group/proc/create_initial_data() + if(rebuild_queued) + return + return parent.object_adjacency_group_create_initial_data(src) + +/datum/object_system/adjacency_group/proc/queue_rebuild() + addtimer(CALLBACK(src, PROC_REF(rebuild_if_needed)), 0) + rebuild_queued = TRUE + +/datum/object_system/adjacency_group/proc/rebuild_if_needed() + if(!rebuild_queued) + return + rebuild_queued = FALSE + if(group) + return + rebuild() + +/datum/object_system/adjacency_group/proc/rebuild() + QDEL_NULL(group) + var/datum/adjacency_group/new_group = new(key, create_initial_data()) + new_group.build(src) + +/datum/adjacency_group + /// all systems in group + var/list/datum/object_system/adjacency_group/in_group + /// string key + var/key + /// arbitrary data variable you can set to whatever you want + var/data + +/datum/adjacency_group/New(key) + src.key = key + src.data = data + in_group = list() + +/datum/adjacency_group/Destroy() + teardown() + return ..() + +/datum/adjacency_group/proc/build(datum/object_system/adjacency_group/from_node) + // very destructive build process; when encountering 'enemy', immediately destroy it and trample over + var/list/datum/object_system/adjacency_group/expanding = list(from_node) + var/trait = TRAIT_OBJ_ADJACENCY_GROUP(key) + while(length(expanding)) + var/datum/object_system/adjacency_group/expand_this = expanding[length(expanding)] + --expanding.len + var/dirs_connecting = NONE + + var/atom/expand_loc = expand_this.parent.loc + if(isturf(expand_loc)) + for(var/dir in GLOB.cardinal) + for(var/obj/in_cardinal_tile in get_step(expand_loc, dir)) + var/datum/object_system/adjacency_group/their_cardinal_group = in_cardinal_tile.status_traits?[trait] + if(their_cardinal_group) + dirs_connecting |= dir + if(their_cardinal_group.group == src) + break + expanding += in_cardinal_tile.status_traits[trait] + break + + for(var/obj/in_tile in expand_loc) + var/datum/object_system/adjacency_group/their_group = in_tile.status_traits?[trait] + if(!their_group || their_group == expand_this) + continue + dirs_connecting |= ONTOP_BIT + if(their_group.group != src) + if(their_group.group) + qdel(their_group.group) + their_group.group = src + their_group.connected_directions = dirs_connecting + in_group += their_group + + if(expand_this.group != src) + if(expand_this.group) + qdel(expand_this.group) + expand_this.group = src + expand_this.connected_directions = dirs_connecting + in_group += expand_this + + for(var/datum/object_system/adjacency_group/member as anything in in_group) + member.parent.object_adjacency_group_join(member, src, member.connected_directions) + +/datum/adjacency_group/proc/teardown() + for(var/datum/object_system/adjacency_group/member as anything in in_group) + member.parent.object_adjacency_group_leave(member, src, member.connected_directions) + member.group = null + member.connected_directions = NONE + member.queue_rebuild() + in_group = null + +//* /obj hooks *// + +/** + * Called when an adjacency group is being built from ourselves and initial data is needed + */ +/obj/proc/object_adjacency_group_create_initial_data(datum/object_system/adjacency_group/group_holder) + return null + +/** + * Called on adjacency group join + * + * @params + * * group_holder - object system handling the group + * * group - the group in question + * * directions - directions we're connecting in, as X_BIT (e.g. NORTHEAST_BIT). This will only have cardinals and ONTOP_BIT if something is atop us. + */ +/obj/proc/object_adjacency_group_join(datum/object_system/adjacency_group/group_holder, datum/object_system/adjacency_group/group, directions) + return + +/** + * Called on adjacency group leave + * + * @params + * * group_holder - object system handling the group + * * group - the group in question + * * directions - directions we **were** connected in, as X_BIT (e.g. NORTHEAST_BIT). This will only have cardinals and ONTOP_BIT if something was atop us. + */ +/obj/proc/object_adjacency_group_leave(datum/object_system/adjacency_group/group_holder, datum/object_system/adjacency_group/group, directions) + return diff --git a/code/modules/automata/types/wave.dm b/code/modules/automata/types/wave.dm index eaf22a77f8c..ec60b1061a9 100644 --- a/code/modules/automata/types/wave.dm +++ b/code/modules/automata/types/wave.dm @@ -33,7 +33,7 @@ if(WAVE_SPREAD_SHADOW_LIKE) // directionals fully allowed // we use our own dir bits - edges[T] = dir? dir : ALL_DIRECTION_BITS + edges[T] = dir? dir : CARDINAL_DIAGONAL_DIRECTION_BITS powers[T] = power if(WAVE_SPREAD_SHOCKWAVE) // no directionals. we use cardinal bits, though, due to the algorithm.