Multi-Z Support for Lazy Templates | Cleans up some turf flag misuse (#77786)

## About The Pull Request

Adds multi-z support for lazy templates
Also fixes some improper use and placement for turf flags
## Why It's Good For The Game

Shadow needs/wants this for bit runner maps.
Turf flags are also why lava has been generating in places it shouldnt.
(inside of ruins)
## Changelog
🆑
fix: Lava can no longer occasionally generate inside of previously
loaded templates and breach and/or destroy shit
/🆑

---------

Co-authored-by: Jeremiah <42397676+jlsnow301@users.noreply.github.com>
Co-authored-by: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com>
This commit is contained in:
Zephyr
2023-09-19 01:06:08 +00:00
committed by GitHub
co-authored by Jeremiah LemonInTheDark
parent a67404f5dc
commit 1b96345e44
37 changed files with 1048 additions and 183 deletions
+159 -54
View File
@@ -114,26 +114,72 @@
var/turfsSkipped = 0
#endif
/datum/parsed_map/proc/copy()
// Avoids duped work just in case
build_cache()
var/datum/parsed_map/newfriend = new()
newfriend.original_path = original_path
newfriend.map_format = map_format
newfriend.key_len = key_len
newfriend.line_len = line_len
newfriend.grid_models = grid_models.Copy()
newfriend.gridSets = gridSets.Copy()
newfriend.modelCache = modelCache.Copy()
newfriend.parsed_bounds = parsed_bounds.Copy()
// Copy parsed bounds to reset to initial values
newfriend.bounds = parsed_bounds.Copy()
newfriend.turf_blacklist = turf_blacklist?.Copy()
return newfriend
//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).
/// - `x_offset`, `y_offset`, `z_offset`: Positions representign where to load the map (Optional).
/// - `cropMap`: When true, the map will be cropped to fit the existing world dimensions (Optional).
/// - `measureOnly`: When true, no changes will be made to the world (Optional).
/// - `no_changeturf`: When true, [/turf/proc/AfterChange] won't be called on loaded turfs
/// - `x_lower`, `x_upper`, `y_lower`, `y_upper`: Coordinates (relative to the map) to crop to (Optional).
/// - `placeOnTop`: Whether to use [/turf/proc/PlaceOnTop] rather than [/turf/proc/ChangeTurf] (Optional).
/proc/load_map(dmm_file as file, x_offset as num, y_offset as num, z_offset as num, cropMap as num, measureOnly as num, no_changeturf as num, x_lower = -INFINITY as num, x_upper = INFINITY as num, y_lower = -INFINITY as num, y_upper = INFINITY as num, placeOnTop = FALSE as num, new_z)
var/datum/parsed_map/parsed = new(dmm_file, x_lower, x_upper, y_lower, y_upper, measureOnly)
if(parsed.bounds && !measureOnly)
parsed.load(x_offset, y_offset, z_offset, cropMap, no_changeturf, x_lower, x_upper, y_lower, y_upper, placeOnTop, new_z = new_z)
return parsed
/**
* Helper and recommened way to load a map file
* - dmm_file: The path to the map file
* - x_offset: The x offset to load the map at
* - y_offset: The y offset to load the map at
* - z_offset: The z offset to load the map at
* - crop_map: If true, the map will be cropped to the world bounds
* - measure_only: If true, the map will not be loaded, but the bounds will be calculated
* - no_changeturf: If true, the map will not call /turf/AfterChange
* - x_lower: The minimum x coordinate to load
* - x_upper: The maximum x coordinate to load
* - y_lower: The minimum y coordinate to load
* - y_upper: The maximum y coordinate to load
* - z_lower: The minimum z coordinate to load
* - z_upper: The maximum z coordinate to load
* - place_on_top: Whether to use /turf/proc/PlaceOnTop rather than /turf/proc/ChangeTurf
* - new_z: If true, a new z level will be created for the map
*/
/proc/load_map(
dmm_file,
x_offset = 0,
y_offset = 0,
z_offset = 0,
crop_map = FALSE,
measure_only = FALSE,
no_changeturf = FALSE,
x_lower = -INFINITY,
x_upper = INFINITY,
y_lower = -INFINITY,
y_upper = INFINITY,
z_lower = -INFINITY,
z_upper = INFINITY,
place_on_top = FALSE,
new_z = FALSE,
)
if(!(dmm_file in GLOB.cached_maps))
GLOB.cached_maps[dmm_file] = new /datum/parsed_map(dmm_file)
var/datum/parsed_map/parsed_map = GLOB.cached_maps[dmm_file]
parsed_map = parsed_map.copy()
if(!measure_only && !isnull(parsed_map.bounds))
parsed_map.load(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z)
return parsed_map
/// 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)
/datum/parsed_map/New(tfile, x_lower = -INFINITY, x_upper = INFINITY, y_lower = -INFINITY, y_upper=INFINITY, z_lower = -INFINITY, z_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 :)
@@ -184,20 +230,26 @@
CRASH("Coords before model definition in DMM")
var/curr_x = text2num(regexOutput[3])
if(curr_x < x_lower || curr_x > x_upper)
continue
var/curr_y = text2num(regexOutput[4])
if(curr_y < y_lower || curr_y > y_upper)
continue
var/curr_z = text2num(regexOutput[5])
if(curr_z < z_lower || curr_z > z_upper)
continue
var/datum/grid_set/gridSet = new
gridSet.xcrd = curr_x
//position of the currently processed square
gridSet.ycrd = text2num(regexOutput[4])
gridSet.zcrd = text2num(regexOutput[5])
gridSet.ycrd = curr_y
gridSet.zcrd = curr_z
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)
bounds[MAP_MINZ] = min(bounds[MAP_MINZ], curr_y)
bounds[MAP_MAXZ] = max(bounds[MAP_MAXZ], curr_z)
var/list/gridLines = splittext(regexOutput[6], "\n")
gridSet.gridLines = gridLines
@@ -238,16 +290,29 @@
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)
bounds[MAP_MINZ] = clamp(bounds[MAP_MINZ], z_lower, z_upper)
bounds[MAP_MAXZ] = clamp(bounds[MAP_MAXZ], z_lower, z_upper)
parsed_bounds = src.bounds
src.key_len = key_len
src.line_len = line_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, whitelist = FALSE, new_z)
/// Iterates over all grid sets and returns ones with z values within the given bounds. Inclusive
/datum/parsed_map/proc/filter_grid_sets_based_on_z_bounds(lower_z, upper_z)
var/list/filtered_sets = list()
for(var/datum/grid_set/grid_set as anything in gridSets)
if(grid_set.zcrd < lower_z)
continue
if(grid_set.zcrd > upper_z)
continue
filtered_sets += grid_set
return filtered_sets
/// Load the parsed map into the world. You probably want [/proc/load_map]. Keep the signature the same.
/datum/parsed_map/proc/load(x_offset = 0, y_offset = 0, z_offset = 0, crop_map = FALSE, no_changeturf = FALSE, x_lower = -INFINITY, x_upper = INFINITY, y_lower = -INFINITY, y_upper = INFINITY, z_lower = -INFINITY, z_upper = INFINITY, place_on_top = FALSE, new_z = 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, new_z)
. = _load_impl(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z)
Master.StopLoadingMap()
#define MAPLOADING_CHECK_TICK \
@@ -262,7 +327,7 @@
}
// 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, new_z = FALSE)
/datum/parsed_map/proc/_load_impl(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z)
PRIVATE_PROC(TRUE)
// 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
@@ -275,9 +340,9 @@
var/sucessful = FALSE
switch(map_format)
if(MAP_TGM)
sucessful = _tgm_load(x_offset, y_offset, z_offset, cropMap, no_changeturf, x_lower, x_upper, y_lower, y_upper, placeOnTop, new_z)
sucessful = _tgm_load(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z)
else
sucessful = _dmm_load(x_offset, y_offset, z_offset, cropMap, no_changeturf, x_lower, x_upper, y_lower, y_upper, placeOnTop, new_z)
sucessful = _dmm_load(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z)
// And we are done lads, call it off
SSatoms.map_loader_stop(REF(src))
@@ -309,7 +374,7 @@
// 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
// You can think of dmm as storing maps in rows, whereas tgm stores them in columns
/datum/parsed_map/proc/_tgm_load(x_offset, y_offset, z_offset, cropMap, no_changeturf, x_lower, x_upper, y_lower, y_upper, placeOnTop, new_z)
/datum/parsed_map/proc/_tgm_load(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z)
// setup
var/list/modelCache = build_cache(no_changeturf)
var/space_key = modelCache[SPACE_KEY]
@@ -330,12 +395,12 @@
var/relative_y = first_column.ycrd
var/highest_y = relative_y + y_relative_to_absolute
if(!cropMap && highest_y > world.maxy)
if(!crop_map && highest_y > world.maxy)
if(new_z)
// Need to avoid improperly loaded area/turf_contents
world.increaseMaxY(highest_y, max_zs_to_load = z_offset - 1)
world.increase_max_y(highest_y, map_load_z_cutoff = z_offset - 1)
else
world.increaseMaxY(highest_y)
world.increase_max_y(highest_y)
expanded_y = TRUE
// Skip Y coords that are above the smallest of the three params
@@ -345,7 +410,6 @@
var/y_starting_skip = relative_y - y_skip_above
highest_y -= 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(first_column.gridLines)
var/lowest_y = relative_y - (line_count - 1) // -1 because we decrement at the end of the loop, not the start
@@ -353,7 +417,7 @@
// X setup
var/x_delta_with = x_upper
if(cropMap)
if(crop_map)
// Take our smaller crop threshold yes?
x_delta_with = min(x_delta_with, world.maxx)
@@ -367,33 +431,51 @@
// 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)
final_x -= delta
if(final_x > world.maxx && !cropMap)
if(final_x > world.maxx && !crop_map)
if(new_z)
// Need to avoid improperly loaded area/turf_contents
world.increaseMaxX(final_x, max_zs_to_load = z_offset - 1)
world.increase_max_x(final_x, map_load_z_cutoff = z_offset - 1)
else
world.increaseMaxX(final_x)
world.increase_max_x(final_x)
expanded_x = TRUE
var/lowest_x = max(x_lower, 1 - x_relative_to_absolute)
// Amount we offset the grid zcrd to get the true zcrd
var/grid_z_offset = z_offset - 1
var/z_upper_set = z_upper < INFINITY
var/z_lower_set = z_lower > -INFINITY
// We make the assumption that the last block of turfs will have the highest embedded z in it
var/highest_z = last_column.zcrd + z_offset - 1 // Lets not just make a new z level each time we increment maxz
// true max zcrd
var/map_bounds_z_max = last_column.zcrd
var/z_upper_parsed = map_bounds_z_max + z_offset - 1
if(z_upper_set)
z_upper_parsed -= map_bounds_z_max - z_upper
if(z_lower_set)
var/offset_amount = z_lower - 1
z_upper_parsed -= offset_amount
grid_z_offset -= offset_amount
var/list/target_grid_sets = gridSets
if(z_lower_set || z_upper_set) // bounds are set, filter out gridsets for z levels we don't want
target_grid_sets = filter_grid_sets_based_on_z_bounds(z_lower, z_upper)
var/z_threshold = world.maxz
if(highest_z > z_threshold && cropMap)
for(var/i in z_threshold + 1 to highest_z) //create a new z_level if needed
if(z_upper_parsed > z_threshold && crop_map)
for(var/i in z_threshold + 1 to z_upper_parsed) //create a new z_level if needed
world.incrementMaxZ()
if(!no_changeturf)
WARNING("Z-level expansion occurred without no_changeturf set, this may cause problems when /turf/AfterChange is called")
for(var/datum/grid_set/gset as anything in gridSets)
for(var/datum/grid_set/gset as anything in target_grid_sets)
var/true_xcrd = gset.xcrd + x_relative_to_absolute
// any cutoff of x means we just shouldn't iterate this gridset
if(final_x < true_xcrd || lowest_x > gset.xcrd)
continue
var/zcrd = gset.zcrd + z_offset - 1
var/zcrd = gset.zcrd + grid_z_offset
// If we're using changeturf, we disable it if we load into a z level we JUST created
var/no_afterchange = no_changeturf || zcrd > z_threshold
@@ -420,7 +502,7 @@
if(!cache)
SSatoms.map_loader_stop(REF(src))
CRASH("Undefined model key in DMM: [gset.gridLines[i]]")
build_coordinate(cache, locate(true_xcrd, ycrd, zcrd), no_afterchange, placeOnTop, new_z)
build_coordinate(cache, locate(true_xcrd, ycrd, zcrd), no_afterchange, place_on_top, new_z)
// only bother with bounds that actually exist
if(!first_found)
@@ -444,7 +526,7 @@
/// Stanrdard loading, not used in production
/// Doesn't take advantage of any tgm optimizations, which makes it slower but also more general
/// Use this if for some reason your map format is messy
/datum/parsed_map/proc/_dmm_load(x_offset, y_offset, z_offset, cropMap, no_changeturf, x_lower, x_upper, y_lower, y_upper, placeOnTop, new_z)
/datum/parsed_map/proc/_dmm_load(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z)
// setup
var/list/modelCache = build_cache(no_changeturf)
var/space_key = modelCache[SPACE_KEY]
@@ -455,23 +537,46 @@
var/y_relative_to_absolute = y_offset - 1
var/x_relative_to_absolute = x_offset - 1
var/line_len = src.line_len
for(var/datum/grid_set/gset as anything in gridSets)
// Amount we offset the grid zcrd to get the true zcrd
var/grid_z_offset = z_offset - 1
var/z_upper_set = z_upper < INFINITY
var/z_lower_set = z_lower > -INFINITY
// we now need to find the maximum z, fun!
var/map_bounds_z_max = 1
for(var/datum/grid_set/grid_set as anything in gridSets)
map_bounds_z_max = max(map_bounds_z_max, grid_set.zcrd)
var/z_upper_parsed = map_bounds_z_max + z_offset - 1
if(z_upper_set)
z_upper_parsed -= map_bounds_z_max - z_upper
if(z_lower_set)
var/offset_amount = z_lower - 1
z_upper_parsed -= offset_amount
grid_z_offset -= offset_amount
var/list/target_grid_sets = gridSets
if(z_lower_set || z_upper_set) // bounds are set, filter out gridsets for z levels we don't want
target_grid_sets = filter_grid_sets_based_on_z_bounds(z_lower, z_upper)
for(var/datum/grid_set/gset as anything in target_grid_sets)
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)
var/zcrd = gset.zcrd + grid_z_offset
if(!crop_map && ycrd > world.maxy)
if(new_z)
// Need to avoid improperly loaded area/turf_contents
world.increaseMaxY(ycrd, max_zs_to_load = z_offset - 1)
world.increase_max_y(ycrd, map_load_z_cutoff = z_offset - 1)
else
world.increaseMaxY(ycrd)
world.increase_max_y(ycrd)
expanded_y = TRUE
var/zexpansion = zcrd > world.maxz
var/no_afterchange = no_changeturf
if(zexpansion)
if(cropMap)
if(crop_map)
continue
else
while (zcrd > world.maxz) //create a new z_level if needed
@@ -508,7 +613,7 @@
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)
if(crop_map)
// Take our smaller crop threshold yes?
x_delta_with = min(x_delta_with, world.maxx)
if(final_x > x_delta_with)
@@ -517,12 +622,12 @@
x_step_count -= delta
final_x -= delta
x_target = x_step_count * key_len
if(final_x > world.maxx && !cropMap)
if(final_x > world.maxx && !crop_map)
if(new_z)
// Need to avoid improperly loaded area/turf_contents
world.increaseMaxX(final_x, max_zs_to_load = z_offset - 1)
world.increase_max_x(final_x, map_load_z_cutoff = z_offset - 1)
else
world.increaseMaxX(final_x)
world.increase_max_x(final_x)
expanded_x = TRUE
// We're gonna track the first and last pairs of coords we find
@@ -553,7 +658,7 @@
if(!cache)
SSatoms.map_loader_stop(REF(src))
CRASH("Undefined model key in DMM: [model_key]")
build_coordinate(cache, locate(xcrd, ycrd, zcrd), no_afterchange, placeOnTop, new_z)
build_coordinate(cache, locate(xcrd, ycrd, zcrd), no_afterchange, place_on_top, new_z)
// only bother with bounds that actually exist
if(!first_found)