rsc-landscape
(de)serialize runescape classic landscape files. parse the original land
and
maps
archives into a tile objects, dump PNGs, make changes and encode +
compress them back to an original archive.
a world map generated with rsc-landscape
comparison with jagex's world map
the official world map generated by jagex contains less detail due to GIF
palette compression, as well as clipped object symbols between sectors. it's
also missing some areas compared to the latest revision (gertrude's house,
digsite, shilo village, etc.).
install
$ npm install @2003scape/rsc-landscape
cli usage
rsc-landscape <command>
Commands:
rsc-landscape generate-map <archives> generate a world map image
rsc-landscape dump-json <archives> dump JSON files of each sector
rsc-landscape pack-json <directory> generate land and maps archives from a
directory of JSON files
Options:
--help Show help [boolean]
--version Show version number [boolean]
example
const fs = require('fs');
const { Landscape } = require('./src');
const landscape = new Landscape();
landscape.loadJag(fs.readFileSync('./land63.jag'),
fs.readFileSync('./maps63.jag'));
landscape.loadMem(fs.readFileSync('./land63.mem'),
fs.readFileSync('./maps63.mem'));
landscape.parseArchives();
const lumbridge = landscape.sectors[50][50][0];
process.stdout.write(lumbridge.toString(true));
fs.writeFileSync(`./sector-lumbridge.png`, lumbridge.toCanvas().toBuffer());
(async () => {
fs.writeFileSync(`./worldmap.png`, (await landscape.toCanvas({
points: require('./map-points.json'),
objects: require('./object-locs.json'),
labels: require('./map-labels.json')
})).toBuffer());
})();
file formats
the runescape classic world is separated into sectors, each containing 48x48
(2304) tiles. each sector has at least 1 file associated with it:
- a
.hei
file in the land archive which stores the elevation and colour of
each tile - a
.dat
file in the maps archive which stores the wall and object direction
of each tile - an optional
.loc
file in the maps archive which stores object IDs (used
for the login screen previews)
api
.terrainColours.integer
array of the original, undarkened 256 colours the client uses for each tile.
.terrainColours.rgb
array of 256 map colours used for each tile, darkened by 50% and converted to
rgb(r, g, b)
format.
.overlays
map of overlay names to overlay IDs.
.overlayColours
map of overlay names corresponding to the colour that should be used to
represent it on a map.
tile = new Tile({ sector, x, y, ... })
create a new sector tile. accepts all of the properties below in object.
tile.colour
number from 0-255 corresponding to colour in .terrainColours
.
tile.elevation
number from 0-255 describing the height of the tile.
tile.direction
number from 0-6 describing the direction objects should face on this tile.
tile.overlay
overlay type index. the corresponding names are stored in .overlays
.
tile.wall
object with the following potential properties:
{
diagonal: {
direction: '/' || '\\',
overlay: overlay
} || null,
vertical: overlay || 0,
horizontal: overlay || 0,
roof: roofOverlay || 0
}
tile.objectId
store an object here for the login screen previews.
tile.populate()
read buffers from tile's sector and populate its properties.
sector = new Sector({ x, y, plane, members?, tiles? })
create a new sector instance.
sector.members
should this be stored in the .jag
or .mem
?
sector.width
amount of tiles on the x axis (48).
sector.height
amount of tiles on the y axis (48).
sector.terrainHeight
sector.terrainColour
sector.wallsVertical
sector.wallsHorizontal
sector.wallsRoof
sector.tileDecoration
sector.tileDirection
Int8Array buffers populated from archive files with sector.parse*
or from
sector's tile objects with sector.populateBuffers()
. these buffers are
encoded + compressed into final file buffers.
sector.wallsDiagonal
Int32Array buffer, similar to above but 32-bit to store values > 255 (
potentially larger than 48000 if objects are stored).
sector.tiles[width][height]
2d array of tile objects. populate this field from the archive buffers with
sector.populateTiles()
, or populate the future archive buffers with
sector.populateBuffers()
.
sector.parseHei(buffer)
populate sector.terrainHeight
and sector.terrainColour
from a .hei
file.
sector.parseDat(buffer)
populate sector.walls*
, sector.tileDecoration
and sector.tileDirection
from a .dat
file.
sector.parseLoc(buffer)
populate sector.wallsDiagonal
with object IDs from a .loc
file.
sector.populateTiles()
populate sector.tiles
with a 2d array (48x48) of tile objects based on buffers
we parsed from archived files.
sector.populateBuffers()
populate future archive buffers (sector.terrain*
, sector.wall*
, etc.) with
sector.tiles
.
sector.getEntryName()
get the main portion of a landscape archive filename.
sector.toHei()
get a .hei
file buffer for this sector.
sector.toDat()
get a .dat
file buffer for this sector.
sector.toLoc()
get a .loc
file buffer for this sector (or null if no objects ID are stored).
sector.toCanvas(options, [ north, east, south, west ])
render an individual sector to a canvas. the second argument is optional if you
want to antialias the edges properly using the neighbouring sectors (world map
generation does this automatically).
in node, you can turn this into a PNG
with .toBuffer()
.
sector.toString(terminal = false)
if terminal
is true, return a nethack-esque terminal rendering of the sector:
...otherwise just return the name and size of the sector.
landscape = new Landscape()
generate a new landscape (de)serializer instance.
landscape.loadJag(landBuffer, mapBuffer)
landscape.loadMem(landBuffer, mapBuffer)
prepare .jag
and .mem
buffers to be parsed. any sectors loaded with
landscape.loadMem
will have sector.members = true
.
landscape.parseArchives()
populate landscape.sectors
with loaded buffers.
*landscape.getPopulatedSectors()
return an iterator of all the non-empty sectors.
landscape.getSectorNeighbours(x, y, plane)
return neighbours to a sector position as [north, east, south, west]
.
async landscape.toCanvas({ objects, points, labels })
create a world map image from all of the non-empty sectors.
objects
is an optional array of the following:
{
id: 1,
position: [x, y]
}
its x
and y
are multipled by the tile size.
points
is an optional array of the following:
{
type: 'altar',
x, y
}
each point image is 15x15.
labels
is an optional array of the following:
{
text: 'label\nfor\nsomething',
x, y,
size: 10,
align: 'center',
bold: true || undefined,
colour: 'rgb(254, 165, 0)' || undefined
}
license
Copyright 2019 2003Scape Team
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the
Free Software Foundation, either version 3 of the License, or (at your option)
any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along
with this program. If not, see http://www.gnu.org/licenses/.