├── .gitignore ├── land63.jag ├── land63.mem ├── maps63.jag ├── maps63.mem ├── doc ├── terminal.png ├── map-comparison.gif ├── worldmap-final.png ├── map-comparison2.gif └── map-comparison3.gif ├── res ├── key │ ├── altar.png │ ├── anvil.png │ ├── bank.png │ ├── bed.png │ ├── pub.png │ ├── quest.png │ ├── dungeon.png │ ├── furnace.png │ ├── tannery.png │ ├── apothecary.png │ ├── axe-shop.png │ ├── food-shop.png │ ├── gem-shop.png │ ├── kebab-shop.png │ ├── mace-shop.png │ ├── magic-shop.png │ ├── rare-trees.png │ ├── staff-shop.png │ ├── sword-shop.png │ ├── amulet-shop.png │ ├── archery-shop.png │ ├── clothes-shop.png │ ├── cookery-shop.png │ ├── fishing-shop.png │ ├── general-shop.png │ ├── helmet-shop.png │ ├── herblaw-shop.png │ ├── mining-site.png │ ├── shield-shop.png │ ├── silk-trader.png │ ├── combat-practice.png │ ├── crafting-shop.png │ ├── fishing-point.png │ ├── jewellery-shop.png │ ├── leg-armour-shop.png │ ├── pickable-lock.png │ ├── scimitar-shop.png │ ├── spinning-wheel.png │ ├── armour-conversion.png │ ├── body-armour-shop.png │ ├── skirt-armour-shop.png │ └── certificate-trader.png ├── key.json └── tile-overlays.json ├── src ├── index.js ├── terrain-colours.js ├── tile.js ├── landscape.js ├── map-painter.js ├── sector-painter.js ├── bin.js └── sector.js ├── package.json ├── example.js ├── README.md ├── map-labels.json ├── map-points.json └── COPYING /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /land63.jag: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/land63.jag -------------------------------------------------------------------------------- /land63.mem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/land63.mem -------------------------------------------------------------------------------- /maps63.jag: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/maps63.jag -------------------------------------------------------------------------------- /maps63.mem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/maps63.mem -------------------------------------------------------------------------------- /doc/terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/doc/terminal.png -------------------------------------------------------------------------------- /res/key/altar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/altar.png -------------------------------------------------------------------------------- /res/key/anvil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/anvil.png -------------------------------------------------------------------------------- /res/key/bank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/bank.png -------------------------------------------------------------------------------- /res/key/bed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/bed.png -------------------------------------------------------------------------------- /res/key/pub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/pub.png -------------------------------------------------------------------------------- /res/key/quest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/quest.png -------------------------------------------------------------------------------- /res/key/dungeon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/dungeon.png -------------------------------------------------------------------------------- /res/key/furnace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/furnace.png -------------------------------------------------------------------------------- /res/key/tannery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/tannery.png -------------------------------------------------------------------------------- /doc/map-comparison.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/doc/map-comparison.gif -------------------------------------------------------------------------------- /doc/worldmap-final.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/doc/worldmap-final.png -------------------------------------------------------------------------------- /res/key/apothecary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/apothecary.png -------------------------------------------------------------------------------- /res/key/axe-shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/axe-shop.png -------------------------------------------------------------------------------- /res/key/food-shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/food-shop.png -------------------------------------------------------------------------------- /res/key/gem-shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/gem-shop.png -------------------------------------------------------------------------------- /res/key/kebab-shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/kebab-shop.png -------------------------------------------------------------------------------- /res/key/mace-shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/mace-shop.png -------------------------------------------------------------------------------- /res/key/magic-shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/magic-shop.png -------------------------------------------------------------------------------- /res/key/rare-trees.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/rare-trees.png -------------------------------------------------------------------------------- /res/key/staff-shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/staff-shop.png -------------------------------------------------------------------------------- /res/key/sword-shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/sword-shop.png -------------------------------------------------------------------------------- /doc/map-comparison2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/doc/map-comparison2.gif -------------------------------------------------------------------------------- /doc/map-comparison3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/doc/map-comparison3.gif -------------------------------------------------------------------------------- /res/key/amulet-shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/amulet-shop.png -------------------------------------------------------------------------------- /res/key/archery-shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/archery-shop.png -------------------------------------------------------------------------------- /res/key/clothes-shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/clothes-shop.png -------------------------------------------------------------------------------- /res/key/cookery-shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/cookery-shop.png -------------------------------------------------------------------------------- /res/key/fishing-shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/fishing-shop.png -------------------------------------------------------------------------------- /res/key/general-shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/general-shop.png -------------------------------------------------------------------------------- /res/key/helmet-shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/helmet-shop.png -------------------------------------------------------------------------------- /res/key/herblaw-shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/herblaw-shop.png -------------------------------------------------------------------------------- /res/key/mining-site.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/mining-site.png -------------------------------------------------------------------------------- /res/key/shield-shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/shield-shop.png -------------------------------------------------------------------------------- /res/key/silk-trader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/silk-trader.png -------------------------------------------------------------------------------- /res/key/combat-practice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/combat-practice.png -------------------------------------------------------------------------------- /res/key/crafting-shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/crafting-shop.png -------------------------------------------------------------------------------- /res/key/fishing-point.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/fishing-point.png -------------------------------------------------------------------------------- /res/key/jewellery-shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/jewellery-shop.png -------------------------------------------------------------------------------- /res/key/leg-armour-shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/leg-armour-shop.png -------------------------------------------------------------------------------- /res/key/pickable-lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/pickable-lock.png -------------------------------------------------------------------------------- /res/key/scimitar-shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/scimitar-shop.png -------------------------------------------------------------------------------- /res/key/spinning-wheel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/spinning-wheel.png -------------------------------------------------------------------------------- /res/key/armour-conversion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/armour-conversion.png -------------------------------------------------------------------------------- /res/key/body-armour-shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/body-armour-shop.png -------------------------------------------------------------------------------- /res/key/skirt-armour-shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/skirt-armour-shop.png -------------------------------------------------------------------------------- /res/key/certificate-trader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2003scape/rsc-landscape/HEAD/res/key/certificate-trader.png -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | module.exports.Landscape = require('./landscape'); 2 | module.exports.Sector = require('./sector'); 3 | module.exports.Tile = require('./tile'); 4 | module.exports.key = require('../res/key'); 5 | module.exports.tileOverlays = require('../res/tile-overlays'); 6 | -------------------------------------------------------------------------------- /res/key.json: -------------------------------------------------------------------------------- 1 | [ 2 | "altar", "amulet-shop", "anvil", "apothecary", "archery-shop", 3 | "armour-conversion", "axe-shop", "bank", "bed", "body-armour-shop", 4 | "certificate-trader", "clothes-shop", "combat-practice", "cookery-shop", 5 | "crafting-shop", "dungeon", "fishing-point", "fishing-shop", "food-shop", 6 | "furnace", "gem-shop", "general-shop", "helmet-shop", "herblaw-shop", 7 | "jewellery-shop", "kebab-shop", "leg-armour-shop", "mace-shop", 8 | "magic-shop", "mining-site", "pickable-lock", "pub", "quest", 9 | "rare-trees", "scimitar-shop", "shield-shop", "silk-trader", 10 | "skirt-armour-shop", "spinning-wheel", "staff-shop", "sword-shop", "tannery" 11 | ] 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@2003scape/rsc-landscape", 3 | "version": "2.0.1", 4 | "description": "(de)serialize runescape classic landscape files", 5 | "main": "./src/index.js", 6 | "bin": { 7 | "rsc-landscape": "./src/bin.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/2003scape/rsc-landscape.git" 12 | }, 13 | "keywords": [ 14 | "runescape", 15 | "rsc", 16 | "landscape", 17 | "archive", 18 | "serialize", 19 | "png" 20 | ], 21 | "author": "2003Scape Team", 22 | "license": "AGPL-3.0+", 23 | "bugs": { 24 | "url": "https://github.com/2003scape/rsc-landscape/issues" 25 | }, 26 | "homepage": "https://github.com/2003scape/rsc-landscape#readme", 27 | "dependencies": { 28 | "@2003scape/rsc-archiver": "^1.1.1", 29 | "canvas": "^2.6.1", 30 | "chalk": "^4.1.0", 31 | "color-functions": "^3.0.1", 32 | "mkdirp-promise": "^5.0.1", 33 | "yargs": "^16.1.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const { Landscape } = require('./src'); 3 | 4 | const landscape = new Landscape(); 5 | 6 | landscape.loadJag(fs.readFileSync('./land63.jag'), 7 | fs.readFileSync('./maps63.jag')); 8 | landscape.loadMem(fs.readFileSync('./land63.mem'), 9 | fs.readFileSync('./maps63.mem')); 10 | 11 | landscape.parseArchives(); 12 | 13 | const lumbridge = landscape.sectors[50][50][0]; 14 | 15 | const tile = lumbridge.tiles[0][0]; 16 | console.log(tile.colour, tile.getGameCoords()); 17 | 18 | const tile2 = landscape.getTileAtGameCoords(126, 1468); 19 | console.log(tile2.getTileDef()); 20 | 21 | process.stdout.write(lumbridge.toString(true)); 22 | fs.writeFileSync(`./sector-lumbridge.png`, lumbridge.toCanvas().toBuffer()); 23 | 24 | (async () => { 25 | fs.writeFileSync('./worldmap.png', (await landscape.toCanvas({ 26 | points: require('./map-points.json'), 27 | objects: require('./object-locs.json'), 28 | labels: require('./map-labels.json') 29 | })).toBuffer()); 30 | })(); 31 | -------------------------------------------------------------------------------- /src/terrain-colours.js: -------------------------------------------------------------------------------- 1 | const { cssColor, rgb2hsv, hsv2hex } = require('color-functions'); 2 | 3 | // number of different colours used in floor tiles 4 | const MAX_TERRAIN_COLOURS = 256; 5 | 6 | const terrainColours = { 7 | integer: new Int32Array(MAX_TERRAIN_COLOURS), 8 | rgb: [] 9 | }; 10 | 11 | function rgbToInt(r, g, b) { 12 | return -1 - ((r / 8) | 0) * 1024 - ((g / 8) | 0) * 32 - ((b / 8) | 0); 13 | } 14 | 15 | for (let i = 0; i < 64; i += 1) { 16 | const r = 255 - i * 4; 17 | const g = 255 - ((i * 1.75) | 0); 18 | const b = 255 - i * 4 ; 19 | 20 | terrainColours.rgb.push(`rgb(${r}, ${g}, ${b})`); 21 | terrainColours.integer[i] = rgbToInt(r, b, g); 22 | } 23 | 24 | for (let i = 0; i < 64; i += 1) { 25 | const r = i * 3; 26 | const g = 144; 27 | const b = 0; 28 | 29 | terrainColours.rgb.push(`rgb(${r}, ${g}, ${b})`); 30 | terrainColours.integer[i + 64] = rgbToInt(r, g, b); 31 | } 32 | 33 | for (let i = 0; i < 64; i += 1) { 34 | const r = 192 - ((i * 1.5) | 0); 35 | const g = 144 - ((i * 1.5) | 0); 36 | const b = 0; 37 | 38 | terrainColours.rgb.push(`rgb(${r}, ${g}, ${b})`); 39 | terrainColours.integer[i + 128] = rgbToInt(r, g, b); 40 | } 41 | 42 | for (let l = 0; l < 64; l++) { 43 | const r = 96 - ((l * 1.5) | 0); 44 | const g = 48 + ((l * 1.5) | 0); 45 | const b = 0; 46 | 47 | terrainColours.rgb.push(`rgb(${r}, ${g}, ${b})`); 48 | terrainColours.integer[l + 192] = rgbToInt(r, g, b); 49 | } 50 | 51 | // terrainColours are generated in the same manner as the client, but the client 52 | // applies lighting which darkens them. comparing with the original map, 53 | // halving the lightness (in HSL) results in the same colours as the 54 | // minimap/world map. 55 | terrainColours.rgb = terrainColours.rgb.map(css => { 56 | const rgb = cssColor(css); 57 | const hsv = rgb2hsv(rgb.r, rgb.g, rgb.b); 58 | hsv.v = Math.floor(hsv.v / 2); 59 | 60 | return hsv2hex(hsv.h, hsv.s, hsv.v); 61 | }); 62 | 63 | module.exports = terrainColours; 64 | -------------------------------------------------------------------------------- /src/tile.js: -------------------------------------------------------------------------------- 1 | const terrainColours = require('./terrain-colours'); 2 | const tileOverlays = require('../res/tile-overlays'); 3 | 4 | const BLACK = 'rgb(0, 0, 0)'; 5 | 6 | const PLANE_HEIGHT = 944; 7 | 8 | // offset for \ diagonals (as opposed to / which are 0-12000) 9 | const NW_SE_OFFSET = 12000; 10 | 11 | // offset in diagonals? for storing entity IDs on particular tiles 12 | const OBJECT_OFFSET = 48000; 13 | const ITEM_OFFSET = 36000; 14 | const NPC_OFFSET = 24000; 15 | 16 | class Tile { 17 | constructor(tile) { 18 | this.sector = tile.sector; 19 | this.x = tile.x; 20 | this.y = tile.y; 21 | 22 | this.index = tile.x * this.sector.width + tile.y; 23 | 24 | this.colour = tile.colour || 0; 25 | this.elevation = tile.elevation || 0; 26 | this.direction = tile.direction || 0; 27 | this.overlay = tile.overlay || 0; 28 | this.wall = { 29 | diagonal: tile.wall ? tile.wall.diagonal || null : null, 30 | horizontal: tile.wall ? tile.wall.horizontal || null : null, 31 | roof: tile.wall ? tile.wall.roof || null : null, 32 | vertical: tile.wall ? tile.wall.vertical || null : null 33 | }; 34 | this.objectId = 35 | typeof tile.objectId === 'undefined' ? null : tile.objectId; 36 | this.itemId = typeof tile.itemId === 'undefined' ? null : tile.itemId; 37 | this.npcId = typeof tile.npcId === 'undefined' ? null : tile.npcId; 38 | } 39 | 40 | // read the appropriate buffers in Sector and set our internal variables 41 | populate() { 42 | let diagonal = this.sector.wallsDiagonal[this.index]; 43 | 44 | if (diagonal > 48000) { 45 | this.objectId = diagonal - 48001; 46 | diagonal = null; 47 | } else if (diagonal > 36000) { 48 | this.itemId = diagonal - 36001; 49 | diagonal = null; 50 | } else if (diagonal > 24000) { 51 | this.npcId = diagonal - 24001; 52 | diagonal = null; 53 | } else if (diagonal > 12000) { 54 | diagonal = { 55 | direction: '\\', 56 | overlay: diagonal - 12000 57 | }; 58 | } else if (diagonal > 0) { 59 | diagonal = { 60 | direction: '/', 61 | overlay: diagonal 62 | }; 63 | } else { 64 | diagonal = null; 65 | } 66 | 67 | this.colour = this.sector.terrainColour[this.index] & 0xff; 68 | this.elevation = this.sector.terrainHeight[this.index] & 0xff; 69 | this.direction = this.sector.tileDirection[this.index]; 70 | this.overlay = this.sector.tileDecoration[this.index] & 0xff; 71 | this.wall = { 72 | diagonal, 73 | vertical: this.sector.wallsVertical[this.index] & 0xff || null, 74 | horizontal: this.sector.wallsHorizontal[this.index] & 0xff || null, 75 | roof: this.sector.wallsRoof[this.index] & 0xff || null 76 | }; 77 | } 78 | 79 | // return the map colour for this tile 80 | getTerrainColour() { 81 | const plane = this.sector.plane; 82 | 83 | if (plane === 0 || plane === 3) { 84 | return terrainColours.rgb[this.colour]; 85 | } else { 86 | return BLACK; 87 | } 88 | } 89 | 90 | // get the overlay tile def 91 | getTileDef() { 92 | return tileOverlays[this.overlay] || {}; 93 | } 94 | 95 | // return the {x, y} the game would use for this tile 96 | getGameCoords() { 97 | const x = this.x + (this.sector.x - 48) * 48; 98 | const y = 99 | (this.sector.y - 36) * 48 + 100 | this.y + 101 | 96 - 102 | 144 + 103 | this.sector.plane * PLANE_HEIGHT; 104 | 105 | return { x, y }; 106 | } 107 | 108 | toJSON() { 109 | return { 110 | colour: this.colour, 111 | elevation: this.elevation, 112 | direction: this.direction, 113 | overlay: this.overlay, 114 | wall: this.wall, 115 | objectId: this.objectId, 116 | itemId: this.itemId, 117 | npcId: this.npcId 118 | }; 119 | } 120 | 121 | toString() { 122 | return ( 123 | `[object ${this.constructor.name} ` + 124 | `${this.sector.getEntryName()} ${this.index}]` 125 | ); 126 | } 127 | } 128 | 129 | Tile.NW_SE_OFFSET = NW_SE_OFFSET; 130 | Tile.OBJECT_OFFSET = OBJECT_OFFSET; 131 | Tile.ITEM_OFFSET = ITEM_OFFSET; 132 | Tile.NPC_OFFSET = NPC_OFFSET; 133 | 134 | module.exports = Tile; 135 | -------------------------------------------------------------------------------- /res/tile-overlays.json: -------------------------------------------------------------------------------- 1 | { 2 | "1": { 3 | "name": "road", 4 | "blocked": false, 5 | "bridge": false, 6 | "indoors": false, 7 | "antialias": true, 8 | "colour": "rgb(64, 64, 64)" 9 | }, 10 | "2": { 11 | "name": "water", 12 | "blocked": true, 13 | "bridge": false, 14 | "indoors": false, 15 | "antialias": true, 16 | "colour": "rgb(36, 64, 127)" 17 | }, 18 | "3": { 19 | "name": "brown_floor", 20 | "blocked": false, 21 | "bridge": false, 22 | "indoors": true, 23 | "antialias": false, 24 | "colour": "rgb(100, 48, 2)" 25 | }, 26 | "4": { 27 | "name": "bridge", 28 | "blocked": false, 29 | "bridge": true, 30 | "indoors": false, 31 | "antialias": false, 32 | "colour": "rgb(100, 48, 2)" 33 | }, 34 | "5": { 35 | "name": "stone_floor", 36 | "blocked": false, 37 | "bridge": false, 38 | "indoors": true, 39 | "antialias": false, 40 | "colour": "rgb(64, 64, 64)" 41 | }, 42 | "6": { 43 | "name": "maroon_floor", 44 | "blocked": false, 45 | "bridge": false, 46 | "indoors": true, 47 | "antialias": false, 48 | "colour": "rgb(107, 3, 15)" 49 | }, 50 | "7": { 51 | "name": "swamp_water", 52 | "blocked": true, 53 | "bridge": false, 54 | "indoors": false, 55 | "antialias": true, 56 | "colour": "rgb(34, 48, 58)" 57 | }, 58 | "8": { 59 | "name": "hole", 60 | "blocked": true, 61 | "bridge": false, 62 | "indoors": false, 63 | "antialias": false, 64 | "colour": "rgb(0, 0, 0)" 65 | }, 66 | "9": { 67 | "name": "mountain", 68 | "blocked": true, 69 | "bridge": false, 70 | "indoors": false, 71 | "antialias": true, 72 | "colour": "rgb(97, 97, 97)" 73 | }, 74 | "10": { 75 | "name": "black", 76 | "blocked": true, 77 | "bridge": false, 78 | "indoors": false, 79 | "antialias": false, 80 | "colour": "rgb(255, 0, 255)" 81 | }, 82 | "11": { 83 | "name": "lava", 84 | "blocked": true, 85 | "bridge": false, 86 | "indoors": false, 87 | "antialias": true, 88 | "colour": "rgb(96, 48, 1)" 89 | }, 90 | "12": { 91 | "name": "bridge_2", 92 | "blocked": false, 93 | "bridge": true, 94 | "indoors": false, 95 | "antialias": false, 96 | "colour": "rgb(100, 48, 2)" 97 | }, 98 | "13": { 99 | "name": "blue_floor", 100 | "blocked": false, 101 | "bridge": false, 102 | "indoors": true, 103 | "antialias": false, 104 | "colour": "rgb(31, 63, 125)" 105 | }, 106 | "14": { 107 | "name": "pentagram", 108 | "blocked": false, 109 | "bridge": false, 110 | "indoors": true, 111 | "antialias": false, 112 | "colour": "rgb(64, 64, 64)" 113 | }, 114 | "15": { 115 | "name": "purple_floor", 116 | "blocked": false, 117 | "bridge": false, 118 | "indoors": true, 119 | "antialias": false, 120 | "colour": "rgb(36, 22, 9)" 121 | }, 122 | "16": { 123 | "name": "black_floor", 124 | "blocked": false, 125 | "bridge": false, 126 | "indoors": true, 127 | "antialias": false, 128 | "colour": "rgb(0, 0, 0)" 129 | }, 130 | "17": { 131 | "name": "light_stone_floor", 132 | "blocked": false, 133 | "bridge": false, 134 | "indoors": true, 135 | "antialias": false, 136 | "colour": "rgb(121, 123, 120)" 137 | }, 138 | "18": { 139 | "name": "object_platform", 140 | "blocked": false, 141 | "bridge": false, 142 | "indoors": false, 143 | "antialias": false, 144 | "colour": "rgb(0, 0, 0)" 145 | }, 146 | "19": { 147 | "name": "black_2", 148 | "blocked": true, 149 | "bridge": false, 150 | "indoors": false, 151 | "antialias": false, 152 | "colour": "rgb(0, 0, 0)" 153 | }, 154 | "20": { 155 | "name": "object_platform_2", 156 | "blocked": false, 157 | "bridge": true, 158 | "indoors": false, 159 | "antialias": false, 160 | "colour": "rgb(0, 0, 0)" 161 | }, 162 | "21": { 163 | "name": "log", 164 | "blocked": false, 165 | "bridge": true, 166 | "indoors": false, 167 | "antialias": false, 168 | "colour": "rgb(0, 0, 0)" 169 | }, 170 | "23": { 171 | "name": "sand_floor", 172 | "blocked": false, 173 | "bridge": false, 174 | "indoors": true, 175 | "antialias": false, 176 | "colour": "rgb(63, 33, 0)" 177 | }, 178 | "24": { 179 | "name": "mud_floor", 180 | "blocked": false, 181 | "bridge": false, 182 | "indoors": true, 183 | "antialias": false, 184 | "colour": "rgb(63, 33, 0)" 185 | }, 186 | "25": { 187 | "name": "water_floor", 188 | "blocked": false, 189 | "bridge": false, 190 | "indoors": false, 191 | "antialias": false, 192 | "colour": "rgb(63, 33, 0)" 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/landscape.js: -------------------------------------------------------------------------------- 1 | const MapPainter = require('./map-painter'); 2 | const Sector = require('./sector'); 3 | const { JagArchive, hashFilename } = require('@2003scape/rsc-archiver'); 4 | 5 | const MAX_X_SECTORS = 65; 6 | const MAX_Y_SECTORS = 56; 7 | 8 | // ground, first floor, second floor, dungeon/basement 9 | const MAX_PLANES = 4; 10 | 11 | class Landscape { 12 | constructor() { 13 | this.width = MAX_X_SECTORS; 14 | this.height = MAX_Y_SECTORS; 15 | this.depth = MAX_PLANES; 16 | 17 | this.minRegionX = 48; 18 | this.minRegionY = 37; 19 | this.maxRegionX = null; 20 | this.maxRegionY = null; 21 | 22 | this.landArchives = []; 23 | this.mapArchives = []; 24 | 25 | this.initSectors(); 26 | } 27 | 28 | // initialize the multidimensional sector array 29 | initSectors() { 30 | this.sectors = []; 31 | 32 | for (let i = 0; i < MAX_X_SECTORS; i += 1) { 33 | this.sectors.push([]); 34 | 35 | for (let j = 0; j < MAX_Y_SECTORS; j += 1) { 36 | this.sectors[i].push([]); 37 | 38 | for (let k = 0; k < MAX_PLANES; k += 1) { 39 | this.sectors[i][j].push(null); 40 | } 41 | } 42 | } 43 | } 44 | 45 | loadArchive(landBuffer, mapBuffer, members = false) { 46 | // TODO JagArchive.fromArchive? 47 | const landArchive = new JagArchive(); 48 | landArchive.readArchive(landBuffer); 49 | 50 | const mapArchive = new JagArchive(); 51 | mapArchive.readArchive(mapBuffer); 52 | 53 | this.landArchives.push({ archive: landArchive, members }); 54 | this.mapArchives.push({ archive: mapArchive, members }); 55 | } 56 | 57 | loadJmJag(mapBuffer) { 58 | this.jmJag = true; 59 | 60 | const mapArchive = new JagArchive(); 61 | mapArchive.readArchive(mapBuffer); 62 | 63 | this.mapArchives.push({ archive: mapArchive, members: false }); 64 | } 65 | 66 | // load f2p maps 67 | loadJag(landBuffer, mapBuffer) { 68 | this.jmJag = false; 69 | this.loadArchive(landBuffer, mapBuffer, false); 70 | } 71 | 72 | // load p2p maps 73 | loadMem(landBuffer, mapBuffer) { 74 | this.jmJag = false; 75 | this.loadArchive(landBuffer, mapBuffer, true); 76 | } 77 | 78 | // populate our internal structures 79 | parseArchives() { 80 | for (let plane = 0; plane < MAX_PLANES; plane += 1) { 81 | for (let y = this.minRegionY; y < MAX_Y_SECTORS; y += 1) { 82 | for (let x = this.minRegionX; x < MAX_X_SECTORS; x += 1) { 83 | const sector = new Sector({ x, y, plane }); 84 | const entry = sector.getEntryName(); 85 | 86 | if (this.jmJag) { 87 | const hash = hashFilename(`${entry}.jm`); 88 | 89 | for (const { members, archive } of this.mapArchives) { 90 | if (archive.entries.has(hash)) { 91 | sector.parseJm(archive.getEntry(`${entry}.jm`)); 92 | sector.members = members; 93 | } 94 | } 95 | } else { 96 | for (const { members, archive } of this.landArchives) { 97 | const hash = hashFilename(`${entry}.hei`); 98 | 99 | if (archive.entries.has(hash)) { 100 | sector.parseHei( 101 | archive.getEntry(`${entry}.hei`) 102 | ); 103 | 104 | sector.members = members; 105 | } 106 | } 107 | 108 | for (const { members, archive } of this.mapArchives) { 109 | const datHash = hashFilename(`${entry}.dat`); 110 | 111 | if (archive.entries.has(datHash)) { 112 | sector.parseDat( 113 | archive.getEntry(`${entry}.dat`) 114 | ); 115 | 116 | sector.members = members; 117 | } 118 | 119 | const locHash = hashFilename(`${entry}.loc`); 120 | 121 | if (archive.entries.has(locHash)) { 122 | sector.parseLoc( 123 | archive.getEntry(`${entry}.loc`) 124 | ); 125 | } 126 | } 127 | } 128 | 129 | sector.populateTiles(); 130 | 131 | if (!sector.empty) { 132 | this.sectors[x][y][plane] = sector; 133 | 134 | if (x > this.maxRegionX) { 135 | this.maxRegionX = x; 136 | } 137 | 138 | if (y > this.maxRegionY) { 139 | this.maxRegionY = y; 140 | } 141 | } 142 | } 143 | } 144 | } 145 | } 146 | 147 | // get any sector with any non-zero attributes 148 | *getPopulatedSectors() { 149 | for (let i = this.minRegionX; i < this.maxRegionX; i += 1) { 150 | for (let j = this.minRegionY; j < this.maxRegionY; j += 1) { 151 | for (let k = 0; k < MAX_PLANES; k += 1) { 152 | const sector = this.sectors[i][j][k]; 153 | 154 | if (sector && !sector.empty) { 155 | yield sector; 156 | } 157 | } 158 | } 159 | } 160 | } 161 | 162 | // grab the NESW sectors of another sector (if they exist). used for 163 | // superior world map antialiasing. 164 | getSectorNeighbours(x, y, plane) { 165 | let [north, east, south, west] = [null, null, null, null]; 166 | 167 | if (y - 1 >= 0) { 168 | north = this.sectors[x][y - 1][plane]; 169 | } 170 | 171 | if (x - 1 >= 0) { 172 | east = this.sectors[x - 1][y][plane]; 173 | } 174 | 175 | if (y + 1 < this.sectors[x].length) { 176 | south = this.sectors[x][y + 1][plane]; 177 | } 178 | 179 | if (x + 1 < this.sectors.length) { 180 | west = this.sectors[x + 1][y][plane]; 181 | } 182 | 183 | return [north, east, south, west]; 184 | } 185 | 186 | // convert game coordinates to tile 187 | getTileAtGameCoords(x, y) { 188 | let plane = 0; 189 | 190 | if (y >= 0 && y <= 1007) { 191 | plane = 0; // overworld 192 | } else if (y >= 1007 && y <= 1007 + 943) { 193 | plane = 1; // first floor 194 | y -= 943; 195 | } else if (y >= 1008 + 943 && y <= 1007 + 2 * 943) { 196 | plane = 2; // second floor 197 | y -= 943 * 2; 198 | } else { 199 | plane = 3; // dungeon 200 | y -= 943 * 3; 201 | } 202 | 203 | const sectorX = Math.floor(x / 48) + this.minRegionX; 204 | const sectorY = Math.floor(y / 48) + this.minRegionY; 205 | 206 | const sector = this.sectors[sectorX][sectorY][plane]; 207 | const tile = sector.tiles[47 - (x % 48)][y % 48]; 208 | 209 | return tile; 210 | } 211 | 212 | async toCanvas(options = {}) { 213 | const painter = new MapPainter(this, options); 214 | await painter.draw(); 215 | 216 | return painter.canvas; 217 | } 218 | 219 | toString() { 220 | return ( 221 | `[object ${this.constructor.name} ${this.width}x` + 222 | `${this.height}x${this.depth}]` 223 | ); 224 | } 225 | } 226 | 227 | module.exports = Landscape; 228 | -------------------------------------------------------------------------------- /src/map-painter.js: -------------------------------------------------------------------------------- 1 | // paint the final world map images, combining sector images 2 | 3 | const key = require('../res/key'); 4 | const path = require('path'); 5 | const { Image, createCanvas } = require('canvas'); 6 | 7 | const TILE_SIZE = 3; 8 | 9 | // the orange + symbol colour used to indicate game objects 10 | const OBJECT_COLOUR = 'rgb(175, 95, 0)'; 11 | // only regular/evergreen trees outside of the wild are this colour 12 | const TREE_COLOUR = 'rgb(0, 160, 0)'; 13 | // objects like dead trees and fungus are darker than rocks/signs in the wild 14 | const WILD_TREE_COLOUR = 'rgb(112, 64, 0)'; 15 | // objects in the wild intended to be WILD_TREE_COLOUR 16 | const WILD_SCENERY = [4, 38, 70, 205]; 17 | 18 | const WATER_COLOUR = 'rgb(36, 64, 127)'; 19 | 20 | const FONT = 'Arial'; 21 | 22 | function inWilderness(x, y) { 23 | return x >= 1440 && x <= 2304 && y >= 286 && y <= 1286; 24 | } 25 | 26 | class MapPainter { 27 | constructor(landscape, options) { 28 | this.landscape = landscape; 29 | 30 | this.options = {}; 31 | this.points = options.points && options.points.length ? 32 | options.points : []; 33 | this.objects = options.objects && options.objects.length ? 34 | options.objects : []; 35 | this.labels = options.labels && options.labels.length ? 36 | options.labels : []; 37 | this.plane = options.plane || 0; 38 | 39 | const width = this.landscape.maxRegionX - this.landscape.minRegionX + 1; 40 | const height = 41 | this.landscape.maxRegionY - this.landscape.minRegionY + 1; 42 | 43 | const firstSector = landscape.getPopulatedSectors().next().value; 44 | 45 | this.sectorWidth = firstSector.tiles.length; 46 | this.sectorHeight = firstSector.tiles[0].length; 47 | 48 | this.imageWidth = (this.sectorWidth * TILE_SIZE) * width; 49 | this.imageHeight = (this.sectorHeight * TILE_SIZE * height); 50 | 51 | this.canvas = createCanvas(this.imageWidth, this.imageHeight); 52 | this.ctx = this.canvas.getContext('2d'); 53 | 54 | // change the offset for positions of labels, objects and points based 55 | // on plane 56 | const maxY = this.landscape.maxRegionY; 57 | const minY = this.landscape.minRegionY; 58 | 59 | this.yOffset = 60 | (this.plane * this.sectorHeight * TILE_SIZE * (maxY - minY)) + 61 | (this.plane * 240); 62 | } 63 | 64 | // load the key/legend images for points of interest 65 | async loadKeyImages() { 66 | this.keyImages = new Map(); 67 | 68 | for (const type of key) { 69 | await (new Promise((resolve, reject) => { 70 | const img = new Image(); 71 | img.onerror = reject; 72 | img.onload = resolve; 73 | img.src = path.resolve('res/key', `${type}.png`); 74 | this.keyImages.set(type, img); 75 | })); 76 | } 77 | } 78 | 79 | // render each sector (or water/black sector for empties) and plot them 80 | // on our map canvas 81 | drawSectors() { 82 | const maxX = this.landscape.maxRegionX; 83 | const minX = this.landscape.minRegionX; 84 | const maxY = this.landscape.maxRegionY; 85 | const minY = this.landscape.minRegionY; 86 | 87 | // absolute x/y position of the next sector 88 | let x = 0; 89 | let y = 0; 90 | 91 | for (let i = maxX; i >= minX; i -= 1) { 92 | for (let j = minY; j <= maxY; j += 1) { 93 | const sector = this.landscape.sectors[i][j][this.plane]; 94 | 95 | if (!sector || sector.empty) { 96 | this.ctx.fillStyle = 97 | this.plane === 0 ? WATER_COLOUR : '#000'; 98 | this.ctx.fillRect(x, y, this.sectorWidth * TILE_SIZE, 99 | this.sectorHeight * TILE_SIZE); 100 | } else { 101 | const sectorCanvas = sector.toCanvas(this.options, 102 | this.landscape.getSectorNeighbours(i, j, this.plane)); 103 | this.ctx.drawImage(sectorCanvas, x, y); 104 | } 105 | 106 | y += this.sectorHeight * TILE_SIZE; 107 | } 108 | 109 | x += this.sectorWidth * TILE_SIZE; 110 | y = 0; 111 | } 112 | } 113 | 114 | // draw a + for each game object (and make them green if they're trees) 115 | drawObjects() { 116 | for (let { id, position: [x, y]} of this.objects) { 117 | x *= TILE_SIZE; 118 | x = this.imageWidth - x - 2; 119 | y *= TILE_SIZE; 120 | y -= 1; 121 | 122 | y -= this.yOffset; 123 | 124 | if (inWilderness(x, y)) { 125 | if (WILD_SCENERY.indexOf(id) > -1) { 126 | this.ctx.fillStyle = WILD_TREE_COLOUR; 127 | } else { 128 | this.ctx.fillStyle = OBJECT_COLOUR; 129 | } 130 | } else { 131 | this.ctx.fillStyle = id <= 1 ? TREE_COLOUR : OBJECT_COLOUR; 132 | } 133 | 134 | this.ctx.fillRect(x + 1, y, 1, 3); 135 | this.ctx.fillRect(x, y + 1, 3, 1); 136 | } 137 | } 138 | 139 | // draw the points of interest from the key/legend images 140 | async drawPoints() { 141 | await this.loadKeyImages(); 142 | 143 | for (let { type, x, y } of this.points) { 144 | x -= this.landscape.minRegionX * this.sectorWidth * TILE_SIZE; 145 | y -= this.landscape.minRegionY * this.sectorHeight * TILE_SIZE; 146 | 147 | y -= this.yOffset; 148 | 149 | this.ctx.drawImage(this.keyImages.get(type), x, y); 150 | } 151 | } 152 | 153 | // draw labels for city, building, province, etc. 154 | drawLabels() { 155 | for (const label of this.labels) { 156 | let [x, y] = [label.x, label.y]; 157 | 158 | x -= this.landscape.minRegionX * this.sectorWidth * TILE_SIZE; 159 | y -= this.landscape.minRegionY * this.sectorHeight * TILE_SIZE; 160 | 161 | y -= this.yOffset; 162 | 163 | let sizeInc = 3; 164 | 165 | if (label.bold) { 166 | sizeInc += 2; 167 | } 168 | 169 | label.size += sizeInc; 170 | y -= sizeInc; 171 | 172 | this.ctx.fillStyle = label.colour || '#fff'; 173 | this.ctx.font = 174 | `${label.bold ? 'bold ' : ''} ${label.size}px ${FONT}`; 175 | this.ctx.textBaseline = 'top'; 176 | 177 | if (label.align === 'center') { 178 | // split the text by \n, calculate width for each chunk, find 179 | // the largest and display the others centred relatively 180 | const chunks = label.text.split('\n'); 181 | const chunkWidths = new Map(); 182 | let widestChunk = 0; 183 | 184 | for (const text of chunks) { 185 | const width = this.ctx.measureText(text).width; 186 | chunkWidths.set(text, width); 187 | 188 | if (width > widestChunk) { 189 | widestChunk = width; 190 | } 191 | } 192 | 193 | let yOff = 0; 194 | 195 | for (const [ text, width ] of chunkWidths) { 196 | const xOff = (widestChunk / 2) - (width / 2); 197 | this.ctx.fillText(text, x + xOff, y + yOff); 198 | yOff += (label.size - sizeInc) + 199 | Math.floor((label.size - sizeInc) / 2); 200 | } 201 | } else { 202 | this.ctx.fillText(label.text, x, y); 203 | } 204 | } 205 | } 206 | 207 | async draw() { 208 | this.drawSectors(); 209 | this.drawObjects(); 210 | await this.drawPoints(); 211 | this.drawLabels(); 212 | } 213 | } 214 | 215 | module.exports = MapPainter; 216 | -------------------------------------------------------------------------------- /src/sector-painter.js: -------------------------------------------------------------------------------- 1 | // paint a 2d map of an individual landscape sector 2 | 3 | const chalk = require('chalk'); 4 | const { createCanvas } = require('canvas'); 5 | const { cssColor, rgb2hex } = require('color-functions'); 6 | 7 | // size of a square tile in pixels 8 | const TILE_SIZE = 3; 9 | 10 | const [NORTH, EAST, SOUTH, WEST] = [0, 1, 2, 3]; 11 | const SURROUNDED = [true, true, true, true]; 12 | 13 | const WALL_COLOUR = 'rgb(97, 97, 97)'; 14 | 15 | class SectorPainter { 16 | constructor(sector, options = {}, neighbours = []) { 17 | this.sector = sector; 18 | this.options = options; 19 | this.neighbours = neighbours; 20 | 21 | this.imageWidth = sector.width * TILE_SIZE; 22 | this.imageHeight = sector.height * TILE_SIZE; 23 | 24 | this.canvas = createCanvas(this.imageWidth, this.imageHeight); 25 | this.ctx = this.canvas.getContext('2d'); 26 | } 27 | 28 | // draw a wall from north to south 29 | drawVerticalWall(x, y) { 30 | this.ctx.fillStyle = WALL_COLOUR; 31 | this.ctx.fillRect(x + 2, y, 1, TILE_SIZE); 32 | } 33 | 34 | // draw a wall wall from east to west 35 | drawHorizontalWall(x, y) { 36 | this.ctx.fillStyle = WALL_COLOUR; 37 | this.ctx.fillRect(x, y, TILE_SIZE, 1); 38 | } 39 | 40 | // draw a diagonal wall, "/" signifying northwest to southeast, "\" 41 | // signifying northeast to southwest 42 | drawDiagonalWall(direction, x, y) { 43 | this.ctx.fillStyle = WALL_COLOUR; 44 | 45 | if (direction === '/') { 46 | this.ctx.fillRect(x + 2, y, 1, 1); 47 | this.ctx.fillRect(x + 1, y + 1, 1, 1); 48 | this.ctx.fillRect(x, y + 2, 1, 1); 49 | } else if (direction === '\\') { 50 | this.ctx.fillRect(x, y, 1, 1); 51 | this.ctx.fillRect(x + 1, y + 1, 1, 1); 52 | this.ctx.fillRect(x + 2, y + 2, 1, 1); 53 | } 54 | } 55 | 56 | // draw a coloured tile 57 | drawTile(colour, x, y) { 58 | this.ctx.fillStyle = colour; 59 | this.ctx.fillRect(x, y, TILE_SIZE, TILE_SIZE); 60 | } 61 | 62 | // draw an anti-aliased overlay tile on top of a coloured tile, the last 63 | // argument is an array of booleans describing whether or not each cardinal 64 | // direction contains a matching overlay 65 | drawOverlay(colour, x, y, [ north, east, south, west ]) { 66 | this.ctx.fillStyle = colour; 67 | 68 | // the order of these matter for accuracy 69 | if (!north && !east && !south && !west) { 70 | // alone (usually stairwells) 71 | this.ctx.fillRect(x, y, TILE_SIZE, TILE_SIZE); 72 | } else if (!south && !west) { 73 | // bottom right 74 | this.ctx.fillRect(x + 2, y, 1, 3); 75 | this.ctx.fillRect(x + 1, y, 1, 2); 76 | this.ctx.fillRect(x, y, 1, 1); 77 | } else if (!north && !east) { 78 | // top right 79 | this.ctx.fillRect(x, y + 1, 1, 2); 80 | this.ctx.fillRect(x + 1, y + 2, 1, 1); 81 | } else if (!south && !east) { 82 | // bottom left 83 | this.ctx.fillRect(x, y, 1, 3); 84 | this.ctx.fillRect(x + 1, y, 1, 2); 85 | this.ctx.fillRect(x + 2, y, 1, 1); 86 | } else if (!north && !west) { 87 | // top left 88 | this.ctx.fillRect(x + 2, y + 1, 1, 2); 89 | this.ctx.fillRect(x + 1, y + 2, 1, 1); 90 | } else { 91 | this.ctx.fillRect(x, y, TILE_SIZE, TILE_SIZE); 92 | } 93 | } 94 | 95 | // get NESW neighbours of a tile on our sector, checking neighbouring 96 | // sectors if it's an edge tile 97 | getTileNeighbours(x, y) { 98 | let [north, east, south, west] = [null, null, null, null]; 99 | 100 | if (y - 1 >= 0) { 101 | north = this.sector.tiles[x][y - 1]; 102 | } else if (y - 1 === -1 && this.neighbours[NORTH]) { 103 | north = this.neighbours[NORTH].tiles[x][this.sector.height - 1]; 104 | } 105 | 106 | if (x + 1 < this.sector.width) { 107 | east = this.sector.tiles[x + 1][y]; 108 | } else if (x + 1 >= this.sector.width && this.neighbours[EAST]) { 109 | east = this.neighbours[EAST].tiles[0][y]; 110 | } 111 | 112 | if (y + 1 < this.sector.height) { 113 | south = this.sector.tiles[x][y + 1]; 114 | } else if (y + 1 === this.sector.height && this.neighbours[SOUTH]) { 115 | south = this.neighbours[SOUTH].tiles[x][0]; 116 | } 117 | 118 | if (x - 1 >= 0) { 119 | west = this.sector.tiles[x - 1][y]; 120 | } else if (x - 1 === -1 && this.neighbours[WEST]) { 121 | west = this.neighbours[WEST].tiles[this.sector.width - 1][y]; 122 | } 123 | 124 | return [north, east, south, west]; 125 | } 126 | 127 | // draw each tile in a grid, antialiasing accordingly. 128 | draw() { 129 | let x = 0; 130 | let y = 0; 131 | 132 | for (let i = 0; i < this.sector.width; i += 1) { 133 | for (let j = 0; j < this.sector.height; j += 1) { 134 | let tile = this.sector.tiles[i][j]; 135 | let tileDef = tile.getTileDef(); 136 | let overlay = tile.overlay; 137 | 138 | // TRBL/NESW 139 | const neighbours = this.getTileNeighbours(i, j); 140 | 141 | // add extra bridge tiles on land 142 | if (!/^(water|lava)$/.test(tileDef.name) && !tileDef.bridge) { 143 | for (const neighbour of neighbours) { 144 | if (neighbour && neighbour.getTileDef().bridge) { 145 | overlay = neighbour.overlay; 146 | tileDef = neighbour.getTileDef(); 147 | break; 148 | } 149 | } 150 | } 151 | 152 | const diagonal = tile.wall.diagonal; 153 | 154 | this.drawTile(tile.getTerrainColour(), x, y); 155 | 156 | if (overlay !== 0) { 157 | let overlayNeighbours = SURROUNDED.slice(); 158 | 159 | if (diagonal && tileDef.indoors) { 160 | // this fixes antialiasing for checkered-floor patterns 161 | overlayNeighbours = neighbours.map(neighbour => { 162 | return !!neighbour && 163 | neighbour.getTileDef().indoors; 164 | }); 165 | } else if (diagonal || tileDef.antialias) { 166 | overlayNeighbours = neighbours.map(neighbour => { 167 | return !!neighbour && ( 168 | neighbour.getTileDef().name === 'hole' || 169 | neighbour.overlay === overlay); 170 | }); 171 | } 172 | 173 | // if water is touching a bridge or log, don't antialias 174 | if (/^(water|lava)$/.test(tileDef.name)) { 175 | neighbours.forEach((neighbour, direction) => { 176 | if (neighbour && neighbour.getTileDef().bridge) { 177 | overlayNeighbours[direction] = true; 178 | } 179 | }); 180 | } 181 | 182 | // fix diagonal tiles surrounded by different overlays 183 | if (diagonal) { 184 | if (!overlayNeighbours[SOUTH] && neighbours[SOUTH]) { 185 | this.drawTile( 186 | neighbours[SOUTH].getTileDef().colour, 187 | x, y); 188 | } else if (!overlayNeighbours[NORTH] && 189 | neighbours[NORTH]) { 190 | this.drawTile( 191 | neighbours[NORTH].getTileDef().colour, 192 | x, y); 193 | } 194 | } 195 | 196 | this.drawOverlay(tileDef.colour, x, y, overlayNeighbours); 197 | } 198 | 199 | if (diagonal) { 200 | this.drawDiagonalWall(diagonal.direction, x, y); 201 | } 202 | 203 | if (tile.wall.vertical) { 204 | this.drawVerticalWall(x, y); 205 | } 206 | 207 | if (tile.wall.horizontal) { 208 | this.drawHorizontalWall(x, y); 209 | } 210 | 211 | y += TILE_SIZE; 212 | } 213 | 214 | y = 0; 215 | x += TILE_SIZE; 216 | } 217 | } 218 | 219 | // draw the map for in characters for terminals 220 | write(colourLevel = -1) { 221 | if (colourLevel !== -1) { 222 | chalk.level = colourLevel; 223 | } 224 | 225 | const output = []; 226 | 227 | for (let tileY = 0; tileY < this.sector.height; tileY++) { 228 | output.push(''); 229 | 230 | for (let tileX = 0; tileX < this.sector.width; tileX++) { 231 | const tile = this.sector.tiles[tileX][tileY]; 232 | let colour = tile.getTerrainColour(); 233 | 234 | if (tile.overlay) { 235 | const {r, g, b} = cssColor(tile.getTileDef().colour); 236 | colour = rgb2hex(r, g, b); 237 | } 238 | 239 | let c = ' '; 240 | 241 | if (tile.wall.vertical && tile.wall.horizontal) { 242 | c = '‾|'; 243 | } else if (tile.wall.vertical) { 244 | c = ' |'; 245 | } else if (tile.wall.horizontal) { 246 | c = '‾‾'; 247 | } else if (tile.wall.diagonal) { 248 | if (tile.wall.diagonal.direction === '/') { 249 | c = ' /'; 250 | } else { 251 | c = ' \\'; 252 | } 253 | } 254 | 255 | output[output.length - 1] += 256 | chalk.rgb(97, 97, 97).bgHex(colour)(c); 257 | } 258 | } 259 | 260 | return output.join('\n'); 261 | } 262 | } 263 | 264 | module.exports = SectorPainter; 265 | -------------------------------------------------------------------------------- /src/bin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs').promises; 4 | const mkdirp = require('mkdirp-promise'); 5 | const path = require('path'); 6 | const pkg = require('../package'); 7 | const yargs = require('yargs'); 8 | const { JagArchive } = require('@2003scape/rsc-archiver'); 9 | const { Landscape, Sector } = require('./'); 10 | 11 | async function parseArchives(argv, landscape) { 12 | let landMem, mapsMem, landJag, mapsJag; 13 | 14 | for (const filename of argv.archives) { 15 | if (/^land(\d+)\.jag$/i.test(filename)) { 16 | landJag = filename; 17 | } else if (/^maps(\d+)\.jag$/i.test(filename)) { 18 | mapsJag = filename; 19 | } else if (/^land(\d+)\.mem$/i.test(filename)) { 20 | landMem = filename; 21 | } else if (/^maps(\d+)\.mem$/i.test(filename)) { 22 | mapsMem = filename; 23 | } 24 | } 25 | 26 | if (!(landMem && mapsMem) && !(landJag && mapsJag)) { 27 | process.exitCode = 1; 28 | console.error('provide at least one maps and land archive combination'); 29 | return; 30 | } 31 | 32 | if (landJag && mapsJag) { 33 | landJag = await fs.readFile(landJag); 34 | mapsJag = await fs.readFile(mapsJag); 35 | 36 | landscape.loadJag(landJag, mapsJag); 37 | } 38 | 39 | if (landMem && mapsMem) { 40 | landMem = await fs.readFile(landMem); 41 | mapsMem = await fs.readFile(mapsMem); 42 | 43 | landscape.loadMem(landMem, mapsMem); 44 | } 45 | 46 | landscape.parseArchives(); 47 | } 48 | 49 | yargs 50 | .scriptName('rsc-landscape') 51 | .version(pkg.version) 52 | .command( 53 | 'generate-map ', 54 | 'generate world map png', 55 | yargs => { 56 | yargs.positional('archives', { 57 | description: 'landscape and map .jag and .mem archives', 58 | type: 'array' 59 | }); 60 | 61 | yargs.option('plane', { 62 | alias: 'z', 63 | description: 'change map depth (0 = overworld, 1 = upstairs, ' + 64 | 'etc.)', 65 | type: 'number', 66 | default: 0 67 | }); 68 | 69 | yargs.option('objects', { 70 | alias: 'O', 71 | description: 'JSON file with object locations', 72 | type: 'string' 73 | }); 74 | 75 | yargs.option('points', { 76 | alias: 'p', 77 | description: 'JSON file with point-of-interest locations', 78 | type: 'string' 79 | }); 80 | 81 | yargs.option('labels', { 82 | alias: 'l', 83 | description: 'JSON file with map labels', 84 | type: 'string' 85 | }); 86 | 87 | yargs.option('output', { 88 | alias: 'o', 89 | description: 'filename to write the PNG to', 90 | type: 'string', 91 | default: './worldmap.png' 92 | }); 93 | }, 94 | async argv => { 95 | const landscape = new Landscape(); 96 | 97 | try { 98 | await parseArchives(argv, landscape); 99 | 100 | const options = { points: [], objects: [], labels: [] }; 101 | 102 | for (const option of Object.keys(options)) { 103 | const filename = argv[option]; 104 | 105 | if (filename) { 106 | const file = await fs.readFile(filename); 107 | 108 | if (file) { 109 | options[option] = JSON.parse(file.toString()); 110 | } 111 | } 112 | } 113 | 114 | options.plane = argv.plane; 115 | 116 | const canvas = await landscape.toCanvas(options); 117 | await fs.writeFile(argv.output, canvas.toBuffer()); 118 | } catch (e) { 119 | process.exitCode = 1; 120 | console.error(e); 121 | } 122 | }) 123 | .command( 124 | 'dump-json ', 125 | 'dump JSON files of each sector', 126 | yargs => { 127 | yargs.positional('archives', { 128 | description: 'landscape and map .jag and .mem archives', 129 | type: 'array' 130 | }); 131 | 132 | yargs.option('pretty', { 133 | alias: 'p', 134 | description: 'pretty-print JSON files', 135 | type: 'boolean', 136 | default: false 137 | }); 138 | 139 | yargs.option('output', { 140 | alias: 'o', 141 | description: 'directory to dump sector files (will attempt ' + 142 | 'creation)', 143 | type: 'string', 144 | default: './sectors-json/' 145 | }); 146 | }, 147 | async argv => { 148 | const landscape = new Landscape(); 149 | 150 | try { 151 | await mkdirp(argv.output); 152 | await parseArchives(argv, landscape); 153 | 154 | for (const sector of landscape.getPopulatedSectors()) { 155 | const json = 156 | JSON.stringify(sector, null, argv.pretty ? ' ' : ''); 157 | 158 | await fs.writeFile( 159 | path.join(argv.output, 160 | `${sector.getEntryName()}.json`), 161 | json); 162 | } 163 | } catch (e) { 164 | console.error(e); 165 | process.exitCode = 1; 166 | } 167 | }) 168 | .command( 169 | 'pack-json ', 170 | 'generate land and maps archives from directory of JSON files', 171 | yargs => { 172 | yargs.positional('directory', { 173 | description: 'directory of JSON sectors', 174 | type: 'string' 175 | }); 176 | 177 | yargs.option('vers', { 178 | alias: 'v', 179 | description: 'version to label the archives', 180 | type: 'number', 181 | default: 64 182 | }); 183 | 184 | yargs.option('output', { 185 | alias: 'o', 186 | description: 'directory to dump archive files', 187 | type: 'string', 188 | default: './sectors-jag/' 189 | }); 190 | }, 191 | async argv => { 192 | const writeArchive = async (archive, filename) => { 193 | if (archive.entries.size > 0) { 194 | await fs.writeFile(path.join(argv.output, filename), 195 | archive.toArchive(false)); 196 | } 197 | }; 198 | 199 | const landJag = new JagArchive(); 200 | const landMem = new JagArchive(); 201 | const mapsJag = new JagArchive(); 202 | const mapsMem = new JagArchive(); 203 | 204 | try { 205 | await mkdirp(argv.output); 206 | 207 | const jsonSectors = await fs.readdir(argv.directory); 208 | 209 | for (const filename of jsonSectors) { 210 | const file = await fs.readFile( 211 | path.join(argv.directory, filename)); 212 | 213 | const jsonSector = JSON.parse(file.toString()); 214 | const sector = new Sector(jsonSector); 215 | const entry = sector.getEntryName(); 216 | 217 | const landArchive = sector.members ? landMem : landJag; 218 | const mapsArchive = sector.members ? mapsMem : mapsJag; 219 | 220 | if (sector.plane === 0 || sector.plane === 3) { 221 | landArchive.putEntry(`${entry}.hei`, 222 | Buffer.from(sector.toHei())); 223 | } 224 | 225 | mapsArchive.putEntry(`${entry}.dat`, 226 | Buffer.from(sector.toDat())); 227 | 228 | const loc = sector.toLoc(); 229 | 230 | if (loc) { 231 | mapsArchive.putEntry(`${entry}.loc`, 232 | Buffer.from(loc)); 233 | } 234 | } 235 | 236 | await writeArchive(landJag, `land${argv.vers}.jag`); 237 | await writeArchive(mapsJag, `maps${argv.vers}.jag`); 238 | await writeArchive(landMem, `land${argv.vers}.mem`); 239 | await writeArchive(mapsMem, `maps${argv.vers}.mem`); 240 | } catch (e) { 241 | console.error(e); 242 | process.exitCode = 1; 243 | } 244 | }) 245 | .command( 246 | 'print-sector ', 247 | 'print coloured sector to terminal', 248 | yargs => { 249 | yargs.positional('archives', { 250 | description: 'landscape and map .jag and .mem archives', 251 | type: 'array' 252 | }); 253 | 254 | yargs.option('x', { 255 | type: 'number', 256 | default: 50 257 | }); 258 | 259 | yargs.option('y', { 260 | type: 'number', 261 | default: 50 262 | }); 263 | 264 | yargs.option('plane', { 265 | alias: 'z', 266 | type: 'number', 267 | default: 0 268 | }); 269 | 270 | yargs.option('colours', { 271 | alias: 'c', 272 | description: 'amount of colours to use, or -1 to autodetect', 273 | type: 'number', 274 | default: -1 275 | }); 276 | }, 277 | async argv => { 278 | const landscape = new Landscape(); 279 | 280 | try { 281 | await parseArchives(argv, landscape); 282 | const sector = landscape.sectors[argv.x][argv.y][argv.plane]; 283 | process.stdout.write(sector.toString(true, argv.colours) + 284 | '\n'); 285 | } catch (e) { 286 | process.exitCode = 1; 287 | console.error(e); 288 | } 289 | }) 290 | .demandCommand() 291 | .argv; 292 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rsc-landscape 2 | (de)serialize runescape classic landscape files. parse the original `land` and 3 | `maps` archives into a tile objects, dump PNGs, make changes and encode + 4 | compress them back to an original archive. 5 | 6 | ![](./doc/worldmap-final.png?raw=true) 7 | 8 | *a world map generated with rsc-landscape* 9 | 10 | ![](./doc/map-comparison.gif?raw=true) 11 | ![](./doc/map-comparison2.gif?raw=true) 12 | ![](./doc/map-comparison3.gif?raw=true) 13 | 14 | *comparison with jagex's world map* 15 | 16 | the official world map generated by jagex contains less detail due to GIF 17 | palette compression, as well as clipped object symbols between sectors. it's 18 | also missing some areas compared to the latest revision (gertrude's house, 19 | digsite, shilo village, etc.). 20 | 21 | ## install 22 | 23 | $ npm install @2003scape/rsc-landscape # -g for CLI program 24 | 25 | ## cli usage 26 | ``` 27 | rsc-landscape 28 | 29 | Commands: 30 | rsc-landscape generate-map generate world map png 31 | rsc-landscape dump-json dump JSON files of each sector 32 | rsc-landscape pack-json generate land and maps archives from 33 | directory of JSON files 34 | rsc-landscape print-sector print coloured sector to terminal 35 | 36 | Options: 37 | --help Show help [boolean] 38 | --version Show version number [boolean] 39 | ``` 40 | 41 | $ rsc-landscape generate-map land* maps* -O object-locs.json \ 42 | -p map-points.json -l map-labels.json # generate worldmap.png 43 | $ rsc-landscape generate-map land* maps* --plane 3 -o dungeons.png 44 | $ rsc-landscape print-sector land* maps* -x 50 -y 50 -z 0 -c 2 # lumbridge 45 | 46 | ## example 47 | ```javascript 48 | const fs = require('fs'); 49 | const { Landscape } = require('./src'); 50 | 51 | const landscape = new Landscape(); 52 | 53 | landscape.loadJag(fs.readFileSync('./land63.jag'), 54 | fs.readFileSync('./maps63.jag')); 55 | landscape.loadMem(fs.readFileSync('./land63.mem'), 56 | fs.readFileSync('./maps63.mem')); 57 | 58 | landscape.parseArchives(); 59 | 60 | const lumbridge = landscape.sectors[50][50][0]; 61 | 62 | const tile = lumbridge.tiles[0][0]; 63 | console.log(tile.colour, tile.getGameCoords()); 64 | 65 | const tile2 = landscape.getTileAtGameCoords(126, 1468); 66 | console.log(tile2.getTileDef()); 67 | 68 | process.stdout.write(lumbridge.toString(true)); 69 | fs.writeFileSync(`./sector-lumbridge.png`, lumbridge.toCanvas().toBuffer()); 70 | 71 | (async () => { 72 | fs.writeFileSync('./worldmap.png', (await landscape.toCanvas({ 73 | points: require('./map-points.json'), 74 | objects: require('./object-locs.json'), 75 | labels: require('./map-labels.json') 76 | })).toBuffer()); 77 | })(); 78 | ``` 79 | 80 | ## file formats 81 | the runescape classic world is separated into sectors, each containing 48x48 82 | (2304) tiles. 83 | overworld and dungeon sectors contain both a `.hei` and `.dat` file, sectors 84 | upstairs only contain `.dat` files, and any sector with object locations will 85 | have a `.loc` file. 86 | * `.hei` file in *land* archive which stores elevation and colour of tiles 87 | * `.dat` file in *maps* archive which stores walls and object direction of tiles 88 | * `.loc` file in *maps* archive which stores object IDs (used for the login 89 | screen previews) 90 | 91 | ## api 92 | ### .terrainColours.integer 93 | array of original, undarkened 256 colours client uses to colour tiles. 94 | 95 | ### .terrainColours.rgb 96 | array of 256 map colours used for each tile, darkened by 50% and converted to 97 | `rgb(r, g, b)` format. 98 | 99 | ### .tileOverlays 100 | map of IDs to tile overlay information. 101 | 102 | ### tile = new Tile({ sector, x, y, ... }) 103 | create new sector tile. accepts all of the properties listed below. 104 | 105 | ### tile.colour 106 | number from 0-255 corresponding to colour in `.terrainColours`. 107 | 108 | ### tile.elevation 109 | number from 0-255 describing height of tile. 110 | 111 | ### tile.direction 112 | number from 0-6 describing direction objects should face on tile. 113 | 114 | ### tile.overlay 115 | overlay type index. corresponding names are stored in `.overlays`. 116 | 117 | ### tile.wall 118 | object with following potential properties: 119 | 120 | ```javascript 121 | { 122 | diagonal: { 123 | direction: '/' || '\\', 124 | overlay: overlay 125 | } || null, 126 | vertical: overlay || 0, 127 | horizontal: overlay || 0, 128 | roof: roofOverlay || 0 129 | } 130 | ``` 131 | 132 | ### tile.objectId 133 | store object here for login screen previews. 134 | 135 | ### tile.populate() 136 | read buffers from tile's sector and populate its properties. 137 | 138 | ### tile.getTerrainColour() 139 | return base colour of this tile for maps. 140 | 141 | ### tile.getTileDef() 142 | return object describing attributes of tile's overlay (from 143 | `./res/tile-overlays.json`): 144 | ```javascript 145 | { 146 | name: 'road', 147 | blocked: false, 148 | bridge: false, 149 | indoors: false, 150 | antialias: true, 151 | colour: 'rgb(64, 64, 64)' 152 | } 153 | ``` 154 | 155 | ### tile.getGameCoords() 156 | return `{ x, y }` game uses for this tile. 157 | 158 | ### sector = new Sector({ x, y, plane, members?, tiles? }) 159 | create new sector instance. 160 | 161 | ### sector.members 162 | store in `.jag` or `.mem` file? 163 | 164 | ### sector.width 165 | amount of tiles on x axis (48). 166 | 167 | ### sector.height 168 | amount of tiles on y axis (48). 169 | 170 | ### sector.terrainHeight 171 | ### sector.terrainColour 172 | ### sector.wallsVertical 173 | ### sector.wallsHorizontal 174 | ### sector.wallsRoof 175 | ### sector.tileDecoration 176 | ### sector.tileDirection 177 | Int8Array buffers populated from archive files with `sector.parse*` or from 178 | sector's tile objects with `sector.populateBuffers()`. these buffers are 179 | encoded + compressed into archives. 180 | 181 | ### sector.wallsDiagonal 182 | Int32Array buffer, similar to above but 32-bit to store values > 255 ( 183 | potentially larger than 48000 if objects are stored). 184 | 185 | ### sector.tiles\[width\]\[height\] 186 | 2d array of tile objects. populate this field from the archive buffers with 187 | `sector.populateTiles()`, or populate the future archive buffers with 188 | `sector.populateBuffers()`. 189 | 190 | ### sector.parseHei(buffer) 191 | populate `sector.terrainHeight` and `sector.terrainColour` from a `.hei` file. 192 | 193 | ### sector.parseDat(buffer) 194 | populate `sector.walls*`, `sector.tileDecoration` and `sector.tileDirection` 195 | from a `.dat` file. 196 | 197 | ### sector.parseLoc(buffer) 198 | populate `sector.wallsDiagonal` with object IDs from a `.loc` file. 199 | 200 | ### sector.populateTiles() 201 | populate `sector.tiles` with a 2d array (48x48) of tile objects based on buffers 202 | we parsed from archived files. 203 | 204 | ### sector.populateBuffers() 205 | populate future archive buffers (`sector.terrain*`, `sector.wall*`, etc.) with 206 | `sector.tiles`. 207 | 208 | ### sector.getEntryName() 209 | get the main portion of a landscape archive filename. 210 | 211 | ### sector.toHei() 212 | get a `.hei` file buffer for this sector. 213 | 214 | ### sector.toDat() 215 | get a `.dat` file buffer for this sector. 216 | 217 | ### sector.toLoc() 218 | get a `.loc` file buffer for this sector (or null if no objects ID are stored). 219 | 220 | ### sector.toCanvas(options, [ north, east, south, west ]) 221 | render an individual sector to a canvas. the second argument is optional if you 222 | want to antialias the edges properly using the neighbouring sectors (world map 223 | generation does this automatically). 224 | 225 | in node, you can turn this into a PNG 226 | with [`.toBuffer()`](https://github.com/Automattic/node-canvas#canvastobuffer). 227 | 228 | ### sector.toString(terminal = false, colourLevel = -1) 229 | if `terminal` is true, return a nethack-esque terminal rendering of the sector: 230 | 231 | ![](./doc/terminal.png?raw=true) 232 | 233 | `colourLevel` describes the 234 | [chalk level of colours to use](https://github.com/chalk/chalk#chalklevel). 235 | `-1` automatically detects the maximum support. 236 | 237 | ...otherwise just return the name and size of the sector. 238 | 239 | ### landscape = new Landscape() 240 | create new landscape (de)serializer instance. 241 | 242 | ### landscape.loadJag(landBuffer, mapBuffer) 243 | ### landscape.loadMem(landBuffer, mapBuffer) 244 | prepare `.jag` and `.mem` buffers to be parsed. any sectors loaded with 245 | `landscape.loadMem` will have `sector.members = true`. 246 | 247 | ### landscape.parseArchives() 248 | populate `landscape.sectors` with loaded buffers. 249 | 250 | ### \*landscape.getPopulatedSectors() 251 | return iterator of all the non-empty sectors. 252 | 253 | ### landscape.getSectorNeighbours(x, y, plane) 254 | return neighbours to a sector position as `[north, east, south, west]`. 255 | 256 | ### landscape.getTileAtGameCoords(x, y) 257 | get the tile at coordinates used in game. 258 | 259 | ### async landscape.toCanvas({ objects, points, labels }) 260 | create a world map image from all of the non-empty sectors. 261 | 262 | * `objects` is an optional array of the following: 263 | ```javascript 264 | { 265 | id: 1, 266 | position: [x, y] 267 | } 268 | ``` 269 | 270 | its `x` and `y` are multipled by the tile size. 271 | 272 | * `points` is an optional array of the following: 273 | ```javascript 274 | { 275 | type: 'altar', // 'general-shop', 'dungeon' etc. see ./res/key/ 276 | x, y 277 | } 278 | ``` 279 | 280 | each point image is 15x15. 281 | 282 | * `labels` is an optional array of the following: 283 | ```javascript 284 | { 285 | text: 'label\nfor\nsomething', 286 | x, y, 287 | size: 10, // 8 is the smallest in use, while 14 is the largest 288 | align: 'center' || 'left', 289 | bold: true || undefined, 290 | colour: 'rgb(254, 165, 0)' || '#ff00ff' || undefined 291 | } 292 | ``` 293 | 294 | ## license 295 | Copyright 2019 2003Scape Team 296 | 297 | This program is free software: you can redistribute it and/or modify it under 298 | the terms of the GNU Affero General Public License as published by the 299 | Free Software Foundation, either version 3 of the License, or (at your option) 300 | any later version. 301 | 302 | This program is distributed in the hope that it will be useful, but WITHOUT ANY 303 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 304 | PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 305 | 306 | You should have received a copy of the GNU Affero General Public License along 307 | with this program. If not, see http://www.gnu.org/licenses/. 308 | -------------------------------------------------------------------------------- /map-labels.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "text": "Swamp", 4 | "x": 7156, 5 | "y": 6632, 6 | "size": 10, 7 | "align": "center" 8 | }, 9 | { 10 | "text": "Gnome Ball\nField", 11 | "x": 7113, 12 | "y": 6662, 13 | "size": 10, 14 | "align": "left" 15 | }, 16 | { 17 | "text": "Grand tree", 18 | "x": 7220, 19 | "y": 6681, 20 | "size": 10, 21 | "align": "center" 22 | }, 23 | { 24 | "text": "Tree Gnome\nStronghold", 25 | "x": 7189, 26 | "y": 6731, 27 | "size": 12, 28 | "align": "left", 29 | "bold": true 30 | }, 31 | { 32 | "text": "Baxtorian\nFalls", 33 | "x": 7390, 34 | "y": 6734, 35 | "size": 10, 36 | "align": "left" 37 | }, 38 | { 39 | "text": "Agility\ntraining\narea", 40 | "x": 7274, 41 | "y": 6810, 42 | "size": 10, 43 | "align": "left" 44 | }, 45 | { 46 | "text": "Underground\npass", 47 | "x": 7173, 48 | "y": 7061, 49 | "size": 10, 50 | "align": "left" 51 | }, 52 | { 53 | "text": "Combat\ntraining\ncamp", 54 | "x": 7354, 55 | "y": 6924, 56 | "size": 10, 57 | "align": "left" 58 | }, 59 | { 60 | "text": "West\nArdougne", 61 | "x": 7341, 62 | "y": 7046, 63 | "size": 12, 64 | "align": "center", 65 | "bold": true 66 | }, 67 | { 68 | "text": "Battle field", 69 | "x": 7382, 70 | "y": 7282, 71 | "size": 10, 72 | "align": "left" 73 | }, 74 | { 75 | "text": "Tree Gnome\nVillage", 76 | "x": 7384, 77 | "y": 7400, 78 | "size": 10, 79 | "align": "left" 80 | }, 81 | { 82 | "text": "Coal\ntrucks", 83 | "x": 7555, 84 | "y": 6644, 85 | "size": 10, 86 | "align": "left" 87 | }, 88 | { 89 | "text": "Fishing\nGuild", 90 | "x": 7576, 91 | "y": 6868, 92 | "size": 10, 93 | "align": "center" 94 | }, 95 | { 96 | "text": "Zoo", 97 | "x": 7613, 98 | "y": 7168, 99 | "size": 10, 100 | "align": "center" 101 | }, 102 | { 103 | "text": "Fight\nArena", 104 | "x": 7520, 105 | "y": 7432, 106 | "size": 10, 107 | "align": "left" 108 | }, 109 | { 110 | "text": "Yanille", 111 | "x": 7501, 112 | "y": 7585, 113 | "size": 10, 114 | "align": "center" 115 | }, 116 | { 117 | "text": "Wizards\nGuild", 118 | "x": 7528, 119 | "y": 7604, 120 | "size": 10, 121 | "align": "center" 122 | }, 123 | { 124 | "text": "McGrubor's\nWood", 125 | "x": 7674, 126 | "y": 6673, 127 | "size": 10, 128 | "align": "center" 129 | }, 130 | { 131 | "text": "Hemenster", 132 | "x": 7631, 133 | "y": 6801, 134 | "size": 10, 135 | "align": "left" 136 | }, 137 | { 138 | "text": "Kingdom of\nKandarin", 139 | "x": 7705, 140 | "y": 6821, 141 | "size": 12, 142 | "align": "center", 143 | "bold": true, 144 | "colour": "rgb(254, 165, 0)" 145 | }, 146 | { 147 | "text": "Windmill", 148 | "x": 7640, 149 | "y": 6923, 150 | "size": 8, 151 | "align": "left" 152 | }, 153 | { 154 | "text": "East\nArdougne", 155 | "x": 7628, 156 | "y": 7050, 157 | "size": 12, 158 | "align": "center", 159 | "bold": true 160 | }, 161 | { 162 | "text": "Necromancer", 163 | "x": 7653, 164 | "y": 7269, 165 | "size": 8, 166 | "align": "left" 167 | }, 168 | { 169 | "text": "Port\nKhazard", 170 | "x": 7712, 171 | "y": 7407, 172 | "size": 10, 173 | "align": "left" 174 | }, 175 | { 176 | "text": "Seer's\nVillage", 177 | "x": 7817, 178 | "y": 6695, 179 | "size": 10, 180 | "align": "center" 181 | }, 182 | { 183 | "text": "Sorcerors\nTower", 184 | "x": 7823, 185 | "y": 6834, 186 | "size": 10, 187 | "align": "center" 188 | }, 189 | { 190 | "text": "Camelot\nCastle", 191 | "x": 7934, 192 | "y": 6657, 193 | "size": 10, 194 | "align": "center" 195 | }, 196 | { 197 | "text": "Flax", 198 | "x": 7891, 199 | "y": 6783, 200 | "size": 10, 201 | "align": "left" 202 | }, 203 | { 204 | "text": "Beehives", 205 | "x": 7932, 206 | "y": 6784, 207 | "size": 10, 208 | "align": "left" 209 | }, 210 | { 211 | "text": "Keep\nLeFaye", 212 | "x": 7964, 213 | "y": 6877, 214 | "size": 10, 215 | "align": "center" 216 | }, 217 | { 218 | "text": "Barbarian\nOutpost", 219 | "x": 7882, 220 | "y": 6959, 221 | "size": 10, 222 | "align": "center" 223 | }, 224 | { 225 | "text": "Agility training\narea", 226 | "x": 7874, 227 | "y": 7014, 228 | "size": 10, 229 | "align": "left" 230 | }, 231 | { 232 | "text": "Fishing\nPlatform", 233 | "x": 7897, 234 | "y": 7171, 235 | "size": 10, 236 | "align": "left" 237 | }, 238 | { 239 | "text": "Brimhaven", 240 | "x": 7964, 241 | "y": 7392, 242 | "size": 10, 243 | "align": "center" 244 | }, 245 | { 246 | "text": "Tai Bwo Wannai", 247 | "x": 7973, 248 | "y": 7592, 249 | "size": 10, 250 | "align": "center" 251 | }, 252 | { 253 | "text": "White Wolf\nMountain", 254 | "x": 8101, 255 | "y": 6682, 256 | "size": 10, 257 | "align": "center" 258 | }, 259 | { 260 | "text": "Catherby", 261 | "x": 8065, 262 | "y": 6808, 263 | "size": 10, 264 | "align": "center" 265 | }, 266 | { 267 | "text": "Entrana", 268 | "x": 8075, 269 | "y": 6944, 270 | "size": 12, 271 | "align": "center", 272 | "bold": true, 273 | "colour": "rgb(254, 165, 0)" 274 | }, 275 | { 276 | "text": "Crandor", 277 | "x": 8073, 278 | "y": 7189, 279 | "size": 12, 280 | "align": "center", 281 | "bold": true, 282 | "colour": "rgb(254, 165, 0)" 283 | }, 284 | { 285 | "text": "Karamja", 286 | "x": 8177, 287 | "y": 7425, 288 | "size": 12, 289 | "align": "center", 290 | "bold": true, 291 | "colour": "rgb(254, 165, 0)" 292 | }, 293 | { 294 | "text": "Ship yard", 295 | "x": 8140, 296 | "y": 7566, 297 | "size": 10, 298 | "align": "left" 299 | }, 300 | { 301 | "text": "Hero's\nGuild", 302 | "x": 8231, 303 | "y": 6649, 304 | "size": 10, 305 | "align": "center" 306 | }, 307 | { 308 | "text": "Druid's\nCircle", 309 | "x": 8254, 310 | "y": 6700, 311 | "size": 10, 312 | "align": "center" 313 | }, 314 | { 315 | "text": "Taverley", 316 | "x": 8224, 317 | "y": 6821, 318 | "size": 10, 319 | "align": "left" 320 | }, 321 | { 322 | "text": "Dark Wizards\nTower", 323 | "x": 8238, 324 | "y": 7031, 325 | "size": 10, 326 | "align": "center" 327 | }, 328 | { 329 | "text": "Make over\nmage", 330 | "x": 8249, 331 | "y": 7070, 332 | "size": 10, 333 | "align": "left" 334 | }, 335 | { 336 | "text": "Crafting\nGuild", 337 | "x": 8294, 338 | "y": 7136, 339 | "size": 10, 340 | "align": "center" 341 | }, 342 | { 343 | "text": "Melzar's\nMaze", 344 | "x": 8310, 345 | "y": 7215, 346 | "size": 10, 347 | "align": "center" 348 | }, 349 | { 350 | "text": "Chaos\nTemple", 351 | "x": 8365, 352 | "y": 6626, 353 | "size": 8, 354 | "align": "center" 355 | }, 356 | { 357 | "text": "Goblin\nVillage", 358 | "x": 8356, 359 | "y": 6682, 360 | "size": 10, 361 | "align": "center" 362 | }, 363 | { 364 | "text": "White Knights", 365 | "x": 8392, 366 | "y": 7024, 367 | "size": 8, 368 | "align": "center" 369 | }, 370 | { 371 | "text": "Rimmington", 372 | "x": 8365, 373 | "y": 7285, 374 | "size": 11, 375 | "align": "center" 376 | }, 377 | { 378 | "text": "Black\nKnights", 379 | "x": 8513, 380 | "y": 6633, 381 | "size": 8, 382 | "align": "center" 383 | }, 384 | { 385 | "text": "Monastery", 386 | "x": 8566, 387 | "y": 6713, 388 | "size": 10, 389 | "align": "center" 390 | }, 391 | { 392 | "text": "Ice\nMountain", 393 | "x": 8462, 394 | "y": 6722, 395 | "size": 12, 396 | "align": "center", 397 | "bold": true 398 | }, 399 | { 400 | "text": "Dwarven\nMine", 401 | "x": 8498, 402 | "y": 6820, 403 | "size": 10, 404 | "align": "center" 405 | }, 406 | { 407 | "text": "Park", 408 | "x": 8477, 409 | "y": 6936, 410 | "size": 10, 411 | "align": "center" 412 | }, 413 | { 414 | "text": "Falador", 415 | "x": 8424, 416 | "y": 6981, 417 | "size": 12, 418 | "align": "center", 419 | "bold": true 420 | }, 421 | { 422 | "text": "Mining\nGuild", 423 | "x": 8514, 424 | "y": 7013, 425 | "size": 10, 426 | "align": "center" 427 | }, 428 | { 429 | "text": "Kingdom of\nAsgarnia", 430 | "x": 8433, 431 | "y": 7081, 432 | "size": 12, 433 | "align": "center", 434 | "bold": true, 435 | "colour": "rgb(254, 165, 0)" 436 | }, 437 | { 438 | "text": "Port\nSarim", 439 | "x": 8567, 440 | "y": 7264, 441 | "size": 12, 442 | "align": "center" 443 | }, 444 | { 445 | "text": "Edgeville", 446 | "x": 8731, 447 | "y": 6688, 448 | "size": 10, 449 | "align": "center" 450 | }, 451 | { 452 | "text": "Barbarian\nVillage", 453 | "x": 8666, 454 | "y": 6863, 455 | "size": 10, 456 | "align": "center" 457 | }, 458 | { 459 | "text": "Draynor Manor", 460 | "x": 8677, 461 | "y": 7004, 462 | "size": 10, 463 | "align": "center" 464 | }, 465 | { 466 | "text": "Draynor\nVillage", 467 | "x": 8721, 468 | "y": 7189, 469 | "size": 10, 470 | "align": "center" 471 | }, 472 | { 473 | "text": "Market", 474 | "x": 8667, 475 | "y": 7218, 476 | "size": 10, 477 | "align": "center" 478 | }, 479 | { 480 | "text": "Jail", 481 | "x": 8752, 482 | "y": 7242, 483 | "size": 10, 484 | "align": "center" 485 | }, 486 | { 487 | "text": "Wizard's\nTower", 488 | "x": 8716, 489 | "y": 7396, 490 | "size": 10, 491 | "align": "center" 492 | }, 493 | { 494 | "text": "Tutorial\nIsland", 495 | "x": 8704, 496 | "y": 7537, 497 | "size": 10, 498 | "align": "left" 499 | }, 500 | { 501 | "text": "Cooks\nGuild", 502 | "x": 8817, 503 | "y": 6764, 504 | "size": 10, 505 | "align": "center" 506 | }, 507 | { 508 | "text": "Champions\nGuild", 509 | "x": 8867, 510 | "y": 6985, 511 | "size": 10, 512 | "align": "center" 513 | }, 514 | { 515 | "text": "Windmill", 516 | "x": 8842, 517 | "y": 7126, 518 | "size": 8, 519 | "align": "center" 520 | }, 521 | { 522 | "text": "Palace", 523 | "x": 8948, 524 | "y": 6734, 525 | "size": 10, 526 | "align": "center" 527 | }, 528 | { 529 | "text": "Varrock", 530 | "x": 8940, 531 | "y": 6854, 532 | "size": 12, 533 | "align": "center", 534 | "bold": true 535 | }, 536 | { 537 | "text": "Kingdom of\nMisthalin", 538 | "x": 8933, 539 | "y": 7058, 540 | "size": 12, 541 | "align": "center", 542 | "bold": true, 543 | "colour": "rgb(254, 165, 0)" 544 | }, 545 | { 546 | "text": "Lumbridge", 547 | "x": 8954, 548 | "y": 7292, 549 | "size": 11, 550 | "align": "center" 551 | }, 552 | { 553 | "text": "Toll\ngate", 554 | "x": 9075, 555 | "y": 7261, 556 | "size": 9, 557 | "align": "center" 558 | }, 559 | { 560 | "text": "Al Kharid", 561 | "x": 9111, 562 | "y": 7385, 563 | "size": 12, 564 | "align": "center", 565 | "bold": true 566 | }, 567 | { 568 | "text": "Dig Site", 569 | "x": 9276, 570 | "y": 6860, 571 | "size": 10, 572 | "align": "left" 573 | }, 574 | { 575 | "text": "Exam Centre", 576 | "x": 9236, 577 | "y": 7019, 578 | "size": 10, 579 | "align": "left" 580 | }, 581 | { 582 | "text": "Shantay Pass", 583 | "x": 9130, 584 | "y": 7496, 585 | "size": 10, 586 | "align": "left" 587 | }, 588 | { 589 | "text": "Bedabin\nCamp", 590 | "x": 8818, 591 | "y": 7712, 592 | "size": 10, 593 | "align": "center" 594 | }, 595 | { 596 | "text": "Kharidian\nDesert", 597 | "x": 9036, 598 | "y": 7616, 599 | "size": 12, 600 | "align": "center", 601 | "bold": true, 602 | "colour": "rgb(254, 165, 0)" 603 | }, 604 | { 605 | "text": "Desert Mining\nCamp", 606 | "x": 9062, 607 | "y": 7732, 608 | "size": 10, 609 | "align": "center" 610 | }, 611 | { 612 | "text": "Lumber\nMill", 613 | "x": 9152, 614 | "y": 6638, 615 | "size": 10, 616 | "align": "center" 617 | }, 618 | { 619 | "text": "Legends'\nGuild", 620 | "x": 7796, 621 | "y": 6930, 622 | "size": 10, 623 | "align": "center" 624 | }, 625 | { 626 | "text": "Sinclair\nMansion", 627 | "x": 7862, 628 | "y": 6500, 629 | "size": 10, 630 | "align": "center" 631 | }, 632 | { 633 | "text": "Gu'Tanoth", 634 | "x": 7386, 635 | "y": 7676, 636 | "size": 10, 637 | "align": "left" 638 | }, 639 | { 640 | "text": "Feldip\nHills", 641 | "x": 7468, 642 | "y": 7814, 643 | "size": 12, 644 | "align": "center", 645 | "bold": true, 646 | "colour": "rgb(254, 165, 0)" 647 | }, 648 | { 649 | "text": "Observatory", 650 | "x": 7184, 651 | "y": 7444, 652 | "size": 10, 653 | "align": "left" 654 | }, 655 | { 656 | "text": "Shilo Village", 657 | "x": 8103, 658 | "y": 7822, 659 | "size": 10, 660 | "align": "left" 661 | }, 662 | { 663 | "text": "Kharazi Jungle", 664 | "x": 8082, 665 | "y": 7970, 666 | "size": 10, 667 | "align": "left" 668 | }, 669 | { 670 | "text": "Chaos\nTemple", 671 | "x": 8988, 672 | "y": 6396, 673 | "size": 10, 674 | "align": "center" 675 | }, 676 | { 677 | "text": "Dark Warriors'\nCastle", 678 | "x": 8550, 679 | "y": 6376, 680 | "size": 10, 681 | "align": "center" 682 | }, 683 | { 684 | "text": "Bandit\nCamp", 685 | "x": 8524, 686 | "y": 6229, 687 | "size": 10, 688 | "align": "center" 689 | }, 690 | { 691 | "text": "Overgrown\nvillage", 692 | "x": 8370, 693 | "y": 6214, 694 | "size": 10, 695 | "align": "left" 696 | }, 697 | { 698 | "text": "Graveyard", 699 | "x": 8800, 700 | "y": 6274, 701 | "size": 10, 702 | "align": "left" 703 | }, 704 | { 705 | "text": "Bone Yard", 706 | "x": 8986, 707 | "y": 6110, 708 | "size": 10, 709 | "align": "left" 710 | }, 711 | { 712 | "text": "Ruins", 713 | "x": 8856, 714 | "y": 6094, 715 | "size": 10, 716 | "align": "left" 717 | }, 718 | { 719 | "text": "Red Dragon\nIsle", 720 | "x": 8880, 721 | "y": 5924, 722 | "size": 10, 723 | "align": "center" 724 | }, 725 | { 726 | "text": "Lava Maze", 727 | "x": 8606, 728 | "y": 5885, 729 | "size": 10, 730 | "align": "left" 731 | }, 732 | { 733 | "text": "Demonic\nRuins", 734 | "x": 9086, 735 | "y": 5804, 736 | "size": 10, 737 | "align": "center" 738 | }, 739 | { 740 | "text": "Agility\ntraining\narea", 741 | "x": 8455, 742 | "y": 5628, 743 | "size": 10, 744 | "align": "center" 745 | }, 746 | { 747 | "text": "Pirate\nHall", 748 | "x": 8540, 749 | "y": 5644, 750 | "size": 10, 751 | "align": "center" 752 | }, 753 | { 754 | "text": "Ice\nPlateau", 755 | "x": 8334, 756 | "y": 5718, 757 | "size": 10, 758 | "align": "center" 759 | }, 760 | { 761 | "text": "Mage Arena", 762 | "x": 8640, 763 | "y": 5714, 764 | "size": 10, 765 | "align": "left" 766 | }, 767 | { 768 | "text": "Deserted\nKeep", 769 | "x": 8792, 770 | "y": 5680, 771 | "size": 10, 772 | "align": "center" 773 | }, 774 | { 775 | "text": "Scorpion\nRavine", 776 | "x": 8948, 777 | "y": 5664, 778 | "size": 10, 779 | "align": "center" 780 | }, 781 | { 782 | "text": "Rouges'\nHouse", 783 | "x": 9108, 784 | "y": 5646, 785 | "size": 8, 786 | "align": "center" 787 | }, 788 | { 789 | "text": "The Wilderness", 790 | "x": 8713, 791 | "y": 6020, 792 | "size": 14, 793 | "align": "left", 794 | "bold": true, 795 | "colour": "rgb(254, 165, 0)" 796 | } 797 | ] 798 | -------------------------------------------------------------------------------- /src/sector.js: -------------------------------------------------------------------------------- 1 | const SectorPainter = require('./sector-painter'); 2 | const Tile = require('./tile'); 3 | 4 | const SECTOR_WIDTH = 48; 5 | const SECTOR_HEIGHT = 48; 6 | 7 | // number of tiles in each sector 8 | const MAX_TILES = SECTOR_WIDTH * SECTOR_HEIGHT; 9 | 10 | // encode the height/colour buffers so we can store 0-255 within 0-127 11 | function encodeBuffer(buffer, lastVal) { 12 | const encoded = new Int8Array(MAX_TILES); 13 | 14 | for (let tileY = 0; tileY < SECTOR_HEIGHT; tileY++) { 15 | for (let tileX = 0; tileX < SECTOR_WIDTH; tileX++) { 16 | const index = tileX * SECTOR_WIDTH + tileY; 17 | const enc = (buffer[index] & 0xff) / 2 - (lastVal & 0x7f); 18 | encoded[index] = enc & 0x7f; 19 | lastVal += enc; 20 | } 21 | } 22 | 23 | return encoded; 24 | } 25 | 26 | // compress an encoded buffer to use numbers > 128 to indicate repeat count 27 | // of the number at the previous index 28 | function compressBuffer(buffer, lastVal = -1) { 29 | const compressed = []; 30 | 31 | let valCountIdx = 0; 32 | 33 | for (let i = 0; i < MAX_TILES; i += 1) { 34 | const val = buffer[i]; 35 | 36 | if (val !== lastVal) { 37 | valCountIdx = compressed.push(val); 38 | lastVal = val; 39 | } else { 40 | const countExists = compressed.length - 1 >= valCountIdx; 41 | 42 | if (countExists && compressed[valCountIdx] >= 255) { 43 | valCountIdx = compressed.push(129) - 1; 44 | } else if (!countExists) { 45 | compressed.push(129); 46 | } else { 47 | compressed[valCountIdx] += 1; 48 | } 49 | } 50 | } 51 | 52 | return new Int8Array(compressed); 53 | } 54 | 55 | // similar to the compress above, except we only store repeat counts for zero 56 | // so there's no need to store which value is being repeated if buffer[index] > 57 | // 128 - it always indicates an amount of zeroes! 58 | function compressZeroBuffer(buffer) { 59 | const compressed = []; 60 | 61 | let lastZeroCount = -1; 62 | let lastZeroIdx = -1; 63 | 64 | for (let i = 0; i < MAX_TILES; i += 1) { 65 | const val = buffer[i] & 0xff; 66 | 67 | if (val !== 0) { 68 | compressed.push(val); 69 | lastZeroCount = -1; 70 | } else { 71 | if (lastZeroCount >= 0 && lastZeroCount < 127) { 72 | lastZeroCount += 1; 73 | compressed[lastZeroIdx] = lastZeroCount + 128; 74 | } else { 75 | lastZeroIdx = compressed.push(129) - 1; 76 | lastZeroCount = 1; 77 | } 78 | } 79 | } 80 | 81 | return new Int8Array(compressed); 82 | } 83 | 84 | // used in jm maps 85 | function compressDeltaBuffer(buffer) { 86 | const compressed = new Int8Array(buffer.length); 87 | compressed[0] = buffer[0]; 88 | 89 | for (let i = 1; i < buffer.length; i++) { 90 | compressed[i] = buffer[i] - buffer[i - 1]; 91 | } 92 | 93 | return compressed; 94 | } 95 | 96 | class Sector { 97 | constructor({ x, y, plane, members, tiles }) { 98 | this.x = x; 99 | this.y = y; 100 | this.plane = plane; 101 | this.members = !!members; 102 | 103 | this.width = SECTOR_WIDTH; 104 | this.height = SECTOR_HEIGHT; 105 | this.empty = true; 106 | 107 | // elevation of each tile from 0-255 108 | this.terrainHeight = new Int8Array(MAX_TILES); 109 | 110 | // colour of each tile from terrain-colours.js palette 111 | this.terrainColour = new Int8Array(MAX_TILES); 112 | 113 | // the direction objects should face 114 | this.tileDirection = new Int8Array(MAX_TILES); 115 | 116 | // the overlay textures 117 | this.tileDecoration = new Int8Array(MAX_TILES); 118 | 119 | // if > 0, the overlay 120 | this.wallsVertical = new Int8Array(MAX_TILES); 121 | this.wallsHorizontal = new Int8Array(MAX_TILES); 122 | this.wallsRoof = new Int8Array(MAX_TILES); 123 | 124 | // 0-12000 is /, 12000-48000 is \, 48000+ is an object ID 125 | this.wallsDiagonal = new Int32Array(MAX_TILES); 126 | 127 | if (tiles && Array.isArray(tiles)) { 128 | this.tiles = []; 129 | 130 | for (let tileX = 0; tileX < SECTOR_WIDTH; tileX++) { 131 | this.tiles.push([]); 132 | 133 | for (let tileY = 0; tileY < SECTOR_HEIGHT; tileY++) { 134 | const tile = tiles[tileX][tileY]; 135 | this.tiles[tileX].push(new Tile({ sector: this, ...tile })); 136 | } 137 | } 138 | 139 | this.populateBuffers(); 140 | } 141 | } 142 | 143 | // parse the .jm file in the map archives 144 | parseJm(mapData) { 145 | let offset = 0; 146 | let lastVal = 0; 147 | 148 | for (let tile = 0; tile < MAX_TILES; tile++) { 149 | lastVal = lastVal + (mapData[offset++] & 0xff); 150 | this.terrainHeight[tile] = lastVal; 151 | } 152 | 153 | lastVal = 0; 154 | 155 | for (let tile = 0; tile < MAX_TILES; tile++) { 156 | lastVal = lastVal + (mapData[offset++] & 0xff); 157 | this.terrainColour[tile] = lastVal; 158 | } 159 | 160 | for (let tile = 0; tile < MAX_TILES; tile++) { 161 | this.wallsVertical[tile] = mapData[offset++] & 0xff; 162 | } 163 | 164 | for (let tile = 0; tile < MAX_TILES; tile++) { 165 | this.wallsHorizontal[tile] = mapData[offset++] & 0xff; 166 | } 167 | 168 | for (let tile = 0; tile < MAX_TILES; tile++) { 169 | this.wallsDiagonal[tile] = 170 | (mapData[offset] << 8) | mapData[offset + 1]; 171 | 172 | offset += 2; 173 | } 174 | 175 | for (let tile = 0; tile < MAX_TILES; tile++) { 176 | this.wallsRoof[tile] = mapData[offset++] & 0xff; 177 | } 178 | 179 | for (let tile = 0; tile < MAX_TILES; tile++) { 180 | this.tileDecoration[tile] = mapData[offset++] & 0xff; 181 | } 182 | 183 | for (let tile = 0; tile < MAX_TILES; tile++) { 184 | this.tileDirection[tile] = mapData[offset++] & 0xff; 185 | } 186 | 187 | this.empty = false; 188 | } 189 | 190 | // parse the .hei file in the land archives 191 | parseHei(mapData) { 192 | let offset = 0; 193 | let lastVal = 0; 194 | 195 | // a form of rle compression. if data[offset] < 128, then it sets tile 196 | // at index to that value. however, if data[offset] is > 128, it takes 197 | // the last value that was < 128 and applies it from index to 198 | // index + (value - 128) 199 | for (let tile = 0; tile < MAX_TILES; ) { 200 | let val = mapData[offset++] & 0xff; 201 | 202 | if (val < 128) { 203 | this.terrainHeight[tile++] = val & 0xff; 204 | lastVal = val; 205 | } 206 | 207 | if (val >= 128) { 208 | for (let i = 0; i < val - 128; i++) { 209 | this.terrainHeight[tile++] = lastVal & 0xff; 210 | } 211 | } 212 | } 213 | 214 | lastVal = 64; 215 | 216 | for (let tileY = 0; tileY < SECTOR_HEIGHT; tileY++) { 217 | for (let tileX = 0; tileX < SECTOR_WIDTH; tileX++) { 218 | const index = tileX * SECTOR_WIDTH + tileY; 219 | 220 | lastVal = this.terrainHeight[index] + (lastVal & 0x7f); 221 | this.terrainHeight[index] = (lastVal * 2) & 0xff; 222 | 223 | if (this.terrainHeight[index] > 0) { 224 | this.empty = false; 225 | } 226 | } 227 | } 228 | 229 | lastVal = 0; 230 | 231 | for (let tile = 0; tile < MAX_TILES; ) { 232 | let val = mapData[offset++] & 0xff; 233 | 234 | if (val < 128) { 235 | this.terrainColour[tile++] = val & 0xff; 236 | lastVal = val; 237 | } 238 | 239 | if (val >= 128) { 240 | for (let i = 0; i < val - 128; i++) { 241 | this.terrainColour[tile++] = lastVal & 0xff; 242 | } 243 | } 244 | } 245 | 246 | lastVal = 35; 247 | 248 | for (let tileY = 0; tileY < SECTOR_HEIGHT; tileY++) { 249 | for (let tileX = 0; tileX < SECTOR_WIDTH; tileX++) { 250 | const index = tileX * SECTOR_WIDTH + tileY; 251 | 252 | lastVal = (this.terrainColour[index] + lastVal) & 0x7f; 253 | this.terrainColour[index] = (lastVal * 2) & 0xff; 254 | 255 | if (this.terrainColour[index] > 0) { 256 | this.empty = false; 257 | } 258 | } 259 | } 260 | } 261 | 262 | // parse the .dat files in the map archives 263 | parseDat(mapData) { 264 | let offset = 0; 265 | 266 | // TODO 267 | const version = 2; 268 | 269 | if (version === 0) { 270 | for (let tile = 0; tile < MAX_TILES; tile++) { 271 | this.wallsVertical[tile] = mapData[offset++] & 0xff; 272 | 273 | if (this.wallsVertical[tile] > 0) { 274 | this.empty = false; 275 | } 276 | } 277 | 278 | for (let tile = 0; tile < MAX_TILES; tile++) { 279 | this.wallsHorizontal[tile] = mapData[offset++] & 0xff; 280 | 281 | if (this.wallsHorizontal[tile] > 0) { 282 | this.empty = false; 283 | } 284 | } 285 | 286 | for (let tile = 0; tile < MAX_TILES; tile++) { 287 | this.wallsDiagonal[tile] = mapData[offset++] & 0xff; 288 | 289 | if (this.wallsDiagonal[tile] > 0) { 290 | this.empty = false; 291 | } 292 | } 293 | 294 | for (let tile = 0; tile < MAX_TILES; tile++) { 295 | let val = mapData[offset++] & 0xff; 296 | 297 | if (val > 0) { 298 | this.wallsDiagonal[tile] = val + Tile.NW_SE_OFFSET; 299 | } 300 | 301 | if (this.wallsDiagonal[tile] > 0) { 302 | this.empty = false; 303 | } 304 | } 305 | } else if (version === 1) { 306 | for (let tile = 0; tile < MAX_TILES; ) { 307 | const val = mapData[offset++] & 0xff; 308 | 309 | if (val < 192) { 310 | this.wallsVertical[tile++] = val; 311 | 312 | if (val > 0) { 313 | this.empty = false; 314 | } 315 | } else { 316 | for (let i = 0; i < val - 192; i++) { 317 | this.wallsVertical[tile++] = 0; 318 | } 319 | } 320 | } 321 | 322 | for (let tile = 0; tile < MAX_TILES; ) { 323 | const val = mapData[offset++] & 0xff; 324 | 325 | if (val < 192) { 326 | this.wallsHorizontal[tile++] = val; 327 | 328 | if (val > 0) { 329 | this.empty = false; 330 | } 331 | } else { 332 | for (let i = 0; i < val - 192; i++) { 333 | this.wallsHorizontal[tile++] = 0; 334 | } 335 | } 336 | } 337 | 338 | for (let tile = 0; tile < MAX_TILES; ) { 339 | const val = mapData[offset++] & 0xff; 340 | 341 | if (val < 192) { 342 | this.wallsDiagonal[tile++] = val; 343 | 344 | if (val > 0) { 345 | this.empty = false; 346 | } 347 | } else { 348 | for (let i = 0; i < val - 192; i++) { 349 | this.wallsDiagonal[tile++] = 0; 350 | } 351 | } 352 | } 353 | 354 | for (let tile = 0; tile < MAX_TILES; ) { 355 | const val = mapData[offset++] & 0xff; 356 | 357 | if (val < 192) { 358 | this.wallsDiagonal[tile++] = val + Tile.NW_SE_OFFSET; 359 | 360 | if (val > 0) { 361 | this.empty = false; 362 | } 363 | } else { 364 | tile += val - 192; 365 | } 366 | } 367 | } else if (version === 2) { 368 | for (let tile = 0; tile < MAX_TILES; ) { 369 | const val = mapData[offset++] & 0xff; 370 | 371 | if (val < 128) { 372 | this.wallsVertical[tile++] = val; 373 | 374 | if (val > 0) { 375 | this.empty = false; 376 | } 377 | } else { 378 | for (let i = 0; i < val - 128; i++) { 379 | this.wallsVertical[tile++] = 0; 380 | } 381 | } 382 | } 383 | 384 | for (let tile = 0; tile < MAX_TILES; ) { 385 | const val = mapData[offset++] & 0xff; 386 | 387 | if (val < 128) { 388 | this.wallsHorizontal[tile++] = val; 389 | 390 | if (val > 0) { 391 | this.empty = false; 392 | } 393 | } else { 394 | for (let i = 0; i < val - 128; i++) { 395 | this.wallsHorizontal[tile++] = 0; 396 | } 397 | } 398 | } 399 | 400 | for (let tile = 0; tile < MAX_TILES; ) { 401 | const val = mapData[offset++] & 0xff; 402 | 403 | if (val < 128) { 404 | this.wallsDiagonal[tile++] = val; 405 | 406 | if (val > 0) { 407 | this.empty = false; 408 | } 409 | } else { 410 | for (let i = 0; i < val - 128; i++) { 411 | this.wallsDiagonal[tile++] = 0; 412 | } 413 | } 414 | } 415 | 416 | for (let tile = 0; tile < MAX_TILES; ) { 417 | const val = mapData[offset++] & 0xff; 418 | 419 | if (val < 128) { 420 | this.wallsDiagonal[tile++] = val + Tile.NW_SE_OFFSET; 421 | 422 | if (val > 0) { 423 | this.empty = false; 424 | } 425 | } else { 426 | tile += val - 128; 427 | } 428 | } 429 | } 430 | 431 | for (let tile = 0; tile < MAX_TILES; ) { 432 | let val = mapData[offset++] & 0xff; 433 | 434 | if (val < 128) { 435 | this.wallsRoof[tile++] = val & 0xff; 436 | } else { 437 | for (let i = 0; i < val - 128; i++) { 438 | this.wallsRoof[tile++] = 0; 439 | } 440 | } 441 | } 442 | 443 | let lastVal = 0; 444 | 445 | for (let tile = 0; tile < MAX_TILES; ) { 446 | let val = mapData[offset++] & 0xff; 447 | 448 | if (val < 128) { 449 | this.tileDecoration[tile++] = val & 0xff; 450 | lastVal = val; 451 | } else { 452 | for (let i = 0; i < val - 128; i++) { 453 | this.tileDecoration[tile++] = lastVal; 454 | } 455 | } 456 | } 457 | 458 | for (let tile = 0; tile < MAX_TILES; ) { 459 | let val = mapData[offset++] & 0xff; 460 | 461 | if (val < 128) { 462 | this.tileDirection[tile++] = val & 0xff; 463 | } else { 464 | for (let i = 0; i < val - 128; i++) { 465 | this.tileDirection[tile++] = 0; 466 | } 467 | } 468 | } 469 | } 470 | 471 | // parse the .loc files in map archives. these appear to hold the object IDs 472 | // and locations for the sectors shown in login. 473 | parseLoc(mapData) { 474 | if (mapData === null || mapData.length < 1) { 475 | return; 476 | } 477 | 478 | let offset = 0; 479 | 480 | for (let tile = 0; tile < MAX_TILES; ) { 481 | let val = mapData[offset++] & 0xff; 482 | 483 | if (val < 128) { 484 | this.wallsDiagonal[tile++] = val + Tile.OBJECT_OFFSET; 485 | } else { 486 | tile += val - 128; 487 | } 488 | } 489 | } 490 | 491 | // creates a 48x48 array with sane Tile objects from the buffers recovered 492 | // from archives 493 | populateTiles() { 494 | this.tiles = []; 495 | 496 | for (let i = 0; i < SECTOR_WIDTH; i += 1) { 497 | this.tiles.push([]); 498 | 499 | for (let j = 0; j < SECTOR_HEIGHT; j += 1) { 500 | const tile = new Tile({ sector: this, x: i, y: j }); 501 | tile.populate(); 502 | this.tiles[i].push(tile); 503 | } 504 | } 505 | 506 | this.tiles.reverse(); 507 | } 508 | 509 | // if we edit tiles, repopulate the appropriate buffers for re-compression 510 | populateBuffers() { 511 | for (let tileY = 0; tileY < SECTOR_HEIGHT; tileY++) { 512 | for (let tileX = 0; tileX < SECTOR_WIDTH; tileX++) { 513 | const index = tileX * SECTOR_WIDTH + tileY; 514 | const tile = this.tiles[SECTOR_WIDTH - 1 - tileX][tileY]; 515 | const diagonal = tile.wall ? tile.wall.diagonal : null; 516 | 517 | this.terrainHeight[index] = tile.elevation; 518 | this.terrainColour[index] = tile.colour; 519 | 520 | this.wallsVertical[index] = tile.wall.vertical || 0; 521 | this.wallsHorizontal[index] = tile.wall.horizontal || 0; 522 | this.wallsRoof[index] = tile.wall.roof || 0; 523 | 524 | if (diagonal && diagonal.direction === '/') { 525 | this.wallsDiagonal[index] = diagonal.overlay; 526 | } else if (diagonal && diagonal.direction === '\\') { 527 | this.wallsDiagonal[index] = 528 | diagonal.overlay + Tile.NW_SE_OFFSET; 529 | } 530 | 531 | if (tile.objectId) { 532 | this.wallsDiagonal[index] = 533 | tile.objectId + Tile.OBJECT_OFFSET + 1; 534 | } else if (tile.itemId) { 535 | this.wallsDiagonal[index] = 536 | tile.itemId + Tile.ITEM_OFFSET + 1; 537 | } else if (tile.npcId) { 538 | this.wallsDiagonal[index] = 539 | tile.npcId + Tile.NPC_OFFSET + 1; 540 | } 541 | 542 | this.tileDecoration[index] = tile.overlay || 0; 543 | this.tileDirection[index] = tile.direction || 0; 544 | } 545 | } 546 | } 547 | 548 | // convert sector to the filename used in the archives 549 | getEntryName() { 550 | return ( 551 | 'm' + 552 | this.plane + 553 | Math.floor(this.x / 10) + 554 | (this.x % 10) + 555 | Math.floor(this.y / 10) + 556 | (this.y % 10) 557 | ); 558 | } 559 | 560 | // convert Tile objects back into a `.hei` file for land archives 561 | toHei() { 562 | const encodedElevation = encodeBuffer(this.terrainHeight, 64); 563 | const encodedColour = encodeBuffer(this.terrainColour, 35); 564 | 565 | const compressedElevation = compressBuffer(encodedElevation, 0); 566 | const compressedColour = compressBuffer(encodedColour, 0); 567 | 568 | const mapData = new Int8Array( 569 | compressedElevation.length + compressedColour.length 570 | ); 571 | 572 | mapData.set(compressedElevation, 0); 573 | mapData.set(compressedColour, compressedElevation.length); 574 | 575 | return mapData; 576 | } 577 | 578 | // convert Tile objects to `.dat` file for map archives 579 | toDat() { 580 | const mapData = []; 581 | 582 | mapData.push(...this.wallsVertical); 583 | mapData.push(...this.wallsHorizontal); 584 | 585 | // add / first 586 | mapData.push( 587 | ...this.wallsDiagonal.map((diagonal) => { 588 | return diagonal < Tile.NW_SE_OFFSET ? diagonal : 0; 589 | }) 590 | ); 591 | 592 | // then add \ 593 | mapData.push( 594 | ...this.wallsDiagonal.map((diagonal) => { 595 | return diagonal >= Tile.NW_SE_OFFSET 596 | ? diagonal - Tile.NW_SE_OFFSET 597 | : 0; 598 | }) 599 | ); 600 | 601 | mapData.push(...compressZeroBuffer(this.wallsRoof)); 602 | mapData.push(...compressBuffer(this.tileDecoration, 0)); 603 | mapData.push(...compressZeroBuffer(this.tileDirection)); 604 | 605 | return new Int8Array(mapData); 606 | } 607 | 608 | // convert Tile objects back into a `.loc` file for map archives, or null 609 | // if no objectIds are stored 610 | toLoc() { 611 | let empty = true; 612 | 613 | const compressedObjects = compressZeroBuffer( 614 | this.wallsDiagonal.map((val) => { 615 | val = val >= Tile.OBJECT_OFFSET ? val - Tile.OBJECT_OFFSET : 0; 616 | 617 | if (val !== 0) { 618 | empty = false; 619 | } 620 | 621 | return val; 622 | }) 623 | ); 624 | 625 | return empty ? null : compressedObjects; 626 | } 627 | 628 | toJm() { 629 | let offset = 0; 630 | 631 | // 7 int8 fields, 1 int16 field 632 | const jmData = new Int8Array(7 * MAX_TILES + 2 * MAX_TILES); 633 | 634 | jmData.set(compressDeltaBuffer(this.terrainHeight), offset); 635 | offset += MAX_TILES; 636 | 637 | jmData.set(compressDeltaBuffer(this.terrainColour), offset); 638 | offset += MAX_TILES; 639 | 640 | jmData.set(this.wallsVertical, offset); 641 | offset += MAX_TILES; 642 | 643 | jmData.set(this.wallsHorizontal, offset); 644 | offset += MAX_TILES; 645 | 646 | for (let i = 0; i < MAX_TILES; i++) { 647 | const value = this.wallsDiagonal[i]; 648 | jmData[offset] = (value >> 8) & 0xff; 649 | jmData[offset + 1] = value & 0xff; 650 | 651 | offset += 2; 652 | } 653 | 654 | jmData.set(this.wallsRoof, offset); 655 | offset += MAX_TILES; 656 | 657 | jmData.set(this.tileDecoration, offset); 658 | offset += MAX_TILES; 659 | 660 | jmData.set(this.tileDirection, offset); 661 | 662 | return jmData; 663 | } 664 | 665 | toCanvas(options = {}, neighbours = []) { 666 | const painter = new SectorPainter(this, options, neighbours); 667 | painter.draw(); 668 | 669 | return painter.canvas; 670 | } 671 | 672 | toString(terminal = false, colourLevel = -1) { 673 | if (!terminal) { 674 | return ( 675 | `[object ${this.constructor.name} ${this.getEntryName()} ` + 676 | `${this.width}x${this.height}]` 677 | ); 678 | } 679 | 680 | const painter = new SectorPainter(this); 681 | 682 | return painter.write(colourLevel); 683 | } 684 | 685 | toJSON() { 686 | return { 687 | x: this.x, 688 | y: this.y, 689 | plane: this.plane, 690 | members: this.members, 691 | tiles: this.tiles 692 | }; 693 | } 694 | } 695 | 696 | module.exports = Sector; 697 | -------------------------------------------------------------------------------- /map-points.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "altar", 4 | "x": 7582, 5 | "y": 7322 6 | }, 7 | { 8 | "type": "altar", 9 | "x": 7607, 10 | "y": 7097 11 | }, 12 | { 13 | "type": "altar", 14 | "x": 7776, 15 | "y": 6746 16 | }, 17 | { 18 | "type": "altar", 19 | "x": 7925, 20 | "y": 6641 21 | }, 22 | { 23 | "type": "altar", 24 | "x": 8121, 25 | "y": 7007 26 | }, 27 | { 28 | "type": "altar", 29 | "x": 8227, 30 | "y": 6630 31 | }, 32 | { 33 | "type": "altar", 34 | "x": 8290, 35 | "y": 6711 36 | }, 37 | { 38 | "type": "altar", 39 | "x": 8441, 40 | "y": 7386 41 | }, 42 | { 43 | "type": "altar", 44 | "x": 8523, 45 | "y": 6658 46 | }, 47 | { 48 | "type": "altar", 49 | "x": 8563, 50 | "y": 6698 51 | }, 52 | { 53 | "type": "altar", 54 | "x": 8975, 55 | "y": 6704 56 | }, 57 | { 58 | "type": "altar", 59 | "x": 9018, 60 | "y": 7329 61 | }, 62 | { 63 | "type": "altar", 64 | "x": 9049, 65 | "y": 6753 66 | }, 67 | { 68 | "type": "altar", 69 | "x": 9054, 70 | "y": 6915 71 | }, 72 | { 73 | "type": "amulet-shop", 74 | "x": 8031, 75 | "y": 7425 76 | }, 77 | { 78 | "type": "anvil", 79 | "x": 7371, 80 | "y": 7024 81 | }, 82 | { 83 | "type": "anvil", 84 | "x": 7608, 85 | "y": 7606 86 | }, 87 | { 88 | "type": "anvil", 89 | "x": 7645, 90 | "y": 7423 91 | }, 92 | { 93 | "type": "anvil", 94 | "x": 7654, 95 | "y": 6852 96 | }, 97 | { 98 | "type": "anvil", 99 | "x": 7814, 100 | "y": 6677 101 | }, 102 | { 103 | "type": "anvil", 104 | "x": 7993, 105 | "y": 7555 106 | }, 107 | { 108 | "type": "anvil", 109 | "x": 8383, 110 | "y": 6788 111 | }, 112 | { 113 | "type": "anvil", 114 | "x": 8474, 115 | "y": 7462 116 | }, 117 | { 118 | "type": "anvil", 119 | "x": 8912, 120 | "y": 6858 121 | }, 122 | { 123 | "type": "anvil", 124 | "x": 9005, 125 | "y": 6835 126 | }, 127 | { 128 | "type": "apothecary", 129 | "x": 8921, 130 | "y": 6884 131 | }, 132 | { 133 | "type": "archery-shop", 134 | "x": 8071, 135 | "y": 6789 136 | }, 137 | { 138 | "type": "archery-shop", 139 | "x": 9004, 140 | "y": 6860 141 | }, 142 | { 143 | "type": "armour-conversion", 144 | "x": 7686, 145 | "y": 7129 146 | }, 147 | { 148 | "type": "armour-conversion", 149 | "x": 9034, 150 | "y": 6880 151 | }, 152 | { 153 | "type": "axe-shop", 154 | "x": 8557, 155 | "y": 7209 156 | }, 157 | { 158 | "type": "axe-shop", 159 | "x": 8980, 160 | "y": 7325 161 | }, 162 | { 163 | "type": "bank", 164 | "x": 7197, 165 | "y": 6694 166 | }, 167 | { 168 | "type": "bank", 169 | "x": 7212, 170 | "y": 6838 171 | }, 172 | { 173 | "type": "bank", 174 | "x": 7588, 175 | "y": 7580 176 | }, 177 | { 178 | "type": "bank", 179 | "x": 7696, 180 | "y": 7163 181 | }, 182 | { 183 | "type": "bank", 184 | "x": 8031, 185 | "y": 6804 186 | }, 187 | { 188 | "type": "bank", 189 | "x": 8359, 190 | "y": 6979 191 | }, 192 | { 193 | "type": "bank", 194 | "x": 8502, 195 | "y": 7026 196 | }, 197 | { 198 | "type": "bank", 199 | "x": 8697, 200 | "y": 7231 201 | }, 202 | { 203 | "type": "bank", 204 | "x": 8705, 205 | "y": 6674 206 | }, 207 | { 208 | "type": "bank", 209 | "x": 8904, 210 | "y": 6829 211 | }, 212 | { 213 | "type": "bank", 214 | "x": 9044, 215 | "y": 6859 216 | }, 217 | { 218 | "type": "bank", 219 | "x": 9080, 220 | "y": 7406 221 | }, 222 | { 223 | "type": "bed", 224 | "x": 7118, 225 | "y": 6733 226 | }, 227 | { 228 | "type": "bed", 229 | "x": 7302, 230 | "y": 6764 231 | }, 232 | { 233 | "type": "bed", 234 | "x": 7380, 235 | "y": 6654 236 | }, 237 | { 238 | "type": "bed", 239 | "x": 7396, 240 | "y": 6933 241 | }, 242 | { 243 | "type": "bed", 244 | "x": 7412, 245 | "y": 7025 246 | }, 247 | { 248 | "type": "bed", 249 | "x": 7450, 250 | "y": 7017 251 | }, 252 | { 253 | "type": "bed", 254 | "x": 7466, 255 | "y": 7153 256 | }, 257 | { 258 | "type": "bed", 259 | "x": 7514, 260 | "y": 7184 261 | }, 262 | { 263 | "type": "bed", 264 | "x": 7540, 265 | "y": 7552 266 | }, 267 | { 268 | "type": "bed", 269 | "x": 7612, 270 | "y": 7012 271 | }, 272 | { 273 | "type": "bed", 274 | "x": 7666, 275 | "y": 6930 276 | }, 277 | { 278 | "type": "bed", 279 | "x": 7670, 280 | "y": 7279 281 | }, 282 | { 283 | "type": "bed", 284 | "x": 7671, 285 | "y": 6778 286 | }, 287 | { 288 | "type": "bed", 289 | "x": 7744, 290 | "y": 7102 291 | }, 292 | { 293 | "type": "bed", 294 | "x": 7948, 295 | "y": 6833 296 | }, 297 | { 298 | "type": "bed", 299 | "x": 7971, 300 | "y": 7323 301 | }, 302 | { 303 | "type": "bed", 304 | "x": 8007, 305 | "y": 7615 306 | }, 307 | { 308 | "type": "bed", 309 | "x": 8029, 310 | "y": 7368 311 | }, 312 | { 313 | "type": "bed", 314 | "x": 8036, 315 | "y": 6774 316 | }, 317 | { 318 | "type": "bed", 319 | "x": 8064, 320 | "y": 6997 321 | }, 322 | { 323 | "type": "bed", 324 | "x": 8231, 325 | "y": 7055 326 | }, 327 | { 328 | "type": "bed", 329 | "x": 8333, 330 | "y": 7439 331 | }, 332 | { 333 | "type": "bed", 334 | "x": 8426, 335 | "y": 6888 336 | }, 337 | { 338 | "type": "bed", 339 | "x": 8460, 340 | "y": 7054 341 | }, 342 | { 343 | "type": "bed", 344 | "x": 8486, 345 | "y": 7474 346 | }, 347 | { 348 | "type": "bed", 349 | "x": 8507, 350 | "y": 7241 351 | }, 352 | { 353 | "type": "bed", 354 | "x": 8599, 355 | "y": 6698 356 | }, 357 | { 358 | "type": "bed", 359 | "x": 8674, 360 | "y": 6684 361 | }, 362 | { 363 | "type": "bed", 364 | "x": 8702, 365 | "y": 7158 366 | }, 367 | { 368 | "type": "bed", 369 | "x": 8728, 370 | "y": 6983 371 | }, 372 | { 373 | "type": "bed", 374 | "x": 8831, 375 | "y": 6821 376 | }, 377 | { 378 | "type": "bed", 379 | "x": 8937, 380 | "y": 6746 381 | }, 382 | { 383 | "type": "bed", 384 | "x": 8988, 385 | "y": 6918 386 | }, 387 | { 388 | "type": "bed", 389 | "x": 8999, 390 | "y": 7318 391 | }, 392 | { 393 | "type": "bed", 394 | "x": 9111, 395 | "y": 6678 396 | }, 397 | { 398 | "type": "bed", 399 | "x": 9111, 400 | "y": 6926 401 | }, 402 | { 403 | "type": "bed", 404 | "x": 9155, 405 | "y": 7400 406 | }, 407 | { 408 | "type": "body-armour-shop", 409 | "x": 8443, 410 | "y": 7054 411 | }, 412 | { 413 | "type": "body-armour-shop", 414 | "x": 8999, 415 | "y": 6821 416 | }, 417 | { 418 | "type": "certificate-trader", 419 | "x": 7538, 420 | "y": 6830 421 | }, 422 | { 423 | "type": "certificate-trader", 424 | "x": 7597, 425 | "y": 7012 426 | }, 427 | { 428 | "type": "certificate-trader", 429 | "x": 8025, 430 | "y": 7343 431 | }, 432 | { 433 | "type": "certificate-trader", 434 | "x": 8068, 435 | "y": 6770 436 | }, 437 | { 438 | "type": "certificate-trader", 439 | "x": 8663, 440 | "y": 7229 441 | }, 442 | { 443 | "type": "tannery", 444 | "x": 8310, 445 | "y": 7164 446 | }, 447 | { 448 | "type": "tannery", 449 | "x": 9092, 450 | "y": 7346 451 | }, 452 | { 453 | "type": "clothes-shop", 454 | "x": 7198, 455 | "y": 6715 456 | }, 457 | { 458 | "type": "clothes-shop", 459 | "x": 8938, 460 | "y": 6870 461 | }, 462 | { 463 | "type": "combat-practice", 464 | "x": 9038, 465 | "y": 6829 466 | }, 467 | { 468 | "type": "cookery-shop", 469 | "x": 7208, 470 | "y": 6631 471 | }, 472 | { 473 | "type": "cookery-shop", 474 | "x": 7281, 475 | "y": 6627 476 | }, 477 | { 478 | "type": "cookery-shop", 479 | "x": 7506, 480 | "y": 7570 481 | }, 482 | { 483 | "type": "crafting-shop", 484 | "x": 7686, 485 | "y": 7100 486 | }, 487 | { 488 | "type": "crafting-shop", 489 | "x": 8358, 490 | "y": 7327 491 | }, 492 | { 493 | "type": "crafting-shop", 494 | "x": 9196, 495 | "y": 7346 496 | }, 497 | { 498 | "type": "fishing-shop", 499 | "x": 7551, 500 | "y": 6870 501 | }, 502 | { 503 | "type": "fishing-shop", 504 | "x": 8094, 505 | "y": 6779 506 | }, 507 | { 508 | "type": "fishing-shop", 509 | "x": 8518, 510 | "y": 7264 511 | }, 512 | { 513 | "type": "fishing-point", 514 | "x": 7073, 515 | "y": 6686 516 | }, 517 | { 518 | "type": "fishing-point", 519 | "x": 7118, 520 | "y": 6855 521 | }, 522 | { 523 | "type": "fishing-point", 524 | "x": 7141, 525 | "y": 6744 526 | }, 527 | { 528 | "type": "fishing-point", 529 | "x": 7347, 530 | "y": 6776 531 | }, 532 | { 533 | "type": "fishing-point", 534 | "x": 7381, 535 | "y": 6857 536 | }, 537 | { 538 | "type": "fishing-point", 539 | "x": 7500, 540 | "y": 6952 541 | }, 542 | { 543 | "type": "fishing-point", 544 | "x": 7576, 545 | "y": 6818 546 | }, 547 | { 548 | "type": "fishing-point", 549 | "x": 7601, 550 | "y": 6849 551 | }, 552 | { 553 | "type": "fishing-point", 554 | "x": 7603, 555 | "y": 6820 556 | }, 557 | { 558 | "type": "fishing-point", 559 | "x": 7910, 560 | "y": 7153 561 | }, 562 | { 563 | "type": "fishing-point", 564 | "x": 7937, 565 | "y": 7290 566 | }, 567 | { 568 | "type": "fishing-point", 569 | "x": 8072, 570 | "y": 6822 571 | }, 572 | { 573 | "type": "fishing-point", 574 | "x": 8105, 575 | "y": 6829 576 | }, 577 | { 578 | "type": "fishing-point", 579 | "x": 8135, 580 | "y": 6838 581 | }, 582 | { 583 | "type": "fishing-point", 584 | "x": 8202, 585 | "y": 7604 586 | }, 587 | { 588 | "type": "fishing-point", 589 | "x": 8229, 590 | "y": 7375 591 | }, 592 | { 593 | "type": "fishing-point", 594 | "x": 8240, 595 | "y": 7353 596 | }, 597 | { 598 | "type": "fishing-point", 599 | "x": 8255, 600 | "y": 7379 601 | }, 602 | { 603 | "type": "fishing-point", 604 | "x": 8446, 605 | "y": 7419 606 | }, 607 | { 608 | "type": "fishing-point", 609 | "x": 8457, 610 | "y": 7432 611 | }, 612 | { 613 | "type": "fishing-point", 614 | "x": 8652, 615 | "y": 7255 616 | }, 617 | { 618 | "type": "fishing-point", 619 | "x": 8665, 620 | "y": 7283 621 | }, 622 | { 623 | "type": "fishing-point", 624 | "x": 8682, 625 | "y": 7297 626 | }, 627 | { 628 | "type": "fishing-point", 629 | "x": 8716, 630 | "y": 6841 631 | }, 632 | { 633 | "type": "fishing-point", 634 | "x": 8730, 635 | "y": 6825 636 | }, 637 | { 638 | "type": "fishing-point", 639 | "x": 8972, 640 | "y": 7211 641 | }, 642 | { 643 | "type": "fishing-point", 644 | "x": 9076, 645 | "y": 7467 646 | }, 647 | { 648 | "type": "food-shop", 649 | "x": 7198, 650 | "y": 6655 651 | }, 652 | { 653 | "type": "food-shop", 654 | "x": 7717, 655 | "y": 7117 656 | }, 657 | { 658 | "type": "food-shop", 659 | "x": 7718, 660 | "y": 7091 661 | }, 662 | { 663 | "type": "food-shop", 664 | "x": 7997, 665 | "y": 7375 666 | }, 667 | { 668 | "type": "food-shop", 669 | "x": 8528, 670 | "y": 7293 671 | }, 672 | { 673 | "type": "shield-shop", 674 | "x": 8423, 675 | "y": 6918 676 | }, 677 | { 678 | "type": "furnace", 679 | "x": 7574, 680 | "y": 7097 681 | }, 682 | { 683 | "type": "furnace", 684 | "x": 8093, 685 | "y": 7006 686 | }, 687 | { 688 | "type": "furnace", 689 | "x": 8426, 690 | "y": 6956 691 | }, 692 | { 693 | "type": "furnace", 694 | "x": 8956, 695 | "y": 7205 696 | }, 697 | { 698 | "type": "furnace", 699 | "x": 9096, 700 | "y": 7362 701 | }, 702 | { 703 | "type": "gem-shop", 704 | "x": 7700, 705 | "y": 7119 706 | }, 707 | { 708 | "type": "gem-shop", 709 | "x": 8357, 710 | "y": 7025 711 | }, 712 | { 713 | "type": "gem-shop", 714 | "x": 9091, 715 | "y": 7323 716 | }, 717 | { 718 | "type": "general-shop", 719 | "x": 7293, 720 | "y": 7080 721 | }, 722 | { 723 | "type": "general-shop", 724 | "x": 7403, 725 | "y": 7434 726 | }, 727 | { 728 | "type": "general-shop", 729 | "x": 7611, 730 | "y": 7125 731 | }, 732 | { 733 | "type": "general-shop", 734 | "x": 7970, 735 | "y": 7575 736 | }, 737 | { 738 | "type": "general-shop", 739 | "x": 8037, 740 | "y": 6838 741 | }, 742 | { 743 | "type": "general-shop", 744 | "x": 8265, 745 | "y": 7462 746 | }, 747 | { 748 | "type": "general-shop", 749 | "x": 8360, 750 | "y": 7302 751 | }, 752 | { 753 | "type": "general-shop", 754 | "x": 8393, 755 | "y": 6919 756 | }, 757 | { 758 | "type": "general-shop", 759 | "x": 8679, 760 | "y": 6647 761 | }, 762 | { 763 | "type": "general-shop", 764 | "x": 8945, 765 | "y": 7246 766 | }, 767 | { 768 | "type": "general-shop", 769 | "x": 8968, 770 | "y": 6866 771 | }, 772 | { 773 | "type": "general-shop", 774 | "x": 9182, 775 | "y": 7365 776 | }, 777 | { 778 | "type": "helmet-shop", 779 | "x": 8644, 780 | "y": 6850 781 | }, 782 | { 783 | "type": "herblaw-shop", 784 | "x": 8039, 785 | "y": 7014 786 | }, 787 | { 788 | "type": "herblaw-shop", 789 | "x": 8248, 790 | "y": 6841 791 | }, 792 | { 793 | "type": "jewellery-shop", 794 | "x": 8513, 795 | "y": 7218 796 | }, 797 | { 798 | "type": "kebab-shop", 799 | "x": 9089, 800 | "y": 7375 801 | }, 802 | { 803 | "type": "leg-armour-shop", 804 | "x": 9185, 805 | "y": 7383 806 | }, 807 | { 808 | "type": "mace-shop", 809 | "x": 8365, 810 | "y": 6939 811 | }, 812 | { 813 | "type": "magic-shop", 814 | "x": 8531, 815 | "y": 7220 816 | }, 817 | { 818 | "type": "magic-shop", 819 | "x": 9047, 820 | "y": 6889 821 | }, 822 | { 823 | "type": "mining-site", 824 | "x": 7507, 825 | "y": 7279 826 | }, 827 | { 828 | "type": "mining-site", 829 | "x": 7523, 830 | "y": 6675 831 | }, 832 | { 833 | "type": "mining-site", 834 | "x": 7641, 835 | "y": 7454 836 | }, 837 | { 838 | "type": "mining-site", 839 | "x": 7786, 840 | "y": 7033 841 | }, 842 | { 843 | "type": "mining-site", 844 | "x": 7837, 845 | "y": 7350 846 | }, 847 | { 848 | "type": "mining-site", 849 | "x": 7840, 850 | "y": 7447 851 | }, 852 | { 853 | "type": "mining-site", 854 | "x": 8084, 855 | "y": 6922 856 | }, 857 | { 858 | "type": "mining-site", 859 | "x": 8113, 860 | "y": 7152 861 | }, 862 | { 863 | "type": "mining-site", 864 | "x": 8262, 865 | "y": 6974 866 | }, 867 | { 868 | "type": "mining-site", 869 | "x": 8335, 870 | "y": 7157 871 | }, 872 | { 873 | "type": "mining-site", 874 | "x": 8416, 875 | "y": 7239 876 | }, 877 | { 878 | "type": "mining-site", 879 | "x": 8676, 880 | "y": 6832 881 | }, 882 | { 883 | "type": "mining-site", 884 | "x": 8870, 885 | "y": 6940 886 | }, 887 | { 888 | "type": "mining-site", 889 | "x": 9132, 890 | "y": 6959 891 | }, 892 | { 893 | "type": "mining-site", 894 | "x": 9136, 895 | "y": 7077 896 | }, 897 | { 898 | "type": "pickable-lock", 899 | "x": 7500, 900 | "y": 6989 901 | }, 902 | { 903 | "type": "pickable-lock", 904 | "x": 7517, 905 | "y": 7133 906 | }, 907 | { 908 | "type": "pickable-lock", 909 | "x": 7528, 910 | "y": 7069 911 | }, 912 | { 913 | "type": "pickable-lock", 914 | "x": 7598, 915 | "y": 7060 916 | }, 917 | { 918 | "type": "pickable-lock", 919 | "x": 7732, 920 | "y": 7127 921 | }, 922 | { 923 | "type": "pickable-lock", 924 | "x": 7736, 925 | "y": 7089 926 | }, 927 | { 928 | "type": "pub", 929 | "x": 7281, 930 | "y": 6646 931 | }, 932 | { 933 | "type": "pub", 934 | "x": 7457, 935 | "y": 7610 936 | }, 937 | { 938 | "type": "pub", 939 | "x": 7504, 940 | "y": 7070 941 | }, 942 | { 943 | "type": "pub", 944 | "x": 7565, 945 | "y": 7466 946 | }, 947 | { 948 | "type": "pub", 949 | "x": 7780, 950 | "y": 6675 951 | }, 952 | { 953 | "type": "pub", 954 | "x": 7994, 955 | "y": 7423 956 | }, 957 | { 958 | "type": "pub", 959 | "x": 8394, 960 | "y": 6960 961 | }, 962 | { 963 | "type": "pub", 964 | "x": 8587, 965 | "y": 7194 966 | }, 967 | { 968 | "type": "pub", 969 | "x": 8986, 970 | "y": 6890 971 | }, 972 | { 973 | "type": "pub", 974 | "x": 9105, 975 | "y": 6662 976 | }, 977 | { 978 | "type": "scimitar-shop", 979 | "x": 9121, 980 | "y": 7349 981 | }, 982 | { 983 | "type": "silk-trader", 984 | "x": 7655, 985 | "y": 7107 986 | }, 987 | { 988 | "type": "silk-trader", 989 | "x": 9123, 990 | "y": 7329 991 | }, 992 | { 993 | "type": "skirt-armour-shop", 994 | "x": 9188, 995 | "y": 7407 996 | }, 997 | { 998 | "type": "spinning-wheel", 999 | "x": 7167, 1000 | "y": 6886 1001 | }, 1002 | { 1003 | "type": "spinning-wheel", 1004 | "x": 7185, 1005 | "y": 6862 1006 | }, 1007 | { 1008 | "type": "spinning-wheel", 1009 | "x": 7777, 1010 | "y": 6717 1011 | }, 1012 | { 1013 | "type": "spinning-wheel", 1014 | "x": 8295, 1015 | "y": 7162 1016 | }, 1017 | { 1018 | "type": "spinning-wheel", 1019 | "x": 8662, 1020 | "y": 6847 1021 | }, 1022 | { 1023 | "type": "spinning-wheel", 1024 | "x": 8932, 1025 | "y": 7322 1026 | }, 1027 | { 1028 | "type": "staff-shop", 1029 | "x": 8930, 1030 | "y": 6835 1031 | }, 1032 | { 1033 | "type": "sword-shop", 1034 | "x": 8210, 1035 | "y": 6829 1036 | }, 1037 | { 1038 | "type": "sword-shop", 1039 | "x": 8944, 1040 | "y": 6894 1041 | }, 1042 | { 1043 | "type": "rare-trees", 1044 | "x": 7079, 1045 | "y": 6886 1046 | }, 1047 | { 1048 | "type": "rare-trees", 1049 | "x": 7157, 1050 | "y": 6852 1051 | }, 1052 | { 1053 | "type": "rare-trees", 1054 | "x": 7671, 1055 | "y": 6704 1056 | }, 1057 | { 1058 | "type": "rare-trees", 1059 | "x": 7791, 1060 | "y": 6801 1061 | }, 1062 | { 1063 | "type": "rare-trees", 1064 | "x": 7797, 1065 | "y": 6744 1066 | }, 1067 | { 1068 | "type": "rare-trees", 1069 | "x": 7774, 1070 | "y": 6917 1071 | }, 1072 | { 1073 | "type": "rare-trees", 1074 | "x": 7831, 1075 | "y": 6645 1076 | }, 1077 | { 1078 | "type": "rare-trees", 1079 | "x": 7857, 1080 | "y": 7019 1081 | }, 1082 | { 1083 | "type": "quest", 1084 | "x": 7242, 1085 | "y": 6668 1086 | }, 1087 | { 1088 | "type": "quest", 1089 | "x": 7363, 1090 | "y": 7407 1091 | }, 1092 | { 1093 | "type": "quest", 1094 | "x": 7391, 1095 | "y": 6668 1096 | }, 1097 | { 1098 | "type": "quest", 1099 | "x": 7491, 1100 | "y": 7055 1101 | }, 1102 | { 1103 | "type": "quest", 1104 | "x": 7495, 1105 | "y": 7084 1106 | }, 1107 | { 1108 | "type": "quest", 1109 | "x": 7497, 1110 | "y": 7181 1111 | }, 1112 | { 1113 | "type": "quest", 1114 | "x": 7530, 1115 | "y": 7121 1116 | }, 1117 | { 1118 | "type": "quest", 1119 | "x": 7597, 1120 | "y": 7315 1121 | }, 1122 | { 1123 | "type": "quest", 1124 | "x": 7603, 1125 | "y": 7220 1126 | }, 1127 | { 1128 | "type": "quest", 1129 | "x": 7626, 1130 | "y": 7349 1131 | }, 1132 | { 1133 | "type": "quest", 1134 | "x": 7792, 1135 | "y": 7158 1136 | }, 1137 | { 1138 | "type": "quest", 1139 | "x": 7815, 1140 | "y": 6848 1141 | }, 1142 | { 1143 | "type": "quest", 1144 | "x": 7955, 1145 | "y": 6633 1146 | }, 1147 | { 1148 | "type": "quest", 1149 | "x": 8042, 1150 | "y": 7575 1151 | }, 1152 | { 1153 | "type": "quest", 1154 | "x": 8067, 1155 | "y": 6695 1156 | }, 1157 | { 1158 | "type": "quest", 1159 | "x": 8214, 1160 | "y": 6651 1161 | }, 1162 | { 1163 | "type": "quest", 1164 | "x": 8281, 1165 | "y": 6788 1166 | }, 1167 | { 1168 | "type": "quest", 1169 | "x": 8294, 1170 | "y": 6696 1171 | }, 1172 | { 1173 | "type": "quest", 1174 | "x": 8369, 1175 | "y": 6795 1176 | }, 1177 | { 1178 | "type": "quest", 1179 | "x": 8402, 1180 | "y": 7004 1181 | }, 1182 | { 1183 | "type": "quest", 1184 | "x": 8405, 1185 | "y": 7324 1186 | }, 1187 | { 1188 | "type": "quest", 1189 | "x": 8417, 1190 | "y": 7033 1191 | }, 1192 | { 1193 | "type": "quest", 1194 | "x": 8587, 1195 | "y": 7209 1196 | }, 1197 | { 1198 | "type": "quest", 1199 | "x": 8602, 1200 | "y": 7203 1201 | }, 1202 | { 1203 | "type": "quest", 1204 | "x": 8702, 1205 | "y": 7401 1206 | }, 1207 | { 1208 | "type": "quest", 1209 | "x": 8703, 1210 | "y": 7181 1211 | }, 1212 | { 1213 | "type": "quest", 1214 | "x": 8721, 1215 | "y": 7033 1216 | }, 1217 | { 1218 | "type": "quest", 1219 | "x": 8821, 1220 | "y": 7330 1221 | }, 1222 | { 1223 | "type": "quest", 1224 | "x": 8847, 1225 | "y": 6820 1226 | }, 1227 | { 1228 | "type": "quest", 1229 | "x": 8873, 1230 | "y": 7177 1231 | }, 1232 | { 1233 | "type": "quest", 1234 | "x": 8949, 1235 | "y": 7303 1236 | }, 1237 | { 1238 | "type": "quest", 1239 | "x": 8953, 1240 | "y": 6867 1241 | }, 1242 | { 1243 | "type": "quest", 1244 | "x": 8958, 1245 | "y": 6694 1246 | }, 1247 | { 1248 | "type": "quest", 1249 | "x": 9021, 1250 | "y": 7313 1251 | }, 1252 | { 1253 | "type": "quest", 1254 | "x": 9095, 1255 | "y": 6893 1256 | }, 1257 | { 1258 | "type": "quest", 1259 | "x": 9125, 1260 | "y": 7414 1261 | }, 1262 | { 1263 | "type": "dungeon", 1264 | "x": 7212, 1265 | "y": 7075 1266 | }, 1267 | { 1268 | "type": "dungeon", 1269 | "x": 7388, 1270 | "y": 7449 1271 | }, 1272 | { 1273 | "type": "dungeon", 1274 | "x": 7495, 1275 | "y": 6975 1276 | }, 1277 | { 1278 | "type": "dungeon", 1279 | "x": 7497, 1280 | "y": 7294 1281 | }, 1282 | { 1283 | "type": "dungeon", 1284 | "x": 7550, 1285 | "y": 7489 1286 | }, 1287 | { 1288 | "type": "dungeon", 1289 | "x": 7580, 1290 | "y": 7616 1291 | }, 1292 | { 1293 | "type": "dungeon", 1294 | "x": 7750, 1295 | "y": 6871 1296 | }, 1297 | { 1298 | "type": "dungeon", 1299 | "x": 7780, 1300 | "y": 7136 1301 | }, 1302 | { 1303 | "type": "dungeon", 1304 | "x": 8066, 1305 | "y": 6960 1306 | }, 1307 | { 1308 | "type": "dungeon", 1309 | "x": 8079, 1310 | "y": 7410 1311 | }, 1312 | { 1313 | "type": "dungeon", 1314 | "x": 8093, 1315 | "y": 7207 1316 | }, 1317 | { 1318 | "type": "dungeon", 1319 | "x": 8150, 1320 | "y": 6630 1321 | }, 1322 | { 1323 | "type": "dungeon", 1324 | "x": 8226, 1325 | "y": 6882 1326 | }, 1327 | { 1328 | "type": "dungeon", 1329 | "x": 8492, 1330 | "y": 7449 1331 | }, 1332 | { 1333 | "type": "dungeon", 1334 | "x": 8704, 1335 | "y": 6726 1336 | }, 1337 | { 1338 | "type": "dungeon", 1339 | "x": 9017, 1340 | "y": 6743 1341 | }, 1342 | { 1343 | "type": "quest", 1344 | "x": 9293, 1345 | "y": 7034 1346 | }, 1347 | { 1348 | "type": "quest", 1349 | "x": 8854, 1350 | "y": 6862 1351 | }, 1352 | { 1353 | "type": "quest", 1354 | "x": 8854, 1355 | "y": 6862 1356 | }, 1357 | { 1358 | "type": "quest", 1359 | "x": 9163, 1360 | "y": 7536 1361 | }, 1362 | { 1363 | "type": "bank", 1364 | "x": 9176, 1365 | "y": 7516 1366 | }, 1367 | { 1368 | "type": "general-shop", 1369 | "x": 9162, 1370 | "y": 7508 1371 | }, 1372 | { 1373 | "type": "quest", 1374 | "x": 7812, 1375 | "y": 6970 1376 | }, 1377 | { 1378 | "type": "altar", 1379 | "x": 8350, 1380 | "y": 6628 1381 | }, 1382 | { 1383 | "type": "dungeon", 1384 | "x": 8514, 1385 | "y": 6804 1386 | }, 1387 | { 1388 | "type": "quest", 1389 | "x": 7888, 1390 | "y": 6534 1391 | }, 1392 | { 1393 | "type": "dungeon", 1394 | "x": 8739, 1395 | "y": 6765 1396 | }, 1397 | { 1398 | "type": "quest", 1399 | "x": 7207, 1400 | "y": 7364 1401 | }, 1402 | { 1403 | "type": "quest", 1404 | "x": 7438, 1405 | "y": 7540 1406 | }, 1407 | { 1408 | "type": "mining-site", 1409 | "x": 8073, 1410 | "y": 7787 1411 | }, 1412 | { 1413 | "type": "general-shop", 1414 | "x": 8097, 1415 | "y": 7863 1416 | }, 1417 | { 1418 | "type": "altar", 1419 | "x": 7253, 1420 | "y": 7272 1421 | }, 1422 | { 1423 | "type": "pub", 1424 | "x": 8161, 1425 | "y": 7796 1426 | }, 1427 | { 1428 | "type": "bed", 1429 | "x": 8176, 1430 | "y": 7806 1431 | }, 1432 | { 1433 | "type": "fishing-point", 1434 | "x": 8180, 1435 | "y": 7834 1436 | }, 1437 | { 1438 | "type": "fishing-shop", 1439 | "x": 8171, 1440 | "y": 7850 1441 | }, 1442 | { 1443 | "type": "furnace", 1444 | "x": 8152, 1445 | "y": 7848 1446 | }, 1447 | { 1448 | "type": "bank", 1449 | "x": 8146, 1450 | "y": 7874 1451 | }, 1452 | { 1453 | "type": "quest", 1454 | "x": 8204, 1455 | "y": 7878 1456 | }, 1457 | { 1458 | "type": "mining-site", 1459 | "x": 8119, 1460 | "y": 7707 1461 | }, 1462 | { 1463 | "type": "mining-site", 1464 | "x": 8692, 1465 | "y": 6510 1466 | }, 1467 | { 1468 | "type": "mining-site", 1469 | "x": 8526, 1470 | "y": 6449 1471 | }, 1472 | { 1473 | "type": "mining-site", 1474 | "x": 8674, 1475 | "y": 6076 1476 | }, 1477 | { 1478 | "type": "mining-site", 1479 | "x": 9032, 1480 | "y": 5988 1481 | }, 1482 | { 1483 | "type": "mining-site", 1484 | "x": 8581, 1485 | "y": 5792 1486 | }, 1487 | { 1488 | "type": "fishing-point", 1489 | "x": 8576, 1490 | "y": 6200 1491 | }, 1492 | { 1493 | "type": "cookery-shop", 1494 | "x": 8546, 1495 | "y": 6194 1496 | }, 1497 | { 1498 | "type": "general-shop", 1499 | "x": 8512, 1500 | "y": 6209 1501 | }, 1502 | { 1503 | "type": "furnace", 1504 | "x": 8909, 1505 | "y": 6091 1506 | }, 1507 | { 1508 | "type": "anvil", 1509 | "x": 8444, 1510 | "y": 6214 1511 | }, 1512 | { 1513 | "type": "bed", 1514 | "x": 8594, 1515 | "y": 5741 1516 | }, 1517 | { 1518 | "type": "dungeon", 1519 | "x": 8545, 1520 | "y": 5706 1521 | }, 1522 | { 1523 | "type": "dungeon", 1524 | "x": 8616, 1525 | "y": 5862 1526 | }, 1527 | { 1528 | "type": "dungeon", 1529 | "x": 8505, 1530 | "y": 5879 1531 | }, 1532 | { 1533 | "type": "dungeon", 1534 | "x": 8658, 1535 | "y": 6493 1536 | }, 1537 | { 1538 | "type": "altar", 1539 | "x": 8354, 1540 | "y": 5926 1541 | }, 1542 | { 1543 | "type": "altar", 1544 | "x": 9001, 1545 | "y": 6424 1546 | } 1547 | ] -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published by 637 | the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | --------------------------------------------------------------------------------