#include #include #include #include #include #include #include #include #include "lodepng.h" #include "cart.h" #include "filehelpers.h" #include "stringToDataHelpers.h" #include "utils.h" #include "logger.h" #include "emojiconversion.h" #include "DefaultCart.h" #include "FakoSettings.h" static char const *legacyCompressionLut = "\n 0123456789abcdefghijklmnopqrstuvwxyz!#%(){}[]<>+=/*:;.,~_"; static std::regex _includeRegex = std::regex("\\s*#include\\s+([\\\\/A-Za-z0-9_\\-\\.]+)"); //https://github.com/samhocevar/zepto8/blob/b1a13516945c49e47495c739e6a43a241ad99291/src/pico8/code.cpp // Move to front structure struct move_to_front { move_to_front() { reset(); } void reset() { for (int n = 0; n < 256; ++n) state[n] = uint8_t(n); } // Get the nth byte and move it to front uint8_t get(int n) { std::rotate(state.begin(), state.begin() + n, state.begin() + n + 1); return state.front(); } // Find index of a given byte in the structure int find(uint8_t ch) { auto val = std::find(state.begin(), state.end(), ch); return int(std::distance(state.begin(), val)); } // Push a character and return its previous index, allowing the caller to compute the cost // of the operation. This operation can be undone by pop_op(). int push_op(uint8_t ch) { int n = find(ch); get(n); ops.push(uint8_t(n)); return n; } // Undo an push_op() operation void pop_op() { std::rotate(state.begin(), state.begin() + 1, state.begin() + ops.top() + 1); ops.pop(); } private: std::array state; std::stack ops; }; //implementation from zepto8 code.cpp, pxa_decompress //https://github.com/samhocevar/zepto8/blob/b1a13516945c49e47495c739e6a43a241ad99291/src/pico8/code.cpp static std::string pxa_decompress(uint8_t const *input) { size_t length = input[4] * 256 + input[5]; size_t compressed = input[6] * 256 + input[7]; size_t pos = size_t(8) * 8; // stream position in bits auto get_bits = [&](size_t count) -> uint32_t { uint32_t n = 0; for (size_t i = 0; i < count && pos < compressed * 8; ++i, ++pos) n |= ((input[pos >> 3] >> (pos & 0x7)) & 0x1) << i; return n; }; move_to_front mtf; std::string ret; //TRACE("# Size: %d (%04x)\n", int(compressed), int(compressed)); while (ret.size() < length && pos < compressed * 8) { auto oldpos = pos; (void)oldpos; if (get_bits(1)) { int nbits = 4; while (get_bits(1)) ++nbits; int n = get_bits(nbits) + (1 << nbits) - 16; uint8_t ch = mtf.get(n); if (!ch) break; //TRACE("%04x [%d] %s\n", int(ret.size()), int(pos-oldpos), printable(ch).c_str()); ret.push_back(char(ch)); } else { int nbits = get_bits(1) ? get_bits(1) ? 5 : 10 : 15; int offset = get_bits(nbits) + 1; if (nbits == 10 && offset == 1) { uint8_t ch = get_bits(8); while (ch) { ret.push_back(char(ch)); ch = get_bits(8); } //TRACE("%04x [%d] #%d\n", int(ret.size()), int(pos-oldpos), int(pos-oldpos-21) / 8); } else { int n, len = 3; do len += (n = get_bits(3)); while (n == 7); //TRACE("%04x [%d] %d@-%d\n", int(ret.size()), int(pos-oldpos), len, offset); for (int i = 0; i < len; ++i) ret.push_back(ret[ret.size() - offset]); } } } return ret; } #define HEADERLEN 8 bool Cart::loadCartFromPng(std::vector image) { //160x205 == 32800 == 0x8020 //0x8000 is actual used data size size_t imageBytes = image.size(); uint8_t version = 0; for(size_t i = 0; i < imageBytes; i += 4) { if (i + 3 >= imageBytes) { break; } //get argb values uint8_t r = image[i]; uint8_t g = image[i + 1]; uint8_t b = image[i + 2]; uint8_t a = image[i + 3]; //get lower bits where pico data is encoded a = a & 0x0003; r = r & 0x0003; g = g & 0x0003; b = b & 0x0003; uint8_t extractedByte = (a << 6) + (r << 4) + (g << 2) + b; //TODO: write this all to one array, then memcpy to picoram? //store extracted byte in correct place size_t picoDataIdx = i / 4; if (picoDataIdx < 0x2000) { CartRom.SpriteSheetData[picoDataIdx] = extractedByte; } else if (picoDataIdx < 0x3000) { CartRom.MapData[picoDataIdx - 0x2000] = extractedByte; } else if (picoDataIdx < 0x3100) { CartRom.SpriteFlagsData[picoDataIdx - 0x3000] = extractedByte; } else if (picoDataIdx < 0x3200) { size_t offset = picoDataIdx - 0x3100; size_t songIdx = offset / sizeof(song); size_t channelIdx = offset % sizeof(song); CartRom.SongData[songIdx].data[channelIdx] = extractedByte; } else if (picoDataIdx < 0x4300) { size_t offset = picoDataIdx - 0x3200; size_t sfxIdx = offset / sizeof(sfx); size_t byteIdx = offset % sizeof(sfx); CartRom.SfxData[sfxIdx].data[byteIdx] = extractedByte; } else if (picoDataIdx < 0x8000) { CartLuaData[picoDataIdx - 0x4300] = extractedByte; } else if (picoDataIdx == 0x8000) { version = extractedByte; } } uint8_t compression = 0; if (CartLuaData[0] == '\0' && CartLuaData[1] == 'p' && CartLuaData[2] == 'x' && CartLuaData[3] == 'a'){ compression = 2; } if (CartLuaData[0] == ':' && CartLuaData[1] == 'c' && CartLuaData[2] == ':' && CartLuaData[3] == '\0'){ compression = 1; } Logger_Write("Version: %d Compression %d", version, compression); if (compression == 0) { auto codeBlockLength = 0x8000 - 0x4300; auto length = codeBlockLength; auto endOfCodePtr = (uint8_t const *)std::memchr(CartLuaData, '\0', codeBlockLength); if (endOfCodePtr) { length = endOfCodePtr - CartLuaData; } LuaString = std::string((char const *)CartLuaData, length); } else if (compression == 1){ //from pico 8 wiki: https://pico-8.fandom.com/wiki/P8PNGFileFormat //The first four bytes (0x4300-0x4303) are :c:\x00. //The next two bytes (0x4304-0x4305) are the length of the decompressed code, stored MSB first. //The next two bytes (0x4306-0x4307) are always zero. size_t length = CartLuaData[4] * 256 + CartLuaData[5]; LuaString.resize(0); for (size_t i = 8; i < sizeof(CartLuaData) && LuaString.length() < length; ++i){ //0x00: Copy the next byte directly to the output stream. if(CartLuaData[i] == 0x00){ LuaString += CartLuaData[++i]; } //0x01-0x3b: Emit a character from a lookup table else if (CartLuaData[i] < 0x3c){ LuaString += legacyCompressionLut[CartLuaData[i] - 1]; } //0x3c-0xff: Calculate an offset and length from this byte and the next byte, //then copy those bytes from what has already been emitted. In other words, //go back "offset" characters in the output stream, copy "length" characters, //then paste them to the end of the output stream. Offset and length are //calculated as: // offset = (current_byte - 0x3c) * 16 + (next_byte & 0xf) // length = (next_byte >> 4) + 2 else { size_t offset = (CartLuaData[i] - 0x3c) * 16 + (CartLuaData[i + 1] & 0xf); size_t length = (CartLuaData[i + 1] >> 4) + 2; int startIndex = LuaString.length() - offset; if (startIndex > -1) { for(size_t j = 0; j < length; ++j) { LuaString += LuaString[startIndex + j]; } } ++i; } } } else if (compression == 2){ LuaString = pxa_decompress(CartLuaData); } return true; } bool Cart::loadCartFromPng(const unsigned char* cartData, size_t size){ std::vector image; //the raw pixels unsigned width, height; //decode unsigned error = lodepng::decode(image, width, height, cartData, size); //if there's an error, display it if(error) { LoadError = "png decoder error " + std::string(lodepng_error_text(error)); Logger_Write("%s%s", LoadError.c_str(), "\n"); return false; } if (width != 160 || height != 205) { LoadError = "Invalid png dimensions"; Logger_Write("invalid dimensions\n"); return false; } return loadCartFromPng(image); } bool Cart::loadCartFromPng(std::string filename){ std::vector image; //the raw pixels unsigned width, height; //decode unsigned error = lodepng::decode(image, width, height, filename); //the pixels are now in the vector "image", 4 bytes per pixel, ordered RGBARGBA..., use it as texture, draw it, //if there's an error, display it if(error) { LoadError = "png decoder error " + std::string(lodepng_error_text(error)); Logger_Write("%s%s", LoadError.c_str(), "\n"); return false; } if (width != 160 || height != 205) { LoadError = "Invalid png dimensions"; Logger_Write("invalid dimensions\n"); return false; } return loadCartFromPng(image); } bool Cart::loadCartFromString(std::string cartStr) { fullCartText = cartStr; std::istringstream s(cartStr); std::string line; std::string currSec = ""; std::smatch sm; while (std::getline(s, line)) { line = utils::trimright(line, " \n\r"); line = charset::utf8_to_pico8(line); //line = convert_emojis(line); if (line.length() > 2 && line[0] == '_' && line[1] == '_') { currSec = line; } else if (currSec == "__lua__"){ if (std::regex_match(line, sm, _includeRegex)) { auto dir = getDirectory(FullCartPath); auto fullPath = dir + "/" + sm[1].str(); auto includeContents = get_file_contents(fullPath); if (includeContents.length() > 0){ includeContents = charset::utf8_to_pico8(includeContents); LuaString += includeContents + "\n"; } else{ //todo: report error //error: can't find included file LoadError = "Can't find included file"; return false; } } else { LuaString += line + "\n"; } } else if (currSec == "__gfx__"){ SpriteSheetString += line + "\n"; } else if (currSec == "__gff__"){ SpriteFlagsString += line + "\n"; } else if (currSec == "__map__"){ MapString += line + "\n"; } else if (currSec == "__sfx__"){ SfxString += line + "\n"; } else if (currSec == "__music__"){ MusicString += line + "\n"; } else if (currSec == "__label__"){ LabelString += line + "\n"; } } Logger_Write("Setting cart graphics rom data from strings\n"); setSpriteSheet(SpriteSheetString); setSpriteFlags(SpriteFlagsString); setMapData(MapString); Logger_Write("Setting cart audio rom data from strings\n"); setSfx(SfxString); setMusic(MusicString); return true; } Cart::Cart (const unsigned char* cartData, size_t size){ if (size < 5) { LoadError = "Invalid cart. Less than 5 bytes"; return; } //TODO: check if it is a text cart, decode string, then parse if ((char)cartData[0] == '\x89' && (char)cartData[1] == 'P' && (char)cartData[2] == 'N' && (char)cartData[3] == 'G') { bool success = loadCartFromPng(cartData, size); if (!success){ return; } LoadError = ""; Logger_Write("got valid png cart\n"); } else if((char)cartData[0] == 'p' && (char)cartData[1] == 'i' && (char)cartData[2] == 'c' && (char)cartData[3] == 'o') { std::string strContents(reinterpret_cast(cartData), size); bool success = loadCartFromString(strContents); if (!success){ return; } LoadError = ""; Logger_Write("got valid p8 cart\n"); } else { LoadError = "unknown cart file format"; } } //tac08 based cart parsing and stripping of emoji Cart::Cart(std::string filename, std::string cartDirectory){ //the leading # indicates it is the BBS key. In the future, it would be nice to fetch them, //but for now expect the user to supply the carts if (filename.length() > 0 && filename[0] == '#') { filename = filename.substr(1); } std::string fileExtension = getFileExtension(filename); if (fileExtension == "") { filename = filename + ".p8"; } // Check for built-in carts before prepending cart directory bool isDefaultCart = (filename == "__FAKE08-DEFAULT.p8"); bool isSettingsCart = (filename == "__FAKE08-SETTINGS.p8"); if (isDefaultCart || isSettingsCart) { // Built-in carts don't need a directory path FullCartPath = filename; } else if (cartDirectory.length() > 0 && ! isAbsolutePath(filename)) { FullCartPath = cartDirectory + "/" + filename; } else { FullCartPath = filename; } //zero out cart rom so no garbage is left over initCartRom(); Logger_Write("getting file contents for %s\n", FullCartPath.c_str()); std::string firstFourChars = get_first_four_chars(FullCartPath); if ((fileExtension == "" || fileExtension == ".p8") && firstFourChars.length() == 0 && !isDefaultCart && !isSettingsCart) { //fallback for cart loading by key - support .p8 and .p8.png filename = filename + ".png"; // Rebuild full path with cart directory (same logic as initial path build) if (cartDirectory.length() > 0 && !isAbsolutePath(filename)) { FullCartPath = cartDirectory + "/" + filename; } else { FullCartPath = filename; } firstFourChars = get_first_four_chars(FullCartPath); } // For non-built-in carts, check if file exists first if (!isDefaultCart && !isSettingsCart) { if (firstFourChars.length() == 0) { LoadError = "cart file not found: " + FullCartPath; Logger_Write("%s\n", LoadError.c_str()); return; } } if (isDefaultCart || isSettingsCart || firstFourChars == "pico"){ std::string cartStr; if (isDefaultCart) { Logger_Write("Loading built-in default cart\n"); cartStr = fake08DefaultCart; } else if (isSettingsCart) { Logger_Write("Loading built-in settings cart\n"); cartStr = fake08SettingsP8; } else { cartStr = get_file_contents(FullCartPath.c_str()); // File existence already checked above, but double-check just in case if (cartStr.length() == 0) { LoadError = "cart file not found: " + FullCartPath; Logger_Write("%s\n", LoadError.c_str()); return; } } Logger_Write("Got file contents... parsing cart\n"); bool success = loadCartFromString(cartStr); if (!success){ return; } } else if (firstFourChars == "\x89PNG") { bool success = loadCartFromPng(FullCartPath); if (!success){ return; } LoadError = ""; Logger_Write("got valid png cart\n"); } else { // File exists but format is unknown LoadError = "unknown cart file format: " + FullCartPath; Logger_Write("%s\n", LoadError.c_str()); return; } } Cart::~Cart(){ } void Cart::initCartRom(){ //zero out cart rom so no garbage is left over for(size_t i = 0; i < sizeof(CartRom.SpriteSheetData); i++) { CartRom.SpriteSheetData[i] = 0; } for(size_t i = 0; i < sizeof(CartRom.SpriteFlagsData); i++) { CartRom.SpriteFlagsData[i] = 0; } for(size_t i = 0; i < sizeof(CartRom.MapData); i++) { CartRom.MapData[i] = 0; } for(size_t i = 0; i < 64; i++) { CartRom.SfxData[i] = {0}; } for(size_t i = 0; i < 64; i++) { CartRom.SongData[i] = {0}; } } void Cart::setSpriteSheet(std::string spritesheetstring){ Logger_Write("Copying data to spritesheet\n"); copy_string_to_sprite_memory(CartRom.SpriteSheetData, spritesheetstring); } void Cart::setSpriteFlags(std::string spriteFlagsstring){ Logger_Write("Copying data to sprite flags\n"); copy_string_to_memory(CartRom.SpriteFlagsData, spriteFlagsstring); } void Cart::setMapData(std::string mapDataString){ Logger_Write("Copying data to map data\n"); copy_string_to_memory(CartRom.MapData, mapDataString); } void Cart::setMusic(std::string musicString){ std::istringstream s(musicString); std::string line; char buf[3] = {0}; int musicIdx = 0; while (std::getline(s, line)) { if (line.length() < 11){ continue; } buf[0] = line[0]; buf[1] = line[1]; uint8_t flagByte = (uint8_t)strtol(buf, NULL, 16); uint8_t mode = (flagByte & 8) >> 3; uint8_t fstop = (flagByte & 4) >> 2; uint8_t frepeat = (flagByte & 2) >> 1; uint8_t fnext = flagByte & 1; buf[0] = line[3]; buf[1] = line[4]; uint8_t channel1byte = (uint8_t)strtol(buf, NULL, 16); buf[0] = line[5]; buf[1] = line[6]; uint8_t channel2byte = (uint8_t)strtol(buf, NULL, 16); buf[0] = line[7]; buf[1] = line[8]; uint8_t channel3byte = (uint8_t)strtol(buf, NULL, 16); buf[0] = line[9]; buf[1] = line[10]; uint8_t channel4byte = (uint8_t)strtol(buf, NULL, 16); CartRom.SongData[musicIdx].data[0] = channel1byte | fnext << 7; CartRom.SongData[musicIdx].data[1] = channel2byte | frepeat << 7; CartRom.SongData[musicIdx].data[2] = channel3byte | fstop << 7; CartRom.SongData[musicIdx].data[3] = channel4byte | mode << 7; musicIdx++; if (musicIdx >= 64) { break; } } } void Cart::setSfx(std::string sfxString) { std::istringstream s(sfxString); std::string line; char buf[3] = {0}; int sfxIdx = 0; //SFX speed defaults to 16. Everything else should be zeroed out for (int i = 0; i < 64; i++) { CartRom.SfxData[i].speed = 16; } while (std::getline(s, line)) { buf[0] = line[0]; buf[1] = line[1]; uint8_t filters = (uint8_t)strtol(buf, NULL, 16); buf[0] = line[2]; buf[1] = line[3]; uint8_t noteDuration = (uint8_t)strtol(buf, NULL, 16); buf[0] = line[4]; buf[1] = line[5]; uint8_t loopRangeStart = (uint8_t)strtol(buf, NULL, 16); buf[0] = line[6]; buf[1] = line[7]; uint8_t loopRangeEnd = (uint8_t)strtol(buf, NULL, 16); CartRom.SfxData[sfxIdx].filters = filters; CartRom.SfxData[sfxIdx].speed = noteDuration; CartRom.SfxData[sfxIdx].loopRangeStart = loopRangeStart; CartRom.SfxData[sfxIdx].loopRangeEnd = loopRangeEnd; //32 notes, 5 chars each int noteIdx = 0; for (int i = 8; i < 168; i+=5) { buf[0] = line[i]; buf[1] = line[i + 1]; uint8_t key = (uint8_t)strtol(buf, NULL, 16); buf[0] = '0'; buf[1] = line[i + 2]; uint8_t waveform = (uint8_t)strtol(buf, NULL, 16); uint8_t custom = waveform > 7 ? 1 : 0; buf[0] = '0'; buf[1] = line[i + 3]; uint8_t volume = (uint8_t)strtol(buf, NULL, 16); buf[0] = '0'; buf[1] = line[i + 4]; uint8_t effect = (uint8_t)strtol(buf, NULL, 16); CartRom.SfxData[sfxIdx].notes[noteIdx].setKey(key); CartRom.SfxData[sfxIdx].notes[noteIdx].setWaveform(waveform); CartRom.SfxData[sfxIdx].notes[noteIdx].setVolume(volume); CartRom.SfxData[sfxIdx].notes[noteIdx].setEffect(effect); CartRom.SfxData[sfxIdx].notes[noteIdx].setCustom(custom); noteIdx++; } sfxIdx++; } }