?login_element?

Subversion Repositories NedoOS

Rev

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