diff --git a/code/__HELPERS/_lists.dm b/code/__HELPERS/_lists.dm index b12890139ec..ed26e47f551 100644 --- a/code/__HELPERS/_lists.dm +++ b/code/__HELPERS/_lists.dm @@ -427,6 +427,41 @@ return null +/// Takes a weighted list (see above) and expands it into raw entries +/// This eats more memory, but saves time when actually picking from it +/proc/expand_weights(list/list_to_pick) + var/list/values = list() + for(var/item in list_to_pick) + var/value = list_to_pick[item] + if(!value) + continue + values += value + + var/gcf = greatest_common_factor(values) + + var/list/output = list() + for(var/item in list_to_pick) + var/value = list_to_pick[item] + if(!value) + continue + for(var/i in 1 to value / gcf) + output += item + return output + +/// Takes a list of numbers as input, returns the highest value that is cleanly divides them all +/// Note: this implementation is expensive as heck for large numbers, I only use it because most of my usecase +/// Is < 10 ints +/proc/greatest_common_factor(list/values) + var/smallest = min(arglist(values)) + for(var/i in smallest to 1 step -1) + var/safe = TRUE + for(var/entry in values) + if(entry % i != 0) + safe = FALSE + break + if(safe) + return i + /// Pick a random element from the list and remove it from the list. /proc/pick_n_take(list/list_to_pick) RETURN_TYPE(list_to_pick[_].type) diff --git a/code/__HELPERS/text.dm b/code/__HELPERS/text.dm index dc263b5a467..e8aeafc329a 100644 --- a/code/__HELPERS/text.dm +++ b/code/__HELPERS/text.dm @@ -304,6 +304,24 @@ return copytext(text, 1, i + 1) return "" +//Returns a string with reserved characters and spaces after the first and last letters removed +//Like trim(), but very slightly faster. worth it for niche usecases +/proc/trim_reduced(text) + var/starting_coord = 1 + var/text_len = length(text) + for (var/i in 1 to text_len) + if (text2ascii(text, i) > 32) + starting_coord = i + break + + for (var/i = text_len, i >= starting_coord, i--) + if (text2ascii(text, i) > 32) + return copytext(text, starting_coord, i + 1) + + if(starting_coord > 1) + return copytext(text, starting_coord) + return "" + /** * Truncate a string to the given length * @@ -324,7 +342,7 @@ /proc/trim(text, max_length) if(max_length) text = copytext_char(text, 1, max_length) - return trim_left(trim_right(text)) + return trim_reduced(text) //Returns a string with the first element of the string capitalized. /proc/capitalize(t) @@ -792,6 +810,9 @@ GLOBAL_LIST_INIT(binary, list("0","1")) base = text("[]\herself", rest) if("hers") base = text("[]\hers", rest) + else // Someone fucked up, if you're not a macro just go home yeah? + // This does technically break parsing, but at least it's better then what it used to do + return base . = base if(rest) diff --git a/code/controllers/subsystem/mapping.dm b/code/controllers/subsystem/mapping.dm index 6b0018e66f2..64a3c44b99c 100644 --- a/code/controllers/subsystem/mapping.dm +++ b/code/controllers/subsystem/mapping.dm @@ -166,20 +166,20 @@ SUBSYSTEM_DEF(mapping) // Generate mining ruins var/list/lava_ruins = levels_by_trait(ZTRAIT_LAVA_RUINS) if (lava_ruins.len) - seedRuins(lava_ruins, CONFIG_GET(number/lavaland_budget), list(/area/lavaland/surface/outdoors/unexplored), themed_ruins[ZTRAIT_LAVA_RUINS]) + seedRuins(lava_ruins, CONFIG_GET(number/lavaland_budget), list(/area/lavaland/surface/outdoors/unexplored), themed_ruins[ZTRAIT_LAVA_RUINS], clear_below = TRUE) for (var/lava_z in lava_ruins) spawn_rivers(lava_z) var/list/ice_ruins = levels_by_trait(ZTRAIT_ICE_RUINS) if (ice_ruins.len) // needs to be whitelisted for underground too so place_below ruins work - seedRuins(ice_ruins, CONFIG_GET(number/icemoon_budget), list(/area/icemoon/surface/outdoors/unexplored, /area/icemoon/underground/unexplored), themed_ruins[ZTRAIT_ICE_RUINS]) + seedRuins(ice_ruins, CONFIG_GET(number/icemoon_budget), list(/area/icemoon/surface/outdoors/unexplored, /area/icemoon/underground/unexplored), themed_ruins[ZTRAIT_ICE_RUINS], clear_below = TRUE) for (var/ice_z in ice_ruins) spawn_rivers(ice_z, 4, /turf/open/openspace/icemoon, /area/icemoon/surface/outdoors/unexplored/rivers) var/list/ice_ruins_underground = levels_by_trait(ZTRAIT_ICE_RUINS_UNDERGROUND) if (ice_ruins_underground.len) - seedRuins(ice_ruins_underground, CONFIG_GET(number/icemoon_budget), list(/area/icemoon/underground/unexplored), themed_ruins[ZTRAIT_ICE_RUINS_UNDERGROUND]) + seedRuins(ice_ruins_underground, CONFIG_GET(number/icemoon_budget), list(/area/icemoon/underground/unexplored), themed_ruins[ZTRAIT_ICE_RUINS_UNDERGROUND], clear_below = TRUE) for (var/ice_z in ice_ruins_underground) spawn_rivers(ice_z, 4, level_trait(ice_z, ZTRAIT_BASETURF), /area/icemoon/underground/unexplored/rivers) diff --git a/code/datums/mapgen/CaveGenerator.dm b/code/datums/mapgen/CaveGenerator.dm index 58cfe4f0cd6..385cfc2be12 100644 --- a/code/datums/mapgen/CaveGenerator.dm +++ b/code/datums/mapgen/CaveGenerator.dm @@ -1,18 +1,32 @@ /datum/map_generator/cave_generator var/name = "Cave Generator" ///Weighted list of the types that spawns if the turf is open - var/open_turf_types = list(/turf/open/misc/asteroid/airless = 1) + 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/closed_turf_types = list(/turf/closed/mineral/random = 1) + 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 - // Weighted list of Megafauna that can spawn in the caves + ///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 @@ -36,107 +50,114 @@ /datum/map_generator/cave_generator/New() . = ..() - if(!mob_spawn_list) - mob_spawn_list = list(/mob/living/simple_animal/hostile/asteroid/goldgrub = 1, /mob/living/simple_animal/hostile/asteroid/goliath = 5, /mob/living/simple_animal/hostile/asteroid/basilisk = 4, /mob/living/simple_animal/hostile/asteroid/hivelord = 3) - if(!megafauna_spawn_list) - megafauna_spawn_list = GLOB.megafauna_spawn_list - if(!flora_spawn_list) - 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) - if(!feature_spawn_list) - feature_spawn_list = list(/obj/structure/geyser/random = 1) + if(!weighted_mob_spawn_list) + weighted_mob_spawn_list = list(/mob/living/simple_animal/hostile/asteroid/goldgrub = 1, /mob/living/simple_animal/hostile/asteroid/goliath = 5, /mob/living/simple_animal/hostile/asteroid/basilisk = 4, /mob/living/simple_animal/hostile/asteroid/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) + 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) + +/datum/map_generator/cave_generator/generate_terrain(list/turfs, area/generate_in) . = ..() + if(!(generate_in.area_flags & CAVES_ALLOWED)) + return + var/start_time = REALTIMEOFDAY string_gen = rustg_cnoise_generate("[initial_closed_chance]", "[smoothing_iterations]", "[birth_limit]", "[death_limit]", "[world.maxx]", "[world.maxy]") //Generate the raw CA data + // Area var pullouts to make accessing in the loop faster + var/flora_allowed = (generate_in.area_flags & FLORA_ALLOWED) && length(flora_spawn_list) + var/feature_allowed = (generate_in.area_flags & FLORA_ALLOWED) && length(feature_spawn_list) + var/mobs_allowed = (generate_in.area_flags & MOB_SPAWN_ALLOWED) && length(mob_spawn_list) + var/megas_allowed = (generate_in.area_flags & MEGAFAUNA_SPAWN_ALLOWED) && length(megafauna_spawn_list) + for(var/i in turfs) //Go through all the turfs and generate them var/turf/gen_turf = i - var/area/A = gen_turf.loc - if(!(A.area_flags & CAVES_ALLOWED)) + var/closed = string_gen[world.maxx * (gen_turf.y - 1) + gen_turf.x] != "0" + 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.flags_1 |= NO_RUINS + + if(closed)//Open turfs have some special behavior related to spawning flora and mobs. + CHECK_TICK continue - var/closed = text2num(string_gen[world.maxx * (gen_turf.y - 1) + gen_turf.x]) + // If we've spawned something yet + var/spawned_something = FALSE - var/stored_flags - if(gen_turf.turf_flags & NO_RUINS) - stored_flags |= NO_RUINS + ///Spawning isn't done in procs to save on overhead on the 60k turfs we're going through. + //FLORA SPAWNING HERE + if(flora_allowed && prob(flora_spawn_chance)) + var/flora_type = pick(flora_spawn_list) + new flora_type(new_turf) + spawned_something = TRUE - var/turf/new_turf = pick_weight(closed ? closed_turf_types : open_turf_types) + //FEATURE SPAWNING HERE + if(feature_allowed && prob(feature_spawn_chance)) + var/can_spawn = TRUE - new_turf = gen_turf.ChangeTurf(new_turf, initial(new_turf.baseturfs), CHANGETURF_DEFER_CHANGE) + var/atom/picked_feature = pick(feature_spawn_list) - new_turf.flags_1 |= stored_flags - - if(!closed)//Open turfs have some special behavior related to spawning flora and mobs. - - var/turf/open/new_open_turf = new_turf - - ///Spawning isn't done in procs to save on overhead on the 60k turfs we're going through. - - //FLORA SPAWNING HERE - var/atom/spawned_flora - if(flora_spawn_list && prob(flora_spawn_chance)) - var/can_spawn = TRUE - - if(!(A.area_flags & FLORA_ALLOWED)) + for(var/obj/structure/existing_feature in range(7, new_turf)) + if(istype(existing_feature, picked_feature)) can_spawn = FALSE - if(can_spawn) - spawned_flora = pick_weight(flora_spawn_list) - spawned_flora = new spawned_flora(new_open_turf) + break - //FEATURE SPAWNING HERE - var/atom/spawned_feature - if(feature_spawn_list && prob(feature_spawn_chance)) - var/can_spawn = TRUE + if(can_spawn) + new picked_feature(new_turf) + spawned_something = TRUE - if(!(A.area_flags & FLORA_ALLOWED)) //checks the same flag because lol dunno + //MOB SPAWNING HERE + if(mobs_allowed && !spawned_something && prob(mob_spawn_chance)) + var/atom/picked_mob = pick(mob_spawn_list) + + if(picked_mob == SPAWN_MEGAFAUNA) + if(megas_allowed) //this is danger. it's boss time. + picked_mob = pick(megafauna_spawn_list) + else //this is not danger, don't spawn a boss, spawn something else + picked_mob = pick(mob_spawn_no_mega_list) //What if we used 100% of the brain...and did something (slightly) less shit than a while loop? + + var/can_spawn = TRUE + + // prevents tendrils spawning in each other's collapse range + if(ispath(picked_mob, /obj/structure/spawner/lavaland)) + for(var/obj/structure/spawner/lavaland/spawn_blocker in range(2, new_turf)) can_spawn = FALSE - - var/atom/picked_feature = pick_weight(feature_spawn_list) - - for(var/obj/structure/F in range(7, new_open_turf)) - if(istype(F, picked_feature)) - can_spawn = FALSE - - if(can_spawn) - spawned_feature = new picked_feature(new_open_turf) - - //MOB SPAWNING HERE - - if(mob_spawn_list && !spawned_flora && !spawned_feature && prob(mob_spawn_chance)) - var/can_spawn = TRUE - - if(!(A.area_flags & MOB_SPAWN_ALLOWED)) + break + //if the random is a standard mob, avoid spawning if there's another one within 12 tiles + else if(ispath(picked_mob, /mob/living/simple_animal/hostile/asteroid)) + for(var/mob/living/simple_animal/hostile/asteroid/mob_blocker in range(12, new_turf)) can_spawn = FALSE + break + //if there's a megafauna within standard view don't spawn anything at all (This isn't really consistent, I don't know why we do this. you do you tho) + if(can_spawn) + for(var/mob/living/simple_animal/hostile/megafauna/found_fauna in range(7, new_turf)) + can_spawn = FALSE + break - var/atom/picked_mob = pick_weight(mob_spawn_list) - - if(picked_mob == SPAWN_MEGAFAUNA) // - if((A.area_flags & MEGAFAUNA_SPAWN_ALLOWED) && megafauna_spawn_list?.len) //this is danger. it's boss time. - picked_mob = pick_weight(megafauna_spawn_list) - else //this is not danger, don't spawn a boss, spawn something else - picked_mob = pick_weight(mob_spawn_list - SPAWN_MEGAFAUNA) //What if we used 100% of the brain...and did something (slightly) less shit than a while loop? - - for(var/thing in urange(12, new_open_turf)) //prevents mob clumps - if(!ishostile(thing) && !istype(thing, /obj/structure/spawner)) - continue - if((ispath(picked_mob, /mob/living/simple_animal/hostile/megafauna) || ismegafauna(thing)) && get_dist(new_open_turf, thing) <= 7) - can_spawn = FALSE //if there's a megafauna within standard view don't spawn anything at all - break - if(ispath(picked_mob, /mob/living/simple_animal/hostile/asteroid) || istype(thing, /mob/living/simple_animal/hostile/asteroid)) - can_spawn = FALSE //if the random is a standard mob, avoid spawning if there's another one within 12 tiles - break - if((ispath(picked_mob, /obj/structure/spawner/lavaland) || istype(thing, /obj/structure/spawner/lavaland)) && get_dist(new_open_turf, thing) <= 2) - can_spawn = FALSE //prevents tendrils spawning in each other's collapse range - break - - if(can_spawn) - if(ispath(picked_mob, /mob/living/simple_animal/hostile/megafauna/bubblegum)) //there can be only one bubblegum, so don't waste spawns on it - megafauna_spawn_list.Remove(picked_mob) - - new picked_mob(new_open_turf) + if(can_spawn) + if(ispath(picked_mob, /mob/living/simple_animal/hostile/megafauna/bubblegum)) //there can be only one bubblegum, so don't waste spawns on it + weighted_megafauna_spawn_list.Remove(picked_mob) + megafauna_spawn_list = expand_weights(weighted_megafauna_spawn_list) + megas_allowed = megas_allowed && length(megafauna_spawn_list) + new picked_mob(new_turf) + spawned_something = TRUE CHECK_TICK var/message = "[name] finished in [(REALTIMEOFDAY - start_time)/10]s!" diff --git a/code/datums/mapgen/Cavegens/IcemoonCaves.dm b/code/datums/mapgen/Cavegens/IcemoonCaves.dm index 54e23c99319..f8c3eed6180 100644 --- a/code/datums/mapgen/Cavegens/IcemoonCaves.dm +++ b/code/datums/mapgen/Cavegens/IcemoonCaves.dm @@ -1,19 +1,19 @@ /datum/map_generator/cave_generator/icemoon - open_turf_types = list(/turf/open/misc/asteroid/snow/icemoon = 19, /turf/open/misc/ice/icemoon = 1) - closed_turf_types = list(/turf/closed/mineral/random/snow = 1) + weighted_open_turf_types = list(/turf/open/misc/asteroid/snow/icemoon = 19, /turf/open/misc/ice/icemoon = 1) + weighted_closed_turf_types = list(/turf/closed/mineral/random/snow = 1) - mob_spawn_list = list(/mob/living/simple_animal/hostile/asteroid/wolf = 50, /obj/structure/spawner/ice_moon = 3, \ + weighted_mob_spawn_list = list(/mob/living/simple_animal/hostile/asteroid/wolf = 50, /obj/structure/spawner/ice_moon = 3, \ /mob/living/simple_animal/hostile/asteroid/polarbear = 30, /obj/structure/spawner/ice_moon/polarbear = 3, \ /mob/living/simple_animal/hostile/asteroid/hivelord/legion/snow = 50, /mob/living/simple_animal/hostile/asteroid/goldgrub = 10, \ /mob/living/simple_animal/hostile/asteroid/lobstrosity = 15) - flora_spawn_list = list(/obj/structure/flora/tree/pine/style_random = 2, /obj/structure/flora/rock/icy/style_random = 2, /obj/structure/flora/rock/pile/icy/style_random = 2, /obj/structure/flora/grass/both/style_random = 6, /obj/structure/flora/ash/chilly = 2) + weighted_flora_spawn_list = list(/obj/structure/flora/tree/pine/style_random = 2, /obj/structure/flora/rock/icy/style_random = 2, /obj/structure/flora/rock/pile/icy/style_random = 2, /obj/structure/flora/grass/both/style_random = 6, /obj/structure/flora/ash/chilly = 2) ///Note that this spawn list is also in the lavaland generator - feature_spawn_list = list(/obj/structure/geyser/wittel = 6, /obj/structure/geyser/random = 2, /obj/structure/geyser/plasma_oxide = 10, /obj/structure/geyser/protozine = 10, /obj/structure/geyser/hollowwater = 10) + weighted_feature_spawn_list = list(/obj/structure/geyser/wittel = 6, /obj/structure/geyser/random = 2, /obj/structure/geyser/plasma_oxide = 10, /obj/structure/geyser/protozine = 10, /obj/structure/geyser/hollowwater = 10) /datum/map_generator/cave_generator/icemoon/surface flora_spawn_chance = 4 - mob_spawn_list = null + weighted_mob_spawn_list = null initial_closed_chance = 53 birth_limit = 5 death_limit = 4 @@ -22,10 +22,10 @@ /datum/map_generator/cave_generator/icemoon/surface/noruins //use this for when you don't want ruins to spawn in a certain area /datum/map_generator/cave_generator/icemoon/deep - closed_turf_types = list(/turf/closed/mineral/random/snow/underground = 1) - mob_spawn_list = list(/mob/living/simple_animal/hostile/asteroid/ice_demon = 50, /obj/structure/spawner/ice_moon/demonic_portal = 3, \ + weighted_closed_turf_types = list(/turf/closed/mineral/random/snow/underground = 1) + weighted_mob_spawn_list = list(/mob/living/simple_animal/hostile/asteroid/ice_demon = 50, /obj/structure/spawner/ice_moon/demonic_portal = 3, \ /mob/living/simple_animal/hostile/asteroid/ice_whelp = 30, /obj/structure/spawner/ice_moon/demonic_portal/ice_whelp = 3, \ /mob/living/simple_animal/hostile/asteroid/hivelord/legion/snow = 50, /obj/structure/spawner/ice_moon/demonic_portal/snowlegion = 3, \ SPAWN_MEGAFAUNA = 2) - megafauna_spawn_list = list(/mob/living/simple_animal/hostile/megafauna/colossus = 1) - flora_spawn_list = list(/obj/structure/flora/rock/icy/style_random = 6, /obj/structure/flora/rock/pile/icy/style_random = 6, /obj/structure/flora/ash/chilly = 1) + weighted_megafauna_spawn_list = list(/mob/living/simple_animal/hostile/megafauna/colossus = 1) + weighted_flora_spawn_list = list(/obj/structure/flora/rock/icy/style_random = 6, /obj/structure/flora/rock/pile/icy/style_random = 6, /obj/structure/flora/ash/chilly = 1) diff --git a/code/datums/mapgen/Cavegens/LavalandGenerator.dm b/code/datums/mapgen/Cavegens/LavalandGenerator.dm index 34ae78696ae..a5e147b77da 100644 --- a/code/datums/mapgen/Cavegens/LavalandGenerator.dm +++ b/code/datums/mapgen/Cavegens/LavalandGenerator.dm @@ -1,9 +1,9 @@ /datum/map_generator/cave_generator/lavaland - open_turf_types = list(/turf/open/misc/asteroid/basalt/lava_land_surface = 1) - closed_turf_types = list(/turf/closed/mineral/random/volcanic = 1) + weighted_open_turf_types = list(/turf/open/misc/asteroid/basalt/lava_land_surface = 1) + weighted_closed_turf_types = list(/turf/closed/mineral/random/volcanic = 1) - mob_spawn_list = list( + weighted_mob_spawn_list = list( /mob/living/simple_animal/hostile/asteroid/goliath/beast/random = 50, /obj/structure/spawner/lavaland/goliath = 3, /mob/living/simple_animal/hostile/asteroid/basilisk/watcher/random = 40, @@ -16,9 +16,9 @@ /mob/living/simple_animal/hostile/asteroid/brimdemon = 20, /mob/living/basic/bileworm = 20, ) - 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) + 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) ///Note that this spawn list is also in the icemoon generator - feature_spawn_list = list(/obj/structure/geyser/wittel = 6, /obj/structure/geyser/random = 2, /obj/structure/geyser/plasma_oxide = 10, /obj/structure/geyser/protozine = 10, /obj/structure/geyser/hollowwater = 10) + weighted_feature_spawn_list = list(/obj/structure/geyser/wittel = 6, /obj/structure/geyser/random = 2, /obj/structure/geyser/plasma_oxide = 10, /obj/structure/geyser/protozine = 10, /obj/structure/geyser/hollowwater = 10) initial_closed_chance = 45 smoothing_iterations = 50 diff --git a/code/datums/mapgen/JungleGenerator.dm b/code/datums/mapgen/JungleGenerator.dm index 325dd8ed156..b8c2d112fd1 100644 --- a/code/datums/mapgen/JungleGenerator.dm +++ b/code/datums/mapgen/JungleGenerator.dm @@ -33,7 +33,7 @@ var/perlin_zoom = 65 ///Seeds the rust-g perlin noise with a random number. -/datum/map_generator/jungle_generator/generate_terrain(list/turfs) +/datum/map_generator/jungle_generator/generate_terrain(list/turfs, area/generate_in) . = ..() var/height_seed = rand(0, 50000) var/humidity_seed = rand(0, 50000) diff --git a/code/datums/mapgen/_MapGenerator.dm b/code/datums/mapgen/_MapGenerator.dm index dbf61d43403..dbe7cf5ba94 100644 --- a/code/datums/mapgen/_MapGenerator.dm +++ b/code/datums/mapgen/_MapGenerator.dm @@ -2,5 +2,5 @@ /datum/map_generator ///This proc will be ran by areas on Initialize, and provides the areas turfs as argument to allow for generation. -/datum/map_generator/proc/generate_terrain(list/turfs) +/datum/map_generator/proc/generate_terrain(list/turfs, area/generate_in) return diff --git a/code/game/area/areas.dm b/code/game/area/areas.dm index 9f1f835c732..21e26ab7ebc 100644 --- a/code/game/area/areas.dm +++ b/code/game/area/areas.dm @@ -210,14 +210,14 @@ GLOBAL_LIST_EMPTY(teleportlocs) var/list/turfs = list() for(var/turf/T in contents) turfs += T - map_generator.generate_terrain(turfs) + map_generator.generate_terrain(turfs, src) /area/proc/test_gen() if(map_generator) var/list/turfs = list() for(var/turf/T in contents) turfs += T - map_generator.generate_terrain(turfs) + map_generator.generate_terrain(turfs, src) /** diff --git a/code/game/atoms.dm b/code/game/atoms.dm index a9fbdb275c3..f4dec54d2ee 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -176,7 +176,7 @@ */ /atom/New(loc, ...) //atom creation method that preloads variables at creation - if(GLOB.use_preloader && (src.type == GLOB._preloader.target_path))//in case the instanciated atom is creating other atoms in New() + if(GLOB.use_preloader && src.type == GLOB._preloader_path)//in case the instanciated atom is creating other atoms in New() world.preloader_load(src) var/do_initialize = SSatoms.initialized diff --git a/code/game/turfs/open/river.dm b/code/game/turfs/open/river.dm index fc2681aec53..185f9c91eff 100644 --- a/code/game/turfs/open/river.dm +++ b/code/game/turfs/open/river.dm @@ -8,6 +8,7 @@ var/list/river_nodes = list() var/num_spawned = 0 var/list/possible_locs = block(locate(min_x, min_y, target_z), locate(max_x, max_y, target_z)) + new_baseturfs = baseturfs_string_list(new_baseturfs, pick(possible_locs)) while(num_spawned < nodes && possible_locs.len) var/turf/T = pick(possible_locs) var/area/A = get_area(T) @@ -23,8 +24,11 @@ if (W.z != target_z || W.connected) continue W.connected = TRUE + // Workaround around ChangeTurf that's safe because of when this proc is called var/turf/cur_turf = get_turf(W) - cur_turf.ChangeTurf(turf_type, new_baseturfs, CHANGETURF_IGNORE_AIR) + cur_turf = new turf_type(cur_turf) + if(new_baseturfs) + cur_turf.baseturfs = new_baseturfs var/turf/target_turf = get_turf(pick(river_nodes - W)) if(!target_turf) break @@ -53,7 +57,10 @@ cur_turf = get_step(cur_turf, cur_dir) continue else - var/turf/river_turf = cur_turf.ChangeTurf(turf_type, new_baseturfs, CHANGETURF_IGNORE_AIR) + // Workaround around ChangeTurf that's safe because of when this proc is called + var/turf/river_turf = new turf_type(cur_turf) + if(new_baseturfs) + river_turf.baseturfs = new_baseturfs river_turf.Spread(25, 11, whitelist_area) for(var/WP in river_nodes) @@ -72,35 +79,42 @@ var/list/cardinal_turfs = list() var/list/diagonal_turfs = list() var/logged_turf_type - for(var/F in RANGE_TURFS(1, src) - src) - var/turf/T = F - var/area/new_area = get_area(T) - if(!T || (T.density && !ismineralturf(T)) || isindestructiblefloor(T) || (whitelisted_area && !istype(new_area, whitelisted_area)) || (T.turf_flags & NO_LAVA_GEN) ) + for(var/turf/canidate as anything in RANGE_TURFS(1, src) - src) + if(!canidate || (canidate.density && !ismineralturf(canidate)) || isindestructiblefloor(canidate)) continue - if(!logged_turf_type && ismineralturf(T)) - var/turf/closed/mineral/M = T - logged_turf_type = M.turf_type + var/area/new_area = get_area(canidate) + if((!istype(new_area, whitelisted_area) && whitelisted_area) || (canidate.turf_flags & NO_LAVA_GEN)) + continue - if(get_dir(src, F) in GLOB.cardinals) - cardinal_turfs += F + if(!logged_turf_type && ismineralturf(canidate)) + var/turf/closed/mineral/mineral_canidate = canidate + logged_turf_type = mineral_canidate.turf_type + + if(get_dir(src, canidate) in GLOB.cardinals) + cardinal_turfs += canidate else - diagonal_turfs += F - - for(var/F in cardinal_turfs) //cardinal turfs are always changed but don't always spread - var/turf/T = F - if(!istype(T, logged_turf_type) && T.ChangeTurf(type, baseturfs, CHANGETURF_IGNORE_AIR) && prob(probability)) - T.Spread(probability - prob_loss, prob_loss, whitelisted_area) - - for(var/F in diagonal_turfs) //diagonal turfs only sometimes change, but will always spread if changed - var/turf/T = F - if(!istype(T, logged_turf_type) && prob(probability) && T.ChangeTurf(type, baseturfs, CHANGETURF_IGNORE_AIR)) - T.Spread(probability - prob_loss, prob_loss, whitelisted_area) - else if(ismineralturf(T)) - var/turf/closed/mineral/M = T - M.ChangeTurf(M.turf_type, M.baseturfs, CHANGETURF_IGNORE_AIR) + diagonal_turfs += canidate + for(var/turf/cardinal_canidate as anything in cardinal_turfs) //cardinal turfs are always changed but don't always spread + // NOTE: WE ARE SKIPPING CHANGETURF HERE + // The calls in this proc only serve to provide a satisfactory (if it's not ALREADY this) check. They do not actually call changeturf + // This is safe because this proc can only be run during mapload, and nothing has initialized by now so there's nothing to inherit or delete + if(!istype(cardinal_canidate, logged_turf_type) && cardinal_canidate.ChangeTurf(type, baseturfs, CHANGETURF_SKIP) && prob(probability)) + if(baseturfs) + cardinal_canidate.baseturfs = baseturfs + cardinal_canidate.Spread(probability - prob_loss, prob_loss, whitelisted_area) + for(var/turf/diagonal_canidate as anything in diagonal_turfs) //diagonal turfs only sometimes change, but will always spread if changed + // Important NOTE: SEE ABOVE + if(!istype(diagonal_canidate, logged_turf_type) && prob(probability) && diagonal_canidate.ChangeTurf(type, baseturfs, CHANGETURF_SKIP)) + if(baseturfs) + diagonal_canidate.baseturfs = baseturfs + diagonal_canidate.Spread(probability - prob_loss, prob_loss, whitelisted_area) + else if(ismineralturf(diagonal_canidate)) + var/turf/closed/mineral/diagonal_mineral = diagonal_canidate + // SEE ABOVE, THIS IS ONLY VERY RARELY SAFE + new diagonal_mineral.turf_type(diagonal_mineral) #undef RANDOM_UPPER_X #undef RANDOM_UPPER_Y diff --git a/code/modules/holodeck/computer.dm b/code/modules/holodeck/computer.dm index bf860b7a525..a5369724847 100644 --- a/code/modules/holodeck/computer.dm +++ b/code/modules/holodeck/computer.dm @@ -192,7 +192,7 @@ GLOBAL_LIST_INIT(typecache_holodeck_linked_floorcheck_ok, typecacheof(list(/turf for(var/turf/possible_blacklist as anything in get_affected_turfs(placement)) if (possible_blacklist.holodeck_compatible) continue - input_blacklist += possible_blacklist + input_blacklist[possible_blacklist] = TRUE ///loads the template whose id string it was given ("offline_program" loads datum/map_template/holodeck/offline) /obj/machinery/computer/holodeck/proc/load_program(map_id, force = FALSE, add_delay = TRUE) diff --git a/code/modules/mapping/map_template.dm b/code/modules/mapping/map_template.dm index 7574ddee5fd..7539b0f8272 100644 --- a/code/modules/mapping/map_template.dm +++ b/code/modules/mapping/map_template.dm @@ -169,6 +169,7 @@ var/list/turf_blacklist = list() update_blacklist(T, turf_blacklist) + UNSETEMPTY(turf_blacklist) parsed.turf_blacklist = turf_blacklist if(!parsed.load(T.x, T.y, T.z, cropMap=TRUE, no_changeturf=(SSatoms.initialized == INITIALIZATION_INSSATOMS), placeOnTop=should_place_on_top)) return diff --git a/code/modules/mapping/preloader.dm b/code/modules/mapping/preloader.dm index c2c4cd959a9..80385b68292 100644 --- a/code/modules/mapping/preloader.dm +++ b/code/modules/mapping/preloader.dm @@ -1,6 +1,7 @@ // global datum that will preload variables on atoms instanciation GLOBAL_VAR_INIT(use_preloader, FALSE) -GLOBAL_DATUM_INIT(_preloader, /datum/map_preloader, new) +GLOBAL_LIST_INIT(_preloader_attributes, null) +GLOBAL_LIST_INIT(_preloader_path, null) /// Preloader datum /datum/map_preloader @@ -10,15 +11,14 @@ GLOBAL_DATUM_INIT(_preloader, /datum/map_preloader, new) /world/proc/preloader_setup(list/the_attributes, path) if(the_attributes.len) GLOB.use_preloader = TRUE - var/datum/map_preloader/preloader_local = GLOB._preloader - preloader_local.attributes = the_attributes - preloader_local.target_path = path + GLOB._preloader_attributes = the_attributes + GLOB._preloader_path = path /world/proc/preloader_load(atom/what) GLOB.use_preloader = FALSE - var/datum/map_preloader/preloader_local = GLOB._preloader - for(var/attribute in preloader_local.attributes) - var/value = preloader_local.attributes[attribute] + var/list/attributes = GLOB._preloader_attributes + for(var/attribute in attributes) + var/value = attributes[attribute] if(islist(value)) value = deep_copy_list(value) #ifdef TESTING diff --git a/code/modules/mapping/reader.dm b/code/modules/mapping/reader.dm index 69a53fae535..1649c70e348 100644 --- a/code/modules/mapping/reader.dm +++ b/code/modules/mapping/reader.dm @@ -11,6 +11,7 @@ /datum/parsed_map var/original_path + /// The length of a key in this file. This is promised by the standard to be static var/key_len = 0 var/list/grid_models = list() var/list/gridSets = list() @@ -22,19 +23,21 @@ /// Offset bounds. Same as parsed_bounds until load(). var/list/bounds - ///any turf in this list is skipped inside of build_coordinate - var/list/turf_blacklist = list() + ///any turf in this list is skipped inside of build_coordinate. Lazy assoc list + var/list/turf_blacklist // raw strings used to represent regexes more accurately // '' used to avoid confusing syntax highlighting var/static/regex/dmmRegex = new(@'"([a-zA-Z]+)" = \(((?:.|\n)*?)\)\n(?!\t)|\((\d+),(\d+),(\d+)\) = \{"([a-zA-Z\n]*)"\}', "g") - var/static/regex/trimQuotesRegex = new(@'^[\s\n]+"?|"?[\s\n]+$|^"|"$', "g") var/static/regex/trimRegex = new(@'^[\s\n]+|[\s\n]+$', "g") #ifdef TESTING var/turfsSkipped = 0 #endif +//text trimming (both directions) helper macro +#define TRIM_TEXT(text) (trim_reduced(text)) + /// Shortcut function to parse a map and apply it to the world. /// /// - `dmm_file`: A .dmm file to load (Required). @@ -52,6 +55,9 @@ /// Parse a map, possibly cropping it. /datum/parsed_map/New(tfile, x_lower = -INFINITY, x_upper = INFINITY, y_lower = -INFINITY, y_upper=INFINITY, measureOnly=FALSE) + // This proc sleeps for like 6 seconds. why? + // Is it file accesses? if so, can those be done ahead of time, async to save on time here? I wonder. + // Love ya :) if(isfile(tfile)) original_path = "[tfile]" tfile = file2text(tfile) @@ -59,16 +65,23 @@ // create a new datum without loading a map return - bounds = parsed_bounds = list(1.#INF, 1.#INF, 1.#INF, -1.#INF, -1.#INF, -1.#INF) - var/stored_index = 1 + src.bounds = parsed_bounds = list(1.#INF, 1.#INF, 1.#INF, -1.#INF, -1.#INF, -1.#INF) + // lists are structs don't you know :) + var/list/bounds = src.bounds + var/list/grid_models = src.grid_models + var/key_len = src.key_len + var/stored_index = 1 + var/list/regexOutput //multiz lool while(dmmRegex.Find(tfile, stored_index)) stored_index = dmmRegex.next + // Datum var lookup is expensive, this isn't + regexOutput = dmmRegex.group // "aa" = (/type{vars=blah}) - if(dmmRegex.group[1]) // Model - var/key = dmmRegex.group[1] + if(regexOutput[1]) // Model + var/key = regexOutput[1] if(grid_models[key]) // Duplicate model keys are ignored in DMMs continue if(key_len != length(key)) @@ -77,14 +90,14 @@ else CRASH("Inconsistent key length in DMM") if(!measureOnly) - grid_models[key] = dmmRegex.group[2] + grid_models[key] = regexOutput[2] // (1,1,1) = {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"} - else if(dmmRegex.group[3]) // Coords + else if(regexOutput[3]) // Coords if(!key_len) CRASH("Coords before model definition in DMM") - var/curr_x = text2num(dmmRegex.group[3]) + var/curr_x = text2num(regexOutput[3]) if(curr_x < x_lower || curr_x > x_upper) continue @@ -93,72 +106,96 @@ gridSet.xcrd = curr_x //position of the currently processed square - gridSet.ycrd = text2num(dmmRegex.group[4]) - gridSet.zcrd = text2num(dmmRegex.group[5]) + gridSet.ycrd = text2num(regexOutput[4]) + gridSet.zcrd = text2num(regexOutput[5]) - bounds[MAP_MINX] = min(bounds[MAP_MINX], clamp(gridSet.xcrd, x_lower, x_upper)) + bounds[MAP_MINX] = min(bounds[MAP_MINX], curr_x) bounds[MAP_MINZ] = min(bounds[MAP_MINZ], gridSet.zcrd) bounds[MAP_MAXZ] = max(bounds[MAP_MAXZ], gridSet.zcrd) - var/list/gridLines = splittext(dmmRegex.group[6], "\n") + var/list/gridLines = splittext(regexOutput[6], "\n") gridSet.gridLines = gridLines var/leadingBlanks = 0 - while(leadingBlanks < gridLines.len && gridLines[++leadingBlanks] == "") + while(leadingBlanks < length(gridLines) && gridLines[++leadingBlanks] == "") if(leadingBlanks > 1) gridLines.Cut(1, leadingBlanks) // Remove all leading blank lines. - if(!gridLines.len) // Skip it if only blank lines exist. + if(!length(gridLines)) // Skip it if only blank lines exist. continue gridSets += gridSet - if(gridLines.len && gridLines[gridLines.len] == "") - gridLines.Cut(gridLines.len) // Remove only one blank line at the end. + if(gridLines[length(gridLines)] == "") + gridLines.Cut(length(gridLines)) // Remove only one blank line at the end. - bounds[MAP_MINY] = min(bounds[MAP_MINY], clamp(gridSet.ycrd, y_lower, y_upper)) - gridSet.ycrd += gridLines.len - 1 // Start at the top and work down - bounds[MAP_MAXY] = max(bounds[MAP_MAXY], clamp(gridSet.ycrd, y_lower, y_upper)) + bounds[MAP_MINY] = min(bounds[MAP_MINY], gridSet.ycrd) + gridSet.ycrd += length(gridLines) - 1 // Start at the top and work down + bounds[MAP_MAXY] = max(bounds[MAP_MAXY], gridSet.ycrd) - var/maxx = gridSet.xcrd - if(gridLines.len) //Not an empty map - maxx = max(maxx, gridSet.xcrd + length(gridLines[1]) / key_len - 1) + var/maxx = curr_x + if(length(gridLines)) //Not an empty map + maxx = max(maxx, curr_x + length(gridLines[1]) / key_len - 1) - bounds[MAP_MAXX] = clamp(max(bounds[MAP_MAXX], maxx), x_lower, x_upper) + bounds[MAP_MAXX] = max(bounds[MAP_MAXX], maxx) CHECK_TICK // Indicate failure to parse any coordinates by nulling bounds if(bounds[1] == 1.#INF) - bounds = null - parsed_bounds = bounds + src.bounds = null + else + // Clamp all our mins and maxes down to the proscribed limits + bounds[MAP_MINX] = clamp(bounds[MAP_MINX], x_lower, x_upper) + bounds[MAP_MAXX] = clamp(bounds[MAP_MAXX], x_lower, x_upper) + bounds[MAP_MINY] = clamp(bounds[MAP_MINY], y_lower, y_upper) + bounds[MAP_MAXY] = clamp(bounds[MAP_MAXY], y_lower, y_upper) + + parsed_bounds = src.bounds + src.key_len = key_len /// Load the parsed map into the world. See [/proc/load_map] for arguments. -/datum/parsed_map/proc/load(x_offset, y_offset, z_offset, cropMap, no_changeturf, x_lower, x_upper, y_lower, y_upper, placeOnTop) +/datum/parsed_map/proc/load(x_offset, y_offset, z_offset, cropMap, no_changeturf, x_lower, x_upper, y_lower, y_upper, placeOnTop, whitelist = FALSE) //How I wish for RAII Master.StartLoadingMap() . = _load_impl(x_offset, y_offset, z_offset, cropMap, no_changeturf, x_lower, x_upper, y_lower, y_upper, placeOnTop) Master.StopLoadingMap() + +#define MAPLOADING_CHECK_TICK \ + if(TICK_CHECK) { \ + SSatoms.map_loader_stop(); \ + stoplag(); \ + SSatoms.map_loader_begin(); \ + } // Do not call except via load() above. /datum/parsed_map/proc/_load_impl(x_offset = 1, y_offset = 1, z_offset = world.maxz + 1, cropMap = FALSE, no_changeturf = FALSE, x_lower = -INFINITY, x_upper = INFINITY, y_lower = -INFINITY, y_upper = INFINITY, placeOnTop = FALSE) PRIVATE_PROC(TRUE) - var/list/areaCache = list() var/list/modelCache = build_cache(no_changeturf) var/space_key = modelCache[SPACE_KEY] var/list/bounds + var/key_len = src.key_len src.bounds = bounds = list(1.#INF, 1.#INF, 1.#INF, -1.#INF, -1.#INF, -1.#INF) + // Tell ss atoms that we're doing maploading + // We'll have to account for this in the following tick_checks so it doesn't overflow + SSatoms.map_loader_begin() + //used for sending the maxx and maxy expanded global signals at the end of this proc var/has_expanded_world_maxx = FALSE var/has_expanded_world_maxy = FALSE - + var/y_relative_to_absolute = y_offset - 1 + var/x_relative_to_absolute = x_offset - 1 for(var/datum/grid_set/gset as anything in gridSets) - var/ycrd = gset.ycrd + y_offset - 1 + var/relative_x = gset.xcrd + var/relative_y = gset.ycrd + var/true_xcrd = relative_x + x_relative_to_absolute + var/ycrd = relative_y + y_relative_to_absolute var/zcrd = gset.zcrd + z_offset - 1 if(!cropMap && ycrd > world.maxy) world.maxy = ycrd // Expand Y here. X is expanded in the loop below has_expanded_world_maxy = TRUE var/zexpansion = zcrd > world.maxz + var/no_afterchange = no_changeturf if(zexpansion) if(cropMap) continue @@ -167,50 +204,141 @@ world.incrementMaxZ() if(!no_changeturf) WARNING("Z-level expansion occurred without no_changeturf set, this may cause problems when /turf/AfterChange is called") + no_afterchange = TRUE + // Ok so like. something important + // We talk in "relative" coords here, so the coordinate system of the map datum + // This is so we can do offsets, but it is NOT the same as positions in game + // That's why there's some uses of - y_relative_to_absolute here, to turn absolute positions into relative ones - for(var/line in gset.gridLines) - if((ycrd - y_offset + 1) < y_lower || (ycrd - y_offset + 1) > y_upper) //Reverse operation and check if it is out of bounds of cropping. - --ycrd + // Skip Y coords that are above the smallest of the three params + // So maxy and y_upper get to act as thresholds, and relative_y can play + var/y_skip_above = min(world.maxy - y_relative_to_absolute, y_upper, relative_y) + // How many lines to skip because they'd be above the y cuttoff line + var/y_starting_skip = relative_y - y_skip_above + ycrd += y_starting_skip + + // Y is the LOWEST it will ever be here, so we can easily set a threshold for how low to go + var/line_count = length(gset.gridLines) + var/lowest_y = relative_y - (line_count - 1) // -1 because we decrement at the end of the loop, not the start + var/y_ending_skip = max(max(y_lower, 1 - y_relative_to_absolute) - lowest_y, 0) + + // Now we're gonna precompute the x thresholds + // We skip all the entries below the lower x, or 1 + var/starting_x_delta = max(max(x_lower, 1 - x_relative_to_absolute) - relative_x, 0) + // The x loop counts by key length, so we gotta multiply here + var/x_starting_skip = starting_x_delta * key_len + true_xcrd += starting_x_delta + + var/line_length = 0 + if(line_count) + // This is promised as static, so we will treat it as such + line_length = length(gset.gridLines[1]) + // We're gonna skip all the entries above the upper x, or maxx if cropMap is set + var/x_target = line_length - key_len + 1 + var/x_step_count = ROUND_UP(x_target / key_len) + var/final_x = relative_x + (x_step_count - 1) + var/x_delta_with = x_upper + if(cropMap) + // Take our smaller crop threshold yes? + x_delta_with = min(x_delta_with, world.maxx) + if(final_x > x_delta_with) + // If our relative x is greater then X upper, well then we've gotta limit our expansion + var/delta = max(final_x - x_delta_with, 0) + x_step_count -= delta + final_x -= delta + x_target = x_step_count * key_len + if(final_x > world.maxx && !cropMap) + world.maxx = final_x + has_expanded_world_maxx = TRUE + + // We're gonna track the first and last pairs of coords we find + // The first x is guarenteed to be the lowest, the first y the highest, and vis versa + // This is faster then doing mins and maxes inside the hot loop below + var/first_found = FALSE + var/first_x = 0 + var/first_y = 0 + var/last_x = 0 + var/last_y = 0 + + // Everything following this line is VERY hot. How hot depends on the map format + // (Yes this does mean dmm is technically faster to parse. shut up) + + // This is the "is this map tgm" check + if(key_len == line_length) + // Wanna clear something up about maps, talking in 255x255 here + // In the tgm format, each gridset contains 255 lines, each line representing one tile, with 255 total gridsets + // In the dmm format, each gridset contains 255 lines, each line representing one row of tiles, containing 255 * line length characters, with one gridset per z + // since this is the tgm branch any cutoff of x means we just shouldn't iterate this gridset + if(!x_step_count || x_starting_skip) continue - if(ycrd <= world.maxy && ycrd >= 1) - var/xcrd = gset.xcrd + x_offset - 1 - for(var/tpos = 1 to length(line) - key_len + 1 step key_len) - if((xcrd - x_offset + 1) < x_lower || (xcrd - x_offset + 1) > x_upper) //Same as above. - ++xcrd - continue //X cropping. - if(xcrd > world.maxx) - if(cropMap) - break - else - world.maxx = xcrd - has_expanded_world_maxx = TRUE + for(var/i in 1 + y_starting_skip to line_count - y_ending_skip) + var/line = gset.gridLines[i] + if(line == space_key && no_afterchange) + #ifdef TESTING + ++turfsSkipped + #endif + ycrd-- + MAPLOADING_CHECK_TICK + continue - if(xcrd >= 1) - var/model_key = copytext(line, tpos, tpos + key_len) - var/no_afterchange = no_changeturf || zexpansion - if(!no_afterchange || (model_key != space_key)) - var/list/cache = modelCache[model_key] - if(!cache) - CRASH("Undefined model key in DMM: [model_key]") - build_coordinate(areaCache, cache, locate(xcrd, ycrd, zcrd), no_afterchange, placeOnTop) + var/list/cache = modelCache[line] + if(!cache) + SSatoms.map_loader_stop() + CRASH("Undefined model key in DMM: [line]") + build_coordinate(cache, locate(true_xcrd, ycrd, zcrd), no_afterchange, placeOnTop) - // only bother with bounds that actually exist - bounds[MAP_MINX] = min(bounds[MAP_MINX], xcrd) - bounds[MAP_MINY] = min(bounds[MAP_MINY], ycrd) - bounds[MAP_MINZ] = min(bounds[MAP_MINZ], zcrd) - bounds[MAP_MAXX] = max(bounds[MAP_MAXX], xcrd) - bounds[MAP_MAXY] = max(bounds[MAP_MAXY], ycrd) - bounds[MAP_MAXZ] = max(bounds[MAP_MAXZ], zcrd) + // only bother with bounds that actually exist + if(!first_found) + first_found = TRUE + first_y = ycrd + last_y = ycrd + ycrd-- + MAPLOADING_CHECK_TICK + // The x coord never changes, so this is safe + if(first_found) + first_x = true_xcrd + last_x = true_xcrd + else + // This is the dmm parser, note the double loop + for(var/i in 1 + y_starting_skip to line_count - y_ending_skip) + var/line = gset.gridLines[i] + + var/xcrd = true_xcrd + for(var/tpos in 1 + x_starting_skip to x_target step key_len) + var/model_key = copytext(line, tpos, tpos + key_len) + if(model_key == space_key && no_afterchange) #ifdef TESTING - else - ++turfsSkipped + ++turfsSkipped #endif - CHECK_TICK + MAPLOADING_CHECK_TICK + ++xcrd + continue + var/list/cache = modelCache[model_key] + if(!cache) + SSatoms.map_loader_stop() + CRASH("Undefined model key in DMM: [model_key]") + build_coordinate(cache, locate(xcrd, ycrd, zcrd), no_afterchange, placeOnTop) + + // only bother with bounds that actually exist + if(!first_found) + first_found = TRUE + first_x = xcrd + first_y = ycrd + last_x = xcrd + last_y = ycrd + MAPLOADING_CHECK_TICK ++xcrd - --ycrd - - CHECK_TICK + ycrd-- + MAPLOADING_CHECK_TICK + bounds[MAP_MINX] = min(bounds[MAP_MINX], first_x) + bounds[MAP_MINY] = min(bounds[MAP_MINY], last_y) + bounds[MAP_MINZ] = min(bounds[MAP_MINZ], zcrd) + bounds[MAP_MAXX] = max(bounds[MAP_MAXX], last_x) + bounds[MAP_MAXY] = max(bounds[MAP_MAXY], first_y) + bounds[MAP_MAXZ] = max(bounds[MAP_MAXZ], zcrd) + // And we are done lads, call it off + SSatoms.map_loader_stop() if(!no_changeturf) for(var/turf/T as anything in block(locate(bounds[MAP_MINX], bounds[MAP_MINY], bounds[MAP_MINZ]), locate(bounds[MAP_MAXX], bounds[MAP_MAXY], bounds[MAP_MAXZ]))) //we do this after we load everything in. if we don't; we'll have weird atmos bugs regarding atmos adjacent turfs @@ -226,185 +354,167 @@ return TRUE +GLOBAL_LIST_EMPTY(map_model_default) + /datum/parsed_map/proc/build_cache(no_changeturf, bad_paths=null) if(modelCache && !bad_paths) return modelCache . = modelCache = list() var/list/grid_models = src.grid_models + var/set_space = FALSE + // Use where a list is needed, but where it will not be modified + // Used here to remove the cost of needing to make a new list for each fields entry when it's set manually later + var/static/list/default_list = GLOB.map_model_default for(var/model_key in grid_models) var/model = grid_models[model_key] - var/list/members = list() //will contain all members (paths) in model (in our example : /turf/unsimulated/wall and /area/mine/explored) - var/list/members_attributes = list() //will contain lists filled with corresponding variables, if any (in our example : list(icon_state = "rock") and list()) + // This is safe because dmm strings will never actually newline + // So we can parse things just fine + var/list/entries = splittext(model, ",\n") + //will contain all members (paths) in model (in our example : /turf/unsimulated/wall and /area/mine/explored) + var/list/members = new /list(length(entries)) + //will contain lists filled with corresponding variables, if any (in our example : list(icon_state = "rock") and list()) + //member attributes are rarish, so we could lazyinit this + var/list/members_attributes = new /list(length(entries)) ///////////////////////////////////////////////////////// //Constructing members and corresponding variables lists //////////////////////////////////////////////////////// var/index = 1 - var/old_position = 1 - var/dpos + for(var/member_string in entries) + var/variables_start = 0 + //findtext is a bit expensive, lets only do this if the last char of our string is a } (IE: we know we have vars) + //this saves about 25 miliseconds on my machine. Not a major optimization + if(member_string[length(member_string)] == "}") + variables_start = findtext(member_string, "{") - while(dpos != 0) - //finding next member (e.g /turf/unsimulated/wall{icon_state = "rock"} or /area/mine/explored) - dpos = find_next_delimiter_position(model, old_position, ",", "{", "}") //find next delimiter (comma here) that's not within {...} - - var/full_def = trim_text(copytext(model, old_position, dpos)) //full definition, e.g : /obj/foo/bar{variables=derp} - var/variables_start = findtext(full_def, "{") - var/path_text = trim_text(copytext(full_def, 1, variables_start)) + var/path_text = TRIM_TEXT(copytext(member_string, 1, variables_start)) var/atom_def = text2path(path_text) //path definition, e.g /obj/foo/bar - if(dpos) - old_position = dpos + length(model[dpos]) if(!ispath(atom_def, /atom)) // Skip the item if the path does not exist. Fix your crap, mappers! if(bad_paths) LAZYOR(bad_paths[path_text], model_key) continue - members.Add(atom_def) + members[index] = atom_def //transform the variables in text format into a list (e.g {var1="derp"; var2; var3=7} => list(var1="derp", var2, var3=7)) - var/list/fields = list() - + var/list/fields = default_list if(variables_start)//if there's any variable - full_def = copytext(full_def, variables_start + length(full_def[variables_start]), -length(copytext_char(full_def, -1))) //removing the last '}' - fields = readlist(full_def, ";") - if(fields.len) - if(!trim(fields[fields.len])) - --fields.len - for(var/I in fields) - var/value = fields[I] - if(istext(value)) - fields[I] = apply_text_macros(value) + member_string = copytext(member_string, variables_start + length(member_string[variables_start]), -length(copytext_char(member_string, -1))) //removing the last '}' + fields = readlist(member_string, ";") + for(var/I in fields) + var/value = fields[I] + if(istext(value)) + fields[I] = apply_text_macros(value) //then fill the members_attributes list with the corresponding variables - members_attributes.len++ members_attributes[index++] = fields - CHECK_TICK //check and see if we can just skip this turf //So you don't have to understand this horrid statement, we can do this if - // 1. no_changeturf is set - // 2. the space_key isn't set yet + // 1. the space_key isn't set yet + // 2. no_changeturf is set // 3. there are exactly 2 members // 4. with no attributes // 5. and the members are world.turf and world.area // Basically, if we find an entry like this: "XXX" = (/turf/default, /area/default) // We can skip calling this proc every time we see XXX - if(no_changeturf \ - && !(.[SPACE_KEY]) \ + if(!set_space \ + && no_changeturf \ && members.len == 2 \ && members_attributes.len == 2 \ && length(members_attributes[1]) == 0 \ && length(members_attributes[2]) == 0 \ && (world.area in members) \ && (world.turf in members)) - + set_space = TRUE .[SPACE_KEY] = model_key continue - .[model_key] = list(members, members_attributes) -/datum/parsed_map/proc/build_coordinate(list/areaCache, list/model, turf/crds, no_changeturf as num, placeOnTop as num) +/datum/parsed_map/proc/build_coordinate(list/model, turf/crds, no_changeturf as num, placeOnTop as num) + // If we don't have a turf, nothing we will do next will actually acomplish anything, so just go back + // Note, this would actually drop area vvs in the tile, but like, why tho + if(!crds) + return var/index var/list/members = model[1] var/list/members_attributes = model[2] + // We use static lists here because it's cheaper then passing them around + var/static/list/default_list = GLOB.map_model_default + var/static/list/area_cache = GLOB.areas_by_type //////////////// //Instanciation //////////////// - for (var/turf_in_blacklist in turf_blacklist) - if (crds == turf_in_blacklist) //if the given turf is blacklisted, dont do anything with it - return + if(turf_blacklist?[crds]) + return //The next part of the code assumes there's ALWAYS an /area AND a /turf on a given tile //first instance the /area and remove it from the members list index = members.len + var/atom/instance if(members[index] != /area/template_noop) - var/atype = members[index] - world.preloader_setup(members_attributes[index], atype)//preloader for assigning set variables on atom creation - var/atom/instance = areaCache[atype] + if(members_attributes[index] != default_list) + world.preloader_setup(members_attributes[index], members[index])//preloader for assigning set variables on atom creation + instance = area_cache[members[index]] if (!instance) - instance = GLOB.areas_by_type[atype] - if (!instance) - instance = new atype(null) - areaCache[atype] = instance - if(crds) - instance.contents.Add(crds) + // Done here because it's cheaper then doing it in the outside check + var/area_type = members[index] + instance = new area_type(null) + if(!instance) + CRASH("[area_type] failed to be new'd, what'd you do?") + area_cache[area_type] = instance - if(GLOB.use_preloader && instance) + instance.contents.Add(crds) + + if(GLOB.use_preloader) world.preloader_load(instance) - //then instance the /turf and, if multiple tiles are presents, simulates the DMM underlays piling effect + // Index right before /area is /turf + index-- + //then instance the /turf + //NOTE: this used to place any turfs before the last "underneath" it using .appearance and underlays + //We don't actually use this, and all it did was cost cpu, so we don't do this anymore + if(members[index] != /turf/template_noop) + if(members_attributes[index] != default_list) + world.preloader_setup(members_attributes[index], members[index]) - var/first_turf_index = 1 - while(!ispath(members[first_turf_index], /turf)) //find first /turf object in members - first_turf_index++ + // Note: we make the assertion that the last path WILL be a turf. if it isn't, this will fail. + if(placeOnTop) + instance = crds.PlaceOnTop(null, members[index], CHANGETURF_DEFER_CHANGE | (no_changeturf ? CHANGETURF_SKIP : NONE)) + else if(no_changeturf) + instance = create_atom(members[index], crds)//first preloader pass + else + instance = crds.ChangeTurf(members[index], null, CHANGETURF_DEFER_CHANGE) - //turn off base new Initialization until the whole thing is loaded - SSatoms.map_loader_begin() - //instanciate the first /turf - var/turf/T - if(members[first_turf_index] != /turf/template_noop) - T = instance_atom(members[first_turf_index],members_attributes[first_turf_index],crds,no_changeturf,placeOnTop) - - if(T) - //if others /turf are presents, simulates the underlays piling effect - index = first_turf_index + 1 - while(index <= members.len - 1) // Last item is an /area - var/underlay = T.appearance - T = instance_atom(members[index],members_attributes[index],crds,no_changeturf,placeOnTop)//instance new turf - T.underlays += underlay - index++ + if(GLOB.use_preloader && instance)//second preloader pass, for those atoms that don't ..() in New() + world.preloader_load(instance) + MAPLOADING_CHECK_TICK //finally instance all remainings objects/mobs - for(index in 1 to first_turf_index-1) - instance_atom(members[index],members_attributes[index],crds,no_changeturf,placeOnTop) - //Restore initialization to the previous value - SSatoms.map_loader_stop() + for(var/atom_index in 1 to index-1) + if(members_attributes[atom_index] != default_list) + world.preloader_setup(members_attributes[atom_index], members[atom_index]) + + // We make the assertion that only /atom s will be in this portion of the code. if that isn't true, this will fail + instance = create_atom(members[atom_index], crds)//first preloader pass + + if(GLOB.use_preloader && instance)//second preloader pass, for those atoms that don't ..() in New() + world.preloader_load(instance) + MAPLOADING_CHECK_TICK //////////////// //Helpers procs //////////////// -//Instance an atom at (x,y,z) and gives it the variables in attributes -/datum/parsed_map/proc/instance_atom(path,list/attributes, turf/crds, no_changeturf, placeOnTop) - world.preloader_setup(attributes, path) - - if(crds) - if(ispath(path, /turf)) - if(placeOnTop) - . = crds.PlaceOnTop(null, path, CHANGETURF_DEFER_CHANGE | (no_changeturf ? CHANGETURF_SKIP : NONE)) - else if(!no_changeturf) - . = crds.ChangeTurf(path, null, CHANGETURF_DEFER_CHANGE) - else - . = create_atom(path, crds)//first preloader pass - else - . = create_atom(path, crds)//first preloader pass - - if(GLOB.use_preloader && .)//second preloader pass, for those atoms that don't ..() in New() - world.preloader_load(.) - - //custom CHECK_TICK here because we don't want things created while we're sleeping to not initialize - if(TICK_CHECK) - SSatoms.map_loader_stop() - stoplag() - SSatoms.map_loader_begin() - /datum/parsed_map/proc/create_atom(path, crds) set waitfor = FALSE . = new path (crds) -//text trimming (both directions) helper proc -//optionally removes quotes before and after the text (for variable name) -/datum/parsed_map/proc/trim_text(what as text,trim_quotes=0) - if(trim_quotes) - return trimQuotesRegex.Replace(what, "") - else - return trimRegex.Replace(what, "") - - //find the position of the next delimiter,skipping whatever is comprised between opening_escape and closing_escape //returns 0 if reached the last delimiter /datum/parsed_map/proc/find_next_delimiter_position(text as text,initial_position as num, delimiter=",",opening_escape="\"",closing_escape="\"") @@ -419,7 +529,6 @@ return next_delimiter - //build a list from variables in text form (e.g {var1="derp"; var2; var3=7} => list(var1="derp", var2, var3=7)) //return the filled list /datum/parsed_map/proc/readlist(text as text, delimiter=",") @@ -427,30 +536,49 @@ if (!text) return - var/position - var/old_position = 1 + // If we're using a semi colon, we can do this as splittext rather then constant calls to find_next_delimiter_position + // This does make the code a bit harder to read, but saves a good bit of time so suck it up + var/using_semicolon = delimiter == ";" + if(using_semicolon) + var/list/line_entries = splittext(text, ";\n") + for(var/entry in line_entries) + // check if this is a simple variable (as in list(var1, var2)) or an associative one (as in list(var1="foo",var2=7)) + var/equal_position = findtext(entry,"=") + // This could in theory happen if someone inserts an improper newline + // Let's be nice and kill it here rather then later, it'll save like 0.02 seconds if we don't need to run trims in build_cache + if(!equal_position) + continue + var/trim_left = TRIM_TEXT(copytext(entry,1,equal_position)) - while(position != 0) - // find next delimiter that is not within "..." - position = find_next_delimiter_position(text,old_position,delimiter) - - // check if this is a simple variable (as in list(var1, var2)) or an associative one (as in list(var1="foo",var2=7)) - var/equal_position = findtext(text,"=",old_position, position) - - var/trim_left = trim_text(copytext(text,old_position,(equal_position ? equal_position : position))) - var/left_constant = delimiter == ";" ? trim_left : parse_constant(trim_left) - if(position) - old_position = position + length(text[position]) - - if(equal_position && !isnum(left_constant)) // Associative var, so do the association. // Note that numbers cannot be keys - the RHS is dropped if so. - var/trim_right = trim_text(copytext(text, equal_position + length(text[equal_position]), position)) + var/trim_right = TRIM_TEXT(copytext(entry, equal_position + length(entry[equal_position]))) var/right_constant = parse_constant(trim_right) - .[left_constant] = right_constant + .[trim_left] = right_constant + else + var/position + var/old_position = 1 + while(position != 0) + // find next delimiter that is not within "..." + position = find_next_delimiter_position(text,old_position,delimiter) - else // simple var - . += list(left_constant) + // check if this is a simple variable (as in list(var1, var2)) or an associative one (as in list(var1="foo",var2=7)) + var/equal_position = findtext(text,"=",old_position, position) + var/trim_left = TRIM_TEXT(copytext(text,old_position,(equal_position ? equal_position : position))) + var/left_constant = parse_constant(trim_left) + if(position) + old_position = position + length(text[position]) + if(!left_constant) // damn newlines man. Exists to provide behavior consistency with the above loop. not a major cost becuase this path is cold + continue + + if(equal_position && !isnum(left_constant)) + // Associative var, so do the association. + // Note that numbers cannot be keys - the RHS is dropped if so. + var/trim_right = TRIM_TEXT(copytext(text, equal_position + length(text[equal_position]), position)) + var/right_constant = parse_constant(trim_right) + .[left_constant] = right_constant + else // simple var + . += list(left_constant) /datum/parsed_map/proc/parse_constant(text) // number @@ -460,7 +588,10 @@ // string if(text[1] == "\"") - return copytext(text, length(text[1]) + 1, findtext(text, "\"", length(text[1]) + 1)) + // insert implied locate \" and length("\"") here + // It's a minimal timesave but it is a timesave + // Safe becuase we're guarenteed trimmed constants + return copytext(text, 2, -1) // list if(copytext(text, 1, 6) == "list(")//6 == length("list(") + 1 @@ -488,7 +619,8 @@ /datum/parsed_map/Destroy() ..() - turf_blacklist.Cut() + if(turf_blacklist) + turf_blacklist.Cut() parsed_bounds.Cut() bounds.Cut() grid_models.Cut() diff --git a/code/modules/mapping/ruins.dm b/code/modules/mapping/ruins.dm index 150874bfa3b..dec3516e3da 100644 --- a/code/modules/mapping/ruins.dm +++ b/code/modules/mapping/ruins.dm @@ -1,4 +1,4 @@ -/datum/map_template/ruin/proc/try_to_place(z,allowed_areas,turf/forced_turf) +/datum/map_template/ruin/proc/try_to_place(z, list/allowed_areas_typecache, turf/forced_turf, clear_below) var/sanity = forced_turf ? 1 : PLACEMENT_TRIES if(SSmapping.level_trait(z,ZTRAIT_ISOLATED_RUINS)) return place_on_isolated_level(z) @@ -8,17 +8,21 @@ var/height_border = TRANSITIONEDGE + SPACERUIN_MAP_EDGE_PAD + round(height / 2) var/turf/central_turf = forced_turf ? forced_turf : locate(rand(width_border, world.maxx - width_border), rand(height_border, world.maxy - height_border), z) var/valid = TRUE + var/list/affected_turfs = get_affected_turfs(central_turf,1) + var/list/affected_areas = list() - for(var/turf/check in get_affected_turfs(central_turf,1)) - var/area/new_area = get_area(check) - valid = FALSE // set to false before we check + for(var/turf/check in affected_turfs) + // Use assoc lists to move this out, it's easier that way if(check.turf_flags & NO_RUINS) + valid = FALSE // set to false before we check break - for(var/type in allowed_areas) - if(istype(new_area, type)) // it's at least one of our types so it's whitelisted - valid = TRUE - break - if(!valid) + var/area/new_area = get_area(check) + affected_areas[new_area] = TRUE + + // This is faster yes. Only BARELY but it is faster + for(var/area/affct_area as anything in affected_areas) + if(!allowed_areas_typecache[affct_area.type]) + valid = FALSE break if(!valid) @@ -26,19 +30,21 @@ testing("Ruin \"[name]\" placed at ([central_turf.x], [central_turf.y], [central_turf.z])") - for(var/i in get_affected_turfs(central_turf, 1)) - var/turf/T = i - for(var/obj/structure/spawner/nest in T) - qdel(nest) - for(var/mob/living/simple_animal/monster in T) - qdel(monster) - for(var/obj/structure/flora/plant in T) - qdel(plant) + if(clear_below) + var/list/static/clear_below_typecache = typecacheof(list( + /obj/structure/spawner, + /mob/living/simple_animal, + /obj/structure/flora + )) + for(var/turf/T as anything in affected_turfs) + for(var/atom/thing as anything in T) + if(clear_below_typecache[thing.type]) + qdel(thing) load(central_turf,centered = TRUE) loaded++ - for(var/turf/T in get_affected_turfs(central_turf, 1)) + for(var/turf/T in affected_turfs) T.turf_flags |= NO_RUINS new /obj/effect/landmark/ruin(central_turf, src) @@ -58,10 +64,11 @@ return center -/proc/seedRuins(list/z_levels = null, budget = 0, whitelist = list(/area/space), list/potentialRuins) +/proc/seedRuins(list/z_levels = null, budget = 0, whitelist = list(/area/space), list/potentialRuins, clear_below = FALSE) if(!z_levels || !z_levels.len) WARNING("No Z levels provided - Not generating ruins") return + var/list/whitelist_typecache = typecacheof(whitelist) for(var/zl in z_levels) var/turf/T = locate(1, 1, zl) @@ -123,7 +130,7 @@ else break outer - placed_turf = current_pick.try_to_place(target_z,whitelist,forced_turf) + placed_turf = current_pick.try_to_place(target_z,whitelist_typecache,forced_turf,clear_below) if(!placed_turf) continue else diff --git a/code/modules/mapping/space_management/space_transition.dm b/code/modules/mapping/space_management/space_transition.dm index c52f239e1c3..df9396ceee1 100644 --- a/code/modules/mapping/space_management/space_transition.dm +++ b/code/modules/mapping/space_management/space_transition.dm @@ -22,122 +22,140 @@ neigbours[TEXT_WEST] = P.spl P.spl.neigbours[TEXT_EAST] = src +#define CHORDS_TO_1D(x, y, grid_diameter) ((x) + ((y) - 1) * (grid_diameter)) /datum/space_transition_point //this is explicitly utilitarian datum type made specially for the space map generation and are absolutely unusable for anything else var/list/neigbours = list() var/x var/y var/datum/space_level/spl -/datum/space_transition_point/New(nx, ny, list/point_grid) - if(!point_grid) +/datum/space_transition_point/New(nx, ny, list/grid) + if(!grid) qdel(src) return - var/list/L = point_grid[1] - if(nx > point_grid.len || ny > L.len) + var/grid_diameter = sqrt(length(grid)) + if(nx > grid_diameter || ny > grid_diameter) + stack_trace("Attempted to set a position outside the size of [grid_diameter]") qdel(src) return x = nx y = ny - if(point_grid[x][y]) + var/position = CHORDS_TO_1D(x, y, grid_diameter) + if(grid[position]) return - point_grid[x][y] = src + grid[position] = src -/datum/space_transition_point/proc/set_neigbours(list/grid) - var/max_X = grid.len - var/list/max_Y = grid[1] - max_Y = max_Y.len +/datum/space_transition_point/proc/set_neigbours(list/grid, size) neigbours.Cut() - if(x+1 <= max_X) - neigbours |= grid[x+1][y] + + if(x+1 <= size) + neigbours |= grid[CHORDS_TO_1D(x+1, y, size)] if(x-1 >= 1) - neigbours |= grid[x-1][y] - if(y+1 <= max_Y) - neigbours |= grid[x][y+1] + neigbours |= grid[CHORDS_TO_1D(x-1, y, size)] + if(y+1 <= size) + neigbours |= grid[CHORDS_TO_1D(x, y + 1, size)] if(y-1 >= 1) - neigbours |= grid[x][y-1] + neigbours |= grid[CHORDS_TO_1D(x, y - 1, size)] /datum/controller/subsystem/mapping/proc/setup_map_transitions() //listamania - var/list/SLS = list() + var/list/transition_levels = list() var/list/cached_z_list = z_list - var/conf_set_len = 0 - for(var/A in cached_z_list) - var/datum/space_level/D = A - if (D.linkage == CROSSLINKED) - SLS.Add(D) - conf_set_len++ - var/list/point_grid[conf_set_len*2+1][conf_set_len*2+1] - var/list/grid = list() - var/datum/space_transition_point/P - for(var/x in 1 to conf_set_len*2+1) - for(var/y in 1 to conf_set_len*2+1) - P = new/datum/space_transition_point(x, y, point_grid) - point_grid[x][y] = P - grid.Add(P) - for(var/datum/space_transition_point/pnt in grid) - pnt.set_neigbours(point_grid) - P = point_grid[conf_set_len+1][conf_set_len+1] + for(var/datum/space_level/level as anything in cached_z_list) + if (level.linkage == CROSSLINKED) + transition_levels.Add(level) + + var/grid_diameter = (length(transition_levels) * 2) + 1 + var/list/grid = new /list(grid_diameter ** 2) + + var/datum/space_transition_point/point + for(var/x in 1 to grid_diameter) + for(var/y in 1 to grid_diameter) + point = new/datum/space_transition_point(x, y, grid) + grid[CHORDS_TO_1D(x, y, grid_diameter)] = point + for(point as anything in grid) + point.set_neigbours(grid, grid_diameter) + + var/center = round(grid_diameter / 2) + point = grid[CHORDS_TO_1D(grid_diameter, center, center)] + grid.Cut() + + var/list/transition_pick = transition_levels.Copy() var/list/possible_points = list() var/list/used_points = list() - grid.Cut() - while(SLS.len) - var/datum/space_level/D = pick_n_take(SLS) - D.xi = P.x - D.yi = P.y - P.spl = D - possible_points |= P.neigbours - used_points |= P + while(transition_pick.len) + var/datum/space_level/level = pick_n_take(transition_pick) + level.xi = point.x + level.yi = point.y + point.spl = level + possible_points |= point.neigbours + used_points |= point possible_points.Remove(used_points) - D.set_neigbours(used_points) - P = pick(possible_points) + level.set_neigbours(used_points) + point = pick(possible_points) CHECK_TICK + // Now that we've handed out neighbors, we're gonna handle an edge case + // Need to check if all our levels have neighbors in all directions + // If they don't, we'll make them wrap all the way around to the other side of the grid + for(var/direction in GLOB.cardinals) + var/dir = "[direction]" + var/inverse = "[turn(direction, 180)]" + for(var/datum/space_level/level as anything in transition_levels) + // If we have something in this dir that isn't just us, continue on + if(level.neigbours[dir] && level.neigbours[dir] != level) + continue + var/datum/space_level/head = level + while(head.neigbours[inverse] && head.neigbours[inverse] != head) + head = head.neigbours[inverse] + + // Alllright we've landed on someone who we can wrap around onto safely, let's make that connection yeah? + head.neigbours[inverse] = level + level.neigbours[dir] = head + //Lists below are pre-calculated values arranged in the list in such a way to be easily accessable in the loop by the counter //Its either this or madness with lotsa math - - var/list/x_pos_beginning = list(1, 1, world.maxx - TRANSITIONEDGE, 1) //x values of the lowest-leftest turfs of the respective 4 blocks on each side of zlevel - var/list/y_pos_beginning = list(world.maxy - TRANSITIONEDGE, 1, 1 + TRANSITIONEDGE, 1 + TRANSITIONEDGE) //y values respectively + var/inner_max_x = world.maxx - TRANSITIONEDGE + var/inner_max_y = world.maxy - TRANSITIONEDGE + var/list/x_pos_beginning = list(1, 1, inner_max_x, 1) //x values of the lowest-leftest turfs of the respective 4 blocks on each side of zlevel + var/list/y_pos_beginning = list(inner_max_y, 1, 1 + TRANSITIONEDGE, 1 + TRANSITIONEDGE) //y values respectively var/list/x_pos_ending = list(world.maxx, world.maxx, world.maxx, 1 + TRANSITIONEDGE) //x values of the highest-rightest turfs of the respective 4 blocks on each side of zlevel - var/list/y_pos_ending = list(world.maxy, 1 + TRANSITIONEDGE, world.maxy - TRANSITIONEDGE, world.maxy - TRANSITIONEDGE) //y values respectively - var/list/x_pos_transition = list(1, 1, TRANSITIONEDGE + 2, world.maxx - TRANSITIONEDGE - 1) //values of x for the transition from respective blocks on the side of zlevel, 1 is being translated into turfs respective x value later in the code - var/list/y_pos_transition = list(TRANSITIONEDGE + 2, world.maxy - TRANSITIONEDGE - 1, 1, 1) //values of y for the transition from respective blocks on the side of zlevel, 1 is being translated into turfs respective y value later in the code + var/list/y_pos_ending = list(world.maxy, 1 + TRANSITIONEDGE, inner_max_y, inner_max_y) //y values respectively + var/list/x_pos_transition = list(1, 1, TRANSITIONEDGE + 2, inner_max_x - 1) //values of x for the transition from respective blocks on the side of zlevel, 1 is being translated into turfs respective x value later in the code + var/list/y_pos_transition = list(TRANSITIONEDGE + 2, inner_max_y - 1, 1, 1) //values of y for the transition from respective blocks on the side of zlevel, 1 is being translated into turfs respective y value later in the code - for(var/I in cached_z_list) - var/datum/space_level/D = I - if(!D.neigbours.len) + for(var/datum/space_level/level as anything in cached_z_list) + if(!level.neigbours.len) continue - var/zlevelnumber = D.z_value + var/zlevelnumber = level.z_value for(var/side in 1 to 4) var/turf/beginning = locate(x_pos_beginning[side], y_pos_beginning[side], zlevelnumber) var/turf/ending = locate(x_pos_ending[side], y_pos_ending[side], zlevelnumber) var/list/turfblock = block(beginning, ending) var/dirside = 2**(side-1) - var/zdestination = zlevelnumber - if(D.neigbours["[dirside]"] && D.neigbours["[dirside]"] != D) - D = D.neigbours["[dirside]"] - zdestination = D.z_value - else - dirside = turn(dirside, 180) - while(D.neigbours["[dirside]"] && D.neigbours["[dirside]"] != D) - D = D.neigbours["[dirside]"] - zdestination = D.z_value - D = I + var/x_target = x_pos_transition[side] == 1 ? 0 : x_pos_transition[side] + var/y_target = y_pos_transition[side] == 1 ? 0 : y_pos_transition[side] + var/datum/space_level/neighbor = level.neigbours["[dirside]"] + var/zdestination = neighbor.z_value + for(var/turf/open/space/S in turfblock) - S.destination_x = x_pos_transition[side] == 1 ? S.x : x_pos_transition[side] - S.destination_y = y_pos_transition[side] == 1 ? S.y : y_pos_transition[side] + S.destination_x = x_target || S.x + S.destination_y = y_target || S.y S.destination_z = zdestination // Mirage border code var/mirage_dir if(S.x == 1 + TRANSITIONEDGE) mirage_dir |= WEST - else if(S.x == world.maxx - TRANSITIONEDGE) + else if(S.x == inner_max_x) mirage_dir |= EAST if(S.y == 1 + TRANSITIONEDGE) mirage_dir |= SOUTH - else if(S.y == world.maxy - TRANSITIONEDGE) + else if(S.y == inner_max_y) mirage_dir |= NORTH if(!mirage_dir) continue - var/turf/place = locate(S.destination_x, S.destination_y, S.destination_z) + var/turf/place = locate(S.destination_x, S.destination_y, zdestination) S.AddComponent(/datum/component/mirage_border, place, mirage_dir) + +#undef CHORDS_TO_1D