?login_element?

Subversion Repositories NedoOS

Rev

Rev 129 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | Download | RSS feed

  1. /*
  2.  
  3.   SjASMPlus Z80 Cross Compiler - modified - SAVENEX extension
  4.  
  5.   Copyright (c) 2006 Sjoerd Mastijn (original SW)
  6.   Copyright (c) 2019 Peter Ped Helcmanovsky (SAVENEX extension)
  7.  
  8.   This software is provided 'as-is', without any express or implied warranty.
  9.   In no event will the authors be held liable for any damages arising from the
  10.   use of this software.
  11.  
  12.   Permission is granted to anyone to use this software for any purpose,
  13.   including commercial applications, and to alter it and redistribute it freely,
  14.   subject to the following restrictions:
  15.  
  16.   1. The origin of this software must not be misrepresented; you must not claim
  17.          that you wrote the original software. If you use this software in a product,
  18.          an acknowledgment in the product documentation would be appreciated but is
  19.          not required.
  20.  
  21.   2. Altered source versions must be plainly marked as such, and must not be
  22.          misrepresented as being the original software.
  23.  
  24.   3. This notice may not be removed or altered from any source distribution.
  25.  
  26. */
  27.  
  28. #include "sjdefs.h"
  29. #include "crc32c.h"
  30.  
  31. // Banks in file are ordered in SNA way (but array "banks" in header is in numeric order instead)
  32. static constexpr aint nexBankOrder[8] = {5, 2, 0, 1, 3, 4, 6, 7};
  33.  
  34. #ifdef _MSC_VER
  35. #pragma pack(push, 1)
  36. #endif
  37. struct SNexHeader {
  38.         constexpr static size_t COPPER_SIZE = 0x0800;
  39.         constexpr static size_t PAL_SIZE = 0x200;
  40.         constexpr static aint MAX_BANK = 112;
  41.         constexpr static aint MAX_PAGE = MAX_BANK * 2;
  42.         constexpr static byte SCR_LAYER2        = 0x01;
  43.         constexpr static byte SCR_ULA           = 0x02;
  44.         constexpr static byte SCR_LORES         = 0x04;
  45.         constexpr static byte SCR_HIRES         = 0x08;
  46.         constexpr static byte SCR_HICOL         = 0x10;
  47.         constexpr static byte SCR_EXT2          = 0x40;
  48.         constexpr static byte SCR_NOPAL         = 0x80;
  49.         constexpr static byte SCR2_320x256      = 1;
  50.         constexpr static byte SCR2_640x256      = 2;
  51.         constexpr static byte SCR2_tilemap      = 3;
  52.  
  53.         byte            magicAndVersion[8];     // the "magic" number + file version at the beginning
  54.         byte            ramReq;                         // 0 = 768k, 1 = 1792k
  55.         byte            numBanks;                       // number of 16k banks to load: 0..112
  56.         byte            screen;                         // loading screen flags
  57.         byte            border;                         // border colour 0..7
  58.         word            sp;                                     // stack pointer
  59.         word            pc;                                     // start address (0 = no start)
  60.         word            _obsolete_numfiles;
  61.         byte            banks[MAX_BANK];        // 112 16ki banks (1.75MiB) - non-zero value = in file
  62.         // banks array is ordinary order 0, 1, 2, ..., but banks in file are in order: 5, 2, 0, 1, ...
  63.         byte            loadbar;                        // 0/1 show progress bar
  64.         byte            loadbarColour;          // colour of progress bar (precise meaning depends on gfx mode)
  65.         byte            loadDelay;                      // delay after each bank is loaded (number of frames)
  66.         byte            startDelay;                     // delay after whole file is loaded (number of frames)
  67.         byte            preserveNextRegs;       // 0 = reset whole machine state, 1 = preserve most of it
  68.         byte            coreVersion[3];
  69.         byte            hiResColour;            // bits 5-3 for port 255 (ASM source provides 0..7 value, needs shift)
  70.         byte            entryBank;                      // 16ki bank 0..111 to be mapped into C000..FFFF range
  71.         word            fileHandleCfg;          // 0 = close NEX file, 1 = pass handle in BC, 0x4000+ = address to write handle
  72.         // V1.3 fields
  73.         byte            expBusDisable;          // 0 = disable expansion bus by setting top four bits of NextReg 0x80, 1 = no-op
  74.         byte            hasChecksum;            // 0 = no checksum, 1 = last 4B of header are CRC-32C (Castagnoli)
  75.         uint32_t        banksOffset;            // where data of first bank start in file (NEX V1.3+ file, 0 olders)
  76.         word            cliBuffer;                      // address of buffer for command line copy (0 = off)
  77.         word            cliBufferSize;          // size of the provided buffer (0 = off) (cmd line is truncated to this size)
  78.         byte            screen2;                        // extended screen flag (old "screen" must have +64 flag)
  79.         byte            hasCopperCode;          // 0 = no copper, 1 = 2048B copper block after loading screens
  80.         byte            tilesScrConfig[4];      // NextReg registers $6B, $6C, $6E, $6F values for Tilemode screen
  81.         byte            bigL2barPosY;           // Y position (0..255) of loading bar for new Layer 2 320x256 and 640x256 modes
  82.         byte            _reserved[349];
  83.         uint32_t        crc32c;                         // CRC-32C build by: file offset 512->EOF (including append bin), then 508B header
  84.  
  85.         void init();
  86. }
  87. #ifndef _MSC_VER
  88.         __attribute__((packed));
  89. #else
  90.         ;
  91. #pragma pack(pop)
  92. #endif
  93. static_assert(512 == sizeof(SNexHeader), "NEX header is expected to be 512 bytes long!");
  94.  
  95. struct SNexFile {
  96.         SNexHeader      h;
  97.         aint            reqFileVersion;         // requested file version in OPEN command
  98.         aint            minFileVersion;         // currently auto-detected file version
  99.         FILE*           f = nullptr;            // NEX file handle, stay opened, fseek stays at <EOF>
  100.                 // file is build sequentially, adding further blocks, only finalize does refresh the header
  101.         aint            lastBankIndex;          // numeric order (0, 1, ...) value, -1 is init value
  102.         byte*           copper = nullptr;       // temporary storage of copper code (add it ahead of first bank)
  103.         byte*           palette = nullptr;      // final palette (will override the one stored upon finalize)
  104.         bool            palDefined;                     // whether the palette data/type was enforced by PALETTE command
  105.         bool            canAppend = false;      // true when `fwrite(..., f)` can be used in "append like" way
  106.         // set `canAppend` to false whenever you do fseek/fread, it cancels validity of "next fwrite"
  107.  
  108.         ~SNexFile();
  109.         void init();
  110.         void writeHeader();
  111.         void writePalette();
  112.         void calculateCrc32C();
  113.         void updateIfAheadFirstBankSave();
  114.         void finalizeFile();
  115. };
  116.  
  117. // the instance holding all tooling data and header about currently opened NEX file
  118. static SNexFile nex;
  119.  
  120. void SNexHeader::init() {
  121.         memset(magicAndVersion, 0, sizeof(SNexHeader)); // clear whole 512 bytes
  122.         // set "magic" number and file version
  123.         memcpy(magicAndVersion, "NextV1.2", 8);                 // setup "magic" number at beginning
  124.         // required core version is by default 2.00.28 (latest released)
  125.         coreVersion[0] = 2;
  126.         coreVersion[1] = 0;
  127.         coreVersion[2] = 28;
  128. }
  129.  
  130. SNexFile::~SNexFile() {
  131.         finalizeFile();
  132. }
  133.  
  134. void SNexFile::init() {
  135.         h.init();
  136.         lastBankIndex = -1;             // reset last bank index
  137.         palDefined = false;
  138.         reqFileVersion = 0;
  139.         minFileVersion = 2;
  140. }
  141.  
  142. void SNexFile::updateIfAheadFirstBankSave() {
  143.         // check if already updated or file is not ready for appending (no file, or finalizing)
  144.         if (h.banksOffset || !canAppend) return;
  145.         // updating bank offset after some bank was already stored -> should never happen
  146.         if (-1 != lastBankIndex) Error("[SAVENEX] V1.3?!", NULL, FATAL);        // unreachable
  147.         if (palDefined && 0 == h.screen) {
  148.                 Warning("[SAVENEX] some palette was defined, but without screen it is ignored.");
  149.         }
  150.         // V1.3 feature, copper code is the last block ahead of first bank
  151.         if (h.hasCopperCode) {
  152.                 if (SNexHeader::COPPER_SIZE != fwrite(copper, 1, SNexHeader::COPPER_SIZE, f)) {
  153.                         Error("[SAVENEX] writing copper data failed", NULL, FATAL);
  154.                 }
  155.         }
  156.         // V1.3 feature, offset of very first bank stored in file
  157.         h.banksOffset = ftell(f);
  158. }
  159.  
  160. void SNexFile::writeHeader() {
  161.         if (nullptr == f) return;
  162.         canAppend = false;                                                      // does fseek, cancel the "append" mode
  163.         // refresh/write the file header
  164.         fseek(f, 0, SEEK_SET);
  165.         if (sizeof(SNexHeader) != fwrite(&h, 1, sizeof(SNexHeader), f)) {
  166.                 Error("[SAVENEX] writing header content failed", NULL, SUPPRESS);
  167.         }
  168. }
  169.  
  170. void SNexFile::writePalette() {
  171.         if (!canAppend) return;
  172.         if (!palDefined || nullptr == palette) {        // palette is completely undefined or
  173.                 h.screen = SNexHeader::SCR_NOPAL;               // "no palette" was defined
  174.         } else {
  175.                 if (SNexHeader::PAL_SIZE != fwrite(palette, 1, SNexHeader::PAL_SIZE, f)) {
  176.                         Error("[SAVENEX] writing palette data failed", NULL, FATAL);
  177.                 }
  178.         }
  179. }
  180.  
  181. void SNexFile::calculateCrc32C() {
  182.         if (!h.hasChecksum) return;
  183.         if (nullptr == f) return;
  184.         canAppend = false;                                                      // does fseek+fread, cancel the "append" mode
  185.         // calculate checksum CRC-32C (Castagnoli)
  186.         crc32_init();
  187.         constexpr size_t BUFFER_SIZE = 128 * 1024;      // 128kiB buffer to read file (must be 512+ !!)
  188.         uint8_t *buffer = new uint8_t[BUFFER_SIZE];
  189.         if (nullptr == buffer) ErrorOOM();
  190.         uint32_t crc = 0;
  191.         // calculate CRC of the file part after header (offset 512)
  192.         fseek(f, 512, SEEK_SET);
  193.         size_t bytes_read = 0;
  194.         do {
  195.                 bytes_read = fread(buffer, 1, BUFFER_SIZE, f);
  196.                 if (0 == bytes_read) break;
  197.                 crc = crc32c_append_sw(crc, buffer, bytes_read);
  198.         } while (BUFFER_SIZE == bytes_read);
  199.         // calculate CRC of the header part (first 508 bytes of header)
  200.         fseek(f, 0, SEEK_SET);
  201.         bytes_read = fread(buffer, 1, 508, f);
  202.         h.hasChecksum = (508 == bytes_read);
  203.         if (h.hasChecksum) {
  204.                 h.crc32c = crc32c_append_sw(crc, buffer, bytes_read);
  205.         } else {
  206.                 Error("[SAVENEX] reading file for CRC calculation failed");
  207.         }
  208.         delete[] buffer;
  209. }
  210.  
  211. void SNexFile::finalizeFile() {
  212.         if (nullptr == f) return;
  213.         // do the final V1.2 / V1.3 updates to the header fields
  214.         // V1.3 auto-detected when V1.2 is required should never happen (Error should be unreachable)
  215.         if (3 == minFileVersion && 2 == reqFileVersion) Error("[SAVENEX] V1.3?!", NULL, FATAL);
  216.         updateIfAheadFirstBankSave();   // if no BANK/AUTO/CLOSE was used -> update all now
  217.         if (2 == minFileVersion) {
  218.                 h.banksOffset = 0;              // clear banksOffset for V1.2 files
  219.                 h.bigL2barPosY = 0;             // clear big Layer 2 loading-bar posY for V1.2 files
  220.         } else {
  221.                 h.magicAndVersion[7] = '3';                             // modify file version to "V1.3" string
  222.                 calculateCrc32C();
  223.         }
  224.         // refresh the file header to final state
  225.         writeHeader();
  226.         // close the file
  227.         fclose(f);
  228.         f = nullptr;
  229.         canAppend = false;
  230.         if (nullptr != copper) delete[] copper;
  231.         copper = nullptr;
  232.         if (nullptr != palette) delete[] palette;
  233.         palette = nullptr;
  234.         return;
  235. }
  236.  
  237. enum EBmpType { other, Layer2, LoRes, L2_320x256, L2_640x256 };
  238.  
  239. class SBmpFile {
  240.         FILE*           bmp;
  241.         byte            tempHeader[0x36];               // 14B header + BITMAPINFOHEADER 40B header
  242.         byte*           palBuffer = nullptr;
  243.  
  244. public:
  245.         EBmpType        type = other;
  246.         int32_t         width = 0, height = 0;
  247.         bool            upsideDown = false;
  248.         uint32_t        colorsCount = 0;
  249.  
  250.         ~SBmpFile();
  251.         void close();
  252.         bool open(const char* bmpname);
  253.         word getColor(uint32_t index);
  254.         void loadPixelData(byte* buffer);
  255. };
  256.  
  257. SBmpFile::~SBmpFile() {
  258.         close();
  259. }
  260.  
  261. void SBmpFile::close() {
  262.         if (nullptr == bmp) return;
  263.         fclose(bmp);
  264.         bmp = nullptr;
  265.         delete[] palBuffer;
  266.         palBuffer = nullptr;
  267. }
  268.  
  269. bool SBmpFile::open(const char* bmpname) {
  270.         if (!FOPEN_ISOK(bmp, bmpname, "rb")) {
  271.                 Error("[SAVENEX] Error opening file", bmpname, SUPPRESS);
  272.                 return false;
  273.         }
  274.         // read header of BMP and verify the file is of expected format
  275.         palBuffer = new byte[4*256];
  276.         if (nullptr == palBuffer) ErrorOOM();
  277.         const size_t readElements = fread(tempHeader, 1, 0x36, bmp) + fread(palBuffer, 4, 256, bmp);
  278.         // these following casts assume the sjasmplus itself is running at little-endian platform
  279.         // if you are using big-endian, report the issue, so this can be fixed in more universal way
  280.         const uint32_t header2Size = *reinterpret_cast<uint32_t*>(tempHeader + 14);
  281.         const uint16_t colorPlanes = *reinterpret_cast<uint16_t*>(tempHeader + 26);
  282.         const uint16_t bpp = *reinterpret_cast<uint16_t*>(tempHeader + 28);
  283.         const uint32_t compressionType = *reinterpret_cast<uint32_t*>(tempHeader + 30);
  284.         // check "BM", BITMAPINFOHEADER type (size 40), 8bpp, no compression
  285.         if (0x36+256 != readElements || 'B' != tempHeader[0] || 'M' != tempHeader[1] ||
  286.                 40 != header2Size || 1 != colorPlanes || 8 != bpp || 0 != compressionType)
  287.         {
  288.                 Error("[SAVENEX] BMP file is not in expected format (uncompressed, 8bpp, 40B BITMAPINFOHEADER header)",
  289.                                 bmpname, SUPPRESS);
  290.                 close();
  291.                 return false;
  292.         }
  293.         colorsCount = *reinterpret_cast<uint32_t*>(tempHeader + 46);
  294.         // check if the size is 256x192 (Layer 2) or 128x96 (LoRes), or 320/640 x 256 (V1.3).
  295.         width = *reinterpret_cast<int32_t*>(tempHeader + 18);
  296.         height = *reinterpret_cast<int32_t*>(tempHeader + 22);
  297.         upsideDown = 0 < height;
  298.         if (height < 0) height = -height;
  299.         if (256 == width && 192 == height) type = Layer2;
  300.         if (128 == width && 96 == height) type = LoRes;
  301.         if (256 == height) {
  302.                 if (320 == width) type = L2_320x256;
  303.                 if (640 == width) type = L2_640x256;
  304.         }
  305.         return true;
  306. }
  307.  
  308. word SBmpFile::getColor(uint32_t index) {
  309.         if (nullptr == bmp || nullptr == palBuffer || colorsCount <= index) return 0;
  310.         const byte B = palBuffer[index * 4 + 0] >> 5;
  311.         const byte G = palBuffer[index * 4 + 1] >> 5;
  312.         const byte R = palBuffer[index * 4 + 2] >> 5;
  313.         return ((B&1) << 8) | (B >> 1) | (G << 2) | (R << 5);
  314. }
  315.  
  316. void SBmpFile::loadPixelData(byte* buffer) {
  317.         const uint32_t offset = *reinterpret_cast<uint32_t*>(tempHeader + 10);
  318.         const size_t w = static_cast<size_t>(width);
  319.         for (int32_t y = 0; y < height; ++y) {
  320.                 const int32_t fileY = upsideDown ? (height - y - 1) : y;
  321.                 fseek(bmp, offset + (w * fileY), SEEK_SET);
  322.                 if (w != fread(buffer + (w * y), 1, w, bmp)) {
  323.                         Error("[SAVENEX] reading BMP pixel data failed", NULL, FATAL);
  324.                 }
  325.         }
  326. }
  327.  
  328. static aint getNexBankIndex(const aint bank16kNum) {
  329.         if (8 <= bank16kNum && bank16kNum < SNexHeader::MAX_BANK) return bank16kNum;
  330.         for (aint i = 0; i < 8; ++i) {
  331.                 if (nexBankOrder[i] == bank16kNum) return i;
  332.         }
  333.         return -2;
  334. }
  335.  
  336. static aint getNexBankNum(const aint bankIndex) {
  337.         if (0 <= bankIndex && bankIndex < 8) return nexBankOrder[bankIndex];
  338.         if (8 <= bankIndex && bankIndex < SNexHeader::MAX_BANK) return bankIndex;
  339.         return -1;
  340. }
  341.  
  342. template <int argsN> static bool getIntArguments(aint (&args)[argsN], const bool argOptional[argsN]) {
  343.         for (int i = 0; i < argsN; ++i) {
  344.                 if (0 < i && !comma(lp)) return argOptional[i];
  345.                 aint val;                               // temporary variable to preserve original value in case of error
  346.                 if (!ParseExpression(lp, val)) return (0 == i) && argOptional[i];
  347.                 args[i] = val;
  348.         }
  349.         return !comma(lp);
  350. }
  351.  
  352. static void dirNexOpen() {
  353.         if (nex.f) {
  354.                 Error("[SAVENEX] NEX file is already open", bp, SUPPRESS);
  355.                 return;
  356.         }
  357.         nex.init();                     // reset everything around NEX file data
  358.         // read OPEN command arguments
  359.         char* fname = GetOutputFileName(lp);
  360.         aint openArgs[4] = { (-1 == StartAddress ? 0 : StartAddress), 0xFFFE, 0, 0 };
  361.         if (comma(lp)) {
  362.                 const bool optionals[] = {false, true, true, true};     // start address is mandatory because comma
  363.                 if (!getIntArguments<4>(openArgs, optionals)) {
  364.                         Error("[SAVENEX] expected syntax is OPEN <filename>[,<startAddress>[,<stackAddress>[,<entryBank 0..111>[,<fileVersion 2..3>]]]]", bp, SUPPRESS);
  365.                         delete[] fname;
  366.                         return;
  367.                 }
  368.         }
  369.         // validate argument values
  370.         if (-1 != StartAddress && StartAddress != openArgs[0]) {
  371.                 Warning("[SAVENEX] Start address was also defined by END, OPEN argument used instead");
  372.         }
  373.         check16(openArgs[0]);
  374.         check16(openArgs[1]);
  375.         if (openArgs[2] < 0 || SNexHeader::MAX_BANK <= openArgs[2]) {
  376.                 ErrorInt("[SAVENEX] entry bank can be 0..111 value only", openArgs[2], SUPPRESS);
  377.                 delete[] fname;
  378.                 return;
  379.         }
  380.         if (openArgs[3] && (openArgs[3] < 2 || 3 < openArgs[3])) {
  381.                 ErrorInt("[SAVENEX] only file version 2 (V1.2) or 3 (V1.3) can be enforced", openArgs[3], SUPPRESS);
  382.                 delete[] fname;
  383.                 return;
  384.         }
  385.         // try to open the actual file
  386.         if (!FOPEN_ISOK(nex.f, fname, "w+b")) Error("[SAVENEX] Error opening file", fname, SUPPRESS);
  387.         delete[] fname;
  388.         if (nullptr == nex.f) return;
  389.         // set the argument values into header, and write the initial version of header into file
  390.         nex.h.pc = openArgs[0] & 0xFFFF;
  391.         nex.h.sp = openArgs[1] & 0xFFFF;
  392.         nex.h.entryBank = openArgs[2];
  393.         nex.reqFileVersion = openArgs[3];
  394.         nex.minFileVersion = (3 == nex.reqFileVersion) ? 3 : 2; // reset auto-detected file version
  395.         nex.writeHeader();
  396.         // After writing header first time, the file is ready for "append like" usage
  397.         nex.canAppend = true;
  398. }
  399.  
  400. static void dirNexCore() {
  401.         if (nullptr == nex.f) {
  402.                 Error("[SAVENEX] NEX file is not open", NULL, SUPPRESS);
  403.                 return;
  404.         }
  405.         // parse arguments
  406.         aint coreArgs[3] = {0};
  407.         const bool optionals[] = {false, false, false};
  408.         if (!getIntArguments<3>(coreArgs, optionals)) {
  409.                 Error("[SAVENEX] expected syntax is CORE <major 0..15>,<minor 0..15>,<subminor 0..255>", bp, SUPPRESS);
  410.                 return;
  411.         }
  412.         // warn about invalid values
  413.         if (coreArgs[0] < 0 || 15 < coreArgs[0] ||
  414.                 coreArgs[1] < 0 || 15 < coreArgs[1] ||
  415.                 coreArgs[2] < 0 || 255 < coreArgs[2]) Warning("[SAVENEX] values are not within 0..15,0..15,0..255 ranges");
  416.         // set the values in header
  417.         nex.h.coreVersion[0] = coreArgs[0];
  418.         nex.h.coreVersion[1] = coreArgs[1];
  419.         nex.h.coreVersion[2] = coreArgs[2];
  420. }
  421.  
  422. static void dirNexCfg3() {
  423. // ;; SAVENEX CFG3 <DoCRC 0/1>[,<PreserveExpansionBus 0/1>[,<CLIbufferAdr>,<CLIbufferSize>]]
  424.         if (nullptr == nex.f) {
  425.                 Error("[SAVENEX] NEX file is not open", NULL, SUPPRESS);
  426.                 return;
  427.         }
  428.         if (nex.reqFileVersion == 2) {
  429.                 Error("[SAVENEX] V1.2 was requested with OPEN, but CFG3 is V1.3 feature.", NULL, SUPPRESS);
  430.                 return;
  431.         }
  432.         nex.minFileVersion = 3;         // V1.3 detected
  433.         // parse arguments
  434.         aint cfgArgs[4] = {1, 0};
  435.         const bool optionals[] = {false, true, true, false};
  436.         if (!getIntArguments<4>(cfgArgs, optionals)) {
  437.                 Error("[SAVENEX] expected syntax is CFG3 <DoCRC 0/1>[,<PreserveExpansionBus 0/1>[,<CLIbufferAdr>,<CLIbufferSize>]]", bp, SUPPRESS);
  438.                 return;
  439.         }
  440.         const bool someCliBuffer = cfgArgs[2] || cfgArgs[3];    // [0, 0] = no CLI buffer, don't check validity
  441.         // warn about invalid values
  442.         if (cfgArgs[0] < 0 || 1 < cfgArgs[0] ||
  443.                 cfgArgs[1] < 0 || 1 < cfgArgs[1] ||
  444.                 (someCliBuffer &&
  445.                         (cfgArgs[2] < 0x4000 || 0x10000 < (cfgArgs[2] + cfgArgs[3]) ||
  446.                         cfgArgs[3] < 1 || 0x0800 < cfgArgs[3]))) {
  447.                 Warning("[SAVENEX] crc/preserve values are not 0/1 or CLI buffer doesn't fit into $4000..$FFFF range (size can be 2048 max)");
  448.         }
  449.         // set the values in header
  450.         nex.h.hasChecksum = !!cfgArgs[0];
  451.         nex.h.expBusDisable = !!cfgArgs[1];
  452.         nex.h.cliBuffer = cfgArgs[2];
  453.         nex.h.cliBufferSize = cfgArgs[3];
  454. }
  455.  
  456. static void dirNexCfg() {
  457. // ;; SAVENEX CFG <border 0..7>[,<fileHandle 0/1/$4000+>[,<PreserveNextRegs 0/1>[,<2MbRamReq 0/1>]]]
  458.         if (nullptr == nex.f) {
  459.                 Error("[SAVENEX] NEX file is not open", NULL, SUPPRESS);
  460.                 return;
  461.         }
  462.         // parse arguments
  463.         aint cfgArgs[4] = {0};
  464.         const bool optionals[] = {false, true, true, true};
  465.         if (!getIntArguments<4>(cfgArgs, optionals)) {
  466.                 Error("[SAVENEX] expected syntax is CFG <border 0..7>[,<fileHandle 0/1/$4000+>[,<PreserveNextRegs 0/1>[,<2MbRamReq 0/1>]]]", bp, SUPPRESS);
  467.                 return;
  468.         }
  469.         // warn about invalid values
  470.         if (cfgArgs[0] < 0 || 7 < cfgArgs[0] ||
  471.                 cfgArgs[1] < 0 || 0xFFFE < cfgArgs[1] ||
  472.                 cfgArgs[2] < 0 || 1 < cfgArgs[2] ||
  473.                 cfgArgs[3] < 0 || 1 < cfgArgs[3]) Warning("[SAVENEX] values are not within 0..7,0..65534,0/1,0/1 ranges");
  474.         // set the values in header
  475.         nex.h.border = cfgArgs[0] & 7;
  476.         nex.h.fileHandleCfg = cfgArgs[1];
  477.         nex.h.preserveNextRegs = cfgArgs[2];
  478.         nex.h.ramReq = cfgArgs[3];
  479. }
  480.  
  481. static void dirNexBar() {
  482.         if (nullptr == nex.f) {
  483.                 Error("[SAVENEX] NEX file is not open", NULL, SUPPRESS);
  484.                 return;
  485.         }
  486.         // parse arguments
  487.         aint barArgs[5] = {0, 0, 0, 0, 254};
  488.         const bool optionals[] = {false, false, true, true, true};
  489.         if (!getIntArguments<5>(barArgs, optionals)) {
  490.                 Error("[SAVENEX] expected syntax is BAR <loadBar 0/1>,<barColour 0..255>[,<startDelay 0..255>[,<bankDelay 0..255>[,<posY 0..255>]]]", bp, SUPPRESS);
  491.                 return;
  492.         }
  493.         // warn about invalid values
  494.         if (barArgs[0] < 0 || 1 < barArgs[0] ||
  495.                 barArgs[1] < 0 || 255 < barArgs[1] ||
  496.                 barArgs[2] < 0 || 255 < barArgs[2] ||
  497.                 barArgs[3] < 0 || 255 < barArgs[3] ||
  498.                 barArgs[4] < 0 || 255 < barArgs[4]) Warning("[SAVENEX] values are not within 0/1 or 0..255 ranges");
  499.         // set the values in header
  500.         nex.h.loadbar = barArgs[0];
  501.         nex.h.loadbarColour = barArgs[1];
  502.         nex.h.startDelay = barArgs[2];
  503.         nex.h.loadDelay = barArgs[3];
  504.         nex.h.bigL2barPosY = barArgs[4];
  505. }
  506.  
  507. static void dirNexPaletteDefault() {
  508. // ;; SAVENEX PALETTE DEFAULT
  509.         nex.palDefined = true;
  510.         nex.palette = new byte[SNexHeader::PAL_SIZE];
  511.         if (nullptr == nex.palette) ErrorOOM();
  512.         for (uint32_t i = 0; i < 256; ++i) {
  513.                 nex.palette[i*2 + 0] = static_cast<byte>(i);
  514.                 nex.palette[i*2 + 1] = (i & 3) ? 1 : 0;         // bottom blue bit is 1 when some upper bit is
  515.         }
  516. }
  517.  
  518. static bool dirNexPaletteMem(const aint page8kNum, const aint palOffset) {
  519.         if (nex.palDefined) return true;        // palette was already defined, silently ignore
  520.         if (-1 == page8kNum) {                          // this is used as "no palette" by some screen commands
  521.                 nex.palDefined = true;
  522.                 return true;
  523.         }
  524.         // warn about invalid values
  525.         const size_t totalRam = Device->GetMemoryOffset(Device->PagesCount, 0);
  526.         const int32_t adrPalData = Device->GetMemoryOffset(page8kNum, palOffset);
  527.         if (adrPalData < 0 || totalRam < adrPalData + SNexHeader::PAL_SIZE) {
  528.                 Error("[SAVENEX] palette data address range is outside of Next memory", bp, SUPPRESS);
  529.                 return false;
  530.         }
  531.         // copy the data into internal palette buffer
  532.         nex.palDefined = true;
  533.         nex.palette = new byte[SNexHeader::PAL_SIZE];
  534.         if (nullptr == nex.palette) ErrorOOM();
  535.         memcpy(nex.palette, Device->Memory + adrPalData, SNexHeader::PAL_SIZE);
  536.         return true;
  537. }
  538.  
  539. static bool dirNexPaletteBmp(SBmpFile & bmp) {
  540.         if (nex.palDefined) return true;        // palette was already defined, silently ignore
  541.         if (256 != bmp.colorsCount && warningNotSuppressed()) {
  542.                 char buf[128];
  543.                 SPRINTF1(buf, 128, "[SAVENEX] BMP has only %d colors in palette (expect \"any\" values in remaining colors).", bmp.colorsCount);
  544.                 Warning(buf);
  545.         }
  546.         // copy the data into internal palette buffer
  547.         nex.palDefined = true;
  548.         nex.palette = new byte[SNexHeader::PAL_SIZE];
  549.         if (nullptr == nex.palette) ErrorOOM();
  550.         constexpr size_t palDataSize = 256;
  551.         for (size_t i = 0; i < palDataSize; ++i) {
  552.                 const word nextColor = bmp.getColor(i);
  553.                 nex.palette[i*2 + 0] = nextColor & 0xFF;
  554.                 nex.palette[i*2 + 1] = nextColor >> 8;
  555.         }
  556.         return true;
  557. }
  558.  
  559. static void dirNexPaletteMem() {
  560. // ;; SAVENEX PALETTE MEM <palPage8kNum 0..223>,<palOffset>
  561.         aint palArgs[2] = {0, 0};
  562.         const bool optionals[] = {false, false};
  563.         if (!getIntArguments<2>(palArgs, optionals)
  564.                         || palArgs[0] < 0 || SNexHeader::MAX_PAGE <= palArgs[0] || palArgs[1] < 0) {
  565.                 Error("[SAVENEX] expected syntax is MEM <palPage8kNum 0..223>,<palOffset 0+>", bp, SUPPRESS);
  566.                 return;
  567.         }
  568.         dirNexPaletteMem(palArgs[0], palArgs[1]);
  569. }
  570.  
  571. static void dirNexPaletteBmp() {
  572. // ;; SAVENEX PALETTE BMP <filename>
  573.         const char* const bmpname = GetFileName(lp);
  574.         if (!bmpname[0] || comma(lp)) {
  575.                 Error("[SAVENEX] expected syntax is BMP <filename>", bp, SUPPRESS);
  576.                 delete[] bmpname;
  577.                 return;
  578.         }
  579.         // try to open the actual BMP file
  580.         SBmpFile bmp;
  581.         bool bmpOpened = bmp.open(bmpname);
  582.         delete[] bmpname;
  583.         if (!bmpOpened) return;
  584.         // check the palette if it was requested from this bmp and process it
  585.         dirNexPaletteBmp(bmp);
  586. }
  587.  
  588. static void dirNexPalette() {
  589. // ;; SAVENEX PALETTE (NONE|DEFAULT|MEM|BMP)
  590.         if (nullptr == nex.f) {
  591.                 Error("[SAVENEX] NEX file is not open", NULL, SUPPRESS);
  592.                 return;
  593.         }
  594.         if (nex.palDefined || 0 != nex.h.screen) {
  595.                 Error("[SAVENEX] some palette/screen was already defined (define palette once and ahead)", NULL, SUPPRESS);
  596.                 return;
  597.         }
  598.         if (-1 != nex.lastBankIndex) {
  599.                 Error("[SAVENEX] some bank was already stored (define palette ahead)", NULL, SUPPRESS);
  600.                 return;
  601.         }
  602.         SkipBlanks(lp);
  603.         if (cmphstr(lp, "none")) nex.palDefined = true;
  604.         else if (cmphstr(lp, "default")) dirNexPaletteDefault();
  605.         else if (cmphstr(lp, "mem")) dirNexPaletteMem();
  606.         else if (cmphstr(lp, "bmp")) dirNexPaletteBmp();
  607.         else Error("[SAVENEX] unknown palette command (commands: NONE, DEFAULT, MEM, BMP)", lp, SUPPRESS);
  608. }
  609.  
  610. static void dirNexScreenLayer2andLowRes(EBmpType type) {
  611. // ;; SCREEN L2 [<Page8kNum 0..223>,<offset>[,<palPage8kNum 0..223>,<palOffset>]]
  612. // ;; SCREEN LR [<Page8kNum 0..223>,<offset>[,<palPage8kNum 0..223>,<palOffset>]]
  613. // ;; SCREEN L2_320 [<Page8kNum 0..223>,<offset>[,<palPage8kNum 0..223>,<palOffset>]]
  614. // ;; SCREEN L2_640 [<Page8kNum 0..223>,<offset>[,<palPage8kNum 0..223>,<palOffset>]]
  615.         // check V1.3 features vs V1.2 enforced file version
  616.         if (L2_320x256 == type || L2_640x256 == type) {
  617.                 if (2 == nex.reqFileVersion) {
  618.                         Error("[SAVENEX] V1.2 was requested with OPEN, but 320x256 or 640x256 screen is V1.3 feature.", NULL, SUPPRESS);
  619.                         return;
  620.                 }
  621.                 nex.minFileVersion = 3;
  622.                 nex.h.hiResColour = 0;
  623.         }
  624.         // parse arguments
  625.         aint screenArgs[4] = {-1, 0, -1, 0};
  626.         const bool optionals[] = {true, false, true, false};
  627.         if (!getIntArguments<4>(screenArgs, optionals)
  628.                         || screenArgs[0] < -1 || SNexHeader::MAX_PAGE <= screenArgs[0]          // -1 for default pixel data
  629.                         || screenArgs[2] < -1 || SNexHeader::MAX_PAGE <= screenArgs[2]) {       // -1 for no-palette
  630.                 Error("[SAVENEX] expected syntax is ... [<Page8kNum 0..223>,<offset>[,<palPage8kNum 0..223>,<palOffset>]]", bp, SUPPRESS);
  631.                 return;
  632.         }
  633.         // warn about invalid values
  634.         const size_t totalRam = Device->GetMemoryOffset(Device->PagesCount, 0);
  635.         size_t adrPixelData = (-1 == screenArgs[0]) ? 0 : Device->GetMemoryOffset(screenArgs[0], screenArgs[1]);
  636.         size_t pixelDataSize = 0x14000;         // L2_320x256 and L2_640x256 size is default
  637.         if (Layer2 == type) pixelDataSize = 0xC000;
  638.         if (LoRes == type) pixelDataSize = 0x3000;
  639.         if (totalRam < adrPixelData + pixelDataSize) {
  640.                 Error("[SAVENEX] pixel data address range is outside of Next memory", bp, SUPPRESS);
  641.                 return;
  642.         }
  643.         // extract palette into internal buffer
  644.         if (!dirNexPaletteMem(screenArgs[2], screenArgs[3])) return;    // exit on serious error
  645.         // write palette into file (or update nex.h.screen with NOPAL flag if no palette was defined)
  646.         nex.writePalette();
  647.         // update header loading screen status
  648.         switch (type) {
  649.                 case Layer2:            nex.h.screen |= SNexHeader::SCR_LAYER2;         break;
  650.                 case LoRes:                     nex.h.screen |= SNexHeader::SCR_LORES;          break;
  651.                 case L2_320x256:
  652.                         nex.h.screen |= SNexHeader::SCR_EXT2;
  653.                         nex.h.screen2 = SNexHeader::SCR2_320x256;
  654.                         break;
  655.                 case L2_640x256:
  656.                         nex.h.screen |= SNexHeader::SCR_EXT2;
  657.                         nex.h.screen2 = SNexHeader::SCR2_640x256;
  658.                         break;
  659.                 default:
  660.                         break;
  661.         }
  662.         // write pixel data into file - first check if default RAM position should be used to read data
  663.         if (-1 == screenArgs[0]) {
  664.                 if (LoRes == type) {    // write first half of LoRes data straight from the VRAM position
  665.                         adrPixelData = Device->GetMemoryOffset(5*2, 0);
  666.                         pixelDataSize = 0x1800;
  667.                         if (pixelDataSize != fwrite(Device->Memory + adrPixelData, 1, pixelDataSize, nex.f)) {
  668.                                 Error("[SAVENEX] writing pixel data failed", NULL, FATAL);
  669.                         }
  670.                         adrPixelData += 0x2000;         // address of second half of data
  671.                 } else {
  672.                         adrPixelData = Device->GetMemoryOffset(9*2, 0);
  673.                 }
  674.         }
  675.         // write [remaining] part of pixel data into file
  676.         if (pixelDataSize != fwrite(Device->Memory + adrPixelData, 1, pixelDataSize, nex.f)) {
  677.                 Error("[SAVENEX] writing pixel data failed", NULL, FATAL);
  678.         }
  679. }
  680.  
  681. static void dirNexScreenBmp() {
  682. // ;; SAVENEX SCREEN BMP <filename>[,<savePalette 0/1>[,<paletteOffset 0..15>]]
  683.         const char* const bmpname = GetFileName(lp);
  684.         aint bmpArgs[2] = { 1, -1 };
  685.         if (comma(lp)) {        // empty filename will fall here too, causing syntax error
  686.                 const bool optionals[] = {false, true}; // savePalette is mandatory after comma
  687.                 if (!getIntArguments<2>(bmpArgs, optionals)) {
  688.                         Error("[SAVENEX] expected syntax is BMP <filename>[,<savePalette 0/1>[,<paletteOffset 0..15>]]", bp, SUPPRESS);
  689.                         delete[] bmpname;
  690.                         return;
  691.                 }
  692.         }
  693.         // validate argument values
  694.         if (bmpArgs[0] < 0 || 1 < bmpArgs[0]) {
  695.                 Warning("[SAVENEX] savePalette should be 0 or 1 (defaulting to 1)");
  696.                 bmpArgs[0] = 1;
  697.         }
  698.         if (bmpArgs[1] < -1 || 15 < bmpArgs[1]) {       // -1 is internal "off" value
  699.                 Warning("[SAVENEX] paletteOffset should be in 0..15 range");
  700.         }
  701.         // try to open the actual BMP file
  702.         SBmpFile bmp;
  703.         bool bmpOpened = bmp.open(bmpname);
  704.         if (bmpOpened && other == bmp.type) {
  705.                 Error("[SAVENEX] BMP file is not 256x192, 128x96, 320x256 or 640x256", bmpname, SUPPRESS);
  706.                 bmpOpened = false;
  707.         }
  708.         delete[] bmpname;
  709.         if (!bmpOpened) return;
  710.         // bmp opened, and some known type, verify details
  711.         if ((-1 != bmpArgs[1]) && (Layer2 == bmp.type || LoRes == bmp.type)) {
  712.                 // V1.2 screen types -> no paletteOffset
  713.                 Warning("[SAVENEX] BMP paletteOffset is available only for new V1.3 images (320 or 640 x256)");
  714.                 bmpArgs[1] = -1;
  715.         }
  716.         // check if V1.3 screens were provided, and if V1.3 is allowed, init internals
  717.         if (256 == bmp.height) {
  718.                 if (2 == nex.reqFileVersion) {
  719.                         Error("[SAVENEX] V1.2 was requested with OPEN, but 320x256 or 640x256 BMP is V1.3 feature.", NULL, SUPPRESS);
  720.                         return;
  721.                 } else {
  722.                         nex.minFileVersion = 3;
  723.                         if (-1 == bmpArgs[1]) bmpArgs[1] = 0;
  724.                 }
  725.         }
  726.         // check the palette if it was requested from this bmp and process it
  727.         if (bmpArgs[0]) dirNexPaletteBmp(bmp);
  728.         // palette is written first into file
  729.         nex.writePalette();
  730.         // update header loading screen status
  731.         switch (bmp.type) {
  732.                 case Layer2:
  733.                         nex.h.screen |= SNexHeader::SCR_LAYER2;
  734.                         break;
  735.                 case LoRes:
  736.                         nex.h.screen |= SNexHeader::SCR_LORES;
  737.                         break;
  738.                 case L2_320x256:
  739.                         nex.h.screen |= SNexHeader::SCR_EXT2;
  740.                         nex.h.screen2 = SNexHeader::SCR2_320x256;
  741.                         nex.h.hiResColour = bmpArgs[1];
  742.                         break;
  743.                 case L2_640x256:
  744.                         nex.h.screen |= SNexHeader::SCR_EXT2;
  745.                         nex.h.screen2 = SNexHeader::SCR2_640x256;
  746.                         nex.h.hiResColour = bmpArgs[1];
  747.                         break;
  748.                 default:
  749.                         ; // should be unreachable
  750.         }
  751.         // load and write pixel data
  752.         byte* buffer = new byte[641*256];               // buffer to read pixel data +1 write buffer
  753.         if (nullptr == buffer) ErrorOOM();
  754.         // read BMP first line by line into buffer (undid upside-down also)
  755.         bmp.loadPixelData(buffer);
  756.         // write pixel data into file - do transformation for 320/640 x 256 modes
  757.         if (Layer2 == bmp.type || LoRes == bmp.type) {
  758.                 const size_t pixelBlockSize = static_cast<size_t>(bmp.width) * static_cast<size_t>(bmp.height);
  759.                 if (pixelBlockSize != fwrite(buffer, 1, pixelBlockSize, nex.f)) {
  760.                         Error("[SAVENEX] writing pixel data failed", NULL, FATAL);
  761.                 }
  762.         } else {
  763.                 constexpr size_t h = 256, wB = 320;
  764.                 const bool xMul2 = (L2_640x256 == bmp.type);
  765.                 byte* const wbuf = buffer + 640 * 256;  // write buffer is at last 256B block
  766.                 // transpose data, store them column by column (two columns at time for 640x256)
  767.                 for (size_t x = 0; x < wB; ++x) {
  768.                         const byte* src = buffer + (xMul2 ? x*2 : x);
  769.                         for (size_t y = 0; y < h; ++y) {
  770.                                 const byte pixel = xMul2 ? (src[0]<<4) | (src[1]&0x0F) : src[0];
  771.                                 src += bmp.width;
  772.                                 wbuf[y] = pixel;
  773.                         }
  774.                         if (h != fwrite(wbuf, 1, h, nex.f)) {
  775.                                 Error("[SAVENEX] writing pixel data failed", NULL, FATAL);
  776.                         }
  777.                 }
  778.         }
  779.         delete[] buffer;
  780. }
  781.  
  782. static void dirNexScreenUlaTimex(byte scrType) {
  783. // ;; SCREEN (SCR|SHC|SHR) [<hiResColour 0..7>]
  784.         // parse argument (only HiRes screen type)
  785.         if (SNexHeader::SCR_HIRES == scrType) {
  786.                 aint hiResColor = 0;
  787.                 if (ParseExpression(lp, hiResColor)) {
  788.                         if (hiResColor < 0 || 7 < hiResColor) Warning("[SAVENEX] value is not in 0..7 range", bp);
  789.                         nex.h.hiResColour = hiResColor << 3;
  790.                 }
  791.         }
  792.         // update header loading screen status
  793.         nex.h.screen = scrType;
  794.         // warn about invalid values
  795.         size_t adrPixelData = Device->GetMemoryOffset(5*2, 0);
  796.         size_t pixelDataSize = (SNexHeader::SCR_ULA == scrType) ? 0x1B00 : 0x1800;
  797.         // write pixel data into file (from the default VRAM position of device)
  798.         if (pixelDataSize != fwrite(Device->Memory + adrPixelData, 1, pixelDataSize, nex.f)) {
  799.                 Error("[SAVENEX] writing pixel data failed", NULL, FATAL);
  800.         }
  801.         if (SNexHeader::SCR_ULA == scrType) return;             //ULA is written in one go
  802.         adrPixelData += 0x2000;                                                 // address of second half of data
  803.         // write [remaining] part of pixel data into file
  804.         if (pixelDataSize != fwrite(Device->Memory + adrPixelData, 1, pixelDataSize, nex.f)) {
  805.                 Error("[SAVENEX] writing pixel data failed", NULL, FATAL);
  806.         }
  807. }
  808.  
  809. static bool saveBank(aint bankIndex, aint bankNum, bool onlyNonZero = false);
  810.  
  811. static void dirNexScreenTile() {
  812. // ;; SCREEN TILE <NextReg $6B>,<NextReg $6C>,<NextReg $6E>,<NextReg $6F>[,<AlsoStoreBank5 0/1 = 1>]
  813.         // parse arguments
  814.         aint tileArgs[5] = {0, 0, 0, 0, 1};
  815.         const bool optionals[] = {false, false, false, false, true};
  816.         if (!getIntArguments<5>(tileArgs, optionals)
  817.                         || tileArgs[0] < 0 || 255 < tileArgs[0]
  818.                         || tileArgs[1] < 0 || 255 < tileArgs[1]
  819.                         || tileArgs[2] < 0 || 255 < tileArgs[2]
  820.                         || tileArgs[3] < 0 || 255 < tileArgs[3]
  821.                         || tileArgs[4] < 0 || 1 < tileArgs[4]) {
  822.                 Error("[SAVENEX] expected syntax is TILE <NextReg $6B>,<NextReg $6C>,<NextReg $6E>,<NextReg $6F>[,<AlsoStoreBank5 0/1 = 1>]", bp, SUPPRESS);
  823.                 return;
  824.         }
  825.         // check file version and set it up to V1.3
  826.         if (2 == nex.reqFileVersion) {
  827.                 Error("[SAVENEX] V1.2 was requested with OPEN, but tilemap screen is V1.3 feature.", NULL, SUPPRESS);
  828.                 return;
  829.         }
  830.         nex.minFileVersion = 3;
  831.         // write palette into file (or update nex.h.screen with NOPAL flag if no palette was defined)
  832.         nex.writePalette();
  833.         nex.h.screen |= SNexHeader::SCR_EXT2;
  834.         nex.h.screen2 = SNexHeader::SCR2_tilemap;
  835.         nex.h.tilesScrConfig[0] = static_cast<byte>(tileArgs[0]);
  836.         nex.h.tilesScrConfig[1] = static_cast<byte>(tileArgs[1]);
  837.         nex.h.tilesScrConfig[2] = static_cast<byte>(tileArgs[2]);
  838.         nex.h.tilesScrConfig[3] = static_cast<byte>(tileArgs[3]);
  839.         // write Bank 5 into file, if requested/default
  840.         if (0 == tileArgs[4]) return;           // suppressed
  841.         saveBank(getNexBankIndex(5), 5);
  842. }
  843.  
  844. static void dirNexScreen() {
  845.         if (!nex.canAppend) {
  846.                 Error("[SAVENEX] NEX file is not open", NULL, SUPPRESS);
  847.                 return;
  848.         }
  849.         if (0 != nex.h.screen) {
  850.                 Error("[SAVENEX] screen for this NEX file was already stored", NULL, SUPPRESS);
  851.                 return;
  852.         }
  853.         if (-1 != nex.lastBankIndex) {
  854.                 Error("[SAVENEX] some bank was already stored (store screen ahead)", NULL, SUPPRESS);
  855.                 return;
  856.         }
  857.         SkipBlanks(lp);
  858.         if (cmphstr(lp, "l2")) dirNexScreenLayer2andLowRes(Layer2);
  859.         else if (cmphstr(lp, "lr")) dirNexScreenLayer2andLowRes(LoRes);
  860.         else if (cmphstr(lp, "l2_320")) dirNexScreenLayer2andLowRes(L2_320x256);
  861.         else if (cmphstr(lp, "l2_640")) dirNexScreenLayer2andLowRes(L2_640x256);
  862.         else if (cmphstr(lp, "bmp")) dirNexScreenBmp();
  863.         else if (cmphstr(lp, "scr")) dirNexScreenUlaTimex(SNexHeader::SCR_ULA);
  864.         else if (cmphstr(lp, "shc")) dirNexScreenUlaTimex(SNexHeader::SCR_HICOL);
  865.         else if (cmphstr(lp, "shr")) dirNexScreenUlaTimex(SNexHeader::SCR_HIRES);
  866.         else if (cmphstr(lp, "tile")) dirNexScreenTile();
  867.         else Error("[SAVENEX] unknown screen type (types: BMP, L2, L2_320, L2_640, LR, SCR, SHC, SHR, TILE)", lp, SUPPRESS);
  868. }
  869.  
  870. static void dirNexCopper() {
  871. // ;; SAVENEX COPPER <Page8kNum 0..223>,<offset>
  872.         if (!nex.canAppend) {
  873.                 Error("[SAVENEX] NEX file is not open", NULL, SUPPRESS);
  874.                 return;
  875.         }
  876.         if (nex.reqFileVersion == 2) {
  877.                 Error("[SAVENEX] V1.2 was requested with OPEN, but COPPER is V1.3 feature.", NULL, SUPPRESS);
  878.                 return;
  879.         }
  880.         if (-1 != nex.lastBankIndex) {
  881.                 Error("[SAVENEX] some bank was already stored (store copper ahead)", NULL, SUPPRESS);
  882.                 return;
  883.         }
  884.         nex.minFileVersion = 3;         // V1.3 detected
  885.         // parse arguments
  886.         aint screenArgs[2] = {0, 0};
  887.         const bool optionals[] = {false, false};
  888.         if (!getIntArguments<2>(screenArgs, optionals)
  889.                         || screenArgs[0] < 0 || SNexHeader::MAX_PAGE <= screenArgs[0]) {
  890.                 Error("[SAVENEX] expected syntax is COPPER <Page8kNum 0..223>,<offset>", bp, SUPPRESS);
  891.                 return;
  892.         }
  893.         // warn about invalid values
  894.         const size_t totalRam = Device->GetMemoryOffset(Device->PagesCount, 0);
  895.         size_t adrCopperData = Device->GetMemoryOffset(screenArgs[0], screenArgs[1]);
  896.         if (totalRam < adrCopperData + SNexHeader::COPPER_SIZE) {
  897.                 Error("[SAVENEX] copper data address range is outside of Next memory", bp, SUPPRESS);
  898.                 return;
  899.         }
  900.         // adjust header and remember the copper data for saving them ahead of first bank
  901.         nex.h.hasCopperCode = 1;
  902.         if (nullptr == nex.copper) nex.copper = new byte[SNexHeader::COPPER_SIZE];
  903.         memcpy(nex.copper, Device->Memory + adrCopperData, SNexHeader::COPPER_SIZE);
  904. }
  905.  
  906. static bool saveBank(aint bankIndex, aint bankNum, bool onlyNonZero) {
  907.         if (bankNum < 0 || SNexHeader::MAX_BANK <= bankNum) return false;
  908.         if (bankIndex <= nex.lastBankIndex) {
  909.                 ErrorInt("[SAVENEX] it's too late to save this bank (correct order: 5, 2, 0, 1, 3, 4, 6, ...)",
  910.                                         bankNum, SUPPRESS);
  911.                 return false;
  912.         }
  913.         nex.updateIfAheadFirstBankSave();
  914.         const size_t offset = Device->GetMemoryOffset(bankNum * 2, 0);
  915.         const size_t size = 0x4000;
  916.         nex.lastBankIndex = bankIndex;
  917.         // detect bank which is just full of zeroes and exit early if onlyNonZero is requested
  918.         if (onlyNonZero) {
  919.                 size_t zeroOfs = 0;
  920.                 while (zeroOfs < size) {
  921.                         if (0 != Device->Memory[offset + zeroOfs]) break;
  922.                         ++zeroOfs;
  923.                 }
  924.                 if (size == zeroOfs) return true;
  925.         }
  926.         // update NEX header data
  927.         nex.h.banks[bankNum] = 1;
  928.         ++nex.h.numBanks;
  929.         // save the bank memory
  930.         if (size != fwrite(Device->Memory + offset, 1, size, nex.f)) {
  931.                 Error("[SAVENEX] writing bank data failed", NULL, FATAL);
  932.         }
  933.         return true;
  934. }
  935.  
  936. static void dirNexBank() {
  937.         if (!nex.canAppend) {
  938.                 Error("[SAVENEX] NEX file is not open", NULL, SUPPRESS);
  939.                 return;
  940.         }
  941.         do {
  942.                 aint bankNum, bankIndex;
  943.                 char *nextLp = lp;
  944.                 if (!ParseExpressionNoSyntaxError(lp, bankNum)
  945.                         || (bankIndex = getNexBankIndex(bankNum)) < 0) {
  946.                         Error("[SAVENEX] expected bank number 0..111", nextLp, SUPPRESS);
  947.                         break;
  948.                 }
  949.                 if (!saveBank(bankIndex, bankNum)) break;
  950.         } while (comma(lp));
  951. }
  952.  
  953. static void dirNexAuto() {
  954.         if (!nex.canAppend) {
  955.                 Error("[SAVENEX] NEX file is not open", NULL, SUPPRESS);
  956.                 return;
  957.         }
  958.         if (SNexHeader::MAX_BANK-1 == nex.lastBankIndex) {      // actually there's nothing left to scan
  959.                 Error("[SAVENEX] all banks are already stored", NULL, SUPPRESS);
  960.                 return;
  961.         }
  962.         // parse arguments
  963.         aint autoArgs[2] = { getNexBankNum(nex.lastBankIndex+1), SNexHeader::MAX_BANK-1 };
  964.         const bool optionals[] = {true, true};
  965.         if (!getIntArguments<2>(autoArgs, optionals)
  966.                         || autoArgs[0] < 0 || SNexHeader::MAX_BANK <= autoArgs[0]
  967.                         || autoArgs[1] < 0 || SNexHeader::MAX_BANK <= autoArgs[1]) {
  968.                 Error("[SAVENEX] expected syntax is AUTO [<fromBank 0..111>[,<toBank 0..111>]]", bp, SUPPRESS);
  969.                 return;
  970.         }
  971.         // validate arguments
  972.         aint fromI = getNexBankIndex(autoArgs[0]), toI = getNexBankIndex(autoArgs[1]);
  973.         if (toI < fromI) {
  974.                 Error("[SAVENEX] 'toBank' is less than 'fromBank'", bp, SUPPRESS);
  975.                 return;
  976.         }
  977.         while (fromI <= toI) {
  978.                 if (!saveBank(fromI, getNexBankNum(fromI), true)) return;
  979.                 ++fromI;
  980.         }
  981. }
  982.  
  983. static void dirNexClose() {
  984.         if (!nex.canAppend) {
  985.                 Error("[SAVENEX] NEX file is not open", NULL, SUPPRESS);
  986.                 return;
  987.         }
  988.         // update V1.3 banksOffset in case there was no bank stored at all (before appending binary data!)
  989.         nex.updateIfAheadFirstBankSave();
  990.         // read CLOSE command argument and try to append the proposed file (if some was provided)
  991.         char* appendName = nullptr;
  992.         if (!SkipBlanks(lp)) appendName = GetFileName(lp);
  993.         if (appendName) {       // some append file requested, try to copy its content at tail of NEX
  994.                 FILE* appendF = nullptr;
  995.                 if (!FOPEN_ISOK(appendF, appendName, "rb")) {
  996.                         Error("[SAVENEX] Error opening append file", appendName, SUPPRESS);
  997.                 } else {
  998.                         static constexpr int copyBufSize = 0x4000;
  999.                         byte* copyBuffer = new byte[copyBufSize];
  1000.                         if (nullptr == copyBuffer) ErrorOOM();
  1001.                         do {
  1002.                                 const size_t read = fread(copyBuffer, 1, copyBufSize, appendF);
  1003.                                 if (read) {
  1004.                                         const size_t write = fwrite(copyBuffer, 1, read, nex.f);
  1005.                                         if (write != read) Error("[SAVENEX] writing append data failed", NULL, FATAL);
  1006.                                 }
  1007.                         } while (!feof(appendF));
  1008.                         delete[] copyBuffer;
  1009.                         fclose(appendF);
  1010.                 }
  1011.                 delete[] appendName;
  1012.         }
  1013.         // finalize the NEX file (refresh the header data and close it)
  1014.         nex.finalizeFile();
  1015. }
  1016.  
  1017. void dirSAVENEX() {
  1018.         if (pass != LASTPASS) return;           // syntax error is not visible in early passes
  1019.         if (nullptr == DeviceID || strcmp(DeviceID, "ZXSPECTRUMNEXT")) {
  1020.                 Error("[SAVENEX] is allowed only in ZXSPECTRUMNEXT device mode", NULL, SUPPRESS);
  1021.                 return;
  1022.         }
  1023.         SkipBlanks(lp);
  1024.         if (cmphstr(lp, "open")) dirNexOpen();
  1025.         else if (cmphstr(lp, "core")) dirNexCore();
  1026.         else if (cmphstr(lp, "cfg3")) dirNexCfg3();
  1027.         else if (cmphstr(lp, "cfg")) dirNexCfg();
  1028.         else if (cmphstr(lp, "bar")) dirNexBar();
  1029.         else if (cmphstr(lp, "palette")) dirNexPalette();
  1030.         else if (cmphstr(lp, "screen")) dirNexScreen();
  1031.         else if (cmphstr(lp, "copper")) dirNexCopper();
  1032.         else if (cmphstr(lp, "bank")) dirNexBank();
  1033.         else if (cmphstr(lp, "auto")) dirNexAuto();
  1034.         else if (cmphstr(lp, "close")) dirNexClose();
  1035.         else Error("[SAVENEX] unknown command (commands: OPEN, CORE, CFG, BAR, SCREEN, BANK, AUTO, CLOSE)", lp, SUPPRESS);
  1036. }
  1037.