/*
SjASMPlus Z80 Cross Compiler - modified - SAVENEX extension
Copyright (c) 2006 Sjoerd Mastijn (original SW)
Copyright (c) 2019 Peter Ped Helcmanovsky (SAVENEX extension)
This software is provided 'as-is', without any express or implied warranty.
In no event will the authors be held liable for any damages arising from the
use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it freely,
subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not claim
that you wrote the original software. If you use this software in a product,
an acknowledgment in the product documentation would be appreciated but is
not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
*/
#include "sjdefs.h"
#include "crc32c.h"
// Banks in file are ordered in SNA way (but array "banks" in header is in numeric order instead)
static constexpr aint nexBankOrder[8] = {5, 2, 0, 1, 3, 4, 6, 7};
#ifdef _MSC_VER
#pragma pack(push, 1)
#endif
struct SNexHeader {
constexpr static size_t COPPER_SIZE = 0x0800;
constexpr static size_t PAL_SIZE = 0x200;
constexpr static aint MAX_BANK = 112;
constexpr static aint MAX_PAGE = MAX_BANK * 2;
constexpr static byte SCR_LAYER2 = 0x01;
constexpr static byte SCR_ULA = 0x02;
constexpr static byte SCR_LORES = 0x04;
constexpr static byte SCR_HIRES = 0x08;
constexpr static byte SCR_HICOL = 0x10;
constexpr static byte SCR_EXT2 = 0x40;
constexpr static byte SCR_NOPAL = 0x80;
constexpr static byte SCR2_320x256 = 1;
constexpr static byte SCR2_640x256 = 2;
constexpr static byte SCR2_tilemap = 3;
byte magicAndVersion[8]; // the "magic" number + file version at the beginning
byte ramReq; // 0 = 768k, 1 = 1792k
byte numBanks; // number of 16k banks to load: 0..112
byte screen; // loading screen flags
byte border; // border colour 0..7
word sp; // stack pointer
word pc; // start address (0 = no start)
word _obsolete_numfiles;
byte banks[MAX_BANK]; // 112 16ki banks (1.75MiB) - non-zero value = in file
// banks array is ordinary order 0, 1, 2, ..., but banks in file are in order: 5, 2, 0, 1, ...
byte loadbar; // 0/1 show progress bar
byte loadbarColour; // colour of progress bar (precise meaning depends on gfx mode)
byte loadDelay; // delay after each bank is loaded (number of frames)
byte startDelay; // delay after whole file is loaded (number of frames)
byte preserveNextRegs; // 0 = reset whole machine state, 1 = preserve most of it
byte coreVersion[3];
byte hiResColour; // bits 5-3 for port 255 (ASM source provides 0..7 value, needs shift)
byte entryBank; // 16ki bank 0..111 to be mapped into C000..FFFF range
word fileHandleCfg; // 0 = close NEX file, 1 = pass handle in BC, 0x4000+ = address to write handle
// V1.3 fields
byte expBusDisable; // 0 = disable expansion bus by setting top four bits of NextReg 0x80, 1 = no-op
byte hasChecksum; // 0 = no checksum, 1 = last 4B of header are CRC-32C (Castagnoli)
uint32_t banksOffset; // where data of first bank start in file (NEX V1.3+ file, 0 olders)
word cliBuffer; // address of buffer for command line copy (0 = off)
word cliBufferSize; // size of the provided buffer (0 = off) (cmd line is truncated to this size)
byte screen2; // extended screen flag (old "screen" must have +64 flag)
byte hasCopperCode; // 0 = no copper, 1 = 2048B copper block after loading screens
byte tilesScrConfig[4]; // NextReg registers $6B, $6C, $6E, $6F values for Tilemode screen
byte bigL2barPosY; // Y position (0..255) of loading bar for new Layer 2 320x256 and 640x256 modes
byte _reserved[349];
uint32_t crc32c; // CRC-32C build by: file offset 512->EOF (including append bin), then 508B header
void init();
void prepareLittleEndianBinaryForm();
void restoreHostEndianBinaryForm();
}
#ifndef _MSC_VER
__attribute__((packed));
#else
;
#pragma pack(pop)
#endif
static_assert(512 == sizeof(SNexHeader), "NEX header is expected to be 512 bytes long!");
struct SNexFile {
SNexHeader h;
aint reqFileVersion; // requested file version in OPEN command
aint minFileVersion; // currently auto-detected file version
FILE* f = nullptr; // NEX file handle, stay opened, fseek stays at <EOF>
// file is build sequentially, adding further blocks, only finalize does refresh the header
aint lastBankIndex; // numeric order (0, 1, ...) value, -1 is init value
byte* copper = nullptr; // temporary storage of copper code (add it ahead of first bank)
byte* palette = nullptr; // final palette (will override the one stored upon finalize)
bool palDefined; // whether the palette data/type was enforced by PALETTE command
bool canAppend = false; // true when `fwrite(..., f)` can be used in "append like" way
// set `canAppend` to false whenever you do fseek/fread, it cancels validity of "next fwrite"
~SNexFile();
void init();
void writeHeader();
void writePalette();
void calculateCrc32C();
void updateIfAheadFirstBankSave();
void finalizeFile();
};
// the instance holding all tooling data and header about currently opened NEX file
static SNexFile nex;
void SNexHeader::init() {
memset(magicAndVersion, 0, sizeof(SNexHeader)); // clear whole 512 bytes
// set "magic" number and file version
memcpy(magicAndVersion, "NextV1.2", 8); // setup "magic" number at beginning
// required core version is by default 2.00.28 (latest released)
coreVersion[0] = 2;
coreVersion[1] = 0;
coreVersion[2] = 28;
}
void SNexHeader::prepareLittleEndianBinaryForm() {
if (Options::IsBigEndian) {
sp = sj_bswap16(sp);
pc = sj_bswap16(pc);
_obsolete_numfiles = sj_bswap16(_obsolete_numfiles);
fileHandleCfg = sj_bswap16(fileHandleCfg);
banksOffset = sj_bswap32(banksOffset);
cliBuffer = sj_bswap16(cliBuffer);
cliBufferSize = sj_bswap16(cliBufferSize);
crc32c = sj_bswap32(crc32c);
}
}
void SNexHeader::restoreHostEndianBinaryForm() {
// it's actually identical to the prepareLittleEndianBinaryForm, but keeping unique naming
prepareLittleEndianBinaryForm();
}
SNexFile::~SNexFile() {
finalizeFile();
}
void SNexFile::init() {
h.init();
lastBankIndex = -1; // reset last bank index
palDefined = false;
reqFileVersion = 0;
minFileVersion = 2;
}
void SNexFile::updateIfAheadFirstBankSave() {
// check if already updated or file is not ready for appending (no file, or finalizing)
if (h.banksOffset || !canAppend) return;
// updating bank offset after some bank was already stored -> should never happen
if (-1 != lastBankIndex) Error("[SAVENEX] V1.3?!", NULL, FATAL); // unreachable
if (palDefined && 0 == h.screen) {
Warning("[SAVENEX] some palette was defined, but without screen it is ignored.");
}
// V1.3 feature, copper code is the last block ahead of first bank
if (h.hasCopperCode) {
if (SNexHeader::COPPER_SIZE != fwrite(copper, 1, SNexHeader::COPPER_SIZE, f)) {
Error("[SAVENEX] writing copper data failed", NULL, FATAL);
}
}
// V1.3 feature, offset of very first bank stored in file
h.banksOffset = ftell(f);
}
void SNexFile::writeHeader() {
if (nullptr == f) return;
canAppend = false; // does fseek, cancel the "append" mode
// refresh/write the file header
fseek(f, 0, SEEK_SET);
h.prepareLittleEndianBinaryForm();
if (sizeof(SNexHeader) != fwrite(&h, 1, sizeof(SNexHeader), f)) {
Error("[SAVENEX] writing header content failed", NULL, SUPPRESS);
}
h.restoreHostEndianBinaryForm();
}
void SNexFile::writePalette() {
if (!canAppend) return;
if (!palDefined || nullptr == palette) { // palette is completely undefined or
h.screen = SNexHeader::SCR_NOPAL; // "no palette" was defined
} else {
if (SNexHeader::PAL_SIZE != fwrite(palette, 1, SNexHeader::PAL_SIZE, f)) {
Error("[SAVENEX] writing palette data failed", NULL, FATAL);
}
}
}
void SNexFile::calculateCrc32C() {
if (!h.hasChecksum) return;
if (nullptr == f) return;
canAppend = false; // does fseek+fread, cancel the "append" mode
// calculate checksum CRC-32C (Castagnoli)
crc32_init();
constexpr size_t BUFFER_SIZE = 128 * 1024; // 128kiB buffer to read file (must be 512+ !!)
uint8_t *buffer = new uint8_t[BUFFER_SIZE];
if (nullptr == buffer) ErrorOOM();
uint32_t crc = 0;
// calculate CRC of the file part after header (offset 512)
fseek(f, 512, SEEK_SET);
size_t bytes_read = 0;
do {
bytes_read = fread(buffer, 1, BUFFER_SIZE, f);
if (0 == bytes_read) break;
crc = crc32c_append_sw(crc, buffer, bytes_read);
} while (BUFFER_SIZE == bytes_read);
// calculate CRC of the header part (first 508 bytes of header)
fseek(f, 0, SEEK_SET);
bytes_read = fread(buffer, 1, 508, f);
h.hasChecksum = (508 == bytes_read);
if (h.hasChecksum) {
h.crc32c = crc32c_append_sw(crc, buffer, bytes_read);
} else {
Error("[SAVENEX] reading file for CRC calculation failed");
}
delete[] buffer;
}
void SNexFile::finalizeFile() {
if (nullptr == f) return;
// do the final V1.2 / V1.3 updates to the header fields
// V1.3 auto-detected when V1.2 is required should never happen (Error should be unreachable)
if (3 == minFileVersion && 2 == reqFileVersion) Error("[SAVENEX] V1.3?!", NULL, FATAL);
updateIfAheadFirstBankSave(); // if no BANK/AUTO/CLOSE was used -> update all now
if (2 == minFileVersion) {
h.banksOffset = 0; // clear banksOffset for V1.2 files
h.bigL2barPosY = 0; // clear big Layer 2 loading-bar posY for V1.2 files
} else {
h.magicAndVersion[7] = '3'; // modify file version to "V1.3" string
calculateCrc32C();
}
// refresh the file header to final state
writeHeader();
// close the file
fclose(f);
f = nullptr;
canAppend = false;
if (nullptr != copper) delete[] copper;
copper = nullptr;
if (nullptr != palette) delete[] palette;
palette = nullptr;
// check if there were banks 48+, but 2MB required was not set
byte hasExtendedBank = 0;
for (int i = 48; i < SNexHeader::MAX_BANK; ++i) hasExtendedBank |= h.banks[i];
if (!h.ramReq && hasExtendedBank) {
Error("[SAVENEX] 2MB bank (48..111) stored without 2MbRamReq set in CFG");
}
return;
}
enum EBmpType { other, Layer2, LoRes, L2_320x256, L2_640x256 };
class SBmpFile {
static constexpr size_t HEADERS_SIZE = 0x36; // 14B header + BITMAPINFOHEADER 40B header
static constexpr size_t PALETTE_SIZE = 0x100;
FILE* bmp;
byte tempHeader[HEADERS_SIZE];
byte* palBuffer = nullptr;
public:
EBmpType type = other;
int32_t width = 0, height = 0;
bool upsideDown = false;
uint32_t colorsUsed = 0;
~SBmpFile();
void close();
bool open(const char* bmpname);
word getColor(uint32_t index);
void loadPixelData(byte* buffer);
};
SBmpFile::~SBmpFile() {
close();
}
void SBmpFile::close() {
if (nullptr == bmp) return;
fclose(bmp);
bmp = nullptr;
delete[] palBuffer;
palBuffer = nullptr;
}
bool SBmpFile::open(const char* bmpname) {
if (!FOPEN_ISOK(bmp, bmpname, "rb")) {
Error("[SAVENEX] Error opening file", bmpname, SUPPRESS);
return false;
}
palBuffer = new byte[4*PALETTE_SIZE];
if (nullptr == palBuffer) ErrorOOM();
// read header of BMP and verify the file is of expected format
bool allRead = (HEADERS_SIZE == fread(tempHeader, 1, HEADERS_SIZE, bmp));
allRead = allRead && (PALETTE_SIZE == fread(palBuffer, 4, PALETTE_SIZE, bmp));
// these following casts assume the sjasmplus itself is running at little-endian host
uint32_t header2Size = reinterpret_cast<SAlignSafeCast<uint32_t>*>(tempHeader + 14)->val;
uint16_t colorPlanes = *reinterpret_cast<uint16_t*>(tempHeader + 26);
uint16_t bpp = *reinterpret_cast<uint16_t*>(tempHeader + 28);
uint32_t compressionType = reinterpret_cast<SAlignSafeCast<uint32_t>*>(tempHeader + 30)->val;
// fix values on BE hosts
if (Options::IsBigEndian) {
header2Size = sj_bswap32(header2Size);
colorPlanes = sj_bswap16(colorPlanes);
bpp = sj_bswap16(bpp);
compressionType = sj_bswap32(compressionType);
}
// check "BM", BITMAPINFOHEADER type (size 40), 8bpp, no compression
if (!allRead || 'B' != tempHeader[0] || 'M' != tempHeader[1] ||
40 != header2Size || 1 != colorPlanes || 8 != bpp || 0 != compressionType)
{
Error("[SAVENEX] BMP file is not in expected format (uncompressed, 8bpp, 40B BITMAPINFOHEADER header)",
bmpname, SUPPRESS);
close();
return false;
}
// check if the size is 256x192 (Layer 2) or 128x96 (LoRes), or 320/640 x 256 (V1.3).
colorsUsed = reinterpret_cast<SAlignSafeCast<uint32_t>*>(tempHeader + 46)->val;
width = reinterpret_cast<SAlignSafeCast<int32_t>*>(tempHeader + 18)->val;
height = reinterpret_cast<SAlignSafeCast<int32_t>*>(tempHeader + 22)->val;
// fix values on BE hosts
if (Options::IsBigEndian) {
colorsUsed = sj_bswap32(colorsUsed);
width = sj_bswap32(width);
height = sj_bswap32(height);
}
upsideDown = 0 < height;
if (height < 0) height = -height;
if (256 == width && 192 == height) type = Layer2;
if (128 == width && 96 == height) type = LoRes;
if (256 == height) {
if (320 == width) type = L2_320x256;
if (640 == width) type = L2_640x256;
}
return true;
}
word SBmpFile::getColor(uint32_t index) {
if (nullptr == bmp || nullptr == palBuffer || 256 <= index) return 0;
const byte B = palBuffer[index * 4 + 0] >> 5;
const byte G = palBuffer[index * 4 + 1] >> 5;
const byte R = palBuffer[index * 4 + 2] >> 5;
return ((B&1) << 8) | (B >> 1) | (G << 2) | (R << 5);
}
void SBmpFile::loadPixelData(byte* buffer) {
uint32_t offset = reinterpret_cast<SAlignSafeCast<uint32_t>*>(tempHeader + 10)->val;
if (Options::IsBigEndian) offset = sj_bswap32(offset);
const size_t w = static_cast<size_t>(width);
for (int32_t y = 0; y < height; ++y) {
const int32_t fileY = upsideDown ? (height - y - 1) : y;
fseek(bmp, offset + (w * fileY), SEEK_SET);
if (w != fread(buffer + (w * y), 1, w, bmp)) {
Error("[SAVENEX] reading BMP pixel data failed", NULL, FATAL);
}
}
}
static aint getNexBankIndex(const aint bank16kNum) {
if (8 <= bank16kNum && bank16kNum < SNexHeader::MAX_BANK) return bank16kNum;
for (aint i = 0; i < 8; ++i) {
if (nexBankOrder[i] == bank16kNum) return i;
}
return -2;
}
static aint getNexBankNum(const aint bankIndex) {
if (0 <= bankIndex && bankIndex < 8) return nexBankOrder[bankIndex];
if (8 <= bankIndex && bankIndex < SNexHeader::MAX_BANK) return bankIndex;
return -1;
}
static void checkStackPointer() {
constexpr int CHECK_SIZE = 10;
constexpr int EXPECTED_SLOTS_COUNT = 8;
const int adrMask = Device->GetCurrentSlot()->Size - 1;
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 };
assert(EXPECTED_SLOTS_COUNT == Device->SlotsCount);
// check if SP is too close to ROM (0x0001 ... 0x4009)
if (0x0000 < nex.h.sp && nex.h.sp < 0x4000 + CHECK_SIZE) {
Warning("[SAVENEX] stackAddress is too close to ROM area");
return;
}
// check if good-looking SP points to enough of zeroed memory, warn about overwrite if not
word spCheck = word(nex.h.sp - CHECK_SIZE);
while (spCheck != nex.h.sp) {
const int pageNum = pages[Device->GetSlotOfA16(spCheck)];
const size_t offset = Device->GetMemoryOffset(pageNum, spCheck & adrMask);
if (0 != Device->Memory[offset]) break;
++spCheck;
}
if (spCheck == nex.h.sp) return;
WarningById(W_NEX_STACK);
}
static void dirNexOpen() {
if (nex.f) {
Error("[SAVENEX] NEX file is already open", bp, SUPPRESS);
return;
}
nex.init(); // reset everything around NEX file data
// read OPEN command arguments
std::unique_ptr<char[]> fname(GetOutputFileName(lp));
aint openArgs[4] = { (-1 == StartAddress ? 0 : StartAddress), 0xFFFE, 0, 0 };
if (comma(lp)) {
const bool optionals[] = {false, true, true, true}; // start address is mandatory because comma
if (!getIntArguments<4>(lp, openArgs, optionals)) {
Error("[SAVENEX] expected syntax is OPEN <filename>[,<startAddress>[,<stackAddress>[,<entryBank 0..111>[,<fileVersion 2..3>]]]]", bp, SUPPRESS);
return;
}
}
// validate argument values
if (-1 != StartAddress && StartAddress != openArgs[0]) {
Warning("[SAVENEX] Start address was also defined by END, OPEN argument used instead");
}
check16(openArgs[0]);
check16(openArgs[1]);
if (openArgs[2] < 0 || SNexHeader::MAX_BANK <= openArgs[2]) {
ErrorInt("[SAVENEX] entry bank can be 0..111 value only", openArgs[2], SUPPRESS);
return;
}
if (openArgs[3] && (openArgs[3] < 2 || 3 < openArgs[3])) {
ErrorInt("[SAVENEX] only file version 2 (V1.2) or 3 (V1.3) can be enforced", openArgs[3], SUPPRESS);
return;
}
// try to open the actual file
if (!FOPEN_ISOK(nex.f, fname.get(), "w+b")) Error("[SAVENEX] Error opening file for write", fname.get(), SUPPRESS);
if (nullptr == nex.f) return;
// set the argument values into header, and write the initial version of header into file
nex.h.pc = openArgs[0] & 0xFFFF;
nex.h.sp = openArgs[1] & 0xFFFF;
nex.h.entryBank = openArgs[2];
nex.reqFileVersion = openArgs[3];
nex.minFileVersion = (3 == nex.reqFileVersion) ? 3 : 2; // reset auto-detected file version
nex.writeHeader();
// After writing header first time, the file is ready for "append like" usage
nex.canAppend = true;
checkStackPointer();
}
static void dirNexCore() {
if (nullptr == nex.f) {
Error("[SAVENEX] NEX file is not open", NULL, SUPPRESS);
return;
}
// parse arguments
aint coreArgs[3] = {0};
const bool optionals[] = {false, false, false};
if (!getIntArguments<3>(lp, coreArgs, optionals)) {
Error("[SAVENEX] expected syntax is CORE <major 0..15>,<minor 0..15>,<subminor 0..255>", bp, SUPPRESS);
return;
}
// warn about invalid values
if (coreArgs[0] < 0 || 15 < coreArgs[0] ||
coreArgs[1] < 0 || 15 < coreArgs[1] ||
coreArgs[2] < 0 || 255 < coreArgs[2]) Warning("[SAVENEX] values are not within 0..15,0..15,0..255 ranges");
// set the values in header
nex.h.coreVersion[0] = coreArgs[0];
nex.h.coreVersion[1] = coreArgs[1];
nex.h.coreVersion[2] = coreArgs[2];
}
static void dirNexCfg3() {
// ;; SAVENEX CFG3 <DoCRC 0/1>[,<PreserveExpansionBus 0/1>[,<CLIbufferAdr>,<CLIbufferSize>]]
if (nullptr == nex.f) {
Error("[SAVENEX] NEX file is not open", NULL, SUPPRESS);
return;
}
if (nex.reqFileVersion == 2) {
Error("[SAVENEX] V1.2 was requested with OPEN, but CFG3 is V1.3 feature.", NULL, SUPPRESS);
return;
}
nex.minFileVersion = 3; // V1.3 detected
// parse arguments
aint cfgArgs[4] = {1, 0};
const bool optionals[] = {false, true, true, false};
if (!getIntArguments<4>(lp, cfgArgs, optionals)) {
Error("[SAVENEX] expected syntax is CFG3 <DoCRC 0/1>[,<PreserveExpansionBus 0/1>[,<CLIbufferAdr>,<CLIbufferSize>]]", bp, SUPPRESS);
return;
}
const bool someCliBuffer = cfgArgs[2] || cfgArgs[3]; // [0, 0] = no CLI buffer, don't check validity
// warn about invalid values
if (cfgArgs[0] < 0 || 1 < cfgArgs[0] ||
cfgArgs[1] < 0 || 1 < cfgArgs[1] ||
(someCliBuffer &&
(cfgArgs[2] < 0x4000 || 0x10000 < (cfgArgs[2] + cfgArgs[3]) ||
cfgArgs[3] < 1 || 0x0800 < cfgArgs[3]))) {
Warning("[SAVENEX] crc/preserve values are not 0/1 or CLI buffer doesn't fit into $4000..$FFFF range (size can be 2048 max)");
}
// set the values in header
nex.h.hasChecksum = !!cfgArgs[0];
nex.h.expBusDisable = !!cfgArgs[1];
nex.h.cliBuffer = cfgArgs[2];
nex.h.cliBufferSize = cfgArgs[3];
if (nex.h.hasChecksum && Options::IsBigEndian) {
Error("[SAVENEX] CRC feature is not available at big-endian host machine (wrong CRC implementation in sjasmplus, sorry)");
nex.h.hasChecksum = false;
}
}
static void dirNexCfg() {
// ;; SAVENEX CFG <border 0..7>[,<fileHandle 0/1/$4000+>[,<PreserveNextRegs 0/1>[,<2MbRamReq 0/1>]]]
if (nullptr == nex.f) {
Error("[SAVENEX] NEX file is not open", NULL, SUPPRESS);
return;
}
// parse arguments
aint cfgArgs[4] = {0};
const bool optionals[] = {false, true, true, true};
if (!getIntArguments<4>(lp, cfgArgs, optionals)) {
Error("[SAVENEX] expected syntax is CFG <border 0..7>[,<fileHandle 0/1/$4000+>[,<PreserveNextRegs 0/1>[,<2MbRamReq 0/1>]]]", bp, SUPPRESS);
return;
}
// warn about invalid values
if (cfgArgs[0] < 0 || 7 < cfgArgs[0] ||
cfgArgs[1] < 0 || 0xFFFE < cfgArgs[1] ||
cfgArgs[2] < 0 || 1 < cfgArgs[2] ||
cfgArgs[3] < 0 || 1 < cfgArgs[3]) Warning("[SAVENEX] values are not within 0..7,0..65534,0/1,0/1 ranges");
// set the values in header
nex.h.border = cfgArgs[0] & 7;
nex.h.fileHandleCfg = cfgArgs[1];
nex.h.preserveNextRegs = cfgArgs[2];
nex.h.ramReq = cfgArgs[3];
}
static void dirNexBar() {
if (nullptr == nex.f) {
Error("[SAVENEX] NEX file is not open", NULL, SUPPRESS);
return;
}
// parse arguments
aint barArgs[5] = {0, 0, 0, 0, 254};
const bool optionals[] = {false, false, true, true, true};
if (!getIntArguments<5>(lp, barArgs, optionals)) {
Error("[SAVENEX] expected syntax is BAR <loadBar 0/1>,<barColour 0..255>[,<startDelay 0..255>[,<bankDelay 0..255>[,<posY 0..255>]]]", bp, SUPPRESS);
return;
}
// warn about invalid values
if (barArgs[0] < 0 || 1 < barArgs[0] ||
barArgs[1] < 0 || 255 < barArgs[1] ||
barArgs[2] < 0 || 255 < barArgs[2] ||
barArgs[3] < 0 || 255 < barArgs[3] ||
barArgs[4] < 0 || 255 < barArgs[4]) Warning("[SAVENEX] values are not within 0/1 or 0..255 ranges");
// set the values in header
nex.h.loadbar = barArgs[0];
nex.h.loadbarColour = barArgs[1];
nex.h.startDelay = barArgs[2];
nex.h.loadDelay = barArgs[3];
nex.h.bigL2barPosY = barArgs[4];
}
static void dirNexPaletteDefault() {
// ;; SAVENEX PALETTE DEFAULT
nex.palDefined = true;
nex.palette = new byte[SNexHeader::PAL_SIZE];
if (nullptr == nex.palette) ErrorOOM();
for (uint32_t i = 0; i < 256; ++i) {
nex.palette[i*2 + 0] = static_cast<byte>(i);
nex.palette[i*2 + 1] = (i & 3) ? 1 : 0; // bottom blue bit is 1 when some upper bit is
}
}
static bool dirNexPaletteMem(const aint page8kNum, const aint palOffset) {
if (nex.palDefined) return true; // palette was already defined, silently ignore
if (-1 == page8kNum) { // this is used as "no palette" by some screen commands
nex.palDefined = true;
return true;
}
// warn about invalid values
const size_t totalRam = Device->GetMemoryOffset(Device->PagesCount, 0);
const int32_t adrPalData = Device->GetMemoryOffset(page8kNum, palOffset);
if (adrPalData < 0 || totalRam < adrPalData + SNexHeader::PAL_SIZE) {
Error("[SAVENEX] palette data address range is outside of Next memory", bp, SUPPRESS);
return false;
}
// copy the data into internal palette buffer
nex.palDefined = true;
nex.palette = new byte[SNexHeader::PAL_SIZE];
if (nullptr == nex.palette) ErrorOOM();
memcpy(nex.palette, Device->Memory + adrPalData, SNexHeader::PAL_SIZE);
return true;
}
static bool dirNexPaletteBmp(SBmpFile & bmp) {
if (nex.palDefined) return true; // palette was already defined, silently ignore
// copy the data into internal palette buffer
nex.palDefined = true;
nex.palette = new byte[SNexHeader::PAL_SIZE];
if (nullptr == nex.palette) ErrorOOM();
constexpr size_t palDataSize = 256;
for (size_t i = 0; i < palDataSize; ++i) {
const word nextColor = bmp.getColor(i);
nex.palette[i*2 + 0] = nextColor & 0xFF;
nex.palette[i*2 + 1] = nextColor >> 8;
}
return true;
}
static void dirNexPaletteMem() {
// ;; SAVENEX PALETTE MEM <palPage8kNum 0..223>,<palOffset>
aint palArgs[2] = {0, 0};
const bool optionals[] = {false, false};
if (!getIntArguments<2>(lp, palArgs, optionals)
|| palArgs[0] < 0 || SNexHeader::MAX_PAGE <= palArgs[0] || palArgs[1] < 0) {
Error("[SAVENEX] expected syntax is MEM <palPage8kNum 0..223>,<palOffset 0+>", bp, SUPPRESS);
return;
}
dirNexPaletteMem(palArgs[0], palArgs[1]);
}
static void dirNexPaletteBmp() {
// ;; SAVENEX PALETTE BMP <filename>
const char* const bmpname = GetFileName(lp);
if (!bmpname[0] || comma(lp)) {
Error("[SAVENEX] expected syntax is BMP <filename>", bp, SUPPRESS);
delete[] bmpname;
return;
}
// try to open the actual BMP file
SBmpFile bmp;
bool bmpOpened = bmp.open(bmpname);
delete[] bmpname;
if (!bmpOpened) return;
// check the palette if it was requested from this bmp and process it
dirNexPaletteBmp(bmp);
}
static void dirNexPalette() {
// ;; SAVENEX PALETTE (NONE|DEFAULT|MEM|BMP)
if (nullptr == nex.f) {
Error("[SAVENEX] NEX file is not open", NULL, SUPPRESS);
return;
}
if (nex.palDefined || 0 != nex.h.screen) {
Error("[SAVENEX] some palette/screen was already defined (define palette once and ahead)", NULL, SUPPRESS);
return;
}
if (-1 != nex.lastBankIndex) {
Error("[SAVENEX] some bank was already stored (define palette ahead)", NULL, SUPPRESS);
return;
}
SkipBlanks(lp);
if (cmphstr(lp, "none")) nex.palDefined = true;
else if (cmphstr(lp, "default")) dirNexPaletteDefault();
else if (cmphstr(lp, "mem")) dirNexPaletteMem();
else if (cmphstr(lp, "bmp")) dirNexPaletteBmp();
else Error("[SAVENEX] unknown palette command (commands: NONE, DEFAULT, MEM, BMP)", lp, SUPPRESS);
}
static void dirNexScreenLayer2andLowRes(EBmpType type) {
// ;; SCREEN L2 [<Page8kNum 0..223>,<offset>[,<palPage8kNum 0..223>,<palOffset>]]
// ;; SCREEN LR [<Page8kNum 0..223>,<offset>[,<palPage8kNum 0..223>,<palOffset>]]
// ;; SCREEN L2_320 [<Page8kNum 0..223>,<offset>[,<palPage8kNum 0..223>,<palOffset>]]
// ;; SCREEN L2_640 [<Page8kNum 0..223>,<offset>[,<palPage8kNum 0..223>,<palOffset>]]
// check V1.3 features vs V1.2 enforced file version
if (L2_320x256 == type || L2_640x256 == type) {
if (2 == nex.reqFileVersion) {
Error("[SAVENEX] V1.2 was requested with OPEN, but 320x256 or 640x256 screen is V1.3 feature.", NULL, SUPPRESS);
return;
}
nex.minFileVersion = 3;
nex.h.hiResColour = 0;
}
// parse arguments
aint screenArgs[4] = {-1, 0, -1, 0};
const bool optionals[] = {true, false, true, false};
if (!getIntArguments<4>(lp, screenArgs, optionals)
|| screenArgs[0] < -1 || SNexHeader::MAX_PAGE <= screenArgs[0] // -1 for default pixel data
|| screenArgs[2] < -1 || SNexHeader::MAX_PAGE <= screenArgs[2]) { // -1 for no-palette
Error("[SAVENEX] expected syntax is ... [<Page8kNum 0..223>,<offset>[,<palPage8kNum 0..223>,<palOffset>]]", bp, SUPPRESS);
return;
}
// warn about invalid values
const size_t totalRam = Device->GetMemoryOffset(Device->PagesCount, 0);
size_t adrPixelData = (-1 == screenArgs[0]) ? 0 : Device->GetMemoryOffset(screenArgs[0], screenArgs[1]);
size_t pixelDataSize = 0x14000; // L2_320x256 and L2_640x256 size is default
if (Layer2 == type) pixelDataSize = 0xC000;
if (LoRes == type) pixelDataSize = 0x3000;
if (totalRam < adrPixelData + pixelDataSize) {
Error("[SAVENEX] pixel data address range is outside of Next memory", bp, SUPPRESS);
return;
}
// extract palette into internal buffer
if (!dirNexPaletteMem(screenArgs[2], screenArgs[3])) return; // exit on serious error
// write palette into file (or update nex.h.screen with NOPAL flag if no palette was defined)
nex.writePalette();
// update header loading screen status
switch (type) {
case Layer2: nex.h.screen |= SNexHeader::SCR_LAYER2; break;
case LoRes: nex.h.screen |= SNexHeader::SCR_LORES; break;
case L2_320x256:
nex.h.screen |= SNexHeader::SCR_EXT2;
nex.h.screen2 = SNexHeader::SCR2_320x256;
break;
case L2_640x256:
nex.h.screen |= SNexHeader::SCR_EXT2;
nex.h.screen2 = SNexHeader::SCR2_640x256;
break;
default:
break;
}
// write pixel data into file - first check if default RAM position should be used to read data
if (-1 == screenArgs[0]) {
if (LoRes == type) { // write first half of LoRes data straight from the VRAM position
adrPixelData = Device->GetMemoryOffset(5*2, 0);
pixelDataSize = 0x1800;
if (pixelDataSize != fwrite(Device->Memory + adrPixelData, 1, pixelDataSize, nex.f)) {
Error("[SAVENEX] writing pixel data failed", NULL, FATAL);
}
adrPixelData += 0x2000; // address of second half of data
} else {
adrPixelData = Device->GetMemoryOffset(9*2, 0);
}
}
// write [remaining] part of pixel data into file
if (pixelDataSize != fwrite(Device->Memory + adrPixelData, 1, pixelDataSize, nex.f)) {
Error("[SAVENEX] writing pixel data failed", NULL, FATAL);
}
}
static void dirNexScreenBmp() {
// ;; SAVENEX SCREEN BMP <filename>[,<savePalette 0/1>[,<paletteOffset 0..15>]]
const char* const bmpname = GetFileName(lp);
aint bmpArgs[2] = { 1, -1 };
if (comma(lp)) { // empty filename will fall here too, causing syntax error
const bool optionals[] = {false, true}; // savePalette is mandatory after comma
if (!getIntArguments<2>(lp, bmpArgs, optionals)) {
Error("[SAVENEX] expected syntax is BMP <filename>[,<savePalette 0/1>[,<paletteOffset 0..15>]]", bp, SUPPRESS);
delete[] bmpname;
return;
}
}
// validate argument values
if (bmpArgs[0] < 0 || 1 < bmpArgs[0]) {
Warning("[SAVENEX] savePalette should be 0 or 1 (defaulting to 1)");
bmpArgs[0] = 1;
}
if (bmpArgs[1] < -1 || 15 < bmpArgs[1]) { // -1 is internal "off" value
Warning("[SAVENEX] paletteOffset should be in 0..15 range");
}
// try to open the actual BMP file
SBmpFile bmp;
bool bmpOpened = bmp.open(bmpname);
if (bmpOpened && other == bmp.type) {
Error("[SAVENEX] BMP file is not 256x192, 128x96, 320x256 or 640x256", bmpname, SUPPRESS);
bmpOpened = false;
}
delete[] bmpname;
if (!bmpOpened) return;
// bmp opened, and some known type, verify details
if ((-1 != bmpArgs[1]) && (Layer2 == bmp.type || LoRes == bmp.type)) {
// V1.2 screen types -> no paletteOffset
Warning("[SAVENEX] BMP paletteOffset is available only for new V1.3 images (320 or 640 x256)");
bmpArgs[1] = -1;
}
// check if V1.3 screens were provided, and if V1.3 is allowed, init internals
if (256 == bmp.height) {
if (2 == nex.reqFileVersion) {
Error("[SAVENEX] V1.2 was requested with OPEN, but 320x256 or 640x256 BMP is V1.3 feature.", NULL, SUPPRESS);
return;
} else {
nex.minFileVersion = 3;
if (-1 == bmpArgs[1]) bmpArgs[1] = 0;
}
}
// check the palette if it was requested from this bmp and process it
if (bmpArgs[0]) dirNexPaletteBmp(bmp);
// palette is written first into file
nex.writePalette();
// update header loading screen status
switch (bmp.type) {
case Layer2:
nex.h.screen |= SNexHeader::SCR_LAYER2;
break;
case LoRes:
nex.h.screen |= SNexHeader::SCR_LORES;
break;
case L2_320x256:
nex.h.screen |= SNexHeader::SCR_EXT2;
nex.h.screen2 = SNexHeader::SCR2_320x256;
nex.h.hiResColour = bmpArgs[1];
break;
case L2_640x256:
nex.h.screen |= SNexHeader::SCR_EXT2;
nex.h.screen2 = SNexHeader::SCR2_640x256;
nex.h.hiResColour = bmpArgs[1];
break;
default:
; // should be unreachable
}
// load and write pixel data
byte* buffer = new byte[641*256]; // buffer to read pixel data +1 write buffer
if (nullptr == buffer) ErrorOOM();
// read BMP first line by line into buffer (undid upside-down also)
bmp.loadPixelData(buffer);
// write pixel data into file - do transformation for 320/640 x 256 modes
if (Layer2 == bmp.type || LoRes == bmp.type) {
const size_t pixelBlockSize = static_cast<size_t>(bmp.width) * static_cast<size_t>(bmp.height);
if (pixelBlockSize != fwrite(buffer, 1, pixelBlockSize, nex.f)) {
Error("[SAVENEX] writing pixel data failed", NULL, FATAL);
}
} else {
constexpr size_t h = 256, wB = 320;
const bool xMul2 = (L2_640x256 == bmp.type);
byte* const wbuf = buffer + 640 * 256; // write buffer is at last 256B block
// transpose data, store them column by column (two columns at time for 640x256)
for (size_t x = 0; x < wB; ++x) {
const byte* src = buffer + (xMul2 ? x*2 : x);
for (size_t y = 0; y < h; ++y) {
const byte pixel = xMul2 ? (src[0]<<4) | (src[1]&0x0F) : src[0];
src += bmp.width;
wbuf[y] = pixel;
}
if (h != fwrite(wbuf, 1, h, nex.f)) {
Error("[SAVENEX] writing pixel data failed", NULL, FATAL);
}
}
}
delete[] buffer;
}
static void dirNexScreenUlaTimex(byte scrType) {
// ;; SCREEN (SCR|SHC|SHR) [<hiResColour 0..7>]
// parse argument (only HiRes screen type)
if (SNexHeader::SCR_HIRES == scrType) {
aint hiResColor = 0;
if (ParseExpression(lp, hiResColor)) {
if (hiResColor < 0 || 7 < hiResColor) Warning("[SAVENEX] value is not in 0..7 range", bp);
nex.h.hiResColour = (hiResColor&7) << 3;
}
}
// update header loading screen status
nex.h.screen = scrType;
// warn about invalid values
size_t adrPixelData = Device->GetMemoryOffset(5*2, 0);
size_t pixelDataSize = (SNexHeader::SCR_ULA == scrType) ? 0x1B00 : 0x1800;
// write pixel data into file (from the default VRAM position of device)
if (pixelDataSize != fwrite(Device->Memory + adrPixelData, 1, pixelDataSize, nex.f)) {
Error("[SAVENEX] writing pixel data failed", NULL, FATAL);
}
if (SNexHeader::SCR_ULA == scrType) return; //ULA is written in one go
adrPixelData += 0x2000; // address of second half of data
// write [remaining] part of pixel data into file
if (pixelDataSize != fwrite(Device->Memory + adrPixelData, 1, pixelDataSize, nex.f)) {
Error("[SAVENEX] writing pixel data failed", NULL, FATAL);
}
}
static bool saveBank(aint bankIndex, aint bankNum, bool onlyNonZero = false);
static void dirNexScreenTile() {
// ;; SCREEN TILE <NextReg $6B>,<NextReg $6C>,<NextReg $6E>,<NextReg $6F>[,<AlsoStoreBank5 0/1 = 1>]
// parse arguments
aint tileArgs[5] = {0, 0, 0, 0, 1};
const bool optionals[] = {false, false, false, false, true};
if (!getIntArguments<5>(lp, tileArgs, optionals)
|| tileArgs[0] < 0 || 255 < tileArgs[0]
|| tileArgs[1] < 0 || 255 < tileArgs[1]
|| tileArgs[2] < 0 || 255 < tileArgs[2]
|| tileArgs[3] < 0 || 255 < tileArgs[3]
|| tileArgs[4] < 0 || 1 < tileArgs[4]) {
Error("[SAVENEX] expected syntax is TILE <NextReg $6B>,<NextReg $6C>,<NextReg $6E>,<NextReg $6F>[,<AlsoStoreBank5 0/1 = 1>]", bp, SUPPRESS);
return;
}
// check file version and set it up to V1.3
if (2 == nex.reqFileVersion) {
Error("[SAVENEX] V1.2 was requested with OPEN, but tilemap screen is V1.3 feature.", NULL, SUPPRESS);
return;
}
nex.minFileVersion = 3;
// write palette into file (or update nex.h.screen with NOPAL flag if no palette was defined)
nex.writePalette();
nex.h.screen |= SNexHeader::SCR_EXT2;
nex.h.screen2 = SNexHeader::SCR2_tilemap;
nex.h.tilesScrConfig[0] = static_cast<byte>(tileArgs[0]);
nex.h.tilesScrConfig[1] = static_cast<byte>(tileArgs[1]);
nex.h.tilesScrConfig[2] = static_cast<byte>(tileArgs[2]);
nex.h.tilesScrConfig[3] = static_cast<byte>(tileArgs[3]);
// write Bank 5 into file, if requested/default
if (0 == tileArgs[4]) return; // suppressed
saveBank(getNexBankIndex(5), 5);
}
static void dirNexScreen() {
if (!nex.canAppend) {
Error("[SAVENEX] NEX file is not open", NULL, SUPPRESS);
return;
}
if (0 != nex.h.screen) {
Error("[SAVENEX] screen for this NEX file was already stored", NULL, SUPPRESS);
return;
}
if (-1 != nex.lastBankIndex) {
Error("[SAVENEX] some bank was already stored (store screen ahead)", NULL, SUPPRESS);
return;
}
SkipBlanks(lp);
if (cmphstr(lp, "l2")) dirNexScreenLayer2andLowRes(Layer2);
else if (cmphstr(lp, "lr")) dirNexScreenLayer2andLowRes(LoRes);
else if (cmphstr(lp, "l2_320")) dirNexScreenLayer2andLowRes(L2_320x256);
else if (cmphstr(lp, "l2_640")) dirNexScreenLayer2andLowRes(L2_640x256);
else if (cmphstr(lp, "bmp")) dirNexScreenBmp();
else if (cmphstr(lp, "scr")) dirNexScreenUlaTimex(SNexHeader::SCR_ULA);
else if (cmphstr(lp, "shc")) dirNexScreenUlaTimex(SNexHeader::SCR_HICOL);
else if (cmphstr(lp, "shr")) dirNexScreenUlaTimex(SNexHeader::SCR_HIRES);
else if (cmphstr(lp, "tile")) dirNexScreenTile();
else Error("[SAVENEX] unknown screen type (types: BMP, L2, L2_320, L2_640, LR, SCR, SHC, SHR, TILE)", lp, SUPPRESS);
}
static void dirNexCopper() {
// ;; SAVENEX COPPER <Page8kNum 0..223>,<offset>
if (!nex.canAppend) {
Error("[SAVENEX] NEX file is not open", NULL, SUPPRESS);
return;
}
if (nex.reqFileVersion == 2) {
Error("[SAVENEX] V1.2 was requested with OPEN, but COPPER is V1.3 feature.", NULL, SUPPRESS);
return;
}
if (-1 != nex.lastBankIndex) {
Error("[SAVENEX] some bank was already stored (store copper ahead)", NULL, SUPPRESS);
return;
}
nex.minFileVersion = 3; // V1.3 detected
// parse arguments
aint screenArgs[2] = {0, 0};
const bool optionals[] = {false, false};
if (!getIntArguments<2>(lp, screenArgs, optionals)
|| screenArgs[0] < 0 || SNexHeader::MAX_PAGE <= screenArgs[0]) {
Error("[SAVENEX] expected syntax is COPPER <Page8kNum 0..223>,<offset>", bp, SUPPRESS);
return;
}
// warn about invalid values
const size_t totalRam = Device->GetMemoryOffset(Device->PagesCount, 0);
size_t adrCopperData = Device->GetMemoryOffset(screenArgs[0], screenArgs[1]);
if (totalRam < adrCopperData + SNexHeader::COPPER_SIZE) {
Error("[SAVENEX] copper data address range is outside of Next memory", bp, SUPPRESS);
return;
}
// adjust header and remember the copper data for saving them ahead of first bank
nex.h.hasCopperCode = 1;
if (nullptr == nex.copper) nex.copper = new byte[SNexHeader::COPPER_SIZE];
memcpy(nex.copper, Device->Memory + adrCopperData, SNexHeader::COPPER_SIZE);
}
static bool saveBank(aint bankIndex, aint bankNum, bool onlyNonZero) {
if (bankNum < 0 || SNexHeader::MAX_BANK <= bankNum) return false;
if (bankIndex <= nex.lastBankIndex) {
ErrorInt("[SAVENEX] it's too late to save this bank (correct order: 5, 2, 0, 1, 3, 4, 6, ...)",
bankNum, SUPPRESS);
return false;
}
nex.updateIfAheadFirstBankSave();
const size_t offset = Device->GetMemoryOffset(bankNum * 2, 0);
const size_t size = 0x4000;
nex.lastBankIndex = bankIndex;
// detect bank which is just full of zeroes and exit early if onlyNonZero is requested
if (onlyNonZero) {
size_t zeroOfs = 0;
while (zeroOfs < size) {
if (0 != Device->Memory[offset + zeroOfs]) break;
++zeroOfs;
}
if (size == zeroOfs) return true;
}
// update NEX header data
nex.h.banks[bankNum] = 1;
++nex.h.numBanks;
// save the bank memory
if (size != fwrite(Device->Memory + offset, 1, size, nex.f)) {
Error("[SAVENEX] writing bank data failed", NULL, FATAL);
}
return true;
}
static void dirNexBank() {
if (!nex.canAppend) {
Error("[SAVENEX] NEX file is not open", NULL, SUPPRESS);
return;
}
do {
aint bankNum, bankIndex;
char *nextLp = lp;
if (!ParseExpressionNoSyntaxError(lp, bankNum)
|| (bankIndex = getNexBankIndex(bankNum)) < 0) {
Error("[SAVENEX] expected bank number 0..111", nextLp, SUPPRESS);
break;
}
if (!saveBank(bankIndex, bankNum)) break;
} while (comma(lp));
}
static void dirNexAuto() {
if (!nex.canAppend) {
Error("[SAVENEX] NEX file is not open", NULL, SUPPRESS);
return;
}
if (SNexHeader::MAX_BANK-1 == nex.lastBankIndex) { // actually there's nothing left to scan
Error("[SAVENEX] all banks are already stored", NULL, SUPPRESS);
return;
}
// parse arguments
aint autoArgs[2] = { getNexBankNum(nex.lastBankIndex+1), SNexHeader::MAX_BANK-1 };
const bool optionals[] = {true, true};
if (!getIntArguments<2>(lp, autoArgs, optionals)
|| autoArgs[0] < 0 || SNexHeader::MAX_BANK <= autoArgs[0]
|| autoArgs[1] < 0 || SNexHeader::MAX_BANK <= autoArgs[1]) {
Error("[SAVENEX] expected syntax is AUTO [<fromBank 0..111>[,<toBank 0..111>]]", bp, SUPPRESS);
return;
}
// validate arguments
aint fromI = getNexBankIndex(autoArgs[0]), toI = getNexBankIndex(autoArgs[1]);
if (toI < fromI) {
Error("[SAVENEX] 'toBank' is less than 'fromBank'", bp, SUPPRESS);
return;
}
while (fromI <= toI) {
if (!saveBank(fromI, getNexBankNum(fromI), true)) return;
++fromI;
}
}
static void dirNexClose() {
if (!nex.canAppend) {
Error("[SAVENEX] NEX file is not open", NULL, SUPPRESS);
return;
}
// update V1.3 banksOffset in case there was no bank stored at all (before appending binary data!)
nex.updateIfAheadFirstBankSave();
// read CLOSE command argument and try to append the proposed file (if some was provided)
char* appendName = nullptr;
if (!SkipBlanks(lp)) appendName = GetFileName(lp);
if (appendName) { // some append file requested, try to copy its content at tail of NEX
FILE* appendF = nullptr;
if (!FOPEN_ISOK(appendF, appendName, "rb")) {
Error("[SAVENEX] Error opening append file", appendName, SUPPRESS);
} else {
static constexpr int copyBufSize = 0x4000;
byte* copyBuffer = new byte[copyBufSize];
if (nullptr == copyBuffer) ErrorOOM();
do {
const size_t read = fread(copyBuffer, 1, copyBufSize, appendF);
if (read) {
const size_t write = fwrite(copyBuffer, 1, read, nex.f);
if (write != read) Error("[SAVENEX] writing append data failed", NULL, FATAL);
}
} while (!feof(appendF));
delete[] copyBuffer;
fclose(appendF);
}
delete[] appendName;
}
// finalize the NEX file (refresh the header data and close it)
nex.finalizeFile();
}
void dirSAVENEX() {
if (pass != LASTPASS) return; // syntax error is not visible in early passes
if (nullptr == DeviceID || strcmp(DeviceID, "ZXSPECTRUMNEXT")) {
Error("[SAVENEX] is allowed only in ZXSPECTRUMNEXT device mode", NULL, SUPPRESS);
return;
}
SkipBlanks(lp);
if (cmphstr(lp, "open")) dirNexOpen();
else if (cmphstr(lp, "core")) dirNexCore();
else if (cmphstr(lp, "cfg3")) dirNexCfg3();
else if (cmphstr(lp, "cfg")) dirNexCfg();
else if (cmphstr(lp, "bar")) dirNexBar();
else if (cmphstr(lp, "palette")) dirNexPalette();
else if (cmphstr(lp, "screen")) dirNexScreen();
else if (cmphstr(lp, "copper")) dirNexCopper();
else if (cmphstr(lp, "bank")) dirNexBank();
else if (cmphstr(lp, "auto")) dirNexAuto();
else if (cmphstr(lp, "close")) dirNexClose();
else Error("[SAVENEX] unknown command (commands: OPEN, CORE, CFG, BAR, SCREEN, BANK, AUTO, CLOSE)", lp, SUPPRESS);
}