/// The random offset applied to square coordinates, causes intermingling at biome borders #define BIOME_RANDOM_SQUARE_DRIFT 2 // Categories for spawned things #define CAVE_SPAWN_MOB "mob" #define CAVE_SPAWN_FEATURE "feature" #define CAVE_SPAWN_TENDRIL "tendril" #define CAVE_SPAWN_MEGAFAUNA "megafauna" /datum/map_generator/cave_generator var/name = "Cave Generator" ///Weighted list of the types that spawns if the turf is open var/weighted_open_turf_types = list(/turf/open/misc/asteroid/airless = 1) ///Expanded list of the types that spawns if the turf is open var/open_turf_types ///Weighted list of the types that spawns if the turf is closed var/weighted_closed_turf_types = list(/turf/closed/mineral/random = 1) ///Expanded list of the types that spawns if the turf is closed var/closed_turf_types ///Weighted list of mobs that can spawn in the area. var/list/weighted_mob_spawn_list ///Expanded list of mobs that can spawn in the area. Reads from the weighted list var/list/mob_spawn_list ///The mob spawn list but with no megafauna markers. autogenerated var/list/mob_spawn_no_mega_list // Weighted list of Megafauna that can spawn in the area var/list/weighted_megafauna_spawn_list ///Expanded list of Megafauna that can spawn in the area. Reads from the weighted list var/list/megafauna_spawn_list ///Weighted list of flora that can spawn in the area. var/list/weighted_flora_spawn_list ///Expanded list of flora that can spawn in the area. Reads from the weighted list var/list/flora_spawn_list ///Weighted list of extra features that can spawn in the area, such as geysers. var/list/weighted_feature_spawn_list ///Expanded list of extra features that can spawn in the area. Reads from the weighted list var/list/feature_spawn_list /// An associative list of biome type to the list of turfs that were /// generated of that biome specifically. Helps to improve the efficiency /// of biome-related operations. Is populated through /// `generate_terrain_with_biomes()`. var/list/generated_turfs_per_biome = list() /// Same as generated_turfs_per_biome, but additionally indexed by area var/list/generated_turfs_per_area_biome = list() /// 2D list of all biomes based on heat and humidity combos. Associative by /// `BIOME_X_HEAT` and then by `BIOME_X_HUMIDITY` (i.e. /// `possible_biomes[BIOME_LOW_HEAT][BIOME_LOWMEDIUM_HUMIDITY]`). /// Check /datum/map_generator/cave_generator/jungle for an example /// of how to set it up properly. var/list/possible_biomes = list() /// Used to select "zoom" level into the perlin noise, higher numbers /// result in slower transitions var/perlin_zoom = 65 /// Should we populate turfs through biome datums if we have such var/biome_population = TRUE /// Noise threshold above which a biome is considered high heat var/high_heat_threshold = -0.1 /// Noise threshold above which a biome is considered medium heat var/medium_heat_threshold = -0.3 /// Noise threshold above which a biome is considered high humidity var/high_humidity_threshold = -0.1 /// Noise threshold above which a biome is considered medium humidity var/medium_humidity_threshold = -0.3 /// Should all generators on the same z level use the same seeds? var/shared_seed = TRUE /// Stamp size for DBP noise, aka frequency var/biome_stamp_size = 75 /// Stored noise maps per z level if we use a shared seed var/static_biome_maps = list() /// Base chance of spawning a mob var/mob_spawn_chance = 6 /// Base chance of spawning flora var/flora_spawn_chance = 2 /// Base chance of spawning features var/feature_spawn_chance = 0.25 /// Unique ID for this spawner var/string_gen /// Radius around features within which we avoid spawning other features var/feature_exclusion_radius = 7 /// Radius around mobs which we avoid spawning other mobs var/mob_exclusion_radius = 12 /// Radius around megafauna within which we avoid spawning tendrils var/megafauna_exclusion_radius = 7 ///Cave gen settings below!! /// Minimum dimension of a BSP leaf in the generator. Raising this creates larger pockets but can end up making for big corridors var/min_bsp_size = 25 /// Maximum aspect ratio for BSP splits for lavaland generator var/max_ratio = 1.5 /// Room edge padding within BSP leaf for lavaland generator var/padding = 1 /// How much of each BSP leaf is considered untouchable by the cellular automata. Raising this generally means bigger pockets var/room_fill_percent = 30 /// Width of corridors between rooms for lavaland generator. Raising this just means corridors are AT LEAST this wide. but cellular automata can make them bigger var/corridor_width = 1 /// Chance to add extra MST edges for loops for lavaland generator. This basically results in more corridors / mazier generation var/loop_percent = 15 /// Initial random floor density for lavaland generator var/noise_percent = 51 /// Cellular Automata smoothing iterations for lavaland generator var/ca_steps = 8 /// Neighbors to create floor (>=) for lavaland generator var/birth_limit = 6 /// Neighbors to survive as floor (>=) for lavaland generator var/survival_limit = 4 ///Whether out-of-boudns counts as being alive. Setting this to FALSE results in the edges of the generator generating more closed. Default behavior tends to open up tunnels outside. var/edges_are_alive = TRUE /datum/map_generator/cave_generator/New() . = ..() if(!weighted_mob_spawn_list) weighted_mob_spawn_list = list( /mob/living/basic/mining/basilisk = 4, /mob/living/basic/mining/goldgrub = 1, /mob/living/basic/mining/goliath/ancient = 5, /mob/living/basic/mining/hivelord = 3, ) mob_spawn_list = expand_weights(weighted_mob_spawn_list) mob_spawn_no_mega_list = expand_weights(weighted_mob_spawn_list - SPAWN_MEGAFAUNA) if(!weighted_megafauna_spawn_list) weighted_megafauna_spawn_list = GLOB.megafauna_spawn_list megafauna_spawn_list = expand_weights(weighted_megafauna_spawn_list) if(!weighted_flora_spawn_list) weighted_flora_spawn_list = list( /obj/structure/flora/ash/leaf_shroom = 2, /obj/structure/flora/ash/cap_shroom = 2, /obj/structure/flora/ash/stem_shroom = 2, /obj/structure/flora/ash/cacti = 1, /obj/structure/flora/ash/tall_shroom = 2, /obj/structure/flora/ash/seraka = 2, ) flora_spawn_list = expand_weights(weighted_flora_spawn_list) if(!weighted_feature_spawn_list) weighted_feature_spawn_list = list( /obj/structure/geyser/random = 1, /obj/structure/ore_vent/random = 1, ) feature_spawn_list = expand_weights(weighted_feature_spawn_list) open_turf_types = expand_weights(weighted_open_turf_types) closed_turf_types = expand_weights(weighted_closed_turf_types) /datum/map_generator/cave_generator/generate_terrain(list/turfs, area/generate_in) . = ..() if(!(generate_in.area_flags_mapping & CAVES_ALLOWED)) return if(length(possible_biomes)) return generate_terrain_with_biomes(turfs, generate_in) var/start_time = REALTIMEOFDAY string_gen = generate_cave(generate_in) for(var/turf/gen_turf as anything in turfs) //Go through all the turfs and generate them //SKYRAT EDIT ADDITION if(istype(gen_turf, /turf/open/space/mirage)) continue //SKYRAT EDIT END var/closed = string_gen[world.maxx * (gen_turf.y - 1) + gen_turf.x] != "1" var/turf/new_turf = pick(closed ? closed_turf_types : open_turf_types) // The assumption is this will be faster then changeturf, and changeturf isn't required since by this point // The old tile hasn't got the chance to init yet new_turf = new new_turf(gen_turf) if(gen_turf.turf_flags & NO_RUINS) new_turf.turf_flags |= NO_RUINS var/message = "[name] terrain generation finished in [(REALTIMEOFDAY - start_time)/10]s!" to_chat(world, span_boldannounce("[message]"), MESSAGE_TYPE_DEBUG) log_world(message) /** * This proc handles including biomes in the cave generation. This is slower than * `generate_terrain()`, so please use it only if you actually need biomes. * * This should only be called by `generate_terrain()`, if you have to call this, * you're probably doing something wrong. */ /datum/map_generator/cave_generator/proc/generate_terrain_with_biomes(list/turf/turfs, area/generate_in) if(!(generate_in.area_flags_mapping & CAVES_ALLOWED)) return var/humidity_seed = null var/heat_seed = null // Make sure that all caves on the same z level generate smoothly regardless of the area if (shared_seed) var/static/list/static_heat = null var/static/list/static_humi = null if (!static_heat) static_heat = list() static_humi = list() var/z_key = "[turfs[1].z]" if (!static_heat[z_key]) static_heat[z_key] = rand(0, 50000) static_humi[z_key] = rand(0, 50000) heat_seed = static_heat[z_key] humidity_seed = static_humi[z_key] else humidity_seed = rand(0, 50000) heat_seed = rand(0, 50000) var/start_time = REALTIMEOFDAY string_gen = generate_cave(generate_in) var/humidity_gen = list() humidity_gen[BIOME_HIGH_HUMIDITY] = rustg_dbp_generate("[humidity_seed]", "60", "[biome_stamp_size]", "[world.maxx]", "[high_heat_threshold]", "1.1") humidity_gen[BIOME_MEDIUM_HUMIDITY] = rustg_dbp_generate("[humidity_seed]", "60", "[biome_stamp_size]", "[world.maxx]", "[medium_heat_threshold]", "[high_heat_threshold]") var/heat_gen = list() heat_gen[BIOME_HIGH_HEAT] = rustg_dbp_generate("[heat_seed]", "60", "[biome_stamp_size]", "[world.maxx]", "[high_heat_threshold]", "1.1") heat_gen[BIOME_MEDIUM_HEAT] = rustg_dbp_generate("[heat_seed]", "60", "[biome_stamp_size]", "[world.maxx]", "[medium_heat_threshold]", "[high_heat_threshold]") if (shared_seed) static_biome_maps["[turfs[1].z]"] = list( BIOME_HIGH_HUMIDITY = humidity_gen[BIOME_HIGH_HUMIDITY], BIOME_MEDIUM_HUMIDITY = humidity_gen[BIOME_MEDIUM_HUMIDITY], BIOME_HIGH_HEAT = heat_gen[BIOME_HIGH_HEAT], BIOME_MEDIUM_HEAT = heat_gen[BIOME_MEDIUM_HEAT], ) var/list/to_generate = list() for(var/turf/gen_turf as anything in turfs) //Go through all the turfs and generate them var/closed = string_gen[world.maxx * (gen_turf.y - 1) + gen_turf.x] != "1" var/datum/biome/selected_biome // Here comes the meat of the biome code. var/drift_x = clamp((gen_turf.x + rand(-BIOME_RANDOM_SQUARE_DRIFT, BIOME_RANDOM_SQUARE_DRIFT)), 1, world.maxx) // / perlin_zoom var/drift_y = clamp((gen_turf.y + rand(-BIOME_RANDOM_SQUARE_DRIFT, BIOME_RANDOM_SQUARE_DRIFT)), 2, world.maxy) // / perlin_zoom // Where we go in the generated string (generated outside of the loop for s p e e d) var/coordinate = world.maxx * (drift_y - 1) + drift_x // Type of humidity zone we're in (LOW-MEDIUM-HIGH) var/humidity_level = text2num(humidity_gen[BIOME_HIGH_HUMIDITY][coordinate]) ? \ BIOME_HIGH_HUMIDITY : text2num(humidity_gen[BIOME_MEDIUM_HUMIDITY][coordinate]) ? BIOME_MEDIUM_HUMIDITY : BIOME_LOW_HUMIDITY // Type of heat zone we're in (LOW-MEDIUM-HIGH) var/heat_level = text2num(heat_gen[BIOME_HIGH_HEAT][coordinate]) ? \ BIOME_HIGH_HEAT : text2num(heat_gen[BIOME_MEDIUM_HEAT][coordinate]) ? BIOME_MEDIUM_HEAT : BIOME_LOW_HEAT selected_biome = possible_biomes[heat_level][humidity_level] LAZYSET(to_generate[selected_biome], gen_turf, closed) CHECK_TICK for(var/biome in to_generate) var/datum/biome/generating_biome = SSmapping.biomes[biome] var/list/turf/generated_turfs = generating_biome.generate_turfs_for_terrain(to_generate[biome]) generated_turfs_per_biome[biome] = (generated_turfs_per_biome[biome] || list()) + generated_turfs var/list/area_list = generated_turfs_per_area_biome[biome] if (!area_list) area_list = list() generated_turfs_per_area_biome[biome] = area_list area_list[generate_in] = generated_turfs var/message = "[name] terrain generation finished in [(REALTIMEOFDAY - start_time)/10]s!" to_chat(world, span_boldannounce("[message]"), MESSAGE_TYPE_DEBUG) log_world(message) /// Returns a biome datum that the turf was initialized with, or would be if it is present on our Z level and we use a consistent shared seed /// Not consistent between calls by itself due to using RNG in perlin zoom, needs a static coordinate-based formula /datum/map_generator/cave_generator/proc/get_biome_for_turf(turf/target) for (var/biome in generated_turfs_per_biome) var/list/generated_turfs = generated_turfs_per_biome[biome] if (generated_turfs[target]) return biome var/list/biome_map = static_biome_maps["[target.z]"] if (!shared_seed || !biome_map) return null var/drift_x = clamp((target.x + rand(-BIOME_RANDOM_SQUARE_DRIFT, BIOME_RANDOM_SQUARE_DRIFT)), 1, world.maxx) var/drift_y = clamp((target.y + rand(-BIOME_RANDOM_SQUARE_DRIFT, BIOME_RANDOM_SQUARE_DRIFT)), 2, world.maxy) var/coordinate = world.maxx * (drift_y - 1) + drift_x var/humidity_level = text2num(biome_map[BIOME_HIGH_HUMIDITY][coordinate]) ? \ BIOME_HIGH_HUMIDITY : text2num(biome_map[BIOME_MEDIUM_HUMIDITY][coordinate]) ? BIOME_MEDIUM_HUMIDITY : BIOME_LOW_HUMIDITY var/heat_level = text2num(biome_map[BIOME_HIGH_HEAT][coordinate]) ? \ BIOME_HIGH_HEAT : text2num(biome_map[BIOME_MEDIUM_HEAT][coordinate]) ? BIOME_MEDIUM_HEAT : BIOME_LOW_HEAT return possible_biomes[heat_level][humidity_level] /datum/map_generator/cave_generator/populate_terrain(list/turfs, area/generate_in) if (biome_population && length(possible_biomes)) return populate_terrain_with_biomes(turfs, generate_in) // Area var pullouts to make accessing in the loop faster var/flora_allowed = (generate_in.area_flags_mapping & FLORA_ALLOWED) && length(flora_spawn_list) var/feature_allowed = (generate_in.area_flags_mapping & FLORA_ALLOWED) && length(feature_spawn_list) var/mobs_allowed = (generate_in.area_flags_mapping & MOB_SPAWN_ALLOWED) && length(mob_spawn_list) var/megas_allowed = (generate_in.area_flags_mapping & MEGAFAUNA_SPAWN_ALLOWED) && length(megafauna_spawn_list) var/start_time = REALTIMEOFDAY // Assoc list of spawned categories to var/list/spawn_data = list( CAVE_SPAWN_FEATURE = list(), CAVE_SPAWN_TENDRIL = list(), CAVE_SPAWN_MOB = list(), CAVE_SPAWN_MEGAFAUNA = list(), ) var/list/open_turf_cache = typecacheof(open_turf_types) for (var/turf/target_turf as anything in turfs) // Only put stuff on open turfs we generated, so closed walls and rivers and stuff are skipped if (!is_type_in_typecache(target_turf, open_turf_cache)) continue CHECK_TICK if (!(target_turf.turf_flags & TURF_BLOCKS_POPULATE_TERRAIN_FLORAFEATURES)) if (flora_allowed && prob(flora_spawn_chance)) var/flora_type = pick(flora_spawn_list) new flora_type(target_turf) continue if (feature_allowed && prob(feature_spawn_chance)) var/can_spawn = TRUE for(var/turf/spawn_turf as anything in spawn_data[CAVE_SPAWN_FEATURE]) if (get_dist(spawn_turf, target_turf) <= feature_exclusion_radius) can_spawn = FALSE break if (can_spawn) var/picked_feature = pick(feature_spawn_list) new picked_feature(target_turf) spawn_data[CAVE_SPAWN_FEATURE] += target_turf continue if (!mobs_allowed || !prob(mob_spawn_chance)) continue var/picked_mob = pick(megas_allowed ? mob_spawn_list : mob_spawn_no_mega_list) var/is_megafauna = FALSE if (picked_mob == SPAWN_MEGAFAUNA) picked_mob = pick(megafauna_spawn_list) is_megafauna = TRUE var/can_spawn = TRUE if(ispath(picked_mob, /mob/living/basic/mining/tendril)) // Prevents tendrils spawning in each other's collapse range for(var/turf/spawn_turf as anything in spawn_data[CAVE_SPAWN_TENDRIL]) if (get_dist(spawn_turf, target_turf) <= 2) can_spawn = FALSE break // Also avoid spawning them next to megafauna for(var/turf/spawn_turf as anything in spawn_data[CAVE_SPAWN_MEGAFAUNA]) if (get_dist(spawn_turf, target_turf) <= megafauna_exclusion_radius) can_spawn = FALSE break else if (is_megafauna) // Megafauna can spawn wherever it wants as long as its not next to another mega for(var/turf/spawn_turf as anything in spawn_data[CAVE_SPAWN_MEGAFAUNA]) if (get_dist(spawn_turf, target_turf) <= megafauna_exclusion_radius) can_spawn = FALSE break else for(var/turf/spawn_turf as anything in spawn_data[CAVE_SPAWN_MOB]) if (get_dist(spawn_turf, target_turf) <= mob_exclusion_radius) can_spawn = FALSE break if (!can_spawn) continue if (ispath(picked_mob, /mob/living/basic/mining/tendril)) spawn_data[CAVE_SPAWN_TENDRIL] += target_turf else if (is_megafauna) spawn_data[CAVE_SPAWN_MEGAFAUNA] += target_turf spawn_data[CAVE_SPAWN_MOB] += target_turf new picked_mob(target_turf) // There can be only one Bubblegum, so don't waste spawns on it if(ispath(picked_mob, /mob/living/simple_animal/hostile/megafauna/bubblegum)) weighted_megafauna_spawn_list.Remove(picked_mob) megafauna_spawn_list = expand_weights(weighted_megafauna_spawn_list) megas_allowed = megas_allowed && length(megafauna_spawn_list) var/message = "[name] terrain population finished in [(REALTIMEOFDAY - start_time)/10]s!" add_startup_message(message) //SKYRAT EDIT CHANGE //to_chat(world, span_boldannounce("[message]"), MESSAGE_TYPE_DEBUG) //SKYRAT EDIT ORIGINAL log_world(message) /** * This handles the population of terrain with biomes. Should only be called by * `populate_terrain()`, if you find yourself calling this, you're probably not * doing it right. * * This proc won't do anything if the area we're trying to generate in does not * have `FLORA_ALLOWED` or `MOB_SPAWN_ALLOWED` in its `area_flags`. */ /datum/map_generator/cave_generator/proc/populate_terrain_with_biomes(list/turfs, area/generate_in) // Area var pullouts to make accessing in the loop faster var/flora_allowed = (generate_in.area_flags_mapping & FLORA_ALLOWED) var/features_allowed = (generate_in.area_flags_mapping & FLORA_ALLOWED) var/fauna_allowed = (generate_in.area_flags_mapping & MOB_SPAWN_ALLOWED) var/start_time = REALTIMEOFDAY // No sense in doing anything here if nothing is allowed anyway. if(!flora_allowed && !features_allowed && !fauna_allowed) var/message = "[name] terrain population finished in [(REALTIMEOFDAY - start_time)/10]s!" to_chat(world, span_boldannounce("[message]"), MESSAGE_TYPE_DEBUG) log_world(message) return for(var/biome in generated_turfs_per_area_biome) var/datum/biome/generating_biome = SSmapping.biomes[biome] var/list/areas_list = generated_turfs_per_area_biome[biome] generating_biome.populate_turfs(areas_list[generate_in], flora_allowed, features_allowed, fauna_allowed) CHECK_TICK var/message = "[name] terrain population finished in [(REALTIMEOFDAY - start_time)/10]s!" to_chat(world, span_boldannounce("[message]"), MESSAGE_TYPE_DEBUG) log_world(message) ///Generates the cave shape using Rust-G /datum/map_generator/cave_generator/proc/generate_cave(area/generate_in) ///Loop through all the active ruins for this z-level and make a json format out of it so we can send it to the generator var/list/active_ruins_list = list() for(var/turf/bottom_left_turf in SSmapping.active_ruins) var/datum/map_template/ruin/active_ruin = SSmapping.active_ruins[bottom_left_turf] if(bottom_left_turf.z != generate_in.z) continue active_ruins_list += list(list( "x" = max(1, bottom_left_turf.x - active_ruin.terrain_padding), "y" = max(1, bottom_left_turf.y - active_ruin.terrain_padding), "w" = active_ruin.width + active_ruin.terrain_padding * 2, "h" = active_ruin.height + active_ruin.terrain_padding * 2, "isEnclosed" = active_ruin.enclosed_for_terrain, )) var/active_ruin_string = json_encode(active_ruins_list) var/string_gen = rustg_cave_system_generator_generate("[world.maxx]", "[world.maxy]", active_ruin_string, "[min_bsp_size]","[max_ratio]", "[padding]", "[room_fill_percent]", "[corridor_width]","[loop_percent]", "[noise_percent]", "[ca_steps]", "[birth_limit]", "[survival_limit]", "[edges_are_alive]") return string_gen /datum/map_generator/cave_generator/jungle possible_biomes = list( BIOME_LOW_HEAT = list( BIOME_LOW_HUMIDITY = /datum/biome/plains, BIOME_MEDIUM_HUMIDITY = /datum/biome/mudlands, BIOME_HIGH_HUMIDITY = /datum/biome/water ), BIOME_MEDIUM_HEAT = list( BIOME_LOW_HUMIDITY = /datum/biome/plains, BIOME_MEDIUM_HUMIDITY = /datum/biome/jungle/deep, BIOME_HIGH_HUMIDITY = /datum/biome/jungle ), BIOME_HIGH_HEAT = list( BIOME_LOW_HUMIDITY = /datum/biome/wasteland, BIOME_MEDIUM_HUMIDITY = /datum/biome/plains, BIOME_HIGH_HUMIDITY = /datum/biome/jungle/deep ) ) #undef BIOME_RANDOM_SQUARE_DRIFT #undef CAVE_SPAWN_MOB #undef CAVE_SPAWN_FEATURE #undef CAVE_SPAWN_TENDRIL #undef CAVE_SPAWN_MEGAFAUNA