diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index 000b9471319..397b171772d 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -81,7 +81,7 @@ #define INIT_ORDER_SHUTTLE -21 #define INIT_ORDER_NIGHTSHIFT -22 #define INIT_ORDER_NANOMOB -23 -#define INIT_ORDER_SQUEAK -40 +#define INIT_ORDER_LATE_MAPPING -40 #define INIT_ORDER_PATH -50 #define INIT_ORDER_PERSISTENCE -95 diff --git a/code/controllers/subsystem/late_mapping.dm b/code/controllers/subsystem/late_mapping.dm new file mode 100644 index 00000000000..719ddef2b36 --- /dev/null +++ b/code/controllers/subsystem/late_mapping.dm @@ -0,0 +1,18 @@ +// This subsystem is to initialize things which need to happen after SSatoms +// This is for things which can take a long period of time and shouldnt bog down SSatoms +// Use this for stuff like random room spawners or maze generators +// Basically, this manages atom-based maploaders +SUBSYSTEM_DEF(late_mapping) + name = "Late Mapping" + init_order = INIT_ORDER_LATE_MAPPING + flags = SS_NO_FIRE + /// List of all maze generators to process + var/list/obj/effect/mazegen/generator/maze_generators = list() + +/datum/controller/subsystem/late_mapping/Initialize(start_timeofday) + if(length(maze_generators)) + log_startup_progress("Generating mazes...") + for(var/i in maze_generators) + var/obj/effect/mazegen/generator/MG = i + MG.run_generator() + return ..() diff --git a/code/modules/maze_generation/helper_modules/_helper_module_base.dm b/code/modules/maze_generation/helper_modules/_helper_module_base.dm new file mode 100644 index 00000000000..a68030cfe8e --- /dev/null +++ b/code/modules/maze_generation/helper_modules/_helper_module_base.dm @@ -0,0 +1,14 @@ +/obj/effect/mazegen/module_helper + name = "maze helper" + +/** + * Helper handler proc + * + * This exists as an overridable method so you can do custom helper logic. + * An example of this is removing all windows on a tile. + * + * Arguments: + * * blockwise - This will be TRUE if the maze this is running on is a blockwise maze + */ +/obj/effect/mazegen/module_helper/proc/helper_run(blockwise = FALSE, obj/effect/mazegen/host) + CRASH("spawn_loot() not overriden for [type]") diff --git a/code/modules/maze_generation/helper_modules/entry_exit.dm b/code/modules/maze_generation/helper_modules/entry_exit.dm new file mode 100644 index 00000000000..3c17d4f719c --- /dev/null +++ b/code/modules/maze_generation/helper_modules/entry_exit.dm @@ -0,0 +1,13 @@ +// Forces entry and exit points +/obj/effect/mazegen/module_helper/entry_exit + name = "entrance/exit allocator" + +/obj/effect/mazegen/module_helper/entry_exit/helper_run(blockwise = FALSE, obj/effect/mazegen/host) + if(blockwise) + var/turf/T = get_turf(src) + var/obj/effect/mazegen/generator/blockwise/BWG = host + T.ChangeTurf(BWG.floor_material) + else + for(var/obj/structure/window/reinforced/mazeglass/MG in get_turf(src)) + qdel(MG) + diff --git a/code/modules/maze_generation/loot_modules/_loot_module_base.dm b/code/modules/maze_generation/loot_modules/_loot_module_base.dm new file mode 100644 index 00000000000..ea6a996343b --- /dev/null +++ b/code/modules/maze_generation/loot_modules/_loot_module_base.dm @@ -0,0 +1,16 @@ +/obj/effect/mazegen/module_loot + name = "maze loot" + /// Probability for this to spawn in a dead end at all. 0-100%. I dont recommend using values higher than 10 + var/spawn_probability = 0 + +/** + * Loot spawner proc + * + * This exists as an overridable method so you can do more than just spawn objects. + * An example of this is spawning a specific mob based on a condition. + * + * Arguments: + * * T - The turf loot will be spawned on + */ +/obj/effect/mazegen/module_loot/proc/spawn_loot(turf/T) + CRASH("spawn_loot() not overriden for [type]") diff --git a/code/modules/maze_generation/loot_modules/turf_painter.dm b/code/modules/maze_generation/loot_modules/turf_painter.dm new file mode 100644 index 00000000000..4ba39728928 --- /dev/null +++ b/code/modules/maze_generation/loot_modules/turf_painter.dm @@ -0,0 +1,7 @@ +// Debug spawner +/obj/effect/mazegen/module_loot/turf_painter + name = "turf painter" + spawn_probability = 100 + +/obj/effect/mazegen/module_loot/turf_painter/spawn_loot(turf/T) + T.color = "#12a5f4" diff --git a/code/modules/maze_generation/maze_generator.dm b/code/modules/maze_generation/maze_generator.dm new file mode 100644 index 00000000000..96f6392a3f1 --- /dev/null +++ b/code/modules/maze_generation/maze_generator.dm @@ -0,0 +1,202 @@ +/* + Everything in this file is related to the actual maze generator itself + This does the brunt of the work and handles the actual calculations of the mazes + + It is very finnicky code and isnt easy to work with since BYOND doesnt like wallwise mazes, and this would be much better being blockwise + + I dont recommend touching the stuff in here, its very finnicky and can be difficult to get your head around if youre not familiar with maze generation algorithms + -AA07 + +*/ + +// Nice way to format logs +#define LOG_MAZE_PROGRESS(proc2run, opname) \ +do { \ + var/timer = start_watch(); \ + proc2run ;\ + log_debug("\[MAZE] Operation '[opname]' on maze at [##x],[##y],[##z] took [stop_watch(timer)]s"); \ +} while (FALSE) + + +// These defines are used to mark the cells as explored or not +#define MAZEGEN_TURF_UNSEARCHED "#ff0000" +#define MAZEGEN_TURF_CELL "#00ff00" + +// Dont place this in the very corner of a map. It relies on adjacent turfs, and at the very edges you dont have turfs on all sides +/obj/effect/mazegen/generator + name = "maze generator" + /// List of turfs to iterate in total + var/list/turf_list = list() + /// "Stack" structure to be used while iterating + var/list/working_stack = list() + /// List of all loot modules being used in this maze + var/list/obj/effect/mazegen/module_loot/loot_modules = list() + /// List of all helper modules being used in this maze + var/list/obj/effect/mazegen/module_helper/helper_modules = list() + /// Potential loot spots + var/list/turf/potential_loot_spots = list() + /// Maze width + var/mwidth = 0 + /// Maze height + var/mheight = 0 + +/obj/effect/mazegen/generator/Initialize(mapload) + . = ..() + SSlate_mapping.maze_generators += src + +/obj/effect/mazegen/generator/Destroy() + SSlate_mapping.maze_generators -= src + return ..() + +// "Push" a turf to the working "stack" +/obj/effect/mazegen/generator/proc/push_turf(turf/T) + working_stack.Add(T) // Add it to the end of the list + +// "Pop" a turf off the working "stack" +/obj/effect/mazegen/generator/proc/pop_turf() + var/turf/T = working_stack[length(working_stack)] // Get the last item in the list + working_stack.Remove(T) // Take it off the top + return T // Send it back + +// Main generator runner. Override on children +/obj/effect/mazegen/generator/proc/run_generator() + ASSERT(ISODD(mwidth)) + ASSERT(ISODD(mheight)) + var/total_time = start_watch() + log_debug("\[MAZE] Started generation on maze at [x],[y],[z] | [mwidth * mheight] turfs total") + LOG_MAZE_PROGRESS(generate_path(), "Path Generation") + LOG_MAZE_PROGRESS(apply_helper_modules(FALSE), "Helper Modules") + if(length(loot_modules)) // Only bother with this if we have some + LOG_MAZE_PROGRESS(calculate_loot_spots(), "Loot Spot Calculation") + LOG_MAZE_PROGRESS(apply_loot_modules(), "Loot Modules") + log_debug("\[MAZE] Generation of maze at [x],[y],[z] complete within [stop_watch(total_time)]s") + qdel(src) + +/obj/effect/mazegen/generator/proc/generate_path() + // Setup our turf list + var/width = clamp(mwidth, 0, (world.maxx - x)) // Make sure they dont loop off the world + var/height = clamp(mheight, 0, (world.maxy - y)) // Make sure they dont loop off the world + + // Generate a turf stack + for(var/target_x in 0 to (width - 1)) // -1 so it spawns on the right tile + for(var/target_y in 0 to (height - 1)) + var/turf/T = locate((x + target_x), (y + target_y), z) + // Mark as unsearched + T.color = MAZEGEN_TURF_UNSEARCHED + // Throw it in the list + turf_list |= T + + // Windows time + var/obj/structure/window/reinforced/mazeglass/WN = new(T) + WN.dir = NORTH + var/obj/structure/window/reinforced/mazeglass/WS = new(T) + WS.dir = SOUTH + var/obj/structure/window/reinforced/mazeglass/WE = new(T) + WE.dir = EAST + var/obj/structure/window/reinforced/mazeglass/WW = new(T) + WW.dir = WEST + CHECK_TICK + CHECK_TICK + + // Do the actual work + push_turf(turf_list[1]) // Use the first turf as the stack base + while(length(working_stack)) + var/turf/T = pop_turf() + + // If you dont force cast these to strings, stuff cries. A lot. + var/list/cardinals = list("[NORTH]", "[SOUTH]", "[EAST]", "[WEST]") + + // Unvisited turfs + var/list/turf/unvisited_neighbours = list() + + // Check all cardinal turfs + for(var/D in cardinals) + var/turf/T2 = get_step(T, text2num(D)) + if(T2.color == MAZEGEN_TURF_UNSEARCHED) + unvisited_neighbours["[D]"] += T2 + + if(length(unvisited_neighbours)) + push_turf(T) + var/D = pick(unvisited_neighbours) + var/turf/T3 = unvisited_neighbours["[D]"] // Pick random dir turf + + // Remove the window between the two + for(var/obj/structure/window/reinforced/mazeglass/W in T) + if(W.dir == text2num(D)) + qdel(W) + + // On both tiles + for(var/obj/structure/window/reinforced/mazeglass/W in T3) + if(W.dir == GetOppositeDir(text2num(D))) + qdel(W) + + // Mark as visited + T3.color = MAZEGEN_TURF_CELL + push_turf(T3) + + CHECK_TICK + + // The walls have been generated, now cleanup turfs and gather helpers + for(var/i in turf_list) + var/turf/T = i + // Reset markings + T.color = null + + // Gather loot modules + for(var/obj/effect/mazegen/module_loot/ML in T) + loot_modules |= ML + CHECK_TICK + + // Gather helper modules + for(var/obj/effect/mazegen/module_helper/MH in T) + helper_modules |= MH + CHECK_TICK + + // Make sure we have adequate CPU + CHECK_TICK + +/obj/effect/mazegen/generator/proc/apply_helper_modules(blockwise = FALSE) + // Apply helpers + for(var/i in helper_modules) + var/obj/effect/mazegen/module_helper/MH = i + helper_modules -= MH // Make it happy + MH.helper_run(blockwise, src) + qdel(MH) + +/obj/effect/mazegen/generator/proc/calculate_loot_spots() + for(var/i in turf_list) + var/turf/T = i + // Gather the windows. If a spot has 3 windows on it, its a dead end + var/windowcount = 0 + for(var/obj/structure/window/reinforced/mazeglass/W in T) + pass(W) // Stops DM whining about unused vars + windowcount++ + CHECK_TICK + + // Its a dead end. Mark for loot. + if(windowcount == 3) + potential_loot_spots |= T + + CHECK_TICK + +/obj/effect/mazegen/generator/proc/apply_loot_modules() + for(var/i in potential_loot_spots) + var/turf/T = i + + // Apply loot modules + var/obj/effect/mazegen/module_loot/ML = pick(loot_modules) + if(prob(ML.spawn_probability)) + ML.spawn_loot(T) // Spawn on the turf + + CHECK_TICK + + // Cleanup loot modules + for(var/i in loot_modules) + var/obj/effect/mazegen/module_loot/ML = i + loot_modules -= ML // Take it out to make the GC happy + qdel(ML) + CHECK_TICK + + +#undef MAZEGEN_TURF_UNSEARCHED +#undef MAZEGEN_TURF_CELL diff --git a/code/modules/maze_generation/maze_generator_blockwise.dm b/code/modules/maze_generation/maze_generator_blockwise.dm new file mode 100644 index 00000000000..94850e2ea38 --- /dev/null +++ b/code/modules/maze_generation/maze_generator_blockwise.dm @@ -0,0 +1,124 @@ +// Everything in this file is related to the blockwise maze generator. +// It uses some components from the standard, but tweaks them to its own needs, since it needs to generate a half size maze then expand it out + +#define MAZEGEN_TURF_UNSEARCHED "#ff0000" +#define MAZEGEN_TURF_CELL "#00ff00" + +/obj/effect/mazegen/generator/blockwise + name = "blockwise maze generator" + /// Material to make the walls out of + var/turf/wall_material = /turf/simulated/wall/indestructible/riveted + /// Material to make the floor out of + var/turf/floor_material = /turf/simulated/floor/engine + /// List of open spots (speeds up calculations) + var/list/turf/open_spots = list() + +// First we need to override the main proc so it only calculates half +/obj/effect/mazegen/generator/blockwise/run_generator() + ASSERT(ISODD(mwidth)) + ASSERT(ISODD(mheight)) + var/total_time = start_watch() + log_debug("\[MAZE] Started generation on maze at [x],[y],[z] | [mwidth * mheight] turfs total") + // First we need to partition the maze out into forced walls and open cells + LOG_MAZE_PROGRESS(generate_path(), "Generate Path") + LOG_MAZE_PROGRESS(apply_helper_modules(TRUE), "Helper Modules") + if(length(loot_modules)) // Only bother with this if we have some + LOG_MAZE_PROGRESS(calculate_loot_spots(), "Loot Spot Calculation") + LOG_MAZE_PROGRESS(apply_loot_modules(), "Loot Modules") + log_debug("\[MAZE] Generation of maze at [x],[y],[z] complete within [stop_watch(total_time)]s") + qdel(src) + + +/obj/effect/mazegen/generator/blockwise/generate_path() + // Setup our turf list + var/width = clamp(mwidth, 0, (world.maxx - x)) // Make sure they dont loop off the world + var/height = clamp(mheight, 0, (world.maxy - y)) // Make sure they dont loop off the world + + // Generate a turf stack + for(var/target_x in 0 to (width - 1)) // -1 so it spawns on the right tile + for(var/target_y in 0 to (height - 1)) + var/turf/T = locate((x + target_x), (y + target_y), z) + // Mark as unsearched + T.color = MAZEGEN_TURF_UNSEARCHED + // Throw it in the list + turf_list |= T + CHECK_TICK + CHECK_TICK + + // Do the actual work + push_turf(turf_list[1]) // Use the first turf as the stack base + while(length(working_stack)) + var/turf/T = pop_turf() + + // If you dont force cast these to strings, stuff cries. A lot. + var/list/cardinals = list("[NORTH]", "[SOUTH]", "[EAST]", "[WEST]") + + // Unvisited turfs + var/list/turf/unvisited_neighbours = list() + + // Check all cardinal turfs + for(var/D in cardinals) + var/turf/T2 = get_step(get_step(T, text2num(D)), text2num(D)) // Get the step TWICE because this is wallwise + if(T2?.color == MAZEGEN_TURF_UNSEARCHED) + unvisited_neighbours["[D]"] += T2 + + if(length(unvisited_neighbours)) + push_turf(T) + var/D = pick(unvisited_neighbours) + var/turf/T3 = unvisited_neighbours["[D]"] // Pick random dir turf + + // Remove the color between the two + var/turf/T4 = get_step(T3, GetOppositeDir(text2num(D))) + T4?.color = MAZEGEN_TURF_CELL + + // Mark as visited + T3.color = MAZEGEN_TURF_CELL + push_turf(T3) + + CHECK_TICK + + // The walls have been generated, now cleanup turfs and gather helpers + for(var/i in turf_list) + var/turf/T = i + // Set correct turfs + if(T.color == MAZEGEN_TURF_CELL) + open_spots |= T + T.ChangeTurf(floor_material) + else + T.ChangeTurf(wall_material) + T.color = null + + // Gather loot modules + for(var/obj/effect/mazegen/module_loot/ML in T) + loot_modules |= ML + CHECK_TICK + + // Gather helper modules + for(var/obj/effect/mazegen/module_helper/MH in T) + helper_modules |= MH + CHECK_TICK + + // Make sure we have adequate CPU + CHECK_TICK + +/obj/effect/mazegen/generator/blockwise/calculate_loot_spots() + for(var/i in open_spots) + var/turf/T = i + // Gather amount of dense turfs on each side + var/dense_surrounding_turfs = 0 + // Again, these gotta be casted like this + var/list/cardinals = list("[NORTH]", "[EAST]", "[SOUTH]", "[WEST]") + for(var/D in cardinals) + var/turf/T2 = get_step(T, text2num(D)) + if(T2.density) + dense_surrounding_turfs++ + CHECK_TICK + + // Its a dead end. Mark for loot. + if(dense_surrounding_turfs == 3) + potential_loot_spots |= T + + CHECK_TICK + +#undef MAZEGEN_TURF_UNSEARCHED +#undef MAZEGEN_TURF_CELL diff --git a/code/modules/maze_generation/maze_helper_atoms.dm b/code/modules/maze_generation/maze_helper_atoms.dm new file mode 100644 index 00000000000..3e4c4dfd02f --- /dev/null +++ b/code/modules/maze_generation/maze_helper_atoms.dm @@ -0,0 +1,16 @@ +// Initial declaration +/obj/effect/mazegen + desc = "You should not be seeing this!" + icon = 'icons/mob/screen_gen.dmi' + icon_state = "x2" + color = "#00FF00" + +// Windows which cant be taken apart. Thes are subtypes of windows so you can have atoms you can walk in at some dirs bit not others +/obj/structure/window/reinforced/mazeglass + name = "maze glass" + desc = "You cant take this down. Looks like you have to solve the maze." + resistance_flags = INDESTRUCTIBLE + +// No taking apart +/obj/structure/window/reinforced/mazeglass/tool_act(mob/living/user, obj/item/I, tool_type) + return FALSE diff --git a/paradise.dme b/paradise.dme index e46d1e7f280..6ef21deda02 100644 --- a/paradise.dme +++ b/paradise.dme @@ -242,6 +242,7 @@ #include "code\controllers\subsystem\input.dm" #include "code\controllers\subsystem\ipintel.dm" #include "code\controllers\subsystem\jobs.dm" +#include "code\controllers\subsystem\late_mapping.dm" #include "code\controllers\subsystem\lighting.dm" #include "code\controllers\subsystem\machinery.dm" #include "code\controllers\subsystem\mapping.dm" @@ -1724,6 +1725,13 @@ #include "code\modules\martial_arts\combos\sleeping_carp\head_kick.dm" #include "code\modules\martial_arts\combos\sleeping_carp\stomach_knee.dm" #include "code\modules\martial_arts\combos\sleeping_carp\wrist_wrench.dm" +#include "code\modules\maze_generation\maze_generator.dm" +#include "code\modules\maze_generation\maze_generator_blockwise.dm" +#include "code\modules\maze_generation\maze_helper_atoms.dm" +#include "code\modules\maze_generation\helper_modules\_helper_module_base.dm" +#include "code\modules\maze_generation\helper_modules\entry_exit.dm" +#include "code\modules\maze_generation\loot_modules\_loot_module_base.dm" +#include "code\modules\maze_generation\loot_modules\turf_painter.dm" #include "code\modules\mining\abandonedcrates.dm" #include "code\modules\mining\fulton.dm" #include "code\modules\mining\machine_processing.dm"