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:
- Because the save and load operations sleep, nothing should be moving on a
map when it is saved.
- NPCs and other things that keep a var referencing a mob (or any obj or
turf for that matter) that may or may not be on the map (like a player)
should have that var replaced with something else, like the chracter's
name and player key. Otherwise, the mob may be saved to the file by
mistake, causing unpredictable results the next time the map is
loaded.
- It is possible for a map to be larger in the x and y dimensions than your
world. When this map is loaded, it will expand the size of your world,
and with it any other maps you may have. If you've planned for this
properly by giving all maps opaque boundaries then it won't be a problem,
but otherwise you should make sure all your swapmaps are only as big as
the x,y size of the maps you compiled into the game.
- Swapmaps that are smaller than the world map may end up placed alongside
each other. They should have a boundary of some kind, unless for whatever
reason you want players to be able to walk (and see) from one to the
other. Remember also that if you have any terrain-altering objects like
bombs, your map boundaries should be impervious to them.
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
- Fixed a bug in map placement.
Version 2
- Fixed a bug that was causing SwapMaps_CreateFromTemplate() to
function incorrectly.
- Added chunk support with SwapMaps_LoadChunk() and
SwapMaps_SaveChunk().
- Added SwapMaps_GetSize() to poll map sizes without loading
any maps.