SwapMaps Library

by Lummox JR

Most people think of maps in BYOND as a static part of their game; even if it can be saved and loaded again, the assumption is that the set of game maps will always be the same size and won't change. But in fact BYOND is a lot more flexible than that: It's possible to use maps in a much more dynamic way.

The SwapMaps library was developed for just this purpose. The idea is simple: At any time, you can create, or load, a dynamic map. The library will find room for it, and then you can build on or decorate the space at will. The map will function just like any other place in your game, until you delete or unload it. Best of all, saving a swapmap is simple, and the savefiles can be transferred to other worlds.

Uses of swapmaps are many: You can create larger maps to explore, of forests or caves or what have you, an overworld of infinite size. You can use them to create temporary battle areas for players to battle monsters or each other. You can assign each player a house, a plot of land for them to build on.

One goal of the library is to make its savefiles as small and compact as possible, so that they'll take up less space on your server and take less time to transfer from world to world. Swapmaps make massive multiplayer role-playing games feasible in BYOND.

Nuts And Bolts

At the core of the library is a datum called /swapmap. Every swapmap has a unique id, a name, that can be used to look it up or load it.

There are two ways to create a new swapmap: You can create one based on your existing map by calling new /swapmap(id,turf/corner1,turf/corner2), and this map will never be deleted; it's used for saving purposes only. You can create a standard swapmap, one that can be unloaded at will, by calling new /swapmap(id,width,height,depth), where the width, height, and depth are its size in x, y, and z. Maps don't have to be the same size; you can make them any size you like, and the world map will grow to adjust if necessary.

A map can also be loaded by calling SwapMaps_Load(id), a global proc. This will search for a file named map_<id>.sav and, if found, load it. The proc returns the swapmap that was loaded, or null if no map was found.

When a map is created or loaded, the library will try to find a place to fit it among all the other loaded maps. A swapmap can have smaller x and y dimensions than your world, so it's your responsibility to add a boundary to the swapmap if you don't want people crossing from one to the other. For your convenience, there are procs included that will help you build on your map quickly.

A swapmap that has been loaded can be found by its id with SwapMaps_Find(id). When you want to unload a map, you can do it one of two ways: You can call SwapMaps_Unload(id) to do it by name, or you can use the datum and call swapmap.Unload() directly. The map will be saved and deleted.

Call SwapMaps_Save(id) or swapmap.Save(), just like the unloading process, to save a map without deleting it. You should do this when there's no danger of the process being interrupted, as when the map is in use. To prevent the save from slowing down the game, saves and loads call sleep() at every z level; thus there's a slight potential for some weird bugs if something goes upstairs or down within the swapmap during a save.

When a swapmap is saved, it saves all the turfs in it, plus the areas those turfs belong to (if they belong to any area other than the default), plus items in the map. Any mob with a key (i.e., belonging to a player) is not saved along with the swapmap.

If you delete a swapmap (such as when you unload it, or if you delete the datum), its turfs and their contents will be deleted too, and the world map may shrink down if it's been stretched to fit the swapmap you just deleted. Mobs with keys won't be deleted, just like during the save process; they'll be relocated to null.

Accessing The Map

Each map has its coordinates stored in the datum as vars: x1, y1, and z1 mark out the lower left corner, and x2, y2, and z2 mark the upper right. An easy way to find the corner turfs is to use the swapmap.LoCorner() and swapmap.HiCorner() procs. If you specify a z level, those procs will return the low and high corners (that is, with the lowest and highest x and y coordinates) at that z; otherwise they'll return the corner turfs with the lowest or highest z values.

You can easily get a list of turfs by using swapmap.AllTurfs(), which sends back all the turfs in the map or on one z-level. If you want to find all the turfs on one z-level, call swapmap.AllTurfs(z).

swapmap.Contains(turf/T) is useful for finding out whether a turf is even part of the map. This proc returns 1 if the turf belongs to the map, or 0 if it doesn't. This could also be called for objs and mobs, but it's not really safe for areas because it checks the x, y, and z values; an area's xyz coordinates are the lowest of all the turfs it contains, and so they might technically be on another map even if the area has turfs on this one.

swapmap.InUse() returns 1 if a mob with a key (a player or their leftover mob) is on the map, 0 if there are none.

To quickly build on your map, swapmap.BuildRectangle() and swapmap.BuildFilledRectangle() are very useful. Each should be called with two corner turfs, and a type to build:

proc/SetupBattleMap(n)
  var/swapmap/M = SwapMaps_Find("battle[n]")
  M.BuildRectangle(LoCorner(),\
                   HiCorner()),\
                   /turf/grass{density=1;opacity=1})
  M.BuildFilledRectangle(get_step(LoCorner(),NORTHEAST),\
                         get_step(HiCorner(),SOUTHWEST),\
                         /turf/grass)
The type you build in may be a turf, an obj, or a mob. (Areas are easier; just add a block of turfs to area.contents.) This can be used to quickly build walls for a house, or a stand of trees, or boundary walls as in this example. An open rectangle (not filled) will be open on every z level; in 3 dimensionsthis would look more like 4 sides of a box with no bottom or lid.

Another building proc is swapmap.BuildInTurfs(), which takes a list of turfs and the item type to build.

Template Maps

If you reuse a certain type of map often, like a battle map, one thing you can do to save time is to use SwapMaps_CreateFromTemplate(id), which lets you load a map as a template. A new map is created, but its id is not a text string; instead, it's the map itself. This map will never be saved; it can be deleted using del(), or by unloading it. Here's an example of how to use that.
proc/StartBattle(mob/M1, mob/M2)
  var/turf/T1 = M1.loc
  var/turf/T2 = M2.loc
  var/swapmap/battlemap = SwapMaps_CreateFromTemplate("arena")
  var/turf/T = locate(round(battlemap.x1+battlemap.x2)/2,\
                      round(battlemap.y1+battlemap.y2)/2,\
                      battlemap.z1)
  M1.loc = get_step(T, SOUTH)
  M1.dir = NORTH
  M2.loc = get_step(T, NORTH)
  M2.dir = SOUTH
  M1.inbattle=1
  M2.inbattle=1
  while(M1 && M2 && M1.HP>0 && M2.HP>0)
    M1.BattleTurn(M2)
    if(M2 && M2.HP>=0)
      M2.BattleTurn(M1)
      if(!M1 || M2.HP<=0) M2.loc = T2   // return winner to old location
    else
      M1.loc = T1                       // return winner to old location
  del(battlemap)
This snippet of code assumes that you've already got a map file called map_arena.sav. If this file exists, a copy of it is loaded and its size and features become the battle map. Multiple copies can be loaded from this template, so any number of players (basically limited only by memory) can do battle at once.

Caveat Cartographer

There are a few things you should be careful about when using this library:

Library Reference

User-Defined Vars

These vars should be set at runtime, in world/New() or wherever your initialization is done.

swapmaps_mode
The default save mode: .sav or .txt (plain text). If a file of the expected name isn't found when loading a map, but a different format is found, the other file will be loaded instead. When saved, that file will be saved in its original format regardless of this setting.
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.
swapmaps_iconcache
An associative list of icon files with names, like "player" = 'player.dmi'. When saved, icons using files in this list will be replaced with the string or name you choose, which can save space in your maps. Homemade icons can be added to the list via SwapMaps_AddIconToCache(name, icon), but if you load the map in another world or during another session, the icons will have to be re-created before the map is loaded.

Global 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 the id to a name via the SetID() proc.
SwapMaps_LoadChunk(id, turf/locorner)
Load a map file as a chunk, to be placed at a specific location on the existing game maps. The loaded map does not need to be unloaded and may be deleted. Returns nonzero if successful.
SwapMaps_SaveChunk(id, turf/corner1, turf/corner2)
Save a portion of the map as a chunk. This will become a new map file in its own right, using the id specified. Returns nonzero if successful.
SwapMaps_GetSize(id)
Check a map file to find its size, without loading it. Returns a list containing the x, y, and z sizes of the map if successful, or null if the map was not found.
SwapMaps_AddIconToCache(name, icon)
Cache an icon file by name for space-saving storage

/swapmap Vars

id
Usually this is a string identifying the map uniquely. The map will be saved as map_<id>.sav or map_<id>.txt depending on the mode. If id is set to src, the map will not be saved. This var should not be changed directly; use SetID(id) to change it.
x1, y1, z1
Minimum x, y, z coordinates. These are the same coordinates returned by the LoCorner() proc.
x2, y2, z2
Maximum x, y, z coordinates. These are the same coordinates returned by the HiCorner() proc.
tmp/locked
Save or load in progress.
tmp/mode
The save mode of this map; set at creation-time to swapmaps_mode, or when the map is loaded to the format of the file.
tmp/ischunk
This should not be changed directly. The var indicates that the map is a chunk for purposes of loading, unloading, saving, or deletion. A chunk does not allocate new map space, but instead loads over the turfs of an existing map. When deleted, the turfs will not be erased.

/swapmap Procs

new
Create the datum but initialize nothing. This map will be prepared for a load.
new(id, x, y, z)
Create a new map with the specified id, with a specific size in each dimension. Any or all of the dimensions may be omitted; the default size of a map is world.maxx×world.maxy×1. Space will be allocated for this map, but existing atoms in its boundaries will not be destroyed.
new(id, turf/corner1, turf/corner2)
Create a new map based on a region of the compiled-in game map. (If the corner turfs don't fall within the boundaries of the original game map, the datum will be deleted.) This map is used for saving only. On deletion or unloading, the atoms within this map will not be destroyed.
del()
When the datum is deleted, turfs and their contents will be deleted also, as well as any areas that were unique to this map. Space will be deallocated and the global map will be reduced in size if the space is not needed by other maps. The map is not saved; to save and delete, call Unload() instead.
Write(savefile/S)
An override of datum.Write() for saving.
Read(savefile/S)
An override of datum.Read() for loading.
Save()
Save this map.
Unload()
Save this map and delete.
SetID(id)
Change the id of the map.
AllTurfs(z)
Return all turfs on one z-level of the map, or on all z-levels if none is specified. z is in world coordinates, not local.
Contains(turf/T)
Return 1 if the turf is on this map, 0 if it is not. This is also save for objs and mobs, but not for areas.
InUse()
Return 1 if a mob with a key is on this map, 0 if none.
LoCorner(z=z1)
Return the turf with the lowest x and y coordinates on a given z level of this map, or with the lowest x, y, and z for the whole map. z is in world coordinates, not local.
HiCorner(z=z2)
Return the turf with the highest x and y coordinates on a given z level of this map, or with the highest x, y, and z for the whole map. z is in world coordinates, not local.
BuildFilledRectangle(turf/T1, turf/T2, item)
Build a filled rectangle of item from corner turf T1 to T2. item is a type path like /turf/wall or /obj/food{nutrients=2}. If the corner turfs are on different z-levels, a rectangle will be built on each level from T1 to T2.
BuildRectangle(turf/T1, turf/T2, item)
Build an unfilled rectangle of item from corner turf T1 to T2. item is a type path like /turf/wall or /obj/food{nutrients=2}. If the corner turfs are on different z-levels, a rectangle will be built on each level from T1 to T2.
BuildInTurfs(list/turfs, item)
Build item at every turf in the list turfs. The list doesn't strictly have to contain turfs. item is a type path like /turf/wall or /obj/food{nutrients=2}.

Overridden Procs

atom.Write(savefile/S)
This is overridden to allow for easy icon replacement, as well as to cull out empty lists and save no mobs with keys in a contents list.
atom.Read(savefile/S)
This is overridden to allow for easy icon replacement, as well as to add items to contents if needed.

Version History

Version 2.1

Version 2