mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-31 12:01:47 +00:00
* '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
629 lines
24 KiB
Plaintext
629 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()
|
|
|
|
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
|
|
var/static/list/area_cache = GLOB.areas_by_type
|
|
////////////////
|
|
//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 = area_cache[members[index]]
|
|
if (!instance)
|
|
// 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
|
|
|
|
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
|