Adds the framework for randomlly loading an additional external z-level at server start. Loading during the game should also be possible.

Fixes assorted bugs

git-svn-id: http://tgstation13.googlecode.com/svn/trunk@3559 316c924e-a436-60f5-8080-3fe189b3f50e
This commit is contained in:
VivianFoxfoot@gmail.com
2012-05-06 18:04:31 +00:00
parent abf2585875
commit f1696eb47c
17 changed files with 1569 additions and 6 deletions

View File

@@ -0,0 +1,672 @@
/*
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/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)
del(src)
return
x2=x?(x):world.maxx
y2=y?(y):world.maxy
z2=z?(z):1
AllocateSwapMap()
Del()
// 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) del(O)
for(var/mob/M in A)
if(!M.key) del(M)
else M.loc=null
areas[A.loc]=null
del(A)
// delete areas that belong only to this map
for(var/area/a in areas)
if(a && !a.contents.len) del(a)
if(x2>=world.maxx || y2>=world.maxy || z2>=world.maxz) CutXYZ()
del(areas)
..()
/*
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;y;z;n
list/areas
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=z1,z<=z2,++z)
S.cd="[z-z1+1]"
for(y=y1,y<=y2,++y)
S.cd="[y-y1+1]"
for(x=x1,x<=x2,++x)
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
del(areas)
Read(savefile/S,_id,turf/locorner)
var
x;y;z;n
list/areas
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=z1,z<=z2,++z)
S.cd="[z-z1+1]"
for(y=y1,y<=y2,++y)
S.cd="[y-y1+1]"
for(x=x1,x<=x2,++x)
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) del(O)
for(var/mob/M in T)
if(!M.key) del(M)
else M.loc=null
// finish the read
T.Read(S)
S.cd=".."
S.cd=".."
sleep()
S.cd=oldcd
locked=0
del(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]
del(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()
del(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/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) del(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
del(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
world.log << "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
world.log << "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)
del(M)
return 1
proc/SwapMaps_SaveChunk(chunk_id,turf/corner1,turf/corner2)
if(!corner1 || !corner2)
world.log << "SwapMaps error in SwapMaps_SaveChunk():"
if(!corner1) world.log << " corner1 turf is null"
if(!corner2) world.log << " 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)
del(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
world.log << "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,73 @@
var/global/dmm_suite/maploader = new
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/z_offset 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).
}
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,237 @@
/*
DMP to swapmap converter
version 1.0
by Lummox JR
*/
mob/verb/Convert(filename as file)
dmp2swapmap(filename)
proc/d2sm_prepmap(filename)
var/txt = file2text(filename)
if(!txt) return
var/i,j
i=findText(txt,ascii2text(13)) // eliminate carriage returns
while(i)
txt=copytext(txt,1,i)+copytext(txt,i+1)
i=findText(txt,ascii2text(13),i)
i=findText(txt,"\\\n")
while(i)
for(j=i+2,j<=length(txt),++j) if(text2ascii(txt,j)>32) break
txt=copytext(txt,1,i)+copytext(txt,j)
i=findText(txt,"\\\n",i)
return txt
proc/dmp2swapmap(filename)
//var/txt = file2text(filename)
//if(!txt) return
var/txt = d2sm_prepmap(filename)
var/mapname="[filename]"
var/i,j,k
i=findtext(mapname,".dmp")
while(i && i+4<length(mapname)) i=findtext(mapname,".dmp",i+1)
mapname=copytext(mapname,1,i)
/* i=findText(txt,ascii2text(13))
while(i)
txt=copytext(txt,1,i)+copytext(txt,i+1)
i=findText(txt,ascii2text(13),i)
i=findText(txt,"\\\n")
while(i)
for(j=i+2,j<=length(txt),++j) if(text2ascii(txt,j)>32) break
txt=copytext(txt,1,i)+copytext(txt,j)
i=findText(txt,"\\\n",i) */
var/list/codes=new
var/codelen=1
var/list/areas
var/mode=34
var/z=0
var/X=0,Y=0,Z=0
while(txt)
if(text2ascii(txt)==34)
if(mode!=34)
world << "Corrupt map file [filename]: Unexpected code found after z-level [z]"
return
// standard line:
// "a" = (/obj, /obj, /turf, /area)
i=findtext(txt,"\"",2)
var/code=copytext(txt,2,i)
codelen=length(code)
i=findtext(txt,"(",i)
if(!i)
world << "Corrupt map file [filename]: No type list follows \"[code]\""
return
k=findtext(txt,"\n",++i)
j=(k || length(txt+1))
while(--j>=i && text2ascii(txt,j)!=41)
if(j<i)
world << "Corrupt map file [filename]: Type list following \"[code]\" is incomplete"
return
var/list/L = d2sm_ParseCommaList(copytext(txt,i,j))
if(istext(L))
world << "Corrupt map file [filename]: [L]"
return
if(L.len<2)
world << "Corrupt map file [filename]: Type list following \"[code]\" has only 1 item"
return
txt=k?copytext(txt,k+1):null
if(L[L.len] == "[world.area]") L[L.len]=0
else
if(!areas) areas=list()
i=areas.Find(L[L.len])
if(i) L[L.len]=i
else
areas+=L[L.len]
L[L.len]=areas.len
var/codetrans=d2sm_ConvertType(L[L.len-1],"\t\t\t\t")
if(L[L.len]) codetrans+="\t\t\t\tAREA = [L[L.len]]\n"
if(L.len>2) codetrans+=d2sm_Contents(L,L.len-2,"\t\t\t\t")
codes[code]=copytext(codetrans,1,length(codetrans))
else if(text2ascii(txt)==40)
mode=40
// standard line (top-down, left-right symbol order):
// (1,1,1) = {"
// abcde
// bcdef
// "}
i=d2sm_MatchBrace(txt,1,40)
if(!i)
world << "Corrupt map file [filename]: No matching ) for coordinates: [copytext(txt,1,findtext(txt,"\n"))]"
return
var/list/coords=d2sm_ParseCommaList(copytext(txt,2,i))
if(istext(coords) || coords.len!=3)
world << "Corrupt map file [filename]: [istext(coords)?(coords):"[copytext(txt,1,i+1)] is not a valid (x,y,z) coordinate"]"
return
j=findtext(txt,"{",i+1)
if(!j)
world << "Corrupt map file [filename]: No braces {} following [copytext(txt,1,i+1)]"
return
k=d2sm_MatchBrace(txt,j,123)
if(!k)
world << "Corrupt map file [filename]: No closing brace } following [copytext(txt,1,i+1)]"
return
var/mtxt=copytext(txt,j+1,k)
if(findText(mtxt,"\"\n")!=1 || !findText(mtxt,"\n\"",length(mtxt)-1))
world << findText(mtxt,"\"\n")
world << findText(mtxt,"\n\"",length(mtxt)-1)
world << "Corrupt map file [filename]: No quotes in braces following [copytext(txt,1,i+1)]"
return
mtxt=copytext(mtxt,2,length(mtxt))
var/_x=0,_y=0
for(i=1,,++_y)
j=findText(mtxt,"\n",i+1)
if(!j) break
_x=max(_x,(j-i-1)/codelen)
i=j
X=max(X,_x)
Y=max(Y,_y)
z=text2num(coords[3])
Z=max(Z,z)
txt=copytext(txt,k+1)
else
i=findtext(txt,"\n")
txt=i?copytext(txt,i+1):null
world << "Map size: [X],[Y],[Z]"
//for(var/code in codes)
// world << "Code \"[code]\":\n[codes[code]]"
fdel("map_[mapname].txt")
var/F = file("map_[mapname].txt")
F << ". = object(\".0\")\n.0\n\ttype = /swapmap\n\tid = \"[mapname]\"\n\tz = [Z]\n\ty = [Y]\n\tx = [X]"
if(areas)
txt=""
for(i=0,i<areas.len,++i)
txt+="[i?", ":""]object(\".[i]\")"
F << "\tareas = list([txt])"
for(i=0,i<areas.len,++i)
F << "\t\t.[i]"
txt=d2sm_ConvertType(areas[i+1],"\t\t\t")
F << copytext(txt,1,length(txt))
// 2nd pass
txt=d2sm_prepmap(filename)
while(txt)
// skip all non-data sections
if(text2ascii(txt)!=40)
i=findText(txt,"\n")
if(i) txt=copytext(txt,i+1)
else txt=null
continue
i=d2sm_MatchBrace(txt,1,40)
var/list/coords=d2sm_ParseCommaList(copytext(txt,2,i))
j=findtext(txt,"{",i+1)
k=d2sm_MatchBrace(txt,j,123)
var/mtxt=copytext(txt,j+2,k-1)
var/_x=0,_y=0
for(i=1,,++_y)
j=findText(mtxt,"\n",i+1)
if(!j) break
_x=max(_x,(j-i-1)/codelen)
i=j
// print out this z-level now
F << "\t[coords[3]]"
i=1
for(var/y=_y,y>0,--y) // map is top-down
++i
F << "\t\t[y]"
for(var/x in 1 to _x)
F << "\t\t\t[x]"
j=i+codelen
F << codes[copytext(mtxt,i,j)]
i=j
txt=copytext(txt,k+1)
/* for(z in 1 to Z)
F << "\t[z]"
for(var/y in 1 to Y)
F << "\t\t[y]"
for(var/x in 1 to X)
F << "\t\t\t[x]"
F << codes[pick(codes)] */
proc/d2sm_ParseCommaList(txt)
var/list/L=new
var/i,ch
for(i=1,i<=length(txt),++i)
if(text2ascii(txt,i)>32) break
for(,i<=length(txt),++i)
ch=text2ascii(txt,i)
if(ch==44)
L+=copytext(txt,1,i)
for(++i,i<=length(txt),++i) if(text2ascii(txt,i)>32) break
txt=copytext(txt,i)
i=0;continue
if(ch==40 || ch==91 || ch==123)
i=d2sm_MatchBrace(txt,i,ch)
if(!i) return "No matching brace found for [ascii2text(ch)]"
if(i>1) L+=copytext(txt,1,i)
return L
proc/d2sm_MatchBrace(txt, i, which)
if(which==40) ++which
else which+=2
var/j,ch
for(j=i+1,j<=length(txt),++j)
ch=text2ascii(txt,j)
if(ch==which) return j
if(ch==40 || ch==91 || ch==123)
j=d2sm_MatchBrace(txt,j,ch)
if(!j) return 0
proc/d2sm_ConvertType(tt,tabs="")
var/i=findText(tt,"{")
if(!i) return "[tabs]type = [tt]\n"
.="[tabs]type = [copytext(tt,1,i)]\n"
var/list/L=d2sm_ParseCommaList(copytext(tt,i+1,d2sm_MatchBrace(tt,i,123)))
if(istext(L)) return
for(var/pair in L)
.="[.][tabs][pair]\n"
proc/d2sm_Contents(list/conts,n,tabs="")
.="[tabs]contents = list("
var/i
for(i=0,i<n,++i)
.+="[i?", ":""]object(\".[i]\")"
.+=")\n"
tabs+="\t"
for(i=0,i<n,++i)
.+="[tabs].[i]\n"
.+=d2sm_ConvertType(conts[i+1],tabs+"\t")

View File

@@ -0,0 +1,47 @@
var/list/potentialRandomZlevels = list()
proc/createRandomZlevel()
var/text = file2text("maps/RandomZLevels/fileList.txt")
if (!text) // No random Z-levels for you.
return
world << "\red \b Reticulating Splines"
var/list/CL = dd_text2list(text, "\n")
for (var/t in CL)
if (!t)
continue
t = trim(t)
if (length(t) == 0)
continue
else if (copytext(t, 1, 2) == "#")
continue
var/pos = findtext(t, " ")
var/name = null
// var/value = null
if (pos)
name = lowertext(copytext(t, 1, pos))
// value = copytext(t, pos + 1)
else
name = lowertext(t)
if (!name)
continue
potentialRandomZlevels.Add(t)
if(potentialRandomZlevels.len)
var/map = pick(potentialRandomZlevels)
var/file = file(map)
if(isfile(file))
maploader.load_map(file)
else
return

249
code/modules/maps/reader.dm Normal file
View File

@@ -0,0 +1,249 @@
dmm_suite{
load_map(var/dmm_file as file, var/z_offset as num){
if(!z_offset){
z_offset = world.maxz+1
}
var/quote = ascii2text(34)
var/tfile = file2text(dmm_file)
var/tfile_len = length(tfile)
var/list/grid_models[0]
var/key_len = length(copytext(tfile,2,findtext(tfile,quote,2,0)))
for(var/lpos=1;lpos<tfile_len;lpos=findtext(tfile,"\n",lpos,0)+1){
var/tline = copytext(tfile,lpos,findtext(tfile,"\n",lpos,0))
if(copytext(tline,1,2)!=quote){break}
var/model_key = copytext(tline,2,findtext(tfile,quote,2,0))
var/model_contents = copytext(tline,findtext(tfile,"=")+3,length(tline))
grid_models[model_key] = model_contents
sleep(-1)
}
var/zcrd=-1
var/ycrd=0
var/xcrd=0
for(var/zpos=findtext(tfile,"\n(1,1,");TRUE;zpos=findtext(tfile,"\n(1,1,",zpos+1,0)){
zcrd++
world.maxz = max(world.maxz, zcrd+z_offset)
ycrd=0
var/zgrid = copytext(tfile,findtext(tfile,quote+"\n",zpos,0)+2,findtext(tfile,"\n"+quote,zpos,0)+1)
for(var/gpos=1;gpos!=0;gpos=findtext(zgrid,"\n",gpos,0)+1){
var/grid_line = copytext(zgrid,gpos,findtext(zgrid,"\n",gpos,0)+1)
var/y_depth = length(zgrid)/(length(grid_line))
if(world.maxy<y_depth){world.maxy=y_depth}
grid_line=copytext(grid_line,1,length(grid_line))
if(!ycrd){
ycrd = y_depth
}
else{ycrd--}
xcrd=0
for(var/mpos=1;mpos<=length(grid_line);mpos+=key_len){
xcrd++
if(world.maxx<xcrd){world.maxx=xcrd}
var/model_key = copytext(grid_line,mpos,mpos+key_len)
parse_grid(grid_models[model_key],xcrd,ycrd,zcrd+z_offset)
}
if(gpos+length(grid_line)+1>length(zgrid)){break}
sleep(-1)
}
if(findtext(tfile,quote+"}",zpos,0)+2==tfile_len){break}
sleep(-1)
}
}
proc{
parse_grid(var/model as text,var/xcrd as num,var/ycrd as num,var/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/text_strings[0]
for(var/index=1;findtext(model,quote);index++){
/*Loop: Stores quoted portions of text in text_strings, and replaces them with an
index to that list.
- Each iteration represents one quoted section of text.
*/
text_strings.len=index
text_strings[index] = copytext(model,findtext(model,quote)+1,findtext(model,quote,findtext(model,quote)+1,0))
model = copytext(model,1,findtext(model,quote))+"~[index]"+copytext(model,findtext(model,quote,findtext(model,quote)+1,0)+1,0)
sleep(-1)
}
var/list/old_turf_underlays[0]
var/old_turf_density
var/old_turf_opacity
/*The old_turf variables store information about turfs instantiated in this location/iteration.
This is done to approximate the layered turf effect of DM's map editor.
An image of each turf is stored in old_turf_underlays[], and is later added to the new turf's underlays.
*/
for(var/dpos=1;dpos!=0;dpos=findtext(model,",",dpos,0)+1){
/*Loop: Identifies each object's data, instantiates it, and reconstitues it's fields.
- Each iteration represents one object's data, including type path and field values.
*/
var/full_def = copytext(model,dpos,findtext(model,",",dpos,0))
var/atom_def = text2path(copytext(full_def,1,findtext(full_def,"{")))
var/list/attributes[0]
if(findtext(full_def,"{")){
full_def = copytext(full_def,1,length(full_def))
for(var/apos=findtext(full_def,"{")+1;apos!=0;apos=findtext(full_def,";",apos,0)+1){
//Loop: Identifies each attribute/value pair, and stores it in attributes[].
attributes.Add(copytext(full_def,apos,findtext(full_def,";",apos,0)))
if(!findtext(copytext(full_def,apos,0),";")){break}
sleep(-1)
}
}
//Construct attributes associative list
var/list/fields = new(0)
for(var/index=1;index<=attributes.len;index++){
var/trim_left = trim_text(copytext(attributes[index],1,findtext(attributes[index],"=")))
var/trim_right = trim_text(copytext(attributes[index],findtext(attributes[index],"=")+1,0))
//Check for string
if(findtext(trim_right,"~")){
var/reference_index = copytext(trim_right,findtext(trim_right,"~")+1,0)
trim_right=text_strings[text2num(reference_index)]
}
//Check for number
else if(isnum(text2num(trim_right))){
trim_right = text2num(trim_right)
}
//Check for file
else if(copytext(trim_right,1,2) == "'"){
trim_right = file(copytext(trim_right,2,length(trim_right)))
}
fields[trim_left] = trim_right
}
//End construction
//Begin Instanciation
var/atom/instance
var/dmm_suite/preloader/_preloader = new(fields)
if(ispath(atom_def,/area)){
instance = locate(atom_def)
instance.contents.Add(locate(xcrd,ycrd,zcrd))
}
else if(ispath(atom_def,/turf)){
var/turf/old_turf = locate(xcrd,ycrd,zcrd)
if(old_turf.density){old_turf_density = 1}
if(old_turf.opacity){old_turf_opacity = 1}
if(old_turf.icon){
var/image/old_turf_image = image(old_turf.icon,null,old_turf.icon_state,old_turf.layer,old_turf.dir)
old_turf_underlays.Add(old_turf_image)
}
instance = new atom_def(old_turf, _preloader)
for(var/inverse_index=old_turf_underlays.len;inverse_index;inverse_index--){
var/image/image_underlay = old_turf_underlays[inverse_index]
image_underlay.loc = instance
instance.underlays.Add(image_underlay)
}
if(!instance.density){instance.density = old_turf_density}
if(!instance.opacity){instance.opacity = old_turf_opacity}
}
if(_preloader && instance){
_preloader.load(instance)
}
//End Instanciation
if(!findtext(copytext(model,dpos,0),",")){break}
sleep(-1)
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
for(var/dpos=1;dpos!=0;dpos=findtext(model,",",dpos,0)+1)
{
/*Loop: Identifies each object's data, instantiates it, and reconstitues it's fields.
- Each iteration represents one object's data, including type path and field values.
*/
var/full_def = copytext(model,dpos,findtext(model,",",dpos,0))
var/atom_def = text2path(copytext(full_def,1,findtext(full_def,"{")))
var/list/attributes[0]
if(findtext(full_def,"{")){
full_def = copytext(full_def,1,length(full_def))
for(var/apos=findtext(full_def,"{")+1;apos!=0;apos=findtext(full_def,";",apos,0)+1){
//Loop: Identifies each attribute/value pair, and stores it in attributes[].
attributes.Add(copytext(full_def,apos,findtext(full_def,";",apos,0)))
if(!findtext(copytext(full_def,apos,0),";")){break}
sleep(-1)
}
}
//Construct attributes associative list
var/list/fields = new(0)
for(var/index=1;index<=attributes.len;index++){
var/trim_left = trim_text(copytext(attributes[index],1,findtext(attributes[index],"=")))
var/trim_right = trim_text(copytext(attributes[index],findtext(attributes[index],"=")+1,0))
//Check for string
if(findtext(trim_right,"~")){
var/reference_index = copytext(trim_right,findtext(trim_right,"~")+1,0)
trim_right=text_strings[text2num(reference_index)]
}
//Check for number
else if(isnum(text2num(trim_right))){
trim_right = text2num(trim_right)
}
//Check for file
else if(copytext(trim_right,1,2) == "'"){
trim_right = file(copytext(trim_right,2,length(trim_right)))
}
fields[trim_left] = trim_right
}
//End construction
//Begin Instanciation
var/atom/instance
var/dmm_suite/preloader/_preloader = new(fields)
if(!ispath(atom_def,/area) && !ispath(atom_def,/turf))
{
instance = new atom_def(locate(xcrd,ycrd,zcrd), _preloader)
}
if(_preloader && instance)
{
_preloader.load(instance)
}
//End Instanciation
if(!findtext(copytext(model,dpos,0),",")){break}
sleep(-1)
}
}
trim_text(var/what as text){
while(length(what) && findtext(what," ",1,2)){
what=copytext(what,2,0)
}
while(length(what) && findtext(what," ",length(what),0)){
what=copytext(what,1,length(what))
}
return what
}
}
}
atom/New(atom/loc, dmm_suite/preloader/_dmm_preloader){
if(istype(_dmm_preloader, /dmm_suite/preloader)){
_dmm_preloader.load(src)
}
. = ..()
}
dmm_suite{
preloader{
parent_type = /datum
var{
list/attributes
}
New(list/the_attributes){
.=..()
if(!the_attributes.len){ Del()}
attributes = the_attributes
}
proc{
load(atom/what){
for(var/attribute in attributes){
what.vars[attribute] = attributes[attribute]
}
Del()
}
}
}
}

174
code/modules/maps/writer.dm Normal file
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=nw.z;pos_z<=se.z;pos_z++){
for(var/pos_y=nw.y;pos_y>=se.y;pos_y--){
for(var/pos_x=nw.x;pos_x<=se.x;pos_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=1;key_pos<=templates.len;key_pos++){
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=key_length;digit_pos>=1;digit_pos--){
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
}
}
}