mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-30 19:41:56 +00:00
Did you know that areas can be non unique (unique per area) I didn't, and I wrote mapping code such that we'd make all areas unique This fixes that. It costs 0.1 seconds but without it areas don't work anymore
634 lines
24 KiB
Plaintext
634 lines
24 KiB
Plaintext
///////////////////////////////////////////////////////////////
|
|
//SS13 Optimized Map loader
|
|
//////////////////////////////////////////////////////////////
|
|
#define SPACE_KEY "space"
|
|
|
|
/datum/grid_set
|
|
var/xcrd
|
|
var/ycrd
|
|
var/zcrd
|
|
var/gridLines
|
|
|
|
/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()
|
|
/// List of area types we've loaded AS A PART OF THIS MAP
|
|
/// We do this to allow non unique areas, so we'll only load one per map
|
|
var/list/area/loaded_areas = list()
|
|
|
|
var/list/modelCache
|
|
|
|
/// Unoffset bounds. Null on parse failure.
|
|
var/list/parsed_bounds
|
|
/// Offset bounds. Same as parsed_bounds until load().
|
|
var/list/bounds
|
|
|
|
///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/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).
|
|
/// - `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)
|
|
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)
|
|
return parsed
|
|
|
|
/// 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)
|
|
else if(isnull(tfile))
|
|
// create a new datum without loading a map
|
|
return
|
|
|
|
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(regexOutput[1]) // Model
|
|
var/key = regexOutput[1]
|
|
if(grid_models[key]) // Duplicate model keys are ignored in DMMs
|
|
continue
|
|
if(key_len != length(key))
|
|
if(!key_len)
|
|
key_len = length(key)
|
|
else
|
|
CRASH("Inconsistent key length in DMM")
|
|
if(!measureOnly)
|
|
grid_models[key] = regexOutput[2]
|
|
|
|
// (1,1,1) = {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}
|
|
else if(regexOutput[3]) // Coords
|
|
if(!key_len)
|
|
CRASH("Coords before model definition in DMM")
|
|
|
|
var/curr_x = text2num(regexOutput[3])
|
|
|
|
if(curr_x < x_lower || curr_x > x_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])
|
|
|
|
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(regexOutput[6], "\n")
|
|
gridSet.gridLines = gridLines
|
|
|
|
var/leadingBlanks = 0
|
|
while(leadingBlanks < length(gridLines) && gridLines[++leadingBlanks] == "")
|
|
if(leadingBlanks > 1)
|
|
gridLines.Cut(1, leadingBlanks) // Remove all leading blank lines.
|
|
|
|
if(!length(gridLines)) // Skip it if only blank lines exist.
|
|
continue
|
|
|
|
gridSets += gridSet
|
|
|
|
if(gridLines[length(gridLines)] == "")
|
|
gridLines.Cut(length(gridLines)) // Remove only one blank line at the end.
|
|
|
|
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 = curr_x
|
|
if(length(gridLines)) //Not an empty map
|
|
maxx = max(maxx, curr_x + length(gridLines[1]) / key_len - 1)
|
|
|
|
bounds[MAP_MAXX] = max(bounds[MAP_MAXX], maxx)
|
|
CHECK_TICK
|
|
|
|
// Indicate failure to parse any coordinates by nulling bounds
|
|
if(bounds[1] == 1.#INF)
|
|
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, 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/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/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
|
|
else
|
|
while (zcrd > world.maxz) //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")
|
|
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
|
|
|
|
// 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
|
|
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
|
|
|
|
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
|
|
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
|
|
++turfsSkipped
|
|
#endif
|
|
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--
|
|
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
|
|
T.AfterChange(CHANGETURF_IGNORE_AIR)
|
|
|
|
if(has_expanded_world_maxx || has_expanded_world_maxy)
|
|
SEND_GLOBAL_SIGNAL(COMSIG_GLOB_EXPANDED_WORLD_BOUNDS, has_expanded_world_maxx, has_expanded_world_maxy)
|
|
|
|
#ifdef TESTING
|
|
if(turfsSkipped)
|
|
testing("Skipped loading [turfsSkipped] default turfs")
|
|
#endif
|
|
|
|
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]
|
|
// 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
|
|
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, "{")
|
|
|
|
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(!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[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 = default_list
|
|
if(variables_start)//if there's any variable
|
|
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[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. 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(!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/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
|
|
////////////////
|
|
//Instanciation
|
|
////////////////
|
|
|
|
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)
|
|
if(members_attributes[index] != default_list)
|
|
world.preloader_setup(members_attributes[index], members[index])//preloader for assigning set variables on atom creation
|
|
instance = loaded_areas[members[index]]
|
|
if(!instance)
|
|
var/area_type = members[index]
|
|
// If this parsed map doesn't have that area already, we check the global cache
|
|
instance = GLOB.areas_by_type[area_type]
|
|
// If the global list DOESN'T have this area it's either not a unique area, or it just hasn't been created yet
|
|
if (!instance)
|
|
instance = new area_type(null)
|
|
if(!instance)
|
|
CRASH("[area_type] failed to be new'd, what'd you do?")
|
|
loaded_areas[area_type] = instance
|
|
|
|
instance.contents.Add(crds)
|
|
|
|
if(GLOB.use_preloader)
|
|
world.preloader_load(instance)
|
|
|
|
// 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])
|
|
|
|
// 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)
|
|
|
|
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(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
|
|
////////////////
|
|
|
|
/datum/parsed_map/proc/create_atom(path, crds)
|
|
set waitfor = FALSE
|
|
. = new path (crds)
|
|
|
|
//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="\"")
|
|
var/position = initial_position
|
|
var/next_delimiter = findtext(text,delimiter,position,0)
|
|
var/next_opening = findtext(text,opening_escape,position,0)
|
|
|
|
while((next_opening != 0) && (next_opening < next_delimiter))
|
|
position = findtext(text,closing_escape,next_opening + 1,0)+1
|
|
next_delimiter = findtext(text,delimiter,position,0)
|
|
next_opening = findtext(text,opening_escape,position,0)
|
|
|
|
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=",")
|
|
. = list()
|
|
if (!text)
|
|
return
|
|
|
|
// 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))
|
|
|
|
// Associative var, so do the association.
|
|
// Note that numbers cannot be keys - the RHS is dropped if so.
|
|
var/trim_right = TRIM_TEXT(copytext(entry, equal_position + length(entry[equal_position])))
|
|
var/right_constant = parse_constant(trim_right)
|
|
.[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)
|
|
|
|
// 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
|
|
var/num = text2num(text)
|
|
if(isnum(num))
|
|
return num
|
|
|
|
// string
|
|
if(text[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
|
|
return readlist(copytext(text, 6, -1))
|
|
|
|
// typepath
|
|
var/path = text2path(text)
|
|
if(ispath(path))
|
|
return path
|
|
|
|
// file
|
|
if(text[1] == "'")
|
|
return file(copytext_char(text, 2, -1))
|
|
|
|
// null
|
|
if(text == "null")
|
|
return null
|
|
|
|
// not parsed:
|
|
// - pops: /obj{name="foo"}
|
|
// - new(), newlist(), icon(), matrix(), sound()
|
|
|
|
// fallback: string
|
|
return text
|
|
|
|
/datum/parsed_map/Destroy()
|
|
..()
|
|
if(turf_blacklist)
|
|
turf_blacklist.Cut()
|
|
parsed_bounds.Cut()
|
|
bounds.Cut()
|
|
grid_models.Cut()
|
|
gridSets.Cut()
|
|
return QDEL_HINT_HARDDEL_NOW
|