Merges map templates and map loader (#24140)

* Reorganize the mapping code

* Finish this up

* Reee line endings

* FUCKING LINE ENDINGS

* LINARU ENDARU

* >PLS SET REPO LINE ENDINGS

* Comments

* Hoisted by my own travis.yml changes
This commit is contained in:
Cyberboss
2017-02-18 22:55:02 -05:00
committed by oranges
parent 2d424cf6be
commit 1365ab99d8
18 changed files with 310 additions and 138 deletions

View File

@@ -0,0 +1,73 @@
dmm_suite{
/*
dmm_suite version 1.0
Released January 30th, 2011.
defines the object /dmm_suite
- Provides the proc load_map()
- Loads the specified map file onto the specified z-level.
- provides the proc write_map()
- Returns a text string of the map in dmm format
ready for output to a file.
- provides the proc save_map()
- Returns a .dmm file if map is saved
- Returns FALSE if map fails to save
The dmm_suite provides saving and loading of map files in BYOND's native DMM map
format. It approximates the map saving and loading processes of the Dream Maker
and Dream Seeker programs so as to allow editing, saving, and loading of maps at
runtime.
------------------------
To save a map at runtime, create an instance of /dmm_suite, and then call
write_map(), which accepts three arguments:
- A turf representing one corner of a three dimensional grid (Required).
- Another turf representing the other corner of the same grid (Required).
- Any, or a combination, of several bit flags (Optional, see documentation).
The order in which the turfs are supplied does not matter, the /dmm_writer will
determine the grid containing both, in much the same way as DM's block() function.
write_map() will then return a string representing the saved map in dmm format;
this string can then be saved to a file, or used for any other purose.
------------------------
To load a map at runtime, create an instance of /dmm_suite, and then call load_map(),
which accepts two arguments:
- A .dmm file to load (Required).
- A number representing the z-level on which to start loading the map (Optional).
The /dmm_suite will load the map file starting on the specified z-level. If no
z-level was specified, world.maxz will be increased so as to fit the map. Note
that if you wish to load a map onto a z-level that already has objects on it,
you will have to handle the removal of those objects. Otherwise the new map will
simply load the new objects on top of the old ones.
Also note that all type paths specified in the .dmm file must exist in the world's
code, and that the /dmm_reader trusts that files to be loaded are in fact valid
.dmm files. Errors in the .dmm format will cause runtime errors.
*/
verb/load_map(var/dmm_file as file, var/x_offset as num, var/y_offset as num, var/z_offset as num, var/cropMap as num, var/measureOnly as num){
// dmm_file: A .dmm file 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).
}
verb/write_map(var/turf/t1 as turf, var/turf/t2 as turf, var/flags as num){
// t1: A turf representing one corner of a three dimensional grid (Required).
// t2: Another turf representing the other corner of the same grid (Required).
// flags: Any, or a combination, of several bit flags (Optional, see documentation).
}
// save_map is included as a legacy proc. Use write_map instead.
verb/save_map(var/turf/t1 as turf, var/turf/t2 as turf, var/map_name as text, var/flags as num){
// t1: A turf representing one corner of a three dimensional grid (Required).
// t2: Another turf representing the other corner of the same grid (Required).
// map_name: A valid name for the map to be saved, such as "castle" (Required).
// flags: Any, or a combination, of several bit flags (Optional, see documentation).
}
}

View File

@@ -0,0 +1,100 @@
/datum/map_template
var/name = "Default Template Name"
var/width = 0
var/height = 0
var/mappath = null
var/loaded = 0 // Times loaded this round
var/static/dmm_suite/maploader = new
/datum/map_template/New(path = null, rename = null)
if(path)
mappath = path
if(mappath)
preload_size(mappath)
if(rename)
name = rename
/datum/map_template/proc/preload_size(path)
var/bounds = maploader.load_map(file(path), 1, 1, 1, cropMap=FALSE, measureOnly=TRUE)
if(bounds)
width = bounds[MAP_MAXX] // Assumes all templates are rectangular, have a single Z level, and begin at 1,1,1
height = bounds[MAP_MAXY]
return bounds
/datum/map_template/proc/initTemplateBounds(var/list/bounds)
var/list/obj/machinery/atmospherics/atmos_machines = list()
var/list/obj/structure/cable/cables = list()
var/list/atom/atoms = list()
for(var/L in block(locate(bounds[MAP_MINX], bounds[MAP_MINY], bounds[MAP_MINZ]),
locate(bounds[MAP_MAXX], bounds[MAP_MAXY], bounds[MAP_MAXZ])))
var/turf/B = L
atoms += B
for(var/A in B)
atoms += A
if(istype(A,/obj/structure/cable))
cables += A
continue
if(istype(A,/obj/machinery/atmospherics))
atmos_machines += A
SSatoms.InitializeAtoms(atoms)
SSmachine.setup_template_powernets(cables)
SSair.setup_template_machinery(atmos_machines)
/datum/map_template/proc/load_new_z()
var/x = round(world.maxx/2)
var/y = round(world.maxy/2)
var/list/bounds = maploader.load_map(get_file(), x, y)
if(!bounds)
return FALSE
smooth_zlevel(world.maxz)
SortAreas()
//initialize things that are normally initialized after map load
initTemplateBounds(bounds)
log_game("Z-level [name] loaded at at [x],[y],[world.maxz]")
/datum/map_template/proc/load(turf/T, centered = FALSE)
if(centered)
T = locate(T.x - round(width/2) , T.y - round(height/2) , T.z)
if(!T)
return
if(T.x+width > world.maxx)
return
if(T.y+height > world.maxy)
return
var/list/bounds = maploader.load_map(get_file(), T.x, T.y, T.z, cropMap=TRUE)
if(!bounds)
return
//initialize things that are normally initialized after map load
initTemplateBounds(bounds)
log_game("[name] loaded at at [T.x],[T.y],[T.z]")
return TRUE
/datum/map_template/proc/get_file()
if(mappath)
. = file(mappath)
if(!.)
world.log << "The file of [src] ([mappath]) appears to be empty/non-existent."
/datum/map_template/proc/get_affected_turfs(turf/T, centered = FALSE)
var/turf/placement = T
if(centered)
var/turf/corner = locate(placement.x - round(width/2), placement.y - round(height/2), placement.z)
if(corner)
placement = corner
return block(placement, locate(placement.x+width-1, placement.y+height-1, placement.z))
//for your ever biggening badminnery kevinz000
//❤ - Cyberboss
/proc/load_new_z_level(var/file, var/name)
var/datum/map_template/template = new(file, name)
template.load_new_z()

View File

@@ -0,0 +1,411 @@
///////////////////////////////////////////////////////////////
//SS13 Optimized Map loader
//////////////////////////////////////////////////////////////
//global datum that will preload variables on atoms instanciation
var/global/use_preloader = FALSE
var/global/dmm_suite/preloader/_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")
// /^[\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)
*
*/
/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)
//How I wish for RAII
Master.StartLoadingMap()
. = load_map_impl(dmm_file, x_offset, y_offset, z_offset, cropMap, measureOnly)
Master.StopLoadingMap()
/dmm_suite/proc/load_map_impl(dmm_file, x_offset, y_offset, z_offset, cropMap, measureOnly)
var/tfile = dmm_file//the map file we're creating
if(isfile(tfile))
tfile = file2text(tfile)
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/stored_index = 1
while(dmmRegex.Find(tfile, stored_index))
stored_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(zcrd > world.maxz)
if(cropMap)
continue
else
world.maxz = 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
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.")
parse_grid(grid_models[model_key], xcrd, ycrd, zcrd)
CHECK_TICK
maxx = max(maxx, xcrd)
++xcrd
--ycrd
bounds[MAP_MAXX] = max(bounds[MAP_MAXX], cropMap ? min(maxx, world.maxx) : maxx)
CHECK_TICK
if(bounds[1] == 1.#INF) // Shouldn't need to check every item
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(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/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)
/*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/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/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_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!
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
if(members[index] != /area/template_noop)
var/atom/instance
_preloader.setup(members_attributes[index])//preloader for assigning set variables on atom creation
instance = locate(members[index])
var/turf/crds = locate(xcrd,ycrd,zcrd)
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++
//turn off base new Initialization until the whole thing is loaded
SSatoms.map_loader_begin()
//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 = T.appearance
T = instance_atom(members[index],members_attributes[index],xcrd,ycrd,zcrd)//instance new 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)
//custom CHECK_TICK here because we don't want things created while we're sleeping to not initialize
if(world.tick_usage > CURRENT_TICKLIMIT)
SSatoms.map_loader_stop()
stoplag()
SSatoms.map_loader_begin()
//Restore initialization to the previous value
SSatoms.map_loader_stop()
////////////////
//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, TRUE)
instance = T
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
if(findtext(trim_right,quote,1,2))
trim_right = copytext(trim_right,2,findtext(trim_right,quote,3,0))
//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
//////////////////
/dmm_suite/preloader
parent_type = /datum
var/list/attributes
var/target_path
/dmm_suite/preloader/proc/setup(list/the_attributes, path)
if(the_attributes.len)
use_preloader = TRUE
attributes = the_attributes
target_path = path
/dmm_suite/preloader/proc/load(atom/what)
for(var/attribute in attributes)
var/value = attributes[attribute]
if(islist(value))
value = deepCopyList(value)
what.vars[attribute] = value
use_preloader = FALSE
/area/template_noop
name = "Area Passthrough"
/turf/template_noop
name = "Turf Passthrough"

View File

@@ -0,0 +1,93 @@
/proc/seedRuins(list/z_levels = null, budget = 0, whitelist = /area/space, list/potentialRuins)
if(!z_levels || !z_levels.len)
WARNING("No Z levels provided - Not generating ruins")
return
for(var/zl in z_levels)
var/turf/T = locate(1, 1, zl)
if(!T)
WARNING("Z level [zl] does not exist - Not generating ruins")
return
var/overall_sanity = 100
var/list/ruins = potentialRuins.Copy()
while(budget > 0 && overall_sanity > 0)
// Pick a ruin
var/datum/map_template/ruin/ruin = null
if(ruins && ruins.len)
ruin = ruins[pick(ruins)]
else
world.log << "Ruin loader had no ruins to pick from with [budget] left to spend."
break
// Can we afford it
if(ruin.cost > budget)
overall_sanity--
continue
// If so, try to place it
var/sanity = 100
// And if we can't fit it anywhere, give up, try again
while(sanity > 0)
sanity--
var/width_border = TRANSITIONEDGE + round(ruin.width / 2)
var/height_border = TRANSITIONEDGE + round(ruin.height / 2)
var/z_level = pick(z_levels)
var/turf/T = locate(rand(width_border, world.maxx - width_border), rand(height_border, world.maxy - height_border), z_level)
var/valid = TRUE
for(var/turf/check in ruin.get_affected_turfs(T,1))
var/area/new_area = get_area(check)
if(!(istype(new_area, whitelist)))
valid = FALSE
break
if(!valid)
continue
world.log << "Ruin \"[ruin.name]\" placed at ([T.x], [T.y], [T.z])"
var/obj/effect/ruin_loader/R = new /obj/effect/ruin_loader(T)
R.Load(ruins,ruin)
budget -= ruin.cost
if(!ruin.allow_duplicates)
ruins -= ruin.name
break
if(!overall_sanity)
world.log << "Ruin loader gave up with [budget] left to spend."
/obj/effect/ruin_loader
name = "random ruin"
icon = 'icons/obj/weapons.dmi'
icon_state = "syndballoon"
invisibility = 0
/obj/effect/ruin_loader/proc/Load(list/potentialRuins, datum/map_template/template)
var/list/possible_ruins = list()
for(var/A in potentialRuins)
var/datum/map_template/T = potentialRuins[A]
if(!T.loaded)
possible_ruins += T
if(!template && possible_ruins.len)
template = safepick(possible_ruins)
if(!template)
return FALSE
var/turf/central_turf = get_turf(src)
for(var/i in template.get_affected_turfs(central_turf, 1))
var/turf/T = i
for(var/mob/living/simple_animal/monster in T)
qdel(monster)
for(var/obj/structure/flora/ash/plant in T)
qdel(plant)
template.load(central_turf,centered = TRUE)
template.loaded++
var/datum/map_template/ruin = template
if(istype(ruin))
new /obj/effect/landmark/ruin(central_turf, ruin)
qdel(src)
return TRUE

View File

@@ -0,0 +1,681 @@
/*
SwapMaps library by Lummox JR
developed for digitalBYOND
http://www.digitalbyond.org
Version 2.1
The purpose of this library is to make it easy for authors to swap maps
in and out of their game using savefiles. Swapped-out maps can be
transferred between worlds for an MMORPG, sent to the client, etc.
This is facilitated by the use of a special datum and a global list.
Uses of swapmaps:
- Temporary battle arenas
- House interiors
- Individual custom player houses
- Virtually unlimited terrain
- Sharing maps between servers running different instances of the same
game
- Loading and saving pieces of maps for reusable room templates
*/
/*
User Interface:
VARS:
swapmaps_iconcache
An associative list of icon files with names, like
'player.dmi' = "player"
swapmaps_mode
This must be set at runtime, like in world/New().
SWAPMAPS_SAV 0 (default)
Uses .sav files for raw /savefile output.
SWAPMAPS_TEXT 1
Uses .txt files via ExportText() and ImportText(). These maps
are easily editable and appear to take up less space in the
current version of BYOND.
PROCS:
SwapMaps_Find(id)
Find a map by its id
SwapMaps_Load(id)
Load a map by its id
SwapMaps_Save(id)
Save a map by its id (calls swapmap.Save())
SwapMaps_Unload(id)
Save and unload a map by its id (calls swapmap.Unload())
SwapMaps_Save_All()
Save all maps
SwapMaps_DeleteFile(id)
Delete a map file
SwapMaps_CreateFromTemplate(id)
Create a new map by loading another map to use as a template.
This map has id==src and will not be saved. To make it savable,
change id with swapmap.SetID(newid).
SwapMaps_LoadChunk(id,turf/locorner)
Load a swapmap as a "chunk", at a specific place. A new datum is
created but it's not added to the list of maps to save or unload.
The new datum can be safely deleted without affecting the turfs
it loaded. The purpose of this is to load a map file onto part of
another swapmap or an existing part of the world.
locorner is the corner turf with the lowest x,y,z values.
SwapMaps_SaveChunk(id,turf/corner1,turf/corner2)
Save a piece of the world as a "chunk". A new datum is created
for the chunk, but it can be deleted without destroying any turfs.
The chunk file can be reloaded as a swapmap all its own, or loaded
via SwapMaps_LoadChunk() to become part of another map.
SwapMaps_GetSize(id)
Return a list corresponding to the x,y,z sizes of a map file,
without loading the map.
Returns null if the map is not found.
SwapMaps_AddIconToCache(name,icon)
Cache an icon file by name for space-saving storage
swapmap.New(id,x,y,z)
Create a new map; specify id, width (x), height (y), and
depth (z)
Default size is world.maxx,world.maxy,1
swapmap.New(id,turf1,turf2)
Create a new map; specify id and 2 corners
This becomes a /swapmap for one of the compiled-in maps, for
easy saving.
swapmap.New()
Create a new map datum, but does not allocate space or assign an
ID (used for loading).
swapmap.Del()
Deletes a map but does not save
swapmap.Save()
Saves to map_[id].sav
Maps with id==src are not saved.
swapmap.Unload()
Saves the map and then deletes it
Maps with id==src are not saved.
swapmap.SetID(id)
Change the map's id and make changes to the lookup list
swapmap.AllTurfs(z)
Returns a block of turfs encompassing the entire map, or on just
one z-level
z is in world coordinates; it is optional
swapmap.Contains(turf/T)
Returns nonzero if T is inside the map's boundaries.
Also works for objs and mobs, but the proc is not area-safe.
swapmap.InUse()
Returns nonzero if a mob with a key is within the map's
boundaries.
swapmap.LoCorner(z=z1)
Returns locate(x1,y1,z), where z=z1 if none is specified.
swapmap.HiCorner(z=z2)
Returns locate(x2,y2,z), where z=z2 if none is specified.
swapmap.BuildFilledRectangle(turf/corner1,turf/corner2,item)
Builds a filled rectangle of item from one corner turf to the
other, on multiple z-levels if necessary. The corners may be
specified in any order.
item is a type path like /turf/closed/wall or /obj/barrel{full=1}.
swapmap.BuildRectangle(turf/corner1,turf/corner2,item)
Builds an unfilled rectangle of item from one corner turf to
the other, on multiple z-levels if necessary.
swapmap.BuildInTurfs(list/turfs,item)
Builds item on all of the turfs listed. The list need not
contain only turfs, or even only atoms.
*/
swapmap
var/id // a string identifying this map uniquely
var/x1 // minimum x,y,z coords
var/y1
var/z1
var/x2 // maximum x,y,z coords (also used as width,height,depth until positioned)
var/y2
var/z2
var/tmp/locked // don't move anyone to this map; it's saving or loading
var/tmp/mode // save as text-mode
var/ischunk // tells the load routine to load to the specified location
New(_id,x,y,z)
if(isnull(_id)) return
id=_id
mode=swapmaps_mode
if(isturf(x) && isturf(y))
/*
Special format: Defines a map as an existing set of turfs;
this is useful for saving a compiled map in swapmap format.
Because this is a compiled-in map, its turfs are not deleted
when the datum is deleted.
*/
x1=min(x:x,y:x);x2=max(x:x,y:x)
y1=min(x:y,y:y);y2=max(x:y,y:y)
z1=min(x:z,y:z);z2=max(x:z,y:z)
InitializeSwapMaps()
if(z2>swapmaps_compiled_maxz ||\
y2>swapmaps_compiled_maxy ||\
x2>swapmaps_compiled_maxx)
qdel(src)
return
x2=x?(x):world.maxx
y2=y?(y):world.maxy
z2=z?(z):1
AllocateSwapMap()
Destroy()
// a temporary datum for a chunk can be deleted outright
// for others, some cleanup is necessary
if(!ischunk)
swapmaps_loaded-=src
swapmaps_byname-=id
if(z2>swapmaps_compiled_maxz ||\
y2>swapmaps_compiled_maxy ||\
x2>swapmaps_compiled_maxx)
var/list/areas=new
for(var/atom/A in block(locate(x1,y1,z1),locate(x2,y2,z2)))
for(var/obj/O in A) qdel(O)
for(var/mob/M in A)
if(!M.key) qdel(M)
else M.loc=null
areas[A.loc]=null
qdel(A)
// delete areas that belong only to this map
for(var/area/a in areas)
if(a && !a.contents.len) qdel(a)
if(x2>=world.maxx || y2>=world.maxy || z2>=world.maxz) CutXYZ()
qdel(areas)
..()
return QDEL_HINT_HARDDEL_NOW
/*
Savefile format:
map
id
x // size, not coords
y
z
areas // list of areas, not including default
[each z; 1 to depth]
[each y; 1 to height]
[each x; 1 to width]
type // of turf
AREA // if non-default; saved as a number (index into areas list)
vars // all other changed vars
*/
Write(savefile/S)
var/x
var/y
var/z
var/n
var/list/areas
var/area/defarea=locate(world.area)
if(!defarea) defarea=new world.area
areas=list()
for(var/turf/T in block(locate(x1,y1,z1),locate(x2,y2,z2)))
areas[T.loc]=null
for(n in areas) // quickly eliminate associations for smaller storage
areas-=n
areas+=n
areas-=defarea
InitializeSwapMaps()
locked=1
S["id"] << id
S["z"] << z2-z1+1
S["y"] << y2-y1+1
S["x"] << x2-x1+1
S["areas"] << areas
for(n in 1 to areas.len) areas[areas[n]]=n
var/oldcd=S.cd
for(z in z1 to z2)
S.cd="[z-z1+1]"
for(y in y1 to y2)
S.cd="[y-y1+1]"
for(x in x1 to x2)
S.cd="[x-x1+1]"
var/turf/T=locate(x,y,z)
S["type"] << T.type
if(T.loc!=defarea) S["AREA"] << areas[T.loc]
T.Write(S)
S.cd=".."
S.cd=".."
sleep()
S.cd=oldcd
locked=0
qdel(areas)
Read(savefile/S,_id,turf/locorner)
var/x
var/y
var/z
var/n
var/list/areas
var/area/defarea=locate(world.area)
id=_id
if(locorner)
ischunk=1
x1=locorner.x
y1=locorner.y
z1=locorner.z
if(!defarea) defarea=new world.area
if(!_id)
S["id"] >> id
else
var/dummy
S["id"] >> dummy
S["z"] >> z2 // these are depth,
S["y"] >> y2 // height,
S["x"] >> x2 // width
S["areas"] >> areas
locked=1
AllocateSwapMap() // adjust x1,y1,z1 - x2,y2,z2 coords
var/oldcd=S.cd
for(z in z1 to z2)
S.cd="[z-z1+1]"
for(y in y1 to y2)
S.cd="[y-y1+1]"
for(x in x1 to x2)
S.cd="[x-x1+1]"
var/tp
S["type"]>>tp
var/turf/T=locate(x,y,z)
T.loc.contents-=T
T=new tp(locate(x,y,z))
if("AREA" in S.dir)
S["AREA"]>>n
var/area/A=areas[n]
A.contents+=T
else defarea.contents+=T
// clear the turf
for(var/obj/O in T) qdel(O)
for(var/mob/M in T)
if(!M.key) qdel(M)
else M.loc=null
// finish the read
T.Read(S)
S.cd=".."
S.cd=".."
sleep()
S.cd=oldcd
locked=0
qdel(areas)
/*
Find an empty block on the world map in which to load this map.
If no space is found, increase world.maxz as necessary. (If the
map is greater in x,y size than the current world, expand
world.maxx and world.maxy too.)
Ignore certain operations if loading a map as a chunk. Use the
x1,y1,z1 position for it, and *don't* count it as a loaded map.
*/
proc/AllocateSwapMap()
InitializeSwapMaps()
world.maxx=max(x2,world.maxx) // stretch x/y if necessary
world.maxy=max(y2,world.maxy)
if(!ischunk)
if(world.maxz<=swapmaps_compiled_maxz)
z1=swapmaps_compiled_maxz+1
x1=1;y1=1
else
var/list/l=ConsiderRegion(1,1,world.maxx,world.maxy,swapmaps_compiled_maxz+1)
x1=l[1]
y1=l[2]
z1=l[3]
qdel(l)
x2+=x1-1
y2+=y1-1
z2+=z1-1
world.maxz=max(z2,world.maxz) // stretch z if necessary
if(!ischunk)
swapmaps_loaded[src]=null
swapmaps_byname[id]=src
proc/ConsiderRegion(X1,Y1,X2,Y2,Z1,Z2)
while(1)
var/nextz=0
var/swapmap/M
for(M in swapmaps_loaded)
if(M.z2<Z1 || (Z2 && M.z1>Z2) || M.z1>=Z1+z2 ||\
M.x1>X2 || M.x2<X1 || M.x1>=X1+x2 ||\
M.y1>Y2 || M.y2<Y1 || M.y1>=Y1+y2) continue
// look for sub-regions with a defined ceiling
var/nz2=Z2?(Z2):Z1+z2-1+M.z2-M.z1
if(M.x1>=X1+x2)
.=ConsiderRegion(X1,Y1,M.x1-1,Y2,Z1,nz2)
if(.) return
else if(M.x2<=X2-x2)
.=ConsiderRegion(M.x2+1,Y1,X2,Y2,Z1,nz2)
if(.) return
if(M.y1>=Y1+y2)
.=ConsiderRegion(X1,Y1,X2,M.y1-1,Z1,nz2)
if(.) return
else if(M.y2<=Y2-y2)
.=ConsiderRegion(X1,M.y2+1,X2,Y2,Z1,nz2)
if(.) return
nextz=nextz?min(nextz,M.z2+1):(M.z2+1)
if(!M)
/* If nextz is not 0, then at some point there was an overlap that
could not be resolved by using an area to the side */
if(nextz) Z1=nextz
if(!nextz || (Z2 && Z2-Z1+1<z2))
return (!Z2 || Z2-Z1+1>=z2)?list(X1,Y1,Z1):null
X1=1;X2=world.maxx
Y1=1;Y2=world.maxy
proc/CutXYZ()
var/mx=swapmaps_compiled_maxx
var/my=swapmaps_compiled_maxy
var/mz=swapmaps_compiled_maxz
for(var/swapmap/M in swapmaps_loaded) // may not include src
mx=max(mx,M.x2)
my=max(my,M.y2)
mz=max(mz,M.z2)
world.maxx=mx
world.maxy=my
world.maxz=mz
// save and delete
proc/Unload()
Save()
qdel(src)
proc/Save()
if(id==src) return 0
var/savefile/S=mode?(new):new("map_[id].sav")
S << src
while(locked) sleep(1)
if(mode)
fdel("map_[id].txt")
S.ExportText("/","map_[id].txt")
return 1
// this will not delete existing savefiles for this map
proc/SetID(newid)
swapmaps_byname-=id
id=newid
swapmaps_byname[id]=src
proc/AllTurfs(z)
if(isnum(z) && (z<z1 || z>z2)) return null
return block(LoCorner(z),HiCorner(z))
// this could be safely called for an obj or mob as well, but
// probably not an area
proc/Contains(turf/T)
return (T && T.x>=x1 && T.x<=x2\
&& T.y>=y1 && T.y<=y2\
&& T.z>=z1 && T.z<=z2)
proc/InUse()
for(var/turf/T in AllTurfs())
for(var/mob/M in T) if(M.key) return 1
proc/LoCorner(z=z1)
return locate(x1,y1,z)
proc/HiCorner(z=z2)
return locate(x2,y2,z)
/*
Build procs: Take 2 turfs as corners, plus an item type.
An item may be like:
/turf/closed/wall
/obj/fence{icon_state="iron"}
*/
proc/BuildFilledRectangle(turf/T1,turf/T2,item)
if(!Contains(T1) || !Contains(T2)) return
var/turf/T=T1
// pick new corners in a block()-friendly form
T1=locate(min(T1.x,T2.x),min(T1.y,T2.y),min(T1.z,T2.z))
T2=locate(max(T.x,T2.x),max(T.y,T2.y),max(T.z,T2.z))
for(T in block(T1,T2)) new item(T)
proc/BuildRectangle(turf/T1,turf/T2,item)
if(!Contains(T1) || !Contains(T2)) return
var/turf/T=T1
// pick new corners in a block()-friendly form
T1=locate(min(T1.x,T2.x),min(T1.y,T2.y),min(T1.z,T2.z))
T2=locate(max(T.x,T2.x),max(T.y,T2.y),max(T.z,T2.z))
if(T2.x-T1.x<2 || T2.y-T1.y<2) BuildFilledRectangle(T1,T2,item)
else
//for(T in block(T1,T2)-block(locate(T1.x+1,T1.y+1,T1.z),locate(T2.x-1,T2.y-1,T2.z)))
for(T in block(T1,locate(T2.x,T1.y,T2.z))) new item(T)
for(T in block(locate(T1.x,T2.y,T1.z),T2)) new item(T)
for(T in block(locate(T1.x,T1.y+1,T1.z),locate(T1.x,T2.y-1,T2.z))) new item(T)
for(T in block(locate(T2.x,T1.y+1,T1.z),locate(T2.x,T2.y-1,T2.z))) new item(T)
/*
Supplementary build proc: Takes a list of turfs, plus an item
type. Actually the list doesn't have to be just turfs.
*/
proc/BuildInTurfs(list/turfs,item)
for(var/T in turfs) new item(T)
atom
Write(savefile/S)
for(var/V in vars-"x"-"y"-"z"-"contents"-"icon"-"overlays"-"underlays")
if(issaved(vars[V]))
if(vars[V]!=initial(vars[V])) S[V]<<vars[V]
else S.dir.Remove(V)
if(icon!=initial(icon))
if(swapmaps_iconcache && swapmaps_iconcache[icon])
S["icon"]<<swapmaps_iconcache[icon]
else S["icon"]<<icon
// do not save mobs with keys; do save other mobs
var/mob/M
for(M in src) if(M.key) break
if(overlays.len) S["overlays"]<<overlays
if(underlays.len) S["underlays"]<<underlays
if(contents.len && !isarea(src))
var/list/l=contents
if(M)
l=l.Copy()
for(M in src) if(M.key) l-=M
if(l.len) S["contents"]<<l
if(l!=contents) qdel(l)
Read(savefile/S)
var/list/l
if(contents.len) l=contents
..()
// if the icon was a text string, it would not have loaded properly
// replace it from the cache list
if(!icon && ("icon" in S.dir))
var/ic
S["icon"]>>ic
if(istext(ic)) icon=swapmaps_iconcache[ic]
if(l && contents!=l)
contents+=l
qdel(l)
// set this up (at runtime) as follows:
// list(\
// 'player.dmi'="player",\
// 'monster.dmi'="monster",\
// ...
// 'item.dmi'="item")
var/list/swapmaps_iconcache
// preferred mode; sav or text
var/const/SWAPMAPS_SAV=0
var/const/SWAPMAPS_TEXT=1
var/swapmaps_mode=SWAPMAPS_SAV
var/swapmaps_compiled_maxx
var/swapmaps_compiled_maxy
var/swapmaps_compiled_maxz
var/swapmaps_initialized
var/swapmaps_loaded
var/swapmaps_byname
/proc/InitializeSwapMaps()
if(swapmaps_initialized) return
swapmaps_initialized=1
swapmaps_compiled_maxx=world.maxx
swapmaps_compiled_maxy=world.maxy
swapmaps_compiled_maxz=world.maxz
swapmaps_loaded=list()
swapmaps_byname=list()
if(swapmaps_iconcache)
for(var/V in swapmaps_iconcache)
// reverse-associate everything
// so you can look up an icon file by name or vice-versa
swapmaps_iconcache[swapmaps_iconcache[V]]=V
/proc/SwapMaps_AddIconToCache(name,icon)
if(!swapmaps_iconcache) swapmaps_iconcache=list()
swapmaps_iconcache[name]=icon
swapmaps_iconcache[icon]=name
/proc/SwapMaps_Find(id)
InitializeSwapMaps()
return swapmaps_byname[id]
/proc/SwapMaps_Load(id)
InitializeSwapMaps()
var/swapmap/M=swapmaps_byname[id]
if(!M)
var/savefile/S
var/text=0
if(swapmaps_mode==SWAPMAPS_TEXT && fexists("map_[id].txt"))
text=1
else if(fexists("map_[id].sav"))
S=new("map_[id].sav")
else if(swapmaps_mode!=SWAPMAPS_TEXT && fexists("map_[id].txt"))
text=1
else return // no file found
if(text)
S=new
S.ImportText("/",file("map_[id].txt"))
S >> M
while(M.locked) sleep(1)
M.mode=text
return M
/proc/SwapMaps_Save(id)
InitializeSwapMaps()
var/swapmap/M=swapmaps_byname[id]
if(M) M.Save()
return M
/proc/SwapMaps_Save_All()
InitializeSwapMaps()
for(var/swapmap/M in swapmaps_loaded)
if(M) M.Save()
/proc/SwapMaps_Unload(id)
InitializeSwapMaps()
var/swapmap/M=swapmaps_byname[id]
if(!M) return // return silently from an error
M.Unload()
return 1
/proc/SwapMaps_DeleteFile(id)
fdel("map_[id].sav")
fdel("map_[id].txt")
/proc/SwapMaps_CreateFromTemplate(template_id)
var/swapmap/M=new
var/savefile/S
var/text=0
if(swapmaps_mode==SWAPMAPS_TEXT && fexists("map_[template_id].txt"))
text=1
else if(fexists("map_[template_id].sav"))
S=new("map_[template_id].sav")
else if(swapmaps_mode!=SWAPMAPS_TEXT && fexists("map_[template_id].txt"))
text=1
else
log_world("SwapMaps error in SwapMaps_CreateFromTemplate(): map_[template_id] file not found.")
return
if(text)
S=new
S.ImportText("/",file("map_[template_id].txt"))
/*
This hacky workaround is needed because S >> M will create a brand new
M to fill with data. There's no way to control the Read() process
properly otherwise. The //.0 path should always match the map, however.
*/
S.cd="//.0"
M.Read(S,M)
M.mode=text
while(M.locked) sleep(1)
return M
/proc/SwapMaps_LoadChunk(chunk_id,turf/locorner)
var/swapmap/M=new
var/savefile/S
var/text=0
if(swapmaps_mode==SWAPMAPS_TEXT && fexists("map_[chunk_id].txt"))
text=1
else if(fexists("map_[chunk_id].sav"))
S=new("map_[chunk_id].sav")
else if(swapmaps_mode!=SWAPMAPS_TEXT && fexists("map_[chunk_id].txt"))
text=1
else
log_world("SwapMaps error in SwapMaps_LoadChunk(): map_[chunk_id] file not found.")
return
if(text)
S=new
S.ImportText("/",file("map_[chunk_id].txt"))
/*
This hacky workaround is needed because S >> M will create a brand new
M to fill with data. There's no way to control the Read() process
properly otherwise. The //.0 path should always match the map, however.
*/
S.cd="//.0"
M.Read(S,M,locorner)
while(M.locked) sleep(1)
qdel(M)
return 1
/proc/SwapMaps_SaveChunk(chunk_id,turf/corner1,turf/corner2)
if(!corner1 || !corner2)
log_world("SwapMaps error in SwapMaps_SaveChunk():")
if(!corner1)
log_world(" corner1 turf is null")
if(!corner2)
log_world(" corner2 turf is null")
return
var/swapmap/M=new
M.id=chunk_id
M.ischunk=1 // this is a chunk
M.x1=min(corner1.x,corner2.x)
M.y1=min(corner1.y,corner2.y)
M.z1=min(corner1.z,corner2.z)
M.x2=max(corner1.x,corner2.x)
M.y2=max(corner1.y,corner2.y)
M.z2=max(corner1.z,corner2.z)
M.mode=swapmaps_mode
M.Save()
while(M.locked) sleep(1)
qdel(M)
return 1
/proc/SwapMaps_GetSize(id)
var/savefile/S
var/text=0
if(swapmaps_mode==SWAPMAPS_TEXT && fexists("map_[id].txt"))
text=1
else if(fexists("map_[id].sav"))
S=new("map_[id].sav")
else if(swapmaps_mode!=SWAPMAPS_TEXT && fexists("map_[id].txt"))
text=1
else
log_world("SwapMaps error in SwapMaps_GetSize(): map_[id] file not found.")
return
if(text)
S=new
S.ImportText("/",file("map_[id].txt"))
/*
The //.0 path should always be the map. There's no other way to
read this data.
*/
S.cd="//.0"
var/x
var/y
var/z
S["x"] >> x
S["y"] >> y
S["z"] >> z
return list(x,y,z)

View File

@@ -0,0 +1,174 @@
#define DMM_IGNORE_AREAS 1
#define DMM_IGNORE_TURFS 2
#define DMM_IGNORE_OBJS 4
#define DMM_IGNORE_NPCS 8
#define DMM_IGNORE_PLAYERS 16
#define DMM_IGNORE_MOBS 24
dmm_suite{
var{
quote = "\""
list/letter_digits = list(
"a","b","c","d","e",
"f","g","h","i","j",
"k","l","m","n","o",
"p","q","r","s","t",
"u","v","w","x","y",
"z",
"A","B","C","D","E",
"F","G","H","I","J",
"K","L","M","N","O",
"P","Q","R","S","T",
"U","V","W","X","Y",
"Z"
)
}
save_map(var/turf/t1 as turf, var/turf/t2 as turf, var/map_name as text, var/flags as num){
//Check for illegal characters in file name... in a cheap way.
if(!((ckeyEx(map_name)==map_name) && ckeyEx(map_name))){
CRASH("Invalid text supplied to proc save_map, invalid characters or empty string.")
}
//Check for valid turfs.
if(!isturf(t1) || !isturf(t2)){
CRASH("Invalid arguments supplied to proc save_map, arguments were not turfs.")
}
var/file_text = write_map(t1,t2,flags)
if(fexists("[map_name].dmm")){
fdel("[map_name].dmm")
}
var/saved_map = file("[map_name].dmm")
saved_map << file_text
return saved_map
}
write_map(var/turf/t1 as turf, var/turf/t2 as turf, var/flags as num){
//Check for valid turfs.
if(!isturf(t1) || !isturf(t2)){
CRASH("Invalid arguments supplied to proc write_map, arguments were not turfs.")
}
var/turf/nw = locate(min(t1.x,t2.x),max(t1.y,t2.y),min(t1.z,t2.z))
var/turf/se = locate(max(t1.x,t2.x),min(t1.y,t2.y),max(t1.z,t2.z))
var/list/templates[0]
var/template_buffer = {""}
var/dmm_text = {""}
for(var/pos_z in nw.z to se.z){
for(var/pos_y in nw.y to se.y){
for(var/pos_x in nw.x to se.x){
var/turf/test_turf = locate(pos_x,pos_y,pos_z)
var/test_template = make_template(test_turf, flags)
var/template_number = templates.Find(test_template)
if(!template_number){
templates.Add(test_template)
template_number = templates.len
}
template_buffer += "[template_number],"
}
template_buffer += ";"
}
template_buffer += "."
}
var/key_length = round/*floor*/(log(letter_digits.len,templates.len-1)+1)
var/list/keys[templates.len]
for(var/key_pos in 1 to templates.len){
keys[key_pos] = get_model_key(key_pos,key_length)
dmm_text += {""[keys[key_pos]]" = ([templates[key_pos]])\n"}
}
var/z_level = 0
for(var/z_pos=1;TRUE;z_pos=findtext(template_buffer,".",z_pos)+1){
if(z_pos>=length(template_buffer)){break}
if(z_level){dmm_text+={"\n"}}
dmm_text += {"\n(1,1,[++z_level]) = {"\n"}
var/z_block = copytext(template_buffer,z_pos,findtext(template_buffer,".",z_pos))
for(var/y_pos=1;TRUE;y_pos=findtext(z_block,";",y_pos)+1){
if(y_pos>=length(z_block)){break}
var/y_block = copytext(z_block,y_pos,findtext(z_block,";",y_pos))
for(var/x_pos=1;TRUE;x_pos=findtext(y_block,",",x_pos)+1){
if(x_pos>=length(y_block)){break}
var/x_block = copytext(y_block,x_pos,findtext(y_block,",",x_pos))
var/key_number = text2num(x_block)
var/temp_key = keys[key_number]
dmm_text += temp_key
sleep(-1)
}
dmm_text += {"\n"}
sleep(-1)
}
dmm_text += {"\"}"}
sleep(-1)
}
return dmm_text
}
proc{
make_template(var/turf/model as turf, var/flags as num){
var/template = ""
var/obj_template = ""
var/mob_template = ""
var/turf_template = ""
if(!(flags & DMM_IGNORE_TURFS)){
turf_template = "[model.type][check_attributes(model)],"
} else{ turf_template = "[world.turf],"}
var/area_template = ""
if(!(flags & DMM_IGNORE_OBJS)){
for(var/obj/O in model.contents){
obj_template += "[O.type][check_attributes(O)],"
}
}
for(var/mob/M in model.contents){
if(M.client){
if(!(flags & DMM_IGNORE_PLAYERS)){
mob_template += "[M.type][check_attributes(M)],"
}
}
else{
if(!(flags & DMM_IGNORE_NPCS)){
mob_template += "[M.type][check_attributes(M)],"
}
}
}
if(!(flags & DMM_IGNORE_AREAS)){
var/area/m_area = model.loc
area_template = "[m_area.type][check_attributes(m_area)]"
} else{ area_template = "[world.area]"}
template = "[obj_template][mob_template][turf_template][area_template]"
return template
}
check_attributes(var/atom/A){
var/attributes_text = {"{"}
for(var/V in A.vars){
sleep(-1)
if((!issaved(A.vars[V])) || (A.vars[V]==initial(A.vars[V]))){continue}
if(istext(A.vars[V])){
attributes_text += {"[V] = "[A.vars[V]]""}
}
else if(isnum(A.vars[V])||ispath(A.vars[V])){
attributes_text += {"[V] = [A.vars[V]]"}
}
else if(isicon(A.vars[V])||isfile(A.vars[V])){
attributes_text += {"[V] = '[A.vars[V]]'"}
}
else{
continue
}
if(attributes_text != {"{"}){
attributes_text+={"; "}
}
}
if(attributes_text=={"{"}){
return
}
if(copytext(attributes_text, length(attributes_text)-1, 0) == {"; "}){
attributes_text = copytext(attributes_text, 1, length(attributes_text)-1)
}
attributes_text += {"}"}
return attributes_text
}
get_model_key(var/which as num, var/key_length as num){
var/key = ""
var/working_digit = which-1
for(var/digit_pos in key_length to 1 step -1){
var/place_value = round/*floor*/(working_digit/(letter_digits.len**(digit_pos-1)))
working_digit-=place_value*(letter_digits.len**(digit_pos-1))
key = "[key][letter_digits[place_value+1]]"
}
return key
}
}
}