Runtime map now loads in ~11 seconds instead of ~40, sped up various other things (#19957)

Runtime map now has a bunch of new areas / items with often-tested
stuffs, and some hard-to-put-at-runtime stuffs.
Runtime map jobs now are positioned to make it faster to reach the
aforementioned often-tested stuffs.
Runtime map doesn't generate an overmap anymore by default, which speeds
up the process.
Runtime map now loads in ~11 seconds instead of ~40 seconds as it was
before.
Updated the maploader to be faster in parsing maps.
Bapi is not engaged anymore if we're only measuring the map size, which
speeds up the process.
In fastboot we do not generate the codexes anymore, which speeds up the
process.
In fastboot and if exoplanets and away sites are not enabled, we do not
parse the map templates anymore, which speeds up the process.
Updated the icon smoothing to be faster.
Optimized cargo area code.
Other optimizations.
This commit is contained in:
Fluffy
2024-10-06 23:31:01 +02:00
committed by GitHub
parent 9636363e60
commit b8902e2e16
124 changed files with 1238 additions and 134598 deletions
+147 -129
View File
@@ -12,16 +12,16 @@ GLOBAL_DATUM_INIT(_preloader, /dmm_suite/preloader, new)
/dmm_suite
// /"([a-zA-Z]+)" = \(((?:.|\n)*?)\)\n(?!\t)|\((\d+),(\d+),(\d+)\) = \{"([a-zA-Z\n]*)"\}/g
var/static/regex/dmmRegex = new/regex({""(\[a-zA-Z]+)" = \\(((?:.|\n)*?)\\)\n(?!\t)|\\((\\d+),(\\d+),(\\d+)\\) = \\{"(\[a-zA-Z\n]*)"\\}"}, "g")
var/static/regex/dmmRegex = new(@'"([a-zA-Z]+)" = (?:\(\n|\()((?:.|\n)*?)\)\n(?!\t)|\((\d+),(\d+),(\d+)\) = \{"([a-zA-Z\n]*)"\}', "g")
// /^[\s\n]+"?|"?[\s\n]+$|^"|"$/g
var/static/regex/trimQuotesRegex = new/regex({"^\[\\s\n]+"?|"?\[\\s\n]+$|^"|"$"}, "g")
// /^[\s\n]+|[\s\n]+$/
var/static/regex/trimRegex = new/regex("^\[\\s\n]+|\[\\s\n]+$", "g")
var/static/list/modelCache = list()
var/static/space_key
#ifdef TESTING
var/static/turfsSkipped
#endif
//text trimming (both directions) helper macro
#define TRIM_TEXT(text) (replacetext_char(text, trimRegex, ""))
/**
* Construct the model map and control the loading process
@@ -33,34 +33,42 @@ GLOBAL_DATUM_INIT(_preloader, /dmm_suite/preloader, new)
* 2) Read the map line by line, parsing the result (using parse_grid)
*
*/
// dmm_files: A list of .dmm files to load (Required).
// z_offset: A number representing the z-level on which to start loading 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/AfterChange won't be called on loaded turfs
/dmm_suite/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, lower_crop_x as num, lower_crop_y as num, upper_crop_x as num, upper_crop_y as num)
//How I wish for RAII
Master.StartLoadingMap()
space_key = null
#ifdef TESTING
turfsSkipped = 0
#endif
. = load_map_impl(dmm_file, x_offset, y_offset, z_offset, cropMap, measureOnly, no_changeturf, lower_crop_x, upper_crop_x, lower_crop_y, upper_crop_y)
#ifdef TESTING
if(turfsSkipped)
testing("Skipped loading [turfsSkipped] default turfs")
#endif
Master.StopLoadingMap()
/dmm_suite/proc/load_map_impl(dmm_file, x_offset, y_offset, z_offset, cropMap, measureOnly, no_changeturf, x_lower = -INFINITY, x_upper = INFINITY, y_lower = -INFINITY, y_upper = INFINITY)
var/tfile = dmm_file//the map file we're creating
/*#### WARNING AURORA SNOWFLAKE SECTION ####*/
if(isfile(tfile))
// name/path of dmm file, new var so as to not rename the `tfile` var
// to maybe maintain compatibility with other codebases
var/tfilepath = "[tfile]"
tfile = null
// use bapi to read, parse, process, mapmanip etc
// this will "crash"/stacktrace on fail
tfile = bapi_read_dmm_file(tfilepath)
// if bapi for whatever reason fails and returns null
// Except when measuring; i don't give a shit about bapi when measuring the map size,
// if you're changing the map size from bapi you're stupid and deserve it not to work,
// and i'm not slowing down the whole initialization by a third just for this possibility
if(!measureOnly)
tfile = bapi_read_dmm_file(tfilepath)
// if bapi for whatever reason fails and returns null, or we're measuring
// try to load it the old dm way instead
if(!tfile)
tfile = file2text(tfilepath)
/*#### END AURORA SNOWFLAKE SECTION ####*/
if(!x_offset)
x_offset = 1
if(!y_offset)
@@ -73,32 +81,36 @@ GLOBAL_DATUM_INIT(_preloader, /dmm_suite/preloader, new)
var/key_len = 0
var/stored_index = 1
var/list/atoms_to_initialise = list()
var/has_expanded_world_maxx = FALSE
var/has_expanded_world_maxy = FALSE
while(dmmRegex.Find(tfile, stored_index))
var/list/regexOutput
while(findtext(tfile, dmmRegex, 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))
if(!key_len)
key_len = length(key)
else
throw EXCEPTION("Inconsistant key length in DMM")
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)
throw EXCEPTION("Coords before model definition in DMM")
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
@@ -106,11 +118,11 @@ GLOBAL_DATUM_INIT(_preloader, /dmm_suite/preloader, new)
var/xcrdStart = curr_x + x_offset - 1
//position of the currently processed square
var/xcrd
var/ycrd = text2num(dmmRegex.group[4]) + y_offset - 1
var/zcrd = text2num(dmmRegex.group[5]) + z_offset - 1
var/ycrd = text2num(regexOutput[4]) + y_offset - 1
var/zcrd = text2num(regexOutput[5]) + z_offset - 1
var/zexpansion = zcrd > world.maxz
if(zexpansion && !measureOnly) // don't actually expand the world if we're only measuring bounds
if(zexpansion && !measureOnly) // don't actually expand the world if we're only measuring bounds
if(cropMap)
continue
else
@@ -119,39 +131,42 @@ GLOBAL_DATUM_INIT(_preloader, /dmm_suite/preloader, new)
if(!no_changeturf)
WARNING("Z-level expansion occurred without no_changeturf set, this may cause problems when /turf/post_change is called.")
bounds[MAP_MINX] = min(bounds[MAP_MINX], Clamp(xcrdStart, x_lower, x_upper))
bounds[MAP_MINX] = min(bounds[MAP_MINX], clamp(xcrdStart, x_lower, x_upper))
bounds[MAP_MINZ] = min(bounds[MAP_MINZ], zcrd)
bounds[MAP_MAXZ] = max(bounds[MAP_MAXZ], zcrd)
var/list/gridLines = splittext(dmmRegex.group[6], "\n")
var/list/gridLines = splittext(regexOutput[6], "\n")
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
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(ycrd, y_lower, y_upper))
ycrd += gridLines.len - 1 // Start at the top and work down
bounds[MAP_MINY] = min(bounds[MAP_MINY], clamp(ycrd, y_lower, y_upper))
ycrd += length(gridLines) - 1 // Start at the top and work down
if(!cropMap && ycrd > world.maxy)
if(!measureOnly)
world.maxy = ycrd // Expand Y here. X is expanded in the loop below
has_expanded_world_maxy = TRUE
bounds[MAP_MAXY] = max(bounds[MAP_MAXY], Clamp(ycrd, y_lower, y_upper))
bounds[MAP_MAXY] = max(bounds[MAP_MAXY], clamp(ycrd, y_lower, y_upper))
else
bounds[MAP_MAXY] = max(bounds[MAP_MAXY], Clamp(min(ycrd, world.maxy), y_lower, y_upper))
bounds[MAP_MAXY] = max(bounds[MAP_MAXY], clamp(min(ycrd, world.maxy), y_lower, y_upper))
var/maxx = xcrdStart
if(measureOnly)
for(var/line in gridLines)
maxx = max(maxx, xcrdStart + length(line) / key_len - 1)
else
//turn off base new Initialization until the whole thing is loaded
SSatoms.map_loader_begin(REF(src))
for(var/line in 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
@@ -174,20 +189,20 @@ GLOBAL_DATUM_INIT(_preloader, /dmm_suite/preloader, new)
var/no_afterchange = no_changeturf || zexpansion
if(!no_afterchange || (model_key != space_key))
if(!grid_models[model_key])
throw EXCEPTION("Undefined model key in DMM.")
CRASH("Undefined model key in DMM.")
var/datum/grid_load_metadata/M = parse_grid(grid_models[model_key], model_key, xcrd, ycrd, zcrd, no_changeturf || zexpansion)
if (M)
atoms_to_initialise += M.atoms_to_initialise
#ifdef TESTING
else
++turfsSkipped
#endif
CHECK_TICK
// CHECK_TICK
maxx = max(maxx, xcrd)
++xcrd
--ycrd
bounds[MAP_MAXX] = Clamp(max(bounds[MAP_MAXX], cropMap ? min(maxx, world.maxx) : maxx), x_lower, x_upper)
//Restore initialization to the previous value
SSatoms.map_loader_stop(REF(src))
bounds[MAP_MAXX] = clamp(max(bounds[MAP_MAXX], cropMap ? min(maxx, world.maxx) : maxx), x_lower, x_upper)
CHECK_TICK
@@ -208,11 +223,6 @@ GLOBAL_DATUM_INIT(_preloader, /dmm_suite/preloader, new)
M.atoms_to_initialise = atoms_to_initialise
return M
/datum/grid_load_metadata
var/list/atoms_to_initialise
var/list/atoms_to_delete
/**
* Fill a given tile with its area/turf/objects/mobs
* Variable model is one full map line (e.g /turf/unsimulated/wall{icon_state = "rock"}, /area/mine/explored)
@@ -230,7 +240,15 @@ GLOBAL_DATUM_INIT(_preloader, /dmm_suite/preloader, new)
* 4) Instanciates the atom with its variables
*
*/
/datum/grid_load_metadata
var/list/atoms_to_initialise
var/list/atoms_to_delete
/dmm_suite/proc/parse_grid(model as text, model_key as text, xcrd as num,ycrd as num,zcrd as num, no_changeturf as num)
//This should only ever be called by load_map_impl() after announcing the map is being loaded to SSatoms
PRIVATE_PROC(TRUE)
/*Method parse_grid()
- Accepts a text string containing a comma separated list of type paths of the
same construction as those contained in a .dmm file, and instantiates them.
@@ -260,16 +278,19 @@ GLOBAL_DATUM_INIT(_preloader, /dmm_suite/preloader, new)
//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/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_str = trim_text(copytext(full_def, 1, variables_start))
var/atom_def = text2path(path_str) //path definition, e.g /obj/foo/bar
var/atom_def = text2path(TRIM_TEXT(copytext(full_def, 1, variables_start))) //path definition, e.g /obj/foo/bar
old_position = dpos + 1
if(!atom_def) // Skip the item if the path does not exist. Fix your crap, mappers!
crash_with("Invalid type in map. [path_str]")
continue
#ifdef UNIT_TEST
log_error("Couldn't find atom path specified in map: [full_def]")
#endif
if (dpos == 0)
break
else
continue
members += atom_def
@@ -279,8 +300,8 @@ GLOBAL_DATUM_INIT(_preloader, /dmm_suite/preloader, new)
if(variables_start)//if there's any variable
full_def = copytext(full_def,variables_start+1,length(full_def))//removing the last '}'
fields = readlist(full_def, ";")
if(fields.len)
if(!trim(fields[fields.len]))
if(length(fields))
if(!trimtext(fields[length(fields)]))
--fields.len
for(var/I in fields)
var/value = fields[I]
@@ -303,7 +324,7 @@ GLOBAL_DATUM_INIT(_preloader, /dmm_suite/preloader, new)
// 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 && 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))
if(no_changeturf && !space_key && length(members) == 2 && length(members_attributes) == 2 && length(members_attributes[1]) == 0 && length(members_attributes[2]) == 0 && (world.area in members) && (world.turf in members))
space_key = model_key
return
@@ -315,14 +336,12 @@ GLOBAL_DATUM_INIT(_preloader, /dmm_suite/preloader, new)
//since we've switched off autoinitialisation, record atoms to initialise later
var/list/atoms_to_initialise = list()
//turn off base new Initialization until the whole thing is loaded
SSatoms.map_loader_begin(text_ref(src))
//The next part of the code assumes there's ALWAYS an /area AND a /turf on a given tile
var/turf/crds = locate(xcrd,ycrd,zcrd)
//first instance the /area and remove it from the members list
index = members.len
index = length(members)
if(members[index] != /area/template_noop)
var/atype = members[index]
var/atom/instance = GLOB.areas_by_type[atype]
@@ -353,18 +372,14 @@ GLOBAL_DATUM_INIT(_preloader, /dmm_suite/preloader, new)
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
while(index <= length(members) - 1) // Last item is an /area
T = instance_atom(members[index],members_attributes[index],crds,no_changeturf)//instance new turf
T.underlays += underlay
index++
atoms_to_initialise += T
//finally instance all remainings objects/mobs
for(index in 1 to first_turf_index-1)
atoms_to_initialise += instance_atom(members[index],members_attributes[index],crds,no_changeturf)
//Restore initialization to the previous value
SSatoms.map_loader_stop(text_ref(src))
var/datum/grid_load_metadata/M = new
M.atoms_to_initialise = atoms_to_initialise
@@ -390,22 +405,13 @@ GLOBAL_DATUM_INIT(_preloader, /dmm_suite/preloader, new)
//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(text_ref(src))
SSatoms.map_loader_stop(REF(src))
stoplag()
SSatoms.map_loader_begin(text_ref(src))
SSatoms.map_loader_begin(REF(src))
/dmm_suite/proc/create_atom(path, crds)
// Doing this async is impossible, as we must return the ref.
return new path (crds)
//text trimming (both directions) helper proc
//optionally removes quotes before and after the text (for variable name)
/dmm_suite/proc/trim_text(what as text,trim_quotes=0)
if(trim_quotes)
return trimQuotesRegex.Replace(what, "")
else
return trimRegex.Replace(what, "")
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
@@ -421,70 +427,73 @@ GLOBAL_DATUM_INIT(_preloader, /dmm_suite/preloader, new)
return next_delimiter
/dmm_suite/proc/readlistitem(text as text, is_key = FALSE)
//Check for string
if(findtext(text,"\"",1,2))
. = copytext(text,2,findtext(text,"\"",3,0))
//Check for number
// Keys cannot safely be numbers. This implementation will return null if
// an assoc key is a number.
else if(!is_key && isnum(text2num(text)))
. = text2num(text)
//Check for null
else if(text == "null")
. = null
//Check for list
else if(copytext(text,1,6) == "list(")
. = readlist(copytext(text,6,length(text)))
//Check for file
else if(copytext(text,1,2) == "'")
. = file(copytext(text,2,length(text)))
//Check for path
else if(ispath(text2path(text)))
. = text2path(text)
// Associative keys are fed in without quotation marks.
// So if none of the other cases apply, return simply the string that was given.
// This case is also triggered for item values. So I guess we're also looking for text.
else if(is_key || istext(text))
. = text
//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
/dmm_suite/proc/readlist(text as text, delimiter=",")
var/list/to_return = list()
. = list()
if (!text)
return
var/position
var/old_position = 1
var/list_index = 1
do
//find next delimiter that is not within "..."
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))
// 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
var/trim_left = trim_text(copytext(text,old_position,(equal_position ? equal_position : position)),1)//the name of the variable, must trim quotes to build a BYOND compliant associatives list
old_position = position + 1
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)
if(equal_position) //associative var, so do the association
var/trim_right = trim_text(copytext(text,equal_position+1,position))//the content of the variable
trim_left = readlistitem(trim_left, TRUE) // Assoc vars can be anything that isn't a num!
to_return[trim_left] = readlistitem(trim_right)
list_index++
else if (length(trim_left)) //simple var
to_return.len++
to_return[list_index++] = readlistitem(trim_left)
/dmm_suite/proc/parse_constant(text)
// number
var/num = text2num(text)
if(isnum(num))
return num
while(position != 0)
// 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)
return to_return
// 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
/dmm_suite/Destroy()
..()
@@ -507,12 +516,19 @@ GLOBAL_LIST_INIT(_preloader_path, null)
GLOB._preloader_path = path
/dmm_suite/preloader/proc/load(atom/what)
GLOB.use_preloader = FALSE
var/list/attributes = src.attributes
for(var/attribute in attributes)
var/value = attributes[attribute]
if(islist(value))
value = deepCopyList(value)
value = deep_copy_list(value)
#ifdef TESTING
if(what.vars[attribute] == value)
var/message = "<font color=green>[what.type]</font> at [AREACOORD(what)] - <b>VAR:</b> <font color=red>[attribute] = [isnull(value) ? "null" : (isnum(value) ? value : "\"[value]\"")]</font>"
log_mapping("DIRTY VAR: [message]")
GLOB.dirty_vars += message
#endif
what.vars[attribute] = value
GLOB.use_preloader = FALSE
/area/template_noop
name = "Area Passthrough"
@@ -521,3 +537,5 @@ GLOBAL_LIST_INIT(_preloader_path, null)
/turf/template_noop
name = "Turf Passthrough"
icon_state = "noop"
#undef TRIM_TEXT