mirror of
https://github.com/ParadiseSS13/Paradise.git
synced 2026-01-11 10:02:24 +00:00
This converts the machine and mob processes to the SMC. Additionally, it adds the Atom subsystem, which handles all Initialize() calls in place of the old gameticker. Due to incompatibility with our atmospherics (FUCK OUR ATMOSPHERICS FOR FUCKING EVER JESUS CHRIST WHO THE FUCK MADE THIS PIECE OF GODDAMN SHIT) atmospherics machines do not use Initialize() as they should, instead opting for a custom atmos_init proc that the air controller handles.
498 lines
17 KiB
Plaintext
498 lines
17 KiB
Plaintext
///////////////////////////////////////////////////////////////
|
|
//SS13 Optimized Map loader
|
|
//////////////////////////////////////////////////////////////
|
|
|
|
//As of 3.6.2016
|
|
//global datum that will preload variables on atoms instanciation
|
|
var/global/use_preloader = FALSE
|
|
var/global/dmm_suite/preloader/_preloader = new
|
|
|
|
/dmm_suite
|
|
// These regexes are global - meaning that starting the maploader again mid-load will
|
|
// reset progress - which means we need to track our index per-map, or we'll
|
|
// eternally recurse
|
|
// /"([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")
|
|
// /^[\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()
|
|
|
|
/**
|
|
* Construct the model map and control the loading process
|
|
*
|
|
* WORKING :
|
|
*
|
|
* 1) Makes an associative mapping of model_keys with model
|
|
* e.g aa = /turf/unsimulated/wall{icon_state = "rock"}
|
|
* 2) Read the map line by line, parsing the result (using parse_grid)
|
|
*
|
|
* If `measureOnly` is set, then no atoms will be created, and all this will do
|
|
* is return the bounds after parsing the file
|
|
*
|
|
* If you need to freeze init while you're working, you can use the spacial allocator's
|
|
* "add_dirt" and "remove_dirt" which will put initializations on hold until you say
|
|
* the word. This is important for loading large maps such as the cyberiad, where
|
|
* atmos will attempt to start before it's ready, causing runtimes galore if init is
|
|
* allowed to romp unchecked.
|
|
*/
|
|
/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)
|
|
var/tfile = dmm_file//the map file we're creating
|
|
var/fname = "Lambda"
|
|
if(isfile(tfile))
|
|
fname = "[tfile]"
|
|
tfile = file2text(tfile)
|
|
if(length(tfile) == 0)
|
|
throw EXCEPTION("Map path '[fname]' does not exist!")
|
|
|
|
if(!x_offset)
|
|
x_offset = 1
|
|
if(!y_offset)
|
|
y_offset = 1
|
|
if(!z_offset)
|
|
z_offset = world.maxz + 1
|
|
|
|
var/list/bounds = list(1.#INF, 1.#INF, 1.#INF, -1.#INF, -1.#INF, -1.#INF)
|
|
var/list/grid_models = list()
|
|
var/key_len = 0
|
|
|
|
|
|
|
|
var/dmm_suite/loaded_map/LM = new
|
|
// This try-catch is used as a budget "Finally" clause, as the dirt count
|
|
// needs to be reset
|
|
var/watch = start_watch()
|
|
log_debug("[measureOnly ? "Measuring" : "Loading"] map: [fname]")
|
|
try
|
|
LM.index = 1
|
|
while(dmmRegex.Find(tfile, LM.index))
|
|
LM.index = dmmRegex.next
|
|
|
|
// "aa" = (/type{vars=blah})
|
|
if(dmmRegex.group[1]) // Model
|
|
var/key = dmmRegex.group[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")
|
|
if(!measureOnly)
|
|
grid_models[key] = dmmRegex.group[2]
|
|
|
|
// (1,1,1) = {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}
|
|
else if(dmmRegex.group[3]) // Coords
|
|
if(!key_len)
|
|
throw EXCEPTION("Coords before model definition in DMM")
|
|
|
|
var/xcrdStart = text2num(dmmRegex.group[3]) + 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
|
|
|
|
if(!measureOnly)
|
|
if(zcrd > world.maxz)
|
|
if(cropMap)
|
|
continue
|
|
else
|
|
space_manager.increase_max_zlevel_to(zcrd) //create a new z_level if needed
|
|
|
|
bounds[MAP_MINX] = min(bounds[MAP_MINX], xcrdStart)
|
|
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/leadingBlanks = 0
|
|
while(leadingBlanks < gridLines.len && gridLines[++leadingBlanks] == "")
|
|
if(leadingBlanks > 1)
|
|
gridLines.Cut(1, leadingBlanks) // Remove all leading blank lines.
|
|
|
|
if(!gridLines.len) // 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.
|
|
|
|
bounds[MAP_MINY] = min(bounds[MAP_MINY], ycrd)
|
|
ycrd += gridLines.len - 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
|
|
bounds[MAP_MAXY] = max(bounds[MAP_MAXY], ycrd)
|
|
else
|
|
bounds[MAP_MAXY] = max(bounds[MAP_MAXY], min(ycrd, world.maxy))
|
|
|
|
var/maxx = xcrdStart
|
|
log_debug("[xcrdStart]")
|
|
if(measureOnly)
|
|
for(var/line in gridLines)
|
|
maxx = max(maxx, xcrdStart + length(line) / key_len - 1)
|
|
else
|
|
for(var/line in gridLines)
|
|
if(ycrd <= world.maxy && ycrd >= 1)
|
|
xcrd = xcrdStart
|
|
for(var/tpos = 1 to length(line) - key_len + 1 step key_len)
|
|
if(xcrd > world.maxx)
|
|
if(cropMap)
|
|
break
|
|
else
|
|
world.maxx = xcrd
|
|
|
|
if(xcrd >= 1)
|
|
var/model_key = copytext(line, tpos, tpos + key_len)
|
|
if(!grid_models[model_key])
|
|
throw EXCEPTION("Undefined model key in DMM: [model_key]. Map file: [fname].")
|
|
parse_grid(grid_models[model_key], xcrd, ycrd, zcrd, LM)
|
|
// After this call, it is NOT safe to reference `dmmRegex` without another call to
|
|
// "Find" - we might've hit a map loader here and changed its state
|
|
CHECK_TICK
|
|
|
|
maxx = max(maxx, xcrd)
|
|
++xcrd
|
|
--ycrd
|
|
bounds[MAP_MAXX] = max(bounds[MAP_MAXX], cropMap ? min(maxx, world.maxx) : maxx)
|
|
|
|
CHECK_TICK
|
|
catch(var/exception/e)
|
|
_preloader.reset()
|
|
throw e
|
|
|
|
_preloader.reset()
|
|
log_debug("Loaded map in [stop_watch(watch)]s.")
|
|
qdel(LM)
|
|
if(bounds[MAP_MINX] == 1.#INF) // Shouldn't need to check every item
|
|
log_runtime(EXCEPTION("Bad Map bounds in [fname]"), src, list(
|
|
"Min x: [bounds[MAP_MINX]]",
|
|
"Min y: [bounds[MAP_MINY]]",
|
|
"Min z: [bounds[MAP_MINZ]]",
|
|
"Max x: [bounds[MAP_MAXX]]",
|
|
"Max y: [bounds[MAP_MAXY]]",
|
|
"Max z: [bounds[MAP_MAXZ]]"))
|
|
return null
|
|
else
|
|
if(!measureOnly)
|
|
for(var/t in block(locate(bounds[MAP_MINX], bounds[MAP_MINY], bounds[MAP_MINZ]), locate(bounds[MAP_MAXX], bounds[MAP_MAXY], bounds[MAP_MAXZ])))
|
|
var/turf/T = t
|
|
//we do this after we load everything in. if we don't; we'll have weird atmos bugs regarding atmos adjacent turfs
|
|
T.AfterChange(1,keep_cabling = TRUE)
|
|
return bounds
|
|
|
|
/**
|
|
* 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/dangerous/explored)
|
|
*
|
|
* WORKING :
|
|
*
|
|
* 1) Read the model string, member by member (delimiter is ',')
|
|
*
|
|
* 2) Get the path of the atom and store it into a list
|
|
*
|
|
* 3) a) Check if the member has variables (text within '{' and '}')
|
|
*
|
|
* 3) b) Construct an associative list with found variables, if any (the atom index in members is the same as its variables in members_attributes)
|
|
*
|
|
* 4) Instanciates the atom with its variables
|
|
*
|
|
*/
|
|
/dmm_suite/proc/parse_grid(model as text,xcrd as num,ycrd as num,zcrd as num, dmm_suite/loaded_map/LM)
|
|
/*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.
|
|
*/
|
|
|
|
var/list/members //will contain all members (paths) in model (in our example : /turf/unsimulated/wall and /area/mine/dangerous/explored)
|
|
var/list/members_attributes //will contain lists filled with corresponding variables, if any (in our example : list(icon_state = "rock") and list())
|
|
var/list/cached = modelCache[model]
|
|
var/index
|
|
|
|
if(cached)
|
|
members = cached[1]
|
|
members_attributes = cached[2]
|
|
else
|
|
/////////////////////////////////////////////////////////
|
|
//Constructing members and corresponding variables lists
|
|
////////////////////////////////////////////////////////
|
|
|
|
members = list()
|
|
members_attributes = list()
|
|
index = 1
|
|
|
|
var/old_position = 1
|
|
var/dpos
|
|
|
|
do
|
|
//finding next member (e.g /turf/unsimulated/wall{icon_state = "rock"} or /area/mine/dangerous/explored)
|
|
dpos = find_next_delimiter_position(model, old_position, ",", "{", "}") //find next delimiter (comma here) that's not within {...}
|
|
|
|
var/full_def = trim_text(copytext(model, old_position, dpos)) //full definition, e.g : /obj/foo/bar{variables=derp}
|
|
var/variables_start = findtext(full_def, "{")
|
|
var/atom_text = trim_text(copytext(full_def, 1, variables_start))
|
|
var/atom_def = text2path(atom_text) //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!
|
|
log_runtime(EXCEPTION("Bad path: [atom_text]"), src, list("Source String: [model]", "dpos: [dpos]"))
|
|
continue
|
|
members.Add(atom_def)
|
|
|
|
//transform the variables in text format into a list (e.g {var1="derp"; var2; var3=7} => list(var1="derp", var2, var3=7))
|
|
var/list/fields = list()
|
|
|
|
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, ";")
|
|
|
|
//then fill the members_attributes list with the corresponding variables
|
|
members_attributes.len++
|
|
members_attributes[index++] = fields
|
|
|
|
CHECK_TICK
|
|
while(dpos != 0)
|
|
|
|
modelCache[model] = list(members, members_attributes)
|
|
|
|
|
|
////////////////
|
|
//Instanciation
|
|
////////////////
|
|
|
|
//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/turf/crds = locate(xcrd,ycrd,zcrd)
|
|
if(members[index] != /area/template_noop)
|
|
// We assume `members[index]` is an area path, as above, yes? I will operate
|
|
// on that assumption.
|
|
if(!ispath(members[index], /area))
|
|
throw EXCEPTION("Oh no, I thought this was an area!")
|
|
|
|
var/atom/instance
|
|
_preloader.setup(members_attributes[index])//preloader for assigning set variables on atom creation
|
|
instance = LM.area_path_to_real_area(members[index])
|
|
|
|
if(crds)
|
|
instance.contents.Add(crds)
|
|
|
|
if(use_preloader && instance)
|
|
_preloader.load(instance)
|
|
|
|
//then instance the /turf and, if multiple tiles are presents, simulates the DMM underlays piling effect
|
|
|
|
var/first_turf_index = 1
|
|
while(!ispath(members[first_turf_index],/turf)) //find first /turf object in members
|
|
first_turf_index++
|
|
|
|
//instanciate the first /turf
|
|
var/turf/T
|
|
if(members[first_turf_index] != /turf/template_noop)
|
|
T = instance_atom(members[first_turf_index],members_attributes[first_turf_index],xcrd,ycrd,zcrd)
|
|
|
|
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
|
|
if(istype(T, /turf)) // I blame this on the stupid clown who coded the BYOND map editor
|
|
underlay = T.appearance
|
|
T = instance_atom(members[index],members_attributes[index],xcrd,ycrd,zcrd)//instance new turf
|
|
if(ispath(members[index],/turf))
|
|
T.underlays += underlay
|
|
|
|
index++
|
|
|
|
//finally instance all remainings objects/mobs
|
|
for(index in 1 to first_turf_index-1)
|
|
instance_atom(members[index],members_attributes[index],xcrd,ycrd,zcrd)
|
|
CHECK_TICK
|
|
|
|
////////////////
|
|
//Helpers procs
|
|
////////////////
|
|
|
|
//Instance an atom at (x,y,z) and gives it the variables in attributes
|
|
/dmm_suite/proc/instance_atom(path,list/attributes, x, y, z)
|
|
var/atom/instance
|
|
_preloader.setup(attributes, path)
|
|
|
|
var/turf/T = locate(x,y,z)
|
|
if(T)
|
|
if(ispath(path, /turf))
|
|
T.ChangeTurf(path, 1, 0)
|
|
instance = T
|
|
else if(ispath(path, /area))
|
|
|
|
else
|
|
instance = new path (T)//first preloader pass
|
|
|
|
if(use_preloader && instance)//second preloader pass, for those atoms that don't ..() in New()
|
|
_preloader.load(instance)
|
|
|
|
return instance
|
|
|
|
//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, "")
|
|
|
|
|
|
//find the position of the next delimiter,skipping whatever is comprised between opening_escape and closing_escape
|
|
//returns 0 if reached the last delimiter
|
|
/dmm_suite/proc/find_next_delimiter_position(text as text,initial_position as num, delimiter=",",opening_escape=quote,closing_escape=quote)
|
|
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
|
|
/dmm_suite/proc/readlist(text as text, delimiter=",")
|
|
|
|
var/list/to_return = list()
|
|
|
|
var/position
|
|
var/old_position = 1
|
|
|
|
do
|
|
//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)),1)//the name of the variable, must trim quotes to build a BYOND compliant associatives list
|
|
old_position = position + 1
|
|
|
|
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
|
|
|
|
//Check for string
|
|
// Make it read to the next delimiter, instead of the quote
|
|
if(findtext(trim_right,quote,1,2))
|
|
var/endquote = findtext(trim_right,quote,-1)
|
|
if(!endquote)
|
|
log_runtime(EXCEPTION("Terminating quote not found!"), src)
|
|
// Our map writer escapes quotes and curly brackets to avoid
|
|
// letting our simple parser choke on meanly-crafted names/etc
|
|
// - so we decode it here so it's back to good ol' legibility
|
|
trim_right = dmm_decode(copytext(trim_right,2,endquote))
|
|
|
|
//Check for number
|
|
else if(isnum(text2num(trim_right)))
|
|
trim_right = text2num(trim_right)
|
|
|
|
//Check for null
|
|
else if(trim_right == "null")
|
|
trim_right = null
|
|
|
|
//Check for list
|
|
else if(copytext(trim_right,1,5) == "list")
|
|
trim_right = readlist(copytext(trim_right,6,length(trim_right)))
|
|
|
|
//Check for file
|
|
else if(copytext(trim_right,1,2) == "'")
|
|
trim_right = file(copytext(trim_right,2,length(trim_right)))
|
|
|
|
//Check for path
|
|
else if(ispath(text2path(trim_right)))
|
|
trim_right = text2path(trim_right)
|
|
|
|
to_return[trim_left] = trim_right
|
|
|
|
else//simple var
|
|
to_return[trim_left] = null
|
|
|
|
while(position != 0)
|
|
|
|
return to_return
|
|
|
|
/dmm_suite/Destroy()
|
|
..()
|
|
return QDEL_HINT_HARDDEL_NOW
|
|
|
|
//////////////////
|
|
//Preloader datum
|
|
//////////////////
|
|
|
|
// This ain't re-entrant, but we had this before the maploader update
|
|
/dmm_suite/preloader
|
|
parent_type = /datum
|
|
var/list/attributes
|
|
var/target_path
|
|
var/json_ready = 0
|
|
|
|
/dmm_suite/preloader/proc/setup(list/the_attributes, path)
|
|
if(the_attributes.len)
|
|
json_ready = 0
|
|
if("map_json_data" in the_attributes)
|
|
json_ready = 1
|
|
use_preloader = TRUE
|
|
attributes = the_attributes
|
|
target_path = path
|
|
|
|
/dmm_suite/preloader/proc/load(atom/what)
|
|
if(json_ready)
|
|
var/json_data = attributes["map_json_data"]
|
|
attributes -= "map_json_data"
|
|
json_data = dmm_decode(json_data)
|
|
try
|
|
what.deserialize(json_decode(json_data))
|
|
catch(var/exception/e)
|
|
log_runtime(EXCEPTION("Bad json data: '[json_data]'"), src)
|
|
throw e
|
|
for(var/attribute in attributes)
|
|
var/value = attributes[attribute]
|
|
if(islist(value))
|
|
value = deepCopyList(value)
|
|
what.vars[attribute] = value
|
|
use_preloader = FALSE
|
|
|
|
// If the map loader fails, make this safe
|
|
/dmm_suite/preloader/proc/reset()
|
|
use_preloader = FALSE
|
|
attributes = list()
|
|
target_path = null
|
|
|
|
// A datum for use within the context of loading a single map,
|
|
// so that one can have separate "unpowered" areas for ruins or whatever,
|
|
// yet have a single area type for use of mapping, instead of creating
|
|
// a new area type for each new ruin
|
|
/dmm_suite/loaded_map
|
|
parent_type = /datum
|
|
var/list/area_list = list()
|
|
var/index = 1 // To store the state of the regex
|
|
|
|
/dmm_suite/loaded_map/proc/area_path_to_real_area(area/A)
|
|
if(!ispath(A, /area))
|
|
throw EXCEPTION("Wrong argument to `area_path_to_real_area`")
|
|
|
|
if(!(A in area_list))
|
|
if(initial(A.there_can_be_many))
|
|
area_list[A] = new A
|
|
else
|
|
area_list[A] = locate(A)
|
|
|
|
return area_list[A]
|
|
|
|
/area/template_noop
|
|
name = "Area Passthrough"
|
|
|
|
/turf/template_noop
|
|
name = "Turf Passthrough"
|