From d34fa4c642839215df5ba985d098a04e4d555b5b Mon Sep 17 00:00:00 2001 From: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com> Date: Thu, 22 Sep 2022 15:34:10 -0700 Subject: [PATCH] Macro optimizes SSmapping saving 50% (#69632) * 'optimizes' space transitions by like 0.06 seconds, makes them easier to read tho, so that's an upside * ''''optimizes'''' parsed map loading I'm honestly not sure how big a difference this makes, looked like small percentage points if anything It's a bit more internally concistent at least, which is nice. Also I understand the system now. I'd like to think it helped but I think this is kinda a "do you think it's easier to read" sort of situation. if it did help it was by the skin of its teeth * Saves 0.6 seconds off loading meta and lavaland's map files This is just a lot of micro stuff. 1: Bound checks don't need to be inside for loops, we can instead bound the iteration counts 2: TGM and DMM are parsed differently. in dmm a grid_set is one z level, in tgm it's one collumn. Realizing this allows you to skip copytexts and other such silly in the tgm implemenentation, saving a good bit of time 3: Min/max bounds do not need to be checked inside for loops, and can instead be handled outside of them, because we know the order of x and y iteration. This saves 0.2 seconds I may or may not have made the code harder to read, if so let me know and I'll check it over. * Micro ops key caching significantly. Fixes macros bug inserting \ into a dmm with no valid target would just less then loop the string. Dumb Anyway, optimizations. I save a LOT of time by not needing to call find_next_delimiter_position for every entry and var set. (like maybe 0.5 seconds, not totally sure) I save this by using splittext, which is significantly faster. this would cause parsing issues if you could embed \n into dmms, but you can't, so I'm safe. Lemme see uh, lots of little things, stuff that's suboptimal or could be done cheaper. Some "hey you and I both know a \" is 2 chars long sort of stuff I removed trim_text because the quote trimming was never actually used, and the space trimming was slower then using the code in trim. I also micro'd trim to save a bit of time. this saves another maybe 0.5. Few other things, I think that's the main of it. Gives me the fuzzy feelings * Saves 50% of build_coordinate's time Micro optimizing go brrrrr I made turf_blacklist an assoc list rather then just a normal one, so lookups are O(log n) instead of O(n). Also it's faster for the base case of loading mostly space. Instead of toggling the map loader right before and right after New() calls, we toggle at the start of mapload, and disable then reenable if we check tick. This saves like 0.3 seconds Rather then tracking an area cache ourselves, and needing to pass it around, we use a locally static list to reference the global list of area -> type. This is much faster, if slightly fragile. Rather then checking for a null turf at every line, we do it at the start of the proc and not after. Faster this way, tho it can in theory drop area vvs. Avoids calling world.preloader_setup unless we actually have a unique set of attributes. We use another static list to make this comparison cheap. This saves another 0.3 Rather then checking for area paths in the turf logic, or vis versa, we assume we are creating the type implied by the index we're reading off. So only the last type entry will be loaded like a turf, etc. This is slightly unsafe but saves a good bit of time, and will properly error on fucked maps. Also, rather then using a datum to hold preloader vars, we use 2 global variables. This is faster. This marks the end of my optimizations for direct maploading. I've reduced the cost of loading a map by more then 50% now. Get owned. * Adds a define for maploading tick check * makes shuttles load again, removes some of the hard limits I had on the reader for profiling * Macro ops cave generation Cave generation was insanely more expensive then it had any right to be. Maybe 0.5 seconds was saved off not doing a range(12) for EVERY SPAWNED MOB. 0.14 was saved off using expanded weighted lists (A new idea of mine) This is useful because I can take a weighted list, and condense it into weight * path count. This is more memory heavy, and costs more to create, but is so much faster then the proc. I also added a naive implementation of gcd to make this a bit less bad. It's not great, but it'll do for this usecase. Oh and I changed some ChangeTurfs into New()s. I'm still not entirely sure what the core difference between the two is, but it seems to work fine. I believe it's safe because the turf below us hasn't init'd yet, there's nothing to take from them. It's like 3 seconds faster too so I'll be sad when it turns out I'm being dumb * Micros river spawning This uses the same sort of concepts as the last change, mostly New being preferable to ChangeTurf at this level of code. This bit isn't nearly as detailed as the last few, I honestly got a bit tired. It's still like 0.4 seconds saved tho * Micros ruin loading Turns out it saves time if you don't check area type for every tile on a ruin. Not a whole ton faster, like 0.03, but faster. Saves even more time (0.1) to not iterate all your ruin's turfs 3 times to clear away lavaland mobs, when you're IN SPACE who wrote this. Oh it also saves time to only pull your turf list once, rather then 3 times --- code/__HELPERS/_lists.dm | 35 ++ code/__HELPERS/text.dm | 23 +- code/controllers/subsystem/mapping.dm | 6 +- code/datums/mapgen/CaveGenerator.dm | 189 ++++--- code/datums/mapgen/Cavegens/IcemoonCaves.dm | 20 +- .../mapgen/Cavegens/LavalandGenerator.dm | 10 +- code/datums/mapgen/JungleGenerator.dm | 2 +- code/datums/mapgen/_MapGenerator.dm | 2 +- code/game/area/areas.dm | 4 +- code/game/atoms.dm | 2 +- code/game/turfs/open/river.dm | 64 ++- code/modules/holodeck/computer.dm | 2 +- code/modules/mapping/map_template.dm | 1 + code/modules/mapping/preloader.dm | 14 +- code/modules/mapping/reader.dm | 516 +++++++++++------- code/modules/mapping/ruins.dm | 47 +- .../space_management/space_transition.dm | 156 +++--- 17 files changed, 671 insertions(+), 422 deletions(-) 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