Permalink
Cannot retrieve contributors at this time
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
197 lines (173 sloc)
5.53 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // PICO-8 cart support for Tiled | |
| // | |
| // Copyright © 2020—2021 Sam Hocevar <sam@hocevar.net> | |
| // | |
| // This program is free software. It comes without any warranty, to | |
| // the extent permitted by applicable law. You can redistribute it | |
| // and/or modify it under the terms of the Do What the Fuck You Want | |
| // to Public License, Version 2, as published by the WTFPL Task Force. | |
| // See http://www.wtfpl.net/ for more details. | |
| // | |
| const PALETTE = | |
| [ | |
| '#000000', // black | |
| '#1d2b53', // dark_blue | |
| '#7e2553', // dark_purple | |
| '#008751', // dark_green | |
| '#ab5236', // brown | |
| '#5f574f', // dark_gray | |
| '#c2c3c7', // light_gray | |
| '#fff1e8', // white | |
| '#ff004d', // red | |
| '#ffa300', // orange | |
| '#ffec27', // yellow | |
| '#00e436', // green | |
| '#29adff', // blue | |
| '#83769c', // indigo | |
| '#ff77a8', // pink | |
| '#ffccaa', // peach | |
| '#291814', | |
| '#111d35', | |
| '#422136', | |
| '#125359', | |
| '#742f29', | |
| '#49333b', | |
| '#a28879', | |
| '#f3ef7d', | |
| '#be1250', | |
| '#ff6c24', | |
| '#a8e72e', | |
| '#00b543', | |
| '#065ab5', | |
| '#754665', | |
| '#ff6e59', | |
| '#ff9d81', | |
| ]; | |
| const TILED_VERSION = tiled.version.split('.').map((e,i) => e*100**(2-i)).reduce((a,b) => a+b); | |
| const HEADER = 'pico-8 cartridge'; | |
| const PROPNAME = 'Private Data'; | |
| function tohex(x, ndigits) | |
| { | |
| return (x + (1 << (ndigits * 4))).toString(16).slice(-ndigits); | |
| } | |
| function fromhex(s) | |
| { | |
| return Number('0x'+s); | |
| } | |
| // Extract a hexadecimal section from a p8 cart data, e.g. ‘__gfx__’ | |
| function p8_extract(buf, header) | |
| { | |
| return p8_split(buf, header)[1]; | |
| } | |
| // Split a p8 cart in order to replace a hex section | |
| function p8_split(buf, header) | |
| { | |
| let gpos = buf.indexOf(header); | |
| gpos = gpos < 0 ? buf.length : gpos + header.length; | |
| let gend = buf.indexOf('__', gpos); | |
| if (gend < 0) | |
| gend = buf.length; | |
| return [ buf.slice(0, gpos), | |
| buf.slice(gpos, gend).replace(/[^0-9a-fA-F]+/g, ''), | |
| buf.slice(gend, buf.length) ]; | |
| } | |
| function pico8_read(filename) | |
| { | |
| let f = new BinaryFile(filename); | |
| let cart = f.readAll().toString(); | |
| f.close(); | |
| if (cart.slice(0, HEADER.length) != HEADER) | |
| throw new TypeError('Not a PICO-8 cartridge!'); | |
| // Create a map | |
| let tm = new TileMap(); | |
| tm.setSize(128, 64); | |
| tm.setTileSize(8, 8); | |
| tm.orientation = TileMap.Orthogonal; | |
| tm.backgroundColor = PALETTE[0]; | |
| //tm.setProperty('Show Sprite 0', false); | |
| tm.setProperty(PROPNAME, Qt.btoa(cart)); | |
| // Create an image and a tileset for the palette | |
| let tsize = 12; | |
| // TODO | |
| // Read gfx data into an image | |
| let gfx = p8_extract(cart, '__gfx__'); | |
| let img = new Image(128, 128, Image.Format_Indexed8); | |
| img.setColorTable(PALETTE); | |
| for (let i = 0; i < Math.min(128 * 128, gfx.length); ++i) | |
| img.setPixel(i % 128, Math.floor(i / 128), fromhex(gfx[i])); | |
| // Create a tileset from sprite image | |
| let t = new Tileset('PICO-8 Sprites'); | |
| t.backgroundColor = PALETTE[3]; | |
| t.setTileSize(8, 8); | |
| t.loadFromImage(img); | |
| tm.addTileset(t); | |
| // Read map data into a tile layer | |
| let map = p8_extract(cart, '__map__'); | |
| let tl = new TileLayer(); | |
| tl.width = 128; | |
| tl.height = 64; | |
| let tle = tl.edit(); | |
| function set_tile(x, y, s) | |
| { | |
| let id = fromhex(s); | |
| if (id > 0) | |
| tle.setTile(x, y, t.tile(id)); | |
| } | |
| for (let i = 0; i < Math.min(64 * 128, Math.floor(map.length / 2)); ++i) | |
| set_tile(i % 128, Math.floor(i / 128), map.substring(i * 2, i * 2 + 2)); | |
| // The second part of the sprite data also contains map data | |
| let gfx2 = gfx.slice(128 * 64).replace(/(.)(.)/g, '$2$1');; | |
| for (let i = 0; i < Math.min(128 * 64, gfx2.length); i += 2) | |
| set_tile(Math.floor(i / 2) % 128, 32 + Math.floor(i / 256), gfx2.substring(i, i + 2)); | |
| tle.apply(); | |
| tm.addLayer(tl); | |
| return tm; | |
| } | |
| function pico8_write(tm, filename) | |
| { | |
| let cart = Qt.atob(tm.property(PROPNAME)); | |
| if (cart.slice(0, HEADER.length) != HEADER) | |
| throw new TypeError('This map was not loaded from a PICO-8 cart'); | |
| let layer = tm.layerAt(0); | |
| let eol = cart.indexOf('\r\n') >= 0 ? '\r\n' : '\n'; | |
| // Convert map data to hex | |
| let data = ''; | |
| for (let i = 0; i < 128 * 64; ++i) | |
| { | |
| let t = layer.cellAt(i % 128, Math.floor(i / 128)).tileId; | |
| data += tohex(Math.max(t, 0), 2); | |
| } | |
| // Retrieve gfx data from the original cart. | |
| let [ prefix, gfx, suffix ] = p8_split(cart, '__gfx__'); | |
| // The first 64 lines must be preserved. The next 64 lines are taken from | |
| // the map section because they are from the shared area. | |
| gfx = gfx.slice(0, 128 * 64).padEnd(128 * 64, '0') | |
| + data.slice(128 * 64, 128 * 128).replace(/(.)(.)/g, '$2$1'); | |
| // Remove empty lines and store | |
| gfx = gfx.slice(0, 128 * 128).replace(/(0{128})+$/, ''); | |
| cart = [prefix].concat(gfx.match(/.{128}/g)).concat(suffix).join(eol); | |
| // Store map data. Contrary to gfx data, nothing is preserved. | |
| [ prefix, map, suffix ] = p8_split(cart, '__map__'); | |
| map = data.slice(0, 256 * 32).replace(/(0{256})+$/, ''); | |
| cart = [prefix].concat(map.match(/.{256}/g)).concat(suffix).join(eol); | |
| // Save the file | |
| let f = new BinaryFile(filename, BinaryFile.WriteOnly); | |
| f.write(cart); | |
| f.commit(); | |
| } | |
| if (TILED_VERSION >= 10500) | |
| { | |
| const pico8_format = | |
| { | |
| name: 'PICO-8 cart (*.p8)', | |
| extension: 'p8', | |
| read: pico8_read, | |
| write: pico8_write, | |
| }; | |
| tiled.registerMapFormat('PICO-8', pico8_format); | |
| } | |
| else | |
| { | |
| console.warn(`Tiled version ${tiled.version} is too old for the PICO-8 plugin (1.5.0 required)`); | |
| } |