diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index b885bb12b81..a8db9013b1f 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -204,6 +204,7 @@ GLOBAL_PROTECT(admin_verbs_debug) /client/proc/cmd_display_overlay_log, /client/proc/reload_configuration, /client/proc/reload_interactions, //SKYRAT EDIT ADDITION + /client/proc/test_area_spawner, // SKYRAT EDIT ADDITION - AUTOMAPPER /client/proc/atmos_control, /client/proc/reload_cards, /client/proc/validate_cards, @@ -304,6 +305,7 @@ GLOBAL_LIST_INIT(admin_verbs_hideable, list( /client/proc/toggle_combo_hud, /client/proc/admin_open_event_spawners_menu, //SKYRAT EDIT ADDITION - EVENTS /client/proc/spawn_pollution, // SKYRAT EDIT ADDITION + /client/proc/test_area_spawner, // SKYRAT EDIT ADDITION - AUTOMAPPER /client/proc/spawn_liquid, /client/proc/debug_huds )) diff --git a/modular_skyrat/modules/automapper/code/area_spawn_entries.dm b/modular_skyrat/modules/automapper/code/area_spawn_entries.dm index e38cfd3356f..8bb0744a4a2 100644 --- a/modular_skyrat/modules/automapper/code/area_spawn_entries.dm +++ b/modular_skyrat/modules/automapper/code/area_spawn_entries.dm @@ -1,120 +1,3 @@ -#define RESTRICTED_OBJECTS_LIST list(/obj/machinery/recharge_station) -#define RESTRICTED_CARDINAL_OBJECTS_LIST list(/obj/machinery/disposal/bin, /obj/structure/table, /obj/machinery/door, /obj/structure/closet) - -SUBSYSTEM_DEF(area_spawn) - name = "Area Spawn" - flags = SS_NO_FIRE - -/datum/controller/subsystem/area_spawn/Initialize(start_timeofday) - for(var/iterating_type in subtypesof(/datum/area_spawn)) - var/datum/area_spawn/iterating_area_spawn = new iterating_type - iterating_area_spawn.try_spawn() - return ..() - -/** - * Area spawn datums - * - * Use these to spawn atoms in areas instead of placing them on a map. It will select any available open and entering turf. - */ -/datum/area_spawn - /// The target area for us to spawn the desired atom, the list is formatted, highest priority first. - var/list/target_areas - /// The atom that we want to spawn - var/desired_atom - /// The amount we want to spawn - var/amount_to_spawn = 1 - /// The max amount that the world can have - var/max_amount = 1 - /// Do we need to be adjacent to a wall? This also checks the other 3 cardinals for no density. Generally useful for objects with density and anchored. - var/wall_hug = FALSE - /// Map blacklist, this is used to determine what maps we should not spawn on. - var/list/blacklisted_stations = list("Blueshift", "Runtime Station", "MultiZ Debug") - -/** - * Attempts to find a location using an algorithm to spawn the desired atom. - */ -/datum/area_spawn/proc/try_spawn() - if(SSmapping.config.map_name in blacklisted_stations) - return - - // Turfs that are available - var/list/available_turfs = list() - // Turfs that are available and completely void of contents - var/list/available_empty_turfs = list() - - for(var/area_type in target_areas) - if(LAZYLEN(available_turfs)) - break - var/area/found_area = GLOB.areas_by_type[area_type] - if(!found_area) - continue - for(var/turf/iterating_turf in found_area) - if(iterating_turf.density) - continue - var/density_found = FALSE - for(var/atom/movable/found_movable in iterating_turf) - if(found_movable.density) - density_found = TRUE - break - if(is_type_in_list(found_movable, RESTRICTED_OBJECTS_LIST)) - density_found = TRUE - break - if(density_found) - continue - // Time to check cardinals - // The cardinals must be clear of any density, aside from wall cardinals! - var/cardinal_density_check = TRUE - // Some items want to be hugging a wall, such as lockers, and machines. It just looks better. - var/wall_check = FALSE - for(var/dir in GLOB.cardinals) - var/turf/cardinal_test_turf = get_step(iterating_turf, dir) - if(isclosedturf(cardinal_test_turf)) - wall_check = TRUE - for(var/atom/movable/found_movable in cardinal_test_turf) - if(found_movable.density) - cardinal_density_check = FALSE - if(wall_hug && !wall_check) - continue - if(!cardinal_density_check && wall_hug) - continue - // Finally we want to prioritise entirely empty turfs - var/totally_empty = TRUE - for(var/atom/movable/found_movable in iterating_turf) - if(found_movable.layer > LOW_OBJ_LAYER) - totally_empty = FALSE - break - if(totally_empty) - available_empty_turfs += iterating_turf - available_turfs += iterating_turf - - if(!LAZYLEN(available_turfs)) - CRASH("[src.type] could not find any suitable turfs on map [SSmapping.config.map_name]!") - - for(var/i in 1 to amount_to_spawn) - if(LAZYLEN(available_empty_turfs)) - var/turf/picked_turf = pick(available_empty_turfs) - available_empty_turfs -= picked_turf - new desired_atom(picked_turf) - else - var/turf/picked_turf = pick(available_turfs) - available_turfs -= picked_turf - new desired_atom(picked_turf) - -/obj/effect/turf_test - name = "PASS" - icon = 'modular_skyrat/modules/automapper/icons/area_test.dmi' - icon_state = "area_test" - color = COLOR_GREEN - anchored = TRUE - -/obj/effect/turf_test/fail - name = "FAIL" - color = COLOR_RED - -/obj/effect/turf_test/empty - name = "PASS(EMPTY)" - color = COLOR_BLUE - // Pets /datum/area_spawn/markus target_areas = list(/area/station/cargo/sorting, /area/station/cargo/storage, /area/station/cargo/office, /area/station/cargo/qm) @@ -143,7 +26,7 @@ SUBSYSTEM_DEF(area_spawn) wall_hug = TRUE /datum/area_spawn/command_drobe - target_areas = list(/area/station/command/meeting_room, /area/station/command/bridge) + target_areas = list(/area/station/command/meeting_room, /area/station/command/meeting_room/council, /area/station/command/bridge) desired_atom = /obj/machinery/vending/access/command wall_hug = TRUE @@ -157,8 +40,8 @@ SUBSYSTEM_DEF(area_spawn) desired_atom = /obj/machinery/gun_vendor wall_hug = TRUE -/datum/area_spawn/lustwish_dorms - target_areas = list(/area/station/commons/locker, /area/station/commons/dorms) +/datum/area_spawn/lustwish_public + target_areas = list(/area/station/commons/locker, /area/station/commons/dorms/laundry, /area/station/commons/dorms) desired_atom = /obj/machinery/vending/dorms wall_hug = TRUE @@ -203,6 +86,3 @@ SUBSYSTEM_DEF(area_spawn) /datum/area_spawn/customs_agent_landmark desired_atom = /obj/effect/landmark/start/customs_agent target_areas = list(/area/station/security/checkpoint/supply, /area/station/cargo/storage) - -#undef RESTRICTED_OBJECTS_LIST -#undef RESTRICTED_CARDINAL_OBJECTS_LIST diff --git a/modular_skyrat/modules/automapper/code/area_spawn_subsystem.dm b/modular_skyrat/modules/automapper/code/area_spawn_subsystem.dm new file mode 100644 index 00000000000..6490ca23b47 --- /dev/null +++ b/modular_skyrat/modules/automapper/code/area_spawn_subsystem.dm @@ -0,0 +1,278 @@ +SUBSYSTEM_DEF(area_spawn) + name = "Area Spawn" + flags = SS_NO_FIRE + + // Can't be on tile or a neighbor. + // Usually things where it's important to be sure the players can walk up to them, but aren't dense. + var/list/restricted_objects_list = list( + /obj/machinery/recharge_station, + /obj/machinery/disposal/bin, + /obj/structure/table, + /obj/machinery/door, + /obj/structure/closet, + /obj/structure/stairs, + ) + + // Only Blacklist if on same tile because looks bad, etc, but doesn't need to be reached. + var/list/restricted_overlap_objects_list = list( + /obj/item/kirbyplants, + ) + + // Things here in some way act as walls. This is the result of extensive tweaking. + var/list/allowed_diagonal_objects_list = list( + /obj/structure/grille, + /obj/structure/window, + /obj/machinery/door, + ) + + var/list/datum/area_turf_info/area_turf_cache = list() + +/datum/controller/subsystem/area_spawn/Initialize(start_timeofday) + for(var/iterating_type in subtypesof(/datum/area_spawn)) + var/datum/area_spawn/iterating_area_spawn = new iterating_type + iterating_area_spawn.try_spawn() + clear_cache() + return ..() + +/** + * Clear the cached tiles for optimization or debugging purposes. + */ +/datum/controller/subsystem/area_spawn/proc/clear_cache() + for(var/datum/cache in area_turf_cache) + qdel(cache) + LAZYCLEARLIST(area_turf_cache) + +/** + * Process the geometry of an area and cache the candidates. + * + * Returns turf candidate list. "[priority]" = + * + * Arguments: + * * area - the area to process + * * wall_hug - Are we processing for wall_hug case or not? + */ +/datum/controller/subsystem/area_spawn/proc/get_turf_candidates(area/area, wall_hug) + // Get area cache or make a new one. + var/datum/area_turf_info/area_turf_info + if(!area_turf_cache[area.type]) + area_turf_info = area_turf_cache[area.type] = new /datum/area_turf_info() + else + area_turf_info = area_turf_cache[area.type] + + // Different use cases have different lists of turfs. + // Get or create the cached list. + var/list/turf_list + if(wall_hug) + if(area_turf_info.available_wall_turfs) + return area_turf_info.available_wall_turfs + turf_list = area_turf_info.available_wall_turfs = list() + else + if(area_turf_info.available_open_turfs) + return area_turf_info.available_open_turfs + turf_list = area_turf_info.available_open_turfs = list() + + // Get highest priority items + for(var/turf/iterating_turf in area) + // Only retain turfs of the highest priority + var/priority = process_turf(iterating_turf, wall_hug) + if(priority > 0) + LAZYADDASSOC(turf_list, "[priority]", list(iterating_turf)) + + // Sort the priorities descending + return sortTim(turf_list, /proc/cmp_num_string_asc) + +/** + * Process a specific turf and return priority number from 0 to infinity. + * + * Turfs with highest priority will be picked. Priority 0 means NEVER. + * + * Arguments: + * * turf - The turf to process + * * wall_hug - Are we processing for wall_hug case or not? + */ +/datum/controller/subsystem/area_spawn/proc/process_turf(turf/turf, wall_hug) + // Only spawn on actual floors + if(!isfloorturf(turf)) + return 0 + + // Turf completely empty? + var/totally_empty = TRUE + for(var/atom/movable/found_movable in turf) + // Same tile conditions for no-go + if( + found_movable.density \ + || is_type_in_list(found_movable, restricted_objects_list) \ + || is_type_in_list(found_movable, restricted_overlap_objects_list) + ) + return 0 + if(found_movable.layer > LOW_OBJ_LAYER && found_movable.layer < ABOVE_MOB_LAYER) + totally_empty = FALSE + + // Number of directions that have a closed wall + var/num_walls_found = 0 + // Found a dense object? + var/found_dense_object = FALSE + // Number of directions that have anything dense + var/num_dense_found = 0 + // Number of directions that have 2 squares of open space. + var/num_very_open_floors = 0 + for(var/dir in GLOB.cardinals) + var/turf/neighbor_turf = get_step(turf, dir) + if(isclosedturf(neighbor_turf)) + num_walls_found++ + num_dense_found++ + continue + if(wall_hug) + var/turf/long_test_turf = get_step(neighbor_turf, dir) + if(isopenturf(long_test_turf)) + num_very_open_floors++ + for(var/atom/movable/found_movable in neighbor_turf) + if(found_movable.density || is_type_in_list(found_movable, restricted_objects_list)) + found_dense_object = TRUE + num_dense_found++ + break + + // Wall hugging also, as a low priority, doesn't even want diagnal things + var/num_diagonal_objects = 0 + if(wall_hug) + for(var/dir in GLOB.diagonals) + var/turf/neighbor_turf = get_step(turf, dir) + for(var/atom/movable/found_movable in neighbor_turf) + if( + !is_type_in_list(found_movable, allowed_diagonal_objects_list) \ + && (found_movable.density || is_type_in_list(found_movable, restricted_objects_list)) + ) + num_diagonal_objects++ + break + + if(wall_hug) + // For wall hugging, must be against wall, and not touching another dense object as it may completely block it. + if(num_walls_found == 0 || found_dense_object || num_walls_found == 4) + return 0 + + // #1 Priority after that: be in a totally empty square + // #2 favor being in a cozy wall nook + // #3 (marginally) have clear diagnals + // #4 be in a big room/hallway so we don't pinch a room down to 1 square of passage. + return (totally_empty ? 1000 : 0) + (400 - num_diagonal_objects * 100) + (num_walls_found * 10) + num_very_open_floors + + // For non-wall hug + // #1 priority is totally empty + // #2 priority is being in the middle of the room + return (totally_empty ? 10 : 0) + (4 - num_dense_found) + +/** + * Pick a turf candidate and remove from the list. + * + * Only picks one of the highest priority ones. + * + * Arguments: + * * turf_candidates - Turf candidate list produced by + */ +/datum/controller/subsystem/area_spawn/proc/pick_turf_candidate(list/list/turf/turf_candidates) + // Pick-n-take highest priority. + var/list/turf/sublist = turf_candidates[peek(turf_candidates)] + var/turf/winner = pick_n_take(sublist) + + // To be safe, remove the neighbors too. + for(var/dir in GLOB.cardinals) + var/turf/neighbor = get_step(winner, dir) + sublist -= neighbor + + // Remove this priority if it's now empty. + if(!LAZYLEN(sublist)) + pop(turf_candidates) + + // Extremely specific, but landmarks are immediately destroyed when created so can't be detected another way. + // This is the only landmark list that normally creates solid objects in non-maintenance spaces. + GLOB.secequipment -= winner + + return winner + +/** + * Turf into for a given area + */ +/datum/area_turf_info + /// Turfs in the middle of the room + var/list/list/turf/available_open_turfs + /// Turfs hugging the wall + var/list/list/turf/available_wall_turfs + +/** + * Area spawn datums + * + * Use these to spawn atoms in areas instead of placing them on a map. It will select any available open and entering turf. + */ +/datum/area_spawn + /// The target area for us to spawn the desired atom, the list is formatted, highest priority first. + var/list/target_areas + /// The atom that we want to spawn + var/desired_atom + /// The amount we want to spawn + var/amount_to_spawn = 1 + /// Do we need to be adjacent to a wall? This also checks the other 3 cardinals for no density. Generally useful for objects with density and anchored. + var/wall_hug = FALSE + /// Map blacklist, this is used to determine what maps we should not spawn on. + var/list/blacklisted_stations = list("Blueshift", "Runtime Station", "MultiZ Debug") + +/** + * Attempts to find a location using an algorithm to spawn the desired atom. + */ +/datum/area_spawn/proc/try_spawn() + if(SSmapping.config.map_name in blacklisted_stations) + return + + // Turfs that are available + var/list/available_turfs + + for(var/area_type in target_areas) + var/area/found_area = GLOB.areas_by_type[area_type] + if(!found_area) + continue + available_turfs = SSarea_spawn.get_turf_candidates(found_area, wall_hug) + if(LAZYLEN(available_turfs)) + break + + if(!LAZYLEN(available_turfs)) + CRASH("[src.type] could not find any suitable turfs on map [SSmapping.config.map_name]!") + + for(var/i in 1 to amount_to_spawn) + new desired_atom(SSarea_spawn.pick_turf_candidate(available_turfs)) + +/obj/effect/turf_test + name = "PASS" + icon = 'modular_skyrat/modules/automapper/icons/area_test.dmi' + icon_state = "area_test" + color = COLOR_RED + anchored = TRUE + +/obj/effect/turf_test/wall + color = COLOR_BLUE + maptext_y = 16 + +/** + * Show overlay over area of priorities. Wall priority over open priority. + */ +/client/proc/test_area_spawner(area/area) + set category = "Debug" + set name = "Test Area Spawner" + set desc = "Show area spawner placement candidates as an overlay." + + for(var/obj/effect/turf_test/old_test in area) + qdel(old_test) + + SSarea_spawn.clear_cache() + var/list/list/turf/open_candidates = SSarea_spawn.get_turf_candidates(area, FALSE) + var/list/list/turf/wall_candidates = SSarea_spawn.get_turf_candidates(area, TRUE) + + for(var/priority in open_candidates) + var/list/turf/turfs = open_candidates[priority] + for(var/turf/turf as anything in turfs) + var/obj/overlay = new /obj/effect/turf_test(turf) + overlay.maptext = MAPTEXT(priority) + + for(var/priority in wall_candidates) + var/list/turf/turfs = wall_candidates[priority] + for(var/turf/turf as anything in turfs) + var/obj/overlay = new /obj/effect/turf_test/wall(turf) + overlay.maptext = MAPTEXT(priority) diff --git a/tgstation.dme b/tgstation.dme index 17e4ae0598d..f7443a074dc 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -5019,6 +5019,7 @@ #include "modular_skyrat\modules\assault_operatives\code\armaments\armament_utility.dm" #include "modular_skyrat\modules\assault_operatives\code\armaments\assaultops_armament_station.dm" #include "modular_skyrat\modules\automapper\code\area_spawn_entries.dm" +#include "modular_skyrat\modules\automapper\code\area_spawn_subsystem.dm" #include "modular_skyrat\modules\automapper\code\automap_template.dm" #include "modular_skyrat\modules\automapper\code\automapper_subsystem.dm" #include "modular_skyrat\modules\autotransfer\code\autotransfer.dm"